pi-goal-list-loop-audit 0.28.21 → 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 +13 -3
- package/extensions/goal-loop-display.ts +52 -7
- package/extensions/goal-settings.ts +5 -0
- package/extensions/loops/goal.ts +137 -10
- package/extensions/settings-menu.ts +10 -1
- package/package.json +1 -1
- package/schemas/goal.schema.json +4 -0
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
|
|
@@ -156,6 +156,16 @@ export interface Goal {
|
|
|
156
156
|
stopReason?: string;
|
|
157
157
|
pauseReason?: string;
|
|
158
158
|
pauseSuggestedAction?: string;
|
|
159
|
+
/** v0.28.22: pause classification — drives the widget/status rendering
|
|
160
|
+
* (a decision pause, an operational failure, a time-gated wait, and a
|
|
161
|
+
* generic block must not look alike). Undefined = legacy flat card. */
|
|
162
|
+
pauseKind?: "decision" | "error" | "wait" | "blocked";
|
|
163
|
+
/** v0.28.22: decision pauses — the options the user picks between. */
|
|
164
|
+
pauseOptions?: string[];
|
|
165
|
+
/** v0.28.22: 1-based index into pauseOptions the agent recommends. */
|
|
166
|
+
pauseRecommended?: number;
|
|
167
|
+
/** v0.28.22: ISO time a wait-pause becomes resumable (countdown shown). */
|
|
168
|
+
pauseResumeAt?: string;
|
|
159
169
|
/** v0.28.1 (S1/S2): stale-handle interrupt marker. Set INSTEAD of pausing
|
|
160
170
|
* when pi invalidates the extension handle mid-goal — the goal stays
|
|
161
171
|
* active so a fresh session auto-resumes it via the restore gate. Cleared
|
|
@@ -193,9 +203,9 @@ export interface Goal {
|
|
|
193
203
|
export type GoalRoute =
|
|
194
204
|
| { kind: "draft" }
|
|
195
205
|
| { kind: "set"; text: string }
|
|
196
|
-
| { 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 };
|
|
197
207
|
|
|
198
|
-
const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel"]);
|
|
208
|
+
const GOAL_EXACT_SUBS = new Set(["status", "pause", "resume", "cancel", "decide"]);
|
|
199
209
|
const GOAL_ARG_SUBS = new Set(["tweak", "archive", "start"]);
|
|
200
210
|
|
|
201
211
|
export function routeGoalArgs(raw: string): GoalRoute {
|
|
@@ -205,7 +215,7 @@ export function routeGoalArgs(raw: string): GoalRoute {
|
|
|
205
215
|
const first = (space === -1 ? trimmed : trimmed.slice(0, space)).toLowerCase();
|
|
206
216
|
const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
|
|
207
217
|
if (GOAL_EXACT_SUBS.has(first) && rest === "") {
|
|
208
|
-
return { kind: "sub", name: first as "status" | "pause" | "resume" | "cancel", rest: "" };
|
|
218
|
+
return { kind: "sub", name: first as "status" | "pause" | "resume" | "cancel" | "decide", rest: "" };
|
|
209
219
|
}
|
|
210
220
|
if (GOAL_ARG_SUBS.has(first)) {
|
|
211
221
|
return { kind: "sub", name: first as "tweak" | "archive" | "start", rest };
|
|
@@ -107,6 +107,18 @@ const paint = (theme: DisplayTheme | undefined, color: DisplayColor, text: strin
|
|
|
107
107
|
const ERROR_PAUSE = /token limit|stalled|infra|auditor.*fail/i;
|
|
108
108
|
const pauseIsError = (g: Goal): boolean => ERROR_PAUSE.test(g.pauseReason ?? "");
|
|
109
109
|
|
|
110
|
+
/** v0.28.22: the rendering class of a pause — declared kind wins; legacy
|
|
111
|
+
* pauses (no kind) fall back to the error-regex so old states still
|
|
112
|
+
* classify sensibly. */
|
|
113
|
+
type PauseKind = "decision" | "error" | "wait" | "blocked";
|
|
114
|
+
const pauseKind = (g: Goal): PauseKind | undefined => g.pauseKind ?? (pauseIsError(g) ? "error" : undefined);
|
|
115
|
+
|
|
116
|
+
/** v0.28.22: "06:40 UTC" from an ISO string (wait-pause countdown). */
|
|
117
|
+
const shortClock = (iso: string): string => {
|
|
118
|
+
const d = new Date(iso);
|
|
119
|
+
return Number.isNaN(d.getTime()) ? iso.slice(0, 16) : d.toISOString().slice(11, 16) + " UTC";
|
|
120
|
+
};
|
|
121
|
+
|
|
110
122
|
// ---- status line (one-liner, always-on) ----
|
|
111
123
|
|
|
112
124
|
export interface AuditDisplayProgress {
|
|
@@ -150,6 +162,13 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
|
|
|
150
162
|
return `glla: ${paint(theme, "accent", "auditing…")}${tool}${heldSuffix}`;
|
|
151
163
|
}
|
|
152
164
|
if (g.status === "paused") {
|
|
165
|
+
// v0.28.22: the status line names the ACTIONABILITY, not the reason —
|
|
166
|
+
// "decision needed" / "action needed" / "waiting" tell you at a glance
|
|
167
|
+
// whether the session needs you. Legacy pauses keep the reason dump.
|
|
168
|
+
const kind = pauseKind(g);
|
|
169
|
+
if (kind === "decision") return `glla: ${g.policy} ${paint(theme, "accent", "⏸ decision needed")}${heldSuffix}`;
|
|
170
|
+
if (kind === "error") return `glla: ${g.policy} ${paint(theme, "error", `⏸ action needed — ${truncate(g.pauseReason ?? "", 30)}`)}${heldSuffix}`;
|
|
171
|
+
if (kind === "wait") return `glla: ${g.policy} ${paint(theme, "dim", `⏳ waiting${g.pauseResumeAt ? ` · resumes ${shortClock(g.pauseResumeAt)}` : ""}`)}${heldSuffix}`;
|
|
153
172
|
const label = `${g.policy} paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
|
|
154
173
|
return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}${heldSuffix}`;
|
|
155
174
|
}
|
|
@@ -274,14 +293,38 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
274
293
|
return lines;
|
|
275
294
|
}
|
|
276
295
|
if (g.status === "paused" && g.pauseReason) {
|
|
277
|
-
const
|
|
296
|
+
const kind = pauseKind(g);
|
|
297
|
+
const isErr = kind === "error";
|
|
278
298
|
const budget = budgetFor(width, 3, 60);
|
|
279
|
-
// v0.
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
299
|
+
// v0.28.22: actionability banner — a decision pause, an operational
|
|
300
|
+
// failure, and a time-gated wait must not look alike (user report:
|
|
301
|
+
// "if something actionable is going on it can be hard to tell").
|
|
302
|
+
if (kind === "decision") lines.push(`├─ ${paint(theme, "accent", "decision needed — your call unblocks this")}`);
|
|
303
|
+
else if (kind === "error") lines.push(`├─ ${paint(theme, "error", "action needed — this won't fix itself")}`);
|
|
304
|
+
else if (kind === "wait") lines.push(`├─ ${paint(theme, "dim", "waiting — nothing for you to do")}`);
|
|
305
|
+
// v0.27.1: wrap reason + suggested action (see wrap()). v0.28.22:
|
|
306
|
+
// decision/wait reasons cap at 2 lines — the options/countdown below
|
|
307
|
+
// carry the actionable content; error reasons keep 3.
|
|
308
|
+
const reasonPaint = isErr ? "error" : kind === "wait" ? "dim" : "warning";
|
|
309
|
+
wrap(g.pauseReason, budget, kind === "decision" || kind === "wait" ? 2 : 3).forEach((w, i) => {
|
|
310
|
+
lines.push(`${i === 0 ? "├─" : "│ "} ${paint(theme, reasonPaint, w)}`);
|
|
284
311
|
});
|
|
312
|
+
// v0.28.22: decision options — one numbered line each (Claude Code /
|
|
313
|
+
// muselinn-Ask convention), the recommended one accented and flagged.
|
|
314
|
+
if (kind === "decision" && g.pauseOptions && g.pauseOptions.length > 0) {
|
|
315
|
+
g.pauseOptions.slice(0, 6).forEach((opt, i) => {
|
|
316
|
+
const rec = g.pauseRecommended === i + 1;
|
|
317
|
+
const text = `${i + 1}. ${truncate(opt, budget - 4)}${rec ? " ◂ recommended" : ""}`;
|
|
318
|
+
lines.push(`│ ${paint(theme, rec ? "accent" : "dim", text)}`);
|
|
319
|
+
});
|
|
320
|
+
if (g.pauseOptions.length > 6) lines.push(`│ ${paint(theme, "dim", `… and ${g.pauseOptions.length - 6} more`)}`);
|
|
321
|
+
}
|
|
322
|
+
// v0.28.22: wait countdown — when the pause lifts on its own.
|
|
323
|
+
if (kind === "wait" && g.pauseResumeAt) {
|
|
324
|
+
const ms = Date.parse(g.pauseResumeAt) - now;
|
|
325
|
+
const when = Number.isNaN(ms) ? g.pauseResumeAt : ms <= 0 ? "now" : `${shortClock(g.pauseResumeAt)} (in ${fmtElapsed(ms)})`;
|
|
326
|
+
lines.push(`├─ ${paint(theme, "dim", `resumes ${when} — or /goal resume now`)}`);
|
|
327
|
+
}
|
|
285
328
|
// v0.27.1: what survives the pause — the first question at a pause is
|
|
286
329
|
// "did I lose the work?". Answer it on the card.
|
|
287
330
|
// v0.27.9: when the goal has no telemetry yet (restored-in-fresh-session
|
|
@@ -300,7 +343,9 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
300
343
|
if (g.pauseSuggestedAction) {
|
|
301
344
|
lines.push(`├─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
|
|
302
345
|
const wrapped = wrap(g.pauseSuggestedAction, budget, 3);
|
|
303
|
-
|
|
346
|
+
// v0.28.22: for ACTION NEEDED pauses the action is the point — pop it.
|
|
347
|
+
const actionPaint = kind === "error" ? "warning" : "dim";
|
|
348
|
+
wrapped.forEach((w, i) => lines.push(`${i === wrapped.length - 1 ? "└─" : "│ "} ${paint(theme, actionPaint, w)}`));
|
|
304
349
|
} else {
|
|
305
350
|
lines.push(`└─ ${paint(theme, "dim", truncate(savedLine, budget))}`);
|
|
306
351
|
}
|
|
@@ -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
|
@@ -498,6 +498,7 @@ function escalateSendRearmStorm(ctx: ExtensionContext, kind: "continuation" | "l
|
|
|
498
498
|
if (state.goal && state.goal.status === "active") {
|
|
499
499
|
updateGoal({
|
|
500
500
|
status: "paused",
|
|
501
|
+
pauseKind: "error",
|
|
501
502
|
pauseReason: `send-retry storm: ${mins}m of 50ms re-arms — the session never went idle for the continuation`,
|
|
502
503
|
pauseSuggestedAction: "The session never went idle for the send (wedged queue or permanently busy). Restart pi, then /goal resume.",
|
|
503
504
|
}, ctx);
|
|
@@ -521,6 +522,7 @@ function escalateStallNow(ctx: ExtensionContext, threshold: number): boolean {
|
|
|
521
522
|
if (state.goal && state.goal.status === "active") {
|
|
522
523
|
updateGoal({
|
|
523
524
|
status: "paused",
|
|
525
|
+
pauseKind: "error",
|
|
524
526
|
pauseReason: `stalled: ${threshold} continuation refires landed no turn`,
|
|
525
527
|
pauseSuggestedAction: "The continuation chain is broken in this process (wedged message queue or stale API). Restart pi, then /goal resume.",
|
|
526
528
|
}, ctx);
|
|
@@ -1136,6 +1138,13 @@ async function cmdGoal(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1136
1138
|
if (route.name === "pause") return cmdPause(ctx);
|
|
1137
1139
|
if (route.name === "resume") return cmdResume(ctx);
|
|
1138
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
|
+
}
|
|
1139
1148
|
if (route.name === "tweak") return cmdTweak(route.rest, ctx);
|
|
1140
1149
|
if (route.name === "archive") return cmdGoals(ctx);
|
|
1141
1150
|
// v0.16.0: /goal start <objective> — explicit skip-draft. Activates
|
|
@@ -1259,7 +1268,7 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
1259
1268
|
const usage = state.goal.usage
|
|
1260
1269
|
? { tokensUsed: state.goal.usage.tokensUsed, tokensLimit: freshLimit }
|
|
1261
1270
|
: undefined;
|
|
1262
|
-
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
|
|
1271
|
+
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, pauseKind: undefined, pauseOptions: undefined, pauseRecommended: undefined, pauseResumeAt: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
|
|
1263
1272
|
if (staleEntry) return;
|
|
1264
1273
|
// v0.22.5: say what was resumed — with a non-empty list this also resumes
|
|
1265
1274
|
// the queue (the active goal IS the list's head item).
|
|
@@ -1289,6 +1298,68 @@ async function cmdCancel(ctx: ExtensionContext): Promise<void> {
|
|
|
1289
1298
|
ctx.ui.notify(`Goal aborted.${isLoopActive() ? " A loop is still active — /loop stop ends it." : ""}`, "info");
|
|
1290
1299
|
}
|
|
1291
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
|
+
|
|
1292
1363
|
async function cmdGoals(ctx: ExtensionContext): Promise<void> {
|
|
1293
1364
|
const dir = archiveDir(ctx.cwd);
|
|
1294
1365
|
if (!fs.existsSync(dir)) {
|
|
@@ -2050,10 +2121,10 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2050
2121
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
2051
2122
|
const rest = args.trim().slice(sub.length).trim();
|
|
2052
2123
|
|
|
2053
|
-
if (!sub) {
|
|
2054
|
-
// /loop with no args → resume a held loop
|
|
2055
|
-
// draft the loop config (metric design is
|
|
2056
|
-
// long-running loop; never start one blind).
|
|
2124
|
+
if (!sub || sub === "resume") {
|
|
2125
|
+
// /loop with no args (or /loop resume, v0.28.22) → resume a held loop
|
|
2126
|
+
// if one is waiting; otherwise draft the loop config (metric design is
|
|
2127
|
+
// the whole game for a long-running loop; never start one blind).
|
|
2057
2128
|
if (isLoopActive()) {
|
|
2058
2129
|
ctx.ui.notify("A loop is already active — /loop status to inspect, /loop stop to end it.", "info");
|
|
2059
2130
|
return;
|
|
@@ -2063,7 +2134,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2063
2134
|
// v0.28.14: one-active-thing — a held loop must not resume over an
|
|
2064
2135
|
// active goal/list-item (this was the last unguarded stacking path).
|
|
2065
2136
|
if (state.goal && state.goal.status === "active") {
|
|
2066
|
-
ctx.ui.notify("A goal is active — the held loop stays held. /goal pause or /goal cancel it first, then /loop
|
|
2137
|
+
ctx.ui.notify("A goal is active — the held loop stays held. /goal pause or /goal cancel it first, then /loop resume.", "warning");
|
|
2067
2138
|
return;
|
|
2068
2139
|
}
|
|
2069
2140
|
state.loop = { ...stored, active: true, stopReason: undefined };
|
|
@@ -2075,6 +2146,10 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2075
2146
|
);
|
|
2076
2147
|
return;
|
|
2077
2148
|
}
|
|
2149
|
+
if (sub === "resume") {
|
|
2150
|
+
ctx.ui.notify("No held loop to resume. /loop to draft one, or /loop start \"<target>\" for an infinite metricless loop.", "info");
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2078
2153
|
await startDrafting(ctx, "loop");
|
|
2079
2154
|
return;
|
|
2080
2155
|
}
|
|
@@ -2440,10 +2515,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2440
2515
|
updateGoal({
|
|
2441
2516
|
status: "paused",
|
|
2442
2517
|
auditHistory: history,
|
|
2518
|
+
pauseKind: "decision",
|
|
2519
|
+
pauseOptions: ["Tweak the objective — /goal tweak <new text>", "Cancel the goal (/goal cancel)"],
|
|
2520
|
+
pauseRecommended: 1,
|
|
2443
2521
|
pauseReason: `auditor verdict: IMPOSSIBLE — ${reason}`,
|
|
2444
2522
|
pauseSuggestedAction: "The auditor says this goal can never be satisfied as stated. /goal tweak the objective (or /goal cancel), then /goal resume.",
|
|
2445
2523
|
}, ctx);
|
|
2446
2524
|
ctx.ui.notify(`Auditor: goal IMPOSSIBLE — ${reason}. Goal paused; /goal tweak or /goal cancel, then /goal resume.`, "warning");
|
|
2525
|
+
maybeDecisionPopup(ctx);
|
|
2447
2526
|
appendLedger(ctx.cwd, "goal_paused", { reason: `auditor impossible: ${reason}` });
|
|
2448
2527
|
notifyExternal(ctx, `Goal paused (auditor: impossible): ${reason.slice(0, 120)}`);
|
|
2449
2528
|
return {
|
|
@@ -2472,6 +2551,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2472
2551
|
status: "paused",
|
|
2473
2552
|
auditHistory: history,
|
|
2474
2553
|
auditInfraStreak: undefined, // quota reached the auditor — infra streak broken
|
|
2554
|
+
pauseKind: "wait",
|
|
2555
|
+
pauseResumeAt: new Date(Date.now() + quota.retryAfterSec * 1000).toISOString(),
|
|
2475
2556
|
pauseReason: `auditor quota: ${result.error}`,
|
|
2476
2557
|
pauseSuggestedAction: `Quota auto-retry in ${retryMin}m — or /goal resume to retry now`,
|
|
2477
2558
|
}, ctx);
|
|
@@ -2506,6 +2587,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2506
2587
|
status: "paused",
|
|
2507
2588
|
auditHistory: history,
|
|
2508
2589
|
auditInfraStreak: infraStreak,
|
|
2590
|
+
pauseKind: "error",
|
|
2509
2591
|
pauseReason: `auditor infrastructure failed ${infraStreak}× in a row — the auditor model is likely broken (last: ${result.error.slice(0, 120)})`,
|
|
2510
2592
|
pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) or restart pi, then /goal resume. Your work was NOT judged.",
|
|
2511
2593
|
}, ctx);
|
|
@@ -2612,10 +2694,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2612
2694
|
updateGoal({
|
|
2613
2695
|
status: "paused",
|
|
2614
2696
|
auditHistory: history,
|
|
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,
|
|
2615
2700
|
pauseReason: `auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap})`,
|
|
2616
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.",
|
|
2617
2702
|
}, ctx);
|
|
2618
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);
|
|
2619
2705
|
appendLedger(ctx.cwd, "goal_paused", { reason: `disapproval cap: ${trailingDisapprovals} consecutive (cap ${auditCap})` });
|
|
2620
2706
|
notifyExternal(ctx, `Goal paused: ${trailingDisapprovals} consecutive auditor disapprovals`);
|
|
2621
2707
|
return {
|
|
@@ -2646,21 +2732,30 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2646
2732
|
pi.registerTool(defineTool({
|
|
2647
2733
|
name: "pause_goal",
|
|
2648
2734
|
label: "Pause goal",
|
|
2649
|
-
description: "Pause the active goal with a reason and suggested action. Use when blocked on user input or unable to make progress.",
|
|
2735
|
+
description: "Pause the active goal with a reason and suggested action. Use when blocked on user input or unable to make progress. When the user must CHOOSE between options, pass kind=\"decision\" with the options list (recommended = 1-based index of the best one) — decision pauses render as a prominent DECISION NEEDED card. Time-gated waits (retry at a specific time) use kind=\"wait\" with resumeAt (ISO). Operational failures use kind=\"error\".",
|
|
2650
2736
|
parameters: Type.Object({
|
|
2651
2737
|
reason: Type.String({ description: "Why the work is paused" }),
|
|
2652
2738
|
suggestedAction: Type.Optional(Type.String({ description: "What the user should do next" })),
|
|
2739
|
+
kind: Type.Optional(Type.Union([Type.Literal("decision"), Type.Literal("error"), Type.Literal("wait"), Type.Literal("blocked")], { description: "Pause class: decision (user picks an option), error (operational failure), wait (time-gated), blocked (generic)" })),
|
|
2740
|
+
options: Type.Optional(Type.Array(Type.String(), { description: "For kind=decision: the options the user picks between (one line each)" })),
|
|
2741
|
+
recommended: Type.Optional(Type.Number({ description: "For kind=decision: 1-based index of the recommended option" })),
|
|
2742
|
+
resumeAt: Type.Optional(Type.String({ description: "For kind=wait: ISO time the pause lifts (countdown is shown)" })),
|
|
2653
2743
|
}),
|
|
2654
2744
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
2655
2745
|
const foreign1 = foreignToolGuard(execCtx);
|
|
2656
2746
|
if (foreign1) return { content: [{ type: "text", text: foreign1 }], details: {} };
|
|
2657
|
-
const p = params as { reason: string; suggestedAction?: string };
|
|
2747
|
+
const p = params as { reason: string; suggestedAction?: string; kind?: "decision" | "error" | "wait" | "blocked"; options?: string[]; recommended?: number; resumeAt?: string };
|
|
2658
2748
|
if (!state.goal) return { content: [{ type: "text", text: "No active goal." }], details: {} };
|
|
2659
2749
|
updateGoal({
|
|
2660
2750
|
status: "paused",
|
|
2661
2751
|
pauseReason: p.reason,
|
|
2662
2752
|
pauseSuggestedAction: p.suggestedAction,
|
|
2753
|
+
pauseKind: p.kind,
|
|
2754
|
+
pauseOptions: p.kind === "decision" && p.options && p.options.length > 0 ? p.options : undefined,
|
|
2755
|
+
pauseRecommended: p.kind === "decision" && p.recommended && p.recommended >= 1 ? Math.floor(p.recommended) : undefined,
|
|
2756
|
+
pauseResumeAt: p.kind === "wait" && p.resumeAt ? p.resumeAt : undefined,
|
|
2663
2757
|
}, ctx);
|
|
2758
|
+
if (p.kind === "decision" && p.options && p.options.length > 0) maybeDecisionPopup(ctx);
|
|
2664
2759
|
// v0.27.1: surface the FULL pause contract — reason AND suggested
|
|
2665
2760
|
// action. Before, the action only appeared in /goal status and the
|
|
2666
2761
|
// widget truncated both at ~60 chars, so decision-pauses ("choose a
|
|
@@ -3386,6 +3481,14 @@ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Pr
|
|
|
3386
3481
|
if (v) saveSettings("global", ctx.cwd, { autoAcceptDrafts: v.startsWith("on") ? true : undefined });
|
|
3387
3482
|
return;
|
|
3388
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
|
+
}
|
|
3389
3492
|
case "aggressiveMode": {
|
|
3390
3493
|
const v = await ctx.ui.select("Aggressive mode (flips DEFAULTS toward keep-going — explicit per-key settings still win)", [
|
|
3391
3494
|
"off — current behavior: pause at the audit cap, wedge alerts on, manual resume",
|
|
@@ -3941,6 +4044,16 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3941
4044
|
} else {
|
|
3942
4045
|
ctx.ui.notify(`autoresume must be on or off, got: ${value}`, "warning");
|
|
3943
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
|
+
}
|
|
3944
4057
|
} else if (key === "carryover") {
|
|
3945
4058
|
if (["resume", "pause", "clear"].includes(value)) {
|
|
3946
4059
|
patch.carryover = value as "resume" | "pause" | "clear";
|
|
@@ -4072,7 +4185,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
4072
4185
|
}
|
|
4073
4186
|
}
|
|
4074
4187
|
if (!changed) {
|
|
4075
|
-
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");
|
|
4076
4189
|
return;
|
|
4077
4190
|
}
|
|
4078
4191
|
saveSettings(scope, ctx.cwd, patch);
|
|
@@ -4206,6 +4319,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4206
4319
|
["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
|
|
4207
4320
|
["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
|
|
4208
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)"],
|
|
4209
4323
|
["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)"],
|
|
4210
4324
|
["auditfeedbackchars=", "cap on executor-visible disapproval report chars (0 = full report, the default)"],
|
|
4211
4325
|
["aggressivemode=", "on: keep-going defaults — autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs"],
|
|
@@ -4462,7 +4576,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4462
4576
|
state.loop = { ...l, active: false, stopReason: HELD_ON_RESTORE };
|
|
4463
4577
|
persistState(ctx);
|
|
4464
4578
|
ctx.ui.notify(
|
|
4465
|
-
`Loop held on restore: ${l.target.slice(0, 60)} — /loop to
|
|
4579
|
+
`Loop held on restore: ${l.target.slice(0, 60)} — /loop resume to continue, /glla autoresume=on to auto-resume on session load in this project.`,
|
|
4466
4580
|
"info",
|
|
4467
4581
|
);
|
|
4468
4582
|
}
|
|
@@ -4492,6 +4606,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4492
4606
|
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
|
|
4493
4607
|
updateGoal({
|
|
4494
4608
|
status: "paused",
|
|
4609
|
+
pauseKind: "blocked",
|
|
4495
4610
|
pauseReason: "restored on session load — held for explicit resume",
|
|
4496
4611
|
pauseSuggestedAction: resumeHint,
|
|
4497
4612
|
}, ctx);
|
|
@@ -4522,6 +4637,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4522
4637
|
if (state.loop && state.goal && state.goal.status === "active") {
|
|
4523
4638
|
updateGoal({
|
|
4524
4639
|
status: "paused",
|
|
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,
|
|
4525
4643
|
pauseReason: "held on session load — the loop owns the active slot (one active thing at a time)",
|
|
4526
4644
|
pauseSuggestedAction: "/loop to work the loop, or /loop stop then /goal resume to work the goal",
|
|
4527
4645
|
}, ctx);
|
|
@@ -4529,6 +4647,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4529
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.`,
|
|
4530
4648
|
"info",
|
|
4531
4649
|
);
|
|
4650
|
+
maybeDecisionPopup(ctx);
|
|
4532
4651
|
}
|
|
4533
4652
|
// Always paint on session load (v0.22.1): the branches above only reach
|
|
4534
4653
|
// refreshUI via persistState, so a goal that was ALREADY paused (or any
|
|
@@ -4617,10 +4736,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4617
4736
|
if (state.goal) {
|
|
4618
4737
|
updateGoal({
|
|
4619
4738
|
status: "paused",
|
|
4739
|
+
pauseKind: "decision",
|
|
4740
|
+
pauseOptions: ["Retry — /goal resume", "Tweak the objective — /goal tweak <new text>", "Cancel the goal (/goal cancel)"],
|
|
4741
|
+
pauseRecommended: 1,
|
|
4620
4742
|
pauseReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive unproductive turns (no tools, short or repetitive)`,
|
|
4621
4743
|
pauseSuggestedAction: "Inspect the goal — /goal resume to retry, /goal tweak to narrow it, /goal cancel to abort.",
|
|
4622
4744
|
}, ctx);
|
|
4623
4745
|
ctx.ui.notify(`Goal paused: stalled (${HEARTBEAT_MAX_NUDGES} unproductive turns).`, "warning");
|
|
4746
|
+
maybeDecisionPopup(ctx);
|
|
4624
4747
|
notifyExternal(ctx, "Goal paused: stalled (no tool calls).");
|
|
4625
4748
|
return;
|
|
4626
4749
|
}
|
|
@@ -4664,6 +4787,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4664
4787
|
updateGoal({
|
|
4665
4788
|
usage: { tokensUsed: used, tokensLimit: limit },
|
|
4666
4789
|
status: "paused",
|
|
4790
|
+
pauseKind: "error",
|
|
4667
4791
|
pauseReason: `token limit exceeded (${used.toLocaleString()} > ${limit.toLocaleString()})`,
|
|
4668
4792
|
pauseSuggestedAction: "/glla tokenlimit=<n> to raise the cap (or 0 to disable), then /goal resume",
|
|
4669
4793
|
}, ctx);
|
|
@@ -4687,6 +4811,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4687
4811
|
const reason = `5 consecutive errors${detail}`;
|
|
4688
4812
|
updateGoal({
|
|
4689
4813
|
status: "paused",
|
|
4814
|
+
pauseKind: "wait",
|
|
4815
|
+
pauseResumeAt: new Date(Date.now() + 60_000).toISOString(),
|
|
4690
4816
|
pauseReason: reason,
|
|
4691
4817
|
pauseSuggestedAction: "Transient provider flake? The goal auto-resumes once in 60s if still paused for this reason — or /goal resume now.",
|
|
4692
4818
|
}, ctx);
|
|
@@ -4714,6 +4840,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4714
4840
|
if (consecutiveAbortIterations >= 5) {
|
|
4715
4841
|
updateGoal({
|
|
4716
4842
|
status: "paused",
|
|
4843
|
+
pauseKind: "blocked",
|
|
4717
4844
|
pauseReason: "5 consecutive aborts (user interrupted)",
|
|
4718
4845
|
pauseSuggestedAction: "You interrupted 5 turns in a row — the goal stays paused until you /goal resume (or /goal cancel).",
|
|
4719
4846
|
}, ctx);
|
|
@@ -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",
|
package/schemas/goal.schema.json
CHANGED
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"stopReason": { "type": "string" },
|
|
47
47
|
"pauseReason": { "type": "string" },
|
|
48
48
|
"pauseSuggestedAction": { "type": "string" },
|
|
49
|
+
"pauseKind": { "type": "string", "enum": ["decision", "error", "wait", "blocked"] },
|
|
50
|
+
"pauseOptions": { "type": "array", "items": { "type": "string" } },
|
|
51
|
+
"pauseRecommended": { "type": "number" },
|
|
52
|
+
"pauseResumeAt": { "type": "string" },
|
|
49
53
|
"interruptedAt": { "type": "string" },
|
|
50
54
|
"interruptedReason": { "type": "string" },
|
|
51
55
|
"auditInfraStreak": { "type": "number" },
|