pi-goal-list-loop-audit 0.25.2 → 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;
@@ -922,3 +937,211 @@ export function lastShippedAtMs(cwd: string): number | null {
922
937
  }
923
938
  return best;
924
939
  }
940
+
941
+ // =================================================================
942
+ // v0.25.3: list-philosophy rework — cross-mode recommendation +
943
+ // /list depth rollups
944
+ // =================================================================
945
+
946
+ /**
947
+ * Detect a mode mismatch between what the user described and the mode
948
+ * they invoked. Returns a recommendation string for the drafting
949
+ * injection, or undefined when the seed fits the mode.
950
+ *
951
+ * The canonical failure this prevents (real incidents 2026-07-24):
952
+ * "close 76 weak points, one commit each" folded into ONE wrapper goal
953
+ * with an aggregate "≥ 76 commits" contract → auto-committer squash →
954
+ * literal count fails → auditor correctly disapproves finished work.
955
+ */
956
+ export function crossRecommendMode(seed: string, mode: "goal" | "list"): string | undefined {
957
+ const s = seed.trim();
958
+ if (!s) return undefined;
959
+ // Aggregate seed: "N items/findings/weak points/screens/todos/fixes"
960
+ // (+ "each" / "one commit" flavor) — the wrapper-goal anti-pattern.
961
+ const aggregate = s.match(/(\d+)\s*(?:items?|findings?|weak[\s-]points?|screens?|todos?|fix(?:es)?|tasks?|issues?)/i);
962
+ const n = aggregate ? Number(aggregate[1]) : 0;
963
+ if (n >= 5) {
964
+ return (
965
+ `[MODE CHECK — this seed names ${n} discrete items${/each|one commit|as a tasklist/i.test(s) ? ' ("each"/"tasklist" phrasing)' : ""}. ` +
966
+ `Do NOT fold them into ONE wrapper ${mode === "list" ? "list item" : "goal"} with an aggregate contract ("≥ ${n} commits") — ` +
967
+ `the auto-committer squashes commits and the literal count fails even when the work is done (the 2026-07-24 76-weak-points incident). ` +
968
+ `Propose ${n} SHORT /list items via propose_goal_draft items[] — each item closes exactly ONE finding with its own per-item contract. ` +
969
+ `Any aggregate re-audit becomes the FINAL /goal, not the first.]`
970
+ );
971
+ }
972
+ if (mode === "list") {
973
+ if (/\b(?:take|takes|taking)\s+(?:a\s+)?(?:few|several|\d+)\s+hours?\b/i.test(s) || /\b(?:multi-hour|deep (?:audit|research|dive)|all day|over the weekend)\b/i.test(s)) {
974
+ return (
975
+ `[MODE CHECK — this seed sounds like multi-hour work. /list items are SHORT (minutes, one focused change). ` +
976
+ `Either break it into ≤ 30-minute items via items[], or tell the user this fits /goal better — one big task, ` +
977
+ `ends on auditor approval. If the user overrides ("as a list item anyway"), comply.]`
978
+ );
979
+ }
980
+ } else {
981
+ if (/^(?:fix|typo|rename|bump|remove|delete|clean ?up|tweak)\b/i.test(s) && s.length < 80 && !/\bhours?\b|\ball\b|\bevery\b|\beach\b/i.test(s)) {
982
+ return (
983
+ `[MODE CHECK — this seed sounds like a five-minute cleanup. A full audited /goal may be overkill; ` +
984
+ `suggest /list (queue of short items) or the tasklist plugin. If the user wants the audit anyway, comply.]`
985
+ );
986
+ }
987
+ }
988
+ return undefined;
989
+ }
990
+
991
+ /** /list depth rollup: how deep is the queue, how stale is the head,
992
+ * how long do items actually take (from archived list-policy goals). */
993
+ export interface ListDepthStats {
994
+ queueDepth: number;
995
+ oldestItemId?: string;
996
+ oldestAgeMs?: number;
997
+ avgDurationMs?: number;
998
+ durationSamples: number;
999
+ }
1000
+
1001
+ export function computeListDepth(
1002
+ queue: Array<{ id: string; addedAt: string }>,
1003
+ ledgerEntries: Array<{ type: string; value?: any }>,
1004
+ nowMs: number,
1005
+ ): ListDepthStats {
1006
+ let oldestItemId: string | undefined;
1007
+ let oldestAgeMs: number | undefined;
1008
+ for (const item of queue) {
1009
+ const added = Date.parse(item.addedAt);
1010
+ if (Number.isNaN(added)) continue;
1011
+ const age = nowMs - added;
1012
+ if (oldestAgeMs === undefined || age > oldestAgeMs) {
1013
+ oldestAgeMs = age;
1014
+ oldestItemId = item.id;
1015
+ }
1016
+ }
1017
+ // Average item duration from the ledger's list-policy goals (most
1018
+ // recent 10 with both timestamps).
1019
+ const finals = new Map<string, { createdAt?: string; updatedAt?: string; policy?: string; status?: string }>();
1020
+ for (const e of ledgerEntries) {
1021
+ if (e.type === "state" && e.value?.goal?.id) {
1022
+ finals.set(String(e.value.goal.id), e.value.goal);
1023
+ }
1024
+ }
1025
+ const durations: number[] = [];
1026
+ for (const g of finals.values()) {
1027
+ if (g.policy !== "list") continue;
1028
+ if (g.status !== "complete" && g.status !== "archived") continue;
1029
+ const c = Date.parse(g.createdAt ?? "");
1030
+ const u = Date.parse(g.updatedAt ?? "");
1031
+ if (Number.isNaN(c) || Number.isNaN(u) || u < c) continue;
1032
+ durations.push(u - c);
1033
+ }
1034
+ const recent = durations.slice(-10);
1035
+ const avgDurationMs = recent.length > 0 ? Math.round(recent.reduce((a, b) => a + b, 0) / recent.length) : undefined;
1036
+ return {
1037
+ queueDepth: queue.length,
1038
+ oldestItemId,
1039
+ oldestAgeMs,
1040
+ avgDurationMs,
1041
+ durationSamples: recent.length,
1042
+ };
1043
+ }
1044
+
1045
+ function fmtAge(ms: number): string {
1046
+ const mins = Math.round(ms / 60000);
1047
+ if (mins < 60) return `${mins}m`;
1048
+ const hours = Math.round(mins / 60);
1049
+ if (hours < 24) return `${hours}h`;
1050
+ return `${Math.floor(hours / 24)}d ${Math.round(hours % 24)}h`;
1051
+ }
1052
+
1053
+ /** Contract item 7's exact headline format, then detail lines. */
1054
+ export function formatListDepth(stats: ListDepthStats): string {
1055
+ const oldest = stats.oldestAgeMs !== undefined ? fmtAge(stats.oldestAgeMs) : "—";
1056
+ const avg = stats.avgDurationMs !== undefined ? fmtAge(stats.avgDurationMs) : "—";
1057
+ const lines = [`queue depth: ${stats.queueDepth} · oldest: ${oldest} · avg duration: ${avg}`];
1058
+ if (stats.oldestItemId) lines.push(`oldest item: ${fmtAge(stats.oldestAgeMs!)} (id ${stats.oldestItemId})`);
1059
+ if (stats.durationSamples > 0) lines.push(`avg item duration: ${fmtAge(stats.avgDurationMs!)} (from last ${stats.durationSamples} archived)`);
1060
+ return lines.join("\n");
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,6 +39,15 @@ import {
39
39
  isFullAuditObjective,
40
40
  lastShippedAtMs,
41
41
  resolveEffectiveAggressiveSettings,
42
+ appendAuditLog,
43
+ computeListDepth,
44
+ formatAuditLog,
45
+ readAuditLog,
46
+ stripThinkBlocks,
47
+ type AuditLogEntry,
48
+ ledgerPath,
49
+ crossRecommendMode,
50
+ formatListDepth,
42
51
  shouldSuppressHeartbeatForRecentShip,
43
52
  mergeSettings,
44
53
  parseListImport,
@@ -92,6 +101,7 @@ import {
92
101
  } from "../goal-settings.js";
93
102
  import {
94
103
  discoverGllaProjects,
104
+ parseLedgerEntries,
95
105
  filterPremature,
96
106
  formatRollupJson,
97
107
  formatRollupTable,
@@ -562,7 +572,15 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
562
572
  if (target === "list") {
563
573
  tmpl = tmpl.replace(
564
574
  "[GOAL DRAFTING]",
565
- "[GOAL DRAFTING — the confirmed goal goes into the /list LIST, it does not activate immediately. If the user wants MANY things added at once (a plan, a checklist, 'these 50 tasks'), propose them ALL AT ONCE with the items[] parameter — one Confirm for the whole batch, never 50 separate proposals.]",
575
+ "[LIST DRAFTING — the confirmed item goes into the /list LIST, it does not activate immediately. " +
576
+ "/list items are SHORT tasks, not multi-hour objectives: each item should fit comfortably in a single agent run " +
577
+ "(minutes of work, a single focused change). The list's long-running property is QUEUE DEPTH — hundreds of short " +
578
+ "items activated one at a time over days/weeks — never any single item's scope. " +
579
+ "If the user describes work that would take hours, propose breaking it into multiple /list items, or suggest /goal " +
580
+ "for the big version. When the user has many items to enqueue at once ('queue these 50 audits'), propose them ALL AT " +
581
+ "ONCE with the items[] parameter — one Confirm for the whole batch, never 50 separate proposals. Each items[] entry " +
582
+ "is still a SHORT task — never an aggregate wrapper ('land all N findings' with a '≥N commits' contract is the " +
583
+ "canonical anti-pattern: the auto-committer squashes, the count fails, the auditor disapproves finished work).]",
566
584
  );
567
585
  }
568
586
  } catch {
@@ -573,6 +591,12 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
573
591
  // is blocked until the user has replied at least once (see message_start).
574
592
  if (seed) {
575
593
  tmpl = buildSeedGrillMessage(tmpl, seed, tool);
594
+ // v0.25.3: cross-mode recommendation — catch wrapper-goal seeds and
595
+ // mode mismatches BEFORE the draft crystallizes.
596
+ if (target === "goal" || target === "list") {
597
+ const xr = crossRecommendMode(seed, target);
598
+ if (xr) tmpl += `\n\n${xr}`;
599
+ }
576
600
  }
577
601
  try {
578
602
  extensionApi?.sendUserMessage(tmpl, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
@@ -852,6 +876,20 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
852
876
  const sub = (parts[0] ?? "").toLowerCase();
853
877
  const rest = args.trim().slice(sub.length).trim();
854
878
 
879
+ if (sub === "depth") {
880
+ // v0.25.3: long-running state at a glance — queue depth, oldest item
881
+ // age, average item duration from archived list-policy goals.
882
+ let entries: Array<{ type: string; value?: any }> = [];
883
+ try {
884
+ entries = parseLedgerEntries(fs.readFileSync(ledgerPath(ctx.cwd), "utf-8"));
885
+ } catch {
886
+ /* no ledger yet */
887
+ }
888
+ const stats = computeListDepth(listQueue(), entries, Date.now());
889
+ ctx.ui.notify(`/list depth: ${formatListDepth(stats)}`, "info");
890
+ return;
891
+ }
892
+
855
893
  if (sub === "resume") {
856
894
  // Resume the list's head. The head activates AS the active goal, so this
857
895
  // is the same motion as /goal resume — named for the surface the user is
@@ -1664,7 +1702,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1664
1702
  ctx.ui.notify(`Auditor running (isolated session, model: ${via ?? "setting"})…`, "info");
1665
1703
  // Esc during the audit aborts this tool's signal → threaded into the
1666
1704
  // auditor session, which aborts cleanly and returns "Auditor aborted."
1667
- latestAuditProgress = { label: "starting" };
1705
+ latestAuditProgress = { label: "starting", lastEventAt: Date.now() };
1668
1706
  const result = await runGoalCompletionAuditor({
1669
1707
  ctx,
1670
1708
  goal: state.goal,
@@ -1678,6 +1716,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1678
1716
  currentTool: progress.currentTool,
1679
1717
  label: progress.label,
1680
1718
  elapsedMs: progress.elapsedMs,
1719
+ lastEventAt: Date.now(),
1681
1720
  };
1682
1721
  refreshUI(ctx);
1683
1722
  },
@@ -1690,6 +1729,11 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1690
1729
  const auditorRan = result.output.trim().length > 0;
1691
1730
  const history = state.goal.auditHistory ?? [];
1692
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;
1693
1737
  history.push({
1694
1738
  at: nowIso(),
1695
1739
  approved: result.approved,
@@ -1698,13 +1742,36 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1698
1742
  impossibleReason: result.impossibleReason,
1699
1743
  model: result.model,
1700
1744
  thinkingLevel: result.thinkingLevel,
1701
- report: result.output,
1745
+ report: cleanOutput,
1702
1746
  error: result.error,
1703
1747
  regressionShieldPassed: result.regressionShieldPassed,
1704
1748
  regressionShieldMissing: result.regressionShieldMissing,
1705
1749
  });
1706
1750
  // Cap history — 39 infra errors taught us unbounded growth is real.
1707
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
+ });
1708
1775
  }
1709
1776
 
1710
1777
  // Escape hatch: the user aborted the audit (Esc). Offer the explicit
@@ -1883,7 +1950,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1883
1950
  const auditFeedbackIsFull = auditFeedbackChars === 0 || result.output.length <= auditFeedbackChars;
1884
1951
  const auditFeedbackLabel = auditFeedbackIsFull
1885
1952
  ? "full report"
1886
- : `first ${auditFeedbackChars} chars`;
1953
+ : `last ${auditFeedbackChars} chars (Required-fixes tail)`;
1887
1954
  const auditFeedbackTruncationHint = auditFeedbackIsFull
1888
1955
  ? ""
1889
1956
  : `\n\nReport truncated at the configured limit. /goal status shows the full report; change future feedback with /glla auditfeedbackchars=N (0 = full report).`;
@@ -2711,6 +2778,24 @@ function cmdStats(args: string, ctx: ExtensionContext): void {
2711
2778
  ctx.ui.notify(`glla stats — ${rollups.length} project(s)${prematureOnly ? " (premature filter)" : ""}\n${out}`, "info");
2712
2779
  }
2713
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
+
2714
2799
  async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2715
2800
  // The plugin's ONE config surface — global by default, rarely opened.
2716
2801
  // /glla show effective values + where each comes from
@@ -2729,6 +2814,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2729
2814
  cmdStats(trimmed.slice("stats".length).trim(), ctx);
2730
2815
  return;
2731
2816
  }
2817
+ if (/^audits\b/.test(trimmed)) {
2818
+ cmdAudits(trimmed.slice("audits".length).trim(), ctx);
2819
+ return;
2820
+ }
2732
2821
  if (!trimmed) {
2733
2822
  if (ctx.hasUI) {
2734
2823
  await openSettingsUI(ctx);
@@ -3026,6 +3115,7 @@ export default function (pi: ExtensionAPI): void {
3026
3115
  ["quotaretryminutes=", "N: minutes before auto-retrying a quota-exhausted auditor (default 60)"],
3027
3116
  ["stuckmax=", "N: consecutive stuck interventions before a loop stops (default 5)"],
3028
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"],
3029
3119
  ["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
3030
3120
  ["project", "write a project override: /glla project key=value"],
3031
3121
  ]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.25.2",
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",
@@ -1,5 +1,58 @@
1
1
  # Goal drafting — pi-goal-list-loop-audit
2
2
 
3
+ # Long-running philosophy
4
+
5
+ The three modes are NOT redundant — each has a distinct source of
6
+ long-running-ness:
7
+
8
+ | Mode | Item size | Long-running by | Typical lifetime |
9
+ |---|---|---|---|
10
+ | `/goal` | ONE big multi-hour task | Scope | Hours |
11
+ | `/list` | N items × short (minutes each) | Queue depth | Hours → days → weeks |
12
+ | `/loop` | 1 metric × infinite polish | Bounds | Until plateau/stop/finish |
13
+
14
+ Pick the mode by where the long-running property lives, then draft for
15
+ THAT mode:
16
+
17
+ - **`/goal` is the multi-hour mode.** Its long-running property is SCOPE:
18
+ one big task that spans multiple agent runs, requires deep research, or
19
+ would take hours end-to-end. It ends only when the auditor approves the
20
+ verification contract. If the work is short enough to fit in a single
21
+ agent run (a focused change, a single audit, a small refactor), prefer
22
+ `/list` instead.
23
+ - **`/list` items are short tasks, not multi-hour objectives.** Each item
24
+ should fit comfortably in a single agent run — minutes of work, a
25
+ single focused change. The list's long-running property is QUEUE DEPTH:
26
+ hundreds of short items, activated one at a time, pushed over days or
27
+ weeks. If the user describes work that would take hours, break it up
28
+ into multiple `/list` items — or suggest `/goal` for the big version.
29
+ - **`/loop` is metric-driven infinite polish** — its long-running
30
+ property is open bounds; it ends on plateau, bounds, `/loop stop`, or
31
+ `/loop finish`.
32
+
33
+ ## Cross-recommend `/goal` ↔ `/list`
34
+
35
+ While drafting, watch the seed's shape and recommend the right mode:
36
+
37
+ - **Aggregate seeds belong in `/list` as N items, never as ONE wrapper
38
+ goal.** The canonical failure (real incidents, 2026-07-24): "close
39
+ every weak point in X.md (76 items, one commit each)" or "land all 40
40
+ findings as a tasklist" got folded into ONE goal with an aggregate
41
+ contract ("≥ 76 commits") — the auto-committer squashed commits, the
42
+ literal count failed, the auditor correctly disapproved finished work.
43
+ When the seed contains "N items/findings/weak points/screens" + "each"
44
+ + "one commit", propose N SHORT items via `propose_goal_draft`
45
+ `items[]`, each with its OWN per-item contract ("close IMP-AUD3-68:
46
+ Map.svelte:1528 missing role" — impossible to squash), and let any
47
+ re-audit pass be the FINAL `/goal`, not the first.
48
+ - **Multi-hour seeds in `/list`** ("this will take hours", "deep audit",
49
+ "research all 22 screens"): suggest `/goal` — or break the work into
50
+ ≤ 30-minute items.
51
+ - **Five-minute seeds in `/goal`** ("fix typo in X", "bump version"):
52
+ suggest `/list` or the tasklist plugin instead — a full audited goal is
53
+ overkill.
54
+ - The user can always override ("no, as a list item anyway") — comply.
55
+
3
56
  `[GOAL DRAFTING]`
4
57
 
5
58
  The user invoked `/goal` with no objective. Your job is to turn their vague
@@ -1,5 +1,16 @@
1
1
  # Loop drafting — pi-goal-list-loop-audit
2
2
 
3
+ # Long-running philosophy
4
+
5
+ `/loop` is the metric-driven infinite-polish mode. Its long-running
6
+ property is BOUNDS: one metric, polished forever, ending only on plateau,
7
+ bounds, `/loop stop`, or `/loop finish`. The other modes long-run
8
+ differently — `/goal` by scope (one big multi-hour task), `/list` by
9
+ queue depth (hundreds of short items, minutes each). If the user's work
10
+ is a single big task with a done state, that's `/goal`; if it's many
11
+ small tasks, that's `/list`; only true open-ended metric improvement
12
+ belongs here.
13
+
3
14
  `[LOOP DRAFTING]`
4
15
 
5
16
  The user invoked `/loop` with no arguments. Your job is to turn their rough