pi-sage 0.2.2 → 0.2.4

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.
@@ -335,6 +335,7 @@ function buildCallerContext(lastInputSource: InputSource | undefined, hasUI: boo
335
335
  const isRpcSource = lastInputSource === "rpc";
336
336
  const isSubagent = process.env.PI_SAGE_SUBAGENT === "1";
337
337
  const interactive = hasUI && lastInputSource === "interactive";
338
+ const isInteractiveSupervisor = interactive && roleHint === "supervisor";
338
339
 
339
340
  return {
340
341
  session: {
@@ -343,7 +344,7 @@ function buildCallerContext(lastInputSource: InputSource | undefined, hasUI: boo
343
344
  agent: {
344
345
  role: roleHint ?? "primary",
345
346
  isSubagent,
346
- isRpcOrchestrated: isRpcSource || Boolean(roleHint && roleHint !== "primary")
347
+ isRpcOrchestrated: isRpcSource || Boolean(roleHint && roleHint !== "primary" && !isInteractiveSupervisor)
347
348
  },
348
349
  runtime: {
349
350
  mode: process.env.CI ? "ci" : hasUI ? (isRpcSource ? "rpc" : "interactive") : "non-interactive"
@@ -22,11 +22,17 @@ export function isEligibleCaller(ctx: CallerContext | null | undefined): CallerD
22
22
  return { ok: false, blockCode: "subagent", reason: "Sage disabled for subagents" };
23
23
  }
24
24
 
25
- if (ctx.agent.role !== "primary") {
25
+ const interactiveSupervisor =
26
+ ctx.agent.role === "supervisor" && ctx.session.interactive === true && ctx.runtime.mode === "interactive";
27
+
28
+ if (ctx.agent.role !== "primary" && !interactiveSupervisor) {
26
29
  return { ok: false, blockCode: "ineligible-caller", reason: "Only primary agent may invoke Sage" };
27
30
  }
28
31
 
29
- return { ok: true, reason: "eligible" };
32
+ return {
33
+ ok: true,
34
+ reason: interactiveSupervisor ? "eligible (interactive supervisor)" : "eligible"
35
+ };
30
36
  }
31
37
 
32
38
  export function isHardCostCapExceeded(settings: SageSettings, budgetState: SageBudgetState): boolean {
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { EOL } from "node:os";
3
- import { isAbsolute, resolve } from "node:path";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { EOL, tmpdir } from "node:os";
4
+ import { isAbsolute, join, resolve } from "node:path";
4
5
  import { isPathAllowed, resolveToolPolicy, validateBashCommandForProfile } from "./tool-policy.js";
5
6
  import type { ToolPolicySettings } from "./settings.js";
6
7
  import type {
@@ -76,8 +77,11 @@ export async function runSageSingleShot(input: SageRunnerInput): Promise<SageRun
76
77
  const startedAt = Date.now();
77
78
 
78
79
  const invocation = resolvePiInvocation();
79
- const args = [...invocation.prefixArgs, ...buildPiArgs(input.model, input.reasoningLevel, policy.cliTools)];
80
80
  const prompt = buildSagePrompt(input);
81
+ const promptDir = await mkdtemp(join(tmpdir(), "pi-sage-"));
82
+ const promptPath = join(promptDir, "prompt.txt");
83
+ await writeFile(promptPath, prompt, "utf8");
84
+ const args = [...invocation.prefixArgs, ...buildPiArgs(input.model, input.reasoningLevel, policy.cliTools), `@${promptPath}`];
81
85
 
82
86
  const child = spawn(invocation.command, args, {
83
87
  cwd: input.cwd,
@@ -86,9 +90,16 @@ export async function runSageSingleShot(input: SageRunnerInput): Promise<SageRun
86
90
  PI_SAGE_SUBAGENT: "1"
87
91
  },
88
92
  shell: invocation.shell,
89
- stdio: ["pipe", "pipe", "pipe"]
93
+ stdio: ["ignore", "pipe", "pipe"]
90
94
  });
91
95
 
96
+ const cleanupPromptFile = (): void => {
97
+ void rm(promptDir, { recursive: true, force: true });
98
+ };
99
+
100
+ child.once("close", cleanupPromptFile);
101
+ child.once("error", cleanupPromptFile);
102
+
92
103
  let stdoutBuffer = "";
93
104
  let stderrBuffer = "";
94
105
  let assistantText = "";
@@ -112,12 +123,6 @@ export async function runSageSingleShot(input: SageRunnerInput): Promise<SageRun
112
123
  child.stdout.setEncoding("utf8");
113
124
  child.stderr.setEncoding("utf8");
114
125
 
115
- if (child.stdin) {
116
- child.stdin.setDefaultEncoding("utf8");
117
- child.stdin.write(prompt);
118
- child.stdin.end();
119
- }
120
-
121
126
  child.stdout.on("data", (chunk: string) => {
122
127
  stdoutBuffer += chunk;
123
128
  const lines = stdoutBuffer.split(/\r?\n/);
package/AGENTS.md CHANGED
@@ -11,7 +11,7 @@ Sage should improve decision quality for complex tasks while preserving strict s
11
11
  ## 2) Product Invariants (Do Not Violate)
12
12
 
13
13
  1. Sage invocation is restricted to **interactive top-level primary** sessions.
14
- 2. RPC-orchestrated roles (`supervisor`, `worker`, `reviewer`, `merger`) cannot invoke Sage.
14
+ 2. RPC-orchestrated roles cannot invoke Sage. Exception: an interactive top-level `supervisor` context may invoke Sage when treated as the user-facing primary session.
15
15
  3. Sage is **single-shot** per call.
16
16
  4. Sage cannot recursively invoke Sage.
17
17
  5. Sage is **advisory-only** (analysis/recommendations, not implementation execution).
package/docs/SAGE_SPEC.md CHANGED
@@ -182,8 +182,8 @@ Primary agent guidance to append:
182
182
  - `autonomousEnabled: boolean` (applies only in eligible interactive primary sessions; default: true there)
183
183
  - `explicitRequestAlwaysAllowed: boolean` (default: true)
184
184
  - `invocationScope: "interactive-primary-only"` (default and recommended)
185
- - `sage_consult` is callable only from the top-level interactive primary agent session.
186
- - `sage_consult` is blocked for non-interactive/CI contexts and RPC-orchestrated agent roles (e.g., supervisor, worker, reviewer, merger).
185
+ - `sage_consult` is callable only from top-level interactive user-facing sessions.
186
+ - `sage_consult` is blocked for non-interactive/CI contexts and RPC-orchestrated roles. Exception: interactive top-level `supervisor` may be treated as primary.
187
187
 
188
188
  ## 8.2 Soft limits (defaults)
189
189
  Soft limits are policy/budget controls and are bypassable for explicit user requests (`force=true`).
@@ -213,7 +213,7 @@ Implement a deterministic `isEligibleCaller(context)` check before any policy/bu
213
213
 
214
214
  Required conditions (all must be true):
215
215
  1. `context.session.interactive === true`
216
- 2. `context.agent.role === "primary"`
216
+ 2. `context.agent.role === "primary"` OR (`context.agent.role === "supervisor"` in interactive top-level context)
217
217
  3. `context.agent.isSubagent !== true`
218
218
  4. `context.agent.isRpcOrchestrated !== true`
219
219
  5. `context.runtime.mode !== "ci"`
@@ -241,20 +241,23 @@ function isEligibleCaller(ctx: CallerContext): { ok: boolean; blockCode?: string
241
241
  if (ctx.agent.isSubagent === true) {
242
242
  return { ok: false, blockCode: "subagent", reason: "Sage disabled for subagents" };
243
243
  }
244
- if (ctx.agent.role !== "primary") {
244
+ const interactiveSupervisor =
245
+ ctx.agent.role === "supervisor" && ctx.session.interactive === true && ctx.runtime.mode === "interactive";
246
+ if (ctx.agent.role !== "primary" && !interactiveSupervisor) {
245
247
  return { ok: false, blockCode: "ineligible-caller", reason: "Only primary agent may invoke Sage" };
246
248
  }
247
- return { ok: true, reason: "eligible" };
249
+ return { ok: true, reason: interactiveSupervisor ? "eligible (interactive supervisor)" : "eligible" };
248
250
  }
249
251
  ```
250
252
 
251
253
  ## 8.7 RPC role mapping guidance (orchestration frameworks)
252
- For orchestrated multi-agent frameworks, map caller role metadata so these roles are always ineligible:
253
- - `supervisor`
254
+ For orchestrated multi-agent frameworks, map caller role metadata so these roles are ineligible by default:
254
255
  - `worker`
255
256
  - `reviewer`
256
257
  - `merger`
257
258
 
259
+ `supervisor` is ineligible unless it is the interactive top-level user-facing session.
260
+
258
261
  If runtime only provides a session name/string, derive role via conservative matching and deny on ambiguity.
259
262
 
260
263
  ---
@@ -372,7 +375,7 @@ On tool-policy/data-policy block:
372
375
 
373
376
  1. Primary agent autonomously invokes Sage at least once in a complex debug scenario without user explicitly asking (interactive top-level primary session).
374
377
  2. In CI/non-interactive mode, Sage invocation is blocked (non-bypassable caller-scope gate).
375
- 3. RPC-orchestrated agents (supervisor/worker/reviewer/merger) cannot invoke `sage_consult`.
378
+ 3. RPC-orchestrated agents cannot invoke `sage_consult` (interactive top-level `supervisor` exception).
376
379
  4. User phrase “get a second opinion” in an eligible interactive primary session causes at least one Sage consultation in same task flow.
377
380
  5. Explicit user-requested Sage consultation can bypass soft limits, but still respects hard safety limits and caller-scope restrictions.
378
381
  6. No `/sage` command exists.
@@ -410,7 +413,7 @@ On tool-policy/data-policy block:
410
413
  6. Add system-prompt guidance injection for autonomous invocation.
411
414
  7. Add budget gates and policy telemetry.
412
415
  8. Add rendering polish and acceptance tests.
413
- 9. Add a caller-context + tool-policy test matrix (interactive primary allowed; CI blocked; supervisor/worker/reviewer/merger blocked; subagent blocked; unknown context blocked; disallowed tool attempts blocked).
416
+ 9. Add a caller-context + tool-policy test matrix (interactive primary allowed; interactive supervisor allowed; CI blocked; rpc-orchestrated roles blocked; subagent blocked; unknown context blocked; disallowed tool attempts blocked).
414
417
 
415
418
  ---
416
419
 
@@ -470,12 +473,12 @@ On tool-policy/data-policy block:
470
473
  - [ ] Show effective scope/source (project/global/default) and save target.
471
474
 
472
475
  ### 17.6 Test checklist
473
- - [ ] Unit: `isEligibleCaller` allows only interactive primary; blocks CI, non-interactive, subagent, rpc-role, unknown context.
476
+ - [ ] Unit: `isEligibleCaller` allows interactive primary (and interactive supervisor exception); blocks CI, non-interactive, subagent, rpc-role, unknown context.
474
477
  - [ ] Unit: `resolveToolPolicy` defaults to `read-only-lite` and strips/blocks disallowed custom tools.
475
478
  - [ ] Unit: `isPathAllowed` blocks denylisted paths and non-workspace paths.
476
479
  - [ ] Unit: `checkVolumeCaps` trips correctly on call/file/byte overages.
477
480
  - [ ] Integration: explicit user request with `force=true` bypasses soft limits but not caller gate.
478
- - [ ] Integration: RPC roles (`supervisor`, `worker`, `reviewer`, `merger`) receive structured blocked result.
481
+ - [ ] Integration: RPC-orchestrated roles receive structured blocked result (interactive supervisor exception).
479
482
  - [ ] Integration: Sage subprocess cannot call `sage_consult` (recursion test).
480
483
  - [ ] Integration: tool usage metadata is present in successful responses.
481
484
  - [ ] Integration: blocked results include `blockCode` + human-readable reason.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sage",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Interactive-only advisory Sage extension for Pi",