pi-goal-list-loop-audit 0.22.6 → 0.23.0
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/README.md
CHANGED
|
@@ -61,10 +61,21 @@ matches `/list show`.
|
|
|
61
61
|
/loop start "reduce TODOs" measure="grep -c TODO src.txt | head -1" direction=min
|
|
62
62
|
/loop start "shrink the bundle" measure="..." direction=min time=4 tokens=500000 # arbitrary bounds
|
|
63
63
|
/loop start "reduce TODOs" measure="..." direction=min branch=1 # scratch-branch mode
|
|
64
|
+
/loop start "keep improving SPEC.md" measure=none max=20 # metricless spec loop (v0.23.0)
|
|
64
65
|
/loop status # iteration, best, stall, recent values
|
|
65
66
|
/loop stop # halt with summary
|
|
66
67
|
```
|
|
67
68
|
|
|
69
|
+
**Metricless loops** (`measure=none`): for genuinely endless work — an
|
|
70
|
+
ever-improving spec, continuous hardening — where no number means "better".
|
|
71
|
+
There is **no plateau stop** (nothing to stall on): the loop ends only at
|
|
72
|
+
its bounds (`max` iterations — `max=0` is truly unbounded — `time` hours,
|
|
73
|
+
`tokens` budget) or `/loop stop`. Every iteration must make one real,
|
|
74
|
+
inspectable change; cosmetic churn is the known failure mode
|
|
75
|
+
(doorknob-polishing). The drafter offers this when you say there is no
|
|
76
|
+
number, and tells you the trade-off before you confirm. Work with a finish
|
|
77
|
+
line is still a `/goal`.
|
|
78
|
+
|
|
68
79
|
Subcommands match **exactly** — `/goal pause the pipeline` sets an objective
|
|
69
80
|
about a pipeline; only bare `/goal pause` pauses. (Same rule everywhere, so
|
|
70
81
|
your objectives can start with any verb.)
|
|
@@ -81,10 +81,14 @@ export interface AuditDisplayProgress {
|
|
|
81
81
|
export function buildStatusText(state: State, audit?: AuditDisplayProgress | null, now = Date.now(), theme?: DisplayTheme): string | undefined {
|
|
82
82
|
if (state.loop?.active) {
|
|
83
83
|
const l = state.loop;
|
|
84
|
+
// v0.23.0: metricless spec loop — no arrow/best/stall, no plateau.
|
|
85
|
+
if (!l.measureCmd) {
|
|
86
|
+
return `glla: loop ${paint(theme, "accent", "∞")} iter ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations}` : ""} · metricless`;
|
|
87
|
+
}
|
|
84
88
|
const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
|
|
85
89
|
const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
|
|
86
90
|
const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
|
|
87
|
-
return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations} · best ${l.bestValue ?? "n/a"} · ${stall}`;
|
|
91
|
+
return `glla: loop ${arrow} iter ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"} · best ${l.bestValue ?? "n/a"} · ${stall}`;
|
|
88
92
|
}
|
|
89
93
|
const g = state.goal;
|
|
90
94
|
if (!g) return undefined;
|
|
@@ -93,7 +97,7 @@ export function buildStatusText(state: State, audit?: AuditDisplayProgress | nul
|
|
|
93
97
|
return `glla: ${paint(theme, "accent", "auditing…")}${tool}`;
|
|
94
98
|
}
|
|
95
99
|
if (g.status === "paused") {
|
|
96
|
-
const label =
|
|
100
|
+
const label = `${g.policy} paused ⏸ ${truncate(g.pauseReason ?? "", 40)}`;
|
|
97
101
|
return `glla: ${paint(theme, pauseIsError(g) ? "error" : "warning", label)}`;
|
|
98
102
|
}
|
|
99
103
|
if (g.status === "active") {
|
|
@@ -179,13 +183,23 @@ function goalLines(g: Goal, state: State, audit: AuditDisplayProgress | null | u
|
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: number): string[] {
|
|
186
|
+
// v0.23.0: metricless spec loop — no arrow/best/stall, no plateau.
|
|
187
|
+
if (!l.measureCmd) {
|
|
188
|
+
const lines = [
|
|
189
|
+
`${paint(theme, "accent", "●")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
|
|
190
|
+
`├─ loop ∞ iter ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations}` : ""} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
|
|
191
|
+
`└─ ${paint(theme, "dim", "metricless — work the spec (no plateau)")}`,
|
|
192
|
+
];
|
|
193
|
+
if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, budgetFor(width, 3, 50)))}`);
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
182
196
|
const arrow = paint(theme, "accent", l.direction === "min" ? "↓" : "↑");
|
|
183
197
|
const best = paint(theme, "success", `${l.bestValue ?? "n/a"}`);
|
|
184
198
|
const stallText = `stall ${l.stallCount}/${l.plateauWindow}`;
|
|
185
199
|
const stall = l.stallCount >= l.plateauWindow - 1 ? paint(theme, "warning", stallText) : stallText;
|
|
186
200
|
const lines = [
|
|
187
201
|
`${paint(theme, "accent", "●")} ${truncate(l.target, budgetFor(width, 3, 64))}`,
|
|
188
|
-
`├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
|
|
202
|
+
`├─ loop ${arrow} iter ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"} · ${fmtElapsed(now - Date.parse(l.startedAt))}`,
|
|
189
203
|
`├─ best ${best} · last ${l.lastValue ?? "n/a"} · ${stall}`,
|
|
190
204
|
`└─ ${paint(theme, "dim", truncate(l.measureCmd, budgetFor(width, 3, 56)))}`,
|
|
191
205
|
];
|
|
@@ -30,9 +30,13 @@ export interface LoopRefinement {
|
|
|
30
30
|
|
|
31
31
|
export interface LoopState {
|
|
32
32
|
target: string;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/** v0.23.0: optional — a metricless "spec loop" (measure=none) has no
|
|
34
|
+
* metric, no direction, and NO plateau stop; it ends only at max/time/
|
|
35
|
+
* tokens bounds or /loop stop. */
|
|
36
|
+
measureCmd?: string;
|
|
37
|
+
direction?: LoopDirection;
|
|
35
38
|
iteration: number;
|
|
39
|
+
/** v0.23.0: 0 = unbounded (no iteration cap). Default 50. */
|
|
36
40
|
maxIterations: number;
|
|
37
41
|
plateauWindow: number;
|
|
38
42
|
stallCount: number;
|
|
@@ -124,7 +128,7 @@ export type LoopTickOutcome =
|
|
|
124
128
|
*/
|
|
125
129
|
export function applyMeasurement(loop: LoopState, value: number | null, at: string): LoopTickOutcome {
|
|
126
130
|
loop.iteration++;
|
|
127
|
-
const improved = value !== null && isImprovement(loop.direction, value, loop.bestValue);
|
|
131
|
+
const improved = value !== null && loop.direction !== undefined && isImprovement(loop.direction, value, loop.bestValue);
|
|
128
132
|
if (value === null) {
|
|
129
133
|
loop.stallCount++;
|
|
130
134
|
} else if (improved) {
|
|
@@ -155,7 +159,7 @@ export function applyMeasurement(loop: LoopState, value: number | null, at: stri
|
|
|
155
159
|
loop.stopReason = `plateau — no improvement in ${loop.plateauWindow} consecutive iterations (best: ${loop.bestValue ?? "n/a"})`;
|
|
156
160
|
return { kind: "stop", reason: loop.stopReason };
|
|
157
161
|
}
|
|
158
|
-
if (loop.iteration >= loop.maxIterations) {
|
|
162
|
+
if (loop.maxIterations > 0 && loop.iteration >= loop.maxIterations) {
|
|
159
163
|
loop.active = false;
|
|
160
164
|
loop.stopReason = `max iterations reached (${loop.maxIterations}); best: ${loop.bestValue ?? "n/a"}`;
|
|
161
165
|
return { kind: "stop", reason: loop.stopReason };
|
|
@@ -163,11 +167,45 @@ export function applyMeasurement(loop: LoopState, value: number | null, at: stri
|
|
|
163
167
|
return { kind: "continue", improved, value };
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
/**
|
|
171
|
+
* One iteration of a METRICLESS loop (v0.23.0, measure=none). There is no
|
|
172
|
+
* number to judge movement, so there is no plateau — the loop ends only at
|
|
173
|
+
* the time/token/iteration bounds or /loop stop. This is the Sisyphus mode:
|
|
174
|
+
* work the spec until the bounds say stop. The doorknob risk is real and
|
|
175
|
+
* accepted by the user explicitly; the iteration prompt demands one real,
|
|
176
|
+
* inspectable change per turn.
|
|
177
|
+
*/
|
|
178
|
+
export function applyMetriclessTick(loop: LoopState, at: string): LoopTickOutcome {
|
|
179
|
+
loop.iteration++;
|
|
180
|
+
loop.history.push({ iteration: loop.iteration, value: null, improved: false, at });
|
|
181
|
+
if (loop.history.length > 200) loop.history.splice(0, loop.history.length - 200);
|
|
182
|
+
|
|
183
|
+
if (loop.timeLimitHours !== undefined) {
|
|
184
|
+
const elapsedH = (Date.parse(at) - Date.parse(loop.startedAt)) / 3_600_000;
|
|
185
|
+
if (Number.isFinite(elapsedH) && elapsedH >= loop.timeLimitHours) {
|
|
186
|
+
loop.active = false;
|
|
187
|
+
loop.stopReason = `time bound reached (${loop.timeLimitHours}h) after ${loop.iteration} iterations`;
|
|
188
|
+
return { kind: "stop", reason: loop.stopReason };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (loop.tokenBudget !== undefined && (loop.tokensUsed ?? 0) >= loop.tokenBudget) {
|
|
192
|
+
loop.active = false;
|
|
193
|
+
loop.stopReason = `token budget exhausted (${(loop.tokensUsed ?? 0).toLocaleString()} >= ${loop.tokenBudget.toLocaleString()}) after ${loop.iteration} iterations`;
|
|
194
|
+
return { kind: "stop", reason: loop.stopReason };
|
|
195
|
+
}
|
|
196
|
+
if (loop.maxIterations > 0 && loop.iteration >= loop.maxIterations) {
|
|
197
|
+
loop.active = false;
|
|
198
|
+
loop.stopReason = `max iterations reached (${loop.maxIterations})`;
|
|
199
|
+
return { kind: "stop", reason: loop.stopReason };
|
|
200
|
+
}
|
|
201
|
+
return { kind: "continue", improved: false, value: null };
|
|
202
|
+
}
|
|
203
|
+
|
|
166
204
|
/** Parse `/loop start` args into a config. Throws on missing pieces. */
|
|
167
205
|
export function parseLoopStartArgs(raw: string): {
|
|
168
206
|
target: string;
|
|
169
207
|
measureCmd: string;
|
|
170
|
-
direction
|
|
208
|
+
direction?: LoopDirection;
|
|
171
209
|
plateauWindow: number;
|
|
172
210
|
maxIterations: number;
|
|
173
211
|
branch: boolean;
|
|
@@ -196,10 +234,14 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
196
234
|
target += rest.slice(cursor);
|
|
197
235
|
target = target.trim().replace(/^["']|["']$/g, "").trim();
|
|
198
236
|
|
|
199
|
-
const
|
|
200
|
-
|
|
237
|
+
const measureRaw = (kv.get("measure") ?? "").trim();
|
|
238
|
+
// v0.23.0: measure=none → metricless "spec loop" (Sisyphus mode). No
|
|
239
|
+
// metric, no direction, no plateau — bounds and /loop stop only.
|
|
240
|
+
const metricless = measureRaw.toLowerCase() === "none";
|
|
241
|
+
if (!measureRaw) throw new Error('missing measure="<shell command that prints a number>" (or measure=none for a metricless spec loop — no plateau, ends only at bounds or /loop stop)');
|
|
201
242
|
const dirRaw = (kv.get("direction") ?? "").toLowerCase();
|
|
202
|
-
if (
|
|
243
|
+
if (metricless && dirRaw) throw new Error("direction= is meaningless with measure=none — there is no metric to have a direction");
|
|
244
|
+
if (!metricless && dirRaw !== "min" && dirRaw !== "max") throw new Error("missing direction=min|max");
|
|
203
245
|
if (!target) throw new Error("missing target (what to improve), e.g. /loop start \"reduce test failures\" measure=\"...\" direction=min");
|
|
204
246
|
|
|
205
247
|
const window = Number.parseInt(kv.get("window") ?? "", 10);
|
|
@@ -218,10 +260,11 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
218
260
|
const tokensRaw = Number.parseInt(kv.get("tokens") ?? "", 10);
|
|
219
261
|
return {
|
|
220
262
|
target,
|
|
221
|
-
measureCmd,
|
|
222
|
-
direction: dirRaw,
|
|
263
|
+
measureCmd: metricless ? "" : measureRaw,
|
|
264
|
+
direction: metricless ? undefined : dirRaw as LoopDirection,
|
|
223
265
|
plateauWindow: Number.isFinite(window) && window > 0 ? window : LOOP_DEFAULTS.plateauWindow,
|
|
224
|
-
|
|
266
|
+
// v0.23.0: max=0 = truly unbounded (no iteration cap); absent = 50.
|
|
267
|
+
maxIterations: kv.has("max") ? (Number.isFinite(max) && max >= 0 ? max : LOOP_DEFAULTS.maxIterations) : LOOP_DEFAULTS.maxIterations,
|
|
225
268
|
branch: branchRaw === "1" || branchRaw === "true" || branchRaw === "yes",
|
|
226
269
|
force: forceRaw === "1" || forceRaw === "true" || forceRaw === "yes",
|
|
227
270
|
timeLimitHours: Number.isFinite(timeRaw) && timeRaw > 0 ? timeRaw : undefined,
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -63,6 +63,7 @@ import { runGoalCompletionAuditor } from "../goal-loop-auditor.js";
|
|
|
63
63
|
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
64
64
|
import {
|
|
65
65
|
applyMeasurement,
|
|
66
|
+
applyMetriclessTick,
|
|
66
67
|
applyRefinement,
|
|
67
68
|
loopBranchName,
|
|
68
69
|
parseLoopStartArgs,
|
|
@@ -565,6 +566,12 @@ async function cmdStatus(ctx: ExtensionContext): Promise<void> {
|
|
|
565
566
|
async function cmdPause(ctx: ExtensionContext): Promise<void> {
|
|
566
567
|
if (!state.goal) return;
|
|
567
568
|
updateGoal({ status: "paused" }, ctx);
|
|
569
|
+
// v0.22.7: name WHAT was paused — a list item resumes through /list.
|
|
570
|
+
if (state.goal.policy === "list") {
|
|
571
|
+
const queued = listQueue().length;
|
|
572
|
+
ctx.ui.notify(`List item ${state.goal.id} paused${queued > 0 ? ` (${queued} queued in the list)` : ""}. /list resume to continue.`, "info");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
568
575
|
ctx.ui.notify(`Goal ${state.goal.id} paused. /goal resume to continue.`, "info");
|
|
569
576
|
}
|
|
570
577
|
|
|
@@ -580,9 +587,13 @@ async function cmdResume(ctx: ExtensionContext): Promise<void> {
|
|
|
580
587
|
updateGoal({ status: "active", pauseReason: undefined, pauseSuggestedAction: undefined, ...(usage ? { usage } : {}) }, ctx);
|
|
581
588
|
// v0.22.5: say what was resumed — with a non-empty list this also resumes
|
|
582
589
|
// the queue (the active goal IS the list's head item).
|
|
590
|
+
// v0.22.7: name WHAT was resumed — list items resume through /list.
|
|
583
591
|
const queued = listQueue().length;
|
|
592
|
+
const isListItem = state.goal.policy === "list";
|
|
584
593
|
ctx.ui.notify(
|
|
585
|
-
|
|
594
|
+
isListItem
|
|
595
|
+
? `Resumed list item [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""}`
|
|
596
|
+
: `Resumed goal [${state.goal.id}]: ${state.goal.objective.replace(/\s+/g, " ").slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list — resuming the list's head)` : ""}`,
|
|
586
597
|
"info",
|
|
587
598
|
);
|
|
588
599
|
scheduleContinuation(ctx, true);
|
|
@@ -730,6 +741,22 @@ async function cmdList(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
730
741
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
731
742
|
const rest = args.trim().slice(sub.length).trim();
|
|
732
743
|
|
|
744
|
+
if (sub === "resume") {
|
|
745
|
+
// Resume the list's head. The head activates AS the active goal, so this
|
|
746
|
+
// is the same motion as /goal resume — named for the surface the user is
|
|
747
|
+
// looking at (v0.22.7: "we would just unpause, and that is next").
|
|
748
|
+
if (!state.goal || state.goal.status !== "paused") {
|
|
749
|
+
ctx.ui.notify("No paused list item to resume. /list show to see the queue.", "info");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (state.goal.policy !== "list") {
|
|
753
|
+
ctx.ui.notify("The paused goal didn't come from the list — /goal resume to continue it.", "info");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
await cmdResume(ctx);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
733
760
|
if (!sub || sub === "show") {
|
|
734
761
|
const queue = listQueue();
|
|
735
762
|
const lines: string[] = [];
|
|
@@ -929,18 +956,23 @@ async function runGit(ctx: ExtensionContext, args: string[]): Promise<{ ok: bool
|
|
|
929
956
|
}
|
|
930
957
|
|
|
931
958
|
function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: string, boundsNote: string): string {
|
|
932
|
-
|
|
959
|
+
// v0.23.0: metricless loops get their own prompt — no metric section,
|
|
960
|
+
// anti-doorknob rules instead of anti-gaming rules.
|
|
961
|
+
const metricless = !loop.measureCmd;
|
|
962
|
+
const tmplPath = path.resolve(__dirname, "..", "..", "prompts", metricless ? "goal-loop-forever-metricless.md" : "goal-loop-forever.md");
|
|
933
963
|
let tmpl: string;
|
|
934
964
|
try {
|
|
935
965
|
tmpl = fs.readFileSync(tmplPath, "utf-8");
|
|
936
966
|
} catch {
|
|
937
|
-
tmpl =
|
|
967
|
+
tmpl = metricless
|
|
968
|
+
? `[LOOP ITERATION ${loop.iteration + 1}] Target: ${loop.target}. Metricless spec loop — make ONE real, inspectable change advancing the target. No cosmetic churn.`
|
|
969
|
+
: `[LOOP ITERATION ${loop.iteration + 1}] Target: ${loop.target}. Measure: ${loop.measureCmd} (${loop.direction}). Make ONE small change to improve the metric.`;
|
|
938
970
|
}
|
|
939
971
|
return tmpl
|
|
940
972
|
.replace(/\$\{ITERATION\}/g, String(loop.iteration + 1))
|
|
941
973
|
.replace(/\$\{TARGET\}/g, loop.target)
|
|
942
|
-
.replace(/\$\{MEASURE_CMD\}/g, loop.measureCmd)
|
|
943
|
-
.replace(/\$\{DIRECTION\}/g, loop.direction)
|
|
974
|
+
.replace(/\$\{MEASURE_CMD\}/g, loop.measureCmd ?? "none")
|
|
975
|
+
.replace(/\$\{DIRECTION\}/g, loop.direction ?? "none")
|
|
944
976
|
.replace(/\$\{DIRECTION_WORD\}/g, loop.direction === "min" ? "lower is better" : "higher is better")
|
|
945
977
|
.replace(/\$\{LAST_VALUE\}/g, loop.lastValue === null ? "(none yet)" : String(loop.lastValue))
|
|
946
978
|
.replace(/\$\{BEST_VALUE\}/g, loop.bestValue === null ? "(none yet)" : String(loop.bestValue))
|
|
@@ -985,10 +1017,22 @@ function sendLoopTurn(): void {
|
|
|
985
1017
|
? "**You are one stall from a plateau stop. Small tweaks are not working — try a FUNDAMENTALLY different approach: different file, different technique, or revert and rethink the angle of attack.**"
|
|
986
1018
|
: "";
|
|
987
1019
|
// v0.15.0: arbitrary bounds (never "completion") — surface what's armed.
|
|
1020
|
+
// v0.23.0: for metricless loops the bounds are the ONLY stop (no
|
|
1021
|
+
// plateau), so the note names that — and an unbounded metricless loop
|
|
1022
|
+
// gets the furnace warning.
|
|
1023
|
+
const metricless = !loop.measureCmd;
|
|
988
1024
|
const bounds: string[] = [];
|
|
989
1025
|
if (loop.timeLimitHours !== undefined) bounds.push(`${loop.timeLimitHours}h`);
|
|
990
1026
|
if (loop.tokenBudget !== undefined) bounds.push(`${loop.tokenBudget.toLocaleString()} tokens (used ${(loop.tokensUsed ?? 0).toLocaleString()})`);
|
|
991
|
-
|
|
1027
|
+
let boundsNote = "";
|
|
1028
|
+
if (metricless) {
|
|
1029
|
+
if (loop.maxIterations > 0) bounds.unshift(`${loop.maxIterations} iterations`);
|
|
1030
|
+
boundsNote = bounds.length
|
|
1031
|
+
? `\n- Bounds armed: the loop ends after ${bounds.join(" or ")} — or /loop stop. There is NO plateau stop.`
|
|
1032
|
+
: `\n- NO bounds armed — this loop ends only at /loop stop. Spend each iteration like it costs money; it does.`;
|
|
1033
|
+
} else if (bounds.length) {
|
|
1034
|
+
boundsNote = `\n- Arbitrary bounds: the loop also stops after ${bounds.join(" or ")}`;
|
|
1035
|
+
}
|
|
992
1036
|
try {
|
|
993
1037
|
extensionApi.sendMessage({
|
|
994
1038
|
customType: GOAL_EVENT_ENTRY,
|
|
@@ -1007,7 +1051,8 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
1007
1051
|
if (event?.messages) {
|
|
1008
1052
|
loop.tokensUsed = (loop.tokensUsed ?? 0) + sumNewAssistantTokens(event.messages as unknown[], countedLoopTokenMessages);
|
|
1009
1053
|
}
|
|
1010
|
-
const
|
|
1054
|
+
const metricless = !loop.measureCmd;
|
|
1055
|
+
const value = metricless ? null : await runMeasure(ctx, loop.measureCmd!);
|
|
1011
1056
|
// Hypothesis line (pi-autoresearch's good idea): the agent's stated intent
|
|
1012
1057
|
// for the turn goes into the ledger, making loop history auditable.
|
|
1013
1058
|
let hypothesis: string | undefined;
|
|
@@ -1016,7 +1061,7 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
1016
1061
|
const text = last && Array.isArray(last.content) ? last.content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n") : "";
|
|
1017
1062
|
hypothesis = text.match(/^HYPOTHESIS:\s*(.+)$/m)?.[1]?.trim().slice(0, 200);
|
|
1018
1063
|
}
|
|
1019
|
-
const outcome = applyMeasurement(loop, value, nowIso());
|
|
1064
|
+
const outcome = metricless ? applyMetriclessTick(loop, nowIso()) : applyMeasurement(loop, value, nowIso());
|
|
1020
1065
|
persistState(ctx);
|
|
1021
1066
|
appendLedger(ctx.cwd, "loop_measured", {
|
|
1022
1067
|
iteration: loop.iteration,
|
|
@@ -1026,11 +1071,12 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
1026
1071
|
hypothesis,
|
|
1027
1072
|
});
|
|
1028
1073
|
// branch=1 mode: commit improvements, hard-reset regressions — always and
|
|
1029
|
-
// only on the scratch branch.
|
|
1074
|
+
// only on the scratch branch. v0.23.0: a metricless loop has no regression
|
|
1075
|
+
// signal, so every iteration stands and is committed.
|
|
1030
1076
|
if (loop.branchName && outcome.kind === "continue") {
|
|
1031
|
-
if (outcome.improved) {
|
|
1077
|
+
if (metricless || outcome.improved) {
|
|
1032
1078
|
await runGit(ctx, ["add", "-A"]);
|
|
1033
|
-
const committed = await runGit(ctx, ["commit", "-m", `pi-glla-loop: iteration ${loop.iteration} (${loop.direction}=${loop.bestValue})`]);
|
|
1079
|
+
const committed = await runGit(ctx, ["commit", "-m", metricless ? `pi-glla-loop: iteration ${loop.iteration}` : `pi-glla-loop: iteration ${loop.iteration} (${loop.direction}=${loop.bestValue})`]);
|
|
1034
1080
|
appendLedger(ctx.cwd, "loop_git", { action: "commit", iteration: loop.iteration, ok: committed.ok });
|
|
1035
1081
|
} else {
|
|
1036
1082
|
const reset = await runGit(ctx, ["reset", "--hard", "HEAD"]);
|
|
@@ -1066,8 +1112,9 @@ async function finishLoopGit(ctx: ExtensionContext, loop: LoopState): Promise<vo
|
|
|
1066
1112
|
|
|
1067
1113
|
interface LoopConfig {
|
|
1068
1114
|
target: string;
|
|
1115
|
+
/** Empty string = metricless spec loop (v0.23.0). */
|
|
1069
1116
|
measureCmd: string;
|
|
1070
|
-
direction
|
|
1117
|
+
direction?: "min" | "max";
|
|
1071
1118
|
plateauWindow: number;
|
|
1072
1119
|
maxIterations: number;
|
|
1073
1120
|
branch: boolean;
|
|
@@ -1106,8 +1153,11 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
1106
1153
|
// produces no number is a footgun: without a baseline the loop burns stall
|
|
1107
1154
|
// iterations before plateau stops it. Refuse fast (force=1 overrides for
|
|
1108
1155
|
// measures that only work after the agent builds something first).
|
|
1109
|
-
|
|
1110
|
-
|
|
1156
|
+
// v0.23.0: metricless loops skip the baseline entirely — there is no
|
|
1157
|
+
// measure to run, and no plateau to protect.
|
|
1158
|
+
const metricless = !cfg.measureCmd;
|
|
1159
|
+
const baseline = metricless ? null : await runMeasure(ctx, cfg.measureCmd);
|
|
1160
|
+
if (!metricless && baseline === null && !(cfg as { force?: boolean }).force) {
|
|
1111
1161
|
ctx.ui.notify(
|
|
1112
1162
|
`/loop start refused: the measure produced no number.\nCommand: ${cfg.measureCmd}\nFix it so it prints exactly one number, or re-run with force=1 if it only works after the agent builds something first.\n(Non-numeric goal — research, docs, features? Use /goal: the independent auditor verifies semantically. /loop only believes a number.)`,
|
|
1113
1163
|
"warning",
|
|
@@ -1118,7 +1168,7 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
1118
1168
|
...state,
|
|
1119
1169
|
loop: {
|
|
1120
1170
|
target: cfg.target,
|
|
1121
|
-
measureCmd: cfg.measureCmd,
|
|
1171
|
+
measureCmd: cfg.measureCmd || undefined,
|
|
1122
1172
|
direction: cfg.direction,
|
|
1123
1173
|
iteration: 0,
|
|
1124
1174
|
maxIterations: cfg.maxIterations,
|
|
@@ -1137,10 +1187,13 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
1137
1187
|
},
|
|
1138
1188
|
};
|
|
1139
1189
|
persistState(ctx);
|
|
1140
|
-
appendLedger(ctx.cwd, "loop_started", { target: cfg.target, measureCmd: cfg.measureCmd, direction: cfg.direction, baseline, branch: branchName, timeLimitHours: cfg.timeLimitHours, tokenBudget: cfg.tokenBudget });
|
|
1190
|
+
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 });
|
|
1141
1191
|
ctx.ui.notify(
|
|
1142
|
-
|
|
1143
|
-
|
|
1192
|
+
metricless
|
|
1193
|
+
? `Loop started (metricless spec loop — NO plateau stop): ${cfg.target.slice(0, 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.` +
|
|
1194
|
+
(branchName ? `\nbranch mode: committing each iteration to ${branchName}` : "")
|
|
1195
|
+
: `Loop started: ${cfg.target.slice(0, 60)}\nBaseline: ${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"}` +
|
|
1196
|
+
(branchName ? `\nbranch mode: committing improvements to ${branchName}` : ""),
|
|
1144
1197
|
"info",
|
|
1145
1198
|
);
|
|
1146
1199
|
scheduleLoopTick(ctx);
|
|
@@ -1166,7 +1219,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1166
1219
|
persistState(ctx);
|
|
1167
1220
|
scheduleLoopTick(ctx);
|
|
1168
1221
|
ctx.ui.notify(
|
|
1169
|
-
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations} · best ${stored.bestValue ?? "n/a"} — ${stored.target.slice(0, 60)}`,
|
|
1222
|
+
`Loop resumed: iteration ${stored.iteration}/${stored.maxIterations > 0 ? stored.maxIterations : "∞"} · best ${stored.bestValue ?? "n/a"} — ${stored.target.slice(0, 60)}`,
|
|
1170
1223
|
"info",
|
|
1171
1224
|
);
|
|
1172
1225
|
return;
|
|
@@ -1183,8 +1236,8 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1183
1236
|
}
|
|
1184
1237
|
const lines = [
|
|
1185
1238
|
`Loop: ${loop.active ? "active" : "stopped"} — ${loop.target.slice(0, 80)}`,
|
|
1186
|
-
`Metric: ${loop.measureCmd} (${loop.direction})`,
|
|
1187
|
-
`Iteration ${loop.iteration}/${loop.maxIterations} · best ${loop.bestValue ?? "n/a"} · last ${loop.lastValue ?? "n/a"} · stall ${loop.stallCount}/${loop.plateauWindow}`,
|
|
1239
|
+
`Metric: ${loop.measureCmd ? `${loop.measureCmd} (${loop.direction})` : "none — metricless spec loop (no plateau)"}`,
|
|
1240
|
+
`Iteration ${loop.iteration}/${loop.maxIterations > 0 ? loop.maxIterations : "∞"} · best ${loop.bestValue ?? "n/a"} · last ${loop.lastValue ?? "n/a"} · stall ${loop.stallCount}/${loop.plateauWindow}`,
|
|
1188
1241
|
];
|
|
1189
1242
|
const bounds: string[] = [];
|
|
1190
1243
|
if (loop.timeLimitHours !== undefined) bounds.push(`time ≤ ${loop.timeLimitHours}h`);
|
|
@@ -1608,11 +1661,11 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1608
1661
|
pi.registerTool(defineTool({
|
|
1609
1662
|
name: "propose_loop_draft",
|
|
1610
1663
|
label: "Propose loop draft",
|
|
1611
|
-
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.",
|
|
1664
|
+
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.",
|
|
1612
1665
|
parameters: Type.Object({
|
|
1613
1666
|
target: Type.String({ description: "What to improve, concretely" }),
|
|
1614
|
-
measureCmd: Type.String({ description:
|
|
1615
|
-
direction: Type.Union([Type.Literal("min"), Type.Literal("max")], { description: "min = lower is better, max = higher is better" }),
|
|
1667
|
+
measureCmd: Type.Optional(Type.String({ description: 'Shell command that prints ONE number representing progress — or the literal "none" for a metricless spec loop' })),
|
|
1668
|
+
direction: Type.Optional(Type.Union([Type.Literal("min"), Type.Literal("max")], { description: "min = lower is better, max = higher is better (omit for a metricless loop)" })),
|
|
1616
1669
|
window: Type.Optional(Type.Number({ description: "Plateau stop after N non-improving iterations (default 5)" })),
|
|
1617
1670
|
max: Type.Optional(Type.Number({ description: "Iteration cap (default 50)" })),
|
|
1618
1671
|
time: Type.Optional(Type.Number({ description: "Arbitrary bound: stop after this many hours" })),
|
|
@@ -1620,7 +1673,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1620
1673
|
branch: Type.Optional(Type.Boolean({ description: "branch=true: scratch-branch mode (clean git tree required)" })),
|
|
1621
1674
|
}),
|
|
1622
1675
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1623
|
-
const p = params as { target: string; measureCmd
|
|
1676
|
+
const p = params as { target: string; measureCmd?: string; direction?: "min" | "max"; window?: number; max?: number; time?: number; tokens?: number; branch?: boolean };
|
|
1624
1677
|
if (draftingTarget !== "loop") {
|
|
1625
1678
|
return {
|
|
1626
1679
|
content: [{ type: "text", text: "Not in loop drafting mode. The user starts loop drafting with /loop (no args), or starts directly with /loop start." }],
|
|
@@ -1633,24 +1686,30 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1633
1686
|
if (loopBlock) {
|
|
1634
1687
|
return { content: [{ type: "text", text: loopBlock }], details: {} };
|
|
1635
1688
|
}
|
|
1636
|
-
if (!p.target?.trim()
|
|
1637
|
-
return { content: [{ type: "text", text: "target
|
|
1689
|
+
if (!p.target?.trim()) {
|
|
1690
|
+
return { content: [{ type: "text", text: "target is required." }], details: {} };
|
|
1691
|
+
}
|
|
1692
|
+
// v0.23.0: measureCmd omitted or "none" → metricless spec loop.
|
|
1693
|
+
const metricless = !p.measureCmd?.trim() || p.measureCmd.trim().toLowerCase() === "none";
|
|
1694
|
+
if (!metricless && p.direction !== "min" && p.direction !== "max") {
|
|
1695
|
+
return { content: [{ type: "text", text: 'direction=min|max is required for a measured loop (omit measureCmd or pass "none" for a metricless spec loop).' }], details: {} };
|
|
1638
1696
|
}
|
|
1639
1697
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
1640
1698
|
// THE TEST-RUN: orchestrator runs the proposed measure once. The user
|
|
1641
1699
|
// sees the real number before a single iteration burns tokens.
|
|
1700
|
+
// (Metricless loops skip this — there is no measure to test-run.)
|
|
1642
1701
|
let rawOutput = "";
|
|
1643
1702
|
let parsed: number | null = null;
|
|
1644
|
-
if (extensionApi) {
|
|
1703
|
+
if (!metricless && extensionApi) {
|
|
1645
1704
|
try {
|
|
1646
|
-
const result = await extensionApi.exec("bash", ["-c", p.measureCmd], { cwd: liveCtx.cwd });
|
|
1705
|
+
const result = await extensionApi.exec("bash", ["-c", p.measureCmd!], { cwd: liveCtx.cwd });
|
|
1647
1706
|
rawOutput = String((result as any)?.stdout ?? "").trim();
|
|
1648
1707
|
parsed = parseMetric(rawOutput);
|
|
1649
1708
|
} catch (err) {
|
|
1650
1709
|
rawOutput = `(measure command failed: ${err instanceof Error ? err.message : String(err)})`;
|
|
1651
1710
|
}
|
|
1652
1711
|
}
|
|
1653
|
-
if (parsed === null) {
|
|
1712
|
+
if (!metricless && parsed === null) {
|
|
1654
1713
|
return {
|
|
1655
1714
|
content: [{
|
|
1656
1715
|
type: "text",
|
|
@@ -1660,12 +1719,15 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1660
1719
|
};
|
|
1661
1720
|
}
|
|
1662
1721
|
const window = p.window && p.window > 0 ? Math.floor(p.window) : 5;
|
|
1663
|
-
|
|
1722
|
+
// v0.23.0: explicit max=0 = truly unbounded (no iteration cap).
|
|
1723
|
+
const max = p.max !== undefined && Number.isFinite(p.max) && p.max >= 0 ? Math.floor(p.max) : 50;
|
|
1664
1724
|
let confirmed = false;
|
|
1665
1725
|
try {
|
|
1666
1726
|
confirmed = await liveCtx.ui.confirm(
|
|
1667
1727
|
"Confirm loop",
|
|
1668
|
-
|
|
1728
|
+
metricless
|
|
1729
|
+
? `Target: ${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?`
|
|
1730
|
+
: `Target: ${p.target.trim()}\n\nMeasure: ${p.measureCmd}\nTest-run output: ${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?`,
|
|
1669
1731
|
);
|
|
1670
1732
|
} catch {
|
|
1671
1733
|
confirmed = false;
|
|
@@ -1679,8 +1741,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1679
1741
|
draftingTarget = null;
|
|
1680
1742
|
const started = await startLoopFromConfig(liveCtx, {
|
|
1681
1743
|
target: p.target.trim(),
|
|
1682
|
-
measureCmd: p.measureCmd,
|
|
1683
|
-
direction: p.direction,
|
|
1744
|
+
measureCmd: metricless ? "" : p.measureCmd!.trim(),
|
|
1745
|
+
direction: metricless ? undefined : p.direction,
|
|
1684
1746
|
plateauWindow: window,
|
|
1685
1747
|
maxIterations: max,
|
|
1686
1748
|
timeLimitHours: typeof p.time === "number" && Number.isFinite(p.time) && p.time > 0 ? p.time : undefined,
|
|
@@ -1691,7 +1753,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1691
1753
|
return { content: [{ type: "text", text: "Loop could not start (see the warning above — likely a git/dirty-tree issue with branch mode)." }], details: {} };
|
|
1692
1754
|
}
|
|
1693
1755
|
return {
|
|
1694
|
-
content: [{ type: "text", text: `Loop confirmed and started. Baseline ${parsed}. Make ONE small change per turn to move the metric ${p.direction === "min" ? "down" : "up"}.` }],
|
|
1756
|
+
content: [{ type: "text", text: metricless ? "Loop confirmed and started (metricless — no plateau). Make ONE real, inspectable change per turn." : `Loop confirmed and started. Baseline ${parsed}. Make ONE small change per turn to move the metric ${p.direction === "min" ? "down" : "up"}.` }],
|
|
1695
1757
|
details: {},
|
|
1696
1758
|
};
|
|
1697
1759
|
},
|
|
@@ -1714,7 +1776,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1714
1776
|
return { content: [{ type: "text", text: "No active loop to refine. propose_loop_refine is only valid while a loop is running." }], details: {} };
|
|
1715
1777
|
}
|
|
1716
1778
|
const newTarget = p.target?.trim() || loop.target;
|
|
1717
|
-
const newMeasure = p.measureCmd?.trim() || loop.measureCmd;
|
|
1779
|
+
const newMeasure = p.measureCmd?.trim() || loop.measureCmd || "";
|
|
1780
|
+
// v0.23.0: a metricless loop can't be refined into a measured one
|
|
1781
|
+
// (no direction, no baseline semantics) — stop and restart instead.
|
|
1782
|
+
if (!loop.measureCmd && p.measureCmd?.trim()) {
|
|
1783
|
+
return { content: [{ type: "text", text: "This loop is metricless — refining it into a measured loop isn't supported. /loop stop, then /loop start with a metric." }], details: {} };
|
|
1784
|
+
}
|
|
1718
1785
|
if (newTarget === loop.target && newMeasure === loop.measureCmd) {
|
|
1719
1786
|
return { content: [{ type: "text", text: "Refinement proposed no changes — provide a new target, a new measureCmd, or both." }], details: {} };
|
|
1720
1787
|
}
|
|
@@ -1754,7 +1821,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1754
1821
|
iteration: loop.iteration,
|
|
1755
1822
|
oldTarget: loop.target,
|
|
1756
1823
|
newTarget,
|
|
1757
|
-
oldMeasureCmd: loop.measureCmd,
|
|
1824
|
+
oldMeasureCmd: loop.measureCmd ?? "",
|
|
1758
1825
|
newMeasureCmd: newMeasure,
|
|
1759
1826
|
}, newBaseline);
|
|
1760
1827
|
persistState(liveCtx);
|
|
@@ -2300,9 +2367,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2300
2367
|
handler: settingsHandler,
|
|
2301
2368
|
});
|
|
2302
2369
|
pi.registerCommand("list", {
|
|
2303
|
-
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list next [n] | /list remove <n> | /list clear",
|
|
2370
|
+
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear",
|
|
2304
2371
|
getArgumentCompletions: completions([
|
|
2305
2372
|
["show", "display the queued items"],
|
|
2373
|
+
["resume", "resume the paused list item (the list's head)"],
|
|
2306
2374
|
["next", "activate the next item (or /list next <n> for position n)"],
|
|
2307
2375
|
["remove", "remove an item: /list remove <n>"],
|
|
2308
2376
|
["clear", "empty the list"],
|
|
@@ -2310,7 +2378,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2310
2378
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2311
2379
|
});
|
|
2312
2380
|
pi.registerCommand("loop", {
|
|
2313
|
-
description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50] [time=<hours>] [tokens=<budget>] [branch=1] skips drafting · /loop status · /loop stop. 'Improve until X' is a /goal, not a loop.",
|
|
2381
|
+
description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50] [time=<hours>] [tokens=<budget>] [branch=1] skips drafting · measure=none = metricless spec loop (no plateau; max=0 = unbounded) · /loop status · /loop stop. 'Improve until X' is a /goal, not a loop.",
|
|
2314
2382
|
getArgumentCompletions: completions([
|
|
2315
2383
|
["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50]"],
|
|
2316
2384
|
["status", "show metric, iteration, best/last values, stall count"],
|
|
@@ -2365,7 +2433,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2365
2433
|
const l = state.loop!;
|
|
2366
2434
|
if (autoResume) {
|
|
2367
2435
|
ctx.ui.notify(
|
|
2368
|
-
`Resuming loop (iteration ${l.iteration}/${l.maxIterations}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${l.target.slice(0, 60)}`,
|
|
2436
|
+
`Resuming loop (iteration ${l.iteration}/${l.maxIterations > 0 ? l.maxIterations : "∞"}, best ${l.bestValue ?? "n/a"}, stall ${l.stallCount}/${l.plateauWindow}): ${l.target.slice(0, 60)}`,
|
|
2369
2437
|
"info",
|
|
2370
2438
|
);
|
|
2371
2439
|
scheduleLoopTick(ctx);
|
|
@@ -2380,29 +2448,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2380
2448
|
} else if (state.goal && state.goal.status === "active" && state.goal.autoContinue) {
|
|
2381
2449
|
if (autoResume) {
|
|
2382
2450
|
ctx.ui.notify(
|
|
2383
|
-
`Resuming goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2451
|
+
`Resuming ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2384
2452
|
"info",
|
|
2385
2453
|
);
|
|
2386
2454
|
scheduleContinuation(ctx, true);
|
|
2387
2455
|
} else {
|
|
2388
2456
|
const queued = listQueue().length;
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2457
|
+
// v0.22.7: name WHAT is held — a list head resumes through /list.
|
|
2458
|
+
const isListItem = state.goal.policy === "list";
|
|
2459
|
+
const resumeCmd = isListItem ? "/list resume" : "/goal resume";
|
|
2460
|
+
const resumeHint = `${resumeCmd} to continue${queued > 0 ? ` (+${queued} queued in the list)` : ""} · /glla autoresume=on to auto-resume in this project`;
|
|
2392
2461
|
updateGoal({
|
|
2393
2462
|
status: "paused",
|
|
2394
2463
|
pauseReason: "restored in a fresh session — no work started",
|
|
2395
2464
|
pauseSuggestedAction: resumeHint,
|
|
2396
2465
|
}, ctx);
|
|
2397
2466
|
ctx.ui.notify(
|
|
2398
|
-
|
|
2467
|
+
`${isListItem ? "List item" : "Goal"} held on restore [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${queued > 0 ? ` (+${queued} queued in the list)` : ""} — ${resumeCmd} to continue.`,
|
|
2399
2468
|
"info",
|
|
2400
2469
|
);
|
|
2401
2470
|
}
|
|
2402
2471
|
} else if (state.goal && state.goal.status === "active") {
|
|
2403
2472
|
// Active but autoContinue off: nothing auto-fires — just surface it.
|
|
2404
2473
|
ctx.ui.notify(
|
|
2405
|
-
`Restored goal [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2474
|
+
`Restored ${state.goal.policy === "list" ? "list item" : "goal"} [${state.goal.id}]: ${state.goal.objective.slice(0, 70)}${listQueue().length > 0 ? ` (+${listQueue().length} queued)` : ""}`,
|
|
2406
2475
|
"info",
|
|
2407
2476
|
);
|
|
2408
2477
|
} else if ((!state.goal || state.goal.status === "complete" || state.goal.status === "aborted") && listQueue().length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. — a pi-coding-agent extension that supervises long-running work, with isolated auditor on each completion. Beat bamboozling by design: the auditor runs in a fresh session with no extensions, no skills, no editor — only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
|
@@ -23,16 +23,32 @@ stops it.
|
|
|
23
23
|
(`wc -c < bundle.js`), timings (`hyperfine`-style), scores.
|
|
24
24
|
- Warn the user if the metric is gameable (agent could improve the number
|
|
25
25
|
without improving the target — e.g. deleting tests to reduce failures).
|
|
26
|
+
- **Offer the metricless alternative explicitly** (v0.23.0): if the user
|
|
27
|
+
says there is no number — "just work the spec", "keep improving it",
|
|
28
|
+
Sisyphus-style — a metricless loop is now legitimate: omit `measureCmd`
|
|
29
|
+
(or pass `"none"`) in the proposal. Tell the user the trade-off in one
|
|
30
|
+
sentence: no metric means NO plateau stop — the loop ends only at its
|
|
31
|
+
bounds (max/time/tokens) or `/loop stop`, and cosmetic churn
|
|
32
|
+
(doorknob-polishing) is the known failure mode. Never pick metricless
|
|
33
|
+
silently; the user chooses it after hearing that.
|
|
34
|
+
- Still redirect to `/goal` when the work HAS a finish line (research,
|
|
35
|
+
write document X, build feature Y): "improve until done" is a goal
|
|
36
|
+
with an auditor, not a loop. Metricless loops are for work that is
|
|
37
|
+
genuinely endless (an ever-improving spec, continuous hardening).
|
|
26
38
|
3. Clarify the **direction**: is lower better (min) or higher better (max)?
|
|
39
|
+
(Skip this for a metricless loop — there is no direction without a metric.)
|
|
27
40
|
4. Optional tuning: `window` (plateau stop after N non-improving iterations,
|
|
28
|
-
default 5), `max` (iteration cap, default 50
|
|
29
|
-
expensive measures.
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
default 5 — meaningless for metricless), `max` (iteration cap, default 50;
|
|
42
|
+
`max=0` = truly unbounded). Suggest smaller values for expensive measures.
|
|
43
|
+
For metricless loops, ALWAYS discuss bounds — an unbounded metricless loop
|
|
44
|
+
is a deliberate furnace.
|
|
45
|
+
5. When concrete, call `propose_loop_draft` with `target`, `measureCmd` (or
|
|
46
|
+
omit/`"none"` for metricless), `direction` (measured only), and optional
|
|
47
|
+
`window`/`max`/`time`/`tokens`.
|
|
32
48
|
6. **The orchestrator will run your proposed measure command ONCE** and show
|
|
33
49
|
the user the real output and parsed number in the Confirm dialog. If your
|
|
34
50
|
command produces no number, the proposal is rejected automatically — fix
|
|
35
|
-
the command and propose again.
|
|
51
|
+
the command and propose again. (Metricless proposals skip the test-run.)
|
|
36
52
|
7. If the user rejects, ask what to change, refine, propose again.
|
|
37
53
|
|
|
38
54
|
## Hard rules
|
|
@@ -41,10 +57,7 @@ stops it.
|
|
|
41
57
|
- Do not modify the repo while drafting.
|
|
42
58
|
- Do not propose a measure you have not sanity-checked against the actual
|
|
43
59
|
repo layout (read files first if unsure what exists).
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Offer to hand them a well-structured `/goal` objective instead. Never
|
|
49
|
-
invent a fake metric (word count alone, file exists) just to make a loop
|
|
50
|
-
fit — a number that doesn't track the real target is worse than no number.
|
|
60
|
+
- Never invent a fake metric (word count alone, file exists) just to make a
|
|
61
|
+
measured loop fit — a number that doesn't track the real target is worse
|
|
62
|
+
than no number. When no honest number exists, the choice is metricless
|
|
63
|
+
loop (endless process) or /goal (has a finish line) — and the user picks.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Forever loop (metricless) — pi-goal-list-loop-audit
|
|
2
|
+
|
|
3
|
+
`[LOOP ITERATION ${ITERATION}]`
|
|
4
|
+
|
|
5
|
+
You are inside an endless work loop with NO metric. The user chose this
|
|
6
|
+
deliberately: there is no number that means "better" for this target, so the
|
|
7
|
+
loop cannot judge movement — and cannot plateau-stop. It ends only at its
|
|
8
|
+
bounds or /loop stop. That freedom is the doorknob-polishing risk made
|
|
9
|
+
flesh; your job is to make every iteration count anyway.
|
|
10
|
+
|
|
11
|
+
## Target (the spec you are working)
|
|
12
|
+
|
|
13
|
+
<target>
|
|
14
|
+
${TARGET}
|
|
15
|
+
</target>
|
|
16
|
+
|
|
17
|
+
## Your job THIS turn
|
|
18
|
+
|
|
19
|
+
Start your reply with exactly one line: `HYPOTHESIS: <what you will change and why it is real progress on the spec>`.
|
|
20
|
+
Then make **ONE** concrete, inspectable change that advances the target.
|
|
21
|
+
Then stop.
|
|
22
|
+
|
|
23
|
+
${REGRESSION_NOTE}
|
|
24
|
+
${STRATEGY_NOTE}
|
|
25
|
+
|
|
26
|
+
## Hard rules
|
|
27
|
+
|
|
28
|
+
- ONE change per turn. Small beats clever — the next iteration gets another turn.
|
|
29
|
+
- REAL work only: a change a reviewer could inspect and call progress
|
|
30
|
+
(a section written, a case handled, a defect fixed, a test added).
|
|
31
|
+
Cosmetic reshuffles, reformatting churn, comment shuffling, and re-wording
|
|
32
|
+
what already reads well are doorknob-polishing — the exact failure this
|
|
33
|
+
loop exists despite.
|
|
34
|
+
- Never repeat yourself: before acting, check what earlier iterations
|
|
35
|
+
already did (git log / the artifacts themselves) and pick the next
|
|
36
|
+
unaddressed piece of the spec.
|
|
37
|
+
- When the spec is genuinely, verifiably exhausted — every section
|
|
38
|
+
addressed, every case handled — say so plainly in your reply instead of
|
|
39
|
+
inventing cosmetic work. The user watches an honest loop gladly; a
|
|
40
|
+
furnace that rewrites the same paragraph forever, never.
|
|
41
|
+
- The spec is ALIVE: if the target needs sharpening, call
|
|
42
|
+
propose_loop_refine with your rationale — the user confirms or rejects.
|
|
43
|
+
${BOUNDS_NOTE}
|