pi-advisor-flow 0.5.6 → 0.7.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -0
  3. package/dist/index.js +3160 -1255
  4. package/extensions/index.ts +9 -9
  5. package/package.json +12 -8
  6. package/src/{untracked.ts → attachments.ts} +2 -5
  7. package/src/commands/activation-preparation.ts +6 -6
  8. package/src/commands/activation.ts +7 -7
  9. package/src/commands/lifecycle.ts +5 -5
  10. package/src/commands/manual-command.ts +13 -9
  11. package/src/commands/manual-consultation.ts +24 -10
  12. package/src/commands/manual-progress.ts +3 -3
  13. package/src/commands/model-commands.ts +6 -6
  14. package/src/commands/model-options.ts +4 -4
  15. package/src/commands/model-picker.ts +2 -2
  16. package/src/commands/registration.ts +8 -8
  17. package/src/commands/renderers.ts +5 -5
  18. package/src/commands/runtime.ts +8 -8
  19. package/src/commands/settings-commands.ts +8 -8
  20. package/src/commands/settings-persistence.ts +52 -4
  21. package/src/commands/types.ts +6 -13
  22. package/src/commands.ts +2 -2
  23. package/src/config/args.ts +3 -3
  24. package/src/config/defaults.ts +78 -3
  25. package/src/config/schema.ts +434 -0
  26. package/src/config/state.ts +71 -2
  27. package/src/config/storage.ts +27 -119
  28. package/src/config/types.ts +30 -1
  29. package/src/config/validation.ts +61 -181
  30. package/src/config.ts +7 -7
  31. package/src/content-utils.ts +30 -0
  32. package/src/conversation.ts +13 -168
  33. package/src/git.ts +4 -18
  34. package/src/herdr.ts +6 -8
  35. package/src/jev/client.ts +348 -0
  36. package/src/jev/key-store.ts +252 -0
  37. package/src/jev/ledger.ts +234 -0
  38. package/src/jev/questions.ts +119 -0
  39. package/src/jev/state.ts +47 -0
  40. package/src/jev/transport.ts +58 -0
  41. package/src/model-stream.ts +5 -8
  42. package/src/outcomes.ts +3 -2
  43. package/src/pi-settings.ts +59 -0
  44. package/src/preferences.ts +2 -2
  45. package/src/redaction.ts +50 -0
  46. package/src/scout-context.ts +121 -485
  47. package/src/scout-curation.ts +84 -0
  48. package/src/scout-groups.ts +485 -0
  49. package/src/scout-protocol.ts +122 -0
  50. package/src/scout-reconstruct.ts +44 -0
  51. package/src/scout-types.ts +66 -0
  52. package/src/scout.ts +53 -63
  53. package/src/session-state.ts +215 -78
  54. package/src/tool-result-cap.ts +88 -0
  55. package/src/tools/consult-context.ts +155 -0
  56. package/src/tools/consultation.ts +71 -220
  57. package/src/tools/gate-policy.ts +5 -5
  58. package/src/tools/gate-protocol.ts +2 -2
  59. package/src/tools/jev-filter.ts +202 -0
  60. package/src/tools/jev-turn-gate.ts +183 -0
  61. package/src/tools/loop-gate.ts +71 -61
  62. package/src/tools/outage-notifier.ts +30 -0
  63. package/src/tools/prompts.ts +4 -7
  64. package/src/tools/register-ask-advisor.ts +46 -15
  65. package/src/tools/register-lifecycle.ts +7 -7
  66. package/src/tools/register-outcome.ts +5 -5
  67. package/src/tools/register-renderers.ts +63 -5
  68. package/src/tools/registration.ts +27 -10
  69. package/src/tools/render-advisor-result.ts +54 -22
  70. package/src/tools/render-common.ts +25 -13
  71. package/src/tools/scout-status.ts +5 -5
  72. package/src/tools/session.ts +1 -1
  73. package/src/tools/types.ts +17 -7
  74. package/src/tools.ts +9 -9
  75. package/src/ui/jev-setup-submenu.ts +344 -0
  76. package/src/ui/manual-dialog-render.ts +2 -2
  77. package/src/ui/manual-dialog.ts +4 -9
  78. package/src/ui/masked-input.ts +68 -0
  79. package/src/ui/model-selector.ts +1 -1
  80. package/src/ui/settings-formatting.ts +2 -2
  81. package/src/ui/settings-items.ts +166 -5
  82. package/src/ui/settings-mutations.ts +37 -2
  83. package/src/ui/settings-selector.ts +5 -5
  84. package/src/ui/text-setting-submenu.ts +1 -1
  85. package/src/ui/types.ts +13 -2
  86. package/src/ui.ts +4 -4
  87. package/src/usage.ts +4 -7
@@ -0,0 +1,183 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { Fetch } from "@typesafe-ai/sdk";
3
+ import {
4
+ advisorJevTurnGateEveryTurnsRef,
5
+ advisorJevTurnGateNoulThresholdRef,
6
+ getAdvisorMaxCallsPerSession,
7
+ isSimpleMode,
8
+ } from "../config/state.ts";
9
+ import { herdrAdvisorActivity } from "../herdr.ts";
10
+ import { JevFailure, jevClientFromCredentials } from "../jev/client.ts";
11
+ import { composeTurnGateVerdict } from "../jev/questions.ts";
12
+ import { buildJevState } from "../jev/state.ts";
13
+ import { type JevCredentials, resolveJevTransport } from "../jev/transport.ts";
14
+ import type { AdvisorSessionState } from "../session-state.ts";
15
+ import { advisorUsageCost } from "../usage.ts";
16
+ import type { consultAdvisor } from "./consultation.ts";
17
+ import { updateAdvisorUsageStatus } from "./gate-policy.ts";
18
+ import { createOutageNotifier } from "./outage-notifier.ts";
19
+
20
+ export const turnGateQuestion = {
21
+ criteria: {
22
+ false:
23
+ "The executor is progressing soundly on work that matches the user's intent; interrupting would add nothing material.",
24
+ true: "The executor is approaching a material decision, repeating a failure, about to claim success without validation, or drifting from the user's intent; a second opinion now would change what happens next.",
25
+ },
26
+ instructions:
27
+ "Should a senior engineering advisor be consulted right now, before the executor continues? Judge from `recent_conversation`.",
28
+ type: "noul",
29
+ } as const;
30
+
31
+ export interface JevTurnGateDeps {
32
+ consult?: typeof consultAdvisor;
33
+ fetch?: Fetch;
34
+ resolveTransport?: () => Promise<JevCredentials | undefined>;
35
+ }
36
+
37
+ export interface JevTurnGateRegistration {
38
+ activeTools: () => string[];
39
+ consult: typeof consultAdvisor;
40
+ deps?: JevTurnGateDeps;
41
+ send: (message: {
42
+ content: string;
43
+ customType: string;
44
+ details: Record<string, unknown>;
45
+ display: boolean;
46
+ }) => void;
47
+ session: AdvisorSessionState;
48
+ }
49
+
50
+ const outageNotifier = createOutageNotifier(
51
+ (category, message) =>
52
+ `Advisor Jev turn gate failed (${category}); continuing without a proactive consultation. ${message}`
53
+ );
54
+
55
+ const notifyFailureOnce = outageNotifier.notify;
56
+
57
+ /** Test-only: re-arms the once-per-outage notification. */
58
+ export const resetJevTurnGateNotification = outageNotifier.reset;
59
+
60
+ /**
61
+ * turn_end handler: counts turns without a consultation and, every Nth such
62
+ * turn, asks Jev whether the Advisor should proactively weigh in. Confident
63
+ * true runs a real consultation delivered as a steer message; everything else
64
+ * keeps the status quo.
65
+ */
66
+ export const handleJevTurnEnd = async (
67
+ registration: JevTurnGateRegistration,
68
+ ctx: ExtensionContext
69
+ ): Promise<void> => {
70
+ const { session } = registration;
71
+ session.recordCompletedTurn();
72
+ const interval = advisorJevTurnGateEveryTurnsRef;
73
+ if (
74
+ interval <= 0 ||
75
+ session.turnsSinceConsultation <= 0 ||
76
+ session.turnsSinceConsultation % interval !== 0
77
+ ) {
78
+ return;
79
+ }
80
+ if (
81
+ isSimpleMode() ||
82
+ session.blocked ||
83
+ !registration.activeTools().includes("ask_advisor") ||
84
+ !session.canConsult(getAdvisorMaxCallsPerSession())
85
+ ) {
86
+ return;
87
+ }
88
+
89
+ const consult = registration.deps?.consult ?? registration.consult;
90
+ const deps = registration.deps ?? {};
91
+ try {
92
+ const credentials = await (deps.resolveTransport ?? resolveJevTransport)(
93
+ ctx
94
+ );
95
+ if (!credentials) {
96
+ session.recordJevGateFailure();
97
+ notifyFailureOnce(
98
+ ctx,
99
+ "missing-key",
100
+ "No Jev credentials resolved (no TypeSafe key and no OpenRouter login)."
101
+ );
102
+ return;
103
+ }
104
+ const client = jevClientFromCredentials(credentials, deps.fetch);
105
+ const result = await client.ask(buildJevState(ctx, {}), {
106
+ should_consult: turnGateQuestion,
107
+ });
108
+ session.recordJevGateCheck(result.usage);
109
+ const shouldConsult = composeTurnGateVerdict(
110
+ result.answers,
111
+ advisorJevTurnGateNoulThresholdRef
112
+ );
113
+ if (!shouldConsult) {
114
+ return;
115
+ }
116
+
117
+ session.consumeCall();
118
+ herdrAdvisorActivity.start();
119
+ registration.send({
120
+ content: "Proactive Advisor turn review",
121
+ customType: "advisor-turn-gate-call",
122
+ details: {
123
+ question: `Turn gate: ${session.turnsSinceConsultation} turns without a consultation`,
124
+ turn: session.sessionTurnOrdinal,
125
+ },
126
+ display: true,
127
+ });
128
+ try {
129
+ const consulted = await consult(
130
+ ctx,
131
+ undefined,
132
+ ctx.signal,
133
+ undefined,
134
+ "turn-gate"
135
+ );
136
+ session.recordJevGateConsultation();
137
+ session.resetTurnsSinceConsultation();
138
+ session.recordInvocation({
139
+ cost: advisorUsageCost(consulted.usage),
140
+ executionEffect: "continued",
141
+ kind: "markdown",
142
+ model: consulted.model,
143
+ trigger: "turn-gate",
144
+ usage: consulted.usage,
145
+ });
146
+ updateAdvisorUsageStatus(ctx, session);
147
+ registration.send({
148
+ content: consulted.markdown,
149
+ customType: "advisor-turn-gate-result",
150
+ details: {
151
+ advisor: consulted.model,
152
+ text: consulted.markdown,
153
+ usage: consulted.usage,
154
+ },
155
+ display: true,
156
+ });
157
+ } finally {
158
+ herdrAdvisorActivity.finish();
159
+ }
160
+ } catch (error) {
161
+ session.recordJevGateFailure();
162
+ if (error instanceof JevFailure) {
163
+ notifyFailureOnce(ctx, error.category, error.message);
164
+ } else if (!ctx.signal?.aborted) {
165
+ notifyFailureOnce(
166
+ ctx,
167
+ "error",
168
+ error instanceof Error ? error.message : String(error)
169
+ );
170
+ }
171
+ }
172
+ };
173
+
174
+ /** Wires the turn_end handler on an ExtensionAPI-like surface. */
175
+ export const registerJevTurnGate = <T extends string>(
176
+ on: (
177
+ event: T,
178
+ handler: (event: unknown, ctx: ExtensionContext) => unknown
179
+ ) => void,
180
+ registration: JevTurnGateRegistration
181
+ ): void => {
182
+ on("turn_end" as T, (_event, ctx) => handleJevTurnEnd(registration, ctx));
183
+ };
@@ -12,24 +12,25 @@ import {
12
12
  advisorScoutEnabledRef,
13
13
  getAdvisorMaxCallsPerSession,
14
14
  isSimpleMode,
15
- } from "../config/state.js";
16
- import { herdrAdvisorActivity } from "../herdr.js";
17
- import type { AdvisorSessionState } from "../session-state.js";
18
- import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.js";
19
- import type { runAdvisorGate } from "./consultation.js";
15
+ } from "../config/state.ts";
16
+ import type { GateFailureMode } from "../config/types.ts";
17
+ import { herdrAdvisorActivity } from "../herdr.ts";
18
+ import type { AdvisorSessionState } from "../session-state.ts";
19
+ import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.ts";
20
+ import type { runAdvisorGate } from "./consultation.ts";
20
21
  import {
21
22
  blockedDecisionEffect,
22
23
  failureEffect,
23
24
  gateDecisionEffect,
24
25
  gateFailureEffectForMode,
25
26
  updateAdvisorUsageStatus,
26
- } from "./gate-policy.js";
27
- import { adviceForGateText } from "./gate-protocol.js";
27
+ } from "./gate-policy.ts";
28
+ import { adviceForGateText } from "./gate-protocol.ts";
28
29
  import {
29
30
  appendScoutLifecycleEntry,
30
31
  type ScoutStatusManager,
31
- } from "./scout-status.js";
32
- import type { AdvisorGateResult, ScoutToolDetails } from "./types.js";
32
+ } from "./scout-status.ts";
33
+ import type { AdvisorGateResult, ScoutToolDetails } from "./types.ts";
33
34
 
34
35
  const sendAutomaticGateCall = (pi: ExtensionAPI, event: ToolCallEvent) => {
35
36
  pi.sendMessage(
@@ -87,6 +88,65 @@ const sendAutomaticGateResult = (
87
88
  );
88
89
  };
89
90
 
91
+ /** Records the invocation, reports it, and maps the gate decision to its
92
+ * tool-call result (block reason or proceed). */
93
+ const applyGateDecision = (
94
+ pi: ExtensionAPI,
95
+ ctx: ExtensionContext,
96
+ session: AdvisorSessionState,
97
+ result: Awaited<ReturnType<typeof runAdvisorGate>>,
98
+ reason: string,
99
+ failureMode: GateFailureMode
100
+ ): ToolCallEventResult | undefined => {
101
+ if (!result.ok) {
102
+ session.recordInvocation({
103
+ executionEffect: gateFailureEffectForMode(failureMode),
104
+ failure: result.category,
105
+ kind: "gate",
106
+ model: advisorRef,
107
+ trigger: "repeated-tool-call",
108
+ usage: result.usage,
109
+ });
110
+ updateAdvisorUsageStatus(ctx, session);
111
+ const failure = failureEffect(
112
+ result.category,
113
+ result.message,
114
+ ctx,
115
+ session,
116
+ failureMode
117
+ );
118
+ sendAutomaticGateFailure(
119
+ pi,
120
+ `**Advisor gate failure (${result.category}):** ${result.message}`,
121
+ result.usage
122
+ );
123
+ return failure.block
124
+ ? { block: true, reason: `${reason}\n${failure.reason}` }
125
+ : undefined;
126
+ }
127
+ session.recordInvocation({
128
+ cost: advisorUsageCost(result.usage),
129
+ decision: result.decision,
130
+ executionEffect: gateDecisionEffect(result.decision, failureMode),
131
+ kind: "gate",
132
+ model: result.model,
133
+ trigger: result.trigger,
134
+ usage: result.usage,
135
+ });
136
+ updateAdvisorUsageStatus(ctx, session);
137
+ sendAutomaticGateResult(pi, result);
138
+ if (result.decision === "proceed") {
139
+ session.resetRepetition();
140
+ return;
141
+ }
142
+ const gateReason = `Advisor loop review: ${result.markdown}`;
143
+ if (result.decision === "blocked") {
144
+ const effect = blockedDecisionEffect(gateReason, ctx, session, failureMode);
145
+ return effect.block ? { block: true, reason: effect.reason } : undefined;
146
+ }
147
+ return { block: true, reason: gateReason };
148
+ };
149
+
90
150
  export const handleAutomaticGate = async (
91
151
  pi: ExtensionAPI,
92
152
  event: ToolCallEvent,
@@ -120,6 +180,7 @@ export const handleAutomaticGate = async (
120
180
  return failure.block ? { block: true, reason: failure.reason } : undefined;
121
181
  }
122
182
  session.consumeCall();
183
+ session.resetTurnsSinceConsultation();
123
184
  herdrAdvisorActivity.start();
124
185
  let scoutDetails: ScoutToolDetails | undefined;
125
186
  const scoutStatusToken = Symbol("automatic-gate-scout");
@@ -151,58 +212,7 @@ export const handleAutomaticGate = async (
151
212
  event.toolCallId
152
213
  );
153
214
  ensureGateCall();
154
- if (!result.ok) {
155
- session.recordInvocation({
156
- executionEffect: gateFailureEffectForMode(failureMode),
157
- failure: result.category,
158
- kind: "gate",
159
- model: advisorRef,
160
- trigger: "repeated-tool-call",
161
- usage: result.usage,
162
- });
163
- updateAdvisorUsageStatus(ctx, session);
164
- const failure = failureEffect(
165
- result.category,
166
- result.message,
167
- ctx,
168
- session,
169
- failureMode
170
- );
171
- sendAutomaticGateFailure(
172
- pi,
173
- `**Advisor gate failure (${result.category}):** ${result.message}`,
174
- result.usage
175
- );
176
- return failure.block
177
- ? { block: true, reason: `${reason}\n${failure.reason}` }
178
- : undefined;
179
- }
180
- session.recordInvocation({
181
- cost: advisorUsageCost(result.usage),
182
- decision: result.decision,
183
- executionEffect: gateDecisionEffect(result.decision, failureMode),
184
- kind: "gate",
185
- model: result.model,
186
- trigger: result.trigger,
187
- usage: result.usage,
188
- });
189
- updateAdvisorUsageStatus(ctx, session);
190
- sendAutomaticGateResult(pi, result);
191
- if (result.decision === "proceed") {
192
- session.resetRepetition();
193
- return;
194
- }
195
- const gateReason = `Advisor loop review: ${result.markdown}`;
196
- if (result.decision === "blocked") {
197
- const effect = blockedDecisionEffect(
198
- gateReason,
199
- ctx,
200
- session,
201
- failureMode
202
- );
203
- return effect.block ? { block: true, reason: effect.reason } : undefined;
204
- }
205
- return { block: true, reason: gateReason };
215
+ return applyGateDecision(pi, ctx, session, result, reason, failureMode);
206
216
  } finally {
207
217
  scoutStatus.release(ctx, scoutStatusToken);
208
218
  herdrAdvisorActivity.finish();
@@ -0,0 +1,30 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface OutageNotifier {
4
+ /** Notifies once per distinct category+message; records the key even headless. */
5
+ notify: (ctx: ExtensionContext, category: string, message: string) => void;
6
+ /** Test-only: re-arms the once-per-outage notification. */
7
+ reset: () => void;
8
+ }
9
+
10
+ /** Creates an independently stateful once-per-outage notifier. */
11
+ export const createOutageNotifier = (
12
+ format: (category: string, message: string) => string
13
+ ): OutageNotifier => {
14
+ let lastKey: string | undefined;
15
+ return {
16
+ notify: (ctx, category, message) => {
17
+ const key = `${category}:${message}`;
18
+ if (key === lastKey) {
19
+ return;
20
+ }
21
+ lastKey = key;
22
+ if (ctx.hasUI) {
23
+ ctx.ui.notify(format(category, message), "warning");
24
+ }
25
+ },
26
+ reset: () => {
27
+ lastKey = undefined;
28
+ },
29
+ };
30
+ };
@@ -6,14 +6,14 @@ import {
6
6
  advisorPlanGateRef,
7
7
  contextMaxCharsRef,
8
8
  isSimpleMode,
9
- } from "../config/state.js";
10
- import { recentConversation } from "../conversation.js";
9
+ } from "../config/state.ts";
10
+ import { recentConversation } from "../conversation.ts";
11
11
  import {
12
12
  capRepositoryContext,
13
13
  escapeRepositoryText,
14
14
  type GitContextLevel,
15
15
  type GitContextResult,
16
- } from "../git.js";
16
+ } from "../git.ts";
17
17
 
18
18
  export const advisorMessageText = (
19
19
  conversation: string,
@@ -47,10 +47,7 @@ export const advisorMessageText = (
47
47
  );
48
48
  };
49
49
 
50
- /**
51
- * Splits the character budget so repository context can never starve the
52
- * conversation: it may claim its own cap or half the budget, whichever is less.
53
- */
50
+ /** Splits the character budget so repository context can never starve the conversation. */
54
51
  export const advisorGitContextBudget = (
55
52
  contextMaxChars: number,
56
53
  gitContextMaxChars: number
@@ -5,35 +5,31 @@ import {
5
5
  getAdvisorMaxCallsPerSession,
6
6
  getAdvisorSettings,
7
7
  isSimpleMode,
8
- } from "../config/state.js";
9
- import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.js";
8
+ } from "../config/state.ts";
9
+ import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "../herdr.ts";
10
10
  import {
11
11
  ADVISOR_STREAM_UPDATE_INTERVAL_MS,
12
12
  createCoalescedUpdate,
13
- } from "../model-stream.js";
13
+ } from "../model-stream.ts";
14
14
  import {
15
15
  advisorUsageCost,
16
16
  advisorUsageForPi,
17
17
  snapshotAdvisorUsage,
18
- } from "../usage.js";
19
- import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.js";
20
- import { renderAdvisorResult } from "./render-advisor-result.js";
18
+ } from "../usage.ts";
19
+ import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.ts";
20
+ import { normalizeScreeningQuestion, screeningSkipText } from "./jev-filter.ts";
21
+ import { renderAdvisorResult } from "./render-advisor-result.ts";
21
22
  import {
22
23
  renderAdvisorCallBox,
23
24
  resolveAdvisorRequest,
24
- } from "./render-common.js";
25
- import { scoutDetailsFromEvent } from "./scout-status.js";
25
+ } from "./render-common.ts";
26
+ import { scoutDetailsFromEvent } from "./scout-status.ts";
26
27
  import type {
27
28
  AdvisorToolContext,
28
29
  AdvisorToolDetails,
29
30
  ToolRegistrationContext,
30
- } from "./types.js";
31
+ } from "./types.ts";
31
32
 
32
- /**
33
- * Validates a tracked-file follow-up and consumes the one-shot handoff only
34
- * when the call is worth it: consent must be enabled and the session budget
35
- * was checked by the caller. A rejected call never burns the claim.
36
- */
37
33
  const claimTrackedHandoff = (
38
34
  session: ToolRegistrationContext["session"],
39
35
  includeTrackedFiles: string[] | undefined
@@ -57,6 +53,7 @@ export const registerAskAdvisorTool = ({
57
53
  consult: requestAdvisor,
58
54
  pi,
59
55
  reservedCalls,
56
+ screen,
60
57
  session,
61
58
  }: ToolRegistrationContext): void => {
62
59
  pi.registerTool({
@@ -71,9 +68,36 @@ export const registerAskAdvisorTool = ({
71
68
  ) {
72
69
  throw new Error("Advisor call budget exhausted for this session.");
73
70
  }
71
+ // The Jev screening seam sits after the budget re-check and before the
72
+ // handoff claim: a skipped call consumes neither the one-shot handoff
73
+ // nor the budget.
74
+ const normalizedQuestion = normalizeScreeningQuestion(
75
+ resolveAdvisorRequest(params.question)
76
+ );
77
+ const screening = await screen(ctx, session, {
78
+ draft: params.draft,
79
+ force: params.force,
80
+ question: resolveAdvisorRequest(params.question),
81
+ signal,
82
+ });
83
+ if (screening.decision === "skip") {
84
+ const skipText = screeningSkipText(screening);
85
+ return {
86
+ content: [{ text: skipText, type: "text" }],
87
+ details: {
88
+ jev: {
89
+ kind: screening.kind,
90
+ reason: screening.reason,
91
+ skipped: true,
92
+ },
93
+ text: skipText,
94
+ },
95
+ };
96
+ }
74
97
  claimTrackedHandoff(session, params.includeTrackedFiles);
75
98
  if (!isSimpleMode()) {
76
99
  session.consumeCall();
100
+ session.resetTurnsSinceConsultation();
77
101
  }
78
102
  herdrAdvisorActivity.start();
79
103
  let scoutDetails: AdvisorToolDetails["scout"];
@@ -128,7 +152,8 @@ export const registerAskAdvisorTool = ({
128
152
  result.adviceId,
129
153
  result.markdown,
130
154
  result.trigger,
131
- Boolean(result.draftBytes)
155
+ Boolean(result.draftBytes),
156
+ normalizedQuestion
132
157
  );
133
158
  session.recordInvocation({
134
159
  cost: advisorUsageCost(result.usage),
@@ -194,6 +219,12 @@ export const registerAskAdvisorTool = ({
194
219
  "Concise untrusted draft for plan or completion review; claims are not verification evidence.",
195
220
  })
196
221
  ),
222
+ force: Type.Optional(
223
+ Type.Boolean({
224
+ description:
225
+ "Set true only when you judge a decision genuinely material after a consultation was screened out; bypasses screening.",
226
+ })
227
+ ),
197
228
  gitContext: Type.Optional(
198
229
  Type.Union(
199
230
  [Type.Literal("none"), Type.Literal("summary"), Type.Literal("full")],
@@ -2,13 +2,13 @@ import {
2
2
  advisorSessionSummaryRef,
3
3
  getAdvisorMaxCallsPerSession,
4
4
  isSimpleMode,
5
- } from "../config/state.js";
6
- import { loadConfig } from "../config/storage.js";
7
- import { herdrAdvisorBlock } from "../herdr.js";
8
- import { reserveAdvisorCall } from "./gate-policy.js";
9
- import { handleAutomaticGate } from "./loop-gate.js";
10
- import { advisorInvocationGuidelines } from "./prompts.js";
11
- import type { ToolRegistrationContext } from "./types.js";
5
+ } from "../config/state.ts";
6
+ import { loadConfig } from "../config/storage.ts";
7
+ import { herdrAdvisorBlock } from "../herdr.ts";
8
+ import { reserveAdvisorCall } from "./gate-policy.ts";
9
+ import { handleAutomaticGate } from "./loop-gate.ts";
10
+ import { advisorInvocationGuidelines } from "./prompts.ts";
11
+ import type { ToolRegistrationContext } from "./types.ts";
12
12
 
13
13
  export const registerToolLifecycle = ({
14
14
  pi,
@@ -1,10 +1,10 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import { Type } from "typebox";
3
- import { advisorOutcomeLoggingRef } from "../config/state.js";
4
- import { loadConfig } from "../config/storage.js";
5
- import { textFrom } from "../conversation.js";
6
- import { ADOPTIONS, VALIDATIONS } from "../outcomes.js";
7
- import type { ToolRegistrationContext } from "./types.js";
3
+ import { advisorOutcomeLoggingRef } from "../config/state.ts";
4
+ import { loadConfig } from "../config/storage.ts";
5
+ import { textFrom } from "../conversation.ts";
6
+ import { ADOPTIONS, VALIDATIONS } from "../outcomes.ts";
7
+ import type { ToolRegistrationContext } from "./types.ts";
8
8
 
9
9
  export const registerOutcomeTool = ({
10
10
  appendOutcome: appendAdvisorOutcome,
@@ -3,11 +3,11 @@ import {
3
3
  getMarkdownTheme,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import { Box, Markdown, Text } from "@earendil-works/pi-tui";
6
- import { getAdvisorSettings } from "../config/state.js";
7
- import { formatAdvisorUsage } from "../usage.js";
8
- import { adviceForDisplay, renderAdvisorCallBox } from "./render-common.js";
9
- import { renderScoutDetails } from "./scout-status.js";
10
- import type { GateDecision, ScoutToolDetails } from "./types.js";
6
+ import { getAdvisorSettings } from "../config/state.ts";
7
+ import { formatAdvisorUsage } from "../usage.ts";
8
+ import { adviceForDisplay, renderAdvisorCallBox } from "./render-common.ts";
9
+ import { renderScoutDetails } from "./scout-status.ts";
10
+ import type { GateDecision, ScoutToolDetails } from "./types.ts";
11
11
 
12
12
  export const registerToolRenderers = (pi: ExtensionAPI): void => {
13
13
  pi.registerEntryRenderer?.(
@@ -20,6 +20,64 @@ export const registerToolRenderers = (pi: ExtensionAPI): void => {
20
20
  }
21
21
  );
22
22
 
23
+ pi.registerMessageRenderer?.(
24
+ "advisor-turn-gate-call",
25
+ (message, _options, theme) => {
26
+ const details = message.details as { question?: string } | undefined;
27
+ return renderAdvisorCallBox(details?.question, theme);
28
+ }
29
+ );
30
+
31
+ pi.registerMessageRenderer?.(
32
+ "advisor-turn-gate-result",
33
+ (message, { expanded }, theme) => {
34
+ const details = message.details as
35
+ | { advisor?: string; text?: string; usage?: unknown }
36
+ | undefined;
37
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
38
+ box.addChild(
39
+ new Text(
40
+ theme.fg("warning", theme.bold("◆ ADVISOR · TURN REVIEW")),
41
+ 0,
42
+ 0
43
+ )
44
+ );
45
+ if (details?.advisor) {
46
+ box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
47
+ }
48
+ if (getAdvisorSettings().showUsageDetails) {
49
+ const usage = formatAdvisorUsage(details?.usage);
50
+ if (usage) {
51
+ box.addChild(new Text(theme.fg("dim", ` Usage: ${usage}`), 0, 0));
52
+ }
53
+ }
54
+ if (details?.text) {
55
+ box.addChild(
56
+ new Markdown(
57
+ adviceForDisplay(details.text, Boolean(expanded)),
58
+ 0,
59
+ 0,
60
+ getMarkdownTheme()
61
+ )
62
+ );
63
+ } else {
64
+ box.addChild(
65
+ new Text(
66
+ theme.fg(
67
+ "error",
68
+ typeof message.content === "string"
69
+ ? message.content
70
+ : "Advisor turn review failed."
71
+ ),
72
+ 0,
73
+ 0
74
+ )
75
+ );
76
+ }
77
+ return box;
78
+ }
79
+ );
80
+
23
81
  pi.registerMessageRenderer?.(
24
82
  "advisor-loop-call",
25
83
  (message, _options, theme) => {