pi-advisor-flow 0.10.0 → 0.11.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,7 +4,16 @@ 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/).
6
6
 
7
- ## Unreleased
7
+ ## 0.11.0 - 2026-10-03
8
+
9
+ ### Added
10
+
11
+ - Native Pi Codemode composition for `ask_advisor`: scripts receive structured advice, IDs, normalized usage, and existing skip metadata, while interactive responses remain unchanged. Documented deterministic-check composition and explicit draft disclosure limits.
12
+
13
+ ### Fixed
14
+
15
+ - Advisor invocation guidelines no longer replace the whole system prompt, so prompt sections from other extensions, such as the list of MCP servers, still reach the model.
16
+ - Braces Vulnerability patched (CVE-2026-93687)
8
17
 
9
18
  ## 0.10.0 - 2026-10-02
10
19
 
package/README.md CHANGED
@@ -89,6 +89,37 @@ In the Settings, enable Simple Mode for a quick start.
89
89
 
90
90
  Unknown fields in `advisor.json` are preserved for forward compatibility and reported as non-blocking warnings. Invalid recognized values fail their own Advisor call with a clear message instead of blocking every tool call.
91
91
 
92
+ ## Pi Codemode
93
+
94
+ With Pi Codemode enabled and the Advisor flow active, call the existing tool through `tools.ask_advisor`; no separate review tool or workflow is needed:
95
+
96
+ ```js
97
+ const commands = ["bun test", "bun run typecheck"];
98
+ const checks = await Promise.all(
99
+ commands.map(async (command) => {
100
+ const result = await tools.bash({ command });
101
+ return {
102
+ command,
103
+ exit_code: result.exit_code,
104
+ truncated: result.truncated,
105
+ };
106
+ })
107
+ );
108
+ return await tools.ask_advisor({
109
+ draft: JSON.stringify({
110
+ checks,
111
+ remainingRisk: "Runtime behavior needs review.",
112
+ }),
113
+ gitContext: "full",
114
+ });
115
+ ```
116
+
117
+ Gather and filter deterministic results before paying for Advisor reasoning. Nested results are not transcript entries, so they are not automatically available to reconstructed Advisor context. Use the existing `draft` for permitted, concise summaries (8 KiB after optional redaction); these remain untrusted Executor claims, not independently verified evidence. `question` can focus a specific decision; omit it for general reviews. Use `gitContext` for patches so the user's configured disclosure ceiling applies, rather than copying a diff into the draft.
118
+
119
+ Codemode receives `{ text, adviceId?, advisor?, followUp?, usage?, jev?, skipReason? }`. `text` preserves Advisor Markdown or the existing skip notice; `usage` is the same normalized snapshot shown in response details. Skipped calls have no new `adviceId`. Provider failures and blocked calls reject. Regular consultations never become loop-gate decisions. Interactive responses and usage accounting are unchanged.
120
+
121
+ Draft text is explicit disclosure: it does **not** inherit the policies of the tools that produced it. Do not copy excluded tool output, secrets, or unconsented file bodies into `draft` or `question`. Jev may also receive the draft when screening is enabled. See [Privacy and data handling](docs/privacy.md).
122
+
92
123
  ## Usage and accounting
93
124
 
94
125
  Advisor responses show provider-reported input, output, cache, and cost details when available. Successful `ask_advisor` calls also carry normalized usage into Pi's built-in `/cost` totals. Manual consultations and automatic gates keep their own session-local accounting instead, so nothing is double-counted. Missing or partial provider usage is shown as unavailable rather than fabricated as zero. `/advisor-settings` controls both the per-response details and the optional cumulative footer independently. Configure `advisorFallbackModel` or choose **Fallback Advisor model** in the model/settings pickers to retry one failed primary request; the final response is labelled with the model that answered, and both failures are shown together.
package/dist/index.js CHANGED
@@ -8399,7 +8399,32 @@ var registerJevTurnGate = (on, registration) => {
8399
8399
  };
8400
8400
 
8401
8401
  // src/tools/register-ask-advisor.ts
8402
+ import { Type as Type2 } from "typebox";
8403
+
8404
+ // src/tools/advisor-output.ts
8402
8405
  import { Type } from "typebox";
8406
+ var advisorOutputSchema = Type.Object({
8407
+ adviceId: Type.Optional(Type.String()),
8408
+ advisor: Type.Optional(Type.String()),
8409
+ followUp: Type.Optional(Type.Boolean()),
8410
+ jev: Type.Optional(Type.Object({
8411
+ kind: Type.Union([Type.Literal("screened"), Type.Literal("repeat")]),
8412
+ reason: Type.String(),
8413
+ skipped: Type.Boolean()
8414
+ })),
8415
+ skipReason: Type.Optional(Type.String()),
8416
+ text: Type.String({
8417
+ description: "Advisor Markdown or consultation skip notice."
8418
+ }),
8419
+ usage: Type.Optional(Type.Object({
8420
+ cacheRead: Type.Optional(Type.Number()),
8421
+ cacheWrite: Type.Optional(Type.Number()),
8422
+ cost: Type.Optional(Type.Number()),
8423
+ input: Type.Optional(Type.Number()),
8424
+ output: Type.Optional(Type.Number()),
8425
+ totalTokens: Type.Optional(Type.Number())
8426
+ }))
8427
+ });
8403
8428
 
8404
8429
  // src/tools/render-advisor-result.ts
8405
8430
  import { getMarkdownTheme as getMarkdownTheme4 } from "@earendil-works/pi-coding-agent";
@@ -8580,7 +8605,8 @@ var skippedSameModelResult = (id, notice, reservedCalls, session) => {
8580
8605
  details: {
8581
8606
  skipReason: notice,
8582
8607
  text: notice
8583
- }
8608
+ },
8609
+ structuredContent: { skipReason: notice, text: notice }
8584
8610
  };
8585
8611
  };
8586
8612
  var claimTrackedHandoff = (session, includeTrackedFiles) => {
@@ -8603,7 +8629,7 @@ var registerAskAdvisorTool = ({
8603
8629
  session
8604
8630
  }) => {
8605
8631
  pi.registerTool({
8606
- description: "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. Use followUpTo with a new question to continue one cached consultation without rebuilding its redacted context. If the Advisor explicitly names a missing file, you may make a sequential fresh consultation with includeTrackedFiles when enabled and relevant.",
8632
+ description: "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. Use followUpTo with a new question to continue one cached consultation without rebuilding its redacted context. If the Advisor explicitly names a missing file, you may make a sequential fresh consultation with includeTrackedFiles when enabled and relevant. In Pi Codemode, gather and filter deterministic tool results first, then call tools.ask_advisor. Nested results are not automatically reconstructed: pass only permitted, concise summaries in draft (untrusted, capped at 8 KiB, not independently verified). Draft does not inherit source-tool disclosure policies; use gitContext for patches within the user's allowance.",
8607
8633
  async execute(_id, params, signal, onUpdate, ctx) {
8608
8634
  let followUp;
8609
8635
  try {
@@ -8648,16 +8674,18 @@ var registerAskAdvisorTool = ({
8648
8674
  const skipText = screeningSkipText(screening);
8649
8675
  session.releaseCall(_id);
8650
8676
  reservedCalls.delete(_id);
8677
+ const skipped = {
8678
+ jev: {
8679
+ kind: screening.kind,
8680
+ reason: screening.reason,
8681
+ skipped: true
8682
+ },
8683
+ text: skipText
8684
+ };
8651
8685
  return {
8652
8686
  content: [{ text: skipText, type: "text" }],
8653
- details: {
8654
- jev: {
8655
- kind: screening.kind,
8656
- reason: screening.reason,
8657
- skipped: true
8658
- },
8659
- text: skipText
8660
- }
8687
+ details: skipped,
8688
+ structuredContent: skipped
8661
8689
  };
8662
8690
  }
8663
8691
  if (sameModelAdvisorDisabled(ctx, ctx.model, followUp?.payload.model)) {
@@ -8745,6 +8773,15 @@ var registerAskAdvisorTool = ({
8745
8773
  if (usage) {
8746
8774
  details.usage = usage;
8747
8775
  }
8776
+ const structuredContent = {
8777
+ adviceId: result.adviceId,
8778
+ advisor: result.model,
8779
+ followUp: Boolean(result.followUp),
8780
+ text: result.markdown
8781
+ };
8782
+ if (usage) {
8783
+ structuredContent.usage = usage;
8784
+ }
8748
8785
  const response = {
8749
8786
  content: [
8750
8787
  {
@@ -8754,7 +8791,8 @@ ${result.markdown}`,
8754
8791
  type: "text"
8755
8792
  }
8756
8793
  ],
8757
- details
8794
+ details,
8795
+ structuredContent
8758
8796
  };
8759
8797
  if (piUsage) {
8760
8798
  response.usage = piUsage;
@@ -8783,26 +8821,27 @@ ${result.markdown}`,
8783
8821
  },
8784
8822
  label: "Ask Advisor",
8785
8823
  name: "ask_advisor",
8786
- parameters: Type.Object({
8787
- draft: Type.Optional(Type.String({
8824
+ outputSchema: advisorOutputSchema,
8825
+ parameters: Type2.Object({
8826
+ draft: Type2.Optional(Type2.String({
8788
8827
  description: "Concise untrusted draft for plan or completion review; claims are not verification evidence."
8789
8828
  })),
8790
- followUpTo: Type.Optional(Type.String({
8829
+ followUpTo: Type2.Optional(Type2.String({
8791
8830
  description: "Opaque adviceId from a prior successful consultation. Requires a new non-empty question and reuses that consultation's redacted payload; do not combine with draft, Git context, or file attachments."
8792
8831
  })),
8793
- force: Type.Optional(Type.Boolean({
8832
+ force: Type2.Optional(Type2.Boolean({
8794
8833
  description: "Set true only when you judge a decision genuinely material after a consultation was screened out; bypasses screening."
8795
8834
  })),
8796
- gitContext: Type.Optional(Type.Union([Type.Literal("none"), Type.Literal("summary"), Type.Literal("full")], {
8835
+ gitContext: Type2.Optional(Type2.Union([Type2.Literal("none"), Type2.Literal("summary"), Type2.Literal("full")], {
8797
8836
  description: "How much of the working tree to include. Use full when the review depends on the exact code changes, such as a completion review. Use summary for changed file names only, or none when the question is not about the current changes. The user's configured allowance is the ceiling and a larger request is narrowed to it."
8798
8837
  })),
8799
- includeTrackedFiles: Type.Optional(Type.Array(Type.String({
8838
+ includeTrackedFiles: Type2.Optional(Type2.Array(Type2.String({
8800
8839
  description: "Exact tracked repository-relative files to attach after the Advisor explicitly names a file it cannot review. Requires global advisorTrackedFileContent consent; current working-tree contents are sent as untrusted data."
8801
8840
  }))),
8802
- includeUntracked: Type.Optional(Type.Array(Type.String({
8841
+ includeUntracked: Type2.Optional(Type2.Array(Type2.String({
8803
8842
  description: "Exact new repository-relative files to include only when user configuration allows it."
8804
8843
  }))),
8805
- question: Type.Optional(Type.String({
8844
+ question: Type2.Optional(Type2.String({
8806
8845
  description: "The specific question or decision to get advice on. Omit this for normal reviews: the Advisor already has the conversation context."
8807
8846
  }))
8808
8847
  }),
@@ -8986,7 +9025,9 @@ var registerToolLifecycle = ({
8986
9025
  ctx.ui.setStatus("advisor-usage", undefined);
8987
9026
  }
8988
9027
  });
8989
- pi.on("before_agent_start", (_event, ctx) => {
9028
+ pi.on("before_agent_start", (event, ctx) => {
9029
+ const { sections } = event.systemPromptOptions;
9030
+ delete sections.advisor_invocation_settings;
8990
9031
  if (!pi.getActiveTools().includes("ask_advisor")) {
8991
9032
  return;
8992
9033
  }
@@ -9000,13 +9041,11 @@ var registerToolLifecycle = ({
9000
9041
  guidelines.push(`Advisor calls remaining this session: ${budget}.
9001
9042
  Reserve calls for material decisions, repeated failures, or final review.`);
9002
9043
  }
9003
- return guidelines.length > 0 ? {
9004
- systemPrompt: `${ctx.getSystemPrompt()}
9005
-
9006
- Advisor invocation settings:
9044
+ if (guidelines.length > 0) {
9045
+ sections.advisor_invocation_settings = `Advisor invocation settings:
9007
9046
  ${guidelines.map((rule) => `- ${rule}`).join(`
9008
- `)}`
9009
- } : undefined;
9047
+ `)}`;
9048
+ }
9010
9049
  });
9011
9050
  pi.on("input", () => {
9012
9051
  session.clearFollowUps();
@@ -9067,7 +9106,7 @@ ${guidelines.map((rule) => `- ${rule}`).join(`
9067
9106
 
9068
9107
  // src/tools/register-outcome.ts
9069
9108
  import { Text as Text6 } from "@earendil-works/pi-tui";
9070
- import { Type as Type2 } from "typebox";
9109
+ import { Type as Type3 } from "typebox";
9071
9110
  var registerOutcomeTool = ({
9072
9111
  appendOutcome: appendAdvisorOutcome,
9073
9112
  pi,
@@ -9121,10 +9160,10 @@ var registerOutcomeTool = ({
9121
9160
  },
9122
9161
  label: "Record Advisor Outcome",
9123
9162
  name: "record_advisor_outcome",
9124
- parameters: Type2.Object({
9125
- adoption: Type2.String({ enum: ADOPTIONS }),
9126
- adviceId: Type2.String(),
9127
- validationStatus: Type2.String({ enum: VALIDATIONS })
9163
+ parameters: Type3.Object({
9164
+ adoption: Type3.String({ enum: ADOPTIONS }),
9165
+ adviceId: Type3.String(),
9166
+ validationStatus: Type3.String({ enum: VALIDATIONS })
9128
9167
  }),
9129
9168
  renderCall: () => new Text6("[advisor] Record outcome", 0, 0),
9130
9169
  renderResult: (result) => new Text6(textFrom(result.content), 0, 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-advisor-flow",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
5
5
  "keywords": [
6
6
  "advisor",
@@ -93,6 +93,7 @@
93
93
  },
94
94
  "overrides": {
95
95
  "brace-expansion": "5.0.12",
96
+ "braces": "github:micromatch/braces#d0d575e55e74a4e0218e5248fafb79efc3e54ebb",
96
97
  "is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
97
98
  "safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
98
99
  "undici": "8.11.2"
@@ -0,0 +1,28 @@
1
+ import { Type } from "typebox";
2
+
3
+ export const advisorOutputSchema = Type.Object({
4
+ adviceId: Type.Optional(Type.String()),
5
+ advisor: Type.Optional(Type.String()),
6
+ followUp: Type.Optional(Type.Boolean()),
7
+ jev: Type.Optional(
8
+ Type.Object({
9
+ kind: Type.Union([Type.Literal("screened"), Type.Literal("repeat")]),
10
+ reason: Type.String(),
11
+ skipped: Type.Boolean(),
12
+ })
13
+ ),
14
+ skipReason: Type.Optional(Type.String()),
15
+ text: Type.String({
16
+ description: "Advisor Markdown or consultation skip notice.",
17
+ }),
18
+ usage: Type.Optional(
19
+ Type.Object({
20
+ cacheRead: Type.Optional(Type.Number()),
21
+ cacheWrite: Type.Optional(Type.Number()),
22
+ cost: Type.Optional(Type.Number()),
23
+ input: Type.Optional(Type.Number()),
24
+ output: Type.Optional(Type.Number()),
25
+ totalTokens: Type.Optional(Type.Number()),
26
+ })
27
+ ),
28
+ });
@@ -3,6 +3,7 @@ import type {
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import { Type } from "typebox";
6
+ import type { Static } from "typebox";
6
7
 
7
8
  import {
8
9
  advisorRef,
@@ -22,6 +23,7 @@ import {
22
23
  advisorUsageForPi,
23
24
  snapshotAdvisorUsage,
24
25
  } from "../usage.ts";
26
+ import { advisorOutputSchema } from "./advisor-output.ts";
25
27
  import { assertAdvisorFollowUpPrivacy } from "./consultation.ts";
26
28
  import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.ts";
27
29
  import { normalizeScreeningQuestion, screeningSkipText } from "./jev-filter.ts";
@@ -106,6 +108,7 @@ const skippedSameModelResult = (
106
108
  skipReason: notice,
107
109
  text: notice,
108
110
  },
111
+ structuredContent: { skipReason: notice, text: notice },
109
112
  };
110
113
  };
111
114
 
@@ -138,7 +141,7 @@ export const registerAskAdvisorTool = ({
138
141
  }: ToolRegistrationContext): void => {
139
142
  pi.registerTool({
140
143
  description:
141
- "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. Use followUpTo with a new question to continue one cached consultation without rebuilding its redacted context. If the Advisor explicitly names a missing file, you may make a sequential fresh consultation with includeTrackedFiles when enabled and relevant.",
144
+ "Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. Use followUpTo with a new question to continue one cached consultation without rebuilding its redacted context. If the Advisor explicitly names a missing file, you may make a sequential fresh consultation with includeTrackedFiles when enabled and relevant. In Pi Codemode, gather and filter deterministic tool results first, then call tools.ask_advisor. Nested results are not automatically reconstructed: pass only permitted, concise summaries in draft (untrusted, capped at 8 KiB, not independently verified). Draft does not inherit source-tool disclosure policies; use gitContext for patches within the user's allowance.",
142
145
  async execute(_id, params, signal, onUpdate, ctx) {
143
146
  let followUp:
144
147
  | { adviceId: string; payload: AdvisorFollowUpPayload }
@@ -200,16 +203,18 @@ export const registerAskAdvisorTool = ({
200
203
  const skipText = screeningSkipText(screening);
201
204
  session.releaseCall(_id);
202
205
  reservedCalls.delete(_id);
206
+ const skipped = {
207
+ jev: {
208
+ kind: screening.kind,
209
+ reason: screening.reason,
210
+ skipped: true,
211
+ },
212
+ text: skipText,
213
+ };
203
214
  return {
204
215
  content: [{ text: skipText, type: "text" }],
205
- details: {
206
- jev: {
207
- kind: screening.kind,
208
- reason: screening.reason,
209
- skipped: true,
210
- },
211
- text: skipText,
212
- },
216
+ details: skipped,
217
+ structuredContent: skipped,
213
218
  };
214
219
  }
215
220
  if (sameModelAdvisorDisabled(ctx, ctx.model, followUp?.payload.model)) {
@@ -332,6 +337,15 @@ export const registerAskAdvisorTool = ({
332
337
  if (usage) {
333
338
  details.usage = usage;
334
339
  }
340
+ const structuredContent: Static<typeof advisorOutputSchema> = {
341
+ adviceId: result.adviceId,
342
+ advisor: result.model,
343
+ followUp: Boolean(result.followUp),
344
+ text: result.markdown,
345
+ };
346
+ if (usage) {
347
+ structuredContent.usage = usage;
348
+ }
335
349
  const response: AgentToolResult<AdvisorToolDetails> = {
336
350
  content: [
337
351
  {
@@ -340,6 +354,7 @@ export const registerAskAdvisorTool = ({
340
354
  },
341
355
  ],
342
356
  details,
357
+ structuredContent,
343
358
  };
344
359
  if (piUsage) {
345
360
  response.usage = piUsage;
@@ -372,6 +387,7 @@ export const registerAskAdvisorTool = ({
372
387
  },
373
388
  label: "Ask Advisor",
374
389
  name: "ask_advisor",
390
+ outputSchema: advisorOutputSchema,
375
391
  parameters: Type.Object({
376
392
  draft: Type.Optional(
377
393
  Type.String({
@@ -71,7 +71,9 @@ export const registerToolLifecycle = ({
71
71
  }
72
72
  });
73
73
 
74
- pi.on("before_agent_start", (_event, ctx) => {
74
+ pi.on("before_agent_start", (event, ctx) => {
75
+ const { sections } = event.systemPromptOptions;
76
+ delete sections.advisor_invocation_settings;
75
77
  if (!pi.getActiveTools().includes("ask_advisor")) {
76
78
  return;
77
79
  }
@@ -88,11 +90,9 @@ export const registerToolLifecycle = ({
88
90
  `Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
89
91
  );
90
92
  }
91
- return guidelines.length > 0
92
- ? {
93
- systemPrompt: `${ctx.getSystemPrompt()}\n\nAdvisor invocation settings:\n${guidelines.map((rule) => `- ${rule}`).join("\n")}`,
94
- }
95
- : undefined;
93
+ if (guidelines.length > 0) {
94
+ sections.advisor_invocation_settings = `Advisor invocation settings:\n${guidelines.map((rule) => `- ${rule}`).join("\n")}`;
95
+ }
96
96
  });
97
97
 
98
98
  pi.on("input", () => {