pi-goal-list-loop-audit 0.26.1 → 0.26.3

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.
@@ -583,11 +583,12 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
583
583
  function fireReviewer(
584
584
  ctx: ExtensionContext,
585
585
  source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
586
- opts: { manual?: boolean } = {},
586
+ opts: { manual?: boolean; mode?: "default" | "auto" | "report" } = {},
587
587
  ): void {
588
588
  try {
589
589
  const settings = loadSettings(ctx.cwd);
590
590
  const config = resolveReviewerConfig(settings.reviewer as Partial<ReviewerConfig> | undefined);
591
+ if (opts.mode) config.mode = opts.mode;
591
592
  const sources: Array<{ name: string; text: string }> = [];
592
593
  try {
593
594
  sources.push({ name: "archive", text: fs.readFileSync(archivedGoalPath(ctx.cwd, source.goalId), "utf-8") });
@@ -2884,9 +2885,16 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2884
2885
 
2885
2886
  /** v0.26.0: /review <archived-goal-id> — manual reviewer invocation. */
2886
2887
  async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
2887
- const id = args.trim();
2888
+ const parts = args.trim().split(/\s+/).filter(Boolean);
2889
+ const id = parts[0] ?? "";
2890
+ const modeArg = parts[1];
2891
+ const mode = modeArg === "auto" || modeArg === "report" || modeArg === "default" ? modeArg : undefined;
2892
+ if (modeArg && !mode) {
2893
+ ctx.ui.notify(`Unknown mode "${modeArg}" — use auto | report | default.`, "warning");
2894
+ return;
2895
+ }
2888
2896
  if (!id) {
2889
- ctx.ui.notify("Usage: /review <goal-id> — see /goal archive for ids.", "info");
2897
+ ctx.ui.notify("Usage: /review <goal-id> [auto|report|default] — see /goal archive for ids.", "info");
2890
2898
  return;
2891
2899
  }
2892
2900
  // Resolve the id against the archive (suffix match allowed).
@@ -2907,7 +2915,7 @@ async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
2907
2915
  ctx.ui.notify(`No archive found for ${id}.`, "warning");
2908
2916
  return;
2909
2917
  }
2910
- fireReviewer(ctx, { kind: "goal", goalId, objective, terminal: "goal-complete" }, { manual: true });
2918
+ fireReviewer(ctx, { kind: "goal", goalId, objective, terminal: "goal-complete" }, { manual: true, mode });
2911
2919
  }
2912
2920
 
2913
2921
  /** v0.26.0: /glla reviewer — the reviewer config menu (project-scoped). */
@@ -2930,6 +2938,7 @@ async function cmdReviewerSettings(ctx: ExtensionContext): Promise<void> {
2930
2938
  if (!choice || choice === "Done") return;
2931
2939
  try {
2932
2940
  if (choice.startsWith("Enabled")) save({ enabled: !cfg.enabled });
2941
+ else if (choice.startsWith("Mode")) save({ mode: cfg.mode === "default" ? "auto" : cfg.mode === "auto" ? "report" : "default" });
2933
2942
  else if (choice.startsWith("Leverage mode")) save({ leverageMode: cfg.leverageMode === "fix-without-confirm" ? "confirm-all" : "fix-without-confirm" });
2934
2943
  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"] });
2935
2944
  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"] });
@@ -3368,7 +3377,7 @@ export default function (pi: ExtensionAPI): void {
3368
3377
  handler: settingsHandler,
3369
3378
  });
3370
3379
  pi.registerCommand("review", {
3371
- 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).",
3380
+ description: "Manually run the reviewer on an archived goal: /review <goal-id> [auto|report|default] — extracts findings, writes a report to .pi-glla/reviews/, cascades per the mode (auto = auto-loop, no Confirms). Bypasses the trigger gates (explicit user request).",
3372
3381
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdReview(args, ctx); },
3373
3382
  });
3374
3383
  pi.registerCommand("list", {
@@ -15,8 +15,15 @@
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
17
 
18
+ export type ReviewerMode = "default" | "auto" | "report";
19
+
18
20
  export interface ReviewerConfig {
19
21
  enabled: boolean;
22
+ /** v0.26.2: default = Confirm-gated cascade; auto = auto-loop — every
23
+ * finding class (incl. architectural) and the clean-completion audit
24
+ * become /list items with zero Confirms (strategic stays notify-only —
25
+ * decisions never auto-fire); report = write the report + notify only. */
26
+ mode: ReviewerMode;
20
27
  fireOn: Array<"goal-complete" | "list-complete">;
21
28
  doNotFireOn: string[];
22
29
  cascade: Array<"convert-findings-to-list" | "queue-leftovers" | "fire-audit-on-clean" | "notify-and-idle">;
@@ -30,6 +37,7 @@ export interface ReviewerConfig {
30
37
 
31
38
  export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
32
39
  enabled: true,
40
+ mode: "default",
33
41
  fireOn: ["goal-complete", "list-complete"],
34
42
  doNotFireOn: ["goal-aborted", "goal-paused"],
35
43
  cascade: ["convert-findings-to-list", "queue-leftovers", "fire-audit-on-clean", "notify-and-idle"],
@@ -57,16 +65,28 @@ export interface Finding {
57
65
  /** Leverage classification (contract item 5). Order matters: strategic
58
66
  * and architectural win over bug/refactor — "should we rewrite this
59
67
  * broken schema" is a decision, not a fix. */
68
+ // v0.26.3: the bare words "architectural"/"strategic" are REMOVED — they
69
+ // self-matched the reviewer's own vocabulary ("architectural-class",
70
+ // "architectural findings", the docs' mode matrix) and produced 3 junk
71
+ // findings on the 0.26.2 completion, observed live.
60
72
  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 },
73
+ { class: "strategic", re: /\bshould we\b|\bdeprecat|ship this\??/i },
74
+ { class: "architectural", re: /\brewrite\b|new dependency|schema change|\bredesign\b/i },
63
75
  { 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 },
76
+ { class: "refactor", re: /could be cleaner|consider refactoring|duplicat|refactor|left ?out|follow[\s-]?up|deferred|could be improved|improvement|enhancement|consider adding|would be nice|nice to have/i },
65
77
  ];
66
78
 
79
+ /** v0.26.3: lines that never carry findings — code, markdown tables, and
80
+ * the reviewer's own report/config vocabulary. Observed false positives
81
+ * from the 0.26.2 completion: a test("…architectural…") name, the
82
+ * INSTALL.md mode-matrix row, and ship-doc prose. */
83
+ const SKIP_LINE = /^\s*(test|it|describe|assert|expect)\s*\(|^\s*(const|let|var|function|import|export|require)\b|\{\s*\.\.\.\s*\}|,\s*\.\.\.$|^\s*\|/;
84
+ const REVIEWER_VOCAB = /architectural-class|bug-class|refactor-class|strategic-class|reviewer found|cascade step|\*\*Mode\*\*|problems\s*\/\s*(improvements|architectural)/i;
85
+
67
86
  export function classifyFindingText(line: string): FindingClass | undefined {
68
87
  const t = line.trim();
69
88
  if (t.length < 8) return undefined;
89
+ if (SKIP_LINE.test(t) || REVIEWER_VOCAB.test(t)) return undefined;
70
90
  for (const { class: cls, re } of CLASS_PATTERNS) {
71
91
  if (re.test(t)) return cls;
72
92
  }
@@ -110,6 +130,7 @@ export interface ReviewReport {
110
130
  objective: string;
111
131
  findings: Finding[];
112
132
  cascadeStep: string;
133
+ mode: ReviewerMode;
113
134
  at: string;
114
135
  }
115
136
 
@@ -120,7 +141,7 @@ export function formatReviewReport(r: ReviewReport): string {
120
141
  return [
121
142
  `# Review — ${r.goalId}`,
122
143
  "",
123
- `**Kind**: ${r.kind} · **At**: ${r.at}`,
144
+ `**Kind**: ${r.kind} · **At**: ${r.at} · **Mode**: ${r.mode}`,
124
145
  "",
125
146
  "## Summary",
126
147
  "",
@@ -186,7 +207,11 @@ export function runReviewer(
186
207
  if (config.doNotFireOn.includes(event)) return none(`doNotFireOn: ${event}`);
187
208
  if (source.kind === "goal" && source.terminal !== "goal-complete") return none(`not a completion: ${source.terminal}`);
188
209
  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)) {
210
+ // v0.26.2: in auto mode the queue emptying is the cascade's natural
211
+ // rhythm, not a runaway — the refire window must not strangle it.
212
+ // (The per-day cap below still bounds everything.)
213
+ const refireWindowApplies = !(config.mode === "auto" && source.kind === "list");
214
+ if (refireWindowApplies && reviewerFiredRecently(deps.ledgerEntries, REVIEWER_REFIRE_WINDOW_MS, deps.nowMs)) {
190
215
  deps.ledger("reviewer_suppressed", { reason: "refire-window", goalId: source.goalId });
191
216
  return none("reviewer fired within the last 5 minutes (runaway prevention)");
192
217
  }
@@ -205,32 +230,47 @@ export function runReviewer(
205
230
  let enqueued = 0;
206
231
  let proposed = 0;
207
232
  let cascadeStep = "notify-and-idle";
233
+ const auto = config.mode === "auto";
234
+ const reportOnly = config.mode === "report";
208
235
 
209
236
  // Cascade: findings → list items (leverage: fix-without-confirm).
210
237
  const convertStep = source.kind === "goal" ? "convert-findings-to-list" : "queue-leftovers";
211
- if (bugs.length > 0 && config.cascade.includes(convertStep)) {
238
+ if (bugs.length > 0 && config.cascade.includes(convertStep) && !reportOnly) {
212
239
  deps.enqueueListItems(bugs.map((f) => f.text));
213
240
  enqueued = bugs.length;
214
241
  cascadeStep = convertStep;
215
242
  }
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";
243
+ // Architectural findings: default mode → /goal proposal WITH Confirm;
244
+ // auto mode → /list items (the auto-loop rolls straight into them).
245
+ if (architectural.length > 0 && !reportOnly) {
246
+ if (auto) {
247
+ deps.enqueueListItems(architectural.map((f) => f.text));
248
+ enqueued += architectural.length;
249
+ cascadeStep = convertStep;
250
+ } else {
251
+ deps.proposeGoal(
252
+ architectural.map((f) => f.text).join("; "),
253
+ `reviewer found ${architectural.length} architectural-class finding(s) — needs your Confirm`,
254
+ );
255
+ proposed += architectural.length;
256
+ cascadeStep = "propose-goal";
257
+ }
224
258
  }
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++;
259
+ // Clean completion → audit: default mode proposes a /goal (Confirm);
260
+ // auto mode enqueues the audit as a /list item (no Confirm — the
261
+ // cascade keeps rolling until the findings run dry).
262
+ if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean") && !reportOnly) {
263
+ const auditObjective = `Post-completion regression scan after ${source.goalId} (${config.auditScope})`;
264
+ if (auto) {
265
+ deps.enqueueListItems([auditObjective]);
266
+ enqueued++;
267
+ } else {
268
+ deps.proposeGoal(auditObjective, "reviewer: completion looks clean — firing the audit step");
269
+ proposed++;
270
+ }
232
271
  cascadeStep = "fire-audit-on-clean";
233
272
  }
273
+ if (reportOnly) cascadeStep = "report-only";
234
274
 
235
275
  const report: ReviewReport = {
236
276
  goalId: source.goalId,
@@ -238,6 +278,7 @@ export function runReviewer(
238
278
  objective: source.objective,
239
279
  findings,
240
280
  cascadeStep,
281
+ mode: config.mode,
241
282
  at: new Date(deps.nowMs).toISOString(),
242
283
  };
243
284
  const reportPath = writeReviewReport(deps.cwd, report);
@@ -265,6 +306,7 @@ export function runReviewer(
265
306
  export function reviewerMenuOptions(cfg: ReviewerConfig): string[] {
266
307
  return [
267
308
  `Enabled — ${cfg.enabled ? "ON" : "OFF"}`,
309
+ `Mode — ${cfg.mode} (default = Confirm-gated · auto = auto-loop, no Confirms · report = report only)`,
268
310
  `Leverage mode — ${cfg.leverageMode} (bug/refactor findings)`,
269
311
  `Fire on goal-complete — ${cfg.fireOn.includes("goal-complete") ? "ON" : "OFF"}`,
270
312
  `Fire on list-complete — ${cfg.fireOn.includes("list-complete") ? "ON" : "OFF"}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
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",