pi-goal-list-loop-audit 0.22.5 → 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 a
37
- * distinctive token (the item's longest word >= 5 chars, or the full
38
- * item if shorter) a cheap, honest proxy for "the auditor addressed
39
- * this item".
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
- // Distinctive token: longest word >= 5 chars in the item; fall back to the
48
- // whole item (short items like "npm test" are matched whole).
49
- const words = item.split(/[^A-Za-z0-9_.\-/]+/).filter(Boolean);
50
- const distinctive = words.reduce((a, b) => (b.length >= 5 && b.length > a.length ? b : a), "");
51
- const needle = (distinctive || item).toLowerCase();
52
- if (!reportLower.includes(needle)) missingItems.push(item);
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,
@@ -1314,6 +1314,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1314
1314
  report: result.output,
1315
1315
  error: result.error,
1316
1316
  regressionShieldPassed: result.regressionShieldPassed,
1317
+ regressionShieldMissing: result.regressionShieldMissing,
1317
1318
  });
1318
1319
  // Cap history — 39 infra errors taught us unbounded growth is real.
1319
1320
  if (history.length > 20) history.splice(0, history.length - 20);
@@ -1372,6 +1373,29 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1372
1373
  };
1373
1374
  }
1374
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
+
1375
1399
  const noContractHint = state.goal.verificationContract?.trim()
1376
1400
  ? ""
1377
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.";
@@ -1504,11 +1528,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1504
1528
  // Multi-item list draft: one Confirm for the whole batch.
1505
1529
  if (p.items && p.items.length > 0) {
1506
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";
1507
1532
  let batchConfirmed = false;
1508
1533
  try {
1509
1534
  batchConfirmed = await liveCtx.ui.confirm(
1510
1535
  "Confirm queue batch",
1511
- `${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.)" : ""}`,
1512
1537
  );
1513
1538
  } catch {
1514
1539
  batchConfirmed = false;
@@ -1530,9 +1555,19 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1530
1555
  const contractBlock = p.verificationContract?.trim()
1531
1556
  ? `\n\nDone when:\n${p.verificationContract.trim()}`
1532
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
+ : "";
1533
1568
  let confirmed = false;
1534
1569
  try {
1535
- 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}`);
1536
1571
  } catch {
1537
1572
  confirmed = false;
1538
1573
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.22.5",
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",