pi-goal-list-loop-audit 0.31.2 → 0.31.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.
@@ -21,6 +21,10 @@ import type { SubagentModelStrategy } from "./goal-loop-subagents.js";
21
21
  export interface Settings {
22
22
  /** "provider/model-id" or bare "model-id". Unset → session model. */
23
23
  auditorModel?: string;
24
+ /** v0.31.3: auto-swap target when the session model IS auditorModel —
25
+ * the verifier should differ from the executor. Unset = same model stands
26
+ * (with a loud one-line nudge). */
27
+ auditorModelFallback?: string;
24
28
  auditorThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
25
29
  /** Shell command run on goal complete / goal pause / loop stop; message passed as $1. */
26
30
  notifyCmd?: string;
@@ -115,7 +119,9 @@ export const DEFAULT_SETTINGS: Settings = {
115
119
  // Unset = "high" at the call site (v0.31.2). The auditor is the
116
120
  // verification gate: its depth must NOT ride the session's coding-speed
117
121
  // thinking dial (user 2026-07-31: "we should also select its thinking
118
- // level — we don't keep switching it"). /glla thinking= overrides.
122
+ // level — we don't keep switching it"). v0.31.4: picked alongside the
123
+ // model in /glla → Auditor model (no standalone menu row); /glla
124
+ // thinking= remains the direct path.
119
125
  auditorThinkingLevel: undefined,
120
126
  // v0.24.6: subagents inherit the session model by default — one quota
121
127
  // pool, no surprise 403s from a pinned default agent's provider.
@@ -154,7 +154,6 @@ import {
154
154
  } from "../settings-menu.js";
155
155
  import {
156
156
  buildModelPickItems,
157
- pickDiverseAuditorModel,
158
157
  ModelPickerComponent,
159
158
  type ModelPickItem,
160
159
  } from "../model-picker.js";
@@ -1366,7 +1365,7 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-
1366
1365
  ? "Manual /goal verify — running the isolated auditor now (no agent turn needed)."
1367
1366
  : "Auditor quota window elapsed — retrying the audit with your stored completion claim (no agent turn needed).", "info");
1368
1367
  const settings = loadSettings(liveCtx.cwd);
1369
- const { model: auditorModel, error: modelError, via } = resolveAuditorModel(liveCtx, settings.auditorModel);
1368
+ const { model: auditorModel, error: modelError, via } = resolveAuditorModel(liveCtx, settings.auditorModel, settings.auditorModelFallback);
1370
1369
  if (modelError) liveCtx.ui.notify(`Auditor model issue: ${modelError}`, "warning");
1371
1370
  latestAuditProgress = { label: "quota-retry", lastEventAt: Date.now() };
1372
1371
  completionAuditInFlight = true;
@@ -3174,7 +3173,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
3174
3173
  }
3175
3174
  updateGoal({ status: "auditing", pendingTasks: undefined }, ctx);
3176
3175
  const settings = loadSettings(ctx.cwd);
3177
- const { model: auditorModel, error: modelError, via } = resolveAuditorModel(ctx, settings.auditorModel);
3176
+ const { model: auditorModel, error: modelError, via } = resolveAuditorModel(ctx, settings.auditorModel, settings.auditorModelFallback);
3178
3177
  if (modelError) {
3179
3178
  ctx.ui.notify(`Auditor model issue: ${modelError}`, "warning");
3180
3179
  }
@@ -4221,57 +4220,67 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
4221
4220
  * clear explanation (switch pi's model to a built-in provider, or set the
4222
4221
  * override) — we do NOT silently substitute a different model.
4223
4222
  */
4224
- function resolveAuditorModel(ctx: ExtensionContext, ref?: string): { model: any; error?: string; via?: string } {
4225
- if (ref && ref.trim()) {
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
- }
4245
- // v0.29.17: an unavailable configured model (unknown id, or a provider
4246
- // with no configured auth) falls back LOUDLY to the session model —
4247
- // user request: "fall back to the session if unavailable". The v0.9.12
4248
- // no-SILENT-substitution law stands: the fallback notifies + ledgers.
4249
- // (Quota-exhausted keys stay on the quota-retry path — the model IS
4250
- // available there; the key's window is the failure, not the model.)
4251
- const fail = (reason: string) => {
4252
- const sessionModel = ctx.model as any;
4253
- if (sessionModel) {
4254
- appendLedger(ctx.cwd, "auditor_model_fallback", { configured: trimmed, reason });
4255
- ctx.ui.notify(`Auditor model "${trimmed}" is unavailable (${reason}) — falling back to the session model. Fix via /glla → Auditor model.`, "warning");
4256
- return { model: sessionModel, via: "session-fallback" };
4257
- }
4258
- return { model: undefined, error: `${reason}: ${trimmed}` };
4259
- };
4223
+ /** v0.31.3: the auditor model chain pinned primary, pinned fallback,
4224
+ * session model LAST (user design 2026-07-31: "it can be the primary auditor
4225
+ * and the session model is always the last; we can have a fallback auditor
4226
+ * too" + "if the session model is the same as the auditor we auto fallback").
4227
+ * Two explicit pins and a cascade no preference tables, no strategy
4228
+ * resolution (the v0.31.2 diverse-strategy machinery cost more complexity than it
4229
+ * bought; it lasted one version). Every hop is LOUD (ledger + notify): the
4230
+ * v0.9.12 no-SILENT-substitution law.
4231
+ */
4232
+ function resolveAuditorModel(ctx: ExtensionContext, ref?: string, fallbackRef?: string): { model: any; error?: string; via?: string } {
4233
+ const sessionModel = ctx.model as any;
4234
+ const tryRef = (trimmed: string): { model?: any; reason?: string } => {
4260
4235
  const slash = trimmed.indexOf("/");
4261
4236
  if (slash > 0) {
4262
4237
  const provider = trimmed.slice(0, slash);
4263
- const id = trimmed.slice(slash + 1);
4264
- const model = ctx.modelRegistry.find(provider, id);
4265
- if (!model) return fail("model not found");
4266
- if (!ctx.modelRegistry.hasConfiguredAuth(model)) return fail(`no configured auth for ${provider}`);
4267
- return { model, via: "setting" };
4238
+ const model = ctx.modelRegistry.find(provider, trimmed.slice(slash + 1));
4239
+ if (!model) return { reason: "model not found" };
4240
+ // v0.29.17: an unkeyed provider counts as unavailable. (Quota-exhausted
4241
+ // keys stay on the quota-retry path — the model IS available there;
4242
+ // the key's window is the failure, not the model.)
4243
+ if (!ctx.modelRegistry.hasConfiguredAuth(model)) return { reason: `no configured auth for ${provider}` };
4244
+ return { model };
4268
4245
  }
4269
4246
  const matches = ctx.modelRegistry.getAvailable().filter((m: any) => m.id === trimmed || m.name === trimmed);
4270
- if (matches[0]) return { model: matches[0], via: "setting" };
4271
- return fail("no available model matching");
4247
+ return matches[0] ? { model: matches[0] } : { reason: "no available model matching" };
4248
+ };
4249
+ const isSession = (m: any) => sessionModel && m.provider === sessionModel.provider && m.id === sessionModel.id;
4250
+ const pins = [ref, fallbackRef].map((r) => r?.trim()).filter((r): r is string => !!r);
4251
+ for (let i = 0; i < pins.length; i++) {
4252
+ const pin = pins[i]!;
4253
+ const r = tryRef(pin);
4254
+ if (!r.model) {
4255
+ // Unavailable pin → cascade: next pin, then the session model (LOUD).
4256
+ appendLedger(ctx.cwd, "auditor_model_fallback", { configured: pin, reason: r.reason });
4257
+ ctx.ui.notify(`Auditor model "${pin}" is unavailable (${r.reason}) — ${i + 1 < pins.length ? "trying the fallback pin" : "falling back to the session model"}. Fix via /glla → Auditor model.`, "warning");
4258
+ continue;
4259
+ }
4260
+ if (isSession(r.model) && i + 1 < pins.length) {
4261
+ // The pin IS the session model — the verifier would be the executor's
4262
+ // own model; auto-swap down the chain (the user's move).
4263
+ appendLedger(ctx.cwd, "auditor_model_same_as_session", { model: `${r.model.provider}/${r.model.id}`, fallback: pins[i + 1] });
4264
+ ctx.ui.notify(`Session model IS the pinned auditor (${r.model.provider}/${r.model.id}) — auditor auto-swapped to ${pins[i + 1]} so the verifier differs.`, "info");
4265
+ continue;
4266
+ }
4267
+ if (isSession(r.model) && !fallbackRef?.trim()) {
4268
+ // Last resort reached and it IS the session model, with no fallback
4269
+ // ever pinned — the model stands (the session IS the last resort);
4270
+ // one loud nudge so the user can wire the swap.
4271
+ appendLedger(ctx.cwd, "auditor_model_same_as_session", { model: `${r.model.provider}/${r.model.id}`, fallback: null });
4272
+ ctx.ui.notify(`The session model IS the pinned auditor (${r.model.provider}/${r.model.id}) — pin a different /glla → Auditor fallback model so the verifier can differ.`, "warning");
4273
+ }
4274
+ return { model: r.model, via: i === 0 ? "setting" : "fallback-pin" };
4275
+ }
4276
+ if (sessionModel) {
4277
+ if (pins.length > 0) {
4278
+ appendLedger(ctx.cwd, "auditor_model_fallback", { configured: pins.join(" → "), reason: "all pins exhausted" });
4279
+ ctx.ui.notify("All pinned auditor models are unavailable — falling back to the session model. Fix via /glla → Auditor model.", "warning");
4280
+ return { model: sessionModel, via: "session-fallback" };
4281
+ }
4282
+ return { model: sessionModel, via: "session" };
4272
4283
  }
4273
- const sessionModel = ctx.model as any;
4274
- if (sessionModel) return { model: sessionModel, via: "session" };
4275
4284
  return { model: undefined, error: "no session model and no auditorModel configured — set one with /glla → Auditor model" };
4276
4285
  }
4277
4286
 
@@ -4362,7 +4371,6 @@ async function promptModelRef(
4362
4371
  ctx: ExtensionContext,
4363
4372
  title: string,
4364
4373
  emptyLabel: string,
4365
- extraTop: ModelPickItem[] = [],
4366
4374
  ): Promise<{ kind: "session" } | { kind: "ref"; ref: string } | undefined> {
4367
4375
  if (typeof (ctx.ui as { custom?: unknown }).custom !== "function" || !ctx.modelRegistry) {
4368
4376
  const v = await ctx.ui.input(title, "provider/model-id — empty keeps the default");
@@ -4374,7 +4382,7 @@ async function promptModelRef(
4374
4382
  const models = ctx.modelRegistry
4375
4383
  .getAvailable()
4376
4384
  .filter((m: any) => ctx.modelRegistry.hasConfiguredAuth(m));
4377
- const items = buildModelPickItems(models, sessionLabel, extraTop);
4385
+ const items = buildModelPickItems(models, sessionLabel);
4378
4386
  const pick = await ctx.ui.custom<ModelPickItem | undefined>((tui, theme, keybindings, done) => {
4379
4387
  return new ModelPickerComponent({ title, items }, () => tui.requestRender(), theme, keybindings, done);
4380
4388
  });
@@ -4426,26 +4434,25 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
4426
4434
  return;
4427
4435
  }
4428
4436
  case "auditorModel": {
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]);
4437
+ const pick = await promptModelRef(ctx, "Auditor model override", "provider/model-id empty keeps the pi session model");
4440
4438
  if (pick === undefined) return;
4441
4439
  saveSettings("global", ctx.cwd, { auditorModel: pick.kind === "session" ? undefined : pick.ref });
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");
4440
+ // v0.31.4: thinking is chosen WITH the model (user: "we are setting
4441
+ // the thinking when we select the model now or we should") there is
4442
+ // no standalone thinking row to forget about. Esc keeps the level.
4443
+ const t = await ctx.ui.select("Auditor thinking level (the verification gate — depth over speed)", [
4444
+ "high — recommended: the gate must not ride the session's coding dial",
4445
+ "medium", "low", "minimal", "xhigh", "off",
4446
+ ]);
4447
+ if (t) saveSettings("global", ctx.cwd, { auditorThinkingLevel: t.split(" ")[0] as Settings["auditorThinkingLevel"] });
4448
+ ctx.ui.notify(`Auditor model: ${pick.kind === "session" ? "session model (override cleared)" : pick.ref}${t ? ` · thinking ${t.split(" ")[0]}` : ""}`, "info");
4444
4449
  return;
4445
4450
  }
4446
- case "auditorThinkingLevel": {
4447
- const v = await ctx.ui.select("Auditor thinking level", ["off", "minimal", "low", "medium", "high", "xhigh"]);
4448
- if (v) saveSettings("global", ctx.cwd, { auditorThinkingLevel: v as Settings["auditorThinkingLevel"] });
4451
+ case "auditorModelFallback": {
4452
+ const pick = await promptModelRef(ctx, "Auditor fallback model (used when the session model IS the auditor)", "provider/model-id — empty clears the fallback");
4453
+ if (pick === undefined) return;
4454
+ saveSettings("global", ctx.cwd, { auditorModelFallback: pick.kind === "session" ? undefined : pick.ref });
4455
+ if (pick.kind === "session") ctx.ui.notify("Auditor fallback cleared — a session on the pinned auditor model keeps that model.", "info");
4449
4456
  return;
4450
4457
  }
4451
4458
  case "auditCap": {
@@ -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, extraTop: ModelPickItem[] = []): ModelPickItem[] {
44
+ export function buildModelPickItems(models: RegistryModelLike[], sessionLabel: string): 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,7 +51,6 @@ 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,
55
54
  ...sorted.map((m) => {
56
55
  const ref = `${m.provider}/${m.id}`;
57
56
  return {
@@ -207,47 +206,3 @@ export class ModelPickerComponent {
207
206
 
208
207
  // Re-export for callers that only need the width helper's type signature.
209
208
  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
- }
@@ -165,17 +165,17 @@ export function buildSettingsRows(
165
165
  id: "auditorModel",
166
166
  section: "auditor",
167
167
  label: "Auditor model",
168
- valueText: show("auditorModel", "pi session model"),
168
+ valueText: show("auditorModel", "session model"),
169
169
  sourceText: src("auditorModel"),
170
- description: "provider/model override for the isolated auditor",
170
+ description: "provider/model override for the isolated auditor — you pick its thinking level right after the model",
171
171
  },
172
172
  {
173
- id: "auditorThinkingLevel",
173
+ id: "auditorModelFallback",
174
174
  section: "auditor",
175
- label: "Auditor thinking",
176
- valueText: show("auditorThinkingLevel", "high (fixed — never the session coding dial)"),
177
- sourceText: src("auditorThinkingLevel"),
178
- description: "thinking level for the auditor session unset = high (the verification gate does not ride the session thinking dial)",
175
+ label: "Auditor fallback model",
176
+ valueText: show("auditorModelFallback", "none"),
177
+ sourceText: src("auditorModelFallback"),
178
+ description: "auto-swap target when the session model IS the auditor model — the verifier should differ from the executor",
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.2",
3
+ "version": "0.31.4",
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",