@gotgenes/pi-subagents 7.3.1 → 7.4.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.
@@ -5,6 +5,63 @@ issue_title: "Extract shared test fixtures to reduce test duplication"
5
5
 
6
6
  # Retro: #208 — Extract shared test fixtures to reduce test duplication
7
7
 
8
+ ## Stage: Final Retrospective (2026-05-25T22:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Reviewed the planning, TDD, and shipping sessions for issue #208.
13
+ The work completed Phase 12 of the pi-subagents improvement roadmap.
14
+ Identified Vitest v4 type compatibility as the dominant friction pattern, and the user surfaced a strategic insight: shared test factories are treating symptoms of ISP violations in production interfaces.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The planning session's duplication diff (comparing default values across all copies before writing the shared factory) prevented cascading assertion failures during migration — the testing skill rule paid off.
21
+ - Using a Python script for the `agent-manager.test.ts` spawn consolidation (step 9) handled 42 multi-line replacements with 7 distinct patterns cleanly in one pass.
22
+ - `pnpm run check` caught 4 separate type errors that Vitest's esbuild would have silently ignored at runtime: wrong `findAgentFile` parameter type, dropped `AgentConfig` import, unused `@ts-expect-error` directives, and `Mock<Procedure>` incompatibility.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ 1. `missing-context` — Wrote `vi.fn().mockReturnValue([])` in the shared factory without checking whether `Mock<Procedure>` is structurally assignable to the production function signatures (`RunnerIO`, `AgentFileOps`).
27
+ The testing skill warns about `ReturnType<typeof vi.fn>` at the field level, but I didn't connect this to the structural-compatibility scenario where a factory's return value flows to a typed production parameter.
28
+ Impact: 37 type errors after step 2, requiring a fixup commit (2953adc) and removal of the `assemblerOverrides` parameter.
29
+ 2. `missing-context` — Assumed `findAgentFile` took `(dir: string, name: string)` based on test usage patterns rather than checking the `AgentFileOps` interface.
30
+ The actual signature is `(name: string, dirs: string[])` — the second parameter is `string[]`.
31
+ Impact: caught by `pnpm run check`, fixed in the same commit, no rework.
32
+ 3. `scope-drift` — Removed `import type { AgentConfig }` from `agent-config-editor.test.ts` because the symbols it was needed for (`testDefaultConfig`, `testCustomConfig`) were being removed.
33
+ Didn't check that `buildEjectContent` tests 400 lines later also used `AgentConfig`.
34
+ Impact: caught by `pnpm run check`, one-line fix, no rework.
35
+ 4. `premature-convergence` — Added `@ts-expect-error` directives for `.mock` property access in the factory tests, assuming TypeScript wouldn't know about Mock's `.mock` property.
36
+ Once the `vi.fn()` stubs were typed with implementations, TypeScript recognized the Mock type and `.mock` was accessible, making all 5 directives unused.
37
+ Impact: added friction but no rework — eslint caught them.
38
+
39
+ #### What caused friction (user side)
40
+
41
+ - No friction caused by the user.
42
+ The user's post-ship reflection ("we need to eliminate complex setup, right?") was a strategic insight that elevated the conversation from mechanical duplication reduction to ISP-driven interface narrowing.
43
+ This kind of mid-session reframing is valuable and came at exactly the right time — after the work was done, so it informs future phases rather than disrupting the current one.
44
+
45
+ #### Phase 13 guidance: ISP narrowing targets
46
+
47
+ The `test/helpers/` factory inventory is a concrete map of production interfaces worth narrowing:
48
+
49
+ | Factory | Production interface | Methods | Consumer usage |
50
+ | ------------------- | ------------------------------------------------------------- | ----------------------------- | -------------------------------- |
51
+ | `createRunnerIO` | `RunnerIO` (`EnvironmentIO & SessionFactoryIO`) | 8 (7 functions + assemblerIO) | Most tests use 2–3 |
52
+ | `makeFileOps` | `AgentFileOps` | 6 | Most tests use `exists` + `read` |
53
+ | `createToolDeps` | `AgentToolManager` + `AgentToolRuntime` + `AgentToolSettings` | 12+ across 3 interfaces | Spawners use 2–3 each |
54
+ | `createMockSession` | `AgentSession` (SDK class) | 5 stubs | Observers use `subscribe` only |
55
+ | `createAgentLookup` | `AgentConfigLookup` | 2 | Already narrow ✓ |
56
+
57
+ When a factory needs its own unit tests, the interface it stubs is too wide for its consumers.
58
+ The fix is ISP narrowing of the production interface, not more test infrastructure.
59
+
60
+ ### Changes made
61
+
62
+ 1. Added rule to `testing` skill: typed implementations in shared factories when return value satisfies a production interface.
63
+ 2. Added ISP signal to `improvement-discovery` skill Category D table: shared factory complexity → narrow the production interface.
64
+
8
65
  ## Stage: Implementation — TDD (2026-05-25T21:00:00Z)
9
66
 
10
67
  ### Session summary
@@ -0,0 +1,66 @@
1
+ ---
2
+ issue: 214
3
+ issue_title: "Convert remaining closure factories to classes (Phase 13, Step 1)"
4
+ ---
5
+
6
+ # Retro: #214 — Convert remaining closure factories to classes
7
+
8
+ ## Stage: Planning (2026-05-25T20:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 4-step TDD plan to convert the three remaining closure factories (`createAgentConfigEditor`, `createAgentCreationWizard`, `createSubagentsService`) to classes.
13
+ Each conversion is one commit covering source, test, and consumer updates together.
14
+
15
+ ### Observations
16
+
17
+ - The conversions are entirely mechanical — same pattern as Phase 11 (#195, #196).
18
+ No design ambiguity requiring user input.
19
+ - `AgentCreationWizardDeps` is only used within its own file, so removing it is safe.
20
+ The class dissolves the deps bag into positional constructor params for consistency with `AgentConfigEditor`.
21
+ - The `agent-creation-wizard.test.ts` has ~18 inline `createAgentCreationWizard(deps)` calls; the plan suggests adding a `makeWizard(deps)` helper to centralize construction and reduce the diff size.
22
+ - `SubagentsServiceAdapter` uses `implements SubagentsService` for compile-time verification, unlike the factory which relied on structural typing of the returned object literal.
23
+ - Pure helper functions (`buildMenuOptions`, `buildEjectContent`, `toSubagentRecord`) and narrow interfaces (`AgentManagerLike`, `ServiceRuntimeLike`, `WizardManager`, `WizardRegistry`) remain unchanged.
24
+
25
+ ## Stage: Implementation — TDD (2026-05-25T21:00:00Z)
26
+
27
+ ### Session summary
28
+
29
+ All 4 TDD steps completed in 4 commits.
30
+ Three closure factories converted to classes (`AgentConfigEditor`, `AgentCreationWizard`, `SubagentsServiceAdapter`) with tests and consumers updated in the same commit as each production change.
31
+ Test count held at 913 (57 files) — no new tests needed, no tests removed.
32
+
33
+ ### Observations
34
+
35
+ - All three conversions were mechanical find-and-replace with no behavioral surprises.
36
+ - The `makeWizard(deps)` helper in `agent-creation-wizard.test.ts` centralized 14 inline `createAgentCreationWizard(deps)` calls, keeping the diff readable.
37
+ - `SubagentsServiceAdapter` uses `implements SubagentsService` — the TypeScript compiler confirmed the contract at compile time with no gaps.
38
+ - Adding the `SpawnOptions` import to `service-adapter.ts` was required for the `spawn` method signature; the plan anticipated this correctly.
39
+ - The `sed -i` command required the macOS `-i ''` form (no in-place backup extension) rather than the GNU `sed -i` form.
40
+ - Dead-code gate (`pnpm fallow dead-code`) passed cleanly from the repo root — no suppression needed.
41
+
42
+ ## Stage: Final Retrospective (2026-05-25T22:00:00Z)
43
+
44
+ ### Session summary
45
+
46
+ Shipped `pi-subagents-v7.3.2` with 3 refactor commits converting all remaining closure factories to classes.
47
+ All 4 lifecycle stages (plan → TDD → ship → retro) completed in a single day with zero rework and zero deviations from the plan.
48
+
49
+ ### Observations
50
+
51
+ #### What went well
52
+
53
+ - Strong precedent from Phase 11 (#195, #196) made this issue zero-friction — the plan, implementation, and test updates all followed an established template.
54
+ - The plan's prediction of a `makeWizard(deps)` helper for `agent-creation-wizard.test.ts` kept the step-2 diff readable by centralizing 14 inline constructor calls.
55
+ - `SubagentsServiceAdapter implements SubagentsService` gave compile-time contract verification, catching any interface drift immediately via `pnpm run check`.
56
+ - The plan correctly anticipated the `SpawnOptions` import need in `service-adapter.ts`.
57
+
58
+ #### What caused friction (agent side)
59
+
60
+ - None.
61
+ This was a textbook mechanical refactoring with no behavioral changes, no edge cases, and no test rework.
62
+
63
+ #### What caused friction (user side)
64
+
65
+ - None.
66
+ The issue was well-scoped with explicit target files and a clear precedent to follow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.3.1",
3
+ "version": "7.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
package/src/index.ts CHANGED
@@ -31,7 +31,7 @@ import { buildEventData, type NotificationDetails, NotificationManager } from "#
31
31
  import { createNotificationRenderer } from "#src/observation/renderer";
32
32
  import { createSubagentRuntime } from "#src/runtime";
33
33
  import { publishSubagentsService, unpublishSubagentsService } from "#src/service/service";
34
- import { createSubagentsService } from "#src/service/service-adapter";
34
+ import { SubagentsServiceAdapter } from "#src/service/service-adapter";
35
35
  import { detectEnv } from "#src/session/env";
36
36
 
37
37
  import { resolveModel } from "#src/session/model-resolver";
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI) {
157
157
 
158
158
  // Typed service published via Symbol.for() for cross-extension access.
159
159
  // Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
160
- const service = createSubagentsService(manager, resolveModel, runtime);
160
+ const service = new SubagentsServiceAdapter(manager, resolveModel, runtime);
161
161
  publishSubagentsService(service);
162
162
 
163
163
  const lifecycle = new SessionLifecycleHandler(
@@ -455,8 +455,9 @@ export function getAgentConversation(session: AgentSession): string {
455
455
  if (text.trim()) parts.push(`[User]: ${text.trim()}`);
456
456
  } else if (msg.role === "assistant") {
457
457
  const { textParts, toolNames } = extractAssistantContent(msg.content);
458
+ const attribution = formatAttribution(msg);
458
459
  if (textParts.length > 0)
459
- parts.push(`[Assistant]: ${textParts.join("\n")}`);
460
+ parts.push(`[Assistant${attribution}]: ${textParts.join("\n")}`);
460
461
  if (toolNames.length > 0)
461
462
  parts.push(`[Tool Calls]:\n${toolNames.map((n) => ` Tool: ${n}`).join("\n")}`);
462
463
  } else if (msg.role === "toolResult") {
@@ -468,3 +469,11 @@ export function getAgentConversation(session: AgentSession): string {
468
469
 
469
470
  return parts.join("\n\n");
470
471
  }
472
+
473
+ /** Build a `(provider/model)` attribution suffix for assistant messages. */
474
+ function formatAttribution(msg: { provider?: string; model?: string }): string {
475
+ const { provider, model } = msg;
476
+ if (!provider && !model) return "";
477
+ if (provider && model) return ` (${provider}/${model})`;
478
+ return ` (${provider ?? model})`;
479
+ }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
9
- import type { SubagentRecord, SubagentsService } from "#src/service/service";
9
+ import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
10
10
  import type { ModelRegistry } from "#src/session/model-resolver";
11
11
  import type { AgentRecord, SessionContext } from "#src/types";
12
12
 
@@ -30,83 +30,83 @@ export interface ServiceRuntimeLike {
30
30
  buildSnapshot(inheritContext: boolean): ParentSnapshot;
31
31
  }
32
32
 
33
- /** Create a SubagentsService backed by the given dependencies. */
34
- export function createSubagentsService(
35
- manager: AgentManagerLike,
36
- resolveModel: (input: string, registry: ModelRegistry) => unknown,
37
- runtime: ServiceRuntimeLike,
38
- ): SubagentsService {
39
- return {
40
- spawn(type: string, prompt: string, options?) {
41
- if (!runtime.currentCtx) {
42
- throw new Error("No active session — cannot spawn agents outside a session.");
33
+ /** Adapter that wraps AgentManager to satisfy SubagentsService. */
34
+ export class SubagentsServiceAdapter implements SubagentsService {
35
+ constructor(
36
+ private readonly manager: AgentManagerLike,
37
+ private readonly resolveModel: (input: string, registry: ModelRegistry) => unknown,
38
+ private readonly runtime: ServiceRuntimeLike,
39
+ ) {}
40
+
41
+ spawn(type: string, prompt: string, options?: SpawnOptions): string {
42
+ if (!this.runtime.currentCtx) {
43
+ throw new Error("No active session — cannot spawn agents outside a session.");
44
+ }
45
+
46
+ let model: unknown;
47
+ if (options?.model) {
48
+ const registry = this.runtime.currentCtx.modelRegistry;
49
+ if (!registry) {
50
+ throw new Error("No model registry available.");
43
51
  }
44
-
45
- let model: unknown;
46
- if (options?.model) {
47
- const registry = runtime.currentCtx.modelRegistry;
48
- if (!registry) {
49
- throw new Error("No model registry available.");
50
- }
51
- const resolved = resolveModel(options.model, registry);
52
- if (typeof resolved === "string") {
53
- throw new Error(resolved);
54
- }
55
- model = resolved;
56
- }
57
-
58
- const description = options?.description ?? prompt.slice(0, 80);
59
- const isBackground = !(options?.foreground ?? false);
60
-
61
- const snapshot = runtime.buildSnapshot(options?.inheritContext ?? false);
62
- return manager.spawn(snapshot, type, prompt, {
63
- description,
64
- model,
65
- maxTurns: options?.maxTurns,
66
- thinkingLevel: options?.thinkingLevel,
67
- isolated: options?.isolated,
68
- inheritContext: options?.inheritContext,
69
- bypassQueue: options?.bypassQueue,
70
- isolation: options?.isolation,
71
- isBackground,
72
- });
73
- },
74
-
75
- getRecord(id: string): SubagentRecord | undefined {
76
- const record = manager.getRecord(id);
77
- return record ? toSubagentRecord(record) : undefined;
78
- },
79
-
80
- listAgents(): SubagentRecord[] {
81
- return manager.listAgents().map(toSubagentRecord);
82
- },
83
-
84
- abort(id: string): boolean {
85
- return manager.abort(id);
86
- },
87
-
88
- async steer(id: string, message: string): Promise<boolean> {
89
- const record = manager.getRecord(id);
90
- if (record?.status !== "running") {
91
- return false;
92
- }
93
- const session = record.session;
94
- if (!session) {
95
- // Session not ready yet — queue via manager for delivery once initialized
96
- return manager.queueSteer(id, message);
52
+ const resolved = this.resolveModel(options.model, registry);
53
+ if (typeof resolved === "string") {
54
+ throw new Error(resolved);
97
55
  }
98
- await session.steer(message);
99
- return true;
100
- },
101
-
102
- async waitForAll(): Promise<void> {
103
- return manager.waitForAll();
104
- },
105
-
106
- hasRunning(): boolean {
107
- return manager.hasRunning();
108
- },
109
- };
56
+ model = resolved;
57
+ }
58
+
59
+ const description = options?.description ?? prompt.slice(0, 80);
60
+ const isBackground = !(options?.foreground ?? false);
61
+
62
+ const snapshot = this.runtime.buildSnapshot(options?.inheritContext ?? false);
63
+ return this.manager.spawn(snapshot, type, prompt, {
64
+ description,
65
+ model,
66
+ maxTurns: options?.maxTurns,
67
+ thinkingLevel: options?.thinkingLevel,
68
+ isolated: options?.isolated,
69
+ inheritContext: options?.inheritContext,
70
+ bypassQueue: options?.bypassQueue,
71
+ isolation: options?.isolation,
72
+ isBackground,
73
+ });
74
+ }
75
+
76
+ getRecord(id: string): SubagentRecord | undefined {
77
+ const record = this.manager.getRecord(id);
78
+ return record ? toSubagentRecord(record) : undefined;
79
+ }
80
+
81
+ listAgents(): SubagentRecord[] {
82
+ return this.manager.listAgents().map(toSubagentRecord);
83
+ }
84
+
85
+ abort(id: string): boolean {
86
+ return this.manager.abort(id);
87
+ }
88
+
89
+ async steer(id: string, message: string): Promise<boolean> {
90
+ const record = this.manager.getRecord(id);
91
+ if (record?.status !== "running") {
92
+ return false;
93
+ }
94
+ const session = record.session;
95
+ if (!session) {
96
+ // Session not ready yet — queue via manager for delivery once initialized
97
+ return this.manager.queueSteer(id, message);
98
+ }
99
+ await session.steer(message);
100
+ return true;
101
+ }
102
+
103
+ async waitForAll(): Promise<void> {
104
+ return this.manager.waitForAll();
105
+ }
106
+
107
+ hasRunning(): boolean {
108
+ return this.manager.hasRunning();
109
+ }
110
110
  }
111
111
 
112
112
  /**
@@ -61,86 +61,88 @@ export function buildEjectContent(cfg: AgentConfig): string {
61
61
  return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
62
62
  }
63
63
 
64
- // ---- Factory ----
65
-
66
- export function createAgentConfigEditor(
67
- fileOps: AgentFileOps,
68
- registry: AgentTypeRegistry,
69
- personalAgentsDir: string,
70
- projectAgentsDir: string,
71
- ) {
72
- function agentDirs(): string[] {
73
- return [projectAgentsDir, personalAgentsDir];
64
+ // ---- Class ----
65
+
66
+ export class AgentConfigEditor {
67
+ constructor(
68
+ private readonly fileOps: AgentFileOps,
69
+ private readonly registry: AgentTypeRegistry,
70
+ private readonly personalAgentsDir: string,
71
+ private readonly projectAgentsDir: string,
72
+ ) {}
73
+
74
+ private agentDirs(): string[] {
75
+ return [this.projectAgentsDir, this.personalAgentsDir];
74
76
  }
75
77
 
76
- async function showAgentDetail(ui: MenuUI, name: string) {
77
- if (registry.resolveType(name) == null) {
78
+ async showAgentDetail(ui: MenuUI, name: string): Promise<void> {
79
+ if (this.registry.resolveType(name) == null) {
78
80
  ui.notify(`Agent config not found for "${name}".`, "warning");
79
81
  return;
80
82
  }
81
- const cfg = registry.resolveAgentConfig(name);
82
- const file = fileOps.findAgentFile(name, agentDirs());
83
+ const cfg = this.registry.resolveAgentConfig(name);
84
+ const file = this.fileOps.findAgentFile(name, this.agentDirs());
83
85
 
84
86
  const choice = await ui.select(name, buildMenuOptions(cfg, file));
85
87
  if (!choice || choice === "Back") return;
86
88
 
87
- if (choice === "Edit" && file) await handleEdit(ui, name, file);
88
- else if (choice === "Delete" && file) await handleDelete(ui, name, file);
89
+ if (choice === "Edit" && file) await this.handleEdit(ui, name, file);
90
+ else if (choice === "Delete" && file) await this.handleDelete(ui, name, file);
89
91
  else if (choice === "Reset to default" && file)
90
- await handleReset(ui, name, file);
91
- else if (choice.startsWith("Eject")) await ejectAgent(ui, name, cfg);
92
- else if (choice === "Disable") await disableAgent(ui, name);
93
- else if (choice === "Enable") await enableAgent(ui, name);
92
+ await this.handleReset(ui, name, file);
93
+ else if (choice.startsWith("Eject")) await this.ejectAgent(ui, name, cfg);
94
+ else if (choice === "Disable") await this.disableAgent(ui, name);
95
+ else if (choice === "Enable") await this.enableAgent(ui, name);
94
96
  }
95
97
 
96
- async function handleEdit(ui: MenuUI, name: string, file: string) {
97
- const content = fileOps.read(file);
98
+ private async handleEdit(ui: MenuUI, name: string, file: string): Promise<void> {
99
+ const content = this.fileOps.read(file);
98
100
  if (content === undefined) return;
99
101
  const edited = await ui.editor(`Edit ${name}`, content);
100
102
  if (edited !== undefined && edited !== content) {
101
- fileOps.write(file, edited);
102
- registry.reload();
103
+ this.fileOps.write(file, edited);
104
+ this.registry.reload();
103
105
  ui.notify(`Updated ${file}`, "info");
104
106
  }
105
107
  }
106
108
 
107
- async function handleDelete(ui: MenuUI, name: string, file: string) {
109
+ private async handleDelete(ui: MenuUI, name: string, file: string): Promise<void> {
108
110
  const confirmed = await ui.confirm(
109
111
  "Delete agent",
110
112
  `Delete ${name} (${file})?`,
111
113
  );
112
114
  if (confirmed) {
113
- fileOps.remove(file);
114
- registry.reload();
115
+ this.fileOps.remove(file);
116
+ this.registry.reload();
115
117
  ui.notify(`Deleted ${file}`, "info");
116
118
  }
117
119
  }
118
120
 
119
- async function handleReset(ui: MenuUI, name: string, file: string) {
121
+ private async handleReset(ui: MenuUI, name: string, file: string): Promise<void> {
120
122
  const confirmed = await ui.confirm(
121
123
  "Reset to default",
122
124
  `Delete override ${file} and restore embedded default?`,
123
125
  );
124
126
  if (confirmed) {
125
- fileOps.remove(file);
126
- registry.reload();
127
+ this.fileOps.remove(file);
128
+ this.registry.reload();
127
129
  ui.notify(`Restored default ${name}`, "info");
128
130
  }
129
131
  }
130
132
 
131
- async function ejectAgent(ui: MenuUI, name: string, cfg: AgentConfig) {
133
+ private async ejectAgent(ui: MenuUI, name: string, cfg: AgentConfig): Promise<void> {
132
134
  const location = await ui.select("Choose location", [
133
135
  "Project (.pi/agents/)",
134
- `Personal (${personalAgentsDir})`,
136
+ `Personal (${this.personalAgentsDir})`,
135
137
  ]);
136
138
  if (!location) return;
137
139
 
138
140
  const targetDir = location.startsWith("Project")
139
- ? projectAgentsDir
140
- : personalAgentsDir;
141
+ ? this.projectAgentsDir
142
+ : this.personalAgentsDir;
141
143
 
142
144
  const targetPath = join(targetDir, `${name}.md`);
143
- if (fileOps.exists(targetPath)) {
145
+ if (this.fileOps.exists(targetPath)) {
144
146
  const overwrite = await ui.confirm(
145
147
  "Overwrite",
146
148
  `${targetPath} already exists. Overwrite?`,
@@ -148,23 +150,23 @@ export function createAgentConfigEditor(
148
150
  if (!overwrite) return;
149
151
  }
150
152
 
151
- fileOps.write(targetPath, buildEjectContent(cfg));
152
- registry.reload();
153
+ this.fileOps.write(targetPath, buildEjectContent(cfg));
154
+ this.registry.reload();
153
155
  ui.notify(`Ejected ${name} to ${targetPath}`, "info");
154
156
  }
155
157
 
156
- async function disableAgent(ui: MenuUI, name: string) {
157
- const file = fileOps.findAgentFile(name, agentDirs());
158
+ private async disableAgent(ui: MenuUI, name: string): Promise<void> {
159
+ const file = this.fileOps.findAgentFile(name, this.agentDirs());
158
160
  if (file) {
159
- const content = fileOps.read(file);
161
+ const content = this.fileOps.read(file);
160
162
  if (content?.includes("\nenabled: false\n")) {
161
163
  ui.notify(`${name} is already disabled.`, "info");
162
164
  return;
163
165
  }
164
166
  if (content) {
165
167
  const updated = content.replace(/^---\n/, "---\nenabled: false\n");
166
- fileOps.write(file, updated);
167
- registry.reload();
168
+ this.fileOps.write(file, updated);
169
+ this.registry.reload();
168
170
  ui.notify(`Disabled ${name} (${file})`, "info");
169
171
  }
170
172
  return;
@@ -172,40 +174,38 @@ export function createAgentConfigEditor(
172
174
 
173
175
  const location = await ui.select("Choose location", [
174
176
  "Project (.pi/agents/)",
175
- `Personal (${personalAgentsDir})`,
177
+ `Personal (${this.personalAgentsDir})`,
176
178
  ]);
177
179
  if (!location) return;
178
180
 
179
181
  const targetDir = location.startsWith("Project")
180
- ? projectAgentsDir
181
- : personalAgentsDir;
182
+ ? this.projectAgentsDir
183
+ : this.personalAgentsDir;
182
184
 
183
185
  const targetPath = join(targetDir, `${name}.md`);
184
- fileOps.write(targetPath, "---\nenabled: false\n---\n");
185
- registry.reload();
186
+ this.fileOps.write(targetPath, "---\nenabled: false\n---\n");
187
+ this.registry.reload();
186
188
  ui.notify(`Disabled ${name} (${targetPath})`, "info");
187
189
  }
188
190
 
189
191
  // eslint-disable-next-line @typescript-eslint/require-await
190
- async function enableAgent(ui: MenuUI, name: string) {
191
- const file = fileOps.findAgentFile(name, agentDirs());
192
+ private async enableAgent(ui: MenuUI, name: string): Promise<void> {
193
+ const file = this.fileOps.findAgentFile(name, this.agentDirs());
192
194
  if (!file) return;
193
195
 
194
- const content = fileOps.read(file);
196
+ const content = this.fileOps.read(file);
195
197
  if (!content) return;
196
198
 
197
199
  const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
198
200
 
199
201
  if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
200
- fileOps.remove(file);
201
- registry.reload();
202
+ this.fileOps.remove(file);
203
+ this.registry.reload();
202
204
  ui.notify(`Enabled ${name} (removed ${file})`, "info");
203
205
  } else {
204
- fileOps.write(file, updated);
205
- registry.reload();
206
+ this.fileOps.write(file, updated);
207
+ this.registry.reload();
206
208
  ui.notify(`Enabled ${name} (${file})`, "info");
207
209
  }
208
210
  }
209
-
210
- return { showAgentDetail };
211
211
  }