pi-goal-list-loop-audit 0.35.68 → 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 +12 -0
- package/INSTALL.md +5 -3
- package/README.md +5 -2
- package/extensions/goal-loop-forever.ts +14 -1
- package/extensions/goal-loop.ts +44 -7
- package/extensions/loops/goal-activation.ts +3 -3
- package/extensions/loops/goal-tools.ts +9 -4
- package/package.json +1 -1
- package/prompts/goal-loop-forever-draft.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
3
15
|
## 0.35.68 — bound-stop recovery (2026-08-26)
|
|
4
16
|
|
|
5
17
|
### 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
|
|
|
@@ -120,6 +120,12 @@ export interface LoopState {
|
|
|
120
120
|
tokenBudget?: number;
|
|
121
121
|
/** v0.15.0: accumulated loop tokens (input+output), orchestrator-counted. */
|
|
122
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;
|
|
123
129
|
/** v0.15.0: living spec — user-confirmed target/measure refinements. */
|
|
124
130
|
refinements?: LoopRefinement[];
|
|
125
131
|
/** branch=1 mode: scratch branch holding the loop's commits. */
|
|
@@ -399,6 +405,8 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
399
405
|
timeLimitHours?: number;
|
|
400
406
|
tokenBudget?: number;
|
|
401
407
|
toolSameRepeat?: number;
|
|
408
|
+
/** v0.35.x: optional metricless minimum cadence, supplied in seconds. */
|
|
409
|
+
minimumIterationIntervalMs?: number;
|
|
402
410
|
} {
|
|
403
411
|
// Key=value pairs first (measure= and direction= may hold quoted values),
|
|
404
412
|
// the remaining text is the target. v0.35.4: quoted spans are TARGET
|
|
@@ -409,7 +417,7 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
409
417
|
let rest = raw.trim();
|
|
410
418
|
const kv = new Map<string, string>();
|
|
411
419
|
const kvRe = /(\w+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
|
|
412
|
-
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"]);
|
|
413
421
|
const quoteSpans: Array<[number, number]> = [];
|
|
414
422
|
const quoteRe = /"([^"]*)"|'([^']*)'/g;
|
|
415
423
|
let qm: RegExpExecArray | null;
|
|
@@ -464,6 +472,10 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
464
472
|
}
|
|
465
473
|
const timeRaw = Number.parseFloat(kv.get("time") ?? "");
|
|
466
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;
|
|
467
479
|
return {
|
|
468
480
|
target,
|
|
469
481
|
measureCmd: metricless ? "" : measureRaw,
|
|
@@ -484,6 +496,7 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
484
496
|
const n = Number.parseInt(raw, 10);
|
|
485
497
|
return Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
486
498
|
})(),
|
|
499
|
+
...(cadenceMs !== undefined ? { minimumIterationIntervalMs: cadenceMs } : {}),
|
|
487
500
|
};
|
|
488
501
|
}
|
|
489
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");
|
|
@@ -1021,7 +1053,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1021
1053
|
}
|
|
1022
1054
|
releaseContinuationDispatchStandDown();
|
|
1023
1055
|
releaseAuditorSurface();
|
|
1024
|
-
|
|
1056
|
+
scheduleLoopTickUrgent(ctx);
|
|
1025
1057
|
const boundResetNote = resetTimeWindow
|
|
1026
1058
|
? " · fresh time window"
|
|
1027
1059
|
: resetTokenBudget
|
|
@@ -1048,7 +1080,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1048
1080
|
ctx.ui.notify(formatLoopRecoveryStatus(ctx), "info");
|
|
1049
1081
|
return;
|
|
1050
1082
|
}
|
|
1051
|
-
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");
|
|
1052
1084
|
return;
|
|
1053
1085
|
}
|
|
1054
1086
|
const lines = [
|
|
@@ -1060,6 +1092,11 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1060
1092
|
if (loop.timeLimitHours !== undefined) bounds.push(`time ≤ ${loop.timeLimitHours}h`);
|
|
1061
1093
|
if (loop.tokenBudget !== undefined) bounds.push(`tokens ${(loop.tokensUsed ?? 0).toLocaleString()}/${loop.tokenBudget.toLocaleString()}`);
|
|
1062
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
|
+
}
|
|
1063
1100
|
if (loop.refinements?.length) lines.push(`Spec refined ${loop.refinements.length}× (latest: iteration ${loop.refinements[loop.refinements.length - 1]!.iteration})`);
|
|
1064
1101
|
if (loop.stopReason) lines.push(`Stopped: ${loop.stopReason}`);
|
|
1065
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"],
|
|
@@ -1805,7 +1805,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1805
1805
|
pi.registerTool(defineTool({
|
|
1806
1806
|
name: "propose_loop_draft",
|
|
1807
1807
|
label: "Propose loop draft",
|
|
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.",
|
|
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.",
|
|
1809
1809
|
parameters: Type.Object({
|
|
1810
1810
|
target: Type.String({ description: "What to improve, concretely" }),
|
|
1811
1811
|
measureCmd: Type.Optional(Type.String({ description: 'Shell command that prints ONE number representing progress — or the literal "none" for a metricless spec loop' })),
|
|
@@ -1814,12 +1814,13 @@ function registerAgentTools(pi: any): void {
|
|
|
1814
1814
|
max: Type.Optional(Type.Number({ description: "Iteration cap (default 50)" })),
|
|
1815
1815
|
time: Type.Optional(Type.Number({ description: "Arbitrary bound: stop after this many hours" })),
|
|
1816
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)" })),
|
|
1817
1818
|
branch: Type.Optional(Type.Boolean({ description: "branch=true: scratch-branch mode (clean git tree required)" })),
|
|
1818
1819
|
}),
|
|
1819
1820
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1820
1821
|
const foreign3 = foreignToolGuard(execCtx);
|
|
1821
1822
|
if (foreign3) return { content: [{ type: "text", text: foreign3 }], details: {} };
|
|
1822
|
-
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 };
|
|
1823
1824
|
const liveCtx = currentToolContext(execCtx);
|
|
1824
1825
|
if (!liveCtx) return staleToolResult();
|
|
1825
1826
|
if (warnIfStaleAtEntry(liveCtx, "loop drafting")) {
|
|
@@ -1873,6 +1874,9 @@ function registerAgentTools(pi: any): void {
|
|
|
1873
1874
|
};
|
|
1874
1875
|
}
|
|
1875
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;
|
|
1876
1880
|
// v0.23.0: explicit max=0 = truly unbounded (no iteration cap).
|
|
1877
1881
|
// v0.23.8: metricless + no explicit max = UNBOUNDED here too — the
|
|
1878
1882
|
// drafter path was still defaulting to 50 after v0.23.6 flipped the
|
|
@@ -1890,8 +1894,8 @@ function registerAgentTools(pi: any): void {
|
|
|
1890
1894
|
liveCtx,
|
|
1891
1895
|
"Confirm loop",
|
|
1892
1896
|
metricless
|
|
1893
|
-
? `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?`
|
|
1894
|
-
: `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?`,
|
|
1895
1899
|
);
|
|
1896
1900
|
confirmed = c === "yes";
|
|
1897
1901
|
} catch {
|
|
@@ -1914,6 +1918,7 @@ function registerAgentTools(pi: any): void {
|
|
|
1914
1918
|
maxIterations: max,
|
|
1915
1919
|
timeLimitHours: typeof p.time === "number" && Number.isFinite(p.time) && p.time > 0 ? p.time : undefined,
|
|
1916
1920
|
tokenBudget: typeof p.tokens === "number" && Number.isFinite(p.tokens) && p.tokens > 0 ? Math.floor(p.tokens) : undefined,
|
|
1921
|
+
minimumIterationIntervalMs: cadenceMs,
|
|
1917
1922
|
branch: p.branch === true,
|
|
1918
1923
|
});
|
|
1919
1924
|
if (!started) {
|
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
|