pi-goal-list-loop-audit 0.34.6 → 0.34.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.
- package/extensions/loops/goal.ts +41 -13
- package/package.json +1 -1
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.34.7",
|
|
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",
|