pi-goal-list-loop-audit 0.34.6 → 0.34.8

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.
package/README.md CHANGED
@@ -312,6 +312,35 @@ stuck backoff caps at 5 minutes then pauses, measure commands get a 10m
312
312
  hard timeout, and the auditor aborts after 10m with zero session activity
313
313
  (infrastructure error, never a verdict).
314
314
 
315
+ ## Recommended companions
316
+
317
+ glla is the goal plane — it drives, verifies, and notifies. It does not
318
+ try to be the whole rig. Four plugins round it out (all optional; glla
319
+ works without any of them):
320
+
321
+ - **`@tintinweb/pi-subagents`** — the `Agent` tool: parallel Explore /
322
+ Plan / general-purpose subagents. glla's prompts teach fan-out with ROI
323
+ (parallelize real work, never ceremony spawning) and brief discipline,
324
+ and big audit collects genuinely assume this exists. glla's subagent
325
+ guarantees (the main session owns the goal; workers can't clobber it)
326
+ are plugin-agnostic, but this is the provider we test against.
327
+ - **`@juicesharp/rpiv-advisor`** — a second opinion the executor model
328
+ can request mid-flight: the whole conversation branch is forwarded to a
329
+ stronger reviewer model, which answers with a plan, a correction, or a
330
+ stop signal. Drive the session with a cheap/fast model and buy strong
331
+ judgment per call. Role clarity: the advisor is *advisory*, never
332
+ verification — glla's isolated auditor remains the only completion
333
+ gate.
334
+ - **`@juicesharp/rpiv-ask-user-question`** — structured questions with
335
+ multi-select and markdown previews. glla's drafting interviews and
336
+ DECIDE findings render through it when installed (plain prompts
337
+ otherwise).
338
+ - **`@pi-unipi/notify`** — push beyond the desktop: Telegram, Gotify,
339
+ ntfy, with per-event routing. glla's built-in pushes cover the local
340
+ desktop case and fire only where there is something to DO; add this
341
+ for away-from-desk alerts — route it to critical events only, or every
342
+ glla pause/verdict pings twice.
343
+
315
344
  ## Compatibility (what goes well, what conflicts)
316
345
 
317
346
  **The Two-Driver Rule**: any plugin that drives agent turns on `agent_end`
@@ -325,12 +354,10 @@ contradictory turns. One driver at a time:
325
354
  - **Installed-but-don't-run-simultaneously**: `@tmustier/pi-ralph-wiggum` —
326
355
  fine to keep, never run a ralph loop while a goal/list/loop is active.
327
356
 
328
- **Goes well with it**: `@juicesharp/rpiv-ask-user-question` (drafting uses its
329
- structured forms), any subagent providere.g. `@tintinweb/pi-subagents`
330
- (spawn research/review subagents inside goal work), `pi-chrome` (the
331
- research/search path for goals logged-in browsing with no extra services;
332
- standalone search skills like `mmx-cli`/`pi-search-skill` are optional
333
- conveniences for bulk queries, not requirements).
357
+ **Goes well with it**: see **Recommended companions** above. `pi-chrome` too
358
+ (the research/search path for goalslogged-in browsing with no extra
359
+ services; standalone search skills like `mmx-cli`/`pi-search-skill` are
360
+ optional conveniences for bulk queries, not requirements).
334
361
 
335
362
  **Overlaps — pick one**: `@tintinweb/pi-tasks` is a second task list next to
336
363
  `/list`, and in practice the glla list *is* the task list (queue, statuses,
@@ -411,6 +411,26 @@ function probeExtensionApiStale(): boolean {
411
411
  return extensionApiStale;
412
412
  }
413
413
 
414
+ /** v0.34.7: orchestrator-path sendUserMessage that can NEVER crash the
415
+ * process. Darklord 2026-08-01: fanOutListAuditFindings ran on a stale
416
+ * handle (a /reload landed mid-collect), assertActive threw, the floating
417
+ * promise from sync archiveCurrentGoal turned it into an uncaughtException
418
+ * and pi EXITED mid-audit. Probe first, catch anyway, ledger the skip. */
419
+ function safeSteerUser(ctx: ExtensionContext, text: string): boolean {
420
+ if (probeExtensionApiStale()) {
421
+ appendLedger(ctx.cwd, "steer_skipped_stale", { chars: text.length });
422
+ return false;
423
+ }
424
+ try {
425
+ extensionApi?.sendUserMessage(text, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
426
+ return true;
427
+ } catch (err) {
428
+ if (isStaleApiError(err)) extensionApiStale = true;
429
+ appendLedger(ctx.cwd, "steer_skipped_stale", { chars: text.length, threw: true });
430
+ return false;
431
+ }
432
+ }
433
+
414
434
  /** v0.28.1 (S3): command-entry staleness probe + honest warning. Returns
415
435
  * true when the handle is stale — callers must skip send-dependent paths
416
436
  * and must NOT claim work started (S3's "created — starting now" lie). */
@@ -1407,11 +1427,9 @@ async function fanOutListAuditFindings(ctx: ExtensionContext): Promise<void> {
1407
1427
  // queued or the fan-out was declined.
1408
1428
  if (decisions.length > 0) {
1409
1429
  const decList = decisions.slice(0, 8).map((d, i) => `${i + 1}. ${d.slice(0, 500)}`).join("\n");
1410
- extensionApi?.sendUserMessage(
1411
- `[DECIDE FINDINGS — user decisions needed] The audit surfaced ${decisions.length} DECIDE finding(s) — direction calls only the user can make (a decision is not a task, so they were NOT queued):\n${decList}\nRaise them to the user NOW with ask_user_question — one question per finding, options from the finding's own two sides plus "Defer" (prose numbered list if ask_user_question is unavailable; Esc = Defer). Then record every answer in ${AUDIT_FINDINGS_REL}: replace the "- [?]" line with "- [x] DECIDED: <what was chosen> (<date>)" (or "- [x] DEFERRED") so it stops re-surfacing, and queue any chosen work with list_add — do NOT start the work inline.`,
1412
- { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
1413
- );
1414
- appendLedger(ctx.cwd, "list_audit_decisions_raised", { decisions: decisions.length });
1430
+ if (safeSteerUser(ctx,
1431
+ `[DECIDE FINDINGS — user decisions needed] The audit surfaced ${decisions.length} DECIDE finding(s) — direction calls only the user can make (a decision is not a task, so they were NOT queued):\n${decList}\nRaise them to the user NOW with ask_user_question — one question per finding, options from the finding's own two sides plus "Defer" (prose numbered list if ask_user_question is unavailable; Esc = Defer). Then record every answer in ${AUDIT_FINDINGS_REL}: replace the "- [?]" line with "- [x] DECIDED: <what was chosen> (<date>)" (or "- [x] DEFERRED") so it stops re-surfacing, and queue any chosen work with list_add — do NOT start the work inline.`))
1432
+ appendLedger(ctx.cwd, "list_audit_decisions_raised", { decisions: decisions.length });
1415
1433
  }
1416
1434
  const decideNote =
1417
1435
  decisions.length > 0
@@ -1481,7 +1499,12 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
1481
1499
  // was empty, enqueueItems activates the first fix itself, so the
1482
1500
  // list-complete / reviewer noise below must NOT fire for this item.
1483
1501
  const isListAuditCollect = goal.objective.includes(LIST_AUDIT_COLLECT_MARKER);
1484
- if (isListAuditCollect) void fanOutListAuditFindings(ctx);
1502
+ // v0.34.7: the float gets a catch — ANY rejection here used to become
1503
+ // an uncaughtException and kill pi (darklord 2026-08-01).
1504
+ if (isListAuditCollect)
1505
+ void fanOutListAuditFindings(ctx).catch((err) => {
1506
+ appendLedger(ctx.cwd, "list_audit_fanout_error", { error: String(err).slice(0, 200) });
1507
+ });
1485
1508
  const advanced = activateNextListItem(ctx);
1486
1509
  // v0.26.0: the queue just EMPTIED on a completion → list-complete.
1487
1510
  if (!advanced && !isListAuditCollect) {
@@ -1699,10 +1722,8 @@ function fireReviewer(
1699
1722
  enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer", { autoActivate: loadGlobalSettings().autoResume === true }),
1700
1723
  proposeGoal: (objective, reason) => {
1701
1724
  try {
1702
- extensionApi?.sendUserMessage(
1703
- `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`,
1704
- { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
1705
- );
1725
+ safeSteerUser(ctx,
1726
+ `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`);
1706
1727
  return true;
1707
1728
  } catch (err) {
1708
1729
  // v0.28.8 (E4): the phantom-reviewer hole — a swallowed throw used
@@ -1833,7 +1854,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
1833
1854
  }
1834
1855
  }
1835
1856
  try {
1836
- extensionApi?.sendUserMessage(tmpl, { deliverAs: ctx.isIdle() ? "followUp" : "steer" });
1857
+ safeSteerUser(ctx, tmpl);
1837
1858
  draftingUserReplies = 0;
1838
1859
  draftingBlockedProposals = 0;
1839
1860
  draftingSeedInFlight = true; // our injected prompt also arrives as a user message — don't count it
@@ -2025,6 +2046,7 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
2025
2046
  return;
2026
2047
  }
2027
2048
  appendLedger(ctx.cwd, "resume_rekick", { goalId: state.goal.id, policy: state.goal.policy, via: "/goal resume" });
2049
+ if (state.goal.interruptedAt) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx); // v0.34.7: same marker law here
2028
2050
  ctx.ui.notify(
2029
2051
  `The ${state.goal.policy === "list" ? "list item" : "goal"} is ACTIVE but idle — re-firing its continuation: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}`,
2030
2052
  "info",
@@ -2134,13 +2156,13 @@ async function showDecisionPrompt(ctx: ExtensionContext): Promise<boolean> {
2134
2156
  else if (group === "loop" && verb === "stop") await cmdLoop("stop", ctx);
2135
2157
  else if (group === "loop" && verb === "resume") await cmdLoop("resume", ctx);
2136
2158
  else {
2137
- extensionApi?.sendUserMessage(`Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2159
+ safeSteerUser(ctx, `Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2138
2160
  await cmdResume(ctx);
2139
2161
  }
2140
2162
  return true;
2141
2163
  }
2142
2164
  // Content choice — deliver to the agent, then resume.
2143
- extensionApi?.sendUserMessage(`Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2165
+ safeSteerUser(ctx, `Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
2144
2166
  await cmdResume(ctx);
2145
2167
  return true;
2146
2168
  } finally {
@@ -5352,6 +5374,12 @@ async function cmdGllaResume(ctx: ExtensionContext): Promise<void> {
5352
5374
  // resume"). Re-kick the continuation instead of shrugging.
5353
5375
  if (g && g.status === "active") {
5354
5376
  appendLedger(ctx.cwd, "resume_rekick", { goalId: g.id, policy: g.policy });
5377
+ // v0.34.7: the re-kick fulfills the stale-handle marker's promise too
5378
+ // (junk-runner/polis/neonbreak 2026-08-01: actively working with the
5379
+ // ⚠ interrupted banner still screaming — the v0.34.2 clear only lived
5380
+ // in the paused-resume path; the staleness entry-guard above already
5381
+ // filtered out a stale session reaching this branch).
5382
+ if (g.interruptedAt) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
5355
5383
  ctx.ui.notify(
5356
5384
  `The ${g.policy === "list" ? "list item" : "goal"} is ACTIVE but idle — re-firing its continuation: ${g.objective.replace(/\s+/g, " ").slice(0, 70)}`,
5357
5385
  "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.34.6",
3
+ "version": "0.34.8",
4
4
  "description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. An isolated extension-less auditor re-verifies every completion with raw evidence; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "MIT",
6
6
  "author": "dracon",