pi-cohort 7.6.0 → 7.6.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
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.6.1] - 2026-10-26
4
+
5
+ ### Added
6
+ - Execution backends can declare `surfaceEnvironmentPrefixes`; each child attempt gets the launched surface's own values for matching keys instead of the parent's, and inherits none of the parent's matching keys. Before, a child started in a terminal-mux pane carried the parent pane's identity (for example `CMUX_SURFACE_ID`), so mux-aware tools in the child acted on the parent's pane.
7
+
3
8
  ## [7.6.0] - 2026-10-26
4
9
 
5
10
  ### Breaking
package/README.md CHANGED
@@ -79,6 +79,8 @@ The descriptor has exactly four fields. `packageJsonUrl` must be an absolute `fi
79
79
 
80
80
  Cohort serializes only backend names and validated reload descriptors into its detached coordinator - never live backend objects or an environment capsule. The coordinator reloads registrations in their original order before selecting a backend, and every child launch receives its exact resolved `cwd`. Selecting `native` bypasses reload entirely. An explicit backend selection reports that backend's reload failure; `auto` reports any registered reload failure rather than silently changing policy. Once reload succeeds, normal auto-detection still falls back to native when no registered backend is available. Backends registered without `reload` remain usable in the foreground but cannot be reconstructed for a background run.
81
81
 
82
+ A backend whose surface sets its own identity variables declares them with `surfaceEnvironmentPrefixes` (for example `["CMUX_"]`). Each child attempt then drops the parent's keys with those prefixes and takes the launched surface's values instead, so mux-aware tools in the child address the child's surface. Prefixes must be nonblank strings.
83
+
82
84
  ## Mental model
83
85
 
84
86
  Pi is the parent session. A subagent is a focused child Pi session with its own job. When you ask for a subagent, Pi starts the child, gives it the task, and brings the result back. Foreground runs stream in the conversation; background runs keep working and can be checked later.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.6.0",
3
+ "version": "7.6.1",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@ export async function createChildHostController(options: ChildHostControllerOpti
55
55
  let descriptor: number | undefined;
56
56
  try {
57
57
  descriptor = fileSystem.openSync(configPath, "wx", 0o600);
58
- fileSystem.writeFileSync(descriptor, JSON.stringify({ protocolVersion: 1, socketPath, runId: options.runId, childId: options.childId }));
58
+ fileSystem.writeFileSync(descriptor, JSON.stringify({ protocolVersion: 1, socketPath, runId: options.runId, childId: options.childId, surfaceEnvironmentPrefixes: options.backend.surfaceEnvironmentPrefixes ?? [] }));
59
59
  } finally { if (descriptor !== undefined) fileSystem.closeSync(descriptor); }
60
60
 
61
61
  let lease: ExecutionBackendLease | undefined;
@@ -6,6 +6,7 @@ export interface ChildHostConfig {
6
6
  readonly socketPath: string;
7
7
  readonly runId: string;
8
8
  readonly childId: string;
9
+ readonly surfaceEnvironmentPrefixes: readonly string[];
9
10
  }
10
11
 
11
12
  type Correlation = { readonly protocolVersion: 1; readonly runId: string; readonly childId: string };
@@ -35,9 +36,11 @@ function correlation(input: Record<string, unknown>): Correlation {
35
36
 
36
37
  export function validateChildHostConfig(value: unknown): ChildHostConfig {
37
38
  const input = record(value, "child host config");
38
- exact(input, ["protocolVersion", "socketPath", "runId", "childId"]);
39
+ exact(input, ["protocolVersion", "socketPath", "runId", "childId", "surfaceEnvironmentPrefixes"]);
39
40
  if (input.protocolVersion !== CHILD_HOST_PROTOCOL_VERSION) throw new Error("incompatible child host protocol");
40
- return { ...correlation(input), socketPath: text(input.socketPath, "socketPath") };
41
+ const prefixes = input.surfaceEnvironmentPrefixes;
42
+ if (!Array.isArray(prefixes)) throw new Error("surfaceEnvironmentPrefixes must be an array");
43
+ return { ...correlation(input), socketPath: text(input.socketPath, "socketPath"), surfaceEnvironmentPrefixes: prefixes.map(prefix => text(prefix, "surfaceEnvironmentPrefixes entry")) };
41
44
  }
42
45
 
43
46
  export function validateChildHostMessage(value: unknown): ChildHostMessage {
@@ -17,6 +17,13 @@ export function loadChildHostConfig(configPath = process.env[PI_COHORT_CHILD_HOS
17
17
  } finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
18
18
  }
19
19
 
20
+ function attemptEnvironment(parent: Readonly<Record<string, string>>, surfacePrefixes: readonly string[]): Record<string, string> {
21
+ const surfaceOwned = (key: string) => surfacePrefixes.some(prefix => key.startsWith(prefix));
22
+ const environment = Object.fromEntries(Object.entries(parent).filter(([key]) => !surfaceOwned(key)));
23
+ for (const [key, value] of Object.entries(process.env)) if (value !== undefined && surfaceOwned(key)) environment[key] = value;
24
+ return environment;
25
+ }
26
+
20
27
  export function runChildHost(config = loadChildHostConfig()): void {
21
28
  const correlation = { protocolVersion: 1 as const, runId: config.runId, childId: config.childId };
22
29
  const socket = net.createConnection(config.socketPath);
@@ -36,7 +43,7 @@ export function runChildHost(config = loadChildHostConfig()): void {
36
43
  }
37
44
  if (message.kind !== "start_attempt" || active || stopping) { socket.destroy(); return; }
38
45
  let child: ChildProcess;
39
- try { child = spawn(message.command, [...message.args], { cwd: message.cwd, env: message.environment, shell: false, stdio: "inherit", windowsHide: true }); }
46
+ try { child = spawn(message.command, [...message.args], { cwd: message.cwd, env: attemptEnvironment(message.environment, config.surfaceEnvironmentPrefixes), shell: false, stdio: "inherit", windowsHide: true }); }
40
47
  catch { send({ ...correlation, kind: "attempt_spawn_error", attemptId: message.attemptId }); continue; }
41
48
  active = { attemptId: message.attemptId, process: child };
42
49
  send({ ...correlation, kind: "attempt_started", attemptId: message.attemptId });
@@ -54,6 +54,10 @@ function assertBackend(backend: ExecutionBackend): void {
54
54
  for (const method of ["detect", "launch", "reattach", "close"] as const) {
55
55
  if (typeof backend[method] !== "function") throw new Error(`Execution backend ${method} must be a function`);
56
56
  }
57
+ const prefixes = backend.surfaceEnvironmentPrefixes;
58
+ if (prefixes !== undefined && (!Array.isArray(prefixes) || prefixes.some(prefix => typeof prefix !== "string" || prefix.trim() === ""))) {
59
+ throw new Error("Execution backend surfaceEnvironmentPrefixes must be nonblank strings");
60
+ }
57
61
  }
58
62
 
59
63
  export function registerExecutionBackend(
@@ -77,6 +77,12 @@ export type ExecutionBackendCloseReason = "delivered" | "explicit_cleanup";
77
77
  export interface ExecutionBackend {
78
78
  readonly name: string;
79
79
  readonly protocolVersion: typeof EXECUTION_BACKEND_PROTOCOL_VERSION;
80
+ /**
81
+ * Environment-variable prefixes the surface owns, such as a terminal's own
82
+ * surface identity. Each attempt gets the launched surface's values for
83
+ * matching keys instead of the parent's.
84
+ */
85
+ readonly surfaceEnvironmentPrefixes?: readonly string[];
80
86
  detect(): Promise<ExecutionBackendDetection>;
81
87
  launch(request: ExecutionSurfaceRequest): Promise<ExecutionBackendLease>;
82
88
  reattach(handle: ExecutionSurfaceHandle): Promise<ReattachResult>;