pi-goal-list-loop-audit 0.39.21 → 0.39.22

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
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.39.22 — stalled-audit UI: dead audits named on every surface (2026-10-10)
6
+
7
+ ### Audit visibility
8
+
9
+ - Past one hour of worker silence with no in-budget tool, a quiet audit
10
+ escalates to **stalled** on every surface: the detailed card drops the
11
+ live-worker claim (`no worker responds`) and offers the retry
12
+ (`/goal resume retries the claim`); the compact glance card and footer
13
+ agree (`AUDIT STALLED`, error tone, ✖ glyph) instead of claiming
14
+ `detached worker` / `No action needed`. The quiet watcher fires its own
15
+ one-shot stall notice.
16
+ - Elapsed ages read in days past 24h (`15d 00h`, not `360h 00m`).
17
+ - Display-only: no retry or recovery behavior changed.
18
+
5
19
  ## 0.39.21 — 5h retry-window policy, stall-proof test cleanup (2026-10-09)
6
20
 
7
21
  ### Recovery policy
package/docs/INDEX.md CHANGED
@@ -31,7 +31,7 @@
31
31
  ## Versions and shipped content
32
32
 
33
33
  [CHANGELOG.md](../CHANGELOG.md) records the release history from
34
- v0.35.14–v0.39.21 and unreleased work. `/glla version` reports the loaded version
34
+ v0.35.14–v0.39.22 and unreleased work. `/glla version` reports the loaded version
35
35
  and registry comparison. A checkout can contain changes not yet published.
36
36
 
37
37
  The package includes this documentation, [examples](../examples/),
@@ -3,8 +3,8 @@
3
3
  Generated by `node scripts/generate-inventory.mjs`. Run with `--check` to detect drift. Counts describe files and literal registrations, not executed coverage.
4
4
 
5
5
  - Extension/script files: 109
6
- - Extension/script lines: 67486
7
- - Top-level test files: 378
6
+ - Extension/script lines: 67535
7
+ - Top-level test files: 379
8
8
  - Ambient bridge names: 179 (audit baseline: 179)
9
9
  - Commands: `/glla`, `/goal`, `/list`, `/loop`, `/review`
10
10
  - Tools: `audit_project_increment`, `block_project_requirement`, `claim_project_task`, `complete_goal`, `complete_task`, `list_activate`, `list_add`, `list_status`, `pause_goal`, `plan_project_increment`, `propose_goal_draft`, `propose_loop_draft`, `propose_loop_refine`, `propose_project_refinement`, `propose_project_requirements`, `propose_task_list`, `record_goal_judgment`, `resume_goal`, `unblock_project_requirement`, `update_task_batch`, `update_task_status`, `wait_for_background`
@@ -17,7 +17,7 @@ Generated by `node scripts/generate-inventory.mjs`. Run with `--check` to detect
17
17
  | [extensions/loops/goal-tools.ts](../extensions/loops/goal-tools.ts) | 4227 |
18
18
  | [extensions/loops/goal-activation.ts](../extensions/loops/goal-activation.ts) | 3880 |
19
19
  | [extensions/goal-commands.ts](../extensions/goal-commands.ts) | 3614 |
20
- | [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) | 2858 |
20
+ | [extensions/goal-loop-display.ts](../extensions/goal-loop-display.ts) | 2884 |
21
21
  | [extensions/goal-loop-auditor-process.ts](../extensions/goal-loop-auditor-process.ts) | 2685 |
22
22
  | [extensions/loops/goal-auditor-hooks.ts](../extensions/loops/goal-auditor-hooks.ts) | 2654 |
23
23
  | [extensions/goal-heartbeat.ts](../extensions/goal-heartbeat.ts) | 2104 |
@@ -58,7 +58,11 @@ export function fmtElapsed(ms: number): string {
58
58
  // liveness signal — minute-only granularity looks frozen on a 1s tick.
59
59
  if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
60
60
  const h = Math.floor(m / 60);
61
- return `${h}h ${String(m % 60).padStart(2, "0")}m`;
61
+ if (h < 24) return `${h}h ${String(m % 60).padStart(2, "0")}m`;
62
+ // v0.39.22: day granularity past 24h — a 15-day stalled audit reads
63
+ // "15d 00h", not "360h 00m". Only affects renders past a day.
64
+ const d = Math.floor(h / 24);
65
+ return `${d}d ${String(h % 24).padStart(2, "0")}h`;
62
66
  }
63
67
 
64
68
  export function fmtTokens(n: number): string {
@@ -801,11 +805,17 @@ export interface AuditDisplayProgress {
801
805
  sessionPath?: string;
802
806
  }
803
807
 
804
- type AuditorDisplayPhase = "queued" | "running" | "quiet" | "blocked" | "awaiting-verdict";
808
+ type AuditorDisplayPhase = "queued" | "running" | "quiet" | "stalled" | "blocked" | "awaiting-verdict";
805
809
  /** v0.35.15: exported — the parent-side quiet watcher (goal-ui ticker) uses
806
810
  * the same threshold for the one-shot proactive notify so the notification
807
811
  * and the status chip can never disagree about when "quiet" begins. */
808
812
  export const AUDITOR_QUIET_MS = 3 * 60_000;
813
+ /** v0.39.22 (field 2026-10-10: seo goal quiet 15 days, worker dead):
814
+ * quiet past this bound with no worker activity and no in-budget tool is
815
+ * a dead worker, not a slow one. An hour of total silence exceeds any
816
+ * legitimate inter-tool gap; the stalled presentation names the death and
817
+ * offers the retry (/goal resume) instead of only the discard. */
818
+ export const AUDITOR_STALLED_MS = 60 * 60_000;
809
819
  /** v0.35.15: how long the "silent Xm then resumed" fact stays in the
810
820
  * detailed auditor card after a quiet stretch ends — long enough to be seen,
811
821
  * short enough not to become permanent noise. */
@@ -911,6 +921,7 @@ export function auditorDisplayPhase(g: Goal, audit: AuditDisplayProgress | null
911
921
  toolAgeMs < audit.toolTimeoutMs
912
922
  )
913
923
  return "running";
924
+ if (age > AUDITOR_STALLED_MS) return "stalled";
914
925
  return "quiet";
915
926
  }
916
927
  // Same lifecycle scope as above: a stale goal snapshot that still
@@ -959,6 +970,7 @@ function auditorPhaseLabel(phase: AuditorDisplayPhase): string {
959
970
  case "queued": return "queued";
960
971
  case "running": return "running";
961
972
  case "quiet": return "quiet";
973
+ case "stalled": return "stalled";
962
974
  case "blocked": return "blocked";
963
975
  case "awaiting-verdict": return "awaiting completion review";
964
976
  }
@@ -973,6 +985,7 @@ export function auditorPhaseGlyph(phase: AuditorDisplayPhase): string {
973
985
  case "queued": return "⋯"; // waiting to start
974
986
  case "running": return "▶"; // worker alive
975
987
  case "quiet": return "◌"; // silent — possibly stuck
988
+ case "stalled": return "✖"; // silent past the death bound — worker dead
976
989
  case "blocked": return "⛔"; // infra failure / needs intervention
977
990
  case "awaiting-verdict": return "✓"; // worker done, verdict pending
978
991
  }
@@ -1508,11 +1521,11 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1508
1521
  // v0.35.15: leading phase glyph + draining activity meter — a glance
1509
1522
  // answers "is the audit alive?" without reading the sentence.
1510
1523
  const phaseText = `auditor ${auditorPhaseGlyph(phase)} ${observed}`;
1511
- const color = phase === "blocked" || phase === "quiet" ? "warning" : live ? "success" : "accent";
1524
+ const color = phase === "stalled" ? "error" : phase === "blocked" || phase === "quiet" ? "warning" : live ? "success" : "accent";
1512
1525
  const activityMeter = auditorActivityMeter(audit, phase, now);
1513
1526
  const label = live
1514
1527
  ? `${paint(theme, "success", phaseText)} ${paint(theme, "accent", activityMeter)} ${activityBadge("AUDITOR · DETACHED · LIVE", now, theme)}`
1515
- : `${paint(theme, color, phaseText)} ${paint(theme, color === "warning" ? "warning" : "dim", activityMeter)}`;
1528
+ : `${paint(theme, color, phaseText)} ${paint(theme, color === "warning" || color === "error" ? color : "dim", activityMeter)}`;
1516
1529
  // The persistent footer is the liveness surface only: host, phase, and
1517
1530
  // freshness. The auditor card above already owns the transition hint
1518
1531
  // ("next:"), the worker attribution ("detached worker"), and the
@@ -1521,7 +1534,7 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
1521
1534
  const quietAge = phase === "quiet" ? auditorActivityAge(audit, now) : undefined;
1522
1535
  const quietSuffix = quietAge !== undefined ? ` · silent ${fmtElapsed(quietAge)}` : "";
1523
1536
  const compactPrefix = extras?.compactAuditCard
1524
- ? `${paint(theme, color, durableLabel ?? (toolWait ? `AUDIT TOOL WAIT · ${sanitizeDisplayText(audit!.currentTool!)} ${fmtElapsed(now - audit!.currentToolStartedAt!)} / ${fmtElapsed(audit!.toolTimeoutMs!)}` : phase === "running" ? "AUDIT RUNNING" : phase === "quiet" ? "AUDIT QUIET — may be stuck" : phase === "blocked" ? "AUDIT BLOCKED" : phase === "queued" ? "AUDIT STARTING" : "AUDIT REVIEW"))} · `
1537
+ ? `${paint(theme, color, durableLabel ?? (toolWait ? `AUDIT TOOL WAIT · ${sanitizeDisplayText(audit!.currentTool!)} ${fmtElapsed(now - audit!.currentToolStartedAt!)} / ${fmtElapsed(audit!.toolTimeoutMs!)}` : phase === "running" ? "AUDIT RUNNING" : phase === "quiet" ? "AUDIT QUIET — may be stuck" : phase === "stalled" ? "AUDIT STALLED — worker dead" : phase === "blocked" ? "AUDIT BLOCKED" : phase === "queued" ? "AUDIT STARTING" : "AUDIT REVIEW"))} · `
1525
1538
  : "";
1526
1539
  const activityAge = auditorActivityAge(audit, now);
1527
1540
  const freshness = extras?.compactAuditCard && phase !== "quiet" && !toolWait
@@ -1983,12 +1996,13 @@ function heldLoopLines(l: LoopState, now: number, theme?: DisplayTheme, width?:
1983
1996
  }
1984
1997
 
1985
1998
  /** Phase colour for the auditing card's lead row. Blocked/quiet are
1986
- * warnings; a verdict that landed or live worker evidence is success;
1999
+ * warnings, stalled is an error; a verdict that landed or live worker evidence is success;
1987
2000
  * queued/running-without-evidence stay neutral accent — a hung provider
1988
2001
  * must never wear success (v0.34.39). Colour always pairs with the phase
1989
2002
  * words (paint wraps the label, never replaces it), so a themeless
1990
2003
  * renderer loses colour but never meaning. */
1991
2004
  function auditorPhaseTone(phase: AuditorDisplayPhase, live: boolean): DisplayColor {
2005
+ if (phase === "stalled") return "error";
1992
2006
  if (phase === "blocked" || phase === "quiet") return "warning";
1993
2007
  if (phase === "awaiting-verdict" || (phase === "running" && live)) return "success";
1994
2008
  return "accent";
@@ -2009,6 +2023,7 @@ function auditorCardModelRef(audit: AuditDisplayProgress | null | undefined, cla
2009
2023
  * row and the closing line can never name different actions. */
2010
2024
  function auditorNextAction(phase: AuditorDisplayPhase): string {
2011
2025
  if (phase === "quiet") return "/goal cancel discards the claim";
2026
+ if (phase === "stalled") return "/goal resume retries the claim";
2012
2027
  if (phase === "blocked") return "/goal resume retries the claim";
2013
2028
  if (phase === "awaiting-verdict") return "completion review applying";
2014
2029
  if (phase === "queued") return "worker starting";
@@ -2082,7 +2097,11 @@ function auditingCardBlock(g: Goal, audit: AuditDisplayProgress | null | undefin
2082
2097
  const staleSeg = activity === undefined && durable?.stale && durable.idleMs !== undefined
2083
2098
  ? ` · no progress ${fmtElapsed(durable.idleMs)}`
2084
2099
  : "";
2085
- const lead = [`├─ auditor: ${paint(theme, auditorPhaseTone(phase, phaseLive), effectivePhaseLabel)}${detail} · detached worker · ${ageSeg}${staleSeg}`];
2100
+ // v0.39.22: a stalled audit asserts no live worker — printing
2101
+ // "detached worker" would claim a process that has not spoken for over
2102
+ // an hour. The `auditor:` anchor stays; only the worker segment changes.
2103
+ const workerSeg = phase === "stalled" ? "no worker responds" : "detached worker";
2104
+ const lead = [`├─ auditor: ${paint(theme, auditorPhaseTone(phase, phaseLive), effectivePhaseLabel)}${detail} · ${workerSeg} · ${ageSeg}${staleSeg}`];
2086
2105
  // Lead row 2: current tool + time budget, effective model + thinking.
2087
2106
  // Absent facts are omitted, never invented. The tool observation lives
2088
2107
  // ONLY here (it moved out of the tail) so the card keeps its
@@ -2161,7 +2180,9 @@ function auditingCardBlock(g: Goal, audit: AuditDisplayProgress | null | undefin
2161
2180
  tail.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", observation)}`);
2162
2181
  });
2163
2182
  const last = auditorLastActivity(audit, now);
2164
- if (phase === "quiet") {
2183
+ if (phase === "stalled") {
2184
+ tail.push(`└─ ${paint(theme, "error", `auditor stalled ${fmtElapsed(activity ?? 0)} — no worker activity, claim durable; /goal resume retries, /goal cancel discards`)}`);
2185
+ } else if (phase === "quiet") {
2165
2186
  const quietMs = activity ?? 0;
2166
2187
  tail.push(`└─ ${paint(theme, "warning", `auditor quiet ${fmtElapsed(quietMs)}${last} — may be stuck; /goal cancel discards the claim`)}`);
2167
2188
  } else if (phase === "blocked") {
@@ -2306,7 +2327,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
2306
2327
  if (extras?.compactAuditCard) {
2307
2328
  const phase = auditorDisplayPhase(g, audit, now);
2308
2329
  const durable = auditLifecycleProjection(g.pendingCompletion, { now });
2309
- const needsRecovery = auditRecoveryPending(g) || phase === "quiet" || phase === "blocked" || durable?.phase === "settling";
2330
+ // v0.39.22: stalled needs recovery like quiet/blocked — without it
2331
+ // the glance path falls through to "Audit review pending · detached
2332
+ // worker" + "No action needed", preserving the can't-tell gap on
2333
+ // the production surface. With it, the glance reuses the detailed
2334
+ // lead (stalled · no worker responds) and its retry action row.
2335
+ const needsRecovery = auditRecoveryPending(g) || phase === "quiet" || phase === "stalled" || phase === "blocked" || durable?.phase === "settling";
2310
2336
  const elapsed = auditorElapsedMs(audit, now);
2311
2337
  const finished = audit?.toolCalls?.filter(call => call.finishedAt !== undefined).length;
2312
2338
  const age = auditorActivityAge(audit, now);
@@ -914,6 +914,8 @@ function scheduleUIRefresh(): void {
914
914
  // suppresses the notify (it is automatic machinery), never the recording.
915
915
  let auditorQuietSince: number | null = null;
916
916
  let auditorQuietNotified = false;
917
+ let auditorStallSince: number | null = null;
918
+ let auditorStallNotified = false;
917
919
  let lastAuditorQuietStretch: { ms: number; endedAt: number } | null = null;
918
920
 
919
921
  /** Drive the quiet watcher once per UI tick. Returns a warning message to
@@ -929,6 +931,8 @@ export function __auditorQuietWatchTick(now = Date.now()): string | null {
929
931
  auditorQuietSince = null;
930
932
  auditorQuietNotified = false;
931
933
  }
934
+ auditorStallSince = null;
935
+ auditorStallNotified = false;
932
936
  return null;
933
937
  }
934
938
  const phase = auditorDisplayPhase(g, latestAuditProgress, now);
@@ -945,6 +949,23 @@ export function __auditorQuietWatchTick(now = Date.now()): string | null {
945
949
  }
946
950
  return null;
947
951
  }
952
+ // v0.39.22: a stalled worker gets its own one-shot notify — the quiet
953
+ // notice fired an hour ago and reads as routine by now. The claim stays
954
+ // durable either way; the notify names the retry, not just the discard.
955
+ if (phase === "stalled") {
956
+ if (auditorStallSince === null) {
957
+ auditorStallSince = typeof latestAuditProgress?.lastActivityAt === "number"
958
+ ? latestAuditProgress.lastActivityAt
959
+ : now;
960
+ }
961
+ if (!auditorStallNotified && !supervisorPaused(state)) {
962
+ auditorStallNotified = true;
963
+ return `glla: the detached auditor is STALLED — no worker activity for ${fmtElapsed(now - auditorStallSince)} and no tool running. The claim is durable; /goal resume retries it, /goal cancel discards it.`;
964
+ }
965
+ return null;
966
+ }
967
+ if (auditorStallSince !== null) auditorStallSince = null;
968
+ auditorStallNotified = false;
948
969
  // Left the quiet phase: record the ended stretch for the footer summary.
949
970
  if (auditorQuietSince !== null) {
950
971
  const ms = now - auditorQuietSince;
@@ -1212,6 +1233,8 @@ export function __testOnlyAuditorQuietWatchTick(
1212
1233
  export function __testOnlyResetAuditorQuietWatch(): void {
1213
1234
  auditorQuietSince = null;
1214
1235
  auditorQuietNotified = false;
1236
+ auditorStallSince = null;
1237
+ auditorStallNotified = false;
1215
1238
  lastAuditorQuietStretch = null;
1216
1239
  }
1217
1240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.39.21",
3
+ "version": "0.39.22",
4
4
  "description": "Durable work supervision for Pi: turn outcomes into goals, audited queues and spec-driven builds, recover interruptions, and verify completion against evidence.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "dracon",