pi-goal-list-loop-audit 0.31.1 → 0.31.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.
@@ -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;
@@ -112,9 +116,10 @@ export interface Settings {
112
116
  }
113
117
 
114
118
  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.
119
+ // Unset = "high" at the call site (v0.31.2). The auditor is the
120
+ // verification gate: its depth must NOT ride the session's coding-speed
121
+ // thinking dial (user 2026-07-31: "we should also select its thinking
122
+ // level — we don't keep switching it"). /glla thinking= overrides.
118
123
  auditorThinkingLevel: undefined,
119
124
  // v0.24.6: subagents inherit the session model by default — one quota
120
125
  // pool, no surprise 403s from a pinned default agent's provider.
@@ -1365,7 +1365,7 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-
1365
1365
  ? "Manual /goal verify — running the isolated auditor now (no agent turn needed)."
1366
1366
  : "Auditor quota window elapsed — retrying the audit with your stored completion claim (no agent turn needed).", "info");
1367
1367
  const settings = loadSettings(liveCtx.cwd);
1368
- const { model: auditorModel, error: modelError, via } = resolveAuditorModel(liveCtx, settings.auditorModel);
1368
+ const { model: auditorModel, error: modelError, via } = resolveAuditorModel(liveCtx, settings.auditorModel, settings.auditorModelFallback);
1369
1369
  if (modelError) liveCtx.ui.notify(`Auditor model issue: ${modelError}`, "warning");
1370
1370
  latestAuditProgress = { label: "quota-retry", lastEventAt: Date.now() };
1371
1371
  completionAuditInFlight = true;
@@ -1380,7 +1380,7 @@ async function retryStoredCompletionAudit(ctx: ExtensionContext, origin: "quota-
1380
1380
  completionSummary: claim.completionSummary,
1381
1381
  verificationSummary: claim.verificationSummary,
1382
1382
  model: auditorModel,
1383
- thinkingLevel: settings.auditorThinkingLevel ?? getSessionThinkingLevel(),
1383
+ thinkingLevel: settings.auditorThinkingLevel ?? "high",
1384
1384
  onProgress: (progress) => {
1385
1385
  latestAuditProgress = { currentTool: progress.currentTool, label: progress.label, elapsedMs: progress.elapsedMs, lastEventAt: Date.now() };
1386
1386
  refreshUI(liveCtx);
@@ -3173,7 +3173,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
3173
3173
  }
3174
3174
  updateGoal({ status: "auditing", pendingTasks: undefined }, ctx);
3175
3175
  const settings = loadSettings(ctx.cwd);
3176
- const { model: auditorModel, error: modelError, via } = resolveAuditorModel(ctx, settings.auditorModel);
3176
+ const { model: auditorModel, error: modelError, via } = resolveAuditorModel(ctx, settings.auditorModel, settings.auditorModelFallback);
3177
3177
  if (modelError) {
3178
3178
  ctx.ui.notify(`Auditor model issue: ${modelError}`, "warning");
3179
3179
  }
@@ -3188,7 +3188,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
3188
3188
  completionSummary: p.completionSummary,
3189
3189
  verificationSummary: p.verificationSummary,
3190
3190
  model: auditorModel,
3191
- thinkingLevel: settings.auditorThinkingLevel ?? getSessionThinkingLevel(),
3191
+ thinkingLevel: settings.auditorThinkingLevel ?? "high",
3192
3192
  signal: signal ?? undefined,
3193
3193
  onProgress: (progress) => {
3194
3194
  latestAuditProgress = {
@@ -4207,18 +4207,6 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
4207
4207
  * the thinking level the user selected in pi; if none is set, audits run at
4208
4208
  * "high" — the auditor is the verification gate, depth beats speed there.
4209
4209
  */
4210
- function getSessionThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" {
4211
- try {
4212
- const level = extensionApi?.getThinkingLevel?.();
4213
- if (level && ["off", "minimal", "low", "medium", "high", "xhigh"].includes(level)) {
4214
- return level as "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
4215
- }
4216
- } catch {
4217
- // fall through to the floor
4218
- }
4219
- return "high";
4220
- }
4221
-
4222
4210
  /**
4223
4211
  * Resolve the auditor model (v0.6.2). The principle: **the user selects the
4224
4212
  * model in pi; the auditor uses it.** The plugin never picks a model itself.
@@ -4232,39 +4220,67 @@ function getSessionThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high
4232
4220
  * clear explanation (switch pi's model to a built-in provider, or set the
4233
4221
  * override) — we do NOT silently substitute a different model.
4234
4222
  */
4235
- function resolveAuditorModel(ctx: ExtensionContext, ref?: string): { model: any; error?: string; via?: string } {
4236
- if (ref && ref.trim()) {
4237
- const trimmed = ref.trim();
4238
- // v0.29.17: an unavailable configured model (unknown id, or a provider
4239
- // with no configured auth) falls back LOUDLY to the session model
4240
- // user request: "fall back to the session if unavailable". The v0.9.12
4241
- // no-SILENT-substitution law stands: the fallback notifies + ledgers.
4242
- // (Quota-exhausted keys stay on the quota-retry path — the model IS
4243
- // available there; the key's window is the failure, not the model.)
4244
- const fail = (reason: string) => {
4245
- const sessionModel = ctx.model as any;
4246
- if (sessionModel) {
4247
- appendLedger(ctx.cwd, "auditor_model_fallback", { configured: trimmed, reason });
4248
- ctx.ui.notify(`Auditor model "${trimmed}" is unavailable (${reason}) — falling back to the session model. Fix via /glla → Auditor model.`, "warning");
4249
- return { model: sessionModel, via: "session-fallback" };
4250
- }
4251
- return { model: undefined, error: `${reason}: ${trimmed}` };
4252
- };
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 } => {
4253
4235
  const slash = trimmed.indexOf("/");
4254
4236
  if (slash > 0) {
4255
4237
  const provider = trimmed.slice(0, slash);
4256
- const id = trimmed.slice(slash + 1);
4257
- const model = ctx.modelRegistry.find(provider, id);
4258
- if (!model) return fail("model not found");
4259
- if (!ctx.modelRegistry.hasConfiguredAuth(model)) return fail(`no configured auth for ${provider}`);
4260
- 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 };
4261
4245
  }
4262
4246
  const matches = ctx.modelRegistry.getAvailable().filter((m: any) => m.id === trimmed || m.name === trimmed);
4263
- if (matches[0]) return { model: matches[0], via: "setting" };
4264
- 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" };
4265
4283
  }
4266
- const sessionModel = ctx.model as any;
4267
- if (sessionModel) return { model: sessionModel, via: "session" };
4268
4284
  return { model: undefined, error: "no session model and no auditorModel configured — set one with /glla → Auditor model" };
4269
4285
  }
4270
4286
 
@@ -4424,6 +4440,13 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
4424
4440
  if (pick.kind === "session") ctx.ui.notify("Auditor model override cleared — the auditor follows the pi session model.", "info");
4425
4441
  return;
4426
4442
  }
4443
+ case "auditorModelFallback": {
4444
+ const pick = await promptModelRef(ctx, "Auditor fallback model (used when the session model IS the auditor)", "provider/model-id — empty clears the fallback");
4445
+ if (pick === undefined) return;
4446
+ saveSettings("global", ctx.cwd, { auditorModelFallback: pick.kind === "session" ? undefined : pick.ref });
4447
+ if (pick.kind === "session") ctx.ui.notify("Auditor fallback cleared — a session on the pinned auditor model keeps that model.", "info");
4448
+ return;
4449
+ }
4427
4450
  case "auditorThinkingLevel": {
4428
4451
  const v = await ctx.ui.select("Auditor thinking level", ["off", "minimal", "low", "medium", "high", "xhigh"]);
4429
4452
  if (v) saveSettings("global", ctx.cwd, { auditorThinkingLevel: v as Settings["auditorThinkingLevel"] });
@@ -169,13 +169,21 @@ export function buildSettingsRows(
169
169
  sourceText: src("auditorModel"),
170
170
  description: "provider/model override for the isolated auditor",
171
171
  },
172
+ {
173
+ id: "auditorModelFallback",
174
+ section: "auditor",
175
+ label: "Auditor fallback model",
176
+ valueText: show("auditorModelFallback", "none — same-model sessions keep the pinned auditor"),
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
+ },
172
180
  {
173
181
  id: "auditorThinkingLevel",
174
182
  section: "auditor",
175
183
  label: "Auditor thinking",
176
- valueText: show("auditorThinkingLevel", "session, floor high"),
184
+ valueText: show("auditorThinkingLevel", "high (fixed — never the session coding dial)"),
177
185
  sourceText: src("auditorThinkingLevel"),
178
- description: "thinking level for the auditor session",
186
+ description: "thinking level for the auditor session — unset = high (the verification gate does not ride the session thinking dial)",
179
187
  },
180
188
  {
181
189
  id: "auditCap",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.31.1",
3
+ "version": "0.31.3",
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",