@bacnh85/pi-advisor 0.1.5 → 0.1.7

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
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7 (2026-08-30)
4
+
5
+ ### Changed
6
+
7
+ - Only nits defer during the post-steer cooldown; concerns now always steer
8
+ (wake the agent) even inside the cooldown window. A deferred concern
9
+ contradicted its own delivery template ("address this or state why it does
10
+ not apply") — the agent couldn't address what it wasn't woken for, and the
11
+ note just sat visible-but-unacted until the user's next prompt (session
12
+ 01a051c6: 1h21m). Blockers already always steered; nit ping-pong guard is
13
+ unchanged.
14
+
15
+ ## 0.1.6 (2026-08-30)
16
+
17
+ ### Fixed
18
+
19
+ - Deferred (cooldown) advisor notes are now visible immediately as a display-only
20
+ card ("Advisor (deferred — next turn)") at settle time. Previously they sat in
21
+ the SDK's pending-next-turn queue with no visual presence and only materialized
22
+ when the user's next prompt flushed them into the agent's context — looking
23
+ like the advisor stayed silent and then blurted a note out after user input
24
+ (session 01a05099, 1h37m gap). The flushed LLM message is now `display:false`
25
+ so the note never renders twice. Anti-ping-pong deferral behavior unchanged.
26
+
3
27
  ## 0.1.5 (2026-08-29)
4
28
 
5
29
  ### Added
@@ -18,6 +18,7 @@ interface NoteData {
18
18
  note: string;
19
19
  timestamp: number;
20
20
  downgraded?: boolean;
21
+ deferred?: boolean;
21
22
  }
22
23
 
23
24
  export default function piAdvisor(pi: ExtensionAPI): void {
@@ -32,7 +33,7 @@ export default function piAdvisor(pi: ExtensionAPI): void {
32
33
  ? { ...entry.data, note: sanitizeNote(entry.data.note) }
33
34
  : { severity: "nit" as Severity, note: "(unavailable)", timestamp: 0 };
34
35
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
35
- const label = data.downgraded ? "Advisor (downgraded)" : "Advisor";
36
+ const label = data.downgraded ? "Advisor (downgraded)" : data.deferred ? "Advisor (deferred — next turn)" : "Advisor";
36
37
  const sev = data.severity === "blocker"
37
38
  ? theme.fg("error", data.severity)
38
39
  : data.severity === "concern"
@@ -45,7 +46,9 @@ export default function piAdvisor(pi: ExtensionAPI): void {
45
46
  });
46
47
 
47
48
  // Message renderer for next-turn asides (LLM-visible deferred notes). Guard for
48
- // older Pi builds/tests that only mock registerEntryRenderer.
49
+ // older Pi builds/tests that only mock registerEntryRenderer. In the TUI the
50
+ // deferred message is display:false (the immediate card is the visible surface),
51
+ // so this is a fallback for non-TUI surfaces that render flushed messages.
49
52
  if (typeof (pi as unknown as { registerMessageRenderer?: unknown }).registerMessageRenderer === "function") {
50
53
  (pi as unknown as { registerMessageRenderer: typeof pi.registerEntryRenderer }).registerMessageRenderer<NoteData>(REVIEW_ENTRY, (message, { expanded }, theme) => {
51
54
  const raw = (message as unknown as { details?: unknown }).details as NoteData | undefined;
@@ -53,7 +56,7 @@ export default function piAdvisor(pi: ExtensionAPI): void {
53
56
  ? { ...raw, note: sanitizeNote(raw.note) }
54
57
  : { severity: "nit" as Severity, note: "(unavailable)", timestamp: 0 };
55
58
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
56
- const label = data.downgraded ? "Advisor (downgraded)" : "Advisor";
59
+ const label = data.downgraded ? "Advisor (downgraded)" : data.deferred ? "Advisor (deferred — next turn)" : "Advisor";
57
60
  const sev = data.severity === "blocker"
58
61
  ? theme.fg("error", data.severity)
59
62
  : data.severity === "concern"
@@ -110,6 +113,7 @@ export default function piAdvisor(pi: ExtensionAPI): void {
110
113
  await reviewTurn(runtime, ctx, {
111
114
  sendMessage: (message, options) => pi.sendMessage(message, options as never),
112
115
  sendUserMessage: (content, options) => pi.sendUserMessage(content, options),
116
+ appendEntry: (customType, data) => pi.appendEntry(customType, data),
113
117
  }, testIsolated);
114
118
  });
115
119
 
@@ -121,6 +121,8 @@ export interface WatcherHost {
121
121
  /** Defer a note as an LLM-visible next-turn aside (never wakes the agent now). */
122
122
  sendMessage(message: { customType: string; content: string; display: boolean; details?: unknown }, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }): void;
123
123
  sendUserMessage(content: string, options?: { deliverAs?: "steer" | "followUp" }): void;
124
+ /** Display-only immediate card (session entry; never enters LLM context). */
125
+ appendEntry<T = unknown>(customType: string, data?: T): void;
124
126
  }
125
127
 
126
128
  /** Injectable isolated-model call — defaults to the real one; tests pass a fake. */
@@ -192,20 +194,30 @@ export async function reviewTurn(rt: WatcherRuntime, ctx: ExtensionContext, host
192
194
  else if (verdict.severity === "blocker") rt.stats.blockers++;
193
195
  else rt.stats.concerns++;
194
196
  const isBlocker = verdict.severity === "blocker";
195
- // OMP-parity post-steer cooldown: after any note steers a turn, non-blocker
196
- // notes within the next immuneTurns settled turns are deferred to next-turn
197
- // asides rather than waking the agent again. Otherwise every new settled turn
198
- // produces fresh transcript text, so the reviewer can emit a NEW note each
199
- // cycle and the emission guard's identical-note dedupe never trips an
200
- // unbounded nit/concern ping-pong. Blockers always steer (OMP #5628: handing
201
- // off broken work must be acknowledged), and each steer (any severity) re-arms
202
- // the cooldown. Deferred asides are LLM-visible on the next user- or
203
- // blocker-driven turn never lost, only deferred.
204
- if (!isBlocker && rt.steerCooldownTurns > 0) {
197
+ const isConcern = verdict.severity === "concern";
198
+ // Post-steer cooldown: only nits defer. Concerns carry a must-address contract
199
+ // ("address this or state why it does not apply") deferring one while its own
200
+ // template claims authority contradicts the injected agent instructions, and
201
+ // waiting for the user's next prompt looks like the advisor was ignored.
202
+ // Blockers always steer (OMP #5628: handing off broken work must be
203
+ // acknowledged). Nits within the next immuneTurns settled turns after a steer
204
+ // are deferred to next-turn asides rather than waking the agent again;
205
+ // otherwise every settled turn's fresh transcript text lets the reviewer emit
206
+ // a NEW note each cycle and the identical-note dedupe never trips — an
207
+ // unbounded nit ping-pong. Each steer re-arms the cooldown. Deferred asides are
208
+ // LLM-visible on the next turn — never lost, only deferred.
209
+ if (!isBlocker && !isConcern && rt.steerCooldownTurns > 0) {
205
210
  // The cooldown ticks once per settled turn at the top of reviewTurn — no
206
211
  // extra decrement here (OMP's window is purely turn-count based).
207
212
  // nextTurn injects into the agent's context on the next turn without waking it now.
208
- host.sendMessage({ customType: REVIEW_ENTRY, content: templates[verdict.severity], display: true, details: { severity: verdict.severity, note: verdict.note, timestamp: Date.now() } }, { deliverAs: "nextTurn" });
213
+ // display:false the immediate card below is the visible surface; the flushed
214
+ // message stays LLM-only so the note doesn't render twice.
215
+ const at = Date.now();
216
+ host.sendMessage({ customType: REVIEW_ENTRY, content: templates[verdict.severity], display: false, details: { severity: verdict.severity, note: verdict.note, timestamp: at, deferred: true } }, { deliverAs: "nextTurn" });
217
+ // Immediate display-only card: without it the deferred note is invisible
218
+ // until the next user prompt flushes it, looking like the advisor stayed
219
+ // silent then blurted out a note after user input.
220
+ host.appendEntry(REVIEW_ENTRY, { severity: verdict.severity, note: verdict.note, timestamp: at, deferred: true });
209
221
  return;
210
222
  }
211
223
  // Steering delivery (blockers always steer; non-blockers steer when off-cooldown).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-advisor",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Pi extension for an automatic advisor: a second model that reviews each settled turn and injects severity-routed notes, plus an on-demand consult tool.",
5
5
  "type": "module",
6
6
  "license": "MIT",