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.
@@ -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
- new ConfirmDraftComponent({ title, body, options }, () => tui.requestRender(), theme as any, keybindings as any, done),
935
- );
936
- if (choice === ALWAYS) {
937
- saveSettings("project", ctx.cwd, { autoAcceptDrafts: true });
938
- appendLedger(ctx.cwd, "draft_autoaccept_enabled", { via: title });
939
- ctx.ui.notify("Draft auto-accept ON for this project — future draft confirms are skipped. Undo in /glla settings: Auto-accept drafts = off.", "info");
940
- return "yes";
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
- return choice === "Yes" ? "yes" : "no";
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) return; // replacement/stale/goal boundary — fresh session rebinds durable state
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
- if (typeof (ctx.ui as { custom?: unknown }).custom !== "function") {
7828
- // Headless / no custom shard — fall back to the legacy flat-row select
7829
- // for any environment that lacks the new primitive. This is rare and
7830
- // effectively an emergency hatch; the new UI is the supported path.
7831
- const flat = rows.map((r) => `[${r.section}] ${r.label} — ${r.valueText} [${r.sourceText.replace(/^\[|\]$/g, "")}] — ${r.description}`);
7832
- flat.push("Done");
7833
- const v = await ctx.ui.select(title, flat);
7834
- if (!v || v === "Done") return undefined;
7835
- // v0.34.25: resolve by the full constructed prefix (section + label),
7836
- // not a bare startsWith(label) — a label-prefix collision used to be
7837
- // able to open the wrong editor silently.
7838
- return rows.find((r) => v.startsWith(`[${r.section}] ${r.label} —`))?.id;
7839
- }
7840
- return await ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
7841
- return new SettingsMenuComponent({ rows, title, initialSection }, () => tui.requestRender(), theme, keybindings, done);
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.79",
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",