pi-goal-list-loop-audit 0.33.1 → 0.33.2
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/extensions/goal-loop-display.ts +1 -1
- package/extensions/goal-loop-forever.ts +46 -0
- package/extensions/goal-loop-repetition.ts +13 -1
- package/extensions/loops/goal.ts +115 -11
- package/package.json +1 -1
- package/prompts/goal-loop-forever-metricless.md +2 -0
- package/prompts/goal-loop-forever.md +2 -0
|
@@ -449,7 +449,7 @@ function loopLines(l: LoopState, now: number, theme?: DisplayTheme, width?: numb
|
|
|
449
449
|
lines.push(`├─ ${paint(theme, act.ok ? "success" : "error", act.ok ? "✓" : "✗")} ${act.name}${act.arg ? ` ${paint(theme, "dim", truncate(act.arg, 24))}` : ""}${act.ms > 0 ? ` ${paint(theme, "dim", `(${fmtElapsed(act.ms)})`)}` : ""}`);
|
|
450
450
|
}
|
|
451
451
|
const footer = !l.measureCmd
|
|
452
|
-
? "metricless (no plateau) · /loop stop · /loop
|
|
452
|
+
? "metricless (no plateau) · /loop stop · /loop refine" // v0.33.2: the verb exists now
|
|
453
453
|
: `${l.kind === "audit" ? "metric: closed findings" : truncate(l.measureCmd, budgetFor(width, 3, 30))} · /loop stop`;
|
|
454
454
|
lines.push(`└─ ${paint(theme, "dim", footer)}`);
|
|
455
455
|
if (l.branchName) lines.push(`⎇ ${paint(theme, "muted", truncate(l.branchName, budgetFor(width, 3, 50)))}`);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* self-reports progress.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
13
14
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
14
15
|
import { join } from "node:path";
|
|
15
16
|
|
|
@@ -110,6 +111,20 @@ export interface LoopState {
|
|
|
110
111
|
/** v0.25.1: /loop start toolsamerepeat=N — legacy same-tool-same-result
|
|
111
112
|
* check window. 0 disables it (multi-signal detector only). */
|
|
112
113
|
toolSameRepeat?: number;
|
|
114
|
+
/** v0.33.2: respec loops carry their spec file — drift detection
|
|
115
|
+
* (specHash compared per tick), checkbox progress (specChecked →
|
|
116
|
+
* spec_item_progress events), and the refine tool's specText write path. */
|
|
117
|
+
specFile?: string;
|
|
118
|
+
specHash?: string;
|
|
119
|
+
specChecked?: number;
|
|
120
|
+
/** v0.33.2: hypothesis feedback loop — the last turn's HYPOTHESIS line
|
|
121
|
+
* plus the verdict computed against the metric movement, injected into
|
|
122
|
+
* the next iteration's prompt. */
|
|
123
|
+
lastHypothesis?: string;
|
|
124
|
+
hypothesisFeedback?: string;
|
|
125
|
+
/** v0.33.2: /loop refine <text> — the operator's respec suggestion rides
|
|
126
|
+
* the next iteration's prompt; the agent proposes via propose_loop_refine. */
|
|
127
|
+
refineHint?: string;
|
|
113
128
|
/** v0.25.1: per-iteration progress-signal accumulators for the
|
|
114
129
|
* multi-signal stuck gate. fileWrites bumps on write/edit tool results;
|
|
115
130
|
* iterationStartHead/At snapshot when the iteration BEGAN so the tick can
|
|
@@ -464,6 +479,37 @@ export function countOpenAuditFindings(cwd: string): number {
|
|
|
464
479
|
}
|
|
465
480
|
}
|
|
466
481
|
|
|
482
|
+
/** v0.33.2: the first OPEN finding's text — the reprieve note names what
|
|
483
|
+
* to close, not just how many remain. */
|
|
484
|
+
export function topOpenAuditFinding(cwd: string): string | null {
|
|
485
|
+
try {
|
|
486
|
+
const p = join(cwd, AUDIT_FINDINGS_REL);
|
|
487
|
+
if (!existsSync(p)) return null;
|
|
488
|
+
const line = readFileSync(p, "utf-8").split("\n").find((l) => /^- \[ \]/.test(l));
|
|
489
|
+
return line ? line.replace(/^- \[ \]\s*/, "").trim().slice(0, 120) : null;
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** v0.33.2: spec drift detection — short sha256 of the spec file. */
|
|
496
|
+
export function specFileHash(p: string): string | null {
|
|
497
|
+
try {
|
|
498
|
+
return createHash("sha256").update(readFileSync(p, "utf-8")).digest("hex").slice(0, 16);
|
|
499
|
+
} catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** v0.33.2: checked checkbox count in a spec file (spec_item_progress). */
|
|
505
|
+
export function countCheckedSpecItems(p: string): number | null {
|
|
506
|
+
try {
|
|
507
|
+
return readFileSync(p, "utf-8").split("\n").filter((l) => /^- \[x\]/i.test(l)).length;
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
467
513
|
// ---- /goal audit-project (v0.29.8) ----
|
|
468
514
|
|
|
469
515
|
/**
|
|
@@ -190,7 +190,19 @@ export function forwardTransitionPaired(input: LoopStuckInput): boolean {
|
|
|
190
190
|
* (new detector only), undefined = REPETITION.toolResultRepeat.
|
|
191
191
|
*/
|
|
192
192
|
export function isActuallyStuck(input: LoopStuckInput, toolSameRepeat?: number): string | undefined {
|
|
193
|
-
if ((input.fileWriteCount ?? 0) > 0)
|
|
193
|
+
if ((input.fileWriteCount ?? 0) > 0) {
|
|
194
|
+
// v0.33.2: write-exemption abuse — endless cosmetic edits with a
|
|
195
|
+
// near-identical reply are churn, not progress (the metricless
|
|
196
|
+
// doorknob leak). The stuck ladder's first rung is a prompt note;
|
|
197
|
+
// any genuinely different iteration resets it.
|
|
198
|
+
if (input.previousText && input.assistantText && normalizeForPrint(input.assistantText).length > REPETITION.minSimilarLength) {
|
|
199
|
+
const sim = trigramSimilarity(input.assistantText, input.previousText);
|
|
200
|
+
if (sim >= REPETITION.similarityThreshold) {
|
|
201
|
+
return `cosmetic churn: wrote files but the reply is ~${Math.round(sim * 100)}% identical to the previous iteration`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
194
206
|
if ((input.gitCommitCount ?? 0) > 0) return undefined;
|
|
195
207
|
if ((input.specItemProgressCount ?? 0) > 0) return undefined;
|
|
196
208
|
if (forwardTransitionPaired(input)) return undefined;
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -169,6 +169,9 @@ import {
|
|
|
169
169
|
LOOP_DEFAULTS,
|
|
170
170
|
resolveSpecFiles,
|
|
171
171
|
respecTarget,
|
|
172
|
+
topOpenAuditFinding,
|
|
173
|
+
specFileHash,
|
|
174
|
+
countCheckedSpecItems,
|
|
172
175
|
auditMeasureCmd,
|
|
173
176
|
auditTarget,
|
|
174
177
|
AUDIT_PLATEAU_MAX_REPRIEVES,
|
|
@@ -2614,7 +2617,7 @@ async function runGit(ctx: ExtensionContext, args: string[]): Promise<{ ok: bool
|
|
|
2614
2617
|
}
|
|
2615
2618
|
}
|
|
2616
2619
|
|
|
2617
|
-
function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: string, boundsNote: string, interventionNote = "", variantNote = ""): string {
|
|
2620
|
+
function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: string, boundsNote: string, interventionNote = "", variantNote = "", hypothesisNote = "", refineHintNote = ""): string {
|
|
2618
2621
|
// v0.23.0: metricless loops get their own prompt — no metric section,
|
|
2619
2622
|
// anti-doorknob rules instead of anti-gaming rules.
|
|
2620
2623
|
const metricless = !loop.measureCmd;
|
|
@@ -2641,7 +2644,9 @@ function loopPrompt(loop: LoopState, regressionNote: string, strategyNote: strin
|
|
|
2641
2644
|
.replace(/\$\{STRATEGY_NOTE\}/g, strategyNote)
|
|
2642
2645
|
.replace(/\$\{BOUNDS_NOTE\}/g, boundsNote)
|
|
2643
2646
|
.replace(/\$\{INTERVENTION_NOTE\}/g, interventionNote)
|
|
2644
|
-
.replace(/\$\{VARIANT_NOTE\}/g, variantNote)
|
|
2647
|
+
.replace(/\$\{VARIANT_NOTE\}/g, variantNote)
|
|
2648
|
+
.replace(/\$\{HYPOTHESIS_NOTE\}/g, hypothesisNote)
|
|
2649
|
+
.replace(/\$\{REFINE_HINT\}/g, refineHintNote);
|
|
2645
2650
|
}
|
|
2646
2651
|
|
|
2647
2652
|
function scheduleLoopTick(ctx: ExtensionContext): void {
|
|
@@ -2693,7 +2698,13 @@ function sendLoopTurn(): void {
|
|
|
2693
2698
|
// Strategy rotation (from pi-loop-mode's one good idea): one stall before
|
|
2694
2699
|
// the plateau window closes, stop polishing and change approach entirely.
|
|
2695
2700
|
const strategyNote = loop.stallCount >= loop.plateauWindow - 1 && loop.stallCount > 0
|
|
2696
|
-
? "**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.**"
|
|
2701
|
+
? "**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.**" +
|
|
2702
|
+
// v0.33.2: a metric flat AT BEST may mean the spec stopped capturing
|
|
2703
|
+
// "better" — the loop holds the evidence, so it says so (was: the
|
|
2704
|
+
// prompt said "call propose_loop_refine" but the loop never suggested it).
|
|
2705
|
+
(loop.lastValue !== null && loop.lastValue === loop.bestValue
|
|
2706
|
+
? " **The metric has been flat at best — if the spec no longer captures 'better' (saturated metric, drifted target), call propose_loop_refine.**"
|
|
2707
|
+
: "")
|
|
2697
2708
|
: "";
|
|
2698
2709
|
// v0.15.0: arbitrary bounds (never "completion") — surface what's armed.
|
|
2699
2710
|
// v0.23.0: for metricless loops the bounds are the ONLY stop (no
|
|
@@ -2724,12 +2735,19 @@ function sendLoopTurn(): void {
|
|
|
2724
2735
|
// v0.24.0: identical prompts invite identical answers — rotate the base
|
|
2725
2736
|
// instruction (metricless loops; metric loops already vary via values).
|
|
2726
2737
|
const variantNote = metricless ? continueVariant(loop.iteration) : "";
|
|
2738
|
+
// v0.33.2: one-shot prompt payloads, consumed on use.
|
|
2739
|
+
const hypothesisNote = loop.hypothesisFeedback ?? "";
|
|
2740
|
+
if (hypothesisNote) loop.hypothesisFeedback = undefined;
|
|
2741
|
+
const refineHintNote = loop.refineHint
|
|
2742
|
+
? `**The operator suggests refining the spec:** ${loop.refineHint} — if the current spec no longer captures "better", call propose_loop_refine (target and/or measureCmd${loop.specFile ? " and/or specText/specAppend" : ""}); if it still stands, say why in one line and keep working.`
|
|
2743
|
+
: "";
|
|
2744
|
+
if (refineHintNote) loop.refineHint = undefined;
|
|
2727
2745
|
try {
|
|
2728
2746
|
let loopResync = "";
|
|
2729
2747
|
if (postCompactResyncPending) { try { loopResync = buildPostCompactResync(); } catch { loopResync = ""; } } // v0.33.1
|
|
2730
2748
|
extensionApi.sendMessage({
|
|
2731
2749
|
customType: GOAL_EVENT_ENTRY,
|
|
2732
|
-
content: loopResync + loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote),
|
|
2750
|
+
content: loopResync + loopPrompt(loop, regressionNote, strategyNote, boundsNote, interventionNote, variantNote, hypothesisNote, refineHintNote),
|
|
2733
2751
|
display: false,
|
|
2734
2752
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2735
2753
|
if (loopResync) postCompactResyncPending = false; // consumed only by a landed send
|
|
@@ -2800,6 +2818,23 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
2800
2818
|
}
|
|
2801
2819
|
} catch { /* no ledger yet */ }
|
|
2802
2820
|
}
|
|
2821
|
+
// v0.33.2: respec spec drift + checkbox progress — hash compared per
|
|
2822
|
+
// tick (external edits ledger spec_updated); newly checked boxes emit
|
|
2823
|
+
// the spec_item_progress signal the stuck gate already consumes (it was
|
|
2824
|
+
// consumed-but-never-emitted until now).
|
|
2825
|
+
if (loop.specFile) {
|
|
2826
|
+
const hash = specFileHash(loop.specFile);
|
|
2827
|
+
if (hash && loop.specHash && loop.specHash !== hash) {
|
|
2828
|
+
appendLedger(ctx.cwd, "spec_updated", { via: "external", iteration: loop.iteration });
|
|
2829
|
+
ctx.ui.notify("Spec file changed mid-loop — drift ledgered (spec_updated).", "info");
|
|
2830
|
+
}
|
|
2831
|
+
if (hash) loop.specHash = hash;
|
|
2832
|
+
const checked = countCheckedSpecItems(loop.specFile);
|
|
2833
|
+
if (checked !== null && loop.specChecked !== undefined && checked > loop.specChecked) {
|
|
2834
|
+
appendLedger(ctx.cwd, "spec_item_progress", { iteration: loop.iteration, newlyChecked: checked - loop.specChecked, totalChecked: checked });
|
|
2835
|
+
}
|
|
2836
|
+
if (checked !== null) loop.specChecked = checked;
|
|
2837
|
+
}
|
|
2803
2838
|
const iterSignals = {
|
|
2804
2839
|
fileWrites: loop.iterMetrics?.fileWrites ?? 0,
|
|
2805
2840
|
gitCommits,
|
|
@@ -2839,6 +2874,26 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
2839
2874
|
loop.lastStuckReason = undefined;
|
|
2840
2875
|
}
|
|
2841
2876
|
let outcome: LoopTickOutcome = metricless ? applyMetriclessTick(loop, nowIso()) : applyMeasurement(loop, value, nowIso());
|
|
2877
|
+
// v0.33.2: close the hypothesis feedback loop — the prediction went into
|
|
2878
|
+
// the ledger; now the VERDICT rides the next iteration's prompt.
|
|
2879
|
+
if (loop.lastHypothesis) {
|
|
2880
|
+
const h = loop.history;
|
|
2881
|
+
const cur = h.length >= 1 ? h[h.length - 1]!.value : null;
|
|
2882
|
+
const prev = h.length >= 2 ? h[h.length - 2]!.value : null;
|
|
2883
|
+
if (metricless || cur === null) {
|
|
2884
|
+
loop.hypothesisFeedback = `Last iteration you predicted: "${loop.lastHypothesis}". ${metricless ? "Metricless loop — no number to verify it against; say honestly whether the prediction landed." : "The measure printed no number — the prediction is unverifiable."}`;
|
|
2885
|
+
} else {
|
|
2886
|
+
const moved = prev === null
|
|
2887
|
+
? `first measurement ${cur}`
|
|
2888
|
+
: cur === prev
|
|
2889
|
+
? `flat at ${cur}`
|
|
2890
|
+
: loop.direction === "min"
|
|
2891
|
+
? (cur < prev ? `improved ${prev} → ${cur}` : `regressed ${prev} → ${cur}`)
|
|
2892
|
+
: (cur > prev ? `improved ${prev} → ${cur}` : `regressed ${prev} → ${cur}`);
|
|
2893
|
+
loop.hypothesisFeedback = `Last iteration you predicted: "${loop.lastHypothesis}". Result: metric ${moved} (best ${loop.bestValue}).`;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
loop.lastHypothesis = hypothesis;
|
|
2842
2897
|
persistState(ctx);
|
|
2843
2898
|
appendLedger(ctx.cwd, "loop_measured", {
|
|
2844
2899
|
iteration: loop.iteration,
|
|
@@ -2892,7 +2947,8 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
|
|
|
2892
2947
|
loop.stopReason = undefined;
|
|
2893
2948
|
loop.stallCount = 0;
|
|
2894
2949
|
loop.auditPlateauReprieves = reprieves;
|
|
2895
|
-
|
|
2950
|
+
const topFinding = topOpenAuditFinding(ctx.cwd); // v0.33.2: name what to close, not just the count
|
|
2951
|
+
loop.auditReprieveNote = `PLATEAU REPRIEVE (${reprieves}/${AUDIT_PLATEAU_MAX_REPRIEVES}): ${open} finding(s) still OPEN in ${AUDIT_FINDINGS_REL} — the plateau stop does not fire while the well isn't dry. Stop hunting and stop narrating: pick the smallest OPEN finding and CLOSE it this iteration (fix commit + checked box).${topFinding ? ` Top open: ${topFinding}` : ""} ${AUDIT_PLATEAU_MAX_REPRIEVES - reprieves} reprieve(s) remain.`;
|
|
2896
2952
|
persistState(ctx);
|
|
2897
2953
|
appendLedger(ctx.cwd, "audit_plateau_reprieve", { open, reprieves, best: loop.bestValue });
|
|
2898
2954
|
ctx.ui.notify(`Audit loop plateau reprieve (${reprieves}/${AUDIT_PLATEAU_MAX_REPRIEVES}): ${open} open findings — the well isn't dry, continuing.`, "info");
|
|
@@ -2951,6 +3007,9 @@ interface LoopConfig {
|
|
|
2951
3007
|
deferBaseline?: boolean;
|
|
2952
3008
|
/** v0.29.10: audit loops get audit-flavoured regression wording. */
|
|
2953
3009
|
kind?: "audit";
|
|
3010
|
+
/** v0.33.2: respec loops carry their spec file (drift detection,
|
|
3011
|
+
* checkbox progress, refine specText writes). */
|
|
3012
|
+
specFile?: string;
|
|
2954
3013
|
}
|
|
2955
3014
|
|
|
2956
3015
|
/** Shared loop-start path: /loop start AND propose_loop_draft (after Confirm). */
|
|
@@ -3017,6 +3076,9 @@ async function startLoopFromConfig(ctx: ExtensionContext, cfg: LoopConfig): Prom
|
|
|
3017
3076
|
branchName,
|
|
3018
3077
|
originalBranch,
|
|
3019
3078
|
toolSameRepeat: cfg.toolSameRepeat,
|
|
3079
|
+
specFile: cfg.specFile,
|
|
3080
|
+
specHash: cfg.specFile ? specFileHash(cfg.specFile) ?? undefined : undefined,
|
|
3081
|
+
specChecked: cfg.specFile ? countCheckedSpecItems(cfg.specFile) ?? undefined : undefined,
|
|
3020
3082
|
iterMetrics: { fileWrites: 0, iterationStartAt: nowIso() },
|
|
3021
3083
|
},
|
|
3022
3084
|
};
|
|
@@ -3135,6 +3197,28 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3135
3197
|
|
|
3136
3198
|
// v0.28.14: /loop cancel is a first-class alias — users reached for
|
|
3137
3199
|
// /goal cancel to kill loops because "cancel" is the verb they know.
|
|
3200
|
+
if (sub === "refine" || sub === "polish") {
|
|
3201
|
+
// v0.33.2: the operator's respec verb. The refine flow stays
|
|
3202
|
+
// agent-proposed + user-confirmed (propose_loop_refine) — this command
|
|
3203
|
+
// queues the operator's suggestion into the next iteration's prompt.
|
|
3204
|
+
// ("polish" accepted as an alias: the widget footer advertised it
|
|
3205
|
+
// before the command existed — now it does.)
|
|
3206
|
+
if (!isLoopActive()) {
|
|
3207
|
+
ctx.ui.notify("No active loop to refine — /loop start first.", "warning");
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
const hint = rest.trim();
|
|
3211
|
+
if (!hint) {
|
|
3212
|
+
ctx.ui.notify("Usage: /loop refine <what the spec should capture better> — the suggestion rides the next iteration's prompt; the agent proposes via propose_loop_refine and you confirm.", "info");
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
state.loop!.refineHint = hint.slice(0, 300);
|
|
3216
|
+
persistState(ctx);
|
|
3217
|
+
appendLedger(ctx.cwd, "loop_refine_hint", { iteration: state.loop!.iteration, hint: state.loop!.refineHint });
|
|
3218
|
+
ctx.ui.notify("Refine hint queued — it rides the next iteration's prompt.", "info");
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3138
3222
|
if (sub === "stop" || sub === "cancel") {
|
|
3139
3223
|
if (!state.loop) {
|
|
3140
3224
|
ctx.ui.notify("No loop to stop.", "info");
|
|
@@ -3273,6 +3357,7 @@ async function cmdLoop(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
3273
3357
|
maxIterations: 0,
|
|
3274
3358
|
branch: false,
|
|
3275
3359
|
force: false,
|
|
3360
|
+
specFile: specPath, // v0.33.2
|
|
3276
3361
|
});
|
|
3277
3362
|
return;
|
|
3278
3363
|
}
|
|
@@ -4124,12 +4209,14 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
4124
4209
|
parameters: Type.Object({
|
|
4125
4210
|
target: Type.Optional(Type.String({ description: "The sharpened target text (omit to keep the current target)" })),
|
|
4126
4211
|
measureCmd: Type.Optional(Type.String({ description: "The new measure command printing ONE number (omit to keep the current metric)" })),
|
|
4212
|
+
specText: Type.Optional(Type.String({ description: "v0.33.2: full replacement text for the loop's spec file (respec loops only) — the orchestrator owns the write on user confirm" })),
|
|
4213
|
+
specAppend: Type.Optional(Type.String({ description: "v0.33.2: lines to append to the loop's spec file (respec loops only)" })),
|
|
4127
4214
|
rationale: Type.String({ description: "Why the current spec no longer captures 'better' — shown to the user in the Confirm dialog" }),
|
|
4128
4215
|
}),
|
|
4129
4216
|
async execute(_id, params, _signal, _onUpdate, execCtx) {
|
|
4130
4217
|
const foreign4 = foreignToolGuard(execCtx);
|
|
4131
4218
|
if (foreign4) return { content: [{ type: "text", text: foreign4 }], details: {} };
|
|
4132
|
-
const p = params as { target?: string; measureCmd?: string; rationale: string };
|
|
4219
|
+
const p = params as { target?: string; measureCmd?: string; specText?: string; specAppend?: string; rationale: string };
|
|
4133
4220
|
const liveCtx = (execCtx as ExtensionContext | undefined) ?? ctx;
|
|
4134
4221
|
const loop = state.loop;
|
|
4135
4222
|
if (!loop?.active) {
|
|
@@ -4142,8 +4229,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
4142
4229
|
if (!loop.measureCmd && p.measureCmd?.trim()) {
|
|
4143
4230
|
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: {} };
|
|
4144
4231
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4232
|
+
const specChange = (p.specText?.trim() || p.specAppend?.trim()) ? true : false;
|
|
4233
|
+
if (specChange && !loop.specFile) {
|
|
4234
|
+
return { content: [{ type: "text", text: "This loop has no spec file (specText/specAppend apply to /loop respec loops). Refine the target instead." }], details: {} };
|
|
4235
|
+
}
|
|
4236
|
+
if (newTarget === loop.target && newMeasure === loop.measureCmd && !specChange) {
|
|
4237
|
+
return { content: [{ type: "text", text: "Refinement proposed no changes — provide a new target, a new measureCmd, a spec change, or any combination." }], details: {} };
|
|
4147
4238
|
}
|
|
4148
4239
|
// Measure change → orchestrator test-runs the new command first.
|
|
4149
4240
|
let newBaseline: number | null = null;
|
|
@@ -4174,7 +4265,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
4174
4265
|
confirmed = (await confirmDraft(
|
|
4175
4266
|
liveCtx,
|
|
4176
4267
|
"Confirm loop spec refinement",
|
|
4177
|
-
`Rationale: ${p.rationale}\n\nTarget:\n old: ${loop.target.slice(0, 120)}\n new: ${newTarget.slice(0, 120)}\n\nMeasure:\n old: ${loop.measureCmd}\n new: ${newMeasure}${newMeasure !== loop.measureCmd ? `\n test-run: ${testOutput.slice(0, 120)} → ${newBaseline}` : ""}\n\nThe loop keeps running against the refined spec (iteration ${loop.iteration} so far). Apply?`,
|
|
4268
|
+
`Rationale: ${p.rationale}\n\nTarget:\n old: ${loop.target.slice(0, 120)}\n new: ${newTarget.slice(0, 120)}\n\nMeasure:\n old: ${loop.measureCmd}\n new: ${newMeasure}${newMeasure !== loop.measureCmd ? `\n test-run: ${testOutput.slice(0, 120)} → ${newBaseline}` : ""}${specChange ? `\n\nSpec file (${loop.specFile}):\n ${p.specText?.trim() ? `REPLACE with ${p.specText!.trim().length} chars` : ""}${p.specText?.trim() && p.specAppend?.trim() ? " + " : ""}${p.specAppend?.trim() ? `APPEND: ${p.specAppend!.trim().slice(0, 120)}` : ""}` : ""}\n\nThe loop keeps running against the refined spec (iteration ${loop.iteration} so far). Apply?`,
|
|
4178
4269
|
)) === "yes";
|
|
4179
4270
|
} catch {
|
|
4180
4271
|
confirmed = false;
|
|
@@ -4191,9 +4282,22 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
|
|
|
4191
4282
|
oldMeasureCmd: loop.measureCmd ?? "",
|
|
4192
4283
|
newMeasureCmd: newMeasure,
|
|
4193
4284
|
}, newBaseline);
|
|
4285
|
+
// v0.33.2: the orchestrator owns the spec write (honesty stays
|
|
4286
|
+
// inspectable — the agent never edits the spec it's judged against
|
|
4287
|
+
// outside a confirmed refine).
|
|
4288
|
+
if (specChange && loop.specFile) {
|
|
4289
|
+
try {
|
|
4290
|
+
if (p.specText?.trim()) fs.writeFileSync(loop.specFile, p.specText.trim() + "\n");
|
|
4291
|
+
if (p.specAppend?.trim()) fs.appendFileSync(loop.specFile, (p.specText?.trim() ? "" : "\n") + p.specAppend.trim() + "\n");
|
|
4292
|
+
loop.specHash = specFileHash(loop.specFile) ?? undefined;
|
|
4293
|
+
appendLedger(liveCtx.cwd, "spec_updated", { via: "refine", iteration: loop.iteration, replaced: Boolean(p.specText?.trim()), appended: Boolean(p.specAppend?.trim()) });
|
|
4294
|
+
} catch (e) {
|
|
4295
|
+
return { content: [{ type: "text", text: `Spec file write failed: ${String(e).slice(0, 200)}. The target/measure refinement was applied; re-propose the spec change.` }], details: {} };
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4194
4298
|
persistState(liveCtx);
|
|
4195
|
-
appendLedger(liveCtx.cwd, "loop_refined", { iteration: loop.iteration, newTarget, newMeasureCmd: newMeasure, newBaseline });
|
|
4196
|
-
liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}`, "info");
|
|
4299
|
+
appendLedger(liveCtx.cwd, "loop_refined", { iteration: loop.iteration, newTarget, newMeasureCmd: newMeasure, newBaseline, specChanged: specChange || undefined });
|
|
4300
|
+
liveCtx.ui.notify(`Loop spec refined at iteration ${loop.iteration}.${newBaseline !== null ? ` New baseline: ${newBaseline}.` : ""}${specChange ? " Spec file updated." : ""}`, "info");
|
|
4197
4301
|
return { content: [{ type: "text", text: "Refinement confirmed and applied. Continue improving against the NEW spec — one small change per turn." }], details: {} };
|
|
4198
4302
|
},
|
|
4199
4303
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"description": "Goal. Loop. Audit. Done. \u2014 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 \u2014 only the read tools needed to verify your goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dracon",
|
|
@@ -31,6 +31,8 @@ fails, retry with a different approach — just continue, don't stall the loop
|
|
|
31
31
|
asking permission. You remain the single writer: apply the edit yourself.
|
|
32
32
|
|
|
33
33
|
${INTERVENTION_NOTE}
|
|
34
|
+
${HYPOTHESIS_NOTE}
|
|
35
|
+
${REFINE_HINT}
|
|
34
36
|
${REGRESSION_NOTE}
|
|
35
37
|
${STRATEGY_NOTE}
|
|
36
38
|
|
|
@@ -35,6 +35,8 @@ fails, retry with a different approach — just continue, don't stall the loop
|
|
|
35
35
|
asking permission. You remain the single writer: apply the edit yourself.
|
|
36
36
|
|
|
37
37
|
${INTERVENTION_NOTE}
|
|
38
|
+
${HYPOTHESIS_NOTE}
|
|
39
|
+
${REFINE_HINT}
|
|
38
40
|
${REGRESSION_NOTE}
|
|
39
41
|
${STRATEGY_NOTE}
|
|
40
42
|
|