pi-goal-list-loop-audit 0.38.29 → 0.38.31
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 +21 -0
- package/docs/INDEX.md +1 -1
- package/extensions/approval-render-store.ts +13 -2
- package/extensions/completion-summary.ts +5 -2
- package/extensions/goal-loop-display.ts +72 -16
- package/extensions/goal-loop-forever.ts +20 -3
- package/extensions/loops/goal-activation.ts +13 -5
- package/extensions/loops/goal-session.ts +32 -0
- package/extensions/loops/goal-settings-ui.ts +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.31 — overdue wait-pause stops promising imminent resume (2026-09-08)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- A recovery-timer wait whose retry time passed long ago on a held/idle host no longer claims `next: resuming now` / `auto-retrying · now` forever while also reading `paused` + `safely parked` (field screenshot 20260908_180721). `resuming now` / `resuming…` / `now` are grace-bounded (90s); past that the card and status bar name the timer (`next: recovery timer`, `auto-retrying · overdue — waiting on recovery timer`, `retry overdue`).
|
|
7
|
+
- Recovery-timer waits end at the auto-retry row: the stock provider-failure `pauseSuggestedAction` boilerplate and the generic `awaiting first turn` / `saved` tail are gone (owner + next are already on the rows above); the previous row closes the tree with `└─`. Decision/error/blocked pauses are unchanged.
|
|
8
|
+
- Regression coverage in `tests/paused-overdue-display.test.ts` (6 pins); two v0.34-era pins that encoded the old permanent-`resuming…` wording updated to the grace contract; `tsc` clean.
|
|
9
|
+
|
|
10
|
+
## 0.38.30 — fresh audit pass: nine low-severity fixes (2026-09-08)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Approval-render replay no longer runs before the stale/ownership fence: wrappers skip replay on stale, worker, owner-denied, handoff, and rebind handles (`shouldSkipApprovalRenderReplay`), so a superseded session running stale-allowed `/loop status` cannot mark delivery into a dead session.
|
|
14
|
+
- `countDone` counts all descendants of closed parents, matching the recursive `countTotal` (a closed parent with nested grandchildren no longer under-reads done).
|
|
15
|
+
- Plain-text surfaces use the ANSI-free truncator: `completion-summary.ts` width budgets and the headless settings flat rows route through `truncateCells`; the painted TUI tables keep `truncateToWidth`.
|
|
16
|
+
- Narrow-terminal budgets clamp to the available width instead of the floor, and the compact recovery line takes width-aware inner budgets — provenance/fallback chains truncate indoors instead of hard-cutting mid-token.
|
|
17
|
+
- The display-only objective projection additionally strips `#` headers, `>` quotes, `[label](url)` links, and `*em*` emphasis. Stored objectives unchanged.
|
|
18
|
+
- The approval-render sidecar repairs a corrupt file after ledgering once, truncates objectives by code points, and caps chat lines (60 lines × 1000 chars) behind the 20-entry spillway.
|
|
19
|
+
- The goal card owns goal-kind recovery episodes only: `kind:loop` episodes keep the parked/standalone cards and no longer suppress the goal's provenance; blank-primary recovery falls back to provenance instead of leaving no model fact.
|
|
20
|
+
- `topOpenAuditFinding` strips aligned/tabbed boxes with the same `[ \t]+` class as its matcher, so the reprieve note never carries checkbox markup.
|
|
21
|
+
- `parseLoopStartArgs` consumes known keys only with valid values — junk like unquoted `time=out` stays in the target prose instead of vanishing while defaults silently apply (`done` still always teaches its removal; `measure` accepts any non-empty command).
|
|
22
|
+
- Regression coverage in `tests/audit-2026-09-08.test.ts` (11 pins); `tsc` clean; focused display/loop-forever/approval suites green.
|
|
23
|
+
|
|
3
24
|
## 0.38.29 — compact active-card recovery and judgment details (2026-09-08)
|
|
4
25
|
|
|
5
26
|
### 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.
|
|
21
|
+
v0.35.14–v0.38.31 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
|
|
@@ -31,6 +31,10 @@ export interface PendingApprovalRender {
|
|
|
31
31
|
* briefly for inspection; undelivered ones are never dropped by the cap. */
|
|
32
32
|
const MAX_STORED_RENDERS = 20;
|
|
33
33
|
const MAX_REPLAY_PER_CONTACT = 5;
|
|
34
|
+
// v0.38.30 audit: bound the sidecar behind the "each is ~1KB" comment — a
|
|
35
|
+
// long approval trailer used to grow the 20-entry file without bound.
|
|
36
|
+
const MAX_RENDER_CHAT_LINES = 60;
|
|
37
|
+
const MAX_RENDER_LINE_CHARS = 1000;
|
|
34
38
|
|
|
35
39
|
export function approvalRenderStorePath(cwd: string): string {
|
|
36
40
|
return path.join(piGlaDir(cwd), "pending-approval-renders.json");
|
|
@@ -63,10 +67,15 @@ function readRenders(cwd: string): PendingApprovalRender[] {
|
|
|
63
67
|
dropped: parsed.length - valid.length,
|
|
64
68
|
kept: valid.length,
|
|
65
69
|
});
|
|
70
|
+
// v0.38.30 audit: repair the file after ledgering once — otherwise
|
|
71
|
+
// every user command re-appended the same ledger while the file
|
|
72
|
+
// stayed corrupt. Best-effort; a failed rewrite simply ledgers again.
|
|
73
|
+
writeRenders(cwd, valid);
|
|
66
74
|
}
|
|
67
75
|
return valid;
|
|
68
76
|
} catch {
|
|
69
77
|
appendLedger(cwd, "terminal_approval_render_store_invalid", { dropped: "all", kept: 0 });
|
|
78
|
+
writeRenders(cwd, []);
|
|
70
79
|
return [];
|
|
71
80
|
}
|
|
72
81
|
}
|
|
@@ -94,8 +103,10 @@ export function persistApprovalRender(cwd: string, render: {
|
|
|
94
103
|
const existing = readRenders(cwd);
|
|
95
104
|
const entry: PendingApprovalRender = {
|
|
96
105
|
goalId: render.goalId,
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
// v0.38.30 audit: code-point truncation (char slice split surrogate
|
|
107
|
+
// pairs) + bounded chat lines (the 20-entry cap never bound bytes).
|
|
108
|
+
objective: [...render.objective].slice(0, 300).join(""),
|
|
109
|
+
chatLines: render.chatLines.slice(0, MAX_RENDER_CHAT_LINES).map((line) => [...line].slice(0, MAX_RENDER_LINE_CHARS).join("")),
|
|
99
110
|
createdAt: at,
|
|
100
111
|
...(render.delivered ? { deliveredAt: at } : {}),
|
|
101
112
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Goal, Status } from "./goal-loop-core.js";
|
|
2
|
-
import {
|
|
2
|
+
import { truncateCells } from "./goal-loop-display.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The durable, user-facing terminal recap contract. Keep this as a small
|
|
@@ -303,7 +303,10 @@ export function completionSummaryLines(text: string | undefined, maxValueLength
|
|
|
303
303
|
const line = `${name}: ${clipSummaryValue(rawValue || "not recorded", maxValueLength)}`;
|
|
304
304
|
// Audit 2026-09-06: optional width budget for width-bound surfaces —
|
|
305
305
|
// the default 240-char values previously had no width-conscious path.
|
|
306
|
-
|
|
306
|
+
// v0.38.30 audit: plain-text surface — use the ANSI-free truncator
|
|
307
|
+
// (pi-tui truncateToWidth wraps the ellipsis in resets even for plain
|
|
308
|
+
// text, polluting notify/log/pager consumers).
|
|
309
|
+
return lineWidth && lineWidth > 0 ? truncateCells(line, lineWidth) : line;
|
|
307
310
|
});
|
|
308
311
|
}
|
|
309
312
|
|
|
@@ -95,7 +95,14 @@ function displayObjective(objective: string): string {
|
|
|
95
95
|
return compactDisplayText(objective)
|
|
96
96
|
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
97
97
|
.replace(/__([^_]+)__/g, "$1")
|
|
98
|
-
.replace(/`([^`\n]*)`/g, "$1")
|
|
98
|
+
.replace(/`([^`\n]*)`/g, "$1")
|
|
99
|
+
// v0.38.30 audit: strip the remaining common single-line wrappers so the
|
|
100
|
+
// glance card keeps its plain-text promise (headers, quotes, links,
|
|
101
|
+
// single-emphasis). Display-only; prompts/archives/state keep verbatim.
|
|
102
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
103
|
+
.replace(/^>\s?/gm, "")
|
|
104
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
|
105
|
+
.replace(/\*([^*\n]+)\*/g, "$1");
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
/** v0.33.1: painted strings measure by their terminal-cell width. */
|
|
@@ -261,7 +268,11 @@ export const WORKER_TEXT_SPACER = "\u00A0";
|
|
|
261
268
|
|
|
262
269
|
function budgetFor(width: number | undefined, prefixCols: number, floor: number): number {
|
|
263
270
|
if (!width || width <= 0) return floor;
|
|
264
|
-
|
|
271
|
+
// v0.38.30 audit: clamp to the available width (narrow terminals used to
|
|
272
|
+
// get the floor, so the inner truncate no-opped and the outer hard-cut
|
|
273
|
+
// sliced mid-token). Floor applies only when width is unknown; otherwise
|
|
274
|
+
// the available width wins with a small absolute minimum to avoid junk.
|
|
275
|
+
return Math.max(10, width - WIDGET_HORIZONTAL_MARGIN - prefixCols);
|
|
265
276
|
}
|
|
266
277
|
|
|
267
278
|
function uniqueModelRefs(refs: readonly string[] | undefined): string[] {
|
|
@@ -291,8 +302,10 @@ function modelSourceLabel(source: string): string {
|
|
|
291
302
|
return source.trim();
|
|
292
303
|
}
|
|
293
304
|
|
|
294
|
-
function compactMainModelRecoveryLine(recovery: MainModelRecovery | undefined, configuredBackups: string[] = [], now = Date.now()): string | undefined {
|
|
305
|
+
function compactMainModelRecoveryLine(recovery: MainModelRecovery | undefined, configuredBackups: string[] = [], now = Date.now(), width?: number): string | undefined {
|
|
295
306
|
if (!recovery || !recovery.primary.trim()) return undefined;
|
|
307
|
+
// v0.38.30 audit: width-aware inner budgets (fixed 48/56 silently dropped
|
|
308
|
+
// the attempts/skipped tail at narrow widths when the outer hard-cut ran).
|
|
296
309
|
const primary = recovery.primary.trim();
|
|
297
310
|
const current = (recovery.active ?? primary).trim() || primary;
|
|
298
311
|
const backups = uniqueModelRefs(configuredBackups).slice(0, 10);
|
|
@@ -308,7 +321,7 @@ function compactMainModelRecoveryLine(recovery: MainModelRecovery | undefined, c
|
|
|
308
321
|
const phase = recovery.manualResumeRequired
|
|
309
322
|
? "manual hold"
|
|
310
323
|
: recovery.pendingModelSwitch
|
|
311
|
-
? `switching → ${truncate(recovery.pendingModelSwitch, 48)}`
|
|
324
|
+
? `switching → ${truncate(recovery.pendingModelSwitch, budgetFor(width, 20, 48))}`
|
|
312
325
|
: Number.isFinite(retryMs)
|
|
313
326
|
? retryMs <= 0 ? "retrying now" : `retrying in ${fmtElapsed(retryMs)}`
|
|
314
327
|
: recovery.primaryProbeInFlight
|
|
@@ -330,12 +343,12 @@ function compactMainModelRecoveryLine(recovery: MainModelRecovery | undefined, c
|
|
|
330
343
|
// A recovery object can briefly survive the successful primary selection.
|
|
331
344
|
// Do not label that normal state as an incident, but keep the selected model
|
|
332
345
|
// on the card when no separate provenance row is available.
|
|
333
|
-
if (!hasEpisode) return `model: ${truncate(current, 56)} · primary`;
|
|
346
|
+
if (!hasEpisode) return `model: ${truncate(current, budgetFor(width, 12, 56))} · primary`;
|
|
334
347
|
const history = [
|
|
335
348
|
recovery.attempts > 0 ? `attempts ${recovery.attempts}` : "",
|
|
336
349
|
skipped > 0 ? `skipped ${skipped}` : "",
|
|
337
350
|
].filter(Boolean);
|
|
338
|
-
return `recovery: ${phase} · ${selected} ${truncate(current, 48)}${history.length > 0 ? ` · ${history.join(" · ")}` : ""}`;
|
|
351
|
+
return `recovery: ${phase} · ${selected} ${truncate(current, budgetFor(width, 20, 48))}${history.length > 0 ? ` · ${history.join(" · ")}` : ""}`;
|
|
339
352
|
}
|
|
340
353
|
|
|
341
354
|
function modelProvenanceLines(provenance: ModelProvenanceDisplay | undefined, width?: number): string[] {
|
|
@@ -998,6 +1011,14 @@ function goalTotalText(g: Goal, now: number): string {
|
|
|
998
1011
|
return Number.isFinite(startedAt) ? `total ${fmtElapsed(bucketSilentMs(Math.max(0, now - startedAt)))}` : "";
|
|
999
1012
|
}
|
|
1000
1013
|
|
|
1014
|
+
/** v0.38.31 (field 2026-09-08 180721): "resuming now" is a transient
|
|
1015
|
+
* truth, not a state. A wait-pause whose retry time passed an hour ago on
|
|
1016
|
+
* a held/idle host is overdue — claiming an imminent resume forever while
|
|
1017
|
+
* the card also says "safely parked" is the same contradiction class as
|
|
1018
|
+
* f8d1c2f. Past the grace window the transition falls through to the
|
|
1019
|
+
* pause-kind label ("recovery timer" for waits) instead. */
|
|
1020
|
+
export const PAUSED_RESUME_GRACE_MS = 90_000;
|
|
1021
|
+
|
|
1001
1022
|
function pausedNextTransition(g: Goal, state: State, now: number): string {
|
|
1002
1023
|
const resume = g.policy === "list" ? "/list resume" : "/goal resume";
|
|
1003
1024
|
const retryAt = state.mainModelRecovery?.retryAt ? Date.parse(state.mainModelRecovery.retryAt) : Number.NaN;
|
|
@@ -1013,7 +1034,11 @@ function pausedNextTransition(g: Goal, state: State, now: number): string {
|
|
|
1013
1034
|
}
|
|
1014
1035
|
const resumeAt = g.pauseResumeAt ? Date.parse(g.pauseResumeAt) : Number.NaN;
|
|
1015
1036
|
if (Number.isFinite(resumeAt)) {
|
|
1016
|
-
|
|
1037
|
+
if (resumeAt > now) return `auto-retry in ${fmtElapsed(resumeAt - now)}`;
|
|
1038
|
+
// Inside the grace window the retry is genuinely imminent. Past it the
|
|
1039
|
+
// timer never fired (held host, idle session) — fall through to the
|
|
1040
|
+
// kind label below instead of promising "resuming now" forever.
|
|
1041
|
+
if (now - resumeAt < PAUSED_RESUME_GRACE_MS) return "resuming now";
|
|
1017
1042
|
}
|
|
1018
1043
|
if (isCompletionAuditNoVerdict(g)) {
|
|
1019
1044
|
const retryAt = g.pendingCompletion?.recoveryRetryAt
|
|
@@ -1194,14 +1219,16 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1194
1219
|
if (kind === "wait" || kind === "blocked") {
|
|
1195
1220
|
// v0.34.12: live countdown (the UI ticker keeps rendering through a
|
|
1196
1221
|
// timed wait) — "auto-retry in 23m" beats a static clock time, and a
|
|
1197
|
-
// passed resumeAt says "resuming…" instead of lying about the
|
|
1222
|
+
// freshly-passed resumeAt says "resuming…" instead of lying about the
|
|
1223
|
+
// past. v0.38.31: "resuming…" shares the transition grace window — a
|
|
1224
|
+
// retry time long past with no dispatch reads "retry overdue".
|
|
1198
1225
|
// Every retry-class pause renders the same ⏳ auto-retrying… line +
|
|
1199
1226
|
// countdown; blocked pauses without a recovery timer render as
|
|
1200
1227
|
// ⏸ action needed. A main-model manual hold names its recovery owner.
|
|
1201
1228
|
const rms = g.pauseResumeAt ? Date.parse(g.pauseResumeAt) - now : Number.NaN;
|
|
1202
|
-
const when = Number.isFinite(rms)
|
|
1203
|
-
|
|
1204
|
-
: "";
|
|
1229
|
+
const when = !Number.isFinite(rms) ? ""
|
|
1230
|
+
: rms > 0 ? ` · auto-retry in ${fmtElapsed(rms)}`
|
|
1231
|
+
: -rms >= PAUSED_RESUME_GRACE_MS ? " · retry overdue" : " · resuming…";
|
|
1205
1232
|
if (kind === "blocked") {
|
|
1206
1233
|
const label = state.mainModelRecovery?.manualResumeRequired === true
|
|
1207
1234
|
? "⏸ manual recovery hold"
|
|
@@ -1340,6 +1367,12 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1340
1367
|
|
|
1341
1368
|
function countDone(g: Goal): number {
|
|
1342
1369
|
let n = 0;
|
|
1370
|
+
const countAllDescendants = (ts?: any[]): number => {
|
|
1371
|
+
if (!ts) return 0;
|
|
1372
|
+
let c = 0;
|
|
1373
|
+
for (const s of ts) c += 1 + countAllDescendants(s.subtasks);
|
|
1374
|
+
return c;
|
|
1375
|
+
};
|
|
1343
1376
|
const walk = (ts: Array<{ status: string; subtasks?: any[] }>) => {
|
|
1344
1377
|
for (const t of ts) {
|
|
1345
1378
|
if (t.status === "complete") {
|
|
@@ -1347,7 +1380,9 @@ function countDone(g: Goal): number {
|
|
|
1347
1380
|
// (field: 2 closed parents with pending subtasks displayed "2/24"
|
|
1348
1381
|
// for 6 real tasks). Mid-flight parents still count done subtasks
|
|
1349
1382
|
// independently via the walk below.
|
|
1350
|
-
|
|
1383
|
+
// v0.38.30 audit: count ALL descendants, not just direct children
|
|
1384
|
+
// (a closed parent with nested grandchildren under-read done).
|
|
1385
|
+
n += 1 + countAllDescendants(t.subtasks);
|
|
1351
1386
|
} else if (t.subtasks) {
|
|
1352
1387
|
walk(t.subtasks);
|
|
1353
1388
|
}
|
|
@@ -1648,9 +1683,15 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1648
1683
|
// active. Keep that fact visible, but do not print the full recovery report
|
|
1649
1684
|
// into the glance card; it made the card taller than the space below the
|
|
1650
1685
|
// editor. `/goal status` remains the detailed recovery surface.
|
|
1686
|
+
// v0.38.30 audit: goal card owns goal-kind episodes only (a kind:loop
|
|
1687
|
+
// episode beside an active goal used to misattribute loop recovery here
|
|
1688
|
+
// and hide the goal's own provenance; loop kinds keep the parked /
|
|
1689
|
+
// standalone cards). Fall back to provenance when the compact line is
|
|
1690
|
+
// undefined (blank-primary edge left no model fact at all).
|
|
1691
|
+
const goalRecovery = state.mainModelRecovery?.kind === "goal" ? state.mainModelRecovery : undefined;
|
|
1651
1692
|
let recoveryLine: string | undefined;
|
|
1652
|
-
if (g.status === "active" &&
|
|
1653
|
-
recoveryLine = compactMainModelRecoveryLine(
|
|
1693
|
+
if (g.status === "active" && goalRecovery) {
|
|
1694
|
+
recoveryLine = compactMainModelRecoveryLine(goalRecovery, extras?.mainModelFallbacks, now, width);
|
|
1654
1695
|
if (recoveryLine) lines.push(`├─ ${paint(theme, "dim", recoveryLine)}`);
|
|
1655
1696
|
}
|
|
1656
1697
|
// Model provenance is a card fact, not a notification: keep it visible
|
|
@@ -1658,7 +1699,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1658
1699
|
// active recovery the compact recovery row owns the model fact, so do not
|
|
1659
1700
|
// print a second primary/current row beside it. The full chain and skipped
|
|
1660
1701
|
// refs remain available through `/goal status`.
|
|
1661
|
-
const provenance =
|
|
1702
|
+
const provenance = recoveryLine
|
|
1662
1703
|
? []
|
|
1663
1704
|
: modelProvenanceLines(extras?.modelProvenance, width);
|
|
1664
1705
|
const provenanceStart = lines.length;
|
|
@@ -1860,7 +1901,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1860
1901
|
recoverySummary.forEach((line) => lines.push(`│ ${paint(theme, "dim", line.replace(/^Main-model recovery: /, ""))}`));
|
|
1861
1902
|
}
|
|
1862
1903
|
else if (Number.isFinite(retryMs)) {
|
|
1863
|
-
|
|
1904
|
+
// v0.38.31: "now" shares the transition grace window — a retry time
|
|
1905
|
+
// that passed long ago with no dispatch is overdue, not imminent.
|
|
1906
|
+
const overdue = -retryMs >= PAUSED_RESUME_GRACE_MS;
|
|
1907
|
+
const when = retryMs > 0 ? `next probe in ${fmtElapsed(retryMs)}`
|
|
1908
|
+
: overdue ? "overdue — waiting on recovery timer"
|
|
1909
|
+
: "now";
|
|
1864
1910
|
lines.push(`├─ ${paint(theme, "dim", `auto-retrying · ${when}`)}`);
|
|
1865
1911
|
} else if (kind === "blocked" && state.mainModelRecovery?.manualResumeRequired === true) {
|
|
1866
1912
|
lines.push(`├─ ${paint(theme, "warning", "manual recovery hold — automatic probes stopped")}`);
|
|
@@ -1901,6 +1947,16 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1901
1947
|
// before the first turn), render "awaiting first turn" instead of "saved"
|
|
1902
1948
|
// — the latter was misleading because no work was ever "saved" before the
|
|
1903
1949
|
// session ended.
|
|
1950
|
+
if (kind === "wait") {
|
|
1951
|
+
// v0.38.31 (field 2026-09-08 180721): a recovery-timer wait carries no
|
|
1952
|
+
// objective-specific tail — owner + next are already on the lifecycle /
|
|
1953
|
+
// transition / auto-retry rows, and the parked suggestedAction is stock
|
|
1954
|
+
// provider-failure boilerplate identical on every recovery wait. End
|
|
1955
|
+
// the card here, closing the previous row as the footer.
|
|
1956
|
+
const tail = lines[lines.length - 1];
|
|
1957
|
+
if (tail !== undefined) lines[lines.length - 1] = tail.replace(/^├─|^│\s*/, "└─ ");
|
|
1958
|
+
return lines;
|
|
1959
|
+
}
|
|
1904
1960
|
const spent: string[] = [];
|
|
1905
1961
|
const tokUsed = g.usage?.tokensUsed ?? 0;
|
|
1906
1962
|
const audits = g.auditHistory?.length ?? 0;
|
|
@@ -431,8 +431,23 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
431
431
|
const spans: Array<[number, number]> = [];
|
|
432
432
|
while ((m = kvRe.exec(rest)) !== null) {
|
|
433
433
|
if (insideQuote(m.index)) continue; // key-looking text inside the target's quotes — leave it
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
const key = m[1]!.toLowerCase();
|
|
435
|
+
if (!KNOWN_KEYS.has(key)) continue; // unknown key — leave it in the target
|
|
436
|
+
const value = m[2] ?? m[3] ?? m[4] ?? "";
|
|
437
|
+
// v0.38.30 audit: only consume known keys with VALID values — junk like
|
|
438
|
+
// unquoted `time=out` used to be stripped from the target while silently
|
|
439
|
+
// falling back to defaults. Invalid values stay in the target prose.
|
|
440
|
+
// `done` always consumes (any use teaches the v0.15.0 removal); `measure`
|
|
441
|
+
// accepts any non-empty command string.
|
|
442
|
+
let valid = true;
|
|
443
|
+
if (key === "direction") valid = value.toLowerCase() === "min" || value.toLowerCase() === "max";
|
|
444
|
+
else if (key === "window") { const n = Number.parseInt(value, 10); valid = Number.isFinite(n) && n > 0; }
|
|
445
|
+
else if (key === "max") { const n = Number.parseInt(value, 10); valid = Number.isFinite(n) && n >= 0; }
|
|
446
|
+
else if (key === "branch" || key === "force") valid = ["1", "true", "yes", "0", "false", "no"].includes(value.toLowerCase());
|
|
447
|
+
else if (key === "time" || key === "cadence") { const n = Number.parseFloat(value); valid = Number.isFinite(n) && n > 0; }
|
|
448
|
+
else if (key === "tokens" || key === "toolsamerepeat") { const n = Number.parseInt(value, 10); valid = Number.isInteger(n) && n >= 0; }
|
|
449
|
+
if (!valid) continue; // junk value — leave the span in the target
|
|
450
|
+
kv.set(key, value);
|
|
436
451
|
spans.push([m.index, m.index + m[0].length]);
|
|
437
452
|
}
|
|
438
453
|
// Remove kv spans from the target text.
|
|
@@ -601,7 +616,9 @@ export function topOpenAuditFinding(cwd: string): string | null {
|
|
|
601
616
|
const p = join(piGlaDir(cwd), "audit-loop/findings.md");
|
|
602
617
|
if (!existsSync(p)) return null;
|
|
603
618
|
const line = readFileSync(p, "utf-8").split("\n").find((l) => /^- \[[ \t]+\]/.test(l));
|
|
604
|
-
|
|
619
|
+
// v0.38.30 audit: strip with the same [ \t]+ class as the matcher
|
|
620
|
+
// (an aligned `- [ ]` box used to leak markup into the reprieve note).
|
|
621
|
+
return line ? line.replace(/^- \[[ \t]+\]\s*/, "").trim().slice(0, 120) : null;
|
|
605
622
|
} catch {
|
|
606
623
|
return null;
|
|
607
624
|
}
|
|
@@ -227,6 +227,7 @@ import {
|
|
|
227
227
|
type ProjectRollup,
|
|
228
228
|
} from "../goal-loop-stats.js";
|
|
229
229
|
import { releaseAuditorSurface, suppressAuditorSurfaceAfterColdRestore } from "./goal-auditor-surface.js";
|
|
230
|
+
import { shouldSkipApprovalRenderReplay } from "./goal-session.js";
|
|
230
231
|
import {
|
|
231
232
|
cancelDetachedGoalCompletionAuditor,
|
|
232
233
|
newDetachedAuditJobAttemptId,
|
|
@@ -781,7 +782,10 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
781
782
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
782
783
|
// v0.38.25: live contact — replay any approval render that landed
|
|
783
784
|
// with no live turn (persist-first, never silent).
|
|
784
|
-
|
|
785
|
+
// v0.38.30 audit: skip on stale/worker handles — the wrappers run
|
|
786
|
+
// before the inner stale fence, and a superseded session used to mark
|
|
787
|
+
// delivery into a dead session (stale-allowed /loop status included).
|
|
788
|
+
if (!shouldSkipApprovalRenderReplay(ctx)) replayUndeliveredApprovalRenders(ctx);
|
|
785
789
|
return cmdGoal(args, ctx);
|
|
786
790
|
},
|
|
787
791
|
});
|
|
@@ -790,7 +794,8 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
790
794
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
791
795
|
// v0.38.25: live contact — replay any approval render that landed
|
|
792
796
|
// with no live turn (persist-first, never silent).
|
|
793
|
-
|
|
797
|
+
// v0.38.30 audit: skip on stale/worker handles (see /goal wrapper).
|
|
798
|
+
if (!shouldSkipApprovalRenderReplay(ctx)) replayUndeliveredApprovalRenders(ctx);
|
|
794
799
|
return cmdSettings(args, ctx);
|
|
795
800
|
};
|
|
796
801
|
pi.registerCommand("glla", {
|
|
@@ -821,7 +826,8 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
821
826
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
822
827
|
// v0.38.25: live contact — replay any approval render that landed
|
|
823
828
|
// with no live turn (persist-first, never silent).
|
|
824
|
-
|
|
829
|
+
// v0.38.30 audit: skip on stale/worker handles (see /goal wrapper).
|
|
830
|
+
if (!shouldSkipApprovalRenderReplay(ctx)) replayUndeliveredApprovalRenders(ctx);
|
|
825
831
|
return cmdReview(args, ctx);
|
|
826
832
|
},
|
|
827
833
|
});
|
|
@@ -850,7 +856,8 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
850
856
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
851
857
|
// v0.38.25: live contact — replay any approval render that landed
|
|
852
858
|
// with no live turn (persist-first, never silent).
|
|
853
|
-
|
|
859
|
+
// v0.38.30 audit: skip on stale/worker handles (see /goal wrapper).
|
|
860
|
+
if (!shouldSkipApprovalRenderReplay(ctx)) replayUndeliveredApprovalRenders(ctx);
|
|
854
861
|
return cmdList(args, ctx);
|
|
855
862
|
},
|
|
856
863
|
});
|
|
@@ -875,7 +882,8 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
875
882
|
if (refuseForeignCommand(ctx)) return Promise.resolve();
|
|
876
883
|
// v0.38.25: live contact — replay any approval render that landed
|
|
877
884
|
// with no live turn (persist-first, never silent).
|
|
878
|
-
|
|
885
|
+
// v0.38.30 audit: skip on stale/worker handles (see /goal wrapper).
|
|
886
|
+
if (!shouldSkipApprovalRenderReplay(ctx)) replayUndeliveredApprovalRenders(ctx);
|
|
879
887
|
return cmdLoop(args, ctx);
|
|
880
888
|
},
|
|
881
889
|
});
|
|
@@ -1315,6 +1315,38 @@ function warnIfStaleAtEntry(ctx: ExtensionContext, what: string): boolean {
|
|
|
1315
1315
|
return true;
|
|
1316
1316
|
}
|
|
1317
1317
|
|
|
1318
|
+
/** v0.38.30 audit: non-notifying stale probe for the approval-render replay.
|
|
1319
|
+
* The command wrappers replay undelivered renders on every live contact, but
|
|
1320
|
+
* they run BEFORE the inner warnIfStaleAtEntry fence — a superseded session
|
|
1321
|
+
* running stale-allowed `/loop status` (or any refused command) used to
|
|
1322
|
+
* rewrite pending-approval-renders.json + ledger and notify into a dead
|
|
1323
|
+
* session, marking delivery the user never saw. Workers are never live
|
|
1324
|
+
* contact either. Passive flags only (no absorb side effects); fail closed. */
|
|
1325
|
+
export function shouldSkipApprovalRenderReplay(ctx: ExtensionContext): boolean {
|
|
1326
|
+
try {
|
|
1327
|
+
if (isWorkerSessionCtx(ctx)) return true;
|
|
1328
|
+
} catch {
|
|
1329
|
+
return true;
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
if (processOwnerDeniedCwd === ctx.cwd) return true;
|
|
1333
|
+
} catch {
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
if (sessionHandoffPending) return true;
|
|
1337
|
+
try {
|
|
1338
|
+
if (Date.now() < sessionReplacementUntil) return true;
|
|
1339
|
+
} catch {
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
if (probeExtensionApiStale()) return true;
|
|
1344
|
+
} catch {
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1318
1350
|
/** v0.28.12: draft-class confirm with the auto-accept escape hatch SURFACED.
|
|
1319
1351
|
* The polis incident: a user sat through a 14-item batch Confirm having
|
|
1320
1352
|
* already reviewed every item during drafting, never knowing /glla
|
|
@@ -23,6 +23,7 @@ import * as path from "node:path";
|
|
|
23
23
|
|
|
24
24
|
import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
26
|
+
import { truncateCells } from "../goal-loop-display.js";
|
|
26
27
|
import { Type } from "typebox";
|
|
27
28
|
|
|
28
29
|
// v0.34.109 (decomposition step 1): the state singleton and the persistence
|
|
@@ -742,7 +743,9 @@ async function promptSettingsMenu(
|
|
|
742
743
|
// Audit 2026-09-06: bound the flat option strings — full VALUE +
|
|
743
744
|
// DESCRIPTION previously rendered unbounded. The resolution prefix
|
|
744
745
|
// (`[section] label —`) sits at the head, so tail truncation is safe.
|
|
745
|
-
|
|
746
|
+
// v0.38.30 audit: headless select values are plain-text — use the
|
|
747
|
+
// ANSI-free truncator (truncateToWidth stays for the painted TUI table).
|
|
748
|
+
const flat = rows.map((r) => truncateCells(`[${r.section}] ${r.label} — ${r.valueText} [${r.sourceText.replace(/^\[|\]$/g, "")}] — ${r.description}`, 120));
|
|
746
749
|
flat.push("Done");
|
|
747
750
|
const v = await ctx.ui.select(title, flat);
|
|
748
751
|
if (!v || v === "Done") return undefined;
|
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.31",
|
|
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",
|