@cr1ms0n/pi-subagent 0.8.5 → 0.8.7

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.7 — 2026-09-14
6
+
7
+ ### Provider-safe tool schemas
8
+
9
+ - Strip internal TypeBox `~...` metadata from the `subagent` and `subagent_wait` schemas before Pi provider adapters receive them, while retaining the original schemas for local runtime validation.
10
+
11
+ ## 0.8.6 — 2026-09-14
12
+
13
+ ### Pi context management in child profiles
14
+
15
+ - Keep Pi context-management tools available to child tool allowlists without granting project-file write access, so configured gateway models such as `uwoacrimson/gpt-5.6-luna` and `uwoacrimson/gpt-5.6-sol` can use `contextManagement` from `explore` and `review` profiles.
16
+
3
17
  ## 0.8.5 — 2026-09-10
4
18
 
5
19
  ### Read-only FFF search tools
package/README.md CHANGED
@@ -191,11 +191,19 @@ Set a persona's backend in agent frontmatter with `backend: codex`.
191
191
 
192
192
  ## Profiles
193
193
 
194
- | Profile | Tools | Writes |
195
- | ---------------------------- | ------------------------------- | ------------------------------------ |
196
- | `explore` (parallel default) | read/grep/find/ls + safe extras | no |
197
- | `review` | same as explore | no |
198
- | `general` | inherited active tools | yes if tools include bash/edit/write |
194
+ | Profile | Tools | Writes |
195
+ | ---------------------------- | ------------------------------------------------------- | ------------------------------------------- |
196
+ | `explore` (parallel default) | read/grep/find/ls + safe extras + Pi context tools | no project-file writes |
197
+ | `review` | same as explore | no project-file writes |
198
+ | `general` | inherited active tools + Pi context tools | yes if tools include bash/edit/write |
199
+
200
+ For the Pi backend, the context-management tools `new_context`,
201
+ `get_context_remaining`, `history`, and `notes` are retained in child tool
202
+ allowlists when the parent exposes them. They are control-plane tools: they may
203
+ update continuity notes or the remote context window, but cannot modify the
204
+ child checkout or run a shell command. This exception also applies when a task
205
+ supplies a narrower tool list, so Pi's `contextManagement` remains usable for
206
+ configured gateway models.
199
207
 
200
208
  Parallel write-capable tasks sharing one checkout are rejected unless each uses
201
209
  `isolation: "worktree"`, distinct `cwd`, or explicit `allow_shared_writes: true`.
@@ -29,7 +29,7 @@
29
29
  - `persistence.ts`: versioned active-branch event folding and bounded child transcript metadata.
30
30
  - `maintenance.ts`: filesystem GC (session files) and abort-race helpers; kept out of persistence.
31
31
  - `usage.ts`: provider-reported root/subagent/combined accounting.
32
- - `policy.ts` / `schema.ts`: discriminated request validation and safe capability profiles.
32
+ - `policy.ts` / `schema.ts`: discriminated request validation and safe capability profiles. `schema.ts` retains the canonical TypeBox validators and derives provider-safe tool-schema projections; `extension.ts` registers those projections while validating calls with the originals. Pi context-management control-plane tools remain available to child allowlists without granting project-file write access.
33
33
  - `config.ts`: defaults ← `~/.pi/subagent.json` ← `PI_SUBAGENT_*` env overrides.
34
34
  - `structured.ts`: structured-output contract (dependency-free JSON-Schema subset
35
35
  validation, fenced json:result extraction, contract/repair prompts) and
package/docs/SECURITY.md CHANGED
@@ -8,16 +8,19 @@ and can use tools according to their capability profile.
8
8
 
9
9
  | Profile | Default tools | Writes? |
10
10
  |---------|---------------|---------|
11
- | `explore` | `read`, `grep`, `find`, `ls` (+ explicit read-only extras) | No |
12
- | `review` | same as explore | No |
13
- | `general` | inherited active tools (may include `bash`/`edit`/`write`) | Yes |
11
+ | `explore` | `read`, `grep`, `find`, `ls` (+ safe extras and Pi context tools) | No project-file writes |
12
+ | `review` | same as explore | No project-file writes |
13
+ | `general` | inherited active tools (+ Pi context tools) | Yes if `bash`/`edit`/`write` are active |
14
14
 
15
15
  Parallel mode defaults to `explore` to avoid concurrent shared writes.
16
16
 
17
17
  ## Hard rules
18
18
 
19
- 1. **Read-only means no `bash`.** `bash` can rewrite the disc and is never part of
20
- an explore/review profile.
19
+ 1. **Read-only means no project-file mutation.** `bash` can rewrite the disk and is never part of
20
+ an explore/review profile. Pi context-management tools (`new_context`,
21
+ `get_context_remaining`, `history`, `notes`) are an explicit control-plane
22
+ exception: they may update continuity notes/window state but cannot access
23
+ the project write tools.
21
24
  2. **Parallel writers** require `isolation: "worktree"`, distinct `cwd` values,
22
25
  or an explicit `allow_shared_writes: true` opt-in.
23
26
  3. **Depth is capped** (`maxDepth`, default 2). Nested children at the ceiling do
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cr1ms0n/pi-subagent",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "description": "Community fork of Luke Parke's pi-subagent with explicit model policy and model visibility for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -57,11 +57,17 @@ const routeModel = "<exact model from current modelPolicy route>";
57
57
 
58
58
  ## Profiles
59
59
 
60
- | Profile | Tools | Writes |
61
- | --------- | ---------------------- | ------------------------------------------- |
62
- | `explore` | read/search/ls (+safe) | no |
63
- | `review` | same as explore | no |
64
- | `general` | inherited active tools | yes if tools include bash/edit/write |
60
+ | Profile | Tools | Writes |
61
+ | --------- | --------------------------------------------------------- | ------------------------------------------- |
62
+ | `explore` | read/search/ls (+safe) + Pi context tools | no project-file writes |
63
+ | `review` | same as explore | no project-file writes |
64
+ | `general` | inherited active tools + Pi context tools | yes if tools include bash/edit/write |
65
+
66
+ For Pi children, `new_context`, `get_context_remaining`, `history`, and
67
+ `notes` are control-plane tools. When available in the parent they remain in
68
+ the child allowlist—even if a narrower tool list was requested—so Pi's remote
69
+ `contextManagement` can stay active. They may update context notes/window
70
+ state, but never grant `bash`, `edit`, or `write` access.
65
71
 
66
72
  Parallel write-capable tasks sharing one checkout are rejected unless each uses
67
73
  `isolation: "worktree"`, a distinct `cwd`, or `allow_shared_writes: true`.
package/src/extension.ts CHANGED
@@ -24,11 +24,18 @@ import { abortAsPromise } from "./maintenance.js";
24
24
  import { sweepSessionsLifecycle } from "./distill.js";
25
25
  import { runTasks } from "./orchestrator.js";
26
26
  import { OutputManager } from "./output.js";
27
- import { parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type ResolvedTask } from "./policy.js";
27
+ import { CONTEXT_MANAGEMENT_TOOLS, parseDepth, parseSpawnPolicy, SPAWNS_ENV_VAR, validateSubagentRequest, type ResolvedTask } from "./policy.js";
28
28
  import type { ChildRunner } from "./runner.js";
29
29
  import { ProcessLockManager } from "./process-lock.js";
30
30
  import { SessionScopedRunRegistry, snapshotFromLiveRun } from "./registry.js";
31
- import { SubagentParamsSchema, SubagentWaitParamsSchema, type SubagentParams, type SubagentWaitParams } from "./schema.js";
31
+ import {
32
+ ProviderSubagentParamsSchema,
33
+ ProviderSubagentWaitParamsSchema,
34
+ SubagentParamsSchema,
35
+ SubagentWaitParamsSchema,
36
+ type SubagentParams,
37
+ type SubagentWaitParams,
38
+ } from "./schema.js";
32
39
  import { BTW_ENTRY_TYPE, btwLabel, type BtwEntry } from "./btw.js";
33
40
  import { Semaphore } from "./semaphore.js";
34
41
  import type { RunSnapshot, TaskResult, TaskSpec, UsageStats } from "./types.js";
@@ -563,7 +570,7 @@ async function runSynthesis(
563
570
  label: "synthesis",
564
571
  profile: "review",
565
572
  canWrite: false,
566
- tools: ["read"],
573
+ tools: ["read", ...CONTEXT_MANAGEMENT_TOOLS],
567
574
  model: approved.route.model,
568
575
  fallbackModels: [...approved.route.fallbackModels],
569
576
  thinking: approved.route.thinking ?? "low",
@@ -944,7 +951,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
944
951
  // usable immediately via agent:'name' (execute-time refresh); only the
945
952
  // system-prompt advertisement waits for the next session.
946
953
  promptGuidelines: guidelines(discoverAgents(process.cwd())),
947
- parameters: SubagentParamsSchema,
954
+ parameters: ProviderSubagentParamsSchema,
948
955
  async execute(_id, params: SubagentParams, signal, onUpdate, ctx) {
949
956
  // `subagent_wait` delegates here with a synthesized action:"wait" params
950
957
  // object, carrying its timeout through this non-schema field so the two
@@ -1507,7 +1514,7 @@ export default function registerSubagent(pi: ExtensionAPI): void {
1507
1514
  label: "Subagent wait",
1508
1515
  description:
1509
1516
  "Block until a background subagent run (async:true) settles, then deliver its output. Equivalent to subagent { action: 'wait', id }. Aborting or timing out leaves the run alive and collectable; use subagent { action: 'cancel' } to stop it.",
1510
- parameters: SubagentWaitParamsSchema,
1517
+ parameters: ProviderSubagentWaitParamsSchema,
1511
1518
  async execute(id, params: SubagentWaitParams, signal, onUpdate, ctx) {
1512
1519
  return subagentTool.execute(
1513
1520
  id,
package/src/policy.ts CHANGED
@@ -27,6 +27,19 @@ export const READ_ONLY_TOOLS = new Set([
27
27
  "web_search",
28
28
  "web_fetch",
29
29
  ]);
30
+ /**
31
+ * Pi context-management tools are control-plane capabilities: they may update
32
+ * continuity notes or the remote context window, but they cannot modify the
33
+ * child checkout. Keep them separate from ordinary source-inspection tools so
34
+ * the read-only profile's exception remains explicit.
35
+ */
36
+ export const CONTEXT_MANAGEMENT_TOOLS = new Set([
37
+ "new_context",
38
+ "get_context_remaining",
39
+ "history",
40
+ "notes",
41
+ ]);
42
+ const NON_WRITING_TOOLS = new Set([...READ_ONLY_TOOLS, ...CONTEXT_MANAGEMENT_TOOLS]);
30
43
  export const KNOWN_WRITE_TOOLS = new Set(["bash", "edit", "write"]);
31
44
  /** Backward-compatible export; policy uses fail-closed classification above. */
32
45
  export const WRITE_TOOLS = KNOWN_WRITE_TOOLS;
@@ -75,31 +88,41 @@ function resolveTools(
75
88
  requested: string[] | undefined,
76
89
  availableTools: string[],
77
90
  activeTools: string[],
91
+ backend: BackendName,
78
92
  ): { tools?: string[]; canWrite?: boolean; error?: string } {
79
93
  const available = new Set(availableTools);
94
+ const contextTools = backend === "pi"
95
+ ? [...CONTEXT_MANAGEMENT_TOOLS].filter((tool) => available.has(tool))
96
+ : [];
97
+ const nonWritingTools = backend === "pi" ? NON_WRITING_TOOLS : READ_ONLY_TOOLS;
98
+ // Keep Pi's context-management control plane available to every child when
99
+ // the parent exposes it, even if the task requested a narrower tool subset.
100
+ const addContextTools = (tools: readonly string[]): string[] =>
101
+ [...new Set([...tools, ...contextTools])];
102
+
80
103
  if (requested) {
81
104
  const unknown = requested.filter((tool) => !available.has(tool));
82
105
  if (unknown.length) return { error: `Unknown or unavailable tools: ${unknown.join(", ")}` };
83
106
  }
84
107
 
85
108
  if (profile === "explore" || profile === "review") {
86
- const source = requested ?? [...READ_ONLY_TOOLS].filter((tool) => available.has(tool));
87
- const unsafe = source.filter((tool) => !READ_ONLY_TOOLS.has(tool));
109
+ const source = addContextTools(requested ?? [...nonWritingTools].filter((tool) => available.has(tool)));
110
+ const unsafe = source.filter((tool) => !nonWritingTools.has(tool));
88
111
  if (unsafe.length) {
89
112
  return {
90
113
  error: `${profile} is strictly read-only. Unclassified or writable tools are not allowed: ${unsafe.join(", ")}`,
91
114
  };
92
115
  }
93
- return { tools: [...new Set(source)], canWrite: false };
116
+ return { tools: source, canWrite: false };
94
117
  }
95
118
 
96
- const source = requested ?? activeTools;
119
+ const source = addContextTools(requested ?? activeTools);
97
120
  const unknown = source.filter((tool) => !available.has(tool));
98
121
  if (unknown.length) return { error: `Active tools are unavailable: ${unknown.join(", ")}` };
99
- // General-profile custom tools are conservatively write-capable unless explicitly known read-only.
122
+ // General-profile custom tools are conservatively write-capable unless explicitly known non-writing.
100
123
  return {
101
- tools: [...new Set(source)],
102
- canWrite: source.some((tool) => KNOWN_WRITE_TOOLS.has(tool) || !READ_ONLY_TOOLS.has(tool)),
124
+ tools: source,
125
+ canWrite: source.some((tool) => KNOWN_WRITE_TOOLS.has(tool) || !nonWritingTools.has(tool)),
103
126
  };
104
127
  }
105
128
 
@@ -193,9 +216,13 @@ function normalizeTask(
193
216
  }
194
217
  }
195
218
 
219
+ const backend: BackendName = item.backend ?? agent?.backend ?? "pi";
220
+ if (!BACKEND_NAMES.includes(backend)) {
221
+ return { error: `Task ${index + 1}: unknown backend '${backend}' (expected ${BACKEND_NAMES.join(", ")})` };
222
+ }
196
223
  const profile = item.profile ?? agent?.profile ?? defaultProfile;
197
224
  const requestedTools = item.tools ?? agent?.tools;
198
- const resolved = resolveTools(profile, requestedTools, parent.availableTools, parent.activeTools ?? parent.availableTools);
225
+ const resolved = resolveTools(profile, requestedTools, parent.availableTools, parent.activeTools ?? parent.availableTools, backend);
199
226
  if (resolved.error || !resolved.tools || resolved.canWrite === undefined) return { error: resolved.error ?? "Tool resolution failed" };
200
227
  const cwd = resolvePath(parent.cwd, item.cwd);
201
228
  const output = item.output ? resolvePath(cwd, item.output) : undefined;
@@ -216,10 +243,6 @@ function normalizeTask(
216
243
 
217
244
  // Backend capability gate. Refuse combinations the backend cannot honor
218
245
  // rather than silently dropping a budget or a read-only guarantee.
219
- const backend: BackendName = item.backend ?? agent?.backend ?? "pi";
220
- if (!BACKEND_NAMES.includes(backend)) {
221
- return { error: `Task ${index + 1}: unknown backend '${backend}' (expected ${BACKEND_NAMES.join(", ")})` };
222
- }
223
246
  const capabilities = resolveBackend(backend).capabilities;
224
247
  const problems = checkCapabilities(
225
248
  {
package/src/schema.ts CHANGED
@@ -1,5 +1,27 @@
1
1
  import { Type, type Static } from "typebox";
2
2
 
3
+ /**
4
+ * Remove TypeBox's internal compositor metadata before a schema crosses into
5
+ * Pi's provider-facing tool catalog. The original schema is kept intact for
6
+ * local Value.Check/Value.Errors validation.
7
+ */
8
+ export function sanitizeProviderSchema<T>(value: T): T {
9
+ if (Array.isArray(value)) {
10
+ return value.map((item) => sanitizeProviderSchema(item)) as T;
11
+ }
12
+
13
+ if (value && typeof value === "object") {
14
+ const record = value as Record<string, unknown>;
15
+ return Object.fromEntries(
16
+ Object.entries(record)
17
+ .filter(([key]) => !key.startsWith("~"))
18
+ .map(([key, entry]) => [key, sanitizeProviderSchema(entry)] as const),
19
+ ) as T;
20
+ }
21
+
22
+ return value;
23
+ }
24
+
3
25
  const ThinkingLevel = Type.String({
4
26
  minLength: 1,
5
27
  maxLength: 64,
@@ -29,8 +51,8 @@ export const TaskFields = {
29
51
  system_prompt: Type.Optional(Type.String({ description: "Extra system prompt appended to the child's prompt (does not replace it)." })),
30
52
  model: Type.Optional(Type.String({ description: "**REQUIRED for every spawn call (task/tasks).** Exact model id from modelPolicy, in provider/model-id form. Calls without an explicit model are rejected; agent-file model, taskDefaults, and parent-session inheritance are ignored. Management actions (status/wait/cancel/steer/diff/apply/discard) do not need it." })),
31
53
  thinking: Type.Optional({ ...ThinkingLevel, description: "Opaque Pi thinking level for the child. Values such as max are passed through unchanged; Pi/model support decides validity. Defaults to agent thinking, profile taskDefaults.thinking, modelPolicy route thinking, then the parent's level." }),
32
- tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject write-capable tools." })),
33
- profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review are strictly read-only; general inherits the parent's active tools and may write." }),
54
+ tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool allowlist. explore/review profiles reject project-writing tools; Pi context-management tools remain available for context continuity." })),
55
+ profile: Type.Optional({ ...Profile, description: "Capability profile: explore/review cannot write project files but retain Pi context-management tools; general inherits the parent's active tools and may write." }),
34
56
  cwd: Type.Optional(Type.String({ description: "Working directory for the child process." })),
35
57
  timeout_ms: Type.Optional(Type.Number({ minimum: 1, maximum: 24 * 60 * 60_000, description: "Total budget in milliseconds including queue time. Timed-out runs report which phase timed out." })),
36
58
  max_turns: Type.Optional(Type.Number({ minimum: 1, maximum: 500, description: "Budget: at this many turns the child is steered to wrap up and given grace turns for a final answer; ends as 'partial' with output preserved." })),
@@ -152,6 +174,10 @@ export const SubagentWaitParamsSchema = Type.Object(
152
174
  },
153
175
  );
154
176
 
177
+ /** Provider-facing projections without TypeBox's internal `~...` metadata. */
178
+ export const ProviderSubagentParamsSchema = sanitizeProviderSchema(SubagentParamsSchema);
179
+ export const ProviderSubagentWaitParamsSchema = sanitizeProviderSchema(SubagentWaitParamsSchema);
180
+
155
181
  export type SubagentWaitParams = Static<typeof SubagentWaitParamsSchema>;
156
182
 
157
183
  /** Runtime guard used by tests/docs to assert provider compatibility. */