@jopqior/pi-subagents 1.0.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 +2705 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/public.d.ts +331 -0
- package/dist/settings.d.ts +82 -0
- package/docs/architecture/architecture.md +1566 -0
- package/docs/architecture/client-server-opportunities.md +127 -0
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
- package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
- package/docs/architecture/history/phase-14-strip-policy.md +49 -0
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-20-result-delivery.md +245 -0
- package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/comparison-with-upstream.md +77 -0
- package/docs/configuration.md +364 -0
- package/docs/decisions/0001-deferred-patches.md +80 -0
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
- package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
- package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
- package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
- package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
- package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
- package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
- package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
- package/package.json +91 -0
- package/src/config/agent-types.ts +135 -0
- package/src/config/custom-agents.ts +151 -0
- package/src/config/default-agents.ts +121 -0
- package/src/config/invocation-config.ts +167 -0
- package/src/config/thinking-level.ts +58 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +58 -0
- package/src/handlers/lifecycle.ts +71 -0
- package/src/handlers/widget-events.ts +49 -0
- package/src/index.ts +292 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +115 -0
- package/src/lifecycle/child-shutdown.ts +105 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +335 -0
- package/src/lifecycle/parent-snapshot.ts +119 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/selection-scope.ts +116 -0
- package/src/lifecycle/spawn-selection.ts +259 -0
- package/src/lifecycle/subagent-manager.ts +546 -0
- package/src/lifecycle/subagent-session.ts +347 -0
- package/src/lifecycle/subagent-state.ts +404 -0
- package/src/lifecycle/subagent.ts +885 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +60 -0
- package/src/lifecycle/workspace-bracket.ts +76 -0
- package/src/lifecycle/workspace.ts +46 -0
- package/src/observation/composite-subagent-observer.ts +74 -0
- package/src/observation/notification.ts +430 -0
- package/src/observation/outcome-delivery.ts +239 -0
- package/src/observation/record-observer.ts +78 -0
- package/src/observation/renderer.ts +161 -0
- package/src/observation/subagent-events-observer.ts +148 -0
- package/src/runtime.ts +137 -0
- package/src/service/service-adapter.ts +201 -0
- package/src/service/service.ts +246 -0
- package/src/session/ask-parent-tool.ts +69 -0
- package/src/session/content-items.ts +53 -0
- package/src/session/context.ts +80 -0
- package/src/session/conversation.ts +49 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +126 -0
- package/src/session/notify-parent-tool.ts +83 -0
- package/src/session/package-exclusions.ts +75 -0
- package/src/session/prompts.ts +231 -0
- package/src/session/provider-inheritance.ts +56 -0
- package/src/session/selection-catalogue.ts +143 -0
- package/src/session/session-config.ts +202 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +447 -0
- package/src/tools/agent-tool.ts +305 -0
- package/src/tools/background-spawner.ts +83 -0
- package/src/tools/foreground-runner.ts +159 -0
- package/src/tools/get-result-renderer.ts +119 -0
- package/src/tools/get-result-report.ts +84 -0
- package/src/tools/get-result-tool.ts +192 -0
- package/src/tools/helpers.ts +118 -0
- package/src/tools/result-renderer.ts +153 -0
- package/src/tools/spawn-config.ts +192 -0
- package/src/tools/steer-tool.ts +109 -0
- package/src/types.ts +143 -0
- package/src/ui/agent-widget.ts +333 -0
- package/src/ui/bounded-lines.ts +45 -0
- package/src/ui/display.ts +180 -0
- package/src/ui/glyphs.ts +62 -0
- package/src/ui/session-navigation.ts +150 -0
- package/src/ui/session-navigator.ts +255 -0
- package/src/ui/subagents-settings.ts +179 -0
- package/src/ui/transcript-content.ts +374 -0
- package/src/ui/widget-renderer.ts +301 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Phase 19: Implement the ADR-0004 UI decisions
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 19 implements the per-component UI decisions recorded in [ADR-0004]: shrink the widget to background-only, replace the bespoke conversation viewer with native session navigation, dissolve the monolithic `/agents` menu, and keep the surviving UI in-core.
|
|
6
|
+
|
|
7
|
+
The sequencing follows Kent Beck's "make the change easy, then make the easy change."
|
|
8
|
+
The end state deletes `agent-menu.ts` — the god-command that bundles four unrelated jobs — and everything reachable only from it.
|
|
9
|
+
Rather than surgically mutate that doomed module (and the #1 churn hotspot `index.ts`) once per option, Phase 19 first stood up the replacement surfaces additively, then removed the now-orphaned subtree in a single terminal cut.
|
|
10
|
+
This kept every responsibility's old surface live until its replacement existed (ADR-0004's no-interim-regression invariant), turned the three replacement steps into genuinely parallel work (none touched `agent-menu.ts`), and reduced `index.ts` edits from four surgical removals to one deregistration.
|
|
11
|
+
|
|
12
|
+
Seven numbered steps in three phases, plus two follow-ups (Steps 4a–4b) carved from the #445 slice:
|
|
13
|
+
|
|
14
|
+
- **Phase A — stand up replacements (additive):** spike, settings command, background widget, native session navigation and its renderer/source follow-ups (Steps 1–4, 4a–4b).
|
|
15
|
+
- **Phase B — dissolve `/agents` (terminal cut):** delete the orphaned subtree in two deletion commits, one per subtree (Steps 5–6).
|
|
16
|
+
- **Phase C — test health:** consolidate the test clones that survive the cut (Step 7).
|
|
17
|
+
|
|
18
|
+
All nine steps are closed: [#446], [#447], [#444], [#445], [#462], [#463], [#442], [#441], [#443].
|
|
19
|
+
A follow-on issue, [#470] (README staleness — the terminal cut removed `/agents` but left the README describing it), was filed after Steps 5–6 shipped and closed independently; see "Follow-on issues" below.
|
|
20
|
+
|
|
21
|
+
## Health metrics
|
|
22
|
+
|
|
23
|
+
| Metric | Phase 18 (start) | Phase 19 target | Phase 19 (delivered) |
|
|
24
|
+
| ---------------------- | ------------------------ | -------------------- | ------------------------------------ |
|
|
25
|
+
| Health score | 78/100 (B) | 83/100 (B+) | 78/100 (B) — unchanged |
|
|
26
|
+
| Source LOC | 7,650 (61 files) | ~6,780 (~55 files) | 7,068 (57 files) |
|
|
27
|
+
| Production duplication | 11 lines (1 group) | 0 lines | 0 lines ✅ |
|
|
28
|
+
| Test clone groups | 16 | ≤ 10 | 9 ✅ |
|
|
29
|
+
| Top churn hotspot | `index.ts` (103 commits) | `index.ts` (cooling) | `index.ts` (109 commits, cooling) ✅ |
|
|
30
|
+
|
|
31
|
+
The health score held flat at 78/100 rather than reaching the 83/100 target — the score's `hotspots` and `unit size` deductions are dominated by long-lived test-suite characteristics (large test functions, `index.ts`'s cumulative churn history) that this phase's scope did not target.
|
|
32
|
+
Every metric the phase's steps directly controlled — production duplication, test clone groups, and the churn trend on `index.ts` — hit or beat its target.
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
### Step 1 — Spike: resolve ADR-0004 entry criteria ([#446])
|
|
37
|
+
|
|
38
|
+
Smell: Category C (coupling boundary) — four open decisions block the session-navigation implementation.
|
|
39
|
+
Target: `docs/decisions/0004-reconsider-ui-direction.md` addendum.
|
|
40
|
+
|
|
41
|
+
The four entry criteria from ADR-0004:
|
|
42
|
+
|
|
43
|
+
1. **Root-continuity:** Does the root's in-flight turn survive `ctx.switchSession()` and a return gesture?
|
|
44
|
+
2. **View-only vs interactive:** `switchSession` (full interactive takeover) or `loadEntriesFromFile` (read-only transcript built from JSONL)?
|
|
45
|
+
3. **Parallel-agent navigation:** Operator gesture to select which of N background agents to view (from the widget, a command, or both).
|
|
46
|
+
4. **Settings command name:** `/subagents-settings`, `/agents-settings`, or another form consistent with sibling packages?
|
|
47
|
+
|
|
48
|
+
Produce a minimal spike (observed test or PoC against a real session) that answers each question, then record the answers as an addendum to ADR-0004.
|
|
49
|
+
No production source files change; the spike closes when the ADR addendum is merged.
|
|
50
|
+
|
|
51
|
+
Outcome: ADR-0004 updated with all four entry-criteria answers; Step 4 unblocked.
|
|
52
|
+
|
|
53
|
+
`Release: independent`
|
|
54
|
+
|
|
55
|
+
### Step 2 — Extract settings to a focused `/subagents-settings` command ([#447])
|
|
56
|
+
|
|
57
|
+
Smell: Category E (naming/organization) — settings are buried inside the monolithic `/agents` command per ADR-0004 Decision C. This step is purely additive: it stands up the new surface without touching `agent-menu.ts`.
|
|
58
|
+
Target files:
|
|
59
|
+
|
|
60
|
+
- New `src/ui/subagents-settings.ts` — `SubagentsSettingsHandler` lifted from `AgentsMenuHandler.showSettings`, carrying its own narrow `SubagentsSettingsManager` interface (the three `apply*` methods and three readonly accessors only).
|
|
61
|
+
- `src/index.ts` — register the new command (name confirmed by Step 1); pass `settings` directly.
|
|
62
|
+
- New `test/ui/subagents-settings.test.ts` — unit tests for the extracted handler.
|
|
63
|
+
|
|
64
|
+
`showSettings` depends only on `this.settings` (the self-contained `AgentMenuSettings` shape), so the extraction copies that logic into a new file with zero coupling to the wizard, editor, or viewer.
|
|
65
|
+
The old in-menu Settings option keeps working until the terminal cut deletes `agent-menu.ts` wholesale — there is no surgical removal of `showSettings` or `AgentMenuSettings` from the doomed file.
|
|
66
|
+
|
|
67
|
+
Outcome: new `subagents-settings.ts` (~80 LOC) and focused command registered; `agent-menu.ts` untouched.
|
|
68
|
+
|
|
69
|
+
`Release: independent`
|
|
70
|
+
|
|
71
|
+
### Step 3 — Shrink widget to background agents only ([#444])
|
|
72
|
+
|
|
73
|
+
Smell: Category C (coupling) — the widget shows all agents including foreground ones, duplicating the `subagent` tool's inline `onUpdate` stream for foreground runs.
|
|
74
|
+
Target files:
|
|
75
|
+
|
|
76
|
+
- `src/ui/agent-widget.ts` — funnel both `manager.listAgents()` call sites (`update()` and `renderWidget()`) through a single private accessor, then flip that accessor to background-only via `record.invocation?.runInBackground === true`.
|
|
77
|
+
- `src/ui/widget-renderer.ts` — verify no foreground-specific rendering path survives.
|
|
78
|
+
- `test/ui/agent-widget.test.ts` — add background-only filtering tests; update assertions.
|
|
79
|
+
|
|
80
|
+
The widget calls `listAgents()` at two sites today — `update()` (feeding `seedFinishedAgents`, `assembleWidgetState`, and `clearWidget`) and `renderWidget()` (the tree map).
|
|
81
|
+
Filtering at only one site leaves the other rendering foreground agents, so the enabling move is to route both through one accessor and apply the predicate once at the source.
|
|
82
|
+
`Subagent.invocation.runInBackground` is the reliable signal: set by `spawn-config.ts` → `AgentInvocation.runInBackground` → stored on `Subagent.invocation`.
|
|
83
|
+
ADR-0004 Decision A: foreground runs suppress the widget; the inline `onUpdate` stream is authoritative there.
|
|
84
|
+
|
|
85
|
+
Outcome: widget shows only background agents; foreground/widget duplication eliminated; the background predicate lives at a single funnel.
|
|
86
|
+
|
|
87
|
+
`Release: independent`
|
|
88
|
+
|
|
89
|
+
### Step 4 — Implement native session navigation ([#445])
|
|
90
|
+
|
|
91
|
+
Smell: Category C (coupling) — the bespoke `ConversationViewer` re-implements session-transcript rendering when Pi's own machinery targets the already-persisted child session JSONL.
|
|
92
|
+
This step adds the new surface alongside the existing viewer; it does not touch `agent-menu.ts`.
|
|
93
|
+
Target files:
|
|
94
|
+
|
|
95
|
+
- New `src/ui/session-navigator.ts` — a flat command that lists any subagent with a live record or a persisted session file (foreground included, never background-filtered), lets the operator pick one, and renders that child's transcript read-only.
|
|
96
|
+
- New typed accessor on `Subagent`/`SubagentSession` returning `record.messages` as `AgentMessage[]` (the boundary currently widens it to `readonly unknown[]`).
|
|
97
|
+
- `src/index.ts` — register the new command; the background widget ([#444]) is an optional secondary selection gesture, not a dependency.
|
|
98
|
+
|
|
99
|
+
ADR-0004 Decision B: "Tell-Don't-Ask — hand Pi the session path; Pi owns the viewer."
|
|
100
|
+
Mechanism (confirmed by the Step 1 spike and revised by [ADR-0004] Addendum 2): a **read-only** (non-interactive) transcript **dual-sourced by liveness**, rendered through Pi's own public entry components (no bespoke renderer).
|
|
101
|
+
|
|
102
|
+
- **Tracked agent** (still in `manager.listAgents()`) — render live from the in-memory record: `record.messages` for history, `record.subscribeToUpdates()` to re-render on streaming updates, and `record.activeTools` / `record.responseText` for the running-agent streaming indicator.
|
|
103
|
+
- **Evicted / untracked agent** — render from the file snapshot: `parseSessionEntries(readFileSync(record.outputFile, "utf8"))` → drop the `SessionHeader` → `buildSessionContext(...).messages`.
|
|
104
|
+
|
|
105
|
+
Both sources yield `AgentMessage[]`, so one Pi-component renderer serves both: Pi's public entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / …) or `serializeConversation` (see the [ADR-0004] addendum, Findings 0 and 1).
|
|
106
|
+
Neither `switchSession` (a full takeover that invalidates the root's in-flight turn) nor `loadEntriesFromFile` (a test-only export the package's public barrel does not re-export, in both `0.79.1` and `0.79.8`) is used.
|
|
107
|
+
`Subagent.outputFile` already exposes the persisted child session JSONL path via `subagentSession?.outputFile` — no new SDK dependency.
|
|
108
|
+
The new surface stands up while the old `viewAgentConversation`/`ConversationViewer` path still works; the bespoke viewer is removed only by the terminal cut (Step 5).
|
|
109
|
+
|
|
110
|
+
Outcome: operator views any subagent's session through Pi's native machinery — live for a running agent, a file snapshot for an evicted one; the new surface coexists with the old viewer until Step 5.
|
|
111
|
+
|
|
112
|
+
Landed ([#445], sliced): #445 shipped the first releasable vertical slice — the `/subagent-sessions` command (`src/ui/session-navigator.ts`), the pure selection/sourcing/text-render core (`src/ui/session-navigation.ts`), and the typed `agentMessages` accessor (`SessionMessage` on `SubagentSession`/`Subagent`).
|
|
113
|
+
It is **live-source only** behind a renderer-agnostic `TranscriptSource` seam, rendered via Pi's `serializeConversation` text.
|
|
114
|
+
With the `manager.listAgents()`-only candidate set, no listed record is ever session-disposed (dispose-and-delete are atomic), so the file-snapshot branch has no reachable caller and was deferred to keep `fallow dead-code` clean.
|
|
115
|
+
Step 4 (#445, the slice) is complete and released (`pi-subagents` v17.3.0); the remaining work is now tracked as two follow-up steps behind the same seam: Step 4a ([#462]) upgrades the renderer from `serializeConversation` text to Pi's per-entry TUI components (gates Step 5 for rendering parity); Step 4b ([#463]) broadens the candidate set to evicted agents and adds the file-snapshot source (`parseSessionEntries` → `buildSessionContext`, independent).
|
|
116
|
+
|
|
117
|
+
`Release: independent` (spike-gated)
|
|
118
|
+
|
|
119
|
+
### Step 4a — Upgrade native-navigation renderer to Pi TUI components ([#462])
|
|
120
|
+
|
|
121
|
+
Smell: Category C (coupling) — the #445 slice renders the transcript as `serializeConversation` plain text, while the bespoke `ConversationViewer` it replaces renders richer per-message formatting.
|
|
122
|
+
Until the native renderer reaches parity, the terminal cut (Step 5) cannot delete the bespoke viewer without a fidelity regression.
|
|
123
|
+
This step swaps the renderer behind the existing `TranscriptSource` seam (`src/ui/session-navigation.ts` / `session-navigator.ts`) for Pi's per-entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / …); selection and sourcing are untouched.
|
|
124
|
+
|
|
125
|
+
Gates Step 5: per [ADR-0004]'s no-interim-regression invariant, the native navigator must reach rendering parity with the bespoke viewer before Step 5 deletes it.
|
|
126
|
+
|
|
127
|
+
Outcome: native session navigation renders at parity with the removed `ConversationViewer`; Step 5 can delete the bespoke viewer with no fidelity regression.
|
|
128
|
+
|
|
129
|
+
Landed ([#462]): the renderer now mounts Pi's per-entry components (`AssistantMessageComponent` / `ToolExecutionComponent` / `BashExecutionComponent` / `UserMessageComponent` / `CompactionSummaryMessageComponent` / `BranchSummaryMessageComponent` / `SkillInvocationMessageComponent`) into a `Container`, mirroring Pi's own `renderSessionContext` mapping.
|
|
130
|
+
The `TranscriptOverlay` caches that `Container` and rebuilds it on source change only (Pi's `rebuildChatFromMessages` path), keeping the lightweight `◍` streaming indicator.
|
|
131
|
+
Tool calls render with their real `ToolDefinition`, resolved through a dependency-safe `getToolDefinition` read accessor on the record (mirroring `agentMessages`) surfaced on the `TranscriptSource` seam — no inbound call into the core.
|
|
132
|
+
The pure `session-navigation.ts` sheds `renderTranscriptLines`/`serializeConversation`; rendering now lives in the SDK/TUI `session-navigator.ts`, which threads `cwd` from the command context.
|
|
133
|
+
`custom`-role messages are skipped (the bespoke viewer never rendered them either).
|
|
134
|
+
Selection and sourcing are untouched; native navigation now renders at parity, unblocking Step 5 for rendering fidelity.
|
|
135
|
+
|
|
136
|
+
`Release: independent`
|
|
137
|
+
|
|
138
|
+
### Step 4b — File-snapshot source for evicted agents ([#463])
|
|
139
|
+
|
|
140
|
+
Smell: Category C (coupling) — the #445 slice sources transcripts live from `manager.listAgents()` only; an agent evicted by the 10-minute cleanup sweep has a persisted session JSONL but no live record, so it is unreachable.
|
|
141
|
+
This step adds the file-snapshot `TranscriptSource` branch (`parseSessionEntries(readFile(outputFile))` → drop the `SessionHeader` → `buildSessionContext(...).messages`) and broadens the candidate set to evicted agents, behind the same seam; the renderer is untouched.
|
|
142
|
+
|
|
143
|
+
Independent: this is a new capability the bespoke viewer never had, so it gates nothing and is not a Step 5 prerequisite.
|
|
144
|
+
Best sequenced after Step 4a (shared renderer), but carries no hard dependency.
|
|
145
|
+
|
|
146
|
+
Outcome: the operator can view a fully-evicted agent's transcript from its persisted session file; the dual-source design recorded in [ADR-0004] Addendum 2 is fully realized.
|
|
147
|
+
|
|
148
|
+
Landed ([#463]): `fileSnapshotSource(outputFile, readFile)` lands in the pure `session-navigation.ts` (`parseSessionEntries` → drop the `SessionHeader` → `buildSessionContext(...).messages`; a static no-subscribe, no-streaming source).
|
|
149
|
+
The candidate set is broadened via **manager-retained descriptors**, not a directory scan: the persisted child session carries no subagent `type`/`description` (those live only on the in-memory record), so a scan would yield degraded labels and parse every file per open.
|
|
150
|
+
Instead `SubagentManager.cleanup()` stashes a lightweight `EvictedSubagent` descriptor (label fields + `outputFile`, no messages) before disposing a record with a persisted file, exposed via `listEvicted()` and cleared by `clearCompleted()`/`dispose()`.
|
|
151
|
+
`NavigationEntry` became a `live | evicted` discriminated union; the handler selects `liveSource` vs `fileSnapshotSource` by kind inside a `try/catch` (an unreadable file notifies and skips), and `index.ts` injects `readFileSync`.
|
|
152
|
+
Evicted entries carry an `· evicted (snapshot)` label marker.
|
|
153
|
+
Coverage is in-session evictions (the sweep's only targets); old-session orphan files — the ones a scan would surface with degraded labels — are out of scope.
|
|
154
|
+
|
|
155
|
+
`Release: independent`
|
|
156
|
+
|
|
157
|
+
### Step 5 — Dissolve `/agents` and remove the conversation-viewer subtree ([#442])
|
|
158
|
+
|
|
159
|
+
Smell: Category A (dead subsystem) plus Category B (oversized) — once Steps 2–4 re-home all four menu responsibilities, the `/agents` command and everything reachable only from `agent-menu.ts` is an unreferenced subtree.
|
|
160
|
+
This is the first of two deletion commits (split by subtree).
|
|
161
|
+
The hub `agent-menu.ts` is deleted here, not surgically narrowed, and deleting it is what orphans the leaf subtrees — so it must precede the definition-management deletion (Step 6), because `agent-menu.ts` statically imports the wizard, editor, and file-ops, and dynamically imports the viewer.
|
|
162
|
+
Target files:
|
|
163
|
+
|
|
164
|
+
- `src/index.ts` — remove the `registerCommand("agents", …)` block, the `AgentsMenuHandler` construction and import, and the `FsAgentFileOps` import/construction (its only use is wiring the menu).
|
|
165
|
+
- Delete `src/ui/agent-menu.ts` (331 LOC) and `test/ui/agent-menu.test.ts` (185 LOC).
|
|
166
|
+
- Delete `src/ui/conversation-viewer.ts` (241 LOC) and `test/conversation-viewer.test.ts` (239 LOC) — its only consumer is `agent-menu.ts`'s dynamic import, gone with the hub.
|
|
167
|
+
- Delete `src/ui/message-formatters.ts` (195 LOC) and `test/message-formatters.test.ts` (388 LOC, the largest test function by LOC) — its only consumer is `ConversationViewer`.
|
|
168
|
+
|
|
169
|
+
Running-agent visibility is now owned by the background widget (Step 3); session navigation replaces the bespoke overlay (Step 4); settings live in `/subagents-settings` (Step 2).
|
|
170
|
+
Deleting the hub in one move avoids any surgical edit to the doomed file and leaves the definition-management leaves orphaned for Step 6.
|
|
171
|
+
|
|
172
|
+
Actual approach (vs. original plan): a tidy-first preparatory commit first extracted `MenuUI` from `agent-menu.ts` into a new `src/ui/menu-ui.ts` module, breaking the bidirectional type cycle — the hub imported the wizard/editor classes while the leaves imported back the `MenuUI` type.
|
|
173
|
+
Without the prep commit, deleting either subtree first would have left the other half referencing a deleted module.
|
|
174
|
+
`index.ts` also shed the now-dead `join` (node:path) and `buildParentSnapshot` imports in addition to the two module imports.
|
|
175
|
+
The `menu-ui.ts` module is transient and is removed with the wizard/editor in Step 6.
|
|
176
|
+
|
|
177
|
+
Outcome: ✅ `/agents` dissolved; −767 LOC source (menu hub + viewer + formatters); −812 LOC test; largest test function eliminated; `index.ts` dewired.
|
|
178
|
+
|
|
179
|
+
`Release: batch "dissolve-agents"`
|
|
180
|
+
|
|
181
|
+
### Step 6 — Remove the orphaned agent-definition management subtree ([#441])
|
|
182
|
+
|
|
183
|
+
Smell: Category A (dead subsystem) — the creation wizard and config editor are removed per ADR-0004 Decision C; after Step 5 deletes their only importer (`agent-menu.ts`), they and their file-ops helpers are pure orphans.
|
|
184
|
+
This is the second deletion commit (split by subtree).
|
|
185
|
+
Target files:
|
|
186
|
+
|
|
187
|
+
- Delete `src/ui/agent-creation-wizard.ts` (233 LOC) and `test/ui/agent-creation-wizard.test.ts` (296 LOC).
|
|
188
|
+
- Delete `src/ui/agent-config-editor.ts` (199 LOC) and `test/ui/agent-config-editor.test.ts` (392 LOC) — eliminates the 11-line internal production clone in `disableAgent`/`ejectAgent`, the package's only remaining production duplication.
|
|
189
|
+
- Delete `src/ui/agent-file-ops.ts` (59 LOC) and `test/ui/agent-file-ops.test.ts` (112 LOC) — only consumers were wizard + editor.
|
|
190
|
+
- Delete `src/ui/agent-file-writer.ts` (55 LOC) and `test/ui/agent-file-writer.test.ts` (148 LOC) — only consumers were wizard + editor.
|
|
191
|
+
- `test/helpers/ui-stubs.ts` — delete `makeFileOps`, `createTestSubagentConfig`, and `spawnAndWait` from `makeMenuManager` if no surviving consumer remains; delete the file outright once all consumers are gone.
|
|
192
|
+
|
|
193
|
+
An operator generates a new agent `.md` by asking a Pi session directly (more capable than a fixed wizard) or by writing the file in an editor; viewing and editing definitions is served by opening the `.md` files in an editor or IDE.
|
|
194
|
+
These files are orphaned by Step 5, so this is a pure `git rm` with no surviving references and no edit to any doomed file.
|
|
195
|
+
|
|
196
|
+
Outcome: −546 LOC source (wizard + editor + file-ops + file-writer); −948 LOC test; production duplication → 0 lines; 1 production and 1 test clone group eliminated.
|
|
197
|
+
|
|
198
|
+
Landed ([#441]): deleted `agent-creation-wizard.ts`, `agent-config-editor.ts`, `agent-file-ops.ts`, `agent-file-writer.ts`, and `menu-ui.ts` (orphaned transient) from `src/ui/`, plus their four test files.
|
|
199
|
+
`test/helpers/ui-stubs.ts` pruned to `makeMenuUI` only (still consumed by `subagents-settings.test.ts`); `makeFileOps`, `makeMenuManager`, and `createTestSubagentConfig` removed with their `ui-stubs.test.ts` describe blocks.
|
|
200
|
+
Production duplication: 0 lines (confirmed by `fallow dupes`); `fallow dead-code`: clean. 16 test clone groups remain — Step 7 consolidation target.
|
|
201
|
+
|
|
202
|
+
`Release: batch "dissolve-agents"`
|
|
203
|
+
|
|
204
|
+
### Step 7 — Consolidate remaining test clone families ([#443])
|
|
205
|
+
|
|
206
|
+
Smell: Category D (testability) — 16 clone groups at Phase 18 end; the terminal cut (Steps 5–6) removes ~4 groups; remaining groups are extraction targets.
|
|
207
|
+
Run after the cut so no helper is extracted into a file the cut then deletes.
|
|
208
|
+
Target files:
|
|
209
|
+
|
|
210
|
+
- `test/lifecycle/subagent-manager.test.ts` — extract a shared assertion helper for 3 clone families (23 lines across groups at :92/:109, :282/:330, and :323 shared with `subagent.test.ts`).
|
|
211
|
+
- `test/ui/agent-widget.test.ts` — merge the duplicate `makeWidget` helper defined twice across two `describe` blocks (14-line clone at :225/:284).
|
|
212
|
+
- `test/session/session-config.test.ts` — extract a shared fixture for the 16-line internal clone (lines 131–146 / 151–166).
|
|
213
|
+
- `test/lifecycle/concurrency-limiter.test.ts` — extract shared setup for the 10-line clone (lines 21–30 / 148–155).
|
|
214
|
+
- `test/tools/spawn-config.test.ts` — extract a shared fixture for the 9-line clone (lines 22–30 / 35–43).
|
|
215
|
+
|
|
216
|
+
Outcome (landed): test clone groups reduced from 16 to 9 (≤ 10 target met).
|
|
217
|
+
Eight genuine arrange/fixture/helper families were extracted: `makeNavigable` (shared `test/helpers/make-navigable.ts`), `emitResumeUsageAndCompaction` (shared `test/helpers/mock-session.ts`), and local helpers for `makeWidget`, the captured-overlay render (`renderCapturedOverlay`), the `resultConsumed` observer (`seedResultConsumedObserver`), the ready subagent (`makeReadySubagent`), and the prepared bracket (`preparedBracket`).
|
|
218
|
+
The nine residual families are the repeated system-under-test call (`resolveSpawnConfig`, `assembleSessionConfig`, `schedule`, `SessionNavigatorHandler.handle`, `spawnBg`+`await`, `agent.run()`, `execute`), left intact per the testing guardrail — the repeated act is the test subject, not duplication to remove.
|
|
219
|
+
One family the plan pre-classified as captured-overlay boilerplate (`dup:ea0a1bce`) proved to be an act-clone once the boilerplate was extracted, so it joins the residual set rather than being wrapped.
|
|
220
|
+
|
|
221
|
+
`Release: independent`
|
|
222
|
+
|
|
223
|
+
## Step dependency diagram
|
|
224
|
+
|
|
225
|
+
```mermaid
|
|
226
|
+
flowchart LR
|
|
227
|
+
S1["✅ Step 1 - Spike (#446)"]
|
|
228
|
+
S2["✅ Step 2 - Settings command (#447)"]
|
|
229
|
+
S3["✅ Step 3 - Background widget (#444)"]
|
|
230
|
+
S4["✅ Step 4 - Native session nav slice (#445)"]
|
|
231
|
+
S4a["✅ Step 4a - Renderer to TUI components (#462)"]
|
|
232
|
+
S4b["✅ Step 4b - File-snapshot source (#463)"]
|
|
233
|
+
S5["✅ Step 5 - Dissolve /agents + viewer (#442)"]
|
|
234
|
+
S6["✅ Step 6 - Remove definition mgmt (#441)"]
|
|
235
|
+
S7["✅ Step 7 - Test clones (#443)"]
|
|
236
|
+
|
|
237
|
+
S1 --> S4
|
|
238
|
+
S4 --> S4a
|
|
239
|
+
S4 --> S4b
|
|
240
|
+
S2 --> S5
|
|
241
|
+
S3 --> S5
|
|
242
|
+
S4a --> S5
|
|
243
|
+
S5 --> S6
|
|
244
|
+
S6 --> S7
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The terminal cut (Step 5) depends on all three replacements — settings (Step 2), widget (Step 3), and session navigation **at rendering parity** (Step 4a, which completes the #445 slice) — because each of the four `/agents` options must have its responsibility re-homed, and the viewer replacement at parity, before its branch can die.
|
|
248
|
+
Step 4b (file-snapshot source) is a new capability and gates nothing.
|
|
249
|
+
The old `S1 → S6 → S7` chain hid the widget dependency; this diagram makes it explicit.
|
|
250
|
+
|
|
251
|
+
## Parallel tracks
|
|
252
|
+
|
|
253
|
+
- **Track A — Replacements (Steps 1–4):** the spike gates session navigation (Step 1 → Step 4); settings (Step 2) and the background widget (Step 3) are independent of the spike and of each other.
|
|
254
|
+
None of these steps edits `agent-menu.ts`, so they carry no shared-file collision on the menu — genuinely parallelizable, unlike the prior plan's Steps 2/3/5, which all collided on `agent-menu.ts` and `index.ts`.
|
|
255
|
+
Steps 2 and 4 each append a command registration to `index.ts` (additive, low-conflict).
|
|
256
|
+
Steps 4a (renderer parity) and 4b (file-snapshot source) complete the #445 slice behind its `TranscriptSource` seam; Step 4a gates Step 5, Step 4b is independent.
|
|
257
|
+
- **Track B — Dissolution (Steps 5 → 6):** the terminal cut, gated on all of Track A landing.
|
|
258
|
+
Hub-first ordering is forced: Step 5 deletes `agent-menu.ts` (orphaning the leaves), then Step 6 `git rm`s the now-orphaned definition-management subtree.
|
|
259
|
+
- **Track C — Test health (Step 7):** clone consolidation, run after the cut so no surviving helper is extracted into a doomed file.
|
|
260
|
+
|
|
261
|
+
## Release batches
|
|
262
|
+
|
|
263
|
+
- **Batch "dissolve-agents":** Steps 5, 6 (ship together; tail = Step 6).
|
|
264
|
+
Depends on Steps 2, 3, 4 already merged.
|
|
265
|
+
- Independently releasable: Steps 1, 2, 3, 4, 4a, 4b, 7.
|
|
266
|
+
|
|
267
|
+
## Follow-on issues
|
|
268
|
+
|
|
269
|
+
- [#470] — the terminal cut (Steps 5–6) removed `/agents`, the conversation viewer, and the creation wizard/config editor, but `README.md` was never updated and continued documenting the removed surface through the `pi-subagents-v18.0.0` release.
|
|
270
|
+
Filed and closed independently after the phase's steps landed; the README now documents `/subagents:settings`, `/subagents:sessions`, and the background widget.
|
|
271
|
+
|
|
272
|
+
[ADR-0004]: ../../decisions/0004-reconsider-ui-direction.md
|
|
273
|
+
[#441]: https://github.com/gotgenes/pi-packages/issues/441
|
|
274
|
+
[#442]: https://github.com/gotgenes/pi-packages/issues/442
|
|
275
|
+
[#443]: https://github.com/gotgenes/pi-packages/issues/443
|
|
276
|
+
[#444]: https://github.com/gotgenes/pi-packages/issues/444
|
|
277
|
+
[#445]: https://github.com/gotgenes/pi-packages/issues/445
|
|
278
|
+
[#446]: https://github.com/gotgenes/pi-packages/issues/446
|
|
279
|
+
[#447]: https://github.com/gotgenes/pi-packages/issues/447
|
|
280
|
+
[#462]: https://github.com/gotgenes/pi-packages/issues/462
|
|
281
|
+
[#463]: https://github.com/gotgenes/pi-packages/issues/463
|
|
282
|
+
[#470]: https://github.com/gotgenes/pi-packages/issues/470
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Phase 2: Remove scheduling
|
|
2
|
+
|
|
3
|
+
Deleted `schedule.ts`, `schedule-store.ts`, `ui/schedule-menu.ts`.
|
|
4
|
+
Removed the `schedule` parameter from the `Agent` tool schema.
|
|
5
|
+
Removed scheduler setup and lifecycle hooks from `index.ts`.
|
|
6
|
+
|
|
7
|
+
## Related issues
|
|
8
|
+
|
|
9
|
+
- #52 — Remove scheduling
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Phase 20: Result delivery extraction and boundary cleanup
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Phase 20 realizes the last un-extracted domain from the [first-principles refinement](../architecture.md#first-principles-refinement-and-the-deeper-target) — **result delivery** — and clears the residual boundary and complexity debt discovery surfaced around it.
|
|
6
|
+
|
|
7
|
+
Discovery findings (fallow + entry-point trace + test-constructibility audit, 2026-07-03):
|
|
8
|
+
|
|
9
|
+
1. `NotificationState` (`toolCallId`, `resultConsumed`) still lives on `Subagent`; `get-result-tool` reaches through `record.notification?.markConsumed()` twice, always paired with `notifications.cancelNudge(id)` — the doc's own "homeless field" (result-delivery domain) plus a scattered two-step reset.
|
|
10
|
+
2. Both `steer-tool` and `service-adapter` pre-check `status !== "running"` before calling `record.steer()` — ask-then-tell, contradicting the target's "tell by id, with outcomes" rule.
|
|
11
|
+
3. Five file-level eslint-disable headers (`agent-tool` disables 6 rules; `spawn-config` and `agent-widget` 4 each; `model-resolver` 2; `index` 1) and `model: unknown`/`Model<any>`/`any` threading through 8 files mark the SDK type boundary as the largest remaining `any` surface.
|
|
12
|
+
4. Three src functions carry HIGH CRAP scores (notification renderer arrow 79.4, `service-adapter.spawn` 71.3, `get-result-tool.execute` 63.6 — resolved by Step 2); `subagents-settings.handle` (13 cyclomatic, 24 cognitive) is three copy-pasted select→input→validate→apply branches; `service-adapter.ts` is the sole accelerating churn file.
|
|
13
|
+
5. `createTestSubagent` is the most complex function in the workspace (19 cyclomatic, 25 cognitive) because `SubagentStateInit` accepts only transition fields, forcing mutation loops to seed metrics — a Category D "shared factory complexity" signal pointing at the production init surface.
|
|
14
|
+
6. Test duplication sits at 9 in-package clone groups (81 lines), at the ≤ 10 target but with two consolidatable clone families.
|
|
15
|
+
|
|
16
|
+
No finding scores ≥ 20 on the priority scale (Impact × (6 − Risk)); the phase is a should-fix band (10–15) consolidation.
|
|
17
|
+
Directory organization is healthy (seven domain directories, six root files) — no reorg this phase.
|
|
18
|
+
|
|
19
|
+
All nine steps are closed: [#535], [#536], [#537], [#538], [#539], [#540], [#541], [#542], [#543].
|
|
20
|
+
Two metrics missed their target on delivery — source LOC and `createTestSubagent`'s cyclomatic complexity — recorded honestly below rather than papered over.
|
|
21
|
+
|
|
22
|
+
## Health metrics
|
|
23
|
+
|
|
24
|
+
| Metric | Phase 19 (end) | Phase 20 target | Phase 20 (delivered) |
|
|
25
|
+
| ------------------------------------------------ | ---------------- | ---------------------- | ----------------------------------------------- |
|
|
26
|
+
| Health score | 78/100 (B) | ≥ 78 (B) | 78/100 (B) ✅ |
|
|
27
|
+
| Source LOC | 7,068 (57 files) | ~7,050 (no net growth) | 7,211 (57 files) — +143 LOC ⚠️ (miss, see note) |
|
|
28
|
+
| `record.notification?.` reach-throughs | 4 sites | 0 | 0 ✅ |
|
|
29
|
+
| Steer status pre-checks outside `Subagent.steer` | 2 sites | 0 | 0 ✅ |
|
|
30
|
+
| src functions with CRAP ≥ 60 | 3 | 0 | 0 ✅ |
|
|
31
|
+
| File-level eslint-disable headers | 5 | ≤ 2 | 1 ✅ |
|
|
32
|
+
| `createTestSubagent` cyclomatic | 19 | ≤ 8 | 13 ⚠️ (miss — see Step 8) |
|
|
33
|
+
| Test clone groups (in-package) | 9 (81 lines) | — retired (see note) | retired ✅ |
|
|
34
|
+
|
|
35
|
+
The "Test clone groups (in-package)" metric is retired as of Step 9 ([#543]). fallow 3.2.0 excludes `**/*.test.*` from duplication detection by default, so `pnpm fallow dupes --workspace @gotgenes/pi-subagents` no longer surfaces test-file clones — the tool treats test-suite token runs as expected scaffolding.
|
|
36
|
+
The suites' arrange was already well-factored by Phase 17 and Phase 20 Step 8; the residual repetition is the system-under-test act call, which stays explicit per the `testing` skill.
|
|
37
|
+
|
|
38
|
+
Source LOC grew by 143 lines instead of holding flat: the phase's extraction and typed-boundary work (`get-result-report.ts`, the `NumericSettingDescriptor` table, `SteerOutcome`, `TuiSurface`, the extended `SubagentStateInit`) added more lines than the removed complexity subtracted.
|
|
39
|
+
`createTestSubagent`'s cyclomatic complexity dropped from 19 to 13 (not the targeted ≤ 8) — well below the fallow refactoring-target/large-function thresholds (20/30 CRAP) and off both lists, but short of the specific number the step's `Outcome:` line named.
|
|
40
|
+
Both misses are retro input for the next planning round, not corrected after the fact.
|
|
41
|
+
|
|
42
|
+
## Steps
|
|
43
|
+
|
|
44
|
+
### ✅ Step 1 — Extract result delivery from `Subagent` ([#535])
|
|
45
|
+
|
|
46
|
+
Smell: Category C (anemic domain / misplaced state, Law of Demeter, scattered resets) — the result-delivery domain named in the first-principles refinement is still fused into the execution record.
|
|
47
|
+
Target files:
|
|
48
|
+
|
|
49
|
+
- `src/lifecycle/subagent.ts` — drop `_notification` / `notification`; stop constructing `NotificationState` from `parentSession.toolCallId`.
|
|
50
|
+
- `src/observation/notification.ts` — `NotificationManager` owns consumed-state keyed by agent id behind a single tell operation (e.g. `consume(id)`) that also cancels the pending nudge.
|
|
51
|
+
- `src/observation/notification-state.ts` — dissolve into the manager or move wholly into the observation domain.
|
|
52
|
+
- `src/observation/subagent-events-observer.ts`, `src/tools/get-result-tool.ts` — call the new delivery interface instead of reaching through the record.
|
|
53
|
+
|
|
54
|
+
The `toolCallId` needed by `formatTaskNotification` already travels on `execution.parentSession`; expose it without routing through a notification object.
|
|
55
|
+
The pre-await consumption ordering (the "Bug 1" race tests in `test/lifecycle/subagent-manager.test.ts`) is a preserved invariant — consuming before awaiting must still suppress the completion nudge.
|
|
56
|
+
|
|
57
|
+
Outcome: zero `record.notification?.` reach-throughs in `src/`; `Subagent` carries no notification field; delivery state lives in the observation domain.
|
|
58
|
+
|
|
59
|
+
Landed: `notification-state.ts` deleted; `Subagent.toolCallId` getter added over `execution.parentSession`; `NotificationManager` owns `consumed: Set<string>` behind one `consume(id)` tell that adds to the set and cancels the pending nudge atomically.
|
|
60
|
+
Collapsing the old two-step reset (`markConsumed()` + `cancelNudge()`) into one atomic operation structurally eliminates the historical "Bug 1" race rather than just reordering it — `consume()` now suppresses the nudge regardless of whether it runs before or after the completion promise resolves, as long as it runs within the 200 ms hold window.
|
|
61
|
+
|
|
62
|
+
`Release: batch "result-delivery"`
|
|
63
|
+
|
|
64
|
+
### ✅ Step 2 — Decompose `get-result-tool.execute` ([#536])
|
|
65
|
+
|
|
66
|
+
Smell: Category B (oversized function) — 61 lines, 15 cyclomatic, CRAP 63.6; mixes wait/consume policy, stats formatting, and output assembly.
|
|
67
|
+
Target files:
|
|
68
|
+
|
|
69
|
+
- `src/tools/get-result-tool.ts` — extract a pure report formatter (status line, stats parts, body selection) alongside the existing `result-renderer.ts` pattern; consume via the Step 1 delivery interface.
|
|
70
|
+
- `test/tools/get-result-tool.test.ts` — unit-test the pure formatter directly.
|
|
71
|
+
|
|
72
|
+
Outcome: `execute` ≤ 30 lines with cyclomatic < 10; off the fallow high-complexity list.
|
|
73
|
+
|
|
74
|
+
Landed: `src/tools/get-result-report.ts` added — `AgentReport` value object plus `renderStatsParts` / `renderReportBody` / `formatAgentReport` pure functions, unit-tested directly in `test/tools/get-result-report.test.ts`.
|
|
75
|
+
`GetResultTool.execute` now owns only record lookup and the wait/consume policy (13 lines), delegating report assembly to a private `buildReport` + `formatAgentReport`; output is byte-identical.
|
|
76
|
+
`get-result-tool.execute` is off the HIGH-CRAP list (3 → 2 remaining: `service-adapter.spawn`, the notification renderer arrow).
|
|
77
|
+
|
|
78
|
+
`Release: batch "result-delivery"`
|
|
79
|
+
|
|
80
|
+
### ✅ Step 3 — `Subagent.steer` returns an outcome ([#537])
|
|
81
|
+
|
|
82
|
+
Smell: Category C (ask-then-tell) — coordinators pre-check status before telling.
|
|
83
|
+
Target files:
|
|
84
|
+
|
|
85
|
+
- `src/lifecycle/subagent.ts` — `steer` owns the non-running rejection and returns a discriminated outcome (`delivered` / `buffered` / `rejected` with the observed status).
|
|
86
|
+
- `src/tools/steer-tool.ts`, `src/service/service-adapter.ts` — drop the status pre-checks and switch on the outcome; the adapter maps the outcome to the public `SubagentsService.steer` boolean, so the published contract is unchanged.
|
|
87
|
+
|
|
88
|
+
Outcome: zero steer status pre-checks outside `Subagent.steer`; `steer-tool.execute` cyclomatic drops below 10.
|
|
89
|
+
|
|
90
|
+
Landed: `Subagent.steer` returns a discriminated `SteerOutcome` (`delivered` / `buffered` / `rejected` with the observed status) and owns the non-running rejection as its first guard; `SteerOutcome` is exported from `subagent.ts` and re-exported via `types.ts`.
|
|
91
|
+
`SteerTool.execute` and `SubagentsServiceAdapter.steer` dropped their `status !== "running"` pre-checks and switch on the outcome — the adapter maps `outcome.kind !== "rejected"` to the unchanged public boolean, and the tool's delivered-path stats moved into a private `renderDelivered` helper.
|
|
92
|
+
Zero steer status pre-checks remain outside `Subagent.steer`.
|
|
93
|
+
|
|
94
|
+
`Release: independent`
|
|
95
|
+
|
|
96
|
+
### ✅ Step 4 — Type the model boundary ([#538])
|
|
97
|
+
|
|
98
|
+
Smell: Category C (platform type threading) — `ModelRegistry.find/getAll/getAvailable` return `any`, forcing `any`/`unknown` model threading through `model-resolver`, `spawn-config`, `service-adapter`, and `parent-snapshot`.
|
|
99
|
+
Target files:
|
|
100
|
+
|
|
101
|
+
- `src/session/model-resolver.ts` — type the registry against `Model<any>` from `@earendil-works/pi-ai` (already imported elsewhere); remove the file-level eslint-disable; extract the fuzzy-scoring loop as a named helper if `resolveModel` (17 cyclomatic, 60 lines) still trips the threshold.
|
|
102
|
+
- `src/service/service-adapter.ts` — type the resolved model in `spawn` (16 cyclomatic, CRAP 71.3, sole accelerating churn file) and extract the model-resolution branch.
|
|
103
|
+
- `src/tools/spawn-config.ts` — shrink the 4-rule file-level disable to line-level or remove it.
|
|
104
|
+
|
|
105
|
+
Outcome: `model-resolver.ts` file-level eslint-disable removed; `service-adapter.spawn` off the HIGH CRAP list; `any` model returns eliminated from the resolver.
|
|
106
|
+
|
|
107
|
+
Landed: `ModelRegistry.find/getAll/getAvailable` and `resolveModel`'s return are typed against `Model<any>` from `@earendil-works/pi-ai`; the file-level eslint-disable headers on `model-resolver.ts` (2 rules) and `spawn-config.ts` (4 rules) are both removed — running disable-header tally 5 → 3 (`agent-tool` 6, `agent-widget` 4, `index` 1 remain, all Step 5 scope).
|
|
108
|
+
`resolveModel`'s fuzzy-scoring loop was extracted to a private `findBestFuzzyMatch` helper, dropping `resolveModel` off the complexity list entirely.
|
|
109
|
+
`service-adapter.spawn`'s model-resolution branch was extracted to a private `resolveModelOption`, dropping `spawn` from 16 cyclomatic / CRAP 71.3 (HIGH) to 13 cyclomatic / CRAP 49.5 (moderate) — off the HIGH-CRAP list; running HIGH-CRAP tally 2 → 1 remaining (the notification renderer arrow, untouched — Step 7 scope).
|
|
110
|
+
`resolveInvocationModel` gained a `registry: ModelRegistry | undefined` guard (typed error instead of a crash when a model override is requested with no registry present); the residual `unknown` thread through `ParentSnapshot.model` / `SessionContext.model` is a separate SDK-boundary gap, deferred.
|
|
111
|
+
|
|
112
|
+
`Release: independent`
|
|
113
|
+
|
|
114
|
+
### ✅ Step 5 — Narrow `tui`/`theme` render interfaces ([#539])
|
|
115
|
+
|
|
116
|
+
Smell: Category C/D (platform type threading; wide `any` params in render callbacks).
|
|
117
|
+
Target files:
|
|
118
|
+
|
|
119
|
+
- `src/ui/agent-widget.ts` — replace `tui: any` with a lean local interface (`terminal.columns`, `requestRender()`); shrink the 4-rule file-level disable.
|
|
120
|
+
- `src/tools/agent-tool.ts` — type `renderCall`/`renderResult` params (`theme`, `result`) with lean local interfaces; shrink the 6-rule file-level disable to the genuinely SDK-gapped lines.
|
|
121
|
+
- `src/tools/foreground-runner.ts` — retire the line-level `details as any` cast if the SDK surface allows.
|
|
122
|
+
|
|
123
|
+
Some disables are irreducible SDK export gaps; the goal is line-level precision, not zero.
|
|
124
|
+
|
|
125
|
+
Outcome: file-level eslint-disable headers 5 → ≤ 2; remaining suppressions are line-level with named rules.
|
|
126
|
+
|
|
127
|
+
Landed: `agent-widget.ts` gained a lean local `TuiSurface` interface (`{ terminal: { columns }, requestRender() }`) replacing all three `tui: any` sites; its 4-rule file-level disable is removed.
|
|
128
|
+
`agent-tool.ts`'s `renderCall`/`renderResult` now type `theme` against the existing local `display.Theme` and `result` against the SDK-exported `AgentToolResult<AgentDetails | undefined>`/`ToolRenderResultOptions`; `textResult` was retyped (`details?: AgentDetails`) so the tool's inferred `TDetails` is honest end-to-end, eliminating the `result.details` cast; `ctx` params are typed `ExtensionContext`.
|
|
129
|
+
Its 6-rule file-level disable is removed with zero residual — a pre-existing `params.resume` (`unknown`) gap surfaced at three template-literal sites once the header lifted, fixed with the same `as string` cast already used a few lines away for `getRecord`/`resume`.
|
|
130
|
+
`foreground-runner.ts`'s `details as any` cast and its line-level disable are retired.
|
|
131
|
+
Running disable-header tally 3 → 1 (only `index.ts`'s 1-rule `no-unsafe-argument` remains, an accepted SDK gap outside this step's scope) — under the `≤ 2` Phase 20 target.
|
|
132
|
+
|
|
133
|
+
`Release: independent`
|
|
134
|
+
|
|
135
|
+
### ✅ Step 6 — Table-driven settings handler ([#540])
|
|
136
|
+
|
|
137
|
+
Smell: Category B (function duplication inside one function) — `subagents-settings.handle` (13 cyclomatic, 24 cognitive, 52 lines) repeats the select→input→parse→validate→apply→notify flow three times.
|
|
138
|
+
Target files:
|
|
139
|
+
|
|
140
|
+
- `src/ui/subagents-settings.ts` — describe each numeric setting as a descriptor (label, prompt, minimum, validation message, apply method) and drive one loop over the table.
|
|
141
|
+
- `test/ui/subagents-settings.test.ts` — assert per-descriptor behavior.
|
|
142
|
+
|
|
143
|
+
Outcome: `handle` cyclomatic ≤ 6 and cognitive ≤ 10; off the fallow high-complexity list.
|
|
144
|
+
|
|
145
|
+
Landed: `handle` now dispatches through a module-private `NumericSettingDescriptor` table (label, current-value display, input title/default, minimum, validation message, apply method) with a single `select` → `find` → `input` → `parse` → `validate` → `apply`/`notify` pass; the three copy-pasted branches are gone.
|
|
146
|
+
The validation comparison direction (`n >= descriptor.minimum`) was kept unchanged and pinned with a new non-numeric-input regression test before the rewrite, so `NaN` from a malformed input still warns rather than silently applying.
|
|
147
|
+
`subagents-settings.ts` no longer appears in fallow's hotspot list (file-level cyclomatic 19 / cognitive 7 across 13 small functions, `crap_above_threshold: 0`) — off the fallow high-complexity list.
|
|
148
|
+
|
|
149
|
+
`Release: independent`
|
|
150
|
+
|
|
151
|
+
### ✅ Step 7 — Decompose the notification renderer ([#541])
|
|
152
|
+
|
|
153
|
+
Smell: Category B/D (oversized arrow, untested complexity) — the renderer arrow in `src/observation/renderer.ts` is fallow's top triage concern (17 cyclomatic, CRAP 79.4).
|
|
154
|
+
Target files:
|
|
155
|
+
|
|
156
|
+
- `src/observation/renderer.ts` — extract pure line-assembly helpers (status→icon/label selection, stats-parts assembly, preview truncation) that are unit-testable without `Text` or a theme; the arrow becomes a thin wrapper.
|
|
157
|
+
- `test/observation/renderer.test.ts` — test the pure helpers directly.
|
|
158
|
+
|
|
159
|
+
Soft ordering: land after Step 1 so the notification-domain files settle first.
|
|
160
|
+
|
|
161
|
+
Outcome: renderer arrow cyclomatic < 10; `renderer.ts` off the top of the fallow triage list.
|
|
162
|
+
|
|
163
|
+
Landed: extracted three pure, exported helpers — `resolveStatusPresentation` (status→icon/label, the one OCP dispatch point), `buildStatsParts` (ISP-narrowed `StatsSource` `Pick` over `NotificationDetails`), and `buildPreviewLines` (collapsed 80-column slice vs. expanded 30-line cap).
|
|
164
|
+
The arrow now composes the three helpers and applies theme styling only; marker/indentation/`theme.fg` assembly stayed in the wrapper so rendered output is unchanged.
|
|
165
|
+
`renderer.ts` no longer appears in `fallow health --targets` (0 refactoring targets) or the file-scores list for the package — off the triage list entirely.
|
|
166
|
+
The steered-status wrapper test was pruned as fully subsumed by the new `resolveStatusPresentation` unit test; all other wrapper tests were kept because each exercises genuine multi-piece theme composition the pure helpers don't cover.
|
|
167
|
+
|
|
168
|
+
`Release: independent`
|
|
169
|
+
|
|
170
|
+
### ✅ Step 8 — Full-value `SubagentStateInit` ([#542])
|
|
171
|
+
|
|
172
|
+
Smell: Category D (shared factory complexity → narrow/complete the production init surface) — `createTestSubagent` (19 cyclomatic, 25 cognitive) seeds metrics via mutation loops because `SubagentStateInit` accepts only transition fields.
|
|
173
|
+
Target files:
|
|
174
|
+
|
|
175
|
+
- `src/lifecycle/subagent-state.ts` — extend `SubagentStateInit` to optionally seed the full value (toolUses, lifetimeUsage, compactionCount, turnCount, activeTools, responseText); a value object is legitimately constructible at any point in its value space.
|
|
176
|
+
- `test/helpers/make-subagent.ts` — collapse the mutation loops into direct init.
|
|
177
|
+
|
|
178
|
+
Outcome: `createTestSubagent` cyclomatic ≤ 8; off the fallow complexity list; no production behavior change.
|
|
179
|
+
|
|
180
|
+
Landed: extended `SubagentStateInit` with six optional value fields (`toolUses`, `lifetimeUsage`, `compactionCount`, `turnCount`, `activeTools`, `responseText`), seeded in the constructor — `lifetimeUsage` is spread-copied so a later `addUsage` cannot mutate the caller's object, and `activeTools` is seeded by name through `addActiveTool` to preserve the `_toolKeySeq` keying invariant.
|
|
181
|
+
`createTestSubagent` collapsed its post-construction mutation loops into direct init and dropped off both the fallow refactoring-targets and large-functions lists (was 19 cyclomatic, the workspace's most complex function).
|
|
182
|
+
No production behavior change — the accumulation methods stay as the `record-observer` runtime path.
|
|
183
|
+
|
|
184
|
+
Delivered number (recomputed at archive time): `createTestSubagent` is now 13 cyclomatic / 12 cognitive — a real drop from 19, and off both fallow lists (default thresholds: 20 cyclomatic, 30 CRAP), but short of the `≤ 8` target named in `Outcome:`.
|
|
185
|
+
The destructured-overrides parameter and its four `?? default` / `!== undefined` conditional-spread pairs account for the residual complexity; a further reduction would need a different construction shape (e.g. a builder), judged out of scope for this step.
|
|
186
|
+
|
|
187
|
+
`Release: independent`
|
|
188
|
+
|
|
189
|
+
### ✅ Step 9 — Consolidate remaining test clone families ([#543])
|
|
190
|
+
|
|
191
|
+
Smell: Category D (test duplication) — two clone families (`spawn-config.test.ts`: 2 groups / 21 lines; `subagent-manager.test.ts`: 2 groups / 15 lines) plus the `session-config.test.ts` pair (16 lines).
|
|
192
|
+
Target files: the three test files and `test/helpers/` as needed.
|
|
193
|
+
|
|
194
|
+
Runs last — Steps 1–3 and 8 rewrite portions of these suites, so consolidating first would churn twice.
|
|
195
|
+
|
|
196
|
+
Outcome: the numeric clone-group target is retired — fallow 3.2.0 excludes `**/*.test.*` from duplication detection, so `fallow dupes` no longer surfaces in-package test clones.
|
|
197
|
+
The suites' arrange was already well-factored by Phase 17 and Phase 20 Step 8; the residual repetition is the system-under-test act call, retained explicitly per the `testing` skill.
|
|
198
|
+
|
|
199
|
+
`Release: independent`
|
|
200
|
+
|
|
201
|
+
Landed: the `SubagentManager — lifecycle observer forwarding` describe's shared arrange hoisted into a describe-scoped `beforeEach` (`subagent-manager.test.ts`) — the one genuine arrange consolidation found.
|
|
202
|
+
`spawn-config.test.ts` and `session-config.test.ts` are unchanged: their only repetition is the act call.
|
|
203
|
+
The Phase 20 health-metrics row is retired with a rationale note (fallow's test-ignore).
|
|
204
|
+
|
|
205
|
+
## Step dependencies
|
|
206
|
+
|
|
207
|
+
```mermaid
|
|
208
|
+
flowchart LR
|
|
209
|
+
S1["✅ Step 1 (#535)<br/>Result delivery off Subagent"] --> S2["✅ Step 2 (#536)<br/>Decompose get-result-tool"]
|
|
210
|
+
S1 -.soft.-> S7["✅ Step 7 (#541)<br/>Decompose notification renderer"]
|
|
211
|
+
S3["✅ Step 3 (#537)<br/>Steer returns an outcome"]
|
|
212
|
+
S4["✅ Step 4 (#538)<br/>Type the model boundary"]
|
|
213
|
+
S5["✅ Step 5 (#539)<br/>Narrow tui/theme interfaces"]
|
|
214
|
+
S6["✅ Step 6 (#540)<br/>Table-driven settings handler"]
|
|
215
|
+
S8["✅ Step 8 (#542)<br/>Full-value SubagentStateInit"] --> S9["✅ Step 9 (#543)<br/>Consolidate test clones"]
|
|
216
|
+
S2 --> S9
|
|
217
|
+
S3 --> S9
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Parallel tracks
|
|
221
|
+
|
|
222
|
+
- **Track A — Result delivery:** Steps 1 → 2, then 7 (soft).
|
|
223
|
+
- **Track B — Tell-don't-ask:** Step 3.
|
|
224
|
+
- **Track C — SDK boundary:** Steps 4, 5 (independent of each other).
|
|
225
|
+
- **Track D — UI polish:** Step 6.
|
|
226
|
+
- **Track E — Test health:** Step 8, then 9 (9 also waits on Tracks A/B test churn).
|
|
227
|
+
|
|
228
|
+
Tracks A–D can proceed in parallel; only Step 9 serializes behind the rest.
|
|
229
|
+
|
|
230
|
+
## Release batches
|
|
231
|
+
|
|
232
|
+
- **Batch "result-delivery":** Steps 1, 2 (ship together; tail = Step 2).
|
|
233
|
+
- Independently releasable: Steps 3, 4, 5, 6, 7, 8, 9.
|
|
234
|
+
|
|
235
|
+
Every step lands as a `refactor:`/`test:` commit — hidden changelog types that cut no release on their own; the work auto-batches into the next unhidden release.
|
|
236
|
+
|
|
237
|
+
[#535]: https://github.com/gotgenes/pi-packages/issues/535
|
|
238
|
+
[#536]: https://github.com/gotgenes/pi-packages/issues/536
|
|
239
|
+
[#537]: https://github.com/gotgenes/pi-packages/issues/537
|
|
240
|
+
[#538]: https://github.com/gotgenes/pi-packages/issues/538
|
|
241
|
+
[#539]: https://github.com/gotgenes/pi-packages/issues/539
|
|
242
|
+
[#540]: https://github.com/gotgenes/pi-packages/issues/540
|
|
243
|
+
[#541]: https://github.com/gotgenes/pi-packages/issues/541
|
|
244
|
+
[#542]: https://github.com/gotgenes/pi-packages/issues/542
|
|
245
|
+
[#543]: https://github.com/gotgenes/pi-packages/issues/543
|