pi-goal-list-loop-audit 0.38.67 → 0.38.69

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,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.69 — About and README refresh (2026-09-20)
4
+
5
+ - Refined npm about line and README intro: tighter mission control wording, no em dashes.
6
+ - README range wording uses plain "to" (0 to 10, 1 to 10).
7
+
8
+ ### Antigravity auto-continue port (survey 2026-09-19)
9
+
10
+ - Mid-run interruption budget (`decisionPauseBudget`): decision pauses past
11
+ budget auto-default to the recommended option and ledger
12
+ `decision_budget_auto_default` instead of stalling mid-run.
13
+ - Quota sleep-until-reset: quota-class failures with an explicit upstream
14
+ hint sleep until reset (5h cap; 5s eager first retry preserved); quota
15
+ waits never park at the 24h horizon; background primary probe re-arms at
16
+ reset while fallback serves.
17
+ - Completion-claim evidence-density lint: zero path:line tokens AND zero
18
+ gate rows rides a ledgered NOTE annotation (claim still audits); every
19
+ terminal path already ends with the archive record pointer.
20
+ - Per-repo pitfall registry: `.pi-glla/pitfalls.md` (absent stays absent)
21
+ rides the continuation prompt under REPO PITFALLS, ledgered once per goal.
22
+ - Objection-attached retries: every disapproval extracts durable TODOs via
23
+ `durableObjectionsForDisapproval` (aggressive keeps only cap/no-progress
24
+ behaviors); two pre-existing source-shape pins revised to the new shape.
25
+
26
+ ## 0.38.68 — Relentless auto-continue: heat-routed exhaustion, quota retries, truthful recovering (2026-09-19)
27
+
28
+ ### Length-exhaustion wedge fixed at the root: heat, not budget (field 162348)
29
+
30
+ Repeated output-token truncation at near-full context is a heat problem —
31
+ the prompt no longer fits the model — so growing the truncation budget
32
+ only ping-pongs into the same wall. `decideLengthExhaustion` now routes on
33
+ `contextPercent`: roomy context restarts the truncation budget relentlessly
34
+ (episode 1 of a bounded 2-episode budget, episode 2 parks durably),
35
+ near-full context with a larger-context fallback rotates models, and
36
+ near-full context without one yields to pi auto-compaction and lets the
37
+ settle path own the resume. The compact-defer path no longer kicks a fresh
38
+ turn into the known-hot window, and a manual `/goal resume` between
39
+ episodes restarts the cycle instead of parking one round early.
40
+
41
+ ### Quota-identical auditor failures hammer bounded retries (field 150821)
42
+
43
+ Rate-limit and plan-quota walls are transient, so they are exempt from the
44
+ identical-failure park: the auditor keeps hammering its bounded retry plan
45
+ instead of parking with "check the auditor/model setup". Billing walls
46
+ and non-quota infra failures still park truthfully. A resumed or recovered
47
+ cycle reseeds the dead candidate chain via `freshAuditorCycleClaim`
48
+ instead of re-walking it, and no longer renders the previous cycle's dead
49
+ chain or diagnostic one round late.
50
+
51
+ ### Armed retry renders recovering, not dead-paused (field 151113)
52
+
53
+ A supervised auditor wait with an armed retry now renders `recovering`
54
+ with an auto-retrying status line. Bare user waits with no pending claim
55
+ still render `paused` — no false recovering labels.
56
+
57
+ ### glla-delegate skill states its drafting precondition (field 125302)
58
+
59
+ The skill doc now says `propose_goal_draft` only runs inside an already-open
60
+ drafting session (bare `/goal`, `/list`, or `/list add`), refuses from
61
+ normal chat, and falls back to `list_add` — the refusal is correct
62
+ behavior, so this is a doc fix with a regression pin, no prod change.
63
+
3
64
  ## 0.38.67 — Stop clobbering other extensions' compactions (2026-09-19)
4
65
 
5
66
  ### session_before_compact returns nothing (issue #56, ezoushen)
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.67 continue through the supervisor freeze (`/glla pause`),
19
+ v0.35.14–v0.38.69 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
@@ -113,7 +113,7 @@ export interface CommandDeps {
113
113
  releaseContinuationDispatchStandDown: () => void;
114
114
  releaseInitialSessionLoadBarrier: () => void;
115
115
  resolveCarryover: (ctx: ExtensionContext, trigger: "goal" | "loop" | "list") => boolean;
116
- safeSteerUser: (ctx: ExtensionContext, text: string) => boolean;
116
+ resetLengthExhaustionEpisodes: () => void; safeSteerUser: (ctx: ExtensionContext, text: string) => boolean;
117
117
  scheduleContinuation: (ctx: ExtensionContext, force?: boolean, delayMs?: number) => void;
118
118
  scheduleSessionTimeout: (callback: () => void, delayMs: number) => NodeJS.Timeout;
119
119
  createGoal: (objective: string, ctx: ExtensionContext, policy?: "goal" | "list") => Goal;
@@ -138,7 +138,7 @@ let listQueue: CommandDeps["listQueue"], notifyExternal: CommandDeps["notifyExte
138
138
  archiveCurrentGoal: CommandDeps["archiveCurrentGoal"], healGoalPolicy: CommandDeps["healGoalPolicy"], startDrafting: CommandDeps["startDrafting"], warnIfStaleAtEntry: CommandDeps["warnIfStaleAtEntry"], queuePendingListOperation: CommandDeps["queuePendingListOperation"], freshCtx: CommandDeps["freshCtx"],
139
139
  freshCtxForGeneration: CommandDeps["freshCtxForGeneration"], goStaleTerminal: CommandDeps["goStaleTerminal"], groupOpenChildren: CommandDeps["groupOpenChildren"], activateNextListItem: CommandDeps["activateNextListItem"], clearMainModelRecoveryTimer: CommandDeps["clearMainModelRecoveryTimer"], mainModelRecoveryTimerActive: CommandDeps["mainModelRecoveryTimerActive"], continuationDispatchPending: CommandDeps["continuationDispatchPending"], resetContinuationDispatchState: CommandDeps["resetContinuationDispatchState"],
140
140
  isCompletionAuditRecoveryPending: CommandDeps["isCompletionAuditRecoveryPending"], markCompletionAuditRecoveryPending: CommandDeps["markCompletionAuditRecoveryPending"], retryStoredCompletionAudit: CommandDeps["retryStoredCompletionAudit"], probeMainModelRecovery: CommandDeps["probeMainModelRecovery"], releaseContinuationDispatchStandDown: CommandDeps["releaseContinuationDispatchStandDown"],
141
- releaseInitialSessionLoadBarrier: CommandDeps["releaseInitialSessionLoadBarrier"], resolveCarryover: CommandDeps["resolveCarryover"], safeSteerUser: CommandDeps["safeSteerUser"], scheduleContinuation: CommandDeps["scheduleContinuation"], scheduleSessionTimeout: CommandDeps["scheduleSessionTimeout"],
141
+ releaseInitialSessionLoadBarrier: CommandDeps["releaseInitialSessionLoadBarrier"], resolveCarryover: CommandDeps["resolveCarryover"], resetLengthExhaustionEpisodes: CommandDeps["resetLengthExhaustionEpisodes"], safeSteerUser: CommandDeps["safeSteerUser"], scheduleContinuation: CommandDeps["scheduleContinuation"], scheduleSessionTimeout: CommandDeps["scheduleSessionTimeout"],
142
142
  createGoal: CommandDeps["createGoal"], fireReviewer: CommandDeps["fireReviewer"], openSettingsUI: CommandDeps["openSettingsUI"], manuallyResumeMainModelRecovery: CommandDeps["manuallyResumeMainModelRecovery"], activeGoalCommand: CommandDeps["activeGoalCommand"],
143
143
  activeGoalStatusCommand: CommandDeps["activeGoalStatusCommand"], activeGoalSurfaceCommand: CommandDeps["activeGoalSurfaceCommand"], goalNoun: CommandDeps["goalNoun"], displaySlice: CommandDeps["displaySlice"], shortObj: CommandDeps["shortObj"];
144
144
 
@@ -148,7 +148,7 @@ export function createGoalCommands(d: CommandDeps): void {
148
148
  archiveCurrentGoal = d.archiveCurrentGoal; healGoalPolicy = d.healGoalPolicy; startDrafting = d.startDrafting; warnIfStaleAtEntry = d.warnIfStaleAtEntry; queuePendingListOperation = d.queuePendingListOperation; freshCtx = d.freshCtx;
149
149
  freshCtxForGeneration = d.freshCtxForGeneration; goStaleTerminal = d.goStaleTerminal; groupOpenChildren = d.groupOpenChildren; activateNextListItem = d.activateNextListItem; clearMainModelRecoveryTimer = d.clearMainModelRecoveryTimer; mainModelRecoveryTimerActive = d.mainModelRecoveryTimerActive; continuationDispatchPending = d.continuationDispatchPending; resetContinuationDispatchState = d.resetContinuationDispatchState;
150
150
  isCompletionAuditRecoveryPending = d.isCompletionAuditRecoveryPending; markCompletionAuditRecoveryPending = d.markCompletionAuditRecoveryPending; retryStoredCompletionAudit = d.retryStoredCompletionAudit; probeMainModelRecovery = d.probeMainModelRecovery; releaseContinuationDispatchStandDown = d.releaseContinuationDispatchStandDown;
151
- releaseInitialSessionLoadBarrier = d.releaseInitialSessionLoadBarrier; resolveCarryover = d.resolveCarryover; safeSteerUser = d.safeSteerUser; scheduleContinuation = d.scheduleContinuation; scheduleSessionTimeout = d.scheduleSessionTimeout;
151
+ releaseInitialSessionLoadBarrier = d.releaseInitialSessionLoadBarrier; resolveCarryover = d.resolveCarryover; resetLengthExhaustionEpisodes = d.resetLengthExhaustionEpisodes; safeSteerUser = d.safeSteerUser; scheduleContinuation = d.scheduleContinuation; scheduleSessionTimeout = d.scheduleSessionTimeout;
152
152
  createGoal = d.createGoal; fireReviewer = d.fireReviewer; openSettingsUI = d.openSettingsUI; manuallyResumeMainModelRecovery = d.manuallyResumeMainModelRecovery; activeGoalCommand = d.activeGoalCommand;
153
153
  activeGoalStatusCommand = d.activeGoalStatusCommand; activeGoalSurfaceCommand = d.activeGoalSurfaceCommand; goalNoun = d.goalNoun; displaySlice = d.displaySlice; shortObj = d.shortObj;
154
154
  agentsSnapshot = d.agentsSnapshot;
@@ -589,6 +589,10 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
589
589
  updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, pauseKind: undefined, pauseOptions: undefined, pauseRecommended: undefined, pauseResumeAt: undefined, interruptedAt: undefined, interruptedReason: undefined, autoResumedAt: undefined, autoResumedEvent: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
590
590
  if (staleEntry) return;
591
591
  releaseAuditorSurface();
592
+ // A manual resume starts a fresh relentless cycle: a user pause between
593
+ // an episode-1 exhaustion wedge and the next one must not make that
594
+ // wedge park one cycle early.
595
+ resetLengthExhaustionEpisodes();
592
596
  // A stored completion claim is a direct-audit resume, not an agent turn.
593
597
  // Keeping the claim while merely scheduling a continuation left manual
594
598
  // pause/resume with an ACTIVE goal that no timer would ever consume.
@@ -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.",