pi-goal-list-loop-audit 0.25.5 → 0.26.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/extensions/goal-loop-subagents.ts +120 -13
- package/extensions/goal-settings.ts +3 -0
- package/extensions/loops/goal.ts +186 -6
- package/extensions/quota-retry.ts +10 -0
- package/extensions/reviewer.ts +276 -0
- package/package.json +1 -1
|
@@ -82,19 +82,81 @@ export const EXPLORE_DEFAULT_TOOLS = "read, bash, grep, find, ls";
|
|
|
82
82
|
interface EmbeddedAgentDefault {
|
|
83
83
|
description: string;
|
|
84
84
|
systemPrompt: string;
|
|
85
|
+
/** "" means "all tools" (upstream omits builtinToolNames). */
|
|
85
86
|
tools: string;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
export const PLAN_DEFAULT_DESCRIPTION = "Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.";
|
|
90
|
+
|
|
91
|
+
export const PLAN_DEFAULT_SYSTEM_PROMPT = `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
92
|
+
You are a software architect and planning specialist.
|
|
93
|
+
Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
|
|
94
|
+
You do NOT have access to file editing tools — attempting to edit files will fail.
|
|
95
|
+
|
|
96
|
+
You are STRICTLY PROHIBITED from:
|
|
97
|
+
- Creating new files
|
|
98
|
+
- Modifying existing files
|
|
99
|
+
- Deleting files
|
|
100
|
+
- Moving or copying files
|
|
101
|
+
- Creating temporary files anywhere, including /tmp
|
|
102
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
103
|
+
- Running ANY commands that change system state
|
|
104
|
+
|
|
105
|
+
# Planning Process
|
|
106
|
+
1. Understand requirements
|
|
107
|
+
2. Explore thoroughly (read files, find patterns, understand architecture)
|
|
108
|
+
3. Design solution based on your assigned perspective
|
|
109
|
+
4. Detail the plan with step-by-step implementation strategy
|
|
110
|
+
|
|
111
|
+
# Requirements
|
|
112
|
+
- Consider trade-offs and architectural decisions
|
|
113
|
+
- Identify dependencies and sequencing
|
|
114
|
+
- Anticipate potential challenges
|
|
115
|
+
- Follow existing patterns where appropriate
|
|
116
|
+
|
|
117
|
+
# Tool Usage
|
|
118
|
+
- Use the find tool for file pattern matching (NOT the bash find command)
|
|
119
|
+
- Use the grep tool for content search (NOT bash grep/rg command)
|
|
120
|
+
- Use the read tool for reading files (NOT bash cat/head/tail)
|
|
121
|
+
- Use Bash ONLY for read-only operations
|
|
122
|
+
|
|
123
|
+
# Output Format
|
|
124
|
+
- Use absolute file paths
|
|
125
|
+
- Do not use emojis
|
|
126
|
+
- End your response with:
|
|
127
|
+
|
|
128
|
+
### Critical Files for Implementation
|
|
129
|
+
List 3-5 files most critical for implementing this plan:
|
|
130
|
+
- /absolute/path/to/file.ts - [Brief reason]`;
|
|
131
|
+
|
|
132
|
+
export const GENERAL_PURPOSE_DEFAULT_DESCRIPTION = "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.";
|
|
133
|
+
|
|
134
|
+
/** Embedded copies keyed by agent name. Explore needs an entry for the
|
|
135
|
+
* strategy-driven sync (KNOWN_PINNED_DEFAULT_AGENTS); Plan and
|
|
136
|
+
* general-purpose entries exist so users can pin them per-type via
|
|
137
|
+
* subagentModelOverrides (v0.25.6). */
|
|
90
138
|
const EMBEDDED_DEFAULTS: Record<string, EmbeddedAgentDefault> = {
|
|
91
139
|
Explore: {
|
|
92
140
|
description: EXPLORE_DEFAULT_DESCRIPTION,
|
|
93
141
|
systemPrompt: EXPLORE_DEFAULT_SYSTEM_PROMPT,
|
|
94
142
|
tools: EXPLORE_DEFAULT_TOOLS,
|
|
95
143
|
},
|
|
144
|
+
Plan: {
|
|
145
|
+
description: PLAN_DEFAULT_DESCRIPTION,
|
|
146
|
+
systemPrompt: PLAN_DEFAULT_SYSTEM_PROMPT,
|
|
147
|
+
tools: EXPLORE_DEFAULT_TOOLS, // same read-only set upstream
|
|
148
|
+
},
|
|
149
|
+
"general-purpose": {
|
|
150
|
+
description: GENERAL_PURPOSE_DEFAULT_DESCRIPTION,
|
|
151
|
+
systemPrompt: "", // upstream: empty prompt, promptMode append
|
|
152
|
+
tools: "", // upstream: all tools (builtinToolNames omitted)
|
|
153
|
+
},
|
|
96
154
|
};
|
|
97
155
|
|
|
156
|
+
/** v0.25.6: agent types the user can pin per-type via
|
|
157
|
+
* subagentModelOverrides (embedded defaults exist for each). */
|
|
158
|
+
export const OVERRIDABLE_AGENT_TYPES = Object.keys(EMBEDDED_DEFAULTS);
|
|
159
|
+
|
|
98
160
|
/** Default global agent dir (pi-subagents reads $PI_CODING_AGENT_DIR/agents,
|
|
99
161
|
* default ~/.pi/agent/agents). Parameterized in sync for tests. */
|
|
100
162
|
export function defaultAgentDir(): string {
|
|
@@ -107,12 +169,9 @@ export function buildAgentOverrideMd(name: string, model?: string): string {
|
|
|
107
169
|
const def = EMBEDDED_DEFAULTS[name];
|
|
108
170
|
if (!def) throw new Error(`no embedded default config for agent "${name}"`);
|
|
109
171
|
const yamlDesc = "'" + def.description.replace(/'/g, "''") + "'";
|
|
110
|
-
const lines = [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`tools: ${def.tools}`,
|
|
114
|
-
"prompt_mode: replace",
|
|
115
|
-
];
|
|
172
|
+
const lines = ["---", `description: ${yamlDesc}`];
|
|
173
|
+
if (def.tools) lines.push(`tools: ${def.tools}`);
|
|
174
|
+
lines.push(def.systemPrompt ? "prompt_mode: replace" : "prompt_mode: append");
|
|
116
175
|
if (model) lines.push(`model: ${model}`);
|
|
117
176
|
lines.push(
|
|
118
177
|
`x-managed-by: ${SUBAGENT_MANAGED_MARKER}`,
|
|
@@ -121,7 +180,7 @@ export function buildAgentOverrideMd(name: string, model?: string): string {
|
|
|
121
180
|
: "x-glla-note: model pin removed (upstream default pins a fixed model) so this agent inherits the parent session model and its quota pool. Managed by glla — flip /glla subagent strategy to agent-default to restore upstream behavior.",
|
|
122
181
|
"---",
|
|
123
182
|
"",
|
|
124
|
-
def.systemPrompt,
|
|
183
|
+
def.systemPrompt || "(no system-prompt override — upstream default is an empty append-mode prompt)",
|
|
125
184
|
"",
|
|
126
185
|
);
|
|
127
186
|
return lines.join("\n");
|
|
@@ -135,7 +194,16 @@ export interface SubagentSyncResult {
|
|
|
135
194
|
written: string[];
|
|
136
195
|
removed: string[];
|
|
137
196
|
/** Files left untouched because the user owns them (no marker). */
|
|
138
|
-
skipped: Array<{ name: string
|
|
197
|
+
skipped: Array<{ name: string } & { reason: string }>;
|
|
198
|
+
/** v0.25.6: managed files that were expected (previously written) but
|
|
199
|
+
* found missing or altered, and got re-written — surface these to the
|
|
200
|
+
* user (external edit, pi update, or dracon-sync churn). */
|
|
201
|
+
repaired: string[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** State file tracking what the last sync wrote (repair detection). */
|
|
205
|
+
export function subagentSyncStatePath(agentDir: string): string {
|
|
206
|
+
return path.join(agentDir, "agents", ".glla-subagent-sync.json");
|
|
139
207
|
}
|
|
140
208
|
|
|
141
209
|
/** Sync <agentDir>/agents/<Name>.md with the desired state. Idempotent:
|
|
@@ -145,8 +213,18 @@ export function syncSubagentModelOverrides(opts: {
|
|
|
145
213
|
strategy: SubagentModelStrategy;
|
|
146
214
|
overrides?: Record<string, string>;
|
|
147
215
|
}): SubagentSyncResult {
|
|
148
|
-
const result: SubagentSyncResult = { written: [], removed: [], skipped: [] };
|
|
216
|
+
const result: SubagentSyncResult = { written: [], removed: [], skipped: [], repaired: [] };
|
|
149
217
|
const overrides = opts.overrides ?? {};
|
|
218
|
+
// v0.25.6: load the previous sync state for repair detection — a file
|
|
219
|
+
// we wrote before that is now MISSING or CONTENT-CHANGED was touched
|
|
220
|
+
// externally; re-writing it is a repair the user should hear about.
|
|
221
|
+
let prevWritten: string[] = [];
|
|
222
|
+
try {
|
|
223
|
+
const prev = JSON.parse(fs.readFileSync(subagentSyncStatePath(opts.agentDir), "utf-8"));
|
|
224
|
+
if (Array.isArray(prev?.written)) prevWritten = prev.written.map(String);
|
|
225
|
+
} catch {
|
|
226
|
+
/* first sync or unreadable state */
|
|
227
|
+
}
|
|
150
228
|
const names = new Set<string>([...KNOWN_PINNED_DEFAULT_AGENTS, ...Object.keys(overrides)]);
|
|
151
229
|
|
|
152
230
|
for (const name of names) {
|
|
@@ -159,11 +237,15 @@ export function syncSubagentModelOverrides(opts: {
|
|
|
159
237
|
continue;
|
|
160
238
|
}
|
|
161
239
|
const file = path.join(opts.agentDir, "agents", `${name}.md`);
|
|
240
|
+
// v0.25.6: the strategy-driven (model-less) write applies ONLY to
|
|
241
|
+
// agents that pin a model upstream (Explore) — Plan/general-purpose
|
|
242
|
+
// don't pin, so inherit-parent needs no file for them; they get
|
|
243
|
+
// managed files only via an explicit per-type override.
|
|
162
244
|
const desired = overrideModel
|
|
163
245
|
? buildAgentOverrideMd(name, overrideModel)
|
|
164
|
-
: opts.strategy === "inherit-parent"
|
|
246
|
+
: opts.strategy === "inherit-parent" && (KNOWN_PINNED_DEFAULT_AGENTS as readonly string[]).includes(name)
|
|
165
247
|
? buildAgentOverrideMd(name)
|
|
166
|
-
: undefined; // agent-default + no
|
|
248
|
+
: undefined; // agent-default / no pin upstream + no override → file should be absent
|
|
167
249
|
|
|
168
250
|
const exists = fs.existsSync(file);
|
|
169
251
|
const current = exists ? fs.readFileSync(file, "utf-8") : undefined;
|
|
@@ -186,6 +268,31 @@ export function syncSubagentModelOverrides(opts: {
|
|
|
186
268
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
187
269
|
fs.writeFileSync(file, desired);
|
|
188
270
|
result.written.push(name);
|
|
271
|
+
if (prevWritten.includes(name)) result.repaired.push(name);
|
|
272
|
+
}
|
|
273
|
+
// Persist what we manage now (best-effort).
|
|
274
|
+
try {
|
|
275
|
+
fs.mkdirSync(path.join(opts.agentDir, "agents"), { recursive: true });
|
|
276
|
+
fs.writeFileSync(subagentSyncStatePath(opts.agentDir), JSON.stringify({ written: result.written, at: new Date().toISOString() }));
|
|
277
|
+
} catch {
|
|
278
|
+
/* repair detection is best-effort */
|
|
189
279
|
}
|
|
190
280
|
return result;
|
|
191
281
|
}
|
|
282
|
+
|
|
283
|
+
/** v0.25.6: effective model for an agent type, for the headless settings
|
|
284
|
+
* display. Per-type override wins; inherit-parent falls to the session
|
|
285
|
+
* model; agent-default means upstream's own resolution (Explore's haiku
|
|
286
|
+
* pin for Explore, session model for the others). */
|
|
287
|
+
export function resolveEffectiveSubagentModel(
|
|
288
|
+
name: string,
|
|
289
|
+
settings: { subagentModelStrategy?: string; subagentModelOverrides?: Record<string, string> },
|
|
290
|
+
sessionModel?: string,
|
|
291
|
+
): string {
|
|
292
|
+
const pin = settings.subagentModelOverrides?.[name];
|
|
293
|
+
if (pin) return `${pin} (per-type pin)`;
|
|
294
|
+
if ((settings.subagentModelStrategy ?? "inherit-parent") === "inherit-parent") {
|
|
295
|
+
return sessionModel ? `${sessionModel} (inherits session)` : "(session model)";
|
|
296
|
+
}
|
|
297
|
+
return name === "Explore" ? "anthropic/claude-haiku-4-5 (upstream pin)" : "(agent default)";
|
|
298
|
+
}
|
|
@@ -60,6 +60,9 @@ export interface Settings {
|
|
|
60
60
|
* override without the model pin so subagents share the session model and
|
|
61
61
|
* its quota; "agent-default" restores upstream behavior. Applies to NEW
|
|
62
62
|
* sessions (pi-subagents registers agents at session start). */
|
|
63
|
+
/** v0.26.0: reviewer (post-completion follow-up enqueuer) config —
|
|
64
|
+
* project-scoped; see extensions/reviewer.ts DEFAULT_REVIEWER_CONFIG. */
|
|
65
|
+
reviewer?: Record<string, unknown>;
|
|
63
66
|
subagentModelStrategy?: SubagentModelStrategy;
|
|
64
67
|
/** v0.24.6: per-agent-type model pin, e.g. { "Explore": "minimax/MiniMax-M3" }.
|
|
65
68
|
* Always wins over subagentModelStrategy — the managed override is written
|
package/extensions/loops/goal.ts
CHANGED
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
} from "../goal-loop-core.js";
|
|
88
88
|
import {
|
|
89
89
|
isQuotaError,
|
|
90
|
+
isSubagentQuotaResult,
|
|
90
91
|
parseQuotaError,
|
|
91
92
|
scheduleQuotaRetry,
|
|
92
93
|
cancelQuotaRetry,
|
|
@@ -101,6 +102,13 @@ import {
|
|
|
101
102
|
settingsProvenance,
|
|
102
103
|
type Settings,
|
|
103
104
|
} from "../goal-settings.js";
|
|
105
|
+
import {
|
|
106
|
+
DEFAULT_REVIEWER_CONFIG,
|
|
107
|
+
resolveReviewerConfig,
|
|
108
|
+
reviewerMenuOptions,
|
|
109
|
+
runReviewer,
|
|
110
|
+
type ReviewerConfig,
|
|
111
|
+
} from "../reviewer.js";
|
|
104
112
|
import {
|
|
105
113
|
discoverGllaProjects,
|
|
106
114
|
parseLedgerEntries,
|
|
@@ -122,6 +130,7 @@ import {
|
|
|
122
130
|
import { buildStatusText, buildWidgetLines, type AuditDisplayProgress } from "../goal-loop-display.js";
|
|
123
131
|
import {
|
|
124
132
|
defaultAgentDir,
|
|
133
|
+
resolveEffectiveSubagentModel,
|
|
125
134
|
syncSubagentModelOverrides,
|
|
126
135
|
type SubagentModelStrategy,
|
|
127
136
|
} from "../goal-loop-subagents.js";
|
|
@@ -515,7 +524,73 @@ function archiveCurrentGoal(ctx: ExtensionContext, status: Status, stopReason?:
|
|
|
515
524
|
// (v0.2.0 bug: bare /list next silently consumed TWO items, found by the
|
|
516
525
|
// pick-any-item verification in v0.10.0).
|
|
517
526
|
if (goal.policy === "list" && status === "complete") {
|
|
518
|
-
activateNextListItem(ctx);
|
|
527
|
+
const advanced = activateNextListItem(ctx);
|
|
528
|
+
// v0.26.0: the queue just EMPTIED on a completion → list-complete.
|
|
529
|
+
if (!advanced) fireReviewer(ctx, { kind: "list", goalId: goal.id, objective: goal.objective, terminal: "goal-complete" });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// v0.26.0: a /goal (non-list) reached a terminal state → maybe fire.
|
|
533
|
+
if (goal.policy !== "list") {
|
|
534
|
+
fireReviewer(ctx, { kind: "goal", goalId: goal.id, objective: goal.objective, terminal: status === "complete" ? "goal-complete" : status === "aborted" ? "goal-aborted" : "goal-paused" });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* v0.26.0: bind the reviewer to the live session. Sources for finding
|
|
540
|
+
* extraction: the archived goal markdown + its audit reports + the
|
|
541
|
+
* durable audit log entries for this goal. List items are enqueued via
|
|
542
|
+
* the ONE enqueue path; /goal proposals go through the agent (which
|
|
543
|
+
* calls propose_goal_draft → the user's Confirm dialog).
|
|
544
|
+
*/
|
|
545
|
+
function fireReviewer(
|
|
546
|
+
ctx: ExtensionContext,
|
|
547
|
+
source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
|
|
548
|
+
opts: { manual?: boolean } = {},
|
|
549
|
+
): void {
|
|
550
|
+
try {
|
|
551
|
+
const settings = loadSettings(ctx.cwd);
|
|
552
|
+
const config = resolveReviewerConfig(settings.reviewer as Partial<ReviewerConfig> | undefined);
|
|
553
|
+
const sources: Array<{ name: string; text: string }> = [];
|
|
554
|
+
try {
|
|
555
|
+
sources.push({ name: "archive", text: fs.readFileSync(archivedGoalPath(ctx.cwd, source.goalId), "utf-8") });
|
|
556
|
+
} catch {
|
|
557
|
+
/* archive md may not exist for manual review of a live goal */
|
|
558
|
+
}
|
|
559
|
+
const auditTexts = readAuditLog(ctx.cwd)
|
|
560
|
+
.filter((e) => e.goalId === source.goalId)
|
|
561
|
+
.map((e) => e.report);
|
|
562
|
+
for (const t of auditTexts) sources.push({ name: "audit", text: t });
|
|
563
|
+
let ledgerEntries: Array<{ type: string; at?: string; value?: any }> = [];
|
|
564
|
+
try {
|
|
565
|
+
ledgerEntries = parseLedgerEntries(fs.readFileSync(ledgerPath(ctx.cwd), "utf-8"));
|
|
566
|
+
} catch {
|
|
567
|
+
/* no ledger yet */
|
|
568
|
+
}
|
|
569
|
+
const outcome = runReviewer(config, source, {
|
|
570
|
+
cwd: ctx.cwd,
|
|
571
|
+
nowMs: Date.now(),
|
|
572
|
+
manual: opts.manual,
|
|
573
|
+
ledgerEntries,
|
|
574
|
+
sources,
|
|
575
|
+
enqueueListItems: (objectives) => enqueueItems(ctx, objectives, "reviewer"),
|
|
576
|
+
proposeGoal: (objective, reason) => {
|
|
577
|
+
try {
|
|
578
|
+
extensionApi?.sendUserMessage(
|
|
579
|
+
`[REVIEWER FOLLOW-UP — ${reason}. Propose this as a /goal via propose_goal_draft (the user Confirms or rejects): ${objective}]`,
|
|
580
|
+
{ deliverAs: ctx.isIdle() ? "followUp" : "steer" },
|
|
581
|
+
);
|
|
582
|
+
} catch {
|
|
583
|
+
/* proposal best-effort */
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
587
|
+
ledger: (type, value) => appendLedger(ctx.cwd, type, value),
|
|
588
|
+
});
|
|
589
|
+
if (!outcome.fired && outcome.suppressedReason && opts.manual) {
|
|
590
|
+
ctx.ui.notify(`Reviewer suppressed: ${outcome.suppressedReason}`, "info");
|
|
591
|
+
}
|
|
592
|
+
} catch (err) {
|
|
593
|
+
ctx.ui.notify(`Reviewer failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
519
594
|
}
|
|
520
595
|
}
|
|
521
596
|
|
|
@@ -2666,6 +2741,8 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
|
|
|
2666
2741
|
`Stuck max interventions — ${show("stuckMaxInterventions", "(5 default)")}`,
|
|
2667
2742
|
`Subagent model strategy — ${show("subagentModelStrategy", "(inherit-parent)")}`,
|
|
2668
2743
|
`Subagent Explore model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.Explore ?? "(follows strategy)"}`,
|
|
2744
|
+
`Subagent Plan model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.Plan ?? "(follows strategy)"}`,
|
|
2745
|
+
`Subagent general-purpose model pin — ${loadSettings(ctx.cwd).subagentModelOverrides?.["general-purpose"] ?? "(follows strategy)"}`,
|
|
2669
2746
|
`Audit feedback characters — ${show("auditFeedbackChars", "(full report)")}`,
|
|
2670
2747
|
"Done",
|
|
2671
2748
|
],
|
|
@@ -2735,15 +2812,16 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
|
|
|
2735
2812
|
saveSettings("global", ctx.cwd, { subagentModelStrategy: strategy });
|
|
2736
2813
|
ctx.ui.notify("Subagent model strategy saved — applies to NEW pi sessions (pi-subagents registers agents at session start).", "info");
|
|
2737
2814
|
}
|
|
2738
|
-
} else if (
|
|
2739
|
-
const
|
|
2815
|
+
} else if (/^Subagent (Explore|Plan|general-purpose) model pin/.test(choice)) {
|
|
2816
|
+
const agentType = choice.match(/^Subagent (Explore|Plan|general-purpose) model pin/)![1]!;
|
|
2817
|
+
const v = await ctx.ui.input(`Model pin for ${agentType} subagents`, "provider/model-id e.g. minimax/MiniMax-M3 — always wins over strategy; empty = follow strategy");
|
|
2740
2818
|
if (v !== undefined) {
|
|
2741
2819
|
const current = loadSettings(ctx.cwd).subagentModelOverrides ?? {};
|
|
2742
2820
|
const next = { ...current };
|
|
2743
|
-
if (v.trim()) next
|
|
2744
|
-
else delete next
|
|
2821
|
+
if (v.trim()) next[agentType] = v.trim();
|
|
2822
|
+
else delete next[agentType];
|
|
2745
2823
|
saveSettings("global", ctx.cwd, { subagentModelOverrides: Object.keys(next).length > 0 ? next : undefined });
|
|
2746
|
-
ctx.ui.notify(
|
|
2824
|
+
ctx.ui.notify(`${agentType} model pin saved — applies to NEW pi sessions.`, "info");
|
|
2747
2825
|
}
|
|
2748
2826
|
} else if (choice.startsWith("Audit feedback")) {
|
|
2749
2827
|
const v = await ctx.ui.input("Auditor feedback returned to the executor (characters)", "non-negative integer cap; 0 or empty = full report (default)");
|
|
@@ -2761,6 +2839,73 @@ async function openSettingsUI(ctx: ExtensionContext): Promise<void> {
|
|
|
2761
2839
|
}
|
|
2762
2840
|
}
|
|
2763
2841
|
|
|
2842
|
+
/** v0.26.0: /review <archived-goal-id> — manual reviewer invocation. */
|
|
2843
|
+
async function cmdReview(args: string, ctx: ExtensionContext): Promise<void> {
|
|
2844
|
+
const id = args.trim();
|
|
2845
|
+
if (!id) {
|
|
2846
|
+
ctx.ui.notify("Usage: /review <goal-id> — see /goal archive for ids.", "info");
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
// Resolve the id against the archive (suffix match allowed).
|
|
2850
|
+
let goalId = id;
|
|
2851
|
+
let objective = "(archived goal)";
|
|
2852
|
+
try {
|
|
2853
|
+
const files = fs.readdirSync(archiveDir(ctx.cwd)).filter((f) => f.endsWith(".md"));
|
|
2854
|
+
const match = files.find((f) => f === `${id}.md`) ?? files.find((f) => f.includes(id));
|
|
2855
|
+
if (!match) {
|
|
2856
|
+
ctx.ui.notify(`No archived goal matching "${id}". /goal archive lists them.`, "warning");
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
goalId = match.replace(/\.md$/, "");
|
|
2860
|
+
const md = fs.readFileSync(path.join(archiveDir(ctx.cwd), match), "utf-8");
|
|
2861
|
+
const objMatch = md.match(/## Objective\n\n> ([\s\S]*?)(?:\n\n|$)/);
|
|
2862
|
+
if (objMatch) objective = objMatch[1]!.replace(/\n/g, " ").slice(0, 300);
|
|
2863
|
+
} catch {
|
|
2864
|
+
ctx.ui.notify(`No archive found for ${id}.`, "warning");
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
fireReviewer(ctx, { kind: "goal", goalId, objective, terminal: "goal-complete" }, { manual: true });
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
/** v0.26.0: /glla reviewer — the reviewer config menu (project-scoped). */
|
|
2871
|
+
async function cmdReviewerSettings(ctx: ExtensionContext): Promise<void> {
|
|
2872
|
+
if (!ctx.hasUI) {
|
|
2873
|
+
const cfg = resolveReviewerConfig(loadSettings(ctx.cwd).reviewer as Partial<ReviewerConfig> | undefined);
|
|
2874
|
+
ctx.ui.notify(`reviewer (project): ${JSON.stringify(cfg, null, 2)}`, "info");
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
const load = () => resolveReviewerConfig(loadSettings(ctx.cwd).reviewer as Partial<ReviewerConfig> | undefined);
|
|
2878
|
+
const save = (patch: Partial<ReviewerConfig>) => saveSettings("project", ctx.cwd, { reviewer: { ...load(), ...patch } as Record<string, unknown> });
|
|
2879
|
+
for (;;) {
|
|
2880
|
+
const cfg = load();
|
|
2881
|
+
let choice: string | undefined;
|
|
2882
|
+
try {
|
|
2883
|
+
choice = await ctx.ui.select("Reviewer — post-completion follow-up enqueuer (project settings)", reviewerMenuOptions(cfg));
|
|
2884
|
+
} catch {
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (!choice || choice === "Done") return;
|
|
2888
|
+
try {
|
|
2889
|
+
if (choice.startsWith("Enabled")) save({ enabled: !cfg.enabled });
|
|
2890
|
+
else if (choice.startsWith("Leverage mode")) save({ leverageMode: cfg.leverageMode === "fix-without-confirm" ? "confirm-all" : "fix-without-confirm" });
|
|
2891
|
+
else if (choice.startsWith("Fire on goal-complete")) save({ fireOn: cfg.fireOn.includes("goal-complete") ? cfg.fireOn.filter((e) => e !== "goal-complete") : [...cfg.fireOn, "goal-complete"] });
|
|
2892
|
+
else if (choice.startsWith("Fire on list-complete")) save({ fireOn: cfg.fireOn.includes("list-complete") ? cfg.fireOn.filter((e) => e !== "list-complete") : [...cfg.fireOn, "list-complete"] });
|
|
2893
|
+
else if (choice.startsWith("Cascade: audit-on-clean")) save({ cascade: cfg.cascade.includes("fire-audit-on-clean") ? cfg.cascade.filter((c) => c !== "fire-audit-on-clean") : [...cfg.cascade, "fire-audit-on-clean"] });
|
|
2894
|
+
else if (choice.startsWith("Max findings")) {
|
|
2895
|
+
const v = await ctx.ui.input("Max findings per review", "1-50");
|
|
2896
|
+
const n = Number(v?.trim());
|
|
2897
|
+
if (Number.isSafeInteger(n) && n >= 1 && n <= 50) save({ maxFindingsPerReview: n });
|
|
2898
|
+
} else if (choice.startsWith("Max reviews")) {
|
|
2899
|
+
const v = await ctx.ui.input("Max reviewer fires per day", "1-100");
|
|
2900
|
+
const n = Number(v?.trim());
|
|
2901
|
+
if (Number.isSafeInteger(n) && n >= 1 && n <= 100) save({ maxReviewsPerDay: n });
|
|
2902
|
+
}
|
|
2903
|
+
} catch {
|
|
2904
|
+
/* individual save failures are non-fatal */
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2764
2909
|
/**
|
|
2765
2910
|
* v0.25.2: /glla stats — one command, every project's rollup. Args:
|
|
2766
2911
|
* (none) markdown table, all discovered projects
|
|
@@ -2853,6 +2998,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2853
2998
|
cmdAudits(trimmed.slice("audits".length).trim(), ctx);
|
|
2854
2999
|
return;
|
|
2855
3000
|
}
|
|
3001
|
+
if (/^reviewer\b/.test(trimmed)) {
|
|
3002
|
+
await cmdReviewerSettings(ctx);
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
2856
3005
|
if (!trimmed) {
|
|
2857
3006
|
if (ctx.hasUI) {
|
|
2858
3007
|
await openSettingsUI(ctx);
|
|
@@ -2878,6 +3027,10 @@ async function cmdSettings(args: string, ctx: ExtensionContext): Promise<void> {
|
|
|
2878
3027
|
fmt("aggressiveMode", "aggressiveMode"),
|
|
2879
3028
|
fmt("quotaRetryMinutes", "quotaRetryMinutes"),
|
|
2880
3029
|
fmt("stuckMaxInterventions", "stuckMaxInterventions"),
|
|
3030
|
+
// v0.25.6: effective per-type subagent model resolution.
|
|
3031
|
+
...["Explore", "Plan", "general-purpose"].map(
|
|
3032
|
+
(t) => `subagent ${t}: ${resolveEffectiveSubagentModel(t, loadSettings(ctx.cwd), (ctx.model as any)?.id ? `${(ctx.model as any).provider}/${(ctx.model as any).id}` : undefined)}`,
|
|
3033
|
+
),
|
|
2881
3034
|
`\nglobal: ${globalSettingsPath()}`,
|
|
2882
3035
|
`project: ${projectSettingsPath(ctx.cwd)}`,
|
|
2883
3036
|
`Set with: /glla key=value (global) · /glla project key=value (project override)`,
|
|
@@ -3152,10 +3305,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3152
3305
|
["stats", "per-project ledger rollups: /glla stats [json|premature|project=<path>]"],
|
|
3153
3306
|
["audits", "audit-log browser: /glla audits [N|full] — recent verdicts from .pi-glla/audits.jsonl"],
|
|
3154
3307
|
["autoaccept=", "on: drafts activate without the Confirm dialog (unattended rigs)"],
|
|
3308
|
+
["reviewer", "reviewer config menu (post-completion follow-up enqueuer)"],
|
|
3155
3309
|
["project", "write a project override: /glla project key=value"],
|
|
3156
3310
|
]),
|
|
3157
3311
|
handler: settingsHandler,
|
|
3158
3312
|
});
|
|
3313
|
+
pi.registerCommand("review", {
|
|
3314
|
+
description: "Manually run the reviewer on an archived goal: /review <goal-id> — extracts findings, writes a report to .pi-glla/reviews/, enqueues bug-class fixes to /list, proposes architectural items as /goal. Bypasses the trigger gates (explicit user request).",
|
|
3315
|
+
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdReview(args, ctx); },
|
|
3316
|
+
});
|
|
3159
3317
|
pi.registerCommand("list", {
|
|
3160
3318
|
description: "Loop 2: the list of audited goals — order is the default, not the law. /list <describe tasks or name a plan file> (dumps get shaped into items, files import, 'Done when:' adds directly) | /list show | /list resume | /list next [n] | /list remove <n> | /list clear | /list cancel",
|
|
3161
3319
|
getArgumentCompletions: completions([
|
|
@@ -3164,6 +3322,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3164
3322
|
["next", "activate the next item (or /list next <n> for position n)"],
|
|
3165
3323
|
["remove", "remove an item: /list remove <n>"],
|
|
3166
3324
|
["clear", "empty the list"],
|
|
3325
|
+
["depth", "queue depth, oldest item age, average item duration"],
|
|
3167
3326
|
["cancel", "stop the whole list: abort the active item + drop all waiting"],
|
|
3168
3327
|
]),
|
|
3169
3328
|
handler: (args: string, ctx: ExtensionContext) => { rememberCtx(ctx); return cmdList(args, ctx); },
|
|
@@ -3252,6 +3411,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3252
3411
|
state.goal.telemetry = t;
|
|
3253
3412
|
}
|
|
3254
3413
|
}
|
|
3414
|
+
// v0.25.6: subagent quota errors (the pi-subagents#175 shape —
|
|
3415
|
+
// Explore's upstream haiku pin 403s on shared keys). Surface the
|
|
3416
|
+
// repair path immediately; the continuation prompt's WHEN SUBAGENTS
|
|
3417
|
+
// HIT QUOTA ERRORS section carries the full guidance.
|
|
3418
|
+
if (isSubagentQuotaResult(String(event?.toolName ?? ""), Boolean(event?.isError ?? event?.error), event?.output ?? event?.result ?? event?.details ?? "")) {
|
|
3419
|
+
const errText = typeof (event?.output ?? event?.result) === "string" ? (event?.output ?? event?.result) : JSON.stringify(event?.output ?? event?.result ?? event?.details ?? "");
|
|
3420
|
+
appendLedger(registeredCtx?.cwd ?? process.cwd(), "subagent_quota_error", { error: String(errText).slice(0, 200) });
|
|
3421
|
+
registeredCtx?.ui.notify(
|
|
3422
|
+
"Subagent hit a quota error (403/limit). Repair: re-spawn with an explicit model= on your quota pool, or do the work inline — see the continuation prompt's WHEN SUBAGENTS HIT QUOTA ERRORS. Explore's upstream haiku pin is the usual cause (pi-subagents#175); glla's inherit-parent strategy removes it for NEW sessions.",
|
|
3423
|
+
"warning",
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3255
3426
|
if (draftingTarget === null) return;
|
|
3256
3427
|
if (askUserQuestionAnswered(String(event?.toolName ?? ""), event?.details)) {
|
|
3257
3428
|
draftingUserReplies++;
|
|
@@ -3285,6 +3456,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
3285
3456
|
for (const skip of sync.skipped) {
|
|
3286
3457
|
ctx.ui.notify(`glla subagent override skipped [${skip.name}]: ${skip.reason}`, "warning");
|
|
3287
3458
|
}
|
|
3459
|
+
// v0.25.6: notify-with-repair — a managed override that went missing
|
|
3460
|
+
// or was altered externally (pi update, manual edit, sync churn) is
|
|
3461
|
+
// re-written AND surfaced, not silently restored.
|
|
3462
|
+
if (sync.repaired.length > 0) {
|
|
3463
|
+
ctx.ui.notify(
|
|
3464
|
+
`glla repaired managed subagent override(s): ${sync.repaired.join(", ")} — the file(s) were missing or altered externally; re-written per your subagent settings.`,
|
|
3465
|
+
"warning",
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3288
3468
|
} catch (err) {
|
|
3289
3469
|
ctx.ui.notify(`glla subagent override sync failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
3290
3470
|
}
|
|
@@ -88,3 +88,13 @@ export function scheduleQuotaRetry(
|
|
|
88
88
|
"info",
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/** v0.25.6: detect a SUBAGENT quota failure in a tool_result — the
|
|
93
|
+
* pi-subagents#175 shape (Explore's upstream haiku pin 403s on shared
|
|
94
|
+
* keys). Tool must be an Agent spawn and the payload a quota error. */
|
|
95
|
+
export function isSubagentQuotaResult(toolName: string, isError: boolean, payload: unknown): boolean {
|
|
96
|
+
if (!isError) return false;
|
|
97
|
+
if (toolName !== "Agent" && toolName !== "agent") return false;
|
|
98
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload ?? "");
|
|
99
|
+
return isQuotaError(text);
|
|
100
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// pi-goal-list-loop-audit — v0.26.0
|
|
2
|
+
// extensions/reviewer.ts
|
|
3
|
+
//
|
|
4
|
+
// The Reviewer: post-completion follow-up enqueuer. Fires after a /goal
|
|
5
|
+
// completes or a /list queue empties, extracts findings from the archive
|
|
6
|
+
// + ledger, classifies them by leverage, writes a review report, and
|
|
7
|
+
// cascades: bug/refactor findings → /list items (no Confirm, per the
|
|
8
|
+
// leverage principle), architectural findings → /goal proposal (Confirm),
|
|
9
|
+
// clean completions → audit /goal proposal, strategic-only → notify+idle.
|
|
10
|
+
//
|
|
11
|
+
// Deterministic by design (REVIEWER-DESIGN-2026-07-24: "makes NO new tool
|
|
12
|
+
// calls — purely analytical"). All side effects are injected so tests
|
|
13
|
+
// drive it without a pi host. /loop completions never trigger it.
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
|
|
18
|
+
export interface ReviewerConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
fireOn: Array<"goal-complete" | "list-complete">;
|
|
21
|
+
doNotFireOn: string[];
|
|
22
|
+
cascade: Array<"convert-findings-to-list" | "queue-leftovers" | "fire-audit-on-clean" | "notify-and-idle">;
|
|
23
|
+
auditCadence: string;
|
|
24
|
+
auditScope: string;
|
|
25
|
+
leverageMode: "fix-without-confirm" | "confirm-all";
|
|
26
|
+
confirmOn: string[];
|
|
27
|
+
maxFindingsPerReview: number;
|
|
28
|
+
maxReviewsPerDay: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_REVIEWER_CONFIG: ReviewerConfig = {
|
|
32
|
+
enabled: true,
|
|
33
|
+
fireOn: ["goal-complete", "list-complete"],
|
|
34
|
+
doNotFireOn: ["goal-aborted", "goal-paused"],
|
|
35
|
+
cascade: ["convert-findings-to-list", "queue-leftovers", "fire-audit-on-clean", "notify-and-idle"],
|
|
36
|
+
auditCadence: "every-clean-completion",
|
|
37
|
+
auditScope: "regression-scan",
|
|
38
|
+
leverageMode: "fix-without-confirm",
|
|
39
|
+
confirmOn: ["architectural-decision", "new-dependency", "schema-change"],
|
|
40
|
+
maxFindingsPerReview: 10,
|
|
41
|
+
maxReviewsPerDay: 20,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Merge a partial project-settings block over the defaults. */
|
|
45
|
+
export function resolveReviewerConfig(block?: Partial<ReviewerConfig>): ReviewerConfig {
|
|
46
|
+
return { ...DEFAULT_REVIEWER_CONFIG, ...(block ?? {}) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type FindingClass = "bug" | "refactor" | "architectural" | "strategic";
|
|
50
|
+
|
|
51
|
+
export interface Finding {
|
|
52
|
+
text: string;
|
|
53
|
+
source: string;
|
|
54
|
+
class: FindingClass;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Leverage classification (contract item 5). Order matters: strategic
|
|
58
|
+
* and architectural win over bug/refactor — "should we rewrite this
|
|
59
|
+
* broken schema" is a decision, not a fix. */
|
|
60
|
+
const CLASS_PATTERNS: Array<{ class: FindingClass; re: RegExp }> = [
|
|
61
|
+
{ class: "strategic", re: /\bshould we\b|\bdeprecat|ship this\??|strategic/i },
|
|
62
|
+
{ class: "architectural", re: /\brewrite\b|new dependency|schema change|architectural|redesign/i },
|
|
63
|
+
{ class: "bug", re: /\bTODO\b|\bFIXME\b|\bbug\b|\bissue\b|regression|broken|\bfixme\b/i },
|
|
64
|
+
{ class: "refactor", re: /could be cleaner|consider refactoring|duplicat|refactor|left ?out|follow[\s-]?up|deferred/i },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export function classifyFindingText(line: string): FindingClass | undefined {
|
|
68
|
+
const t = line.trim();
|
|
69
|
+
if (t.length < 8) return undefined;
|
|
70
|
+
for (const { class: cls, re } of CLASS_PATTERNS) {
|
|
71
|
+
if (re.test(t)) return cls;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Scan source texts line-by-line for finding-shaped content. */
|
|
77
|
+
export function extractFindings(sources: Array<{ name: string; text: string }>, max: number): Finding[] {
|
|
78
|
+
const out: Finding[] = [];
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
for (const { name, text } of sources) {
|
|
81
|
+
for (const line of text.split("\n")) {
|
|
82
|
+
const cls = classifyFindingText(line);
|
|
83
|
+
if (!cls) continue;
|
|
84
|
+
const clean = line.trim().replace(/^[-*>\s\[\]x]+/, "").slice(0, 200);
|
|
85
|
+
if (clean.length < 8 || seen.has(clean)) continue;
|
|
86
|
+
seen.add(clean);
|
|
87
|
+
out.push({ text: clean, source: name, class: cls });
|
|
88
|
+
if (out.length >= max) return out;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Runaway prevention (contract item 6/9): a reviewer fire in the last
|
|
95
|
+
* `windowMs` suppresses re-firing — reviewer-created work completing
|
|
96
|
+
* immediately must not recursively fire the reviewer. */
|
|
97
|
+
export function reviewerFiredRecently(entries: Array<{ type: string; at?: string }>, windowMs: number, nowMs: number): boolean {
|
|
98
|
+
return entries.some((e) => e.type === "reviewer_fired" && e.at !== undefined && nowMs - Date.parse(e.at) < windowMs);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Per-day cap (contract item 10). */
|
|
102
|
+
export function reviewsToday(entries: Array<{ type: string; at?: string }>, nowMs: number): number {
|
|
103
|
+
const day = new Date(nowMs).toISOString().slice(0, 10);
|
|
104
|
+
return entries.filter((e) => e.type === "reviewer_fired" && e.at?.startsWith(day)).length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ReviewReport {
|
|
108
|
+
goalId: string;
|
|
109
|
+
kind: "goal" | "list";
|
|
110
|
+
objective: string;
|
|
111
|
+
findings: Finding[];
|
|
112
|
+
cascadeStep: string;
|
|
113
|
+
at: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatReviewReport(r: ReviewReport): string {
|
|
117
|
+
const byClass = (c: FindingClass) => r.findings.filter((f) => f.class === c);
|
|
118
|
+
const section = (title: string, items: Finding[]) =>
|
|
119
|
+
items.length === 0 ? "" : `\n## ${title}\n\n${items.map((f) => `- ${f.text} _(${f.source})_`).join("\n")}\n`;
|
|
120
|
+
return [
|
|
121
|
+
`# Review — ${r.goalId}`,
|
|
122
|
+
"",
|
|
123
|
+
`**Kind**: ${r.kind} · **At**: ${r.at}`,
|
|
124
|
+
"",
|
|
125
|
+
"## Summary",
|
|
126
|
+
"",
|
|
127
|
+
`Completed: ${r.objective.slice(0, 300)}`,
|
|
128
|
+
"",
|
|
129
|
+
`**Cascade step**: ${r.cascadeStep}`,
|
|
130
|
+
"",
|
|
131
|
+
`## Findings (${r.findings.length})`,
|
|
132
|
+
section("Bug-class (enqueued to /list, no Confirm)", byClass("bug")),
|
|
133
|
+
section("Refactor-class (enqueued to /list, no Confirm)", byClass("refactor")),
|
|
134
|
+
section("Architectural-class (proposed as /goal, Confirm required)", byClass("architectural")),
|
|
135
|
+
section("Strategic-class (notify only)", byClass("strategic")),
|
|
136
|
+
r.findings.length === 0 ? "\n(none — completion looks clean)\n" : "",
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function writeReviewReport(cwd: string, report: ReviewReport): string {
|
|
141
|
+
const dir = path.join(cwd, ".pi-glla", "reviews");
|
|
142
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
const ts = report.at.replace(/[:.]/g, "-");
|
|
144
|
+
const file = path.join(dir, `${report.goalId}-${ts}.md`);
|
|
145
|
+
fs.writeFileSync(file, formatReviewReport(report));
|
|
146
|
+
return file;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Injectable side effects — goal.ts binds these to the live session. */
|
|
150
|
+
export interface ReviewerDeps {
|
|
151
|
+
cwd: string;
|
|
152
|
+
nowMs: number;
|
|
153
|
+
/** /review <id> manual invocation — bypasses fireOn/doNotFireOn gates,
|
|
154
|
+
* the refire window, and the day cap (the user asked explicitly). */
|
|
155
|
+
manual?: boolean;
|
|
156
|
+
ledgerEntries: Array<{ type: string; at?: string; value?: any }>;
|
|
157
|
+
/** Source texts for finding extraction (archive md, audit reports). */
|
|
158
|
+
sources: Array<{ name: string; text: string }>;
|
|
159
|
+
enqueueListItems: (objectives: string[]) => void;
|
|
160
|
+
proposeGoal: (objective: string, reason: string) => void;
|
|
161
|
+
notify: (message: string, level: "info" | "warning") => void;
|
|
162
|
+
ledger: (type: string, value: Record<string, unknown>) => void;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ReviewerOutcome {
|
|
166
|
+
fired: boolean;
|
|
167
|
+
suppressedReason?: string;
|
|
168
|
+
report?: ReviewReport;
|
|
169
|
+
reportPath?: string;
|
|
170
|
+
enqueued: number;
|
|
171
|
+
proposed: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const REVIEWER_REFIRE_WINDOW_MS = 5 * 60_000;
|
|
175
|
+
|
|
176
|
+
/** The reviewer lifecycle (contract item 1). */
|
|
177
|
+
export function runReviewer(
|
|
178
|
+
config: ReviewerConfig,
|
|
179
|
+
source: { kind: "goal" | "list"; goalId: string; objective: string; terminal: string },
|
|
180
|
+
deps: ReviewerDeps,
|
|
181
|
+
): ReviewerOutcome {
|
|
182
|
+
const none = (suppressedReason: string): ReviewerOutcome => ({ fired: false, suppressedReason, enqueued: 0, proposed: 0 });
|
|
183
|
+
if (!config.enabled && !deps.manual) return none("reviewer disabled");
|
|
184
|
+
const event = source.kind === "goal" ? `${source.terminal}` : "list-complete";
|
|
185
|
+
if (!deps.manual) {
|
|
186
|
+
if (config.doNotFireOn.includes(event)) return none(`doNotFireOn: ${event}`);
|
|
187
|
+
if (source.kind === "goal" && source.terminal !== "goal-complete") return none(`not a completion: ${source.terminal}`);
|
|
188
|
+
if (!config.fireOn.includes(source.kind === "goal" ? "goal-complete" : "list-complete")) return none("fireOn excludes this event");
|
|
189
|
+
if (reviewerFiredRecently(deps.ledgerEntries, REVIEWER_REFIRE_WINDOW_MS, deps.nowMs)) {
|
|
190
|
+
deps.ledger("reviewer_suppressed", { reason: "refire-window", goalId: source.goalId });
|
|
191
|
+
return none("reviewer fired within the last 5 minutes (runaway prevention)");
|
|
192
|
+
}
|
|
193
|
+
const today = reviewsToday(deps.ledgerEntries, deps.nowMs);
|
|
194
|
+
if (today >= config.maxReviewsPerDay) {
|
|
195
|
+
deps.ledger("reviewer_suppressed", { reason: "day-cap", count: today, cap: config.maxReviewsPerDay, goalId: source.goalId });
|
|
196
|
+
return none(`day cap reached (${today}/${config.maxReviewsPerDay})`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const findings = extractFindings(deps.sources, config.maxFindingsPerReview);
|
|
201
|
+
const bugs = findings.filter((f) => f.class === "bug" || f.class === "refactor");
|
|
202
|
+
const architectural = findings.filter((f) => f.class === "architectural");
|
|
203
|
+
const strategic = findings.filter((f) => f.class === "strategic");
|
|
204
|
+
|
|
205
|
+
let enqueued = 0;
|
|
206
|
+
let proposed = 0;
|
|
207
|
+
let cascadeStep = "notify-and-idle";
|
|
208
|
+
|
|
209
|
+
// Cascade: findings → list items (leverage: fix-without-confirm).
|
|
210
|
+
const convertStep = source.kind === "goal" ? "convert-findings-to-list" : "queue-leftovers";
|
|
211
|
+
if (bugs.length > 0 && config.cascade.includes(convertStep)) {
|
|
212
|
+
deps.enqueueListItems(bugs.map((f) => f.text));
|
|
213
|
+
enqueued = bugs.length;
|
|
214
|
+
cascadeStep = convertStep;
|
|
215
|
+
}
|
|
216
|
+
// Architectural findings → /goal proposal WITH Confirm.
|
|
217
|
+
if (architectural.length > 0) {
|
|
218
|
+
deps.proposeGoal(
|
|
219
|
+
architectural.map((f) => f.text).join("; "),
|
|
220
|
+
`reviewer found ${architectural.length} architectural-class finding(s) — needs your Confirm`,
|
|
221
|
+
);
|
|
222
|
+
proposed += architectural.length;
|
|
223
|
+
cascadeStep = "propose-goal";
|
|
224
|
+
}
|
|
225
|
+
// Clean completion → audit /goal (opt-in cascade step).
|
|
226
|
+
if (findings.length === 0 && config.cascade.includes("fire-audit-on-clean")) {
|
|
227
|
+
deps.proposeGoal(
|
|
228
|
+
`Post-completion regression scan after ${source.goalId} (${config.auditScope})`,
|
|
229
|
+
"reviewer: completion looks clean — firing the audit step",
|
|
230
|
+
);
|
|
231
|
+
proposed++;
|
|
232
|
+
cascadeStep = "fire-audit-on-clean";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const report: ReviewReport = {
|
|
236
|
+
goalId: source.goalId,
|
|
237
|
+
kind: source.kind,
|
|
238
|
+
objective: source.objective,
|
|
239
|
+
findings,
|
|
240
|
+
cascadeStep,
|
|
241
|
+
at: new Date(deps.nowMs).toISOString(),
|
|
242
|
+
};
|
|
243
|
+
const reportPath = writeReviewReport(deps.cwd, report);
|
|
244
|
+
deps.ledger("reviewer_fired", {
|
|
245
|
+
goalId: source.goalId,
|
|
246
|
+
kind: source.kind,
|
|
247
|
+
findings: findings.length,
|
|
248
|
+
enqueued,
|
|
249
|
+
proposed,
|
|
250
|
+
cascadeStep,
|
|
251
|
+
report: path.relative(deps.cwd, reportPath),
|
|
252
|
+
});
|
|
253
|
+
deps.notify(
|
|
254
|
+
`Reviewer: ${findings.length} finding(s) — ${enqueued} enqueued to /list, ${proposed} proposed as /goal (${cascadeStep}). Report: ${path.relative(deps.cwd, reportPath)}`,
|
|
255
|
+
"info",
|
|
256
|
+
);
|
|
257
|
+
if (strategic.length > 0) {
|
|
258
|
+
deps.notify(`Reviewer: ${strategic.length} strategic finding(s) need YOUR call — see the report's Strategic section.`, "warning");
|
|
259
|
+
}
|
|
260
|
+
return { fired: true, report, reportPath, enqueued, proposed };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** /glla reviewer menu options, derived from the config — extracted so
|
|
264
|
+
* the menu surface is unit-testable without a pi host. */
|
|
265
|
+
export function reviewerMenuOptions(cfg: ReviewerConfig): string[] {
|
|
266
|
+
return [
|
|
267
|
+
`Enabled — ${cfg.enabled ? "ON" : "OFF"}`,
|
|
268
|
+
`Leverage mode — ${cfg.leverageMode} (bug/refactor findings)`,
|
|
269
|
+
`Fire on goal-complete — ${cfg.fireOn.includes("goal-complete") ? "ON" : "OFF"}`,
|
|
270
|
+
`Fire on list-complete — ${cfg.fireOn.includes("list-complete") ? "ON" : "OFF"}`,
|
|
271
|
+
`Cascade: audit-on-clean — ${cfg.cascade.includes("fire-audit-on-clean") ? "ON" : "OFF"}`,
|
|
272
|
+
`Max findings per review — ${cfg.maxFindingsPerReview}`,
|
|
273
|
+
`Max reviews per day — ${cfg.maxReviewsPerDay}`,
|
|
274
|
+
"Done",
|
|
275
|
+
];
|
|
276
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goal-list-loop-audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|