pi-goal-list-loop-audit 0.28.22 → 0.28.23

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
@@ -38,6 +38,7 @@ Five top-level commands — `/goal`, `/list`, `/loop`, `/glla`, `/review`:
38
38
  /goal pause # pause
39
39
  /goal resume # resume
40
40
  /goal cancel # abort
41
+ /goal decide # re-open the decision picker (v0.28.23)
41
42
  /goal tweak "<new objective>" # edit in place (Confirm dialog)
42
43
  /goal archive # archived goals, newest first
43
44
  /glla # settings UI table · /glla key=value · /glla stats · /glla audits [N|full] · /glla postaudit · /glla autoaccept=on
@@ -203,9 +203,9 @@ export interface Goal {
203
203
  export type GoalRoute =
204
204
  | { kind: "draft" }
205
205
  | { kind: "set"; text: string }
206
- | { kind: "sub"; name: "status" | "pause" | "resume" | "cancel" | "tweak" | "archive" | "start"; rest: string };
206
+ | { kind: "sub"; name: "status" | "pause" | "resume" | "cancel" | "decide" | "tweak" | "archive" | "start"; rest: string };
207
207
 
208
- const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel"]);
208
+ const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel", "decide"]);
209
209
  const GOAL_ARG_SUBS = new Set(["tweak", "archive", "start"]);
210
210
 
211
211
  export function routeGoalArgs(raw: string): GoalRoute {
@@ -215,7 +215,7 @@ export function routeGoalArgs(raw: string): GoalRoute {
215
215
  const first = (space === -1 ? trimmed : trimmed.slice(0, space)).toLowerCase();
216
216
  const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
217
217
  if (GOAL_EXACT_SUBS.has(first) && rest === "") {
218
- return { kind: "sub", name: first as "status" | "pause" | "resume" | "cancel", rest: "" };
218
+ return { kind: "sub", name: first as "status" | "pause" | "resume" | "cancel" | "decide", rest: "" };
219
219
  }
220
220
  if (GOAL_ARG_SUBS.has(first)) {
221
221
  return { kind: "sub", name: first as "tweak" | "archive" | "start", rest };
@@ -33,6 +33,10 @@ export interface Settings {
33
33
  /** on → restored goals/loops/lists auto-resume even in fresh sessions
34
34
  * (unattended rigs). Default off: restore holds until /goal resume. */
35
35
  autoResume?: boolean;
36
+ /** v0.28.23: off → decision pauses don't pop the select() picker (the
37
+ * widget card still shows the options; /goal decide opens it on demand).
38
+ * Default on; unattended rigs have no UI so this never fires there. */
39
+ decisionPopup?: boolean;
36
40
  /** v0.28.14: what happens to stale carryover (paused goal, waiting list,
37
41
  * held loop from before this session) when NEW work activates.
38
42
  * pause (default) = leave it + ONE summary; clear = drop it all honestly;
@@ -156,6 +160,7 @@ export const SETTINGS_KEYS: Array<keyof Settings> = [
156
160
  "tokenLimit",
157
161
  "wedgeAlertMinutes",
158
162
  "autoResume",
163
+ "decisionPopup",
159
164
  "carryover",
160
165
  "autoAcceptDrafts",
161
166
  "auditCap",
@@ -1138,6 +1138,13 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
1138
1138
  if (route.name === "pause") return cmdPause(ctx);
1139
1139
  if (route.name === "resume") return cmdResume(ctx);
1140
1140
  if (route.name === "cancel") return cmdCancel(ctx);
1141
+ // v0.28.23: re-open the decision picker for a decision pause (the
1142
+ // popup auto-opens when the pause lands; this is the on-demand path).
1143
+ if (route.name === "decide") {
1144
+ const shown = await showDecisionPrompt(ctx);
1145
+ if (!shown) ctx.ui.notify("No pending decision — the goal isn't paused on a choice (or no UI).", "info");
1146
+ return;
1147
+ }
1141
1148
  if (route.name === "tweak") return cmdTweak(route.rest, ctx);
1142
1149
  if (route.name === "archive") return cmdGoals(ctx);
1143
1150
  // v0.16.0: /goal start <objective> — explicit skip-draft. Activates
@@ -1291,6 +1298,68 @@ async function cmdCancel(ctx: ExtensionContext): Promise<void> {
1291
1298
  ctx.ui.notify(`Goal aborted.${isLoopActive() ? " A loop is still active — /loop stop ends it." : ""}`, "info");
1292
1299
  }
1293
1300
 
1301
+ // ---- v0.28.23: decision picker popup ----
1302
+ // A decision pause is ACTIONABLE — the widget card summarizes (and
1303
+ // truncates) it, but picking from a truncated wall was the user's
1304
+ // complaint. Borrow Claude Code / muselinn-Ask: a real select() modal
1305
+ // with the FULL option text, pick → act. Escape leaves the card as the
1306
+ // fallback; /goal decide re-opens the picker at any time.
1307
+
1308
+ let decisionPromptOpen = false;
1309
+
1310
+ /** True when the goal is paused on a user decision with options. */
1311
+ function pendingDecision(): Goal | null {
1312
+ const g = state.goal;
1313
+ return g && g.status === "paused" && g.pauseKind === "decision" && g.pauseOptions && g.pauseOptions.length > 0 ? g : null;
1314
+ }
1315
+
1316
+ /** Open the decision picker for the current decision pause. Returns true
1317
+ * when a picker was shown (false → caller notifies "no pending decision"). */
1318
+ async function showDecisionPrompt(ctx: ExtensionContext): Promise<boolean> {
1319
+ const g = pendingDecision();
1320
+ if (!g || !ctx.hasUI || decisionPromptOpen) return false;
1321
+ decisionPromptOpen = true;
1322
+ try {
1323
+ const title = `Decision needed — ${g.objective.replace(/\s+/g, " ").slice(0, 72)}${g.pauseReason ? ` · ${g.pauseReason.slice(0, 80)}` : ""}`;
1324
+ const options = g.pauseOptions!.map((o, i) => (g.pauseRecommended === i + 1 ? `${o} (recommended)` : o));
1325
+ const pick = await ctx.ui.select(title, options);
1326
+ if (!pick) return true; // Escape — the widget card remains the fallback
1327
+ const idx = options.indexOf(pick);
1328
+ const label = g.pauseOptions![idx] ?? pick.replace(/ {2}\(recommended\)$/, "");
1329
+ // Executable options — "Label (/goal cancel)" — RUN the command.
1330
+ // Placeholder commands (…/<arg>) fall through to the message path.
1331
+ const cmdMatch = label.match(/\(\/(goal|list|loop) ([a-z]+)\)\s*$/);
1332
+ if (cmdMatch && !label.includes("…") && !label.includes("<")) {
1333
+ const [, group, verb] = cmdMatch;
1334
+ if (group === "goal" && verb === "resume") await cmdResume(ctx);
1335
+ else if (group === "goal" && verb === "cancel") await cmdCancel(ctx);
1336
+ else if (group === "loop" && verb === "stop") await cmdLoop("stop", ctx);
1337
+ else if (group === "loop" && verb === "resume") await cmdLoop("resume", ctx);
1338
+ else {
1339
+ extensionApi?.sendUserMessage(`Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
1340
+ await cmdResume(ctx);
1341
+ }
1342
+ return true;
1343
+ }
1344
+ // Content choice — deliver to the agent, then resume.
1345
+ extensionApi?.sendUserMessage(`Decision for the paused goal "${g.objective}": ${label} — continue on this path.`);
1346
+ await cmdResume(ctx);
1347
+ return true;
1348
+ } finally {
1349
+ decisionPromptOpen = false;
1350
+ }
1351
+ }
1352
+
1353
+ /** Pop the picker after a decision pause lands — deferred so the current
1354
+ * turn finishes first (pi serializes dialogs). No-ops without a UI, when
1355
+ * disabled (/glla decisionpopup=off), or when one is already open. */
1356
+ function maybeDecisionPopup(ctx: ExtensionContext): void {
1357
+ if (!ctx.hasUI || loadSettings(ctx.cwd).decisionPopup === false) return;
1358
+ setTimeout(() => {
1359
+ void showDecisionPrompt(ctx).catch(() => {});
1360
+ }, 600);
1361
+ }
1362
+
1294
1363
  async function cmdGoals(ctx: ExtensionContext): Promise<void> {
1295
1364
  const dir = archiveDir(ctx.cwd);
1296
1365
  if (!fs.existsSync(dir)) {
@@ -2447,10 +2516,13 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2447
2516
  status: "paused",
2448
2517
  auditHistory: history,
2449
2518
  pauseKind: "decision",
2519
+ pauseOptions: ["Tweak the objective — /goal tweak <new text>", "Cancel the goal (/goal cancel)"],
2520
+ pauseRecommended: 1,
2450
2521
  pauseReason: `auditor verdict: IMPOSSIBLE — ${reason}`,
2451
2522
  pauseSuggestedAction: "The auditor says this goal can never be satisfied as stated. /goal tweak the objective (or /goal cancel), then /goal resume.",
2452
2523
  }, ctx);
2453
2524
  ctx.ui.notify(`Auditor: goal IMPOSSIBLE — ${reason}. Goal paused; /goal tweak or /goal cancel, then /goal resume.`, "warning");
2525
+ maybeDecisionPopup(ctx);
2454
2526
  appendLedger(ctx.cwd, "goal_paused", { reason: `auditor impossible: ${reason}` });
2455
2527
  notifyExternal(ctx, `Goal paused (auditor: impossible): ${reason.slice(0, 120)}`);
2456
2528
  return {
@@ -2623,10 +2695,13 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2623
2695
  status: "paused",
2624
2696
  auditHistory: history,
2625
2697
  pauseKind: "decision",
2698
+ pauseOptions: ["Fix the disapproval gap, then continue (/goal resume)", "Tweak the objective — /goal tweak <new text>", "Cancel the goal (/goal cancel)"],
2699
+ pauseRecommended: 1,
2626
2700
  pauseReason: `auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap})`,
2627
2701
  pauseSuggestedAction: "Read the audit history (/goal status), fix the actual gap or /goal tweak the objective, then /goal resume. Raise the cap with /glla auditcap=N.",
2628
2702
  }, ctx);
2629
2703
  ctx.ui.notify(`Goal paused: auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap}). /goal status for the reports; /goal resume to continue.`, "warning");
2704
+ maybeDecisionPopup(ctx);
2630
2705
  appendLedger(ctx.cwd, "goal_paused", { reason: `disapproval cap: ${trailingDisapprovals} consecutive (cap ${auditCap})` });
2631
2706
  notifyExternal(ctx, `Goal paused: ${trailingDisapprovals} consecutive auditor disapprovals`);
2632
2707
  return {
@@ -2680,6 +2755,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2680
2755
  pauseRecommended: p.kind === "decision" && p.recommended && p.recommended >= 1 ? Math.floor(p.recommended) : undefined,
2681
2756
  pauseResumeAt: p.kind === "wait" && p.resumeAt ? p.resumeAt : undefined,
2682
2757
  }, ctx);
2758
+ if (p.kind === "decision" && p.options && p.options.length > 0) maybeDecisionPopup(ctx);
2683
2759
  // v0.27.1: surface the FULL pause contract — reason AND suggested
2684
2760
  // action. Before, the action only appeared in /goal status and the
2685
2761
  // widget truncated both at ~60 chars, so decision-pauses ("choose a
@@ -3405,6 +3481,14 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
3405
3481
  if (v) saveSettings("global", ctx.cwd, { autoAcceptDrafts: v.startsWith("on") ? true : undefined });
3406
3482
  return;
3407
3483
  }
3484
+ case "decisionPopup": {
3485
+ const v = await ctx.ui.select("Decision popup (v0.28.23 — decision pauses pop the select() picker)", [
3486
+ "on — a decision pause opens the picker; the widget card is the Escape fallback",
3487
+ "off — widget card only; /goal decide opens the picker on demand",
3488
+ ]);
3489
+ if (v) saveSettings("global", ctx.cwd, { decisionPopup: v.startsWith("off") ? false : undefined });
3490
+ return;
3491
+ }
3408
3492
  case "aggressiveMode": {
3409
3493
  const v = await ctx.ui.select("Aggressive mode (flips DEFAULTS toward keep-going — explicit per-key settings still win)", [
3410
3494
  "off — current behavior: pause at the audit cap, wedge alerts on, manual resume",
@@ -3960,6 +4044,16 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
3960
4044
  } else {
3961
4045
  ctx.ui.notify(`autoresume must be on or off, got: ${value}`, "warning");
3962
4046
  }
4047
+ } else if (key === "decisionpopup") {
4048
+ if (["on", "true", "1", "yes"].includes(value)) {
4049
+ patch.decisionPopup = true;
4050
+ changed = true;
4051
+ } else if (["off", "false", "0", "no"].includes(value)) {
4052
+ patch.decisionPopup = false;
4053
+ changed = true;
4054
+ } else {
4055
+ ctx.ui.notify(`decisionpopup must be on or off, got: ${value}`, "warning");
4056
+ }
3963
4057
  } else if (key === "carryover") {
3964
4058
  if (["resume", "pause", "clear"].includes(value)) {
3965
4059
  patch.carryover = value as "resume" | "pause" | "clear";
@@ -4091,7 +4185,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
4091
4185
  }
4092
4186
  }
4093
4187
  if (!changed) {
4094
- ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit, autoresume, carryover, auditcap, auditfeedbackchars, aggressivemode, quotaretryminutes, stuckmax), optionally prefixed with 'project'.", "info");
4188
+ ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit, autoresume, decisionpopup, carryover, auditcap, auditfeedbackchars, aggressivemode, quotaretryminutes, stuckmax), optionally prefixed with 'project'.", "info");
4095
4189
  return;
4096
4190
  }
4097
4191
  saveSettings(scope, ctx.cwd, patch);
@@ -4225,6 +4319,7 @@ export default function (pi: ExtensionAPI): void {
4225
4319
  ["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
4226
4320
  ["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
4227
4321
  ["autoresume=", "default: hold when a session is loaded, auto-resume on reload/fork; on: always auto-resume; off: never"],
4322
+ ["decisionpopup=", "on|off: decision pauses pop the select() picker (default on; the widget card always lists the options, /goal decide reopens the picker)"],
4228
4323
  ["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)"],
4229
4324
  ["auditfeedbackchars=", "cap on executor-visible disapproval report chars (0 = full report, the default)"],
4230
4325
  ["aggressivemode=", "on: keep-going defaults — autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs"],
@@ -4543,6 +4638,8 @@ export default function (pi: ExtensionAPI): void {
4543
4638
  updateGoal({
4544
4639
  status: "paused",
4545
4640
  pauseKind: "decision",
4641
+ pauseOptions: ["Stop the loop, then resume the goal (/loop stop)", "Cancel the goal (/goal cancel) — the loop keeps running"],
4642
+ pauseRecommended: 1,
4546
4643
  pauseReason: "held on session load — the loop owns the active slot (one active thing at a time)",
4547
4644
  pauseSuggestedAction: "/loop to work the loop, or /loop stop then /goal resume to work the goal",
4548
4645
  }, ctx);
@@ -4550,6 +4647,7 @@ export default function (pi: ExtensionAPI): void {
4550
4647
  `Goal [${state.goal.id}] held — a loop also exists; one active thing at a time. /loop to resume the loop, or /loop stop then /goal resume.`,
4551
4648
  "info",
4552
4649
  );
4650
+ maybeDecisionPopup(ctx);
4553
4651
  }
4554
4652
  // Always paint on session load (v0.22.1): the branches above only reach
4555
4653
  // refreshUI via persistState, so a goal that was ALREADY paused (or any
@@ -4639,10 +4737,13 @@ export default function (pi: ExtensionAPI): void {
4639
4737
  updateGoal({
4640
4738
  status: "paused",
4641
4739
  pauseKind: "decision",
4740
+ pauseOptions: ["Retry — /goal resume", "Tweak the objective — /goal tweak <new text>", "Cancel the goal (/goal cancel)"],
4741
+ pauseRecommended: 1,
4642
4742
  pauseReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive unproductive turns (no tools, short or repetitive)`,
4643
4743
  pauseSuggestedAction: "Inspect the goal — /goal resume to retry, /goal tweak to narrow it, /goal cancel to abort.",
4644
4744
  }, ctx);
4645
4745
  ctx.ui.notify(`Goal paused: stalled (${HEARTBEAT_MAX_NUDGES} unproductive turns).`, "warning");
4746
+ maybeDecisionPopup(ctx);
4646
4747
  notifyExternal(ctx, "Goal paused: stalled (no tool calls).");
4647
4748
  return;
4648
4749
  }
@@ -129,7 +129,16 @@ export function buildSettingsRows(
129
129
  valueText: show("autoResume", "default"),
130
130
  sourceText: src("autoResume"),
131
131
  description:
132
- "on: resume on session load too · off: never · default: hold on load, resume on reload/fork",
132
+ "on: resume on session load too · off: never · default: hold on EVERY load — explicit resume (v0.28.21)",
133
+ },
134
+ {
135
+ id: "decisionPopup",
136
+ section: "keep-going",
137
+ label: "Decision popup",
138
+ valueText: show("decisionPopup", "on"),
139
+ sourceText: src("decisionPopup"),
140
+ description:
141
+ "on: decision pauses pop the select() picker · off: widget card only — /goal decide reopens the picker (v0.28.23)",
133
142
  },
134
143
  {
135
144
  id: "autoAcceptDrafts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.22",
3
+ "version": "0.28.23",
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",