pi-goal-list-loop-audit 0.38.29 → 0.38.30
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 +14 -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 +37 -10
- 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,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.30 — fresh audit pass: nine low-severity fixes (2026-09-08)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- 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.
|
|
7
|
+
- `countDone` counts all descendants of closed parents, matching the recursive `countTotal` (a closed parent with nested grandchildren no longer under-reads done).
|
|
8
|
+
- 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`.
|
|
9
|
+
- 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.
|
|
10
|
+
- The display-only objective projection additionally strips `#` headers, `>` quotes, `[label](url)` links, and `*em*` emphasis. Stored objectives unchanged.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
- `topOpenAuditFinding` strips aligned/tabbed boxes with the same `[ \t]+` class as its matcher, so the reprieve note never carries checkbox markup.
|
|
14
|
+
- `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).
|
|
15
|
+
- Regression coverage in `tests/audit-2026-09-08.test.ts` (11 pins); `tsc` clean; focused display/loop-forever/approval suites green.
|
|
16
|
+
|
|
3
17
|
## 0.38.29 — compact active-card recovery and judgment details (2026-09-08)
|
|
4
18
|
|
|
5
19
|
### 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.30 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[] {
|
|
@@ -1340,6 +1353,12 @@ function buildStatusTextBase(state: State, audit?: AuditDisplayProgress | null,
|
|
|
1340
1353
|
|
|
1341
1354
|
function countDone(g: Goal): number {
|
|
1342
1355
|
let n = 0;
|
|
1356
|
+
const countAllDescendants = (ts?: any[]): number => {
|
|
1357
|
+
if (!ts) return 0;
|
|
1358
|
+
let c = 0;
|
|
1359
|
+
for (const s of ts) c += 1 + countAllDescendants(s.subtasks);
|
|
1360
|
+
return c;
|
|
1361
|
+
};
|
|
1343
1362
|
const walk = (ts: Array<{ status: string; subtasks?: any[] }>) => {
|
|
1344
1363
|
for (const t of ts) {
|
|
1345
1364
|
if (t.status === "complete") {
|
|
@@ -1347,7 +1366,9 @@ function countDone(g: Goal): number {
|
|
|
1347
1366
|
// (field: 2 closed parents with pending subtasks displayed "2/24"
|
|
1348
1367
|
// for 6 real tasks). Mid-flight parents still count done subtasks
|
|
1349
1368
|
// independently via the walk below.
|
|
1350
|
-
|
|
1369
|
+
// v0.38.30 audit: count ALL descendants, not just direct children
|
|
1370
|
+
// (a closed parent with nested grandchildren under-read done).
|
|
1371
|
+
n += 1 + countAllDescendants(t.subtasks);
|
|
1351
1372
|
} else if (t.subtasks) {
|
|
1352
1373
|
walk(t.subtasks);
|
|
1353
1374
|
}
|
|
@@ -1648,9 +1669,15 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1648
1669
|
// active. Keep that fact visible, but do not print the full recovery report
|
|
1649
1670
|
// into the glance card; it made the card taller than the space below the
|
|
1650
1671
|
// editor. `/goal status` remains the detailed recovery surface.
|
|
1672
|
+
// v0.38.30 audit: goal card owns goal-kind episodes only (a kind:loop
|
|
1673
|
+
// episode beside an active goal used to misattribute loop recovery here
|
|
1674
|
+
// and hide the goal's own provenance; loop kinds keep the parked /
|
|
1675
|
+
// standalone cards). Fall back to provenance when the compact line is
|
|
1676
|
+
// undefined (blank-primary edge left no model fact at all).
|
|
1677
|
+
const goalRecovery = state.mainModelRecovery?.kind === "goal" ? state.mainModelRecovery : undefined;
|
|
1651
1678
|
let recoveryLine: string | undefined;
|
|
1652
|
-
if (g.status === "active" &&
|
|
1653
|
-
recoveryLine = compactMainModelRecoveryLine(
|
|
1679
|
+
if (g.status === "active" && goalRecovery) {
|
|
1680
|
+
recoveryLine = compactMainModelRecoveryLine(goalRecovery, extras?.mainModelFallbacks, now, width);
|
|
1654
1681
|
if (recoveryLine) lines.push(`├─ ${paint(theme, "dim", recoveryLine)}`);
|
|
1655
1682
|
}
|
|
1656
1683
|
// Model provenance is a card fact, not a notification: keep it visible
|
|
@@ -1658,7 +1685,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
1658
1685
|
// active recovery the compact recovery row owns the model fact, so do not
|
|
1659
1686
|
// print a second primary/current row beside it. The full chain and skipped
|
|
1660
1687
|
// refs remain available through `/goal status`.
|
|
1661
|
-
const provenance =
|
|
1688
|
+
const provenance = recoveryLine
|
|
1662
1689
|
? []
|
|
1663
1690
|
: modelProvenanceLines(extras?.modelProvenance, width);
|
|
1664
1691
|
const provenanceStart = lines.length;
|
|
@@ -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.30",
|
|
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",
|