@gotgenes/pi-subagents 7.3.2 → 7.5.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,38 @@ 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
+ ## [7.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.4.0...pi-subagents-v7.5.0) (2026-05-26)
9
+
10
+
11
+ ### Features
12
+
13
+ * add permission bridge for cross-extension registration ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([1827720](https://github.com/gotgenes/pi-packages/commit/18277203f7ee10e56f90a0d4db587a4aa95376ab))
14
+ * register child sessions with permission system ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0487828](https://github.com/gotgenes/pi-packages/commit/04878286d7da6660362360482fb916b1b3743ce3))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * resolve pre-existing lint errors in pi-autoformat and pi-permission-system ([68fd516](https://github.com/gotgenes/pi-packages/commit/68fd516e33ddbb9a5e37ef19e949ee9ecdc37252))
20
+
21
+
22
+ ### Documentation
23
+
24
+ * document permission-bridge in architecture ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([d0120ab](https://github.com/gotgenes/pi-packages/commit/d0120abdf049e2aeba14ba75071ed55b24e23dbe))
25
+ * update subagent integration docs for native permission bridge ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0bd456b](https://github.com/gotgenes/pi-packages/commit/0bd456befa8ea6918e74f4393d844868795edc77))
26
+
27
+ ## [7.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.3.2...pi-subagents-v7.4.0) (2026-05-25)
28
+
29
+
30
+ ### Features
31
+
32
+ * add model attribution to formatAssistantMessage ([76f2ada](https://github.com/gotgenes/pi-packages/commit/76f2adaa6bad9d1a3b15a3ba208b3ab96e07ecd1))
33
+ * add model attribution to getAgentConversation ([c186c37](https://github.com/gotgenes/pi-packages/commit/c186c370b975e5ef6596d9a3d7719c720a580640))
34
+
35
+
36
+ ### Documentation
37
+
38
+ * **retro:** add retro notes for issue [#214](https://github.com/gotgenes/pi-packages/issues/214) ([7e39c96](https://github.com/gotgenes/pi-packages/commit/7e39c96563517661c1c8c7f250402115b232a097))
39
+
8
40
  ## [7.3.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.3.1...pi-subagents-v7.3.2) (2026-05-25)
9
41
 
10
42
 
package/README.md CHANGED
@@ -421,27 +421,57 @@ disallowed_tools: write, edit
421
421
 
422
422
  This is useful for creating agents that inherit extension tools but should not have write access.
423
423
 
424
+ ## Permission System Integration
425
+
426
+ When [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) is installed, this extension integrates automatically:
427
+
428
+ - **Per-agent permission policies** — define `permission:` in agent YAML frontmatter to set allow/ask/deny rules per agent type.
429
+ The permission system resolves the agent name from the `<active_agent>` tag in the child system prompt.
430
+ - **Tool filtering** — the permission system's `before_agent_start` handler removes denied tools from the child session before the agent starts.
431
+ - **`ask`-state forwarding** — when a child session triggers an `ask` permission, the prompt forwards to the parent session's UI.
432
+ The parent approves or denies, and the child resumes.
433
+ - **Deterministic child detection** — every child session registers with the permission system's `SubagentSessionRegistry` before `bindExtensions()` fires, so detection does not rely on env vars or filesystem heuristics.
434
+
435
+ No configuration is required.
436
+ When `@gotgenes/pi-permission-system` is not installed, the registration calls are silent no-ops.
437
+
424
438
  ## Architecture
425
439
 
440
+ See `docs/architecture/architecture.md` for the full architecture document with domain decomposition, Mermaid diagrams, and improvement roadmap.
441
+
426
442
  ```text
427
443
  src/
428
- index.ts # Extension entry: tool/command registration, rendering
429
- types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
430
- default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
431
- agent-types.ts # Unified agent registry (defaults + user), tool name resolution
432
- agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
433
- agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
434
- custom-agents.ts # Load user-defined agents from .pi/agents/*.md
435
- memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
436
- skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
437
- output-file.ts # Streaming output file transcripts for agent sessions
438
- worktree.ts # Git worktree isolation (create, cleanup, prune)
439
- prompts.ts # Config-driven system prompt builder
440
- context.ts # Parent conversation context for inherit_context
441
- env.ts # Environment detection (git, platform)
442
- ui/
443
- agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
444
- conversation-viewer.ts # Live conversation overlay for viewing agent sessions
444
+ index.ts # Extension entry: tool/command registration, rendering
445
+ runtime.ts # Session-scoped state bag with methods
446
+ types.ts # Shared type definitions
447
+ settings.ts # Persistent settings (concurrency, turn limits)
448
+ config/ # Agent type registry and configuration
449
+ agent-types.ts # Unified agent registry (defaults + custom)
450
+ default-agents.ts # Embedded default agent configs
451
+ custom-agents.ts # Load user-defined agents from .pi/agents/*.md
452
+ invocation-config.ts # Per-call merge of tool params + agent config
453
+ session/ # Pure session assembly
454
+ session-config.ts # Session configuration assembler
455
+ prompts.ts # Config-driven system prompt builder
456
+ context.ts # Parent conversation context for inherit_context
457
+ skill-loader.ts # Preload skills from Pi-standard + Agent Skills spec
458
+ env.ts # Environment detection (git, platform)
459
+ model-resolver.ts # Fuzzy model matching
460
+ lifecycle/ # Agent execution and state tracking
461
+ agent-manager.ts # Spawn, queue, abort, resume, concurrency
462
+ agent-runner.ts # Session creation, turn loop, tool filtering
463
+ agent-record.ts # Status state machine
464
+ parent-snapshot.ts # Immutable spawn-time parent state
465
+ permission-bridge.ts # Optional bridge to pi-permission-system registry
466
+ worktree.ts # Git worktree isolation
467
+ observation/ # Progress tracking and notification
468
+ record-observer.ts # Session-event stats observer
469
+ notification.ts # Completion nudges
470
+ service/ # Cross-extension API boundary
471
+ service.ts # SubagentsService interface + Symbol.for() accessors
472
+ service-adapter.ts # SubagentsService wrapper around AgentManager
473
+ tools/ # LLM-facing tools
474
+ ui/ # Widget, conversation viewer, /agents menu
445
475
  ```
446
476
 
447
477
  ## Deviations from upstream
@@ -458,6 +488,9 @@ Each has a corresponding upstream PR:
458
488
  3. **`<active_agent>` system-prompt tag** (`src/prompts.ts`) — `buildAgentPrompt` prepends `<active_agent name="${config.name}"/>` to every assembled child system prompt (both `replace` and `append` modes).
459
489
  Downstream extensions like [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) parse this tag to resolve per-agent `permission:` frontmatter inside the child session.
460
490
  Upstream PR: [tintinweb/pi-subagents#73](https://github.com/tintinweb/pi-subagents/pull/73).
491
+ 4. **Permission-system registration** (`src/lifecycle/permission-bridge.ts`) — `runAgent` registers every child session with `@gotgenes/pi-permission-system`'s `SubagentSessionRegistry` before `bindExtensions()` and unregisters in the `finally` block.
492
+ This enables deterministic child detection and `ask`-state forwarding to the parent UI.
493
+ No upstream equivalent — this feature is specific to the `@gotgenes` fork.
461
494
 
462
495
  The upstream `vitest` suite plus tests added for each patch all pass on every commit.
463
496
 
@@ -259,6 +259,7 @@ src/
259
259
  │ ├── agent-record.ts status state machine
260
260
  │ ├── parent-snapshot.ts immutable spawn-time parent state
261
261
  │ ├── execution-state.ts session/output phase state
262
+ │ ├── permission-bridge.ts optional bridge to pi-permission-system registry
262
263
  │ ├── worktree.ts git worktree isolation
263
264
  │ ├── worktree-state.ts worktree phase state
264
265
  │ └── usage.ts token usage tracking
@@ -333,7 +334,8 @@ They declare this package as an optional peer dependency and use dynamic import
333
334
 
334
335
  - The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
335
336
  - `AgentManager` — spawn, queue, abort, resume, concurrency control.
336
- - `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
337
+ - `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3), permission-system registration.
338
+ - `permission-bridge` — optional cross-extension bridge to `@gotgenes/pi-permission-system`; registers each child session with `SubagentSessionRegistry` before `bindExtensions()` so the permission system detects in-process children deterministically.
337
339
  - `session-config` — pure configuration assembler (extracted from `agent-runner`).
338
340
  - `SubagentRuntime` — session-scoped state bag with methods.
339
341
  - `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
@@ -38,3 +38,29 @@ Test count held at 913 (57 files) — no new tests needed, no tests removed.
38
38
  - Adding the `SpawnOptions` import to `service-adapter.ts` was required for the `spawn` method signature; the plan anticipated this correctly.
39
39
  - The `sed -i` command required the macOS `-i ''` form (no in-place backup extension) rather than the GNU `sed -i` form.
40
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.2",
3
+ "version": "7.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -11,6 +11,7 @@ import {
11
11
  import type { AgentConfigLookup } from "#src/config/agent-types";
12
12
  import type { ParentSessionInfo } from "#src/lifecycle/agent-manager";
13
13
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
14
+ import { registerChildSession, unregisterChildSession } from "#src/lifecycle/permission-bridge";
14
15
  import { extractAssistantContent } from "#src/session/content-items";
15
16
  import { extractText } from "#src/session/context";
16
17
  import type { EnvInfo } from "#src/session/env";
@@ -347,6 +348,15 @@ export async function runAgent(
347
348
  session.setActiveToolsByName(filtered);
348
349
  }
349
350
 
351
+ // Register with pi-permission-system's SubagentSessionRegistry before
352
+ // bindExtensions() so isSubagentExecutionContext() hits the registry on the
353
+ // first check during child extension initialization. Unregistered in the
354
+ // finally block below to guarantee cleanup on both success and error paths.
355
+ registerChildSession(sessionDir, {
356
+ agentName: type,
357
+ parentSessionId: options.context.parentSession?.parentSessionId,
358
+ });
359
+
350
360
  // Bind extensions so that session_start fires and extensions can initialize
351
361
  // (e.g. loading credentials, setting up state). Placed after tool filtering
352
362
  // so extension-provided skills/prompts from extendResourcesFromExtensions()
@@ -406,6 +416,7 @@ export async function runAgent(
406
416
  unsubTurns();
407
417
  collector.unsubscribe();
408
418
  cleanupAbort();
419
+ unregisterChildSession(sessionDir);
409
420
  }
410
421
 
411
422
  const responseText =
@@ -455,8 +466,9 @@ export function getAgentConversation(session: AgentSession): string {
455
466
  if (text.trim()) parts.push(`[User]: ${text.trim()}`);
456
467
  } else if (msg.role === "assistant") {
457
468
  const { textParts, toolNames } = extractAssistantContent(msg.content);
469
+ const attribution = formatAttribution(msg);
458
470
  if (textParts.length > 0)
459
- parts.push(`[Assistant]: ${textParts.join("\n")}`);
471
+ parts.push(`[Assistant${attribution}]: ${textParts.join("\n")}`);
460
472
  if (toolNames.length > 0)
461
473
  parts.push(`[Tool Calls]:\n${toolNames.map((n) => ` Tool: ${n}`).join("\n")}`);
462
474
  } else if (msg.role === "toolResult") {
@@ -468,3 +480,11 @@ export function getAgentConversation(session: AgentSession): string {
468
480
 
469
481
  return parts.join("\n\n");
470
482
  }
483
+
484
+ /** Build a `(provider/model)` attribution suffix for assistant messages. */
485
+ function formatAttribution(msg: { provider?: string; model?: string }): string {
486
+ const { provider, model } = msg;
487
+ if (!provider && !model) return "";
488
+ if (provider && model) return ` (${provider}/${model})`;
489
+ return ` (${provider ?? model})`;
490
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * permission-bridge.ts — Cross-extension bridge to @gotgenes/pi-permission-system.
3
+ *
4
+ * pi-subagents does not import pi-permission-system directly. Instead it
5
+ * accesses the published PermissionsService via a process-global Symbol.for()
6
+ * key, the same mechanism pi-permission-system uses to publish itself.
7
+ *
8
+ * When pi-permission-system is not installed, getPermissionsService() returns
9
+ * undefined and all registration calls are silent no-ops.
10
+ */
11
+
12
+ /**
13
+ * The two PermissionsService methods pi-subagents needs.
14
+ *
15
+ * Follows ISP — does not expose the full PermissionsService surface
16
+ * (checkPermission, getToolPermission, etc.) to avoid coupling.
17
+ */
18
+ interface PermissionsServiceConsumer {
19
+ registerSubagentSession(
20
+ sessionKey: string,
21
+ info: { parentSessionId?: string; agentName: string },
22
+ ): void;
23
+ unregisterSubagentSession(sessionKey: string): void;
24
+ }
25
+
26
+ const PERMISSION_SERVICE_KEY = Symbol.for(
27
+ "@gotgenes/pi-permission-system:service",
28
+ );
29
+
30
+ function getPermissionsService(): PermissionsServiceConsumer | undefined {
31
+ return (globalThis as Record<symbol, unknown>)[
32
+ PERMISSION_SERVICE_KEY
33
+ ] as PermissionsServiceConsumer | undefined;
34
+ }
35
+
36
+ /**
37
+ * Register a child session with pi-permission-system's SubagentSessionRegistry.
38
+ *
39
+ * Must be called after deriving sessionDir but before session.bindExtensions()
40
+ * so isSubagentExecutionContext() hits the registry on the first check during
41
+ * child extension initialization.
42
+ *
43
+ * @param sessionKey - The session directory path (unique per session).
44
+ * @param info - Agent name and optional parent session ID for forwarding.
45
+ */
46
+ export function registerChildSession(
47
+ sessionKey: string,
48
+ info: { parentSessionId?: string; agentName: string },
49
+ ): void {
50
+ getPermissionsService()?.registerSubagentSession(sessionKey, info);
51
+ }
52
+
53
+ /**
54
+ * Unregister a child session from pi-permission-system's SubagentSessionRegistry.
55
+ *
56
+ * Must be called in a finally block so cleanup happens on both success and
57
+ * error paths.
58
+ *
59
+ * @param sessionKey - The session directory path used during registration.
60
+ */
61
+ export function unregisterChildSession(sessionKey: string): void {
62
+ getPermissionsService()?.unregisterSubagentSession(sessionKey);
63
+ }
@@ -72,9 +72,9 @@ export class AgentsMenuHandler {
72
72
  private readonly registry: AgentTypeRegistry,
73
73
  private readonly agentActivity: AgentActivityReader,
74
74
  private readonly settings: AgentMenuSettings,
75
- private readonly fileOps: AgentFileOps,
76
- private readonly personalAgentsDir: string,
77
- private readonly projectAgentsDir: string,
75
+ fileOps: AgentFileOps,
76
+ personalAgentsDir: string,
77
+ projectAgentsDir: string,
78
78
  ) {
79
79
  this.editor = new AgentConfigEditor(
80
80
  fileOps,
@@ -116,20 +116,31 @@ export function formatToolResult(
116
116
  ];
117
117
  }
118
118
 
119
+ // ── Types ─────────────────────────────────────────────────────────────────────
120
+
121
+ /** Model/provider attribution for assistant messages. */
122
+ export interface MessageAttribution {
123
+ provider?: string;
124
+ model?: string;
125
+ }
126
+
119
127
  // ── formatAssistantMessage ───────────────────────────────────────────────────
120
128
 
121
129
  /**
122
130
  * Format an assistant message into display lines.
123
131
  * Always returns at least the [Assistant] header line.
132
+ * When attribution is provided, renders as `[Assistant (provider/model)]`.
124
133
  */
125
134
  export function formatAssistantMessage(
126
135
  content: { type: string; [key: string]: unknown }[],
127
136
  width: number,
128
137
  ctx: FormatterContext,
138
+ attribution?: MessageAttribution,
129
139
  ): string[] {
130
140
  const { theme, wrapText } = ctx;
131
141
  const { textParts, toolNames } = extractAssistantContent(content);
132
- const lines: string[] = [theme.bold("[Assistant]")];
142
+ const label = formatAttributionLabel(attribution);
143
+ const lines: string[] = [theme.bold(`[Assistant${label}]`)];
133
144
  if (textParts.length > 0) {
134
145
  lines.push(...wrapText(textParts.join("\n").trim(), width));
135
146
  }
@@ -154,10 +165,15 @@ export function formatMessage(
154
165
  return formatUserMessage(msg.content as string | unknown[], width, ctx);
155
166
  }
156
167
  if (msg.role === "assistant") {
168
+ const attribution: MessageAttribution = {
169
+ provider: msg.provider as string | undefined,
170
+ model: msg.model as string | undefined,
171
+ };
157
172
  return formatAssistantMessage(
158
173
  msg.content as { type: string; [key: string]: unknown }[],
159
174
  width,
160
175
  ctx,
176
+ attribution,
161
177
  );
162
178
  }
163
179
  if (msg.role === "toolResult") {
@@ -168,3 +184,12 @@ export function formatMessage(
168
184
  }
169
185
  return null;
170
186
  }
187
+
188
+ // ── Helpers ──────────────────────────────────────────────────────────────────
189
+
190
+ /** Build a `(provider/model)` attribution suffix, or empty string when absent. */
191
+ function formatAttributionLabel(attr?: MessageAttribution): string {
192
+ if (!attr?.provider && !attr?.model) return "";
193
+ if (attr.provider && attr.model) return ` (${attr.provider}/${attr.model})`;
194
+ return ` (${attr.provider ?? attr.model})`;
195
+ }