pi-goal-list-loop-audit 0.28.14 → 0.28.16

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.
@@ -295,7 +295,7 @@ async function confirmDraft(ctx: ExtensionContext, title: string, body: string):
295
295
  // paused goal — honestly, with a ledger trail. resume: legacy silent
296
296
  // behavior. A new GOAL replacing a paused one archives it in every policy
297
297
  // (one-active-thing: state.goal holds exactly one goal).
298
- function resolveCarryover(ctx: ExtensionContext, trigger: "goal" | "loop"): void {
298
+ function resolveCarryover(ctx: ExtensionContext, trigger: "goal" | "loop" | "list"): void {
299
299
  if (carryoverResolved || !carryoverSnapshot) return;
300
300
  carryoverResolved = true;
301
301
  const snap = carryoverSnapshot;
@@ -305,8 +305,9 @@ function resolveCarryover(ctx: ExtensionContext, trigger: "goal" | "loop"): void
305
305
  const done: string[] = [];
306
306
  const waiting: string[] = [];
307
307
  const pausedGoal = state.goal && state.goal.status === "paused" ? state.goal : null;
308
- if (pausedGoal && (trigger === "goal" || policy === "clear")) {
309
- archiveCurrentGoal(ctx, "aborted", trigger === "goal" ? "replaced by new goal (carryover)" : "carryover cleared");
308
+ // A new goal OR list item replaces the goal slot; a loop leaves it paused.
309
+ if (pausedGoal && (trigger === "goal" || trigger === "list" || policy === "clear")) {
310
+ archiveCurrentGoal(ctx, "aborted", trigger === "loop" ? "carryover cleared" : `replaced by new ${trigger} (carryover)`);
310
311
  done.push(`archived paused goal "${(snap.pausedGoal ?? pausedGoal.objective).slice(0, 60)}"`);
311
312
  } else if (snap.pausedGoal) {
312
313
  waiting.push(`paused goal "${snap.pausedGoal}" (/goal resume)`);
@@ -1025,6 +1026,10 @@ function activateNextListItem(ctx: ExtensionContext, n = 1): boolean {
1025
1026
  appendLedger(ctx.cwd, "list_activation_blocked_loop", {});
1026
1027
  return false;
1027
1028
  }
1029
+ // v0.28.14: carryover resolution runs BEFORE the item is taken — under
1030
+ // carryover=clear the stale queue is dropped first and there is nothing
1031
+ // to activate; under pause the ONE summary precedes the activation.
1032
+ resolveCarryover(ctx, "list");
1028
1033
  const queue = listQueue();
1029
1034
  const taken = takeAt(queue, n);
1030
1035
  if (!taken) return false;
@@ -4226,12 +4231,13 @@ export default function (pi: ExtensionAPI): void {
4226
4231
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
4227
4232
  });
4228
4233
  pi.registerCommand("loop", {
4229
- description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" = infinite metricless loop (no plateau, no cap; ends at time=/tokens= or /loop stop) · /loop respec = infinite metricless reconcile against the root SPEC.md · add measure=\"<cmd>\" direction=min|max [window=5] [max=50] [branch=1] for a metric loop · /loop status · /loop stop. 'Improve until X' is a /goal, not a loop.",
4234
+ description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" = infinite metricless loop (no plateau, no cap; ends at time=/tokens= or /loop stop) · /loop respec = infinite metricless reconcile against the root SPEC.md · add measure=\"<cmd>\" direction=min|max [window=5] [max=50] [branch=1] for a metric loop · /loop status · /loop stop (alias /loop cancel). 'Improve until X' is a /goal, not a loop.",
4230
4235
  getArgumentCompletions: completions([
4231
4236
  ["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50]"],
4232
4237
  ["respec", "infinite metricless loop reconciling the codebase against the root SPEC.md"],
4233
4238
  ["status", "show metric, iteration, best/last values, stall count"],
4234
4239
  ["stop", "end the loop (keeps the best state)"],
4240
+ ["cancel", "alias of /loop stop — end the loop"],
4235
4241
  ["finish", "end the loop cleanly: /loop finish [reason] → stopReason 'completed: <reason>'"],
4236
4242
  ]),
4237
4243
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdLoop(args, ctx); },
@@ -213,12 +213,22 @@ export interface ReviewerOutcome {
213
213
  reportPath?: string;
214
214
  enqueued: number;
215
215
  proposed: number;
216
- /** v0.27.5: the cascade step that actually fired (notify-and-idle | convert-findings-to-list | queue-leftovers | fire-audit-on-clean | propose-goal | aggressive-relaunch | report-only) — surfaces in /goal status and tests. */
216
+ /** v0.27.5: the cascade step that actually fired (notify-and-idle | convert-findings-to-list | queue-leftovers | fire-audit-on-clean | propose-goal | aggressive-relaunch | duplicate-suppressed) — surfaces in /goal status and tests. v0.28.16: "duplicate-suppressed" = the clean completion WAS a regression scan, so the follow-up scan was deduped. */
217
217
  cascadeStep?: string;
218
218
  }
219
219
 
220
220
  export const REVIEWER_REFIRE_WINDOW_MS = 5 * 60_000;
221
221
 
222
+ /** v0.28.16: normalize an objective for duplicate-compare — lowercase,
223
+ * goal-ids (yyyyMMddHHmmss-xxxxxx) become <id>, whitespace collapses. */
224
+ export function normalizeObjective(s: string): string {
225
+ return s
226
+ .toLowerCase()
227
+ .replace(/\b\d{14}-[a-z0-9]{6}\b/g, "<id>")
228
+ .replace(/\s+/g, " ")
229
+ .trim();
230
+ }
231
+
222
232
  /** The reviewer lifecycle (contract item 1). */
223
233
  export function runReviewer(
224
234
  config: ReviewerConfig,
@@ -310,7 +320,16 @@ export function runReviewer(
310
320
  // aggressive mode → relaunch the audit goal directly (no Confirm).
311
321
  if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")) {
312
322
  const auditObjective = `Post-completion regression scan after ${source.goalId} (${config.auditScope})`;
313
- if (aggressive) {
323
+ // v0.28.16: duplicate-scan dedupe — the reviewer fires ON completion,
324
+ // so source.objective IS the most recent completion. If the goal that
325
+ // just completed was itself this same scan (normalized: goal-ids
326
+ // stripped), the proposal would be a scan-of-a-scan — the cascade loop
327
+ // that fired twice on 2026-07-28. Suppress the proposal/enqueue (the
328
+ // report is still written; the ledger records the reason).
329
+ if (normalizeObjective(source.objective) === normalizeObjective(auditObjective)) {
330
+ deps.ledger("reviewer_suppressed", { reason: "duplicate-scan", goalId: source.goalId, objective: auditObjective });
331
+ cascadeStep = "duplicate-suppressed";
332
+ } else if (aggressive) {
314
333
  if (deps.proposeGoal(auditObjective, "aggressive postaudit: clean completion — relaunching the regression scan as /goal")) {
315
334
  proposed++;
316
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.14",
3
+ "version": "0.28.16",
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",