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

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.7 — session visibility: recovery banner + durable verdict tally (2026-09-03)
4
+
5
+ ### Added
6
+ 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.
7
+
8
+ ### Fixed
9
+ 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.
10
+
3
11
  ## 0.38.6 — over-cap starvation ladder: compact first, then 5 ordered recoveries (2026-09-03)
4
12
 
5
13
  ### 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.7 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)) {
@@ -954,7 +1016,12 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
954
1016
  const quietSuffix = quietAge !== undefined ? ` · silent ${fmtElapsed(quietAge)}` : "";
955
1017
  const next = ` · next: ${auditorNextTransition(phase)}`;
956
1018
  const detachedSuffix = live ? "" : " · detached worker";
957
- return `glla: ${host} · ${label}${quietSuffix}${next}${detachedSuffix}${heldSuffix}`;
1019
+ // v0.38.7: durable verdict tally on the always-on footer — after a
1020
+ // reload there is no live auditor evidence, so the stored verdicts
1021
+ // (disapproval count + last-verdict age) answer "are we progressing?".
1022
+ const tallyText = formatVerdictTallySegment(auditorVerdictTally(g.auditHistory, now), now);
1023
+ const verdictSuffix = tallyText ? ` · ${tallyText}` : "";
1024
+ return `glla: ${host} · ${label}${quietSuffix}${next}${detachedSuffix}${verdictSuffix}${heldSuffix}`;
958
1025
  }
959
1026
  if (g.status === "paused") {
960
1027
  // v0.28.22: the status line names the ACTIONABILITY, not the reason —
@@ -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
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.7",
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",