pi-subagents 0.13.0 → 0.13.1

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
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.13.1] - 2026-04-13
6
+
7
+ ### Added
8
+ - Added optional intercom orchestration bridge for delegated runs. When enabled via `intercomBridge` (default `fork-only`) and `pi-intercom` is available, child subagents get runtime coordination instructions for contacting the orchestrator session via `intercom`, and `intercom` is auto-added to the child tool allowlist when needed.
9
+ - Added unit coverage for intercom bridge activation, config handling, and extension allowlist behavior.
10
+
11
+ ### Changed
12
+ - Normalized `subagent-executor.ts` relative imports to `.ts` specifiers to match direct TypeScript runtime loading.
13
+ - Documented `pi-intercom` installation and activation requirements in README.
14
+
15
+ ### Fixed
16
+ - Tightened intercom extension allowlist matching to avoid false positives from similarly named extension paths.
17
+
5
18
  ## [0.13.0] - 2026-04-11
6
19
 
7
20
  ### Added
package/README.md CHANGED
@@ -331,6 +331,9 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
331
331
  | Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
332
332
 
333
333
  Execution context defaults to `context: "fresh"`, which starts each child run from a clean session. Set `context: "fork"` to start each child from a real branched session created from the parent's current leaf.
334
+ When `intercomBridge` is enabled (default: `fork-only`) and `pi-intercom` is installed/enabled, forked children get runtime instructions for contacting the orchestrator session via `intercom({ action: "ask"|"send", ... })`.
335
+
336
+ > **Note:** Intercom bridging requires the [pi-intercom](https://github.com/nicobailon/pi-intercom) extension. Install it with `pi install npm:pi-intercom`.
334
337
 
335
338
  All modes support foreground and background execution. Foreground is the default (the call waits and streams progress). For programmatic background launch, use `clarify: false, async: true`. For interactive background launch, use `clarify: true` and press `b` in the TUI before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
336
339
 
@@ -778,6 +781,27 @@ Sessions are always enabled — every subagent run gets a session directory for
778
781
 
779
782
  Per-agent `maxSubagentDepth` can tighten that limit further for child runs, but it does not relax an already inherited stricter limit.
780
783
 
784
+ ### `intercomBridge`
785
+
786
+ Controls whether subagents receive runtime intercom coordination instructions (and `intercom` is auto-added to their tool allowlist when needed).
787
+
788
+ ```json
789
+ {
790
+ "intercomBridge": "fork-only"
791
+ }
792
+ ```
793
+
794
+ Values:
795
+ - `"fork-only"` (default): inject intercom bridge only when `context: "fork"`
796
+ - `"always"`: inject bridge in both `fresh` and `fork`
797
+ - `"off"`: disable bridge entirely
798
+
799
+ Bridge activation also requires all of the following:
800
+ - [pi-intercom](https://github.com/nicobailon/pi-intercom) is installed (`pi install npm:pi-intercom`)
801
+ - `~/.pi/agent/intercom/config.json` is not set to `"enabled": false`
802
+ - the current session has a name to target as orchestrator
803
+ - if agent `extensions` is an explicit allowlist, it must include `pi-intercom`
804
+
781
805
  ### `worktreeSetupHook`
782
806
 
783
807
  `worktreeSetupHook` configures an optional setup hook for worktree-isolated parallel runs. The hook runs once per created worktree, after `git worktree add` succeeds and before the agent starts.
package/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * Toggle: async parameter (default: false, configurable via config.json)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
12
- * { "asyncByDefault": true, "maxSubagentDepth": 1, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
12
+ * { "asyncByDefault": true, "maxSubagentDepth": 1, "intercomBridge": "fork-only", "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
13
  */
14
14
 
15
15
  import * as fs from "node:fs";
@@ -0,0 +1,115 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { AgentConfig } from "./agents.ts";
5
+ import type { ExtensionConfig } from "./types.ts";
6
+
7
+ const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
8
+ const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
9
+ const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
10
+
11
+ export type IntercomBridgeMode = NonNullable<ExtensionConfig["intercomBridge"]>;
12
+
13
+ export interface IntercomBridgeState {
14
+ active: boolean;
15
+ mode: IntercomBridgeMode;
16
+ orchestratorTarget?: string;
17
+ extensionDir: string;
18
+ }
19
+
20
+ interface ResolveIntercomBridgeInput {
21
+ mode: unknown;
22
+ context: "fresh" | "fork" | undefined;
23
+ orchestratorTarget?: string;
24
+ extensionDir?: string;
25
+ configPath?: string;
26
+ }
27
+
28
+ export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
29
+ if (value === "off" || value === "always" || value === "fork-only") return value;
30
+ return "fork-only";
31
+ }
32
+
33
+ function intercomEnabled(configPath: string): boolean {
34
+ if (!fs.existsSync(configPath)) return true;
35
+ try {
36
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as { enabled?: unknown };
37
+ return parsed.enabled !== false;
38
+ } catch (error) {
39
+ console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, error);
40
+ return true;
41
+ }
42
+ }
43
+
44
+ function extensionSandboxAllowsIntercom(extensions: string[] | undefined, extensionDir: string): boolean {
45
+ if (extensions === undefined) return true;
46
+ const lowerExtensionDir = extensionDir.toLowerCase();
47
+ return extensions.some((entry) => {
48
+ const normalized = entry.toLowerCase();
49
+ if (normalized.includes(lowerExtensionDir)) return true;
50
+ return /(^|[\\/])pi-intercom([\\/]|$)/i.test(normalized);
51
+ });
52
+ }
53
+
54
+ function buildIntercomBridgeInstruction(orchestratorTarget: string): string {
55
+ const escapedTarget = JSON.stringify(orchestratorTarget);
56
+ return `${INTERCOM_BRIDGE_MARKER}
57
+ Use intercom only for coordination with the orchestrator session:
58
+ - Need a decision or blocked: intercom({ action: "ask", to: ${escapedTarget}, message: "<question>" })
59
+ - Completion/update: intercom({ action: "send", to: ${escapedTarget}, message: "DONE: <summary>" })
60
+ If intercom is unavailable in this run, continue the task normally.`;
61
+ }
62
+
63
+ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
64
+ const mode = resolveIntercomBridgeMode(input.mode);
65
+ const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
66
+ const orchestratorTarget = input.orchestratorTarget?.trim();
67
+
68
+ if (mode === "off") {
69
+ return { active: false, mode, extensionDir };
70
+ }
71
+ if (mode === "fork-only" && input.context !== "fork") {
72
+ return { active: false, mode, extensionDir };
73
+ }
74
+ if (!orchestratorTarget) {
75
+ return { active: false, mode, extensionDir };
76
+ }
77
+ if (!fs.existsSync(extensionDir)) {
78
+ return { active: false, mode, extensionDir };
79
+ }
80
+
81
+ const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
82
+ if (!intercomEnabled(configPath)) {
83
+ return { active: false, mode, extensionDir };
84
+ }
85
+
86
+ return {
87
+ active: true,
88
+ mode,
89
+ orchestratorTarget,
90
+ extensionDir,
91
+ };
92
+ }
93
+
94
+ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
95
+ if (!bridge.active || !bridge.orchestratorTarget) return agent;
96
+ if (!extensionSandboxAllowsIntercom(agent.extensions, bridge.extensionDir)) return agent;
97
+
98
+ const tools = agent.tools && !agent.tools.includes("intercom")
99
+ ? [...agent.tools, "intercom"]
100
+ : agent.tools;
101
+ const instruction = buildIntercomBridgeInstruction(bridge.orchestratorTarget);
102
+ const trimmedPrompt = agent.systemPrompt?.trim() || "";
103
+ const systemPrompt = trimmedPrompt.includes(INTERCOM_BRIDGE_MARKER)
104
+ ? trimmedPrompt
105
+ : trimmedPrompt
106
+ ? `${trimmedPrompt}\n\n${instruction}`
107
+ : instruction;
108
+
109
+ if (tools === agent.tools && systemPrompt === agent.systemPrompt) return agent;
110
+ return {
111
+ ...agent,
112
+ tools,
113
+ systemPrompt,
114
+ };
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -3,27 +3,28 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
5
5
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
6
- import { type AgentConfig, type AgentScope } from "./agents.js";
7
- import { getArtifactsDir } from "./artifacts.js";
8
- import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.js";
9
- import { executeChain } from "./chain-execution.js";
10
- import { resolveExecutionAgentScope } from "./agent-scope.js";
11
- import { handleManagementAction } from "./agent-management.js";
12
- import { runSync } from "./execution.js";
13
- import { aggregateParallelOutputs } from "./parallel-utils.js";
14
- import { recordRun } from "./run-history.js";
6
+ import { type AgentConfig, type AgentScope } from "./agents.ts";
7
+ import { getArtifactsDir } from "./artifacts.ts";
8
+ import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "./chain-clarify.ts";
9
+ import { executeChain } from "./chain-execution.ts";
10
+ import { resolveExecutionAgentScope } from "./agent-scope.ts";
11
+ import { handleManagementAction } from "./agent-management.ts";
12
+ import { runSync } from "./execution.ts";
13
+ import { aggregateParallelOutputs } from "./parallel-utils.ts";
14
+ import { recordRun } from "./run-history.ts";
15
15
  import {
16
16
  getStepAgents,
17
17
  isParallelStep,
18
18
  resolveStepBehavior,
19
19
  type ChainStep,
20
20
  type SequentialStep,
21
- } from "./settings.js";
22
- import { discoverAvailableSkills, normalizeSkillInput } from "./skills.js";
23
- import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.js";
24
- import { createForkContextResolver } from "./fork-context.js";
25
- import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
26
- import { getSingleResultOutput, mapConcurrent } from "./utils.js";
21
+ } from "./settings.ts";
22
+ import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
23
+ import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
24
+ import { createForkContextResolver } from "./fork-context.ts";
25
+ import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
26
+ import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
27
+ import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
27
28
  import {
28
29
  cleanupWorktrees,
29
30
  createWorktrees,
@@ -32,7 +33,7 @@ import {
32
33
  formatWorktreeDiffSummary,
33
34
  formatWorktreeTaskCwdConflict,
34
35
  type WorktreeSetup,
35
- } from "./worktree.js";
36
+ } from "./worktree.ts";
36
37
  import {
37
38
  type AgentProgress,
38
39
  type ArtifactConfig,
@@ -49,7 +50,7 @@ import {
49
50
  resolveChildMaxSubagentDepth,
50
51
  resolveCurrentMaxSubagentDepth,
51
52
  wrapForkTask,
52
- } from "./types.js";
53
+ } from "./types.ts";
53
54
 
54
55
  interface TaskParam {
55
56
  agent: string;
@@ -1100,7 +1101,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1100
1101
  const scope: AgentScope = resolveExecutionAgentScope(normalizedParams.agentScope);
1101
1102
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
1102
1103
  deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1103
- const agents = deps.discoverAgents(ctx.cwd, scope).agents;
1104
+ const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
1105
+ const piWithSessionName = deps.pi as ExtensionAPI & { getSessionName?: () => string | undefined };
1106
+ const orchestratorTarget = piWithSessionName.getSessionName?.();
1107
+ const intercomBridge = resolveIntercomBridge({
1108
+ mode: deps.config.intercomBridge,
1109
+ context: normalizedParams.context,
1110
+ orchestratorTarget,
1111
+ });
1112
+ const agents = intercomBridge.active
1113
+ ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge))
1114
+ : discoveredAgents;
1104
1115
  const runId = randomUUID().slice(0, 8);
1105
1116
  const shareEnabled = normalizedParams.share === true;
1106
1117
  const hasChain = (normalizedParams.chain?.length ?? 0) > 0;
package/types.ts CHANGED
@@ -269,6 +269,7 @@ export interface ExtensionConfig {
269
269
  maxSubagentDepth?: number;
270
270
  worktreeSetupHook?: string;
271
271
  worktreeSetupHookTimeoutMs?: number;
272
+ intercomBridge?: "off" | "fork-only" | "always";
272
273
  }
273
274
 
274
275
  // ============================================================================