pi-goal-list-loop-audit 0.35.67 → 0.35.69
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 +26 -0
- package/INSTALL.md +5 -3
- package/README.md +5 -2
- package/extensions/goal-loop-forever.ts +31 -1
- package/extensions/goal-loop.ts +78 -9
- package/extensions/loops/goal-activation.ts +3 -3
- package/extensions/loops/goal-tools.ts +20 -10
- package/package.json +1 -1
- package/prompts/goal-loop-forever-draft.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.69 — metricless-loop cadence (2026-08-26)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
Metricless and measured loops accept an opt-in `cadence=<seconds>` minimum
|
|
7
|
+
interval between successful automatic iterations. The cadence is persisted,
|
|
8
|
+
shown in `/loop status` and loop prompts, and explicit starts/resumes bypass
|
|
9
|
+
it for an urgent wake. The default remains unchanged.
|
|
10
|
+
|
|
11
|
+
### Tests
|
|
12
|
+
Coverage verifies cadence parsing, delayed automatic re-wakes, urgent
|
|
13
|
+
explicit starts, and the existing unbounded metricless behavior.
|
|
14
|
+
|
|
15
|
+
## 0.35.68 — bound-stop recovery (2026-08-26)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
Explicit `/loop resume` now recovers time- and token-bound stops as fresh
|
|
19
|
+
supervised windows without discarding iteration, history, or best-value
|
|
20
|
+
state. Recoverable stopped loops can accept a confirmed
|
|
21
|
+
`propose_loop_refine` change while remaining stopped until explicitly
|
|
22
|
+
resumed. Clean max-iteration and finished loops remain terminal; automatic
|
|
23
|
+
startup does not silently reset an explicit budget.
|
|
24
|
+
|
|
25
|
+
### Tests
|
|
26
|
+
Coverage verifies fresh time windows, token-budget resets, preserved loop
|
|
27
|
+
history, stopped-loop refinement, and the unchanged max-iteration guard.
|
|
28
|
+
|
|
3
29
|
## 0.35.67 — in-band provider-result recovery (2026-08-26)
|
|
4
30
|
|
|
5
31
|
### Fixed
|
package/INSTALL.md
CHANGED
|
@@ -98,13 +98,15 @@ waiting for a decision.
|
|
|
98
98
|
|
|
99
99
|
/loop
|
|
100
100
|
/loop start "reduce flaky tests" measure="..." direction=min
|
|
101
|
-
/loop start "keep improving the spec" measure=none max=20
|
|
101
|
+
/loop start "keep improving the spec" measure=none max=20 cadence=900
|
|
102
102
|
/loop audit
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Use `/goal` for one outcome, `/list` for several independently auditable
|
|
106
|
-
outcomes, and `/loop` for an improvement process without one final item.
|
|
107
|
-
|
|
106
|
+
outcomes, and `/loop` for an improvement process without one final item. For
|
|
107
|
+
metricless loops that intentionally mature between checks, add optional
|
|
108
|
+
`cadence=<seconds>`; the interval is visible in `/loop status`, while explicit
|
|
109
|
+
starts/resumes remain urgent. See the README for the full command semantics.
|
|
108
110
|
|
|
109
111
|
## Modes
|
|
110
112
|
|
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ automatic repeats are fenced. Use `/list resume` for an intentional retry and
|
|
|
170
170
|
/loop # interview + Confirm
|
|
171
171
|
/loop plan # research-first loop design
|
|
172
172
|
/loop start "reduce flaky tests" measure="..." direction=min
|
|
173
|
-
/loop start "keep improving the spec" measure=none max=20
|
|
173
|
+
/loop start "keep improving the spec" measure=none max=20 cadence=900
|
|
174
174
|
/loop audit # recurring project-audit cadence
|
|
175
175
|
/loop status
|
|
176
176
|
/loop stop
|
|
@@ -183,7 +183,10 @@ There are three loop styles:
|
|
|
183
183
|
before you confirm it and stops on plateau or a configured bound.
|
|
184
184
|
- **Metricless specification:** no honest number exists, so the loop advances
|
|
185
185
|
a specification or checklist. It ends at its time/token/iteration bound or
|
|
186
|
-
`/loop stop`; it has no fake plateau metric.
|
|
186
|
+
`/loop stop`; it has no fake plateau metric. Add optional
|
|
187
|
+
`cadence=<seconds>` to put a minimum gap between successful automatic
|
|
188
|
+
iterations; explicit starts/resumes remain urgent and `/loop status` shows
|
|
189
|
+
the armed cadence.
|
|
187
190
|
- **Project audit:** each iteration looks for the next important finding,
|
|
188
191
|
appends evidence to the audit ledger, and works through the findings.
|
|
189
192
|
|
|
@@ -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
|
|
@@ -103,6 +120,12 @@ export interface LoopState {
|
|
|
103
120
|
tokenBudget?: number;
|
|
104
121
|
/** v0.15.0: accumulated loop tokens (input+output), orchestrator-counted. */
|
|
105
122
|
tokensUsed?: number;
|
|
123
|
+
/** v0.35.x: optional minimum gap between successful metricless-loop
|
|
124
|
+
* iterations. Units are milliseconds internally; absent means unchanged
|
|
125
|
+
* immediate cadence. */
|
|
126
|
+
minimumIterationIntervalMs?: number;
|
|
127
|
+
/** v0.35.x: completion timestamp used to arm the next cadence window. */
|
|
128
|
+
lastIterationCompletedAt?: string;
|
|
106
129
|
/** v0.15.0: living spec — user-confirmed target/measure refinements. */
|
|
107
130
|
refinements?: LoopRefinement[];
|
|
108
131
|
/** branch=1 mode: scratch branch holding the loop's commits. */
|
|
@@ -382,6 +405,8 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
382
405
|
timeLimitHours?: number;
|
|
383
406
|
tokenBudget?: number;
|
|
384
407
|
toolSameRepeat?: number;
|
|
408
|
+
/** v0.35.x: optional metricless minimum cadence, supplied in seconds. */
|
|
409
|
+
minimumIterationIntervalMs?: number;
|
|
385
410
|
} {
|
|
386
411
|
// Key=value pairs first (measure= and direction= may hold quoted values),
|
|
387
412
|
// the remaining text is the target. v0.35.4: quoted spans are TARGET
|
|
@@ -392,7 +417,7 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
392
417
|
let rest = raw.trim();
|
|
393
418
|
const kv = new Map<string, string>();
|
|
394
419
|
const kvRe = /(\w+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
|
|
395
|
-
const KNOWN_KEYS = new Set(["measure", "direction", "window", "max", "branch", "force", "done", "time", "tokens", "toolsamerepeat"]);
|
|
420
|
+
const KNOWN_KEYS = new Set(["measure", "direction", "window", "max", "branch", "force", "done", "time", "tokens", "toolsamerepeat", "cadence"]);
|
|
396
421
|
const quoteSpans: Array<[number, number]> = [];
|
|
397
422
|
const quoteRe = /"([^"]*)"|'([^']*)'/g;
|
|
398
423
|
let qm: RegExpExecArray | null;
|
|
@@ -447,6 +472,10 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
447
472
|
}
|
|
448
473
|
const timeRaw = Number.parseFloat(kv.get("time") ?? "");
|
|
449
474
|
const tokensRaw = Number.parseInt(kv.get("tokens") ?? "", 10);
|
|
475
|
+
const cadenceRaw = Number.parseFloat(kv.get("cadence") ?? "");
|
|
476
|
+
const cadenceMs = Number.isFinite(cadenceRaw) && cadenceRaw > 0
|
|
477
|
+
? Math.min(Math.round(cadenceRaw * 1_000), 24 * 60 * 60_000)
|
|
478
|
+
: undefined;
|
|
450
479
|
return {
|
|
451
480
|
target,
|
|
452
481
|
measureCmd: metricless ? "" : measureRaw,
|
|
@@ -467,6 +496,7 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
467
496
|
const n = Number.parseInt(raw, 10);
|
|
468
497
|
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
469
498
|
})(),
|
|
499
|
+
...(cadenceMs !== undefined ? { minimumIterationIntervalMs: cadenceMs } : {}),
|
|
470
500
|
};
|
|
471
501
|
}
|
|
472
502
|
|
package/extensions/goal-loop.ts
CHANGED
|
@@ -282,6 +282,17 @@ function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: strin
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
285
|
+
// v0.35.15: `/glla pause` freezes loop re-arms too — the supervisor's
|
|
286
|
+
if (supervisorPaused(state)) return;
|
|
287
|
+
scheduleLoopTickWithUrgency(ctx, false);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function scheduleLoopTickUrgent(ctx: ExtensionContext): void {
|
|
291
|
+
if (supervisorPaused(state)) return;
|
|
292
|
+
scheduleLoopTickWithUrgency(ctx, true);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function scheduleLoopTickWithUrgency(ctx: ExtensionContext, urgent: boolean): void {
|
|
285
296
|
// v0.35.15: `/glla pause` freezes loop re-arms too — the supervisor's
|
|
286
297
|
// automatic machinery includes the metric loop's turn dispatch.
|
|
287
298
|
if (supervisorPaused(state)) return;
|
|
@@ -295,7 +306,18 @@ function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
|
295
306
|
} catch {
|
|
296
307
|
return;
|
|
297
308
|
}
|
|
298
|
-
|
|
309
|
+
// A cadence is an intentional maturity gap between successful iterations,
|
|
310
|
+
// not a replacement for the busy-send backoff. Explicit starts/resumes are
|
|
311
|
+
// urgent wakes and bypass the gap once; automatic re-arms honor it.
|
|
312
|
+
if (!urgent) {
|
|
313
|
+
const loop = state.loop;
|
|
314
|
+
const intervalMs = loop?.minimumIterationIntervalMs;
|
|
315
|
+
const completedAt = loop?.lastIterationCompletedAt ? Date.parse(loop.lastIterationCompletedAt) : Number.NaN;
|
|
316
|
+
if (intervalMs !== undefined && Number.isFinite(completedAt)) {
|
|
317
|
+
delay = Math.max(delay, completedAt + intervalMs - Date.now());
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
loopTimer = scheduleSessionTimeout(() => sendLoopTurn(), Math.max(0, delay));
|
|
299
321
|
}
|
|
300
322
|
|
|
301
323
|
function sendLoopTurn(): void {
|
|
@@ -379,6 +401,9 @@ function sendLoopTurn(): void {
|
|
|
379
401
|
} else if (bounds.length) {
|
|
380
402
|
boundsNote = `\n- Arbitrary bounds: the loop also stops after ${bounds.join(" or ")}`;
|
|
381
403
|
}
|
|
404
|
+
if (loop.minimumIterationIntervalMs !== undefined) {
|
|
405
|
+
boundsNote += `\n- Minimum cadence: wait at least ${Math.ceil(loop.minimumIterationIntervalMs / 1_000)}s after each completed iteration before the next automatic wake; explicit starts/resumes are urgent.`;
|
|
406
|
+
}
|
|
382
407
|
// v0.24.0: a stuck intervention REPLACES the pep talk — the rotating
|
|
383
408
|
// directive names why the loop is stuck and what rung of the ladder it's on.
|
|
384
409
|
// v0.29.19: a plateau reprieve's one-shot shove takes priority over the
|
|
@@ -584,7 +609,9 @@ async function runLoopTick(initialCtx: ExtensionContext, event?: any): Promise<v
|
|
|
584
609
|
loop.consecutiveStuck = 0;
|
|
585
610
|
loop.lastStuckReason = undefined;
|
|
586
611
|
}
|
|
587
|
-
|
|
612
|
+
const completedAt = nowIso();
|
|
613
|
+
let outcome: LoopTickOutcome = metricless ? applyMetriclessTick(loop, completedAt) : applyMeasurement(loop, value, completedAt);
|
|
614
|
+
loop.lastIterationCompletedAt = completedAt;
|
|
588
615
|
// v0.33.2: close the hypothesis feedback loop — the prediction went into
|
|
589
616
|
// the ledger; now the VERDICT rides the next iteration's prompt.
|
|
590
617
|
if (loop.lastHypothesis) {
|
|
@@ -766,6 +793,8 @@ interface LoopConfig {
|
|
|
766
793
|
force?: boolean;
|
|
767
794
|
timeLimitHours?: number;
|
|
768
795
|
tokenBudget?: number;
|
|
796
|
+
/** v0.35.x: optional metricless minimum cadence in milliseconds. */
|
|
797
|
+
minimumIterationIntervalMs?: number;
|
|
769
798
|
/** v0.25.1: /loop start toolsamerepeat=N (0 = disable legacy check). */
|
|
770
799
|
toolSameRepeat?: number;
|
|
771
800
|
/** v0.29.10: don't seed bestValue from the pre-work baseline measure —
|
|
@@ -871,6 +900,7 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
871
900
|
startedAt: nowIso(),
|
|
872
901
|
timeLimitHours: cfg.timeLimitHours,
|
|
873
902
|
tokenBudget: cfg.tokenBudget,
|
|
903
|
+
minimumIterationIntervalMs: cfg.minimumIterationIntervalMs,
|
|
874
904
|
tokensUsed: 0,
|
|
875
905
|
branchName,
|
|
876
906
|
originalBranch,
|
|
@@ -882,16 +912,18 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
882
912
|
},
|
|
883
913
|
});
|
|
884
914
|
persistState(ctx);
|
|
885
|
-
appendLedger(ctx.cwd, "loop_started", { target: cfg.target, measureCmd: cfg.measureCmd || "none", direction: cfg.direction ?? "none", baseline, branch: branchName, timeLimitHours: cfg.timeLimitHours, tokenBudget: cfg.tokenBudget });
|
|
915
|
+
appendLedger(ctx.cwd, "loop_started", { target: cfg.target, measureCmd: cfg.measureCmd || "none", direction: cfg.direction ?? "none", baseline, branch: branchName, timeLimitHours: cfg.timeLimitHours, tokenBudget: cfg.tokenBudget, minimumIterationIntervalMs: cfg.minimumIterationIntervalMs });
|
|
886
916
|
ctx.ui.notify(
|
|
887
917
|
metricless
|
|
888
918
|
? `Loop started (metricless spec loop — NO plateau stop): ${displaySlice(cfg.target, 60)}\nEnds only at ${cfg.maxIterations > 0 ? `max ${cfg.maxIterations} iterations` : "no iteration cap"}${cfg.timeLimitHours ? ` · ${cfg.timeLimitHours}h` : ""}${cfg.tokenBudget ? ` · ${cfg.tokenBudget.toLocaleString()} tokens` : ""} · /loop stop. Every iteration must make ONE real, inspectable change — cosmetic churn is the doorknob failure.` +
|
|
919
|
+
(cfg.minimumIterationIntervalMs ? ` · cadence ≥ ${Math.ceil(cfg.minimumIterationIntervalMs / 1_000)}s` : "") +
|
|
889
920
|
(branchName ? `\nbranch mode: committing each iteration to ${branchName}` : "")
|
|
890
921
|
: `Loop started: ${displaySlice(cfg.target, 60)}\nBaseline: ${cfg.deferBaseline ? "deferred — the first real measurement seeds it" : (baseline ?? "(forced without a number — first turn must produce one)")} · direction ${cfg.direction} · window ${cfg.plateauWindow} · ${cfg.maxIterations > 0 ? `max ${cfg.maxIterations}` : "no iteration cap"}` +
|
|
922
|
+
(cfg.minimumIterationIntervalMs ? ` · cadence ≥ ${Math.ceil(cfg.minimumIterationIntervalMs / 1_000)}s` : "") +
|
|
891
923
|
(branchName ? `\nbranch mode: committing improvements to ${branchName}` : ""),
|
|
892
924
|
"info",
|
|
893
925
|
);
|
|
894
|
-
|
|
926
|
+
scheduleLoopTickUrgent(ctx);
|
|
895
927
|
return true;
|
|
896
928
|
}
|
|
897
929
|
|
|
@@ -920,7 +952,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
920
952
|
if (flags.continuationDispatchStoodDown) {
|
|
921
953
|
releaseContinuationDispatchStandDown();
|
|
922
954
|
releaseAuditorSurface();
|
|
923
|
-
|
|
955
|
+
scheduleLoopTickUrgent(ctx);
|
|
924
956
|
ctx.ui.notify("Loop dispatch stand-down cleared — retrying one continuation explicitly.", "info");
|
|
925
957
|
} else {
|
|
926
958
|
ctx.ui.notify("A loop is already active — /loop status to inspect, /loop stop to end it.", "info");
|
|
@@ -939,6 +971,8 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
939
971
|
!!r?.startsWith("plateau —") ||
|
|
940
972
|
!!r?.startsWith("stalled:") ||
|
|
941
973
|
!!r?.startsWith("stuck —") ||
|
|
974
|
+
!!r?.startsWith("time bound reached") ||
|
|
975
|
+
!!r?.startsWith("token budget exhausted") ||
|
|
942
976
|
// v0.35.54 (collect-pass HIGH finding): the v0.35.31 "metric never
|
|
943
977
|
// moved" stop message promises "/loop resume retries or /loop stop",
|
|
944
978
|
// but this predicate never matched that prefix — the promised command
|
|
@@ -983,8 +1017,33 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
983
1017
|
// An explicit resume re-arms the counters: fresh stall window,
|
|
984
1018
|
// cleared dead-turn/stuck streaks, reprieves restored — the user
|
|
985
1019
|
// saying "push again" wins over the ladder's memory (v0.29.19).
|
|
986
|
-
|
|
1020
|
+
// Time and token bounds are per supervised run window: resuming a
|
|
1021
|
+
// bound-stopped loop preserves its iteration/history/best but starts a
|
|
1022
|
+
// fresh elapsed-time window or token budget instead of stopping again
|
|
1023
|
+
// on the same bound.
|
|
1024
|
+
const resetTimeWindow = stored.stopReason?.startsWith("time bound reached") ?? false;
|
|
1025
|
+
const resetTokenBudget = stored.stopReason?.startsWith("token budget exhausted") ?? false;
|
|
1026
|
+
const resumedAt = nowIso();
|
|
1027
|
+
state.loop = {
|
|
1028
|
+
...stored,
|
|
1029
|
+
active: true,
|
|
1030
|
+
stopReason: undefined,
|
|
1031
|
+
consecutiveErrors: 0,
|
|
1032
|
+
consecutiveStuck: 0,
|
|
1033
|
+
lastStuckReason: undefined,
|
|
1034
|
+
stallCount: 0,
|
|
1035
|
+
auditPlateauReprieves: 0,
|
|
1036
|
+
...(resetTimeWindow ? { startedAt: resumedAt } : {}),
|
|
1037
|
+
...(resetTokenBudget ? { tokensUsed: 0 } : {}),
|
|
1038
|
+
};
|
|
987
1039
|
persistState(ctx);
|
|
1040
|
+
if (resetTimeWindow || resetTokenBudget) {
|
|
1041
|
+
appendLedger(ctx.cwd, "loop_bound_window_reset", {
|
|
1042
|
+
timeWindow: resetTimeWindow,
|
|
1043
|
+
tokenBudget: resetTokenBudget,
|
|
1044
|
+
iteration: stored.iteration,
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
988
1047
|
// v0.35.23 (note.md Next #2): an explicit resume is exactly the
|
|
989
1048
|
// decision a load hold waits for — release it or the tick below
|
|
990
1049
|
// would be frozen.
|
|
@@ -994,9 +1053,14 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
994
1053
|
}
|
|
995
1054
|
releaseContinuationDispatchStandDown();
|
|
996
1055
|
releaseAuditorSurface();
|
|
997
|
-
|
|
1056
|
+
scheduleLoopTickUrgent(ctx);
|
|
1057
|
+
const boundResetNote = resetTimeWindow
|
|
1058
|
+
? " · fresh time window"
|
|
1059
|
+
: resetTokenBudget
|
|
1060
|
+
? " · fresh token budget"
|
|
1061
|
+
: "";
|
|
998
1062
|
ctx.ui.notify(
|
|
999
|
-
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"} — ${displaySlice(stored.target, 60)}`,
|
|
1063
|
+
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"}${boundResetNote} — ${displaySlice(stored.target, 60)}`,
|
|
1000
1064
|
"info",
|
|
1001
1065
|
);
|
|
1002
1066
|
return;
|
|
@@ -1016,7 +1080,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1016
1080
|
ctx.ui.notify(formatLoopRecoveryStatus(ctx), "info");
|
|
1017
1081
|
return;
|
|
1018
1082
|
}
|
|
1019
|
-
ctx.ui.notify("No loop. /loop to draft one, /loop start \"<target>\" for an infinite metricless loop, or add measure=\"<cmd>\" direction=min|max for a metric loop [window=5] [max=50] [time=<hours>] [tokens=<budget>]", "info");
|
|
1083
|
+
ctx.ui.notify("No loop. /loop to draft one, /loop start \"<target>\" for an infinite metricless loop, or add measure=\"<cmd>\" direction=min|max for a metric loop [window=5] [max=50] [time=<hours>] [tokens=<budget>] [cadence=<seconds>]", "info");
|
|
1020
1084
|
return;
|
|
1021
1085
|
}
|
|
1022
1086
|
const lines = [
|
|
@@ -1028,6 +1092,11 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1028
1092
|
if (loop.timeLimitHours !== undefined) bounds.push(`time ≤ ${loop.timeLimitHours}h`);
|
|
1029
1093
|
if (loop.tokenBudget !== undefined) bounds.push(`tokens ${(loop.tokensUsed ?? 0).toLocaleString()}/${loop.tokenBudget.toLocaleString()}`);
|
|
1030
1094
|
if (bounds.length) lines.push(`Bounds: ${bounds.join(" · ")}`);
|
|
1095
|
+
if (loop.minimumIterationIntervalMs !== undefined) {
|
|
1096
|
+
const lastCompleted = loop.lastIterationCompletedAt ? Date.parse(loop.lastIterationCompletedAt) : Number.NaN;
|
|
1097
|
+
const nextDelay = Number.isFinite(lastCompleted) ? Math.max(0, lastCompleted + loop.minimumIterationIntervalMs - Date.now()) : 0;
|
|
1098
|
+
lines.push(`Cadence: ≥ ${Math.ceil(loop.minimumIterationIntervalMs / 1_000)}s between iterations${nextDelay > 0 ? ` · next in ${Math.ceil(nextDelay / 1_000)}s` : " · ready"}`);
|
|
1099
|
+
}
|
|
1031
1100
|
if (loop.refinements?.length) lines.push(`Spec refined ${loop.refinements.length}× (latest: iteration ${loop.refinements[loop.refinements.length - 1]!.iteration})`);
|
|
1032
1101
|
if (loop.stopReason) lines.push(`Stopped: ${loop.stopReason}`);
|
|
1033
1102
|
if (state.mainModelRecovery?.kind === "loop") lines.push(...formatLoopRecoveryStatusLines(ctx));
|
|
@@ -752,13 +752,13 @@ export function registerGoalRuntime(pi: ExtensionAPI): void {
|
|
|
752
752
|
},
|
|
753
753
|
});
|
|
754
754
|
pi.registerCommand("loop", {
|
|
755
|
-
description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" = infinite metricless loop (no plateau, no cap; ends at time=/tokens= or /loop stop) · /loop respec = infinite metricless reconcile against the root SPEC.md · add measure=\"<cmd>\" direction=min|max [window=5] [max=50] [branch=1] for a
|
|
755
|
+
description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" = infinite metricless loop (no plateau, no cap; ends at time=/tokens= or /loop stop) · /loop respec = infinite metricless reconcile against the root SPEC.md · add measure=\"<cmd>\" direction=min|max [window=5] [max=50] [cadence=<seconds>] [branch=1] for a loop · cadence is opt-in and limits automatic wakes between successful iterations · /loop status · /loop stop (alias /loop cancel). 'Improve until X' is a /goal, not a loop.",
|
|
756
756
|
getArgumentCompletions: completions([
|
|
757
|
-
["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50]"],
|
|
757
|
+
["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50] [cadence=<seconds>]"],
|
|
758
758
|
["respec", "infinite metricless loop reconciling the codebase against the root SPEC.md"],
|
|
759
759
|
["plan", "extended loop draft: deep research + multi-round metric design, same Confirm as a regular draft"],
|
|
760
760
|
["audit", "project-audit loop: each iteration audits fresh, appends findings, fixes the top ones — plateau stops when the well is dry (v0.29.0)"],
|
|
761
|
-
["status", "show metric, iteration, best/last values, stall count"],
|
|
761
|
+
["status", "show metric, iteration, best/last values, stall count, and cadence"],
|
|
762
762
|
["resume", "resume a held loop (session-restore gate / manual main-model recovery)"],
|
|
763
763
|
["refine", "queue an operator respec suggestion into the next iteration's prompt: /loop refine <text>"],
|
|
764
764
|
["polish", "alias of /loop refine"],
|
|
@@ -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 {
|
|
@@ -1804,7 +1805,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1804
1805
|
pi.registerTool(defineTool({
|
|
1805
1806
|
name: "propose_loop_draft",
|
|
1806
1807
|
label: "Propose loop draft",
|
|
1807
|
-
description: "During loop drafting (/loop with no args), propose the loop configuration. The orchestrator test-runs the measure command ONCE and shows the user real output + parsed number in a Confirm dialog. A measure producing no number is auto-rejected. Omit measureCmd (or pass \"none\") for a metricless spec loop — no plateau stop; ends only at bounds or /loop stop.",
|
|
1808
|
+
description: "During loop drafting (/loop with no args), propose the loop configuration. The orchestrator test-runs the measure command ONCE and shows the user real output + parsed number in a Confirm dialog. A measure producing no number is auto-rejected. Omit measureCmd (or pass \"none\") for a metricless spec loop — no plateau stop; ends only at bounds or /loop stop. An optional cadence is the minimum seconds between automatic wakes after successful iterations.",
|
|
1808
1809
|
parameters: Type.Object({
|
|
1809
1810
|
target: Type.String({ description: "What to improve, concretely" }),
|
|
1810
1811
|
measureCmd: Type.Optional(Type.String({ description: 'Shell command that prints ONE number representing progress — or the literal "none" for a metricless spec loop' })),
|
|
@@ -1813,12 +1814,13 @@ function registerAgentTools(pi: any): void {
|
|
|
1813
1814
|
max: Type.Optional(Type.Number({ description: "Iteration cap (default 50)" })),
|
|
1814
1815
|
time: Type.Optional(Type.Number({ description: "Arbitrary bound: stop after this many hours" })),
|
|
1815
1816
|
tokens: Type.Optional(Type.Number({ description: "Arbitrary bound: stop after this many tokens (input+output)" })),
|
|
1817
|
+
cadence: Type.Optional(Type.Number({ description: "Minimum seconds between automatic wakes after successful iterations (opt-in; explicit starts/resumes are urgent)" })),
|
|
1816
1818
|
branch: Type.Optional(Type.Boolean({ description: "branch=true: scratch-branch mode (clean git tree required)" })),
|
|
1817
1819
|
}),
|
|
1818
1820
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1819
1821
|
const foreign3 = foreignToolGuard(execCtx);
|
|
1820
1822
|
if (foreign3) return { content: [{ type: "text", text: foreign3 }], details: {} };
|
|
1821
|
-
const p = params as { target: string; measureCmd?: string; direction?: "min" | "max"; window?: number; max?: number; time?: number; tokens?: number; branch?: boolean };
|
|
1823
|
+
const p = params as { target: string; measureCmd?: string; direction?: "min" | "max"; window?: number; max?: number; time?: number; tokens?: number; cadence?: number; branch?: boolean };
|
|
1822
1824
|
const liveCtx = currentToolContext(execCtx);
|
|
1823
1825
|
if (!liveCtx) return staleToolResult();
|
|
1824
1826
|
if (warnIfStaleAtEntry(liveCtx, "loop drafting")) {
|
|
@@ -1872,6 +1874,9 @@ function registerAgentTools(pi: any): void {
|
|
|
1872
1874
|
};
|
|
1873
1875
|
}
|
|
1874
1876
|
const window = p.window && p.window > 0 ? Math.floor(p.window) : 5;
|
|
1877
|
+
const cadenceMs = typeof p.cadence === "number" && Number.isFinite(p.cadence) && p.cadence > 0
|
|
1878
|
+
? Math.min(Math.round(p.cadence * 1_000), 24 * 60 * 60_000)
|
|
1879
|
+
: undefined;
|
|
1875
1880
|
// v0.23.0: explicit max=0 = truly unbounded (no iteration cap).
|
|
1876
1881
|
// v0.23.8: metricless + no explicit max = UNBOUNDED here too — the
|
|
1877
1882
|
// drafter path was still defaulting to 50 after v0.23.6 flipped the
|
|
@@ -1889,8 +1894,8 @@ function registerAgentTools(pi: any): void {
|
|
|
1889
1894
|
liveCtx,
|
|
1890
1895
|
"Confirm loop",
|
|
1891
1896
|
metricless
|
|
1892
|
-
? `Target: ${sanitizeDisplayText(p.target.trim())}\n\nMeasure: NONE — metricless spec loop. There is NO plateau stop: the loop ends only at ${max > 0 ? `${max} iterations` : "NO iteration cap"}${typeof p.time === "number" && p.time > 0 ? ` · Time bound: ${p.time}h` : ""}${typeof p.tokens === "number" && p.tokens > 0 ? ` · Token bound: ${p.tokens.toLocaleString()}` : ""} · /loop stop.${p.branch ? "\nbranch mode: scratch branch, every iteration committed (clean tree required)" : ""}\n\nEvery iteration must make ONE real, inspectable change — cosmetic churn is the known failure mode (doorknob-polishing). Start it?`
|
|
1893
|
-
: `Target: ${sanitizeDisplayText(p.target.trim())}\n\nMeasure: ${sanitizeDisplayText(p.measureCmd ?? "")}\nTest-run output: ${sanitizeDisplayText(rawOutput).slice(0, 200)}\nParsed number: ${parsed} (${p.direction === "min" ? "lower is better" : "higher is better"})\n\nPlateau stop: ${window} non-improving iterations · Cap: ${max > 0 ? `${max} iterations` : "none (unbounded)"}${typeof p.time === "number" && p.time > 0 ? ` · Time bound: ${p.time}h` : ""}${typeof p.tokens === "number" && p.tokens > 0 ? ` · Token bound: ${p.tokens.toLocaleString()}` : ""}${p.branch ? "\nbranch mode: scratch branch (clean tree required)" : ""}\n\nThe loop never completes — it runs until one of these bounds, plateau, or /loop stop. Start it?`,
|
|
1897
|
+
? `Target: ${sanitizeDisplayText(p.target.trim())}\n\nMeasure: NONE — metricless spec loop. There is NO plateau stop: the loop ends only at ${max > 0 ? `${max} iterations` : "NO iteration cap"}${typeof p.time === "number" && p.time > 0 ? ` · Time bound: ${p.time}h` : ""}${typeof p.tokens === "number" && p.tokens > 0 ? ` · Token bound: ${p.tokens.toLocaleString()}` : ""}${cadenceMs ? ` · Cadence: ≥ ${Math.ceil(cadenceMs / 1_000)}s` : ""} · /loop stop.${p.branch ? "\nbranch mode: scratch branch, every iteration committed (clean tree required)" : ""}\n\nEvery iteration must make ONE real, inspectable change — cosmetic churn is the known failure mode (doorknob-polishing). Start it?`
|
|
1898
|
+
: `Target: ${sanitizeDisplayText(p.target.trim())}\n\nMeasure: ${sanitizeDisplayText(p.measureCmd ?? "")}\nTest-run output: ${sanitizeDisplayText(rawOutput).slice(0, 200)}\nParsed number: ${parsed} (${p.direction === "min" ? "lower is better" : "higher is better"})\n\nPlateau stop: ${window} non-improving iterations · Cap: ${max > 0 ? `${max} iterations` : "none (unbounded)"}${typeof p.time === "number" && p.time > 0 ? ` · Time bound: ${p.time}h` : ""}${typeof p.tokens === "number" && p.tokens > 0 ? ` · Token bound: ${p.tokens.toLocaleString()}` : ""}${cadenceMs ? ` · Cadence: ≥ ${Math.ceil(cadenceMs / 1_000)}s` : ""}${p.branch ? "\nbranch mode: scratch branch (clean tree required)" : ""}\n\nThe loop never completes — it runs until one of these bounds, plateau, or /loop stop. Start it?`,
|
|
1894
1899
|
);
|
|
1895
1900
|
confirmed = c === "yes";
|
|
1896
1901
|
} catch {
|
|
@@ -1913,6 +1918,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1913
1918
|
maxIterations: max,
|
|
1914
1919
|
timeLimitHours: typeof p.time === "number" && Number.isFinite(p.time) && p.time > 0 ? p.time : undefined,
|
|
1915
1920
|
tokenBudget: typeof p.tokens === "number" && Number.isFinite(p.tokens) && p.tokens > 0 ? Math.floor(p.tokens) : undefined,
|
|
1921
|
+
minimumIterationIntervalMs: cadenceMs,
|
|
1916
1922
|
branch: p.branch === true,
|
|
1917
1923
|
});
|
|
1918
1924
|
if (!started) {
|
|
@@ -1928,7 +1934,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1928
1934
|
pi.registerTool(defineTool({
|
|
1929
1935
|
name: "propose_loop_refine",
|
|
1930
1936
|
label: "Propose loop spec refinement",
|
|
1931
|
-
description: "While a loop is
|
|
1937
|
+
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
1938
|
parameters: Type.Object({
|
|
1933
1939
|
target: Type.Optional(Type.String({ description: "The sharpened target text (omit to keep the current target)" })),
|
|
1934
1940
|
measureCmd: Type.Optional(Type.String({ description: "The new measure command printing ONE number (omit to keep the current metric)" })),
|
|
@@ -1943,9 +1949,11 @@ function registerAgentTools(pi: any): void {
|
|
|
1943
1949
|
const liveCtx = currentToolContext(execCtx);
|
|
1944
1950
|
if (!liveCtx) return staleToolResult();
|
|
1945
1951
|
const loop = state.loop;
|
|
1946
|
-
|
|
1947
|
-
|
|
1952
|
+
const stoppedRefinable = !!loop && !loop.active && isRefinableStoppedLoopReason(loop.stopReason);
|
|
1953
|
+
if (!loop || (!loop.active && !stoppedRefinable)) {
|
|
1954
|
+
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
1955
|
}
|
|
1956
|
+
const wasActive = loop.active;
|
|
1949
1957
|
const newTarget = p.target?.trim() || loop.target;
|
|
1950
1958
|
const newMeasure = p.measureCmd?.trim() || loop.measureCmd || "";
|
|
1951
1959
|
// v0.23.0: a metricless loop can't be refined into a measured one
|
|
@@ -1989,7 +1997,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1989
1997
|
confirmed = (await confirmDraft(
|
|
1990
1998
|
liveCtx,
|
|
1991
1999
|
"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?`,
|
|
2000
|
+
`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
2001
|
)) === "yes";
|
|
1994
2002
|
} catch {
|
|
1995
2003
|
confirmed = false;
|
|
@@ -2026,8 +2034,10 @@ function registerAgentTools(pi: any): void {
|
|
|
2026
2034
|
}
|
|
2027
2035
|
persistState(liveCtx);
|
|
2028
2036
|
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:
|
|
2037
|
+
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");
|
|
2038
|
+
return { content: [{ type: "text", text: wasActive
|
|
2039
|
+
? "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn."
|
|
2040
|
+
: "Refinement confirmed and applied to the stopped loop. Run /loop resume to continue with the preserved history." }], details: {} };
|
|
2031
2041
|
},
|
|
2032
2042
|
}));
|
|
2033
2043
|
|
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.69",
|
|
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",
|
|
@@ -65,7 +65,9 @@ stops it.
|
|
|
65
65
|
furnace.
|
|
66
66
|
5. When concrete, call `propose_loop_draft` with `target`, `measureCmd` (or
|
|
67
67
|
omit/`"none"` for metricless), `direction` (measured only), and optional
|
|
68
|
-
`window`/`max`/`time`/`tokens`.
|
|
68
|
+
`window`/`max`/`time`/`tokens`/`cadence`. `cadence` is seconds between
|
|
69
|
+
successful automatic iterations; it is opt-in, shown in `/loop status`, and
|
|
70
|
+
explicit starts/resumes remain urgent.
|
|
69
71
|
6. **The orchestrator will run your proposed measure command ONCE** and show
|
|
70
72
|
the user the real output and parsed number in the Confirm dialog. If your
|
|
71
73
|
command produces no number, the proposal is rejected automatically — fix
|