@ctliz/agent-intercom-claude 0.12.0-connect.3 → 0.13.0-connect.3

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
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0-connect.3 - 2026-08-23
4
+
5
+ - Add complete OpenAI MCP annotations for all nine Intercom tools and verify their exact semantics in tests.
6
+ - Reference every MCP tool by name so tool coverage is complete.
7
+ - Explicitly disable shell execution for production and release-verification child processes.
8
+
9
+ ## 0.13.0-connect.2 - 2026-08-18
10
+
11
+ - Document standard stdio MCP setup for Grok Build and AGY, including host-neutral identity variables, scope requirements, source-checkout configuration, manual `intercom_pending` polling, and the absence of wake-on-message support.
12
+
3
13
  ## 0.12.0-connect.3 - 2026-08-14
4
14
 
5
15
  - Fix package bundling by adding `monitors/**/*` (`monitors/monitors.json`) to `package.json` `files`, ensuring Claude Code plugin monitor configurations resolve properly at runtime when installed from the packaged npm artifact.
package/README.md CHANGED
@@ -149,6 +149,44 @@ claude mcp add claude-intercom -- claude-intercom-mcp
149
149
 
150
150
  With `--transport mcp`, `cci` does this automatically for each normal headless worker. Native headless workers are still woken and replied through the worker daemon's broker connection, but omit the packaged MCP server from the Claude turn. `ccim` intentionally uses Claude's `--safe-mode`, which disables MCP servers along with plugins, hooks, and skills.
151
151
 
152
+ ### Other stdio MCP hosts
153
+
154
+ The `claude-intercom-mcp` executable is a standard stdio MCP server, so Grok Build, AGY, and other compatible local hosts can use it without installing the Claude Code plugin. An npm/global installation provides that executable; for a source checkout, configure `node` with the absolute path to `dist/claude-server.mjs`. Node 20+ is required.
155
+
156
+ Use `AGENT_INTERCOM_SESSION_ID` and `AGENT_INTERCOM_SESSION_NAME` for a host-neutral identity; `CLAUDE_INTERCOM_SESSION_ID` and `CLAUDE_INTERCOM_NAME` remain higher-priority compatibility aliases. Do not share a fixed session ID between concurrent host sessions.
157
+
158
+ A plain MCP host is connected only while its MCP process is alive. Incoming Intercom messages are durable and appear through `intercom_pending`, but this server cannot itself inject a new prompt into Grok or AGY. Use `intercom_send` plus `intercom_pending` for manual-polling workflows; a host-specific relay is required for wake-on-message.
159
+
160
+ For Grok Build, register the server and use a unique static ID for an individual long-lived probe or worker:
161
+
162
+ ```bash
163
+ grok mcp add agent-intercom \
164
+ -e AGENT_INTERCOM_SESSION_ID=grok-probe-01 \
165
+ -e AGENT_INTERCOM_SESSION_NAME=grok-probe \
166
+ -e CLAUDE_INTERCOM_MODEL=grok-build \
167
+ -- claude-intercom-mcp
168
+ ```
169
+
170
+ For AGY, add the equivalent entry to `~/.gemini/config/mcp_config.json` (replace the source-checkout paths when using an installed `claude-intercom-mcp` executable):
171
+
172
+ ```json
173
+ {
174
+ "mcpServers": {
175
+ "agent-intercom": {
176
+ "command": "/absolute/path/to/node",
177
+ "args": ["/absolute/path/to/agent-intercom-claude/dist/claude-server.mjs"],
178
+ "env": {
179
+ "AGENT_INTERCOM_SESSION_ID": "agy-probe-01",
180
+ "AGENT_INTERCOM_SESSION_NAME": "agy-probe",
181
+ "CLAUDE_INTERCOM_MODEL": "agy"
182
+ }
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ Both hosts must use the same `AGENT_INTERCOM_SCOPE_ID` as their intended peers, or leave it unset for the default local scope. Call `intercom_whoami` once after startup, then use `intercom_list`, `intercom_send`, and `intercom_pending` to verify delivery.
189
+
152
190
  Optional identity variables can be attached at registration time:
153
191
 
154
192
  ```bash
@@ -413,6 +451,8 @@ environment when no config file is given (`CLAUDE_INTERCOM_WORKER_ID`, `…_NAME
413
451
  | `CLAUDE_INTERCOM_NAME` | MCP server | Discoverable session name |
414
452
  | `CLAUDE_INTERCOM_SESSION_ID` | MCP server | Stable intercom id |
415
453
  | `CLAUDE_INTERCOM_MODEL` | MCP server | Model label shown to peers |
454
+ | `AGENT_INTERCOM_SESSION_ID` / `_NAME` | MCP server | Host-neutral identity fallback; Claude-specific values take precedence |
455
+ | `AGENT_INTERCOM_SCOPE_ID` | MCP server | Broker-enforced local routing scope |
416
456
  | `CLAUDE_INTERCOM_EFFORT` | `cci` / `ccim` | Effort level forwarded to every Claude turn |
417
457
  | `CLAUDE_INTERCOM_CWD` / `_INSTRUCTIONS` | `cci` / `ccim` | Defaults for `--cwd` / `--instructions` |
418
458
  | `CLAUDE_INTERCOM_CLAUDE_COMMAND` | workers | Claude Code executable (default `claude`) |
package/broker/spawn.ts CHANGED
@@ -191,6 +191,7 @@ export function getBrokerSpawnOptions(
191
191
  cwd: string;
192
192
  env: NodeJS.ProcessEnv;
193
193
  windowsHide: true;
194
+ shell: false;
194
195
  } {
195
196
  return {
196
197
  detached: true,
@@ -198,6 +199,7 @@ export function getBrokerSpawnOptions(
198
199
  cwd: extensionDir,
199
200
  env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
200
201
  windowsHide: true,
202
+ shell: false,
201
203
  };
202
204
  }
203
205
 
package/claude/cci.ts CHANGED
@@ -66,6 +66,7 @@ function gitString(cwd: string, args: string[]): string | null {
66
66
  cwd,
67
67
  encoding: "utf8",
68
68
  stdio: ["ignore", "pipe", "ignore"],
69
+ shell: false,
69
70
  });
70
71
  if (result.status !== 0) return null;
71
72
  const trimmed = result.stdout.trim();
@@ -341,6 +342,7 @@ async function runCciTui(options: CciOptions, id: string, name: string, scopeId:
341
342
  const child = spawn(options.claudeCommand, args, {
342
343
  cwd: options.cwd,
343
344
  stdio: "inherit",
345
+ shell: false,
344
346
  env: {
345
347
  ...process.env,
346
348
  ...scopeEnv,
@@ -116,6 +116,7 @@ export function runClaudeTurn(options: ClaudeTurnOptions): Promise<ClaudeTurnRes
116
116
  cwd: launch.cwd,
117
117
  env: { ...process.env, ...options.env },
118
118
  stdio: ["pipe", "pipe", "pipe"],
119
+ shell: false,
119
120
  });
120
121
  } finally {
121
122
  // Directory descriptors remain open across the synchronous spawn setup,
package/claude/contact.ts CHANGED
@@ -20,14 +20,14 @@ export function copyTextToClipboard(text: string): boolean {
20
20
  ["clip.exe", []],
21
21
  ];
22
22
  for (const [command, args] of candidates) {
23
- if (spawnSync("which", [command], { stdio: "ignore" }).status !== 0) continue;
23
+ if (spawnSync("which", [command], { stdio: "ignore", shell: false }).status !== 0) continue;
24
24
  if (command === "wl-copy") {
25
- const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
25
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"], shell: false });
26
26
  child.stdin.end(text);
27
27
  child.unref();
28
28
  return true;
29
29
  }
30
- if (spawnSync(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] }).status === 0) return true;
30
+ if (spawnSync(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"], shell: false }).status === 0) return true;
31
31
  }
32
32
  return false;
33
33
  }
@@ -17,6 +17,12 @@ interface ToolDefinition {
17
17
  name: string;
18
18
  description: string;
19
19
  inputSchema: Record<string, unknown>;
20
+ annotations: {
21
+ readOnlyHint: boolean;
22
+ destructiveHint: boolean;
23
+ idempotentHint: boolean;
24
+ openWorldHint: boolean;
25
+ };
20
26
  handler: ToolHandler;
21
27
  }
22
28
 
@@ -78,18 +84,21 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
78
84
  name: "intercom_whoami",
79
85
  description: "Return this Claude session's intercom identity for reliable targeting.",
80
86
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
87
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
81
88
  handler: async () => runtime.whoami(),
82
89
  },
83
90
  {
84
91
  name: "intercom_team",
85
92
  description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
86
93
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
94
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
87
95
  handler: async () => runtime.team(),
88
96
  },
89
97
  {
90
98
  name: "intercom_status",
91
99
  description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
92
100
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
101
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
93
102
  handler: async () => runtime.status(),
94
103
  },
95
104
  {
@@ -103,6 +112,7 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
103
112
  },
104
113
  additionalProperties: false,
105
114
  },
115
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
106
116
  handler: async (args) => runtime.list(
107
117
  args.scope === "directory" || args.scope === "repo" ? args.scope : "machine",
108
118
  asBoolean(args.include_self, false),
@@ -117,6 +127,7 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
117
127
  required: ["summary"],
118
128
  additionalProperties: false,
119
129
  },
130
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
120
131
  handler: async (args) => runtime.setSummary(asString(args.summary, "summary")),
121
132
  },
122
133
  {
@@ -132,6 +143,7 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
132
143
  required: ["to", "message"],
133
144
  additionalProperties: false,
134
145
  },
146
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
135
147
  handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments)),
136
148
  },
137
149
  {
@@ -148,6 +160,7 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
148
160
  required: ["to", "message"],
149
161
  additionalProperties: false,
150
162
  },
163
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
151
164
  handler: async (args, signal) => runtime.ask(
152
165
  asString(args.to, "to"),
153
166
  asString(args.message, "message"),
@@ -164,6 +177,7 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
164
177
  properties: { mark_read: { type: "boolean", default: false } },
165
178
  additionalProperties: false,
166
179
  },
180
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
167
181
  handler: async (args) => runtime.pending(asBoolean(args.mark_read, false)),
168
182
  },
169
183
  {
@@ -179,6 +193,7 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
179
193
  required: ["message"],
180
194
  additionalProperties: false,
181
195
  },
196
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
182
197
  handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : undefined, args.which === "oldest" || args.which === "latest" ? args.which : undefined),
183
198
  },
184
199
  ];
@@ -222,7 +237,7 @@ export async function handleMcpRequest(request: JsonRpcRequest, runtime: ClaudeI
222
237
  return ok(request.id, {});
223
238
  case "tools/list":
224
239
  return ok(request.id, {
225
- tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
240
+ tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, annotations })),
226
241
  });
227
242
  case "tools/call": {
228
243
  const name = request.params?.name;
@@ -271,7 +271,7 @@ export async function bindNativeClaudeSocket(socketPath: string, onFrame: (frame
271
271
 
272
272
  function processStart(pid: number): Promise<string | undefined> {
273
273
  return new Promise((resolve) => {
274
- execFile("ps", ["-o", "lstart=", "-p", String(pid)], (error, stdout) => resolve(error ? undefined : stdout.trim() || undefined));
274
+ execFile("ps", ["-o", "lstart=", "-p", String(pid)], { shell: false }, (error, stdout) => resolve(error ? undefined : stdout.trim() || undefined));
275
275
  });
276
276
  }
277
277
 
package/claude/runtime.ts CHANGED
@@ -96,13 +96,28 @@ function shortHash(value: string): string {
96
96
  return createHash("sha256").update(value).digest("hex").slice(0, 8);
97
97
  }
98
98
 
99
+ const GENERIC_SESSION_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
100
+
99
101
  export function buildClaudeRuntimeIdentity(env: NodeJS.ProcessEnv = process.env, cwd = env.PWD || processCwd(), pid = process.pid): ClaudeRuntimeIdentity {
100
- const sessionId = env.CLAUDE_INTERCOM_SESSION_ID?.trim()
101
- || env.CLAUDE_PEER_ID?.trim()
102
- || `claude-${pid}-${shortHash(cwd)}`;
102
+ let sessionId = env.CLAUDE_INTERCOM_SESSION_ID?.trim()
103
+ || env.CLAUDE_PEER_ID?.trim();
104
+ if (!sessionId && env.AGENT_INTERCOM_SESSION_ID !== undefined) {
105
+ const raw = env.AGENT_INTERCOM_SESSION_ID.trim();
106
+ if (raw) {
107
+ if (!GENERIC_SESSION_ID_REGEX.test(raw)) {
108
+ throw new Error("Invalid AGENT_INTERCOM_SESSION_ID: must match ^[A-Za-z0-9_-]{1,128}$");
109
+ }
110
+ sessionId = raw;
111
+ }
112
+ }
113
+ if (!sessionId) {
114
+ sessionId = `claude-${pid}-${shortHash(cwd)}`;
115
+ }
116
+
103
117
  const cwdName = basename(cwd) || "workspace";
104
118
  const name = env.CLAUDE_INTERCOM_NAME?.trim()
105
119
  || env.CLAUDE_PEER_NAME?.trim()
120
+ || (env.AGENT_INTERCOM_SESSION_NAME !== undefined && env.AGENT_INTERCOM_SESSION_NAME.trim() ? env.AGENT_INTERCOM_SESSION_NAME.trim() : undefined)
106
121
  || `claude-${cwdName}-${pid}`;
107
122
  return {
108
123
  sessionId,
@@ -168,6 +183,7 @@ export function detectGitRoot(cwd: string): string | null {
168
183
  cwd,
169
184
  encoding: "utf8",
170
185
  stdio: ["ignore", "pipe", "ignore"],
186
+ shell: false,
171
187
  });
172
188
  if (result.status !== 0) return null;
173
189
  return result.stdout.trim() || null;
package/claude/team.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
4
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
4
5
  import type { BossSessionMetadata } from "../types.ts";
5
6
  import { getAgentDirPath } from "../broker/paths.ts";
6
7
 
8
+ export type TeamSource = "orchestrator" | "boss" | "manifest" | "live" | "standalone";
9
+
7
10
  export interface TeamSession {
8
11
  id: string;
9
12
  name?: string;
13
+ model?: string;
14
+ origin?: "local" | "remote";
10
15
  /** Already parsed and broker-authenticated by the client session decoder. */
11
16
  boss?: Pick<BossSessionMetadata, "binding" | "principal" | "liveWorker">;
12
17
  }
@@ -42,6 +47,7 @@ export interface IntercomTeam {
42
47
  self: { id: string; workerId?: string; isManager: boolean };
43
48
  manager?: { target: string; connected: boolean };
44
49
  coworkers: TeamMember[];
50
+ source: TeamSource;
45
51
  }
46
52
 
47
53
  const LEGACY_LIVE_STATES = new Set(["provisioning", "running", "idle", "needs_attention", "stopping"]);
@@ -212,11 +218,106 @@ function teamMember(worker: StoredWorker, session: TeamSession): TeamMember {
212
218
  };
213
219
  }
214
220
 
215
- function resolveLegacyTeam(
221
+ async function resolveNonAuthoritativeTeam(
222
+ input: { selfId: string; sessions: TeamSession[] },
223
+ env: NodeJS.ProcessEnv,
224
+ ): Promise<IntercomTeam> {
225
+ if (env.AGENT_INTERCOM_TEAM_MANIFEST !== undefined) {
226
+ const rawPath = env.AGENT_INTERCOM_TEAM_MANIFEST.trim();
227
+ if (!rawPath) {
228
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
229
+ }
230
+ const manifest = await readTeamManifestAsync(rawPath);
231
+ if (!manifest) {
232
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
233
+ }
234
+ const selfMember = manifest.members.find((m) => m.sessionId === input.selfId);
235
+ if (!selfMember) {
236
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
237
+ }
238
+
239
+ const isManager = input.selfId === manifest.leadId;
240
+ const managerTarget = manifest.leadId;
241
+ const managerConnected = connectedTo(input.sessions, managerTarget);
242
+
243
+ const coworkers: TeamMember[] = manifest.members
244
+ .filter((m) => m.sessionId !== input.selfId)
245
+ .filter((m) => isManager || m.sessionId !== managerTarget)
246
+ .map((m) => {
247
+ const live = input.sessions.find((s) => s.id === m.sessionId || s.name === m.sessionId);
248
+ return {
249
+ id: m.sessionId,
250
+ target: m.sessionId,
251
+ role: m.role,
252
+ connected: Boolean(live),
253
+ };
254
+ });
255
+
256
+ return {
257
+ teamId: manifest.runId,
258
+ self: { id: input.selfId, isManager },
259
+ manager: { target: managerTarget, connected: managerConnected },
260
+ coworkers,
261
+ source: "manifest",
262
+ };
263
+ }
264
+
265
+ if (stringValue(env.AGENT_INTERCOM_SCOPE_ID)) {
266
+ const managerTarget = stringValue(env.AGENT_INTERCOM_MANAGER_TARGET)
267
+ ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
268
+ const role = stringValue(env.AGENT_INTERCOM_ROLE)?.toLowerCase();
269
+
270
+ const isManager = role === "manager"
271
+ || (managerTarget !== undefined && managerTarget === input.selfId)
272
+ || (managerTarget === undefined && role !== "worker");
273
+
274
+ const effectiveManagerTarget = isManager ? input.selfId : managerTarget;
275
+
276
+ const coworkers: TeamMember[] = input.sessions
277
+ .filter((session) => session.id !== input.selfId)
278
+ .filter((session) => session.model !== "human")
279
+ .filter((session) => session.id !== effectiveManagerTarget)
280
+ .map((session): TeamMember => ({
281
+ id: session.id,
282
+ target: session.id,
283
+ ...(session.model ? { harness: session.model } : {}),
284
+ connected: true,
285
+ }));
286
+
287
+ const manager = effectiveManagerTarget
288
+ ? {
289
+ target: effectiveManagerTarget,
290
+ connected: isManager ? true : connectedTo(input.sessions, effectiveManagerTarget),
291
+ }
292
+ : undefined;
293
+
294
+ return {
295
+ teamId: effectiveManagerTarget ?? input.selfId,
296
+ self: { id: input.selfId, isManager },
297
+ ...(manager ? { manager } : {}),
298
+ coworkers,
299
+ source: "live",
300
+ };
301
+ }
302
+
303
+ const managerTarget = stringValue(env.AGENT_INTERCOM_MANAGER_TARGET)
304
+ ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
305
+ return {
306
+ teamId: managerTarget ?? input.selfId,
307
+ self: { id: input.selfId, isManager: !managerTarget },
308
+ manager: managerTarget
309
+ ? { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) }
310
+ : { target: input.selfId, connected: true },
311
+ coworkers: [],
312
+ source: "standalone",
313
+ };
314
+ }
315
+
316
+ async function resolveLegacyTeam(
216
317
  input: { selfId: string; sessions: TeamSession[] },
217
318
  workers: StoredWorker[],
218
319
  env: NodeJS.ProcessEnv,
219
- ): IntercomTeam {
320
+ ): Promise<IntercomTeam> {
220
321
  const workerId = stringValue(env.AGENT_INTERCOM_WORKER_ID);
221
322
  const runId = stringValue(env.AGENT_INTERCOM_RUN_ID);
222
323
  const current = workerId
@@ -225,37 +326,86 @@ function resolveLegacyTeam(
225
326
  && (!runId || stringValue(worker.runId) === runId)
226
327
  )
227
328
  : undefined;
228
- const managerTarget = stringValue(current?.managerSessionId)
229
- ?? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET)
230
- ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
231
- const teamId = managerTarget ?? input.selfId;
232
- const coworkers = workers
233
- .filter((worker) => worker.owned === true)
234
- .filter((worker) => stringValue(worker.managerSessionId) === teamId)
235
- .filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? ""))
236
- .filter((worker) => stringValue(worker.id) !== workerId)
237
- .map((worker): TeamMember | undefined => {
238
- const id = stringValue(worker.id);
239
- if (!id) return undefined;
240
- const target = stringValue(worker.intercomTarget) ?? id;
329
+
330
+ if (current) {
331
+ const managerTarget = stringValue(current?.managerSessionId)
332
+ ?? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET)
333
+ ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
334
+ if (!managerTarget) {
241
335
  return {
242
- id,
243
- target,
244
- ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
245
- ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
246
- ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
247
- connected: connectedTo(input.sessions, target),
336
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
337
+ coworkers: [],
338
+ source: "orchestrator",
248
339
  };
249
- })
250
- .filter((member): member is TeamMember => Boolean(member));
251
- return {
252
- teamId,
253
- self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: !managerTarget },
254
- manager: managerTarget
255
- ? { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) }
256
- : { target: input.selfId, connected: true },
257
- coworkers,
258
- };
340
+ }
341
+ const coworkers = workers
342
+ .filter((worker) => worker.owned === true)
343
+ .filter((worker) => {
344
+ const mgr = stringValue(worker.managerSessionId);
345
+ return mgr !== undefined && mgr === managerTarget;
346
+ })
347
+ .filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? ""))
348
+ .filter((worker) => stringValue(worker.id) !== workerId && stringValue(worker.id) !== input.selfId)
349
+ .map((worker): TeamMember | undefined => {
350
+ const id = stringValue(worker.id);
351
+ if (!id) return undefined;
352
+ const target = stringValue(worker.intercomTarget) ?? id;
353
+ return {
354
+ id,
355
+ target,
356
+ ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
357
+ ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
358
+ ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
359
+ connected: connectedTo(input.sessions, target),
360
+ };
361
+ })
362
+ .filter((member): member is TeamMember => Boolean(member));
363
+
364
+ return {
365
+ teamId: managerTarget,
366
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
367
+ manager: { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) },
368
+ coworkers,
369
+ source: "orchestrator",
370
+ };
371
+ }
372
+
373
+ if (workerId === undefined) {
374
+ const ownedCoworkers = workers
375
+ .filter((worker) => worker.owned === true)
376
+ .filter((worker) => {
377
+ const mgr = stringValue(worker.managerSessionId);
378
+ return mgr !== undefined && mgr === input.selfId;
379
+ })
380
+ .filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? ""))
381
+ .filter((worker) => stringValue(worker.id) !== input.selfId)
382
+ .map((worker): TeamMember | undefined => {
383
+ const id = stringValue(worker.id);
384
+ if (!id) return undefined;
385
+ const target = stringValue(worker.intercomTarget) ?? id;
386
+ return {
387
+ id,
388
+ target,
389
+ ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
390
+ ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
391
+ ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
392
+ connected: connectedTo(input.sessions, target),
393
+ };
394
+ })
395
+ .filter((member): member is TeamMember => Boolean(member));
396
+
397
+ if (ownedCoworkers.length > 0) {
398
+ return {
399
+ teamId: input.selfId,
400
+ self: { id: input.selfId, isManager: true },
401
+ manager: { target: input.selfId, connected: true },
402
+ coworkers: ownedCoworkers,
403
+ source: "orchestrator",
404
+ };
405
+ }
406
+ }
407
+
408
+ return resolveNonAuthoritativeTeam(input, env);
259
409
  }
260
410
 
261
411
  export async function resolveIntercomTeam(input: {
@@ -276,6 +426,7 @@ export async function resolveIntercomTeam(input: {
276
426
  teamId: selfSession.boss.binding.bossRunId,
277
427
  self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
278
428
  coworkers: [],
429
+ source: "boss",
279
430
  };
280
431
  }
281
432
 
@@ -301,6 +452,7 @@ export async function resolveIntercomTeam(input: {
301
452
  teamId: selfSession.boss.binding.bossRunId,
302
453
  self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
303
454
  coworkers: [],
455
+ source: "boss",
304
456
  };
305
457
  }
306
458
 
@@ -327,9 +479,36 @@ export async function resolveIntercomTeam(input: {
327
479
  self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: selfIsManager },
328
480
  manager: { target: managerTarget, connected: true },
329
481
  coworkers,
482
+ source: "boss",
330
483
  };
331
484
  }
332
485
 
486
+ /** Authorizes a read-only local inbox lookup using exact orchestrator ownership. */
487
+ export function resolveManagedInboxSession(input: {
488
+ team: IntercomTeam;
489
+ sessions: TeamSession[];
490
+ requestedSession: string;
491
+ }): TeamSession {
492
+ if (input.team.source !== "orchestrator" && input.team.source !== "boss") {
493
+ throw new Error("Pending-ask inbox access denied: cross-session inbox inspection is only permitted for Orchestrator/Boss-managed teams");
494
+ }
495
+ if (!input.team.self.isManager) {
496
+ throw new Error("Only a manager may inspect another session's pending-ask inbox");
497
+ }
498
+ const member = input.team.coworkers.find((entry) => entry.target === input.requestedSession);
499
+ if (!member) {
500
+ throw new Error(`Pending-ask inbox access denied for "${input.requestedSession}"; select an owned coworker target returned by intercom_team`);
501
+ }
502
+ const liveSession = input.sessions.find((session) => session.id === input.requestedSession);
503
+ if (!liveSession) {
504
+ throw new Error(`Pending-ask inbox access denied for "${input.requestedSession}"; the owned coworker target must equal an exact connected stable session ID`);
505
+ }
506
+ if (liveSession.origin === "remote") {
507
+ throw new Error(`Pending-ask inbox "${input.requestedSession}" is remote and cannot be read from this host`);
508
+ }
509
+ return liveSession;
510
+ }
511
+
333
512
  export function formatIntercomTeam(team: IntercomTeam): string {
334
513
  const lines = [
335
514
  `Manager: ${team.manager ? `${team.manager.target} [${team.manager.connected ? "connected" : "not connected"}]` : "unknown"}`,
@@ -49,6 +49,7 @@ export function probeClaudeVersion(command = "claude"): ClaudeVersionProbe {
49
49
  const result = spawnSync(command, ["--version"], {
50
50
  encoding: "utf8",
51
51
  stdio: ["ignore", "pipe", "pipe"],
52
+ shell: false,
52
53
  });
53
54
  if (result.error || result.status !== 0) {
54
55
  return {
package/dist/cci.mjs CHANGED
@@ -274,7 +274,8 @@ function runClaudeTurn(options) {
274
274
  child = spawn(command, launch.args, {
275
275
  cwd: launch.cwd,
276
276
  env: { ...process.env, ...options.env },
277
- stdio: ["pipe", "pipe", "pipe"]
277
+ stdio: ["pipe", "pipe", "pipe"],
278
+ shell: false
278
279
  });
279
280
  } finally {
280
281
  launch.release();
@@ -566,7 +567,8 @@ function isNativeClaudeVersionCompatible(version) {
566
567
  function probeClaudeVersion(command = "claude") {
567
568
  const result = spawnSync(command, ["--version"], {
568
569
  encoding: "utf8",
569
- stdio: ["ignore", "pipe", "pipe"]
570
+ stdio: ["ignore", "pipe", "pipe"],
571
+ shell: false
570
572
  });
571
573
  if (result.error || result.status !== 0) {
572
574
  return {
@@ -2063,7 +2065,8 @@ function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env)
2063
2065
  stdio: "ignore",
2064
2066
  cwd: extensionDir,
2065
2067
  env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
2066
- windowsHide: true
2068
+ windowsHide: true,
2069
+ shell: false
2067
2070
  };
2068
2071
  }
2069
2072
  function toError2(error) {
@@ -2399,6 +2402,7 @@ function defaultInboxPath(sessionId, intercomDir = getIntercomDirPath()) {
2399
2402
 
2400
2403
  // claude/team.ts
2401
2404
  import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
2405
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
2402
2406
  var BOSS_NONTERMINAL_STATES = new Set(
2403
2407
  PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
2404
2408
  );
@@ -2443,14 +2447,14 @@ function copyTextToClipboard(text) {
2443
2447
  ["clip.exe", []]
2444
2448
  ];
2445
2449
  for (const [command, args] of candidates) {
2446
- if (spawnSync2("which", [command], { stdio: "ignore" }).status !== 0) continue;
2450
+ if (spawnSync2("which", [command], { stdio: "ignore", shell: false }).status !== 0) continue;
2447
2451
  if (command === "wl-copy") {
2448
- const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"] });
2452
+ const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"], shell: false });
2449
2453
  child.stdin.end(text);
2450
2454
  child.unref();
2451
2455
  return true;
2452
2456
  }
2453
- if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] }).status === 0) return true;
2457
+ if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"], shell: false }).status === 0) return true;
2454
2458
  }
2455
2459
  return false;
2456
2460
  }
@@ -3017,7 +3021,7 @@ async function bindNativeClaudeSocket(socketPath, onFrame) {
3017
3021
  }
3018
3022
  function processStart(pid) {
3019
3023
  return new Promise((resolve5) => {
3020
- execFile("ps", ["-o", "lstart=", "-p", String(pid)], (error, stdout) => resolve5(error ? void 0 : stdout.trim() || void 0));
3024
+ execFile("ps", ["-o", "lstart=", "-p", String(pid)], { shell: false }, (error, stdout) => resolve5(error ? void 0 : stdout.trim() || void 0));
3021
3025
  });
3022
3026
  }
3023
3027
  async function registerNativeClaudePeer(options) {
@@ -3201,7 +3205,8 @@ function gitString(cwd, args) {
3201
3205
  const result = spawnSync3("git", args, {
3202
3206
  cwd,
3203
3207
  encoding: "utf8",
3204
- stdio: ["ignore", "pipe", "ignore"]
3208
+ stdio: ["ignore", "pipe", "ignore"],
3209
+ shell: false
3205
3210
  });
3206
3211
  if (result.status !== 0) return null;
3207
3212
  const trimmed = result.stdout.trim();
@@ -3445,6 +3450,7 @@ async function runCciTui(options, id, name, scopeId) {
3445
3450
  const child = spawn4(options.claudeCommand, args, {
3446
3451
  cwd: options.cwd,
3447
3452
  stdio: "inherit",
3453
+ shell: false,
3448
3454
  env: {
3449
3455
  ...process.env,
3450
3456
  ...scopeEnv,
package/dist/ccim.mjs CHANGED
@@ -278,7 +278,8 @@ function runClaudeTurn(options) {
278
278
  child = spawn(command, launch.args, {
279
279
  cwd: launch.cwd,
280
280
  env: { ...process.env, ...options.env },
281
- stdio: ["pipe", "pipe", "pipe"]
281
+ stdio: ["pipe", "pipe", "pipe"],
282
+ shell: false
282
283
  });
283
284
  } finally {
284
285
  launch.release();
@@ -570,7 +571,8 @@ function isNativeClaudeVersionCompatible(version) {
570
571
  function probeClaudeVersion(command = "claude") {
571
572
  const result = spawnSync(command, ["--version"], {
572
573
  encoding: "utf8",
573
- stdio: ["ignore", "pipe", "pipe"]
574
+ stdio: ["ignore", "pipe", "pipe"],
575
+ shell: false
574
576
  });
575
577
  if (result.error || result.status !== 0) {
576
578
  return {
@@ -2067,7 +2069,8 @@ function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env)
2067
2069
  stdio: "ignore",
2068
2070
  cwd: extensionDir,
2069
2071
  env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
2070
- windowsHide: true
2072
+ windowsHide: true,
2073
+ shell: false
2071
2074
  };
2072
2075
  }
2073
2076
  function toError2(error) {
@@ -2403,6 +2406,7 @@ function defaultInboxPath(sessionId, intercomDir = getIntercomDirPath()) {
2403
2406
 
2404
2407
  // claude/team.ts
2405
2408
  import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
2409
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
2406
2410
  var BOSS_NONTERMINAL_STATES = new Set(
2407
2411
  PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
2408
2412
  );
@@ -2447,14 +2451,14 @@ function copyTextToClipboard(text) {
2447
2451
  ["clip.exe", []]
2448
2452
  ];
2449
2453
  for (const [command, args] of candidates) {
2450
- if (spawnSync2("which", [command], { stdio: "ignore" }).status !== 0) continue;
2454
+ if (spawnSync2("which", [command], { stdio: "ignore", shell: false }).status !== 0) continue;
2451
2455
  if (command === "wl-copy") {
2452
- const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"] });
2456
+ const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"], shell: false });
2453
2457
  child.stdin.end(text);
2454
2458
  child.unref();
2455
2459
  return true;
2456
2460
  }
2457
- if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] }).status === 0) return true;
2461
+ if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"], shell: false }).status === 0) return true;
2458
2462
  }
2459
2463
  return false;
2460
2464
  }
@@ -3021,7 +3025,7 @@ async function bindNativeClaudeSocket(socketPath, onFrame) {
3021
3025
  }
3022
3026
  function processStart(pid) {
3023
3027
  return new Promise((resolve5) => {
3024
- execFile("ps", ["-o", "lstart=", "-p", String(pid)], (error, stdout) => resolve5(error ? void 0 : stdout.trim() || void 0));
3028
+ execFile("ps", ["-o", "lstart=", "-p", String(pid)], { shell: false }, (error, stdout) => resolve5(error ? void 0 : stdout.trim() || void 0));
3025
3029
  });
3026
3030
  }
3027
3031
  async function registerNativeClaudePeer(options) {
@@ -3205,7 +3209,8 @@ function gitString(cwd, args) {
3205
3209
  const result = spawnSync3("git", args, {
3206
3210
  cwd,
3207
3211
  encoding: "utf8",
3208
- stdio: ["ignore", "pipe", "ignore"]
3212
+ stdio: ["ignore", "pipe", "ignore"],
3213
+ shell: false
3209
3214
  });
3210
3215
  if (result.status !== 0) return null;
3211
3216
  const trimmed = result.stdout.trim();
@@ -3449,6 +3454,7 @@ async function runCciTui(options, id, name, scopeId) {
3449
3454
  const child = spawn4(options.claudeCommand, args, {
3450
3455
  cwd: options.cwd,
3451
3456
  stdio: "inherit",
3457
+ shell: false,
3452
3458
  env: {
3453
3459
  ...process.env,
3454
3460
  ...scopeEnv,
@@ -1501,7 +1501,8 @@ function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env)
1501
1501
  stdio: "ignore",
1502
1502
  cwd: extensionDir,
1503
1503
  env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
1504
- windowsHide: true
1504
+ windowsHide: true,
1505
+ shell: false
1505
1506
  };
1506
1507
  }
1507
1508
  function toError2(error2) {
@@ -1870,6 +1871,7 @@ function appendInboxMessage(path, from, message) {
1870
1871
  import { readFile } from "node:fs/promises";
1871
1872
  import { join as join6 } from "node:path";
1872
1873
  import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
1874
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
1873
1875
  var LEGACY_LIVE_STATES = /* @__PURE__ */ new Set(["provisioning", "running", "idle", "needs_attention", "stopping"]);
1874
1876
  var BOSS_NONTERMINAL_STATES = new Set(
1875
1877
  PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
@@ -1948,33 +1950,139 @@ function teamMember(worker, session) {
1948
1950
  connected: session.id === id
1949
1951
  };
1950
1952
  }
1951
- function resolveLegacyTeam(input, workers, env) {
1953
+ async function resolveNonAuthoritativeTeam(input, env) {
1954
+ if (env.AGENT_INTERCOM_TEAM_MANIFEST !== void 0) {
1955
+ const rawPath = env.AGENT_INTERCOM_TEAM_MANIFEST.trim();
1956
+ if (!rawPath) {
1957
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
1958
+ }
1959
+ const manifest = await readTeamManifestAsync(rawPath);
1960
+ if (!manifest) {
1961
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
1962
+ }
1963
+ const selfMember = manifest.members.find((m) => m.sessionId === input.selfId);
1964
+ if (!selfMember) {
1965
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
1966
+ }
1967
+ const isManager = input.selfId === manifest.leadId;
1968
+ const managerTarget2 = manifest.leadId;
1969
+ const managerConnected = connectedTo(input.sessions, managerTarget2);
1970
+ const coworkers = manifest.members.filter((m) => m.sessionId !== input.selfId).filter((m) => isManager || m.sessionId !== managerTarget2).map((m) => {
1971
+ const live = input.sessions.find((s) => s.id === m.sessionId || s.name === m.sessionId);
1972
+ return {
1973
+ id: m.sessionId,
1974
+ target: m.sessionId,
1975
+ role: m.role,
1976
+ connected: Boolean(live)
1977
+ };
1978
+ });
1979
+ return {
1980
+ teamId: manifest.runId,
1981
+ self: { id: input.selfId, isManager },
1982
+ manager: { target: managerTarget2, connected: managerConnected },
1983
+ coworkers,
1984
+ source: "manifest"
1985
+ };
1986
+ }
1987
+ if (stringValue(env.AGENT_INTERCOM_SCOPE_ID)) {
1988
+ const managerTarget2 = stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
1989
+ const role = stringValue(env.AGENT_INTERCOM_ROLE)?.toLowerCase();
1990
+ const isManager = role === "manager" || managerTarget2 !== void 0 && managerTarget2 === input.selfId || managerTarget2 === void 0 && role !== "worker";
1991
+ const effectiveManagerTarget = isManager ? input.selfId : managerTarget2;
1992
+ const coworkers = input.sessions.filter((session) => session.id !== input.selfId).filter((session) => session.model !== "human").filter((session) => session.id !== effectiveManagerTarget).map((session) => ({
1993
+ id: session.id,
1994
+ target: session.id,
1995
+ ...session.model ? { harness: session.model } : {},
1996
+ connected: true
1997
+ }));
1998
+ const manager = effectiveManagerTarget ? {
1999
+ target: effectiveManagerTarget,
2000
+ connected: isManager ? true : connectedTo(input.sessions, effectiveManagerTarget)
2001
+ } : void 0;
2002
+ return {
2003
+ teamId: effectiveManagerTarget ?? input.selfId,
2004
+ self: { id: input.selfId, isManager },
2005
+ ...manager ? { manager } : {},
2006
+ coworkers,
2007
+ source: "live"
2008
+ };
2009
+ }
2010
+ const managerTarget = stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
2011
+ return {
2012
+ teamId: managerTarget ?? input.selfId,
2013
+ self: { id: input.selfId, isManager: !managerTarget },
2014
+ manager: managerTarget ? { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) } : { target: input.selfId, connected: true },
2015
+ coworkers: [],
2016
+ source: "standalone"
2017
+ };
2018
+ }
2019
+ async function resolveLegacyTeam(input, workers, env) {
1952
2020
  const workerId = stringValue(env.AGENT_INTERCOM_WORKER_ID);
1953
2021
  const runId = stringValue(env.AGENT_INTERCOM_RUN_ID);
1954
2022
  const current = workerId ? workers.find(
1955
2023
  (worker) => stringValue(worker.id) === workerId && (!runId || stringValue(worker.runId) === runId)
1956
2024
  ) : void 0;
1957
- const managerTarget = stringValue(current?.managerSessionId) ?? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
1958
- const teamId = managerTarget ?? input.selfId;
1959
- const coworkers = workers.filter((worker) => worker.owned === true).filter((worker) => stringValue(worker.managerSessionId) === teamId).filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? "")).filter((worker) => stringValue(worker.id) !== workerId).map((worker) => {
1960
- const id = stringValue(worker.id);
1961
- if (!id) return void 0;
1962
- const target = stringValue(worker.intercomTarget) ?? id;
2025
+ if (current) {
2026
+ const managerTarget = stringValue(current?.managerSessionId) ?? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
2027
+ if (!managerTarget) {
2028
+ return {
2029
+ self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: false },
2030
+ coworkers: [],
2031
+ source: "orchestrator"
2032
+ };
2033
+ }
2034
+ const coworkers = workers.filter((worker) => worker.owned === true).filter((worker) => {
2035
+ const mgr = stringValue(worker.managerSessionId);
2036
+ return mgr !== void 0 && mgr === managerTarget;
2037
+ }).filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? "")).filter((worker) => stringValue(worker.id) !== workerId && stringValue(worker.id) !== input.selfId).map((worker) => {
2038
+ const id = stringValue(worker.id);
2039
+ if (!id) return void 0;
2040
+ const target = stringValue(worker.intercomTarget) ?? id;
2041
+ return {
2042
+ id,
2043
+ target,
2044
+ ...stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {},
2045
+ ...stringValue(worker.role) ? { role: stringValue(worker.role) } : {},
2046
+ ...stringValue(worker.state) ? { state: stringValue(worker.state) } : {},
2047
+ connected: connectedTo(input.sessions, target)
2048
+ };
2049
+ }).filter((member) => Boolean(member));
1963
2050
  return {
1964
- id,
1965
- target,
1966
- ...stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {},
1967
- ...stringValue(worker.role) ? { role: stringValue(worker.role) } : {},
1968
- ...stringValue(worker.state) ? { state: stringValue(worker.state) } : {},
1969
- connected: connectedTo(input.sessions, target)
2051
+ teamId: managerTarget,
2052
+ self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: false },
2053
+ manager: { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) },
2054
+ coworkers,
2055
+ source: "orchestrator"
1970
2056
  };
1971
- }).filter((member) => Boolean(member));
1972
- return {
1973
- teamId,
1974
- self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: !managerTarget },
1975
- manager: managerTarget ? { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) } : { target: input.selfId, connected: true },
1976
- coworkers
1977
- };
2057
+ }
2058
+ if (workerId === void 0) {
2059
+ const ownedCoworkers = workers.filter((worker) => worker.owned === true).filter((worker) => {
2060
+ const mgr = stringValue(worker.managerSessionId);
2061
+ return mgr !== void 0 && mgr === input.selfId;
2062
+ }).filter((worker) => LEGACY_LIVE_STATES.has(stringValue(worker.state) ?? "")).filter((worker) => stringValue(worker.id) !== input.selfId).map((worker) => {
2063
+ const id = stringValue(worker.id);
2064
+ if (!id) return void 0;
2065
+ const target = stringValue(worker.intercomTarget) ?? id;
2066
+ return {
2067
+ id,
2068
+ target,
2069
+ ...stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {},
2070
+ ...stringValue(worker.role) ? { role: stringValue(worker.role) } : {},
2071
+ ...stringValue(worker.state) ? { state: stringValue(worker.state) } : {},
2072
+ connected: connectedTo(input.sessions, target)
2073
+ };
2074
+ }).filter((member) => Boolean(member));
2075
+ if (ownedCoworkers.length > 0) {
2076
+ return {
2077
+ teamId: input.selfId,
2078
+ self: { id: input.selfId, isManager: true },
2079
+ manager: { target: input.selfId, connected: true },
2080
+ coworkers: ownedCoworkers,
2081
+ source: "orchestrator"
2082
+ };
2083
+ }
2084
+ }
2085
+ return resolveNonAuthoritativeTeam(input, env);
1978
2086
  }
1979
2087
  async function resolveIntercomTeam(input) {
1980
2088
  const env = input.env ?? process.env;
@@ -1987,7 +2095,8 @@ async function resolveIntercomTeam(input) {
1987
2095
  return {
1988
2096
  teamId: selfSession.boss.binding.bossRunId,
1989
2097
  self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: false },
1990
- coworkers: []
2098
+ coworkers: [],
2099
+ source: "boss"
1991
2100
  };
1992
2101
  }
1993
2102
  const currentRole = selfSession.boss.binding.role;
@@ -2000,7 +2109,8 @@ async function resolveIntercomTeam(input) {
2000
2109
  return {
2001
2110
  teamId: selfSession.boss.binding.bossRunId,
2002
2111
  self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: false },
2003
- coworkers: []
2112
+ coworkers: [],
2113
+ source: "boss"
2004
2114
  };
2005
2115
  }
2006
2116
  const coworkers = workers.filter((worker) => exactStringValue(worker.id) !== workerId).filter((worker) => exactStringValue(worker.managerSessionId) === managerTarget).map((worker) => {
@@ -2014,7 +2124,8 @@ async function resolveIntercomTeam(input) {
2014
2124
  teamId: selfSession.boss.binding.bossRunId,
2015
2125
  self: { id: input.selfId, ...workerId ? { workerId } : {}, isManager: selfIsManager },
2016
2126
  manager: { target: managerTarget, connected: true },
2017
- coworkers
2127
+ coworkers,
2128
+ source: "boss"
2018
2129
  };
2019
2130
  }
2020
2131
  function formatIntercomTeam(team) {
@@ -2082,10 +2193,23 @@ function publicPendingEntry(entry, selector) {
2082
2193
  function shortHash(value) {
2083
2194
  return createHash2("sha256").update(value).digest("hex").slice(0, 8);
2084
2195
  }
2196
+ var GENERIC_SESSION_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
2085
2197
  function buildClaudeRuntimeIdentity(env = process.env, cwd = env.PWD || processCwd(), pid = process.pid) {
2086
- const sessionId = env.CLAUDE_INTERCOM_SESSION_ID?.trim() || env.CLAUDE_PEER_ID?.trim() || `claude-${pid}-${shortHash(cwd)}`;
2198
+ let sessionId = env.CLAUDE_INTERCOM_SESSION_ID?.trim() || env.CLAUDE_PEER_ID?.trim();
2199
+ if (!sessionId && env.AGENT_INTERCOM_SESSION_ID !== void 0) {
2200
+ const raw = env.AGENT_INTERCOM_SESSION_ID.trim();
2201
+ if (raw) {
2202
+ if (!GENERIC_SESSION_ID_REGEX.test(raw)) {
2203
+ throw new Error("Invalid AGENT_INTERCOM_SESSION_ID: must match ^[A-Za-z0-9_-]{1,128}$");
2204
+ }
2205
+ sessionId = raw;
2206
+ }
2207
+ }
2208
+ if (!sessionId) {
2209
+ sessionId = `claude-${pid}-${shortHash(cwd)}`;
2210
+ }
2087
2211
  const cwdName = basename(cwd) || "workspace";
2088
- const name = env.CLAUDE_INTERCOM_NAME?.trim() || env.CLAUDE_PEER_NAME?.trim() || `claude-${cwdName}-${pid}`;
2212
+ const name = env.CLAUDE_INTERCOM_NAME?.trim() || env.CLAUDE_PEER_NAME?.trim() || (env.AGENT_INTERCOM_SESSION_NAME !== void 0 && env.AGENT_INTERCOM_SESSION_NAME.trim() ? env.AGENT_INTERCOM_SESSION_NAME.trim() : void 0) || `claude-${cwdName}-${pid}`;
2089
2213
  return {
2090
2214
  sessionId,
2091
2215
  name,
@@ -2151,7 +2275,8 @@ function detectGitRoot(cwd) {
2151
2275
  const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
2152
2276
  cwd,
2153
2277
  encoding: "utf8",
2154
- stdio: ["ignore", "pipe", "ignore"]
2278
+ stdio: ["ignore", "pipe", "ignore"],
2279
+ shell: false
2155
2280
  });
2156
2281
  if (result.status !== 0) return null;
2157
2282
  return result.stdout.trim() || null;
@@ -2506,18 +2631,21 @@ function buildToolDefinitions(runtime2) {
2506
2631
  name: "intercom_whoami",
2507
2632
  description: "Return this Claude session's intercom identity for reliable targeting.",
2508
2633
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
2634
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2509
2635
  handler: async () => runtime2.whoami()
2510
2636
  },
2511
2637
  {
2512
2638
  name: "intercom_team",
2513
2639
  description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
2514
2640
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
2641
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2515
2642
  handler: async () => runtime2.team()
2516
2643
  },
2517
2644
  {
2518
2645
  name: "intercom_status",
2519
2646
  description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
2520
2647
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
2648
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2521
2649
  handler: async () => runtime2.status()
2522
2650
  },
2523
2651
  {
@@ -2531,6 +2659,7 @@ function buildToolDefinitions(runtime2) {
2531
2659
  },
2532
2660
  additionalProperties: false
2533
2661
  },
2662
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2534
2663
  handler: async (args) => runtime2.list(
2535
2664
  args.scope === "directory" || args.scope === "repo" ? args.scope : "machine",
2536
2665
  asBoolean(args.include_self, false)
@@ -2545,6 +2674,7 @@ function buildToolDefinitions(runtime2) {
2545
2674
  required: ["summary"],
2546
2675
  additionalProperties: false
2547
2676
  },
2677
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
2548
2678
  handler: async (args) => runtime2.setSummary(asString(args.summary, "summary"))
2549
2679
  },
2550
2680
  {
@@ -2560,6 +2690,7 @@ function buildToolDefinitions(runtime2) {
2560
2690
  required: ["to", "message"],
2561
2691
  additionalProperties: false
2562
2692
  },
2693
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2563
2694
  handler: async (args) => runtime2.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments))
2564
2695
  },
2565
2696
  {
@@ -2576,6 +2707,7 @@ function buildToolDefinitions(runtime2) {
2576
2707
  required: ["to", "message"],
2577
2708
  additionalProperties: false
2578
2709
  },
2710
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2579
2711
  handler: async (args, signal) => runtime2.ask(
2580
2712
  asString(args.to, "to"),
2581
2713
  asString(args.message, "message"),
@@ -2592,6 +2724,7 @@ function buildToolDefinitions(runtime2) {
2592
2724
  properties: { mark_read: { type: "boolean", default: false } },
2593
2725
  additionalProperties: false
2594
2726
  },
2727
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2595
2728
  handler: async (args) => runtime2.pending(asBoolean(args.mark_read, false))
2596
2729
  },
2597
2730
  {
@@ -2607,6 +2740,7 @@ function buildToolDefinitions(runtime2) {
2607
2740
  required: ["message"],
2608
2741
  additionalProperties: false
2609
2742
  },
2743
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
2610
2744
  handler: async (args) => runtime2.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : void 0, args.which === "oldest" || args.which === "latest" ? args.which : void 0)
2611
2745
  }
2612
2746
  ];
@@ -2644,7 +2778,7 @@ async function handleMcpRequest(request, runtime2) {
2644
2778
  return ok(request.id, {});
2645
2779
  case "tools/list":
2646
2780
  return ok(request.id, {
2647
- tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }))
2781
+ tools: tools.map(({ name, description, inputSchema, annotations }) => ({ name, description, inputSchema, annotations }))
2648
2782
  });
2649
2783
  case "tools/call": {
2650
2784
  const name = request.params?.name;
@@ -265,7 +265,8 @@ function runClaudeTurn(options) {
265
265
  child = spawn(command, launch.args, {
266
266
  cwd: launch.cwd,
267
267
  env: { ...process.env, ...options.env },
268
- stdio: ["pipe", "pipe", "pipe"]
268
+ stdio: ["pipe", "pipe", "pipe"],
269
+ shell: false
269
270
  });
270
271
  } finally {
271
272
  launch.release();
@@ -557,7 +558,8 @@ function isNativeClaudeVersionCompatible(version) {
557
558
  function probeClaudeVersion(command = "claude") {
558
559
  const result = spawnSync(command, ["--version"], {
559
560
  encoding: "utf8",
560
- stdio: ["ignore", "pipe", "pipe"]
561
+ stdio: ["ignore", "pipe", "pipe"],
562
+ shell: false
561
563
  });
562
564
  if (result.error || result.status !== 0) {
563
565
  return {
@@ -2054,7 +2056,8 @@ function getBrokerSpawnOptions(extensionDir = EXTENSION_DIR, env = process.env)
2054
2056
  stdio: "ignore",
2055
2057
  cwd: extensionDir,
2056
2058
  env: { ...env, PI_CODING_AGENT_DIR: getAgentDirPath(env), NODE_NO_WARNINGS: "1" },
2057
- windowsHide: true
2059
+ windowsHide: true,
2060
+ shell: false
2058
2061
  };
2059
2062
  }
2060
2063
  function toError2(error) {
@@ -2381,6 +2384,7 @@ function loadConfig() {
2381
2384
 
2382
2385
  // claude/team.ts
2383
2386
  import { PARTICIPANT_STATES, TERMINAL_PARTICIPANT_STATES } from "@ctliz/agent-intercom-core/boss";
2387
+ import { readTeamManifestAsync, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
2384
2388
  var BOSS_NONTERMINAL_STATES = new Set(
2385
2389
  PARTICIPANT_STATES.filter((state) => !TERMINAL_PARTICIPANT_STATES.includes(state))
2386
2390
  );
@@ -2425,14 +2429,14 @@ function copyTextToClipboard(text) {
2425
2429
  ["clip.exe", []]
2426
2430
  ];
2427
2431
  for (const [command, args] of candidates) {
2428
- if (spawnSync2("which", [command], { stdio: "ignore" }).status !== 0) continue;
2432
+ if (spawnSync2("which", [command], { stdio: "ignore", shell: false }).status !== 0) continue;
2429
2433
  if (command === "wl-copy") {
2430
- const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"] });
2434
+ const child = spawn3(command, args, { stdio: ["pipe", "ignore", "ignore"], shell: false });
2431
2435
  child.stdin.end(text);
2432
2436
  child.unref();
2433
2437
  return true;
2434
2438
  }
2435
- if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] }).status === 0) return true;
2439
+ if (spawnSync2(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"], shell: false }).status === 0) return true;
2436
2440
  }
2437
2441
  return false;
2438
2442
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-claude",
3
- "version": "0.12.0-connect.3",
3
+ "version": "0.13.0-connect.3",
4
4
  "description": "Claude Code MCP plugin and wakeable worker for direct local messaging with Pi, Codex, and Claude coding-agent sessions.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -67,10 +67,10 @@
67
67
  "codex"
68
68
  ],
69
69
  "peerDependencies": {
70
- "@ctliz/agent-intercom-core": "0.1.0"
70
+ "@ctliz/agent-intercom-core": "0.2.0"
71
71
  },
72
72
  "devDependencies": {
73
- "@ctliz/agent-intercom-core": "git+https://github.com/ctliz/agent-intercom-core.git#37e074970e2a9de32a16fc325607c3b476b0bd45",
73
+ "@ctliz/agent-intercom-core": "0.2.0",
74
74
  "@types/node": "^24.0.0",
75
75
  "esbuild": "^0.28.1",
76
76
  "tsx": "^4.20.0",
@@ -5,25 +5,25 @@ import { spawnSync } from "node:child_process";
5
5
 
6
6
  export const APPROVED_CORE = Object.freeze({
7
7
  name: "@ctliz/agent-intercom-core",
8
- version: "0.1.0",
9
- commit: "37e074970e2a9de32a16fc325607c3b476b0bd45",
10
- integrity: "sha512-b5MXnfKh/RCjKoVor0tsFO+NKAqPROWgkgbHt7jiHxhdVbSh7gWvOdnVUtpSdPwJLYKSQrYXT+TJl7QocdJEPA==",
8
+ version: "0.2.0",
9
+ integrity: "sha512-bpifL9cc8cwMm74fpvjgRBarXMwn6BY4cST4ry6HrGtfpRTXyiJOtwfNnhORF6xTKwQNOWakxS1sZALczInvkQ==",
10
+ resolved: "https://registry.npmjs.org/@ctliz/agent-intercom-core/-/agent-intercom-core-0.2.0.tgz",
11
11
  });
12
12
 
13
13
  const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
14
- const devSpec = `git+https://github.com/ctliz/agent-intercom-core.git#${APPROVED_CORE.commit}`;
14
+ const devSpec = APPROVED_CORE.version;
15
15
 
16
16
  function readJson(path) {
17
17
  return JSON.parse(readFileSync(path, "utf8"));
18
18
  }
19
19
 
20
- function requireEqual(actual, expected, field) {
20
+ export function requireEqual(actual, expected, field) {
21
21
  if (actual !== expected) {
22
22
  throw new Error(`${field} must be ${JSON.stringify(expected)}; received ${JSON.stringify(actual)}`);
23
23
  }
24
24
  }
25
25
 
26
- function verifyLocalProvenance() {
26
+ export function verifyLocalProvenance() {
27
27
  const manifest = readJson(join(repositoryRoot, "package.json"));
28
28
  const lock = readJson(join(repositoryRoot, "package-lock.json"));
29
29
  const lockRoot = lock.packages?.[""];
@@ -36,28 +36,30 @@ function verifyLocalProvenance() {
36
36
  requireEqual(lockRoot?.peerDependencies?.[APPROVED_CORE.name], APPROVED_CORE.version, "locked Core peer dependency");
37
37
  requireEqual(lockRoot?.devDependencies?.[APPROVED_CORE.name], devSpec, "locked Core development provenance");
38
38
  requireEqual(lockedCore?.version, APPROVED_CORE.version, "supplied Core version");
39
- requireEqual(lockedCore?.resolved, devSpec, "supplied Core commit");
39
+ requireEqual(lockedCore?.resolved, APPROVED_CORE.resolved, "supplied Core resolved URL");
40
40
  requireEqual(lockedCore?.integrity, APPROVED_CORE.integrity, "supplied Core artifact integrity");
41
41
  requireEqual(lockedCore?.dev, true, "supplied Core dependency class");
42
42
  }
43
43
 
44
- function verifyPublishedProvenance() {
45
- const result = spawnSync("npm", [
44
+ export function parseAndVerifyPublishedProvenance(jsonString) {
45
+ const published = typeof jsonString === "string" ? JSON.parse(jsonString) : jsonString;
46
+ requireEqual(published.version, APPROVED_CORE.version, "published Core version");
47
+ requireEqual(published["dist.integrity"] ?? published.dist?.integrity, APPROVED_CORE.integrity, "published Core artifact integrity");
48
+ }
49
+
50
+ export function verifyPublishedProvenance(runner = spawnSync) {
51
+ const result = runner("npm", [
46
52
  "view",
47
53
  `${APPROVED_CORE.name}@${APPROVED_CORE.version}`,
48
54
  "version",
49
55
  "dist.integrity",
50
- "gitHead",
51
56
  "--json",
52
- ], { encoding: "utf8" });
57
+ ], { encoding: "utf8", shell: false });
53
58
  if (result.error) throw result.error;
54
59
  if (result.status !== 0) {
55
60
  throw new Error(`Unable to inspect published Core ${APPROVED_CORE.version}: ${(result.stderr || result.stdout).trim()}`);
56
61
  }
57
- const published = JSON.parse(result.stdout);
58
- requireEqual(published.version, APPROVED_CORE.version, "published Core version");
59
- requireEqual(published["dist.integrity"] ?? published.dist?.integrity, APPROVED_CORE.integrity, "published Core artifact integrity");
60
- requireEqual(published.gitHead, APPROVED_CORE.commit, "published Core commit");
62
+ parseAndVerifyPublishedProvenance(result.stdout);
61
63
  }
62
64
 
63
65
  verifyLocalProvenance();
@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
6
6
 
7
7
  const repositoryRoot = resolve(import.meta.dirname, "..");
8
8
  const corePackage = "@ctliz/agent-intercom-core";
9
- const coreDevSpec = "git+https://github.com/ctliz/agent-intercom-core.git#37e074970e2a9de32a16fc325607c3b476b0bd45";
9
+ const coreDevSpec = "0.2.0";
10
10
  const coreConsumers = new Set([
11
11
  "broker.mjs",
12
12
  "cci.mjs",
@@ -51,6 +51,7 @@ function createPackageArchive(temp) {
51
51
  cwd: repositoryRoot,
52
52
  env: { ...process.env, npm_config_cache: join(temp, "npm-cache") },
53
53
  encoding: "utf8",
54
+ shell: false,
54
55
  });
55
56
  if (result.error) throw result.error;
56
57
  if (result.status !== 0) throw new Error(`npm pack failed: ${result.stderr || result.stdout}`);
@@ -72,10 +73,10 @@ try {
72
73
  const manifestEntry = entries.get("package/package.json");
73
74
  if (!manifestEntry) throw new Error("Packed adapter is missing package.json");
74
75
  const manifest = JSON.parse(manifestEntry.toString("utf8"));
75
- if (manifest.peerDependencies?.[corePackage] !== "0.1.0") throw new Error("Packed adapter must require Core peer 0.1.0");
76
+ if (manifest.peerDependencies?.[corePackage] !== "0.2.0") throw new Error("Packed adapter must require Core peer 0.2.0");
76
77
  if (manifest.peerDependenciesMeta?.[corePackage]?.optional === true) throw new Error("Packed Core peer must not be optional");
77
78
  if (manifest.dependencies?.[corePackage] !== undefined) throw new Error("Packed adapter must not install a private Core dependency");
78
- if (manifest.devDependencies?.[corePackage] !== coreDevSpec) throw new Error("Packed adapter lost the approved Core build commit");
79
+ if (manifest.devDependencies?.[corePackage] !== coreDevSpec) throw new Error("Packed adapter lost the approved Core dependency");
79
80
  if ([...entries.keys()].some((path) => path.includes("node_modules/@ctliz/agent-intercom-core/"))) {
80
81
  throw new Error("Packed adapter contains a private Core module tree");
81
82
  }
@@ -134,7 +135,7 @@ try {
134
135
  }
135
136
  }
136
137
 
137
- console.log(`Verified complete plugin+MCP+monitors+dist chain and ${bundles.length} packed bundles using Core peer 0.1.0`);
138
+ console.log(`Verified complete plugin+MCP+monitors+dist chain and ${bundles.length} packed bundles using Core peer 0.2.0`);
138
139
  } finally {
139
140
  if (temp) rmSync(temp, { recursive: true, force: true });
140
141
  }