pi-advisor-flow 0.6.0 → 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.
@@ -0,0 +1,202 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { Fetch } from "@typesafe-ai/sdk";
3
+ import {
4
+ advisorJevFilterEnabledRef,
5
+ advisorJevFilterNoulMarginRef,
6
+ advisorJevFilterOverrideWindowRef,
7
+ advisorJevFilterSkipConfidenceRef,
8
+ isSimpleMode,
9
+ } from "../config/state.ts";
10
+ import { JevFailure, jevClientFromCredentials } from "../jev/client.ts";
11
+ import { consumePlaintextKeyWarning } from "../jev/key-store.ts";
12
+ import {
13
+ composeScreeningVerdict,
14
+ screeningQuestions,
15
+ } from "../jev/questions.ts";
16
+ import { buildJevState } from "../jev/state.ts";
17
+ import { type JevCredentials, resolveJevTransport } from "../jev/transport.ts";
18
+ import type { AdvisorSessionState } from "../session-state.ts";
19
+ import { createOutageNotifier } from "./outage-notifier.ts";
20
+
21
+ export type JevSkipKind = "screened" | "repeat";
22
+
23
+ export type ScreeningOutcome =
24
+ | { decision: "allow" }
25
+ | {
26
+ decision: "skip";
27
+ kind: JevSkipKind;
28
+ reason: string;
29
+ reattachedAdvice?: string;
30
+ };
31
+
32
+ export interface ScreenConsultationOptions {
33
+ draft?: string;
34
+ force?: boolean;
35
+ question?: string;
36
+ signal?: AbortSignal;
37
+ }
38
+
39
+ export interface ScreeningDeps {
40
+ fetch?: Fetch;
41
+ resolveTransport?: () => Promise<JevCredentials | undefined>;
42
+ }
43
+
44
+ export const normalizeScreeningQuestion = (question: string | undefined) =>
45
+ question?.trim().toLowerCase().replace(/\s+/g, " ") || undefined;
46
+
47
+ const REATTACHED_ADVICE_CAP_BYTES = 4 * 1024;
48
+
49
+ const SCREENED_SKIP_TEXT =
50
+ "Advisor consultation skipped (screened out): the stakes are low and you can resolve this yourself with available tools and context. Proceed on your own judgment with what you already have.";
51
+
52
+ const repeatSkipText = (advice: string) =>
53
+ `Advisor consultation skipped (already answered): this question was answered earlier in this session; the earlier advice is reattached below. Consult again only if the situation has materially changed.\n\n${advice}`;
54
+
55
+ const outageNotifier = createOutageNotifier(
56
+ (category, message) =>
57
+ `Advisor Jev filter failed (${category}); allowing consultations. ${message}`
58
+ );
59
+
60
+ const notifyOutageOnce = outageNotifier.notify;
61
+
62
+ /** Test-only: re-arms the once-per-outage notification. */
63
+ export const resetJevOutageNotification = outageNotifier.reset;
64
+
65
+ const allow = (): ScreeningOutcome => ({ decision: "allow" });
66
+
67
+ /**
68
+ * Fail-open screening: decides run/skip for one ask_advisor call. Disabled or
69
+ * simple mode, missing credentials, and every Jev failure allow the
70
+ * consultation; only the hard conjunction (negligible-mass ≥ skip confidence
71
+ * AND confidently self-answerable) or an exact repeat skips.
72
+ */
73
+ export const screenConsultation = (
74
+ ctx: ExtensionContext,
75
+ session: AdvisorSessionState,
76
+ options: ScreenConsultationOptions,
77
+ deps: ScreeningDeps = {}
78
+ ): Promise<ScreeningOutcome> => {
79
+ if (isSimpleMode()) {
80
+ return Promise.resolve(allow());
81
+ }
82
+ // Repeat reattachment and its force/passthrough escape hatch are code-side
83
+ // and free: they run regardless of the Jev enable flag, which only gates
84
+ // the (paid, network) Jev screening call.
85
+ const normalizedQuestion = normalizeScreeningQuestion(options.question);
86
+ const bypass = bypassOutcome(session, options, normalizedQuestion);
87
+ if (bypass) {
88
+ return Promise.resolve(bypass);
89
+ }
90
+ const reattached = session.reattachedAdviceFor(normalizedQuestion);
91
+ if (reattached) {
92
+ session.recordJevFilterSkipped(true, normalizedQuestion);
93
+ return Promise.resolve({
94
+ decision: "skip",
95
+ kind: "repeat",
96
+ reason: "already answered earlier in this session",
97
+ reattachedAdvice: reattached.slice(0, REATTACHED_ADVICE_CAP_BYTES),
98
+ });
99
+ }
100
+ if (!advisorJevFilterEnabledRef) {
101
+ return Promise.resolve(allow());
102
+ }
103
+ return screenWithJev(ctx, session, options, deps, normalizedQuestion);
104
+ };
105
+
106
+ const bypassOutcome = (
107
+ session: AdvisorSessionState,
108
+ options: ScreenConsultationOptions,
109
+ normalizedQuestion: string | undefined
110
+ ): ScreeningOutcome | undefined => {
111
+ const lastSkip = session.lastJevSkip;
112
+ if (options.force) {
113
+ if (
114
+ lastSkip?.normalizedQuestion !== undefined &&
115
+ lastSkip.normalizedQuestion === normalizedQuestion
116
+ ) {
117
+ session.recordJevFilterOverride();
118
+ }
119
+ return allow();
120
+ }
121
+ if (
122
+ normalizedQuestion !== undefined &&
123
+ lastSkip?.normalizedQuestion === normalizedQuestion &&
124
+ session.sessionTurnOrdinal - lastSkip.turn <=
125
+ advisorJevFilterOverrideWindowRef
126
+ ) {
127
+ session.recordJevFilterOverride();
128
+ return allow();
129
+ }
130
+ return undefined;
131
+ };
132
+
133
+ const screenWithJev = async (
134
+ ctx: ExtensionContext,
135
+ session: AdvisorSessionState,
136
+ options: ScreenConsultationOptions,
137
+ deps: ScreeningDeps,
138
+ normalizedQuestion: string | undefined
139
+ ): Promise<ScreeningOutcome> => {
140
+ const credentials = await (deps.resolveTransport ?? resolveJevTransport)(ctx);
141
+ if (!credentials) {
142
+ session.recordJevFilterFailure();
143
+ notifyOutageOnce(
144
+ ctx,
145
+ "missing-key",
146
+ "No Jev credentials resolved (no TypeSafe key and no OpenRouter login)."
147
+ );
148
+ return allow();
149
+ }
150
+ if (credentials.source === "advisor-json") {
151
+ const warning = consumePlaintextKeyWarning();
152
+ if (warning && ctx.hasUI) {
153
+ ctx.ui.notify(warning, "warning");
154
+ }
155
+ }
156
+
157
+ const client = jevClientFromCredentials(credentials, deps.fetch);
158
+ try {
159
+ const result = await client.ask(
160
+ buildJevState(ctx, options),
161
+ screeningQuestions,
162
+ options.signal
163
+ );
164
+ session.recordJevFilterUsage(result.usage);
165
+ const verdict = composeScreeningVerdict(result.answers, {
166
+ noulMargin: advisorJevFilterNoulMarginRef,
167
+ skipConfidence: advisorJevFilterSkipConfidenceRef,
168
+ });
169
+ if (verdict.skip) {
170
+ session.recordJevFilterSkipped(false, normalizedQuestion);
171
+ return {
172
+ decision: "skip",
173
+ kind: "screened",
174
+ reason: "low stakes and resolvable without a consultation",
175
+ };
176
+ }
177
+ session.recordJevFilterAllowed();
178
+ return allow();
179
+ } catch (error) {
180
+ session.recordJevFilterFailure();
181
+ if (error instanceof JevFailure) {
182
+ notifyOutageOnce(ctx, error.category, error.message);
183
+ } else if (options.signal?.aborted) {
184
+ throw error;
185
+ } else {
186
+ notifyOutageOnce(
187
+ ctx,
188
+ "error",
189
+ error instanceof Error ? error.message : String(error)
190
+ );
191
+ }
192
+ return allow();
193
+ }
194
+ };
195
+
196
+ export const screeningSkipText = (outcome: {
197
+ kind: JevSkipKind;
198
+ reattachedAdvice?: string;
199
+ }) =>
200
+ outcome.kind === "repeat" && outcome.reattachedAdvice
201
+ ? repeatSkipText(outcome.reattachedAdvice)
202
+ : SCREENED_SKIP_TEXT;
@@ -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
+ };
@@ -180,6 +180,7 @@ export const handleAutomaticGate = async (
180
180
  return failure.block ? { block: true, reason: failure.reason } : undefined;
181
181
  }
182
182
  session.consumeCall();
183
+ session.resetTurnsSinceConsultation();
183
184
  herdrAdvisorActivity.start();
184
185
  let scoutDetails: ScoutToolDetails | undefined;
185
186
  const scoutStatusToken = Symbol("automatic-gate-scout");
@@ -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
+ };
@@ -17,6 +17,7 @@ import {
17
17
  snapshotAdvisorUsage,
18
18
  } from "../usage.ts";
19
19
  import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.ts";
20
+ import { normalizeScreeningQuestion, screeningSkipText } from "./jev-filter.ts";
20
21
  import { renderAdvisorResult } from "./render-advisor-result.ts";
21
22
  import {
22
23
  renderAdvisorCallBox,
@@ -52,6 +53,7 @@ export const registerAskAdvisorTool = ({
52
53
  consult: requestAdvisor,
53
54
  pi,
54
55
  reservedCalls,
56
+ screen,
55
57
  session,
56
58
  }: ToolRegistrationContext): void => {
57
59
  pi.registerTool({
@@ -66,9 +68,36 @@ export const registerAskAdvisorTool = ({
66
68
  ) {
67
69
  throw new Error("Advisor call budget exhausted for this session.");
68
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
+ }
69
97
  claimTrackedHandoff(session, params.includeTrackedFiles);
70
98
  if (!isSimpleMode()) {
71
99
  session.consumeCall();
100
+ session.resetTurnsSinceConsultation();
72
101
  }
73
102
  herdrAdvisorActivity.start();
74
103
  let scoutDetails: AdvisorToolDetails["scout"];
@@ -123,7 +152,8 @@ export const registerAskAdvisorTool = ({
123
152
  result.adviceId,
124
153
  result.markdown,
125
154
  result.trigger,
126
- Boolean(result.draftBytes)
155
+ Boolean(result.draftBytes),
156
+ normalizedQuestion
127
157
  );
128
158
  session.recordInvocation({
129
159
  cost: advisorUsageCost(result.usage),
@@ -189,6 +219,12 @@ export const registerAskAdvisorTool = ({
189
219
  "Concise untrusted draft for plan or completion review; claims are not verification evidence.",
190
220
  })
191
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
+ ),
192
228
  gitContext: Type.Optional(
193
229
  Type.Union(
194
230
  [Type.Literal("none"), Type.Literal("summary"), Type.Literal("full")],
@@ -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) => {
@@ -2,6 +2,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { appendOutcome } from "../outcomes.ts";
3
3
  import type { AdvisorSessionState } from "../session-state.ts";
4
4
  import { consultAdvisor, runAdvisorGate } from "./consultation.ts";
5
+ import { screenConsultation } from "./jev-filter.ts";
6
+ import { registerJevTurnGate } from "./jev-turn-gate.ts";
5
7
  import { registerAskAdvisorTool } from "./register-ask-advisor.ts";
6
8
  import { registerToolLifecycle } from "./register-lifecycle.ts";
7
9
  import { registerOutcomeTool } from "./register-outcome.ts";
@@ -25,6 +27,7 @@ export const registerAdvisorTool = (
25
27
  reservedCalls: new Set<string>(),
26
28
  runGate: dependencies.runGate ?? runAdvisorGate,
27
29
  scoutStatus: dependencies.statusManager ?? new ScoutStatusManager(),
30
+ screen: dependencies.screen ?? screenConsultation,
28
31
  session,
29
32
  };
30
33
 
@@ -32,4 +35,18 @@ export const registerAdvisorTool = (
32
35
  registerToolLifecycle(registration);
33
36
  registerAskAdvisorTool(registration);
34
37
  registerOutcomeTool(registration);
38
+ registerJevTurnGate(
39
+ (
40
+ event: Parameters<ExtensionAPI["on"]>[0],
41
+ handler: (event: unknown, ctx: never) => unknown
42
+ ) => pi.on(event, handler as never),
43
+ {
44
+ activeTools: () => pi.getActiveTools(),
45
+ consult: registration.consult,
46
+ ...(dependencies.turnGateDeps ? { deps: dependencies.turnGateDeps } : {}),
47
+ send: (message) =>
48
+ pi.sendMessage(message as never, { deliverAs: "steer" } as never),
49
+ session,
50
+ }
51
+ );
35
52
  };
@@ -29,6 +29,44 @@ const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
29
29
  context.state.phase = phase;
30
30
  };
31
31
 
32
+ const attachmentLabels = (details: AdvisorToolDetails | undefined) =>
33
+ [
34
+ details?.draftBytes
35
+ ? `Draft attached · ${details.draftBytes} B`
36
+ : undefined,
37
+ details?.preferenceBytes
38
+ ? `Project preferences attached · ${details.preferenceBytes} B`
39
+ : undefined,
40
+ details?.trackedBytes
41
+ ? `Tracked files attached · ${details.trackedBytes} B`
42
+ : undefined,
43
+ details?.untrackedBytes
44
+ ? `Untracked files attached · ${details.untrackedBytes} B`
45
+ : undefined,
46
+ ].filter((label): label is string => label !== undefined);
47
+
48
+ const renderJevSkipBox = (
49
+ box: Box,
50
+ result: AgentToolResult<AdvisorToolDetails>,
51
+ expanded: boolean,
52
+ theme: Theme
53
+ ) => {
54
+ const details = advisorResultDetails(result);
55
+ const lines = [
56
+ theme.fg("dim", theme.bold("◆ ADVISOR · SKIPPED")),
57
+ theme.fg("dim", ` ${details?.jev?.reason ?? ""}`),
58
+ ];
59
+ box.addChild(new Text(lines.join("\n"), 0, 0));
60
+ box.addChild(
61
+ new Markdown(
62
+ adviceForDisplay(textFrom(result.content), expanded),
63
+ 0,
64
+ 0,
65
+ getMarkdownTheme()
66
+ )
67
+ );
68
+ };
69
+
32
70
  const renderPartialAdvisorResult = (
33
71
  box: Box,
34
72
  result: AgentToolResult<AdvisorToolDetails>,
@@ -91,6 +129,10 @@ const renderFinalAdvisorResult = (
91
129
  context.state.timerId = undefined;
92
130
  }
93
131
  const details = advisorResultDetails(result);
132
+ if (details?.jev?.skipped) {
133
+ renderJevSkipBox(box, result, expanded, theme);
134
+ return;
135
+ }
94
136
  if (details?.scout) {
95
137
  context.state.scout = details.scout;
96
138
  }
@@ -112,20 +154,7 @@ const renderFinalAdvisorResult = (
112
154
  lines.push(theme.fg("dim", ` Usage: ${usage}`));
113
155
  }
114
156
  }
115
- const attachments = [
116
- details?.draftBytes
117
- ? `Draft attached · ${details.draftBytes} B`
118
- : undefined,
119
- details?.preferenceBytes
120
- ? `Project preferences attached · ${details.preferenceBytes} B`
121
- : undefined,
122
- details?.trackedBytes
123
- ? `Tracked files attached · ${details.trackedBytes} B`
124
- : undefined,
125
- details?.untrackedBytes
126
- ? `Untracked files attached · ${details.untrackedBytes} B`
127
- : undefined,
128
- ].filter(Boolean);
157
+ const attachments = attachmentLabels(details);
129
158
  if (attachments.length) {
130
159
  lines.push(theme.fg("dim", ` ${attachments.join(" · ")}`));
131
160
  }