pi-goal-list-loop-audit 0.19.3 → 0.20.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.
@@ -37,6 +37,18 @@ function sinceIso(iso: string): number {
37
37
  return Number.isFinite(t) ? Date.now() - t : 0;
38
38
  }
39
39
 
40
+ // ---- semantic colors (optional; tests call without a theme → plain strings) ----
41
+
42
+ export type DisplayColor = "accent" | "success" | "warning" | "error" | "muted" | "dim";
43
+ export interface DisplayTheme {
44
+ fg(color: DisplayColor, text: string): string;
45
+ }
46
+ const paint = (theme: DisplayTheme | undefined, color: DisplayColor, text: string): string => (theme ? theme.fg(color, text) : text);
47
+
48
+ /** Pause reasons that mean "something broke", not "waiting on the user". */
49
+ const ERROR_PAUSE = /token limit|stalled|infra|auditor.*fail/i;
50
+ const pauseIsError = (g: Goal): boolean => ERROR_PAUSE.test(g.pauseReason ?? "");
51
+
40
52
  // ---- status line (one-liner, always-on) ----
41
53
 
42
54
  export interface AuditDisplayProgress {
@@ -49,25 +61,28 @@ export interface AuditDisplayProgress {
49
61
  * One-line status for ctx.ui.setStatus("pi-glla", …).
50
62
  * Returns undefined when nothing is being supervised (clears the segment).
51
63
  */
52
- export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now()): string | undefined {
64
+ export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme): string | undefined {
53
65
  if (state.loop?.active) {
54
66
  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}`;
67
+ const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
68
+ const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
69
+ const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
70
+ return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations} · best ${l.bestValue ?? "n/a"} · ${stall}`;
57
71
  }
58
72
  const g = state.goal;
59
73
  if (!g) return undefined;
60
74
  if (g.status === "auditing") {
61
75
  const tool = audit?.currentTool ? ` · ${audit.currentTool}` : "";
62
- return `glla: auditing…${tool}`;
76
+ return `glla: ${paint(theme, "accent", "auditing…")}${tool}`;
63
77
  }
64
78
  if (g.status === "paused") {
65
- return `glla: paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
79
+ const label = `paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
80
+ return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
66
81
  }
67
82
  if (g.status === "active") {
68
83
  const queue = state.list?.length ? ` · list ${state.list.length}` : "";
69
84
  const tasks = g.taskList ? ` ${countDone(g)}/${countTotal(g)} tasks ·` : "";
70
- return `glla: ${g.policy} ●${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}`;
85
+ return `glla: ${g.policy} ${paint(theme, "success", "●")}${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}`;
71
86
  }
72
87
  return undefined; // complete/aborted → clear
73
88
  }
@@ -102,52 +117,60 @@ function countTotal(g: Goal): number {
102
117
  * Widget lines for ctx.ui.setWidget("pi-glla", lines).
103
118
  * Returns undefined when nothing is worth showing.
104
119
  */
105
- export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now()): string[] | undefined {
106
- if (state.loop?.active) return loopLines(state.loop, now);
120
+ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme): string[] | undefined {
121
+ if (state.loop?.active) return loopLines(state.loop, now, theme);
107
122
  const g = state.goal;
108
123
  if (!g) return undefined;
109
124
  if (g.status === "complete" || g.status === "aborted") return undefined;
110
- return goalLines(g, state, audit, now);
125
+ return goalLines(g, state, audit, now, theme);
111
126
  }
112
127
 
113
- function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number): string[] {
114
- const icon = g.status === "paused" ? "⏸" : g.status === "auditing" ? "⟡" : "◆";
128
+ // Branch lines indent one space (pi-tasks convention): the tree sits under
129
+ // the head glyph, text column consistent across widgets.
130
+ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | undefined, now: number, theme?: DisplayTheme): string[] {
131
+ const icon =
132
+ g.status === "paused"
133
+ ? paint(theme, pauseIsError(g) ? "error" : "warning", "⏸")
134
+ : g.status === "auditing"
135
+ ? paint(theme, "accent", "⟡")
136
+ : paint(theme, "success", "◆");
115
137
  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`];
138
+ const statusWord = g.status === "active" ? paint(theme, "success", "active") : g.status;
139
+ const tokens = paint(theme, "dim", `${fmtTokens(g.usage?.tokensUsed ?? 0)}/${fmtTokens(g.usage?.tokensLimit ?? 10_000_000)} tok`);
140
+ const lines = [head, ` ├─ ${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))} · ${tokens}`];
117
141
  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`);
142
+ lines.push(` ├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
143
+ if (audit?.elapsedMs) lines.push(` └─ ${paint(theme, "dim", `${fmtElapsed(audit.elapsedMs)} in isolated session`)}`);
144
+ else lines.push(` └─ ${paint(theme, "dim", "isolated session, read-only tools")}`);
121
145
  return lines;
122
146
  }
123
147
  if (g.status === "paused" && g.pauseReason) {
124
- lines.push(`├─ ${truncate(g.pauseReason, 60)}`);
125
- if (g.pauseSuggestedAction) lines.push(`└─ ${truncate(g.pauseSuggestedAction, 60)}`);
148
+ lines.push(` ├─ ${paint(theme, pauseIsError(g) ? "error" : "warning", truncate(g.pauseReason, 60))}`);
149
+ if (g.pauseSuggestedAction) lines.push(` └─ ${paint(theme, "dim", truncate(g.pauseSuggestedAction, 60))}`);
126
150
  return lines;
127
151
  }
128
152
  const next = nextPending(g);
129
- if (next) lines.push(`├─ next: ${truncate(next, 56)}`);
153
+ if (next) lines.push(` ├─ next: ${truncate(next, 56)}`);
130
154
  const queue = state.list?.length ?? 0;
131
- lines.push(`└─ ${queue > 0 ? `list ${queue} · ` : ""}/goal status · /glla`);
155
+ lines.push(` └─ ${paint(theme, "dim", `${queue > 0 ? `list ${queue} · ` : ""}/goal status · /glla`)}`);
132
156
  return lines;
133
157
  }
134
158
 
135
- function loopLines(l: LoopState, now: number): string[] {
136
- const arrow = l.direction === "min" ? "↓" : "↑";
159
+ function loopLines(l: LoopState, now: number, theme?: DisplayTheme): string[] {
160
+ const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
161
+ const best = paint(theme, "success", `${l.bestValue ?? "n/a"}`);
162
+ const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
163
+ const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
137
164
  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)}`,
165
+ `${paint(theme, "accent", "◆")} ${truncate(l.target, 64)}`,
166
+ ` ├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
167
+ ` ├─ best ${best} · last ${l.lastValue ?? "n/a"} · ${stall}`,
168
+ ` └─ ${paint(theme, "dim", truncate(l.measureCmd, 56))}`,
142
169
  ];
143
- if (l.branchName) lines.push(` ⎇ ${truncate(l.branchName, 50)}`);
170
+ if (l.branchName) lines.push(` ⎇ ${paint(theme, "muted", truncate(l.branchName, 50))}`);
144
171
  return lines;
145
172
  }
146
173
 
147
- function statusLabel(s: string): string {
148
- return s === "active" ? "active" : s;
149
- }
150
-
151
174
  function nextPending(g: Goal): string | undefined {
152
175
  const tasks = g.taskList?.tasks ?? [];
153
176
  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
  }
@@ -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.0",
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",