pi-goal-list-loop-audit 0.20.0 → 0.21.0
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.
|
@@ -578,3 +578,16 @@ export function buildTaskSummary(tasks: Task[]): string {
|
|
|
578
578
|
export function cloneGoal(goal: Goal): Goal {
|
|
579
579
|
return JSON.parse(JSON.stringify(goal));
|
|
580
580
|
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Session-restore gate (v0.21.0): a session that carries conversation
|
|
584
|
+
* history ("resume" | "reload" | "fork") IS the goal's own context —
|
|
585
|
+
* auto-resuming work there is natural. A fresh session ("startup" | "new",
|
|
586
|
+
* or an older pi that reports no reason) has no context — restored state
|
|
587
|
+
* HOLDS until an explicit /goal resume (or /glla autoresume=on, the rig
|
|
588
|
+
* setting for unattended restarts). One mechanical predicate; no heuristics.
|
|
589
|
+
*/
|
|
590
|
+
export function shouldAutoResumeOnSessionStart(reason: string | undefined, autoResume: boolean | undefined): boolean {
|
|
591
|
+
if (autoResume === true) return true;
|
|
592
|
+
return reason === "resume" || reason === "reload" || reason === "fork";
|
|
593
|
+
}
|
|
@@ -17,9 +17,11 @@ export function fmtElapsed(ms: number): string {
|
|
|
17
17
|
const s = Math.floor(ms / 1000);
|
|
18
18
|
if (s < 60) return `${s}s`;
|
|
19
19
|
const m = Math.floor(s / 60);
|
|
20
|
-
|
|
20
|
+
// Seconds stay visible up to the hour: the elapsed counter is the
|
|
21
|
+
// liveness signal — minute-only granularity looks frozen on a 1s tick.
|
|
22
|
+
if (m < 60) return `${m}m ${String(s % 60).padStart(2, "0")}s`;
|
|
21
23
|
const h = Math.floor(m / 60);
|
|
22
|
-
return `${h}h${m % 60}m`;
|
|
24
|
+
return `${h}h ${String(m % 60).padStart(2, "0")}m`;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function fmtTokens(n: number): string {
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
piGlaDir,
|
|
56
56
|
readState,
|
|
57
57
|
renderGoalMarkdown,
|
|
58
|
+
shouldAutoResumeOnSessionStart,
|
|
58
59
|
statusLabel,
|
|
59
60
|
writeGoalMd,
|
|
60
61
|
} from "../goal-loop-core.js";
|
|
@@ -87,6 +88,8 @@ import {
|
|
|
87
88
|
|
|
88
89
|
const GOAL_EVENT_ENTRY = "goal-event";
|
|
89
90
|
const STATE_ENTRY = "goal-state";
|
|
91
|
+
/** stopReason marker for a loop held (not stopped) by the fresh-session restore gate. */
|
|
92
|
+
const HELD_ON_RESTORE = "held: restored in a fresh session";
|
|
90
93
|
|
|
91
94
|
// =================================================================
|
|
92
95
|
// Module-level state (one per session)
|
|
@@ -158,7 +161,7 @@ function startUITicker(): void {
|
|
|
158
161
|
uiTicker = setInterval(() => {
|
|
159
162
|
const ctx = freshCtx();
|
|
160
163
|
if (ctx && isSupervising()) refreshUI(ctx);
|
|
161
|
-
},
|
|
164
|
+
}, 1_000);
|
|
162
165
|
uiTicker.unref?.();
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -1142,12 +1145,24 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1142
1145
|
const rest = args.trim().slice(sub.length).trim();
|
|
1143
1146
|
|
|
1144
1147
|
if (!sub) {
|
|
1145
|
-
// /loop with no args →
|
|
1146
|
-
//
|
|
1148
|
+
// /loop with no args → resume a held loop if one is waiting; otherwise
|
|
1149
|
+
// draft the loop config (metric design is the whole game for a
|
|
1150
|
+
// long-running loop; never start one blind).
|
|
1147
1151
|
if (isLoopActive()) {
|
|
1148
1152
|
ctx.ui.notify("A loop is already active — /loop status to inspect, /loop stop to end it.", "info");
|
|
1149
1153
|
return;
|
|
1150
1154
|
}
|
|
1155
|
+
const stored = state.loop;
|
|
1156
|
+
if (stored && !stored.active && stored.stopReason === HELD_ON_RESTORE) {
|
|
1157
|
+
state.loop = { ...stored, active: true, stopReason: undefined };
|
|
1158
|
+
persistState(ctx);
|
|
1159
|
+
scheduleLoopTick(ctx);
|
|
1160
|
+
ctx.ui.notify(
|
|
1161
|
+
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations} · best ${stored.bestValue ?? "n/a"} — ${stored.target.slice(0, 60)}`,
|
|
1162
|
+
"info",
|
|
1163
|
+
);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1151
1166
|
await startDrafting(ctx, "loop");
|
|
1152
1167
|
return;
|
|
1153
1168
|
}
|
|
@@ -1843,6 +1858,9 @@ interface Settings {
|
|
|
1843
1858
|
notifyCmd?: string;
|
|
1844
1859
|
/** Per-goal token budget; crossing it pauses the goal. Default 1,000,000. */
|
|
1845
1860
|
tokenLimit?: number;
|
|
1861
|
+
/** on → restored goals/loops/lists auto-resume even in fresh sessions
|
|
1862
|
+
* (unattended rigs). Default off: restore holds until /goal resume. */
|
|
1863
|
+
autoResume?: boolean;
|
|
1846
1864
|
}
|
|
1847
1865
|
|
|
1848
1866
|
const DEFAULT_SETTINGS: Settings = {
|
|
@@ -1887,7 +1905,7 @@ function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknow
|
|
|
1887
1905
|
const glob = readSettingsFile(globalSettingsPath());
|
|
1888
1906
|
const effective = loadSettings(cwd);
|
|
1889
1907
|
const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
|
|
1890
|
-
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit"];
|
|
1908
|
+
const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "autoResume"];
|
|
1891
1909
|
for (const k of keys) {
|
|
1892
1910
|
if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
|
|
1893
1911
|
else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
|
|
@@ -2046,6 +2064,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2046
2064
|
fmt("auditorThinkingLevel", "thinking"),
|
|
2047
2065
|
fmt("notifyCmd", "notify"),
|
|
2048
2066
|
fmt("tokenLimit", "tokenLimit"),
|
|
2067
|
+
fmt("autoResume", "autoResume"),
|
|
2049
2068
|
`\nglobal: ${globalSettingsPath()}`,
|
|
2050
2069
|
`project: ${projectSettingsPath(ctx.cwd)}`,
|
|
2051
2070
|
`Set with: /glla key=value (global) · /glla project key=value (project override)`,
|
|
@@ -2084,6 +2103,16 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2084
2103
|
} else {
|
|
2085
2104
|
ctx.ui.notify(`tokenlimit must be a positive integer, got: ${value}`, "warning");
|
|
2086
2105
|
}
|
|
2106
|
+
} else if (key === "autoresume") {
|
|
2107
|
+
if (["on", "true", "1", "yes"].includes(value)) {
|
|
2108
|
+
patch.autoResume = true;
|
|
2109
|
+
changed = true;
|
|
2110
|
+
} else if (["off", "false", "0", "no", "unset"].includes(value)) {
|
|
2111
|
+
patch.autoResume = undefined;
|
|
2112
|
+
changed = true;
|
|
2113
|
+
} else {
|
|
2114
|
+
ctx.ui.notify(`autoresume must be on or off, got: ${value}`, "warning");
|
|
2115
|
+
}
|
|
2087
2116
|
} else if (key === "thinking" || key === "auditorthinkinglevel") {
|
|
2088
2117
|
if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
|
|
2089
2118
|
patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
|
|
@@ -2094,13 +2123,13 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2094
2123
|
}
|
|
2095
2124
|
}
|
|
2096
2125
|
if (!changed) {
|
|
2097
|
-
ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit), optionally prefixed with 'project'.", "info");
|
|
2126
|
+
ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit, autoresume), optionally prefixed with 'project'.", "info");
|
|
2098
2127
|
return;
|
|
2099
2128
|
}
|
|
2100
2129
|
saveSettings(scope, ctx.cwd, patch);
|
|
2101
2130
|
const effective = loadSettings(ctx.cwd);
|
|
2102
2131
|
ctx.ui.notify(
|
|
2103
|
-
`Saved to ${scope} config. Effective now: model=${effective.auditorModel ?? "(session model)"} thinking=${effective.auditorThinkingLevel ?? "(session)"} notify=${effective.notifyCmd ?? "(off)"} tokenLimit=${effective.tokenLimit ?? 0}${(effective.tokenLimit ?? 0) > 0 ? "" : " (off)"}\n` +
|
|
2132
|
+
`Saved to ${scope} config. Effective now: model=${effective.auditorModel ?? "(session model)"} thinking=${effective.auditorThinkingLevel ?? "(session)"} notify=${effective.notifyCmd ?? "(off)"} tokenLimit=${effective.tokenLimit ?? 0}${(effective.tokenLimit ?? 0) > 0 ? "" : " (off)"} autoResume=${effective.autoResume === true ? "on" : "off"}\n` +
|
|
2104
2133
|
`Note: the auditor runs without extensions — it must be a built-in provider, not an extension-registered one.`,
|
|
2105
2134
|
"info",
|
|
2106
2135
|
);
|
|
@@ -2228,7 +2257,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2228
2257
|
}
|
|
2229
2258
|
});
|
|
2230
2259
|
|
|
2231
|
-
pi.on("session_start", async (
|
|
2260
|
+
pi.on("session_start", async (event: any, ctx: ExtensionContext) => {
|
|
2232
2261
|
rememberCtx(ctx);
|
|
2233
2262
|
state = readState(ctx.cwd);
|
|
2234
2263
|
if (!registeredCtx) {
|
|
@@ -2237,27 +2266,60 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2237
2266
|
}
|
|
2238
2267
|
warnOnCommandCollision(ctx);
|
|
2239
2268
|
warnIfAuditorProviderRisky(ctx);
|
|
2240
|
-
//
|
|
2269
|
+
// Restore gate (v0.21.0): a fresh session ("startup"/"new", or a pi too
|
|
2270
|
+
// old to report a reason) has no conversation context for the restored
|
|
2271
|
+
// work — HOLD instead of auto-firing, so opening pi in a folder never
|
|
2272
|
+
// starts working before you can even load your session. Sessions with
|
|
2273
|
+
// history ("resume"/"reload"/"fork") auto-resume; /glla autoresume=on
|
|
2274
|
+
// opts a project into auto-resume everywhere (unattended rigs).
|
|
2275
|
+
const autoResume = shouldAutoResumeOnSessionStart(event?.reason, loadSettings(ctx.cwd).autoResume);
|
|
2241
2276
|
if (isLoopActive()) {
|
|
2242
2277
|
const l = state.loop!;
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2278
|
+
if (autoResume) {
|
|
2279
|
+
ctx.ui.notify(
|
|
2280
|
+
`Resuming loop (iteration ${l.iteration}/${l.maxIterations}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${l.target.slice(0, 60)}`,
|
|
2281
|
+
"info",
|
|
2282
|
+
);
|
|
2283
|
+
scheduleLoopTick(ctx);
|
|
2284
|
+
} else {
|
|
2285
|
+
state.loop = { ...l, active: false, stopReason: HELD_ON_RESTORE };
|
|
2286
|
+
persistState(ctx);
|
|
2287
|
+
ctx.ui.notify(
|
|
2288
|
+
`Loop held on restore (fresh session, no work started): ${l.target.slice(0, 60)} — /loop to resume, /glla autoresume=on to auto-resume in this project.`,
|
|
2289
|
+
"info",
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
2293
|
+
if (autoResume) {
|
|
2294
|
+
ctx.ui.notify(
|
|
2295
|
+
`Resuming goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2296
|
+
"info",
|
|
2297
|
+
);
|
|
2298
|
+
scheduleContinuation(ctx, true);
|
|
2299
|
+
} else {
|
|
2300
|
+
updateGoal({
|
|
2301
|
+
status: "paused",
|
|
2302
|
+
pauseReason: "restored in a fresh session — no work started",
|
|
2303
|
+
pauseSuggestedAction: "/goal resume to continue · /glla autoresume=on to auto-resume in this project",
|
|
2304
|
+
}, ctx);
|
|
2305
|
+
ctx.ui.notify(
|
|
2306
|
+
`Goal held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)} — /goal resume to continue.`,
|
|
2307
|
+
"info",
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2247
2310
|
} else if (state.goal && state.goal.status === "active") {
|
|
2311
|
+
// Active but autoContinue off: nothing auto-fires — just surface it.
|
|
2248
2312
|
ctx.ui.notify(
|
|
2249
|
-
`
|
|
2313
|
+
`Restored goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2250
2314
|
"info",
|
|
2251
2315
|
);
|
|
2252
|
-
}
|
|
2253
|
-
if (isLoopActive()) {
|
|
2254
|
-
// Session restarted mid-loop: resume measuring from persisted state.
|
|
2255
|
-
scheduleLoopTick(ctx);
|
|
2256
|
-
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
2257
|
-
scheduleContinuation(ctx, true);
|
|
2258
2316
|
} else if ((!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") && listQueue().length > 0) {
|
|
2259
|
-
|
|
2260
|
-
|
|
2317
|
+
if (autoResume) {
|
|
2318
|
+
// Session restarted with a non-empty queue but no active goal.
|
|
2319
|
+
activateNextListItem(ctx);
|
|
2320
|
+
} else {
|
|
2321
|
+
ctx.ui.notify(`List has ${listQueue().length} item(s) waiting — /list next to activate the head.`, "info");
|
|
2322
|
+
}
|
|
2261
2323
|
}
|
|
2262
2324
|
});
|
|
2263
2325
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. — 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 — only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|