@gotgenes/pi-permission-system 5.1.1 → 5.2.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,29 @@ 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.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.2.0](https://github.com/gotgenes/pi-permission-system/compare/v5.1.2...v5.2.0) (2026-05-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES, iterate in resolver ([#96](https://github.com/gotgenes/pi-permission-system/issues/96)) ([ac6831d](https://github.com/gotgenes/pi-permission-system/commit/ac6831d3418db0aee5d5ed4757d5833730a6e130))
14
+ * broaden SUBAGENT_ENV_HINT_KEYS for nicobailon + HazAT extensions ([#96](https://github.com/gotgenes/pi-permission-system/issues/96)) ([8adafdb](https://github.com/gotgenes/pi-permission-system/commit/8adafdb45af66cf99b000b3ad011bee5a2c90476))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * plan broaden subagent env hint keys ([#96](https://github.com/gotgenes/pi-permission-system/issues/96)) ([9fa97b7](https://github.com/gotgenes/pi-permission-system/commit/9fa97b7385e5b9203a35c80d15f980ac4501f788))
20
+ * update target-architecture subagent detection for [#96](https://github.com/gotgenes/pi-permission-system/issues/96) ([64cce35](https://github.com/gotgenes/pi-permission-system/commit/64cce3569c09cede690858af41d2f35611a8705f))
21
+
22
+ ## [5.1.2](https://github.com/gotgenes/pi-permission-system/compare/v5.1.1...v5.1.2) (2026-05-05)
23
+
24
+
25
+ ### Documentation
26
+
27
+ * fix README per-agent frontmatter example to flat format ([#78](https://github.com/gotgenes/pi-permission-system/issues/78)) ([1295427](https://github.com/gotgenes/pi-permission-system/commit/129542795218a6ada1f8d069a22b5ace5ec6c445))
28
+ * plan fix README frontmatter example and add missing tests ([#78](https://github.com/gotgenes/pi-permission-system/issues/78)) ([3fc99e1](https://github.com/gotgenes/pi-permission-system/commit/3fc99e1b3adf8193c94ac778617ca830488fa621))
29
+ * **retro:** add retro notes for issue [#93](https://github.com/gotgenes/pi-permission-system/issues/93) ([c9e8e89](https://github.com/gotgenes/pi-permission-system/commit/c9e8e89eb4057866198402add374d72a90a2fa2e))
30
+
8
31
  ## [5.1.1](https://github.com/gotgenes/pi-permission-system/compare/v5.1.0...v5.1.1) (2026-05-05)
9
32
 
10
33
 
package/README.md CHANGED
@@ -170,22 +170,21 @@ Override global permissions for specific agents via YAML frontmatter in the glob
170
170
  ---
171
171
  name: my-agent
172
172
  permission:
173
- tools:
174
- read: allow
175
- write: deny
176
- mcp: allow
173
+ read: allow
174
+ write: deny
175
+ mcp: allow
177
176
  bash:
178
177
  git status: allow
179
178
  git *: ask
180
179
  mcp:
181
180
  chrome_devtools_*: deny
182
181
  exa_*: allow
183
- skills:
182
+ skill:
184
183
  "*": ask
185
184
  ---
186
185
  ```
187
186
 
188
- **MCP behavior:** `permission.tools.mcp` is the coarse entry/fallback permission for a registered `mcp` tool when one is available. More specific `permission.mcp` target rules override that fallback when they match.
187
+ **MCP behavior:** `permission.mcp` is the coarse entry/fallback permission for a registered `mcp` tool when one is available. More specific `permission.mcp` target rules override that fallback when they match.
189
188
 
190
189
  **Limitations:** The frontmatter parser is intentionally minimal. Use only `key: value` scalars and nested maps. Avoid arrays, multi-line scalars, and YAML anchors.
191
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -18,6 +18,7 @@ import {
18
18
  PERMISSION_FORWARDING_POLL_INTERVAL_MS,
19
19
  PERMISSION_FORWARDING_TIMEOUT_MS,
20
20
  resolvePermissionForwardingTargetSessionId,
21
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
21
22
  } from "../permission-forwarding";
22
23
  import { isSubagentExecutionContext } from "../subagent-context";
23
24
 
@@ -110,7 +111,10 @@ export async function waitForForwardedPermissionApproval(
110
111
  if (!targetSessionId) {
111
112
  logPermissionForwardingError(
112
113
  deps.logger,
113
- "Permission forwarding target session could not be resolved from subagent runtime metadata (expected PI_AGENT_ROUTER_PARENT_SESSION_ID)",
114
+ `Permission forwarding target session could not be resolved. ` +
115
+ `Checked env vars: ${SUBAGENT_PARENT_SESSION_ENV_CANDIDATES.join(", ")}. ` +
116
+ `If you are using nicobailon/pi-subagents or HazAT/pi-interactive-subagents, ` +
117
+ `parent-session forwarding is not yet supported for those extensions (see issue #98).`,
114
118
  );
115
119
  return { approved: false, state: "denied" };
116
120
  }
@@ -5,12 +5,30 @@ import type { PermissionDecisionState } from "./permission-dialog";
5
5
  export const PERMISSION_FORWARDING_POLL_INTERVAL_MS = 250;
6
6
  export const PERMISSION_FORWARDING_TIMEOUT_MS = 10 * 60 * 1000;
7
7
  export const SUBAGENT_ENV_HINT_KEYS = [
8
+ // pi-agent-router (original)
8
9
  "PI_IS_SUBAGENT",
9
10
  "PI_SUBAGENT_SESSION_ID",
10
11
  "PI_AGENT_ROUTER_SUBAGENT",
12
+ // nicobailon/pi-subagents
13
+ "PI_SUBAGENT_CHILD",
14
+ "PI_SUBAGENT_RUN_ID",
15
+ "PI_SUBAGENT_CHILD_AGENT",
16
+ "PI_SUBAGENT_DEPTH",
17
+ // HazAT/pi-interactive-subagents
18
+ "PI_SUBAGENT_NAME",
19
+ "PI_SUBAGENT_ID",
20
+ "PI_SUBAGENT_SESSION",
21
+ "PI_SUBAGENT_ACTIVITY_FILE",
11
22
  ] as const;
23
+ /** Ordered list of env var names to check for the parent session ID. First match wins. */
24
+ export const SUBAGENT_PARENT_SESSION_ENV_CANDIDATES: readonly string[] = [
25
+ // pi-agent-router (original)
26
+ "PI_AGENT_ROUTER_PARENT_SESSION_ID",
27
+ ] as const;
28
+
29
+ /** @deprecated Use SUBAGENT_PARENT_SESSION_ENV_CANDIDATES */
12
30
  export const SUBAGENT_PARENT_SESSION_ENV_KEY =
13
- "PI_AGENT_ROUTER_PARENT_SESSION_ID";
31
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0];
14
32
 
15
33
  const SESSION_FORWARDING_ROOT_DIRECTORY_NAME = "sessions";
16
34
  const SESSION_FORWARDING_REQUESTS_DIRECTORY_NAME = "requests";
@@ -106,9 +124,12 @@ export function resolvePermissionForwardingTargetSessionId(options: {
106
124
  return null;
107
125
  }
108
126
 
109
- return normalizePermissionForwardingSessionId(
110
- options.env?.[SUBAGENT_PARENT_SESSION_ENV_KEY],
111
- );
127
+ const env = options.env ?? process.env;
128
+ for (const key of SUBAGENT_PARENT_SESSION_ENV_CANDIDATES) {
129
+ const resolved = normalizePermissionForwardingSessionId(env[key]);
130
+ if (resolved) return resolved;
131
+ }
132
+ return null;
112
133
  }
113
134
 
114
135
  export function isForwardedPermissionRequestForSession(
@@ -0,0 +1,143 @@
1
+ import { afterEach, describe, expect, test, vi } from "vitest";
2
+ import {
3
+ resolvePermissionForwardingTargetSessionId,
4
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
5
+ SUBAGENT_PARENT_SESSION_ENV_KEY,
6
+ } from "../src/permission-forwarding";
7
+
8
+ afterEach(() => {
9
+ vi.unstubAllEnvs();
10
+ });
11
+
12
+ describe("SUBAGENT_PARENT_SESSION_ENV_CANDIDATES", () => {
13
+ test("is an array containing PI_AGENT_ROUTER_PARENT_SESSION_ID", () => {
14
+ expect(Array.isArray(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES)).toBe(true);
15
+ expect(SUBAGENT_PARENT_SESSION_ENV_CANDIDATES).toContain(
16
+ "PI_AGENT_ROUTER_PARENT_SESSION_ID",
17
+ );
18
+ });
19
+
20
+ test("deprecated SUBAGENT_PARENT_SESSION_ENV_KEY equals the first candidate", () => {
21
+ expect(SUBAGENT_PARENT_SESSION_ENV_KEY).toBe(
22
+ SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0],
23
+ );
24
+ });
25
+ });
26
+
27
+ describe("resolvePermissionForwardingTargetSessionId", () => {
28
+ test("hasUI=true returns the current session ID (UI host owns forwarding)", () => {
29
+ expect(
30
+ resolvePermissionForwardingTargetSessionId({
31
+ hasUI: true,
32
+ isSubagent: false,
33
+ currentSessionId: "parent-session-abc",
34
+ env: {},
35
+ }),
36
+ ).toBe("parent-session-abc");
37
+ });
38
+
39
+ test("hasUI=true with isSubagent=true still returns current session ID", () => {
40
+ expect(
41
+ resolvePermissionForwardingTargetSessionId({
42
+ hasUI: true,
43
+ isSubagent: true,
44
+ currentSessionId: "session-xyz",
45
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "other" },
46
+ }),
47
+ ).toBe("session-xyz");
48
+ });
49
+
50
+ test("hasUI=false, isSubagent=false returns null", () => {
51
+ expect(
52
+ resolvePermissionForwardingTargetSessionId({
53
+ hasUI: false,
54
+ isSubagent: false,
55
+ currentSessionId: "session-xyz",
56
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "parent-session-abc" },
57
+ }),
58
+ ).toBeNull();
59
+ });
60
+
61
+ test("isSubagent=true, no candidates set returns null", () => {
62
+ expect(
63
+ resolvePermissionForwardingTargetSessionId({
64
+ hasUI: false,
65
+ isSubagent: true,
66
+ currentSessionId: "session-xyz",
67
+ env: {},
68
+ }),
69
+ ).toBeNull();
70
+ });
71
+
72
+ test("isSubagent=true, PI_AGENT_ROUTER_PARENT_SESSION_ID set returns its value", () => {
73
+ expect(
74
+ resolvePermissionForwardingTargetSessionId({
75
+ hasUI: false,
76
+ isSubagent: true,
77
+ currentSessionId: "session-xyz",
78
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "parent-session-abc" },
79
+ }),
80
+ ).toBe("parent-session-abc");
81
+ });
82
+
83
+ test("isSubagent=true, first candidate absent but second set returns second value", () => {
84
+ // Inject a second candidate at test-time to validate the iteration logic
85
+ // without waiting for a real extension to adopt the convention.
86
+ const originalCandidates = [...SUBAGENT_PARENT_SESSION_ENV_CANDIDATES];
87
+ // Mutate the array via index-assignment through a cast so we can test
88
+ // multi-candidate iteration without changing the exported constant type.
89
+ // This is test-only; production code never mutates the array.
90
+ (SUBAGENT_PARENT_SESSION_ENV_CANDIDATES as unknown as string[]).push(
91
+ "PI_SUBAGENT_PARENT_SESSION_ID_TEST_ONLY",
92
+ );
93
+
94
+ try {
95
+ expect(
96
+ resolvePermissionForwardingTargetSessionId({
97
+ hasUI: false,
98
+ isSubagent: true,
99
+ currentSessionId: "session-xyz",
100
+ env: {
101
+ PI_SUBAGENT_PARENT_SESSION_ID_TEST_ONLY: "parent-from-second",
102
+ },
103
+ }),
104
+ ).toBe("parent-from-second");
105
+ } finally {
106
+ // Restore original array contents.
107
+ (SUBAGENT_PARENT_SESSION_ENV_CANDIDATES as unknown as string[]).length =
108
+ originalCandidates.length;
109
+ }
110
+ });
111
+
112
+ test("isSubagent=true, candidate value is empty string returns null", () => {
113
+ expect(
114
+ resolvePermissionForwardingTargetSessionId({
115
+ hasUI: false,
116
+ isSubagent: true,
117
+ currentSessionId: "session-xyz",
118
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "" },
119
+ }),
120
+ ).toBeNull();
121
+ });
122
+
123
+ test("isSubagent=true, candidate value is 'unknown' returns null", () => {
124
+ expect(
125
+ resolvePermissionForwardingTargetSessionId({
126
+ hasUI: false,
127
+ isSubagent: true,
128
+ currentSessionId: "session-xyz",
129
+ env: { PI_AGENT_ROUTER_PARENT_SESSION_ID: "unknown" },
130
+ }),
131
+ ).toBeNull();
132
+ });
133
+
134
+ test("env defaults to process.env when omitted", () => {
135
+ vi.stubEnv("PI_AGENT_ROUTER_PARENT_SESSION_ID", "env-session-abc");
136
+ expect(
137
+ resolvePermissionForwardingTargetSessionId({
138
+ hasUI: false,
139
+ isSubagent: true,
140
+ }),
141
+ ).toBe("env-session-abc");
142
+ });
143
+ });
@@ -7,7 +7,7 @@ import {
7
7
  rmSync,
8
8
  writeFileSync,
9
9
  } from "node:fs";
10
- import { tmpdir } from "node:os";
10
+ import { homedir, tmpdir } from "node:os";
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  import { test } from "vitest";
13
13
 
@@ -1831,6 +1831,188 @@ test("external_directory permission is not affected by unrelated surface keys",
1831
1831
  }
1832
1832
  });
1833
1833
 
1834
+ test("skill pattern map in agent frontmatter overrides global skill policy", () => {
1835
+ const { manager, cleanup } = createManager(
1836
+ {
1837
+ permission: { "*": "deny", skill: "deny" },
1838
+ },
1839
+ {
1840
+ reviewer: `---
1841
+ name: reviewer
1842
+ permission:
1843
+ skill:
1844
+ "*": ask
1845
+ "pi-*": allow
1846
+ ---
1847
+ `,
1848
+ },
1849
+ );
1850
+
1851
+ try {
1852
+ // Matches agent frontmatter pi-* pattern
1853
+ const allowed = manager.checkPermission(
1854
+ "skill",
1855
+ { name: "pi-code-review" },
1856
+ "reviewer",
1857
+ );
1858
+ assert.equal(allowed.state, "allow");
1859
+ assert.equal(allowed.matchedPattern, "pi-*");
1860
+ assert.equal(allowed.source, "skill");
1861
+
1862
+ // Falls through to agent frontmatter catch-all
1863
+ const asked = manager.checkPermission(
1864
+ "skill",
1865
+ { name: "other-skill" },
1866
+ "reviewer",
1867
+ );
1868
+ assert.equal(asked.state, "ask");
1869
+ assert.equal(asked.matchedPattern, "*");
1870
+
1871
+ // No agent override — global deny applies
1872
+ const denied = manager.checkPermission("skill", { name: "pi-code-review" });
1873
+ assert.equal(denied.state, "deny");
1874
+ assert.equal(denied.source, "skill");
1875
+ } finally {
1876
+ cleanup();
1877
+ }
1878
+ });
1879
+
1880
+ test("external_directory pattern map in agent frontmatter overrides global policy", () => {
1881
+ const { manager, cleanup } = createManager(
1882
+ {
1883
+ permission: { "*": "allow", external_directory: "deny" },
1884
+ },
1885
+ {
1886
+ trusted: `---
1887
+ name: trusted
1888
+ permission:
1889
+ external_directory:
1890
+ "*": deny
1891
+ "~/Downloads/*": allow
1892
+ ---
1893
+ `,
1894
+ },
1895
+ );
1896
+
1897
+ try {
1898
+ // Matches agent frontmatter ~/Downloads/* pattern
1899
+ const allowed = manager.checkPermission(
1900
+ "external_directory",
1901
+ { path: `${homedir()}/Downloads/file.txt` },
1902
+ "trusted",
1903
+ );
1904
+ assert.equal(allowed.state, "allow");
1905
+ assert.equal(allowed.matchedPattern, "~/Downloads/*");
1906
+ assert.equal(allowed.source, "special");
1907
+
1908
+ // Falls through to agent frontmatter catch-all deny
1909
+ const denied = manager.checkPermission(
1910
+ "external_directory",
1911
+ { path: `${homedir()}/Documents/secret.txt` },
1912
+ "trusted",
1913
+ );
1914
+ assert.equal(denied.state, "deny");
1915
+ assert.equal(denied.matchedPattern, "*");
1916
+
1917
+ // No agent override — global deny applies
1918
+ const globalDenied = manager.checkPermission("external_directory", {});
1919
+ assert.equal(globalDenied.state, "deny");
1920
+ assert.equal(globalDenied.source, "special");
1921
+ } finally {
1922
+ cleanup();
1923
+ }
1924
+ });
1925
+
1926
+ test("project-agent frontmatter skill rules override global-agent frontmatter skill rules", () => {
1927
+ const { manager, cleanup } = createManagerWithProject(
1928
+ {
1929
+ permission: { "*": "deny" },
1930
+ },
1931
+ {
1932
+ analyst: `---
1933
+ name: analyst
1934
+ permission:
1935
+ skill:
1936
+ "*": ask
1937
+ ---
1938
+ `,
1939
+ },
1940
+ {
1941
+ projectAgentFiles: {
1942
+ analyst: `---
1943
+ name: analyst
1944
+ permission:
1945
+ skill:
1946
+ "pi-*": allow
1947
+ "*": deny
1948
+ ---
1949
+ `,
1950
+ },
1951
+ },
1952
+ );
1953
+
1954
+ try {
1955
+ // Project-agent pi-* wins over global-agent *: ask
1956
+ const allowed = manager.checkPermission(
1957
+ "skill",
1958
+ { name: "pi-code-review" },
1959
+ "analyst",
1960
+ );
1961
+ assert.equal(allowed.state, "allow");
1962
+ assert.equal(allowed.matchedPattern, "pi-*");
1963
+
1964
+ // Project-agent *: deny wins over global-agent *: ask
1965
+ const denied = manager.checkPermission(
1966
+ "skill",
1967
+ { name: "other-skill" },
1968
+ "analyst",
1969
+ );
1970
+ assert.equal(denied.state, "deny");
1971
+ assert.equal(denied.matchedPattern, "*");
1972
+ } finally {
1973
+ cleanup();
1974
+ }
1975
+ });
1976
+
1977
+ test("project-agent frontmatter external_directory rules override global-agent frontmatter rules", () => {
1978
+ const { manager, cleanup } = createManagerWithProject(
1979
+ {
1980
+ permission: { "*": "allow", external_directory: "deny" },
1981
+ },
1982
+ {
1983
+ analyst: `---
1984
+ name: analyst
1985
+ permission:
1986
+ external_directory: ask
1987
+ ---
1988
+ `,
1989
+ },
1990
+ {
1991
+ projectAgentFiles: {
1992
+ analyst: `---
1993
+ name: analyst
1994
+ permission:
1995
+ external_directory: allow
1996
+ ---
1997
+ `,
1998
+ },
1999
+ },
2000
+ );
2001
+
2002
+ try {
2003
+ // Project-agent allow wins over global-agent ask
2004
+ const result = manager.checkPermission("external_directory", {}, "analyst");
2005
+ assert.equal(result.state, "allow");
2006
+ assert.equal(result.source, "special");
2007
+
2008
+ // Without agent context, global config deny applies
2009
+ const globalResult = manager.checkPermission("external_directory", {});
2010
+ assert.equal(globalResult.state, "deny");
2011
+ } finally {
2012
+ cleanup();
2013
+ }
2014
+ });
2015
+
1834
2016
  test("tool_call blocks path-bearing tools outside cwd when external_directory is denied", async () => {
1835
2017
  const rootDir = mkdtempSync(join(tmpdir(), "pi-permission-system-boundary-"));
1836
2018
  const cwd = join(rootDir, "repo");
@@ -61,11 +61,86 @@ describe("isSubagentExecutionContext — env hint detection", () => {
61
61
  ).toBe(true);
62
62
  });
63
63
 
64
- test("covers all three declared SUBAGENT_ENV_HINT_KEYS", () => {
64
+ // nicobailon/pi-subagents keys
65
+ test("returns true when PI_SUBAGENT_CHILD is set", () => {
66
+ vi.stubEnv("PI_SUBAGENT_CHILD", "1");
67
+ expect(
68
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
69
+ ).toBe(true);
70
+ });
71
+
72
+ test("returns true when PI_SUBAGENT_RUN_ID is set", () => {
73
+ vi.stubEnv("PI_SUBAGENT_RUN_ID", "run-abc");
74
+ expect(
75
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
76
+ ).toBe(true);
77
+ });
78
+
79
+ test("returns true when PI_SUBAGENT_CHILD_AGENT is set", () => {
80
+ vi.stubEnv("PI_SUBAGENT_CHILD_AGENT", "worker");
81
+ expect(
82
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
83
+ ).toBe(true);
84
+ });
85
+
86
+ test("returns true when PI_SUBAGENT_DEPTH is set", () => {
87
+ vi.stubEnv("PI_SUBAGENT_DEPTH", "1");
88
+ expect(
89
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
90
+ ).toBe(true);
91
+ });
92
+
93
+ test("returns true when PI_SUBAGENT_DEPTH is zero (depth-0 is still a subagent context)", () => {
94
+ vi.stubEnv("PI_SUBAGENT_DEPTH", "0");
95
+ expect(
96
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
97
+ ).toBe(true);
98
+ });
99
+
100
+ // HazAT/pi-interactive-subagents keys
101
+ test("returns true when PI_SUBAGENT_NAME is set", () => {
102
+ vi.stubEnv("PI_SUBAGENT_NAME", "my-agent");
103
+ expect(
104
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
105
+ ).toBe(true);
106
+ });
107
+
108
+ test("returns true when PI_SUBAGENT_ID is set", () => {
109
+ vi.stubEnv("PI_SUBAGENT_ID", "id-xyz");
110
+ expect(
111
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
112
+ ).toBe(true);
113
+ });
114
+
115
+ test("returns true when PI_SUBAGENT_SESSION is set", () => {
116
+ vi.stubEnv("PI_SUBAGENT_SESSION", "session-xyz");
117
+ expect(
118
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
119
+ ).toBe(true);
120
+ });
121
+
122
+ test("returns true when PI_SUBAGENT_ACTIVITY_FILE is set", () => {
123
+ vi.stubEnv("PI_SUBAGENT_ACTIVITY_FILE", "/tmp/activity.json");
124
+ expect(
125
+ isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
126
+ ).toBe(true);
127
+ });
128
+
129
+ test("covers all declared SUBAGENT_ENV_HINT_KEYS", () => {
65
130
  // Verify the keys we test match what the module declares.
66
131
  expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_IS_SUBAGENT");
67
132
  expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_SESSION_ID");
68
133
  expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_AGENT_ROUTER_SUBAGENT");
134
+ // nicobailon/pi-subagents
135
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_CHILD");
136
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_RUN_ID");
137
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_CHILD_AGENT");
138
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_DEPTH");
139
+ // HazAT/pi-interactive-subagents
140
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_NAME");
141
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_ID");
142
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_SESSION");
143
+ expect(SUBAGENT_ENV_HINT_KEYS).toContain("PI_SUBAGENT_ACTIVITY_FILE");
69
144
  });
70
145
 
71
146
  test("returns false when env hint value is empty string", () => {