pi-goal-list-loop-audit 0.24.9 → 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 +4 -1
- package/extensions/goal-loop-auditor.ts +1 -0
- package/extensions/goal-loop-core.ts +157 -0
- package/extensions/goal-settings.ts +153 -0
- package/extensions/loops/goal.ts +241 -115
- package/extensions/quota-retry.ts +90 -0
- package/package.json +1 -1
- package/prompts/goal-loop-continuation.md +59 -3
- package/prompts/goal-loop-draft.md +5 -0
- package/prompts/goal-loop-forever-metricless.md +6 -0
- package/prompts/goal-loop-forever.md +6 -0
package/README.md
CHANGED
|
@@ -200,7 +200,10 @@ 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
|
|
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)
|
|
204
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
|
|
@@ -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: {
|
|
@@ -761,3 +766,155 @@ export function missingGllaTools(activeNames: readonly string[]): readonly GllaT
|
|
|
761
766
|
const active = new Set(activeNames);
|
|
762
767
|
return GLLA_TOOL_NAMES.filter((n) => !active.has(n));
|
|
763
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
|
+
}
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 0 (full report). */
|
|
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,6 +2495,9 @@ 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
2503
|
`Audit feedback characters — ${show("auditFeedbackChars", "(full report)")}`,
|
|
@@ -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)",
|
|
@@ -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
|
|
2901
|
+
["auditcap=", "N: pause goal after N consecutive auditor disapprovals (default 5, 0 = unlimited)"],
|
|
2787
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.
|
|
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
|
-
- **
|
|
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
|
|
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}
|