pi-goal-list-loop-audit 0.24.8 → 0.25.0

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
@@ -200,8 +200,11 @@ No external watchdog plugin needed.
200
200
  /glla tokenlimit=0 # explicitly no cap (the default)
201
201
  /glla wedgealert=30 # hung-command alert minutes (default: 30, 0 = off)
202
202
  /glla autoresume=on # held goals/loops auto-resume in fresh sessions (unattended rigs)
203
- /glla auditcap=5 # pause the goal after N consecutive auditor disapprovals (default 3, 0 = unlimited)
204
- /glla auditfeedbackchars=16000 # auditor report returned to the executor (default 800, 0 = full report)
203
+ /glla auditcap=5 # pause the goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)
204
+ /glla aggressivemode=on # keep-going defaults: autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs
205
+ /glla quotaretryminutes=60 # minutes before auto-retrying a quota-exhausted auditor
206
+ /glla stuckmax=10 # consecutive stuck interventions before a loop stops (default 5)
207
+ /glla auditfeedbackchars=800 # cap the executor-visible auditor report (default 0 = full report)
205
208
  /glla autoaccept=on # drafts ACTIVATE without the Confirm dialog (unattended rigs)
206
209
  /glla project tokenlimit=500 # rare per-project override
207
210
  ```
@@ -212,12 +215,11 @@ auditor can't auth it — you're told once (info level) with the fix:
212
215
  `/glla model=provider/id`, set once, rarely touched again. The plugin never
213
216
  picks a model itself. Thinking follows the session too (floor `high`).
214
217
 
215
- On disapproval, the executor receives up to `auditFeedbackChars` characters
216
- from the auditor report (default 800, preserving the previous behavior).
217
- Increase it for multi-item `<evidence>` blocks and raw verification output,
218
- or set `/glla auditfeedbackchars=0` to return the full report. The complete
219
- report continues to be stored in audit history and is available through
220
- `/goal status`.
218
+ On disapproval, the executor receives the full auditor report by default
219
+ (`auditFeedbackChars=0`, since v0.24.9 a truncated report loses exactly the
220
+ actionable tail of multi-item `<evidence>` blocks). Set a positive
221
+ `auditFeedbackChars` to cap it. The complete report is always stored in audit
222
+ history and is available through `/goal status` regardless.
221
223
 
222
224
  `autoaccept=on` skips BOTH the Confirm dialog and the drafting interview
223
225
  floor — every `propose_*` draft (goal, list batch, loop, task list)
@@ -107,6 +107,7 @@ function buildGoalAuditorPrompt(goal: Goal, completionSummary: string | null | u
107
107
  "Use read/grep/find/ls/bash as needed to inspect real artifacts. Do not mutate files or run destructive commands.",
108
108
  "If the work is only an alpha scaffold, generated template, shallow draft, proxy milestone, or lacks the user-facing value requested, disapprove.",
109
109
  "If any explicit requirement is missing, weakly verified, contradicted, or not inspectable with the available evidence, disapprove.",
110
+ "Objective shift: if the executor's <completion_summary> explicitly states that the work has shifted to other items (different from the original <goal> objective), and the shift is justified — e.g. the original objective was blocked, the agent pivoted to higher-ROI items, and the new items are independently verified — accept the shift as evidence of reasonable engineering judgment. Do NOT rigidly disapprove because the original objective is not literally shipped; audit the shifted work on its merits and say so in the report.",
110
111
  "Return a concise audit report. The final line MUST be exactly one of:",
111
112
  "<approved/>",
112
113
  "<disapproved/>",
@@ -9,6 +9,7 @@
9
9
 
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
+ import { execSync } from "node:child_process";
12
13
 
13
14
  // =================================================================
14
15
  // Types
@@ -144,6 +145,10 @@ export interface Goal {
144
145
  stopReason?: string;
145
146
  pauseReason?: string;
146
147
  pauseSuggestedAction?: string;
148
+ /** v0.25.0 (contract item 22): auditor objections extracted as TODOs when
149
+ * aggressiveMode keeps the goal active past the disapproval cap. Rendered
150
+ * into every continuation prompt until the next audit clears them. */
151
+ pendingTasks?: string[];
147
152
  activePath?: string;
148
153
  archivedPath?: string;
149
154
  usage: {
@@ -287,8 +292,14 @@ export function mergeSettings<T extends Record<string, unknown>>(base: T, ...lay
287
292
  return out as T;
288
293
  }
289
294
 
290
- /** Backward-compatible default for executor-visible auditor feedback. */
291
- export const DEFAULT_AUDIT_FEEDBACK_CHARS = 800;
295
+ /**
296
+ * Default for executor-visible auditor feedback. 0 = no cap: the executor
297
+ * gets the FULL disapproval report (v0.24.9 — truncating by default cut
298
+ * exactly the actionable tail of multi-item <evidence> blocks; a few KB of
299
+ * report is negligible next to a wasted re-attempt). Set a positive
300
+ * auditFeedbackChars to cap.
301
+ */
302
+ export const DEFAULT_AUDIT_FEEDBACK_CHARS = 0;
292
303
 
293
304
  /**
294
305
  * Bound the auditor report returned to the executor after disapproval.
@@ -755,3 +766,155 @@ export function missingGllaTools(activeNames: readonly string[]): readonly GllaT
755
766
  const active = new Set(activeNames);
756
767
  return GLLA_TOOL_NAMES.filter((n) => !active.has(n));
757
768
  }
769
+
770
+ // -----------------------------------------------------------------
771
+ // v0.25.0 — eager-continuation contract helpers
772
+ // -----------------------------------------------------------------
773
+
774
+ /** Base defaults (aggressiveMode OFF). auditCap base raised 3 → 5 in
775
+ * v0.25.0 (contract item 7 — the "fairly eager" baseline). */
776
+ export const BASE_AUDIT_CAP = 5;
777
+ export const BASE_STUCK_MAX_INTERVENTIONS = 5;
778
+ /** aggressiveMode defaults (contract item 5). Explicit per-key settings
779
+ * always win over these — aggressiveMode flips DEFAULTS, not user choices. */
780
+ export const AGGRESSIVE_AUDIT_CAP = 10;
781
+ export const AGGRESSIVE_STUCK_MAX_INTERVENTIONS = 10;
782
+ export const DEFAULT_QUOTA_RETRY_MINUTES = 60;
783
+
784
+ export interface EffectiveAggressiveSettings {
785
+ auditCap: number;
786
+ stuckMaxInterventions: number;
787
+ /** 0 = wedge alerts off. */
788
+ wedgeAlertMinutes: number;
789
+ autoResume: boolean;
790
+ aggressiveMode: boolean;
791
+ }
792
+
793
+ /** Layered resolution: explicit per-key value > aggressiveMode default >
794
+ * base default (contract items 5+7). Pure so tests can assert the matrix
795
+ * without a settings file. */
796
+ export function resolveEffectiveAggressiveSettings(s: {
797
+ aggressiveMode?: boolean;
798
+ auditCap?: number;
799
+ stuckMaxInterventions?: number;
800
+ wedgeAlertMinutes?: number;
801
+ autoResume?: boolean;
802
+ }): EffectiveAggressiveSettings {
803
+ const aggressiveMode = s.aggressiveMode === true;
804
+ return {
805
+ aggressiveMode,
806
+ auditCap: s.auditCap ?? (aggressiveMode ? AGGRESSIVE_AUDIT_CAP : BASE_AUDIT_CAP),
807
+ stuckMaxInterventions:
808
+ s.stuckMaxInterventions ?? (aggressiveMode ? AGGRESSIVE_STUCK_MAX_INTERVENTIONS : BASE_STUCK_MAX_INTERVENTIONS),
809
+ wedgeAlertMinutes: s.wedgeAlertMinutes ?? (aggressiveMode ? 0 : 30),
810
+ autoResume: s.autoResume ?? aggressiveMode,
811
+ };
812
+ }
813
+
814
+ /** Extract up to `cap` actionable objection lines from an auditor report
815
+ * (contract item 22): numbered/bulleted lines, most recent last-report
816
+ * wins, longest tails trimmed. Pure — the audit-cap branch and its test
817
+ * share this. */
818
+ export function extractPendingTasks(report: string, cap = 5): string[] {
819
+ const out: string[] = [];
820
+ for (const raw of report.split("\n")) {
821
+ const line = raw.trim();
822
+ const m = line.match(/^(?:[-*•]|\d+[.)])\s+(.{8,200})$/);
823
+ if (!m) continue;
824
+ const text = m[1]!.trim();
825
+ // Skip pure-evidence bullets ("file X exists", "tests pass") — we want
826
+ // OBJECTIONS: missing/failing/not-done language.
827
+ if (!/miss|fail|not |no |lack|absent|doesn|didn|won|can'?t|remain|todo|fix|requir|incomplete|unverified/i.test(text)) continue;
828
+ if (!out.includes(text)) out.push(text);
829
+ if (out.length >= cap) break;
830
+ }
831
+ return out;
832
+ }
833
+
834
+ /** Contract item 23: is the auditor's IMPOSSIBLE reason about the WHOLE
835
+ * goal or only part of it? Default "full" (safe — keeps the pause);
836
+ * partial only on explicit subset language. */
837
+ export function classifyImpossibleReason(reason: string): "partial" | "full" {
838
+ if (/\b(partial|some items|subset|remaining items|narrow|only .{0,30}(item|part|section)|the rest|rest of)\b/i.test(reason)) {
839
+ return "partial";
840
+ }
841
+ return "full";
842
+ }
843
+
844
+ /** Contract items 25/28: does this objective read as a full-audit /
845
+ * survey pivot? */
846
+ export function isFullAuditObjective(objective: string): boolean {
847
+ return /full audit|survey|find all|task ?list|enumerate|audit the (whole |entire )?project/i.test(objective);
848
+ }
849
+
850
+ // --- Auto-committer daemon sentinel (contract item 31) ---
851
+
852
+ /** Sentinel the auto-committer (dracon-sync) filter checks: when present,
853
+ * the daemon must not rewrite/commit in this repo. The agent writes it
854
+ * after detecting filter-branch damage (see DETACHED COMMIT DETECTION in
855
+ * the continuation prompt). */
856
+ export const PAUSE_AUTO_COMMIT_SENTINEL = ".pause-auto-commit";
857
+
858
+ export function pauseAutoCommit(cwd: string, reason: string): string {
859
+ const dir = path.join(cwd, ".pi-glla");
860
+ fs.mkdirSync(dir, { recursive: true });
861
+ const file = path.join(dir, PAUSE_AUTO_COMMIT_SENTINEL);
862
+ fs.writeFileSync(file, `pausedAt: ${nowIso()}\nreason: ${reason}\n`, "utf-8");
863
+ return file;
864
+ }
865
+
866
+ export function resumeAutoCommit(cwd: string): boolean {
867
+ const file = path.join(cwd, ".pi-glla", PAUSE_AUTO_COMMIT_SENTINEL);
868
+ try {
869
+ fs.unlinkSync(file);
870
+ return true;
871
+ } catch {
872
+ return false;
873
+ }
874
+ }
875
+
876
+ export function isAutoCommitPaused(cwd: string): boolean {
877
+ try {
878
+ fs.accessSync(path.join(cwd, ".pi-glla", PAUSE_AUTO_COMMIT_SENTINEL));
879
+ return true;
880
+ } catch {
881
+ return false;
882
+ }
883
+ }
884
+
885
+ // --- Heartbeat ship-suppression (contract item 27) ---
886
+
887
+ /** Suppress the heartbeat when work shipped very recently — a session
888
+ * that just committed is transitioning, not stalled. Pure; the tick
889
+ * gathers the timestamps. */
890
+ export function shouldSuppressHeartbeatForRecentShip(args: {
891
+ nowMs: number;
892
+ lastShippedAtMs: number | null;
893
+ windowMs?: number;
894
+ }): boolean {
895
+ const windowMs = args.windowMs ?? 5 * 60_000;
896
+ if (args.lastShippedAtMs === null) return false;
897
+ return args.nowMs - args.lastShippedAtMs < windowMs;
898
+ }
899
+
900
+ /** Best-effort "when did work last ship" for a repo: newest of the HEAD
901
+ * commit time and the .pi-glla state file mtime. Null when unknown. */
902
+ export function lastShippedAtMs(cwd: string): number | null {
903
+ let best: number | null = null;
904
+ try {
905
+ const out = execSync("git log -1 --format=%ct", { cwd, stdio: ["ignore", "pipe", "ignore"] })
906
+ .toString()
907
+ .trim();
908
+ const sec = Number(out);
909
+ if (Number.isFinite(sec) && sec > 0) best = sec * 1000;
910
+ } catch {
911
+ /* not a git repo or no commits */
912
+ }
913
+ try {
914
+ const mtime = fs.statSync(path.join(cwd, ".pi-glla", "active.jsonl")).mtimeMs;
915
+ if (best === null || mtime > best) best = mtime;
916
+ } catch {
917
+ /* no state file yet */
918
+ }
919
+ return best;
920
+ }
@@ -0,0 +1,153 @@
1
+ // pi-goal-list-loop-audit — v0.25.0
2
+ // extensions/goal-settings.ts
3
+ //
4
+ // The settings layer, extracted from loops/goal.ts so tests can drive it
5
+ // without importing the whole extension. Two-tier config (v0.7.0): GLOBAL
6
+ // is the normal home, PROJECT the rare local override. Resolution:
7
+ // project > global > defaults (per key).
8
+
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ import {
14
+ DEFAULT_AUDIT_FEEDBACK_CHARS,
15
+ DEFAULT_QUOTA_RETRY_MINUTES,
16
+ mergeSettings,
17
+ piGlaDir,
18
+ } from "./goal-loop-core.ts";
19
+ import type { SubagentModelStrategy } from "./goal-loop-subagents.js";
20
+
21
+ export interface Settings {
22
+ /** "provider/model-id" or bare "model-id". Unset → session model. */
23
+ auditorModel?: string;
24
+ auditorThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
25
+ /** Shell command run on goal complete / goal pause / loop stop; message passed as $1. */
26
+ notifyCmd?: string;
27
+ /** Per-goal token budget; crossing it pauses the goal. Off by default
28
+ * (opt-in guard, v0.12.0): unset/0 = no budget. */
29
+ tokenLimit?: number;
30
+ /** v0.23.2: minutes of busy-but-silent before the wedge alert fires
31
+ * (hung-command detector). Unset = 45; 0 = off. */
32
+ wedgeAlertMinutes?: number;
33
+ /** on → restored goals/loops/lists auto-resume even in fresh sessions
34
+ * (unattended rigs). Default off: restore holds until /goal resume. */
35
+ autoResume?: boolean;
36
+ /** v0.24.2: pause the goal after N consecutive auditor disapprovals (0 = unlimited).
37
+ * Default 5 (raised from 3 in v0.25.0, contract item 7). */
38
+ auditCap?: number;
39
+ /** Maximum auditor-report characters returned to the executor after a
40
+ * disapproval (0 = full report). Default 0 (full report). */
41
+ auditFeedbackChars?: number;
42
+ /** v0.25.0: flip the continuation defaults toward keep-going
43
+ * (contract item 5): autoResume on, auditCap 10, stuckMax 10, wedge off,
44
+ * quota errors auto-retry silently. Explicit per-key settings still win. */
45
+ aggressiveMode?: boolean;
46
+ /** Minutes to wait before auto-retrying a quota-exhausted auditor when
47
+ * the upstream gave no Retry-After hint (contract item 11). Default 60. */
48
+ quotaRetryMinutes?: number;
49
+ /** Consecutive stuck interventions before a loop stops (default 5,
50
+ * 10 under aggressiveMode). */
51
+ stuckMaxInterventions?: number;
52
+ /** on → propose_* drafts activate WITHOUT the Confirm dialog and the
53
+ * interview floor is skipped — the seed carries the intent (unattended
54
+ * rigs). Default off: nothing activates before the user confirms. */
55
+ autoAcceptDrafts?: boolean;
56
+ /** v0.24.6: subagent model strategy for pi-subagents default agents that
57
+ * pin a model (Explore pins claude-haiku-4-5, which silently routes
58
+ * subagents to a different provider/quota pool than the session).
59
+ * "inherit-parent" (default) writes a managed ~/.pi/agent/agents/Explore.md
60
+ * override without the model pin so subagents share the session model and
61
+ * its quota; "agent-default" restores upstream behavior. Applies to NEW
62
+ * sessions (pi-subagents registers agents at session start). */
63
+ subagentModelStrategy?: SubagentModelStrategy;
64
+ /** v0.24.6: per-agent-type model pin, e.g. { "Explore": "minimax/MiniMax-M3" }.
65
+ * Always wins over subagentModelStrategy — the managed override is written
66
+ * WITH this pin regardless of strategy. */
67
+ subagentModelOverrides?: Record<string, string>;
68
+ }
69
+
70
+ export const DEFAULT_SETTINGS: Settings = {
71
+ // Unset = follow the pi session thinking level (user selects thinking in
72
+ // pi, auditor follows), floor "high" — the auditor is the verification
73
+ // gate, depth is worth more there than speed. /glla thinking= overrides.
74
+ auditorThinkingLevel: undefined,
75
+ // v0.24.6: subagents inherit the session model by default — one quota
76
+ // pool, no surprise 403s from a pinned default agent's provider.
77
+ subagentModelStrategy: "inherit-parent",
78
+ auditFeedbackChars: DEFAULT_AUDIT_FEEDBACK_CHARS,
79
+ // v0.25.0 (contract Section B): keep-going is opt-in via aggressiveMode;
80
+ // the dial flips DEFAULTS, never explicit per-key user settings.
81
+ aggressiveMode: false,
82
+ quotaRetryMinutes: DEFAULT_QUOTA_RETRY_MINUTES,
83
+ };
84
+
85
+ export function globalSettingsPath(): string {
86
+ return path.join(os.homedir(), ".pi", "agent", "pi-goal-list-loop-audit.settings.json");
87
+ }
88
+
89
+ export function projectSettingsPath(cwd: string): string {
90
+ return path.join(piGlaDir(cwd), "settings.json");
91
+ }
92
+
93
+ export function readSettingsFile(file: string): Partial<Settings> {
94
+ try {
95
+ if (!fs.existsSync(file)) return {};
96
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
97
+ return typeof parsed === "object" && parsed !== null ? parsed as Partial<Settings> : {};
98
+ } catch {
99
+ return {};
100
+ }
101
+ }
102
+
103
+ export function loadSettings(cwd: string): Settings {
104
+ return mergeSettings(
105
+ DEFAULT_SETTINGS as unknown as Record<string, unknown>,
106
+ readSettingsFile(globalSettingsPath()) as Record<string, unknown>,
107
+ readSettingsFile(projectSettingsPath(cwd)) as Record<string, unknown>,
108
+ ) as unknown as Settings;
109
+ }
110
+
111
+ /** Every provenance-tracked key (the /glla headless display + UI). */
112
+ export const SETTINGS_KEYS: Array<keyof Settings> = [
113
+ "auditorModel",
114
+ "auditorThinkingLevel",
115
+ "notifyCmd",
116
+ "tokenLimit",
117
+ "wedgeAlertMinutes",
118
+ "autoResume",
119
+ "autoAcceptDrafts",
120
+ "auditCap",
121
+ "auditFeedbackChars",
122
+ "subagentModelStrategy",
123
+ "subagentModelOverrides",
124
+ "aggressiveMode",
125
+ "quotaRetryMinutes",
126
+ "stuckMaxInterventions",
127
+ ];
128
+
129
+ /** Where each effective setting comes from (for the /glla display). */
130
+ export function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknown; source: "project" | "global" | "default" }> {
131
+ const proj = readSettingsFile(projectSettingsPath(cwd));
132
+ const glob = readSettingsFile(globalSettingsPath());
133
+ const effective = loadSettings(cwd);
134
+ const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
135
+ for (const k of SETTINGS_KEYS) {
136
+ if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
137
+ else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
138
+ else out[k] = { value: (effective as any)[k], source: "default" };
139
+ }
140
+ return out as Record<keyof Settings, { value: unknown; source: "project" | "global" | "default" }>;
141
+ }
142
+
143
+ export function saveSettings(scope: "global" | "project", cwd: string, patch: Partial<Settings>): void {
144
+ const file = scope === "global" ? globalSettingsPath() : projectSettingsPath(cwd);
145
+ const current = readSettingsFile(file);
146
+ const next: Record<string, unknown> = { ...current };
147
+ for (const [k, v] of Object.entries(patch)) {
148
+ if (v === undefined) delete next[k]; // key=unset removes the key
149
+ else next[k] = v;
150
+ }
151
+ fs.mkdirSync(path.dirname(file), { recursive: true });
152
+ fs.writeFileSync(file, JSON.stringify(next, null, 2));
153
+ }
@@ -32,7 +32,14 @@ import {
32
32
  buildTaskSummary,
33
33
  auditFeedbackExcerpt,
34
34
  DEFAULT_AUDIT_FEEDBACK_CHARS,
35
+ DEFAULT_QUOTA_RETRY_MINUTES,
35
36
  DEFAULT_TOKEN_LIMIT,
37
+ classifyImpossibleReason,
38
+ extractPendingTasks,
39
+ isFullAuditObjective,
40
+ lastShippedAtMs,
41
+ resolveEffectiveAggressiveSettings,
42
+ shouldSuppressHeartbeatForRecentShip,
36
43
  mergeSettings,
37
44
  parseListImport,
38
45
 
@@ -67,6 +74,22 @@ import {
67
74
  writeGoalMd,
68
75
  missingGllaTools,
69
76
  } from "../goal-loop-core.js";
77
+ import {
78
+ isQuotaError,
79
+ parseQuotaError,
80
+ scheduleQuotaRetry,
81
+ cancelQuotaRetry,
82
+ } from "../quota-retry.js";
83
+ import {
84
+ DEFAULT_SETTINGS,
85
+ SETTINGS_KEYS,
86
+ globalSettingsPath,
87
+ loadSettings,
88
+ projectSettingsPath,
89
+ saveSettings,
90
+ settingsProvenance,
91
+ type Settings,
92
+ } from "../goal-settings.js";
70
93
  import { runGoalCompletionAuditor } from "../goal-loop-auditor.js";
71
94
  import {
72
95
  REPETITION,
@@ -239,7 +262,7 @@ function heartbeatTick(): void {
239
262
  // the entire goal hostage; field-observed at 5,056s and 6,800s on the
240
263
  // same wedged tool call). Independent of the refire path, which only
241
264
  // watches idle sessions.
242
- const wedgeMinutes = loadSettings(ctx.cwd).wedgeAlertMinutes ?? WEDGE_ALERT_DEFAULT_MINUTES;
265
+ const wedgeMinutes = resolveEffectiveAggressiveSettings(loadSettings(ctx.cwd)).wedgeAlertMinutes ?? WEDGE_ALERT_DEFAULT_MINUTES;
243
266
  if (
244
267
  shouldWedgeAlert({
245
268
  supervising: isSupervising(),
@@ -256,6 +279,18 @@ function heartbeatTick(): void {
256
279
  notifyExternal(ctx, msg);
257
280
  }
258
281
  if (!fire) return;
282
+ // v0.25.0 (contract item 27): a session that SHIPPED in the last 5
283
+ // minutes (commit or ledger write) is transitioning, not stalled —
284
+ // suppress the refire so rapid iteration isn't interrupted.
285
+ if (
286
+ shouldSuppressHeartbeatForRecentShip({
287
+ nowMs: Date.now(),
288
+ lastShippedAtMs: lastShippedAtMs(ctx.cwd),
289
+ })
290
+ ) {
291
+ appendLedger(ctx.cwd, "heartbeat_suppressed", { reason: "recent ship (<5m)" });
292
+ return;
293
+ }
259
294
  noteActivity();
260
295
  appendLedger(ctx.cwd, "heartbeat_refire", { nudgesSoFar: heartbeatNudges });
261
296
  ctx.ui.notify("Heartbeat: supervisor active but session stalled — re-firing continuation.", "info");
@@ -371,12 +406,29 @@ function continuationPrompt(goal: Goal): string {
371
406
  } catch {
372
407
  tmpl = "[template-not-found]";
373
408
  }
409
+ // v0.25.0 (contract items 22/28): conditional directives — aggressiveMode
410
+ // TODOs from the audit cap, and the full-audit fan-out directive when the
411
+ // objective reads as a survey pivot.
412
+ const directives: string[] = [];
413
+ const effSettings = resolveEffectiveAggressiveSettings(loadSettings(freshCtx()?.cwd ?? process.cwd()));
414
+ if (goal.pendingTasks && goal.pendingTasks.length > 0) {
415
+ directives.push(
416
+ `## AUDITOR TODO LIST (from ${goal.pauseReason?.includes("cap") ? "the disapproval cap" : "the last audit"})\n\nAddress these objections, in order, before re-calling complete_goal:\n${goal.pendingTasks.map((t, i) => `${i + 1}. ${t}`).join("\n")}`,
417
+ );
418
+ }
419
+ if (effSettings.aggressiveMode && isFullAuditObjective(goal.objective)) {
420
+ directives.push(
421
+ "## FULL-AUDIT MODE (aggressiveMode + survey objective)\n\nThis objective is a survey, not a single fix. Spawn 3+ `Explore` subagents NOW — one per subsystem, in a single message so they run in parallel — synthesize their findings, and call `propose_task_list` with the result. Do not start fixing before the task list exists.",
422
+ );
423
+ }
424
+ const dynamicDirectives = directives.length > 0 ? directives.join("\n\n") : "(no active directives)";
374
425
  return tmpl
375
426
  .replace(/\$\{GOAL_ID\}/g, goal.id)
376
427
  .replace(/\$\{OBJECTIVE\}/g, goal.objective)
377
428
  .replace(/\$\{VERIFICATION_CONTRACT\}/g, goal.verificationContract || "(none — auditor will decide based on objective)")
378
429
  .replace(/\$\{TASK_LIST\}/g, taskSummary)
379
- .replace(/\$\{NEXT_PENDING_TASK_BLOCK\}/g, nextBlock);
430
+ .replace(/\$\{NEXT_PENDING_TASK_BLOCK\}/g, nextBlock)
431
+ .replace(/\$\{DYNAMIC_DIRECTIVES\}/g, dynamicDirectives);
380
432
  }
381
433
 
382
434
  // =================================================================
@@ -1201,7 +1253,9 @@ async function runLoopTick(ctx: ExtensionContext, event?: any): Promise<void> {
1201
1253
  }
1202
1254
  // v0.24.0: the top of the stuck ladder — bounded and surfaced, same
1203
1255
  // philosophy as a plateau stop. The loop ends WITH the reason, not in silence.
1204
- if (outcome.kind !== "stop" && (loop.consecutiveStuck ?? 0) >= REPETITION.maxInterventions) {
1256
+ // v0.25.0: aggressiveMode raises the ladder (default 5 10, explicit wins).
1257
+ const maxStuckInterventions = resolveEffectiveAggressiveSettings(loadSettings(ctx.cwd)).stuckMaxInterventions;
1258
+ if (outcome.kind !== "stop" && (loop.consecutiveStuck ?? 0) >= maxStuckInterventions) {
1205
1259
  loop.active = false;
1206
1260
  loop.stopReason = `stuck — ${loop.lastStuckReason} (${loop.consecutiveStuck} consecutive interventions)`;
1207
1261
  persistState(ctx);
@@ -1501,6 +1555,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1501
1555
  parameters: Type.Object({
1502
1556
  completionSummary: Type.Optional(Type.String({ description: "1-paragraph completion claim" })),
1503
1557
  verificationSummary: Type.Optional(Type.String({ description: "Per-item evidence for the verification contract" })),
1558
+ newObjective: Type.Optional(Type.String({ description: "v0.25.0 (contract item 15): when the work has legitimately shifted, pass the new objective here — it atomically replaces the goal objective AND the audit proceeds against the NEW objective in this same call. Do not use to dodge a legitimate disapproval; the auditor sees the change." })),
1504
1559
  }),
1505
1560
  async execute(_id, params, signal, _onUpdate, execCtx) {
1506
1561
  const foreign0 = foreignToolGuard(execCtx);
@@ -1508,8 +1563,19 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1508
1563
  if (!state.goal || state.goal.status !== "active") {
1509
1564
  return { content: [{ type: "text", text: "No active goal." }], details: {} };
1510
1565
  }
1511
- const p = params as { completionSummary?: string; verificationSummary?: string };
1512
- updateGoal({ status: "auditing" }, ctx);
1566
+ const p = params as { completionSummary?: string; verificationSummary?: string; newObjective?: string };
1567
+ // v0.25.0 (contract item 15): atomic objective update + audit in one
1568
+ // call — the objective-drift disapprove loop (ship shifted work →
1569
+ // auditor disapproves the ORIGINAL objective) ends here. Ledgered so
1570
+ // the shift is auditable.
1571
+ if (p.newObjective?.trim()) {
1572
+ const oldObjective = state.goal.objective;
1573
+ const { objective: cleanObj, verificationContract } = extractVerificationContract(p.newObjective.trim());
1574
+ updateGoal({ objective: cleanObj, ...(verificationContract ? { verificationContract } : {}) }, ctx);
1575
+ appendLedger(ctx.cwd, "goal_tweaked", { via: "complete_goal.newObjective", from: oldObjective.slice(0, 200), to: cleanObj.slice(0, 200) });
1576
+ ctx.ui.notify(`Objective updated (complete_goal newObjective): ${cleanObj.slice(0, 80)}`, "info");
1577
+ }
1578
+ updateGoal({ status: "auditing", pendingTasks: undefined }, ctx);
1513
1579
  const settings = loadSettings(ctx.cwd);
1514
1580
  const { model: auditorModel, error: modelError, via } = resolveAuditorModel(ctx, settings.auditorModel);
1515
1581
  if (modelError) {
@@ -1600,6 +1666,29 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1600
1666
  // Bounded and surfaced: the goal pauses and the user decides.
1601
1667
  if (result.impossible) {
1602
1668
  const reason = result.impossibleReason || "(no reason given)";
1669
+ // v0.25.0 (contract item 23): under aggressiveMode, a PARTIAL
1670
+ // impossible (some items can't ship) keeps the loop going — the
1671
+ // agent narrows to the remainder. A FULL impossible still pauses:
1672
+ // auto-resuming a provably unwinnable objective just burns tokens.
1673
+ const effectiveImp = resolveEffectiveAggressiveSettings(loadSettings(ctx.cwd));
1674
+ if (effectiveImp.aggressiveMode && classifyImpossibleReason(reason) === "partial") {
1675
+ updateGoal({
1676
+ status: "active",
1677
+ auditHistory: history,
1678
+ pauseReason: `auditor verdict: IMPOSSIBLE (partial) — ${reason}`,
1679
+ pauseSuggestedAction: "Narrow the objective past the impossible part (complete_goal newObjective or /goal tweak) and continue",
1680
+ }, ctx);
1681
+ ctx.ui.notify(`Auditor: part of the goal is IMPOSSIBLE — ${reason.slice(0, 100)}. aggressiveMode: narrowing and continuing.`, "warning");
1682
+ appendLedger(ctx.cwd, "impossible_partial_continue", { reason: reason.slice(0, 200) });
1683
+ scheduleContinuation(ctx, true);
1684
+ return {
1685
+ content: [{
1686
+ type: "text",
1687
+ text: `The auditor says PART of this goal can never be satisfied: ${reason}\n\naggressiveMode is ON, so the goal stays ACTIVE. Do NOT keep attempting the impossible part. Narrow the objective to the remaining shippable items — pass newObjective to complete_goal at completion time (or pause_goal proposing /goal tweak if the narrowing needs the user's call) — and continue working the rest now.`,
1688
+ }],
1689
+ details: {},
1690
+ };
1691
+ }
1603
1692
  updateGoal({
1604
1693
  status: "paused",
1605
1694
  auditHistory: history,
@@ -1622,6 +1711,42 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1622
1711
  // The wild-caught case: 6 silent "disapprovals" that were really a dead
1623
1712
  // auditor model. The agent must be able to tell the difference.
1624
1713
  if (result.error && !result.disapproved) {
1714
+ // v0.25.0 (contract Section C): quota errors used to re-fire the
1715
+ // continuation FOREVER against a window that resets in an hour.
1716
+ // Now: pause with a one-shot scheduled retry at the upstream's own
1717
+ // Retry-After hint (default quotaRetryMinutes).
1718
+ if (isQuotaError(result.error)) {
1719
+ const settingsNow = loadSettings(ctx.cwd);
1720
+ const defaultSec = (settingsNow.quotaRetryMinutes ?? DEFAULT_QUOTA_RETRY_MINUTES) * 60;
1721
+ const quota = parseQuotaError(result.error, defaultSec);
1722
+ const retryMin = Math.max(1, Math.round(quota.retryAfterSec / 60));
1723
+ updateGoal({
1724
+ status: "paused",
1725
+ auditHistory: history,
1726
+ pauseReason: `auditor quota: ${result.error}`,
1727
+ pauseSuggestedAction: `Quota auto-retry in ${retryMin}m — or /goal resume to retry now`,
1728
+ }, ctx);
1729
+ appendLedger(ctx.cwd, "goal_paused", { reason: `auditor quota: retry in ${quota.retryAfterSec}s (${quota.fromUpstream ? "upstream hint" : "default"})` });
1730
+ scheduleQuotaRetry(ctx, quota.retryAfterSec, result.error, () => {
1731
+ // Re-check: only auto-resume if STILL paused for the quota
1732
+ // reason (a user /goal pause during the window is not stomped).
1733
+ if (state.goal && state.goal.status === "paused" && (state.goal.pauseReason ?? "").startsWith("auditor quota:")) {
1734
+ updateGoal({ status: "active" }, ctx);
1735
+ appendLedger(ctx.cwd, "goal_resumed", { via: "quota-retry" });
1736
+ if (resolveEffectiveAggressiveSettings(loadSettings(ctx.cwd)).aggressiveMode) {
1737
+ ctx.ui.notify("Auto-resume fired (event: auditor quota window elapsed). Continue working.", "info");
1738
+ }
1739
+ scheduleContinuation(ctx, true);
1740
+ }
1741
+ });
1742
+ return {
1743
+ content: [{
1744
+ type: "text",
1745
+ text: `The auditor hit a QUOTA / rate-limit error (infrastructure, NOT a verdict): ${result.error}\nThe goal is PAUSED with an automatic retry scheduled in ${retryMin} minute(s)${quota.fromUpstream ? " (upstream Retry-After hint)" : " (default window — /glla quotaretryminutes=N to change)"}. Your completion claim was not evaluated; do not change your deliverable for this. /goal resume retries immediately.`,
1746
+ }],
1747
+ details: {},
1748
+ };
1749
+ }
1625
1750
  updateGoal({
1626
1751
  status: "active",
1627
1752
  auditHistory: history,
@@ -1668,7 +1793,8 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1668
1793
  // goal the auditor can NEVER approve used to re-continue forever.
1669
1794
  // auditCap consecutive disapprovals → pause + notify, bounded and
1670
1795
  // surfaced like every other stop in this stack.
1671
- const auditCap = settings.auditCap ?? 3;
1796
+ const effectiveCap = resolveEffectiveAggressiveSettings(settings);
1797
+ const auditCap = effectiveCap.auditCap;
1672
1798
  const configuredFeedbackChars = settings.auditFeedbackChars;
1673
1799
  const auditFeedbackChars = Number.isInteger(configuredFeedbackChars) && configuredFeedbackChars! >= 0
1674
1800
  ? configuredFeedbackChars!
@@ -1683,6 +1809,32 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1683
1809
  : `\n\nReport truncated at the configured limit. /goal status shows the full report; change future feedback with /glla auditfeedbackchars=N (0 = full report).`;
1684
1810
  const trailingDisapprovals = countTrailingDisapprovals(history);
1685
1811
  if (auditCap > 0 && trailingDisapprovals >= auditCap) {
1812
+ // v0.25.0 (contract item 22): aggressiveMode turns the cap into a
1813
+ // TODO list and keeps going — the objections become pendingTasks
1814
+ // rendered into every continuation until addressed. OFF preserves
1815
+ // the pause (contract item 24 test 2).
1816
+ if (effectiveCap.aggressiveMode) {
1817
+ const pendingTasks = extractPendingTasks(result.output, 5);
1818
+ updateGoal({
1819
+ status: "active",
1820
+ auditHistory: history,
1821
+ pendingTasks,
1822
+ pauseReason: `auditor disapproved ${trailingDisapprovals}× consecutively (cap ${auditCap}) — aggressiveMode: continuing with TODOs`,
1823
+ }, ctx);
1824
+ const todoBlock = pendingTasks.length > 0
1825
+ ? pendingTasks.map((t, i) => ` ${i + 1}. ${t}`).join("\n")
1826
+ : " (no discrete objections extracted — re-read the latest report in /goal status)";
1827
+ ctx.ui.notify(`Auditor disapproved ${trailingDisapprovals}× (cap). Treating as TODOs:\n${todoBlock}`, "warning");
1828
+ appendLedger(ctx.cwd, "audit_cap_keep_going", { trailingDisapprovals, auditCap, pendingTasks });
1829
+ scheduleContinuation(ctx, true);
1830
+ return {
1831
+ content: [{
1832
+ type: "text",
1833
+ text: `The auditor has disapproved ${trailingDisapprovals} times in a row (cap ${auditCap}), but aggressiveMode is ON — the goal stays ACTIVE and the objections are now your TODO list:\n${todoBlock}\n\nLatest report (${auditFeedbackLabel}):\n${auditFeedback}\n\nWork the TODOs in order. If the auditor is WRONG about an objection, follow WHEN THE AUDITOR DISAPPROVES: investigate, quote its objection, compare against what you shipped, and present the user YOUR ASSESSMENT. If the objective itself has drifted, pass newObjective to complete_goal.`,
1834
+ }],
1835
+ details: {},
1836
+ };
1837
+ }
1686
1838
  updateGoal({
1687
1839
  status: "paused",
1688
1840
  auditHistory: history,
@@ -1695,7 +1847,7 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
1695
1847
  return {
1696
1848
  content: [{
1697
1849
  type: "text",
1698
- text: `The auditor has now disapproved ${trailingDisapprovals} times in a row (cap ${auditCap}). The goal is PAUSED — continuing to re-attempt without addressing the pattern wastes tokens. Latest report (${auditFeedbackLabel}):\n${auditFeedback}\n\nDo not call complete_goal again. Summarize the repeated objections for the user and ask how to proceed (/goal status shows all reports; /goal resume resumes).`,
1850
+ text: `The auditor has now disapproved ${trailingDisapprovals} times in a row (cap ${auditCap}). The goal is PAUSED — continuing to re-attempt without addressing the pattern wastes tokens.\n\nBefore asking the user, INVESTIGATE:\n1. Read the audit history (the auditor's previous reports — /goal status shows them; state.goal.auditHistory holds them).\n2. Identify the SPECIFIC objections — quote them.\n3. Compare against what you actually shipped (commits, diffs, test output, screenshots).\n4. Form a clear opinion: is the auditor right, wrong, or partially right?\n5. Present the user YOUR ASSESSMENT with quoted objections and shipped evidence — not a generic menu of options.\n\nLatest report (${auditFeedbackLabel}):\n${auditFeedback}\n\nDo not call complete_goal again until the pattern is addressed. /goal resume resumes; /goal tweak fixes a drifted objective.`,
1699
1851
  }],
1700
1852
  details: {},
1701
1853
  };
@@ -2264,111 +2416,6 @@ function registerAgentTools(pi: any, ctx: ExtensionContext): void {
2264
2416
  // Settings (auditor model, thinking level)
2265
2417
  // =================================================================
2266
2418
 
2267
- interface Settings {
2268
- /** "provider/model-id" or bare "model-id". Unset → session model. */
2269
- auditorModel?: string;
2270
- auditorThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
2271
- /** Shell command run on goal complete / goal pause / loop stop; message passed as $1. */
2272
- notifyCmd?: string;
2273
- /** Per-goal token budget; crossing it pauses the goal. Off by default
2274
- * (opt-in guard, v0.12.0): unset/0 = no budget. */
2275
- tokenLimit?: number;
2276
- /** v0.23.2: minutes of busy-but-silent before the wedge alert fires
2277
- * (hung-command detector). Unset = 45; 0 = off. */
2278
- wedgeAlertMinutes?: number;
2279
- /** on → restored goals/loops/lists auto-resume even in fresh sessions
2280
- * (unattended rigs). Default off: restore holds until /goal resume. */
2281
- autoResume?: boolean;
2282
- /** v0.24.2: pause the goal after N consecutive auditor disapprovals (0 = unlimited). Default 3. */
2283
- auditCap?: number;
2284
- /** Maximum auditor-report characters returned to the executor after a
2285
- * disapproval (0 = full report). Default 800. */
2286
- auditFeedbackChars?: number;
2287
- /** on → propose_* drafts activate WITHOUT the Confirm dialog and the
2288
- * interview floor is skipped — the seed carries the intent (unattended
2289
- * rigs). Default off: nothing activates before the user confirms. */
2290
- autoAcceptDrafts?: boolean;
2291
- /** v0.24.6: subagent model strategy for pi-subagents default agents that
2292
- * pin a model (Explore pins claude-haiku-4-5, which silently routes
2293
- * subagents to a different provider/quota pool than the session).
2294
- * "inherit-parent" (default) writes a managed ~/.pi/agent/agents/Explore.md
2295
- * override without the model pin so subagents share the session model and
2296
- * its quota; "agent-default" restores upstream behavior. Applies to NEW
2297
- * sessions (pi-subagents registers agents at session start). */
2298
- subagentModelStrategy?: SubagentModelStrategy;
2299
- /** v0.24.6: per-agent-type model pin, e.g. { "Explore": "minimax/MiniMax-M3" }.
2300
- * Always wins over subagentModelStrategy — the managed override is written
2301
- * WITH this pin regardless of strategy. */
2302
- subagentModelOverrides?: Record<string, string>;
2303
- }
2304
-
2305
- const DEFAULT_SETTINGS: Settings = {
2306
- // Unset = follow the pi session thinking level (user selects thinking in
2307
- // pi, auditor follows), floor "high" — the auditor is the verification
2308
- // gate, depth is worth more there than speed. /glla thinking= overrides.
2309
- auditorThinkingLevel: undefined,
2310
- // v0.24.6: subagents inherit the session model by default — one quota
2311
- // pool, no surprise 403s from a pinned default agent's provider.
2312
- subagentModelStrategy: "inherit-parent",
2313
- auditFeedbackChars: DEFAULT_AUDIT_FEEDBACK_CHARS,
2314
- };
2315
-
2316
- // Two-tier config (v0.7.0): GLOBAL is the normal home — you set things once
2317
- // and rarely open this again. PROJECT is the rare local override.
2318
- // Resolution: project > global > defaults (per key).
2319
- function globalSettingsPath(): string {
2320
- return path.join(os.homedir(), ".pi", "agent", "pi-goal-list-loop-audit.settings.json");
2321
- }
2322
-
2323
- function projectSettingsPath(cwd: string): string {
2324
- return path.join(piGlaDir(cwd), "settings.json");
2325
- }
2326
-
2327
- function readSettingsFile(file: string): Partial<Settings> {
2328
- try {
2329
- if (!fs.existsSync(file)) return {};
2330
- const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
2331
- return typeof parsed === "object" && parsed !== null ? parsed as Partial<Settings> : {};
2332
- } catch {
2333
- return {};
2334
- }
2335
- }
2336
-
2337
- function loadSettings(cwd: string): Settings {
2338
- return mergeSettings(
2339
- DEFAULT_SETTINGS as unknown as Record<string, unknown>,
2340
- readSettingsFile(globalSettingsPath()) as Record<string, unknown>,
2341
- readSettingsFile(projectSettingsPath(cwd)) as Record<string, unknown>,
2342
- ) as unknown as Settings;
2343
- }
2344
-
2345
- /** Where each effective setting comes from (for the /glla display). */
2346
- function settingsProvenance(cwd: string): Record<keyof Settings, { value: unknown; source: "project" | "global" | "default" }> {
2347
- const proj = readSettingsFile(projectSettingsPath(cwd));
2348
- const glob = readSettingsFile(globalSettingsPath());
2349
- const effective = loadSettings(cwd);
2350
- const out: Record<string, { value: unknown; source: "project" | "global" | "default" }> = {};
2351
- const keys: Array<keyof Settings> = ["auditorModel", "auditorThinkingLevel", "notifyCmd", "tokenLimit", "wedgeAlertMinutes", "autoResume", "autoAcceptDrafts", "auditCap", "auditFeedbackChars", "subagentModelStrategy", "subagentModelOverrides"];
2352
- for (const k of keys) {
2353
- if ((proj as Record<string, unknown>)[k] !== undefined) out[k] = { value: (proj as any)[k], source: "project" };
2354
- else if ((glob as Record<string, unknown>)[k] !== undefined) out[k] = { value: (glob as any)[k], source: "global" };
2355
- else out[k] = { value: (effective as any)[k], source: "default" };
2356
- }
2357
- return out as Record<keyof Settings, { value: unknown; source: "project" | "global" | "default" }>;
2358
- }
2359
-
2360
- function saveSettings(scope: "global" | "project", cwd: string, patch: Partial<Settings>): void {
2361
- const file = scope === "global" ? globalSettingsPath() : projectSettingsPath(cwd);
2362
- const current = readSettingsFile(file);
2363
- const next: Record<string, unknown> = { ...current };
2364
- for (const [k, v] of Object.entries(patch)) {
2365
- if (v === undefined) delete next[k]; // key=unset removes the key
2366
- else next[k] = v;
2367
- }
2368
- fs.mkdirSync(path.dirname(file), { recursive: true });
2369
- fs.writeFileSync(file, JSON.stringify(next, null, 2));
2370
- }
2371
-
2372
2419
  /**
2373
2420
  * Session thinking level with a "high" floor (v0.8.5): the auditor follows
2374
2421
  * the thinking level the user selected in pi; if none is set, audits run at
@@ -2448,9 +2495,12 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2448
2495
  `Notify command — ${show("notifyCmd", "(off)")}`,
2449
2496
  `Token limit per goal — ${show("tokenLimit", "(off)")}`,
2450
2497
  `Wedge alert minutes — ${show("wedgeAlertMinutes", `(${WEDGE_ALERT_DEFAULT_MINUTES}m default)`)}`,
2498
+ `Aggressive mode — ${show("aggressiveMode", "(off)")}`,
2499
+ `Quota retry minutes — ${show("quotaRetryMinutes", `(${DEFAULT_QUOTA_RETRY_MINUTES}m default)`)}`,
2500
+ `Stuck max interventions — ${show("stuckMaxInterventions", "(5 default)")}`,
2451
2501
  `Subagent model strategy — ${show("subagentModelStrategy", "(inherit-parent)")}`,
2452
2502
  `Subagent Explore model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.Explore ?? "(follows strategy)"}`,
2453
- `Audit feedback characters — ${show("auditFeedbackChars", `(${DEFAULT_AUDIT_FEEDBACK_CHARS} default)`)}`,
2503
+ `Audit feedback characters — ${show("auditFeedbackChars", "(full report)")}`,
2454
2504
  "Done",
2455
2505
  ],
2456
2506
  );
@@ -2484,6 +2534,31 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2484
2534
  else if (!v.trim()) saveSettings("global", ctx.cwd, { wedgeAlertMinutes: undefined });
2485
2535
  else ctx.ui.notify(`Not a non-negative integer: ${v}`, "warning");
2486
2536
  }
2537
+ } else if (choice.startsWith("Aggressive mode")) {
2538
+ const v = await ctx.ui.select("Aggressive mode (flips DEFAULTS toward keep-going — explicit per-key settings still win)", [
2539
+ "off — current behavior: pause at the audit cap, wedge alerts on, manual resume",
2540
+ "on — autoResume, audit cap 10, stuck max 10, wedge alerts off, quota auto-retry, cap disapprovals become a TODO list and the goal KEEPS GOING",
2541
+ ]);
2542
+ if (v) {
2543
+ saveSettings("global", ctx.cwd, { aggressiveMode: v.startsWith("on") });
2544
+ ctx.ui.notify(`Aggressive mode ${v.startsWith("on") ? "ON — goals keep going past the audit cap; objections become TODOs" : "off"}.`, "info");
2545
+ }
2546
+ } else if (choice.startsWith("Quota retry minutes")) {
2547
+ const v = await ctx.ui.input("Minutes before auto-retrying a quota-exhausted auditor", `positive integer; empty = default ${DEFAULT_QUOTA_RETRY_MINUTES}`);
2548
+ if (v !== undefined) {
2549
+ const n = Number.parseInt(v.trim(), 10);
2550
+ if (Number.isFinite(n) && n > 0) saveSettings("global", ctx.cwd, { quotaRetryMinutes: n });
2551
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { quotaRetryMinutes: undefined });
2552
+ else ctx.ui.notify(`Not a positive integer: ${v}`, "warning");
2553
+ }
2554
+ } else if (choice.startsWith("Stuck max interventions")) {
2555
+ const v = await ctx.ui.input("Consecutive stuck interventions before a loop stops", "positive integer; empty = default 5 (10 under aggressiveMode)");
2556
+ if (v !== undefined) {
2557
+ const n = Number.parseInt(v.trim(), 10);
2558
+ if (Number.isFinite(n) && n > 0) saveSettings("global", ctx.cwd, { stuckMaxInterventions: n });
2559
+ else if (!v.trim()) saveSettings("global", ctx.cwd, { stuckMaxInterventions: undefined });
2560
+ else ctx.ui.notify(`Not a positive integer: ${v}`, "warning");
2561
+ }
2487
2562
  } else if (choice.startsWith("Subagent model strategy")) {
2488
2563
  const v = await ctx.ui.select("Subagent model (pi-subagents default agents)", [
2489
2564
  "inherit-parent — subagents share your session model + its quota pool (fixes separate-provider 403s; search agents may run on a pricier model)",
@@ -2505,7 +2580,7 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
2505
2580
  ctx.ui.notify("Explore model pin saved — applies to NEW pi sessions.", "info");
2506
2581
  }
2507
2582
  } else if (choice.startsWith("Audit feedback")) {
2508
- const v = await ctx.ui.input("Auditor feedback returned to the executor (characters)", `non-negative integer; 0 = full report, empty = default ${DEFAULT_AUDIT_FEEDBACK_CHARS}`);
2583
+ const v = await ctx.ui.input("Auditor feedback returned to the executor (characters)", "non-negative integer cap; 0 or empty = full report (default)");
2509
2584
  if (v !== undefined) {
2510
2585
  const raw = v.trim();
2511
2586
  const n = Number(raw);
@@ -2528,7 +2603,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2528
2603
  // /glla notify='cmd $1' write to GLOBAL config
2529
2604
  // /glla tokenlimit=2000000 write to GLOBAL config
2530
2605
  // /glla wedgealert=30 hung-command alert minutes (0=off, unset=30)
2531
- // /glla auditfeedbackchars=800 executor-visible disapproval report chars (0=full)
2606
+ // /glla auditfeedbackchars=800 cap executor-visible disapproval report (0=full, the default)
2532
2607
  // /glla project model=... write to PROJECT override (rare)
2533
2608
  // /glla model=unset remove key (from global; project model=unset for project)
2534
2609
  const trimmed = args.trim();
@@ -2554,6 +2629,9 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2554
2629
  fmt("autoAcceptDrafts", "autoAccept"),
2555
2630
  fmt("auditCap", "auditCap"),
2556
2631
  fmt("auditFeedbackChars", "auditFeedbackChars"),
2632
+ fmt("aggressiveMode", "aggressiveMode"),
2633
+ fmt("quotaRetryMinutes", "quotaRetryMinutes"),
2634
+ fmt("stuckMaxInterventions", "stuckMaxInterventions"),
2557
2635
  `\nglobal: ${globalSettingsPath()}`,
2558
2636
  `project: ${projectSettingsPath(ctx.cwd)}`,
2559
2637
  `Set with: /glla key=value (global) · /glla project key=value (project override)`,
@@ -2652,6 +2730,43 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2652
2730
  ctx.ui.notify(`auditfeedbackchars must be a non-negative integer (0 = full report), got: ${value}`, "warning");
2653
2731
  }
2654
2732
  }
2733
+ } else if (key === "aggressivemode" || key === "aggressive") {
2734
+ if (["on", "true", "1", "yes"].includes(value)) {
2735
+ patch.aggressiveMode = true;
2736
+ changed = true;
2737
+ ctx.ui.notify("aggressivemode=on: autoResume, audit cap 10, stuck max 10, wedge off, quota auto-retry — and audit-cap disapprovals become TODOs while the goal KEEPS GOING. Explicit per-key settings still win.", "warning");
2738
+ } else if (["off", "false", "0", "no", "unset"].includes(value)) {
2739
+ patch.aggressiveMode = undefined;
2740
+ changed = true;
2741
+ } else {
2742
+ ctx.ui.notify(`aggressivemode must be on or off, got: ${value}`, "warning");
2743
+ }
2744
+ } else if (key === "quotaretryminutes") {
2745
+ if (["unset", "default"].includes(value)) {
2746
+ patch.quotaRetryMinutes = undefined;
2747
+ changed = true;
2748
+ } else {
2749
+ const n = Number.parseInt(value, 10);
2750
+ if (Number.isInteger(n) && n > 0) {
2751
+ patch.quotaRetryMinutes = n;
2752
+ changed = true;
2753
+ } else {
2754
+ ctx.ui.notify(`quotaretryminutes must be a positive integer, got: ${value}`, "warning");
2755
+ }
2756
+ }
2757
+ } else if (key === "stuckmax" || key === "stuckmaxinterventions") {
2758
+ if (["unset", "default"].includes(value)) {
2759
+ patch.stuckMaxInterventions = undefined;
2760
+ changed = true;
2761
+ } else {
2762
+ const n = Number.parseInt(value, 10);
2763
+ if (Number.isInteger(n) && n > 0) {
2764
+ patch.stuckMaxInterventions = n;
2765
+ changed = true;
2766
+ } else {
2767
+ ctx.ui.notify(`stuckmax must be a positive integer, got: ${value}`, "warning");
2768
+ }
2769
+ }
2655
2770
  } else if (key === "thinking" || key === "auditorthinkinglevel") {
2656
2771
  if (["off", "minimal", "low", "medium", "high", "xhigh"].includes(value)) {
2657
2772
  patch.auditorThinkingLevel = value as Settings["auditorThinkingLevel"];
@@ -2662,7 +2777,7 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
2662
2777
  }
2663
2778
  }
2664
2779
  if (!changed) {
2665
- ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit, autoresume, auditcap, auditfeedbackchars), optionally prefixed with 'project'.", "info");
2780
+ ctx.ui.notify("Nothing changed. Use key=value (model, thinking, notify, tokenlimit, autoresume, auditcap, auditfeedbackchars, aggressivemode, quotaretryminutes, stuckmax), optionally prefixed with 'project'.", "info");
2666
2781
  return;
2667
2782
  }
2668
2783
  saveSettings(scope, ctx.cwd, patch);
@@ -2783,8 +2898,11 @@ export default function (pi: ExtensionAPI): void {
2783
2898
  ["notify=", "desktop push command: /glla notify='notify-send pi \"$1\"'"],
2784
2899
  ["tokenlimit=", "per-goal token budget (0 = off): /glla tokenlimit=2000000"],
2785
2900
  ["autoresume=", "on: auto-resume held goals/loops in fresh sessions"],
2786
- ["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 3, 0 = unlimited)"],
2787
- ["auditfeedbackchars=", `executor-visible disapproval report characters (default ${DEFAULT_AUDIT_FEEDBACK_CHARS}, 0 = full report)`],
2901
+ ["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)"],
2902
+ ["auditfeedbackchars=", "cap on executor-visible disapproval report chars (0 = full report, the default)"],
2903
+ ["aggressivemode=", "on: keep-going defaults — autoResume, cap 10, stuck 10, wedge off, quota auto-retry, cap→TODOs"],
2904
+ ["quotaretryminutes=", "N: minutes before auto-retrying a quota-exhausted auditor (default 60)"],
2905
+ ["stuckmax=", "N: consecutive stuck interventions before a loop stops (default 5)"],
2788
2906
  ["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
2789
2907
  ["project", "write a project override: /glla project key=value"],
2790
2908
  ]),
@@ -2910,7 +3028,15 @@ export default function (pi: ExtensionAPI): void {
2910
3028
  // starts working before you can even load your session. Sessions with
2911
3029
  // history ("resume"/"reload"/"fork") auto-resume; /glla autoresume=on
2912
3030
  // opts a project into auto-resume everywhere (unattended rigs).
2913
- const autoResume = shouldAutoResumeOnSessionStart(event?.reason, loadSettings(ctx.cwd).autoResume);
3031
+ const autoResume = shouldAutoResumeOnSessionStart(event?.reason, resolveEffectiveAggressiveSettings(loadSettings(ctx.cwd)).autoResume);
3032
+ // v0.25.0 (contract item 6): aggressiveMode announces every auto-event.
3033
+ if (
3034
+ autoResume &&
3035
+ resolveEffectiveAggressiveSettings(loadSettings(ctx.cwd)).aggressiveMode &&
3036
+ (isLoopActive() || (state.goal && state.goal.status === "active") || listQueue().length > 0)
3037
+ ) {
3038
+ ctx.ui.notify("Auto-resume fired (event: session start). Continue working.", "info");
3039
+ }
2914
3040
  if (isLoopActive()) {
2915
3041
  const l = state.loop!;
2916
3042
  if (autoResume) {
@@ -0,0 +1,90 @@
1
+ // pi-goal-list-loop-audit — v0.25.0
2
+ // extensions/quota-retry.ts
3
+ //
4
+ // Quota-aware retry (eager-continuation contract, Section C). Before this
5
+ // module, an auditor that failed with a 429 / quota error re-fired the
6
+ // continuation loop FOREVER (the error branch just rescheduled), burning
7
+ // tokens against a quota window that only resets in an hour. Now a quota
8
+ // error pauses the goal with a scheduled one-shot retry at the upstream's
9
+ // own Retry-After hint (default 60m, configurable via quotaRetryMinutes).
10
+
11
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+
13
+ export interface QuotaError {
14
+ raw: string;
15
+ /** Seconds until retry, from the upstream hint or the default. */
16
+ retryAfterSec: number;
17
+ /** True when retryAfterSec came from the upstream (Retry-After /
18
+ * "retry in Ns"), false when the default was used. */
19
+ fromUpstream: boolean;
20
+ }
21
+
22
+ /** Match 429, "quota", "rate limit", "temporarily rate-limited upstream",
23
+ * credit exhaustion — the shapes we have caught in the wild (OpenRouter,
24
+ * MiniMax, Anthropic). */
25
+ export function isQuotaError(error: string | undefined): boolean {
26
+ if (!error) return false;
27
+ return /429|quota|rate.?limit|temporarily|credits?|key limit exceeded|insufficient.?balance|too many requests/i.test(error);
28
+ }
29
+
30
+ /** Parse the retry window out of an error string. Understands:
31
+ * - `Retry-After: 5` (header echoed into the error text)
32
+ * - `retry after 30 seconds` / `retry in 2m` prose
33
+ * - default 3600s when no hint (contract item 11). */
34
+ export function parseQuotaError(error: string, defaultRetryAfterSec = 3600): QuotaError {
35
+ let m = error.match(/retry-after:\s*(\d+)/i);
36
+ if (m) {
37
+ const sec = Number(m[1]);
38
+ if (Number.isFinite(sec) && sec >= 0) return { raw: error, retryAfterSec: sec, fromUpstream: true };
39
+ }
40
+ m = error.match(/retry (?:after|in)\s+(\d+)\s*(s|sec|seconds|m|min|minutes|h|hours?)/i);
41
+ if (m) {
42
+ const n = Number(m[1]);
43
+ const unit = m[2]!.toLowerCase();
44
+ const mult = unit.startsWith("h") ? 3600 : unit.startsWith("m") ? 60 : 1;
45
+ if (Number.isFinite(n) && n >= 0) return { raw: error, retryAfterSec: n * mult, fromUpstream: true };
46
+ }
47
+ return { raw: error, retryAfterSec: defaultRetryAfterSec, fromUpstream: false };
48
+ }
49
+
50
+ let quotaRetryTimer: NodeJS.Timeout | null = null;
51
+
52
+ /** Test hook — is a quota retry currently scheduled? */
53
+ export function isQuotaRetryPending(): boolean {
54
+ return quotaRetryTimer !== null;
55
+ }
56
+
57
+ /** Cancel any pending quota retry (e.g. the user resumed manually). */
58
+ export function cancelQuotaRetry(): void {
59
+ if (quotaRetryTimer) {
60
+ clearTimeout(quotaRetryTimer);
61
+ quotaRetryTimer = null;
62
+ }
63
+ }
64
+
65
+ /** Schedule a one-shot auto-resume after the quota window. The fire
66
+ * callback re-checks the goal is STILL paused for the quota reason before
67
+ * resuming (contract item 10/12 — a user /goal pause during the window
68
+ * must not be stomped). */
69
+ export function scheduleQuotaRetry(
70
+ ctx: ExtensionContext,
71
+ retryAfterSec: number,
72
+ reason: string,
73
+ fire: () => void,
74
+ ): void {
75
+ cancelQuotaRetry();
76
+ const ms = Math.max(1_000, retryAfterSec * 1_000);
77
+ quotaRetryTimer = setTimeout(() => {
78
+ quotaRetryTimer = null;
79
+ try {
80
+ fire();
81
+ } catch {
82
+ /* session may be gone; session_start will re-evaluate */
83
+ }
84
+ }, ms);
85
+ quotaRetryTimer.unref?.();
86
+ ctx.ui.notify(
87
+ `Auditor quota exhausted — auto-retry in ${Math.round(retryAfterSec / 60)}m (${reason.slice(0, 80)}). /goal resume retries now.`,
88
+ "info",
89
+ );
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goal-list-loop-audit",
3
- "version": "0.24.8",
3
+ "version": "0.25.0",
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",
@@ -37,9 +37,11 @@ ${TASK_LIST}
37
37
 
38
38
  ${NEXT_PENDING_TASK_BLOCK}
39
39
 
40
+ ${DYNAMIC_DIRECTIVES}
41
+
40
42
  ## Available tools
41
43
 
42
- You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, and the goal toolkit (`propose_task_list`, `complete_task`, `update_task_status`, `pause_goal`, `complete_goal`), plus the list tools (`list_add`, `list_status`, `list_activate`) — when the user asks to queue more work ("add these to my list", "queue these 10 things"), call `list_add` with the items; when unsure what is running or waiting, call `list_status`.
44
+ You have `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`, the `Agent` subagent tool, and the goal toolkit (`propose_task_list`, `complete_task`, `update_task_status`, `pause_goal`, `complete_goal`), plus the list tools (`list_add`, `list_status`, `list_activate`) — when the user asks to queue more work ("add these to my list", "queue these 10 things"), call `list_add` with the items; when unsure what is running or waiting, call `list_status`.
43
45
 
44
46
  If the objective decomposes into milestones and no task list exists yet, call `propose_task_list` early — the user confirms it, then you track progress with `complete_task` / `update_task_status` as you go (not in a batch at the end). Limits: 20 tasks, 5 subtasks per task.
45
47
 
@@ -47,9 +49,61 @@ When the agent calls any of these, the orchestrator tracks the call and persists
47
49
 
48
50
  ## EXECUTION DISCIPLINE
49
51
 
50
- - **Delegate independent chunks to subagents.** When the work contains parallel, independent streams — surveying separate subsystems, reading disjoint file sets, running independent verifications — spawn `Agent` subagents (`Explore` for read-only research, `general-purpose` for implementation) instead of serializing everything through your own context. You remain the single writer: synthesize subagent findings and apply edits yourself.
52
+ - **Default to subagents.** For any task that decomposes into independent chunks, spawn `Agent` subagents. Use `Explore` for read-only research, `general-purpose` for implementation, `Plan` for architecture. Spawn multiple in PARALLEL — don't serialise through your own context. You remain the single writer: synthesize findings and apply edits yourself.
53
+ - **Eager continuation.** When in doubt, KEEP GOING on sub-tasks. If a subagent fails, retry with a different approach. Don't ask permission to continue — just continue. Pause only when you are genuinely blocked on information that does not exist in the repo, or the user explicitly pauses you.
51
54
  - **Bound every long command.** Wrap test suites, builds, and dev servers in `timeout <seconds>` (e.g. `timeout 120 bun test src/lib`). An unbounded command that hangs burns an hour; a bounded one burns two minutes and tells you it hung. If a command produces no output for many minutes, treat it as hung: kill it, diagnose why, rerun bounded.
52
55
 
56
+ ## WHEN THE AUDITOR DISAPPROVES
57
+
58
+ If the orchestrator tells you the auditor disapproved, **investigate before asking the user**:
59
+
60
+ 1. Read the audit history (the latest reports via `/goal status`, or `state.goal.auditHistory` directly).
61
+ 2. For each disapproval, identify the SPECIFIC objections the auditor raised — quote them.
62
+ 3. Compare against what you actually shipped (commits, file diffs, test output, screenshots).
63
+ 4. Form a clear opinion: is the auditor right, wrong, or partially right?
64
+ 5. Present the user with YOUR ASSESSMENT, not a generic menu of options. Example format:
65
+
66
+ "The auditor's last 3 reports all complain about saves-3 not shipping. The current objective IS saves-3, but the work shipped is menu-3 + kingdom-2 (different items). I shipped those because [reason]. The auditor is disapproving because the original objective isn't literally shipped. Three options: A. /goal tweak the objective to menu-3+kingdom-2, then /goal resume — B. Re-scope saves-3 and ship it — C. Pivot to a different item entirely."
67
+
68
+ Do NOT ask the user to choose between generic options like "/goal resume / Move on silently / Different item". Those options tell the user nothing. Always include YOUR ASSESSMENT with quoted objections and shipped evidence.
69
+
70
+ ## PIVOT DETECTION
71
+
72
+ When the user says "do a full audit", "survey the project", "find all problems", "mark a tasklist", or similar — the goal is a SURVEY, not a single fix. You must:
73
+
74
+ 1. Call `propose_task_list` IMMEDIATELY with the structured task list of items you find.
75
+ 2. Each task should be SHORT (minutes, not hours).
76
+ 3. Use subagents to PARALLEL-survey different subsystems (game logic, UI, audio, tests, docs) — one `Explore` agent per subsystem, spawned in a single message.
77
+ 4. Don't ship a single bug fix and then ask if the user wants to continue — the user already said "do a full audit".
78
+ 5. After the task list is confirmed, work through tasks systematically with `complete_task` / `update_task_status`.
79
+
80
+ ## WHEN SUBAGENTS HIT QUOTA ERRORS
81
+
82
+ If a subagent fails with `Key limit exceeded (total limit)`, `429 Too Many Requests`, or another rate-limit error, the parent model and subagent model have DIFFERENT quota pools. Two fixes:
83
+
84
+ 1. **Switch subagent model strategy to `inherit-parent`** (`/glla` → Settings → Subagent model strategy). The subagent then shares your session model and its quota pool.
85
+ 2. **Wait for the upstream quota to reset.** OpenRouter free keys typically reset every 24h. Check the error message for the specific key URL.
86
+
87
+ Do NOT spawn more subagents of the failed type until quota resets — you will just pile up more failed calls. Do the work inline meanwhile, or spawn a different agent type.
88
+
89
+ ## DETACHED COMMIT DETECTION
90
+
91
+ If your commits keep getting rewritten away (same content, new SHA — or worse, content reverted), check the auto-committer daemon BEFORE diagnosing yourself as stuck or broken:
92
+
93
+ ```bash
94
+ git reflog --date=iso | grep -E "filter-branch|filter-repo"
95
+ git reflog --date=iso | grep -E ": reset:"
96
+ ps -fea | grep -E "dracon-sync|filter-repo" | grep -v grep
97
+ ```
98
+
99
+ The auto-committer (`dracon-sync daemon`) may be running `auto_rewrite_large_blobs` on your commits. The fix is NOT to keep re-committing — it is to:
100
+
101
+ 1. Pause the daemon (`dracon-sync pause`, or write the `.pi-glla/.pause-auto-commit` sentinel via the goal tools).
102
+ 2. Investigate the rewrite trigger (daemon config `max_push_blob_bytes`, `auto_rewrite_large_blobs`).
103
+ 3. Add `.pi-glla/` to the daemon's exclude list.
104
+
105
+ Do NOT conclude "the loop is too eager" or "I am broken" before checking what the daemon is doing — the daemon rewriting your commits makes YOUR loop look broken when it is not.
106
+
53
107
  ## TASK WORKFLOW
54
108
 
55
109
  Use tasks as PROGRESS TRACKERS during your work — not as a post-hoc checklist to batch-mark at the end.
@@ -70,6 +124,8 @@ verificationSummary: "Concrete evidence per item (file path, test result, comman
70
124
 
71
125
  Then call `complete_goal`. The orchestrator will spawn an **isolated auditor** in a fresh session to verify, and either accept (mark goal complete) or reject (continue work).
72
126
 
127
+ If your work has shifted to items different from the original objective (the original was blocked, higher-ROI items emerged): pass `newObjective` to `complete_goal` to atomically update the objective and audit against the NEW one — do NOT call `complete_goal` on the original objective after shipping different work, the auditor will disapprove because the original isn't shipped. Alternatively `pause_goal` proposing a `/goal tweak` if the shift needs the user's call.
128
+
73
129
  When the goal is genuinely blocked and you cannot make progress without user input:
74
130
 
75
131
  ```
@@ -78,7 +134,7 @@ pause_goal({reason: "...", suggestedAction: "..."})
78
134
 
79
135
  ## HARD RULES
80
136
 
81
- - **Do not modify the objective autonomously.** The objective is the user's; if it has drifted from what makes sense, call `pause_goal` and propose a `/goal tweak` instead.
137
+ - **Do not modify the objective silently.** The objective is the user's; if it has drifted from what makes sense, use `complete_goal`'s `newObjective` at completion time, or `pause_goal` and propose a `/goal tweak` mid-flight — never just work on something else and claim the original.
82
138
  - **Do not pretend completion.** If verification evidence is missing, call `pause_goal` instead of `complete_goal`.
83
139
  - **Do not polish doorknobs.** If you are out of work and the goal is satisfied, call `complete_goal` instead of inventing a side-improvement.
84
140
  - **Do not give up early.** If a task is hard, run it down properly. The auditor will catch doorknobs; the agent's job is to do the real work.
@@ -14,6 +14,11 @@ request into a **confirmed goal contract**. Do NOT start substantive work yet.
14
14
  fine otherwise and for free-form answers.
15
15
  2. Targeted read-only research is allowed when it helps define a better
16
16
  contract (read a file, check the repo layout). Do NOT implement anything.
17
+ **Default to subagents for research**: spawn an `Explore` `Agent` subagent
18
+ (in parallel with your own reading when there are several areas) rather
19
+ than paging large files through the drafting context yourself. Eager
20
+ continuation applies here too — if a subagent fails, just continue with
21
+ another approach or a `general-purpose` agent; don't stall the draft.
17
22
  3. The contract needs, at minimum:
18
23
  - **objective** — what to do, concretely.
19
24
  - **verification contract** — how an independent auditor can tell it is
@@ -24,6 +24,12 @@ Start your reply with exactly one line: `HYPOTHESIS: <what you will change and w
24
24
  Then make **ONE** concrete, inspectable change that advances the target.
25
25
  Then stop.
26
26
 
27
+ **Default to subagents.** If identifying the change needs research, spawn an
28
+ `Explore` `Agent` subagent (several in parallel for disjoint areas); if the
29
+ change decomposes, use `general-purpose`. Eager continuation: if a subagent
30
+ fails, retry with a different approach — just continue, don't stall the loop
31
+ asking permission. You remain the single writer: apply the edit yourself.
32
+
27
33
  ${INTERVENTION_NOTE}
28
34
  ${REGRESSION_NOTE}
29
35
  ${STRATEGY_NOTE}
@@ -28,6 +28,12 @@ Start your reply with exactly one line: `HYPOTHESIS: <what you will change and w
28
28
  Then make **ONE** small, concrete change that moves the metric in the right
29
29
  direction. Then stop.
30
30
 
31
+ **Default to subagents.** If identifying the change needs research, spawn an
32
+ `Explore` `Agent` subagent (several in parallel for disjoint areas); if the
33
+ change decomposes, use `general-purpose`. Eager continuation: if a subagent
34
+ fails, retry with a different approach — just continue, don't stall the loop
35
+ asking permission. You remain the single writer: apply the edit yourself.
36
+
31
37
  ${INTERVENTION_NOTE}
32
38
  ${REGRESSION_NOTE}
33
39
  ${STRATEGY_NOTE}