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 +14 -0
- package/extensions/goal-loop-forever.ts +17 -0
- package/extensions/goal-loop.ts +34 -2
- package/extensions/loops/goal-tools.ts +11 -6
- package/package.json +1 -1
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
|
package/extensions/goal-loop.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
1947
|
-
|
|
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:
|
|
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.
|
|
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",
|