pi-goal-list-loop-audit 0.22.7 → 0.23.1
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 +11 -0
- package/extensions/goal-loop-display.ts +16 -2
- package/extensions/goal-loop-forever.ts +54 -11
- package/extensions/loops/goal.ts +79 -38
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +5 -0
- package/prompts/goal-loop-forever-draft.md +25 -12
- package/prompts/goal-loop-forever-metricless.md +43 -0
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;
|
|
@@ -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,
|
|
@@ -955,18 +956,23 @@ async function runGit(ctx: ExtensionContext, args: string[]): Promise<{ ok: bool
|
|
|
955
956
|
}
|
|
956
957
|
|
|
957
958
|
function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: string, boundsNote: string): string {
|
|
958
|
-
|
|
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");
|
|
959
963
|
let tmpl: string;
|
|
960
964
|
try {
|
|
961
965
|
tmpl = fs.readFileSync(tmplPath, "utf-8");
|
|
962
966
|
} catch {
|
|
963
|
-
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.`;
|
|
964
970
|
}
|
|
965
971
|
return tmpl
|
|
966
972
|
.replace(/\$\{ITERATION\}/g, String(loop.iteration + 1))
|
|
967
973
|
.replace(/\$\{TARGET\}/g, loop.target)
|
|
968
|
-
.replace(/\$\{MEASURE_CMD\}/g, loop.measureCmd)
|
|
969
|
-
.replace(/\$\{DIRECTION\}/g, loop.direction)
|
|
974
|
+
.replace(/\$\{MEASURE_CMD\}/g, loop.measureCmd ?? "none")
|
|
975
|
+
.replace(/\$\{DIRECTION\}/g, loop.direction ?? "none")
|
|
970
976
|
.replace(/\$\{DIRECTION_WORD\}/g, loop.direction === "min" ? "lower is better" : "higher is better")
|
|
971
977
|
.replace(/\$\{LAST_VALUE\}/g, loop.lastValue === null ? "(none yet)" : String(loop.lastValue))
|
|
972
978
|
.replace(/\$\{BEST_VALUE\}/g, loop.bestValue === null ? "(none yet)" : String(loop.bestValue))
|
|
@@ -1011,10 +1017,22 @@ function sendLoopTurn(): void {
|
|
|
1011
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.**"
|
|
1012
1018
|
: "";
|
|
1013
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;
|
|
1014
1024
|
const bounds: string[] = [];
|
|
1015
1025
|
if (loop.timeLimitHours !== undefined) bounds.push(`${loop.timeLimitHours}h`);
|
|
1016
1026
|
if (loop.tokenBudget !== undefined) bounds.push(`${loop.tokenBudget.toLocaleString()} tokens (used ${(loop.tokensUsed ?? 0).toLocaleString()})`);
|
|
1017
|
-
|
|
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
|
+
}
|
|
1018
1036
|
try {
|
|
1019
1037
|
extensionApi.sendMessage({
|
|
1020
1038
|
customType: GOAL_EVENT_ENTRY,
|
|
@@ -1033,7 +1051,8 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
1033
1051
|
if (event?.messages) {
|
|
1034
1052
|
loop.tokensUsed = (loop.tokensUsed ?? 0) + sumNewAssistantTokens(event.messages as unknown[], countedLoopTokenMessages);
|
|
1035
1053
|
}
|
|
1036
|
-
const
|
|
1054
|
+
const metricless = !loop.measureCmd;
|
|
1055
|
+
const value = metricless ? null : await runMeasure(ctx, loop.measureCmd!);
|
|
1037
1056
|
// Hypothesis line (pi-autoresearch's good idea): the agent's stated intent
|
|
1038
1057
|
// for the turn goes into the ledger, making loop history auditable.
|
|
1039
1058
|
let hypothesis: string | undefined;
|
|
@@ -1042,7 +1061,7 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
1042
1061
|
const text = last && Array.isArray(last.content) ? last.content.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n") : "";
|
|
1043
1062
|
hypothesis = text.match(/^HYPOTHESIS:\s*(.+)$/m)?.[1]?.trim().slice(0, 200);
|
|
1044
1063
|
}
|
|
1045
|
-
const outcome = applyMeasurement(loop, value, nowIso());
|
|
1064
|
+
const outcome = metricless ? applyMetriclessTick(loop, nowIso()) : applyMeasurement(loop, value, nowIso());
|
|
1046
1065
|
persistState(ctx);
|
|
1047
1066
|
appendLedger(ctx.cwd, "loop_measured", {
|
|
1048
1067
|
iteration: loop.iteration,
|
|
@@ -1052,11 +1071,12 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
1052
1071
|
hypothesis,
|
|
1053
1072
|
});
|
|
1054
1073
|
// branch=1 mode: commit improvements, hard-reset regressions — always and
|
|
1055
|
-
// 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.
|
|
1056
1076
|
if (loop.branchName && outcome.kind === "continue") {
|
|
1057
|
-
if (outcome.improved) {
|
|
1077
|
+
if (metricless || outcome.improved) {
|
|
1058
1078
|
await runGit(ctx, ["add", "-A"]);
|
|
1059
|
-
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})`]);
|
|
1060
1080
|
appendLedger(ctx.cwd, "loop_git", { action: "commit", iteration: loop.iteration, ok: committed.ok });
|
|
1061
1081
|
} else {
|
|
1062
1082
|
const reset = await runGit(ctx, ["reset", "--hard", "HEAD"]);
|
|
@@ -1092,8 +1112,9 @@ async function finishLoopGit(ctx: ExtensionContext, loop: LoopState): Promise<vo
|
|
|
1092
1112
|
|
|
1093
1113
|
interface LoopConfig {
|
|
1094
1114
|
target: string;
|
|
1115
|
+
/** Empty string = metricless spec loop (v0.23.0). */
|
|
1095
1116
|
measureCmd: string;
|
|
1096
|
-
direction
|
|
1117
|
+
direction?: "min" | "max";
|
|
1097
1118
|
plateauWindow: number;
|
|
1098
1119
|
maxIterations: number;
|
|
1099
1120
|
branch: boolean;
|
|
@@ -1132,8 +1153,11 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
1132
1153
|
// produces no number is a footgun: without a baseline the loop burns stall
|
|
1133
1154
|
// iterations before plateau stops it. Refuse fast (force=1 overrides for
|
|
1134
1155
|
// measures that only work after the agent builds something first).
|
|
1135
|
-
|
|
1136
|
-
|
|
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) {
|
|
1137
1161
|
ctx.ui.notify(
|
|
1138
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.)`,
|
|
1139
1163
|
"warning",
|
|
@@ -1144,7 +1168,7 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
1144
1168
|
...state,
|
|
1145
1169
|
loop: {
|
|
1146
1170
|
target: cfg.target,
|
|
1147
|
-
measureCmd: cfg.measureCmd,
|
|
1171
|
+
measureCmd: cfg.measureCmd || undefined,
|
|
1148
1172
|
direction: cfg.direction,
|
|
1149
1173
|
iteration: 0,
|
|
1150
1174
|
maxIterations: cfg.maxIterations,
|
|
@@ -1163,10 +1187,13 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
1163
1187
|
},
|
|
1164
1188
|
};
|
|
1165
1189
|
persistState(ctx);
|
|
1166
|
-
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 });
|
|
1167
1191
|
ctx.ui.notify(
|
|
1168
|
-
|
|
1169
|
-
|
|
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}` : ""),
|
|
1170
1197
|
"info",
|
|
1171
1198
|
);
|
|
1172
1199
|
scheduleLoopTick(ctx);
|
|
@@ -1192,7 +1219,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1192
1219
|
persistState(ctx);
|
|
1193
1220
|
scheduleLoopTick(ctx);
|
|
1194
1221
|
ctx.ui.notify(
|
|
1195
|
-
`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)}`,
|
|
1196
1223
|
"info",
|
|
1197
1224
|
);
|
|
1198
1225
|
return;
|
|
@@ -1209,8 +1236,8 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1209
1236
|
}
|
|
1210
1237
|
const lines = [
|
|
1211
1238
|
`Loop: ${loop.active ? "active" : "stopped"} — ${loop.target.slice(0, 80)}`,
|
|
1212
|
-
`Metric: ${loop.measureCmd} (${loop.direction})`,
|
|
1213
|
-
`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}`,
|
|
1214
1241
|
];
|
|
1215
1242
|
const bounds: string[] = [];
|
|
1216
1243
|
if (loop.timeLimitHours !== undefined) bounds.push(`time ≤ ${loop.timeLimitHours}h`);
|
|
@@ -1634,11 +1661,11 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1634
1661
|
pi.registerTool(defineTool({
|
|
1635
1662
|
name: "propose_loop_draft",
|
|
1636
1663
|
label: "Propose loop draft",
|
|
1637
|
-
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.",
|
|
1638
1665
|
parameters: Type.Object({
|
|
1639
1666
|
target: Type.String({ description: "What to improve, concretely" }),
|
|
1640
|
-
measureCmd: Type.String({ description:
|
|
1641
|
-
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)" })),
|
|
1642
1669
|
window: Type.Optional(Type.Number({ description: "Plateau stop after N non-improving iterations (default 5)" })),
|
|
1643
1670
|
max: Type.Optional(Type.Number({ description: "Iteration cap (default 50)" })),
|
|
1644
1671
|
time: Type.Optional(Type.Number({ description: "Arbitrary bound: stop after this many hours" })),
|
|
@@ -1646,7 +1673,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1646
1673
|
branch: Type.Optional(Type.Boolean({ description: "branch=true: scratch-branch mode (clean git tree required)" })),
|
|
1647
1674
|
}),
|
|
1648
1675
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
1649
|
-
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 };
|
|
1650
1677
|
if (draftingTarget !== "loop") {
|
|
1651
1678
|
return {
|
|
1652
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." }],
|
|
@@ -1659,24 +1686,30 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1659
1686
|
if (loopBlock) {
|
|
1660
1687
|
return { content: [{ type: "text", text: loopBlock }], details: {} };
|
|
1661
1688
|
}
|
|
1662
|
-
if (!p.target?.trim()
|
|
1663
|
-
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: {} };
|
|
1664
1696
|
}
|
|
1665
1697
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
1666
1698
|
// THE TEST-RUN: orchestrator runs the proposed measure once. The user
|
|
1667
1699
|
// sees the real number before a single iteration burns tokens.
|
|
1700
|
+
// (Metricless loops skip this — there is no measure to test-run.)
|
|
1668
1701
|
let rawOutput = "";
|
|
1669
1702
|
let parsed: number | null = null;
|
|
1670
|
-
if (extensionApi) {
|
|
1703
|
+
if (!metricless && extensionApi) {
|
|
1671
1704
|
try {
|
|
1672
|
-
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 });
|
|
1673
1706
|
rawOutput = String((result as any)?.stdout ?? "").trim();
|
|
1674
1707
|
parsed = parseMetric(rawOutput);
|
|
1675
1708
|
} catch (err) {
|
|
1676
1709
|
rawOutput = `(measure command failed: ${err instanceof Error ? err.message : String(err)})`;
|
|
1677
1710
|
}
|
|
1678
1711
|
}
|
|
1679
|
-
if (parsed === null) {
|
|
1712
|
+
if (!metricless && parsed === null) {
|
|
1680
1713
|
return {
|
|
1681
1714
|
content: [{
|
|
1682
1715
|
type: "text",
|
|
@@ -1686,12 +1719,15 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1686
1719
|
};
|
|
1687
1720
|
}
|
|
1688
1721
|
const window = p.window && p.window > 0 ? Math.floor(p.window) : 5;
|
|
1689
|
-
|
|
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;
|
|
1690
1724
|
let confirmed = false;
|
|
1691
1725
|
try {
|
|
1692
1726
|
confirmed = await liveCtx.ui.confirm(
|
|
1693
1727
|
"Confirm loop",
|
|
1694
|
-
|
|
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?`,
|
|
1695
1731
|
);
|
|
1696
1732
|
} catch {
|
|
1697
1733
|
confirmed = false;
|
|
@@ -1705,8 +1741,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1705
1741
|
draftingTarget = null;
|
|
1706
1742
|
const started = await startLoopFromConfig(liveCtx, {
|
|
1707
1743
|
target: p.target.trim(),
|
|
1708
|
-
measureCmd: p.measureCmd,
|
|
1709
|
-
direction: p.direction,
|
|
1744
|
+
measureCmd: metricless ? "" : p.measureCmd!.trim(),
|
|
1745
|
+
direction: metricless ? undefined : p.direction,
|
|
1710
1746
|
plateauWindow: window,
|
|
1711
1747
|
maxIterations: max,
|
|
1712
1748
|
timeLimitHours: typeof p.time === "number" && Number.isFinite(p.time) && p.time > 0 ? p.time : undefined,
|
|
@@ -1717,7 +1753,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1717
1753
|
return { content: [{ type: "text", text: "Loop could not start (see the warning above — likely a git/dirty-tree issue with branch mode)." }], details: {} };
|
|
1718
1754
|
}
|
|
1719
1755
|
return {
|
|
1720
|
-
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"}.` }],
|
|
1721
1757
|
details: {},
|
|
1722
1758
|
};
|
|
1723
1759
|
},
|
|
@@ -1740,7 +1776,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1740
1776
|
return { content: [{ type: "text", text: "No active loop to refine. propose_loop_refine is only valid while a loop is running." }], details: {} };
|
|
1741
1777
|
}
|
|
1742
1778
|
const newTarget = p.target?.trim() || loop.target;
|
|
1743
|
-
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
|
+
}
|
|
1744
1785
|
if (newTarget === loop.target && newMeasure === loop.measureCmd) {
|
|
1745
1786
|
return { content: [{ type: "text", text: "Refinement proposed no changes — provide a new target, a new measureCmd, or both." }], details: {} };
|
|
1746
1787
|
}
|
|
@@ -1780,7 +1821,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1780
1821
|
iteration: loop.iteration,
|
|
1781
1822
|
oldTarget: loop.target,
|
|
1782
1823
|
newTarget,
|
|
1783
|
-
oldMeasureCmd: loop.measureCmd,
|
|
1824
|
+
oldMeasureCmd: loop.measureCmd ?? "",
|
|
1784
1825
|
newMeasureCmd: newMeasure,
|
|
1785
1826
|
}, newBaseline);
|
|
1786
1827
|
persistState(liveCtx);
|
|
@@ -2337,7 +2378,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2337
2378
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2338
2379
|
});
|
|
2339
2380
|
pi.registerCommand("loop", {
|
|
2340
|
-
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.",
|
|
2341
2382
|
getArgumentCompletions: completions([
|
|
2342
2383
|
["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50]"],
|
|
2343
2384
|
["status", "show metric, iteration, best/last values, stall count"],
|
|
@@ -2392,7 +2433,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2392
2433
|
const l = state.loop!;
|
|
2393
2434
|
if (autoResume) {
|
|
2394
2435
|
ctx.ui.notify(
|
|
2395
|
-
`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)}`,
|
|
2396
2437
|
"info",
|
|
2397
2438
|
);
|
|
2398
2439
|
scheduleLoopTick(ctx);
|
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.1",
|
|
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",
|
|
@@ -45,6 +45,11 @@ If the objective decomposes into milestones and no task list exists yet, call `p
|
|
|
45
45
|
|
|
46
46
|
When the agent calls any of these, the orchestrator tracks the call and persists state to `.pi-glla/active.jsonl`.
|
|
47
47
|
|
|
48
|
+
## EXECUTION DISCIPLINE
|
|
49
|
+
|
|
50
|
+
- **Delegate independent chunks to subagents.** When the work contains parallel, independent streams — surveying separate subsystems, reading disjoint file sets, running independent verifications — spawn `Agent` subagents (`Explore` for read-only research, `general-purpose` for implementation) instead of serializing everything through your own context. You remain the single writer: synthesize subagent findings and apply edits yourself.
|
|
51
|
+
- **Bound every long command.** Wrap test suites, builds, and dev servers in `timeout <seconds>` (e.g. `timeout 120 bun test src/lib`). An unbounded command that hangs burns an hour; a bounded one burns two minutes and tells you it hung. If a command produces no output for many minutes, treat it as hung: kill it, diagnose why, rerun bounded.
|
|
52
|
+
|
|
48
53
|
## TASK WORKFLOW
|
|
49
54
|
|
|
50
55
|
Use tasks as PROGRESS TRACKERS during your work — not as a post-hoc checklist to batch-mark at the end.
|
|
@@ -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}
|