pi-goal-list-loop-audit 0.35.67 → 0.35.68

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.68 — bound-stop recovery (2026-08-26)
4
+
5
+ ### Fixed
6
+ Explicit `/loop resume` now recovers time- and token-bound stops as fresh
7
+ supervised windows without discarding iteration, history, or best-value
8
+ state. Recoverable stopped loops can accept a confirmed
9
+ `propose_loop_refine` change while remaining stopped until explicitly
10
+ resumed. Clean max-iteration and finished loops remain terminal; automatic
11
+ startup does not silently reset an explicit budget.
12
+
13
+ ### Tests
14
+ Coverage verifies fresh time windows, token-budget resets, preserved loop
15
+ history, stopped-loop refinement, and the unchanged max-iteration guard.
16
+
3
17
  ## 0.35.67 — in-band provider-result recovery (2026-08-26)
4
18
 
5
19
  ### Fixed
@@ -54,6 +54,23 @@ export function isLifecycleHeldLoopReason(reason?: string): boolean {
54
54
  || !!reason?.startsWith("send-retry storm:");
55
55
  }
56
56
 
57
+ /** A stopped loop can be respecified without discarding its history when the
58
+ * stop is a recoverable work failure or an explicit time/token window. Max
59
+ * iterations and clean/user stops remain terminal until a fresh `/loop start`.
60
+ */
61
+ export function isRefinableStoppedLoopReason(reason?: string): boolean {
62
+ return !!reason && (
63
+ reason.startsWith("time bound reached")
64
+ || reason.startsWith("token budget exhausted")
65
+ || reason.startsWith("stuck —")
66
+ || reason.startsWith("plateau —")
67
+ || reason.startsWith("metric never moved —")
68
+ || reason.startsWith("measure command broken —")
69
+ || reason.startsWith("provider errors —")
70
+ || reason.startsWith("stalled:")
71
+ );
72
+ }
73
+
57
74
  export interface LoopState {
58
75
  target: string;
59
76
  /** v0.23.0: optional — a metricless "spec loop" (measure=none) has no
@@ -939,6 +939,8 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
939
939
  !!r?.startsWith("plateau —") ||
940
940
  !!r?.startsWith("stalled:") ||
941
941
  !!r?.startsWith("stuck —") ||
942
+ !!r?.startsWith("time bound reached") ||
943
+ !!r?.startsWith("token budget exhausted") ||
942
944
  // v0.35.54 (collect-pass HIGH finding): the v0.35.31 "metric never
943
945
  // moved" stop message promises "/loop resume retries or /loop stop",
944
946
  // but this predicate never matched that prefix — the promised command
@@ -983,8 +985,33 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
983
985
  // An explicit resume re-arms the counters: fresh stall window,
984
986
  // cleared dead-turn/stuck streaks, reprieves restored — the user
985
987
  // saying "push again" wins over the ladder's memory (v0.29.19).
986
- state.loop = { ...stored, active: true, stopReason: undefined, consecutiveErrors: 0, consecutiveStuck: 0, lastStuckReason: undefined, stallCount: 0, auditPlateauReprieves: 0 };
988
+ // Time and token bounds are per supervised run window: resuming a
989
+ // bound-stopped loop preserves its iteration/history/best but starts a
990
+ // fresh elapsed-time window or token budget instead of stopping again
991
+ // on the same bound.
992
+ const resetTimeWindow = stored.stopReason?.startsWith("time bound reached") ?? false;
993
+ const resetTokenBudget = stored.stopReason?.startsWith("token budget exhausted") ?? false;
994
+ const resumedAt = nowIso();
995
+ state.loop = {
996
+ ...stored,
997
+ active: true,
998
+ stopReason: undefined,
999
+ consecutiveErrors: 0,
1000
+ consecutiveStuck: 0,
1001
+ lastStuckReason: undefined,
1002
+ stallCount: 0,
1003
+ auditPlateauReprieves: 0,
1004
+ ...(resetTimeWindow ? { startedAt: resumedAt } : {}),
1005
+ ...(resetTokenBudget ? { tokensUsed: 0 } : {}),
1006
+ };
987
1007
  persistState(ctx);
1008
+ if (resetTimeWindow || resetTokenBudget) {
1009
+ appendLedger(ctx.cwd, "loop_bound_window_reset", {
1010
+ timeWindow: resetTimeWindow,
1011
+ tokenBudget: resetTokenBudget,
1012
+ iteration: stored.iteration,
1013
+ });
1014
+ }
988
1015
  // v0.35.23 (note.md Next #2): an explicit resume is exactly the
989
1016
  // decision a load hold waits for — release it or the tick below
990
1017
  // would be frozen.
@@ -995,8 +1022,13 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
995
1022
  releaseContinuationDispatchStandDown();
996
1023
  releaseAuditorSurface();
997
1024
  scheduleLoopTick(ctx);
1025
+ const boundResetNote = resetTimeWindow
1026
+ ? " · fresh time window"
1027
+ : resetTokenBudget
1028
+ ? " · fresh token budget"
1029
+ : "";
998
1030
  ctx.ui.notify(
999
- `Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"} — ${displaySlice(stored.target, 60)}`,
1031
+ `Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"}${boundResetNote} — ${displaySlice(stored.target, 60)}`,
1000
1032
  "info",
1001
1033
  );
1002
1034
  return;
@@ -318,6 +318,7 @@ import {
318
318
  listAuditFanoutItemText,
319
319
  type LoopTickOutcome,
320
320
  HELD_ON_RESTORE,
321
+ isRefinableStoppedLoopReason,
321
322
  type LoopState,
322
323
  } from "../goal-loop-forever.js";
323
324
  import {
@@ -1928,7 +1929,7 @@ function registerAgentTools(pi: any): void {
1928
1929
  pi.registerTool(defineTool({
1929
1930
  name: "propose_loop_refine",
1930
1931
  label: "Propose loop spec refinement",
1931
- description: "While a loop is ACTIVE, propose refining the loop's spec — sharpen the target and/or change the measure command — when the current spec no longer captures 'better'. The user confirms; on a measure change the orchestrator test-runs the new command and re-baselines. Never edit the measure command or its inputs directly — that is gaming the metric.",
1932
+ description: "While a loop is active or safely stopped by a recoverable bound/failure, propose refining its spec — sharpen the target and/or change the measure command — when the current spec no longer captures 'better'. The user confirms; on a measure change the orchestrator test-runs the new command and re-baselines. Never edit the measure command or its inputs directly — that is gaming the metric.",
1932
1933
  parameters: Type.Object({
1933
1934
  target: Type.Optional(Type.String({ description: "The sharpened target text (omit to keep the current target)" })),
1934
1935
  measureCmd: Type.Optional(Type.String({ description: "The new measure command printing ONE number (omit to keep the current metric)" })),
@@ -1943,9 +1944,11 @@ function registerAgentTools(pi: any): void {
1943
1944
  const liveCtx = currentToolContext(execCtx);
1944
1945
  if (!liveCtx) return staleToolResult();
1945
1946
  const loop = state.loop;
1946
- if (!loop?.active) {
1947
- return { content: [{ type: "text", text: "No active loop to refine. propose_loop_refine is only valid while a loop is running." }], details: {} };
1947
+ const stoppedRefinable = !!loop && !loop.active && isRefinableStoppedLoopReason(loop.stopReason);
1948
+ if (!loop || (!loop.active && !stoppedRefinable)) {
1949
+ return { content: [{ type: "text", text: "No refinable loop is available. propose_loop_refine applies while a loop is running or after a recoverable bound/failure stop; clean max-iteration and user-finished loops require /loop start." }], details: {} };
1948
1950
  }
1951
+ const wasActive = loop.active;
1949
1952
  const newTarget = p.target?.trim() || loop.target;
1950
1953
  const newMeasure = p.measureCmd?.trim() || loop.measureCmd || "";
1951
1954
  // v0.23.0: a metricless loop can't be refined into a measured one
@@ -1989,7 +1992,7 @@ function registerAgentTools(pi: any): void {
1989
1992
  confirmed = (await confirmDraft(
1990
1993
  liveCtx,
1991
1994
  "Confirm loop spec refinement",
1992
- `Rationale: ${sanitizeDisplayText(p.rationale)}\n\nTarget:\n old: ${displaySlice(loop.target, 120)}\n new: ${displaySlice(newTarget, 120)}\n\nMeasure:\n old: ${sanitizeDisplayText(loop.measureCmd ?? "none")}\n new: ${sanitizeDisplayText(newMeasure)}${newMeasure !== loop.measureCmd ? `\n test-run: ${sanitizeDisplayText(testOutput).slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${sanitizeDisplayText(loop.specFile ?? "")}:\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${sanitizeDisplayText(p.specAppend!.trim()).slice(0, 120)}` : ""}` : ""}\n\nThe loop keeps running against the refined spec (iteration ${loop.iteration} so far). Apply?`,
1995
+ `Rationale: ${sanitizeDisplayText(p.rationale)}\n\nTarget:\n old: ${displaySlice(loop.target, 120)}\n new: ${displaySlice(newTarget, 120)}\n\nMeasure:\n old: ${sanitizeDisplayText(loop.measureCmd ?? "none")}\n new: ${sanitizeDisplayText(newMeasure)}${newMeasure !== loop.measureCmd ? `\n test-run: ${sanitizeDisplayText(testOutput).slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${sanitizeDisplayText(loop.specFile ?? "")}:\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${sanitizeDisplayText(p.specAppend!.trim()).slice(0, 120)}` : ""}` : ""}\n\nThe loop ${wasActive ? "keeps running" : "stays stopped until /loop resume"} against the refined spec (iteration ${loop.iteration} so far). Apply?`,
1993
1996
  )) === "yes";
1994
1997
  } catch {
1995
1998
  confirmed = false;
@@ -2026,8 +2029,10 @@ function registerAgentTools(pi: any): void {
2026
2029
  }
2027
2030
  persistState(liveCtx);
2028
2031
  appendLedger(liveCtx.cwd, "loop_refined", { iteration: loop.iteration, newTarget, newMeasureCmd: newMeasure, newBaseline, specChanged: specChange || undefined });
2029
- liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}`, "info");
2030
- return { content: [{ type: "text", text: "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn." }], details: {} };
2032
+ liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}${wasActive ? "" : " Run /loop resume to continue with the preserved history."}`, "info");
2033
+ return { content: [{ type: "text", text: wasActive
2034
+ ? "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn."
2035
+ : "Refinement confirmed and applied to the stopped loop. Run /loop resume to continue with the preserved history." }], details: {} };
2031
2036
  },
2032
2037
  }));
2033
2038
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.35.67",
3
+ "version": "0.35.68",
4
4
  "description": "Mission control for autonomous pi: interview-drafted goals, an audited task queue, and forever-loops (metric, spec, project-audit) that run for hours. A detached extension-less auditor process re-verifies every completion with raw evidence without holding the main pi turn; confirmed drafts, decision pauses and consent gates keep you in charge.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "dracon",