@gotgenes/pi-subagents 6.17.2 → 6.18.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 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.18.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.17.2...pi-subagents-v6.18.0) (2026-05-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * add eslint config with type-aware rules and import enforcement ([4fb3cc6](https://github.com/gotgenes/pi-packages/commit/4fb3cc678da10d350b85c464318476ba9ae99dca))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **pi-subagents:** add missing "type": "module" to package.json ([8cfd07d](https://github.com/gotgenes/pi-packages/commit/8cfd07dfbfd44f52dc43ac7ae67d5824304825ae))
19
+
20
+
21
+ ### Documentation
22
+
23
+ * **retro:** add retro notes for issue [#164](https://github.com/gotgenes/pi-packages/issues/164) ([d8e2861](https://github.com/gotgenes/pi-packages/commit/d8e28615d6adabc86415f4d41ffd1bd90184fd0f))
24
+
8
25
  ## [6.17.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.17.1...pi-subagents-v6.17.2) (2026-05-23)
9
26
 
10
27
 
@@ -44,3 +44,39 @@ Updated `docs/architecture/architecture.md` to reflect the completed restructuri
44
44
  This eliminates all `../` relative cross-directory imports from `src/`.
45
45
  Future file moves in `src/` now only require updating the `#src/domain/name` string — no relative depth arithmetic.
46
46
  - Biome auto-fixed 14 files (import sorting / trailing whitespace) during the `#src/` conversion step; committed via `git add -A` after the pre-commit hook run.
47
+
48
+ ## Stage: Final Retrospective (2026-05-23T17:10:00Z)
49
+
50
+ ### Session summary
51
+
52
+ Shipped #164 (6 commits, `pi-subagents-v6.17.2`), filed #174 (ESLint for type-aware rules + import path enforcement), and reviewed the full issue lifecycle across planning, implementation, and shipping sessions.
53
+
54
+ ### Observations
55
+
56
+ #### What went well
57
+
58
+ - The user's mid-implementation redirect to use `#src/` aliases in `src/` files was the highest-impact intervention across the entire issue.
59
+ It eliminated the `../` depth-arithmetic problem, simplified the final commit to a one-liner `sed` command across 40 files, and directly motivated #174.
60
+ - `pnpm run check` (`tsc --noEmit`) caught every missed consumer immediately — no broken commit ever landed.
61
+ - The four-step dependency ordering (config → session → lifecycle+observation → service) kept every commit green despite the circular dependency between `lifecycle` and `observation`.
62
+
63
+ #### What caused friction (agent side)
64
+
65
+ - `wrong-abstraction` — Used ~60 individual `Edit` tool calls across steps 1–4 for what was a mechanical find-and-replace.
66
+ The fifth commit proved `sed` handles bulk import rewrites across 40 files in a single command.
67
+ Impact: hundreds of unnecessary tool calls and significant token waste across four commits.
68
+ - `missing-context` — The plan's consumer tables missed 4 files (`src/ui/widget-renderer.ts`, `src/session-config.ts`, `src/service-adapter.ts`, `test/parent-snapshot.test.ts` `vi.mock` path).
69
+ The plan manually traced imports instead of using `grep` to enumerate all consumers of each moving module.
70
+ Impact: mid-step rework in steps 1, 2, and 3 to fix un-updated imports caught by `tsc`.
71
+ - `missing-context` — Did not recognize that `src/` files should use `#src/` aliases (same as `test/` files) even though #157 set up the aliases for exactly this purpose.
72
+ Impact: all four domain-move commits used relative `../` imports, requiring a fifth unplanned commit to convert them.
73
+ User-caught.
74
+ - `wrong-abstraction` — The agent lacks access to LSP-level refactoring tools ("Move to file", "Rename symbol") that a human developer would use for this kind of reorganization.
75
+ A human with an LSP would have completed the entire issue in minutes with zero missed consumers.
76
+ This is a fundamental capability gap — the agent compensated with low-level text manipulation, which is error-prone and token-expensive.
77
+
78
+ #### What caused friction (user side)
79
+
80
+ - The `#src/` alias convention was established in #157 but the user didn't flag it during the planning session.
81
+ Had this been raised during planning, all four domain-move commits would have used `#src/` from the start.
82
+ The user did catch it during implementation, which was still early enough to save the final result.
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.17.2",
3
+ "version": "6.18.0",
4
+ "type": "module",
4
5
  "exports": {
5
6
  ".": "./src/service.ts"
6
7
  },
@@ -66,6 +67,6 @@
66
67
  "test": "vitest run",
67
68
  "test:watch": "vitest",
68
69
  "lint:md": "rumdl check *.md docs/**/*.md",
69
- "lint": "biome check . && pnpm run lint:md"
70
+ "lint": "biome check . && eslint . && pnpm run lint:md"
70
71
  }
71
72
  }
@@ -51,7 +51,7 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
51
51
  continue;
52
52
  }
53
53
 
54
- const { frontmatter: fm, body } = parseFrontmatter<Record<string, unknown>>(content);
54
+ const { frontmatter: fm, body } = parseFrontmatter(content);
55
55
 
56
56
  agents.set(name, {
57
57
  name,
@@ -95,6 +95,7 @@ function nonNegativeInt(val: unknown): number | undefined {
95
95
  */
96
96
  function parseCsvField(val: unknown): string[] | undefined {
97
97
  if (val === undefined || val === null) return undefined;
98
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string -- val is already narrowed past null/undefined; String() is the intended coercion here
98
99
  const s = String(val).trim();
99
100
  if (!s || s === "none") return undefined;
100
101
  const items = s.split(",").map(t => t.trim()).filter(Boolean);
@@ -52,11 +52,12 @@ export class SessionLifecycleHandler {
52
52
  // 3. Abort all agents — stop running work
53
53
  // 4. Dispose notifications — cancel pending nudges/timers
54
54
  // 5. Dispose manager — final cleanup
55
- async handleSessionShutdown(): Promise<void> {
55
+ handleSessionShutdown(): Promise<void> {
56
56
  this.unpublishService();
57
57
  this.runtime.clearSessionContext();
58
58
  this.manager.abortAll();
59
59
  this.disposeNotifications();
60
60
  this.manager.dispose();
61
+ return Promise.resolve();
61
62
  }
62
63
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * pi-agents — A pi extension providing Claude Code-style autonomous sub-agents.
3
4
  *
@@ -235,6 +235,7 @@ export class AgentManager {
235
235
  onSessionCreated: (session) => {
236
236
  // Capture the session file path early so it's available for display
237
237
  // before the run completes (e.g. in background agent status messages).
238
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- sessionManager is typed as always present but Pi SDK may not provide it
238
239
  const outputFile = session.sessionManager?.getSessionFile?.() ?? undefined;
239
240
  // Set the execution-state collaborator — born complete at session creation.
240
241
  record.execution = { session, outputFile };
@@ -282,7 +283,7 @@ export class AgentManager {
282
283
  }
283
284
  return responseText;
284
285
  })
285
- .catch((err) => {
286
+ .catch((err: unknown) => {
286
287
  record.markError(err);
287
288
 
288
289
  unsubRecordObserver?.();
@@ -313,7 +314,7 @@ export class AgentManager {
313
314
  while (this.queue.length > 0 && this.runningBackground < this._getMaxConcurrent()) {
314
315
  const next = this.queue.shift()!;
315
316
  const record = this.agents.get(next.id);
316
- if (!record || record.status !== "queued") continue;
317
+ if (record?.status !== "queued") continue;
317
318
  try {
318
319
  this.startAgent(next.id, record, next.args);
319
320
  } catch (err) {
@@ -402,6 +403,7 @@ export class AgentManager {
402
403
 
403
404
  /** Dispose a record's session and remove it from the map. */
404
405
  private removeRecord(id: string, record: AgentRecord): void {
406
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose may not exist on all session implementations
405
407
  record.session?.dispose?.();
406
408
  this.agents.delete(id);
407
409
  this.pendingSteers.delete(id);
@@ -464,12 +466,13 @@ export class AgentManager {
464
466
  async waitForAll(): Promise<void> {
465
467
  // Loop because drainQueue respects the concurrency limit — as running
466
468
  // agents finish they start queued ones, which need awaiting too.
469
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- intentional infinite loop with explicit break
467
470
  while (true) {
468
471
  this.drainQueue();
469
472
  const pending = [...this.agents.values()]
470
473
  .filter(r => r.status === "running" || r.status === "queued")
471
474
  .map(r => r.promise)
472
- .filter(Boolean);
475
+ .filter((p): p is Promise<string> => p != null);
473
476
  if (pending.length === 0) break;
474
477
  await Promise.allSettled(pending);
475
478
  }
@@ -246,7 +246,7 @@ function forwardAbortSignal(
246
246
  signal?: AbortSignal,
247
247
  ): () => void {
248
248
  if (!signal) return () => {};
249
- const onAbort = () => session.abort();
249
+ const onAbort = (): void => { void session.abort(); };
250
250
  signal.addEventListener("abort", onAbort, { once: true });
251
251
  return () => signal.removeEventListener("abort", onAbort);
252
252
  }
@@ -375,12 +375,12 @@ export async function runAgent(
375
375
  if (maxTurns != null) {
376
376
  if (!softLimitReached && turnCount >= maxTurns) {
377
377
  softLimitReached = true;
378
- session.steer(
378
+ void session.steer(
379
379
  "You have reached your turn limit. Wrap up immediately — provide your final answer now.",
380
380
  );
381
381
  } else if (softLimitReached && turnCount >= maxTurns + (options.graceTurns ?? 5)) {
382
382
  aborted = true;
383
- session.abort();
383
+ void session.abort();
384
384
  }
385
385
  }
386
386
  }
@@ -41,6 +41,7 @@ export function buildParentSnapshot(
41
41
  systemPrompt: ctx.getSystemPrompt(),
42
42
  model: ctx.model,
43
43
  modelRegistry: ctx.modelRegistry,
44
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts empty string to undefined as well as null/undefined
44
45
  parentContext: parentContext || undefined,
45
46
  };
46
47
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * record-observer.ts — Subscribes to session events and updates AgentRecord stats.
3
4
  *
@@ -26,7 +26,7 @@ export interface AgentManagerLike {
26
26
  /** Create a SubagentsService backed by the given dependencies. */
27
27
  export function createSubagentsService(
28
28
  manager: AgentManagerLike,
29
- resolveModel: (input: string, registry: ModelRegistry) => unknown | string,
29
+ resolveModel: (input: string, registry: ModelRegistry) => unknown,
30
30
  getCtx: () => { pi: unknown; ctx: unknown } | undefined,
31
31
  getModelRegistry: () => ModelRegistry | undefined,
32
32
  ): SubagentsService {
@@ -85,7 +85,7 @@ export function createSubagentsService(
85
85
 
86
86
  async steer(id: string, message: string): Promise<boolean> {
87
87
  const record = manager.getRecord(id);
88
- if (!record || record.status !== "running") {
88
+ if (record?.status !== "running") {
89
89
  return false;
90
90
  }
91
91
  const session = record.session;
@@ -100,5 +100,6 @@ export function getSubagentsService(): SubagentsService | undefined {
100
100
 
101
101
  /** Remove the SubagentsService from globalThis (call on shutdown/reload). */
102
102
  export function unpublishSubagentsService(): void {
103
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property; Map.delete() is not applicable
103
104
  delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
104
105
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * context.ts — Extract parent conversation context for subagent inheritance.
3
4
  */
@@ -19,6 +20,7 @@ export function extractText(content: unknown[]): string {
19
20
  */
20
21
  export function buildParentContext(ctx: ExtensionContext): string {
21
22
  const entries = ctx.sessionManager.getBranch();
23
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
22
24
  if (!entries || entries.length === 0) return "";
23
25
 
24
26
  const parts: string[] = [];
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-redundant-type-constituents -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * Model resolution: exact match ("provider/modelId") with fuzzy fallback.
3
4
  */
@@ -16,7 +17,7 @@ export interface ModelRegistry {
16
17
 
17
18
  /** Successful model resolution — `model` is the resolved or inherited model instance. */
18
19
  export interface ModelResolutionResult {
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+
20
21
  model: any;
21
22
  error?: undefined;
22
23
  }
@@ -57,7 +57,7 @@ Platform: ${env.platform}`;
57
57
  extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : "";
58
58
 
59
59
  if (config.promptMode === "append") {
60
- const identity = parentSystemPrompt || genericBase;
60
+ const identity = parentSystemPrompt ?? genericBase;
61
61
 
62
62
  const bridge = `<sub_agent_context>
63
63
  You are operating as a sub-agent invoked to handle a specific task.
@@ -72,7 +72,7 @@ You are operating as a sub-agent invoked to handle a specific task.
72
72
  - Be concise but complete
73
73
  </sub_agent_context>`;
74
74
 
75
- const customSection = config.systemPrompt?.trim()
75
+ const customSection = config.systemPrompt.trim()
76
76
  ? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
77
77
  : "";
78
78
 
@@ -69,7 +69,7 @@ function findSkillDirectory(root: string, name: string): string | undefined {
69
69
  const current = queue.shift();
70
70
  if (current === undefined) continue;
71
71
 
72
- let entries: Dirent<string>[];
72
+ let entries: Dirent[];
73
73
  try {
74
74
  entries = readdirSync(current, { withFileTypes: true });
75
75
  } catch (err) {
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
2
3
  import { Text } from "@earendil-works/pi-tui";
3
4
  import { Type } from "@sinclair/typebox";
@@ -170,7 +171,7 @@ Guidelines:
170
171
  const displayName = args.subagent_type
171
172
  ? getDisplayName(args.subagent_type as string, registry)
172
173
  : "Agent";
173
- const desc = (args.description as string) ?? "";
174
+ const desc = (args.description as string | undefined) ?? "";
174
175
  return new Text(
175
176
  "▸ " +
176
177
  theme.fg("toolTitle", theme.bold(displayName)) +
@@ -325,7 +326,7 @@ Guidelines:
325
326
  return textResult(`Failed to resume agent "${params.resume}".`);
326
327
  }
327
328
  return textResult(
328
- record.result?.trim() || record.error?.trim() || "No output.",
329
+ record.result?.trim() ?? record.error?.trim() ?? "No output.",
329
330
  buildDetails(config.detailBase, record),
330
331
  );
331
332
  }
@@ -80,6 +80,7 @@ export async function runForeground(
80
80
  };
81
81
  onUpdate?.({
82
82
  content: [{ type: "text", text: `${toolUses} tool uses...` }],
83
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Pi SDK ToolCallUpdate details type is not exported
83
84
  details: details as any,
84
85
  });
85
86
  };
@@ -151,7 +152,7 @@ export async function runForeground(
151
152
  if (tokenText) statsParts.push(tokenText);
152
153
  return textResult(
153
154
  `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
154
- (record.result?.trim() || "No output."),
155
+ (record.result?.trim() ?? "No output."),
155
156
  details,
156
157
  );
157
158
  }
@@ -79,7 +79,7 @@ export function createGetResultTool(
79
79
  } else if (record.status === "error") {
80
80
  output += `Error: ${record.error}`;
81
81
  } else {
82
- output += record.result?.trim() || "No output.";
82
+ output += record.result?.trim() ?? "No output.";
83
83
  }
84
84
 
85
85
  // Mark result as consumed — suppresses the completion notification
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * spawn-config.ts — Pure config resolution for the Agent tool.
3
4
  *
@@ -115,7 +115,7 @@ export function createAgentConfigEditor(
115
115
  const fmFields: string[] = [];
116
116
  fmFields.push(`description: ${cfg.description}`);
117
117
  if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
118
- fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
118
+ fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") ?? "all"}`);
119
119
  if (cfg.model) fmFields.push(`model: ${cfg.model}`);
120
120
  if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
121
121
  if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
@@ -174,6 +174,7 @@ export function createAgentConfigEditor(
174
174
  ui.notify(`Disabled ${name} (${targetPath})`, "info");
175
175
  }
176
176
 
177
+ // eslint-disable-next-line @typescript-eslint/require-await
177
178
  async function enableAgent(ui: MenuUI, name: string) {
178
179
  const file = fileOps.findAgentFile(name, agentDirs());
179
180
  if (!file) return;
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
3
  import { AgentTypeRegistry } from "#src/config/agent-types";
3
4
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-redundant-type-constituents -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * agent-widget.ts — Persistent widget showing running/completed agents above the editor.
3
4
  *
@@ -76,9 +77,7 @@ export class AgentWidget {
76
77
  /** Ensure the widget update timer is running. */
77
78
  // fallow-ignore-next-line unused-class-member
78
79
  ensureTimer() {
79
- if (!this.widgetInterval) {
80
- this.widgetInterval = setInterval(() => this.update(), 80);
81
- }
80
+ this.widgetInterval ??= setInterval(() => this.update(), 80);
82
81
  }
83
82
 
84
83
  /** Check if a finished agent should still be shown in the widget. */
@@ -299,7 +299,7 @@ export class ConversationViewer implements Component {
299
299
  } else if (isBashExecution(msg)) {
300
300
  if (needsSeparator) lines.push(th.fg("dim", "───"));
301
301
  lines.push(truncateToWidth(th.fg("muted", ` $ ${msg.command}`), width));
302
- if (msg.output?.trim()) {
302
+ if (msg.output.trim()) {
303
303
  const out = msg.output.length > 500
304
304
  ? msg.output.slice(0, 500) + "... (truncated)"
305
305
  : msg.output;
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
2
  /**
2
3
  * ui-observer.ts — Subscribes to session events and updates AgentActivityTracker state.
3
4
  *