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 +33 -7
- package/package.json +33 -30
- package/src/commands.ts +34 -4
- package/src/config.ts +102 -5
- package/src/conversation.ts +17 -0
- package/src/git.ts +190 -0
- package/src/outcomes.ts +85 -0
- package/src/preferences.ts +50 -0
- package/src/session-state.ts +36 -1
- package/src/tools.ts +276 -18
- package/src/ui.ts +104 -24
- package/src/untracked.ts +98 -0
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
|
-
|
|
5
|
+

|
|
7
6
|
|
|
8
|
-
</
|
|
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
|
-
|
|
9
|
+
</div>
|
|
11
10
|
|
|
12
|
-
This extension introduces a strategic "Executor/Advisor" workflow
|
|
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
|
-
"
|
|
45
|
-
|
|
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
|
-
"
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
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<{
|
|
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([
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
561
|
-
|
|
627
|
+
if (path) {
|
|
628
|
+
applyConfig(readConfigCached(path));
|
|
562
629
|
}
|
|
563
|
-
|
|
564
|
-
|
|
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;
|
package/src/conversation.ts
CHANGED
|
@@ -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;
|