pi-goal-list-loop-audit 0.32.1 → 0.33.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.
@@ -48,6 +48,31 @@ 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
+ /** v0.33.0: 5-cell meter with a rounding guard (command-code's rule — never
52
+ * shows empty or full unless the value truly is 0 or 1). */
53
+ export function meter(frac: number, cells = 5): string {
54
+ if (!Number.isFinite(frac) || frac <= 0) return "▱".repeat(cells);
55
+ if (frac >= 1) return "▰".repeat(cells);
56
+ let filled = Math.round(frac * cells);
57
+ if (filled === 0) filled = 1;
58
+ if (filled === cells) filled = cells - 1;
59
+ return "▰".repeat(filled) + "▱".repeat(cells - filled);
60
+ }
61
+
62
+ /** v0.33.0: one finished tool call, for the slim card's "last action" line. */
63
+ export interface RecentActionDisplay {
64
+ name: string;
65
+ arg?: string;
66
+ ms: number;
67
+ ok: boolean;
68
+ }
69
+
70
+ /** v0.33.0: widget extras — the refire streak plus the recent-action feed. */
71
+ export interface WidgetExtras {
72
+ stalls?: number;
73
+ recent?: RecentActionDisplay[];
74
+ }
75
+
51
76
  /**
52
77
  * Word-wrap to `width`, capped at `maxLines` (v0.27.1). A pause is the one
53
78
  * state where the FULL text matters — the reason often carries a decision
@@ -134,7 +159,7 @@ export interface AuditDisplayProgress {
134
159
  * One-line status for ctx.ui.setStatus("pi-glla", …).
135
160
  * Returns undefined when nothing is being supervised (clears the segment).
136
161
  */
137
- export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, extras?: { stalls?: number }): string | undefined {
162
+ export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, extras?: WidgetExtras): string | undefined {
138
163
  if (state.loop?.active) {
139
164
  const l = state.loop;
140
165
  // v0.26.1: surface the refire streak — a spinning supervisor is the
@@ -223,7 +248,7 @@ function countTotal(g: Goal): number {
223
248
  * Widget lines for ctx.ui.setWidget("pi-glla", lines).
224
249
  * Returns undefined when nothing is worth showing.
225
250
  */
226
- export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
251
+ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] | undefined {
227
252
  const inner = buildWidgetLinesInner(state, audit, now, theme, width, extras);
228
253
  // v0.28.6 (E1): a persistence failure outranks everything — first line,
229
254
  // on every render, until a write lands again.
@@ -234,7 +259,7 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
234
259
  return inner;
235
260
  }
236
261
 
237
- function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
262
+ function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] | undefined {
238
263
  if (state.loop?.active) return loopLines(state.loop, now, theme, width, extras);
239
264
  const g = state.goal;
240
265
  const held = heldLoop(state);
@@ -242,7 +267,7 @@ function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null
242
267
  // v0.28.17: no visible goal — the held loop gets its own card.
243
268
  return held ? heldLoopLines(held, now, theme, width) : undefined;
244
269
  }
245
- const lines = goalLines(g, state, audit, now, theme, width);
270
+ const lines = goalLines(g, state, audit, now, theme, width, extras);
246
271
  // v0.28.17: a held loop rides the goal card as a trailing line.
247
272
  if (held) {
248
273
  lines.push(`${paint(theme, "warning", "⏸")} ${truncate(held.target, budgetFor(width, 3, 64))}`);
@@ -262,7 +287,7 @@ function heldLoopLines(l: LoopState, now: number, theme?: DisplayTheme, width?:
262
287
 
263
288
  // Branch lines sit flush-left (pi-tasks convention): pi's widget renderer
264
289
  // adds its own one-space gutter, so any indent here doubles up.
265
- function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme, width?: number): string[] {
290
+ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] {
266
291
  // Head glyph is ● (not ◆): U+25C6 renders as a color-emoji diamond in some
267
292
  // terminal fonts and ignores ANSI color; ● takes the paint everywhere.
268
293
  const icon =
@@ -271,22 +296,37 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
271
296
  : g.status === "auditing"
272
297
  ? paint(theme, "accent", "⟡")
273
298
  : paint(theme, "success", "●");
274
- const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), budgetFor(width, 3, 64))}`;
299
+ const headBase = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), budgetFor(width, 3, 48))}`;
275
300
  // v0.24.7: a list item is named as such and points at /list — before,
276
301
  // the widget called it "active" and hinted "/goal status", reading as if
277
302
  // queue work were a standalone goal.
278
303
  const isList = g.policy === "list";
279
304
  const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
280
- // v0.28.30: the status line ALWAYS names the type (user note: "I don't
281
- // always see the type — I'd need to scroll up to see if goal/list/loop").
282
- // Before, only list items were named; a plain goal's card said "paused ·
283
- // 3m" with no type word. The loop surface has its own card.
284
- const typeWord = isList ? "list item · " : "goal · ";
305
+ // v0.33.0: slim card — status folds INTO the head line as middot segments
306
+ // (filter(Boolean).join, the universal CLI idiom). Line 2 is the live
307
+ // "last action · next task" line; the footer stays the hint line.
308
+ // v0.28.30: the type stays visible — v0.33.0 names it via the "list item"
309
+ // header segment (list policy) and the distinct card icons (● goal,
310
+ // ∞/↓/↑ loop, ⟡ auditing, ⏸ paused) + the type-named footer verbs.
285
311
  // Token segment only when a budget is set (v0.22.0): the guard is opt-in,
286
312
  // and "0/0 tok" carried no information when off.
287
313
  const tokenLimit = g.usage?.tokensLimit ?? 0;
288
- const tokens = tokenLimit > 0 ? ` · ${paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(tokenLimit)} tok`)}` : "";
289
- const lines = [head, `├─ ${typeWord}${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))}${tokens}`];
314
+ const headSegs: string[] = [];
315
+ if (isList) headSegs.push("list item");
316
+ headSegs.push(statusWord);
317
+ headSegs.push(fmtElapsed(now - Date.parse(g.createdAt)));
318
+ const taskTotal = countTotal(g);
319
+ if (taskTotal > 0) headSegs.push(`${countDone(g)}/${taskTotal} ${paint(theme, "dim", meter(countDone(g) / taskTotal))}`);
320
+ const tokUsed0 = g.usage?.tokensUsed ?? 0;
321
+ if (tokenLimit > 0) headSegs.push(paint(theme, "dim", `${fmtTokens(tokUsed0)}/${fmtTokens(tokenLimit)} ${meter(tokUsed0 / tokenLimit)}`));
322
+ else if (tokUsed0 > 0) headSegs.push(paint(theme, "dim", `${fmtTokens(tokUsed0)} tok`));
323
+ // v0.28.30: the type stays visible — v0.33.0 names it via the "list item"
324
+ // header segment (list policy) and the distinct card icons (● goal,
325
+ // ∞/↓/↑ loop, ⟡ auditing, ⏸ paused) + the type-named footer verbs.
326
+ // Token segment only when a budget is set (v0.22.0): the guard is opt-in,
327
+ // and "0/0 tok" carried no information when off.
328
+ const head = `${headBase} ${paint(theme, "dim", "·")} ${headSegs.join(` ${paint(theme, "dim", "·")} `)}`;
329
+ const lines = [head];
290
330
  if (g.status === "auditing") {
291
331
  lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
292
332
  // v0.25.4: auditor-quiet stall — progress events stopped arriving
@@ -357,8 +397,16 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
357
397
  }
358
398
  return lines;
359
399
  }
400
+ // v0.33.0: "last action · next task" — Claude's done-row format meets the
401
+ // pending queue. Segments join with a dim middot; missing ones drop out.
402
+ const act = extras?.recent?.[extras.recent.length - 1];
403
+ const mid: string[] = [];
404
+ if (act) {
405
+ mid.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)})`)}` : ""}`);
406
+ }
360
407
  const next = nextPending(g);
361
- if (next) lines.push(`├─ next: ${truncate(next, budgetFor(width, 9, 56))}`);
408
+ if (next) mid.push(`next: ${truncate(next, budgetFor(width, 9, 40))}`);
409
+ if (mid.length > 0) lines.push(`├─ ${mid.join(` ${paint(theme, "dim", "·")} `)}`);
362
410
  const queue = state.list?.length ?? 0;
363
411
  const footer = isList
364
412
  ? `${queue > 0 ? `${queue} queued · ` : ""}/list · /glla`
@@ -367,34 +415,30 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
367
415
  return lines;
368
416
  }
369
417
 
370
- function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] {
371
- // v0.26.1: the refire streak, shown only while nonzero.
372
- const stallNote = (extras?.stalls ?? 0) > 0 ? ` · ${paint(theme, "warning", `stalls:${extras!.stalls}`)}` : "";
373
- // v0.23.0: metricless spec loop — no arrow/best/stall, no plateau.
374
- if (!l.measureCmd) {
375
- const lines = [
376
- `${paint(theme, "accent", "●")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
377
- `├─ loop ∞ iter ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations}` : ""} · ${fmtElapsed(now - Date.parse(l.startedAt))}${stallNote}`,
378
- `└─ ${paint(theme, "dim", "metricless — work the spec (no plateau)")}`,
379
- ];
380
- if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, budgetFor(width, 3, 50)))}`);
381
- return lines;
418
+ function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number, extras?: WidgetExtras): string[] {
419
+ // v0.33.0: slim loop card — header icon names the kind (∞ metricless,
420
+ // ↓/↑ metric), all state folds into middot segments; line 2 is the live
421
+ // "last action" line; footer is hints. The old per-line "loop ∞ iter" /
422
+ // "best/last/stall" rows collapse into the header.
423
+ const stallNote = (extras?.stalls ?? 0) > 0 ? ` ${paint(theme, "dim", "·")} ${paint(theme, "warning", `stalls:${extras!.stalls}`)}` : "";
424
+ const icon = !l.measureCmd ? paint(theme, "accent", "∞") : paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
425
+ const segs: string[] = [];
426
+ segs.push(`iter ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations} ${paint(theme, "dim", meter(l.iteration / l.maxIterations))}` : ""}`);
427
+ segs.push(fmtElapsed(now - Date.parse(l.startedAt)));
428
+ if (l.measureCmd) {
429
+ segs.push(`best ${paint(theme, "success", `${l.bestValue ?? "n/a"}`)}`);
430
+ const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
431
+ segs.push(l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText);
382
432
  }
383
- const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
384
- const best = paint(theme, "success", `${l.bestValue ?? "n/a"}`);
385
- const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
386
- const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
387
- const lines = [
388
- `${paint(theme, "accent", "●")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
389
- `├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
390
- `├─ best ${best} · last ${l.lastValue ?? "n/a"} · ${stall}`,
391
- // v0.29.15: the audit loop's measure is orchestrator-owned shell — the
392
- // raw grep reads like leaked internals ("that weird line"). Name what
393
- // it measures instead; user-authored measures still show raw.
394
- `└─ ${paint(theme, "dim", l.kind === "audit"
395
- ? "metric: closed findings ('- [x]' count)"
396
- : truncate(l.measureCmd, budgetFor(width, 3, 56)))}`,
397
- ];
433
+ const lines = [`${icon} ${truncate(l.target, budgetFor(width, 3, 44))} ${paint(theme, "dim", "·")} ${segs.join(` ${paint(theme, "dim", "·")} `)}${stallNote}`];
434
+ const act = extras?.recent?.[extras.recent.length - 1];
435
+ if (act) {
436
+ 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)})`)}` : ""}`);
437
+ }
438
+ const footer = !l.measureCmd
439
+ ? "metricless (no plateau) · /loop stop · /loop polish"
440
+ : `${l.kind === "audit" ? "metric: closed findings" : truncate(l.measureCmd, budgetFor(width, 3, 30))} · /loop stop`;
441
+ lines.push(`└─ ${paint(theme, "dim", footer)}`);
398
442
  if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, budgetFor(width, 3, 50)))}`);
399
443
  return lines;
400
444
  }
@@ -630,6 +630,34 @@ function isSupervising(): boolean {
630
630
  let latestAuditProgress: AuditDisplayProgress | null = null;
631
631
  let uiTicker: NodeJS.Timeout | null = null;
632
632
 
633
+ // v0.33.0: slim widget "last action" feed — a tiny ring of finished tool
634
+ // calls {name, arg, ms, ok} captured from the tool_call/tool_result stream.
635
+ // Display-only, never persisted; cleared implicitly as new actions land.
636
+ const recentActions: import("../goal-loop-display.js").RecentActionDisplay[] = [];
637
+ const inFlightToolCalls = new Map<string, { name: string; arg?: string; at: number }>();
638
+ function summarizeToolArg(name: string, input: any): string | undefined {
639
+ if (!input || typeof input !== "object") return undefined;
640
+ const v = input.file_path ?? input.path ?? input.command ?? input.pattern ?? input.query ?? input.url ?? input.title;
641
+ if (typeof v !== "string" || v.length === 0) return undefined;
642
+ const base = name === "bash" ? v : v.split("/").pop() || v;
643
+ return base.length <= 24 ? base : base.slice(0, 23) + "…";
644
+ }
645
+ function noteToolCall(event: any): void {
646
+ const name = String(event?.toolName ?? "?");
647
+ const id = String(event?.toolCallId ?? event?.id ?? `anon-${Date.now()}`);
648
+ if (inFlightToolCalls.size > 20) inFlightToolCalls.delete(inFlightToolCalls.keys().next().value!);
649
+ inFlightToolCalls.set(id, { name, arg: summarizeToolArg(name, event?.input ?? event?.args), at: Date.now() });
650
+ }
651
+ function noteToolResult(event: any): void {
652
+ const id = String(event?.toolCallId ?? event?.id ?? "");
653
+ const f = id ? inFlightToolCalls.get(id) : undefined;
654
+ if (id) inFlightToolCalls.delete(id);
655
+ const ok = !Boolean(event?.isError ?? event?.error);
656
+ const name = f?.name ?? String(event?.toolName ?? "?");
657
+ recentActions.push({ name, arg: f?.arg ?? summarizeToolArg(name, event?.input ?? event?.args), ms: f ? Date.now() - f.at : 0, ok });
658
+ if (recentActions.length > 3) recentActions.shift();
659
+ }
660
+
633
661
  function refreshUI(ctx: ExtensionContext): void {
634
662
  if (!ctx.hasUI) return;
635
663
  try {
@@ -638,7 +666,7 @@ function refreshUI(ctx: ExtensionContext): void {
638
666
  // uses the room instead of cutting at fixed ~60-char floors.
639
667
  const width = process.stdout.columns || 80;
640
668
  ctx.ui.setStatus("pi-glla", buildStatusText(state, latestAuditProgress, Date.now(), theme, { stalls: consecutiveStalls }));
641
- ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress, Date.now(), theme, width, { stalls: consecutiveStalls }));
669
+ ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress, Date.now(), theme, width, { stalls: consecutiveStalls, recent: recentActions }));
642
670
  } catch {
643
671
  // stale ctx — next event refreshes
644
672
  }
@@ -5817,6 +5845,7 @@ export default function (pi: ExtensionAPI): void {
5817
5845
  // v0.15.1: ask_user_question answers arrive as tool results, not chat
5818
5846
  // messages — count answered (non-cancelled) questionnaires as replies too.
5819
5847
  pi.on("tool_result", async (event: any) => {
5848
+ noteToolResult(event); // v0.33.0: slim widget "last action" feed
5820
5849
  // v0.24.0: roll loop tool-result fingerprints (same-tool-same-result
5821
5850
  // detection) — recorded for ANY tool result while a loop is active.
5822
5851
  if (isLoopActive()) {
@@ -6390,10 +6419,11 @@ export default function (pi: ExtensionAPI): void {
6390
6419
  scheduleContinuation(ctx, false);
6391
6420
  });
6392
6421
 
6393
- pi.on("tool_call", () => {
6422
+ pi.on("tool_call", (event: any) => {
6394
6423
  toolCallsThisTurn++;
6395
6424
  noteActivity(true);
6396
6425
  lastStreamActivityAt = Date.now();
6426
+ noteToolCall(event); // v0.33.0
6397
6427
  // v0.24.0: count loop-iteration tool calls (narration-only detection).
6398
6428
  if (isLoopActive()) {
6399
6429
  state.loop!.toolsThisTurn = (state.loop!.toolsThisTurn ?? 0) + 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
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",