pi-goal-list-loop-audit 0.25.6 → 0.26.0

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.
@@ -60,6 +60,9 @@ export interface Settings {
60
60
  * override without the model pin so subagents share the session model and
61
61
  * its quota; "agent-default" restores upstream behavior. Applies to NEW
62
62
  * sessions (pi-subagents registers agents at session start). */
63
+ /** v0.26.0: reviewer (post-completion follow-up enqueuer) config —
64
+ * project-scoped; see extensions/reviewer.ts DEFAULT_REVIEWER_CONFIG. */
65
+ reviewer?: Record<string, unknown>;
63
66
  subagentModelStrategy?: SubagentModelStrategy;
64
67
  /** v0.24.6: per-agent-type model pin, e.g. { "Explore": "minimax/MiniMax-M3" }.
65
68
  * Always wins over subagentModelStrategy — the managed override is written
@@ -102,6 +102,13 @@ import {
102
102
  settingsProvenance,
103
103
  type Settings,
104
104
  } from "../goal-settings.js";
105
+ import {
106
+ DEFAULT_REVIEWER_CONFIG,
107
+ resolveReviewerConfig,
108
+ reviewerMenuOptions,
109
+ runReviewer,
110
+ type ReviewerConfig,
111
+ } from "../reviewer.js";
105
112
  import {
106
113
  discoverGllaProjects,
107
114
  parseLedgerEntries,
@@ -517,7 +524,73 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
517
524
  // (v0.2.0 bug: bare /list next silently consumed TWO items, found by the
518
525
  // pick-any-item verification in v0.10.0).
519
526
  if (goal.policy === "list" && status === "complete") {
520
- activateNextListItem(ctx);
527
+ const advanced = activateNextListItem(ctx);
528
+ // v0.26.0: the queue just EMPTIED on a completion → list-complete.
529
+ if (!advanced) fireReviewer(ctx, { kind: "list", goalId: goal.id, objective: goal.objective, terminal: "goal-complete" });
530
+ return;
531
+ }
532
+ // v0.26.0: a /goal (non-list) reached a terminal state → maybe fire.
533
+ if (goal.policy !== "list") {
534
+ fireReviewer(ctx, { kind: "goal", goalId: goal.id, objective: goal.objective, terminal: status === "complete" ? "goal-complete" : status === "aborted" ? "goal-aborted" : "goal-paused" });
535
+ }
536
+ }
537
+
538
+ /**
539
+ * v0.26.0: bind the reviewer to the live session. Sources for finding
540
+ * extraction: the archived goal markdown + its audit reports + the
541
+ * durable audit log entries for this goal. List items are enqueued via
542
+ * the ONE enqueue path; /goal proposals go through the agent (which
543
+ * calls propose_goal_draft → the user's Confirm dialog).
544
+ */
545
+ function fireReviewer(
546
+ ctx: ExtensionContext,
547
+ source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
548
+ opts: { manual?: boolean } = {},
549
+ ): void {
550
+ try {
551
+ const settings = loadSettings(ctx.cwd);
552
+ const config = resolveReviewerConfig(settings.reviewer as Partial<ReviewerConfig> | undefined);
553
+ const sources: Array<{ name: string; text: string }> = [];
554
+ try {
555
+ sources.push({ name: "archive", text: fs.readFileSync(archivedGoalPath(ctx.cwd, source.goalId), "utf-8") });
556
+ } catch {
557
+ /* archive md may not exist for manual review of a live goal */
558
+ }
559
+ const auditTexts = readAuditLog(ctx.cwd)
560
+ .filter((e) => e.goalId === source.goalId)
561
+ .map((e) => e.report);
562
+ for (const t of auditTexts) sources.push({ name: "audit", text: t });
563
+ let ledgerEntries: Array<{ type: string; at?: string; value?: any }> = [];
564
+ try {
565
+ ledgerEntries = parseLedgerEntries(fs.readFileSync(ledgerPath(ctx.cwd), "utf-8"));
566
+ } catch {
567
+ /* no ledger yet */
568
+ }
569
+ const outcome = runReviewer(config, source, {
570
+ cwd: ctx.cwd,
571
+ nowMs: Date.now(),
572
+ manual: opts.manual,
573
+ ledgerEntries,
574
+ sources,
575
+ enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer"),
576
+ proposeGoal: (objective, reason) => {
577
+ try {
578
+ extensionApi?.sendUserMessage(
579
+ `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`,
580
+ { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
581
+ );
582
+ } catch {
583
+ /* proposal best-effort */
584
+ }
585
+ },
586
+ notify: (message, level) => ctx.ui.notify(message, level),
587
+ ledger: (type, value) => appendLedger(ctx.cwd, type, value),
588
+ });
589
+ if (!outcome.fired && outcome.suppressedReason && opts.manual) {
590
+ ctx.ui.notify(`Reviewer suppressed: ${outcome.suppressedReason}`, "info");
591
+ }
592
+ } catch (err) {
593
+ ctx.ui.notify(`Reviewer failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`, "warning");
521
594
  }
522
595
  }
523
596
 
@@ -2766,6 +2839,73 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2766
2839
  }
2767
2840
  }
2768
2841
 
2842
+ /** v0.26.0: /review <archived-goal-id> — manual reviewer invocation. */
2843
+ async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
2844
+ const id = args.trim();
2845
+ if (!id) {
2846
+ ctx.ui.notify("Usage: /review <goal-id> — see /goal archive for ids.", "info");
2847
+ return;
2848
+ }
2849
+ // Resolve the id against the archive (suffix match allowed).
2850
+ let goalId = id;
2851
+ let objective = "(archived goal)";
2852
+ try {
2853
+ const files = fs.readdirSync(archiveDir(ctx.cwd)).filter((f) => f.endsWith(".md"));
2854
+ const match = files.find((f) => f === `${id}.md`) ?? files.find((f) => f.includes(id));
2855
+ if (!match) {
2856
+ ctx.ui.notify(`No archived goal matching "${id}". /goal archive lists them.`, "warning");
2857
+ return;
2858
+ }
2859
+ goalId = match.replace(/\.md$/, "");
2860
+ const md = fs.readFileSync(path.join(archiveDir(ctx.cwd), match), "utf-8");
2861
+ const objMatch = md.match(/## Objective\n\n> ([\s\S]*?)(?:\n\n|$)/);
2862
+ if (objMatch) objective = objMatch[1]!.replace(/\n/g, " ").slice(0, 300);
2863
+ } catch {
2864
+ ctx.ui.notify(`No archive found for ${id}.`, "warning");
2865
+ return;
2866
+ }
2867
+ fireReviewer(ctx, { kind: "goal", goalId, objective, terminal: "goal-complete" }, { manual: true });
2868
+ }
2869
+
2870
+ /** v0.26.0: /glla reviewer — the reviewer config menu (project-scoped). */
2871
+ async function cmdReviewerSettings(ctx: ExtensionContext): Promise<void> {
2872
+ if (!ctx.hasUI) {
2873
+ const cfg = resolveReviewerConfig(loadSettings(ctx.cwd).reviewer as Partial<ReviewerConfig> | undefined);
2874
+ ctx.ui.notify(`reviewer (project): ${JSON.stringify(cfg, null, 2)}`, "info");
2875
+ return;
2876
+ }
2877
+ const load = () => resolveReviewerConfig(loadSettings(ctx.cwd).reviewer as Partial<ReviewerConfig> | undefined);
2878
+ const save = (patch: Partial<ReviewerConfig>) => saveSettings("project", ctx.cwd, { reviewer: { ...load(), ...patch } as Record<string, unknown> });
2879
+ for (;;) {
2880
+ const cfg = load();
2881
+ let choice: string | undefined;
2882
+ try {
2883
+ choice = await ctx.ui.select("Reviewer — post-completion follow-up enqueuer (project settings)", reviewerMenuOptions(cfg));
2884
+ } catch {
2885
+ return;
2886
+ }
2887
+ if (!choice || choice === "Done") return;
2888
+ try {
2889
+ if (choice.startsWith("Enabled")) save({ enabled: !cfg.enabled });
2890
+ else if (choice.startsWith("Leverage mode")) save({ leverageMode: cfg.leverageMode === "fix-without-confirm" ? "confirm-all" : "fix-without-confirm" });
2891
+ else if (choice.startsWith("Fire on goal-complete")) save({ fireOn: cfg.fireOn.includes("goal-complete") ? cfg.fireOn.filter((e) => e !== "goal-complete") : [...cfg.fireOn, "goal-complete"] });
2892
+ else if (choice.startsWith("Fire on list-complete")) save({ fireOn: cfg.fireOn.includes("list-complete") ? cfg.fireOn.filter((e) => e !== "list-complete") : [...cfg.fireOn, "list-complete"] });
2893
+ else if (choice.startsWith("Cascade: audit-on-clean")) save({ cascade: cfg.cascade.includes("fire-audit-on-clean") ? cfg.cascade.filter((c) => c !== "fire-audit-on-clean") : [...cfg.cascade, "fire-audit-on-clean"] });
2894
+ else if (choice.startsWith("Max findings")) {
2895
+ const v = await ctx.ui.input("Max findings per review", "1-50");
2896
+ const n = Number(v?.trim());
2897
+ if (Number.isSafeInteger(n) && n >= 1 && n <= 50) save({ maxFindingsPerReview: n });
2898
+ } else if (choice.startsWith("Max reviews")) {
2899
+ const v = await ctx.ui.input("Max reviewer fires per day", "1-100");
2900
+ const n = Number(v?.trim());
2901
+ if (Number.isSafeInteger(n) && n >= 1 && n <= 100) save({ maxReviewsPerDay: n });
2902
+ }
2903
+ } catch {
2904
+ /* individual save failures are non-fatal */
2905
+ }
2906
+ }
2907
+ }
2908
+
2769
2909
  /**
2770
2910
  * v0.25.2: /glla stats — one command, every project's rollup. Args:
2771
2911
  * (none) markdown table, all discovered projects
@@ -2858,6 +2998,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2858
2998
  cmdAudits(trimmed.slice("audits".length).trim(), ctx);
2859
2999
  return;
2860
3000
  }
3001
+ if (/^reviewer\b/.test(trimmed)) {
3002
+ await cmdReviewerSettings(ctx);
3003
+ return;
3004
+ }
2861
3005
  if (!trimmed) {
2862
3006
  if (ctx.hasUI) {
2863
3007
  await openSettingsUI(ctx);
@@ -3161,10 +3305,15 @@ export default function (pi: ExtensionAPI): void {
3161
3305
  ["stats", "per-project ledger rollups: /glla stats [json|premature|project=<path>]"],
3162
3306
  ["audits", "audit-log browser: /glla audits [N|full] — recent verdicts from .pi-glla/audits.jsonl"],
3163
3307
  ["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
3308
+ ["reviewer", "reviewer config menu (post-completion follow-up enqueuer)"],
3164
3309
  ["project", "write a project override: /glla project key=value"],
3165
3310
  ]),
3166
3311
  handler: settingsHandler,
3167
3312
  });
3313
+ pi.registerCommand("review", {
3314
+ description: "Manually run the reviewer on an archived goal: /review <goal-id> — extracts findings, writes a report to .pi-glla/reviews/, enqueues bug-class fixes to /list, proposes architectural items as /goal. Bypasses the trigger gates (explicit user request).",
3315
+ handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdReview(args, ctx); },
3316
+ });
3168
3317
  pi.registerCommand("list", {
3169
3318
  description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear | /list cancel",
3170
3319
  getArgumentCompletions: completions([
@@ -3173,6 +3322,7 @@ export default function (pi: ExtensionAPI): void {
3173
3322
  ["next", "activate the next item (or /list next <n> for position n)"],
3174
3323
  ["remove", "remove an item: /list remove <n>"],
3175
3324
  ["clear", "empty the list"],
3325
+ ["depth", "queue depth, oldest item age, average item duration"],
3176
3326
  ["cancel", "stop the whole list: abort the active item + drop all waiting"],
3177
3327
  ]),
3178
3328
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
@@ -0,0 +1,276 @@
1
+ // pi-goal-list-loop-audit — v0.26.0
2
+ // extensions/reviewer.ts
3
+ //
4
+ // The Reviewer: post-completion follow-up enqueuer. Fires after a /goal
5
+ // completes or a /list queue empties, extracts findings from the archive
6
+ // + ledger, classifies them by leverage, writes a review report, and
7
+ // cascades: bug/refactor findings → /list items (no Confirm, per the
8
+ // leverage principle), architectural findings → /goal proposal (Confirm),
9
+ // clean completions → audit /goal proposal, strategic-only → notify+idle.
10
+ //
11
+ // Deterministic by design (REVIEWER-DESIGN-2026-07-24: "makes NO new tool
12
+ // calls — purely analytical"). All side effects are injected so tests
13
+ // drive it without a pi host. /loop completions never trigger it.
14
+
15
+ import * as fs from "node:fs";
16
+ import * as path from "node:path";
17
+
18
+ export interface ReviewerConfig {
19
+ enabled: boolean;
20
+ fireOn: Array<"goal-complete" | "list-complete">;
21
+ doNotFireOn: string[];
22
+ cascade: Array<"convert-findings-to-list" | "queue-leftovers" | "fire-audit-on-clean" | "notify-and-idle">;
23
+ auditCadence: string;
24
+ auditScope: string;
25
+ leverageMode: "fix-without-confirm" | "confirm-all";
26
+ confirmOn: string[];
27
+ maxFindingsPerReview: number;
28
+ maxReviewsPerDay: number;
29
+ }
30
+
31
+ export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
32
+ enabled: true,
33
+ fireOn: ["goal-complete", "list-complete"],
34
+ doNotFireOn: ["goal-aborted", "goal-paused"],
35
+ cascade: ["convert-findings-to-list", "queue-leftovers", "fire-audit-on-clean", "notify-and-idle"],
36
+ auditCadence: "every-clean-completion",
37
+ auditScope: "regression-scan",
38
+ leverageMode: "fix-without-confirm",
39
+ confirmOn: ["architectural-decision", "new-dependency", "schema-change"],
40
+ maxFindingsPerReview: 10,
41
+ maxReviewsPerDay: 20,
42
+ };
43
+
44
+ /** Merge a partial project-settings block over the defaults. */
45
+ export function resolveReviewerConfig(block?: Partial<ReviewerConfig>): ReviewerConfig {
46
+ return { ...DEFAULT_REVIEWER_CONFIG, ...(block ?? {}) };
47
+ }
48
+
49
+ export type FindingClass = "bug" | "refactor" | "architectural" | "strategic";
50
+
51
+ export interface Finding {
52
+ text: string;
53
+ source: string;
54
+ class: FindingClass;
55
+ }
56
+
57
+ /** Leverage classification (contract item 5). Order matters: strategic
58
+ * and architectural win over bug/refactor — "should we rewrite this
59
+ * broken schema" is a decision, not a fix. */
60
+ const CLASS_PATTERNS: Array<{ class: FindingClass; re: RegExp }> = [
61
+ { class: "strategic", re: /\bshould we\b|\bdeprecat|ship this\??|strategic/i },
62
+ { class: "architectural", re: /\brewrite\b|new dependency|schema change|architectural|redesign/i },
63
+ { class: "bug", re: /\bTODO\b|\bFIXME\b|\bbug\b|\bissue\b|regression|broken|\bfixme\b/i },
64
+ { class: "refactor", re: /could be cleaner|consider refactoring|duplicat|refactor|left ?out|follow[\s-]?up|deferred/i },
65
+ ];
66
+
67
+ export function classifyFindingText(line: string): FindingClass | undefined {
68
+ const t = line.trim();
69
+ if (t.length < 8) return undefined;
70
+ for (const { class: cls, re } of CLASS_PATTERNS) {
71
+ if (re.test(t)) return cls;
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ /** Scan source texts line-by-line for finding-shaped content. */
77
+ export function extractFindings(sources: Array<{ name: string; text: string }>, max: number): Finding[] {
78
+ const out: Finding[] = [];
79
+ const seen = new Set<string>();
80
+ for (const { name, text } of sources) {
81
+ for (const line of text.split("\n")) {
82
+ const cls = classifyFindingText(line);
83
+ if (!cls) continue;
84
+ const clean = line.trim().replace(/^[-*>\s\[\]x]+/, "").slice(0, 200);
85
+ if (clean.length < 8 || seen.has(clean)) continue;
86
+ seen.add(clean);
87
+ out.push({ text: clean, source: name, class: cls });
88
+ if (out.length >= max) return out;
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ /** Runaway prevention (contract item 6/9): a reviewer fire in the last
95
+ * `windowMs` suppresses re-firing — reviewer-created work completing
96
+ * immediately must not recursively fire the reviewer. */
97
+ export function reviewerFiredRecently(entries: Array<{ type: string; at?: string }>, windowMs: number, nowMs: number): boolean {
98
+ return entries.some((e) => e.type === "reviewer_fired" && e.at !== undefined && nowMs - Date.parse(e.at) < windowMs);
99
+ }
100
+
101
+ /** Per-day cap (contract item 10). */
102
+ export function reviewsToday(entries: Array<{ type: string; at?: string }>, nowMs: number): number {
103
+ const day = new Date(nowMs).toISOString().slice(0, 10);
104
+ return entries.filter((e) => e.type === "reviewer_fired" && e.at?.startsWith(day)).length;
105
+ }
106
+
107
+ export interface ReviewReport {
108
+ goalId: string;
109
+ kind: "goal" | "list";
110
+ objective: string;
111
+ findings: Finding[];
112
+ cascadeStep: string;
113
+ at: string;
114
+ }
115
+
116
+ export function formatReviewReport(r: ReviewReport): string {
117
+ const byClass = (c: FindingClass) => r.findings.filter((f) => f.class === c);
118
+ const section = (title: string, items: Finding[]) =>
119
+ items.length === 0 ? "" : `\n## ${title}\n\n${items.map((f) => `- ${f.text} _(${f.source})_`).join("\n")}\n`;
120
+ return [
121
+ `# Review — ${r.goalId}`,
122
+ "",
123
+ `**Kind**: ${r.kind} · **At**: ${r.at}`,
124
+ "",
125
+ "## Summary",
126
+ "",
127
+ `Completed: ${r.objective.slice(0, 300)}`,
128
+ "",
129
+ `**Cascade step**: ${r.cascadeStep}`,
130
+ "",
131
+ `## Findings (${r.findings.length})`,
132
+ section("Bug-class (enqueued to /list, no Confirm)", byClass("bug")),
133
+ section("Refactor-class (enqueued to /list, no Confirm)", byClass("refactor")),
134
+ section("Architectural-class (proposed as /goal, Confirm required)", byClass("architectural")),
135
+ section("Strategic-class (notify only)", byClass("strategic")),
136
+ r.findings.length === 0 ? "\n(none — completion looks clean)\n" : "",
137
+ ].join("\n");
138
+ }
139
+
140
+ export function writeReviewReport(cwd: string, report: ReviewReport): string {
141
+ const dir = path.join(cwd, ".pi-glla", "reviews");
142
+ fs.mkdirSync(dir, { recursive: true });
143
+ const ts = report.at.replace(/[:.]/g, "-");
144
+ const file = path.join(dir, `${report.goalId}-${ts}.md`);
145
+ fs.writeFileSync(file, formatReviewReport(report));
146
+ return file;
147
+ }
148
+
149
+ /** Injectable side effects — goal.ts binds these to the live session. */
150
+ export interface ReviewerDeps {
151
+ cwd: string;
152
+ nowMs: number;
153
+ /** /review <id> manual invocation — bypasses fireOn/doNotFireOn gates,
154
+ * the refire window, and the day cap (the user asked explicitly). */
155
+ manual?: boolean;
156
+ ledgerEntries: Array<{ type: string; at?: string; value?: any }>;
157
+ /** Source texts for finding extraction (archive md, audit reports). */
158
+ sources: Array<{ name: string; text: string }>;
159
+ enqueueListItems: (objectives: string[]) => void;
160
+ proposeGoal: (objective: string, reason: string) => void;
161
+ notify: (message: string, level: "info" | "warning") => void;
162
+ ledger: (type: string, value: Record<string, unknown>) => void;
163
+ }
164
+
165
+ export interface ReviewerOutcome {
166
+ fired: boolean;
167
+ suppressedReason?: string;
168
+ report?: ReviewReport;
169
+ reportPath?: string;
170
+ enqueued: number;
171
+ proposed: number;
172
+ }
173
+
174
+ export const REVIEWER_REFIRE_WINDOW_MS = 5 * 60_000;
175
+
176
+ /** The reviewer lifecycle (contract item 1). */
177
+ export function runReviewer(
178
+ config: ReviewerConfig,
179
+ source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
180
+ deps: ReviewerDeps,
181
+ ): ReviewerOutcome {
182
+ const none = (suppressedReason: string): ReviewerOutcome => ({ fired: false, suppressedReason, enqueued: 0, proposed: 0 });
183
+ if (!config.enabled && !deps.manual) return none("reviewer disabled");
184
+ const event = source.kind === "goal" ? `${source.terminal}` : "list-complete";
185
+ if (!deps.manual) {
186
+ if (config.doNotFireOn.includes(event)) return none(`doNotFireOn: ${event}`);
187
+ if (source.kind === "goal" && source.terminal !== "goal-complete") return none(`not a completion: ${source.terminal}`);
188
+ if (!config.fireOn.includes(source.kind === "goal" ? "goal-complete" : "list-complete")) return none("fireOn excludes this event");
189
+ if (reviewerFiredRecently(deps.ledgerEntries, REVIEWER_REFIRE_WINDOW_MS, deps.nowMs)) {
190
+ deps.ledger("reviewer_suppressed", { reason: "refire-window", goalId: source.goalId });
191
+ return none("reviewer fired within the last 5 minutes (runaway prevention)");
192
+ }
193
+ const today = reviewsToday(deps.ledgerEntries, deps.nowMs);
194
+ if (today >= config.maxReviewsPerDay) {
195
+ deps.ledger("reviewer_suppressed", { reason: "day-cap", count: today, cap: config.maxReviewsPerDay, goalId: source.goalId });
196
+ return none(`day cap reached (${today}/${config.maxReviewsPerDay})`);
197
+ }
198
+ }
199
+
200
+ const findings = extractFindings(deps.sources, config.maxFindingsPerReview);
201
+ const bugs = findings.filter((f) => f.class === "bug" || f.class === "refactor");
202
+ const architectural = findings.filter((f) => f.class === "architectural");
203
+ const strategic = findings.filter((f) => f.class === "strategic");
204
+
205
+ let enqueued = 0;
206
+ let proposed = 0;
207
+ let cascadeStep = "notify-and-idle";
208
+
209
+ // Cascade: findings → list items (leverage: fix-without-confirm).
210
+ const convertStep = source.kind === "goal" ? "convert-findings-to-list" : "queue-leftovers";
211
+ if (bugs.length > 0 && config.cascade.includes(convertStep)) {
212
+ deps.enqueueListItems(bugs.map((f) => f.text));
213
+ enqueued = bugs.length;
214
+ cascadeStep = convertStep;
215
+ }
216
+ // Architectural findings → /goal proposal WITH Confirm.
217
+ if (architectural.length > 0) {
218
+ deps.proposeGoal(
219
+ architectural.map((f) => f.text).join("; "),
220
+ `reviewer found ${architectural.length} architectural-class finding(s) — needs your Confirm`,
221
+ );
222
+ proposed += architectural.length;
223
+ cascadeStep = "propose-goal";
224
+ }
225
+ // Clean completion → audit /goal (opt-in cascade step).
226
+ if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")) {
227
+ deps.proposeGoal(
228
+ `Post-completion regression scan after ${source.goalId} (${config.auditScope})`,
229
+ "reviewer: completion looks clean — firing the audit step",
230
+ );
231
+ proposed++;
232
+ cascadeStep = "fire-audit-on-clean";
233
+ }
234
+
235
+ const report: ReviewReport = {
236
+ goalId: source.goalId,
237
+ kind: source.kind,
238
+ objective: source.objective,
239
+ findings,
240
+ cascadeStep,
241
+ at: new Date(deps.nowMs).toISOString(),
242
+ };
243
+ const reportPath = writeReviewReport(deps.cwd, report);
244
+ deps.ledger("reviewer_fired", {
245
+ goalId: source.goalId,
246
+ kind: source.kind,
247
+ findings: findings.length,
248
+ enqueued,
249
+ proposed,
250
+ cascadeStep,
251
+ report: path.relative(deps.cwd, reportPath),
252
+ });
253
+ deps.notify(
254
+ `Reviewer: ${findings.length} finding(s) — ${enqueued} enqueued to /list, ${proposed} proposed as /goal (${cascadeStep}). Report: ${path.relative(deps.cwd, reportPath)}`,
255
+ "info",
256
+ );
257
+ if (strategic.length > 0) {
258
+ deps.notify(`Reviewer: ${strategic.length} strategic finding(s) need YOUR call — see the report's Strategic section.`, "warning");
259
+ }
260
+ return { fired: true, report, reportPath, enqueued, proposed };
261
+ }
262
+
263
+ /** /glla reviewer menu options, derived from the config — extracted so
264
+ * the menu surface is unit-testable without a pi host. */
265
+ export function reviewerMenuOptions(cfg: ReviewerConfig): string[] {
266
+ return [
267
+ `Enabled — ${cfg.enabled ? "ON" : "OFF"}`,
268
+ `Leverage mode — ${cfg.leverageMode} (bug/refactor findings)`,
269
+ `Fire on goal-complete — ${cfg.fireOn.includes("goal-complete") ? "ON" : "OFF"}`,
270
+ `Fire on list-complete — ${cfg.fireOn.includes("list-complete") ? "ON" : "OFF"}`,
271
+ `Cascade: audit-on-clean — ${cfg.cascade.includes("fire-audit-on-clean") ? "ON" : "OFF"}`,
272
+ `Max findings per review — ${cfg.maxFindingsPerReview}`,
273
+ `Max reviews per day — ${cfg.maxReviewsPerDay}`,
274
+ "Done",
275
+ ];
276
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.25.6",
3
+ "version": "0.26.0",
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",