pi-goal-list-loop-audit 0.38.42 → 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 +24 -0
- package/INSTALL.md +20 -0
- package/docs/INDEX.md +1 -1
- package/extensions/context-checkpoint.ts +28 -95
- package/extensions/glla-update-check.ts +106 -0
- package/extensions/glla-version.ts +48 -3
- package/extensions/goal-commands.ts +2 -1
- package/extensions/goal-loop-display.ts +15 -2
- package/extensions/loops/goal-activation.ts +5 -0
- package/extensions/loops/goal-ui.ts +15 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.38.43 — ship merged PR #50, drop orphaned helpers (2026-09-10)
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **PR #50 (Bjynt, merged):** authoritative + overflow checkpoints replace dynamic fields (task state, pending completion, auditor TODOs, latest audit, stopReason/pauseKind) with the byte-stable `see .pi-glla/active.jsonl` pointer, so tool calls stop invalidating the provider KV-cache prefix. Tests pin dynamic absence, pointer presence, and smaller byte fixtures.
|
|
21
|
+
- **Follow-up cleanup:** removed the now-callerless `taskState`, `pendingCompletionState`, `compactAuditEvidence`, `auditLabel`, `boundedTail`, and the orphaned `OVERFLOW_AUDIT_CHARS`.
|
|
22
|
+
|
|
23
|
+
### Verification
|
|
24
|
+
|
|
25
|
+
- Full `release:check` 0 failures; `tsc --noEmit` clean.
|
|
26
|
+
|
|
3
27
|
## 0.38.42 — trailer cleanup: model-free folded approval (2026-09-09)
|
|
4
28
|
|
|
5
29
|
### 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.
|
|
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
|
|
@@ -49,14 +49,6 @@ function boundedText(value: unknown, maxChars: number): string {
|
|
|
49
49
|
return normalized.slice(0, Math.max(0, maxChars - suffix.length)) + suffix;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function boundedTail(value: unknown, maxChars: number): string {
|
|
53
|
-
if (typeof value !== "string") return "";
|
|
54
|
-
const normalized = value.replace(/\r\n?/g, "\n").replace(/\u0000/g, "");
|
|
55
|
-
if (normalized.length <= maxChars) return normalized;
|
|
56
|
-
const prefix = "[head truncated; full audit evidence remains in durable state]\n…";
|
|
57
|
-
return prefix + normalized.slice(-Math.max(0, maxChars - prefix.length));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
52
|
function safeInline(value: unknown, maxChars: number): string {
|
|
61
53
|
return boundedText(value, maxChars)
|
|
62
54
|
.replace(/\n+/g, " ")
|
|
@@ -87,7 +79,7 @@ function loopNumber(value: unknown): string {
|
|
|
87
79
|
* provider call. Any byte change here invalidates the cache prefix for every
|
|
88
80
|
* token after it. This block therefore renders ONLY fields that are stable
|
|
89
81
|
* for the life of the loop run (target / measure / bounds config). Live
|
|
90
|
-
* counters (iteration, best, last, stall,
|
|
82
|
+
* counters (iteration, best, last, stall, history, tokens, timestamps) are
|
|
91
83
|
* intentionally NOT repeated here: they already ride the newest retained
|
|
92
84
|
* `[LOOP ITERATION …]` prompt (message-list suffix) and durable
|
|
93
85
|
* `.pi-glla/active.jsonl`. Rendering them here changed the prefix on every
|
|
@@ -103,38 +95,6 @@ function loopCheckpointLines(loop: LoopState): string[] {
|
|
|
103
95
|
];
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
function auditLabel(audit: { approved?: unknown; disapproved?: unknown; impossible?: unknown; error?: unknown; regressionShieldPassed?: unknown }): string {
|
|
107
|
-
if (audit.approved === true && audit.regressionShieldPassed === false) return "shield-blocked";
|
|
108
|
-
if (audit.approved === true) return "approved";
|
|
109
|
-
if (audit.impossible === true) return "impossible";
|
|
110
|
-
if (audit.disapproved === true) return "disapproved";
|
|
111
|
-
if (audit.error) return "infrastructure failure";
|
|
112
|
-
return "no verdict";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function taskState(goal: Goal): string {
|
|
116
|
-
const tasks = goal.taskList?.tasks ?? [];
|
|
117
|
-
if (tasks.length === 0) return "(no task list)";
|
|
118
|
-
const lines = tasks.slice(0, 40).map((task) =>
|
|
119
|
-
`${safeInline(task.id, 40)} [${safeInline(task.status, 30) || "unknown"}] ${safeInline(task.title, 180)}`,
|
|
120
|
-
);
|
|
121
|
-
if (tasks.length > lines.length) lines.push(`[…${tasks.length - lines.length} task(s) omitted; re-read durable task state]`);
|
|
122
|
-
return lines.join("\n");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function pendingCompletionState(goal: Goal): string {
|
|
126
|
-
const pending = goal.pendingCompletion;
|
|
127
|
-
if (!pending) return "(none)";
|
|
128
|
-
return [
|
|
129
|
-
`phase=${safeInline(pending.phase, 40) || "legacy/recovery-pending"}`,
|
|
130
|
-
`attemptId=${safeInline(pending.attemptId, 100) || "(none)"}`,
|
|
131
|
-
`recoveryReason=${safeInline(pending.recoveryReason, 180) || "(none)"}`,
|
|
132
|
-
`failureClass=${safeInline(pending.auditorFailureClass, 40) || "(none)"}`,
|
|
133
|
-
`failureCount=${typeof pending.auditorFailureCount === "number" ? pending.auditorFailureCount : "(none)"}`,
|
|
134
|
-
`fallbackExhausted=${pending.auditorFallbackExhausted === true ? "true" : "false"}`,
|
|
135
|
-
].join("; ");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
98
|
// The ordinary checkpoint keeps the complete bounded representation for
|
|
139
99
|
// readable, normal-sized state. Once that representation exceeds the hard
|
|
140
100
|
// cap, these are explicit reservations for the fields that must survive a
|
|
@@ -144,7 +104,6 @@ const OVERFLOW_OBJECTIVE_CHARS = 1_400;
|
|
|
144
104
|
const OVERFLOW_CONTRACT_CHARS = 1_400;
|
|
145
105
|
const OVERFLOW_LOOP_TARGET_CHARS = 1_100;
|
|
146
106
|
const OVERFLOW_LOOP_MEASURE_CHARS = 360;
|
|
147
|
-
const OVERFLOW_AUDIT_CHARS = 1_100;
|
|
148
107
|
const OVERFLOW_EMERGENCY_LINE_CHARS = 500;
|
|
149
108
|
|
|
150
109
|
function compactLoopCheckpointLines(loop: LoopState): string[] {
|
|
@@ -158,29 +117,6 @@ function compactLoopCheckpointLines(loop: LoopState): string[] {
|
|
|
158
117
|
];
|
|
159
118
|
}
|
|
160
119
|
|
|
161
|
-
function compactAuditEvidence(
|
|
162
|
-
latestAudit: { approved?: unknown; disapproved?: unknown; impossible?: unknown; error?: unknown; regressionShieldPassed?: unknown; at?: unknown; model?: unknown; revision?: unknown; report?: unknown } | undefined,
|
|
163
|
-
maxChars: number,
|
|
164
|
-
): string {
|
|
165
|
-
if (!latestAudit) return "(no audits captured)";
|
|
166
|
-
const metadata = [
|
|
167
|
-
`label=${auditLabel(latestAudit)}`,
|
|
168
|
-
`at=${safeInline(latestAudit.at, 80) || "(unknown)"}`,
|
|
169
|
-
`model=${safeInline(latestAudit.model, 100) || "(unknown)"}`,
|
|
170
|
-
`revision=${typeof latestAudit.revision === "number" ? latestAudit.revision : "legacy/unspecified"}`,
|
|
171
|
-
`shield=${latestAudit.regressionShieldPassed === false ? "failed" : latestAudit.regressionShieldPassed === true ? "passed" : "unspecified"}`,
|
|
172
|
-
].join("\n");
|
|
173
|
-
const opening = "<audit-evidence>";
|
|
174
|
-
const closing = "</audit-evidence>";
|
|
175
|
-
const reportBudget = maxChars - metadata.length - opening.length - closing.length - 2;
|
|
176
|
-
if (reportBudget <= 0) return boundedText(metadata, maxChars);
|
|
177
|
-
const report = safeBlock(
|
|
178
|
-
boundedTail(latestAudit.report || "(no report captured)", reportBudget),
|
|
179
|
-
reportBudget,
|
|
180
|
-
);
|
|
181
|
-
return `${metadata}\n${opening}\n${report}\n${closing}`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
120
|
function buildOverflowCheckpoint(
|
|
185
121
|
input: AuthoritativeCheckpointInput,
|
|
186
122
|
goal: Goal | null | undefined,
|
|
@@ -204,7 +140,9 @@ function buildOverflowCheckpoint(
|
|
|
204
140
|
goal
|
|
205
141
|
? `Verification contract: ${safeBlock(goal.verificationContract, OVERFLOW_CONTRACT_CHARS) || "(none recorded)"}`
|
|
206
142
|
: "Verification contract: (none — metric/loop bounds above are authoritative)",
|
|
207
|
-
|
|
143
|
+
goal
|
|
144
|
+
? `Dynamic state (task state / pending completion / auditor TODOs / latest audit): see .pi-glla/active.jsonl — read before acting`
|
|
145
|
+
: `Dynamic state (loop progress): see .pi-glla/active.jsonl — read before acting`,
|
|
208
146
|
loop
|
|
209
147
|
? "Lifecycle fence: continue only for this active loop target, current loop state, and session owner; if a goal is present, preserve its id/revision as paused context. Use durable state and artifacts as the authority after compaction, restart, or session replacement."
|
|
210
148
|
: "Lifecycle fence: continue only for this goal id and current revision/session owner; use durable state and artifacts as the authority after compaction, restart, or session replacement.",
|
|
@@ -214,12 +152,6 @@ function buildOverflowCheckpoint(
|
|
|
214
152
|
loop
|
|
215
153
|
? "Loop history summary: omitted by design — live history rides the newest retained loop prompt (keeps this checkpoint byte-stable for prefix-cache stability)"
|
|
216
154
|
: null,
|
|
217
|
-
goal
|
|
218
|
-
? `Auto-continuation: ${goal.autoContinue === true ? "enabled" : "disabled/unknown"}; stopReason=${safeInline(goal.stopReason, 220) || "(none)"}; pauseKind=${safeInline(goal.pauseKind, 40) || "(none)"}`
|
|
219
|
-
: `Auto-continuation: ${loop?.active === true ? "loop active" : "loop inactive"}; stopReason=${safeInline(loop?.stopReason, 220) || "(none)"}; pauseKind=(none)`,
|
|
220
|
-
`Pending completion: ${boundedText(goal ? pendingCompletionState(goal) : "(none — loop has no detached auditor claim)", 420)}`,
|
|
221
|
-
`Pending auditor TODOs:\n${boundedText(goal?.pendingTasks?.length ? goal.pendingTasks.slice(0, 12).map((task, index) => `${index + 1}. ${safeInline(task, 160)}`).join("\n") : "(none)", 520)}`,
|
|
222
|
-
`Task state:\n${boundedText(goal ? taskState(goal) : "(no goal task list)", 520)}`,
|
|
223
155
|
].filter((line): line is string => line !== null);
|
|
224
156
|
|
|
225
157
|
const lines = [...requiredLines];
|
|
@@ -243,26 +175,28 @@ function buildOverflowCheckpoint(
|
|
|
243
175
|
* long continuation template remains available in the newest retained
|
|
244
176
|
* payload, while this checkpoint protects state when older payloads are
|
|
245
177
|
* removed from the effective context.
|
|
178
|
+
*
|
|
179
|
+
* CACHE-STABILITY INVARIANT: This checkpoint is inserted EARLY in the message
|
|
180
|
+
* history (at the first removed goal-event payload position). Any byte change
|
|
181
|
+
* here invalidates the provider's KV cache prefix for all subsequent tokens.
|
|
182
|
+
* Therefore, ONLY stable fields are included:
|
|
183
|
+
* - Goal/loop identity, policy, revision, session generation
|
|
184
|
+
* - Objective, verification contract (stable for the life of the goal)
|
|
185
|
+
* - Loop target, measure, bounds (stable for the life of the loop)
|
|
186
|
+
* - Lifecycle fences
|
|
187
|
+
*
|
|
188
|
+
* DYNAMIC FIELDS EXCLUDED (they belong in the newest retained payload at the
|
|
189
|
+
* message suffix, not the checkpoint prefix):
|
|
190
|
+
* - Task state (changes on complete_task)
|
|
191
|
+
* - Pending completion (changes on complete_goal)
|
|
192
|
+
* - Pending auditor TODOs (changes on auditor disapproval)
|
|
193
|
+
* - Latest audit report (changes on auditor completion)
|
|
194
|
+
* - stopReason, pauseKind (change on pause/resume)
|
|
195
|
+
* - Auto-continuation setting (goal.autoContinue) is STABLE — included
|
|
246
196
|
*/
|
|
247
197
|
export function buildAuthoritativeContextCheckpoint(input: AuthoritativeCheckpointInput): string {
|
|
248
198
|
const { goal, loop } = input;
|
|
249
199
|
const latestAudit = goal?.auditHistory?.[goal.auditHistory.length - 1];
|
|
250
|
-
const auditReport = latestAudit?.report ? boundedTail(latestAudit.report, 2_000) : "(no report captured)";
|
|
251
|
-
const auditEvidence = latestAudit
|
|
252
|
-
? [
|
|
253
|
-
`label=${auditLabel(latestAudit)}`,
|
|
254
|
-
`at=${safeInline(latestAudit.at, 80) || "(unknown)"}`,
|
|
255
|
-
`model=${safeInline(latestAudit.model, 100) || "(unknown)"}`,
|
|
256
|
-
`revision=${typeof latestAudit.revision === "number" ? latestAudit.revision : "legacy/unspecified"}`,
|
|
257
|
-
`shield=${latestAudit.regressionShieldPassed === false ? "failed" : latestAudit.regressionShieldPassed === true ? "passed" : "unspecified"}`,
|
|
258
|
-
`<audit-evidence>\n${safeBlock(auditReport, 2_000)}\n</audit-evidence>`,
|
|
259
|
-
].join("\n")
|
|
260
|
-
: goal
|
|
261
|
-
? "(no audits on this goal yet)"
|
|
262
|
-
: "(no goal audits; active loop state is authoritative)";
|
|
263
|
-
const pendingTasks = goal?.pendingTasks?.length
|
|
264
|
-
? goal.pendingTasks.slice(0, 12).map((task, index) => `${index + 1}. ${safeInline(task, 240)}`).join("\n")
|
|
265
|
-
: "(none)";
|
|
266
200
|
const repairTarget = goal?.repairTarget
|
|
267
201
|
? [
|
|
268
202
|
`id=${safeInline(goal.repairTarget.id, 80)}`,
|
|
@@ -290,12 +224,11 @@ export function buildAuthoritativeContextCheckpoint(input: AuthoritativeCheckpoi
|
|
|
290
224
|
? `Verification contract: ${safeBlock(goal.verificationContract, 2_000) || "(none recorded)"}`
|
|
291
225
|
: "Verification contract: (none — metric/loop bounds above are authoritative)",
|
|
292
226
|
goal
|
|
293
|
-
? `Auto-continuation: ${goal.autoContinue === true ? "enabled" : "disabled/unknown"}
|
|
294
|
-
: `Auto-continuation: ${loop?.active === true ? "loop active" : "loop inactive"}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
`Task state:\n${goal ? taskState(goal) : "(no goal task list)"}`,
|
|
227
|
+
? `Auto-continuation: ${goal.autoContinue === true ? "enabled" : "disabled/unknown"}`
|
|
228
|
+
: `Auto-continuation: ${loop?.active === true ? "loop active" : "loop inactive"}`,
|
|
229
|
+
goal
|
|
230
|
+
? `Dynamic state (task state / pending completion / auditor TODOs / latest audit): see .pi-glla/active.jsonl — read before acting`
|
|
231
|
+
: `Dynamic state (loop progress): see .pi-glla/active.jsonl — read before acting`,
|
|
299
232
|
`Repair/replan target:\n${repairTarget}`,
|
|
300
233
|
loop
|
|
301
234
|
? "Lifecycle fence: continue only for this active loop target, current loop state, and session owner; if a goal is present, preserve its id/revision as paused context. Use durable state and artifacts as the authority after compaction, restart, or session replacement."
|
|
@@ -315,7 +248,7 @@ function customType(message: unknown): string | null {
|
|
|
315
248
|
// Audit 2026-09-06: a substring-based `isGllaControlMessage` predicate used
|
|
316
249
|
// to live here (any message containing "[GOAL CHECKPOINT" / "[STALL
|
|
317
250
|
// WARNING" counted as control plane). It had zero callers — the live
|
|
318
|
-
// projection keys on customType only —
|
|
251
|
+
// projection keys on customType only — any future caller would inherit
|
|
319
252
|
// the forgeable-substring hazard (crafted text projected out of context).
|
|
320
253
|
// Deleted instead of hardened: control-plane identity is customType.
|
|
321
254
|
function isGoalEventPayload(message: unknown): boolean {
|
|
@@ -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
|
-
|
|
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
|
-
]
|
|
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
|
-
|
|
1141
|
-
|
|
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.
|
|
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",
|