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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
if (g.status === "complete" || g.status === "aborted")
|
|
204
|
-
|
|
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
|
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
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
|
-
/**
|
|
341
|
-
const
|
|
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
|
-
|
|
454
|
-
|
|
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 *
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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",
|