pi-goal-list-loop-audit 0.28.16 → 0.28.17

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.
@@ -12,7 +12,17 @@
12
12
 
13
13
  import type { Goal, State } from "./goal-loop-core.js";
14
14
  import { isPersistenceDegraded, lastPersistenceFailure } from "./goal-loop-core.js";
15
- import type { LoopState } from "./goal-loop-forever.js";
15
+ import { HELD_ON_RESTORE, type LoopState } from "./goal-loop-forever.js";
16
+
17
+ /** v0.28.17: a loop parked by the session-restore gate (was active when the
18
+ * last session ended). Held loops must stay VISIBLE — before, only
19
+ * state.loop?.active rendered anything and a reload made the loop vanish
20
+ * from the always-on UI (user report 2026-07-29: "loops are the most
21
+ * immature"). Stopped loops (any other stopReason) stay invisible. */
22
+ function heldLoop(state: State): LoopState | undefined {
23
+ const l = state.loop;
24
+ return l && !l.active && l.stopReason === HELD_ON_RESTORE ? l : undefined;
25
+ }
16
26
 
17
27
  // ---- formatters ----
18
28
 
@@ -128,20 +138,26 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
128
138
  return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"} · best ${l.bestValue ?? "n/a"} · ${stall}${stallSuffix}`;
129
139
  }
130
140
  const g = state.goal;
131
- if (!g) return undefined;
141
+ const held = heldLoop(state);
142
+ // v0.28.17: a held loop rides every goal state as a compact suffix.
143
+ const heldSuffix = held ? paint(theme, "warning", " · loop⏸held") : "";
144
+ if (!g) {
145
+ if (held) return `glla: loop ${paint(theme, "warning", "⏸ held")} · iter ${held.iteration} — /loop to resume`;
146
+ return undefined;
147
+ }
132
148
  if (g.status === "auditing") {
133
149
  const tool = audit?.currentTool ? ` · ${audit.currentTool}` : "";
134
- return `glla: ${paint(theme, "accent", "auditing…")}${tool}`;
150
+ return `glla: ${paint(theme, "accent", "auditing…")}${tool}${heldSuffix}`;
135
151
  }
136
152
  if (g.status === "paused") {
137
153
  const label = `${g.policy} paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
138
- return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
154
+ return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}${heldSuffix}`;
139
155
  }
140
156
  if (g.status === "active") {
141
157
  // v0.28.1 (S1/S2): a stale-handle interrupt keeps the goal ACTIVE (the
142
158
  // next fresh session auto-resumes it) — say so instead of looking healthy.
143
159
  if (g.interruptedAt) {
144
- return `glla: ${g.policy} ${paint(theme, "error", "⚠ interrupted — stale handle · auto-resumes on pi restart")}`;
160
+ return `glla: ${g.policy} ${paint(theme, "error", "⚠ interrupted — stale handle · auto-resumes on pi restart")}${heldSuffix}`;
145
161
  }
146
162
  // v0.24.7: list policy gets its own wording — a queue item is not a goal.
147
163
  // v0.28.11 (U10): goal policy joins it — "list 29" read as a command
@@ -150,9 +166,11 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
150
166
  const n = state.list?.length ?? 0;
151
167
  const queue = n === 0 ? "" : ` · ${n} queued`;
152
168
  const tasks = g.taskList ? ` ${countDone(g)}/${countTotal(g)} tasks ·` : "";
153
- return `glla: ${g.policy} ${paint(theme, "success", "●")}${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}`;
169
+ return `glla: ${g.policy} ${paint(theme, "success", "●")}${tasks} ${fmtElapsed(now - Date.parse(g.createdAt))}${queue}${heldSuffix}`;
154
170
  }
155
- return undefined; // complete/aborted → clear
171
+ // complete/aborted → clear — but a held loop still shows.
172
+ if (held) return `glla: loop ${paint(theme, "warning", "⏸ held")} · iter ${held.iteration} — /loop to resume`;
173
+ return undefined;
156
174
  }
157
175
 
158
176
  function countDone(g: Goal): number {
@@ -199,9 +217,27 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
199
217
  function buildWidgetLinesInner(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme, width?: number, extras?: { stalls?: number }): string[] | undefined {
200
218
  if (state.loop?.active) return loopLines(state.loop, now, theme, width, extras);
201
219
  const g = state.goal;
202
- if (!g) return undefined;
203
- if (g.status === "complete" || g.status === "aborted") return undefined;
204
- return goalLines(g, state, audit, now, theme, width);
220
+ const held = heldLoop(state);
221
+ if (!g || g.status === "complete" || g.status === "aborted") {
222
+ // v0.28.17: no visible goal — the held loop gets its own card.
223
+ return held ? heldLoopLines(held, now, theme, width) : undefined;
224
+ }
225
+ const lines = goalLines(g, state, audit, now, theme, width);
226
+ // v0.28.17: a held loop rides the goal card as a trailing line.
227
+ if (held) {
228
+ lines.push(`${paint(theme, "warning", "⏸")} ${truncate(held.target, budgetFor(width, 3, 64))}`);
229
+ lines.push(`└─ ${paint(theme, "dim", `loop held · iter ${held.iteration} — /loop to resume`)}`);
230
+ }
231
+ return lines;
232
+ }
233
+
234
+ /** v0.28.17: standalone card for a restore-held loop (no goal visible). */
235
+ function heldLoopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number): string[] {
236
+ return [
237
+ `${paint(theme, "warning", "⏸")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
238
+ `├─ loop held · iter ${l.iteration} · ${fmtElapsed(now - Date.parse(l.startedAt))} so far`,
239
+ `└─ ${paint(theme, "dim", "held by the session-restore gate — /loop to resume, /loop stop to drop")}`,
240
+ ];
205
241
  }
206
242
 
207
243
  // Branch lines sit flush-left (pi-tasks convention): pi's widget renderer
@@ -31,6 +31,13 @@ export interface LoopRefinement {
31
31
  newMeasureCmd: string;
32
32
  }
33
33
 
34
+ /** v0.28.17: stopReason marking a loop parked by the session-restore gate
35
+ * (it was active when the last session ended; the fresh session holds it
36
+ * until the user resumes with /loop). Exported so the display layer can
37
+ * recognize held loops — they must stay VISIBLE in the status/widget,
38
+ * unlike stopped loops which are genuinely gone. */
39
+ export const HELD_ON_RESTORE = "held: restored in a fresh session";
40
+
34
41
  export interface LoopState {
35
42
  target: string;
36
43
  /** v0.23.0: optional — a metricless "spec loop" (measure=none) has no
@@ -161,6 +161,7 @@ import {
161
161
  LOOP_DEFAULTS,
162
162
  resolveSpecFiles,
163
163
  respecTarget,
164
+ HELD_ON_RESTORE,
164
165
  type LoopState,
165
166
  } from "../goal-loop-forever.js";
166
167
  import {
@@ -184,8 +185,8 @@ import {
184
185
  // =================================================================
185
186
 
186
187
  const GOAL_EVENT_ENTRY = "goal-event";
187
- /** stopReason marker for a loop held (not stopped) by the fresh-session restore gate. */
188
- const HELD_ON_RESTORE = "held: restored in a fresh session";
188
+ // HELD_ON_RESTORE (stopReason marker for a restore-held loop) lives in
189
+ // goal-loop-forever.js since v0.28.17 — the display layer imports it too.
189
190
 
190
191
  // =================================================================
191
192
  // Module-level state (one per session)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.16",
3
+ "version": "0.28.17",
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",