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.
- package/extensions/goal-loop-display.ts +85 -41
- package/extensions/loops/goal.ts +32 -2
- package/package.json +1 -1
|
@@ -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?:
|
|
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?:
|
|
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?:
|
|
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
|
|
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.
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
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
|
|
289
|
-
|
|
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)
|
|
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?:
|
|
371
|
-
// v0.
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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.
|
|
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",
|