pi-goal-list-loop-audit 0.38.43 → 0.38.44

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.44 — stale-version surfacing: running version in the status line (2026-09-10)
4
+
5
+ ### Fixed
6
+
7
+ - **Stale-session visibility (field 20260909_161057):** a live session rendered the pre-0.38.39 summary voice while the repo shipped 0.38.42 with nothing visible saying so. The status line now always carries the running version (`glla: … · v0.38.44`) on every branch; when the registry is ahead it nudges (`· update vX available`).
8
+ - **Cached staleness check:** new `extensions/glla-update-check.ts` — the render reads the `.pi-glla/update-check.json` sidecar only and never touches the network; a throttled (24h TTL) fire-and-forget `npm view` refresh rides the command/lifecycle contact gate and fails silently offline.
9
+ - **`/glla version` names staleness:** appends `Registry latest` plus the concrete update path (`pi install npm:pi-goal-list-loop-audit@latest` then `/reload`) when the sidecar proves the session stale; legacy three lines unchanged with no cache.
10
+ - **INSTALL.md `Updating` section:** documents the nudge, the update command, and the `/reload`-every-session requirement. Pure version/compare helpers live in `glla-version.ts` (no import cycle); the display module keeps zero runtime imports via a precomputed `versionTail` extra.
11
+
12
+ ### Verification
13
+
14
+ - Full `release:check` 0 failures; `tsc --noEmit` clean.
15
+
3
16
  ## 0.38.43 — ship merged PR #50, drop orphaned helpers (2026-09-10)
4
17
 
5
18
  ### Fixed
package/INSTALL.md CHANGED
@@ -29,6 +29,26 @@ GLLA loads into new pi sessions. If pi is already open, reload that session:
29
29
  /reload
30
30
  ```
31
31
 
32
+ ## Updating
33
+
34
+ The status line always shows the running version (`glla: … · v0.38.44`).
35
+ When the npm registry is ahead, it also nudges:
36
+
37
+ ```text
38
+ glla: … · v0.38.43 · update v0.38.44 available
39
+ ```
40
+
41
+ The nudge comes from a daily sidecar check (`.pi-glla/update-check.json`),
42
+ refreshed on command contact and never blocking a turn. To update:
43
+
44
+ ```bash
45
+ pi install npm:pi-goal-list-loop-audit@latest
46
+ ```
47
+
48
+ Then `/reload` every open pi session — a session keeps running the
49
+ version it loaded with until reloaded. `/glla version` says whether the
50
+ current session is stale and repeats the update command.
51
+
32
52
  ### Recommended companions
33
53
 
34
54
  The structured-question companion is recommended for the intended drafting
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.43 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.44 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
@@ -0,0 +1,106 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { compareVersions, GLLA_PACKAGE_NAME, updateCheckPath } from "./glla-version.js";
5
+
6
+ export { compareVersions, updateCheckPath };
7
+
8
+ /**
9
+ * v0.38.44 (field 20260909_161057): a live session rendered the
10
+ * pre-0.38.39 summary voice while the repo shipped 0.38.42 — nothing
11
+ * visible told the session it runs old GLLA. Shipped fixes only reach
12
+ * sessions that know they are stale, so the status line carries the
13
+ * running version plus a nudge when the registry is ahead.
14
+ *
15
+ * Render discipline: the status/card render reads the sidecar cache ONLY
16
+ * and never touches the network. A throttled fire-and-forget refresh
17
+ * rides the command/lifecycle contact gate; failures (offline, slow
18
+ * registry, missing npm) leave the old cache in place and never surface.
19
+ */
20
+
21
+ export const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1_000;
22
+ export const UPDATE_CHECK_TIMEOUT_MS = 15_000;
23
+
24
+ export interface UpdateCheckCache {
25
+ latest: string;
26
+ checkedAt: number;
27
+ }
28
+
29
+ /** Read the cache; null when missing, unreadable, malformed, or future-dated. */
30
+ export function readUpdateCheck(cwd: string): UpdateCheckCache | null {
31
+ try {
32
+ const parsed: unknown = JSON.parse(fs.readFileSync(updateCheckPath(cwd), "utf8"));
33
+ if (typeof parsed !== "object" || parsed === null) return null;
34
+ const record = parsed as Record<string, unknown>;
35
+ if (typeof record.latest !== "string" || !record.latest.trim()) return null;
36
+ if (typeof record.checkedAt !== "number" || !Number.isFinite(record.checkedAt)) return null;
37
+ if (record.checkedAt > Date.now() + 60_000) return null;
38
+ return { latest: record.latest.trim(), checkedAt: record.checkedAt };
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export type SpawnFn = (
45
+ command: string,
46
+ args: string[],
47
+ options: { timeout: number },
48
+ ) => { on(event: "close", listener: (code: number | null) => void): void; stdout: { on(event: "data", listener: (chunk: Buffer) => void): void } };
49
+
50
+ /**
51
+ * Throttled fire-and-forget refresh: returns immediately in every case.
52
+ * Skips when the cache is within TTL; otherwise shells `npm view` with a
53
+ * bounded timeout and rewrites the cache on success. Never throws — an
54
+ * offline session simply keeps rendering without a nudge.
55
+ */
56
+ export function refreshUpdateCheck(
57
+ cwd: string,
58
+ now = Date.now(),
59
+ spawnFn: SpawnFn = spawn as unknown as SpawnFn,
60
+ ): void {
61
+ try {
62
+ const cached = readUpdateCheck(cwd);
63
+ if (cached && now - cached.checkedAt < UPDATE_CHECK_TTL_MS) return;
64
+ const child = spawnFn("npm", ["view", GLLA_PACKAGE_NAME, "version"], { timeout: UPDATE_CHECK_TIMEOUT_MS });
65
+ let out = "";
66
+ child.stdout.on("data", (chunk) => {
67
+ out += chunk.toString();
68
+ if (out.length > 256) out = out.slice(-256);
69
+ });
70
+ child.on("close", (code) => {
71
+ try {
72
+ if (code !== 0) return;
73
+ const latest = out.trim().split(/\s+/).pop() ?? "";
74
+ if (!latest) return;
75
+ const dir = path.dirname(updateCheckPath(cwd));
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ fs.writeFileSync(updateCheckPath(cwd), JSON.stringify({ latest, checkedAt: Date.now() }));
78
+ } catch {
79
+ // Cache write failure is invisible by design.
80
+ }
81
+ });
82
+ } catch {
83
+ // Spawn failure (no npm, no shell) is invisible by design.
84
+ }
85
+ }
86
+
87
+ /**
88
+ * The status-tail segment: `· v<running>` always (so the running
89
+ * version is visible on every branch), plus `· update v<latest>
90
+ * available` only when the cache proves the registry is ahead.
91
+ * Empty string when the running version is unknown.
92
+ */
93
+ export function buildVersionTail(running: string, latest: string | null): string {
94
+ if (!running || running === "unknown") return "";
95
+ const nudge = latest ? staleUpdateNudge(running, { latest, checkedAt: 0 }) : null;
96
+ return `· v${running}${nudge ? ` · ${nudge}` : ""}`;
97
+ }
98
+
99
+ /** The nudge proper: null when there is nothing to say (no cache yet,
100
+ * unparseable versions, or already current). "unknown" running versions
101
+ * never nudge — a damaged manifest must not cry wolf. */
102
+ export function staleUpdateNudge(running: string, cached: UpdateCheckCache | null): string | null {
103
+ if (!cached || running === "unknown") return null;
104
+ if (compareVersions(cached.latest, running) > 0) return `update v${cached.latest} available`;
105
+ return null;
106
+ }
@@ -4,6 +4,28 @@ import { fileURLToPath } from "node:url";
4
4
 
5
5
  export const GLLA_PACKAGE_NAME = "pi-goal-list-loop-audit";
6
6
 
7
+ /** Sidecar path shared with the update check (kept here so both
8
+ * glla-version and glla-update-check resolve it without a cycle). */
9
+ export function updateCheckPath(cwd: string): string {
10
+ return path.join(cwd, ".pi-glla", "update-check.json");
11
+ }
12
+
13
+ /** Numeric semver-ish compare: 1 when a > b, -1 when a < b, 0 when equal
14
+ * or unparseable. Pure — shared by the version command and the nudge. */
15
+ export function compareVersions(a: string, b: string): number {
16
+ const pa = a.trim().replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10));
17
+ const pb = b.trim().replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10));
18
+ if (pa.some((n) => !Number.isFinite(n)) || pb.some((n) => !Number.isFinite(n))) return 0;
19
+ const len = Math.max(pa.length, pb.length);
20
+ for (let i = 0; i < len; i++) {
21
+ const x = pa[i] ?? 0;
22
+ const y = pb[i] ?? 0;
23
+ if (x > y) return 1;
24
+ if (x < y) return -1;
25
+ }
26
+ return 0;
27
+ }
28
+
7
29
  const PACKAGE_JSON_PATH = path.resolve(
8
30
  path.dirname(fileURLToPath(import.meta.url)),
9
31
  "../package.json",
@@ -38,10 +60,33 @@ export function readGllaVersionInfo(): GllaVersionInfo {
38
60
  return { name: GLLA_PACKAGE_NAME, version: "unknown" };
39
61
  }
40
62
 
41
- export function formatGllaVersion(info: GllaVersionInfo = readGllaVersionInfo()): string {
42
- return [
63
+ export function formatGllaVersion(cwd?: string, info: GllaVersionInfo = readGllaVersionInfo()): string {
64
+ const lines = [
43
65
  `${info.name} v${info.version}`,
44
66
  "Installed package version.",
45
67
  `Compare with registry latest: npm view ${info.name} version`,
46
- ].join("\n");
68
+ ];
69
+ // v0.38.44 (field 20260909_161057): when the update sidecar has a
70
+ // cached registry latest, say whether this session is stale and how
71
+ // to fix it. No cache (offline / never checked) keeps the legacy
72
+ // three lines — absence of evidence is not evidence of freshness.
73
+ if (cwd) {
74
+ try {
75
+ const parsed: unknown = JSON.parse(fs.readFileSync(updateCheckPath(cwd), "utf8"));
76
+ const latest = typeof parsed === "object" && parsed !== null
77
+ ? (parsed as Record<string, unknown>).latest
78
+ : undefined;
79
+ if (typeof latest === "string" && latest.trim()) {
80
+ lines.push(`Registry latest: v${latest.trim()}.`);
81
+ lines.push(
82
+ compareVersions(latest, info.version) > 0
83
+ ? `This session is stale — update: pi install npm:${info.name}@latest then /reload.`
84
+ : "This session is up to date.",
85
+ );
86
+ }
87
+ } catch {
88
+ // Sidecar read failure keeps the legacy output.
89
+ }
90
+ }
91
+ return lines.join("\n");
47
92
  }
@@ -36,6 +36,7 @@ import type { SettingsSectionId } from "./settings-menu.js";
36
36
  import { cmdLoop, clearLoopTimer, finishLoopGit, isLoopActive, scheduleLoopTick } from "./goal-loop.js";
37
37
  import { chooseObjectiveConflict, liveObjectives } from "./goal-objective-conflict.js";
38
38
  import { formatGllaVersion } from "./glla-version.js";
39
+ import { readUpdateCheck } from "./glla-update-check.js";
39
40
  import { cmdGllaOwner, cmdGllaTakeover } from "./state-root-owner.js";
40
41
  import { AUDIT_JOB_CLEANUP_MIN_AGE_MS, cancelDetachedGoalCompletionAuditor, cleanupDeadAuditJobs, inspectAuditJobHealth, DEFAULT_AUDITOR_STALL_MS, DEFAULT_AUDITOR_TOOL_TIMEOUT_MS } from "./goal-loop-auditor-process.js";
41
42
  import { releaseAuditorSurface } from "./loops/goal-auditor-surface.js";
@@ -2385,7 +2386,7 @@ function cmdAudits(args: string, ctx: ExtensionContext): void {
2385
2386
  // version lookup next to the command surface makes an installed extension
2386
2387
  // identify itself even when its host session is stale or no live goal exists.
2387
2388
  function cmdGllaVersion(ctx: ExtensionContext): void {
2388
- ctx.ui.notify(formatGllaVersion(), "info");
2389
+ ctx.ui.notify(formatGllaVersion(ctx.cwd), "info");
2389
2390
  }
2390
2391
 
2391
2392
  // v0.35.72: /glla bug — lightweight failure capture that never touches
@@ -216,6 +216,12 @@ export interface WidgetExtras {
216
216
  auditorProgressSignals?: boolean;
217
217
  /** Effective global main-model backup order for truthful recovery HUDs. */
218
218
  mainModelFallbacks?: string[];
219
+ /** v0.38.44 (field 20260909_161057): precomputed status-tail version
220
+ * segment (`· v<running>` plus the update nudge when the cache proves
221
+ * the registry is ahead). Resolved at render contact by the caller —
222
+ * this module keeps zero runtime imports, so the pure compare/nudge
223
+ * logic lives in glla-update-check.ts. */
224
+ versionTail?: string;
219
225
  /** Truthful model-selection provenance for the active goal card/footer. */
220
226
  modelProvenance?: ModelProvenanceDisplay;
221
227
  /** Deterministic durable-vs-defer decision surface. Kept optional so the
@@ -1137,8 +1143,15 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
1137
1143
  const withAgentSummary = base && extras?.agents?.line
1138
1144
  ? `${base} · ${extras.agents.line.replace(/^●\s*/, "")}`
1139
1145
  : base;
1140
- if (!withAgentSummary || typeof state.supervisorPausedAt !== "number") return truncateStatusToWidth(withAgentSummary, width);
1141
- return truncateStatusToWidth(withAgentSummary.replace(/^glla:/, `glla: ${paint(theme, "warning", "⏸ supervisor")} ·`), width);
1146
+ // v0.38.44 (field 20260909_161057): the running version rides the tail
1147
+ // of EVERY branch — a stale session must be visible at a glance, not
1148
+ // discoverable via a command. The update nudge appears only when the
1149
+ // sidecar cache proves the registry is ahead; render never touches the
1150
+ // network (refresh rides the command-contact gate).
1151
+ const versionTail = extras?.versionTail ? ` ${extras.versionTail}` : "";
1152
+ const withVersion = withAgentSummary && versionTail ? `${withAgentSummary}${versionTail}` : withAgentSummary;
1153
+ if (!withVersion || typeof state.supervisorPausedAt !== "number") return truncateStatusToWidth(withVersion, width);
1154
+ return truncateStatusToWidth(withVersion.replace(/^glla:/, `glla: ${paint(theme, "warning", "⏸ supervisor")} ·`), width);
1142
1155
  }
1143
1156
 
1144
1157
  /** Status-line width budget — the auditing/paused/loop branches concatenate
@@ -125,6 +125,7 @@ isGoalRevisionCurrent,
125
125
  type ListItem,
126
126
  } from "../goal-loop-core.js";
127
127
  import { replayUndeliveredApprovalRenders } from "../approval-render-store.js";
128
+ import { refreshUpdateCheck } from "../glla-update-check.js"; // v0.38.44 stale-version nudge
128
129
  import {
129
130
  createContinuationDispatch,
130
131
  dispatchMatchesOwner,
@@ -739,6 +740,10 @@ export function __testOnlyClassifyStaleContinuation(content: string, cwd: string
739
740
  /** Commands and lifecycle contacts share the same ownership and delivery gates. */
740
741
  function replayApprovalSummariesOnContact(ctx: ExtensionContext): void {
741
742
  if (shouldSkipApprovalRenderReplay(ctx)) return;
743
+ // v0.38.44 (field 20260909_161057): the update-sidecar refresh rides
744
+ // the same contact gate — throttled + fire-and-forget inside, so this
745
+ // never blocks the turn and never surfaces on failure.
746
+ refreshUpdateCheck(ctx.cwd);
742
747
  replayUndeliveredApprovalRenders(ctx, (entry) => sendTerminalCompletionNotice(ctx, {
743
748
  goalId: entry.goalId, outcome: entry.objective, details: [], chatLines: entry.chatLines,
744
749
  }));
@@ -259,6 +259,8 @@ import {
259
259
  type ModelPickItem,
260
260
  } from "../model-picker.js";
261
261
  import { consumeRecoveryResume } from "../goal-recovery.js"; // decomposition step 3 (v0.34.111)
262
+ import { buildVersionTail, readUpdateCheck } from "../glla-update-check.js"; // v0.38.44 version tail
263
+ import { readGllaVersionInfo } from "../glla-version.js"; // v0.38.44 version tail
262
264
  import {
263
265
  createGoalHeartbeat,
264
266
  endSubagentHangProbe,
@@ -500,6 +502,14 @@ let lastUIRenderAt = 0;
500
502
  let lastUIRenderContext: ExtensionContext | null = null;
501
503
  let lastUIStatusText: string | undefined;
502
504
  let lastUIWidgetKey: string | undefined;
505
+ // v0.38.44: the running version is fixed for the process lifetime (the
506
+ // loaded code IS the version), so resolve package.json once instead of on
507
+ // every render tick.
508
+ let cachedRunningGllaVersion: string | null = null;
509
+ function runningGllaVersion(): string {
510
+ if (cachedRunningGllaVersion === null) cachedRunningGllaVersion = readGllaVersionInfo().version;
511
+ return cachedRunningGllaVersion;
512
+ }
503
513
 
504
514
  /**
505
515
  * Completion-auditor progress is ephemeral, but it still has an owner. A
@@ -826,6 +836,11 @@ function refreshUI(ctx: ExtensionContext, force = false): void {
826
836
  modelProvenance,
827
837
  ...(durableDeferRecommendation ? { durableDeferRecommendation } : {}),
828
838
  ...(lastAuditorQuietStretch ? { auditorQuietStretch: lastAuditorQuietStretch } : {}),
839
+ // v0.38.44 (field 20260909_161057): precomputed version tail for
840
+ // the status line — package.json running version + update-sidecar
841
+ // latest. Render never touches the network; the sidecar refresh
842
+ // rides the command-contact gate.
843
+ versionTail: buildVersionTail(runningGllaVersion(), readUpdateCheck(ctx.cwd)?.latest ?? null),
829
844
  };
830
845
  const statusText = buildStatusText(state, latestAuditProgress, now, theme, extras, width);
831
846
  const widgetLines = buildWidgetLines(state, latestAuditProgress, now, theme, width, extras);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.38.43",
3
+ "version": "0.38.44",
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",