pi-sage 0.2.3 → 0.2.5

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.
@@ -39,6 +39,7 @@ const ROLE_HINT_ENV_KEYS = ["TASKPLANE_ROLE", "TASKPLANE_AGENT_ROLE", "PI_AGENT_
39
39
 
40
40
  const REASONING_LEVELS: ReasoningLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
41
41
  const TOOL_PROFILES: ToolProfile[] = ["none", "read-only-lite", "git-review-readonly", "custom-read-only"];
42
+ const MODEL_INHERIT_VALUE = "inherit";
42
43
 
43
44
  const SAGE_GUIDANCE = [
44
45
  "You may call `sage_consult` for high-complexity reasoning, ambiguous debugging, risky architectural decisions, or explicit second-opinion requests.",
@@ -335,6 +336,7 @@ function buildCallerContext(lastInputSource: InputSource | undefined, hasUI: boo
335
336
  const isRpcSource = lastInputSource === "rpc";
336
337
  const isSubagent = process.env.PI_SAGE_SUBAGENT === "1";
337
338
  const interactive = hasUI && lastInputSource === "interactive";
339
+ const isInteractiveSupervisor = interactive && roleHint === "supervisor";
338
340
 
339
341
  return {
340
342
  session: {
@@ -343,7 +345,7 @@ function buildCallerContext(lastInputSource: InputSource | undefined, hasUI: boo
343
345
  agent: {
344
346
  role: roleHint ?? "primary",
345
347
  isSubagent,
346
- isRpcOrchestrated: isRpcSource || Boolean(roleHint && roleHint !== "primary")
348
+ isRpcOrchestrated: isRpcSource || Boolean(roleHint && roleHint !== "primary" && !isInteractiveSupervisor)
347
349
  },
348
350
  runtime: {
349
351
  mode: process.env.CI ? "ci" : hasUI ? (isRpcSource ? "rpc" : "interactive") : "non-interactive"
@@ -373,12 +375,15 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
373
375
  const action = await ctx.ui.select("Sage settings", [
374
376
  `Enabled: ${onOff(draft.enabled)}`,
375
377
  `Autonomous mode: ${onOff(draft.autonomousEnabled)}`,
378
+ `Explicit requests bypass soft limits: ${onOff(draft.explicitRequestAlwaysAllowed)}`,
376
379
  `Model: ${draft.model}`,
377
380
  `Reasoning level: ${draft.reasoningLevel}`,
378
381
  `Timeout ms: ${draft.timeoutMs}`,
379
382
  `Max calls/turn: ${draft.maxCallsPerTurn}`,
380
383
  `Max calls/session: ${draft.maxCallsPerSession}`,
381
384
  `Cooldown turns: ${draft.cooldownTurnsBetweenAutoCalls}`,
385
+ `Max question chars: ${draft.maxQuestionChars}`,
386
+ `Max context chars: ${draft.maxContextChars}`,
382
387
  `Tool profile: ${draft.toolPolicy.profile}`,
383
388
  `Custom tools: ${(draft.toolPolicy.customAllowedTools ?? []).join(",") || "(none)"}`,
384
389
  `Max tool calls: ${draft.toolPolicy.maxToolCalls ?? 10}`,
@@ -418,8 +423,12 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
418
423
  draft = { ...draft, autonomousEnabled: !draft.autonomousEnabled };
419
424
  continue;
420
425
  }
426
+ if (action.startsWith("Explicit requests bypass soft limits:")) {
427
+ draft = { ...draft, explicitRequestAlwaysAllowed: !draft.explicitRequestAlwaysAllowed };
428
+ continue;
429
+ }
421
430
  if (action.startsWith("Model:")) {
422
- const selected = await pickModel(ctx);
431
+ const selected = await pickModel(ctx, draft.model);
423
432
  if (selected) draft = { ...draft, model: selected };
424
433
  continue;
425
434
  }
@@ -446,6 +455,14 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
446
455
  draft = await setNumberSetting(ctx, draft, "cooldownTurnsBetweenAutoCalls", "Cooldown turns", 0);
447
456
  continue;
448
457
  }
458
+ if (action.startsWith("Max question chars:")) {
459
+ draft = await setNumberSetting(ctx, draft, "maxQuestionChars", "Max question chars", 256);
460
+ continue;
461
+ }
462
+ if (action.startsWith("Max context chars:")) {
463
+ draft = await setNumberSetting(ctx, draft, "maxContextChars", "Max context chars", 512);
464
+ continue;
465
+ }
449
466
  if (action.startsWith("Tool profile:")) {
450
467
  const selected = await ctx.ui.select("Tool profile", [...TOOL_PROFILES]);
451
468
  if (selected && TOOL_PROFILES.includes(selected as ToolProfile)) {
@@ -520,23 +537,63 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
520
537
  }
521
538
  }
522
539
 
523
- async function pickModel(ctx: ExtensionContext): Promise<string | undefined> {
540
+ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<string | undefined> {
524
541
  const available = ctx.modelRegistry.getAvailable();
525
542
  if (available.length === 0) {
526
543
  ctx.ui.notify("No available models found", "warning");
527
544
  return undefined;
528
545
  }
529
546
 
530
- const sorted = [...available].sort((a, b) => {
531
- const left = `${a.provider}/${a.id}`.toLowerCase();
532
- const right = `${b.provider}/${b.id}`.toLowerCase();
533
- return left.localeCompare(right);
534
- });
547
+ const currentLower = currentModel.trim().toLowerCase();
548
+ const providers = [...new Set(available.map((model) => model.provider))].sort((a, b) => a.localeCompare(b));
549
+
550
+ while (true) {
551
+ const providerOptions = [
552
+ `${MODEL_INHERIT_VALUE} (use current session model)`,
553
+ ...providers.map((provider) => {
554
+ const count = available.filter((model) => model.provider === provider).length;
555
+ return `${provider} (${count})`;
556
+ })
557
+ ];
558
+
559
+ const providerChoice = await ctx.ui.select("Choose Sage model provider", providerOptions);
560
+ if (!providerChoice) return undefined;
561
+
562
+ if (providerChoice.startsWith(MODEL_INHERIT_VALUE)) {
563
+ return MODEL_INHERIT_VALUE;
564
+ }
565
+
566
+ const provider = providerChoice.replace(/\s*\(\d+\)$/, "");
567
+ const providerModels = available
568
+ .filter((model) => model.provider === provider)
569
+ .sort((a, b) => {
570
+ const aComposite = `${a.provider}/${a.id}`.toLowerCase();
571
+ const bComposite = `${b.provider}/${b.id}`.toLowerCase();
572
+ const aCurrent = aComposite === currentLower;
573
+ const bCurrent = bComposite === currentLower;
574
+ if (aCurrent && !bCurrent) return -1;
575
+ if (!aCurrent && bCurrent) return 1;
576
+ return a.id.localeCompare(b.id);
577
+ });
535
578
 
536
- const options = sorted.slice(0, 200).map((model) => `${model.provider}/${model.id}${model.name ? ` — ${model.name}` : ""}`);
537
- const selected = await ctx.ui.select("Choose Sage model", options);
538
- if (!selected) return undefined;
539
- return selected.split(" ")[0];
579
+ const modelOptionMap = new Map<string, string>();
580
+ const modelOptions = [" Back to providers"];
581
+
582
+ for (const model of providerModels) {
583
+ const composite = `${model.provider}/${model.id}`;
584
+ const isCurrent = composite.toLowerCase() === currentLower;
585
+ const option = `${model.id}${isCurrent ? " ✓ current" : ""}`;
586
+ modelOptions.push(option);
587
+ modelOptionMap.set(option, composite);
588
+ }
589
+
590
+ const modelChoice = await ctx.ui.select(`Choose Sage model (${provider})`, modelOptions);
591
+ if (!modelChoice) return undefined;
592
+ if (modelChoice === "← Back to providers") continue;
593
+
594
+ const resolved = modelOptionMap.get(modelChoice);
595
+ if (resolved) return resolved;
596
+ }
540
597
  }
541
598
 
542
599
  function resolveModelSpec(
@@ -548,6 +605,22 @@ function resolveModelSpec(
548
605
  return { modelArg: undefined, reason: "no available models" };
549
606
  }
550
607
 
608
+ if (configuredModel.trim().toLowerCase() === MODEL_INHERIT_VALUE) {
609
+ if (currentModel) {
610
+ const present = availableModels.find(
611
+ (model) => model.provider === currentModel.provider && model.id === currentModel.id
612
+ );
613
+ if (present) {
614
+ return { modelArg: `${present.provider}/${present.id}`, reason: "inherit current model" };
615
+ }
616
+ }
617
+
618
+ const firstAvailable = availableModels.at(0);
619
+ return firstAvailable
620
+ ? { modelArg: `${firstAvailable.provider}/${firstAvailable.id}`, reason: "inherit fallback first available" }
621
+ : { modelArg: undefined, reason: "no available models" };
622
+ }
623
+
551
624
  const exactConfigured = availableModels.find((model) => `${model.provider}/${model.id}` === configuredModel);
552
625
  if (exactConfigured) {
553
626
  return { modelArg: `${exactConfigured.provider}/${exactConfigured.id}`, reason: "configured exact match" };
@@ -596,7 +669,13 @@ function onOff(value: boolean): string {
596
669
  async function setNumberSetting(
597
670
  ctx: ExtensionContext,
598
671
  settings: SageSettings,
599
- key: "timeoutMs" | "maxCallsPerTurn" | "maxCallsPerSession" | "cooldownTurnsBetweenAutoCalls",
672
+ key:
673
+ | "timeoutMs"
674
+ | "maxCallsPerTurn"
675
+ | "maxCallsPerSession"
676
+ | "cooldownTurnsBetweenAutoCalls"
677
+ | "maxQuestionChars"
678
+ | "maxContextChars",
600
679
  title: string,
601
680
  min: number
602
681
  ): Promise<SageSettings> {
@@ -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 {
@@ -46,18 +46,18 @@ export const DEFAULT_SAGE_SETTINGS: SageSettings = {
46
46
  autonomousEnabled: true,
47
47
  explicitRequestAlwaysAllowed: true,
48
48
  invocationScope: "interactive-primary-only",
49
- model: "anthropic/claude-opus-4-6",
49
+ model: "inherit",
50
50
  reasoningLevel: "high",
51
- timeoutMs: 120000,
51
+ timeoutMs: 720000,
52
52
  maxCallsPerTurn: 1,
53
- maxCallsPerSession: 6,
53
+ maxCallsPerSession: 100000,
54
54
  cooldownTurnsBetweenAutoCalls: 1,
55
- maxQuestionChars: 6000,
56
- maxContextChars: 20000,
55
+ maxQuestionChars: 12000,
56
+ maxContextChars: 64000,
57
57
  toolPolicy: {
58
- profile: "read-only-lite",
59
- maxToolCalls: 10,
60
- maxFilesRead: 8,
58
+ profile: "git-review-readonly",
59
+ maxToolCalls: 250,
60
+ maxFilesRead: 100,
61
61
  maxBytesPerFile: 200 * 1024,
62
62
  maxTotalBytesRead: 1024 * 1024,
63
63
  sensitivePathDenylist: DEFAULT_DENYLIST
@@ -82,6 +82,14 @@ function normalizeNumber(value: unknown): number | undefined {
82
82
  return value;
83
83
  }
84
84
 
85
+ function normalizeModelSetting(value: unknown): string | undefined {
86
+ if (typeof value !== "string") return undefined;
87
+ const trimmed = value.trim();
88
+ if (!trimmed) return undefined;
89
+ if (trimmed.toLowerCase() === "inherit-current") return "inherit";
90
+ return trimmed;
91
+ }
92
+
85
93
  export function mergeSettings(override: Partial<SageSettings> | undefined): SageSettings {
86
94
  const merged: SageSettings = {
87
95
  ...DEFAULT_SAGE_SETTINGS,
@@ -92,6 +100,8 @@ export function mergeSettings(override: Partial<SageSettings> | undefined): Sage
92
100
  }
93
101
  };
94
102
 
103
+ merged.model = normalizeModelSetting(merged.model) ?? DEFAULT_SAGE_SETTINGS.model;
104
+
95
105
  merged.timeoutMs = Math.max(1000, Math.floor(merged.timeoutMs));
96
106
  merged.maxCallsPerTurn = Math.max(1, Math.floor(merged.maxCallsPerTurn));
97
107
  merged.maxCallsPerSession = Math.max(1, Math.floor(merged.maxCallsPerSession));
@@ -145,7 +155,7 @@ function parseSettingsRaw(content: string): Partial<SageSettings> | undefined {
145
155
  typeof parsed.explicitRequestAlwaysAllowed === "boolean" ? parsed.explicitRequestAlwaysAllowed : undefined,
146
156
  invocationScope:
147
157
  parsed.invocationScope === "interactive-primary-only" ? "interactive-primary-only" : undefined,
148
- model: typeof parsed.model === "string" ? parsed.model : undefined,
158
+ model: normalizeModelSetting(parsed.model),
149
159
  reasoningLevel:
150
160
  parsed.reasoningLevel === "minimal" ||
151
161
  parsed.reasoningLevel === "low" ||
@@ -194,7 +204,15 @@ export function loadSettings(cwd: string): LoadedSettings {
194
204
 
195
205
  export function saveSettings(path: string, settings: SageSettings): void {
196
206
  mkdirSync(dirname(path), { recursive: true });
197
- writeFileSync(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
207
+
208
+ const persisted: Omit<SageSettings, "invocationScope"> & { invocationScope?: SageSettings["invocationScope"] } = {
209
+ ...settings
210
+ };
211
+
212
+ // invocationScope is currently fixed by policy and not user-configurable in UI.
213
+ delete persisted.invocationScope;
214
+
215
+ writeFileSync(path, `${JSON.stringify(persisted, null, 2)}\n`, "utf8");
198
216
  }
199
217
 
200
218
  export function getSettingsPathForScope(cwd: string, scope: SettingsScope): string {
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/README.md CHANGED
@@ -44,8 +44,8 @@ Then in Pi run:
44
44
 
45
45
  ## Tool profiles
46
46
 
47
- - `read-only-lite` (default): `ls, glob, grep, read`
48
- - `git-review-readonly`: adds restricted `bash` for allowlisted **read-only git** commands
47
+ - `read-only-lite`: `ls, glob, grep, read`
48
+ - `git-review-readonly` (default): adds restricted `bash` for allowlisted **read-only git** commands
49
49
  - `none`
50
50
  - `custom-read-only`
51
51
 
package/docs/SAGE_SPEC.md CHANGED
@@ -72,7 +72,7 @@ This spec targets a **final architecture** (not MVP): Sage is implemented as a *
72
72
  - `pi --mode json -p --no-session --model <configured-model> ...`
73
73
  - Provides dedicated Sage system prompt.
74
74
  - Enforces **single-shot execution** per call (one request → one response).
75
- - Enforces advisory tool policy (default `read-only-lite`: `ls,glob,grep,read`).
75
+ - Enforces advisory tool policy (default `git-review-readonly`: `ls,glob,grep,read,bash` with strict allowlisted read-only git commands).
76
76
  - Supports stricter `none` mode, optional `git-review-readonly` mode, and constrained custom read-only lists.
77
77
  - Explicitly disables `sage_consult` inside Sage subprocess context to prevent subagent recursion.
78
78
 
@@ -182,21 +182,21 @@ 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`).
190
190
  - `maxCallsPerTurn: 1`
191
- - `maxCallsPerSession: 6`
191
+ - `maxCallsPerSession: 100000`
192
192
  - `cooldownTurnsBetweenAutoCalls: 1`
193
- - `maxQuestionChars: 6000`
194
- - `maxContextChars: 20000`
193
+ - `maxQuestionChars: 12000`
194
+ - `maxContextChars: 64000`
195
195
 
196
196
  ## 8.3 Hard safety limits (non-bypassable)
197
197
  - Disallowed caller context (not top-level interactive primary session).
198
198
  - Model unavailable or no API key.
199
- - Timeout exceeded (`timeoutMs`, default `120000`).
199
+ - Timeout exceeded (`timeoutMs`, default `720000`).
200
200
  - Absolute cost cap exceeded (`maxEstimatedCostPerSession`, optional).
201
201
 
202
202
  ## 8.4 Explicit user request behavior
@@ -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
  ---
@@ -291,11 +294,11 @@ Interactive command to configure Sage runtime behavior.
291
294
  8. **Cooldown between autonomous calls**
292
295
  9. **Tool profile for Sage subagent**:
293
296
  - none (strict advisor mode)
294
- - read-only-lite (`ls,glob,grep,read`) **default**
295
- - git-review-readonly (`ls,glob,grep,read,bash`) with strict allowlisted git read commands only
297
+ - read-only-lite (`ls,glob,grep,read`)
298
+ - git-review-readonly (`ls,glob,grep,read,bash`) with strict allowlisted git read commands only **default**
296
299
  - custom read-only list (must exclude mutating/execution tools)
297
- 10. **Per-call max tool calls** (default: 10)
298
- 11. **Per-call max files read** (default: 8)
300
+ 10. **Per-call max tool calls** (default: 250)
301
+ 11. **Per-call max files read** (default: 100)
299
302
  12. **Per-file max bytes** (default: 200KB)
300
303
  13. **Per-call max total bytes read** (default: 1MB)
301
304
  14. **Sensitive path denylist** (default on; configurable additions)
@@ -317,7 +320,7 @@ Interactive command to configure Sage runtime behavior.
317
320
  4. Never pass secrets intentionally unless present in existing prompt/context.
318
321
 
319
322
  ### 11.1 Tool-access policy (advisor model)
320
- 1. Default Sage tool profile is `read-only-lite`: `ls,glob,grep,read`.
323
+ 1. Default Sage tool profile is `git-review-readonly`: `ls,glob,grep,read,bash` with strict read-only git command allowlist.
321
324
  2. Allow `none` profile for stricter environments.
322
325
  3. Custom profile must be read-only; mutating/execution tools are disallowed.
323
326
  4. Explicitly disallow: `edit`, `write`, git-mutating tools, orchestration-control tools, and network/http tools.
@@ -327,8 +330,8 @@ Interactive command to configure Sage runtime behavior.
327
330
  1. Restrict reads to workspace/project roots.
328
331
  2. Denylist sensitive paths by default (e.g., `.env*`, `*.pem`, `*.key`, `id_rsa*`, credential stores).
329
332
  3. Enforce caps per Sage call:
330
- - max tool calls: `10`
331
- - max files read: `8`
333
+ - max tool calls: `250`
334
+ - max files read: `100`
332
335
  - max bytes per file: `200KB`
333
336
  - max total bytes read: `1MB`
334
337
  4. Cap `grep`/`glob` result counts to avoid context explosion.
@@ -372,12 +375,12 @@ 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.
379
- 7. `/sage-settings` lists available models interactively and persists selected model.
380
- 8. Default Sage tool profile is `read-only-lite` (`ls,glob,grep,read`), with `none`, `git-review-readonly`, and constrained custom read-only options available.
382
+ 7. `/sage-settings` supports model configuration interactively (including `inherit`) and persists selected value.
383
+ 8. Default Sage tool profile is `git-review-readonly` (`ls,glob,grep,read,bash` with strict allowlisted read-only git commands), with `none`, `read-only-lite`, and constrained custom read-only options available.
381
384
  9. Sage tool profile blocks mutating/execution/network/orchestration tools (including `edit`, `write`, and `sage_consult`), except allowlisted git-read bash commands in `git-review-readonly`.
382
385
  10. Sage enforces per-call guardrails (max tool calls/files/bytes and capped `grep`/`glob` output).
383
386
  11. Sage calls obey hard safety limits (caller context, model/auth availability, timeout, optional absolute cost cap).
@@ -396,7 +399,7 @@ On tool-policy/data-policy block:
396
399
  5. Start with model interpretation for explicit-request detection; add phrase-detection hook only if testing reveals reliability issues.
397
400
  6. Sage subagents cannot invoke other Sage subagents (no recursion).
398
401
  7. Sage is advisory-only and should not perform implementation actions.
399
- 8. Default Sage tool profile is `read-only-lite` (`ls,glob,grep,read`) with strict guardrails; optional `git-review-readonly` exists for code-review workflows.
402
+ 8. Default Sage tool profile is `git-review-readonly` (`ls,glob,grep,read,bash`) with strict guardrails; `bash` is restricted to allowlisted read-only git commands.
400
403
 
401
404
  ---
402
405
 
@@ -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.
@@ -74,7 +74,7 @@ Every implementation must preserve these behaviors:
74
74
 
75
75
  ## 7) Security and Access Controls
76
76
 
77
- 1. Enforce advisory tool policy by default (`read-only-lite`: `ls,glob,grep,read`).
77
+ 1. Enforce advisory tool policy by default (`git-review-readonly`: `ls,glob,grep,read,bash` with strict read-only git command allowlist).
78
78
  2. Disallow mutating/execution/network/orchestration tools in Sage.
79
79
  3. Restrict filesystem reads to workspace/project roots.
80
80
  4. Apply sensitive-path denylist by default (`.env*`, `*.pem`, `*.key`, etc.).
@@ -125,7 +125,7 @@ Required E2E checks:
125
125
  - [ ] no recursion
126
126
 
127
127
  ### Tool/data policy
128
- - [ ] default profile is `read-only-lite`
128
+ - [ ] default profile is `git-review-readonly`
129
129
  - [ ] disallowed tools blocked (`edit`, `write`, `bash`, `sage_consult`, etc.)
130
130
  - [ ] denylisted paths blocked
131
131
  - [ ] volume caps enforced
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sage",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Interactive-only advisory Sage extension for Pi",