pi-goal-list-loop-audit 0.38.68 → 0.38.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.70 — Paused action cards lead with the action (2026-09-20)
4
+
5
+ - Paused cards render lifecycle/next, banner, reason, and suggested action before tally, provenance, and judgment, so Pi core tail truncation cuts history instead of the instruction (field 20260920_152744).
6
+ - Judgment rides collapsed on action cards (header + selection; full plaques stay in `/goal status`).
7
+
8
+ ## 0.38.69 — About and README refresh (2026-09-20)
9
+
10
+ - Refined npm about line and README intro: tighter mission control wording, no em dashes.
11
+ - README range wording uses plain "to" (0 to 10, 1 to 10).
12
+
13
+ ### Antigravity auto-continue port (survey 2026-09-19)
14
+
15
+ - Mid-run interruption budget (`decisionPauseBudget`): decision pauses past
16
+ budget auto-default to the recommended option and ledger
17
+ `decision_budget_auto_default` instead of stalling mid-run.
18
+ - Quota sleep-until-reset: quota-class failures with an explicit upstream
19
+ hint sleep until reset (5h cap; 5s eager first retry preserved); quota
20
+ waits never park at the 24h horizon; background primary probe re-arms at
21
+ reset while fallback serves.
22
+ - Completion-claim evidence-density lint: zero path:line tokens AND zero
23
+ gate rows rides a ledgered NOTE annotation (claim still audits); every
24
+ terminal path already ends with the archive record pointer.
25
+ - Per-repo pitfall registry: `.pi-glla/pitfalls.md` (absent stays absent)
26
+ rides the continuation prompt under REPO PITFALLS, ledgered once per goal.
27
+ - Objection-attached retries: every disapproval extracts durable TODOs via
28
+ `durableObjectionsForDisapproval` (aggressive keeps only cap/no-progress
29
+ behaviors); two pre-existing source-shape pins revised to the new shape.
30
+
3
31
  ## 0.38.68 — Relentless auto-continue: heat-routed exhaustion, quota retries, truthful recovering (2026-09-19)
4
32
 
5
33
  ### Length-exhaustion wedge fixed at the root: heat, not budget (field 162348)
package/README.md CHANGED
@@ -4,20 +4,20 @@
4
4
  <img src="media/glla2.png" alt="GLLA mission control" width="960">
5
5
  </p>
6
6
 
7
- > **Long-running, high-leverage autonomy for pi.**
7
+ > **Long running, high leverage autonomy for pi.**
8
8
  >
9
9
  > Give pi a meaningful outcome. GLLA helps it research, plan, execute,
10
10
  > recover, and prove the result over hours or days instead of treating one
11
11
  > chat turn as the whole job.
12
12
 
13
13
  `pi-goal-list-loop-audit` (GLLA) is mission control for autonomous work in
14
- [pi](https://github.com/badlogic/pi-mono). It is for the work that is too broad,
14
+ [pi](https://github.com/badlogic/pi-mono). It fits work that is too broad,
15
15
  too long, or too important to leave to a single uninterrupted prompt:
16
16
  repo-wide changes, migrations, audits, research, documentation overhauls,
17
17
  large refactors, and continuous improvement.
18
18
 
19
- GLLA does not promise that an agent can never make a mistake. It makes the
20
- agent's work **more effective, durable, recoverable, and difficult to declare
19
+ GLLA does not promise that an agent never makes a mistake. It makes the
20
+ agent's work **more effective, durable, recoverable, and hard to declare
21
21
  finished without evidence**:
22
22
 
23
23
  - You state the outcome and what “done” means.
@@ -30,7 +30,7 @@ finished without evidence**:
30
30
  - A separate detached auditor checks the saved completion claim before GLLA
31
31
  accepts it.
32
32
 
33
- The aim is not “run forever.” The aim is **more useful work per unit of
33
+ The aim is not "run forever." The aim is **more useful work per unit of
34
34
  attention, with event-driven progress instead of guessed-duration waiting, and
35
35
  better evidence at the end**. See `docs/DESIGN-long-running-supervision.md` for
36
36
  the long-running policy.
@@ -398,7 +398,7 @@ proof of a quota or billing state.
398
398
 
399
399
  - automatic retries are bounded and visible; a BUSY/no-stream turn is parked
400
400
  and re-dispatched within the configurable **Zero-stream retries** budget
401
- (default 3, range 0–10), then requires explicit resume;
401
+ (default 3, range 0 to 10), then requires explicit resume;
402
402
  - `/goal resume`, `/list resume`, and `/loop resume` are explicit recovery
403
403
  paths;
404
404
  - a user abort means stop, not “try again behind my back”;
@@ -431,7 +431,7 @@ Open `/glla` for the settings table. The most important choices are:
431
431
  - **Subagent hang escalation:** warning-only at `0`, or one child-specific
432
432
  action after a confirmed frozen interval;
433
433
  - **Zero-stream retries:** automatic GLLA recovery attempts after a busy,
434
- stream-silent Pi turn; `0` keeps recovery manual and `1–10` bounds repeats;
434
+ stream-silent Pi turn; `0` keeps recovery manual and `1 to 10` bounds repeats;
435
435
  - **Audit cap and retry cadence:** bounds for repeated objections and
436
436
  infrastructure recovery.
437
437
 
package/docs/INDEX.md CHANGED
@@ -16,7 +16,7 @@ Policy contracts and recent changes live in the `audit/` directory of the
16
16
  failback; v0.35.9 hardened cross-version npm tarball checks; v0.35.10
17
17
  handles multi-entry npm dry-run reports; v0.35.11 accepts both npm report
18
18
  shapes; v0.35.12 supports npm 12's keyed pack reports; v0.35.13 fixes stale-API recovery loops.
19
- v0.35.14–v0.38.68 continue through the supervisor freeze (`/glla pause`),
19
+ v0.35.14–v0.38.70 continue through the supervisor freeze (`/glla pause`),
20
20
  load hold, auditor picker parity, Windows launch fix, zombie-watchdog
21
21
  subagent carve-out, due-wait backstop, the `/glla agents` visibility panel,
22
22
  durable state-root selection, blank-until-resume auditor context, frozen
package/docs/SETTINGS.md CHANGED
@@ -64,6 +64,7 @@ copies are ignored (the recovery runtime reads the global file):
64
64
  | `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. |
65
65
  | `autoResume` | `false` | Restored goals/loops/lists auto-resume in fresh sessions. Global-only. |
66
66
  | `decisionPopup` | `true` | Decision pauses pop the picker (`false` = widget card only). |
67
+ | `decisionPauseBudget` | unset (unlimited) | Max agent-authored decision pauses per goal before auto-default: the (N+1)-th adopts the recommended option as a logged assumption without pausing. `0` = relentless from the first decision. |
67
68
  | `carryover` | `"pause"` | Stale carryover on new activation: `"pause"` / `"clear"` / `"resume"`. |
68
69
  | `autoAcceptDrafts` | `false` | Drafts activate without the Confirm dialog (unattended rigs). |
69
70
  | `auditCap` | `5` (10 aggressive) | Pause after N consecutive auditor disapprovals (`0` = unlimited). |
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import type { FindingGroup, GateRow, Goal, Status } from "./goal-loop-core.js";
2
+ import { sanitizeDisplayText, type FindingGroup, type GateRow, type Goal, type Status } from "./goal-loop-core.js";
3
3
  import { fmtElapsed, truncateCells } from "./goal-loop-display.js";
4
4
 
5
5
  /**
@@ -180,9 +180,10 @@ export interface HumanCompletionBrief {
180
180
  * the durable archive keeps the full text. Falls back to the original
181
181
  * when stripping would empty the value. */
182
182
  export function chatSafeDetailValue(value: string): string {
183
+ const safeValue = sanitizeDisplayText(value);
183
184
  // v0.38.45 audit: loop the innermost-group strip to a fixpoint
184
185
  // (bounded) — a single pass left nested husks like "(log )" behind.
185
- let withoutGroups = stripMachineGroups(value);
186
+ let withoutGroups = stripMachineGroups(safeValue);
186
187
  const stripped = withoutGroups
187
188
  .replace(/(?:\/var)?\/tmp\/\S+/g, "")
188
189
  .replace(/\btarballs?\s+\S+\.tgz\b/gi, "")
@@ -190,7 +191,7 @@ export function chatSafeDetailValue(value: string): string {
190
191
  .replace(/\(\s*\)/g, "")
191
192
  .replace(/\s{2,}/g, " ")
192
193
  .trim();
193
- return stripped || value;
194
+ return stripped || safeValue;
194
195
  }
195
196
 
196
197
  /** The human briefing: outcome first in its own words, then only the
@@ -384,7 +385,7 @@ function stripMachineTokens(line: string): string {
384
385
  export function structuredSummaryLines(summary: string | undefined, label = "Outcome:"): string[] | null {
385
386
  const raw = rawLabelValue(summary ?? "", label);
386
387
  if (!raw || !isSectionStructured(raw)) return null;
387
- const lines = raw.split("\n").map(stripMachineTokens);
388
+ const lines = raw.split("\n").map((line) => stripMachineTokens(sanitizeDisplayText(line)));
388
389
  while (lines.length > 0 && !(lines[0] ?? "").trim()) lines.shift();
389
390
  while (lines.length > 0 && !(lines[lines.length - 1] ?? "").trim()) lines.pop();
390
391
  let text = lines.join("\n");
@@ -416,7 +417,7 @@ export interface RichTerminalParts {
416
417
  }
417
418
 
418
419
  function escapeTableCell(value: string): string {
419
- return value.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
420
+ return sanitizeDisplayText(value).replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
420
421
  }
421
422
 
422
423
  function testsRowStatus(value: string): string {
@@ -470,9 +471,10 @@ function auditRowStatus(history: Goal["auditHistory"]): string {
470
471
 
471
472
  /** Split a stale-filtered `Label: value` detail into a bold lead + body. */
472
473
  function leadBody(detail: string): { lead: string; body: string } {
473
- const separator = detail.indexOf(":");
474
- if (separator < 0) return { lead: "Note", body: detail };
475
- return { lead: detail.slice(0, separator).trim() || "Note", body: detail.slice(separator + 1).trim() };
474
+ const safeDetail = sanitizeDisplayText(detail);
475
+ const separator = safeDetail.indexOf(":");
476
+ if (separator < 0) return { lead: "Note", body: safeDetail };
477
+ return { lead: safeDetail.slice(0, separator).trim() || "Note", body: safeDetail.slice(separator + 1).trim() };
476
478
  }
477
479
 
478
480
  /**
@@ -487,7 +489,7 @@ const EVIDENCE_TOKEN_PATTERN = /(?<![/~+\w])[\w.+][\w.+/-]*\.[A-Za-z0-9]{1,8}:\d
487
489
 
488
490
  export function extractEvidenceTokens(text: string): { text: string; evidence: string[] } {
489
491
  const evidence: string[] = [];
490
- const stripped = text
492
+ const stripped = sanitizeDisplayText(text)
491
493
  .replace(EVIDENCE_TOKEN_PATTERN, (match) => {
492
494
  if (evidence.length < RICH_EVIDENCE_TOKENS_PER_FINDING && !evidence.includes(match)) evidence.push(match);
493
495
  // The token often rides in parentheses — move the wrapper too, so
@@ -501,6 +503,29 @@ export function extractEvidenceTokens(text: string): { text: string; evidence: s
501
503
  return { text: stripped, evidence };
502
504
  }
503
505
 
506
+ /**
507
+ * v0.38.69 (Antigravity port): completion-claim evidence-density lint.
508
+ * A walkthrough always ships areas + counts; GLLA richness rode optional
509
+ * agent params, so a flat six-label claim with zero path:line tokens and
510
+ * zero gate rows still passed. Returns a NOTE annotation when the claim
511
+ * carries no verifiable pointer at all (no evidence tokens across the
512
+ * summary and every group finding, and no gate rows) — the claim still
513
+ * audits, but the terminal render falls back to recorded facts. Tokens in
514
+ * finding groups count; gate rows count as density without tokens.
515
+ */
516
+ export function completionSummaryDensityNote(
517
+ summary: string | undefined,
518
+ groups?: FindingGroup[],
519
+ gates?: GateRow[],
520
+ ): string | undefined {
521
+ if (!summary?.trim()) return undefined;
522
+ const pool = [summary, ...(groups ?? []).flatMap((group) => group.findings ?? [])].join("\n");
523
+ const { evidence } = extractEvidenceTokens(pool);
524
+ if (evidence.length > 0) return undefined;
525
+ if ((gates ?? []).length > 0) return undefined;
526
+ return "low evidence density: no path:line evidence tokens and no verification gate rows — add file:line pointers (e.g. extensions/goal-recovery.ts:847) or a gateRows inventory so the terminal render is verifiable";
527
+ }
528
+
504
529
  /**
505
530
  * v0.38.50: one compact duration line from durable goal state — turns,
506
531
  * wall-clock elapsed since creation, and audit count. Only known facts
@@ -509,7 +534,9 @@ export function extractEvidenceTokens(text: string): { text: string; evidence: s
509
534
  export function buildDurationLine(goal: Goal, now = Date.now()): string | null {
510
535
  const segs: string[] = [];
511
536
  const turns = goal.telemetry?.turns;
512
- if (typeof turns === "number" && Number.isFinite(turns) && turns >= 0) {
537
+ // Field 20260918_172705: a zero turns count means untracked telemetry,
538
+ // not a known fact — omit it while elapsed/audits still render.
539
+ if (typeof turns === "number" && Number.isFinite(turns) && turns > 0) {
513
540
  segs.push(`${turns} turn${turns === 1 ? "" : "s"}`);
514
541
  }
515
542
  const started = Date.parse(goal.createdAt ?? "");
@@ -519,15 +546,25 @@ export function buildDurationLine(goal: Goal, now = Date.now()): string | null {
519
546
  return segs.length > 0 ? `\u2014 ${segs.join(" \u00b7 ")}` : null;
520
547
  }
521
548
 
522
- /** Partition informing details into findings / Tests / next buckets. */
549
+ /** Partition informing details into findings / Tests / next buckets.
550
+ * Field 20260918_172705: exact-duplicate lines collapse to one per bucket
551
+ * (repeated claim details rendered as doubled rows). Near-duplicates with
552
+ * different wording still render — that prose belongs to the claim. */
523
553
  export function partitionRichDetails(details: string[]): { findings: string[]; tests: string[]; next: string[] } {
554
+ const seen = new Set<string>();
555
+ const push = (bucket: string[], detail: string) => {
556
+ const key = detail.trim();
557
+ if (seen.has(key)) return;
558
+ seen.add(key);
559
+ bucket.push(detail);
560
+ };
524
561
  const findings: string[] = [];
525
562
  const tests: string[] = [];
526
563
  const next: string[] = [];
527
564
  for (const detail of details) {
528
- if (/^\s*Tests\s*:/i.test(detail)) tests.push(detail);
529
- else if (/^\s*(Next|Unresolved|Left out)\s*:/i.test(detail)) next.push(detail);
530
- else findings.push(detail);
565
+ if (/^\s*Tests\s*:/i.test(detail)) push(tests, detail);
566
+ else if (/^\s*(Next|Unresolved|Left out)\s*:/i.test(detail)) push(next, detail);
567
+ else push(findings, detail);
531
568
  }
532
569
  return { findings, tests, next };
533
570
  }
@@ -538,15 +575,17 @@ export function partitionRichDetails(details: string[]): { findings: string[]; t
538
575
  * `## Done — outcome` shape stays, so direct unit callers are unaffected.
539
576
  */
540
577
  export function requestEchoHeadline(kind: "Done" | "Aborted", objective: string | undefined, outcome: string): string {
541
- const echo = objective?.trim() ? clipSummaryValue(objective.trim(), RICH_OBJECTIVE_ECHO_CHARS) : null;
542
- return echo ? `## ${kind}: ${echo} \u2014 ${outcome}` : `## ${kind} \u2014 ${outcome}`;
578
+ const safeObjective = objective ? sanitizeDisplayText(objective) : "";
579
+ const safeOutcome = sanitizeDisplayText(outcome);
580
+ const echo = safeObjective.trim() ? clipSummaryValue(safeObjective.trim(), RICH_OBJECTIVE_ECHO_CHARS) : null;
581
+ return echo ? `## ${kind}: ${echo} \u2014 ${safeOutcome}` : `## ${kind} \u2014 ${safeOutcome}`;
543
582
  }
544
583
 
545
584
  /** Chat-only projection: keep explanations and counts, not command/hash receipts.
546
585
  * Repository-only findings are filtered separately; useful implementation
547
586
  * references in substantive explanations are not themselves bookkeeping. */
548
587
  function chatNarrative(value: string): string {
549
- return stripMachineGroups(chatSafeDetailValue(value)
588
+ return stripMachineGroups(chatSafeDetailValue(sanitizeDisplayText(value))
550
589
  .replace(/\b(?:fixed in|commit|HEAD(?: at)?|built from)\s+`?[a-f0-9]{7,64}`?/gi, "")
551
590
  .replace(/\b(?=[a-f0-9]*[a-f])(?=[a-f0-9]*\d)[a-f0-9]{7,64}\b/gi, "")
552
591
  .replace(/`(?:bun|npm|npx|node|git|tsc)\s+[^`]+`/g, "")
@@ -620,9 +659,12 @@ export function buildFinalRepoStateLines(cwd: string): string[] | undefined {
620
659
  return undefined;
621
660
  }
622
661
  };
623
- const branch = run(["branch", "--show-current"]);
624
- const head = run(["log", "-1", "--format=%h %s"]);
625
- const short = run(["status", "--short"]);
662
+ const branchRaw = run(["branch", "--show-current"]);
663
+ const headRaw = run(["log", "-1", "--format=%h %s"]);
664
+ const shortRaw = run(["status", "--short"]);
665
+ const branch = branchRaw === undefined ? undefined : sanitizeDisplayText(branchRaw);
666
+ const head = headRaw === undefined ? undefined : sanitizeDisplayText(headRaw);
667
+ const short = shortRaw === undefined ? undefined : shortRaw.split("\n").map((line) => sanitizeDisplayText(line)).join("\n");
626
668
  if (!branch && !head && short === undefined) return undefined;
627
669
  const lines = [`Branch ${branch ?? "detached"}${head ? ` @ ${head}` : ""}`];
628
670
  if (short === undefined) lines.push("Tree state unreadable");
@@ -660,7 +702,7 @@ export function buildRichTerminalParts(args: {
660
702
  chat?: boolean;
661
703
  }): RichTerminalParts {
662
704
  const { findings, tests, next } = partitionRichDetails(args.details);
663
- const outcome = args.outcome;
705
+ const outcome = sanitizeDisplayText(args.outcome);
664
706
  const kind = args.kind ?? "Done";
665
707
  const headline = args.chat ? `## ${kind} — ${outcome}` : requestEchoHeadline(kind, args.objective, outcome);
666
708
  const auditStatus = auditRowStatus(args.auditHistory);
@@ -676,11 +718,11 @@ export function buildRichTerminalParts(args: {
676
718
  findingLines.push("| Area | Finding | Evidence |", "| --- | --- | --- |");
677
719
  for (const group of groups) {
678
720
  group.findings.forEach((finding, fi) => {
679
- const { text, evidence } = extractEvidenceTokens(finding);
721
+ const { text, evidence } = extractEvidenceTokens(sanitizeDisplayText(finding));
680
722
  const { lead, body } = leadBody(text);
681
723
  // v0.38.52: test proof rides the Evidence cell (tables have no
682
724
  // sub-bullets); cells stay pipe-escaped. v0.38.55: unclipped.
683
- const proof = group.tests?.[fi]?.trim();
725
+ const proof = group.tests?.[fi] ? sanitizeDisplayText(group.tests[fi]).trim() : "";
684
726
  const evidenceCell = [evidence.join(", ") || "\u2014", ...(proof ? [`Tests: ${proof}`] : [])].join(" \u00b7 ");
685
727
  findingLines.push(
686
728
  `| ${escapeTableCell(group.title)} | ${escapeTableCell(`**${lead}** \u2014 ${body}`)} | ${escapeTableCell(evidenceCell)} |`,
@@ -689,12 +731,12 @@ export function buildRichTerminalParts(args: {
689
731
  }
690
732
  } else if (groups.length > 0) {
691
733
  groups.forEach((group, i) => {
692
- findingLines.push(`#### ${i + 1}. ${group.title}`);
734
+ findingLines.push(`#### ${i + 1}. ${sanitizeDisplayText(group.title)}`);
693
735
  group.findings.forEach((finding, fi) => {
694
- const { lead, body } = leadBody(args.chat ? chatNarrative(extractEvidenceTokens(finding).text) : finding);
736
+ const { lead, body } = leadBody(args.chat ? chatNarrative(extractEvidenceTokens(finding).text) : sanitizeDisplayText(finding));
695
737
  findingLines.push(`- **${lead}** \u2014 ${body}`);
696
738
  // v0.38.52: per-finding test proof (shot C) — absent stays absent.
697
- const proof = group.tests?.[fi]?.trim();
739
+ const proof = group.tests?.[fi] ? sanitizeDisplayText(group.tests[fi]).trim() : "";
698
740
  if (proof) findingLines.push(` - Test Results: ${args.chat ? chatNarrative(proof) : proof}`);
699
741
  });
700
742
  });
@@ -716,22 +758,25 @@ export function buildRichTerminalParts(args: {
716
758
  const showCommand = !args.chat && gates.some((row) => row.command?.trim());
717
759
  if (gates.length > 0) {
718
760
  for (const row of gates) {
719
- const derived = testsRowStatus(row.notes ?? "");
720
- const notes = args.chat ? chatNarrative(row.notes ?? "") : row.notes?.trim();
721
- const command = row.command?.trim() || "\u2014";
761
+ const safeNotes = row.notes ? sanitizeDisplayText(row.notes).trim() : "";
762
+ const derived = testsRowStatus(safeNotes);
763
+ const notes = args.chat ? chatNarrative(safeNotes) : safeNotes;
764
+ const command = row.command ? sanitizeDisplayText(row.command).trim() : "\u2014";
765
+ const gate = sanitizeDisplayText(row.gate);
766
+ const scope = row.scope ? sanitizeDisplayText(row.scope).trim() : "\u2014";
722
767
  tableRows.push(showCommand
723
- ? `| ${escapeTableCell(row.gate)} | ${escapeTableCell(command)} | ${escapeTableCell(row.scope?.trim() || "\u2014")} | ${derived} | ${escapeTableCell(notes || "\u2014")} |`
724
- : `| ${escapeTableCell(row.gate)} | ${escapeTableCell(row.scope?.trim() || "\u2014")} | ${derived} | ${escapeTableCell(notes || "\u2014")} |`);
768
+ ? `| ${escapeTableCell(gate)} | ${escapeTableCell(command)} | ${escapeTableCell(scope)} | ${derived} | ${escapeTableCell(notes || "\u2014")} |`
769
+ : `| ${escapeTableCell(gate)} | ${escapeTableCell(scope)} | ${derived} | ${escapeTableCell(notes || "\u2014")} |`);
725
770
  }
726
771
  } else {
727
772
  for (const detail of tests) {
728
773
  const { body } = leadBody(detail);
729
774
  const status = testsRowStatus(body);
730
- tableRows.push(`| Tests | ${status} | ${escapeTableCell(args.chat ? chatNarrative(body) : body)} |`);
775
+ tableRows.push(`| Tests | ${status} | ${escapeTableCell(args.chat ? chatNarrative(body) : sanitizeDisplayText(body))} |`);
731
776
  }
732
777
  }
733
778
  if (!args.chat && auditStatus !== "NO VERDICT") {
734
- const auditBody = args.countsLine.replace(/^\u2014\s*/, "").replace(/\.\s*$/, "");
779
+ const auditBody = sanitizeDisplayText(args.countsLine).replace(/^\u2014\s*/, "").replace(/\.\s*$/, "");
735
780
  // v0.38.55 audit: the Audit row's Scope names the row kind — the old
736
781
  // shape duplicated the counts text in Scope and Notes.
737
782
  if (gates.length > 0) {
@@ -763,8 +808,8 @@ export function buildRichTerminalParts(args: {
763
808
  findingLines,
764
809
  tableLines,
765
810
  nextLines,
766
- repoLines: args.chat ? [] : args.repoState ?? [],
767
- summaryLines: args.summaryLines ?? [],
811
+ repoLines: args.chat ? [] : (args.repoState ?? []).map((line) => sanitizeDisplayText(line)),
812
+ summaryLines: (args.summaryLines ?? []).map((line) => sanitizeDisplayText(line)),
768
813
  };
769
814
  }
770
815
 
@@ -776,28 +821,30 @@ export function buildRichTerminalParts(args: {
776
821
  * final repository state closes it — findings, verification, Next,
777
822
  * repo state, in that order. */
778
823
  export function composeRichTerminalLines(parts: RichTerminalParts): string[] {
779
- const lines = [parts.banner ?? parts.headline, ""];
780
- if (parts.headline !== lines[0]) lines.push(parts.headline, "");
781
- if (parts.durationLine) lines.push(parts.durationLine, "");
824
+ const banner = sanitizeDisplayText(parts.banner ?? parts.headline);
825
+ const headline = sanitizeDisplayText(parts.headline);
826
+ const lines = [banner, ""];
827
+ if (headline !== lines[0]) lines.push(headline, "");
828
+ if (parts.durationLine) lines.push(sanitizeDisplayText(parts.durationLine), "");
782
829
  // Structured-long (field 2026-09-16): the full Outcome body rides its
783
830
  // own section between the headline and the findings — findings-first
784
831
  // order is preserved (findings, verification, Next keep their relative
785
832
  // order), the headline echo stays short, and the one-action Next rule
786
833
  // is untouched.
787
834
  if (parts.summaryLines.length > 0) {
788
- lines.push("### Summary", ...parts.summaryLines, "");
835
+ lines.push("### Summary", ...parts.summaryLines.map((line) => sanitizeDisplayText(line)), "");
789
836
  }
790
837
  if (parts.findingLines.length > 0) {
791
- lines.push("### Key Findings & Remediation", ...parts.findingLines, "");
838
+ lines.push("### Key Findings & Remediation", ...parts.findingLines.map((line) => sanitizeDisplayText(line)), "");
792
839
  }
793
840
  if (parts.tableLines.length > 0) {
794
- lines.push("### Verification Summary", ...parts.tableLines, "");
841
+ lines.push("### Verification Summary", ...parts.tableLines.map((line) => sanitizeDisplayText(line)), "");
795
842
  }
796
843
  if (parts.nextLines.length > 0) {
797
- lines.push("### Next", ...parts.nextLines, "");
844
+ lines.push("### Next", ...parts.nextLines.map((line) => sanitizeDisplayText(line)), "");
798
845
  }
799
846
  if (parts.repoLines.length > 0) {
800
- lines.push("### Final Repository State", ...parts.repoLines.map((line) => `- ${line}`), "");
847
+ lines.push("### Final Repository State", ...parts.repoLines.map((line) => `- ${sanitizeDisplayText(line)}`), "");
801
848
  }
802
849
  return lines;
803
850
  }
@@ -889,7 +936,7 @@ function stripApprovalModel(line: string): string {
889
936
  return line.replace(/auditor\s+\S+\s+approved/, "auditor approved");
890
937
  }
891
938
  function trailerBullet(line: string): string {
892
- return `• ${line.replace(/^—\s*/, "")}`;
939
+ return `• ${sanitizeDisplayText(line).replace(/^—\s*/, "")}`;
893
940
  }
894
941
 
895
942
  /** v0.38.25: the audit-goal counts line — verdict proof ONLY, built from
@@ -1420,7 +1420,7 @@ export function sendStallEscalation(ctx: ExtensionContext, nudges: number): void
1420
1420
  if (supervisorPaused(state)) return;
1421
1421
  // Audit 2026-09-07 (HIGH): a stall nudge must not resurrect a stood-down
1422
1422
  // chain — same abort-latch reasoning as sendContinuation.
1423
- if (flags.sessionHandoffPending || flags.initialSessionLoadPending || !flags.extensionApi || flags.extensionApiStale || continuationDispatchStoodDown || pendingContinuationDispatch || flags.abortedStandDown) return;
1423
+ if (flags.sessionHandoffPending || flags.initialSessionLoadPending || !flags.extensionApi || flags.extensionApiStale || flags.staleTerminalDone || flags.zombieStoodDown || continuationDispatchStoodDown || pendingContinuationDispatch || flags.abortedStandDown) return;
1424
1424
  if (!state.goal || !guardGoalBeforeContinuation(ctx, "stall-escalation")) return;
1425
1425
  const remaining = HEARTBEAT_MAX_NUDGES - nudges;
1426
1426
  const text = [
@@ -1464,7 +1464,7 @@ export function sendLengthContinue(ctx: ExtensionContext, consecutive: number):
1464
1464
  if (supervisorPaused(state)) return;
1465
1465
  // Audit 2026-09-07 (HIGH): a length nudge must not resurrect a stood-down
1466
1466
  // chain — same abort-latch reasoning as sendContinuation.
1467
- if (flags.sessionHandoffPending || flags.initialSessionLoadPending || !flags.extensionApi || flags.extensionApiStale || continuationDispatchStoodDown || pendingContinuationDispatch || flags.abortedStandDown) return;
1467
+ if (flags.sessionHandoffPending || flags.initialSessionLoadPending || !flags.extensionApi || flags.extensionApiStale || flags.staleTerminalDone || flags.zombieStoodDown || continuationDispatchStoodDown || pendingContinuationDispatch || flags.abortedStandDown) return;
1468
1468
  if (state.goal && !guardGoalBeforeContinuation(ctx, "length-continuation")) return;
1469
1469
  // v0.38.66 (PR #55, FOF11): active goals get completion-aware recovery
1470
1470
  // text — a truncated turn must offer closure, not another blind work
@@ -1525,7 +1525,34 @@ export function buildPostCompactResync(briefExcerpt?: string): string {
1525
1525
  return lines.join("\n") + "\n\n";
1526
1526
  }
1527
1527
 
1528
- export function continuationPrompt(goal: Goal, opts: { includeRestartDetail?: boolean } = {}): string {
1528
+ /**
1529
+ * v0.38.69 (Antigravity port, 09-04 borrow candidate #1): per-repo pitfall
1530
+ * registry. `.pi-glla/pitfalls.md` holds distilled, answer-agnostic rakes
1531
+ * (never ledger prose — the ledger is forensics, never distilled). Read at
1532
+ * goal start (and re-read when edited mid-goal); absent/blank/unreadable
1533
+ * resolves absent so repos without one render byte-identical prompts.
1534
+ */
1535
+ export const PITFALLS_BRIEF_MAX_CHARS = 1500;
1536
+
1537
+ export function readPitfallsBrief(cwd: string): string | undefined {
1538
+ let body: string;
1539
+ try {
1540
+ body = fs.readFileSync(path.join(cwd, ".pi-glla", "pitfalls.md"), "utf8");
1541
+ } catch {
1542
+ return undefined;
1543
+ }
1544
+ const trimmed = body.trim();
1545
+ if (!trimmed) return undefined;
1546
+ return trimmed.length > PITFALLS_BRIEF_MAX_CHARS
1547
+ ? trimmed.slice(0, PITFALLS_BRIEF_MAX_CHARS)
1548
+ : trimmed;
1549
+ }
1550
+
1551
+ /** Goals already ledgered for their pitfalls consult this process — the
1552
+ * ledger records the consult once per goal, never once per turn. */
1553
+ const pitfallsLedgeredGoals = new Set<string>();
1554
+
1555
+ export function continuationPrompt(goal: Goal, opts: { includeRestartDetail?: boolean; pitfallsBrief?: string } = {}): string {
1529
1556
  // Read the .md file as the template, then substitute {{tokens}}.
1530
1557
  // For v0.1.0 we inline-substitute so we don't need fs at runtime.
1531
1558
  const next = findNextPendingTask(goal.taskList?.tasks ?? []);
@@ -1583,6 +1610,20 @@ export function continuationPrompt(goal: Goal, opts: { includeRestartDetail?: bo
1583
1610
  if (loadSettings(settingsCwd).visionAssist !== false) {
1584
1611
  directives.push(VISION_ASSIST_GUIDANCE);
1585
1612
  }
1613
+ // v0.38.69 (Antigravity port): the repo pitfall registry rides the
1614
+ // continuation prompt — consulted at goal start, re-read when edited.
1615
+ // Explicit opts win (tests); otherwise the repo file resolves, absent
1616
+ // keeping the prompt byte-identical for repos without one.
1617
+ const pitfallsBrief = opts.pitfallsBrief ?? readPitfallsBrief(settingsCwd);
1618
+ if (pitfallsBrief?.trim()) {
1619
+ directives.push(
1620
+ `## REPO PITFALLS (distilled — consult before acting)\n\nThese rakes already caught this repo. Check your plan against them before the first tool call and before every risky step:\n\n${pitfallsBrief.trim()}`,
1621
+ );
1622
+ if (!pitfallsLedgeredGoals.has(goal.id)) {
1623
+ pitfallsLedgeredGoals.add(goal.id);
1624
+ appendLedger(settingsCwd, "pitfalls_consulted", { goalId: goal.id });
1625
+ }
1626
+ }
1586
1627
  if (effSettings.aggressiveMode && isFullAuditObjective(goal.objective)) {
1587
1628
  directives.push(
1588
1629
  "## FULL-AUDIT MODE (aggressiveMode + survey objective)\n\nThis objective is a survey, not a single fix. Spawn 3+ `scout` subagents NOW — one per subsystem, in a single message so they run in parallel — synthesize their findings, and call `propose_task_list` with the result. Do not start fixing before the task list exists.",
@@ -656,6 +656,15 @@ export interface Goal {
656
656
  pauseOptions?: string[];
657
657
  /** v0.28.22: 1-based index into pauseOptions the agent recommends. */
658
658
  pauseRecommended?: number;
659
+ /** v0.38.69 (Antigravity port): mid-run decision-pause accounting — every
660
+ * agent-authored kind="decision" pause increments this, in-budget or
661
+ * not, so the interruption budget has a durable counter across reloads. */
662
+ midRunDecisionCount?: number;
663
+ /** v0.38.69 (Antigravity port): decisions auto-resolved to the
664
+ * recommended default when the budget was exhausted — the assumption
665
+ * the agent must carry into its completion recap's Left out. Bounded
666
+ * to the trailing 20. */
667
+ autoDefaultLog?: Array<{ at: string; reason: string; chosen: string; options: string[] }>;
659
668
  /** v0.28.22: ISO time a wait-pause becomes resumable (countdown shown). */
660
669
  pauseResumeAt?: string;
661
670
  /** v0.35.28 (issue #16): set when glla AUTO-resumed a lapsed wait — the
@@ -1120,6 +1129,11 @@ export interface MainModelRecovery {
1120
1129
  retryAt?: string;
1121
1130
  /** Next preferred-primary health probe while a fallback is serving. */
1122
1131
  primaryProbeAt?: string;
1132
+ /** v0.38.69 (Antigravity port): absolute reset of the quota-walled
1133
+ * primary, stamped when the episode failed over with an explicit
1134
+ * upstream reset hint. While a fallback serves, the background primary
1135
+ * probe fires at this reset instead of the generic probe cadence. */
1136
+ primaryResetAt?: string;
1123
1137
  /** A preferred-primary switch was accepted and awaits one supervised turn. */
1124
1138
  primaryProbeInFlight?: boolean;
1125
1139
  /** Number of completed recovery waits; drives the bounded per-attempt exponential cadence. */
@@ -1228,6 +1242,7 @@ export function sanitizeMainModelRecovery(value: unknown): MainModelRecovery | u
1228
1242
  ...(Array.isArray(raw.recoveryNoticeKeys) ? { recoveryNoticeKeys: raw.recoveryNoticeKeys.filter((key): key is string => typeof key === "string").slice(-16).map((key) => key.slice(0, 300)) } : {}),
1229
1243
  ...(date(raw.retryAt) ? { retryAt: date(raw.retryAt) } : {}),
1230
1244
  ...(date(raw.primaryProbeAt) ? { primaryProbeAt: date(raw.primaryProbeAt) } : {}),
1245
+ ...(date(raw.primaryResetAt) ? { primaryResetAt: date(raw.primaryResetAt) } : {}),
1231
1246
  ...(raw.primaryProbeInFlight === true ? { primaryProbeInFlight: true } : {}),
1232
1247
  ...(date(raw.firstFailureAt) ? { firstFailureAt: date(raw.firstFailureAt) } : {}),
1233
1248
  ...(date(raw.autoRetryUntil) ? { autoRetryUntil: date(raw.autoRetryUntil) } : {}),
@@ -3626,6 +3641,21 @@ export function extractPendingTasks(report: string, cap = 5): string[] {
3626
3641
  return out;
3627
3642
  }
3628
3643
 
3644
+ /**
3645
+ * v0.38.69 (Antigravity port): objection-attached retries. Every
3646
+ * disapproval — aggressive or not — becomes a durable TODO projection:
3647
+ * extracted objection bullets, or a single review-the-report TODO when
3648
+ * nothing extractable survives. Never an empty list: the retry argues the
3649
+ * objection, not generic effort. The aggressive gate keeps only the
3650
+ * cap-keep-going / no-progress-stop behaviors, never the TODOs.
3651
+ */
3652
+ export function durableObjectionsForDisapproval(report: string, statusCommand = "/goal status"): string[] {
3653
+ const extracted = extractPendingTasks(report, 5);
3654
+ return extracted.length > 0
3655
+ ? extracted
3656
+ : [`Review the latest auditor disapproval in ${statusCommand}.`];
3657
+ }
3658
+
3629
3659
  /** Contract item 23: is the auditor's IMPOSSIBLE reason about the WHOLE
3630
3660
  * goal or only part of it? Default "full" (safe — keeps the pause);
3631
3661
  * partial only on explicit subset language. */
@@ -1112,6 +1112,21 @@ function pausedNextTransition(g: Goal, state: State, now: number): string {
1112
1112
  }
1113
1113
  }
1114
1114
 
1115
+ /** v0.38.70 (field 20260920_152744): close a paused action card with its
1116
+ * deferred history. The pause body already ended on a `└─` closer; reopen
1117
+ * it to `├─` and hang the tally/provenance/collapsed-judgment rows after
1118
+ * the action, closing on the last history row. Empty history is a no-op,
1119
+ * so cards with nothing deferred keep their exact shape. */
1120
+ function closePausedWithHistory(lines: string[], history: string[], theme?: DisplayTheme): string[] {
1121
+ if (history.length === 0) return lines;
1122
+ const tail = lines[lines.length - 1];
1123
+ if (tail !== undefined) lines[lines.length - 1] = tail.replace(/^└─/, "├─");
1124
+ history.forEach((h, i) => {
1125
+ lines.push(`${i === history.length - 1 ? "└─" : "├─"} ${paint(theme, "dim", h)}`);
1126
+ });
1127
+ return lines;
1128
+ }
1129
+
1115
1130
  function pausedLifecycleLines(g: Goal, state: State, extras: WidgetExtras | undefined, now: number): [string, string] {
1116
1131
  const queued = state.list?.length ?? 0;
1117
1132
  return [
@@ -1932,9 +1947,20 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1932
1947
  // v0.38.8: durable verdict tally as a first-class card row — the widget
1933
1948
  // is the glance surface, and stored verdicts are the progress evidence
1934
1949
  // when no auditor is live. Silent when history is empty.
1950
+ // v0.38.70 (field 20260920_152744): a paused action card must lead with
1951
+ // the action, not history. Pi core truncates the widget tail (~10 rows),
1952
+ // so judgment plaques above the blocked banner pushed the suggested action
1953
+ // below the cut — "... (widget truncated)" with no action visible.
1954
+ // Tally, provenance, and judgment ride AFTER the pause closer instead;
1955
+ // judgment rides collapsed (header + selection; full plaques stay in
1956
+ // /goal status). Interrupted/attention cards keep the existing order.
1957
+ const deferHistory = g.status === "paused" && !!g.pauseReason && !interrupted && !attention;
1958
+ const deferredHistory: string[] = [];
1935
1959
  const headTally = formatVerdictTallySegment(auditorVerdictTally(g.auditHistory, now), now);
1936
1960
  if (headTally) {
1937
- lines.push(`├─ ${paint(theme, "dim", `audits: ${truncate(headTally, Math.max(20, (width ?? 80) - 12))}`)}`);
1961
+ const tallyRow = `audits: ${truncate(headTally, Math.max(20, (width ?? 80) - 12))}`;
1962
+ if (deferHistory) deferredHistory.push(tallyRow);
1963
+ else lines.push(`├─ ${paint(theme, "dim", tallyRow)}`);
1938
1964
  }
1939
1965
  if (g.repairTarget) {
1940
1966
  lines.push(`├─ ${paint(theme, "warning", `REPLAN REQUIRED · original target: ${truncate(g.repairTarget.objective.replace(/\s+/g, " "), Math.max(30, (width ?? 80) - 28))}`)}`);
@@ -1967,12 +1993,22 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
1967
1993
  ? []
1968
1994
  : modelProvenanceLines(extras?.modelProvenance, width);
1969
1995
  const provenanceStart = lines.length;
1970
- provenance.forEach((line, i) => {
1971
- lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
1972
- });
1996
+ if (deferHistory) {
1997
+ provenance.forEach((line) => deferredHistory.push(line));
1998
+ } else {
1999
+ provenance.forEach((line, i) => {
2000
+ lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, "dim", line)}`);
2001
+ });
2002
+ }
1973
2003
  if (extras?.durableDeferRecommendation) {
1974
2004
  const judgment = buildDurableDeferDecisionLines(extras.durableDeferRecommendation, width);
1975
- judgment.forEach((line) => lines.push(`├─ ${paint(theme, "dim", line)}`));
2005
+ // Collapsed on action cards: header + selection only. Plaque bodies
2006
+ // stay in /goal status; the glance card must fit the action.
2007
+ const collapsed = deferHistory && judgment.length > 2
2008
+ ? [judgment[0]!, judgment[judgment.length - 1]!]
2009
+ : judgment;
2010
+ if (deferHistory) deferredHistory.push(...collapsed);
2011
+ else judgment.forEach((line) => lines.push(`├─ ${paint(theme, "dim", line)}`));
1976
2012
  }
1977
2013
  if (interrupted) {
1978
2014
  const resumeCmd = isList ? "/list resume" : "/goal resume";
@@ -2031,7 +2067,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
2031
2067
  lines.push(`├─ ${paint(theme, "dim", lifecycle)}`);
2032
2068
  lines.push(`│ ${paint(theme, "dim", transition)}`);
2033
2069
  lines.push(`└─ ${paint(theme, "warning", sanitizeProviderDisplayText(g.pauseSuggestedAction ?? `The claim is safe; ${isList ? "/list resume" : "/goal resume"} starts exactly one fresh auditor.`))}`);
2034
- return lines;
2070
+ return closePausedWithHistory(lines, deferredHistory, theme);
2035
2071
  }
2036
2072
  const [lifecycle, transition] = pausedLifecycleLines(g, state, extras, now);
2037
2073
  lines.push(`├─ ${paint(theme, "dim", lifecycle)}`);
@@ -2128,7 +2164,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
2128
2164
  // the card here, closing the previous row as the footer.
2129
2165
  const tail = lines[lines.length - 1];
2130
2166
  if (tail !== undefined) lines[lines.length - 1] = tail.replace(/^├─|^│\s*/, "└─ ");
2131
- return lines;
2167
+ return closePausedWithHistory(lines, deferredHistory, theme);
2132
2168
  }
2133
2169
  const spent: string[] = [];
2134
2170
  const tokUsed = g.usage?.tokensUsed ?? 0;
@@ -2148,7 +2184,7 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
2148
2184
  } else {
2149
2185
  lines.push(`└─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
2150
2186
  }
2151
- return lines;
2187
+ return closePausedWithHistory(lines, deferredHistory, theme);
2152
2188
  }
2153
2189
  if (attention) {
2154
2190
  const budget = budgetFor(width, 3, 60);