pi-goal-list-loop-audit 0.38.28 → 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,13 @@
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
+
3
11
  ## 0.38.28 — close redundant provenance rows in the active goal card (2026-09-08)
4
12
 
5
13
  ### Fixed
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.28 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), the v0.38.27 selective port of PRs #45/#46 (`/loop pause` soft-hold, widget subtask count), and the v0.38.28 provenance-row closure fix — 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);
@@ -328,9 +386,14 @@ export function buildDurableDeferDecisionLines(input: DurableDeferRecommendation
328
386
  // Reserve room for the recommendation marker and the outer tree prefix
329
387
  // so a normal 80-column production widget cannot truncate away the
330
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;
331
394
  const bodyBudget = width && width > 0
332
- ? Math.max(8, width - WIDGET_HORIZONTAL_MARGIN - 3 - visibleLen(prefix) - visibleLen(marker))
333
- : 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));
334
397
  return `${prefix}${truncate(plaque.body, bodyBudget)}${marker}`;
335
398
  }),
336
399
  `selected: ${recommendation.choice}${recommendation.choice === "inline" ? " (durable fix)" : " (reversible workaround)"}`,
@@ -1163,21 +1226,21 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1163
1226
  return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}${pausedStatusSuffix(g, state, extras, now)}${heldSuffix}`;
1164
1227
  }
1165
1228
  if (g.status === "active") {
1166
- const recoverySummary = formatMainModelRecoveryStatus(state.mainModelRecovery, extras?.mainModelFallbacks);
1167
- const withRecovery = (value: string): string => recoverySummary.length > 0
1168
- ? `${value} · ${recoverySummary.join(" · ")}`
1169
- : 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.
1170
1233
  // v0.28.1 (S1/S2): a stale-handle interrupt keeps the goal ACTIVE.
1171
1234
  // It outranks any older operational note on the same state snapshot.
1172
1235
  if (g.interruptedAt) {
1173
1236
  const label = interruptedForNoStart(g)
1174
1237
  ? "⚠ turn start not observed — automatic retry held"
1175
1238
  : "⚠ interrupted — stale handle · /new (or a fresh session_start) rebinds";
1176
- return withRecovery(`glla: ${paint(theme, "error", label)}${heldSuffix}`);
1239
+ return `glla: ${paint(theme, "error", label)}${heldSuffix}`;
1177
1240
  }
1178
1241
  const attention = activeAttention(g);
1179
1242
  if (attention) {
1180
- return withRecovery(`glla: ${paint(theme, attention.color, `⚠ ${attention.label}`)}${heldSuffix}`);
1243
+ return `glla: ${paint(theme, attention.color, `⚠ ${attention.label}`)}${heldSuffix}`;
1181
1244
  }
1182
1245
  const activity = goalDisplayActivity(g, extras, now);
1183
1246
  // v0.34.97: while the post-compaction grace window is open, surface
@@ -1186,10 +1249,10 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1186
1249
  const compactAgeMs = state.lastCompactionAt ? now - state.lastCompactionAt : Number.POSITIVE_INFINITY;
1187
1250
  const compacting = Number.isFinite(compactAgeMs) && compactAgeMs >= 0 && compactAgeMs < 180_000; // COMPACTION_GRACE_MS = 3 min
1188
1251
  if (compacting) {
1189
- return withRecovery(`glla: ${paint(theme, "warning", `⏳ compacting… (${fmtElapsed(compactAgeMs)} ago)`)}${heldSuffix}`);
1252
+ return `glla: ${paint(theme, "warning", `⏳ compacting… (${fmtElapsed(compactAgeMs)} ago)`)}${heldSuffix}`;
1190
1253
  }
1191
1254
  if (activity === "awaiting-first-turn") {
1192
- return withRecovery(`glla: ${activityStateBadge("AWAITING FIRST TURN", theme, "warning")}${heldSuffix}`);
1255
+ return `glla: ${activityStateBadge("AWAITING FIRST TURN", theme, "warning")}${heldSuffix}`;
1193
1256
  }
1194
1257
  if (activity === "idle") {
1195
1258
  // Audit 2026-09-07 (DECIDED: head owns liveness): the status keeps
@@ -1200,7 +1263,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1200
1263
  goalTotalText(g, now),
1201
1264
  (state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "",
1202
1265
  ].filter(Boolean);
1203
- 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}`;
1204
1267
  }
1205
1268
  // v0.34.39: distinguish durable state from evidence of a live host turn.
1206
1269
  // A spinner is reserved for recent stream/tool evidence; BUSY without
@@ -1212,7 +1275,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1212
1275
  g.taskList ? `${countDone(g)}/${countTotal(g)} tasks` : "",
1213
1276
  (state.list?.length ?? 0) > 0 ? `${state.list!.length} queued` : "",
1214
1277
  ].filter(Boolean);
1215
- 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}`;
1216
1279
  }
1217
1280
  // v0.34.16: a fresh session_start owns the handoff. A cold boot still
1218
1281
  // follows the global autoResume setting, so the widget names the actual
@@ -1258,7 +1321,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1258
1321
  monitoring ? "next check" : "",
1259
1322
  n > 0 ? `${n} queued` : "",
1260
1323
  ].filter(Boolean);
1261
- return withRecovery(`glla: ${marker}${details.length > 0 ? ` ${details.join(" · ")}` : ""}${recoverySuffix}${heldSuffix}`);
1324
+ return `glla: ${marker}${details.length > 0 ? ` ${details.join(" · ")}` : ""}${recoverySuffix}${heldSuffix}`;
1262
1325
  }
1263
1326
  // v0.34.65: a terminal goal names its outcome + wall duration instead of
1264
1327
  // clearing the segment (note.md 2026-08-07: "this seems weak for a complete
@@ -1352,6 +1415,16 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
1352
1415
  withAgents = orphanHead ? [orphanHead, ...agentLines] : [...agentLines];
1353
1416
  }
1354
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
+ }
1355
1428
  // v0.28.6 (E1): a persistence failure outranks everything — first line,
1356
1429
  // on every render, until a write lands again.
1357
1430
  let lines: string[] | undefined = withAgents;
@@ -1373,7 +1446,7 @@ export function buildWidgetLines(state: State, audit?: AuditDisplayProgress | nu
1373
1446
  function waitingListStatus(state: State, _now: number, theme?: DisplayTheme, width?: number): string {
1374
1447
  const queue = state.list ?? [];
1375
1448
  const head = queue[0];
1376
- const objective = head?.objective?.trim() ? sanitizeDisplayText(head.objective) : "unnamed queued item";
1449
+ const objective = head?.objective?.trim() ? displayObjective(head.objective) : "unnamed queued item";
1377
1450
  const hold = typeof state.loadHoldAt === "number" ? " · held on restore" : "";
1378
1451
  // Audit 2026-09-06: the width budget truncates the END of the status
1379
1452
  // line — the `/glla resume` action must survive, so the OBJECTIVE takes
@@ -1390,7 +1463,7 @@ function waitingListStatus(state: State, _now: number, theme?: DisplayTheme, wid
1390
1463
  function waitingListLines(state: State, theme?: DisplayTheme, width?: number): string[] {
1391
1464
  const queue = state.list ?? [];
1392
1465
  const head = queue[0];
1393
- const objective = head?.objective?.trim() ? sanitizeDisplayText(head.objective) : "unnamed queued item";
1466
+ const objective = head?.objective?.trim() ? displayObjective(head.objective) : "unnamed queued item";
1394
1467
  const objectiveBudget = budgetFor(width, visibleLen("├─ up next: "), 56);
1395
1468
  const action = typeof state.loadHoldAt === "number"
1396
1469
  ? "held on restore · /glla resume starts the queue · /list next skips/chooses"
@@ -1555,7 +1628,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1555
1628
  const headIcon = headLive
1556
1629
  ? paint(theme, headLive.band === "fresh" ? "success" : headLive.band === "aging" ? "warning" : "error", headLive.breath)
1557
1630
  : icon;
1558
- 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}`;
1559
1632
  const lines = [head];
1560
1633
  // v0.38.8: durable verdict tally as a first-class card row — the widget
1561
1634
  // is the glance surface, and stored verdicts are the progress evidence
@@ -1572,20 +1645,22 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1572
1645
  lines.push(`├─ ${paint(theme, "dim", `Recovery: ${repairStep}`)}`);
1573
1646
  }
1574
1647
  // A model switch crosses an asynchronous boundary while the goal remains
1575
- // active. Keep the complete durable recovery projection on the widget too;
1576
- // otherwise the head says only "active" while pending/attempted/skipped
1577
- // 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;
1578
1652
  if (g.status === "active" && state.mainModelRecovery) {
1579
- const recoverySummary = formatMainModelRecoveryStatus(state.mainModelRecovery, extras?.mainModelFallbacks);
1580
- recoverySummary.forEach((line, i) => {
1581
- lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
1582
- });
1653
+ recoveryLine = compactMainModelRecoveryLine(state.mainModelRecovery, extras?.mainModelFallbacks, now);
1654
+ if (recoveryLine) lines.push(`├─ ${paint(theme, "dim", recoveryLine)}`);
1583
1655
  }
1584
1656
  // Model provenance is a card fact, not a notification: keep it visible
1585
- // across active, interrupted, auditing, and paused branches. In
1586
- // particular, never replace a configured forbidden ref with "none" — the
1587
- // skipped line explains why it did not handle the turn.
1588
- 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);
1589
1664
  const provenanceStart = lines.length;
1590
1665
  provenance.forEach((line, i) => {
1591
1666
  lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
@@ -1915,12 +1990,22 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1915
1990
  // no recent action, pending task, or queue footer. Close that block with a
1916
1991
  // terminator instead of leaving `├─`/`│` suggesting missing rows.
1917
1992
  const provenanceEnd = provenanceStart + provenance.length;
1918
- if (provenance.length > 0 && provenanceEnd === lines.length) {
1993
+ const detailedAgentRows = extras?.agents?.lines ?? (extras?.agents?.line ? [extras.agents.line] : []);
1994
+ if (provenance.length > 0 && detailedAgentRows.length === 0 && provenanceEnd === lines.length) {
1919
1995
  const last = lines[provenanceEnd - 1]!;
1920
1996
  if (last.startsWith("├─ ") || last.startsWith("│ ")) {
1921
1997
  lines[provenanceEnd - 1] = `└─ ${last.slice(3)}`;
1922
1998
  }
1923
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
+ }
1924
2009
  return lines;
1925
2010
  }
1926
2011
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.28",
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",