pi-goal-list-loop-audit 0.22.5 → 0.22.7
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
|
/**
|
|
@@ -93,7 +93,7 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
|
|
|
93
93
|
return `glla: ${paint(theme, "accent", "auditing…")}${tool}`;
|
|
94
94
|
}
|
|
95
95
|
if (g.status === "paused") {
|
|
96
|
-
const label =
|
|
96
|
+
const label = `${g.policy} paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
|
|
97
97
|
return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
|
|
98
98
|
}
|
|
99
99
|
if (g.status === "active") {
|
|
@@ -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
|
@@ -565,6 +565,12 @@ async function cmdStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
565
565
|
async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
566
566
|
if (!state.goal) return;
|
|
567
567
|
updateGoal({ status: "paused" }, ctx);
|
|
568
|
+
// v0.22.7: name WHAT was paused — a list item resumes through /list.
|
|
569
|
+
if (state.goal.policy === "list") {
|
|
570
|
+
const queued = listQueue().length;
|
|
571
|
+
ctx.ui.notify(`List item ${state.goal.id} paused${queued > 0 ? ` (${queued} queued in the list)` : ""}. /list resume to continue.`, "info");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
568
574
|
ctx.ui.notify(`Goal ${state.goal.id} paused. /goal resume to continue.`, "info");
|
|
569
575
|
}
|
|
570
576
|
|
|
@@ -580,9 +586,13 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
580
586
|
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(usage ? { usage } : {}) }, ctx);
|
|
581
587
|
// v0.22.5: say what was resumed — with a non-empty list this also resumes
|
|
582
588
|
// the queue (the active goal IS the list's head item).
|
|
589
|
+
// v0.22.7: name WHAT was resumed — list items resume through /list.
|
|
583
590
|
const queued = listQueue().length;
|
|
591
|
+
const isListItem = state.goal.policy === "list";
|
|
584
592
|
ctx.ui.notify(
|
|
585
|
-
|
|
593
|
+
isListItem
|
|
594
|
+
? `Resumed list item [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""}`
|
|
595
|
+
: `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
596
|
"info",
|
|
587
597
|
);
|
|
588
598
|
scheduleContinuation(ctx, true);
|
|
@@ -730,6 +740,22 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
730
740
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
731
741
|
const rest = args.trim().slice(sub.length).trim();
|
|
732
742
|
|
|
743
|
+
if (sub === "resume") {
|
|
744
|
+
// Resume the list's head. The head activates AS the active goal, so this
|
|
745
|
+
// is the same motion as /goal resume — named for the surface the user is
|
|
746
|
+
// looking at (v0.22.7: "we would just unpause, and that is next").
|
|
747
|
+
if (!state.goal || state.goal.status !== "paused") {
|
|
748
|
+
ctx.ui.notify("No paused list item to resume. /list show to see the queue.", "info");
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (state.goal.policy !== "list") {
|
|
752
|
+
ctx.ui.notify("The paused goal didn't come from the list — /goal resume to continue it.", "info");
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
await cmdResume(ctx);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
733
759
|
if (!sub || sub === "show") {
|
|
734
760
|
const queue = listQueue();
|
|
735
761
|
const lines: string[] = [];
|
|
@@ -1314,6 +1340,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1314
1340
|
report: result.output,
|
|
1315
1341
|
error: result.error,
|
|
1316
1342
|
regressionShieldPassed: result.regressionShieldPassed,
|
|
1343
|
+
regressionShieldMissing: result.regressionShieldMissing,
|
|
1317
1344
|
});
|
|
1318
1345
|
// Cap history — 39 infra errors taught us unbounded growth is real.
|
|
1319
1346
|
if (history.length > 20) history.splice(0, history.length - 20);
|
|
@@ -1372,6 +1399,29 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1372
1399
|
};
|
|
1373
1400
|
}
|
|
1374
1401
|
|
|
1402
|
+
// Shield-blocked approval (v0.22.6): the auditor APPROVED but the
|
|
1403
|
+
// regression shield found contract items the evidence never
|
|
1404
|
+
// referenced. NOT a verdict on the work — the next audit is told
|
|
1405
|
+
// exactly what to quote. (The hegemon case: three genuine approvals
|
|
1406
|
+
// shield-blocked on vocabulary mismatches read as a "parser bug".)
|
|
1407
|
+
if (result.regressionShieldPassed === false && result.regressionShieldMissing && result.regressionShieldMissing.length > 0) {
|
|
1408
|
+
const missing = result.regressionShieldMissing;
|
|
1409
|
+
updateGoal({
|
|
1410
|
+
status: "active",
|
|
1411
|
+
auditHistory: history,
|
|
1412
|
+
pauseReason: `regression shield: auditor approved, but evidence never referenced ${missing.length} contract item(s)`,
|
|
1413
|
+
pauseSuggestedAction: "call complete_goal again — the next auditor run is told exactly which items to quote evidence for",
|
|
1414
|
+
}, ctx);
|
|
1415
|
+
scheduleContinuation(ctx, true);
|
|
1416
|
+
return {
|
|
1417
|
+
content: [{
|
|
1418
|
+
type: "text",
|
|
1419
|
+
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.`,
|
|
1420
|
+
}],
|
|
1421
|
+
details: {},
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1375
1425
|
const noContractHint = state.goal.verificationContract?.trim()
|
|
1376
1426
|
? ""
|
|
1377
1427
|
: "\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.";
|
|
@@ -1504,11 +1554,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1504
1554
|
// Multi-item list draft: one Confirm for the whole batch.
|
|
1505
1555
|
if (p.items && p.items.length > 0) {
|
|
1506
1556
|
const preview = p.items.slice(0, 6).map((t, i) => ` ${i + 1}. ${t.slice(0, 60)}`).join("\n");
|
|
1557
|
+
const batchActivates = !state.goal || state.goal.status === "complete" || state.goal.status === "aborted";
|
|
1507
1558
|
let batchConfirmed = false;
|
|
1508
1559
|
try {
|
|
1509
1560
|
batchConfirmed = await liveCtx.ui.confirm(
|
|
1510
1561
|
"Confirm queue batch",
|
|
1511
|
-
`${p.items.length} items:\n${preview}${p.items.length > 6 ? `\n … and ${p.items.length - 6} more` : ""}`,
|
|
1562
|
+
`${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.)" : ""}`,
|
|
1512
1563
|
);
|
|
1513
1564
|
} catch {
|
|
1514
1565
|
batchConfirmed = false;
|
|
@@ -1530,9 +1581,19 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1530
1581
|
const contractBlock = p.verificationContract?.trim()
|
|
1531
1582
|
? `\n\nDone when:\n${p.verificationContract.trim()}`
|
|
1532
1583
|
: "\n\n(No verification contract — the auditor will infer done-criteria from the objective. Consider adding one.)";
|
|
1584
|
+
// v0.22.6: a list draft that will activate immediately must SAY so in
|
|
1585
|
+
// the Confirm dialog — "I started a list and ended up with a running
|
|
1586
|
+
// goal" was a real surprise. Title + trailing note name the outcome.
|
|
1587
|
+
const isListDraft = draftingTarget === "list";
|
|
1588
|
+
const willActivate = isListDraft && (!state.goal || state.goal.status === "complete" || state.goal.status === "aborted");
|
|
1589
|
+
const activationNote = isListDraft
|
|
1590
|
+
? willActivate
|
|
1591
|
+
? "\n\n(List is empty — confirming ACTIVATES this immediately as the active goal. Reject if you only wanted to queue it.)"
|
|
1592
|
+
: "\n\n(Goes into the list, queued behind the active goal.)"
|
|
1593
|
+
: "";
|
|
1533
1594
|
let confirmed = false;
|
|
1534
1595
|
try {
|
|
1535
|
-
confirmed = await liveCtx.ui.confirm("Confirm goal", `${p.objective.trim()}${contractBlock}`);
|
|
1596
|
+
confirmed = await liveCtx.ui.confirm(isListDraft ? "Confirm list item" : "Confirm goal", `${p.objective.trim()}${contractBlock}${activationNote}`);
|
|
1536
1597
|
} catch {
|
|
1537
1598
|
confirmed = false;
|
|
1538
1599
|
}
|
|
@@ -2265,9 +2326,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2265
2326
|
handler: settingsHandler,
|
|
2266
2327
|
});
|
|
2267
2328
|
pi.registerCommand("list", {
|
|
2268
|
-
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",
|
|
2329
|
+
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 resume | /list next [n] | /list remove <n> | /list clear",
|
|
2269
2330
|
getArgumentCompletions: completions([
|
|
2270
2331
|
["show", "display the queued items"],
|
|
2332
|
+
["resume", "resume the paused list item (the list's head)"],
|
|
2271
2333
|
["next", "activate the next item (or /list next <n> for position n)"],
|
|
2272
2334
|
["remove", "remove an item: /list remove <n>"],
|
|
2273
2335
|
["clear", "empty the list"],
|
|
@@ -2345,29 +2407,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2345
2407
|
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
2346
2408
|
if (autoResume) {
|
|
2347
2409
|
ctx.ui.notify(
|
|
2348
|
-
`Resuming goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2410
|
+
`Resuming ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2349
2411
|
"info",
|
|
2350
2412
|
);
|
|
2351
2413
|
scheduleContinuation(ctx, true);
|
|
2352
2414
|
} else {
|
|
2353
2415
|
const queued = listQueue().length;
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2416
|
+
// v0.22.7: name WHAT is held — a list head resumes through /list.
|
|
2417
|
+
const isListItem = state.goal.policy === "list";
|
|
2418
|
+
const resumeCmd = isListItem ? "/list resume" : "/goal resume";
|
|
2419
|
+
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} queued in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
|
|
2357
2420
|
updateGoal({
|
|
2358
2421
|
status: "paused",
|
|
2359
2422
|
pauseReason: "restored in a fresh session — no work started",
|
|
2360
2423
|
pauseSuggestedAction: resumeHint,
|
|
2361
2424
|
}, ctx);
|
|
2362
2425
|
ctx.ui.notify(
|
|
2363
|
-
|
|
2426
|
+
`${isListItem ? "List item" : "Goal"} held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""} — ${resumeCmd} to continue.`,
|
|
2364
2427
|
"info",
|
|
2365
2428
|
);
|
|
2366
2429
|
}
|
|
2367
2430
|
} else if (state.goal && state.goal.status === "active") {
|
|
2368
2431
|
// Active but autoContinue off: nothing auto-fires — just surface it.
|
|
2369
2432
|
ctx.ui.notify(
|
|
2370
|
-
`Restored goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2433
|
+
`Restored ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2371
2434
|
"info",
|
|
2372
2435
|
);
|
|
2373
2436
|
} else if ((!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") && listQueue().length > 0) {
|
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.7",
|
|
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",
|