pi-goal-list-loop-audit 0.28.6 → 0.28.8

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.
@@ -897,7 +897,12 @@ export interface EffectiveAggressiveSettings {
897
897
  stuckMaxInterventions: number;
898
898
  /** 0 = wedge alerts off. */
899
899
  wedgeAlertMinutes: number;
900
- autoResume: boolean;
900
+ /** Tri-state: true = always auto-resume; false = never; undefined =
901
+ * DEFAULT (hold on human session loads, resume on reload/fork).
902
+ * v0.28.7: must stay tri-state here — coercing unset→false broke the
903
+ * restore gate's default branch (the 0.28.3 regression the behavioral
904
+ * harness caught). */
905
+ autoResume: boolean | undefined;
901
906
  aggressiveMode: boolean;
902
907
  }
903
908
 
@@ -918,7 +923,7 @@ export function resolveEffectiveAggressiveSettings(s: {
918
923
  stuckMaxInterventions:
919
924
  s.stuckMaxInterventions ?? (aggressiveMode ? AGGRESSIVE_STUCK_MAX_INTERVENTIONS : BASE_STUCK_MAX_INTERVENTIONS),
920
925
  wedgeAlertMinutes: s.wedgeAlertMinutes ?? (aggressiveMode ? 0 : 30),
921
- autoResume: s.autoResume ?? aggressiveMode,
926
+ autoResume: s.autoResume ?? (aggressiveMode ? true : undefined),
922
927
  };
923
928
  }
924
929
 
@@ -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"})`;
@@ -224,6 +224,13 @@ function goStaleTerminal(ctx: ExtensionContext, where: string): void {
224
224
  notifyExternal(ctx, `glla: extension api stale — restart pi. (${where})`);
225
225
  }
226
226
 
227
+ /** TEST-ONLY hook (tests/harness): the stale flag is process-terminal in
228
+ * production — only a pi restart clears it — so behavioral tests reset it
229
+ * between stale scenarios. Never called by production code. */
230
+ export function __testOnlyResetStaleFlag(): void {
231
+ extensionApiStale = false;
232
+ }
233
+
227
234
  /** v0.28.1 (S3): side-effect-free staleness probe — getSessionName()
228
235
  * routes through pi's assertActive() and throws the stale signature iff
229
236
  * pi invalidated this factory handle (session replacement). A positive
@@ -872,8 +879,16 @@ function fireReviewer(
872
879
  `[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`,
873
880
  { deliverAs: ctx.isIdle() ? "followUp" : "steer" },
874
881
  );
875
- } catch {
876
- /* 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;
877
892
  }
878
893
  },
879
894
  notify: (message, level) => ctx.ui.notify(message, level),
@@ -2523,11 +2538,12 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2523
2538
  pi.registerTool(defineTool({
2524
2539
  name: "complete_task",
2525
2540
  label: "Complete task",
2526
- description: "Mark a task in the active goal's task list as complete (does not stop the turn).",
2527
- parameters: Type.Object({
2541
+ description: "Mark a task in the active goal's task list as complete (does not stop the turn).", parameters: Type.Object({
2528
2542
  id: Type.String({ description: "Task id to complete" }),
2529
2543
  }),
2530
- async execute(_id, params) {
2544
+ async execute(_id, params, _signal, _onUpdate, execCtx) {
2545
+ const foreign7 = foreignToolGuard(execCtx);
2546
+ if (foreign7) return { content: [{ type: "text", text: foreign7 }], details: {} };
2531
2547
  const p = params as { id: string };
2532
2548
  if (!state.goal || !state.goal.taskList) {
2533
2549
  return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
@@ -2555,7 +2571,9 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2555
2571
  id: Type.String(),
2556
2572
  status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("complete")]),
2557
2573
  }),
2558
- async execute(_id, params) {
2574
+ async execute(_id, params, _signal, _onUpdate, execCtx) {
2575
+ const foreign8 = foreignToolGuard(execCtx);
2576
+ if (foreign8) return { content: [{ type: "text", text: foreign8 }], details: {} };
2559
2577
  const p = params as { id: string; status: "pending" | "in_progress" | "complete" };
2560
2578
  if (!state.goal || !state.goal.taskList) {
2561
2579
  return { content: [{ type: "text", text: "No task list in this goal." }], details: {} };
@@ -3012,6 +3030,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
3012
3030
  })),
3013
3031
  }),
3014
3032
  async execute(_id, params, _signal, _onUpdate, execCtx) {
3033
+ const foreign9 = foreignToolGuard(execCtx);
3034
+ if (foreign9) return { content: [{ type: "text", text: foreign9 }], details: {} };
3015
3035
  if (!state.goal || state.goal.status !== "active") {
3016
3036
  return { content: [{ type: "text", text: "No active goal to break down." }], details: {} };
3017
3037
  }
@@ -3179,7 +3199,9 @@ async function promptSettingsMenu(
3179
3199
  * Same handlers as v0.27.0's if/else chain — only the trigger changed from
3180
3200
  * `startsWith(label)` strings to stable ids.
3181
3201
  */
3182
- async function handleSettingChoice(id: string, ctx: ExtensionContext): Promise<void> {
3202
+ // v0.28.7 (T4): exported for the behavioral settings-editor tests
3203
+ // (tests/settings-editors.test.ts drives each editor class end-to-end).
3204
+ export async function handleSettingChoice(id: string, ctx: ExtensionContext): Promise<void> {
3183
3205
  switch (id) {
3184
3206
  case "autoResume": {
3185
3207
  const v = await ctx.ui.select("Auto-resume goals/loops on session start", [
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.28.6",
3
+ "version": "0.28.8",
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",