pi-advisor-flow 0.2.8 → 0.3.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
@@ -4,6 +4,24 @@ All notable changes to this project are documented here.
4
4
 
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 0.3.0
8
+
9
+ ### Added
10
+
11
+ - Added discretionary, sequential Advisor file handoff: when globally enabled, the Executor can attach explicitly named tracked working-tree files with `includeTrackedFiles` after the Advisor says it cannot review one. Tracked and untracked attachments remain separate consent paths and share bounded, redacted disclosure limits.
12
+
13
+ ## 0.2.9
14
+
15
+ ### Fixed
16
+
17
+ - Preserved custom numeric values when stepping `/advisor-settings` controls instead of resetting off-preset budgets and disclosure caps.
18
+ - Kept recorded session blocks enforced after `/advisor-off`, restored configuration after failed activation, and charged Advisor-call budgets only when consultation execution begins.
19
+ - Kept conversation and repository disclosure within their configured caps and explicitly identified disabled repository context as withheld.
20
+ - Made concurrent outcome records append safely with one exclusively initialized digest salt, and stopped superseded manual consultations from entering session summaries.
21
+ - Preserved and ignored forward-compatible `advisor.json` fields with a warning, and made malformed configuration errors actionable in every Advisor command.
22
+ - Isolated call budgets, repeated-action counters, and safety blocks between concurrent same-process sessions.
23
+ - Kept the repository-context withheld warning visible when its disclosure budget is zero.
24
+
7
25
  ## 0.2.8
8
26
 
9
27
  ### Fixed
package/README.md CHANGED
@@ -8,17 +8,22 @@ A configurable second-opinion workflow for <a href="https://github.com/earendil-
8
8
 
9
9
  </div>
10
10
 
11
- This extension introduces a strategic "Executor/Advisor" workflow.
11
+ `pi-advisor-flow` keeps one model focused on execution and makes a second, smarter model available for consequential decisions, stalled work, and final reviews. The Executor still owns the work. The Advisor challenges assumptions, exposes risks, and suggests verification steps without taking over or running tools.
12
12
 
13
- `pi-advisor-flow` keeps one model focused on execution and makes a second, smarter model available for consequential decisions, stalled work, and final reviews. The Executor still owns the work. The Advisor provides a concise review, answers questions and can provide help; it does not take over planning or run tools.
13
+ The idea is simple: keep implementation on a fast model and borrow frontier reasoning only when decisions matter. [Read why this workflow is useful](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need).
14
14
 
15
- The concept is simple, keep implementation on a fast model, borrow frontier reasoning only when decisions actually matter. [Read more about Advisors here](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need).
15
+ ## Features
16
16
 
17
- ## Install
17
+ - **On-demand second opinions** through the `ask_advisor` tool or `/advisor-manual`.
18
+ - **Configurable review gates** before plans, after repeated failures, and before declaring completion.
19
+ - **Automatic loop detection** for repeated tool calls, with explicit proceed, revise, or blocked decisions.
20
+ - **Separate model and reasoning controls** for the Executor and Advisor.
21
+ - **Privacy controls** for conversation history, repository context, explicit tracked/untracked file handoff, tool results, secret redaction, and outcome logging.
22
+ - **Optional persistent activation, Simple mode, session summaries, and Herdr integration.**
18
23
 
19
- Requires Pi 0.80.7 or later. The extension installs no dependencies of its own; Pi supplies the modules it uses at runtime.
24
+ ## Install
20
25
 
21
- Install into your Pi agent environment:
26
+ Current release: **0.3.0**. Requires Pi 0.80.7 or later. The extension installs no dependencies of its own; Pi supplies its runtime modules.
22
27
 
23
28
  ```bash
24
29
  # npm
@@ -27,197 +32,65 @@ pi install npm:pi-advisor-flow
27
32
  # GitHub
28
33
  pi install git:github.com/philipbrembeck/pi-advisor.git
29
34
 
30
- # local checkout, useful during development
35
+ # local checkout
31
36
  pi install /path/to/pi-advisor
32
37
  ```
33
38
 
34
- Restart or reload Pi after installation, then run `/advisor` in a session.
39
+ Restart or reload Pi after installation.
35
40
 
36
- ## Start using it
41
+ ## Quick start
37
42
 
38
43
  1. Run `/advisor` to enable the flow and register `ask_advisor`.
39
44
  2. Run `/advisor-models` to choose the Executor and Advisor models.
40
- 3. Run `/advisor-settings` to set Simple mode, persistent activation, context, gates, privacy controls, and output limits.
41
-
42
- Enable with models in one command when preferred:
43
-
44
- ```text
45
- /advisor executor=anthropic/claude-sonnet-5 advisor=openai/gpt-5.6-sol
46
- ```
47
-
48
- `/advisor contextMaxChars=30000` sets the reconstructed-context limit for the current session. Use `0` for no history. The `ALL` option in settings represents the complete current branch and is still subject to the Advisor model's context limit.
49
-
50
- ## Commands
51
-
52
- | Command | Purpose |
53
- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
54
- | `/advisor` | Enable the flow, select the configured Executor, and register `ask_advisor`. Accepts `executor=`, `advisor=`, and `contextMaxChars=` overrides. |
55
- | `/advisor-manual [focus]` | Start a parallel Advisor consultation without interrupting the current Executor turn. |
56
- | `/advisor-models` | Choose Executor and Advisor models plus their reasoning effort. |
57
- | `/advisor-settings` | Open all Advisor settings in one keyboard-navigable screen. |
58
- | `/advisor-off` | Disable the flow and remove `ask_advisor` from the active session. |
59
-
60
- ### `ask_advisor`
61
-
62
- The Executor calls `ask_advisor({})` for a general review of the current task and reconstructed conversation. It can pass a `question` for a targeted review, or a concise `draft` for plan and completion reviews. A draft should name proposed work, validation, and remaining risks; it is an unverified claim, not evidence.
63
-
64
- Successful calls return an opaque `adviceId`. When global outcome logging is enabled, the Executor may voluntarily call `record_advisor_outcome` once with that ID, an adoption value, and a final validation status.
65
-
66
- Use the Advisor after the Executor has investigated and formed a candidate direction. It is intended to challenge assumptions, expose risks, and confirm the next verification step—not to replace the Executor's work.
67
-
68
- Normal consultations preserve the provider's final Markdown and never block execution. When the Advisor has no material concern or recommendation, it may begin with the exact first line `Verdict: sound`; Pi renders that response with the static `◆ ADVISOR · SOUND` header, for both `ask_advisor` results and `/advisor-manual`.
45
+ 3. Run `/advisor-settings` to configure review gates, context, privacy, and limits.
69
46
 
70
- ## Automatic loop gate
47
+ Unknown fields in `advisor.json` are preserved for forward compatibility and reported as non-blocking warnings. Invalid recognized values remain errors, and Advisor commands show the configuration problem without crashing their handlers.
71
48
 
72
- The optional loop gate detects consecutive calls with the same normalized tool signature. By default, it consults the Advisor after three repeats.
73
-
74
- Unlike ordinary consultations, a loop-gate reply must start with exactly one decision header:
49
+ You can also enable the flow and select both models at once:
75
50
 
76
51
  ```text
77
- Decision: proceed
78
- Decision: revise
79
- Decision: blocked
80
- ```
81
-
82
- | Decision | Effect |
83
- | --------- | --------------------------------------------------- |
84
- | `proceed` | Reset the repeat counter and allow the tool action. |
85
- | `revise` | Block the repeated tool action. |
86
- | `blocked` | Apply the configured gate-failure policy. |
87
-
88
- Malformed, missing, duplicate, or contradictory decisions are gate failures. The same policy also applies when the Advisor is unavailable or the shared call budget is exhausted.
89
-
90
- | Failure mode | Effect |
91
- | ------------------------- | ----------------------------------- |
92
- | `block-session` (default) | Block the session. |
93
- | `block-tool` | Block only the current tool action. |
94
- | `warn-and-continue` | Show a warning and continue. |
95
-
96
- | Condition | `block-session` | `block-tool` | `warn-and-continue` |
97
- | -------------------------------------------------------- | --------------- | ----------------- | ------------------- |
98
- | Advisor unavailable or timed out | Block session | Block tool action | Warn and continue |
99
- | Missing, malformed, duplicate, or contradictory decision | Block session | Block tool action | Warn and continue |
100
- | Shared budget exhausted | Block session | Block tool action | Warn and continue |
101
- | `Decision: blocked` | Block session | Block tool action | Warn and continue |
102
-
103
- `advisorBlockOnBlocked` controls whether a session block immediately aborts the active run. It never turns a session block into a tool-only block.
104
-
105
- ## Settings and configuration
106
-
107
- `/advisor-models` and `/advisor-settings` save to global `advisor.json` in the Pi agent directory. Repository-controlled project `advisor.json` files are not applied; models, prompts, gates, budgets, disclosure, redaction, integrations, and consent remain under the user's global configuration.
108
-
109
- All fields are optional. This example shows the available settings and their normal defaults:
110
-
111
- ```json
112
- {
113
- "executor": "openai/gpt-5.6-luna",
114
- "advisor": "anthropic/claude-fable-5",
115
- "executorEffort": "medium",
116
- "advisorEffort": "xhigh",
117
- "contextMaxChars": 25000,
118
-
119
- "advisorPlanGate": true,
120
- "advisorFailureGate": true,
121
- "advisorCompletionGate": true,
122
- "advisorCustomInvocation": "before changing a production deployment",
123
- "advisorCollapseResponses": false,
124
-
125
- "advisorAutoLoopGate": true,
126
- "advisorLoopThreshold": 3,
127
- "advisorMaxCallsPerSession": 5,
128
- "advisorBlockOnBlocked": true,
129
- "gateFailureMode": "block-session",
130
-
131
- "advisorSessionSummary": false,
132
- "advisorGitContext": "summary",
133
- "advisorGitContextMaxChars": 20000,
134
- "simpleMode": false,
135
- "alwaysOn": false,
136
- "advisorHerdrIntegration": true,
137
- "advisorToolResultMaxLines": 2000,
138
- "advisorToolResultMaxBytes": 51200,
139
-
140
- "advisorRedactSecrets": false,
141
- "advisorUntrackedContent": false,
142
- "advisorOutcomeLogging": false,
143
- "advisorToolPolicies": {
144
- "bash": "summary",
145
- "deploy": "exclude"
146
- }
147
- }
52
+ /advisor executor=anthropic/claude-sonnet-5 advisor=openai/gpt-5.6-sol
148
53
  ```
149
54
 
150
- ### Simple mode and persistent activation
151
-
152
- - `simpleMode` defaults to `false`. When enabled, `ask_advisor` and `/advisor-manual` remain available for voluntary second opinions, while plan/failure/completion rules, loop gates, blocking, call budgets, and session summaries are disabled. Context limits, result caps, redaction, and tool disclosure policies still apply.
153
- - `alwaysOn` defaults to `false`. When enabled, Pi restores the configured Executor and activates `ask_advisor` for new, resumed, forked, and reloaded sessions. While the Advisor flow is active, an explicit `/model` selection becomes the persisted Executor for the next activation; a model restored with a session does not change the saved Executor. `/advisor-off` turns `alwaysOn` off so the flow stays disabled in later sessions.
154
- - In Simple mode, settings keeps the Context window/history slider alongside Simple mode and Always on; advanced values remain saved and take effect when Simple mode is disabled.
155
-
156
- ### Repository context
157
-
158
- - `advisorGitContext` defaults to `summary`. It controls how much of the working tree reaches the Advisor:
159
- - `off` sends no repository information.
160
- - `summary` sends changed file names, change status, and line counts. It never sends file contents.
161
- - `full` additionally sends the patch.
162
- - Changes are measured against the last commit and cover staged and unstaged work. Untracked files are always listed by name only; their contents are never sent by `gitContext: full`.
163
- - `advisorUntrackedContent` defaults to false. When enabled, `includeUntracked` can attach only exact named, repository-relative, untracked regular files. Files are redacted and capped before egress; sibling files remain withheld.
164
- - `advisorGitContextMaxChars` defaults to `20000`. Repository context may claim its own cap or half of `contextMaxChars`, whichever is smaller, so it cannot crowd out the conversation.
165
- - The Executor may pass `gitContext` to `ask_advisor` as `none`, `summary`, or `full`. `advisorGitContext` is the ceiling: a larger request is narrowed to the configured level and the Advisor is told that a fuller view was withheld, so it does not claim verification it could not perform.
166
- - `summary` deliberately excludes diff hunk headers. Git derives those from surrounding file content, so a hunk header can reproduce a line the change never touched, including a credential.
167
- - Redaction runs before the region is capped, and repository content is labelled as untrusted data in the request. Paths and patch text are escaped so a crafted path cannot close the region early and have the remainder read as instructions.
168
- - File names themselves can be sensitive. `summary` withholds file contents, not file names; use `off` when names must not leave the machine.
169
- - Collection shares a single overall time budget across its git commands and degrades to a stated failure rather than implying a clean tree.
170
-
171
- ### Project preferences and outcomes
55
+ ## How it works
172
56
 
173
- In a trusted project only, `.pi/advisor-preferences.md` may provide a short local brief. It is never written by pi-advisor, is treated as lower-priority untrusted text, and is redacted/capped before egress. Symlinks, unreadable files, and paths outside the project are ignored.
57
+ 1. The Executor investigates the task and forms its own candidate direction.
58
+ 2. For a consequential decision, stalled attempt, or final review, it calls `ask_advisor` with the reconstructed conversation and allowed repository context.
59
+ 3. The Advisor returns a concise review. It may challenge assumptions, identify risks, or recommend the next verification step.
60
+ 4. The Executor decides what to adopt, performs the work, and validates the result.
174
61
 
175
- `advisorOutcomeLogging` defaults to false and is global-only: a project config cannot enable it. When enabled, `~/.pi/agent/advisor-outcomes.jsonl` stores bounded rotating JSONL records with only a version, timestamp, salted truncated advice digest, trigger, adoption, and validation status. It stores no prompt, advice, paths, tool output, repository data, session ID, or advice ID.
62
+ A normal consultation never blocks execution. The optional automatic loop gate is different: it evaluates repeated tool calls and applies the configured failure policy when the Advisor says to revise, reports a block, is unavailable, or returns an invalid decision.
176
63
 
177
- ### Context and limits
64
+ Successful calls return an opaque `adviceId`. If global outcome logging is enabled, the Executor can call `record_advisor_outcome` once to record whether the advice was adopted and whether final validation passed.
178
65
 
179
- - `contextMaxChars` defaults to `15000`. It preserves complete semantic entries and adds an omission marker rather than splitting a message.
180
- - Set `contextMaxChars` to `0` to omit reconstructed history. `9007199254740991` is the persisted value for `ALL`.
181
- - Tool results default to Pi's `2000` lines and `50 KiB` limits. Oversized results preserve their beginning and end with an omission marker.
182
- - `advisorLoopThreshold` is an integer of at least `2`; its default is `3`.
183
- - Omit `advisorMaxCallsPerSession` for an unlimited shared budget. Otherwise it must be a non-negative safe integer.
184
-
185
- ### Privacy controls
186
-
187
- Advisor context can contain user messages, tool calls, and tool results. Configure disclosure deliberately:
188
-
189
- - `advisorRedactSecrets` defaults to `false`. When enabled, pi-advisor locally redacts common credential patterns before including context in an Advisor request.
190
- - `advisorToolPolicies` matches an **exact tool name**. Each tool may use `full`, `summary`, or `exclude`.
191
- - `full` includes the call arguments and capped result output.
192
- - `summary` omits call arguments and result output but includes result status and size metadata.
193
- - `exclude` omits both call details and output.
194
- - Tools not listed in `advisorToolPolicies`, including custom and newly added tools, use `full` for backward compatibility.
195
-
196
- Redaction and output limits reduce accidental disclosure; they are not a data-classification system and cannot guarantee every secret is found. Use tool policies for content that must not be sent to the Advisor.
66
+ ## Commands
197
67
 
198
- ### Session summary and Herdr
68
+ | Command | Purpose |
69
+ | --- | --- |
70
+ | `/advisor` | Enable the flow and optionally override the Executor, Advisor, or context limit. |
71
+ | `/advisor-manual [focus]` | Start a parallel consultation without interrupting the current Executor turn. |
72
+ | `/advisor-models` | Choose both models and their reasoning effort. |
73
+ | `/advisor-settings` | Configure behavior, context, gates, privacy, and output limits. |
74
+ | `/advisor-off` | Disable the flow and turn off persistent activation. |
199
75
 
200
- The optional Session Advisor Summary defaults to off. When enabled, it is local and in-memory only, appears after a non-blocked settled run, and is never persisted.
76
+ The Executor calls `ask_advisor({})` for a general review. It can pass a targeted `question` or a concise `draft` describing proposed work, validation, and remaining risks. If the Advisor explicitly says it cannot review a specifically named file, the Executor may make a sequential follow-up call with `includeTrackedFiles` when global consent is enabled and the file is relevant. Draft claims give the Advisor review context; they are not verification evidence.
201
77
 
202
- It distinguishes regular Markdown advice from gate decisions and records the trigger, model, usage/cost when available, failures, budget, and execution effect.
78
+ ## What gets sent to the Advisor
203
79
 
204
- [Herdr](https://github.com/ogulcancelik/herdr) integration is enabled by default. It reports Advisor activity and a bounded, redacted blocked-state summary through Herdr's metadata paths; disable it with `advisorHerdrIntegration`. Previously reported state is still cleared when integration is disabled.
80
+ Advisor context can include user messages, tool calls, tool results, and repository information. Secret redaction is off by default, and tools without an explicit disclosure policy default to full context. Review the privacy settings before using the extension with sensitive work.
205
81
 
206
- ## Development
82
+ Repository context is configurable from no access through changed-file summaries to a capped patch. When context is disabled or its budget is zero, the Advisor is told it was withheld rather than shown an apparently clean tree. Explicit tracked and untracked file contents require separate global opt-ins; attachments are capped, redacted when configured, and sent as untrusted data.
207
83
 
208
- ```bash
209
- git clone git@github.com:philipbrembeck/pi-advisor.git
210
- cd pi-advisor
211
- bun install
84
+ ## Documentation
212
85
 
213
- bun test
214
- bun run typecheck
215
- bun run lint
216
- ```
86
+ - [Configuration and automatic loop gates](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/configuration.md)
87
+ - [Privacy and data handling](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/privacy.md)
88
+ - [Development](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/development.md)
89
+ - [Documentation index](https://github.com/philipbrembeck/pi-advisor/blob/main/docs/README.md)
217
90
 
218
91
  ## Links
219
92
 
220
- - [MIT LICENSE](LICENSE)
93
+ - [MIT License](LICENSE)
221
94
  - [Changelog](CHANGELOG.md)
222
95
  - [npm package](https://www.npmjs.com/package/pi-advisor-flow)
223
96
  - [Why use an Advisor flow?](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need)
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { registerCommands } from "../src/commands.js";
3
3
  import { setHerdrBlockedEmitter } from "../src/herdr.js";
4
+ import { AdvisorSessionState } from "../src/session-state.js";
4
5
  import {
5
6
  consultAdvisor as consultAdvisorImplementation,
6
7
  parseAutomaticDecision as parseAutomaticDecisionImplementation,
@@ -29,9 +30,10 @@ export const runAdvisorGate = (
29
30
  ) => runAdvisorGateImplementation(...args);
30
31
 
31
32
  export default function (pi: ExtensionAPI) {
33
+ const sessionState = new AdvisorSessionState();
32
34
  setHerdrBlockedEmitter((active, label) =>
33
35
  pi.events.emit("herdr:blocked", { active, label })
34
36
  );
35
- registerAdvisorTool(pi);
36
- registerCommands(pi);
37
+ registerAdvisorTool(pi, sessionState);
38
+ registerCommands(pi, { sessionState });
37
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "author": "Philip Brembeck",
5
5
  "repository": {
6
6
  "type": "git",
@@ -62,7 +62,8 @@
62
62
  "*.{json,jsonc,ts}": "bun run lint:fix --"
63
63
  },
64
64
  "overrides": {
65
- "brace-expansion": "5.0.8"
65
+ "brace-expansion": "5.0.9",
66
+ "undici": "8.9.0"
66
67
  },
67
68
  "pi": {
68
69
  "extensions": [
package/src/commands.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  advisorMaxCallsPerSessionRef,
10
10
  advisorRef,
11
11
  alwaysOnRef,
12
+ contextMaxCharsRef,
12
13
  executorEffortRef,
13
14
  executorRef,
14
15
  getAdvisorSettings,
@@ -38,6 +39,7 @@ import {
38
39
  setAdvisorToolPoliciesRef,
39
40
  setAdvisorToolResultMaxBytesRef,
40
41
  setAdvisorToolResultMaxLinesRef,
42
+ setAdvisorTrackedFileContentRef,
41
43
  setAdvisorUntrackedContentRef,
42
44
  setAlwaysOnRef,
43
45
  setContextMaxCharsRef,
@@ -47,10 +49,11 @@ import {
47
49
  splitRef,
48
50
  } from "./config.js";
49
51
  import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
52
+ import type { AdvisorSessionState } from "./session-state.js";
50
53
  import {
51
54
  adviceForDisplay,
52
- advisorSessionState,
53
55
  consultAdvisor,
56
+ advisorSessionState as defaultAdvisorSessionState,
54
57
  hasSoundVerdict,
55
58
  renderAdvisorCallBox,
56
59
  renderAdvisorResponseHeader,
@@ -138,8 +141,13 @@ const findConfiguredModel = (ctx: ExtensionContext, ref: string) => {
138
141
 
139
142
  export const registerCommands = (
140
143
  pi: ExtensionAPI,
141
- dependencies: { consult?: ManualConsult } = {}
144
+ dependencies: {
145
+ consult?: ManualConsult;
146
+ sessionState?: AdvisorSessionState;
147
+ } = {}
142
148
  ) => {
149
+ const advisorSessionState =
150
+ dependencies.sessionState ?? defaultAdvisorSessionState;
143
151
  const flowEnabled = () => pi.getActiveTools().includes("ask_advisor");
144
152
  const requestAdvisor =
145
153
  dependencies.consult ??
@@ -155,15 +163,15 @@ export const registerCommands = (
155
163
  herdrAdvisorActivity.start();
156
164
  return requestAdvisor(ctx, question, controller.signal)
157
165
  .then(({ markdown }) => {
166
+ if (controller.signal.aborted) {
167
+ return;
168
+ }
158
169
  advisorSessionState.recordInvocation({
159
170
  executionEffect: "continued",
160
171
  kind: "markdown",
161
172
  model: advisorRef,
162
173
  trigger: "manual",
163
174
  });
164
- if (controller.signal.aborted) {
165
- return;
166
- }
167
175
  pi.sendMessage(
168
176
  {
169
177
  content: `Manual Advisor consultation${question ? ` (${question})` : ""}:\n\n${markdown}`,
@@ -232,19 +240,48 @@ export const registerCommands = (
232
240
  return {};
233
241
  };
234
242
 
243
+ const loadCommandConfig = (ctx: ExtensionContext) => {
244
+ try {
245
+ loadConfig(ctx);
246
+ return true;
247
+ } catch (error) {
248
+ const message = error instanceof Error ? error.message : String(error);
249
+ notify(
250
+ ctx,
251
+ `Advisor command could not load configuration: ${message} Fix advisor.json and retry.`,
252
+ "error"
253
+ );
254
+ return false;
255
+ }
256
+ };
257
+
235
258
  const activateAdvisor = async (
236
259
  args: string,
237
260
  ctx: ExtensionContext,
238
261
  announce = true
239
262
  ) => {
240
- loadConfig(ctx);
263
+ if (!loadCommandConfig(ctx)) {
264
+ return;
265
+ }
266
+ const previous = {
267
+ advisor: advisorRef,
268
+ contextMaxChars: contextMaxCharsRef,
269
+ executor: executorRef,
270
+ };
271
+ const restoreRefs = () => {
272
+ setAdvisorRef(previous.advisor);
273
+ setContextMaxCharsRef(previous.contextMaxChars);
274
+ setExecutorRef(previous.executor);
275
+ };
241
276
  const argumentError = parseArgs(args);
242
277
  if (argumentError) {
278
+ restoreRefs();
243
279
  notify(ctx, argumentError, "error");
244
280
  return;
245
281
  }
246
282
  const { error } = await resolveActivationModels(ctx);
247
283
  if (error) {
284
+ restoreRefs();
248
285
  notify(ctx, error, "error");
249
286
  return;
250
287
  }
@@ -356,7 +393,9 @@ export const registerCommands = (
356
393
  description:
357
394
  "Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
358
395
  handler: (args, ctx) => {
359
- loadConfig(ctx);
396
+ if (!loadCommandConfig(ctx)) {
397
+ return Promise.resolve();
398
+ }
360
399
  if (
361
400
  !(
362
401
  isSimpleMode() ||
@@ -396,8 +435,7 @@ export const registerCommands = (
396
435
  description:
397
436
  "Select and persist the Executor and Advisor models with reasoning levels",
398
437
  handler: async (_args, ctx) => {
399
- loadConfig(ctx);
400
- if (!ctx.hasUI) {
438
+ if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
401
439
  return;
402
440
  }
403
441
  const refs = ctx.modelRegistry
@@ -475,8 +513,7 @@ export const registerCommands = (
475
513
  description: "Configure Advisor context and reasoning effort",
476
514
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
477
515
  handler: async (_args, ctx) => {
478
- loadConfig(ctx);
479
- if (!ctx.hasUI) {
516
+ if (!(loadCommandConfig(ctx) && ctx.hasUI)) {
480
517
  return;
481
518
  }
482
519
 
@@ -523,6 +560,7 @@ export const registerCommands = (
523
560
  setAdvisorGitContextRef(settings.gitContext ?? "summary");
524
561
  setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
525
562
  setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
563
+ setAdvisorTrackedFileContentRef(settings.trackedFileContent ?? false);
526
564
  setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
527
565
  setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
528
566
  const path = saveConfig(ctx);
package/src/config.ts CHANGED
@@ -64,6 +64,7 @@ export let advisorGitContextMaxCharsRef = DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS;
64
64
  export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
65
65
  export let advisorOutcomeLoggingRef = false;
66
66
  export let advisorUntrackedContentRef = false;
67
+ export let advisorTrackedFileContentRef = false;
67
68
 
68
69
  export const setExecutorRef = (ref: string) => {
69
70
  executorRef = ref;
@@ -166,6 +167,9 @@ export const setAdvisorOutcomeLoggingRef = (enabled: boolean) => {
166
167
  export const setAdvisorUntrackedContentRef = (enabled: boolean) => {
167
168
  advisorUntrackedContentRef = enabled;
168
169
  };
170
+ export const setAdvisorTrackedFileContentRef = (enabled: boolean) => {
171
+ advisorTrackedFileContentRef = enabled;
172
+ };
169
173
 
170
174
  /**
171
175
  * Returns the current live settings state. Use this at UI boundaries instead of
@@ -195,6 +199,7 @@ export const getAdvisorSettings = () => ({
195
199
  toolPolicies: { ...advisorToolPoliciesRef },
196
200
  toolResultMaxBytes: advisorToolResultMaxBytesRef,
197
201
  toolResultMaxLines: advisorToolResultMaxLinesRef,
202
+ trackedFileContent: advisorTrackedFileContentRef,
198
203
  untrackedContent: advisorUntrackedContentRef,
199
204
  });
200
205
 
@@ -231,6 +236,7 @@ export interface AdvisorConfig {
231
236
  advisorToolPolicies?: AdvisorToolPolicies;
232
237
  advisorToolResultMaxBytes?: number;
233
238
  advisorToolResultMaxLines?: number;
239
+ advisorTrackedFileContent?: boolean;
234
240
  advisorUntrackedContent?: boolean;
235
241
  alwaysOn?: boolean;
236
242
  contextMaxChars?: number;
@@ -260,6 +266,7 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
260
266
  "alwaysOn",
261
267
  "advisorToolResultMaxBytes",
262
268
  "advisorToolResultMaxLines",
269
+ "advisorTrackedFileContent",
263
270
  "advisorRedactSecrets",
264
271
  "advisorUntrackedContent",
265
272
  "advisorOutcomeLogging",
@@ -280,6 +287,7 @@ const BOOLEAN_CONFIG_KEYS = [
280
287
  "simpleMode",
281
288
  "alwaysOn",
282
289
  "advisorHerdrIntegration",
290
+ "advisorTrackedFileContent",
283
291
  "advisorRedactSecrets",
284
292
  "advisorUntrackedContent",
285
293
  "advisorOutcomeLogging",
@@ -305,16 +313,10 @@ const invalidConfigValue = (
305
313
  );
306
314
  };
307
315
 
308
- const validateKnownKeys = (config: ConfigRecord, path: string) => {
309
- const unknownKeys = Object.keys(config).filter(
316
+ const unknownConfigKeys = (config: ConfigRecord) =>
317
+ Object.keys(config).filter(
310
318
  (key) => !CONFIG_KEYS.has(key as keyof AdvisorConfig)
311
319
  );
312
- if (unknownKeys.length > 0) {
313
- throw new TypeError(
314
- `Invalid advisor configuration at ${path}: unknown key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. Remove them or upgrade pi-advisor.`
315
- );
316
- }
317
- };
318
320
 
319
321
  const validateStringValues = (config: ConfigRecord, path: string) => {
320
322
  for (const key of STRING_CONFIG_KEYS) {
@@ -406,7 +408,6 @@ export const validateConfig = (
406
408
  );
407
409
  }
408
410
  const config = value as ConfigRecord;
409
- validateKnownKeys(config, path);
410
411
  validateStringValues(config, path);
411
412
  validateBooleanValues(config, path);
412
413
  validateNumericValues(config, path);
@@ -467,6 +468,7 @@ const resetDefaults = () => {
467
468
  advisorToolPoliciesRef = {};
468
469
  advisorOutcomeLoggingRef = false;
469
470
  advisorUntrackedContentRef = false;
471
+ advisorTrackedFileContentRef = false;
470
472
  };
471
473
 
472
474
  const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
@@ -568,6 +570,11 @@ const applyConfig = (config: AdvisorConfig) => {
568
570
  "advisorUntrackedContent",
569
571
  setAdvisorUntrackedContentRef
570
572
  );
573
+ applyOptionalConfig(
574
+ config,
575
+ "advisorTrackedFileContent",
576
+ setAdvisorTrackedFileContentRef
577
+ );
571
578
  };
572
579
 
573
580
  const readConfig = (path: string): AdvisorConfig => {
@@ -587,10 +594,12 @@ const configCache = new Map<
587
594
  string,
588
595
  { config: AdvisorConfig; identity: string }
589
596
  >();
597
+ const warnedUnknownConfigIdentities = new Set<string>();
590
598
 
591
599
  /** Drops the parsed-configuration cache; the next load re-reads from disk. */
592
600
  export const resetConfigCache = () => {
593
601
  configCache.clear();
602
+ warnedUnknownConfigIdentities.clear();
594
603
  };
595
604
 
596
605
  /**
@@ -626,6 +635,19 @@ export const loadConfig = (_ctx: ExtensionContext) => {
626
635
  : undefined;
627
636
  if (globalConfig) {
628
637
  applyConfig(globalConfig);
638
+ const unknownKeys = unknownConfigKeys(globalConfig as ConfigRecord);
639
+ const warningIdentity = `${global}:${configIdentity(global)}`;
640
+ if (
641
+ unknownKeys.length > 0 &&
642
+ _ctx.hasUI &&
643
+ !warnedUnknownConfigIdentities.has(warningIdentity)
644
+ ) {
645
+ _ctx.ui.notify(
646
+ `Advisor configuration at ${global} contains unrecognized key(s) ${unknownKeys.map((key) => JSON.stringify(key)).join(", ")}. They were preserved but ignored; check for typos or upgrade pi-advisor.`,
647
+ "warning"
648
+ );
649
+ warnedUnknownConfigIdentities.add(warningIdentity);
650
+ }
629
651
  }
630
652
  // Repository-controlled project configuration is never applied. Models,
631
653
  // prompts, gates, budgets, disclosure, redaction, integrations, and consent
@@ -675,6 +697,7 @@ export const saveConfig = (_ctx: ExtensionContext) => {
675
697
  advisorToolPolicies: advisorToolPoliciesRef,
676
698
  advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
677
699
  advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
700
+ advisorTrackedFileContent: advisorTrackedFileContentRef,
678
701
  advisorUntrackedContent: advisorUntrackedContentRef,
679
702
  alwaysOn: alwaysOnRef,
680
703
  gateFailureMode: advisorFailureModeRef,