pi-goal-list-loop-audit 0.38.21 → 0.38.24

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +10 -0
  3. package/docs/DESIGN-subagent-visibility.md +38 -5
  4. package/docs/INDEX.md +5 -3
  5. package/docs/SETTINGS.md +85 -0
  6. package/extensions/auditor-extensions.ts +19 -6
  7. package/extensions/completion-summary.ts +8 -3
  8. package/extensions/context-checkpoint.ts +26 -42
  9. package/extensions/drafter-model.ts +8 -1
  10. package/extensions/goal-agents-panel.ts +121 -35
  11. package/extensions/goal-commands.ts +42 -5
  12. package/extensions/goal-compactor.ts +42 -2
  13. package/extensions/goal-continuation.ts +80 -8
  14. package/extensions/goal-heartbeat.ts +12 -0
  15. package/extensions/goal-loop-core.ts +90 -0
  16. package/extensions/goal-loop-display.ts +154 -52
  17. package/extensions/goal-loop-shield.ts +31 -0
  18. package/extensions/goal-recovery.ts +24 -5
  19. package/extensions/goal-settings.ts +84 -2
  20. package/extensions/loops/goal-activation.ts +31 -5
  21. package/extensions/loops/goal-auditor-hooks.ts +1 -0
  22. package/extensions/loops/goal-list-queue.ts +9 -0
  23. package/extensions/loops/goal-orchestrator.ts +5 -1
  24. package/extensions/loops/goal-runtime-globals.ts +24 -0
  25. package/extensions/loops/goal-session.ts +40 -5
  26. package/extensions/loops/goal-settings-ui.ts +48 -7
  27. package/extensions/loops/goal-tools.ts +6 -4
  28. package/extensions/loops/goal-ui.ts +21 -12
  29. package/extensions/model-selector.ts +13 -5
  30. package/extensions/multi-model-picker.ts +12 -0
  31. package/extensions/settings-menu.ts +13 -2
  32. package/extensions/state-root-owner.ts +79 -4
  33. package/package.json +1 -1
  34. package/scripts/release-pack-smoke.mjs +4 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.24 — audit pass: abort-latch send guards, ownership compare-and-swap, auditor inherit parity (2026-09-07)
4
+
5
+ ### Fixed
6
+ - Full display/lifecycle/settings audit pass (goal `20260907131550-12ddoy`, three parallel read-only scouts): 44 findings → 0 open. Display decisions (user-grilled): head owns liveness (status drops the `LIVE` capsule/tails, keeps `[WORKING]` + counts), exceptions-only `quiet` default (troubled rows + count line; healthy fan-out lives on the fleet panel), `active {age}` / `quiet {age}` worker labels.
7
+ - Lifecycle HIGH: `sendContinuation` / `sendStallEscalation` / `sendLengthContinue` refuse on `flags.abortedStandDown`; terminal-notice refusal scoped to the abort latch so transcript closure still wins; zombie retry routes on the abort-closure owner (loop abort no longer stranded by a goal started mid-delay); cycle-reset consumes budget (`attempts+1`) and honors the 24h horizon while the probe turn stays prompt per the v0.34.132 contract.
8
+ - Lifecycle MED/LOW: fallback `agent_start`/`turn_start` acks refuse on intervening user messages; successor absorb + self-heal share `clearDeadGenerationDispatch` (no more idle behind a "re-armed" lie); ownership refresh is compare-and-swap (live foreign claims never clobbered; dead records still refresh for heartbeat reclaim); command entry forces a fresh ownership read past the background throttle; length-continue lost its stale-classifier exemption.
9
+ - Settings: `stallShortWords: 0` (= off) survives normalization; auditor thinking gains the drafter's `session — inherit` row + non-reasoning picks clear stale overrides; bare `/glla fallbacks` reads on stale handles (only `clear/off/unset/none` mutates); typing `auto` in the notify editor round-trips to unset; v0.38.23 comment/doc drift swept (richness/quiet copy, reviewer write→migrate lifecycle, DESIGN one-line rows, SETTINGS wedge vs aggressive default). Full audit: `audit/AUDIT-PASS-2026-09-07.md`. Coverage: `release:check` 1979 pass / 0 fail.
10
+
11
+ ## 0.38.23 — below-chat widget: forced placement, single-line rows, evidence lifesign (2026-09-07)
12
+
13
+ ### Fixed
14
+ - Below-chat pinning (field: GLLA card vs fleet visibly swapping slots across ticks, 2026-09-06 screenshots). The `pi-glla` widget now passes `{ placement: "belowEditor" }` — the same zone the native fleet defaults to — so both surfaces stop trading places. Forced, not a setting: one stable layout for everyone, no new settings row or drift pin.
15
+ - Extension-meta footers deleted. The orphan-worker `└─ /glla agents for full worker detail`, the card `└─ /goal status · /glla` / `└─ /list · /glla` hints, and the `… N more agents · /glla agents` overflow pointer all named our extension, not the user's task — pure noise once `/glla` is known. Queue depth survives as task info (`└─ N queued`); empty queue means no footer at all.
16
+ - Option-2 worker rows: one glyph-first line per tracked child (`▶ scout · ui-fixes · silent 2m`, `⚠ worker · art-batch · silent 31m`), age before any suffix so narrow-terminal truncation cuts the least important field first. The `→ <objective>` task-linkage header is gone (the card head already names the objective — it repeated it verbatim), ids stay in the `/glla agents` table, overflow names its count only. `/glla agents` full table untouched (`└ blocks:`, Recent hangs, abort guidance all intact).
17
+ - Evidence lifesign on active-clear heads: freshest-worker-evidence readout as the last head segment (`· stream 8s`), breathing glyph derived from evidence counters (`Σ toolUses + outputTokens mod 4` over `●→◉→○→◉`) — never from wall-clock, so the widget key stays stable between evidence and the v0.37.1 re-layout cannot return. Fresh (<5m) breathes, aging (5–30m) freezes the ring, stale (30m+) hollows it, a hung child triangles the head. Non-active statuses (⏸/⟡/⚠/⏳) keep their own glyph language; no rows means no readout invented.
18
+ - Semantic color ramp on head glyph + age text + row glyphs only (success <5m, warning to 30m, error past it; queued caps at amber — waiting is not broken; meter stays progress-colored, fleet line untouched). Color never rides alone: glyph shape (▶/◉/⚠) plus the silence number carry the same meaning unpainted, and a no-theme render contains zero ANSI. Coverage: `tests/lifesign.test.ts` (7 tests: bands incl. queued cap, empty-invents-nothing, counter-derived breath, head readout/triangle/pause/bare, row color + unpainted shape+number, 80-col fit + bucket key-stability) plus reshaped pins in `display`/`agents-panel`/`subagent-display-richness`.
19
+
20
+ ## 0.38.22 — subagent display unification: richness ladder + upstream triple-render report (2026-09-05)
21
+
22
+ ### Fixed
23
+ - Subagent display unification (field: triple-rendered run panels swapping order with agent prose, 2026-09-05 screenshot). New `subagentDisplayRichness` setting (`/glla` → Subagents): `rich` (default — detailed worker rows + the task-linkage header `→ <objective>` native UI can never show), `compact` (today's count line), `quiet` (hung/aborting workers only — HUNG is never silent at any level). Rich is safe against the v0.37.1 jumping because `renderAgentsWidgetLines` now buckets silence ages exactly like the compact line, so the widget key only moves on genuine state transitions. Status-bar worker count follows the same ladder automatically. `/glla agents` keeps full detail regardless. Upstream triple-render + order-swap filed as nicobailon/pi-subagents#1931 (read-only diagnosis; GLLA cannot suppress/reorder/merge native panels — no ordering API, transcript components order by arrival); README documents the coexistence escapes (`inlineToolDisplay: "summary"` + `fleetViewPlacement` pin). Coverage: `tests/subagent-display-richness.test.ts` (5 tests: default/normalization, ladder assembly + linkage header, bucket stability, hung invariant, width safety).
24
+
3
25
  ## 0.38.21 — objection-attached retries + PR #43 auditor observability (2026-09-04)
4
26
 
5
27
  ### Fixed
package/README.md CHANGED
@@ -298,6 +298,16 @@ GLLA is the supervisor. These companions add capabilities around it:
298
298
  sequential goal can still run cleanly without workers — install when
299
299
  parallelism will pay for its coordination and model usage.
300
300
 
301
+ Display coexistence: pi-subagents renders its own inline run panels +
302
+ FleetView, GLLA renders one compact widget line + the status-bar worker
303
+ count + `/glla agents`. If you see the same run stacked 3× or panels
304
+ swapping order, set pi-subagents `inlineToolDisplay: "summary"` (one
305
+ stable row per run) and pin `fleetViewPlacement`; GLLA's own richness is
306
+ the `subagentDisplayRichness` setting (`/glla` → Subagents): `rich`
307
+ (default — worker rows + task linkage), `compact` (count line), `quiet`
308
+ (hung/aborting workers only). Upstream triple-render report:
309
+ nicobailon/pi-subagents#1931.
310
+
301
311
  Install (or keep pinned):
302
312
 
303
313
  ```bash
@@ -29,15 +29,29 @@ holds (no new instrumentation):
29
29
  - Ledger history of `subagent_hang_detected` for the "Recent hangs" footer
30
30
  of the panel.
31
31
 
32
- Row shape:
32
+ Row shape (shipped one-glyph-line since v0.38.23: age-first single lines
33
+ replace the v0.38.22 two-line shape — footers deleted, ids live in
34
+ `/glla agents`; audit 2026-09-07 verified this against the doc and kept
35
+ the code — the doc, not the pins, was stale):
33
36
 
34
37
  ```
35
- ● explore map-model-picker RUNNING 4m12s tools 18 · out 2.1k · silent 0m
36
- ● plan audit-contract HUNG? 31m tools 6 → frozen · out 890 · silent 26m
37
- └ blocks: foreground subagent call (zombie stand-down active)
38
- ✓ explore schema-check ENDED ok 3m44s
38
+ ▶ explore · map-model-picker · active 8s
39
+ ▶ plan · audit-contract · quiet 26m
40
+ ⚠ plan · stuck-render · HUNG · quiet 31m · id e5f6…
39
41
  ```
40
42
 
43
+ Troubled rows append the short id; healthy rows stay self-contained
44
+ without the fleet panel (the v0.38.22 `└ check the Agents panel` footer
45
+ is gone — the row itself says what is wrong). `/glla agents` keeps the
46
+ full detail view (recent hangs read the durable `subagent_hang_detected`
47
+ ledger, absent when empty — never a placeholder).
48
+
49
+ The `└ blocks:` row renders only from an OBSERVED in-flight parent
50
+ subagent wait (tool args carry no run id, so per-row correlation is
51
+ impossible — the wait NAME is named, never inferred). The "Recent hangs"
52
+ footer reads the durable `subagent_hang_detected` ledger (last 3, absent
53
+ when empty — never a placeholder).
54
+
41
55
  Hung classification reuses `classifyHungSubagents` semantics (record-frozen
42
56
  vs event-only evidence). Cap display at ~20 rows; prune ended probes per
43
57
  the existing SUBAGENT_HANG_PRUNE_MS rule.
@@ -74,3 +88,22 @@ children so idle rigs see no change.
74
88
  pin hung classification, cap behavior, loud-missing-file behavior for
75
89
  --tail, and widget hide-at-zero.
76
90
  - Version + CHANGELOG + README at ship time; full release gate.
91
+
92
+ ## v0.38.22 supersede — display unification (2026-09-05)
93
+
94
+ The compact-only doctrine above was the v0.37.1 jitter fix (raw per-second
95
+ silence ages re-laid out the editor every tick). v0.38.22 keeps the doctrine's
96
+ safety property by other means — `renderAgentsWidgetLines` buckets silence
97
+ ages exactly like the compact line, so the widget key only moves on genuine
98
+ state transitions — and restores rich ambient rows behind the
99
+ `subagentDisplayRichness` ladder (`rich` default / `compact` / `quiet`, HUNG
100
+ never silent), plus — until v0.38.23 — the task-linkage header
101
+ (`→ <objective>`) only GLLA can show. v0.38.23 removes the header (the
102
+ card head already names the objective), collapses worker rows to one
103
+ glyph-first line each, forces the card below the chat
104
+ (`{ placement: "belowEditor" }`), and adds the evidence lifesign
105
+ (counter-derived breathing glyph + `stream {age}` readout + semantic
106
+ band colors on head and rows, queued capped at amber). Triplication/ordering against pi-subagents native panels is upstream
107
+ (nicobailon/pi-subagents#1931, read-only); GLLA owns its slot only and
108
+ documents the native escapes. Assembly is the pure `assembleAgentsExtras`
109
+ (pinned in `tests/subagent-display-richness.test.ts`).
package/docs/INDEX.md CHANGED
@@ -18,15 +18,16 @@ 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 continue through the supervisor freeze (`/glla pause`),
21
+ v0.35.14–v0.38.24 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
25
25
  subagent recovery, bounded repair/replan recovery, production RPC child
26
26
  stopping, mandatory hermetic auditor-extension validation, optional provider
27
27
  extensions, bounded zero-stream retry containment, crash-safe persistence,
28
- and packed-artifact release verification — see CHANGELOG.md for the full
29
- trail.
28
+ packed-artifact release verification, and the 2026-09-07 display/lifecycle/
29
+ settings audit pass (abort-latch send guards, ownership compare-and-swap,
30
+ auditor inherit/clear parity) — see CHANGELOG.md for the full trail.
30
31
  - `../README.md` — what the plugin is, install, quickstart, and the
31
32
  architectural guarantee (drafting + confirm + detached auditor).
32
33
  - `../INSTALL.md` — source install / local development setup; the recommended
@@ -35,6 +36,7 @@ For shipped docs, the relevant entry points are:
35
36
 
36
37
  ## Entry points
37
38
  - `../README.md` — what the plugin is, install, quickstart
39
+ - `SETTINGS.md` — canonical settings reference (files, precedence, every key)
38
40
  - `../INSTALL.md` — source install / local development setup
39
41
  - `../CHANGELOG.md` — user-facing changelog; the first versioned heading is
40
42
  the current released package version (use `/glla version` to compare with
@@ -0,0 +1,85 @@
1
+ # GLLA settings reference
2
+
3
+ Canonical settings reference (audit 2026-09-06 — settings truth previously
4
+ lived only in the menu/headless renderers and the changelog). The live
5
+ views remain `/glla` (headless display with provenance) and the `/glla`
6
+ settings menu; this file is the shipped, reviewable contract. A release-gate
7
+ test pins that every `SETTINGS_KEYS` entry in `extensions/goal-settings.ts`
8
+ appears here, so the table cannot silently drift.
9
+
10
+ ## Files and precedence
11
+
12
+ - Global file: `~/.pi/agent/glla/settings.json` (machine/provider policy).
13
+ - Project file: `<cwd>/.pi-glla/settings.json` (project artifact).
14
+ - Effective value: project wins over global wins over built-in default.
15
+ - `/glla` shows per-key provenance (`project` / `global` / `default`).
16
+ - Hand-edited files are normalized on every load
17
+ (`normalizeLoadedSettings`): unknown enums fall back, out-of-range
18
+ numbers reset to unset (consumer `??` fallbacks apply), junk strings are
19
+ dropped, and legacy keys migrate (`reviewer` → `postaudit`,
20
+ `auditorModelFallback` → `auditorModelFallbacks`,
21
+ `hourlyQuotaProbe` → `hourlyRetryProbe`).
22
+
23
+ ## Global-only keys
24
+
25
+ These describe machine/provider policy, not a project artifact. Project
26
+ copies are ignored (the recovery runtime reads the global file):
27
+
28
+ `stateRoot`, `mainModelFallbacks`, `mainModelRetryMinutes`,
29
+ `mainModelFailback`, `mainModelPrimaryProbeMinutes`, `hourlyRetryProbe`,
30
+ `autoResume`, `drafterModel`, `drafterThinkingLevel`,
31
+ `drafterModelFallbacks`, `compactorModel`, `compactorModelFallbacks`,
32
+ `auditorModelFallbacks`, `auditorToolTimeoutMs`, `auditorStallMs`,
33
+ `auditJobRetentionMs`, `auditorInspection`.
34
+
35
+ ## Keys
36
+
37
+ | Key | Default | Meaning |
38
+ | --- | ------- | ------- |
39
+ | `stateRoot` | `"workingDir"` | Where durable state lives; `"sessionDir"` is explicit opt-in. Global-only. |
40
+ | `mainModelFallbacks` | `[]` | Ordered provider/model refs on main-model failure. Global-only. |
41
+ | `drafterModel` | unset (session) | Drafting-only primary model. Global-only. |
42
+ | `drafterThinkingLevel` | unset (inherit) | Thinking level for the drafting agent. Global-only. |
43
+ | `drafterModelFallbacks` | `[]` | Ordered drafting fallbacks; session model is final. Global-only. |
44
+ | `compactorModel` | unset (plan B) | Emergency-compactor primary; never the session model. Global-only. |
45
+ | `compactorModelFallbacks` | `[]` | Ordered compactor fallbacks; no session last resort. Global-only. |
46
+ | `mainModelRetryMinutes` | `15` | Base minutes before main-session recovery; doubles per attempt, caps 5h. Global-only. |
47
+ | `mainModelFailback` | `"auto"` | `"auto"` re-probes the primary; `"sticky"` keeps the fallback. Global-only. |
48
+ | `mainModelPrimaryProbeMinutes` | `15` | Minutes between preferred-primary health probes. Global-only. |
49
+ | `forbiddenModels` | `[]` | Refs that must never be selected (case-insensitive substring). |
50
+ | `blockForbiddenModelSwitches` | `true` | Revert forbidden switches (`false` = stand but ledger). |
51
+ | `visionAssist` | `true` | Continuation prompts carry the prefer-native-vision directive. |
52
+ | `auditorModel` | unset (session) | Detached auditor primary (`"provider/id"` or bare id). |
53
+ | `auditorModelFallbacks` | `[]` | Ordered auditor fallbacks; session model is final. Global-only. |
54
+ | `auditorAllowedExtensions` | `[]` | Extension specs the detached auditor may load; resolved fail-closed, `[]` = isolated. |
55
+ | `auditorSameSessionSwap` | `true` | Walk the fallback chain when the auditor is the session model. |
56
+ | `auditorThinkingLevel` | unset (inherit) | Detached auditor reasoning level; picked with the model. |
57
+ | `auditorToolTimeoutMs` | `300000` | Base budget per auditor tool call (30s–6h). Global-only. |
58
+ | `auditorStallMs` | `600000` | Base silence budget for the detached auditor (1m–24h). Global-only. |
59
+ | `auditJobRetentionMs` | `900000` | How long proven-dead audit job dirs are kept (0–7d, 0 = reap now). Global-only. |
60
+ | `auditorInspection` | `false` | Auditor runs as a persistent session you can tail/resume. Global-only. |
61
+ | `notifyCmd` | unset | Shell command on goal complete / pause / loop stop; message is `$1`. |
62
+ | `tokenLimit` | unset (off) | Per-goal token budget; crossing it pauses. `0` = off. |
63
+ | `wedgeAlertMinutes` | unset (30, or off while aggressive mode is on — the default) | Busy-but-silent minutes before the wedge alert; `0` = off. The menu shows the effective value. |
64
+ | `autoResume` | `false` | Restored goals/loops/lists auto-resume in fresh sessions. Global-only. |
65
+ | `decisionPopup` | `true` | Decision pauses pop the picker (`false` = widget card only). |
66
+ | `carryover` | `"pause"` | Stale carryover on new activation: `"pause"` / `"clear"` / `"resume"`. |
67
+ | `autoAcceptDrafts` | `false` | Drafts activate without the Confirm dialog (unattended rigs). |
68
+ | `auditCap` | `5` (10 aggressive) | Pause after N consecutive auditor disapprovals (`0` = unlimited). |
69
+ | `auditFeedbackChars` | `0` (full) | Max auditor-report chars returned after disapproval (`0` = full). |
70
+ | `auditorSilent` | `true` | Auditor report renders final-only, no word-by-word HUD. |
71
+ | `auditorProgressSignals` | `true` | Silent audits show phase label + byte counter. |
72
+ | `hourlyRetryProbe` | `true` | Extra blind retry at :00:30 every hour while parked. Global-only. |
73
+ | `subagentModelStrategy` | `"inherit-parent"` | Default subagent model policy for new sessions. |
74
+ | `subagentModelOverrides` | unset | Per-agent-type model pin; always wins over strategy. |
75
+ | `subagentFallbacks` | unset | Per-role fallback chains (first eligible ref wins). |
76
+ | `aggressiveMode` | `true` | Keep-going defaults (`false` = pause-first policy). |
77
+ | `stuckMaxInterventions` | `5` (10 aggressive) | Consecutive stuck interventions before a loop stops. |
78
+ | `subagentHangEscalationMinutes` | `30` | Confirmed no-progress minutes before child-specific abort (`0` = warn only). |
79
+ | `stallEscalationRefires` | `5` | Heartbeat refires before pause/stop (`0` = never). |
80
+ | `zombieRetryMaxAttempts` | `3` | Re-dispatches per busy/no-stream episode (`0` = manual; max 10). |
81
+ | `stallShortWords` | `15` | Tool-less turn under this many words is a nudge. |
82
+ | `stallSimilarityThreshold` | `0.6` | Trigram similarity above this (tool-less) is a nudge. |
83
+ | `postaudit` | unset | Post-completion audit config (same shape as legacy `reviewer`). |
84
+ | `toolOverrides` | unset | Per-tool allow/hide/per-tool-config overrides. |
85
+ | `reviewer` | legacy | Deprecated alias for `postaudit`; migrated on load, `postaudit` wins. |
@@ -185,7 +185,10 @@ export function resolveAuditorAllowedExtensions(
185
185
  return out;
186
186
  }
187
187
 
188
- /** Bounded, deterministic normalization of the allowlist value. */
188
+ /** Bounded, deterministic normalization of the allowlist value.
189
+ * Audit 2026-09-06: dedup is case-insensitive like the model fallback
190
+ * chains (npm names cannot differ by case; a same-case pair is always the
191
+ * same intent). The first spelling wins; the original is preserved. */
189
192
  export function normalizeAuditorAllowedExtensions(value: unknown): string[] {
190
193
  if (!Array.isArray(value)) return [];
191
194
  const seen = new Set<string>();
@@ -193,9 +196,10 @@ export function normalizeAuditorAllowedExtensions(value: unknown): string[] {
193
196
  for (const entry of value) {
194
197
  if (typeof entry !== "string") continue;
195
198
  const spec = entry.trim();
196
- if (!spec || seen.has(spec)) continue;
199
+ const key = spec.toLowerCase();
200
+ if (!spec || seen.has(key)) continue;
197
201
  if (out.length >= 32) break; // bounded: the request hash stays sane
198
- seen.add(spec);
202
+ seen.add(key);
199
203
  out.push(spec);
200
204
  }
201
205
  return out;
@@ -294,10 +298,19 @@ export function discoverAuditorExtensions(home: string, cwd?: string): Discovere
294
298
  }
295
299
 
296
300
  /** Expand the allowlist into `pi` CLI args: ["--extension", spec, …].
297
- * Empty/absent list → [] (the worker keeps plain --no-extensions). */
298
- export function auditorExtensionArgs(allowed: string[] | undefined): string[] {
301
+ * Audit 2026-09-06: entries resolve fail-closed through
302
+ * resolveAuditorAllowedExtensions first — raw `npm:`/relative specs are
303
+ * never emitted (a raw npm: entry would trigger a fresh network install,
304
+ * or silently load nothing offline). Empty/absent list → [] (the worker
305
+ * keeps plain --no-extensions). */
306
+ export function auditorExtensionArgs(
307
+ allowed: string[] | undefined,
308
+ home: string,
309
+ cwd?: string,
310
+ settingsBase?: string,
311
+ ): string[] {
299
312
  const args: string[] = [];
300
- for (const spec of normalizeAuditorAllowedExtensions(allowed)) {
313
+ for (const spec of resolveAuditorAllowedExtensions(allowed, home, cwd, settingsBase)) {
301
314
  args.push("--extension", spec);
302
315
  }
303
316
  return args;
@@ -1,4 +1,5 @@
1
1
  import type { Goal, Status } from "./goal-loop-core.js";
2
+ import { truncateToWidth } from "@earendil-works/pi-tui";
2
3
 
3
4
  /**
4
5
  * The durable, user-facing terminal recap contract. Keep this as a small
@@ -191,7 +192,7 @@ export function buildApprovalChatLines(notice: {
191
192
  * word-bounded values. This is the user-facing `✓ done` block — six short
192
193
  * facts that stay scannable in chat. The single-line projection remains
193
194
  * for width-bound surfaces (TUI widget card, external notifies). */
194
- export function completionSummaryLines(text: string | undefined, maxValueLength = 240): string[] {
195
+ export function completionSummaryLines(text: string | undefined, maxValueLength = 240, lineWidth?: number): string[] {
195
196
  const source = completionSummaryBody(text ?? "").replace(/\s+/g, " ").trim();
196
197
  const lower = source.toLowerCase();
197
198
  const positions = COMPLETION_SUMMARY_LABELS
@@ -207,7 +208,10 @@ export function completionSummaryLines(text: string | undefined, maxValueLength
207
208
  .map((entry) => entry.start)
208
209
  .sort((a, b) => a - b)[0] ?? source.length;
209
210
  const rawValue = source.slice(valueStart, nextStart).trim();
210
- return `${name}: ${clipSummaryValue(rawValue || "not recorded", maxValueLength)}`;
211
+ const line = `${name}: ${clipSummaryValue(rawValue || "not recorded", maxValueLength)}`;
212
+ // Audit 2026-09-06: optional width budget for width-bound surfaces —
213
+ // the default 240-char values previously had no width-conscious path.
214
+ return lineWidth && lineWidth > 0 ? truncateToWidth(line, lineWidth, "…") : line;
211
215
  });
212
216
  }
213
217
 
@@ -322,8 +326,9 @@ export function terminalCompletionSummaryLines(
322
326
  facts: CompletionSummaryFacts,
323
327
  candidate = facts.goal.completionSummary,
324
328
  maxValueLength = 240,
329
+ lineWidth?: number,
325
330
  ): string[] {
326
- return completionSummaryLines(resolveCompletionSummary(facts, candidate).summary, maxValueLength);
331
+ return completionSummaryLines(resolveCompletionSummary(facts, candidate).summary, maxValueLength, lineWidth);
327
332
  }
328
333
 
329
334
  /**
@@ -78,26 +78,28 @@ function loopNumber(value: unknown): string {
78
78
 
79
79
  /**
80
80
  * The loop is a separate work surface from Goal. Keep its durable target and
81
- * progress in the checkpoint so a loop-only session does not lose its
82
- * authority when old goal-event payloads are projected out. Recent history is
83
- * useful for orientation, but is deliberately capped and treated as data.
81
+ * authority in the checkpoint so a loop-only session does not lose its
82
+ * context when old goal-event payloads are projected out.
83
+ *
84
+ * CACHE-STABILITY INVARIANT (provider prefix-cache rule): this checkpoint is
85
+ * inserted into the effective per-send message list at the position of the
86
+ * first removed goal-event payload — i.e. EARLY in history, before every
87
+ * provider call. Any byte change here invalidates the cache prefix for every
88
+ * token after it. This block therefore renders ONLY fields that are stable
89
+ * for the life of the loop run (target / measure / bounds config). Live
90
+ * counters (iteration, best, last, stall, tokens, history, timestamps) are
91
+ * intentionally NOT repeated here: they already ride the newest retained
92
+ * `[LOOP ITERATION …]` prompt (message-list suffix) and durable
93
+ * `.pi-glla/active.jsonl`. Rendering them here changed the prefix on every
94
+ * tick, making every loop iteration a full cache miss.
84
95
  */
85
96
  function loopCheckpointLines(loop: LoopState): string[] {
86
- const history = Array.isArray(loop.history) ? loop.history : [];
87
- const recentHistory = history.slice(-8).map((entry) =>
88
- `iteration=${loopNumber(entry.iteration)}, value=${entry.value === null ? "null" : loopNumber(entry.value)}, improved=${entry.improved === true ? "true" : "false"}, at=${safeInline(entry.at, 80) || "(unknown)"}`,
89
- );
90
- if (history.length > recentHistory.length) {
91
- recentHistory.unshift(`[…${history.length - recentHistory.length} earlier loop measurement(s) omitted]`);
92
- }
93
97
  return [
94
98
  "Active loop authority (durable loop state; not a user request):",
95
99
  `Loop target: ${safeInline(loop.target, 2_000) || "(missing — recover from durable loop state before proceeding)"}`,
96
100
  `Loop measure: command=${safeInline(loop.measureCmd, 1_200) || "(metricless)"}; direction=${safeInline(loop.direction, 20) || "(none)"}`,
97
- `Loop progress: active=${loop.active === true ? "true" : "false"}; iteration=${loopNumber(loop.iteration)}; best=${loopNumber(loop.bestValue)}; last=${loopNumber(loop.lastValue)}; stall=${loopNumber(loop.stallCount)}; nullMeasures=${loopNumber(loop.consecutiveNullMeasures)}; errors=${loopNumber(loop.consecutiveErrors)}`,
98
- `Loop bounds: maxIterations=${loopNumber(loop.maxIterations)}; plateauWindow=${loopNumber(loop.plateauWindow)}; timeLimitHours=${loopNumber(loop.timeLimitHours)}; tokenBudget=${loopNumber(loop.tokenBudget)}; tokensUsed=${loopNumber(loop.tokensUsed)}; stopReason=${safeInline(loop.stopReason, 400) || "(none)"}`,
99
- `Loop history (latest ${Math.min(history.length, 8)} of ${history.length}):\n${recentHistory.length > 0 ? recentHistory.join("\n") : "(none yet)"}`,
100
- `Loop lifecycle: startedAt=${safeInline(loop.startedAt, 100) || "(unknown)"}; lastIterationCompletedAt=${safeInline(loop.lastIterationCompletedAt, 100) || "(none)"}; specFile=${safeInline(loop.specFile, 300) || "(none)"}; refineHint=${safeInline(loop.refineHint, 600) || "(none)"}`,
101
+ `Loop bounds: maxIterations=${loopNumber(loop.maxIterations)}; plateauWindow=${loopNumber(loop.plateauWindow)}; timeLimitHours=${loopNumber(loop.timeLimitHours)}; tokenBudget=${loopNumber(loop.tokenBudget)}; specFile=${safeInline(loop.specFile, 300) || "(none)"}; startedAt=${safeInline(loop.startedAt, 100) || "(unknown)"}`,
102
+ "Loop progress: live counters (iteration/best/last/stall/history/tokens/completed-at) are intentionally omitted here — read the newest retained [LOOP ITERATION …] prompt and .pi-glla/active.jsonl. This checkpoint stays byte-stable across iterations so provider prefix caches keep hitting.",
101
103
  ];
102
104
  }
103
105
 
@@ -146,15 +148,13 @@ const OVERFLOW_AUDIT_CHARS = 1_100;
146
148
  const OVERFLOW_EMERGENCY_LINE_CHARS = 500;
147
149
 
148
150
  function compactLoopCheckpointLines(loop: LoopState): string[] {
149
- const historyCount = Array.isArray(loop.history) ? loop.history.length : 0;
150
- const latest = historyCount > 0 ? loop.history[historyCount - 1] : undefined;
151
+ // Same cache-stability invariant as loopCheckpointLines: stable fields only.
151
152
  return [
152
153
  "Active loop authority (durable loop state; not a user request):",
153
154
  `Loop target: ${safeInline(loop.target, OVERFLOW_LOOP_TARGET_CHARS) || "(missing — recover from durable loop state before proceeding)"}`,
154
155
  `Loop measure: command=${safeInline(loop.measureCmd, OVERFLOW_LOOP_MEASURE_CHARS) || "(metricless)"}; direction=${safeInline(loop.direction, 20) || "(none)"}`,
155
- `Loop progress: active=${loop.active === true ? "true" : "false"}; iteration=${loopNumber(loop.iteration)}; best=${loopNumber(loop.bestValue)}; last=${loopNumber(loop.lastValue)}; stall=${loopNumber(loop.stallCount)}; nullMeasures=${loopNumber(loop.consecutiveNullMeasures)}; errors=${loopNumber(loop.consecutiveErrors)}`,
156
- `Loop bounds: maxIterations=${loopNumber(loop.maxIterations)}; plateauWindow=${loopNumber(loop.plateauWindow)}; timeLimitHours=${loopNumber(loop.timeLimitHours)}; tokenBudget=${loopNumber(loop.tokenBudget)}; tokensUsed=${loopNumber(loop.tokensUsed)}; stopReason=${safeInline(loop.stopReason, 180) || "(none)"}`,
157
- `Loop lifecycle: startedAt=${safeInline(loop.startedAt, 70) || "(unknown)"}; lastIterationCompletedAt=${safeInline(loop.lastIterationCompletedAt, 70) || "(none)"}; specFile=${safeInline(loop.specFile, 160) || "(none)"}; historyCount=${historyCount}; latestIteration=${latest ? loopNumber(latest.iteration) : "(none)"}`,
156
+ `Loop bounds: maxIterations=${loopNumber(loop.maxIterations)}; plateauWindow=${loopNumber(loop.plateauWindow)}; timeLimitHours=${loopNumber(loop.timeLimitHours)}; tokenBudget=${loopNumber(loop.tokenBudget)}; specFile=${safeInline(loop.specFile, 160) || "(none)"}`,
157
+ "Loop progress: live counters omitted by design — see the newest retained loop prompt and .pi-glla/active.jsonl (keeps this checkpoint byte-stable for prefix-cache stability).",
158
158
  ];
159
159
  }
160
160
 
@@ -212,7 +212,7 @@ function buildOverflowCheckpoint(
212
212
 
213
213
  const optionalLines = [
214
214
  loop
215
- ? `Loop history summary: count=${Array.isArray(loop.history) ? loop.history.length : 0}; latest=${Array.isArray(loop.history) && loop.history.length > 0 ? loopNumber(loop.history[loop.history.length - 1]?.value) : "(none)"}`
215
+ ? "Loop history summary: omitted by design — live history rides the newest retained loop prompt (keeps this checkpoint byte-stable for prefix-cache stability)"
216
216
  : null,
217
217
  goal
218
218
  ? `Auto-continuation: ${goal.autoContinue === true ? "enabled" : "disabled/unknown"}; stopReason=${safeInline(goal.stopReason, 220) || "(none)"}; pauseKind=${safeInline(goal.pauseKind, 40) || "(none)"}`
@@ -312,28 +312,12 @@ function customType(message: unknown): string | null {
312
312
  return typeof value === "string" ? value : null;
313
313
  }
314
314
 
315
- function messageText(message: unknown): string {
316
- if (typeof message !== "object" || message === null) return "";
317
- const content = (message as { content?: unknown }).content;
318
- if (typeof content === "string") return content;
319
- if (!Array.isArray(content)) return "";
320
- return content.map((block) => {
321
- if (typeof block === "string") return block;
322
- if (typeof block !== "object" || block === null) return "";
323
- const text = (block as { text?: unknown }).text;
324
- return typeof text === "string" ? text : "";
325
- }).join("\n");
326
- }
327
-
328
- /** True for GLLA control-plane messages that may be safely bounded. */
329
- export function isGllaControlMessage(message: unknown): boolean {
330
- const type = customType(message);
331
- if (type === "goal-event" || type === AUTHORITATIVE_CHECKPOINT_CUSTOM_TYPE) return true;
332
- const text = messageText(message);
333
- return typeof (message as { role?: unknown } | null)?.role === "string"
334
- && (text.includes("[GOAL CHECKPOINT") || text.includes("[STALL WARNING"));
335
- }
336
-
315
+ // Audit 2026-09-06: a substring-based `isGllaControlMessage` predicate used
316
+ // to live here (any message containing "[GOAL CHECKPOINT" / "[STALL
317
+ // WARNING" counted as control plane). It had zero callers — the live
318
+ // projection keys on customType only — and any future caller would inherit
319
+ // the forgeable-substring hazard (crafted text projected out of context).
320
+ // Deleted instead of hardened: control-plane identity is customType.
337
321
  function isGoalEventPayload(message: unknown): boolean {
338
322
  return customType(message) === "goal-event";
339
323
  }
@@ -98,7 +98,14 @@ export function resolveDrafterModel(ctx: ExtensionContext, settings: Pick<Settin
98
98
  candidates.push({ ref: selected.ref, model: selected.model, via: "configured" });
99
99
  }
100
100
  if (ctx.model && currentRef && !candidates.some((candidate) => candidate.ref.toLowerCase() === currentRef.toLowerCase())) {
101
- candidates.push({ ref: currentRef, model: ctx.model, via: "session-last-resort" });
101
+ // Audit 2026-09-06: the session last resort must not bypass the
102
+ // forbiddenModels gate — a forbidden session model is refused (and
103
+ // ledgered) instead of being leased silently.
104
+ if (forbidden(currentRef)) {
105
+ appendLedger(ctx.cwd, "forbidden_model_switch", { scope: "drafter", ref: currentRef, via: "session-last-resort", blocked: true });
106
+ } else {
107
+ candidates.push({ ref: currentRef, model: ctx.model, via: "session-last-resort" });
108
+ }
102
109
  }
103
110
  return { configuredRefs, candidates, selected: candidates[0] };
104
111
  }