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

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
@@ -118,6 +118,11 @@ export const DEFAULT_SETTINGS: Settings = {
118
118
  };
119
119
 
120
120
  export function globalSettingsPath(): string {
121
+ // v0.28.18: test/embedding override — the suite must be hermetic from
122
+ // the developer's real global settings file (a user setting autoAccept
123
+ // globally once made draft-Confirm tests auto-accept and fail).
124
+ const override = process.env.GLLA_GLOBAL_SETTINGS_PATH;
125
+ if (override) return override;
121
126
  return path.join(os.homedir(), ".pi", "agent", "pi-goal-list-loop-audit.settings.json");
122
127
  }
123
128
 
@@ -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)
@@ -337,8 +337,10 @@ export function buildSettingsRows(
337
337
  // TUI table component
338
338
  // =================================================================
339
339
 
340
- /** Padding + gutter between columns. */
341
- const COL_GUTTER = 2;
340
+ /** Column separator (v0.28.18: box-drawing — the menu reads as a table). */
341
+ const COL_SEP = " │ ";
342
+ /** Header-rule junction matching COL_SEP's visible width. */
343
+ const COL_RULE_SEP = "─┼─";
342
344
 
343
345
  /** Maximum width for each fixed column before truncation kicks in. */
344
346
  const MAX_KEY_W = 32;
@@ -450,15 +452,20 @@ export class SettingsMenuComponent implements Component {
450
452
  let keyW = visibleWidth(this.theme.bold("KEY"));
451
453
  let valueW = visibleWidth(this.theme.bold("VALUE"));
452
454
  let sourceW = visibleWidth(this.theme.bold("SOURCE"));
453
- for (const r of this.visibleRows()) {
454
- if (visibleWidth(r.label) > keyW) keyW = visibleWidth(r.label);
455
+ // v0.28.18: widths are computed across ALL sections (not just the
456
+ // active one) so the grid does NOT reflow on tab switch — a table's
457
+ // columns stay put. The 2-char selection prefix ("▶ "/" ") counts
458
+ // toward the KEY column (before, rows overflowed keyW by 2 and the
459
+ // whole VALUE column sat 2 chars right of the header's VALUE).
460
+ for (const r of this.rows) {
461
+ if (visibleWidth(r.label) + 2 > keyW) keyW = visibleWidth(r.label) + 2;
455
462
  if (visibleWidth(r.valueText) > valueW) valueW = visibleWidth(r.valueText);
456
463
  if (visibleWidth(r.sourceText) > sourceW) sourceW = visibleWidth(r.sourceText);
457
464
  }
458
465
  keyW = Math.min(keyW, MAX_KEY_W);
459
466
  valueW = Math.min(valueW, MAX_VALUE_W);
460
467
  sourceW = Math.min(sourceW, MAX_SOURCE_W);
461
- const descW = Math.max(MIN_DESC_W, width - keyW - valueW - sourceW - 3 * COL_GUTTER);
468
+ const descW = Math.max(MIN_DESC_W, width - keyW - valueW - sourceW - 3 * visibleWidth(COL_SEP));
462
469
  return { keyW, valueW, sourceW, descW };
463
470
  }
464
471
 
@@ -469,17 +476,19 @@ export class SettingsMenuComponent implements Component {
469
476
 
470
477
  private renderBody(width: number): string[] {
471
478
  const { keyW, valueW, sourceW, descW } = this.widths(width);
472
- const gutter = " ".repeat(COL_GUTTER);
479
+ const sep = this.theme.fg("dim", COL_SEP);
473
480
 
474
481
  const lines: string[] = [];
475
482
 
476
483
  lines.push(this.theme.fg("accent", this.theme.bold(this.title)));
477
484
 
485
+ // v0.28.18: EVERY tab is bracketed (active = accent, inactive = dim) —
486
+ // bare words read as floating text, not tabs.
478
487
  lines.push(
479
488
  SETTINGS_SECTIONS.map((s, i) =>
480
489
  i === this.activeSectionIdx
481
490
  ? this.theme.fg("accent", `[${s.label}]`)
482
- : this.theme.fg("dim", s.label),
491
+ : this.theme.fg("dim", `[${s.label}]`),
483
492
  ).join(" "),
484
493
  );
485
494
 
@@ -489,7 +498,14 @@ export class SettingsMenuComponent implements Component {
489
498
  this.padEnd(this.theme.bold("VALUE"), valueW),
490
499
  this.padEnd(this.theme.bold("SOURCE"), sourceW),
491
500
  this.theme.bold("DESCRIPTION"),
492
- ].join(gutter),
501
+ ].join(sep),
502
+ );
503
+ // Header rule — the grid line that makes it read as a table.
504
+ lines.push(
505
+ this.theme.fg(
506
+ "dim",
507
+ ["─".repeat(keyW), "─".repeat(valueW), "─".repeat(sourceW), "─".repeat(descW)].join(COL_RULE_SEP),
508
+ ),
493
509
  );
494
510
 
495
511
  const vs = this.visibleRows();
@@ -499,12 +515,18 @@ export class SettingsMenuComponent implements Component {
499
515
  vs.forEach((r, i) => {
500
516
  const selected = i === this.selectedIdx;
501
517
  const prefix = selected ? "▶ " : " ";
518
+ // v0.28.18: KEY (incl. prefix) and VALUE are truncated to their
519
+ // column — before, an over-long VALUE (e.g. the subagent effective-
520
+ // resolution composite) overflowed and shoved SOURCE/DESCRIPTION
521
+ // right on that row only, breaking the grid.
502
522
  const row = [
503
- this.padEnd(prefix + r.label, keyW),
504
- this.padEnd(r.valueText, valueW),
505
- this.padEnd(r.sourceText, sourceW),
523
+ this.padEnd(truncateToWidth(prefix + r.label, keyW, "…"), keyW),
524
+ this.padEnd(truncateToWidth(r.valueText, valueW, "…"), valueW),
525
+ this.padEnd(truncateToWidth(r.sourceText, sourceW, "…"), sourceW),
506
526
  truncateToWidth(r.description, descW, "…"),
507
- ].join(gutter);
527
+ // Selected row: plain separators — the whole row gets one accent
528
+ // wrap; a nested dim separator's reset code would end it early.
529
+ ].join(selected ? COL_SEP : sep);
508
530
  lines.push(selected ? this.theme.fg("accent", row) : row);
509
531
  });
510
532
  }
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.18",
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",