pi-goal-list-loop-audit 0.19.3 → 0.20.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.
@@ -17,9 +17,11 @@ export function fmtElapsed(ms: number): string {
17
17
  const s = Math.floor(ms / 1000);
18
18
  if (s < 60) return `${s}s`;
19
19
  const m = Math.floor(s / 60);
20
- if (m < 60) return `${m}m`;
20
+ // Seconds stay visible up to the hour: the elapsed counter is the
21
+ // liveness signal — minute-only granularity looks frozen on a 1s tick.
22
+ if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
21
23
  const h = Math.floor(m / 60);
22
- return `${h}h${m % 60}m`;
24
+ return `${h}h ${String(m % 60).padStart(2, "0")}m`;
23
25
  }
24
26
 
25
27
  export function fmtTokens(n: number): string {
@@ -37,6 +39,18 @@ function sinceIso(iso: string): number {
37
39
  return Number.isFinite(t) ? Date.now() - t : 0;
38
40
  }
39
41
 
42
+ // ---- semantic colors (optional; tests call without a theme → plain strings) ----
43
+
44
+ export type DisplayColor = "accent" | "success" | "warning" | "error" | "muted" | "dim";
45
+ export interface DisplayTheme {
46
+ fg(color: DisplayColor, text: string): string;
47
+ }
48
+ const paint = (theme: DisplayTheme | undefined, color: DisplayColor, text: string): string => (theme ? theme.fg(color, text) : text);
49
+
50
+ /** Pause reasons that mean "something broke", not "waiting on the user". */
51
+ const ERROR_PAUSE = /token limit|stalled|infra|auditor.*fail/i;
52
+ const pauseIsError = (g: Goal): boolean => ERROR_PAUSE.test(g.pauseReason ?? "");
53
+
40
54
  // ---- status line (one-liner, always-on) ----
41
55
 
42
56
  export interface AuditDisplayProgress {
@@ -49,25 +63,28 @@ export interface AuditDisplayProgress {
49
63
  * One-line status for ctx.ui.setStatus("pi-glla", …).
50
64
  * Returns undefined when nothing is being supervised (clears the segment).
51
65
  */
52
- export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now()): string | undefined {
66
+ export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme): string | undefined {
53
67
  if (state.loop?.active) {
54
68
  const l = state.loop;
55
- const arrow = l.direction === "min" ? "↓" : "↑";
56
- return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations} · best ${l.bestValue ?? "n/a"} · stall ${l.stallCount}/${l.plateauWindow}`;
69
+ const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
70
+ const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
71
+ const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
72
+ return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations} · best ${l.bestValue ?? "n/a"} · ${stall}`;
57
73
  }
58
74
  const g = state.goal;
59
75
  if (!g) return undefined;
60
76
  if (g.status === "auditing") {
61
77
  const tool = audit?.currentTool ? ` · ${audit.currentTool}` : "";
62
- return `glla: auditing…${tool}`;
78
+ return `glla: ${paint(theme, "accent", "auditing…")}${tool}`;
63
79
  }
64
80
  if (g.status === "paused") {
65
- return `glla: paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
81
+ const label = `paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
82
+ return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
66
83
  }
67
84
  if (g.status === "active") {
68
85
  const queue = state.list?.length ? ` · list ${state.list.length}` : "";
69
86
  const tasks = g.taskList ? ` ${countDone(g)}/${countTotal(g)} tasks ·` : "";
70
- return `glla: ${g.policy} ●${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}`;
87
+ return `glla: ${g.policy} ${paint(theme, "success", "●")}${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}`;
71
88
  }
72
89
  return undefined; // complete/aborted → clear
73
90
  }
@@ -102,52 +119,60 @@ function countTotal(g: Goal): number {
102
119
  * Widget lines for ctx.ui.setWidget("pi-glla", lines).
103
120
  * Returns undefined when nothing is worth showing.
104
121
  */
105
- export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now()): string[] | undefined {
106
- if (state.loop?.active) return loopLines(state.loop, now);
122
+ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme): string[] | undefined {
123
+ if (state.loop?.active) return loopLines(state.loop, now, theme);
107
124
  const g = state.goal;
108
125
  if (!g) return undefined;
109
126
  if (g.status === "complete" || g.status === "aborted") return undefined;
110
- return goalLines(g, state, audit, now);
127
+ return goalLines(g, state, audit, now, theme);
111
128
  }
112
129
 
113
- function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number): string[] {
114
- const icon = g.status === "paused" ? "⏸" : g.status === "auditing" ? "⟡" : "◆";
130
+ // Branch lines indent one space (pi-tasks convention): the tree sits under
131
+ // the head glyph, text column consistent across widgets.
132
+ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme): string[] {
133
+ const icon =
134
+ g.status === "paused"
135
+ ? paint(theme, pauseIsError(g) ? "error" : "warning", "⏸")
136
+ : g.status === "auditing"
137
+ ? paint(theme, "accent", "⟡")
138
+ : paint(theme, "success", "◆");
115
139
  const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), 64)}`;
116
- const lines = [head, `├─ ${statusLabel(g.status)} · ${fmtElapsed(now - Date.parse(g.createdAt))} · ${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(g.usage?.tokensLimit ?? 10_000_000)} tok`];
140
+ const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
141
+ const tokens = paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(g.usage?.tokensLimit ?? 10_000_000)} tok`);
142
+ const lines = [head, ` ├─ ${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))} · ${tokens}`];
117
143
  if (g.status === "auditing") {
118
- lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
119
- if (audit?.elapsedMs) lines.push(`└─ ${fmtElapsed(audit.elapsedMs)} in isolated session`);
120
- else lines.push(`└─ isolated session, read-only tools`);
144
+ lines.push(` ├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
145
+ if (audit?.elapsedMs) lines.push(` └─ ${paint(theme, "dim", `${fmtElapsed(audit.elapsedMs)} in isolated session`)}`);
146
+ else lines.push(` └─ ${paint(theme, "dim", "isolated session, read-only tools")}`);
121
147
  return lines;
122
148
  }
123
149
  if (g.status === "paused" && g.pauseReason) {
124
- lines.push(`├─ ${truncate(g.pauseReason, 60)}`);
125
- if (g.pauseSuggestedAction) lines.push(`└─ ${truncate(g.pauseSuggestedAction, 60)}`);
150
+ lines.push(` ├─ ${paint(theme, pauseIsError(g) ? "error" : "warning", truncate(g.pauseReason, 60))}`);
151
+ if (g.pauseSuggestedAction) lines.push(` └─ ${paint(theme, "dim", truncate(g.pauseSuggestedAction, 60))}`);
126
152
  return lines;
127
153
  }
128
154
  const next = nextPending(g);
129
- if (next) lines.push(`├─ next: ${truncate(next, 56)}`);
155
+ if (next) lines.push(` ├─ next: ${truncate(next, 56)}`);
130
156
  const queue = state.list?.length ?? 0;
131
- lines.push(`└─ ${queue > 0 ? `list ${queue} · ` : ""}/goal status · /glla`);
157
+ lines.push(` └─ ${paint(theme, "dim", `${queue > 0 ? `list ${queue} · ` : ""}/goal status · /glla`)}`);
132
158
  return lines;
133
159
  }
134
160
 
135
- function loopLines(l: LoopState, now: number): string[] {
136
- const arrow = l.direction === "min" ? "↓" : "↑";
161
+ function loopLines(l: LoopState, now: number, theme?: DisplayTheme): string[] {
162
+ const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
163
+ const best = paint(theme, "success", `${l.bestValue ?? "n/a"}`);
164
+ const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
165
+ const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
137
166
  const lines = [
138
- `◆ ${truncate(l.target, 64)}`,
139
- `├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
140
- `├─ best ${l.bestValue ?? "n/a"} · last ${l.lastValue ?? "n/a"} · stall ${l.stallCount}/${l.plateauWindow}`,
141
- `└─ ${truncate(l.measureCmd, 56)}`,
167
+ `${paint(theme, "accent", "◆")} ${truncate(l.target, 64)}`,
168
+ ` ├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
169
+ ` ├─ best ${best} · last ${l.lastValue ?? "n/a"} · ${stall}`,
170
+ ` └─ ${paint(theme, "dim", truncate(l.measureCmd, 56))}`,
142
171
  ];
143
- if (l.branchName) lines.push(` ⎇ ${truncate(l.branchName, 50)}`);
172
+ if (l.branchName) lines.push(` ⎇ ${paint(theme, "muted", truncate(l.branchName, 50))}`);
144
173
  return lines;
145
174
  }
146
175
 
147
- function statusLabel(s: string): string {
148
- return s === "active" ? "active" : s;
149
- }
150
-
151
176
  function nextPending(g: Goal): string | undefined {
152
177
  const tasks = g.taskList?.tasks ?? [];
153
178
  const queue = [...tasks];
@@ -60,7 +60,7 @@ export interface LoopState {
60
60
  export function loopBranchName(startedAtIso: string, target: string): string {
61
61
  const stamp = startedAtIso.replace(/[^0-9]/g, "").slice(0, 14);
62
62
  const slug = target.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30) || "loop";
63
- return `pi-gla-loop/${stamp}-${slug}`;
63
+ return `pi-glla-loop/${stamp}-${slug}`;
64
64
  }
65
65
 
66
66
  export const LOOP_DEFAULTS = {
@@ -145,8 +145,9 @@ let uiTicker: NodeJS.Timeout | null = null;
145
145
  function refreshUI(ctx: ExtensionContext): void {
146
146
  if (!ctx.hasUI) return;
147
147
  try {
148
- ctx.ui.setStatus("pi-glla", buildStatusText(state, latestAuditProgress));
149
- ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress));
148
+ const theme = ctx.ui.theme as unknown as import("../goal-loop-display.js").DisplayTheme | undefined;
149
+ ctx.ui.setStatus("pi-glla", buildStatusText(state, latestAuditProgress, Date.now(), theme));
150
+ ctx.ui.setWidget("pi-glla", buildWidgetLines(state, latestAuditProgress, Date.now(), theme));
150
151
  } catch {
151
152
  // stale ctx — next event refreshes
152
153
  }
@@ -157,7 +158,7 @@ function startUITicker(): void {
157
158
  uiTicker = setInterval(() => {
158
159
  const ctx = freshCtx();
159
160
  if (ctx && isSupervising()) refreshUI(ctx);
160
- }, 5_000);
161
+ }, 1_000);
161
162
  uiTicker.unref?.();
162
163
  }
163
164
 
@@ -1018,7 +1019,7 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
1018
1019
  if (loop.branchName && outcome.kind === "continue") {
1019
1020
  if (outcome.improved) {
1020
1021
  await runGit(ctx, ["add", "-A"]);
1021
- const committed = await runGit(ctx, ["commit", "-m", `pi-gla-loop: iteration ${loop.iteration} (${loop.direction}=${loop.bestValue})`]);
1022
+ const committed = await runGit(ctx, ["commit", "-m", `pi-glla-loop: iteration ${loop.iteration} (${loop.direction}=${loop.bestValue})`]);
1022
1023
  appendLedger(ctx.cwd, "loop_git", { action: "commit", iteration: loop.iteration, ok: committed.ok });
1023
1024
  } else {
1024
1025
  const reset = await runGit(ctx, ["reset", "--hard", "HEAD"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.19.3",
3
+ "version": "0.20.1",
4
4
  "description": "Goal. Loop. Audit. Done. — 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 — only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",