pi-goal-list-loop-audit 0.34.79 → 0.34.80
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 +94 -27
- package/package.json +1 -1
package/extensions/loops/goal.ts
CHANGED
|
@@ -926,20 +926,36 @@ type DraftChoice = "yes" | "no" | "stale";
|
|
|
926
926
|
let lastConfirmDialog: { title: string; body: string; options: string[] } | null = null;
|
|
927
927
|
async function confirmDraft(ctx: ExtensionContext, title: string, body: string): Promise<DraftChoice> {
|
|
928
928
|
const ALWAYS = "Yes — and always auto-accept drafts (sets autoAcceptDrafts for this project)";
|
|
929
|
+
const options = ["Yes", ALWAYS, "No"];
|
|
930
|
+
// v0.34.80 (GitHub #4 rework): `custom` is ALWAYS a function in real pi
|
|
931
|
+
// 0.84.1 — RPC mode resolves `undefined` WITHOUT ever invoking the factory
|
|
932
|
+
// (`async custom() { /* Custom UI not supported */ return undefined; }`),
|
|
933
|
+
// so `typeof custom === "function"` alone never fires the headless/RPC
|
|
934
|
+
// fallback and RPC drafts were silently rejected ("no" — the dialog never
|
|
935
|
+
// rendered, the host never answered). Detect unavailability by whether the
|
|
936
|
+
// builder RAN: if `custom()` settles and the factory was never invoked,
|
|
937
|
+
// treat custom as unavailable and fall through to the byte-identical
|
|
938
|
+
// select path (the host-dialog route in RPC mode).
|
|
929
939
|
if (typeof (ctx.ui as { custom?: unknown }).custom === "function") {
|
|
930
|
-
const options = ["Yes", ALWAYS, "No"];
|
|
931
940
|
lastConfirmDialog = { title, body, options };
|
|
941
|
+
let factoryInvoked = false;
|
|
932
942
|
try {
|
|
933
|
-
const choice = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) =>
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
943
|
+
const choice = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
944
|
+
factoryInvoked = true;
|
|
945
|
+
return new ConfirmDraftComponent({ title, body, options }, () => tui.requestRender(), theme as any, keybindings as any, done);
|
|
946
|
+
});
|
|
947
|
+
if (factoryInvoked) {
|
|
948
|
+
if (choice === ALWAYS) {
|
|
949
|
+
saveSettings("project", ctx.cwd, { autoAcceptDrafts: true });
|
|
950
|
+
appendLedger(ctx.cwd, "draft_autoaccept_enabled", { via: title });
|
|
951
|
+
ctx.ui.notify("Draft auto-accept ON for this project — future draft confirms are skipped. Undo in /glla settings: Auto-accept drafts = off.", "info");
|
|
952
|
+
return "yes";
|
|
953
|
+
}
|
|
954
|
+
return choice === "Yes" ? "yes" : "no";
|
|
941
955
|
}
|
|
942
|
-
|
|
956
|
+
// real RPC/noOp stub — custom is unavailable in this mode; the
|
|
957
|
+
// select fallback below is the host-dialog path.
|
|
958
|
+
appendLedger(ctx.cwd, "confirm_dialog_fallback_select", { via: "custom-stub" });
|
|
943
959
|
} catch (err) {
|
|
944
960
|
if (isStaleApiError(err)) return "stale";
|
|
945
961
|
// a non-stale custom failure degrades to the legacy select path
|
|
@@ -1852,6 +1868,31 @@ function heartbeatTick(): void {
|
|
|
1852
1868
|
// a durable interruption marker. Keep the last context long enough for the
|
|
1853
1869
|
// terminal path to persist the honest orphan state.
|
|
1854
1870
|
const knownCtx = lastCtx;
|
|
1871
|
+
// v0.34.80 (field: 2026-08-07 item-2 freeze): a latched-stale LIVE session
|
|
1872
|
+
// (transient heartbeat-probe failures tripping goStaleTerminal) freezes an
|
|
1873
|
+
// auditing goal with no in-flight audit: the completed verdict's apply path
|
|
1874
|
+
// silently bails on freshCtxForGeneration()=null and the full stranded-
|
|
1875
|
+
// recovery block below is unreachable while the latch holds — the queue sat
|
|
1876
|
+
// blocked ~30m+ while the worker's disapproval sat on disk. Park the stuck
|
|
1877
|
+
// claim via the kept last context. A heartbeat must still NEVER launch
|
|
1878
|
+
// another worker; the park is the explicit-resume gate.
|
|
1879
|
+
if (
|
|
1880
|
+
extensionApiStale &&
|
|
1881
|
+
knownCtx &&
|
|
1882
|
+
state.goal?.status === "auditing" &&
|
|
1883
|
+
!completionAuditInFlight &&
|
|
1884
|
+
state.goal.pendingCompletion &&
|
|
1885
|
+
Date.now() - lastActivityAt >= 90_000
|
|
1886
|
+
) {
|
|
1887
|
+
appendLedger(knownCtx.cwd, "stranded_audit_recovered", { goalId: state.goal.id, via: "stale-latch" });
|
|
1888
|
+
markCompletionAuditRecoveryPending(knownCtx, "stale-latch-recovery");
|
|
1889
|
+
try {
|
|
1890
|
+
knownCtx.ui.notify(`Completion audit blocked — no verdict (stale session). The stored claim is safe; ${activeGoalSurfaceCommand("resume")} starts exactly one fresh auditor.`, "warning");
|
|
1891
|
+
} catch {
|
|
1892
|
+
/* stale handle — the ledger + park are the durable record */
|
|
1893
|
+
}
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1855
1896
|
if (extensionApiStale || probeExtensionApiStaleRaw()) {
|
|
1856
1897
|
if (!extensionApiStale) {
|
|
1857
1898
|
// v0.34.62: debounce — ONE transient probe failure (pi mid-settle,
|
|
@@ -3876,7 +3917,21 @@ async function retryStoredCompletionAudit(origin: CompletionAuditOrigin = "quota
|
|
|
3876
3917
|
}
|
|
3877
3918
|
}
|
|
3878
3919
|
const currentAfterAudit = freshCtxForGeneration(generation);
|
|
3879
|
-
if (!currentAfterAudit || !state.goal || state.goal.id !== goalId)
|
|
3920
|
+
if (!currentAfterAudit || !state.goal || state.goal.id !== goalId) {
|
|
3921
|
+
// v0.34.80 (field: 2026-08-07): NEVER drop a completed verdict silently.
|
|
3922
|
+
// The gate nulls out on a latched-stale LIVE session (extensionApiStale
|
|
3923
|
+
// from transient heartbeat-probe failures) — the goal then froze in
|
|
3924
|
+
// "auditing" with no in-flight audit and the stranded backstop
|
|
3925
|
+
// unreachable below the stale latch in heartbeatTick. The worker's
|
|
3926
|
+
// verdict (result.json) sat complete on disk for 30m+ while the queue
|
|
3927
|
+
// was blocked. Leave a durable marker via the kept last context so the
|
|
3928
|
+
// fresh session's recovery path parks the claim for an explicit resume.
|
|
3929
|
+
if (state.goal?.status === "auditing" && state.goal.pendingCompletion?.attemptId === claim.attemptId && lastCtx) {
|
|
3930
|
+
appendLedger(lastCtx.cwd, "audit_verdict_deferred", { goalId, attemptId: claim.attemptId, reason: "stale-latch-apply-gate" });
|
|
3931
|
+
markCompletionAuditRecoveryPending(lastCtx, "verdict-apply-gate");
|
|
3932
|
+
}
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3880
3935
|
if (state.goal.pendingCompletion?.attemptId !== claim.attemptId) return; // a newer attempt owns the durable claim
|
|
3881
3936
|
liveCtx = currentAfterAudit;
|
|
3882
3937
|
|
|
@@ -7824,22 +7879,34 @@ async function promptSettingsMenu(
|
|
|
7824
7879
|
initialSection?: SettingsSectionId,
|
|
7825
7880
|
): Promise<string | undefined> {
|
|
7826
7881
|
const title = `pi-goal-list-loop-audit settings — global: ${globalSettingsPath()}`;
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7882
|
+
// v0.34.80: `custom` is a function in EVERY pi 0.84.1 mode — RPC/noOp
|
|
7883
|
+
// resolve `undefined` without invoking the builder. Detect availability by
|
|
7884
|
+
// whether the factory RAN; a settled stub falls through to the flat-row
|
|
7885
|
+
// select (the host-dialog path in RPC mode).
|
|
7886
|
+
if (typeof (ctx.ui as { custom?: unknown }).custom === "function") {
|
|
7887
|
+
let factoryInvoked = false;
|
|
7888
|
+
try {
|
|
7889
|
+
const v = await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
7890
|
+
factoryInvoked = true;
|
|
7891
|
+
return new SettingsMenuComponent({ rows, title, initialSection }, () => tui.requestRender(), theme, keybindings, done);
|
|
7892
|
+
});
|
|
7893
|
+
if (factoryInvoked) return v;
|
|
7894
|
+
appendLedger(ctx.cwd, "settings_menu_fallback_select", { via: "custom-stub" });
|
|
7895
|
+
} catch {
|
|
7896
|
+
// a non-stale custom failure degrades to the legacy select path
|
|
7897
|
+
}
|
|
7898
|
+
}
|
|
7899
|
+
// Headless / no custom shard — fall back to the legacy flat-row select
|
|
7900
|
+
// for any environment that lacks the new primitive. This is rare and
|
|
7901
|
+
// effectively an emergency hatch; the new UI is the supported path.
|
|
7902
|
+
const flat = rows.map((r) => `[${r.section}] ${r.label} — ${r.valueText} [${r.sourceText.replace(/^\[|\]$/g, "")}] — ${r.description}`);
|
|
7903
|
+
flat.push("Done");
|
|
7904
|
+
const v = await ctx.ui.select(title, flat);
|
|
7905
|
+
if (!v || v === "Done") return undefined;
|
|
7906
|
+
// v0.34.25: resolve by the full constructed prefix (section + label),
|
|
7907
|
+
// not a bare startsWith(label) — a label-prefix collision used to be
|
|
7908
|
+
// able to open the wrong editor silently.
|
|
7909
|
+
return rows.find((r) => v.startsWith(`[${r.section}] ${r.label} —`))?.id;
|
|
7843
7910
|
}
|
|
7844
7911
|
|
|
7845
7912
|
/**
|
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.80",
|
|
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. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|