pi-goal-list-loop-audit 0.33.0 → 0.33.1

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.
@@ -338,6 +338,7 @@ export async function runGoalCompletionAuditor(args: {
338
338
  } finally {
339
339
  clearInterval(stallTimer);
340
340
  unsub();
341
+ args.signal?.removeEventListener("abort", abort); // v0.33.1: don't retain the session via the signal
341
342
  // v0.32.0: dispose the auditor session — each complete_goal leaked one
342
343
  // session's subscriptions/stream resources for the parent's lifetime.
343
344
  (session as any).dispose?.();
@@ -48,6 +48,12 @@ export function truncate(s: string, max: number): string {
48
48
  return s.length <= max ? s : s.slice(0, Math.max(0, max - 1)) + "…";
49
49
  }
50
50
 
51
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
52
+ /** v0.33.1: painted strings measure by their VISIBLE width. */
53
+ function visibleLen(s: string): number {
54
+ return s.replace(ANSI_RE, "").length;
55
+ }
56
+
51
57
  /** v0.33.0: 5-cell meter with a rounding guard (command-code's rule — never
52
58
  * shows empty or full unless the value truly is 0 or 1). */
53
59
  export function meter(frac: number, cells = 5): string {
@@ -296,7 +302,6 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
296
302
  : g.status === "auditing"
297
303
  ? paint(theme, "accent", "⟡")
298
304
  : paint(theme, "success", "●");
299
- const headBase = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), budgetFor(width, 3, 48))}`;
300
305
  // v0.24.7: a list item is named as such and points at /list — before,
301
306
  // the widget called it "active" and hinted "/goal status", reading as if
302
307
  // queue work were a standalone goal.
@@ -325,7 +330,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
325
330
  // ∞/↓/↑ loop, ⟡ auditing, ⏸ paused) + the type-named footer verbs.
326
331
  // Token segment only when a budget is set (v0.22.0): the guard is opt-in,
327
332
  // and "0/0 tok" carried no information when off.
328
- const head = `${headBase} ${paint(theme, "dim", "·")} ${headSegs.join(` ${paint(theme, "dim", "·")} `)}`;
333
+ // v0.33.1: the head must FIT the terminal — the segments are fixed, so
334
+ // the objective absorbs whatever room is left (was: objective budgeted
335
+ // alone, segments appended unbudgeted → 140-col heads at width 100).
336
+ const segsText = headSegs.join(` ${paint(theme, "dim", "·")} `);
337
+ const objBudget = width && width > 0 ? Math.max(16, width - 1 - 2 - 3 - visibleLen(segsText)) : 48;
338
+ const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), objBudget)} ${paint(theme, "dim", "·")} ${segsText}`;
329
339
  const lines = [head];
330
340
  if (g.status === "auditing") {
331
341
  lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
@@ -427,10 +437,13 @@ function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: numb
427
437
  segs.push(fmtElapsed(now - Date.parse(l.startedAt)));
428
438
  if (l.measureCmd) {
429
439
  segs.push(`best ${paint(theme, "success", `${l.bestValue ?? "n/a"}`)}`);
440
+ segs.push(`last ${l.lastValue ?? "n/a"}`); // v0.33.1: a plateauing loop's current reading stays visible
430
441
  const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
431
442
  segs.push(l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText);
432
443
  }
433
- const lines = [`${icon} ${truncate(l.target, budgetFor(width, 3, 44))} ${paint(theme, "dim", "·")} ${segs.join(` ${paint(theme, "dim", "·")} `)}${stallNote}`];
444
+ const segsText = segs.join(` ${paint(theme, "dim", "·")} `);
445
+ const targetBudget = width && width > 0 ? Math.max(16, width - 1 - 2 - 3 - visibleLen(segsText) - visibleLen(stallNote)) : 44;
446
+ const lines = [`${icon} ${truncate(l.target, targetBudget)} ${paint(theme, "dim", "·")} ${segsText}${stallNote}`];
434
447
  const act = extras?.recent?.[extras.recent.length - 1];
435
448
  if (act) {
436
449
  lines.push(`├─ ${paint(theme, act.ok ? "success" : "error", act.ok ? "✓" : "✗")} ${act.name}${act.arg ? ` ${paint(theme, "dim", truncate(act.arg, 24))}` : ""}${act.ms > 0 ? ` ${paint(theme, "dim", `(${fmtElapsed(act.ms)})`)}` : ""}`);
@@ -211,6 +211,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
211
211
  "stallSimilarityThreshold",
212
212
  "postaudit",
213
213
  "toolOverrides",
214
+ "reviewer", // v0.33.1: legacy alias — menu saves can write it; provenance must know it exists
214
215
  ];
215
216
 
216
217
  /** Where each effective setting comes from (for the /glla display). */
@@ -639,13 +639,17 @@ function summarizeToolArg(name: string, input: any): string | undefined {
639
639
  if (!input || typeof input !== "object") return undefined;
640
640
  const v = input.file_path ?? input.path ?? input.command ?? input.pattern ?? input.query ?? input.url ?? input.title;
641
641
  if (typeof v !== "string" || v.length === 0) return undefined;
642
- const base = name === "bash" ? v : v.split("/").pop() || v;
642
+ // v0.33.1: strip control chars BEFORE truncating — a raw \n breaks the
643
+ // widget card into un-prefixed lines (footer spoof), a raw ESC corrupts
644
+ // the TUI. The objective path was already whitespace-collapsed; this
645
+ // path was the gap.
646
+ const base = (name === "bash" ? v : v.split("/").pop() || v).replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
643
647
  return base.length <= 24 ? base : base.slice(0, 23) + "…";
644
648
  }
645
649
  function noteToolCall(event: any): void {
646
650
  const name = String(event?.toolName ?? "?");
647
651
  const id = String(event?.toolCallId ?? event?.id ?? `anon-${Date.now()}`);
648
- if (inFlightToolCalls.size > 20) inFlightToolCalls.delete(inFlightToolCalls.keys().next().value!);
652
+ if (inFlightToolCalls.size >= 20) inFlightToolCalls.delete(inFlightToolCalls.keys().next().value!); // v0.33.1: evict BEFORE the 21st
649
653
  inFlightToolCalls.set(id, { name, arg: summarizeToolArg(name, event?.input ?? event?.args), at: Date.now() });
650
654
  }
651
655
  function noteToolResult(event: any): void {
@@ -865,6 +869,14 @@ function heartbeatTick(): void {
865
869
  if (!absorbStaleIfSuperseded(ctx)) goStaleTerminal(ctx, "heartbeat probe");
866
870
  return;
867
871
  }
872
+ // v0.33.1: nothing supervised → the compact debt/resync belong to a dead
873
+ // goal/loop. Discharge here so a later goal can't inherit a bogus RESYNC
874
+ // block or a spurious forced refire (the old in-guard `else` was
875
+ // unreachable — isSupervising() ≡ isLoopActive() || isActionableGoal()).
876
+ if (!isSupervising() && (postCompactResumeOwed || postCompactResyncPending)) {
877
+ postCompactResumeOwed = false;
878
+ postCompactResyncPending = false;
879
+ }
868
880
  // v0.32.1: post-compaction resume debt — retry on every heartbeat tick
869
881
  // past grace until a turn actually starts. Fixed-offset settles alone
870
882
  // can both lose (pi busy at 2s AND at grace+2s = a dangling chain).
@@ -1104,7 +1116,10 @@ function sendContinuation(goalId: string): void {
1104
1116
  }
1105
1117
  if (!extensionApi || extensionApiStale) return;
1106
1118
  try {
1107
- const resync = postCompactResyncPending ? buildPostCompactResync() : "";
1119
+ let resync = "";
1120
+ // v0.33.1: a builder throw (corrupt restored state) must not masquerade
1121
+ // as a transport failure — send without the block instead.
1122
+ if (postCompactResyncPending) { try { resync = buildPostCompactResync(); } catch { resync = ""; } }
1108
1123
  extensionApi.sendMessage({
1109
1124
  customType: GOAL_EVENT_ENTRY,
1110
1125
  content: resync + continuationPrompt(state.goal!),
@@ -1279,6 +1294,14 @@ function notifyPersistenceState(ctx: ExtensionContext): void {
1279
1294
  }
1280
1295
 
1281
1296
  function setGoal(goal: Goal, ctx: ExtensionContext, via = "user"): void {
1297
+ // v0.33.1: per-goal module state resets at activation — a new goal must
1298
+ // not inherit the previous goal's compact debt/resync, quota streak,
1299
+ // token-message dedupe set, or widget action feed.
1300
+ postCompactResumeOwed = false;
1301
+ postCompactResyncPending = false;
1302
+ quotaRetryStreak = 0;
1303
+ countedTokenMessages.clear();
1304
+ recentActions.length = 0;
1282
1305
  // v0.28.14: never silently orphan a live goal — a paused/active goal
1283
1306
  // being replaced is archived honestly first (the old behavior left it in
1284
1307
  // goals/ but untracked: "older goals lying around leading to confusion").
@@ -1397,6 +1420,8 @@ async function fanOutListAuditFindings(ctx: ExtensionContext): Promise<void> {
1397
1420
  }
1398
1421
 
1399
1422
  function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?: string): void {
1423
+ postCompactResumeOwed = false; // v0.33.1: the dead goal's compact debt/resync dies with it
1424
+ postCompactResyncPending = false;
1400
1425
  if (!state.goal) return;
1401
1426
  const goal = state.goal;
1402
1427
  ensureDirs(ctx.cwd);
@@ -2638,7 +2663,13 @@ function sendLoopTurn(): void {
2638
2663
  if (!isLoopActive() || !extensionApi) return;
2639
2664
  const ctx = freshCtx();
2640
2665
  if (!ctx || !ctx.isIdle() || ctx.hasPendingMessages()) {
2641
- if (ctx) accountSendRearm(ctx, "loop");
2666
+ if (!ctx) {
2667
+ // v0.33.1: mirror sendContinuation — probe the handle (terminal exit)
2668
+ // and advance the streak so the cadence backs off instead of spinning
2669
+ // a flat 50ms below every watchdog.
2670
+ if (probeExtensionApiStale()) return;
2671
+ loopRearmStreak++;
2672
+ } else accountSendRearm(ctx, "loop");
2642
2673
  loopTimer = setTimeout(() => sendLoopTurn(), sendRearmDelayMs(loopRearmStreak)); // v0.28.29: backing-off cadence
2643
2674
  loopTimer.unref?.();
2644
2675
  return;
@@ -2694,7 +2725,8 @@ function sendLoopTurn(): void {
2694
2725
  // instruction (metricless loops; metric loops already vary via values).
2695
2726
  const variantNote = metricless ? continueVariant(loop.iteration) : "";
2696
2727
  try {
2697
- const loopResync = postCompactResyncPending ? buildPostCompactResync() : "";
2728
+ let loopResync = "";
2729
+ if (postCompactResyncPending) { try { loopResync = buildPostCompactResync(); } catch { loopResync = ""; } } // v0.33.1
2698
2730
  extensionApi.sendMessage({
2699
2731
  customType: GOAL_EVENT_ENTRY,
2700
2732
  content: loopResync + loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote),
@@ -5920,6 +5952,9 @@ export default function (pi: ExtensionAPI): void {
5920
5952
  writeOwnerFile(ctx.cwd);
5921
5953
  sessionReplacementUntil = 0;
5922
5954
  zombieStoodDown = false;
5955
+ staleTerminalDone = false; // v0.33.1: a rebound session must be able to go terminal AGAIN (was: one-shot for the process lifetime)
5956
+ postCompactResumeOwed = false; // v0.33.1: a compact from a previous session must not resync THIS one
5957
+ postCompactResyncPending = false;
5923
5958
  const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
5924
5959
  appendLedger(ctx.cwd, "session_rebound", { reason: startReason });
5925
5960
  if (extensionApiStale) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "description": "Goal. Loop. Audit. Done. \u2014 a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor \u2014 only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",