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 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. See
107
- the README for the full command semantics.
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
 
@@ -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
- loopTimer = scheduleSessionTimeout(() => sendLoopTurn(), delay);
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
- let outcome: LoopTickOutcome = metricless ? applyMetriclessTick(loop, nowIso()) : applyMeasurement(loop, value, nowIso());
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
- scheduleLoopTick(ctx);
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
- scheduleLoopTick(ctx);
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
- scheduleLoopTick(ctx);
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 metric loop · /loop status · /loop stop (alias /loop cancel). 'Improve until X' is a /goal, not a 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] [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.68",
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