pi-advisor-flow 0.2.4 → 0.2.6

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/README.md CHANGED
@@ -1,19 +1,18 @@
1
1
  # pi-advisor
2
2
 
3
3
  <div align="center">
4
- <img src="https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/screenshot.png" alt="Pi Advisor consultation in the terminal" width="760">
5
4
 
6
- A configurable second-opinion workflow for <a href="https://github.com/earendil-works/pi">Pi</a> coding agents.
5
+ ![Pi Advisor consultation in the terminal](https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/screenshot.png)
7
6
 
8
- </div>
7
+ A configurable second-opinion workflow for <a href="https://github.com/earendil-works/pi">Pi</a> coding agents, inspired by the ["Steering Black-Box LLMs with Advisor Models" paper](https://arxiv.org/abs/2510.02453) and Claude's [Advisor](https://code.claude.com/docs/en/advisor) feature.
9
8
 
10
- Keep the work to the executor model, while the advisor steers it.
9
+ </div>
11
10
 
12
- This extension introduces a strategic "Executor/Advisor" workflow, inspired by Claudes [Advisor](https://code.claude.com/docs/en/advisor).
11
+ This extension introduces a strategic "Executor/Advisor" workflow.
13
12
 
14
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.
15
14
 
16
- [Read more about Advisors here](https://philipbrembeck.com/writings/2026/07/only-as-much-intelligence-as-you-need).
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).
17
16
 
18
17
  ## Install
19
18
 
@@ -58,7 +57,9 @@ Enable with models in one command when preferred:
58
57
 
59
58
  ### `ask_advisor`
60
59
 
61
- 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.
60
+ 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.
61
+
62
+ 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.
62
63
 
63
64
  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.
64
65
 
@@ -126,6 +127,8 @@ All fields are optional. This example shows the available settings and their nor
126
127
  "gateFailureMode": "block-session",
127
128
 
128
129
  "advisorSessionSummary": false,
130
+ "advisorGitContext": "summary",
131
+ "advisorGitContextMaxChars": 20000,
129
132
  "simpleMode": false,
130
133
  "alwaysOn": false,
131
134
  "advisorHerdrIntegration": true,
@@ -133,6 +136,8 @@ All fields are optional. This example shows the available settings and their nor
133
136
  "advisorToolResultMaxBytes": 51200,
134
137
 
135
138
  "advisorRedactSecrets": false,
139
+ "advisorUntrackedContent": false,
140
+ "advisorOutcomeLogging": false,
136
141
  "advisorToolPolicies": {
137
142
  "bash": "summary",
138
143
  "deploy": "exclude"
@@ -146,6 +151,27 @@ All fields are optional. This example shows the available settings and their nor
146
151
  - `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.
147
152
  - 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.
148
153
 
154
+ ### Repository context
155
+
156
+ - `advisorGitContext` defaults to `summary`. It controls how much of the working tree reaches the Advisor:
157
+ - `off` sends no repository information.
158
+ - `summary` sends changed file names, change status, and line counts. It never sends file contents.
159
+ - `full` additionally sends the patch.
160
+ - 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`.
161
+ - `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.
162
+ - `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.
163
+ - 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.
164
+ - `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.
165
+ - 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.
166
+ - File names themselves can be sensitive. `summary` withholds file contents, not file names; use `off` when names must not leave the machine.
167
+ - Collection shares a single overall time budget across its git commands and degrades to a stated failure rather than implying a clean tree.
168
+
169
+ ### Project preferences and outcomes
170
+
171
+ 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.
172
+
173
+ `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.
174
+
149
175
  ### Context and limits
150
176
 
151
177
  - `contextMaxChars` defaults to `15000`. It preserves complete semantic entries and adds an omission marker rather than splitting a message.
package/package.json CHANGED
@@ -1,4 +1,12 @@
1
1
  {
2
+ "name": "pi-advisor-flow",
3
+ "version": "0.2.6",
4
+ "author": "Philip Brembeck",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/philipbrembeck/pi-advisor.git"
8
+ },
9
+ "main": "extensions/index.ts",
2
10
  "devDependencies": {
3
11
  "@biomejs/biome": "2.5.3",
4
12
  "@earendil-works/pi-ai": "^0.80.7",
@@ -12,37 +20,19 @@
12
20
  "typescript": "^5.3.3",
13
21
  "ultracite": "7.9.4"
14
22
  },
15
- "name": "pi-advisor-flow",
16
23
  "peerDependencies": {
17
24
  "@earendil-works/pi-ai": "^0.80.7",
18
25
  "@earendil-works/pi-coding-agent": "^0.80.7",
19
26
  "@earendil-works/pi-tui": "^0.80.7",
20
27
  "typebox": "^1.1.38"
21
28
  },
22
- "scripts": {
23
- "test": "bun test",
24
- "typecheck": "tsc --noEmit",
25
- "format": "bunx ultracite fix --linter-enabled=false",
26
- "lint": "bunx ultracite check",
27
- "lint:fix": "bunx ultracite fix",
28
- "package:check": "npm pack --dry-run --json >/dev/null",
29
- "prepare": "husky"
30
- },
31
- "lint-staged": {
32
- "*.{json,jsonc,ts}": "bun run lint:fix --"
33
- },
34
- "type": "module",
35
- "version": "0.2.4",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/philipbrembeck/pi-advisor.git"
40
- },
41
- "homepage": "https://github.com/philipbrembeck/pi-advisor",
42
- "author": "Philip Brembeck",
43
29
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
44
- "main": "extensions/index.ts",
45
- "types": "extensions/index.ts",
30
+ "files": [
31
+ "extensions",
32
+ "src",
33
+ "README.md"
34
+ ],
35
+ "homepage": "https://github.com/philipbrembeck/pi-advisor",
46
36
  "keywords": [
47
37
  "pi-package",
48
38
  "pi-extension",
@@ -51,15 +41,28 @@
51
41
  "pi-advisor",
52
42
  "herdr"
53
43
  ],
54
- "files": [
55
- "extensions",
56
- "src",
57
- "README.md"
58
- ],
44
+ "license": "MIT",
45
+ "lint-staged": {
46
+ "*.{json,jsonc,ts}": "bun run lint:fix --"
47
+ },
48
+ "overrides": {
49
+ "brace-expansion": "5.0.8"
50
+ },
59
51
  "pi": {
60
52
  "extensions": [
61
53
  "./extensions/index.ts"
62
54
  ],
63
55
  "image": "https://raw.githubusercontent.com/philipbrembeck/pi-advisor/refs/heads/main/assets/hero.png"
64
- }
56
+ },
57
+ "scripts": {
58
+ "test": "bun test",
59
+ "typecheck": "tsc --noEmit",
60
+ "format": "bunx ultracite fix --linter-enabled=false",
61
+ "lint": "bunx ultracite check",
62
+ "lint:fix": "bunx ultracite fix",
63
+ "package:check": "npm pack --dry-run --json >/dev/null",
64
+ "prepare": "husky"
65
+ },
66
+ "type": "module",
67
+ "types": "extensions/index.ts"
65
68
  }
package/src/commands.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  loadConfig,
17
17
  parseArgs,
18
18
  saveConfig,
19
+ saveGlobalOutcomeLogging,
19
20
  setAdvisorAutoLoopGateRef,
20
21
  setAdvisorBlockOnBlockedRef,
21
22
  setAdvisorCollapseResponsesRef,
@@ -24,9 +25,12 @@ import {
24
25
  setAdvisorEffortRef,
25
26
  setAdvisorFailureGateRef,
26
27
  setAdvisorFailureModeRef,
28
+ setAdvisorGitContextMaxCharsRef,
29
+ setAdvisorGitContextRef,
27
30
  setAdvisorHerdrIntegrationRef,
28
31
  setAdvisorLoopThresholdRef,
29
32
  setAdvisorMaxCallsPerSessionRef,
33
+ setAdvisorOutcomeLoggingRef,
30
34
  setAdvisorPlanGateRef,
31
35
  setAdvisorRedactSecretsRef,
32
36
  setAdvisorRef,
@@ -34,6 +38,7 @@ import {
34
38
  setAdvisorToolPoliciesRef,
35
39
  setAdvisorToolResultMaxBytesRef,
36
40
  setAdvisorToolResultMaxLinesRef,
41
+ setAdvisorUntrackedContentRef,
37
42
  setAlwaysOnRef,
38
43
  setContextMaxCharsRef,
39
44
  setExecutorEffortRef,
@@ -108,7 +113,12 @@ type ManualConsult = (
108
113
  ctx: ExtensionContext,
109
114
  question?: string,
110
115
  signal?: AbortSignal
111
- ) => Promise<{ markdown: string; thinkingText: string }>;
116
+ ) => Promise<{
117
+ markdown: string;
118
+ thinkingText: string;
119
+ draftBytes?: number;
120
+ preferenceBytes?: number;
121
+ }>;
112
122
  type ThinkingLevel = Parameters<ExtensionAPI["setThinkingLevel"]>[0];
113
123
 
114
124
  const notify = (
@@ -248,7 +258,11 @@ export const registerCommands = (
248
258
  pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
249
259
  }
250
260
  if (!flowEnabled()) {
251
- pi.setActiveTools([...pi.getActiveTools(), "ask_advisor"]);
261
+ pi.setActiveTools([
262
+ ...pi.getActiveTools(),
263
+ "ask_advisor",
264
+ "record_advisor_outcome",
265
+ ]);
252
266
  }
253
267
  if (announce) {
254
268
  notify(
@@ -459,6 +473,7 @@ export const registerCommands = (
459
473
 
460
474
  pi.registerCommand("advisor-settings", {
461
475
  description: "Configure Advisor context and reasoning effort",
476
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one settings form maps every persisted control.
462
477
  handler: async (_args, ctx) => {
463
478
  loadConfig(ctx);
464
479
  if (!ctx.hasUI) {
@@ -505,9 +520,19 @@ export const registerCommands = (
505
520
  setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
506
521
  setAdvisorToolResultMaxBytesRef(settings.toolResultMaxBytes ?? 50 * 1024);
507
522
  setAdvisorRedactSecretsRef(settings.redactSecrets ?? false);
523
+ setAdvisorGitContextRef(settings.gitContext ?? "summary");
524
+ setAdvisorGitContextMaxCharsRef(settings.gitContextMaxChars ?? 20_000);
508
525
  setAdvisorToolPoliciesRef(settings.toolPolicies ?? {});
526
+ setAdvisorUntrackedContentRef(settings.untrackedContent ?? false);
527
+ setAdvisorOutcomeLoggingRef(settings.outcomeLogging ?? false);
509
528
  const path = saveConfig(ctx);
510
- ctx.ui.notify(`Saved Advisor settings to ${path}`, "info");
529
+ const globalPath = saveGlobalOutcomeLogging(
530
+ settings.outcomeLogging ?? false
531
+ );
532
+ ctx.ui.notify(
533
+ `Saved Advisor settings to ${path}; outcome logging globally to ${globalPath}`,
534
+ "info"
535
+ );
511
536
  },
512
537
  });
513
538
 
@@ -515,7 +540,12 @@ export const registerCommands = (
515
540
  description: "Disable on-demand Advisor calls; keep the current model",
516
541
  handler: (_args, ctx) => {
517
542
  pi.setActiveTools(
518
- pi.getActiveTools().filter((name) => name !== "ask_advisor")
543
+ pi
544
+ .getActiveTools()
545
+ .filter(
546
+ (name) =>
547
+ name !== "ask_advisor" && name !== "record_advisor_outcome"
548
+ )
519
549
  );
520
550
  // Leaving alwaysOn set would silently reactivate the flow next session.
521
551
  const wasAlwaysOn = alwaysOnRef;
package/src/config.ts CHANGED
@@ -7,6 +7,11 @@ import {
7
7
  type ExtensionContext,
8
8
  getAgentDir,
9
9
  } from "@earendil-works/pi-coding-agent";
10
+ import {
11
+ GIT_CONTEXT_LEVELS,
12
+ type GitContextLevel,
13
+ isValidGitContextLevel,
14
+ } from "./git.js";
10
15
 
11
16
  export const FALLBACK_EXECUTOR = "aikeys/claude-sonnet-5";
12
17
  export const FALLBACK_ADVISOR = "aikeys/claude-fable-5";
@@ -14,6 +19,7 @@ export const DEFAULT_CONTEXT_MAX_CHARS = 15_000;
14
19
  export const MAX_CONTEXT_MAX_CHARS = Number.MAX_SAFE_INTEGER;
15
20
  export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES = DEFAULT_MAX_LINES;
16
21
  export const DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES = DEFAULT_MAX_BYTES;
22
+ export const DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS = 20_000;
17
23
  export type AdvisorToolPolicy = "full" | "summary" | "exclude";
18
24
  export type AdvisorToolPolicies = Record<string, AdvisorToolPolicy>;
19
25
  export const ADVISOR_TOOL_POLICIES: AdvisorToolPolicy[] = [
@@ -53,7 +59,11 @@ export let advisorHerdrIntegrationRef = true;
53
59
  export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
54
60
  export let advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
55
61
  export let advisorRedactSecretsRef = false;
62
+ export let advisorGitContextRef: GitContextLevel = "summary";
63
+ export let advisorGitContextMaxCharsRef = DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS;
56
64
  export let advisorToolPoliciesRef: AdvisorToolPolicies = {};
65
+ export let advisorOutcomeLoggingRef = false;
66
+ export let advisorUntrackedContentRef = false;
57
67
 
58
68
  export const setExecutorRef = (ref: string) => {
59
69
  executorRef = ref;
@@ -141,9 +151,21 @@ export const setAdvisorToolResultMaxBytesRef = (value: number) => {
141
151
  export const setAdvisorRedactSecretsRef = (enabled: boolean) => {
142
152
  advisorRedactSecretsRef = enabled;
143
153
  };
154
+ export const setAdvisorGitContextRef = (level: GitContextLevel) => {
155
+ advisorGitContextRef = level;
156
+ };
157
+ export const setAdvisorGitContextMaxCharsRef = (value: number) => {
158
+ advisorGitContextMaxCharsRef = value;
159
+ };
144
160
  export const setAdvisorToolPoliciesRef = (policies: AdvisorToolPolicies) => {
145
161
  advisorToolPoliciesRef = { ...policies };
146
162
  };
163
+ export const setAdvisorOutcomeLoggingRef = (enabled: boolean) => {
164
+ advisorOutcomeLoggingRef = enabled;
165
+ };
166
+ export const setAdvisorUntrackedContentRef = (enabled: boolean) => {
167
+ advisorUntrackedContentRef = enabled;
168
+ };
147
169
 
148
170
  /**
149
171
  * Returns the current live settings state. Use this at UI boundaries instead of
@@ -160,9 +182,12 @@ export const getAdvisorSettings = () => ({
160
182
  effort: advisorEffortRef,
161
183
  failureGate: advisorFailureGateRef,
162
184
  failureMode: advisorFailureModeRef,
185
+ gitContext: advisorGitContextRef,
186
+ gitContextMaxChars: advisorGitContextMaxCharsRef,
163
187
  herdrIntegration: advisorHerdrIntegrationRef,
164
188
  loopThreshold: advisorLoopThresholdRef,
165
189
  maxCallsPerSession: advisorMaxCallsPerSessionRef,
190
+ outcomeLogging: advisorOutcomeLoggingRef,
166
191
  planGate: advisorPlanGateRef,
167
192
  redactSecrets: advisorRedactSecretsRef,
168
193
  sessionSummary: advisorSessionSummaryRef,
@@ -170,6 +195,7 @@ export const getAdvisorSettings = () => ({
170
195
  toolPolicies: { ...advisorToolPoliciesRef },
171
196
  toolResultMaxBytes: advisorToolResultMaxBytesRef,
172
197
  toolResultMaxLines: advisorToolResultMaxLinesRef,
198
+ untrackedContent: advisorUntrackedContentRef,
173
199
  });
174
200
 
175
201
  export const splitRef = (ref: string): [string, string] => {
@@ -193,15 +219,19 @@ export interface AdvisorConfig {
193
219
  advisorCustomInvocation?: string;
194
220
  advisorEffort?: string;
195
221
  advisorFailureGate?: boolean;
222
+ advisorGitContext?: GitContextLevel;
223
+ advisorGitContextMaxChars?: number;
196
224
  advisorHerdrIntegration?: boolean;
197
225
  advisorLoopThreshold?: number;
198
226
  advisorMaxCallsPerSession?: number;
227
+ advisorOutcomeLogging?: boolean;
199
228
  advisorPlanGate?: boolean;
200
229
  advisorRedactSecrets?: boolean;
201
230
  advisorSessionSummary?: boolean;
202
231
  advisorToolPolicies?: AdvisorToolPolicies;
203
232
  advisorToolResultMaxBytes?: number;
204
233
  advisorToolResultMaxLines?: number;
234
+ advisorUntrackedContent?: boolean;
205
235
  alwaysOn?: boolean;
206
236
  contextMaxChars?: number;
207
237
  executor?: string;
@@ -219,6 +249,8 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
219
249
  "advisorCustomInvocation",
220
250
  "advisorEffort",
221
251
  "advisorFailureGate",
252
+ "advisorGitContext",
253
+ "advisorGitContextMaxChars",
222
254
  "advisorHerdrIntegration",
223
255
  "advisorLoopThreshold",
224
256
  "advisorMaxCallsPerSession",
@@ -229,6 +261,8 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
229
261
  "advisorToolResultMaxBytes",
230
262
  "advisorToolResultMaxLines",
231
263
  "advisorRedactSecrets",
264
+ "advisorUntrackedContent",
265
+ "advisorOutcomeLogging",
232
266
  "advisorToolPolicies",
233
267
  "contextMaxChars",
234
268
  "executor",
@@ -247,6 +281,8 @@ const BOOLEAN_CONFIG_KEYS = [
247
281
  "alwaysOn",
248
282
  "advisorHerdrIntegration",
249
283
  "advisorRedactSecrets",
284
+ "advisorUntrackedContent",
285
+ "advisorOutcomeLogging",
250
286
  ] as const;
251
287
  const STRING_CONFIG_KEYS = [
252
288
  "executor",
@@ -347,6 +383,11 @@ const validateNumericValues = (config: ConfigRecord, path: string) => {
347
383
  isValidToolResultMaxBytes,
348
384
  "a non-negative safe integer",
349
385
  ],
386
+ [
387
+ "advisorGitContextMaxChars",
388
+ isValidToolResultMaxBytes,
389
+ "a non-negative safe integer",
390
+ ],
350
391
  ];
351
392
  for (const [key, isValid, description] of numericRules) {
352
393
  if (config[key] !== undefined && !isValid(config[key])) {
@@ -385,6 +426,16 @@ export const validateConfig = (
385
426
  ) {
386
427
  invalidConfigValue(path, "gateFailureMode", GATE_FAILURE_MODES.join(", "));
387
428
  }
429
+ if (
430
+ config.advisorGitContext !== undefined &&
431
+ !isValidGitContextLevel(config.advisorGitContext)
432
+ ) {
433
+ invalidConfigValue(
434
+ path,
435
+ "advisorGitContext",
436
+ GIT_CONTEXT_LEVELS.join(", ")
437
+ );
438
+ }
388
439
  return true;
389
440
  };
390
441
 
@@ -411,7 +462,11 @@ const resetDefaults = () => {
411
462
  advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
412
463
  advisorToolResultMaxBytesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_BYTES;
413
464
  advisorRedactSecretsRef = false;
465
+ advisorGitContextRef = "summary";
466
+ advisorGitContextMaxCharsRef = DEFAULT_ADVISOR_GIT_CONTEXT_MAX_CHARS;
414
467
  advisorToolPoliciesRef = {};
468
+ advisorOutcomeLoggingRef = false;
469
+ advisorUntrackedContentRef = false;
415
470
  };
416
471
 
417
472
  const applyOptionalConfig = <Key extends keyof AdvisorConfig>(
@@ -501,7 +556,18 @@ const applyConfig = (config: AdvisorConfig) => {
501
556
  "advisorRedactSecrets",
502
557
  setAdvisorRedactSecretsRef
503
558
  );
559
+ applyOptionalConfig(config, "advisorGitContext", setAdvisorGitContextRef);
560
+ applyOptionalConfig(
561
+ config,
562
+ "advisorGitContextMaxChars",
563
+ setAdvisorGitContextMaxCharsRef
564
+ );
504
565
  applyOptionalConfig(config, "advisorToolPolicies", setAdvisorToolPoliciesRef);
566
+ applyOptionalConfig(
567
+ config,
568
+ "advisorUntrackedContent",
569
+ setAdvisorUntrackedContentRef
570
+ );
505
571
  };
506
572
 
507
573
  const readConfig = (path: string): AdvisorConfig => {
@@ -553,17 +619,25 @@ const readConfigCached = (path: string): AdvisorConfig => {
553
619
 
554
620
  export const loadConfig = (ctx: ExtensionContext) => {
555
621
  resetDefaults();
556
- const path = configPaths(ctx).find(
622
+ const paths = configPaths(ctx);
623
+ const path = paths.find(
557
624
  (candidate): candidate is string =>
558
625
  candidate !== null && existsSync(candidate)
559
626
  );
560
- if (!path) {
561
- return null;
627
+ if (path) {
628
+ applyConfig(readConfigCached(path));
562
629
  }
563
- applyConfig(readConfigCached(path));
564
- return path;
630
+ // Persistent outcome consent is global-only: project configuration cannot enable it.
631
+ const global = join(getAgentDir(), "advisor.json");
632
+ const globalConfig = existsSync(global)
633
+ ? readConfigCached(global)
634
+ : undefined;
635
+ // Never apply this field through project-first configuration selection.
636
+ advisorOutcomeLoggingRef = globalConfig?.advisorOutcomeLogging === true;
637
+ return path ?? null;
565
638
  };
566
639
 
640
+ /** Saves the ordinary project-preferred configuration without outcome consent. */
567
641
  export const saveConfig = (ctx: ExtensionContext) => {
568
642
  const project = join(ctx.cwd, CONFIG_DIR_NAME, "advisor.json");
569
643
  const path =
@@ -600,12 +674,15 @@ export const saveConfig = (ctx: ExtensionContext) => {
600
674
  ...(advisorMaxCallsPerSessionRef === undefined
601
675
  ? {}
602
676
  : { advisorMaxCallsPerSession: advisorMaxCallsPerSessionRef }),
677
+ advisorGitContext: advisorGitContextRef,
678
+ advisorGitContextMaxChars: advisorGitContextMaxCharsRef,
603
679
  advisorHerdrIntegration: advisorHerdrIntegrationRef,
604
680
  advisorRedactSecrets: advisorRedactSecretsRef,
605
681
  advisorSessionSummary: advisorSessionSummaryRef,
606
682
  advisorToolPolicies: advisorToolPoliciesRef,
607
683
  advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
608
684
  advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
685
+ advisorUntrackedContent: advisorUntrackedContentRef,
609
686
  alwaysOn: alwaysOnRef,
610
687
  gateFailureMode: advisorFailureModeRef,
611
688
  simpleMode: simpleModeRef,
@@ -615,6 +692,26 @@ export const saveConfig = (ctx: ExtensionContext) => {
615
692
  return path;
616
693
  };
617
694
 
695
+ /** Outcome logging is deliberately written only to the global Pi configuration. */
696
+ export const saveGlobalOutcomeLogging = (enabled: boolean) => {
697
+ const path = join(getAgentDir(), "advisor.json");
698
+ let existing: Record<string, unknown> = {};
699
+ try {
700
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
701
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
702
+ existing = parsed as Record<string, unknown>;
703
+ }
704
+ } catch {
705
+ /* create it */
706
+ }
707
+ writeFileSync(
708
+ path,
709
+ `${JSON.stringify({ ...existing, advisorOutcomeLogging: enabled }, null, 2)}\n`
710
+ );
711
+ resetConfigCache();
712
+ return path;
713
+ };
714
+
618
715
  export const parseArgs = (args: string): string | undefined => {
619
716
  let nextExecutor = executorRef;
620
717
  let nextAdvisor = advisorRef;
@@ -59,6 +59,23 @@ export const redactSecrets = (value: string): string => {
59
59
  return redacted;
60
60
  };
61
61
 
62
+ /** Redacts before a byte-safe cap so no secret fragment survives truncation. */
63
+ export const redactAndCapText = (
64
+ value: string,
65
+ maxBytes: number,
66
+ redact = true
67
+ ): string => {
68
+ const source = redact ? redactSecrets(value) : value;
69
+ let result = "";
70
+ for (const character of source) {
71
+ if (byteLength(result + character) > maxBytes) {
72
+ break;
73
+ }
74
+ result += character;
75
+ }
76
+ return result;
77
+ };
78
+
62
79
  export interface ToolResultTruncation {
63
80
  content: string;
64
81
  omittedLines: number;