pi-goal-list-loop-audit 0.31.0 → 0.31.2

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.
@@ -547,7 +547,16 @@ export function listAuditFanoutItemText(finding: string): string {
547
547
  return `Fix audit finding: ${finding} — Done when: the fix is committed on the current branch with the repo's configured identity, and this finding's box in ${AUDIT_FINDINGS_REL} is checked ("- [x] … — fixed in <commit>").`;
548
548
  }
549
549
 
550
+ /** v0.31.1: stacking-detection markers (junk-runner 2026-07-31: a held
551
+ * one-shot audit goal + a running audit loop = two stacked audit initiatives
552
+ * — the held one-shot read as "stalled" for 8h while the loop did all the
553
+ * work, and the agent conflated them). The guards in goal.ts match on these;
554
+ * the unit tests pin that the built targets still contain them.
555
+ */
556
+ export const GOAL_AUDIT_ONESHOT_MARKER = "Run ONE project audit pass and leave the project in a known state";
557
+ export const LOOP_AUDIT_MARKER = "iteration by iteration — FIX-FIRST";
558
+
550
559
  export function projectAuditTarget(focus?: string): string {
551
560
  const scope = focus && focus.trim() ? focus.trim() : "the whole project";
552
- return `Run ONE project audit pass and leave the project in a known state. Scope: ${scope}. (1) Run a FRESH audit pass over the codebase — spawn Explore subagents for breadth — hunting real problems: bugs, broken flows, regressions, drift between docs and code, dead code, security holes. Not style nits, not speculative refactors. (2) Append every NEW finding to ${AUDIT_FINDINGS_REL} (create the file on the first finding; append-only — never delete, rewrite, or reorder existing lines; never re-report a finding already listed), classified: "- [ ] FIX: SEVERITY: short description (file:line)" for bugs and polish — whether to fix these is NOT a decision — and "- [?] DECIDE: short description (what the choice is, what each side costs)" for direction, trade-offs, and scope questions where two reasonable answers exist. (3) Fix every NEW FIX finding from this pass — real fixes, committed with the repo's configured identity on the current branch (no invented identities or branches) — then check the box: "- [x] … — fixed in <commit>". (4) Change NOTHING for DECIDE findings — present them in the completion report instead. (5) Honesty law: never fabricate findings to look busy; never check a box without the fix commit existing; never silently turn a DECIDE into a fix. Done when: the audit pass is complete, every new FIX finding has a fix commit and a checked box in ${AUDIT_FINDINGS_REL}, and every DECIDE finding is listed in the file and presented in the completion report.`;
561
+ return `${GOAL_AUDIT_ONESHOT_MARKER}. Scope: ${scope}. (1) Run a FRESH audit pass over the codebase — spawn Explore subagents for breadth — hunting real problems: bugs, broken flows, regressions, drift between docs and code, dead code, security holes. Not style nits, not speculative refactors. (2) Append every NEW finding to ${AUDIT_FINDINGS_REL} (create the file on the first finding; append-only — never delete, rewrite, or reorder existing lines; never re-report a finding already listed), classified: "- [ ] FIX: SEVERITY: short description (file:line)" for bugs and polish — whether to fix these is NOT a decision — and "- [?] DECIDE: short description (what the choice is, what each side costs)" for direction, trade-offs, and scope questions where two reasonable answers exist. (3) Fix every NEW FIX finding from this pass — real fixes, committed with the repo's configured identity on the current branch (no invented identities or branches) — then check the box: "- [x] … — fixed in <commit>". (4) Change NOTHING for DECIDE findings — present them in the completion report instead. (5) Honesty law: never fabricate findings to look busy; never check a box without the fix commit existing; never silently turn a DECIDE into a fix. Done when: the audit pass is complete, every new FIX finding has a fix commit and a checked box in ${AUDIT_FINDINGS_REL}, and every DECIDE finding is listed in the file and presented in the completion report.`;
553
562
  }
@@ -112,9 +112,10 @@ export interface Settings {
112
112
  }
113
113
 
114
114
  export const DEFAULT_SETTINGS: Settings = {
115
- // Unset = follow the pi session thinking level (user selects thinking in
116
- // pi, auditor follows), floor "high" the auditor is the verification
117
- // gate, depth is worth more there than speed. /glla thinking= overrides.
115
+ // Unset = "high" at the call site (v0.31.2). The auditor is the
116
+ // verification gate: its depth must NOT ride the session's coding-speed
117
+ // thinking dial (user 2026-07-31: "we should also select its thinking
118
+ // level — we don't keep switching it"). /glla thinking= overrides.
118
119
  auditorThinkingLevel: undefined,
119
120
  // v0.24.6: subagents inherit the session model by default — one quota
120
121
  // pool, no surprise 403s from a pinned default agent's provider.
@@ -154,6 +154,7 @@ import {
154
154
  } from "../settings-menu.js";
155
155
  import {
156
156
  buildModelPickItems,
157
+ pickDiverseAuditorModel,
157
158
  ModelPickerComponent,
158
159
  type ModelPickItem,
159
160
  } from "../model-picker.js";
@@ -176,6 +177,8 @@ import {
176
177
  AUDIT_FINDINGS_REL,
177
178
  projectAuditTarget,
178
179
  LIST_AUDIT_COLLECT_MARKER,
180
+ GOAL_AUDIT_ONESHOT_MARKER,
181
+ LOOP_AUDIT_MARKER,
179
182
  listAuditCollectTarget,
180
183
  parseAuditFindingsForFanout,
181
184
  listAuditFanoutItemText,
@@ -1378,7 +1381,7 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-
1378
1381
  completionSummary: claim.completionSummary,
1379
1382
  verificationSummary: claim.verificationSummary,
1380
1383
  model: auditorModel,
1381
- thinkingLevel: settings.auditorThinkingLevel ?? getSessionThinkingLevel(),
1384
+ thinkingLevel: settings.auditorThinkingLevel ?? "high",
1382
1385
  onProgress: (progress) => {
1383
1386
  latestAuditProgress = { currentTool: progress.currentTool, label: progress.label, elapsedMs: progress.elapsedMs, lastEventAt: Date.now() };
1384
1387
  refreshUI(liveCtx);
@@ -1706,6 +1709,15 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
1706
1709
  // DECIDE findings presented, untouched. Runs as a normal goal through
1707
1710
  // cmdSet — the isolated auditor verifies the finish line.
1708
1711
  if (route.name === "audit") {
1712
+ // v0.31.1: an active audit loop already owns auditing here — the
1713
+ // one-shot duplicates it (same stacking confusion as junk-runner).
1714
+ if (state.loop?.active && state.loop.target.includes(LOOP_AUDIT_MARKER)) {
1715
+ appendLedger(ctx.cwd, "audit_stack_warn", { have: "loop", starting: "goal" });
1716
+ ctx.ui.notify(
1717
+ "An audit loop is already running here — a one-shot /goal audit duplicates its work. /loop status to see it; /loop stop first if you want the one-shot instead.",
1718
+ "warning",
1719
+ );
1720
+ }
1709
1721
  return cmdSet(projectAuditTarget(route.rest || undefined), ctx, true);
1710
1722
  }
1711
1723
  // v0.28.27 (renamed /goal audit → /goal verify in v0.29.8): run the
@@ -2158,6 +2170,12 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
2158
2170
  // list drains them fix by fix, each with its own isolated audit.
2159
2171
  // Distinct from /goal audit (fix-in-pass, one audited unit) and
2160
2172
  // /loop audit (forever fix-first cadence).
2173
+ // v0.31.1: an active audit loop is already draining this findings file —
2174
+ // a collect pass would double-hunt the same ground.
2175
+ if (state.loop?.active && state.loop.target.includes(LOOP_AUDIT_MARKER)) {
2176
+ appendLedger(ctx.cwd, "audit_stack_warn", { have: "loop", starting: "list" });
2177
+ ctx.ui.notify("An audit loop is already draining findings here — /list audit would double-hunt the same ground. /loop status to see it.", "warning");
2178
+ }
2161
2179
  const objective = listAuditCollectTarget(rest || undefined);
2162
2180
  const n = enqueueItems(ctx, [objective], "/list audit");
2163
2181
  if (n === 0) return; // zombie-twin guard already explained itself
@@ -3021,6 +3039,18 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
3021
3039
  ctx.ui.notify("A goal is active — /goal cancel or /goal pause it before starting a loop.", "warning");
3022
3040
  return;
3023
3041
  }
3042
+ // v0.31.1: a paused/active one-shot audit goal + this loop = two stacked
3043
+ // audit initiatives (junk-runner 2026-07-31: the held one-shot read as
3044
+ // "stalled" for 8h while the loop did all the work — the agent conflated
3045
+ // them and proposed completing the goal for the loop's work). Warn, name
3046
+ // the supersession, don't block — the user's agency, the user's call.
3047
+ if (state.goal && state.goal.objective.includes(GOAL_AUDIT_ONESHOT_MARKER)) {
3048
+ appendLedger(ctx.cwd, "audit_stack_warn", { have: "goal", starting: "loop", goalStatus: state.goal.status });
3049
+ ctx.ui.notify(
3050
+ `Heads up: a ${state.goal.status} one-shot audit goal exists in this session — the audit loop SUPERSEDES it (one pass + fixes IS the loop's job). /goal cancel clears it; one audit initiative per session.`,
3051
+ "warning",
3052
+ );
3053
+ }
3024
3054
  if (isLoopActive()) {
3025
3055
  ctx.ui.notify("A loop is already active. /loop stop first.", "warning");
3026
3056
  return;
@@ -3159,7 +3189,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
3159
3189
  completionSummary: p.completionSummary,
3160
3190
  verificationSummary: p.verificationSummary,
3161
3191
  model: auditorModel,
3162
- thinkingLevel: settings.auditorThinkingLevel ?? getSessionThinkingLevel(),
3192
+ thinkingLevel: settings.auditorThinkingLevel ?? "high",
3163
3193
  signal: signal ?? undefined,
3164
3194
  onProgress: (progress) => {
3165
3195
  latestAuditProgress = {
@@ -4178,18 +4208,6 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
4178
4208
  * the thinking level the user selected in pi; if none is set, audits run at
4179
4209
  * "high" — the auditor is the verification gate, depth beats speed there.
4180
4210
  */
4181
- function getSessionThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" {
4182
- try {
4183
- const level = extensionApi?.getThinkingLevel?.();
4184
- if (level && ["off", "minimal", "low", "medium", "high", "xhigh"].includes(level)) {
4185
- return level as "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
4186
- }
4187
- } catch {
4188
- // fall through to the floor
4189
- }
4190
- return "high";
4191
- }
4192
-
4193
4211
  /**
4194
4212
  * Resolve the auditor model (v0.6.2). The principle: **the user selects the
4195
4213
  * model in pi; the auditor uses it.** The plugin never picks a model itself.
@@ -4206,6 +4224,24 @@ function getSessionThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high
4206
4224
  function resolveAuditorModel(ctx: ExtensionContext, ref?: string): { model: any; error?: string; via?: string } {
4207
4225
  if (ref && ref.trim()) {
4208
4226
  const trimmed = ref.trim();
4227
+ // v0.31.2: "diverse" — the cross-vendor auditor (user design 2026-07-31:
4228
+ // "there is benefit to have a different auditor"). Independent blind
4229
+ // spots + a separate quota pool; the fresh auditor session shares no
4230
+ // prompt cache anyway, so cross-vendor costs nothing extra.
4231
+ if (trimmed.toLowerCase() === "diverse") {
4232
+ const sessionModel = ctx.model as any;
4233
+ const available = ctx.modelRegistry
4234
+ .getAvailable()
4235
+ .filter((m: any) => ctx.modelRegistry.hasConfiguredAuth(m));
4236
+ const pick = pickDiverseAuditorModel(available, sessionModel?.provider);
4237
+ if (pick) return { model: pick, via: "diverse" };
4238
+ if (sessionModel) {
4239
+ appendLedger(ctx.cwd, "auditor_model_fallback", { configured: "diverse", reason: "no configured-auth model outside the session's provider" });
4240
+ ctx.ui.notify("Auditor model \"diverse\" found no model outside the session's provider — falling back to the session model (LOUD fallback; pin one via /glla → Auditor model).", "warning");
4241
+ return { model: sessionModel, via: "session-fallback" };
4242
+ }
4243
+ return { model: undefined, error: "auditorModel \"diverse\": no configured-auth model at all" };
4244
+ }
4209
4245
  // v0.29.17: an unavailable configured model (unknown id, or a provider
4210
4246
  // with no configured auth) falls back LOUDLY to the session model —
4211
4247
  // user request: "fall back to the session if unavailable". The v0.9.12
@@ -4326,6 +4362,7 @@ async function promptModelRef(
4326
4362
  ctx: ExtensionContext,
4327
4363
  title: string,
4328
4364
  emptyLabel: string,
4365
+ extraTop: ModelPickItem[] = [],
4329
4366
  ): Promise<{ kind: "session" } | { kind: "ref"; ref: string } | undefined> {
4330
4367
  if (typeof (ctx.ui as { custom?: unknown }).custom !== "function" || !ctx.modelRegistry) {
4331
4368
  const v = await ctx.ui.input(title, "provider/model-id — empty keeps the default");
@@ -4337,7 +4374,7 @@ async function promptModelRef(
4337
4374
  const models = ctx.modelRegistry
4338
4375
  .getAvailable()
4339
4376
  .filter((m: any) => ctx.modelRegistry.hasConfiguredAuth(m));
4340
- const items = buildModelPickItems(models, sessionLabel);
4377
+ const items = buildModelPickItems(models, sessionLabel, extraTop);
4341
4378
  const pick = await ctx.ui.custom<ModelPickItem | undefined>((tui, theme, keybindings, done) => {
4342
4379
  return new ModelPickerComponent({ title, items }, () => tui.requestRender(), theme, keybindings, done);
4343
4380
  });
@@ -4389,10 +4426,21 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
4389
4426
  return;
4390
4427
  }
4391
4428
  case "auditorModel": {
4392
- const pick = await promptModelRef(ctx, "Auditor model override", "provider/model-id empty keeps the pi session model");
4429
+ // v0.31.2: "diverse" sits at the top — the cross-vendor auditor
4430
+ // (independent blind spots + a separate quota pool from the coding
4431
+ // session). Picked like a model; resolution happens per-audit so
4432
+ // provider availability is re-checked every time.
4433
+ const diverseItem: ModelPickItem = {
4434
+ kind: "model",
4435
+ ref: "diverse",
4436
+ label: "diverse — cross-vendor auditor: a different provider than the session, picked fresh per audit (Recommended)",
4437
+ searchText: "diverse cross vendor independent different provider quota strategy recommended",
4438
+ };
4439
+ const pick = await promptModelRef(ctx, "Auditor model override", "provider/model-id — empty keeps the pi session model", [diverseItem]);
4393
4440
  if (pick === undefined) return;
4394
4441
  saveSettings("global", ctx.cwd, { auditorModel: pick.kind === "session" ? undefined : pick.ref });
4395
4442
  if (pick.kind === "session") ctx.ui.notify("Auditor model override cleared — the auditor follows the pi session model.", "info");
4443
+ if (pick.kind === "ref" && pick.ref === "diverse") ctx.ui.notify("Auditor model: DIVERSE — each audit picks a configured model outside the session's provider (deepseek ↔ MiniMax first).", "info");
4396
4444
  return;
4397
4445
  }
4398
4446
  case "auditorThinkingLevel": {
@@ -5834,11 +5882,24 @@ export default function (pi: ExtensionAPI): void {
5834
5882
  const isListItem = state.goal.policy === "list";
5835
5883
  const resumeCmd = isListItem ? "/list resume" : "/goal resume";
5836
5884
  const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume on load (global setting)`;
5885
+ // v0.31.1: name the supersession — a held one-shot audit whose work a
5886
+ // live audit loop now owns reads as "stalled" for HOURS otherwise
5887
+ // (junk-runner: 8h21m of "held for explicit resume" on a goal the
5888
+ // loop had superseded). The widget surface must say so.
5889
+ const auditSuperseded =
5890
+ state.goal.objective.includes(GOAL_AUDIT_ONESHOT_MARKER) &&
5891
+ !!state.loop &&
5892
+ (state.loop.active || state.loop.stopReason === HELD_ON_RESTORE) &&
5893
+ !!state.loop.target?.includes(LOOP_AUDIT_MARKER);
5837
5894
  updateGoal({
5838
5895
  status: "paused",
5839
5896
  pauseKind: "blocked",
5840
- pauseReason: "restored on session load — held for explicit resume",
5841
- pauseSuggestedAction: resumeHint,
5897
+ pauseReason: auditSuperseded
5898
+ ? "restored on session load — SUPERSEDED by the audit loop in this session"
5899
+ : "restored on session load — held for explicit resume",
5900
+ pauseSuggestedAction: auditSuperseded
5901
+ ? `/goal cancel clears it (the loop already owns the audit) · ${resumeHint} if you disagree`
5902
+ : resumeHint,
5842
5903
  }, ctx);
5843
5904
  ctx.ui.notify(
5844
5905
  `${isListItem ? "List item" : "Goal"} held on restore: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} waiting in the list)` : ""} — ${resumeCmd} to continue.`,
@@ -41,7 +41,7 @@ export interface RegistryModelLike {
41
41
  /** Build the picker's static item list from registry models (already
42
42
  * filtered to configured-auth providers by the caller). Session row first,
43
43
  * manual-entry row last; models sorted by provider then id. */
44
- export function buildModelPickItems(models: RegistryModelLike[], sessionLabel: string): ModelPickItem[] {
44
+ export function buildModelPickItems(models: RegistryModelLike[], sessionLabel: string, extraTop: ModelPickItem[] = []): ModelPickItem[] {
45
45
  const sorted = [...models].sort((a, b) =>
46
46
  a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider),
47
47
  );
@@ -51,6 +51,7 @@ export function buildModelPickItems(models: RegistryModelLike[], sessionLabel: s
51
51
  label: `session model (${sessionLabel}) — clear the override`,
52
52
  searchText: "session model default clear override follow",
53
53
  },
54
+ ...extraTop,
54
55
  ...sorted.map((m) => {
55
56
  const ref = `${m.provider}/${m.id}`;
56
57
  return {
@@ -206,3 +207,47 @@ export class ModelPickerComponent {
206
207
 
207
208
  // Re-export for callers that only need the width helper's type signature.
208
209
  export { visibleWidth };
210
+
211
+ /** v0.31.2: provider preference for the "diverse" auditor strategy (user
212
+ * design 2026-07-31: "there is benefit to have a different auditor — M3's
213
+ * auditor could be deepseek and vice versa"). A cross-vendor auditor reads
214
+ * the executor's claims with INDEPENDENT blind spots (same-family models
215
+ * share failure modes), and it spends a DIFFERENT provider's quota pool —
216
+ * audits stop eating the coding session's MiniMax window. The auditor is
217
+ * already a fresh session (no prompt-cache sharing), so cross-vendor costs
218
+ * nothing in cache terms.
219
+ *
220
+ * Order: deepseek-via-openrouter first, MiniMax second, then the other
221
+ * providers seen on this rig. The session's own provider is EXCLUDED
222
+ * entirely (openrouter hosts everything — family-level reasoning about its
223
+ * catalogue isn't reliable, so exclusion is at provider granularity).
224
+ */
225
+ export const DIVERSE_AUDITOR_PREFERENCE: Array<{ provider: string; match?: string }> = [
226
+ { provider: "openrouter", match: "deepseek/deepseek-chat" },
227
+ { provider: "openrouter", match: "deepseek" },
228
+ { provider: "minimax", match: "MiniMax-M3" },
229
+ { provider: "minimax" },
230
+ { provider: "kimi-coding" },
231
+ { provider: "kimi" },
232
+ { provider: "xai-auth" },
233
+ { provider: "opencode" },
234
+ { provider: "zenmux" },
235
+ ];
236
+
237
+ /** Pure selection: the first preference entry (outside the session's
238
+ * provider) with at least one available model wins; within an entry the
239
+ * models arrive pre-filtered (auth checked by the caller) and sorted.
240
+ * Returns undefined when nothing outside the session provider is available
241
+ * — the caller falls back LOUDLY to the session model.
242
+ */
243
+ export function pickDiverseAuditorModel(
244
+ models: RegistryModelLike[],
245
+ sessionProvider: string | undefined,
246
+ ): RegistryModelLike | undefined {
247
+ for (const pref of DIVERSE_AUDITOR_PREFERENCE) {
248
+ if (pref.provider === sessionProvider) continue;
249
+ const hit = models.find((m) => m.provider === pref.provider && (!pref.match || m.id.includes(pref.match)));
250
+ if (hit) return hit;
251
+ }
252
+ return undefined;
253
+ }
@@ -173,9 +173,9 @@ export function buildSettingsRows(
173
173
  id: "auditorThinkingLevel",
174
174
  section: "auditor",
175
175
  label: "Auditor thinking",
176
- valueText: show("auditorThinkingLevel", "session, floor high"),
176
+ valueText: show("auditorThinkingLevel", "high (fixed — never the session coding dial)"),
177
177
  sourceText: src("auditorThinkingLevel"),
178
- description: "thinking level for the auditor session",
178
+ description: "thinking level for the auditor session — unset = high (the verification gate does not ride the session thinking dial)",
179
179
  },
180
180
  {
181
181
  id: "auditCap",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.31.0",
3
+ "version": "0.31.2",
4
4
  "description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",