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").
|
|
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.
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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
|
-
|
|
4271
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4443
|
-
|
|
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 "
|
|
4447
|
-
const
|
|
4448
|
-
if (
|
|
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
|
|
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", "
|
|
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: "
|
|
173
|
+
id: "auditorModelFallback",
|
|
174
174
|
section: "auditor",
|
|
175
|
-
label: "Auditor
|
|
176
|
-
valueText: show("
|
|
177
|
-
sourceText: src("
|
|
178
|
-
description: "
|
|
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.
|
|
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",
|