pi-goal-list-loop-audit 0.25.3 → 0.25.4

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.
@@ -159,7 +159,9 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
159
159
  ? ["4. Verify that the executor has satisfied every item in the <verification_contract>. If any item is missing or weakly addressed, disapprove."]
160
160
  : []),
161
161
  "5. Explain missing or weak evidence, especially scaffold-vs-final quality gaps.",
162
- "6. End with exactly <approved/> only if the objective is truly complete; <impossible>reason</impossible> if it can never be satisfied as stated; otherwise end with exactly <disapproved/>.",
162
+ "6. When you disapprove, end the report body with a '## Required fixes' section: one line per blocking gap, each an actionable instruction the executor can complete (most critical first). This tail is what the executor sees first — make it self-sufficient.",
163
+ "7. Write the report in English, and never emit <think> blocks or fragments — your reasoning stays private; the report is the verdict plus evidence.",
164
+ "8. End with exactly <approved/> only if the objective is truly complete; <impossible>reason</impossible> if it can never be satisfied as stated; otherwise end with exactly <disapproved/>.",
163
165
  ...(goal.verificationContract?.trim()
164
166
  ? [
165
167
  "",
@@ -309,8 +309,14 @@ export const DEFAULT_AUDIT_FEEDBACK_CHARS = 0;
309
309
  * Bound the auditor report returned to the executor after disapproval.
310
310
  * A limit of 0 explicitly means "show the full report".
311
311
  */
312
+ /** Executor-visible excerpt of a disapproval report. Full by default
313
+ * (maxChars 0). When capped, keep the TAIL: since v0.25.4 the auditor
314
+ * ends disapprovals with the actionable `## Required fixes` section —
315
+ * head-slicing would cut exactly what the executor needs. */
312
316
  export function auditFeedbackExcerpt(output: string, maxChars: number): string {
313
- return maxChars === 0 ? output : output.slice(0, maxChars);
317
+ if (maxChars === 0 || output.length <= maxChars) return output;
318
+ return `[head truncated — full report via /goal status]
319
+ …${output.slice(-maxChars)}`;
314
320
  }
315
321
 
316
322
  export interface ListItem {
@@ -400,10 +406,19 @@ export interface State {
400
406
  /** v0.24.2: count TRAILING consecutive disapprovals (the disapproval-cap
401
407
  * input). Shield-blocks (approved:true) and infra errors (neither flag)
402
408
  * break the streak — they are not verdicts on the work. */
409
+ /** v0.24.2: count TRAILING consecutive disapprovals (the disapproval-cap
410
+ * input). Shield-blocks (approved:true) break the streak — the work was
411
+ * judged good. v0.25.4: pure infra errors (error set, neither verdict
412
+ * flag) are TRANSPARENT, not streak-breakers — the auditor never judged
413
+ * the work, so D,D,infra,D is still 3 trailing disapprovals (before, 39
414
+ * hegemon-style infra errors would reset the cap and re-open infinite
415
+ * re-continuation). */
403
416
  export function countTrailingDisapprovals(history: AuditVerdict[]): number {
404
417
  let n = 0;
405
418
  for (let i = history.length - 1; i >= 0; i--) {
406
- if (history[i]!.disapproved) n++;
419
+ const v = history[i]!;
420
+ if (v.disapproved) n++;
421
+ else if (v.error && !v.approved) continue; // infra: not a verdict
407
422
  else break;
408
423
  }
409
424
  return n;
@@ -1044,3 +1059,89 @@ export function formatListDepth(stats: ListDepthStats): string {
1044
1059
  if (stats.durationSamples > 0) lines.push(`avg item duration: ${fmtAge(stats.avgDurationMs!)} (from last ${stats.durationSamples} archived)`);
1045
1060
  return lines.join("\n");
1046
1061
  }
1062
+
1063
+ // =================================================================
1064
+ // v0.25.4: auditor polish — durable audit log, think-block hygiene,
1065
+ // actionable-tail slicing, infra-transparent streaks
1066
+ // =================================================================
1067
+
1068
+ /** Strip think-block leakage from auditor reports before storage/display.
1069
+ * Motivation (wild, 2026-07-25): MiniMax-M3 reports arrive with
1070
+ * `<think>...</think>` bodies, stray `</think>` fragments, and non-English
1071
+ * reasoning spillover — the executor's feedback should be the verdict,
1072
+ * not the auditor's private monologue. */
1073
+ export function stripThinkBlocks(text: string): string {
1074
+ return text
1075
+ .replace(/<think>[\s\S]*?<\/think>/gi, "")
1076
+ .replace(/<\/?think>/gi, "")
1077
+ .replace(/<200b>/g, "") // stray partial-tag artifact seen in the wild
1078
+ .replace(/^\s+/, "");
1079
+ }
1080
+
1081
+ /** One durable audit-log entry — survives state-snapshot rotation, so
1082
+ * /glla audits can answer "where are we weak" across the whole project. */
1083
+ export interface AuditLogEntry {
1084
+ at: string;
1085
+ goalId: string;
1086
+ objective: string;
1087
+ verdict: "approved" | "disapproved" | "impossible" | "shield_blocked" | "error";
1088
+ model: string;
1089
+ thinkingLevel: string;
1090
+ report: string;
1091
+ impossibleReason?: string;
1092
+ error?: string;
1093
+ }
1094
+
1095
+ export function auditLogPath(cwd: string): string {
1096
+ return path.join(cwd, ".pi-glla", "audits.jsonl");
1097
+ }
1098
+
1099
+ export function appendAuditLog(cwd: string, entry: AuditLogEntry): void {
1100
+ try {
1101
+ ensureDirs(cwd);
1102
+ fs.appendFileSync(auditLogPath(cwd), JSON.stringify(entry) + "\n");
1103
+ } catch {
1104
+ /* log best-effort — never block the verdict path */
1105
+ }
1106
+ }
1107
+
1108
+ export function readAuditLog(cwd: string, limit?: number): AuditLogEntry[] {
1109
+ let raw: string;
1110
+ try {
1111
+ raw = fs.readFileSync(auditLogPath(cwd), "utf-8");
1112
+ } catch {
1113
+ return [];
1114
+ }
1115
+ const out: AuditLogEntry[] = [];
1116
+ for (const line of raw.split("\n")) {
1117
+ const t = line.trim();
1118
+ if (!t) continue;
1119
+ try {
1120
+ const e = JSON.parse(t);
1121
+ if (e && typeof e.goalId === "string" && typeof e.verdict === "string") out.push(e as AuditLogEntry);
1122
+ } catch {
1123
+ /* skip malformed */
1124
+ }
1125
+ }
1126
+ return limit !== undefined ? out.slice(-limit) : out;
1127
+ }
1128
+
1129
+ const VERDICT_GLYPH: Record<AuditLogEntry["verdict"], string> = {
1130
+ approved: "✔",
1131
+ disapproved: "✖",
1132
+ impossible: "⛔",
1133
+ shield_blocked: "🛡",
1134
+ error: "⚠",
1135
+ };
1136
+
1137
+ /** /glla audits list view: one line per verdict, newest last. */
1138
+ export function formatAuditLog(entries: AuditLogEntry[]): string {
1139
+ if (entries.length === 0) return "(no audits logged yet — the log starts with the next verdict)";
1140
+ return entries
1141
+ .map((e) => {
1142
+ const day = e.at.slice(5, 16).replace("T", " ");
1143
+ const firstLine = (e.report.split("\n").find((l) => l.trim()) ?? "").trim().slice(0, 90);
1144
+ return `${VERDICT_GLYPH[e.verdict]} ${day} [${e.goalId.slice(-6)}] ${e.model} — ${firstLine}`;
1145
+ })
1146
+ .join("\n");
1147
+ }
@@ -72,6 +72,9 @@ export interface AuditDisplayProgress {
72
72
  currentTool?: string;
73
73
  label?: string;
74
74
  elapsedMs?: number;
75
+ /** v0.25.4: last progress-event time — the widget flags auditor-quiet
76
+ * stalls when this goes stale while the audit is in flight. */
77
+ lastEventAt?: number;
75
78
  }
76
79
 
77
80
  /**
@@ -175,7 +178,12 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
175
178
  const lines = [head, `├─ ${isList ? "list item · " : ""}${statusWord} · ${fmtElapsed(now - Date.parse(g.createdAt))}${tokens}`];
176
179
  if (g.status === "auditing") {
177
180
  lines.push(`├─ auditor: ${audit?.label ?? "running"}${audit?.currentTool ? ` · ${truncate(audit.currentTool, 30)}` : ""}`);
178
- if (audit?.elapsedMs) lines.push(`└─ ${paint(theme, "dim", `${fmtElapsed(audit.elapsedMs)} in isolated session`)}`);
181
+ // v0.25.4: auditor-quiet stall progress events stopped arriving
182
+ // while the audit is in flight (hung model call, stuck tool).
183
+ const quietMs = audit?.lastEventAt !== undefined ? now - audit.lastEventAt : 0;
184
+ if (quietMs > 3 * 60_000) {
185
+ lines.push(`└─ ${paint(theme, "warning", `auditor quiet ${fmtElapsed(quietMs)} — may be stuck; Esc aborts, verdict is not counted`)}`);
186
+ } else if (audit?.elapsedMs) lines.push(`└─ ${paint(theme, "dim", `${fmtElapsed(audit.elapsedMs)} in isolated session`)}`);
179
187
  else lines.push(`└─ ${paint(theme, "dim", "isolated session, read-only tools")}`);
180
188
  return lines;
181
189
  }
@@ -39,7 +39,12 @@ import {
39
39
  isFullAuditObjective,
40
40
  lastShippedAtMs,
41
41
  resolveEffectiveAggressiveSettings,
42
+ appendAuditLog,
42
43
  computeListDepth,
44
+ formatAuditLog,
45
+ readAuditLog,
46
+ stripThinkBlocks,
47
+ type AuditLogEntry,
43
48
  ledgerPath,
44
49
  crossRecommendMode,
45
50
  formatListDepth,
@@ -1697,7 +1702,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1697
1702
  ctx.ui.notify(`Auditor running (isolated session, model: ${via ?? "setting"})…`, "info");
1698
1703
  // Esc during the audit aborts this tool's signal → threaded into the
1699
1704
  // auditor session, which aborts cleanly and returns "Auditor aborted."
1700
- latestAuditProgress = { label: "starting" };
1705
+ latestAuditProgress = { label: "starting", lastEventAt: Date.now() };
1701
1706
  const result = await runGoalCompletionAuditor({
1702
1707
  ctx,
1703
1708
  goal: state.goal,
@@ -1711,6 +1716,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1711
1716
  currentTool: progress.currentTool,
1712
1717
  label: progress.label,
1713
1718
  elapsedMs: progress.elapsedMs,
1719
+ lastEventAt: Date.now(),
1714
1720
  };
1715
1721
  refreshUI(ctx);
1716
1722
  },
@@ -1723,6 +1729,11 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1723
1729
  const auditorRan = result.output.trim().length > 0;
1724
1730
  const history = state.goal.auditHistory ?? [];
1725
1731
  if (auditorRan) {
1732
+ // v0.25.4: strip think-block leakage (MiniMax-M3 `</think>`
1733
+ // fragments + reasoning spillover) before anything stores or
1734
+ // displays the report.
1735
+ const cleanOutput = stripThinkBlocks(result.output);
1736
+ result.output = cleanOutput;
1726
1737
  history.push({
1727
1738
  at: nowIso(),
1728
1739
  approved: result.approved,
@@ -1731,13 +1742,36 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1731
1742
  impossibleReason: result.impossibleReason,
1732
1743
  model: result.model,
1733
1744
  thinkingLevel: result.thinkingLevel,
1734
- report: result.output,
1745
+ report: cleanOutput,
1735
1746
  error: result.error,
1736
1747
  regressionShieldPassed: result.regressionShieldPassed,
1737
1748
  regressionShieldMissing: result.regressionShieldMissing,
1738
1749
  });
1739
1750
  // Cap history — 39 infra errors taught us unbounded growth is real.
1740
1751
  if (history.length > 20) history.splice(0, history.length - 20);
1752
+ // v0.25.4: durable append-only audit log — survives state-snapshot
1753
+ // rotation; the review surface for "where are we weak".
1754
+ const verdict: AuditLogEntry["verdict"] =
1755
+ result.error && !result.approved && !result.disapproved
1756
+ ? "error"
1757
+ : result.approved && result.regressionShieldPassed === false
1758
+ ? "shield_blocked"
1759
+ : result.approved
1760
+ ? "approved"
1761
+ : result.impossible
1762
+ ? "impossible"
1763
+ : "disapproved";
1764
+ appendAuditLog(ctx.cwd, {
1765
+ at: nowIso(),
1766
+ goalId: state.goal.id,
1767
+ objective: state.goal.objective.slice(0, 200),
1768
+ verdict,
1769
+ model: result.model,
1770
+ thinkingLevel: result.thinkingLevel ?? "(default)",
1771
+ report: cleanOutput,
1772
+ impossibleReason: result.impossibleReason,
1773
+ error: result.error,
1774
+ });
1741
1775
  }
1742
1776
 
1743
1777
  // Escape hatch: the user aborted the audit (Esc). Offer the explicit
@@ -1916,7 +1950,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1916
1950
  const auditFeedbackIsFull = auditFeedbackChars === 0 || result.output.length <= auditFeedbackChars;
1917
1951
  const auditFeedbackLabel = auditFeedbackIsFull
1918
1952
  ? "full report"
1919
- : `first ${auditFeedbackChars} chars`;
1953
+ : `last ${auditFeedbackChars} chars (Required-fixes tail)`;
1920
1954
  const auditFeedbackTruncationHint = auditFeedbackIsFull
1921
1955
  ? ""
1922
1956
  : `\n\nReport truncated at the configured limit. /goal status shows the full report; change future feedback with /glla auditfeedbackchars=N (0 = full report).`;
@@ -2744,6 +2778,24 @@ function cmdStats(args: string, ctx: ExtensionContext): void {
2744
2778
  ctx.ui.notify(`glla stats — ${rollups.length} project(s)${prematureOnly ? " (premature filter)" : ""}\n${out}`, "info");
2745
2779
  }
2746
2780
 
2781
+ /**
2782
+ * v0.25.4: /glla audits [N|full] — browse the durable per-project audit
2783
+ * log (.pi-glla/audits.jsonl). Default: last 10 verdicts, one line each.
2784
+ * "full" prints the latest report in full.
2785
+ */
2786
+ function cmdAudits(args: string, ctx: ExtensionContext): void {
2787
+ const full = /\bfull\b/.test(args);
2788
+ const nMatch = args.match(/\b(\d+)\b/);
2789
+ if (full) {
2790
+ const latest = readAuditLog(ctx.cwd).at(-1);
2791
+ ctx.ui.notify(latest ? `Latest audit — ${latest.verdict} (${latest.model}, ${latest.at})\n${latest.report}` : "No audits logged yet.", "info");
2792
+ return;
2793
+ }
2794
+ const n = nMatch ? Number(nMatch[1]) : 10;
2795
+ const entries = readAuditLog(ctx.cwd, n);
2796
+ ctx.ui.notify(`glla audits — last ${entries.length} verdict(s) in ${ctx.cwd}\n${formatAuditLog(entries)}`, "info");
2797
+ }
2798
+
2747
2799
  async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2748
2800
  // The plugin's ONE config surface — global by default, rarely opened.
2749
2801
  // /glla show effective values + where each comes from
@@ -2762,6 +2814,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2762
2814
  cmdStats(trimmed.slice("stats".length).trim(), ctx);
2763
2815
  return;
2764
2816
  }
2817
+ if (/^audits\b/.test(trimmed)) {
2818
+ cmdAudits(trimmed.slice("audits".length).trim(), ctx);
2819
+ return;
2820
+ }
2765
2821
  if (!trimmed) {
2766
2822
  if (ctx.hasUI) {
2767
2823
  await openSettingsUI(ctx);
@@ -3059,6 +3115,7 @@ export default function (pi: ExtensionAPI): void {
3059
3115
  ["quotaretryminutes=", "N: minutes before auto-retrying a quota-exhausted auditor (default 60)"],
3060
3116
  ["stuckmax=", "N: consecutive stuck interventions before a loop stops (default 5)"],
3061
3117
  ["stats", "per-project ledger rollups: /glla stats [json|premature|project=<path>]"],
3118
+ ["audits", "audit-log browser: /glla audits [N|full] — recent verdicts from .pi-glla/audits.jsonl"],
3062
3119
  ["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
3063
3120
  ["project", "write a project override: /glla project key=value"],
3064
3121
  ]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.25.3",
3
+ "version": "0.25.4",
4
4
  "description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",