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 +1 -0
- package/extensions/goal-loop-core.ts +3 -3
- package/extensions/goal-settings.ts +5 -0
- package/extensions/loops/goal.ts +102 -1
- package/extensions/settings-menu.ts +10 -1
- package/package.json +1 -1
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",
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|