pi-goal-list-loop-audit 0.38.27 → 0.38.29

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.29 — compact active-card recovery and judgment details (2026-09-08)
4
+
5
+ ### Fixed
6
+ - The active goal card now renders main-model recovery as one compact, truthful row instead of repeating the full chain, current model, pending switch, attempts, and skip details. Full recovery diagnostics remain available through `/goal status`; the status footer no longer duplicates that report.
7
+ - Durable-vs-defer plaques cap their glance-card prose while preserving ordering, recommendation, and selected-choice facts. The final judgment row closes the tree cleanly.
8
+ - Markdown emphasis/code wrappers are removed from the display-only objective projection, so `**objective**` and backtick-wrapped commands no longer add visual noise. Stored objectives remain unchanged.
9
+ - Added regression coverage for compact recovery, duplicate suppression, Markdown cleanup, and the closed judgment tail.
10
+
11
+ ## 0.38.28 — close redundant provenance rows in the active goal card (2026-09-08)
12
+
13
+ ### Fixed
14
+ - The active card no longer shows a duplicate `handled turn` row when it is the same model as the primary model. When provenance is the final detail block, its last row now closes with `└─` instead of a dangling `│`/`├─`, so the card no longer implies missing rows. Failover-handled turns remain visible. Added focused regression coverage; `tsc` and the focused display suite pass.
15
+
3
16
  ## 0.38.27 — selective port of PRs #45/#46: /loop pause + widget subtask count (2026-09-08)
4
17
 
5
18
  ### 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.27 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.29 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
@@ -28,7 +28,7 @@ For shipped docs, the relevant entry points are:
28
28
  packed-artifact release verification, and the 2026-09-07 display/lifecycle/
29
29
  settings audit pass (abort-latch send guards, ownership compare-and-swap,
30
30
  auditor inherit/clear parity) and the v0.38.25 post-objective summary
31
- (canonical approval render, persist + replay, audit-goal counts line) and the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count) — see CHANGELOG.md for the full trail.
31
+ (canonical approval render, persist + replay, audit-goal counts line), the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count), the v0.38.28 provenance-row closure fix, and the v0.38.29 compact active-card recovery/judgment projection — see CHANGELOG.md for the full trail.
32
32
  - `../README.md` — what the plugin is, install, quickstart, and the
33
33
  architectural guarantee (drafting + confirm + detached auditor).
34
34
  - `../INSTALL.md` — source install / local development setup; the recommended
@@ -87,6 +87,17 @@ function displayPauseReason(reason: string): string {
87
87
  return compactDisplayText(sanitizeProviderDisplayText(reason));
88
88
  }
89
89
 
90
+ /** Objectives are stored verbatim, but the glance card is a plain-text
91
+ * surface. Remove the common Markdown emphasis/code wrappers that otherwise
92
+ * turn a long objective into noisy `**...**` and backtick litter. This is a
93
+ * display-only projection; prompts, archives, and state keep the original. */
94
+ function displayObjective(objective: string): string {
95
+ return compactDisplayText(objective)
96
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
97
+ .replace(/__([^_]+)__/g, "$1")
98
+ .replace(/`([^`\n]*)`/g, "$1");
99
+ }
100
+
90
101
  /** v0.33.1: painted strings measure by their terminal-cell width. */
91
102
  function visibleLen(s: string): number {
92
103
  return tuiVisibleWidth(s);
@@ -280,6 +291,53 @@ function modelSourceLabel(source: string): string {
280
291
  return source.trim();
281
292
  }
282
293
 
294
+ function compactMainModelRecoveryLine(recovery: MainModelRecovery | undefined, configuredBackups: string[] = [], now = Date.now()): string | undefined {
295
+ if (!recovery || !recovery.primary.trim()) return undefined;
296
+ const primary = recovery.primary.trim();
297
+ const current = (recovery.active ?? primary).trim() || primary;
298
+ const backups = uniqueModelRefs(configuredBackups).slice(0, 10);
299
+ const chain = [primary, ...backups];
300
+ const currentIndex = chain.findIndex((ref) => ref.toLowerCase() === current.toLowerCase());
301
+ const selected = currentIndex === 0
302
+ ? "primary"
303
+ : currentIndex > 0
304
+ ? `backup ${currentIndex}/${backups.length}`
305
+ : "fallback";
306
+ const retryMs = recovery.retryAt ? Date.parse(recovery.retryAt) - now : Number.NaN;
307
+ const probeMs = recovery.primaryProbeAt ? Date.parse(recovery.primaryProbeAt) - now : Number.NaN;
308
+ const phase = recovery.manualResumeRequired
309
+ ? "manual hold"
310
+ : recovery.pendingModelSwitch
311
+ ? `switching → ${truncate(recovery.pendingModelSwitch, 48)}`
312
+ : Number.isFinite(retryMs)
313
+ ? retryMs <= 0 ? "retrying now" : `retrying in ${fmtElapsed(retryMs)}`
314
+ : recovery.primaryProbeInFlight
315
+ ? "primary probe pending"
316
+ : Number.isFinite(probeMs)
317
+ ? probeMs <= 0 ? "probing primary now" : `primary probe in ${fmtElapsed(probeMs)}`
318
+ : `${selected} selected`;
319
+ const attempted = recovery.attempted?.length ?? 0;
320
+ const skipped = recovery.skipped?.length ?? 0;
321
+ const hasEpisode = !!recovery.manualResumeRequired
322
+ || !!recovery.pendingModelSwitch
323
+ || Number.isFinite(retryMs)
324
+ || Number.isFinite(probeMs)
325
+ || !!recovery.primaryProbeInFlight
326
+ || recovery.attempts > 0
327
+ || attempted > 1
328
+ || skipped > 0
329
+ || currentIndex !== 0;
330
+ // A recovery object can briefly survive the successful primary selection.
331
+ // Do not label that normal state as an incident, but keep the selected model
332
+ // on the card when no separate provenance row is available.
333
+ if (!hasEpisode) return `model: ${truncate(current, 56)} · primary`;
334
+ const history = [
335
+ recovery.attempts > 0 ? `attempts ${recovery.attempts}` : "",
336
+ skipped > 0 ? `skipped ${skipped}` : "",
337
+ ].filter(Boolean);
338
+ return `recovery: ${phase} · ${selected} ${truncate(current, 48)}${history.length > 0 ? ` · ${history.join(" · ")}` : ""}`;
339
+ }
340
+
283
341
  function modelProvenanceLines(provenance: ModelProvenanceDisplay | undefined, width?: number): string[] {
284
342
  if (!provenance) return [];
285
343
  const budget = budgetFor(width, 3, 60);
@@ -294,7 +352,13 @@ function modelProvenanceLines(provenance: ModelProvenanceDisplay | undefined, wi
294
352
  const skipped = uniqueModelRefs(provenance.skippedForbiddenRefs);
295
353
  if (skipped.length > 0) lines.push(`skipped forbidden: ${truncate(skipped.join(", "), budget)}`);
296
354
  const handledTurn = typeof provenance.handledTurn === "string" ? provenance.handledTurn.trim() : "";
297
- if (handledTurn) lines.push(`handled turn: ${truncate(handledTurn, budget)}`);
355
+ // A normal turn is handled by the configured primary model. Repeating the
356
+ // same ref immediately below it adds no information and, when it is the
357
+ // last row, leaves a misleading continuation glyph in the card. Keep the
358
+ // row only when a recovery/failover actually handled the turn.
359
+ if (handledTurn && handledTurn.toLowerCase() !== primary.toLowerCase()) {
360
+ lines.push(`handled turn: ${truncate(handledTurn, budget)}`);
361
+ }
298
362
  const handledAudit = typeof provenance.handledAudit === "string" ? provenance.handledAudit.trim() : "";
299
363
  if (handledAudit) {
300
364
  const source = provenance.handledAuditSource?.trim() ? modelSourceLabel(provenance.handledAuditSource) : "";
@@ -322,9 +386,14 @@ export function buildDurableDeferDecisionLines(input: DurableDeferRecommendation
322
386
  // Reserve room for the recommendation marker and the outer tree prefix
323
387
  // so a normal 80-column production widget cannot truncate away the
324
388
  // fact that the durable plaque is the selected action.
389
+ // Recommendation bodies are durable facts, but the glance card is not
390
+ // the command's full report. A wide terminal used to turn these plaques
391
+ // into sentence-length banners and crowd out the actual work state;
392
+ // /goal status retains the complete text.
393
+ const bodyCap = plaque.kind === "durable" ? 96 : 72;
325
394
  const bodyBudget = width && width > 0
326
- ? Math.max(8, width - WIDGET_HORIZONTAL_MARGIN - 3 - visibleLen(prefix) - visibleLen(marker))
327
- : budgetFor(width, 3, 60);
395
+ ? Math.max(8, Math.min(bodyCap, width - WIDGET_HORIZONTAL_MARGIN - 3 - visibleLen(prefix) - visibleLen(marker)))
396
+ : Math.min(bodyCap, budgetFor(width, 3, 60));
328
397
  return `${prefix}${truncate(plaque.body, bodyBudget)}${marker}`;
329
398
  }),
330
399
  `selected: ${recommendation.choice}${recommendation.choice === "inline" ? " (durable fix)" : " (reversible workaround)"}`,
@@ -1157,21 +1226,21 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1157
1226
  return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}${pausedStatusSuffix(g, state, extras, now)}${heldSuffix}`;
1158
1227
  }
1159
1228
  if (g.status === "active") {
1160
- const recoverySummary = formatMainModelRecoveryStatus(state.mainModelRecovery, extras?.mainModelFallbacks);
1161
- const withRecovery = (value: string): string => recoverySummary.length > 0
1162
- ? `${value} · ${recoverySummary.join(" · ")}`
1163
- : value;
1229
+ // The footer is a glance/liveness surface. Recovery details belong to the
1230
+ // card (and the full /goal status report), not a second horizontally
1231
+ // concatenated copy here. This keeps exceptions readable without making
1232
+ // a healthy WORKING line look like an incident.
1164
1233
  // v0.28.1 (S1/S2): a stale-handle interrupt keeps the goal ACTIVE.
1165
1234
  // It outranks any older operational note on the same state snapshot.
1166
1235
  if (g.interruptedAt) {
1167
1236
  const label = interruptedForNoStart(g)
1168
1237
  ? "⚠ turn start not observed — automatic retry held"
1169
1238
  : "⚠ interrupted — stale handle · /new (or a fresh session_start) rebinds";
1170
- return withRecovery(`glla: ${paint(theme, "error", label)}${heldSuffix}`);
1239
+ return `glla: ${paint(theme, "error", label)}${heldSuffix}`;
1171
1240
  }
1172
1241
  const attention = activeAttention(g);
1173
1242
  if (attention) {
1174
- return withRecovery(`glla: ${paint(theme, attention.color, `⚠ ${attention.label}`)}${heldSuffix}`);
1243
+ return `glla: ${paint(theme, attention.color, `⚠ ${attention.label}`)}${heldSuffix}`;
1175
1244
  }
1176
1245
  const activity = goalDisplayActivity(g, extras, now);
1177
1246
  // v0.34.97: while the post-compaction grace window is open, surface
@@ -1180,10 +1249,10 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1180
1249
  const compactAgeMs = state.lastCompactionAt ? now - state.lastCompactionAt : Number.POSITIVE_INFINITY;
1181
1250
  const compacting = Number.isFinite(compactAgeMs) && compactAgeMs >= 0 && compactAgeMs < 180_000; // COMPACTION_GRACE_MS = 3 min
1182
1251
  if (compacting) {
1183
- return withRecovery(`glla: ${paint(theme, "warning", `⏳ compacting… (${fmtElapsed(compactAgeMs)} ago)`)}${heldSuffix}`);
1252
+ return `glla: ${paint(theme, "warning", `⏳ compacting… (${fmtElapsed(compactAgeMs)} ago)`)}${heldSuffix}`;
1184
1253
  }
1185
1254
  if (activity === "awaiting-first-turn") {
1186
- return withRecovery(`glla: ${activityStateBadge("AWAITING FIRST TURN", theme, "warning")}${heldSuffix}`);
1255
+ return `glla: ${activityStateBadge("AWAITING FIRST TURN", theme, "warning")}${heldSuffix}`;
1187
1256
  }
1188
1257
  if (activity === "idle") {
1189
1258
  // Audit 2026-09-07 (DECIDED: head owns liveness): the status keeps
@@ -1194,7 +1263,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1194
1263
  goalTotalText(g, now),
1195
1264
  (state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "",
1196
1265
  ].filter(Boolean);
1197
- return withRecovery(`glla: ${activityStateBadge("IDLE", theme, "warning")}${idleDetails.length > 0 ? ` ${idleDetails.join(" · ")}` : ""}${heldSuffix}`);
1266
+ return `glla: ${activityStateBadge("IDLE", theme, "warning")}${idleDetails.length > 0 ? ` ${idleDetails.join(" · ")}` : ""}${heldSuffix}`;
1198
1267
  }
1199
1268
  // v0.34.39: distinguish durable state from evidence of a live host turn.
1200
1269
  // A spinner is reserved for recent stream/tool evidence; BUSY without
@@ -1206,7 +1275,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1206
1275
  g.taskList ? `${countDone(g)}/${countTotal(g)} tasks` : "",
1207
1276
  (state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "",
1208
1277
  ].filter(Boolean);
1209
- return withRecovery(`glla: ${activityStateBadge("BUSY", theme, "warning")}${busyDetails.length > 0 ? ` ${busyDetails.join(" · ")}` : ""}${heldSuffix}`);
1278
+ return `glla: ${activityStateBadge("BUSY", theme, "warning")}${busyDetails.length > 0 ? ` ${busyDetails.join(" · ")}` : ""}${heldSuffix}`;
1210
1279
  }
1211
1280
  // v0.34.16: a fresh session_start owns the handoff. A cold boot still
1212
1281
  // follows the global autoResume setting, so the widget names the actual
@@ -1252,7 +1321,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1252
1321
  monitoring ? "next check" : "",
1253
1322
  n > 0 ? `${n} queued` : "",
1254
1323
  ].filter(Boolean);
1255
- return withRecovery(`glla: ${marker}${details.length > 0 ? ` ${details.join(" · ")}` : ""}${recoverySuffix}${heldSuffix}`);
1324
+ return `glla: ${marker}${details.length > 0 ? ` ${details.join(" · ")}` : ""}${recoverySuffix}${heldSuffix}`;
1256
1325
  }
1257
1326
  // v0.34.65: a terminal goal names its outcome + wall duration instead of
1258
1327
  // clearing the segment (note.md 2026-08-07: "this seems weak for a complete
@@ -1346,6 +1415,16 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
1346
1415
  withAgents = orphanHead ? [orphanHead, ...agentLines] : [...agentLines];
1347
1416
  }
1348
1417
  }
1418
+ // Keep the final tree row visually closed when the compact judgment block
1419
+ // ends without a queue footer. Without this, its last `├─` reads like a
1420
+ // missing continuation and makes an already dense card feel unfinished.
1421
+ if (withAgents && inner && detailedAgents.length === 0 && extras?.durableDeferRecommendation && withAgents.length > 0) {
1422
+ const tailIndex = withAgents.length - 1;
1423
+ const tail = withAgents[tailIndex]!;
1424
+ if (tail.startsWith("├─ ") || tail.startsWith("│ ")) {
1425
+ withAgents[tailIndex] = `└─ ${tail.slice(3)}`;
1426
+ }
1427
+ }
1349
1428
  // v0.28.6 (E1): a persistence failure outranks everything — first line,
1350
1429
  // on every render, until a write lands again.
1351
1430
  let lines: string[] | undefined = withAgents;
@@ -1367,7 +1446,7 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
1367
1446
  function waitingListStatus(state: State, _now: number, theme?: DisplayTheme, width?: number): string {
1368
1447
  const queue = state.list ?? [];
1369
1448
  const head = queue[0];
1370
- const objective = head?.objective?.trim() ? sanitizeDisplayText(head.objective) : "unnamed queued item";
1449
+ const objective = head?.objective?.trim() ? displayObjective(head.objective) : "unnamed queued item";
1371
1450
  const hold = typeof state.loadHoldAt === "number" ? " · held on restore" : "";
1372
1451
  // Audit 2026-09-06: the width budget truncates the END of the status
1373
1452
  // line — the `/glla resume` action must survive, so the OBJECTIVE takes
@@ -1384,7 +1463,7 @@ function waitingListStatus(state: State, _now: number, theme?: DisplayTheme, wid
1384
1463
  function waitingListLines(state: State, theme?: DisplayTheme, width?: number): string[] {
1385
1464
  const queue = state.list ?? [];
1386
1465
  const head = queue[0];
1387
- const objective = head?.objective?.trim() ? sanitizeDisplayText(head.objective) : "unnamed queued item";
1466
+ const objective = head?.objective?.trim() ? displayObjective(head.objective) : "unnamed queued item";
1388
1467
  const objectiveBudget = budgetFor(width, visibleLen("├─ up next: "), 56);
1389
1468
  const action = typeof state.loadHoldAt === "number"
1390
1469
  ? "held on restore · /glla resume starts the queue · /list next skips/chooses"
@@ -1549,7 +1628,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1549
1628
  const headIcon = headLive
1550
1629
  ? paint(theme, headLive.band === "fresh" ? "success" : headLive.band === "aging" ? "warning" : "error", headLive.breath)
1551
1630
  : icon;
1552
- const head = `${headIcon} ${truncate(g.objective.replace(/\s+/g, " "), objBudget)} ${paint(theme, "dim", "·")} ${segsText}`;
1631
+ const head = `${headIcon} ${truncate(displayObjective(g.objective), objBudget)} ${paint(theme, "dim", "·")} ${segsText}`;
1553
1632
  const lines = [head];
1554
1633
  // v0.38.8: durable verdict tally as a first-class card row — the widget
1555
1634
  // is the glance surface, and stored verdicts are the progress evidence
@@ -1566,20 +1645,23 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1566
1645
  lines.push(`├─ ${paint(theme, "dim", `Recovery: ${repairStep}`)}`);
1567
1646
  }
1568
1647
  // A model switch crosses an asynchronous boundary while the goal remains
1569
- // active. Keep the complete durable recovery projection on the widget too;
1570
- // otherwise the head says only "active" while pending/attempted/skipped
1571
- // candidates are invisible until the goal is parked.
1648
+ // active. Keep that fact visible, but do not print the full recovery report
1649
+ // into the glance card; it made the card taller than the space below the
1650
+ // editor. `/goal status` remains the detailed recovery surface.
1651
+ let recoveryLine: string | undefined;
1572
1652
  if (g.status === "active" && state.mainModelRecovery) {
1573
- const recoverySummary = formatMainModelRecoveryStatus(state.mainModelRecovery, extras?.mainModelFallbacks);
1574
- recoverySummary.forEach((line, i) => {
1575
- lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
1576
- });
1653
+ recoveryLine = compactMainModelRecoveryLine(state.mainModelRecovery, extras?.mainModelFallbacks, now);
1654
+ if (recoveryLine) lines.push(`├─ ${paint(theme, "dim", recoveryLine)}`);
1577
1655
  }
1578
1656
  // Model provenance is a card fact, not a notification: keep it visible
1579
- // across active, interrupted, auditing, and paused branches. In
1580
- // particular, never replace a configured forbidden ref with "none" — the
1581
- // skipped line explains why it did not handle the turn.
1582
- const provenance = modelProvenanceLines(extras?.modelProvenance, width);
1657
+ // across active, interrupted, auditing, and paused branches. During an
1658
+ // active recovery the compact recovery row owns the model fact, so do not
1659
+ // print a second primary/current row beside it. The full chain and skipped
1660
+ // refs remain available through `/goal status`.
1661
+ const provenance = state.mainModelRecovery
1662
+ ? []
1663
+ : modelProvenanceLines(extras?.modelProvenance, width);
1664
+ const provenanceStart = lines.length;
1583
1665
  provenance.forEach((line, i) => {
1584
1666
  lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
1585
1667
  });
@@ -1904,6 +1986,26 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1904
1986
  // `/goal status` / `/glla` command hints were extension meta, not task
1905
1987
  // information — dropped. No queue, no footer at all.
1906
1988
  if (queue > 0) lines.push(`└─ ${paint(theme, "dim", `${queue} queued`)}`);
1989
+ // v0.38.28: provenance rows can be the entire detail block when there is
1990
+ // no recent action, pending task, or queue footer. Close that block with a
1991
+ // terminator instead of leaving `├─`/`│` suggesting missing rows.
1992
+ const provenanceEnd = provenanceStart + provenance.length;
1993
+ const detailedAgentRows = extras?.agents?.lines ?? (extras?.agents?.line ? [extras.agents.line] : []);
1994
+ if (provenance.length > 0 && detailedAgentRows.length === 0 && provenanceEnd === lines.length) {
1995
+ const last = lines[provenanceEnd - 1]!;
1996
+ if (last.startsWith("├─ ") || last.startsWith("│ ")) {
1997
+ lines[provenanceEnd - 1] = `└─ ${last.slice(3)}`;
1998
+ }
1999
+ }
2000
+ // A compact recovery/model row can be the final detail when no other card
2001
+ // fact follows it. Close that one row without changing the established
2002
+ // open-row shape for recent actions or worker details.
2003
+ if (recoveryLine && detailedAgentRows.length === 0 && lines.length > 0) {
2004
+ const last = lines[lines.length - 1]!;
2005
+ if (last.includes(recoveryLine) && (last.startsWith("├─ ") || last.startsWith("│ "))) {
2006
+ lines[lines.length - 1] = `└─ ${last.slice(3)}`;
2007
+ }
2008
+ }
1907
2009
  return lines;
1908
2010
  }
1909
2011
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.27",
3
+ "version": "0.38.29",
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",