pi-goal-list-loop-audit 0.28.20 → 0.28.22
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 -1
- package/extensions/goal-loop-core.ts +20 -10
- package/extensions/goal-loop-display.ts +52 -7
- package/extensions/loops/goal.ts +66 -16
- package/package.json +1 -1
- package/schemas/goal.schema.json +4 -0
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ No external watchdog plugin needed.
|
|
|
203
203
|
/glla tokenlimit=10000000 # per-goal token budget (default: off) → GLOBAL
|
|
204
204
|
/glla tokenlimit=0 # explicitly no cap (the default)
|
|
205
205
|
/glla wedgealert=30 # hung-command alert minutes (default: 30, 0 = off)
|
|
206
|
-
/glla autoresume=on # auto-resume goals/loops on session
|
|
206
|
+
/glla autoresume=on # auto-resume goals/loops on ANY session start (default: load HELD, never auto-start — explicit /goal resume, /list resume, or /loop; off: never)
|
|
207
207
|
/glla auditcap=5 # pause the goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)
|
|
208
208
|
/glla aggressivemode=on # keep-going defaults: autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs
|
|
209
209
|
/glla quotaretryminutes=60 # minutes before auto-retrying a quota-exhausted auditor
|
|
@@ -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
|
|
@@ -728,21 +738,21 @@ export function cloneGoal(goal: Goal): Goal {
|
|
|
728
738
|
* setting for unattended restarts). One mechanical predicate; no heuristics.
|
|
729
739
|
*/
|
|
730
740
|
export function shouldAutoResumeOnSessionStart(reason: string | undefined, autoResume: boolean | undefined): boolean {
|
|
731
|
-
// v0.
|
|
732
|
-
//
|
|
741
|
+
// v0.28.21: the DEFAULT flipped to hold-everything (user directive:
|
|
742
|
+
// "load it on session load but not auto start it"). Tri-state:
|
|
743
|
+
// true → auto-resume on EVERY session start (unattended rigs;
|
|
744
|
+
// /glla autoresume=on — this is the ONLY auto-resume path).
|
|
733
745
|
// false → never auto-resume; always hold for an explicit resume.
|
|
734
|
-
// undefined → DEFAULT:
|
|
735
|
-
// "resume",
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
// extension reload or session fork never strands work.
|
|
746
|
+
// undefined → DEFAULT: never auto-resume either — whatever the reason
|
|
747
|
+
// ("startup"/"new"/"resume"/"reload"/"fork"/none), the
|
|
748
|
+
// item is LOADED (visible, state intact) but HELD until an
|
|
749
|
+
// explicit /goal resume, /list resume, or /loop.
|
|
739
750
|
// Mid-session continuation (agent_end chains, heartbeat refires,
|
|
740
751
|
// post-compaction, list/loop transitions) is not gated here at all — it
|
|
741
752
|
// auto-continues forever unless a super-stuck brake (stall escalation,
|
|
742
753
|
// stale-api terminal, pending-latch watchdog) stops it loudly.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
return reason === "reload" || reason === "fork";
|
|
754
|
+
void reason; // retained for the signature; no reason auto-resumes by default anymore
|
|
755
|
+
return autoResume === true;
|
|
746
756
|
}
|
|
747
757
|
|
|
748
758
|
/**
|
|
@@ -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
|
}
|
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);
|
|
@@ -1194,9 +1196,9 @@ async function cmdSet(args: string, ctx: ExtensionContext, skipDraft = false): P
|
|
|
1194
1196
|
consecutiveNoToolIterations = 0;
|
|
1195
1197
|
if (staleEntry) {
|
|
1196
1198
|
// v0.28.1 (S3): the goal is persisted — mark the interrupt so the next
|
|
1197
|
-
// fresh session
|
|
1199
|
+
// fresh session LOADS it (held by default since v0.28.21), and tell the truth instead of "starting now".
|
|
1198
1200
|
updateGoal({ interruptedAt: nowIso(), interruptedReason: "created in a stale session" }, ctx);
|
|
1199
|
-
ctx.ui.notify(`Goal saved: ${shortObj(goal.objective)} — safe in .pi-glla/, but this stale process can't send continuations. Restart pi
|
|
1201
|
+
ctx.ui.notify(`Goal saved: ${shortObj(goal.objective)} — safe in .pi-glla/, but this stale process can't send continuations. Restart pi, then /goal resume (v0.28.21: session loads no longer auto-start by default). (id: ${goal.id})`, "warning");
|
|
1200
1202
|
return;
|
|
1201
1203
|
}
|
|
1202
1204
|
ctx.ui.notify(`Goal started: ${shortObj(goal.objective)} — the auditor will verify on completion. (id: ${goal.id})`, "info");
|
|
@@ -1239,6 +1241,13 @@ async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
|
1239
1241
|
|
|
1240
1242
|
async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
1241
1243
|
if (!state.goal || state.goal.status !== "paused") return;
|
|
1244
|
+
// v0.28.21: one-active-thing — the LAST unguarded activation path. A
|
|
1245
|
+
// paused goal/list-item must not resume over a live loop (covers
|
|
1246
|
+
// /goal resume AND /list resume, which routes here).
|
|
1247
|
+
if (isLoopActive()) {
|
|
1248
|
+
ctx.ui.notify("A loop is active — one active thing at a time. /loop stop it first, then resume the goal.", "warning");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1242
1251
|
// v0.28.1 (S1/S3): resuming in a stale session used to flip status to
|
|
1243
1252
|
// active, claim "Resumed goal", then re-pause on the stale send failure
|
|
1244
1253
|
// (or zombie — S1). Now: persist the resume (the next fresh session
|
|
@@ -1252,7 +1261,7 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
1252
1261
|
const usage = state.goal.usage
|
|
1253
1262
|
? { tokensUsed: state.goal.usage.tokensUsed, tokensLimit: freshLimit }
|
|
1254
1263
|
: undefined;
|
|
1255
|
-
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(staleEntry ? { interruptedAt: nowIso(), interruptedReason: "resumed in a stale session" } : {}), ...(usage ? { usage } : {}) }, ctx);
|
|
1264
|
+
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);
|
|
1256
1265
|
if (staleEntry) return;
|
|
1257
1266
|
// v0.22.5: say what was resumed — with a non-empty list this also resumes
|
|
1258
1267
|
// the queue (the active goal IS the list's head item).
|
|
@@ -2043,10 +2052,10 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2043
2052
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
2044
2053
|
const rest = args.trim().slice(sub.length).trim();
|
|
2045
2054
|
|
|
2046
|
-
if (!sub) {
|
|
2047
|
-
// /loop with no args → resume a held loop
|
|
2048
|
-
// draft the loop config (metric design is
|
|
2049
|
-
// long-running loop; never start one blind).
|
|
2055
|
+
if (!sub || sub === "resume") {
|
|
2056
|
+
// /loop with no args (or /loop resume, v0.28.22) → resume a held loop
|
|
2057
|
+
// if one is waiting; otherwise draft the loop config (metric design is
|
|
2058
|
+
// the whole game for a long-running loop; never start one blind).
|
|
2050
2059
|
if (isLoopActive()) {
|
|
2051
2060
|
ctx.ui.notify("A loop is already active — /loop status to inspect, /loop stop to end it.", "info");
|
|
2052
2061
|
return;
|
|
@@ -2056,7 +2065,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2056
2065
|
// v0.28.14: one-active-thing — a held loop must not resume over an
|
|
2057
2066
|
// active goal/list-item (this was the last unguarded stacking path).
|
|
2058
2067
|
if (state.goal && state.goal.status === "active") {
|
|
2059
|
-
ctx.ui.notify("A goal is active — the held loop stays held. /goal pause or /goal cancel it first, then /loop
|
|
2068
|
+
ctx.ui.notify("A goal is active — the held loop stays held. /goal pause or /goal cancel it first, then /loop resume.", "warning");
|
|
2060
2069
|
return;
|
|
2061
2070
|
}
|
|
2062
2071
|
state.loop = { ...stored, active: true, stopReason: undefined };
|
|
@@ -2068,6 +2077,10 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2068
2077
|
);
|
|
2069
2078
|
return;
|
|
2070
2079
|
}
|
|
2080
|
+
if (sub === "resume") {
|
|
2081
|
+
ctx.ui.notify("No held loop to resume. /loop to draft one, or /loop start \"<target>\" for an infinite metricless loop.", "info");
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2071
2084
|
await startDrafting(ctx, "loop");
|
|
2072
2085
|
return;
|
|
2073
2086
|
}
|
|
@@ -2433,6 +2446,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2433
2446
|
updateGoal({
|
|
2434
2447
|
status: "paused",
|
|
2435
2448
|
auditHistory: history,
|
|
2449
|
+
pauseKind: "decision",
|
|
2436
2450
|
pauseReason: `auditor verdict: IMPOSSIBLE — ${reason}`,
|
|
2437
2451
|
pauseSuggestedAction: "The auditor says this goal can never be satisfied as stated. /goal tweak the objective (or /goal cancel), then /goal resume.",
|
|
2438
2452
|
}, ctx);
|
|
@@ -2465,6 +2479,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2465
2479
|
status: "paused",
|
|
2466
2480
|
auditHistory: history,
|
|
2467
2481
|
auditInfraStreak: undefined, // quota reached the auditor — infra streak broken
|
|
2482
|
+
pauseKind: "wait",
|
|
2483
|
+
pauseResumeAt: new Date(Date.now() + quota.retryAfterSec * 1000).toISOString(),
|
|
2468
2484
|
pauseReason: `auditor quota: ${result.error}`,
|
|
2469
2485
|
pauseSuggestedAction: `Quota auto-retry in ${retryMin}m — or /goal resume to retry now`,
|
|
2470
2486
|
}, ctx);
|
|
@@ -2499,6 +2515,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2499
2515
|
status: "paused",
|
|
2500
2516
|
auditHistory: history,
|
|
2501
2517
|
auditInfraStreak: infraStreak,
|
|
2518
|
+
pauseKind: "error",
|
|
2502
2519
|
pauseReason: `auditor infrastructure failed ${infraStreak}× in a row — the auditor model is likely broken (last: ${result.error.slice(0, 120)})`,
|
|
2503
2520
|
pauseSuggestedAction: "Fix the auditor model (/glla model=provider/id) or restart pi, then /goal resume. Your work was NOT judged.",
|
|
2504
2521
|
}, ctx);
|
|
@@ -2605,6 +2622,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2605
2622
|
updateGoal({
|
|
2606
2623
|
status: "paused",
|
|
2607
2624
|
auditHistory: history,
|
|
2625
|
+
pauseKind: "decision",
|
|
2608
2626
|
pauseReason: `auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap})`,
|
|
2609
2627
|
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.",
|
|
2610
2628
|
}, ctx);
|
|
@@ -2639,20 +2657,28 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
2639
2657
|
pi.registerTool(defineTool({
|
|
2640
2658
|
name: "pause_goal",
|
|
2641
2659
|
label: "Pause goal",
|
|
2642
|
-
description: "Pause the active goal with a reason and suggested action. Use when blocked on user input or unable to make progress.",
|
|
2660
|
+
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\".",
|
|
2643
2661
|
parameters: Type.Object({
|
|
2644
2662
|
reason: Type.String({ description: "Why the work is paused" }),
|
|
2645
2663
|
suggestedAction: Type.Optional(Type.String({ description: "What the user should do next" })),
|
|
2664
|
+
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)" })),
|
|
2665
|
+
options: Type.Optional(Type.Array(Type.String(), { description: "For kind=decision: the options the user picks between (one line each)" })),
|
|
2666
|
+
recommended: Type.Optional(Type.Number({ description: "For kind=decision: 1-based index of the recommended option" })),
|
|
2667
|
+
resumeAt: Type.Optional(Type.String({ description: "For kind=wait: ISO time the pause lifts (countdown is shown)" })),
|
|
2646
2668
|
}),
|
|
2647
2669
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
2648
2670
|
const foreign1 = foreignToolGuard(execCtx);
|
|
2649
2671
|
if (foreign1) return { content: [{ type: "text", text: foreign1 }], details: {} };
|
|
2650
|
-
const p = params as { reason: string; suggestedAction?: string };
|
|
2672
|
+
const p = params as { reason: string; suggestedAction?: string; kind?: "decision" | "error" | "wait" | "blocked"; options?: string[]; recommended?: number; resumeAt?: string };
|
|
2651
2673
|
if (!state.goal) return { content: [{ type: "text", text: "No active goal." }], details: {} };
|
|
2652
2674
|
updateGoal({
|
|
2653
2675
|
status: "paused",
|
|
2654
2676
|
pauseReason: p.reason,
|
|
2655
2677
|
pauseSuggestedAction: p.suggestedAction,
|
|
2678
|
+
pauseKind: p.kind,
|
|
2679
|
+
pauseOptions: p.kind === "decision" && p.options && p.options.length > 0 ? p.options : undefined,
|
|
2680
|
+
pauseRecommended: p.kind === "decision" && p.recommended && p.recommended >= 1 ? Math.floor(p.recommended) : undefined,
|
|
2681
|
+
pauseResumeAt: p.kind === "wait" && p.resumeAt ? p.resumeAt : undefined,
|
|
2656
2682
|
}, ctx);
|
|
2657
2683
|
// v0.27.1: surface the FULL pause contract — reason AND suggested
|
|
2658
2684
|
// action. Before, the action only appeared in /goal status and the
|
|
@@ -4455,17 +4481,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4455
4481
|
state.loop = { ...l, active: false, stopReason: HELD_ON_RESTORE };
|
|
4456
4482
|
persistState(ctx);
|
|
4457
4483
|
ctx.ui.notify(
|
|
4458
|
-
`Loop held on restore: ${l.target.slice(0, 60)} — /loop to
|
|
4484
|
+
`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.`,
|
|
4459
4485
|
"info",
|
|
4460
4486
|
);
|
|
4461
4487
|
}
|
|
4462
4488
|
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
4463
|
-
// v0.28.3 (S2 completed): an infra interrupt outranks the DEFAULT
|
|
4464
|
-
// hold — the goal never chose to stop; pi killed its handle. With
|
|
4465
|
-
// autoresume unset, an interrupted goal auto-resumes even on a human
|
|
4466
|
-
// session load; explicit /glla autoresume=off still holds.
|
|
4467
4489
|
const wasInterrupted = !!state.goal.interruptedAt;
|
|
4468
|
-
|
|
4490
|
+
// v0.28.21: the 0.28.3 interrupted-goal exemption is SUPERSEDED —
|
|
4491
|
+
// the default is now hold-everything on session load (user directive:
|
|
4492
|
+
// "load it but not auto start it"). Interrupted goals hold like
|
|
4493
|
+
// everything else; autoresume=on (unattended rigs) still auto-resumes
|
|
4494
|
+
// them, and the marker is cleared only on that promised auto-resume.
|
|
4495
|
+
if (autoResume) {
|
|
4469
4496
|
// v0.28.1 (S2): clear the stale-handle interrupt marker — this IS
|
|
4470
4497
|
// the auto-resume the marker promised.
|
|
4471
4498
|
if (wasInterrupted) updateGoal({ interruptedAt: undefined, interruptedReason: undefined }, ctx);
|
|
@@ -4484,6 +4511,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4484
4511
|
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} waiting in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
|
|
4485
4512
|
updateGoal({
|
|
4486
4513
|
status: "paused",
|
|
4514
|
+
pauseKind: "blocked",
|
|
4487
4515
|
pauseReason: "restored on session load — held for explicit resume",
|
|
4488
4516
|
pauseSuggestedAction: resumeHint,
|
|
4489
4517
|
}, ctx);
|
|
@@ -4506,6 +4534,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4506
4534
|
ctx.ui.notify(`List has ${listQueue().length} item(s) waiting — /list next to activate the head.`, "info");
|
|
4507
4535
|
}
|
|
4508
4536
|
}
|
|
4537
|
+
// v0.28.21: enforce one-active-thing at the restore boundary for DIRTY
|
|
4538
|
+
// legacy states — pre-guard versions could persist an active goal AND
|
|
4539
|
+
// an active/held loop; the chain above handles the loop first, and the
|
|
4540
|
+
// goal would otherwise stay active and fire on agent_end. Pause it:
|
|
4541
|
+
// at most one thing owns the active slot, and nothing auto-starts.
|
|
4542
|
+
if (state.loop && state.goal && state.goal.status === "active") {
|
|
4543
|
+
updateGoal({
|
|
4544
|
+
status: "paused",
|
|
4545
|
+
pauseKind: "decision",
|
|
4546
|
+
pauseReason: "held on session load — the loop owns the active slot (one active thing at a time)",
|
|
4547
|
+
pauseSuggestedAction: "/loop to work the loop, or /loop stop then /goal resume to work the goal",
|
|
4548
|
+
}, ctx);
|
|
4549
|
+
ctx.ui.notify(
|
|
4550
|
+
`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
|
+
"info",
|
|
4552
|
+
);
|
|
4553
|
+
}
|
|
4509
4554
|
// Always paint on session load (v0.22.1): the branches above only reach
|
|
4510
4555
|
// refreshUI via persistState, so a goal that was ALREADY paused (or any
|
|
4511
4556
|
// state that doesn't mutate on load) rendered nothing — "can't tell if
|
|
@@ -4593,6 +4638,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4593
4638
|
if (state.goal) {
|
|
4594
4639
|
updateGoal({
|
|
4595
4640
|
status: "paused",
|
|
4641
|
+
pauseKind: "decision",
|
|
4596
4642
|
pauseReason: `stalled: ${HEARTBEAT_MAX_NUDGES} consecutive unproductive turns (no tools, short or repetitive)`,
|
|
4597
4643
|
pauseSuggestedAction: "Inspect the goal — /goal resume to retry, /goal tweak to narrow it, /goal cancel to abort.",
|
|
4598
4644
|
}, ctx);
|
|
@@ -4640,6 +4686,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4640
4686
|
updateGoal({
|
|
4641
4687
|
usage: { tokensUsed: used, tokensLimit: limit },
|
|
4642
4688
|
status: "paused",
|
|
4689
|
+
pauseKind: "error",
|
|
4643
4690
|
pauseReason: `token limit exceeded (${used.toLocaleString()} > ${limit.toLocaleString()})`,
|
|
4644
4691
|
pauseSuggestedAction: "/glla tokenlimit=<n> to raise the cap (or 0 to disable), then /goal resume",
|
|
4645
4692
|
}, ctx);
|
|
@@ -4663,6 +4710,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4663
4710
|
const reason = `5 consecutive errors${detail}`;
|
|
4664
4711
|
updateGoal({
|
|
4665
4712
|
status: "paused",
|
|
4713
|
+
pauseKind: "wait",
|
|
4714
|
+
pauseResumeAt: new Date(Date.now() + 60_000).toISOString(),
|
|
4666
4715
|
pauseReason: reason,
|
|
4667
4716
|
pauseSuggestedAction: "Transient provider flake? The goal auto-resumes once in 60s if still paused for this reason — or /goal resume now.",
|
|
4668
4717
|
}, ctx);
|
|
@@ -4690,6 +4739,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
4690
4739
|
if (consecutiveAbortIterations >= 5) {
|
|
4691
4740
|
updateGoal({
|
|
4692
4741
|
status: "paused",
|
|
4742
|
+
pauseKind: "blocked",
|
|
4693
4743
|
pauseReason: "5 consecutive aborts (user interrupted)",
|
|
4694
4744
|
pauseSuggestedAction: "You interrupted 5 turns in a row — the goal stays paused until you /goal resume (or /goal cancel).",
|
|
4695
4745
|
}, ctx);
|
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.22",
|
|
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" },
|