pi-goal-list-loop-audit 0.28.7 → 0.28.9

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.
@@ -43,6 +43,11 @@ export interface LoopState {
43
43
  maxIterations: number;
44
44
  plateauWindow: number;
45
45
  stallCount: number;
46
+ /** v0.28.8 (E5): consecutive iterations where the measure printed NO
47
+ * number. Tracked separately from stallCount — plateau judges movement
48
+ * (a real number that didn't improve); a broken measure says nothing
49
+ * about movement and must stop the loop with its own loud reason. */
50
+ consecutiveNullMeasures?: number;
46
51
  bestValue: number | null;
47
52
  lastValue: number | null;
48
53
  active: boolean;
@@ -172,14 +177,20 @@ export type LoopTickOutcome =
172
177
  */
173
178
  export function applyMeasurement(loop: LoopState, value: number | null, at: string): LoopTickOutcome {
174
179
  loop.iteration++;
180
+ // improved is judged BEFORE bestValue moves (post-mutation it would read false).
175
181
  const improved = value !== null && loop.direction !== undefined && isImprovement(loop.direction, value, loop.bestValue);
176
182
  if (value === null) {
177
- loop.stallCount++;
178
- } else if (improved) {
179
- loop.bestValue = value;
180
- loop.stallCount = 0;
183
+ // E5: a null measure is NOT a stall — it carries no information about
184
+ // improvement. Plateau stays reserved for real non-improving numbers.
185
+ loop.consecutiveNullMeasures = (loop.consecutiveNullMeasures ?? 0) + 1;
181
186
  } else {
182
- loop.stallCount++;
187
+ loop.consecutiveNullMeasures = 0;
188
+ if (improved) {
189
+ loop.bestValue = value;
190
+ loop.stallCount = 0;
191
+ } else {
192
+ loop.stallCount++;
193
+ }
183
194
  }
184
195
  loop.lastValue = value;
185
196
  loop.history.push({ iteration: loop.iteration, value, improved, at });
@@ -198,6 +209,14 @@ export function applyMeasurement(loop: LoopState, value: number | null, at: stri
198
209
  loop.stopReason = `token budget exhausted (${(loop.tokensUsed ?? 0).toLocaleString()} >= ${loop.tokenBudget.toLocaleString()}); best: ${loop.bestValue ?? "n/a"}`;
199
210
  return { kind: "stop", reason: loop.stopReason };
200
211
  }
212
+ // E5: a broken measure command gets its OWN loud stop — never the
213
+ // misleading "plateau — no improvement" (there was nothing to improve
214
+ // against; the metric itself is dead).
215
+ if ((loop.consecutiveNullMeasures ?? 0) >= loop.plateauWindow) {
216
+ loop.active = false;
217
+ loop.stopReason = `measure command broken — ${loop.consecutiveNullMeasures} consecutive iterations printed no number (cmd: \`${loop.measureCmd ?? "?"}\`). Fix the measure command, or /loop stop.`;
218
+ return { kind: "stop", reason: loop.stopReason };
219
+ }
201
220
  if (loop.stallCount >= loop.plateauWindow) {
202
221
  loop.active = false;
203
222
  loop.stopReason = `plateau — no improvement in ${loop.plateauWindow} consecutive iterations (best: ${loop.bestValue ?? "n/a"})`;
@@ -879,8 +879,16 @@ function fireReviewer(
879
879
  `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`,
880
880
  { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
881
881
  );
882
- } catch {
883
- /* proposal best-effort */
882
+ return true;
883
+ } catch (err) {
884
+ // v0.28.8 (E4): the phantom-reviewer hole — a swallowed throw used
885
+ // to still count as "proposed" in the report + notify. Now the
886
+ // failure is LOUD and the proposal goes uncounted.
887
+ ctx.ui.notify(
888
+ `Reviewer /goal proposal NOT delivered: ${err instanceof Error ? err.message : String(err)} — the follow-up never reached the session. Restart pi if the session was just replaced.`,
889
+ "warning",
890
+ );
891
+ return false;
884
892
  }
885
893
  },
886
894
  notify: (message, level) => ctx.ui.notify(message, level),
@@ -197,7 +197,11 @@ export interface ReviewerDeps {
197
197
  /** Source texts for finding extraction (archive md, audit reports). */
198
198
  sources: Array<{ name: string; text: string }>;
199
199
  enqueueListItems: (objectives: string[]) => void;
200
- proposeGoal: (objective: string, reason: string) => void;
200
+ /** Deliver a /goal proposal message to the session. Returns true when the
201
+ * message was actually sent; false when the send failed (the v0.28.8 E4
202
+ * contract — a failed send must NOT count as `proposed`, else the user is
203
+ * told about phantom proposals that never arrived). */
204
+ proposeGoal: (objective: string, reason: string) => boolean;
201
205
  notify: (message: string, level: "info" | "warning") => void;
202
206
  ledger: (type: string, value: Record<string, unknown>) => void;
203
207
  }
@@ -274,23 +278,29 @@ export function runReviewer(
274
278
  // v0.27.5 aggressive: also propose the FIRST architectural finding
275
279
  // as a relaunch so the queue gets burned through even when the
276
280
  // unattended rig can't Confirm.
277
- deps.proposeGoal(
278
- architectural[0]!.text,
279
- `aggressive postaudit: relaunching as /goal without Confirm (${architectural.length} architectural findings total)`,
280
- );
281
+ if (
282
+ deps.proposeGoal(
283
+ architectural[0]!.text,
284
+ `aggressive postaudit: relaunching as /goal without Confirm (${architectural.length} architectural findings total)`,
285
+ )
286
+ ) {
287
+ proposed += 1;
288
+ }
281
289
  enqueued += architectural.length;
282
- proposed += 1;
283
290
  cascadeStep = "aggressive-relaunch";
284
291
  } else if (auto) {
285
292
  deps.enqueueListItems(architectural.map((f) => f.text));
286
293
  enqueued += architectural.length;
287
294
  cascadeStep = convertStep;
288
295
  } else {
289
- deps.proposeGoal(
290
- architectural.map((f) => f.text).join("; "),
291
- `reviewer found ${architectural.length} architectural-class finding(s) needs your Confirm`,
292
- );
293
- proposed += architectural.length;
296
+ if (
297
+ deps.proposeGoal(
298
+ architectural.map((f) => f.text).join("; "),
299
+ `reviewer found ${architectural.length} architectural-class finding(s) — needs your Confirm`,
300
+ )
301
+ ) {
302
+ proposed += architectural.length;
303
+ }
294
304
  cascadeStep = "propose-goal";
295
305
  }
296
306
  }
@@ -301,16 +311,18 @@ export function runReviewer(
301
311
  if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")) {
302
312
  const auditObjective = `Post-completion regression scan after ${source.goalId} (${config.auditScope})`;
303
313
  if (aggressive) {
304
- deps.proposeGoal(auditObjective, "aggressive postaudit: clean completion — relaunching the regression scan as /goal");
305
- proposed++;
314
+ if (deps.proposeGoal(auditObjective, "aggressive postaudit: clean completion — relaunching the regression scan as /goal")) {
315
+ proposed++;
316
+ }
306
317
  cascadeStep = "aggressive-relaunch";
307
318
  } else if (auto) {
308
319
  deps.enqueueListItems([auditObjective]);
309
320
  enqueued++;
310
321
  cascadeStep = "fire-audit-on-clean";
311
322
  } else {
312
- deps.proposeGoal(auditObjective, "reviewer: completion looks clean — firing the audit step");
313
- proposed++;
323
+ if (deps.proposeGoal(auditObjective, "reviewer: completion looks clean — firing the audit step")) {
324
+ proposed++;
325
+ }
314
326
  cascadeStep = "fire-audit-on-clean";
315
327
  }
316
328
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.7",
3
+ "version": "0.28.9",
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",