pi-goal-list-loop-audit 0.29.23 → 0.31.0

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.
@@ -478,6 +478,75 @@ export function countOpenAuditFindings(cwd: string): number {
478
478
  * reasonable answers exist) are presented, never touched. DECIDE lines use
479
479
  * "- [?]" so they never inflate the loop's open-findings measure.
480
480
  */
481
+ /** v0.31.0: /list audit — the collect-then-drain audit (user design
482
+ * 2026-07-31: "this command could run a project audit, collect a bunch of
483
+ * tasks, then do them all too"). Division of labor:
484
+ * /goal audit — one audited unit: audit + fix in the SAME pass (small scopes).
485
+ * /list audit — audit once, then every open finding becomes its own queued,
486
+ * individually-audited list item (the actionables stop living
487
+ * only inside findings.md — the user's "audits don't make a
488
+ * list of actionables" pain).
489
+ * /loop audit — forever fix-first cadence for living codebases.
490
+ * The collection item FIXES NOTHING: every fix lands as its own list item with
491
+ * its own isolated audit trail. DECIDE findings are presented at collection
492
+ * completion, never queued — a decision is not a task (the agent can't "do" it).
493
+ * The marker survives restarts inside the objective itself (no schema change):
494
+ * the completion fan-out matches on it.
495
+ */
496
+ export const LIST_AUDIT_COLLECT_MARKER = "[LIST-AUDIT-COLLECT]";
497
+
498
+ export function listAuditCollectTarget(focus?: string): string {
499
+ const scope = focus && focus.trim() ? focus.trim() : "the whole project";
500
+ return `${LIST_AUDIT_COLLECT_MARKER} Run ONE project audit pass that COLLECTS work — the follow-up fixes are queued as separate list items, so this pass changes no code. Scope: ${scope}. (1) Run a FRESH audit pass over the codebase — spawn Explore subagents for breadth — hunting real problems: bugs, broken flows, regressions, drift between docs and code, dead code, security holes. Not style nits, not speculative refactors. (2) Append every NEW finding to ${AUDIT_FINDINGS_REL} (create the file on the first finding; append-only — never delete, rewrite, or reorder existing lines; never re-report a finding already listed), classified: "- [ ] FIX: SEVERITY: short description (file:line)" for bugs and polish — and "- [?] DECIDE: short description (what the choice is, what each side costs)" for direction, trade-offs, and scope questions where two reasonable answers exist. (3) Change NOTHING — no fixes, no refactors, no drive-by edits: the orchestrator queues each open FIX finding as its own list item after this pass completes, and each fix lands with its own commit and its own audit. (4) DECIDE findings are listed in the completion report — they are presented to the user, never queued and never silently fixed. (5) Honesty law: never fabricate findings to look busy; if the pass is genuinely clean, say so plainly — an empty findings set is a success, not a failure. Done when: the audit pass is complete and every finding it surfaced is appended to ${AUDIT_FINDINGS_REL} with the right classification (or the report states plainly that nothing was found).`;
501
+ }
502
+
503
+ /** One parsed open finding from the audit findings file. */
504
+ export interface AuditFindingLine {
505
+ /** Raw finding text with the checkbox + optional "FIX:" prefix stripped. */
506
+ text: string;
507
+ /** Severity rank for sorting: 0 = CRITICAL … 4 = unclassified. */
508
+ rank: number;
509
+ }
510
+
511
+ const AUDIT_SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
512
+
513
+ /** v0.31.0: parse findings.md into the fan-out shape — OPEN boxes become
514
+ * actionable findings (severity-sorted, stable within a rank), DECIDE boxes
515
+ * ("- [?]") are returned separately for presentation (never queued). Tolerates
516
+ * the /loop audit format (no "FIX:" prefix): any open box that isn't a
517
+ * decision is actionable.
518
+ */
519
+ export function parseAuditFindingsForFanout(md: string): { open: AuditFindingLine[]; decisions: string[] } {
520
+ const open: AuditFindingLine[] = [];
521
+ const decisions: string[] = [];
522
+ md.split("\n").forEach((line, idx) => {
523
+ const decide = line.match(/^\s*-\s*\[\?\]\s*(.*)$/);
524
+ if (decide) {
525
+ const text = (decide[1] ?? "").replace(/^DECIDE:\s*/i, "").trim();
526
+ if (text) decisions.push(text);
527
+ return;
528
+ }
529
+ const box = line.match(/^\s*-\s*\[ \]\s*(.*)$/);
530
+ if (!box) return;
531
+ const text = (box[1] ?? "").replace(/^FIX:\s*/i, "").trim();
532
+ if (!text) return;
533
+ const sev = text.match(/^([A-Z]+)\s*:/);
534
+ const rank = sev ? AUDIT_SEVERITY_ORDER.indexOf(sev[1]!) : -1;
535
+ open.push({ text, rank: rank >= 0 ? rank : AUDIT_SEVERITY_ORDER.length + (idx / 100000) });
536
+ });
537
+ // Severity first; stable within a rank (file order) via the fractional idx.
538
+ open.sort((a, b) => a.rank - b.rank);
539
+ return { open, decisions };
540
+ }
541
+
542
+ /** v0.31.0: the list-item text for one finding — short objective + a checkable
543
+ * Done when (the fix commit exists AND the box is checked with its hash, so
544
+ * findings.md stays honest as the drain proceeds).
545
+ */
546
+ export function listAuditFanoutItemText(finding: string): string {
547
+ return `Fix audit finding: ${finding} — Done when: the fix is committed on the current branch with the repo's configured identity, and this finding's box in ${AUDIT_FINDINGS_REL} is checked ("- [x] … — fixed in <commit>").`;
548
+ }
549
+
481
550
  export function projectAuditTarget(focus?: string): string {
482
551
  const scope = focus && focus.trim() ? focus.trim() : "the whole project";
483
552
  return `Run ONE project audit pass and leave the project in a known state. Scope: ${scope}. (1) Run a FRESH audit pass over the codebase — spawn Explore subagents for breadth — hunting real problems: bugs, broken flows, regressions, drift between docs and code, dead code, security holes. Not style nits, not speculative refactors. (2) Append every NEW finding to ${AUDIT_FINDINGS_REL} (create the file on the first finding; append-only — never delete, rewrite, or reorder existing lines; never re-report a finding already listed), classified: "- [ ] FIX: SEVERITY: short description (file:line)" for bugs and polish — whether to fix these is NOT a decision — and "- [?] DECIDE: short description (what the choice is, what each side costs)" for direction, trade-offs, and scope questions where two reasonable answers exist. (3) Fix every NEW FIX finding from this pass — real fixes, committed with the repo's configured identity on the current branch (no invented identities or branches) — then check the box: "- [x] … — fixed in <commit>". (4) Change NOTHING for DECIDE findings — present them in the completion report instead. (5) Honesty law: never fabricate findings to look busy; never check a box without the fix commit existing; never silently turn a DECIDE into a fix. Done when: the audit pass is complete, every new FIX finding has a fix commit and a checked box in ${AUDIT_FINDINGS_REL}, and every DECIDE finding is listed in the file and presented in the completion report.`;
@@ -175,6 +175,10 @@ import {
175
175
  countOpenAuditFindings,
176
176
  AUDIT_FINDINGS_REL,
177
177
  projectAuditTarget,
178
+ LIST_AUDIT_COLLECT_MARKER,
179
+ listAuditCollectTarget,
180
+ parseAuditFindingsForFanout,
181
+ listAuditFanoutItemText,
178
182
  type LoopTickOutcome,
179
183
  HELD_ON_RESTORE,
180
184
  type LoopState,
@@ -225,6 +229,72 @@ let extensionApiStale = false;
225
229
  * here stranded goals until manual /goal resume (hegemon/sraaal shape).
226
230
  * sendContinuation's extensionApiStale guard already stops further sends
227
231
  * in this doomed process; the next fresh session auto-resumes. */
232
+ /** v0.30.0: rebind-first session-replacement survival. pi's sanctioned
233
+ * pattern (docs/extensions.md lifecycle + the stale error text itself):
234
+ * session_shutdown → cleanup, session_start → re-establish with the NEW
235
+ * ctx. glla used to treat every stale handle as terminal ("run /reload"),
236
+ * but three replacement shapes need three responses:
237
+ * (a) switch (resume/new/fork): pi rebinds THIS module to the new
238
+ * session — session_start delivers a fresh ctx. No user action, no
239
+ * warning; reset the stale flag via a re-probe and continue.
240
+ * (b) /reload: pi re-imports the extension modules — a SUCCESSOR
241
+ * instance owns this cwd in the same process. The old module stands
242
+ * down silently (owner-file check) instead of screaming + injecting
243
+ * /reload (v0.29.22's injection is right for orphans, wrong here).
244
+ * (c) orphan: the session died with NO replacement (hegemon 2026-07-31:
245
+ * handle dead ~06:03, zero ledger events for 5h). Only a rebuild
246
+ * revives extension function — goStaleTerminal's warning + self-heal
247
+ * stays for this case ONLY.
248
+ * session_shutdown is now ledgered with pi's reason, so the next
249
+ * unexplained disposal is attributable from the ledger alone. */
250
+ const SESSION_REBIND_GRACE_MS = 60_000;
251
+ let sessionReplacementUntil = 0;
252
+ const instanceStartedAt = Date.now();
253
+ const instanceId = `${process.pid}:${instanceStartedAt}`;
254
+ let zombieStoodDown = false;
255
+
256
+ function ownerFilePath(cwd: string): string {
257
+ return path.join(cwd, ".pi-glla", "owner.json");
258
+ }
259
+
260
+ function writeOwnerFile(cwd: string): void {
261
+ try {
262
+ fs.mkdirSync(path.join(cwd, ".pi-glla"), { recursive: true });
263
+ fs.writeFileSync(ownerFilePath(cwd), JSON.stringify({ instanceId, pid: process.pid, at: Date.now() }));
264
+ } catch {
265
+ /* owner file is advisory — never block activation on it */
266
+ }
267
+ }
268
+
269
+ function readOwnerFile(cwd: string): { instanceId?: string; pid?: number; at?: number } | null {
270
+ try {
271
+ return JSON.parse(fs.readFileSync(ownerFilePath(cwd), "utf8")) as { instanceId?: string; pid?: number; at?: number };
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+
277
+ /** A stale probe is terminal only for ORPHANS. Returns true when the
278
+ * stale sighting was absorbed (a rebind window is open, or a successor
279
+ * instance owns this cwd and we stand down silently), false when the
280
+ * caller should go terminal (orphan — no replacement came). */
281
+ function absorbStaleIfSuperseded(ctx: ExtensionContext): boolean {
282
+ if (Date.now() < sessionReplacementUntil) {
283
+ appendLedger(ctx.cwd, "stale_awaiting_rebind", {});
284
+ return true;
285
+ }
286
+ const owner = readOwnerFile(ctx.cwd);
287
+ if (owner && owner.pid === process.pid && typeof owner.instanceId === "string" && owner.instanceId !== instanceId) {
288
+ appendLedger(ctx.cwd, "zombie_stood_down", { owner: owner.instanceId });
289
+ zombieStoodDown = true;
290
+ extensionApiStale = true; // silence the send paths WITHOUT the terminal theatre
291
+ clearLoopTimer();
292
+ if (continuationTimer) { clearTimeout(continuationTimer); continuationTimer = null; }
293
+ return true;
294
+ }
295
+ return false;
296
+ }
297
+
228
298
  function goStaleTerminal(ctx: ExtensionContext, where: string): void {
229
299
  if (extensionApiStale) return; // already terminal — don't re-spam
230
300
  extensionApiStale = true;
@@ -325,6 +395,13 @@ function probeExtensionApiStale(): boolean {
325
395
  * and must NOT claim work started (S3's "created — starting now" lie). */
326
396
  function warnIfStaleAtEntry(ctx: ExtensionContext, what: string): boolean {
327
397
  if (!probeExtensionApiStale()) return false;
398
+ // v0.30.0: a successor may already own this session (e.g. /reload
399
+ // re-imported the modules) — the user's command belongs to the fresh
400
+ // instance; say so softly instead of demanding a reload.
401
+ if (absorbStaleIfSuperseded(ctx)) {
402
+ ctx.ui.notify(`glla: a refreshed instance owns this session — ${what} is handled there; nothing to do.`, "info");
403
+ return true;
404
+ }
328
405
  appendLedger(ctx.cwd, "extension_api_stale", { where: `entry probe (${what})` });
329
406
  ctx.ui.notify(
330
407
  `glla: this session's extension handle is stale (pi session replacement) — ${what} can't send continuations in this process. State is safe in .pi-glla/ — run /reload (extensions rebuild in place), then /glla resume. Restart pi only if /reload fails.`,
@@ -693,6 +770,7 @@ function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
693
770
  }
694
771
 
695
772
  function heartbeatTick(): void {
773
+ if (zombieStoodDown) return; // v0.30.0: a superseded instance stays silent forever
696
774
  const ctx = freshCtx();
697
775
  if (!ctx) return;
698
776
  let idle = false;
@@ -717,7 +795,12 @@ function heartbeatTick(): void {
717
795
  // on: refiring into a dead process is misleading, and worse, the stall
718
796
  // escalation would PAUSE the goal — silently cancelling the
719
797
  // interruptedAt → hold-on-restart promise the footer shows.
720
- if (probeExtensionApiStale()) { goStaleTerminal(ctx, "heartbeat probe"); return; }
798
+ if (probeExtensionApiStale()) {
799
+ // v0.30.0: stale ≠ terminal — absorb the rebind-window and
800
+ // successor-instance cases; only orphans go terminal.
801
+ if (!absorbStaleIfSuperseded(ctx)) goStaleTerminal(ctx, "heartbeat probe");
802
+ return;
803
+ }
721
804
  // v0.29.16: zombie-run watchdog. pi reports BUSY (a run is "active") but
722
805
  // zero stream events for 20 min = the provider stream hung silently —
723
806
  // queued continuations can't land, and every other watchdog stays quiet
@@ -1149,6 +1232,62 @@ function autoArbitrateStackedState(ctx: ExtensionContext): void {
1149
1232
  );
1150
1233
  }
1151
1234
 
1235
+ /** v0.31.0: /list audit completion fan-out — read the audit findings file,
1236
+ * queue every OPEN finding as its own list item (severity-sorted, deduped
1237
+ * against the live queue), present DECIDE findings without queueing them.
1238
+ * Confirm-gated like every bulk import (v0.23.7: the user reads what lands
1239
+ * in the queue); a decline leaves the findings open for a later re-run.
1240
+ */
1241
+ async function fanOutListAuditFindings(ctx: ExtensionContext): Promise<void> {
1242
+ let md = "";
1243
+ try {
1244
+ md = fs.readFileSync(path.join(ctx.cwd, AUDIT_FINDINGS_REL), "utf-8");
1245
+ } catch {
1246
+ /* no findings file — the audit was clean or never wrote */
1247
+ }
1248
+ const { open, decisions } = parseAuditFindingsForFanout(md);
1249
+ // Dedupe against the live queue (a re-run must not double-queue a finding
1250
+ // that's already waiting) — match on the finding text's first 60 chars.
1251
+ const queuedText = listQueue().map((i) => i.objective).join("\n");
1252
+ const fresh = open.filter((f) => !queuedText.includes(f.text.slice(0, 60)));
1253
+ const alreadyQueued = open.length - fresh.length;
1254
+ const decideNote =
1255
+ decisions.length > 0
1256
+ ? `\n${decisions.length} DECIDE finding(s) need YOU (not queued — a decision is not a task):\n` +
1257
+ decisions.slice(0, 10).map((d) => ` ? ${d.slice(0, 110)}`).join("\n")
1258
+ : "";
1259
+ if (fresh.length === 0) {
1260
+ ctx.ui.notify(
1261
+ open.length > 0
1262
+ ? `Audit collected ${open.length} open finding(s) — all already queued.${decideNote}`
1263
+ : `Audit complete — no open findings; the project is clean, nothing to queue.${decideNote}`,
1264
+ "info",
1265
+ );
1266
+ appendLedger(ctx.cwd, "list_audit_fanout_empty", { open: open.length, decisions: decisions.length });
1267
+ return;
1268
+ }
1269
+ const preview = fresh.map((f, i) => ` ${i + 1}. ${f.text.slice(0, 110)}`).join("\n");
1270
+ let confirmed = true;
1271
+ if (ctx.hasUI) {
1272
+ try {
1273
+ confirmed = await ctx.ui.confirm(`Queue ${fresh.length} audit finding(s) as list items?`, preview);
1274
+ } catch {
1275
+ confirmed = false;
1276
+ }
1277
+ }
1278
+ if (!confirmed) {
1279
+ appendLedger(ctx.cwd, "list_audit_fanout_declined", { findings: fresh.length });
1280
+ ctx.ui.notify(`Fan-out declined — the findings stay open in ${AUDIT_FINDINGS_REL}; /list audit re-queues them any time.`, "info");
1281
+ return;
1282
+ }
1283
+ const n = enqueueItems(ctx, fresh.map((f) => listAuditFanoutItemText(f.text)), "list audit fan-out");
1284
+ appendLedger(ctx.cwd, "list_audit_fanout", { queued: n, alreadyQueued, decisions: decisions.length });
1285
+ ctx.ui.notify(
1286
+ `Queued ${n} finding(s) — the list drains them fix by fix, each with its own audited commit.${alreadyQueued > 0 ? ` (${alreadyQueued} already queued.)` : ""}${decideNote}`,
1287
+ "info",
1288
+ );
1289
+ }
1290
+
1152
1291
  function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?: string): void {
1153
1292
  if (!state.goal) return;
1154
1293
  const goal = state.goal;
@@ -1174,9 +1313,15 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
1174
1313
  // (v0.2.0 bug: bare /list next silently consumed TWO items, found by the
1175
1314
  // pick-any-item verification in v0.10.0).
1176
1315
  if (goal.policy === "list" && status === "complete") {
1316
+ // v0.31.0: a /list audit collection item completed → fan the open
1317
+ // findings out into the queue (async — Confirm-gated). When the queue
1318
+ // was empty, enqueueItems activates the first fix itself, so the
1319
+ // list-complete / reviewer noise below must NOT fire for this item.
1320
+ const isListAuditCollect = goal.objective.includes(LIST_AUDIT_COLLECT_MARKER);
1321
+ if (isListAuditCollect) void fanOutListAuditFindings(ctx);
1177
1322
  const advanced = activateNextListItem(ctx);
1178
1323
  // v0.26.0: the queue just EMPTIED on a completion → list-complete.
1179
- if (!advanced) {
1324
+ if (!advanced && !isListAuditCollect) {
1180
1325
  fireReviewer(ctx, { kind: "list", goalId: goal.id, objective: goal.objective, terminal: "goal-complete" });
1181
1326
  // v0.29.0: the well ran dry — point at the project-audit loop. A
1182
1327
  // suggestion, not an action: consent, never auto-start (v0.28.28).
@@ -2005,6 +2150,26 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
2005
2150
  const sub = (parts[0] ?? "").toLowerCase();
2006
2151
  const rest = args.trim().slice(sub.length).trim();
2007
2152
 
2153
+ if (sub === "audit") {
2154
+ // v0.31.0: /list audit [focus] — collect-then-drain (user design
2155
+ // 2026-07-31: "run a project audit, collect a bunch of tasks, then do
2156
+ // them all too"). The audit item COLLECTS findings (changes no code);
2157
+ // its completion fans each open finding out into the queue and the
2158
+ // list drains them fix by fix, each with its own isolated audit.
2159
+ // Distinct from /goal audit (fix-in-pass, one audited unit) and
2160
+ // /loop audit (forever fix-first cadence).
2161
+ const objective = listAuditCollectTarget(rest || undefined);
2162
+ const n = enqueueItems(ctx, [objective], "/list audit");
2163
+ if (n === 0) return; // zombie-twin guard already explained itself
2164
+ ctx.ui.notify(
2165
+ "Audit collection item queued — it CHANGES NO CODE: it appends findings to " +
2166
+ AUDIT_FINDINGS_REL +
2167
+ ", and on completion each open finding becomes its own list item (fixes drain one audited commit at a time). DECIDE findings are presented to you, never queued.",
2168
+ "info",
2169
+ );
2170
+ return;
2171
+ }
2172
+
2008
2173
  if (sub === "depth") {
2009
2174
  // v0.25.3: long-running state at a glance — queue depth, oldest item
2010
2175
  // age, average item duration from archived list-policy goals.
@@ -5303,9 +5468,10 @@ export default function (pi: ExtensionAPI): void {
5303
5468
  handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdReview(args, ctx); },
5304
5469
  });
5305
5470
  pi.registerCommand("list", {
5306
- 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 | /list cancel",
5471
+ 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 audit [focus] (collect findings, then drain them as items) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear | /list cancel",
5307
5472
  getArgumentCompletions: completions([
5308
5473
  ["show", "display the waiting items"],
5474
+ ["audit", "collect-then-drain: audit the project, queue every finding as its own item"],
5309
5475
  ["resume", "resume the paused list item (the list's head)"],
5310
5476
  ["next", "activate the next item (or /list next <n> for position n)"],
5311
5477
  ["remove", "remove an item: /list remove <n>"],
@@ -5493,12 +5659,43 @@ export default function (pi: ExtensionAPI): void {
5493
5659
  }
5494
5660
  });
5495
5661
 
5662
+ pi.on("session_shutdown", async (event: any, ctx: ExtensionContext) => {
5663
+ if (isForeignCtx(ctx)) return;
5664
+ // v0.30.0: attribution + rebind window. pi announces WHY the session
5665
+ // is being replaced (reload/resume/new/fork/quit) — the ledger can
5666
+ // now answer "what killed the handle?" without guesswork (hegemon's
5667
+ // 5-hour orphan silence 2026-07-31 was unattributable). The window
5668
+ // tells the stale probe that a rebind (session_start) is imminent.
5669
+ const shutdownReason = typeof event?.reason === "string" ? event.reason : "unknown";
5670
+ appendLedger(ctx.cwd, "session_shutdown", { reason: shutdownReason });
5671
+ sessionReplacementUntil = Date.now() + SESSION_REBIND_GRACE_MS;
5672
+ });
5673
+
5496
5674
  pi.on("session_start", async (event: any, ctx: ExtensionContext) => {
5497
5675
  rememberCtx(ctx);
5498
5676
  // v0.23.8: subagent sessions (pi-subagents binds extensions there too)
5499
5677
  // are workers — never run the restore gate or reschedule the loop from
5500
5678
  // a foreign session.
5501
5679
  if (isForeignCtx(ctx)) return;
5680
+ // v0.30.0: rebind bookkeeping — claim ownership, close any replacement
5681
+ // window, and reset a stale flag left over from the PREVIOUS session's
5682
+ // invalidation. pi rebinds THIS module to the new session (switch) or
5683
+ // re-imports it (/reload — fresh module, flag already false); either
5684
+ // way the fresh ctx makes the old poison flag wrong. Re-probe to
5685
+ // confirm the new handle actually works.
5686
+ writeOwnerFile(ctx.cwd);
5687
+ sessionReplacementUntil = 0;
5688
+ zombieStoodDown = false;
5689
+ const startReason = typeof event?.reason === "string" ? event.reason : "unknown";
5690
+ appendLedger(ctx.cwd, "session_rebound", { reason: startReason });
5691
+ if (extensionApiStale) {
5692
+ extensionApiStale = false; // fresh ctx delivered — re-probe
5693
+ const stillStale = probeExtensionApiStale();
5694
+ appendLedger(ctx.cwd, "stale_flag_reset_on_rebind", { reason: startReason, stillStale });
5695
+ if (stillStale) {
5696
+ ctx.ui.notify("glla: session rebound but the extension handle is still stale — run /reload (extensions rebuild in place), then /glla resume.", "warning");
5697
+ }
5698
+ }
5502
5699
  state = readState(ctx.cwd);
5503
5700
  // v0.28.14: snapshot carryover BEFORE any restore logic mutates state —
5504
5701
  // a paused goal, waiting list items, or a loop that was live/held when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.29.23",
3
+ "version": "0.31.0",
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",