pi-goal-list-loop-audit 0.24.1 → 0.24.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.
- package/README.md +1 -0
- package/extensions/goal-loop-auditor.ts +17 -9
- package/extensions/goal-loop-core.ts +16 -1
- package/extensions/goal-loop-shield.ts +18 -0
- package/extensions/loops/goal.ts +69 -1
- package/package.json +1 -1
- package/prompts/goal-loop-forever-metricless.md +2 -0
- package/prompts/goal-loop-forever.md +2 -0
package/README.md
CHANGED
|
@@ -199,6 +199,7 @@ No external watchdog plugin needed.
|
|
|
199
199
|
/glla tokenlimit=0 # explicitly no cap (the default)
|
|
200
200
|
/glla wedgealert=30 # hung-command alert minutes (default: 30, 0 = off)
|
|
201
201
|
/glla autoresume=on # held goals/loops auto-resume in fresh sessions (unattended rigs)
|
|
202
|
+
/glla auditcap=5 # pause the goal after N consecutive auditor disapprovals (default 3, 0 = unlimited)
|
|
202
203
|
/glla autoaccept=on # drafts ACTIVATE without the Confirm dialog (unattended rigs)
|
|
203
204
|
/glla project tokenlimit=500 # rare per-project override
|
|
204
205
|
```
|
|
@@ -34,6 +34,9 @@ import { AUDITOR_STALL_MS } from "./goal-loop-backoff.js";
|
|
|
34
34
|
export interface GoalAuditorResult {
|
|
35
35
|
approved: boolean;
|
|
36
36
|
disapproved: boolean;
|
|
37
|
+
/** v0.24.2: third verdict — the goal can NEVER be satisfied as stated. */
|
|
38
|
+
impossible?: boolean;
|
|
39
|
+
impossibleReason?: string;
|
|
37
40
|
output: string;
|
|
38
41
|
model: string;
|
|
39
42
|
thinkingLevel?: ThinkingLevel;
|
|
@@ -107,6 +110,8 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
|
|
|
107
110
|
"Return a concise audit report. The final line MUST be exactly one of:",
|
|
108
111
|
"<approved/>",
|
|
109
112
|
"<disapproved/>",
|
|
113
|
+
"<impossible>one-line reason</impossible>",
|
|
114
|
+
"Use <impossible> ONLY when the objective can NEVER be satisfied as stated — contradictory requirements, a premise that is factually wrong, or resources the agent can never obtain. Incomplete or shoddy work is <disapproved/>, not impossible.",
|
|
110
115
|
"",
|
|
111
116
|
"Goal markdown (full state):",
|
|
112
117
|
"<goal>",
|
|
@@ -149,7 +154,7 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
|
|
|
149
154
|
? ["4. Verify that the executor has satisfied every item in the <verification_contract>. If any item is missing or weakly addressed, disapprove."]
|
|
150
155
|
: []),
|
|
151
156
|
"5. Explain missing or weak evidence, especially scaffold-vs-final quality gaps.",
|
|
152
|
-
"6. End with exactly <approved/> only if the objective is truly complete; otherwise end with exactly <disapproved/>.",
|
|
157
|
+
"6. End with exactly <approved/> only if the objective is truly complete; <impossible>reason</impossible> if it can never be satisfied as stated; otherwise end with exactly <disapproved/>.",
|
|
153
158
|
...(goal.verificationContract?.trim()
|
|
154
159
|
? [
|
|
155
160
|
"",
|
|
@@ -175,8 +180,8 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
|
|
|
175
180
|
|
|
176
181
|
// regression_shield lives in goal-loop-shield.ts (dependency-free, so unit
|
|
177
182
|
// tests can import it without pulling in pi). Re-exported for callers.
|
|
178
|
-
export { checkRegressionShield, contractItems, type RegressionShieldResult } from "./goal-loop-shield.js";
|
|
179
|
-
import { checkRegressionShield } from "./goal-loop-shield.js";
|
|
183
|
+
export { checkRegressionShield, contractItems, parseAuditorVerdict, type RegressionShieldResult } from "./goal-loop-shield.js";
|
|
184
|
+
import { checkRegressionShield, parseAuditorVerdict } from "./goal-loop-shield.js";
|
|
180
185
|
|
|
181
186
|
// =================================================================
|
|
182
187
|
// Auditor entry point
|
|
@@ -354,18 +359,19 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
354
359
|
};
|
|
355
360
|
}
|
|
356
361
|
|
|
357
|
-
const
|
|
358
|
-
const approved =
|
|
359
|
-
const disapproved =
|
|
362
|
+
const parsed = parseAuditorVerdict(outputParts.join("\n\n"));
|
|
363
|
+
const approved = parsed.approved;
|
|
364
|
+
const disapproved = parsed.disapproved;
|
|
365
|
+
const impossible = parsed.impossible;
|
|
360
366
|
|
|
361
|
-
if (!approved && !disapproved) {
|
|
367
|
+
if (!approved && !disapproved && !impossible) {
|
|
362
368
|
return {
|
|
363
369
|
approved: false,
|
|
364
370
|
disapproved: false,
|
|
365
371
|
output,
|
|
366
372
|
model: modelLabel(model),
|
|
367
373
|
thinkingLevel,
|
|
368
|
-
error: `Auditor produced no verdict marker (<approved/>/<disapproved
|
|
374
|
+
error: `Auditor produced no verdict marker (<approved/>/<disapproved/>/<impossible>)${streamError ? ` — stream error: ${streamError}` : ""}. Treating as an error, not a verdict.`,
|
|
369
375
|
};
|
|
370
376
|
}
|
|
371
377
|
|
|
@@ -407,6 +413,8 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
407
413
|
return {
|
|
408
414
|
approved,
|
|
409
415
|
disapproved,
|
|
416
|
+
impossible,
|
|
417
|
+
impossibleReason: parsed.impossibleReason,
|
|
410
418
|
output,
|
|
411
419
|
model: modelLabel(model),
|
|
412
420
|
thinkingLevel,
|
|
@@ -416,7 +424,7 @@ export async function runGoalCompletionAuditor(args: {
|
|
|
416
424
|
|
|
417
425
|
progress.phase = "complete";
|
|
418
426
|
emitProgress();
|
|
419
|
-
return { approved, disapproved, output, model: modelLabel(model), thinkingLevel };
|
|
427
|
+
return { approved, disapproved, impossible, impossibleReason: parsed.impossibleReason, output, model: modelLabel(model), thinkingLevel };
|
|
420
428
|
} catch (err) {
|
|
421
429
|
// v0.11.1 (audit critical): a runtime exception is INFRASTRUCTURE, never
|
|
422
430
|
// a verdict. The three-way split identifies infra by `error &&
|
|
@@ -88,6 +88,9 @@ export interface AuditVerdict {
|
|
|
88
88
|
at: string;
|
|
89
89
|
approved: boolean;
|
|
90
90
|
disapproved: boolean;
|
|
91
|
+
/** v0.24.2: the auditor's third verdict — the goal can NEVER be satisfied as stated. */
|
|
92
|
+
impossible?: boolean;
|
|
93
|
+
impossibleReason?: string;
|
|
91
94
|
model: string;
|
|
92
95
|
thinkingLevel?: string;
|
|
93
96
|
report?: string;
|
|
@@ -368,6 +371,18 @@ export interface State {
|
|
|
368
371
|
loop?: import("./goal-loop-forever.js").LoopState;
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
/** v0.24.2: count TRAILING consecutive disapprovals (the disapproval-cap
|
|
375
|
+
* input). Shield-blocks (approved:true) and infra errors (neither flag)
|
|
376
|
+
* break the streak — they are not verdicts on the work. */
|
|
377
|
+
export function countTrailingDisapprovals(history: AuditVerdict[]): number {
|
|
378
|
+
let n = 0;
|
|
379
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
380
|
+
if (history[i]!.disapproved) n++;
|
|
381
|
+
else break;
|
|
382
|
+
}
|
|
383
|
+
return n;
|
|
384
|
+
}
|
|
385
|
+
|
|
371
386
|
/** Default per-goal token budget (v0.9.7): a runaway threshold, not a
|
|
372
387
|
* "big goal" threshold — real research/feature goals legitimately burn 2-4M.
|
|
373
388
|
* Loop 3 doesn't rely on this cap (it has max-iterations + plateau brakes). */
|
|
@@ -499,7 +514,7 @@ export function renderGoalMarkdown(goal: Goal): string {
|
|
|
499
514
|
lines.push("## Audit history");
|
|
500
515
|
lines.push("");
|
|
501
516
|
for (const v of goal.auditHistory) {
|
|
502
|
-
lines.push(`- ${v.at} — ${v.approved ? "approved" : "disapproved"} — \`${v.model}\``);
|
|
517
|
+
lines.push(`- ${v.at} — ${v.approved ? "approved" : v.impossible ? "impossible" : "disapproved"} — \`${v.model}\``);
|
|
503
518
|
}
|
|
504
519
|
lines.push("");
|
|
505
520
|
}
|
|
@@ -94,3 +94,21 @@ export function checkRegressionShield(report: string, contract: string): Regress
|
|
|
94
94
|
hasEvidenceBlock,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* v0.24.2: pure auditor-verdict parser (approved / disapproved / impossible).
|
|
100
|
+
* Lives here (not goal-loop-auditor.ts) so tests can import it without
|
|
101
|
+
* dragging in the auditor's relative .js imports. The verdict is read from
|
|
102
|
+
* the last output block that mentions any verdict tag.
|
|
103
|
+
*/
|
|
104
|
+
export function parseAuditorVerdict(output: string): { approved: boolean; disapproved: boolean; impossible: boolean; impossibleReason?: string } {
|
|
105
|
+
const parts = output.split("\n\n");
|
|
106
|
+
const lastAssistant = [...parts].reverse().find((t) => /<\/?(approved|disapproved|impossible)[ />]/i.test(t)) ?? output;
|
|
107
|
+
const impossibleMatch = /<impossible>([\s\S]*?)<\/impossible>/i.exec(lastAssistant);
|
|
108
|
+
return {
|
|
109
|
+
approved: /<approved\/>/i.test(lastAssistant),
|
|
110
|
+
disapproved: /<disapproved\/>/i.test(lastAssistant),
|
|
111
|
+
impossible: impossibleMatch !== null,
|
|
112
|
+
impossibleReason: impossibleMatch?.[1]?.trim().slice(0, 300) || undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
LIST_DRAFTING_BLOCK_MESSAGE,
|
|
41
41
|
sumNewAssistantTokens,
|
|
42
42
|
takeAt,
|
|
43
|
+
countTrailingDisapprovals,
|
|
43
44
|
goalArgsNeedDrafting,
|
|
44
45
|
buildSeedGrillMessage,
|
|
45
46
|
askUserQuestionAnswered,
|
|
@@ -1477,6 +1478,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1477
1478
|
at: nowIso(),
|
|
1478
1479
|
approved: result.approved,
|
|
1479
1480
|
disapproved: result.disapproved,
|
|
1481
|
+
impossible: result.impossible,
|
|
1482
|
+
impossibleReason: result.impossibleReason,
|
|
1480
1483
|
model: result.model,
|
|
1481
1484
|
thinkingLevel: result.thinkingLevel,
|
|
1482
1485
|
report: result.output,
|
|
@@ -1521,6 +1524,30 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1521
1524
|
return { content: [{ type: "text", text: `Goal approved by auditor ${result.model}.` }], details: {} };
|
|
1522
1525
|
}
|
|
1523
1526
|
|
|
1527
|
+
// IMPOSSIBLE (v0.24.2, Claude-Code lesson): the auditor's escape hatch
|
|
1528
|
+
// for goals that can NEVER be satisfied as stated. Not a disapproval —
|
|
1529
|
+
// continuing would burn tokens on a provably unwinnable objective.
|
|
1530
|
+
// Bounded and surfaced: the goal pauses and the user decides.
|
|
1531
|
+
if (result.impossible) {
|
|
1532
|
+
const reason = result.impossibleReason || "(no reason given)";
|
|
1533
|
+
updateGoal({
|
|
1534
|
+
status: "paused",
|
|
1535
|
+
auditHistory: history,
|
|
1536
|
+
pauseReason: `auditor verdict: IMPOSSIBLE — ${reason}`,
|
|
1537
|
+
pauseSuggestedAction: "The auditor says this goal can never be satisfied as stated. /goal tweak the objective (or /goal cancel), then /goal resume.",
|
|
1538
|
+
}, ctx);
|
|
1539
|
+
ctx.ui.notify(`Auditor: goal IMPOSSIBLE — ${reason}. Goal paused; /goal tweak or /goal cancel, then /goal resume.`, "warning");
|
|
1540
|
+
appendLedger(ctx.cwd, "goal_paused", { reason: `auditor impossible: ${reason}` });
|
|
1541
|
+
notifyExternal(ctx, `Goal paused (auditor: impossible): ${reason.slice(0, 120)}`);
|
|
1542
|
+
return {
|
|
1543
|
+
content: [{
|
|
1544
|
+
type: "text",
|
|
1545
|
+
text: `The auditor's verdict is IMPOSSIBLE: ${reason}\n\nThis is not a disapproval — the auditor says the objective can never be satisfied as stated. The goal is now PAUSED. Do not call complete_goal again. Report the verdict to the user and suggest /goal tweak (narrow or correct the objective) or /goal cancel.`,
|
|
1546
|
+
}],
|
|
1547
|
+
details: {},
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1524
1551
|
// THREE-WAY SPLIT (v0.9.9): infrastructure failure is NOT a verdict.
|
|
1525
1552
|
// The wild-caught case: 6 silent "disapprovals" that were really a dead
|
|
1526
1553
|
// auditor model. The agent must be able to tell the difference.
|
|
@@ -1567,6 +1594,30 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1567
1594
|
const noContractHint = state.goal.verificationContract?.trim()
|
|
1568
1595
|
? ""
|
|
1569
1596
|
: "\n\nNote: this goal has no verification contract, so the auditor inferred done-criteria from the objective text. For sharper verdicts, /goal tweak the objective to add a 'Done when: ...' clause.";
|
|
1597
|
+
// v0.24.2 (Claude-Code lesson — their stop-hook blocks cap at 8): a
|
|
1598
|
+
// goal the auditor can NEVER approve used to re-continue forever.
|
|
1599
|
+
// auditCap consecutive disapprovals → pause + notify, bounded and
|
|
1600
|
+
// surfaced like every other stop in this stack.
|
|
1601
|
+
const auditCap = loadSettings(ctx.cwd).auditCap ?? 3;
|
|
1602
|
+
const trailingDisapprovals = countTrailingDisapprovals(history);
|
|
1603
|
+
if (auditCap > 0 && trailingDisapprovals >= auditCap) {
|
|
1604
|
+
updateGoal({
|
|
1605
|
+
status: "paused",
|
|
1606
|
+
auditHistory: history,
|
|
1607
|
+
pauseReason: `auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap})`,
|
|
1608
|
+
pauseSuggestedAction: "Read the audit history (/goal status), fix the actual gap or /goal tweak the objective, then /goal resume. Raise the cap with /glla auditcap=N.",
|
|
1609
|
+
}, ctx);
|
|
1610
|
+
ctx.ui.notify(`Goal paused: auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap}). /goal status for the reports; /goal resume to continue.`, "warning");
|
|
1611
|
+
appendLedger(ctx.cwd, "goal_paused", { reason: `disapproval cap: ${trailingDisapprovals} consecutive (cap ${auditCap})` });
|
|
1612
|
+
notifyExternal(ctx, `Goal paused: ${trailingDisapprovals} consecutive auditor disapprovals`);
|
|
1613
|
+
return {
|
|
1614
|
+
content: [{
|
|
1615
|
+
type: "text",
|
|
1616
|
+
text: `The auditor has now disapproved ${trailingDisapprovals} times in a row (cap ${auditCap}). The goal is PAUSED — continuing to re-attempt without addressing the pattern wastes tokens. Latest report (first 800 chars):\n${result.output.slice(0, 800)}\n\nDo not call complete_goal again. Summarize the repeated objections for the user and ask how to proceed (/goal status shows all reports; /goal resume resumes).`,
|
|
1617
|
+
}],
|
|
1618
|
+
details: {},
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1570
1621
|
updateGoal({
|
|
1571
1622
|
status: "active",
|
|
1572
1623
|
auditHistory: history,
|
|
@@ -2146,6 +2197,8 @@ interface Settings {
|
|
|
2146
2197
|
/** on → restored goals/loops/lists auto-resume even in fresh sessions
|
|
2147
2198
|
* (unattended rigs). Default off: restore holds until /goal resume. */
|
|
2148
2199
|
autoResume?: boolean;
|
|
2200
|
+
/** v0.24.2: pause the goal after N consecutive auditor disapprovals (0 = unlimited). Default 3. */
|
|
2201
|
+
auditCap?: number;
|
|
2149
2202
|
/** on → propose_* drafts activate WITHOUT the Confirm dialog and the
|
|
2150
2203
|
* interview floor is skipped — the seed carries the intent (unattended
|
|
2151
2204
|
* rigs). Default off: nothing activates before the user confirms. */
|
|
@@ -2194,7 +2247,7 @@ function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknow
|
|
|
2194
2247
|
const glob = readSettingsFile(globalSettingsPath());
|
|
2195
2248
|
const effective = loadSettings(cwd);
|
|
2196
2249
|
const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
|
|
2197
|
-
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume", "autoAcceptDrafts"];
|
|
2250
|
+
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume", "autoAcceptDrafts", "auditCap"];
|
|
2198
2251
|
for (const k of keys) {
|
|
2199
2252
|
if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
|
|
2200
2253
|
else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
|
|
@@ -2365,6 +2418,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2365
2418
|
fmt("tokenLimit", "tokenLimit"),
|
|
2366
2419
|
fmt("autoResume", "autoResume"),
|
|
2367
2420
|
fmt("autoAcceptDrafts", "autoAccept"),
|
|
2421
|
+
fmt("auditCap", "auditCap"),
|
|
2368
2422
|
`\nglobal: ${globalSettingsPath()}`,
|
|
2369
2423
|
`project: ${projectSettingsPath(ctx.cwd)}`,
|
|
2370
2424
|
`Set with: /glla key=value (global) · /glla project key=value (project override)`,
|
|
@@ -2437,6 +2491,19 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2437
2491
|
} else {
|
|
2438
2492
|
ctx.ui.notify(`autoaccept must be on or off, got: ${value}`, "warning");
|
|
2439
2493
|
}
|
|
2494
|
+
} else if (key === "auditcap") {
|
|
2495
|
+
if (["off", "unset", "default"].includes(value)) {
|
|
2496
|
+
patch.auditCap = undefined;
|
|
2497
|
+
changed = true;
|
|
2498
|
+
} else {
|
|
2499
|
+
const n = Number.parseInt(value, 10);
|
|
2500
|
+
if (Number.isInteger(n) && n >= 0) {
|
|
2501
|
+
patch.auditCap = n;
|
|
2502
|
+
changed = true;
|
|
2503
|
+
} else {
|
|
2504
|
+
ctx.ui.notify(`auditcap must be a non-negative integer (0 = unlimited), got: ${value}`, "warning");
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2440
2507
|
} else if (key === "thinking" || key === "auditorthinkinglevel") {
|
|
2441
2508
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
2442
2509
|
patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
|
|
@@ -2568,6 +2635,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2568
2635
|
["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
|
|
2569
2636
|
["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
|
|
2570
2637
|
["autoresume=", "on: auto-resume held goals/loops in fresh sessions"],
|
|
2638
|
+
["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 3, 0 = unlimited)"],
|
|
2571
2639
|
["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
|
|
2572
2640
|
["project", "write a project override: /glla project key=value"],
|
|
2573
2641
|
]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
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",
|