pi-goal-list-loop-audit 0.38.6 → 0.38.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.8 — TUI information-density pass (2026-09-03)
4
+
5
+ ### Added
6
+ Widget `audits:` row (verdict tally, width-truncated, silent when empty); verdict tally suffix on all paused status lines via `pausedStatusSuffix`; per-tab row counts in the settings menu (`settingsTabLabel`, pure). Starvation ladder reflowed to one recovery per line, same content and phrases.
7
+
8
+ ### Fixed
9
+ Glance-density gaps: parked sessions read as waiting-with-history instead of dead; ladder recoveries scannable; settings tabs locate content. Display-only — no automation, hold, auditor, or menu-structure changes. `tests/tui-density.test.ts` (5 tests) pins all four surfaces plus an 80-column widget fit; before/after captures in `audit/TUI-DENSITY-2026-09-03.md`.
10
+
11
+ ## 0.38.7 — session visibility: recovery banner + durable verdict tally (2026-09-03)
12
+
13
+ ### Added
14
+ Load-hold recovery banner: a consent-less cold load that engages the load hold now also paints objective + next pending task + verdict tally + resume command (`/goal`/`/list`/`/loop` by policy), all from durable disk state — the transcript is empty in exactly the sessions that need this. Fires once with the fresh hold; ledger `load_hold_recovery_banner`. Durable verdict tally (`auditorVerdictTally` via `auditVerdictLabel`): disapproval count + last-verdict age on the auditing status-line footer and the `/goal status` `Audits:` line, silent when history is empty — a reloaded session answers "are we progressing?" from stored verdicts.
15
+
16
+ ### Fixed
17
+ Reload invisibility: resumed-but-empty sessions no longer look goal-less, and capped/queued auditor sessions show stored-verdict evidence instead of a dead surface. Hold mechanics, hold text, auditor phase machine, and auditor card untouched. `tests/session-visibility.test.ts` (5 tests) pins tally classification, banner text, status-line tally, and a behavioral reload.
18
+
3
19
  ## 0.38.6 — over-cap starvation ladder: compact first, then 5 ordered recoveries (2026-09-03)
4
20
 
5
21
  ### Added
package/docs/INDEX.md CHANGED
@@ -18,7 +18,7 @@ For shipped docs, the relevant entry points are:
18
18
  failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
19
19
  handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
20
20
  shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
21
- v0.35.14–v0.38.6 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.8 continue through the supervisor freeze (`/glla pause`),
22
22
  load hold, auditor picker parity, Windows launch fix, zombie-watchdog
23
23
  subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
24
24
  durable state-root selection, blank-until-resume auditor context, frozen
@@ -23,7 +23,7 @@ import {
23
23
  } from "./goal-loop-core.js";
24
24
  import { clearDispatchRecord, dispatchRecordExists } from "./goal-loop-dispatch.js";
25
25
  import type { AuditDisplayProgress } from "./goal-loop-display.js";
26
- import { fmtElapsed } from "./goal-loop-display.js";
26
+ import { auditorVerdictTally, fmtElapsed, formatVerdictTallySegment } from "./goal-loop-display.js";
27
27
  import { AUDIT_FINDINGS_REL, HELD_ON_RESTORE, LOOP_AUDIT_MARKER, listAuditCollectTarget, projectAuditTarget } from "./goal-loop-forever.js";
28
28
  import { buildLoopCompletionSummary, compactCompletionSummary, compactTerminalCompletionSummary } from "./completion-summary.js";
29
29
  import { ProjectRollup, discoverGllaProjects, filterPremature, formatRollupJson, formatRollupTable, rollupProject } from "./goal-loop-stats.js";
@@ -381,8 +381,12 @@ async function cmdStatus(ctx: ExtensionContext): Promise<void> {
381
381
  `Tokens: ${(g.usage?.tokensUsed ?? 0).toLocaleString()}${(g.usage?.tokensLimit ?? 0) > 0 ? ` / ${(g.usage!.tokensLimit).toLocaleString()}` : " (no cap — set Token limit in /glla settings)"}`,
382
382
  ...formatMainModelRecoveryStatus(state.mainModelRecovery, normalizeMainModelFallbackRefs(loadSettings(ctx.cwd).mainModelFallbacks)),
383
383
  ];
384
- if (g.auditHistory && g.auditHistory.length > 0) {
385
- lines.push(`Audits: ${g.auditHistory.length} (${g.auditHistory.filter((v) => v.approved).length} approved)`);
384
+ // v0.38.7: /goal status names disapprovals + last-verdict age, not just
385
+ // the approval count — a capped/queued session must show what unblocks.
386
+ const statusTally = auditorVerdictTally(g.auditHistory);
387
+ if (statusTally.total > 0) {
388
+ const tallyText = formatVerdictTallySegment(statusTally);
389
+ lines.push(`Audits: ${tallyText} (${statusTally.approvals} approved)`);
386
390
  }
387
391
  if (g.status === "auditing") {
388
392
  lines.push(`Completion audit: ${isCompletionAuditRecoveryPending(g) ? `recovery pending — ${activeGoalSurfaceCommand("resume")} retries the stored claim` : flags.completionAuditInFlight && flags.latestAuditProgress?.label === "queued" ? "detached auditor queued" : flags.completionAuditInFlight ? "detached auditor running" : "awaiting lifecycle recovery"}`);
@@ -13,7 +13,7 @@
13
13
  import { truncateToWidth as tuiTruncateToWidth, visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
14
14
 
15
15
  import type { DurableDeferRecommendationInput, Goal, MainModelRecovery, State } from "./goal-loop-core.js";
16
- import { buildDurableDeferRecommendation, compactDisplayText, formatMainModelRecoveryStatus, isMonitorGoal, isPersistenceDegraded, lastPersistenceFailure, sanitizeDisplayText, sanitizeProviderAuditReport, sanitizeProviderDisplayText, stripThinkBlocks } from "./goal-loop-core.js";
16
+ import { auditVerdictLabel, buildDurableDeferRecommendation, compactDisplayText, formatMainModelRecoveryStatus, isMonitorGoal, isPersistenceDegraded, lastPersistenceFailure, sanitizeDisplayText, sanitizeProviderAuditReport, sanitizeProviderDisplayText, stripThinkBlocks } from "./goal-loop-core.js";
17
17
 
18
18
  export { isMonitorGoal };
19
19
  import { HELD_ON_RESTORE, type LoopState } from "./goal-loop-forever.js";
@@ -432,6 +432,68 @@ function latestAuditFeedback(g: Goal): LatestAuditFeedback | undefined {
432
432
  };
433
433
  }
434
434
 
435
+ /** v0.38.7 (note.md Next: reload recovery + progress signal): durable
436
+ * verdict tally — disapproval count + last-verdict age from auditHistory.
437
+ * After a reload the in-memory auditor progress is gone; the stored
438
+ * verdicts are what answers "are we progressing?". Classification goes
439
+ * through auditVerdictLabel so a shield-blocked approval is never counted
440
+ * as a disapproval and an infra error is never counted as a verdict. */
441
+ export interface AuditorVerdictTally {
442
+ total: number;
443
+ approvals: number;
444
+ disapprovals: number;
445
+ lastAt: number | null;
446
+ lastLabel: string | null;
447
+ }
448
+ export function auditorVerdictTally(history: Goal["auditHistory"], now = Date.now()): AuditorVerdictTally {
449
+ void now;
450
+ const entries = Array.isArray(history) ? history : [];
451
+ let approvals = 0;
452
+ let disapprovals = 0;
453
+ for (const v of entries) {
454
+ const label = auditVerdictLabel(v);
455
+ if (label === "approved") approvals++;
456
+ else if (label === "disapproved") disapprovals++;
457
+ }
458
+ const last = entries[entries.length - 1];
459
+ const lastMs = last ? Date.parse(last.at) : Number.NaN;
460
+ return {
461
+ total: entries.length,
462
+ approvals,
463
+ disapprovals,
464
+ lastAt: last && Number.isFinite(lastMs) ? lastMs : null,
465
+ lastLabel: last ? auditVerdictLabel(last) : null,
466
+ };
467
+ }
468
+ /** Compact tally segment for the always-on surfaces. "" when no verdicts. */
469
+ export function formatVerdictTallySegment(t: AuditorVerdictTally, now = Date.now()): string {
470
+ if (t.total <= 0) return "";
471
+ const dis = t.disapprovals > 0 ? ` · ${t.disapprovals} disapproved` : "";
472
+ const age = t.lastAt !== null && t.lastLabel ? ` · last ${t.lastLabel} ${fmtElapsed(now - t.lastAt)} ago` : "";
473
+ return `${t.total} verdict${t.total === 1 ? "" : "s"}${dis}${age}`;
474
+ }
475
+ /** v0.38.7: the load-hold recovery banner — objective + next task + verdict
476
+ * tally + resume command, all from durable disk state (never transcript
477
+ * memory, which is empty in exactly the sessions that need this). */
478
+ export interface LoadHoldRecoverySummary {
479
+ objective?: string | null;
480
+ status?: string;
481
+ nextTask?: string | null;
482
+ tally: AuditorVerdictTally;
483
+ resumeCommand: string;
484
+ listWaiting?: number;
485
+ }
486
+ export function buildLoadHoldRecoveryLines(s: LoadHoldRecoverySummary, now = Date.now()): string[] {
487
+ const lines = [
488
+ `glla: recovered from disk — "${truncate((s.objective ?? "").trim() || "(no objective recorded)", 120)}" (${s.status ?? "held"})`,
489
+ ];
490
+ lines.push(s.nextTask ? `next: ${truncate(s.nextTask, 100)}` : `next: no pending tasks recorded`);
491
+ const tally = formatVerdictTallySegment(s.tally, now);
492
+ lines.push(tally ? `audits: ${tally}` : `audits: none yet`);
493
+ if ((s.listWaiting ?? 0) > 0) lines.push(`list: ${s.listWaiting} waiting — /list to manage`);
494
+ lines.push(`run ${s.resumeCommand} to continue`);
495
+ return lines;
496
+ }
435
497
  function activeAttention(g: Goal): ActiveAttention | undefined {
436
498
  if (g.status !== "active" || !g.pauseReason) return undefined;
437
499
  if (/regression shield/i.test(g.pauseReason)) {
@@ -861,10 +923,14 @@ function pausedStatusSuffix(g: Goal, state: State, extras: WidgetExtras | undefi
861
923
  const queueLabel = (state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "queue empty";
862
924
  const lifecycleText = lifecycle.replace(/^lifecycle: /, "");
863
925
  const repair = g.repairTarget ? ` · replan required: ${truncate(g.repairTarget.objective.replace(/\s+/g, " "), 100)}` : "";
926
+ // v0.38.8: parked goals carry their verdict tally — a paused session
927
+ // with 2 disapprovals reads as "waiting with history", not dead.
928
+ const tallyText = formatVerdictTallySegment(auditorVerdictTally(g.auditHistory, now), now);
929
+ const verdict = tallyText ? ` · ${tallyText}` : "";
864
930
  if (queueFirst) {
865
- return ` · ${queueLabel} · ${lifecycleText.replace(` · ${queueLabel}`, "")} · ${transition}${repair}`;
931
+ return ` · ${queueLabel} · ${lifecycleText.replace(` · ${queueLabel}`, "")} · ${transition}${repair}${verdict}`;
866
932
  }
867
- return ` · ${lifecycleText} · ${transition}${repair}`;
933
+ return ` · ${lifecycleText} · ${transition}${repair}${verdict}`;
868
934
  }
869
935
 
870
936
  /**
@@ -954,7 +1020,12 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
954
1020
  const quietSuffix = quietAge !== undefined ? ` · silent ${fmtElapsed(quietAge)}` : "";
955
1021
  const next = ` · next: ${auditorNextTransition(phase)}`;
956
1022
  const detachedSuffix = live ? "" : " · detached worker";
957
- return `glla: ${host} · ${label}${quietSuffix}${next}${detachedSuffix}${heldSuffix}`;
1023
+ // v0.38.7: durable verdict tally on the always-on footer — after a
1024
+ // reload there is no live auditor evidence, so the stored verdicts
1025
+ // (disapproval count + last-verdict age) answer "are we progressing?".
1026
+ const tallyText = formatVerdictTallySegment(auditorVerdictTally(g.auditHistory, now), now);
1027
+ const verdictSuffix = tallyText ? ` · ${tallyText}` : "";
1028
+ return `glla: ${host} · ${label}${quietSuffix}${next}${detachedSuffix}${verdictSuffix}${heldSuffix}`;
958
1029
  }
959
1030
  if (g.status === "paused") {
960
1031
  // v0.28.22: the status line names the ACTIONABILITY, not the reason —
@@ -1357,6 +1428,13 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1357
1428
  : 48;
1358
1429
  const head = `${icon} ${truncate(g.objective.replace(/\s+/g, " "), objBudget)} ${paint(theme, "dim", "·")} ${segsText}`;
1359
1430
  const lines = [head];
1431
+ // v0.38.8: durable verdict tally as a first-class card row — the widget
1432
+ // is the glance surface, and stored verdicts are the progress evidence
1433
+ // when no auditor is live. Silent when history is empty.
1434
+ const headTally = formatVerdictTallySegment(auditorVerdictTally(g.auditHistory, now), now);
1435
+ if (headTally) {
1436
+ lines.push(`├─ ${paint(theme, "dim", `audits: ${truncate(headTally, Math.max(20, (width ?? 80) - 12))}`)}`);
1437
+ }
1360
1438
  if (g.repairTarget) {
1361
1439
  lines.push(`├─ ${paint(theme, "warning", `REPLAN REQUIRED · original target: ${truncate(g.repairTarget.objective.replace(/\s+/g, " "), Math.max(30, (width ?? 80) - 28))}`)}`);
1362
1440
  const repairStep = g.repairTarget.replanPromptedAt
@@ -238,7 +238,7 @@ import {
238
238
  textFingerprint,
239
239
  pushCapped as pushRepetitionCapped,
240
240
  } from "../goal-loop-repetition.js";
241
- import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
241
+ import { auditorVerdictTally, buildLoadHoldRecoveryLines, buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
242
242
  import { compactLoopCompletionSummary } from "../completion-summary.js";
243
243
  import {
244
244
  defaultAgentDir,
@@ -1864,6 +1864,34 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
1864
1864
  "Loaded without starting: your goal/list/loop state is restored and shown below, but automation is HELD for your decision. /goal resume, /list resume, or /list next starts work; enable Auto-resume in /glla settings to restore load-time automation.",
1865
1865
  "warning",
1866
1866
  );
1867
+ // v0.38.7 (note.md Next: objectives seemingly lost on reload) —
1868
+ // the recovery banner: objective + next task + verdict tally +
1869
+ // resume command, all from durable disk state. The transcript is
1870
+ // empty in exactly the sessions that need this, so nothing here
1871
+ // may depend on in-memory progress. Fires with the fresh hold
1872
+ // (guarded above), never as a timer re-arm.
1873
+ const tasks = state.goal?.taskList?.tasks ?? [];
1874
+ const pendingTasks = tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
1875
+ const tally = auditorVerdictTally(state.goal?.auditHistory);
1876
+ const resumeCommand = state.goal
1877
+ ? (state.goal.policy === "list" ? "/list resume" : "/goal resume")
1878
+ : (state.list?.length ?? 0) > 0 ? "/list resume" : state.loop ? "/loop resume" : "/goal resume";
1879
+ const banner = buildLoadHoldRecoveryLines({
1880
+ objective: state.goal?.objective ?? ((state.list?.length ?? 0) > 0 ? `${state.list!.length} queued list items` : null),
1881
+ status: state.goal?.status ?? (state.loop ? "loop" : "held"),
1882
+ nextTask: pendingTasks[0]?.title ?? null,
1883
+ tally,
1884
+ resumeCommand,
1885
+ listWaiting: state.goal?.policy === "list" ? undefined : state.list?.length ?? 0,
1886
+ });
1887
+ appendLedger(ctx.cwd, "load_hold_recovery_banner", {
1888
+ goalId: state.goal?.id ?? null,
1889
+ status: state.goal?.status ?? null,
1890
+ pendingTasks: pendingTasks.length,
1891
+ totalVerdicts: tally.total,
1892
+ disapprovals: tally.disapprovals,
1893
+ });
1894
+ ctx.ui.notify(banner.join("\n"), "info");
1867
1895
  }
1868
1896
  } else if ((autoResume || explicitRecovery) && typeof state.loadHoldAt === "number") {
1869
1897
  // v0.35.28 (issue #16): a hold persisted by a PREVIOUS process must
@@ -997,11 +997,16 @@ export function shouldCompactFirstNudge(percent: number | null | undefined): boo
997
997
  export function buildStarvationLadderMessage(input: { percent?: number | null; streak?: number } = {}): string {
998
998
  const at = typeof input.percent === "number" && Number.isFinite(input.percent)
999
999
  ? `${input.percent.toFixed(1)}%` : "nearly full";
1000
- return `glla: output-token stop was context starvation (tiny output at ${at} context) — yielding to pi auto-compaction instead of re-sending. ` +
1001
- `If compaction fails or context is already over cap, in order: (1) run /compact again — GLLA already trimmed repeat payloads to a checkpoint, so the retry may now fit; ` +
1002
- `(2) switch to a larger-context model, then /compact (GLLA auto-rotates its fallback chain after a failed compact-and-retry); ` +
1003
- `(3) /new, then /goal resume — the goal, tasks, ledger and audits are durable on disk, and the post-compact resync re-anchors the fresh session with no summarization needed. ` +
1004
- `Automatic turns stay parked until a real compaction lands.`;
1000
+ // v0.38.8: one recovery per line — the single-paragraph version buried
1001
+ // the ladder. Same content, scannable shape.
1002
+ return [
1003
+ `glla: output-token stop was context starvation (tiny output at ${at} context) — yielding to pi auto-compaction instead of re-sending.`,
1004
+ `If compaction fails or context is already over cap, in order:`,
1005
+ `(1) run /compact again — GLLA already trimmed repeat payloads to a checkpoint, so the retry may now fit;`,
1006
+ `(2) switch to a larger-context model, then /compact (GLLA auto-rotates its fallback chain after a failed compact-and-retry);`,
1007
+ `(3) /new, then /goal resume — the goal, tasks, ledger and audits are durable on disk, and the post-compact resync re-anchors the fresh session with no summarization needed.`,
1008
+ `Automatic turns stay parked until a real compaction lands.`,
1009
+ ].join("\n");
1005
1010
  }
1006
1011
  function noteContextStarvedYield(): { streak: number; shouldRefuse: boolean } {
1007
1012
  const now = Date.now();
@@ -58,6 +58,10 @@ export type SettingsSectionId =
58
58
  | "stall-brakes"
59
59
  | "other";
60
60
 
61
+ /** v0.38.8: tab label with its row count — pure so tests pin the shape. */
62
+ export function settingsTabLabel(label: string, rowCount: number): string {
63
+ return rowCount > 0 ? `${label} (${rowCount})` : label;
64
+ }
61
65
  export const SETTINGS_SECTIONS: readonly { id: SettingsSectionId; label: string }[] = [
62
66
  { id: "keep-going", label: "Keep-going" },
63
67
  { id: "main-agent", label: "Main agent" },
@@ -766,11 +770,15 @@ export class SettingsMenuComponent implements Component {
766
770
 
767
771
  // v0.28.19: color-only tabs (user call: "dropping the brackets") —
768
772
  // active = accent + bold, inactive = dim. No bracket chrome.
773
+ // v0.38.8: per-tab row counts — the menu holds 7 sections and the
774
+ // count says where the settings live at a glance.
775
+ const tabLabel = (s: { id: SettingsSectionId; label: string }): string =>
776
+ settingsTabLabel(s.label, this.rows.filter((r) => r.section === s.id).length);
769
777
  lines.push(
770
778
  SETTINGS_SECTIONS.map((s, i) =>
771
779
  i === this.activeSectionIdx
772
- ? this.theme.fg("accent", this.theme.bold(s.label))
773
- : this.theme.fg("dim", s.label),
780
+ ? this.theme.fg("accent", this.theme.bold(tabLabel(s)))
781
+ : this.theme.fg("dim", tabLabel(s)),
774
782
  ).join(" "),
775
783
  );
776
784
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.6",
3
+ "version": "0.38.8",
4
4
  "description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "dracon",