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
- if (m < 60) return `${m}m`;
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 {
@@ -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
- }, 5_000);
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 → draft the loop config (metric design is the whole
1146
- // game for a long-running loop; never start one blind).
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 (_event: any, ctx: ExtensionContext) => {
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
- // Resumption notice: make persisted supervisor state visible on startup.
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
- ctx.ui.notify(
2244
- `Resuming loop (iteration ${l.iteration}/${l.maxIterations}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${l.target.slice(0, 60)}`,
2245
- "info",
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
- `Resuming goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
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
- // Session restarted with a non-empty queue but no active goal.
2260
- activateNextListItem(ctx);
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.20.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",