pi-goal-list-loop-audit 0.23.5 → 0.23.7
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
|
@@ -58,10 +58,11 @@ matches `/list show`.
|
|
|
58
58
|
/list remove <n> # drop item n from the queue
|
|
59
59
|
/list clear # empty the queue
|
|
60
60
|
/loop # draft the loop (agent grills; measure is test-run before you confirm)
|
|
61
|
+
/loop start "keep polishing the UI" # infinite metricless loop (v0.23.6): no plateau, no cap — ends at time=/tokens= or /loop stop
|
|
61
62
|
/loop start "reduce TODOs" measure="grep -c TODO src.txt | head -1" direction=min
|
|
62
63
|
/loop start "shrink the bundle" measure="..." direction=min time=4 tokens=500000 # arbitrary bounds
|
|
63
64
|
/loop start "reduce TODOs" measure="..." direction=min branch=1 # scratch-branch mode
|
|
64
|
-
/loop start "keep improving SPEC.md" measure=none max=20 # metricless
|
|
65
|
+
/loop start "keep improving SPEC.md" measure=none max=20 # metricless with an explicit cap (v0.23.0)
|
|
65
66
|
/loop status # iteration, best, stall, recent values
|
|
66
67
|
/loop stop # halt with summary
|
|
67
68
|
```
|
|
@@ -300,7 +300,10 @@ export interface ListItem {
|
|
|
300
300
|
export function goalArgsNeedDrafting(args: string): boolean {
|
|
301
301
|
const t = args.trim();
|
|
302
302
|
if (!t) return false; // no-args is already the drafting path
|
|
303
|
-
|
|
303
|
+
// v0.23.7: any "done when" phrase counts — requiring the colon to
|
|
304
|
+
// immediately follow made "Done when ALL of the following are true:"
|
|
305
|
+
// route to the interview even though the user wrote a contract.
|
|
306
|
+
return !/\bdone\s+when\b/i.test(t);
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
/**
|
|
@@ -627,3 +630,43 @@ export function normalizeDraftContract(raw: string): string {
|
|
|
627
630
|
export function draftContractItemCount(normalized: string): number {
|
|
628
631
|
return normalized.split("\n").filter((l) => /^\d+\.\s/.test(l)).length;
|
|
629
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Split raw objective text into { objective, verificationContract } at the
|
|
636
|
+
* first "Done when…:"-family marker (line-start preferred, inline fallback
|
|
637
|
+
* for one-liners). v0.23.7: the marker family accepts ANY text between the
|
|
638
|
+
* keyword and the colon ("Done when ALL of the following are true:") —
|
|
639
|
+
* the shield's contractItems already drops such introducer lines
|
|
640
|
+
* (v0.23.4), and goalArgsNeedDrafting recognizes the same phrase, so the
|
|
641
|
+
* three "done when" parsers can no longer drift apart. Lives in the pure
|
|
642
|
+
* module so tests exercise THIS function, not a copy (the pre-0.23.7 test
|
|
643
|
+
* re-implemented it and silently went stale).
|
|
644
|
+
*/
|
|
645
|
+
export function extractVerificationContract(raw: string): { objective: string; verificationContract: string } {
|
|
646
|
+
// Line-based first: a marker at line start begins the contract block.
|
|
647
|
+
const lines = raw.split("\n");
|
|
648
|
+
let mode: "obj" | "verify" = "obj";
|
|
649
|
+
const objParts: string[] = [];
|
|
650
|
+
const verifyParts: string[] = [];
|
|
651
|
+
for (const line of lines) {
|
|
652
|
+
if (line.match(/^\s*(?:done when|verified when|verify|verification|done)\b[^:]*:/i)) {
|
|
653
|
+
mode = "verify";
|
|
654
|
+
}
|
|
655
|
+
if (mode === "obj") objParts.push(line);
|
|
656
|
+
else verifyParts.push(line);
|
|
657
|
+
}
|
|
658
|
+
let objective = objParts.join("\n").trim();
|
|
659
|
+
let verificationContract = verifyParts.join("\n").trim();
|
|
660
|
+
|
|
661
|
+
// Inline fallback: users write one-liners like
|
|
662
|
+
// "Create x.txt. Done when: grep -q ok x.txt"
|
|
663
|
+
// where the marker is mid-line. Split at the first inline marker.
|
|
664
|
+
if (!verificationContract) {
|
|
665
|
+
const m = raw.match(/^(.*?)(?:\.|;)??\s+(?:done when|verified when|verify|verification)\b[^:]*:\s*(.+)$/is);
|
|
666
|
+
if (m) {
|
|
667
|
+
objective = (m[1] ?? "").trim().replace(/[.;]\s*$/, "");
|
|
668
|
+
verificationContract = (m[2] ?? "").trim();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return { objective, verificationContract };
|
|
672
|
+
}
|
|
@@ -237,12 +237,17 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
237
237
|
const measureRaw = (kv.get("measure") ?? "").trim();
|
|
238
238
|
// v0.23.0: measure=none → metricless "spec loop" (Sisyphus mode). No
|
|
239
239
|
// metric, no direction, no plateau — bounds and /loop stop only.
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
// v0.23.6: a bare `/loop start "<target>"` IS the infinite command —
|
|
241
|
+
// no measure= means metricless too. The Confirm dialog names "NO
|
|
242
|
+
// plateau · NO iteration cap · /loop stop" before anything runs, so
|
|
243
|
+
// the choice is never silent (the v0.23.0 rule). Metric loops keep
|
|
244
|
+
// the 50-iteration default cap; metricless loops default to UNBOUNDED
|
|
245
|
+
// (max=0) unless max= is given explicitly.
|
|
246
|
+
const metricless = !measureRaw || measureRaw.toLowerCase() === "none";
|
|
242
247
|
const dirRaw = (kv.get("direction") ?? "").toLowerCase();
|
|
243
|
-
if (metricless && dirRaw) throw new Error("direction= is meaningless
|
|
244
|
-
if (!metricless && dirRaw !== "min" && dirRaw !== "max") throw new Error("missing direction=min|max");
|
|
245
|
-
if (!target) throw new Error("missing target (what to improve), e.g. /loop start \"
|
|
248
|
+
if (metricless && dirRaw) throw new Error("direction= is meaningless without a metric — add measure=\"<cmd>\" or drop direction=");
|
|
249
|
+
if (!metricless && dirRaw !== "min" && dirRaw !== "max") throw new Error("missing direction=min|max (a metric loop needs to know which way is better; a bare /loop start \"<target>\" with no measure= is the infinite metricless form)");
|
|
250
|
+
if (!target) throw new Error("missing target (what to improve), e.g. /loop start \"keep polishing the UI\" — bare start is metricless + unbounded; add measure=\"<cmd>\" direction=min|max for a metric loop");
|
|
246
251
|
|
|
247
252
|
const window = Number.parseInt(kv.get("window") ?? "", 10);
|
|
248
253
|
const max = Number.parseInt(kv.get("max") ?? "", 10);
|
|
@@ -263,8 +268,11 @@ export function parseLoopStartArgs(raw: string): {
|
|
|
263
268
|
measureCmd: metricless ? "" : measureRaw,
|
|
264
269
|
direction: metricless ? undefined : dirRaw as LoopDirection,
|
|
265
270
|
plateauWindow: Number.isFinite(window) && window > 0 ? window : LOOP_DEFAULTS.plateauWindow,
|
|
266
|
-
// v0.23.0: max=0 = truly unbounded (no iteration cap)
|
|
267
|
-
|
|
271
|
+
// v0.23.0: max=0 = truly unbounded (no iteration cap).
|
|
272
|
+
// v0.23.6: metricless with no explicit max= defaults to UNBOUNDED —
|
|
273
|
+
// an infinite loop is the point of the bare form. Metric loops keep
|
|
274
|
+
// the 50-cap default.
|
|
275
|
+
maxIterations: kv.has("max") ? (Number.isFinite(max) && max >= 0 ? max : LOOP_DEFAULTS.maxIterations) : metricless ? 0 : LOOP_DEFAULTS.maxIterations,
|
|
268
276
|
branch: branchRaw === "1" || branchRaw === "true" || branchRaw === "yes",
|
|
269
277
|
force: forceRaw === "1" || forceRaw === "true" || forceRaw === "yes",
|
|
270
278
|
timeLimitHours: Number.isFinite(timeRaw) && timeRaw > 0 ? timeRaw : undefined,
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
piGlaDir,
|
|
56
56
|
normalizeDraftContract,
|
|
57
57
|
draftContractItemCount,
|
|
58
|
+
extractVerificationContract,
|
|
58
59
|
readState,
|
|
59
60
|
renderGoalMarkdown,
|
|
60
61
|
shouldAutoResumeOnSessionStart,
|
|
@@ -354,36 +355,6 @@ function createGoal(objective: string, ctx: ExtensionContext, policy: "goal" | "
|
|
|
354
355
|
return goal;
|
|
355
356
|
}
|
|
356
357
|
|
|
357
|
-
function extractVerificationContract(raw: string): { objective: string; verificationContract: string } {
|
|
358
|
-
// Line-based first: a marker at line start begins the contract block.
|
|
359
|
-
const lines = raw.split("\n");
|
|
360
|
-
let mode: "obj" | "verify" = "obj";
|
|
361
|
-
const objParts: string[] = [];
|
|
362
|
-
const verifyParts: string[] = [];
|
|
363
|
-
for (const line of lines) {
|
|
364
|
-
const lower = line.toLowerCase();
|
|
365
|
-
if (lower.match(/^\s*(?:done when|verify|verified when|verification|done):/)) {
|
|
366
|
-
mode = "verify";
|
|
367
|
-
}
|
|
368
|
-
if (mode === "obj") objParts.push(line);
|
|
369
|
-
else verifyParts.push(line);
|
|
370
|
-
}
|
|
371
|
-
let objective = objParts.join("\n").trim();
|
|
372
|
-
let verificationContract = verifyParts.join("\n").trim();
|
|
373
|
-
|
|
374
|
-
// Inline fallback: users write one-liners like
|
|
375
|
-
// "Create x.txt. Done when: grep -q ok x.txt"
|
|
376
|
-
// where the marker is mid-line. Split at the first inline marker.
|
|
377
|
-
if (!verificationContract) {
|
|
378
|
-
const m = raw.match(/^(.*?)(?:\.|;)??\s+(done when|verified when|verify|verification)\s*:\s*(.+)$/is);
|
|
379
|
-
if (m) {
|
|
380
|
-
objective = (m[1] ?? "").trim().replace(/[.;]\s*$/, "");
|
|
381
|
-
verificationContract = (m[3] ?? "").trim();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return { objective, verificationContract };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
358
|
function persistState(ctx: ExtensionContext): void {
|
|
388
359
|
appendLedger(ctx.cwd, "state", { goal: state.goal, list: state.list ?? [], loop: state.loop ?? null });
|
|
389
360
|
refreshUI(ctx); // every state transition flows through here → the TUI is always current
|
|
@@ -467,7 +438,7 @@ async function startDrafting(ctx: ExtensionContext, target: "goal" | "list" | "l
|
|
|
467
438
|
target === "list"
|
|
468
439
|
? `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm). Add directly instead: include a "Done when:" clause.`
|
|
469
440
|
: target === "loop"
|
|
470
|
-
? `${label}: a loop target needs a metric and a direction — the agent will help you design them first (nothing activates until you confirm). Skip the interview entirely: /loop start "<target>" measure="<cmd>" direction=min|max [window=5] [max=50] [time=h] [tokens=n] [branch=1].`
|
|
441
|
+
? `${label}: a loop target needs a metric and a direction — the agent will help you design them first (nothing activates until you confirm). Skip the interview entirely: /loop start "<target>" (bare = infinite metricless) or /loop start "<target>" measure="<cmd>" direction=min|max [window=5] [max=50] [time=h] [tokens=n] [branch=1].`
|
|
471
442
|
: `${label}: the objective has no "Done when:" clause — the agent will grill you about it first (nothing activates until you confirm). Skip the interview entirely: /goal start <objective>.`;
|
|
472
443
|
ctx.ui.notify(
|
|
473
444
|
seed
|
|
@@ -1258,7 +1229,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
1258
1229
|
if (sub === "status") {
|
|
1259
1230
|
const loop = state.loop;
|
|
1260
1231
|
if (!loop) {
|
|
1261
|
-
ctx.ui.notify("No loop. /loop to draft one,
|
|
1232
|
+
ctx.ui.notify("No loop. /loop to draft one, /loop start \"<target>\" for an infinite metricless loop, or add measure=\"<cmd>\" direction=min|max for a metric loop [window=5] [max=50] [time=<hours>] [tokens=<budget>]", "info");
|
|
1262
1233
|
return;
|
|
1263
1234
|
}
|
|
1264
1235
|
const lines = [
|
|
@@ -1607,13 +1578,15 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
1607
1578
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
1608
1579
|
// Multi-item list draft: one Confirm for the whole batch.
|
|
1609
1580
|
if (p.items && p.items.length > 0) {
|
|
1610
|
-
|
|
1581
|
+
// v0.23.7: show ALL items in full — the user approves the whole
|
|
1582
|
+
// batch; hidden items would be approved blind.
|
|
1583
|
+
const preview = p.items.map((t, i) => ` ${i + 1}. ${t}`).join("\n");
|
|
1611
1584
|
const batchActivates = !state.goal || state.goal.status === "complete" || state.goal.status === "aborted";
|
|
1612
1585
|
let batchConfirmed = false;
|
|
1613
1586
|
try {
|
|
1614
1587
|
batchConfirmed = await liveCtx.ui.confirm(
|
|
1615
1588
|
"Confirm queue batch",
|
|
1616
|
-
`${p.items.length} items:\n${preview}${
|
|
1589
|
+
`${p.items.length} items:\n${preview}${batchActivates ? "\n\n(List is empty — confirming ACTIVATES item 1 immediately as the active goal.)" : ""}`,
|
|
1617
1590
|
);
|
|
1618
1591
|
} catch {
|
|
1619
1592
|
batchConfirmed = false;
|
|
@@ -2181,7 +2154,7 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
|
|
|
2181
2154
|
else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
|
|
2182
2155
|
}
|
|
2183
2156
|
} else if (choice.startsWith("Wedge alert")) {
|
|
2184
|
-
const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default
|
|
2157
|
+
const v = await ctx.ui.input("Wedge alert threshold (minutes)", "non-negative integer; 0 = off, empty = default 30");
|
|
2185
2158
|
if (v !== undefined) {
|
|
2186
2159
|
const n = Number.parseInt(v.trim(), 10);
|
|
2187
2160
|
if (Number.isFinite(n) && n >= 0) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: n });
|
|
@@ -2270,7 +2243,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2270
2243
|
} else {
|
|
2271
2244
|
const n = Number.parseInt(value, 10);
|
|
2272
2245
|
if (Number.isFinite(n) && n >= 0) {
|
|
2273
|
-
patch.wedgeAlertMinutes = n; // 0 = off; unset = default
|
|
2246
|
+
patch.wedgeAlertMinutes = n; // 0 = off; unset = default 30
|
|
2274
2247
|
changed = true;
|
|
2275
2248
|
} else {
|
|
2276
2249
|
ctx.ui.notify(`wedgealert must be a non-negative integer (minutes, 0 = off), got: ${value}`, "warning");
|
|
@@ -2433,7 +2406,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
2433
2406
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
2434
2407
|
});
|
|
2435
2408
|
pi.registerCommand("loop", {
|
|
2436
|
-
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] [
|
|
2409
|
+
description: "Loop 3: metric-driven process — it never completes. /loop <target> drafts the metric with you · /loop start \"<target>\" = infinite metricless loop (no plateau, no cap; ends at time=/tokens= or /loop stop) · add measure=\"<cmd>\" direction=min|max [window=5] [max=50] [branch=1] for a metric loop · /loop status · /loop stop. 'Improve until X' is a /goal, not a loop.",
|
|
2437
2410
|
getArgumentCompletions: completions([
|
|
2438
2411
|
["start", "skip drafting: /loop start \"<target>\" measure=\"<cmd>\" direction=min|max [window=5] [max=50]"],
|
|
2439
2412
|
["status", "show metric, iteration, best/last values, stall count"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.7",
|
|
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",
|
|
@@ -38,10 +38,12 @@ stops it.
|
|
|
38
38
|
3. Clarify the **direction**: is lower better (min) or higher better (max)?
|
|
39
39
|
(Skip this for a metricless loop — there is no direction without a metric.)
|
|
40
40
|
4. Optional tuning: `window` (plateau stop after N non-improving iterations,
|
|
41
|
-
default 5 — meaningless for metricless), `max` (iteration cap
|
|
42
|
-
`max=0`
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
default 5 — meaningless for metricless), `max` (iteration cap: default 50
|
|
42
|
+
for METRIC loops; default UNBOUNDED (`max=0`) for metricless loops,
|
|
43
|
+
v0.23.6 — a bare infinite loop is the point of the metricless form).
|
|
44
|
+
Suggest smaller values for expensive measures. For metricless loops,
|
|
45
|
+
ALWAYS discuss bounds — an unbounded metricless loop is a deliberate
|
|
46
|
+
furnace.
|
|
45
47
|
5. When concrete, call `propose_loop_draft` with `target`, `measureCmd` (or
|
|
46
48
|
omit/`"none"` for metricless), `direction` (measured only), and optional
|
|
47
49
|
`window`/`max`/`time`/`tokens`.
|