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.
@@ -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 polish"
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) return undefined;
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;
@@ -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
- 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). ${AUDIT_PLATEAU_MAX_REPRIEVES - reprieves} reprieve(s) remain.`;
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
- if (newTarget === loop.target && newMeasure === loop.measureCmd) {
4146
- return { content: [{ type: "text", text: "Refinement proposed no changes — provide a new target, a new measureCmd, or both." }], details: {} };
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.1",
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