supipowers 1.2.5 → 1.3.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/package.json +1 -1
- package/skills/context-mode/SKILL.md +3 -7
- package/src/bootstrap.ts +3 -0
- package/src/commands/commit.ts +8 -0
- package/src/commands/fix-pr.ts +20 -49
- package/src/commands/mcp.ts +14 -0
- package/src/commands/model.ts +3 -2
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +8 -9
- package/src/commands/qa.ts +15 -0
- package/src/commands/release.ts +16 -1
- package/src/commands/review.ts +7 -8
- package/src/commands/supi.ts +1 -0
- package/src/config/model-resolver.ts +87 -0
- package/src/context/analyzer.ts +57 -0
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +62 -7
- package/src/context-mode/routing.ts +9 -14
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/fix-pr/config.ts +0 -5
- package/src/fix-pr/prompt-builder.ts +7 -6
- package/src/fix-pr/types.ts +0 -11
- package/src/git/commit.ts +74 -26
- package/src/planning/approval-flow.ts +14 -1
- package/src/platform/omp.ts +5 -2
- package/src/platform/types.ts +2 -1
|
@@ -9,19 +9,255 @@ const CAPS = {
|
|
|
9
9
|
git: 5,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
/** Escape all 5 XML special characters in user data */
|
|
13
|
+
function escapeXML(str: string): string {
|
|
14
|
+
return str
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """)
|
|
19
|
+
.replace(/'/g, "'");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SnapshotOpts {
|
|
23
|
+
compactCount?: number;
|
|
24
|
+
searchTool?: string;
|
|
25
|
+
searchAvailable?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
/** Build a resume snapshot from tracked events for a session */
|
|
13
|
-
export function buildResumeSnapshot(
|
|
29
|
+
export function buildResumeSnapshot(
|
|
30
|
+
eventStore: EventStore,
|
|
31
|
+
sessionId: string,
|
|
32
|
+
opts?: SnapshotOpts,
|
|
33
|
+
): string {
|
|
14
34
|
const counts = eventStore.getEventCounts(sessionId);
|
|
15
35
|
const hasAnyEvents = Object.values(counts).some((c) => c > 0);
|
|
16
36
|
if (!hasAnyEvents) return "";
|
|
17
37
|
|
|
38
|
+
if (opts?.searchAvailable) {
|
|
39
|
+
return buildReferenceSnapshot(eventStore, sessionId, opts);
|
|
40
|
+
}
|
|
41
|
+
return buildFallbackSnapshot(eventStore, sessionId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Reference-based format (context-mode MCP available)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function buildReferenceSnapshot(eventStore: EventStore, sessionId: string, opts: SnapshotOpts): string {
|
|
49
|
+
const compactCount = opts.compactCount ?? 0;
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
const sections: string[] = [
|
|
52
|
+
`<session_knowledge compact_count="${compactCount}" generated_at="${escapeXML(now)}">`,
|
|
53
|
+
" <how_to_search>",
|
|
54
|
+
" Each section below contains a summary of prior work.",
|
|
55
|
+
" For FULL DETAILS, run the exact tool call shown under each section.",
|
|
56
|
+
" Do NOT ask the user to re-explain prior work. Search first.",
|
|
57
|
+
" </how_to_search>",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
let hasSections = false;
|
|
61
|
+
|
|
62
|
+
// --- rules ---
|
|
63
|
+
const ruleEvents = eventStore.getEvents(sessionId, { categories: ["rule"] });
|
|
64
|
+
if (ruleEvents.length > 0) {
|
|
65
|
+
const files = new Set<string>();
|
|
66
|
+
for (const r of ruleEvents) {
|
|
67
|
+
const data = safeParse(r.data);
|
|
68
|
+
const file = typeof data?.file === "string" ? data.file : typeof data?.path === "string" ? data.path : null;
|
|
69
|
+
if (file) files.add(file);
|
|
70
|
+
}
|
|
71
|
+
if (files.size > 0) {
|
|
72
|
+
const fileList = [...files];
|
|
73
|
+
sections.push("");
|
|
74
|
+
sections.push(` <rules>`);
|
|
75
|
+
sections.push(` Loaded ${fileList.length} project rule files: ${fileList.map(escapeXML).join(", ")}`);
|
|
76
|
+
sections.push(` For full details:`);
|
|
77
|
+
sections.push(` ctx_search(queries: [${fileList.map((f) => `"${escapeXML(f)}"`).join(", ")}], source: "session-events")`);
|
|
78
|
+
sections.push(` </rules>`);
|
|
79
|
+
hasSections = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- files ---
|
|
84
|
+
const fileEvents = eventStore.getEvents(sessionId, { categories: ["file"], limit: 200 });
|
|
85
|
+
if (fileEvents.length > 0) {
|
|
86
|
+
const edited = new Set<string>();
|
|
87
|
+
const read = new Set<string>();
|
|
88
|
+
for (const f of fileEvents) {
|
|
89
|
+
const data = safeParse(f.data);
|
|
90
|
+
const p = typeof data?.path === "string" ? data.path : null;
|
|
91
|
+
if (!p) continue;
|
|
92
|
+
if (data?.op === "edit" || data?.op === "write") edited.add(p);
|
|
93
|
+
else if (data?.op === "read") read.add(p);
|
|
94
|
+
}
|
|
95
|
+
if (edited.size > 0 || read.size > 0) {
|
|
96
|
+
sections.push("");
|
|
97
|
+
sections.push(` <files count="${edited.size + read.size}">`);
|
|
98
|
+
if (edited.size > 0) sections.push(` Edited: ${[...edited].map(escapeXML).join(", ")}`);
|
|
99
|
+
if (read.size > 0) sections.push(` Read: ${[...read].map(escapeXML).join(", ")}`);
|
|
100
|
+
const queryPaths = [...edited, ...read].slice(0, 5);
|
|
101
|
+
sections.push(` For full details:`);
|
|
102
|
+
sections.push(` ctx_search(queries: [${queryPaths.map((p) => `"${escapeXML(p)}"`).join(", ")}], source: "session-events")`);
|
|
103
|
+
sections.push(` </files>`);
|
|
104
|
+
hasSections = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- tasks ---
|
|
109
|
+
const tasks = eventStore.getEvents(sessionId, { categories: ["task"], limit: CAPS.tasks });
|
|
110
|
+
if (tasks.length > 0) {
|
|
111
|
+
const summaries: string[] = [];
|
|
112
|
+
for (const t of tasks) {
|
|
113
|
+
const data = safeParse(t.data);
|
|
114
|
+
const content = extractTaskContent(data);
|
|
115
|
+
if (content) summaries.push(escapeXML(content.slice(0, 100)));
|
|
116
|
+
}
|
|
117
|
+
if (summaries.length > 0) {
|
|
118
|
+
sections.push("");
|
|
119
|
+
sections.push(` <tasks>`);
|
|
120
|
+
for (const s of summaries) sections.push(` ${s}`);
|
|
121
|
+
sections.push(` For full details:`);
|
|
122
|
+
sections.push(` ctx_search(queries: ["task", "todo"], source: "session-events")`);
|
|
123
|
+
sections.push(` </tasks>`);
|
|
124
|
+
hasSections = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- decisions ---
|
|
129
|
+
const decisions = eventStore.getEvents(sessionId, { categories: ["decision"], limit: CAPS.decisions });
|
|
130
|
+
if (decisions.length > 0) {
|
|
131
|
+
const summaries: string[] = [];
|
|
132
|
+
for (const d of decisions) {
|
|
133
|
+
const data = safeParse(d.data);
|
|
134
|
+
const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
|
|
135
|
+
if (prompt) summaries.push(escapeXML(prompt));
|
|
136
|
+
}
|
|
137
|
+
if (summaries.length > 0) {
|
|
138
|
+
sections.push("");
|
|
139
|
+
sections.push(` <decisions>`);
|
|
140
|
+
for (const s of summaries) sections.push(` ${s}`);
|
|
141
|
+
sections.push(` </decisions>`);
|
|
142
|
+
hasSections = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- errors ---
|
|
147
|
+
const errors = eventStore.getEvents(sessionId, { categories: ["error"], limit: CAPS.errors });
|
|
148
|
+
if (errors.length > 0) {
|
|
149
|
+
const summaries: string[] = [];
|
|
150
|
+
for (const e of errors) {
|
|
151
|
+
const data = safeParse(e.data);
|
|
152
|
+
const summary = formatErrorSummary(data);
|
|
153
|
+
if (summary) summaries.push(escapeXML(summary.slice(0, 150)));
|
|
154
|
+
}
|
|
155
|
+
if (summaries.length > 0) {
|
|
156
|
+
sections.push("");
|
|
157
|
+
sections.push(` <errors>`);
|
|
158
|
+
for (const s of summaries) sections.push(` ${s}`);
|
|
159
|
+
sections.push(` </errors>`);
|
|
160
|
+
hasSections = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- git ---
|
|
165
|
+
const gitEvents = eventStore.getEvents(sessionId, { categories: ["git"], limit: CAPS.git });
|
|
166
|
+
if (gitEvents.length > 0) {
|
|
167
|
+
const summaries: string[] = [];
|
|
168
|
+
for (const g of gitEvents) {
|
|
169
|
+
const data = safeParse(g.data);
|
|
170
|
+
const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
|
|
171
|
+
if (cmd) summaries.push(escapeXML(cmd));
|
|
172
|
+
}
|
|
173
|
+
if (summaries.length > 0) {
|
|
174
|
+
sections.push("");
|
|
175
|
+
sections.push(` <git>`);
|
|
176
|
+
for (const s of summaries) sections.push(` ${s}`);
|
|
177
|
+
sections.push(` </git>`);
|
|
178
|
+
hasSections = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- skills ---
|
|
183
|
+
const skillEvents = eventStore.getEvents(sessionId, { categories: ["skill"] });
|
|
184
|
+
if (skillEvents.length > 0) {
|
|
185
|
+
const names = new Set<string>();
|
|
186
|
+
for (const s of skillEvents) {
|
|
187
|
+
const data = safeParse(s.data);
|
|
188
|
+
const name = typeof data?.name === "string" ? data.name : typeof data?.skill === "string" ? data.skill : null;
|
|
189
|
+
if (name) names.add(name);
|
|
190
|
+
}
|
|
191
|
+
if (names.size > 0) {
|
|
192
|
+
sections.push("");
|
|
193
|
+
sections.push(` <skills>`);
|
|
194
|
+
sections.push(` Activated: ${[...names].map(escapeXML).join(", ")}`);
|
|
195
|
+
sections.push(` </skills>`);
|
|
196
|
+
hasSections = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- intent ---
|
|
201
|
+
const intentEvents = eventStore.getEvents(sessionId, { categories: ["intent"], limit: 1 });
|
|
202
|
+
if (intentEvents.length > 0) {
|
|
203
|
+
const data = safeParse(intentEvents[0].data);
|
|
204
|
+
const mode = typeof data?.mode === "string" ? data.mode : typeof data?.intent === "string" ? data.intent : null;
|
|
205
|
+
if (mode) {
|
|
206
|
+
sections.push("");
|
|
207
|
+
sections.push(` <intent>Session mode: ${escapeXML(mode)}</intent>`);
|
|
208
|
+
hasSections = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- env ---
|
|
213
|
+
const envEvents = eventStore.getEvents(sessionId, { categories: ["env"] });
|
|
214
|
+
if (envEvents.length > 0) {
|
|
215
|
+
const details: string[] = [];
|
|
216
|
+
for (const e of envEvents) {
|
|
217
|
+
const data = safeParse(e.data);
|
|
218
|
+
const detail = typeof data?.detail === "string" ? data.detail : typeof data?.env === "string" ? data.env : null;
|
|
219
|
+
if (detail) details.push(escapeXML(detail.slice(0, 100)));
|
|
220
|
+
}
|
|
221
|
+
if (details.length > 0) {
|
|
222
|
+
sections.push("");
|
|
223
|
+
sections.push(` <env>`);
|
|
224
|
+
for (const d of details) sections.push(` ${d}`);
|
|
225
|
+
sections.push(` </env>`);
|
|
226
|
+
hasSections = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- cwd ---
|
|
231
|
+
const cwdEvents = eventStore.getEvents(sessionId, { categories: ["cwd"], limit: 1 });
|
|
232
|
+
if (cwdEvents.length > 0) {
|
|
233
|
+
const data = safeParse(cwdEvents[0].data);
|
|
234
|
+
const cwd = typeof data?.cwd === "string" ? data.cwd : typeof data?.path === "string" ? data.path : null;
|
|
235
|
+
if (cwd) {
|
|
236
|
+
sections.push("");
|
|
237
|
+
sections.push(` <cwd>${escapeXML(cwd)}</cwd>`);
|
|
238
|
+
hasSections = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
sections.push("</session_knowledge>");
|
|
243
|
+
|
|
244
|
+
if (!hasSections) return "";
|
|
245
|
+
|
|
246
|
+
return sections.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Fallback inline-truncated format (no context-mode MCP)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
function buildFallbackSnapshot(eventStore: EventStore, sessionId: string): string {
|
|
18
254
|
const sections: string[] = ["<session_knowledge>"];
|
|
19
255
|
|
|
20
256
|
// Last request
|
|
21
257
|
const prompts = eventStore.getEvents(sessionId, { categories: ["prompt"], limit: 1 });
|
|
22
258
|
if (prompts.length > 0) {
|
|
23
259
|
const data = safeParse(prompts[0].data);
|
|
24
|
-
const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 200) : "";
|
|
260
|
+
const prompt = typeof data?.prompt === "string" ? escapeXML(data.prompt.slice(0, 200)) : "";
|
|
25
261
|
if (prompt) {
|
|
26
262
|
sections.push(` <last_request>${prompt}</last_request>`);
|
|
27
263
|
}
|
|
@@ -34,7 +270,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
|
|
|
34
270
|
for (const t of tasks) {
|
|
35
271
|
const data = safeParse(t.data);
|
|
36
272
|
const content = extractTaskContent(data);
|
|
37
|
-
if (content) sections.push(` - ${content.slice(0, 100)}`);
|
|
273
|
+
if (content) sections.push(` - ${escapeXML(content.slice(0, 100))}`);
|
|
38
274
|
}
|
|
39
275
|
sections.push(" </pending_tasks>");
|
|
40
276
|
}
|
|
@@ -45,7 +281,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
|
|
|
45
281
|
sections.push(" <key_decisions>");
|
|
46
282
|
for (const d of decisions) {
|
|
47
283
|
const data = safeParse(d.data);
|
|
48
|
-
const prompt = typeof data?.prompt === "string" ? data.prompt.slice(0, 100) : "";
|
|
284
|
+
const prompt = typeof data?.prompt === "string" ? escapeXML(data.prompt.slice(0, 100)) : "";
|
|
49
285
|
if (prompt) sections.push(` - ${prompt}`);
|
|
50
286
|
}
|
|
51
287
|
sections.push(" </key_decisions>");
|
|
@@ -63,7 +299,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
|
|
|
63
299
|
if (modifiedPaths.size > 0) {
|
|
64
300
|
sections.push(" <files_modified>");
|
|
65
301
|
const paths = [...modifiedPaths].slice(0, CAPS.files);
|
|
66
|
-
for (const p of paths) sections.push(` - ${p}`);
|
|
302
|
+
for (const p of paths) sections.push(` - ${escapeXML(p)}`);
|
|
67
303
|
sections.push(" </files_modified>");
|
|
68
304
|
}
|
|
69
305
|
|
|
@@ -74,7 +310,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
|
|
|
74
310
|
for (const e of errors) {
|
|
75
311
|
const data = safeParse(e.data);
|
|
76
312
|
const summary = formatErrorSummary(data);
|
|
77
|
-
if (summary) sections.push(` - ${summary.slice(0, 150)}`);
|
|
313
|
+
if (summary) sections.push(` - ${escapeXML(summary.slice(0, 150))}`);
|
|
78
314
|
}
|
|
79
315
|
sections.push(" </recent_errors>");
|
|
80
316
|
}
|
|
@@ -85,7 +321,7 @@ export function buildResumeSnapshot(eventStore: EventStore, sessionId: string):
|
|
|
85
321
|
sections.push(" <git_state>");
|
|
86
322
|
for (const g of gitEvents) {
|
|
87
323
|
const data = safeParse(g.data);
|
|
88
|
-
const cmd = typeof data?.command === "string" ? data.command.slice(0, 100) : "";
|
|
324
|
+
const cmd = typeof data?.command === "string" ? escapeXML(data.command.slice(0, 100)) : "";
|
|
89
325
|
if (cmd) sections.push(` - ${cmd}`);
|
|
90
326
|
}
|
|
91
327
|
sections.push(" </git_state>");
|
package/src/fix-pr/config.ts
CHANGED
|
@@ -13,11 +13,6 @@ export const DEFAULT_FIX_PR_CONFIG: FixPrConfig = {
|
|
|
13
13
|
reviewer: { type: "none", triggerMethod: null },
|
|
14
14
|
commentPolicy: "answer-selective",
|
|
15
15
|
loop: { delaySeconds: 180, maxIterations: 3 },
|
|
16
|
-
models: {
|
|
17
|
-
orchestrator: { provider: "anthropic", model: "claude-opus-4-6", tier: "high" },
|
|
18
|
-
planner: { provider: "anthropic", model: "claude-opus-4-6", tier: "high" },
|
|
19
|
-
fixer: { provider: "anthropic", model: "claude-sonnet-4-6", tier: "low" },
|
|
20
|
-
},
|
|
21
16
|
};
|
|
22
17
|
|
|
23
18
|
export function loadFixPrConfig(paths: PlatformPaths, cwd: string): FixPrConfig | null {
|
|
@@ -10,6 +10,8 @@ export interface FixPrPromptOptions {
|
|
|
10
10
|
config: FixPrConfig;
|
|
11
11
|
iteration: number;
|
|
12
12
|
skillContent: string;
|
|
13
|
+
/** Resolved model ID for sub-agent tasks (planner, fixer roles). */
|
|
14
|
+
taskModel: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function buildReplyInstructions(config: FixPrConfig): string {
|
|
@@ -47,8 +49,8 @@ function buildReplyInstructions(config: FixPrConfig): string {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): string {
|
|
50
|
-
const { prNumber, repo, comments, sessionDir, scriptsDir, config, iteration, skillContent } = options;
|
|
51
|
-
const { loop,
|
|
52
|
+
const { prNumber, repo, comments, sessionDir, scriptsDir, config, iteration, skillContent, taskModel } = options;
|
|
53
|
+
const { loop, reviewer } = config;
|
|
52
54
|
const maxIter = loop.maxIterations;
|
|
53
55
|
const delay = loop.delaySeconds;
|
|
54
56
|
|
|
@@ -190,11 +192,10 @@ export function buildFixPrOrchestratorPrompt(options: FixPrPromptOptions): strin
|
|
|
190
192
|
sections.push(
|
|
191
193
|
"## Model Guidance",
|
|
192
194
|
"",
|
|
193
|
-
`- **Orchestrator** (
|
|
194
|
-
`- **Planner** (
|
|
195
|
-
`- **Fixer** (code changes): ${models.fixer.model} (${models.fixer.tier} tier) — focused execution`,
|
|
195
|
+
`- **Orchestrator** (this session): handles assessment & grouping`,
|
|
196
|
+
`- **Planner & Fixer** (sub-agents): use model \`${taskModel}\``,
|
|
196
197
|
"",
|
|
197
|
-
"
|
|
198
|
+
"Sub-agents inherit the task model for planning and code changes.",
|
|
198
199
|
);
|
|
199
200
|
|
|
200
201
|
return sections.join("\n");
|
package/src/fix-pr/types.ts
CHANGED
|
@@ -4,12 +4,6 @@ export type ReviewerType = "coderabbit" | "copilot" | "gemini" | "none";
|
|
|
4
4
|
/** How to handle comment replies */
|
|
5
5
|
export type CommentReplyPolicy = "answer-all" | "answer-selective" | "no-answer";
|
|
6
6
|
|
|
7
|
-
/** Model preference for a specific role */
|
|
8
|
-
export interface ModelPref {
|
|
9
|
-
provider: string;
|
|
10
|
-
model: string;
|
|
11
|
-
tier: "low" | "high";
|
|
12
|
-
}
|
|
13
7
|
|
|
14
8
|
/** Per-repo fix-pr configuration */
|
|
15
9
|
export interface FixPrConfig {
|
|
@@ -22,11 +16,6 @@ export interface FixPrConfig {
|
|
|
22
16
|
delaySeconds: number;
|
|
23
17
|
maxIterations: number;
|
|
24
18
|
};
|
|
25
|
-
models: {
|
|
26
|
-
orchestrator: ModelPref;
|
|
27
|
-
planner: ModelPref;
|
|
28
|
-
fixer: ModelPref;
|
|
29
|
-
};
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/** A PR review comment from GitHub API */
|
package/src/git/commit.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { validateCommitMessage } from "./commit-msg.js";
|
|
|
10
10
|
import { getWorkingTreeStatus } from "./status.js";
|
|
11
11
|
import { discoverCommitConventions } from "./conventions.js";
|
|
12
12
|
import { notifyInfo, notifyError, notifySuccess } from "../notifications/renderer.js";
|
|
13
|
+
import { modelRegistry } from "../config/model-registry-instance.js";
|
|
14
|
+
import { resolveModelForAction, createModelBridge } from "../config/model-resolver.js";
|
|
15
|
+
import { loadModelConfig } from "../config/model-config.js";
|
|
13
16
|
|
|
14
17
|
// ── Public types ───────────────────────────────────────────
|
|
15
18
|
|
|
@@ -213,6 +216,7 @@ function createProgress(ctx: any) {
|
|
|
213
216
|
dispose() {
|
|
214
217
|
stopTimer();
|
|
215
218
|
ctx.ui.setStatus?.(STATUS_KEY, undefined);
|
|
219
|
+
ctx.ui.setStatus?.("supi-model", undefined);
|
|
216
220
|
ctx.ui.setWidget?.(WIDGET_KEY, undefined);
|
|
217
221
|
},
|
|
218
222
|
};
|
|
@@ -292,8 +296,26 @@ export async function analyzeAndCommit(
|
|
|
292
296
|
|
|
293
297
|
if (platform.capabilities.agentSessions) {
|
|
294
298
|
progress.activate(4, `${fileList.length} file(s)`);
|
|
295
|
-
|
|
299
|
+
// Resolve the commit sub-agent model from config (falls back to session default)
|
|
300
|
+
const modelCfg = loadModelConfig(platform.paths, cwd);
|
|
301
|
+
const bridge = createModelBridge(platform);
|
|
302
|
+
const commitModel = resolveModelForAction("commit", modelRegistry, modelCfg, bridge);
|
|
303
|
+
|
|
304
|
+
// Show model override in status bar if not using the main session model
|
|
305
|
+
if (commitModel.source !== "main" && commitModel.model) {
|
|
306
|
+
const sourceLabel =
|
|
307
|
+
commitModel.source === "action" ? "configured for commit" :
|
|
308
|
+
commitModel.source === "default" ? "supipowers default" :
|
|
309
|
+
"harness role";
|
|
310
|
+
let detail = sourceLabel;
|
|
311
|
+
if (commitModel.thinkingLevel) {
|
|
312
|
+
detail += ` \u00b7 ${commitModel.thinkingLevel} thinking`;
|
|
313
|
+
}
|
|
314
|
+
ctx.ui?.setStatus?.("supi-model", `Model: ${commitModel.model} (${detail})`);
|
|
315
|
+
}
|
|
316
|
+
plan = await tryAgentPlan(platform, cwd, prompt, commitModel.model);
|
|
296
317
|
if (plan) {
|
|
318
|
+
plan = validatePlanFiles(plan, fileList);
|
|
297
319
|
progress.complete(4, `${plan.commits.length} commit(s)`);
|
|
298
320
|
} else {
|
|
299
321
|
progress.skip(4, "unavailable");
|
|
@@ -332,7 +354,7 @@ export async function analyzeAndCommit(
|
|
|
332
354
|
|
|
333
355
|
// 7. Execute commits
|
|
334
356
|
progress.activate(6, `0/${plan.commits.length}`);
|
|
335
|
-
return executeCommitPlan(platform, ctx, cwd, plan, progress);
|
|
357
|
+
return executeCommitPlan(platform, ctx, cwd, plan, fileList, progress);
|
|
336
358
|
} finally {
|
|
337
359
|
// Always clean up, even on unexpected errors
|
|
338
360
|
progress.dispose();
|
|
@@ -345,10 +367,11 @@ async function tryAgentPlan(
|
|
|
345
367
|
platform: Platform,
|
|
346
368
|
cwd: string,
|
|
347
369
|
prompt: string,
|
|
370
|
+
model?: string,
|
|
348
371
|
): Promise<CommitPlan | null> {
|
|
349
372
|
let session: Awaited<ReturnType<Platform["createAgentSession"]>> | null = null;
|
|
350
373
|
try {
|
|
351
|
-
session = await platform.createAgentSession({ cwd, hasUI: false });
|
|
374
|
+
session = await platform.createAgentSession({ cwd, hasUI: false, ...(model ? { model } : {}) });
|
|
352
375
|
|
|
353
376
|
const agentDone = new Promise<void>((resolve) => {
|
|
354
377
|
session!.subscribe((event: any) => {
|
|
@@ -426,6 +449,26 @@ async function manualFallback(
|
|
|
426
449
|
return { committed: 1, messages: [message] };
|
|
427
450
|
}
|
|
428
451
|
|
|
452
|
+
// ── Plan validation ────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Filter an AI-generated commit plan against the actual staged file list.
|
|
456
|
+
* Removes hallucinated paths that aren't staged, and drops empty groups.
|
|
457
|
+
* Falls back to the original plan if filtering would leave nothing.
|
|
458
|
+
*/
|
|
459
|
+
export function validatePlanFiles(plan: CommitPlan, stagedFiles: string[]): CommitPlan {
|
|
460
|
+
const stagedSet = new Set(stagedFiles);
|
|
461
|
+
const validCommits = plan.commits
|
|
462
|
+
.map((group) => ({
|
|
463
|
+
...group,
|
|
464
|
+
files: group.files.filter((f) => stagedSet.has(f)),
|
|
465
|
+
}))
|
|
466
|
+
.filter((group) => group.files.length > 0);
|
|
467
|
+
|
|
468
|
+
return validCommits.length > 0 ? { commits: validCommits } : plan;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
429
472
|
// ── Commit execution ───────────────────────────────────────
|
|
430
473
|
|
|
431
474
|
async function executeCommitPlan(
|
|
@@ -433,38 +476,45 @@ async function executeCommitPlan(
|
|
|
433
476
|
ctx: any,
|
|
434
477
|
cwd: string,
|
|
435
478
|
plan: CommitPlan,
|
|
479
|
+
stagedFiles: string[],
|
|
436
480
|
progress: ReturnType<typeof createProgress>,
|
|
437
481
|
): Promise<CommitResult | null> {
|
|
438
482
|
const exec = platform.exec.bind(platform);
|
|
439
483
|
const committedMessages: string[] = [];
|
|
440
484
|
|
|
485
|
+
// Snapshot the full index as a tree object. This lets us restore the
|
|
486
|
+
// staging area for each commit group via `git read-tree` — which reads
|
|
487
|
+
// from git's object store and never consults .gitignore.
|
|
488
|
+
const writeTreeResult = await exec("git", ["write-tree"], { cwd });
|
|
489
|
+
if (writeTreeResult.code !== 0) {
|
|
490
|
+
progress.dispose();
|
|
491
|
+
notifyError(ctx, "Commit failed", "Could not snapshot index (git write-tree)");
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
const savedTree = writeTreeResult.stdout.trim();
|
|
495
|
+
|
|
441
496
|
for (let i = 0; i < plan.commits.length; i++) {
|
|
442
497
|
const group = plan.commits[i];
|
|
443
498
|
const header = formatCommitMessage(group).split("\n")[0];
|
|
444
499
|
progress.detail(`${i + 1}/${plan.commits.length}: ${header}`);
|
|
445
500
|
|
|
446
|
-
//
|
|
447
|
-
await exec("git", ["
|
|
501
|
+
// Restore the full saved index (no gitignore involvement)
|
|
502
|
+
await exec("git", ["read-tree", savedTree], { cwd });
|
|
448
503
|
|
|
449
|
-
//
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const reason = addResult.stderr?.trim() || "git add returned non-zero";
|
|
455
|
-
return reportPartialFailure(ctx, exec, cwd, committedMessages, {
|
|
456
|
-
step: `Commit ${i + 1}/${plan.commits.length}`,
|
|
457
|
-
error: `Could not stage files (${failedFiles}): ${reason}`,
|
|
458
|
-
});
|
|
504
|
+
// Unstage everything NOT in this group
|
|
505
|
+
const groupSet = new Set(group.files);
|
|
506
|
+
const filesToUnstage = stagedFiles.filter((f) => !groupSet.has(f));
|
|
507
|
+
if (filesToUnstage.length > 0) {
|
|
508
|
+
await exec("git", ["reset", "HEAD", "--", ...filesToUnstage], { cwd });
|
|
459
509
|
}
|
|
460
510
|
|
|
461
|
-
// Build commit message
|
|
462
511
|
const message = formatCommitMessage(group);
|
|
463
|
-
|
|
464
512
|
const commitResult = await commitStaged(exec, cwd, message);
|
|
465
513
|
if (!commitResult.success) {
|
|
466
514
|
progress.dispose();
|
|
467
|
-
|
|
515
|
+
// Restore full staging area so the user isn't left with a partial index
|
|
516
|
+
await exec("git", ["read-tree", savedTree], { cwd });
|
|
517
|
+
return reportPartialFailure(ctx, committedMessages, {
|
|
468
518
|
step: `Commit ${i + 1}/${plan.commits.length}`,
|
|
469
519
|
error: commitResult.error!,
|
|
470
520
|
});
|
|
@@ -473,6 +523,10 @@ async function executeCommitPlan(
|
|
|
473
523
|
committedMessages.push(message);
|
|
474
524
|
}
|
|
475
525
|
|
|
526
|
+
// Restore the saved index so any staged files NOT in the plan remain staged.
|
|
527
|
+
// Files already committed now match HEAD, so they appear as not-staged.
|
|
528
|
+
await exec("git", ["read-tree", savedTree], { cwd });
|
|
529
|
+
|
|
476
530
|
progress.complete(6, `${committedMessages.length} done`);
|
|
477
531
|
progress.dispose();
|
|
478
532
|
notifySuccess(
|
|
@@ -486,18 +540,12 @@ async function executeCommitPlan(
|
|
|
486
540
|
|
|
487
541
|
/**
|
|
488
542
|
* Report a mid-plan failure with context on what succeeded and what failed.
|
|
489
|
-
* Re-stages remaining files so the user isn't left with a half-reset index.
|
|
490
543
|
*/
|
|
491
|
-
|
|
544
|
+
function reportPartialFailure(
|
|
492
545
|
ctx: any,
|
|
493
|
-
exec: Platform["exec"],
|
|
494
|
-
cwd: string,
|
|
495
546
|
committedMessages: string[],
|
|
496
547
|
failure: { step: string; error: string },
|
|
497
|
-
):
|
|
498
|
-
// Re-stage everything so the user isn't stuck with a partial index
|
|
499
|
-
await exec("git", ["add", "-A"], { cwd });
|
|
500
|
-
|
|
548
|
+
): CommitResult | null {
|
|
501
549
|
const lines: string[] = [];
|
|
502
550
|
lines.push(`Failed at ${failure.step}: ${failure.error}`);
|
|
503
551
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Platform } from "../platform/types.js";
|
|
2
|
+
import type { ResolvedModel } from "../types.js";
|
|
3
|
+
import { applyModelOverride } from "../config/model-resolver.js";
|
|
2
4
|
import { listPlans, readPlanFile } from "../storage/plans.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -14,17 +16,21 @@ let plansBefore: string[] = [];
|
|
|
14
16
|
let planCwd: string = "";
|
|
15
17
|
/** newSession function captured from the command context at plan start. */
|
|
16
18
|
let capturedNewSession: ((options?: any) => Promise<{ cancelled: boolean }>) | null = null;
|
|
19
|
+
/** Resolved model for plan action — re-applied on execution handoff. */
|
|
20
|
+
let capturedResolvedModel: ResolvedModel | null = null;
|
|
17
21
|
|
|
18
22
|
/** Mark planning as started (called by plan command after sending steer). */
|
|
19
23
|
export function startPlanTracking(
|
|
20
24
|
cwd: string,
|
|
21
25
|
paths: any,
|
|
22
26
|
newSession?: (options?: any) => Promise<{ cancelled: boolean }>,
|
|
27
|
+
resolvedModel?: ResolvedModel,
|
|
23
28
|
): void {
|
|
24
29
|
planningActive = true;
|
|
25
30
|
planCwd = cwd;
|
|
26
31
|
plansBefore = listPlans(paths, cwd);
|
|
27
32
|
capturedNewSession = newSession ?? null;
|
|
33
|
+
capturedResolvedModel = resolvedModel ?? null;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/** Cancel plan tracking (e.g., session change). */
|
|
@@ -33,6 +39,7 @@ export function cancelPlanTracking(): void {
|
|
|
33
39
|
plansBefore = [];
|
|
34
40
|
planCwd = "";
|
|
35
41
|
capturedNewSession = null;
|
|
42
|
+
capturedResolvedModel = null;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/** Whether a planning session is currently active. */
|
|
@@ -89,10 +96,16 @@ async function executeApproveFlow(
|
|
|
89
96
|
): Promise<void> {
|
|
90
97
|
const prompt = buildExecutionPrompt(planContent, planPath);
|
|
91
98
|
|
|
99
|
+
// Re-apply the plan model override for the execution turn.
|
|
100
|
+
// The planning turn's restore hook already fired (model reverted to default).
|
|
101
|
+
// We must switch again so the execution LLM turn uses the configured model.
|
|
102
|
+
if (capturedResolvedModel) {
|
|
103
|
+
await applyModelOverride(platform, ctx, "plan", capturedResolvedModel);
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
if (capturedNewSession) {
|
|
93
107
|
const result = await capturedNewSession();
|
|
94
108
|
if (result?.cancelled) {
|
|
95
|
-
// User dismissed the new-session prompt — keep plan state intact.
|
|
96
109
|
ctx.ui.notify("Session start cancelled. Plan saved; run /supi:plan again to execute.");
|
|
97
110
|
return;
|
|
98
111
|
}
|
package/src/platform/omp.ts
CHANGED
|
@@ -17,8 +17,11 @@ export function createOmpAdapter(api: any): Platform {
|
|
|
17
17
|
sendUserMessage: (text: string) => api.sendUserMessage(text),
|
|
18
18
|
registerMessageRenderer: (type, fn) => api.registerMessageRenderer(type, fn),
|
|
19
19
|
|
|
20
|
-
setModel(model:
|
|
21
|
-
api.setModel(model);
|
|
20
|
+
async setModel(model: any): Promise<boolean> {
|
|
21
|
+
return api.setModel(model);
|
|
22
|
+
},
|
|
23
|
+
setThinkingLevel(level: string, persist?: boolean): void {
|
|
24
|
+
api.setThinkingLevel?.(level, persist);
|
|
22
25
|
},
|
|
23
26
|
getCurrentModel(): string {
|
|
24
27
|
return api.getCurrentModel?.() ?? "unknown";
|
package/src/platform/types.ts
CHANGED
|
@@ -138,7 +138,8 @@ export interface Platform {
|
|
|
138
138
|
registerMessageRenderer<T>(type: string, renderer: any): void;
|
|
139
139
|
|
|
140
140
|
// Model access
|
|
141
|
-
setModel?(model:
|
|
141
|
+
setModel?(model: any): Promise<boolean>;
|
|
142
|
+
setThinkingLevel?(level: string, persist?: boolean): void;
|
|
142
143
|
getCurrentModel?(): string;
|
|
143
144
|
getModelForRole?(role: string): string | null;
|
|
144
145
|
|