pi-goal-list-loop-audit 0.22.4 → 0.22.6
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.
|
@@ -91,6 +91,11 @@ function makeAuditorResourceLoader(): ResourceLoader {
|
|
|
91
91
|
|
|
92
92
|
function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | undefined, verificationSummary: string | null | undefined): string {
|
|
93
93
|
const goalMd = renderGoalMarkdown(goal);
|
|
94
|
+
// v0.22.6: if a previous audit APPROVED but the regression shield blocked
|
|
95
|
+
// it, tell THIS run exactly which contract items went unreferenced — the
|
|
96
|
+
// auditor quotes evidence for them explicitly and the loop converges
|
|
97
|
+
// instead of repeating the same gap.
|
|
98
|
+
const shieldGaps = [...(goal.auditHistory ?? [])].reverse().find((v) => v.regressionShieldPassed === false)?.regressionShieldMissing;
|
|
94
99
|
return [
|
|
95
100
|
"You are the independent completion auditor for pi-goal-list-loop-audit.",
|
|
96
101
|
"The executor claims the goal is complete. Your job is to decide whether the user's objective is actually satisfied.",
|
|
@@ -125,6 +130,13 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
|
|
|
125
130
|
goal.verificationContract.trim(),
|
|
126
131
|
"</verification_contract>",
|
|
127
132
|
] : []),
|
|
133
|
+
...(shieldGaps && shieldGaps.length > 0 ? [
|
|
134
|
+
"",
|
|
135
|
+
"REGRESSION SHIELD RETRY: a previous audit of yours ended in <approved/>, but the orchestrator blocked it",
|
|
136
|
+
"because the report never referenced these contract items in its evidence:",
|
|
137
|
+
...shieldGaps.map((i) => `- ${i}`),
|
|
138
|
+
"This time, address each of them explicitly: name the item and paste the raw output that proves it.",
|
|
139
|
+
] : []),
|
|
128
140
|
"",
|
|
129
141
|
"Audit checklist:",
|
|
130
142
|
"1. Extract the real success criteria from the objective, including quality/reader outcomes.",
|
|
@@ -95,6 +95,8 @@ export interface AuditVerdict {
|
|
|
95
95
|
error?: string;
|
|
96
96
|
/** regression_shield outcome when the goal had a verification contract. */
|
|
97
97
|
regressionShieldPassed?: boolean;
|
|
98
|
+
/** Contract items the shield found unreferenced (fed into the next audit's prompt, v0.22.6). */
|
|
99
|
+
regressionShieldMissing?: string[];
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
@@ -20,7 +20,10 @@ export function contractItems(contract: string): string[] {
|
|
|
20
20
|
.map((l) => l.trim())
|
|
21
21
|
.map((l) => l.replace(/^(?:done when|verify|verified when|verification|done)\s*:\s*/i, ""))
|
|
22
22
|
.map((l) => l.replace(/^[-*•]\s+/, "").replace(/^\d+[.)]\s+/, ""))
|
|
23
|
-
.filter((l) => l.length > 0)
|
|
23
|
+
.filter((l) => l.length > 0)
|
|
24
|
+
// Boundary lines ("Out of scope: ...") constrain the auditor's judgment;
|
|
25
|
+
// they are not deliverables and have no evidence to quote (v0.22.6).
|
|
26
|
+
.filter((l) => !/^out of scope\b/i.test(l));
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface RegressionShieldResult {
|
|
@@ -29,14 +32,35 @@ export interface RegressionShieldResult {
|
|
|
29
32
|
hasEvidenceBlock: boolean;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** Strip prose punctuation glued to a token ("file/element." → "file/element"). */
|
|
36
|
+
function stripEdgePunct(w: string): string {
|
|
37
|
+
return w.replace(/^[^A-Za-z0-9]+/, "").replace(/[^A-Za-z0-9/_.-]+$/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Is a candidate token present in the report? Compound tokens joined by
|
|
42
|
+
* "-" or "/" (left-cropped, file/element, Phaser/Svelte) count as present
|
|
43
|
+
* when ALL their segments (len >= 3) appear — a good-faith report writes
|
|
44
|
+
* "no cropped strip on the left", not the contract's literal compound.
|
|
45
|
+
*/
|
|
46
|
+
function tokenPresent(candidate: string, reportLower: string): boolean {
|
|
47
|
+
const c = candidate.toLowerCase();
|
|
48
|
+
if (reportLower.includes(c)) return true;
|
|
49
|
+
const segments = c.split(/[-/]+/).filter((s) => s.length >= 3);
|
|
50
|
+
return segments.length > 1 && segments.every((s) => reportLower.includes(s));
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* Check an approved auditor report against the verification contract.
|
|
34
55
|
* Rules (deliberately simple + auditable):
|
|
35
56
|
* 1. The report must contain an <evidence> ... </evidence> block.
|
|
36
|
-
* 2. Every contract item must be referenced inside the report by
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
57
|
+
* 2. Every contract item must be referenced inside the report by ANY of
|
|
58
|
+
* its top-3 longest tokens (>= 5 chars, edge punctuation stripped;
|
|
59
|
+
* compounds match via their segments). v0.22.6: the previous
|
|
60
|
+
* single-longest-word rule false-rejected genuine approvals when the
|
|
61
|
+
* longest word was contract-only vocabulary ("left-cropped") or had
|
|
62
|
+
* prose punctuation glued on ("file/element.") — three real approved
|
|
63
|
+
* audits on hegemon were converted to disapprovals that way.
|
|
40
64
|
*/
|
|
41
65
|
export function checkRegressionShield(report: string, contract: string): RegressionShieldResult {
|
|
42
66
|
const hasEvidenceBlock = /<evidence>[\t\n\r ]*[\s\S]*?<\/evidence>/i.test(report);
|
|
@@ -44,12 +68,16 @@ export function checkRegressionShield(report: string, contract: string): Regress
|
|
|
44
68
|
const missingItems: string[] = [];
|
|
45
69
|
const reportLower = report.toLowerCase();
|
|
46
70
|
for (const item of items) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
const candidates = item
|
|
72
|
+
.split(/[^A-Za-z0-9_.\-/]+/)
|
|
73
|
+
.map(stripEdgePunct)
|
|
74
|
+
.filter((w) => w.length >= 5)
|
|
75
|
+
.sort((a, b) => b.length - a.length)
|
|
76
|
+
.slice(0, 3);
|
|
77
|
+
const addressed = candidates.length > 0
|
|
78
|
+
? candidates.some((c) => tokenPresent(c, reportLower))
|
|
79
|
+
: reportLower.includes(item.toLowerCase());
|
|
80
|
+
if (!addressed) missingItems.push(item);
|
|
53
81
|
}
|
|
54
82
|
return {
|
|
55
83
|
passed: hasEvidenceBlock && missingItems.length === 0,
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -578,6 +578,13 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
578
578
|
? { tokensUsed: state.goal.usage.tokensUsed, tokensLimit: freshLimit }
|
|
579
579
|
: undefined;
|
|
580
580
|
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(usage ? { usage } : {}) }, ctx);
|
|
581
|
+
// v0.22.5: say what was resumed — with a non-empty list this also resumes
|
|
582
|
+
// the queue (the active goal IS the list's head item).
|
|
583
|
+
const queued = listQueue().length;
|
|
584
|
+
ctx.ui.notify(
|
|
585
|
+
`Resumed goal [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list — resuming the list's head)` : ""}`,
|
|
586
|
+
"info",
|
|
587
|
+
);
|
|
581
588
|
scheduleContinuation(ctx, true);
|
|
582
589
|
}
|
|
583
590
|
|
|
@@ -1307,6 +1314,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1307
1314
|
report: result.output,
|
|
1308
1315
|
error: result.error,
|
|
1309
1316
|
regressionShieldPassed: result.regressionShieldPassed,
|
|
1317
|
+
regressionShieldMissing: result.regressionShieldMissing,
|
|
1310
1318
|
});
|
|
1311
1319
|
// Cap history — 39 infra errors taught us unbounded growth is real.
|
|
1312
1320
|
if (history.length > 20) history.splice(0, history.length - 20);
|
|
@@ -1365,6 +1373,29 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1365
1373
|
};
|
|
1366
1374
|
}
|
|
1367
1375
|
|
|
1376
|
+
// Shield-blocked approval (v0.22.6): the auditor APPROVED but the
|
|
1377
|
+
// regression shield found contract items the evidence never
|
|
1378
|
+
// referenced. NOT a verdict on the work — the next audit is told
|
|
1379
|
+
// exactly what to quote. (The hegemon case: three genuine approvals
|
|
1380
|
+
// shield-blocked on vocabulary mismatches read as a "parser bug".)
|
|
1381
|
+
if (result.regressionShieldPassed === false && result.regressionShieldMissing && result.regressionShieldMissing.length > 0) {
|
|
1382
|
+
const missing = result.regressionShieldMissing;
|
|
1383
|
+
updateGoal({
|
|
1384
|
+
status: "active",
|
|
1385
|
+
auditHistory: history,
|
|
1386
|
+
pauseReason: `regression shield: auditor approved, but evidence never referenced ${missing.length} contract item(s)`,
|
|
1387
|
+
pauseSuggestedAction: "call complete_goal again — the next auditor run is told exactly which items to quote evidence for",
|
|
1388
|
+
}, ctx);
|
|
1389
|
+
scheduleContinuation(ctx, true);
|
|
1390
|
+
return {
|
|
1391
|
+
content: [{
|
|
1392
|
+
type: "text",
|
|
1393
|
+
text: `The auditor APPROVED, but the orchestrator's regression shield blocked completion: the report's evidence never referenced these contract items:\n${missing.map((i) => `- ${i}`).join("\n")}\n\nThis is NOT a verdict on your work — do not change your deliverable for this. Call complete_goal again; the next auditor run is explicitly told to quote raw evidence for each of these items.`,
|
|
1394
|
+
}],
|
|
1395
|
+
details: {},
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1368
1399
|
const noContractHint = state.goal.verificationContract?.trim()
|
|
1369
1400
|
? ""
|
|
1370
1401
|
: "\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.";
|
|
@@ -1497,11 +1528,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1497
1528
|
// Multi-item list draft: one Confirm for the whole batch.
|
|
1498
1529
|
if (p.items && p.items.length > 0) {
|
|
1499
1530
|
const preview = p.items.slice(0, 6).map((t, i) => ` ${i + 1}. ${t.slice(0, 60)}`).join("\n");
|
|
1531
|
+
const batchActivates = !state.goal || state.goal.status === "complete" || state.goal.status === "aborted";
|
|
1500
1532
|
let batchConfirmed = false;
|
|
1501
1533
|
try {
|
|
1502
1534
|
batchConfirmed = await liveCtx.ui.confirm(
|
|
1503
1535
|
"Confirm queue batch",
|
|
1504
|
-
`${p.items.length} items:\n${preview}${p.items.length > 6 ? `\n … and ${p.items.length - 6} more` : ""}`,
|
|
1536
|
+
`${p.items.length} items:\n${preview}${p.items.length > 6 ? `\n … and ${p.items.length - 6} more` : ""}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
|
|
1505
1537
|
);
|
|
1506
1538
|
} catch {
|
|
1507
1539
|
batchConfirmed = false;
|
|
@@ -1523,9 +1555,19 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1523
1555
|
const contractBlock = p.verificationContract?.trim()
|
|
1524
1556
|
? `\n\nDone when:\n${p.verificationContract.trim()}`
|
|
1525
1557
|
: "\n\n(No verification contract — the auditor will infer done-criteria from the objective. Consider adding one.)";
|
|
1558
|
+
// v0.22.6: a list draft that will activate immediately must SAY so in
|
|
1559
|
+
// the Confirm dialog — "I started a list and ended up with a running
|
|
1560
|
+
// goal" was a real surprise. Title + trailing note name the outcome.
|
|
1561
|
+
const isListDraft = draftingTarget === "list";
|
|
1562
|
+
const willActivate = isListDraft && (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted");
|
|
1563
|
+
const activationNote = isListDraft
|
|
1564
|
+
? willActivate
|
|
1565
|
+
? "\n\n(List is empty — confirming ACTIVATES this immediately as the active goal. Reject if you only wanted to queue it.)"
|
|
1566
|
+
: "\n\n(Goes into the list, queued behind the active goal.)"
|
|
1567
|
+
: "";
|
|
1526
1568
|
let confirmed = false;
|
|
1527
1569
|
try {
|
|
1528
|
-
confirmed = await liveCtx.ui.confirm("Confirm goal", `${p.objective.trim()}${contractBlock}`);
|
|
1570
|
+
confirmed = await liveCtx.ui.confirm(isListDraft ? "Confirm list item" : "Confirm goal", `${p.objective.trim()}${contractBlock}${activationNote}`);
|
|
1529
1571
|
} catch {
|
|
1530
1572
|
confirmed = false;
|
|
1531
1573
|
}
|
|
@@ -2225,21 +2267,55 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2225
2267
|
// /list — the list (add|show|next|remove|clear)
|
|
2226
2268
|
// /loop — the metric loop (draft|start|status|stop)
|
|
2227
2269
|
// /glla — the settings UI (+ scriptable key=value)
|
|
2270
|
+
// v0.22.5: subcommand autocomplete for the /-menu.
|
|
2271
|
+
const completions = (items: Array<[string, string]>) => (prefix: string) =>
|
|
2272
|
+
items
|
|
2273
|
+
.filter(([value]) => value.startsWith(prefix))
|
|
2274
|
+
.map(([value, description]) => ({ value, label: value, description }));
|
|
2275
|
+
|
|
2228
2276
|
pi.registerCommand("goal", {
|
|
2229
2277
|
description: "Set/draft a goal, or /goal status|pause|resume|cancel|tweak <text>|archive|start <objective>. Objectives without a 'Done when:' clause are grilled into a contract first; include the clause or use /goal start to skip the interview and activate instantly.",
|
|
2278
|
+
getArgumentCompletions: completions([
|
|
2279
|
+
["start", "skip drafting — /goal start <objective> activates immediately"],
|
|
2280
|
+
["status", "show the active goal and its task list"],
|
|
2281
|
+
["pause", "pause the active goal"],
|
|
2282
|
+
["resume", "resume a paused goal (and the list, when items are queued)"],
|
|
2283
|
+
["cancel", "abort the active goal"],
|
|
2284
|
+
["tweak", "change the objective: /goal tweak <text>"],
|
|
2285
|
+
["archive", "list archived goals"],
|
|
2286
|
+
]),
|
|
2230
2287
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdGoal(args, ctx); },
|
|
2231
2288
|
});
|
|
2232
2289
|
const settingsHandler = (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdSettings(args, ctx); };
|
|
2233
2290
|
pi.registerCommand("glla", {
|
|
2234
2291
|
description: "Open the settings UI for goals, loops, lists, and the auditor. Scriptable form: /glla key=value · /glla project key=value",
|
|
2292
|
+
getArgumentCompletions: completions([
|
|
2293
|
+
["model=", "auditor model override: /glla model=provider/id"],
|
|
2294
|
+
["thinking=", "auditor thinking level: /glla thinking=high"],
|
|
2295
|
+
["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
|
|
2296
|
+
["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
|
|
2297
|
+
["autoresume=", "on: auto-resume held goals/loops in fresh sessions"],
|
|
2298
|
+
["project", "write a project override: /glla project key=value"],
|
|
2299
|
+
]),
|
|
2235
2300
|
handler: settingsHandler,
|
|
2236
2301
|
});
|
|
2237
2302
|
pi.registerCommand("list", {
|
|
2238
2303
|
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list next [n] | /list remove <n> | /list clear",
|
|
2304
|
+
getArgumentCompletions: completions([
|
|
2305
|
+
["show", "display the queued items"],
|
|
2306
|
+
["next", "activate the next item (or /list next <n> for position n)"],
|
|
2307
|
+
["remove", "remove an item: /list remove <n>"],
|
|
2308
|
+
["clear", "empty the list"],
|
|
2309
|
+
]),
|
|
2239
2310
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2240
2311
|
});
|
|
2241
2312
|
pi.registerCommand("loop", {
|
|
2242
2313
|
description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50] [time=<hours>] [tokens=<budget>] [branch=1] skips drafting · /loop status · /loop stop. 'Improve until X' is a /goal, not a loop.",
|
|
2314
|
+
getArgumentCompletions: completions([
|
|
2315
|
+
["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50]"],
|
|
2316
|
+
["status", "show metric, iteration, best/last values, stall count"],
|
|
2317
|
+
["stop", "end the loop (keeps the best state)"],
|
|
2318
|
+
]),
|
|
2243
2319
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdLoop(args, ctx); },
|
|
2244
2320
|
});
|
|
2245
2321
|
|
|
@@ -2309,13 +2385,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2309
2385
|
);
|
|
2310
2386
|
scheduleContinuation(ctx, true);
|
|
2311
2387
|
} else {
|
|
2388
|
+
const queued = listQueue().length;
|
|
2389
|
+
const resumeHint = queued > 0
|
|
2390
|
+
? `/goal resume to continue (+${queued} queued in the list) · /glla autoresume=on to auto-resume in this project`
|
|
2391
|
+
: "/goal resume to continue · /glla autoresume=on to auto-resume in this project";
|
|
2312
2392
|
updateGoal({
|
|
2313
2393
|
status: "paused",
|
|
2314
2394
|
pauseReason: "restored in a fresh session — no work started",
|
|
2315
|
-
pauseSuggestedAction:
|
|
2395
|
+
pauseSuggestedAction: resumeHint,
|
|
2316
2396
|
}, ctx);
|
|
2317
2397
|
ctx.ui.notify(
|
|
2318
|
-
`Goal held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)} — /goal resume to continue.`,
|
|
2398
|
+
`Goal held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""} — /goal resume to continue.`,
|
|
2319
2399
|
"info",
|
|
2320
2400
|
);
|
|
2321
2401
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.6",
|
|
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",
|