panopticon-cli 0.4.0 → 0.4.4
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 +442 -109
- package/dist/{agents-RCAPFJG7.js → agents-B5NRTVHK.js} +3 -4
- package/dist/chunk-7HHDVXBM.js +349 -0
- package/dist/chunk-7HHDVXBM.js.map +1 -0
- package/dist/chunk-H45CLB7E.js +2044 -0
- package/dist/chunk-H45CLB7E.js.map +1 -0
- package/dist/{chunk-7BGFIAWQ.js → chunk-ITI4IC5A.js} +2 -2
- package/dist/cli/index.js +1525 -432
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/public/assets/index-BDd8hGYb.css +32 -0
- package/dist/dashboard/public/assets/index-sFwLPko-.js +556 -0
- package/dist/dashboard/public/index.html +3 -2
- package/dist/dashboard/server.js +21702 -14954
- package/dist/index.d.ts +125 -1
- package/dist/index.js +30 -5
- package/package.json +9 -2
- package/templates/claude-md/sections/warnings.md +27 -2
- package/templates/context/CLAUDE.md.template +22 -0
- package/templates/context/REOPEN_PROMPT.md.template +75 -0
- package/dist/chunk-4BIZ4OVN.js +0 -827
- package/dist/chunk-4BIZ4OVN.js.map +0 -1
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-U4LCHEVU.js +0 -116
- package/dist/chunk-U4LCHEVU.js.map +0 -1
- package/dist/dashboard/public/assets/index-BZXQno9X.js +0 -540
- package/dist/dashboard/public/assets/index-BtAxF_yl.css +0 -32
- package/dist/js-yaml-DLUPUHNL.js +0 -2648
- package/dist/js-yaml-DLUPUHNL.js.map +0 -1
- /package/dist/{agents-RCAPFJG7.js.map → agents-B5NRTVHK.js.map} +0 -0
- /package/dist/{chunk-7BGFIAWQ.js.map → chunk-ITI4IC5A.js.map} +0 -0
|
@@ -0,0 +1,2044 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENTS_DIR,
|
|
3
|
+
getProviderEnv,
|
|
4
|
+
getProviderForModel,
|
|
5
|
+
loadSettings
|
|
6
|
+
} from "./chunk-7HHDVXBM.js";
|
|
7
|
+
|
|
8
|
+
// src/lib/agents.ts
|
|
9
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, appendFileSync, unlinkSync as unlinkSync2 } from "fs";
|
|
10
|
+
import { join as join4 } from "path";
|
|
11
|
+
import { homedir as homedir2 } from "os";
|
|
12
|
+
import { exec } from "child_process";
|
|
13
|
+
import { promisify } from "util";
|
|
14
|
+
|
|
15
|
+
// src/lib/tmux.ts
|
|
16
|
+
import { execSync } from "child_process";
|
|
17
|
+
import { writeFileSync, chmodSync } from "fs";
|
|
18
|
+
function listSessions() {
|
|
19
|
+
try {
|
|
20
|
+
const output = execSync('tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}|#{session_windows}"', {
|
|
21
|
+
encoding: "utf8"
|
|
22
|
+
});
|
|
23
|
+
return output.trim().split("\n").filter(Boolean).map((line) => {
|
|
24
|
+
const [name, created, attached, windows] = line.split("|");
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
created: new Date(parseInt(created) * 1e3),
|
|
28
|
+
attached: attached === "1",
|
|
29
|
+
windows: parseInt(windows)
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function sessionExists(name) {
|
|
37
|
+
try {
|
|
38
|
+
execSync(`tmux has-session -t ${name} 2>/dev/null`);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function createSession(name, cwd, initialCommand, options) {
|
|
45
|
+
const escapedCwd = cwd.replace(/"/g, '\\"');
|
|
46
|
+
let envFlags = "";
|
|
47
|
+
if (options?.env) {
|
|
48
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
49
|
+
envFlags += ` -e ${key}="${value.replace(/"/g, '\\"')}"`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (initialCommand && (initialCommand.includes("`") || initialCommand.includes("\n") || initialCommand.length > 500)) {
|
|
53
|
+
execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags}`);
|
|
54
|
+
execSync("sleep 0.5");
|
|
55
|
+
const tmpFile = `/tmp/pan-cmd-${name}.sh`;
|
|
56
|
+
writeFileSync(tmpFile, initialCommand);
|
|
57
|
+
chmodSync(tmpFile, "755");
|
|
58
|
+
execSync(`tmux send-keys -t ${name} "bash ${tmpFile}"`);
|
|
59
|
+
execSync(`tmux send-keys -t ${name} C-m`);
|
|
60
|
+
} else if (initialCommand) {
|
|
61
|
+
const cmd = `tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags} "${initialCommand.replace(/"/g, '\\"')}"`;
|
|
62
|
+
execSync(cmd);
|
|
63
|
+
} else {
|
|
64
|
+
execSync(`tmux new-session -d -s ${name} -c "${escapedCwd}"${envFlags}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function killSession(name) {
|
|
68
|
+
execSync(`tmux kill-session -t ${name}`);
|
|
69
|
+
}
|
|
70
|
+
function sendKeys(sessionName, keys) {
|
|
71
|
+
const escapedKeys = keys.replace(/"/g, '\\"');
|
|
72
|
+
execSync(`tmux send-keys -t ${sessionName} "${escapedKeys}"`);
|
|
73
|
+
execSync(`tmux send-keys -t ${sessionName} C-m`);
|
|
74
|
+
}
|
|
75
|
+
function getAgentSessions() {
|
|
76
|
+
return listSessions().filter((s) => s.name.startsWith("agent-"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/lib/hooks.ts
|
|
80
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2, readdirSync, unlinkSync } from "fs";
|
|
81
|
+
import { join } from "path";
|
|
82
|
+
function getHookDir(agentId) {
|
|
83
|
+
return join(AGENTS_DIR, agentId);
|
|
84
|
+
}
|
|
85
|
+
function getHookFile(agentId) {
|
|
86
|
+
return join(getHookDir(agentId), "hook.json");
|
|
87
|
+
}
|
|
88
|
+
function getMailDir(agentId) {
|
|
89
|
+
return join(getHookDir(agentId), "mail");
|
|
90
|
+
}
|
|
91
|
+
function initHook(agentId) {
|
|
92
|
+
const hookDir = getHookDir(agentId);
|
|
93
|
+
const mailDir = getMailDir(agentId);
|
|
94
|
+
mkdirSync(hookDir, { recursive: true });
|
|
95
|
+
mkdirSync(mailDir, { recursive: true });
|
|
96
|
+
const hookFile = getHookFile(agentId);
|
|
97
|
+
if (!existsSync(hookFile)) {
|
|
98
|
+
const hook = {
|
|
99
|
+
agentId,
|
|
100
|
+
items: []
|
|
101
|
+
};
|
|
102
|
+
writeFileSync2(hookFile, JSON.stringify(hook, null, 2));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function getHook(agentId) {
|
|
106
|
+
const hookFile = getHookFile(agentId);
|
|
107
|
+
if (!existsSync(hookFile)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const content = readFileSync(hookFile, "utf-8");
|
|
112
|
+
return JSON.parse(content);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function pushToHook(agentId, item) {
|
|
118
|
+
initHook(agentId);
|
|
119
|
+
const hook = getHook(agentId) || { agentId, items: [] };
|
|
120
|
+
const newItem = {
|
|
121
|
+
...item,
|
|
122
|
+
id: `hook-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
123
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
124
|
+
};
|
|
125
|
+
hook.items.push(newItem);
|
|
126
|
+
writeFileSync2(getHookFile(agentId), JSON.stringify(hook, null, 2));
|
|
127
|
+
return newItem;
|
|
128
|
+
}
|
|
129
|
+
function checkHook(agentId) {
|
|
130
|
+
const hook = getHook(agentId);
|
|
131
|
+
if (!hook || hook.items.length === 0) {
|
|
132
|
+
const mailDir = getMailDir(agentId);
|
|
133
|
+
if (existsSync(mailDir)) {
|
|
134
|
+
const mails = readdirSync(mailDir).filter((f) => f.endsWith(".json"));
|
|
135
|
+
if (mails.length > 0) {
|
|
136
|
+
const mailItems = mails.map((file) => {
|
|
137
|
+
try {
|
|
138
|
+
const content = readFileSync(join(mailDir, file), "utf-8");
|
|
139
|
+
return JSON.parse(content);
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}).filter(Boolean);
|
|
144
|
+
return {
|
|
145
|
+
hasWork: mailItems.length > 0,
|
|
146
|
+
urgentCount: mailItems.filter((i) => i.priority === "urgent").length,
|
|
147
|
+
items: mailItems
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { hasWork: false, urgentCount: 0, items: [] };
|
|
152
|
+
}
|
|
153
|
+
const now = /* @__PURE__ */ new Date();
|
|
154
|
+
const activeItems = hook.items.filter((item) => {
|
|
155
|
+
if (item.expiresAt) {
|
|
156
|
+
return new Date(item.expiresAt) > now;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
|
|
161
|
+
activeItems.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
162
|
+
return {
|
|
163
|
+
hasWork: activeItems.length > 0,
|
|
164
|
+
urgentCount: activeItems.filter((i) => i.priority === "urgent").length,
|
|
165
|
+
items: activeItems
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function popFromHook(agentId, itemId) {
|
|
169
|
+
const hook = getHook(agentId);
|
|
170
|
+
if (!hook) return false;
|
|
171
|
+
const index = hook.items.findIndex((i) => i.id === itemId);
|
|
172
|
+
if (index === -1) return false;
|
|
173
|
+
hook.items.splice(index, 1);
|
|
174
|
+
hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
|
|
175
|
+
writeFileSync2(getHookFile(agentId), JSON.stringify(hook, null, 2));
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
function clearHook(agentId) {
|
|
179
|
+
const hook = getHook(agentId);
|
|
180
|
+
if (!hook) return;
|
|
181
|
+
hook.items = [];
|
|
182
|
+
hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
|
|
183
|
+
writeFileSync2(getHookFile(agentId), JSON.stringify(hook, null, 2));
|
|
184
|
+
}
|
|
185
|
+
function sendMail(toAgentId, from, message, priority = "normal") {
|
|
186
|
+
initHook(toAgentId);
|
|
187
|
+
const mailDir = getMailDir(toAgentId);
|
|
188
|
+
const mailItem = {
|
|
189
|
+
id: `mail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
190
|
+
type: "message",
|
|
191
|
+
priority,
|
|
192
|
+
source: from,
|
|
193
|
+
payload: { message },
|
|
194
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
};
|
|
196
|
+
writeFileSync2(
|
|
197
|
+
join(mailDir, `${mailItem.id}.json`),
|
|
198
|
+
JSON.stringify(mailItem, null, 2)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
function generateFixedPointPrompt(agentId) {
|
|
202
|
+
const { hasWork, urgentCount, items } = checkHook(agentId);
|
|
203
|
+
if (!hasWork) return null;
|
|
204
|
+
const lines = [
|
|
205
|
+
"# FPP: Work Found on Your Hook",
|
|
206
|
+
"",
|
|
207
|
+
'> "Any runnable action is a fixed point and must resolve before the system can rest."',
|
|
208
|
+
""
|
|
209
|
+
];
|
|
210
|
+
if (urgentCount > 0) {
|
|
211
|
+
lines.push(`\u26A0\uFE0F **${urgentCount} URGENT item(s) require immediate attention**`);
|
|
212
|
+
lines.push("");
|
|
213
|
+
}
|
|
214
|
+
lines.push(`## Pending Work Items (${items.length})`);
|
|
215
|
+
lines.push("");
|
|
216
|
+
for (const item of items) {
|
|
217
|
+
const priorityEmoji = {
|
|
218
|
+
urgent: "\u{1F534}",
|
|
219
|
+
high: "\u{1F7E0}",
|
|
220
|
+
normal: "\u{1F7E2}",
|
|
221
|
+
low: "\u26AA"
|
|
222
|
+
}[item.priority];
|
|
223
|
+
lines.push(`### ${priorityEmoji} ${item.type.toUpperCase()}: ${item.id}`);
|
|
224
|
+
lines.push(`- Source: ${item.source}`);
|
|
225
|
+
lines.push(`- Created: ${item.createdAt}`);
|
|
226
|
+
if (item.payload.issueId) {
|
|
227
|
+
lines.push(`- Issue: ${item.payload.issueId}`);
|
|
228
|
+
}
|
|
229
|
+
if (item.payload.message) {
|
|
230
|
+
lines.push(`- Message: ${item.payload.message}`);
|
|
231
|
+
}
|
|
232
|
+
if (item.payload.action) {
|
|
233
|
+
lines.push(`- Action: ${item.payload.action}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push("");
|
|
236
|
+
}
|
|
237
|
+
lines.push("---");
|
|
238
|
+
lines.push("");
|
|
239
|
+
lines.push("Execute these items in priority order. Use `bd hook pop <id>` after completing each item.");
|
|
240
|
+
return lines.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/lib/cv.ts
|
|
244
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
245
|
+
import { join as join2 } from "path";
|
|
246
|
+
function getCVFile(agentId) {
|
|
247
|
+
return join2(AGENTS_DIR, agentId, "cv.json");
|
|
248
|
+
}
|
|
249
|
+
function getAgentCV(agentId) {
|
|
250
|
+
const cvFile = getCVFile(agentId);
|
|
251
|
+
if (existsSync2(cvFile)) {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(readFileSync2(cvFile, "utf-8"));
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const cv = {
|
|
258
|
+
agentId,
|
|
259
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
260
|
+
lastActive: (/* @__PURE__ */ new Date()).toISOString(),
|
|
261
|
+
runtime: "claude",
|
|
262
|
+
model: "sonnet",
|
|
263
|
+
stats: {
|
|
264
|
+
totalIssues: 0,
|
|
265
|
+
successCount: 0,
|
|
266
|
+
failureCount: 0,
|
|
267
|
+
abandonedCount: 0,
|
|
268
|
+
avgDuration: 0,
|
|
269
|
+
successRate: 0
|
|
270
|
+
},
|
|
271
|
+
skillsUsed: [],
|
|
272
|
+
recentWork: []
|
|
273
|
+
};
|
|
274
|
+
saveAgentCV(cv);
|
|
275
|
+
return cv;
|
|
276
|
+
}
|
|
277
|
+
function saveAgentCV(cv) {
|
|
278
|
+
const dir = join2(AGENTS_DIR, cv.agentId);
|
|
279
|
+
mkdirSync2(dir, { recursive: true });
|
|
280
|
+
writeFileSync3(getCVFile(cv.agentId), JSON.stringify(cv, null, 2));
|
|
281
|
+
}
|
|
282
|
+
function startWork(agentId, issueId, skills) {
|
|
283
|
+
const cv = getAgentCV(agentId);
|
|
284
|
+
const entry = {
|
|
285
|
+
issueId,
|
|
286
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
287
|
+
outcome: "in_progress",
|
|
288
|
+
skills
|
|
289
|
+
};
|
|
290
|
+
cv.recentWork.unshift(entry);
|
|
291
|
+
cv.stats.totalIssues++;
|
|
292
|
+
cv.lastActive = (/* @__PURE__ */ new Date()).toISOString();
|
|
293
|
+
if (skills) {
|
|
294
|
+
for (const skill of skills) {
|
|
295
|
+
if (!cv.skillsUsed.includes(skill)) {
|
|
296
|
+
cv.skillsUsed.push(skill);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (cv.recentWork.length > 50) {
|
|
301
|
+
cv.recentWork = cv.recentWork.slice(0, 50);
|
|
302
|
+
}
|
|
303
|
+
saveAgentCV(cv);
|
|
304
|
+
}
|
|
305
|
+
function getAgentRankings() {
|
|
306
|
+
const rankings = [];
|
|
307
|
+
if (!existsSync2(AGENTS_DIR)) return rankings;
|
|
308
|
+
const dirs = readdirSync2(AGENTS_DIR, { withFileTypes: true }).filter(
|
|
309
|
+
(d) => d.isDirectory()
|
|
310
|
+
);
|
|
311
|
+
for (const dir of dirs) {
|
|
312
|
+
const cv = getAgentCV(dir.name);
|
|
313
|
+
if (cv.stats.totalIssues > 0) {
|
|
314
|
+
rankings.push({
|
|
315
|
+
agentId: dir.name,
|
|
316
|
+
successRate: cv.stats.successRate,
|
|
317
|
+
totalIssues: cv.stats.totalIssues,
|
|
318
|
+
avgDuration: cv.stats.avgDuration
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
rankings.sort((a, b) => {
|
|
323
|
+
if (b.successRate !== a.successRate) {
|
|
324
|
+
return b.successRate - a.successRate;
|
|
325
|
+
}
|
|
326
|
+
return b.totalIssues - a.totalIssues;
|
|
327
|
+
});
|
|
328
|
+
return rankings;
|
|
329
|
+
}
|
|
330
|
+
function formatCV(cv) {
|
|
331
|
+
const lines = [
|
|
332
|
+
`# Agent CV: ${cv.agentId}`,
|
|
333
|
+
"",
|
|
334
|
+
`Runtime: ${cv.runtime} (${cv.model})`,
|
|
335
|
+
`Created: ${cv.createdAt}`,
|
|
336
|
+
`Last Active: ${cv.lastActive}`,
|
|
337
|
+
"",
|
|
338
|
+
"## Statistics",
|
|
339
|
+
"",
|
|
340
|
+
`- Total Issues: ${cv.stats.totalIssues}`,
|
|
341
|
+
`- Success Rate: ${(cv.stats.successRate * 100).toFixed(1)}%`,
|
|
342
|
+
`- Successes: ${cv.stats.successCount}`,
|
|
343
|
+
`- Failures: ${cv.stats.failureCount}`,
|
|
344
|
+
`- Abandoned: ${cv.stats.abandonedCount}`,
|
|
345
|
+
`- Avg Duration: ${cv.stats.avgDuration} minutes`,
|
|
346
|
+
""
|
|
347
|
+
];
|
|
348
|
+
if (cv.skillsUsed.length > 0) {
|
|
349
|
+
lines.push("## Skills Used");
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push(cv.skillsUsed.join(", "));
|
|
352
|
+
lines.push("");
|
|
353
|
+
}
|
|
354
|
+
if (cv.recentWork.length > 0) {
|
|
355
|
+
lines.push("## Recent Work");
|
|
356
|
+
lines.push("");
|
|
357
|
+
for (const work of cv.recentWork.slice(0, 10)) {
|
|
358
|
+
const statusIcon = {
|
|
359
|
+
success: "\u2713",
|
|
360
|
+
failed: "\u2717",
|
|
361
|
+
abandoned: "\u2298",
|
|
362
|
+
in_progress: "\u25CF"
|
|
363
|
+
}[work.outcome];
|
|
364
|
+
const duration = work.duration ? ` (${work.duration}m)` : "";
|
|
365
|
+
lines.push(`${statusIcon} ${work.issueId}${duration}`);
|
|
366
|
+
if (work.failureReason) {
|
|
367
|
+
lines.push(` Reason: ${work.failureReason}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
lines.push("");
|
|
371
|
+
}
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/lib/work-types.ts
|
|
376
|
+
var WORK_TYPES = {
|
|
377
|
+
// Issue agent phases (6)
|
|
378
|
+
"issue-agent:exploration": {
|
|
379
|
+
phase: "exploration",
|
|
380
|
+
category: "issue-agent",
|
|
381
|
+
description: "Exploring codebase and understanding requirements"
|
|
382
|
+
},
|
|
383
|
+
"issue-agent:planning": {
|
|
384
|
+
phase: "planning",
|
|
385
|
+
category: "issue-agent",
|
|
386
|
+
description: "Planning implementation approach and architecture"
|
|
387
|
+
},
|
|
388
|
+
"issue-agent:implementation": {
|
|
389
|
+
phase: "implementation",
|
|
390
|
+
category: "issue-agent",
|
|
391
|
+
description: "Writing code to implement features or fixes"
|
|
392
|
+
},
|
|
393
|
+
"issue-agent:testing": {
|
|
394
|
+
phase: "testing",
|
|
395
|
+
category: "issue-agent",
|
|
396
|
+
description: "Running tests and verifying functionality"
|
|
397
|
+
},
|
|
398
|
+
"issue-agent:documentation": {
|
|
399
|
+
phase: "documentation",
|
|
400
|
+
category: "issue-agent",
|
|
401
|
+
description: "Writing documentation and updating docs"
|
|
402
|
+
},
|
|
403
|
+
"issue-agent:review-response": {
|
|
404
|
+
phase: "review-response",
|
|
405
|
+
category: "issue-agent",
|
|
406
|
+
description: "Responding to code review feedback"
|
|
407
|
+
},
|
|
408
|
+
// Specialist agents (3)
|
|
409
|
+
"specialist-review-agent": {
|
|
410
|
+
category: "specialist",
|
|
411
|
+
description: "Comprehensive code review specialist"
|
|
412
|
+
},
|
|
413
|
+
"specialist-test-agent": {
|
|
414
|
+
category: "specialist",
|
|
415
|
+
description: "Test generation and verification specialist"
|
|
416
|
+
},
|
|
417
|
+
"specialist-merge-agent": {
|
|
418
|
+
category: "specialist",
|
|
419
|
+
description: "Merge request finalization specialist"
|
|
420
|
+
},
|
|
421
|
+
// Subagents (4)
|
|
422
|
+
"subagent:explore": {
|
|
423
|
+
category: "subagent",
|
|
424
|
+
description: "Fast codebase exploration subagent"
|
|
425
|
+
},
|
|
426
|
+
"subagent:plan": {
|
|
427
|
+
category: "subagent",
|
|
428
|
+
description: "Implementation planning subagent"
|
|
429
|
+
},
|
|
430
|
+
"subagent:bash": {
|
|
431
|
+
category: "subagent",
|
|
432
|
+
description: "Command execution specialist subagent"
|
|
433
|
+
},
|
|
434
|
+
"subagent:general-purpose": {
|
|
435
|
+
category: "subagent",
|
|
436
|
+
description: "General-purpose task subagent"
|
|
437
|
+
},
|
|
438
|
+
// Convoy members (4)
|
|
439
|
+
"convoy:security-reviewer": {
|
|
440
|
+
category: "convoy",
|
|
441
|
+
description: "Security-focused code reviewer in convoy"
|
|
442
|
+
},
|
|
443
|
+
"convoy:performance-reviewer": {
|
|
444
|
+
category: "convoy",
|
|
445
|
+
description: "Performance-focused code reviewer in convoy"
|
|
446
|
+
},
|
|
447
|
+
"convoy:correctness-reviewer": {
|
|
448
|
+
category: "convoy",
|
|
449
|
+
description: "Correctness-focused code reviewer in convoy"
|
|
450
|
+
},
|
|
451
|
+
"convoy:synthesis-agent": {
|
|
452
|
+
category: "convoy",
|
|
453
|
+
description: "Synthesizes findings from convoy reviewers"
|
|
454
|
+
},
|
|
455
|
+
// Pre-work agents (4)
|
|
456
|
+
"prd-agent": {
|
|
457
|
+
category: "pre-work",
|
|
458
|
+
description: "Generates Product Requirement Documents"
|
|
459
|
+
},
|
|
460
|
+
"decomposition-agent": {
|
|
461
|
+
category: "pre-work",
|
|
462
|
+
description: "Breaks down work into tasks"
|
|
463
|
+
},
|
|
464
|
+
"triage-agent": {
|
|
465
|
+
category: "pre-work",
|
|
466
|
+
description: "Prioritizes and triages issues"
|
|
467
|
+
},
|
|
468
|
+
"planning-agent": {
|
|
469
|
+
category: "pre-work",
|
|
470
|
+
description: "Explores and plans implementation approach"
|
|
471
|
+
},
|
|
472
|
+
// CLI contexts (2)
|
|
473
|
+
"cli:interactive": {
|
|
474
|
+
category: "cli",
|
|
475
|
+
description: "Interactive CLI session"
|
|
476
|
+
},
|
|
477
|
+
"cli:quick-command": {
|
|
478
|
+
category: "cli",
|
|
479
|
+
description: "Quick one-off CLI commands"
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
function getAllWorkTypes() {
|
|
483
|
+
return Object.keys(WORK_TYPES);
|
|
484
|
+
}
|
|
485
|
+
function isValidWorkType(id) {
|
|
486
|
+
return id in WORK_TYPES;
|
|
487
|
+
}
|
|
488
|
+
function validateWorkType(id) {
|
|
489
|
+
if (!isValidWorkType(id)) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Invalid work type ID: ${id}. Valid types: ${getAllWorkTypes().join(", ")}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/lib/model-fallback.ts
|
|
497
|
+
var MODEL_PROVIDERS = {
|
|
498
|
+
// Anthropic models
|
|
499
|
+
"claude-opus-4-5": "anthropic",
|
|
500
|
+
"claude-sonnet-4-5": "anthropic",
|
|
501
|
+
"claude-haiku-4-5": "anthropic",
|
|
502
|
+
// OpenAI models
|
|
503
|
+
"gpt-5.2-codex": "openai",
|
|
504
|
+
"o3-deep-research": "openai",
|
|
505
|
+
"gpt-4o": "openai",
|
|
506
|
+
"gpt-4o-mini": "openai",
|
|
507
|
+
// Google models
|
|
508
|
+
"gemini-3-pro-preview": "google",
|
|
509
|
+
"gemini-3-flash-preview": "google",
|
|
510
|
+
"gemini-2.5-pro": "google",
|
|
511
|
+
"gemini-2.5-flash": "google",
|
|
512
|
+
// Z.AI models
|
|
513
|
+
"glm-4.7": "zai",
|
|
514
|
+
"glm-4.7-flash": "zai",
|
|
515
|
+
// Kimi models
|
|
516
|
+
"kimi-k2": "kimi",
|
|
517
|
+
"kimi-k2.5": "kimi"
|
|
518
|
+
};
|
|
519
|
+
var FALLBACK_MAP = {
|
|
520
|
+
// OpenAI → Anthropic
|
|
521
|
+
"gpt-5.2-codex": "claude-sonnet-4-5",
|
|
522
|
+
// Premium code model → Sonnet
|
|
523
|
+
"o3-deep-research": "claude-sonnet-4-5",
|
|
524
|
+
// Premium research model → Sonnet
|
|
525
|
+
"gpt-4o": "claude-sonnet-4-5",
|
|
526
|
+
// Flagship model → Sonnet
|
|
527
|
+
"gpt-4o-mini": "claude-haiku-4-5",
|
|
528
|
+
// Economy model → Haiku
|
|
529
|
+
// Google → Anthropic
|
|
530
|
+
"gemini-3-pro-preview": "claude-sonnet-4-5",
|
|
531
|
+
// Premium model → Sonnet
|
|
532
|
+
"gemini-3-flash-preview": "claude-haiku-4-5",
|
|
533
|
+
// Fast model → Haiku
|
|
534
|
+
// Z.AI → Anthropic
|
|
535
|
+
"glm-4.7": "claude-haiku-4-5",
|
|
536
|
+
// Standard model → Haiku
|
|
537
|
+
"glm-4.7-flash": "claude-haiku-4-5",
|
|
538
|
+
// Fast model → Haiku
|
|
539
|
+
// Kimi → Anthropic
|
|
540
|
+
"kimi-k2": "claude-sonnet-4-5",
|
|
541
|
+
// Good balance model → Sonnet
|
|
542
|
+
"kimi-k2.5": "claude-sonnet-4-5"
|
|
543
|
+
// Premium model → Sonnet
|
|
544
|
+
};
|
|
545
|
+
var DEFAULT_FALLBACK = "claude-sonnet-4-5";
|
|
546
|
+
function getModelProvider(modelId) {
|
|
547
|
+
return MODEL_PROVIDERS[modelId];
|
|
548
|
+
}
|
|
549
|
+
function getModelsByProvider(provider) {
|
|
550
|
+
return Object.entries(MODEL_PROVIDERS).filter(([_, p]) => p === provider).map(([modelId]) => modelId);
|
|
551
|
+
}
|
|
552
|
+
function isProviderEnabled(provider, enabledProviders) {
|
|
553
|
+
if (provider === "anthropic") return true;
|
|
554
|
+
return enabledProviders.has(provider);
|
|
555
|
+
}
|
|
556
|
+
function applyFallback(modelId, enabledProviders) {
|
|
557
|
+
const provider = getModelProvider(modelId);
|
|
558
|
+
if (isProviderEnabled(provider, enabledProviders)) {
|
|
559
|
+
return modelId;
|
|
560
|
+
}
|
|
561
|
+
const fallback = FALLBACK_MAP[modelId] || DEFAULT_FALLBACK;
|
|
562
|
+
console.warn(
|
|
563
|
+
`Model ${modelId} requires ${provider} API key - falling back to ${fallback}`
|
|
564
|
+
);
|
|
565
|
+
return fallback;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/lib/config-yaml.ts
|
|
569
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
570
|
+
import { join as join3 } from "path";
|
|
571
|
+
import { homedir } from "os";
|
|
572
|
+
import yaml from "js-yaml";
|
|
573
|
+
var DEFAULT_CONFIG = {
|
|
574
|
+
enabledProviders: /* @__PURE__ */ new Set(["anthropic"]),
|
|
575
|
+
// Only Anthropic by default
|
|
576
|
+
apiKeys: {},
|
|
577
|
+
overrides: {},
|
|
578
|
+
geminiThinkingLevel: 3
|
|
579
|
+
};
|
|
580
|
+
var GLOBAL_CONFIG_PATH = join3(homedir(), ".panopticon", "config.yaml");
|
|
581
|
+
function normalizeProviderConfig(providerConfig, fallbackKey) {
|
|
582
|
+
if (providerConfig === void 0) {
|
|
583
|
+
return { enabled: false };
|
|
584
|
+
}
|
|
585
|
+
if (typeof providerConfig === "boolean") {
|
|
586
|
+
return { enabled: providerConfig, api_key: fallbackKey };
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
enabled: providerConfig.enabled,
|
|
590
|
+
api_key: providerConfig.api_key || fallbackKey
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function resolveEnvVar(value) {
|
|
594
|
+
if (!value) return void 0;
|
|
595
|
+
return value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g, (match, varName) => {
|
|
596
|
+
const envValue = process.env[varName];
|
|
597
|
+
return envValue !== void 0 ? envValue : match;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function loadYamlFile(filePath) {
|
|
601
|
+
if (!existsSync3(filePath)) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
606
|
+
const parsed = yaml.load(content);
|
|
607
|
+
return parsed || {};
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error(`Error loading YAML config from ${filePath}:`, error);
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
614
|
+
let currentDir = startDir;
|
|
615
|
+
while (currentDir !== "/") {
|
|
616
|
+
if (existsSync3(join3(currentDir, ".git"))) {
|
|
617
|
+
return currentDir;
|
|
618
|
+
}
|
|
619
|
+
currentDir = join3(currentDir, "..");
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
function loadProjectConfig() {
|
|
624
|
+
const projectRoot = findProjectRoot();
|
|
625
|
+
if (!projectRoot) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const projectConfigPath = join3(projectRoot, ".panopticon.yaml");
|
|
629
|
+
return loadYamlFile(projectConfigPath);
|
|
630
|
+
}
|
|
631
|
+
function loadGlobalConfig() {
|
|
632
|
+
return loadYamlFile(GLOBAL_CONFIG_PATH);
|
|
633
|
+
}
|
|
634
|
+
function mergeConfigs(...configs) {
|
|
635
|
+
const result = {
|
|
636
|
+
...DEFAULT_CONFIG,
|
|
637
|
+
enabledProviders: new Set(DEFAULT_CONFIG.enabledProviders)
|
|
638
|
+
};
|
|
639
|
+
const validConfigs = configs.filter((c) => c !== null);
|
|
640
|
+
for (const config of validConfigs.reverse()) {
|
|
641
|
+
if (config.models?.providers) {
|
|
642
|
+
const providers = config.models.providers;
|
|
643
|
+
const legacyKeys = config.api_keys || {};
|
|
644
|
+
result.enabledProviders.add("anthropic");
|
|
645
|
+
const openai = normalizeProviderConfig(providers.openai, legacyKeys.openai);
|
|
646
|
+
if (openai.enabled) {
|
|
647
|
+
result.enabledProviders.add("openai");
|
|
648
|
+
if (openai.api_key) {
|
|
649
|
+
result.apiKeys.openai = resolveEnvVar(openai.api_key);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const google = normalizeProviderConfig(providers.google, legacyKeys.google);
|
|
653
|
+
if (google.enabled) {
|
|
654
|
+
result.enabledProviders.add("google");
|
|
655
|
+
if (google.api_key) {
|
|
656
|
+
result.apiKeys.google = resolveEnvVar(google.api_key);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const zai = normalizeProviderConfig(providers.zai, legacyKeys.zai);
|
|
660
|
+
if (zai.enabled) {
|
|
661
|
+
result.enabledProviders.add("zai");
|
|
662
|
+
if (zai.api_key) {
|
|
663
|
+
result.apiKeys.zai = resolveEnvVar(zai.api_key);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const kimi = normalizeProviderConfig(providers.kimi, legacyKeys.kimi);
|
|
667
|
+
if (kimi.enabled) {
|
|
668
|
+
result.enabledProviders.add("kimi");
|
|
669
|
+
if (kimi.api_key) {
|
|
670
|
+
result.apiKeys.kimi = resolveEnvVar(kimi.api_key);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (config.api_keys) {
|
|
675
|
+
if (config.api_keys.openai) {
|
|
676
|
+
result.apiKeys.openai = resolveEnvVar(config.api_keys.openai);
|
|
677
|
+
result.enabledProviders.add("openai");
|
|
678
|
+
}
|
|
679
|
+
if (config.api_keys.google) {
|
|
680
|
+
result.apiKeys.google = resolveEnvVar(config.api_keys.google);
|
|
681
|
+
result.enabledProviders.add("google");
|
|
682
|
+
}
|
|
683
|
+
if (config.api_keys.zai) {
|
|
684
|
+
result.apiKeys.zai = resolveEnvVar(config.api_keys.zai);
|
|
685
|
+
result.enabledProviders.add("zai");
|
|
686
|
+
}
|
|
687
|
+
if (config.api_keys.kimi) {
|
|
688
|
+
result.apiKeys.kimi = resolveEnvVar(config.api_keys.kimi);
|
|
689
|
+
result.enabledProviders.add("kimi");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (config.models?.overrides) {
|
|
693
|
+
result.overrides = {
|
|
694
|
+
...result.overrides,
|
|
695
|
+
...config.models.overrides
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (config.models?.gemini_thinking_level) {
|
|
699
|
+
result.geminiThinkingLevel = config.models.gemini_thinking_level;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return result;
|
|
703
|
+
}
|
|
704
|
+
function loadConfig() {
|
|
705
|
+
const globalConfig = loadGlobalConfig();
|
|
706
|
+
const projectConfig = loadProjectConfig();
|
|
707
|
+
const config = mergeConfigs(projectConfig, globalConfig);
|
|
708
|
+
if (process.env.OPENAI_API_KEY && !config.apiKeys.openai) {
|
|
709
|
+
config.apiKeys.openai = process.env.OPENAI_API_KEY;
|
|
710
|
+
config.enabledProviders.add("openai");
|
|
711
|
+
}
|
|
712
|
+
if (process.env.GOOGLE_API_KEY && !config.apiKeys.google) {
|
|
713
|
+
config.apiKeys.google = process.env.GOOGLE_API_KEY;
|
|
714
|
+
config.enabledProviders.add("google");
|
|
715
|
+
}
|
|
716
|
+
if (process.env.ZAI_API_KEY && !config.apiKeys.zai) {
|
|
717
|
+
config.apiKeys.zai = process.env.ZAI_API_KEY;
|
|
718
|
+
config.enabledProviders.add("zai");
|
|
719
|
+
}
|
|
720
|
+
if (process.env.KIMI_API_KEY && !config.apiKeys.kimi) {
|
|
721
|
+
config.apiKeys.kimi = process.env.KIMI_API_KEY;
|
|
722
|
+
config.enabledProviders.add("kimi");
|
|
723
|
+
}
|
|
724
|
+
return config;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/lib/model-capabilities.ts
|
|
728
|
+
var MODEL_CAPABILITIES = {
|
|
729
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
730
|
+
// ANTHROPIC MODELS
|
|
731
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
732
|
+
"claude-opus-4-5": {
|
|
733
|
+
model: "claude-opus-4-5",
|
|
734
|
+
provider: "anthropic",
|
|
735
|
+
displayName: "Claude Opus 4.5",
|
|
736
|
+
costPer1MTokens: 45,
|
|
737
|
+
// $15 in / $75 out → avg ~$45
|
|
738
|
+
contextWindow: 2e5,
|
|
739
|
+
skills: {
|
|
740
|
+
"code-generation": 96,
|
|
741
|
+
// 80.9% SWE-bench (first >80%), 89.4% Aider Polyglot
|
|
742
|
+
"code-review": 98,
|
|
743
|
+
debugging: 97,
|
|
744
|
+
planning: 99,
|
|
745
|
+
// User confirms: "Opus 4.5 planning for sure"
|
|
746
|
+
documentation: 95,
|
|
747
|
+
testing: 92,
|
|
748
|
+
security: 98,
|
|
749
|
+
// Best for security review
|
|
750
|
+
performance: 90,
|
|
751
|
+
synthesis: 98,
|
|
752
|
+
// Best for combining info across domains
|
|
753
|
+
speed: 40,
|
|
754
|
+
// Slower but 76% more token efficient
|
|
755
|
+
"context-length": 95
|
|
756
|
+
},
|
|
757
|
+
notes: "First to exceed 80% SWE-bench. Best for planning, security, complex reasoning. Leads 7/8 languages."
|
|
758
|
+
},
|
|
759
|
+
"claude-sonnet-4-5": {
|
|
760
|
+
model: "claude-sonnet-4-5",
|
|
761
|
+
provider: "anthropic",
|
|
762
|
+
displayName: "Claude Sonnet 4.5",
|
|
763
|
+
costPer1MTokens: 9,
|
|
764
|
+
// $3 in / $15 out → avg ~$9
|
|
765
|
+
contextWindow: 2e5,
|
|
766
|
+
skills: {
|
|
767
|
+
"code-generation": 92,
|
|
768
|
+
// 77.2% SWE-bench (82% parallel), beats GPT-5 Codex (74.5%)
|
|
769
|
+
"code-review": 92,
|
|
770
|
+
debugging: 90,
|
|
771
|
+
planning: 88,
|
|
772
|
+
documentation: 90,
|
|
773
|
+
// 100% AIME with Python
|
|
774
|
+
testing: 90,
|
|
775
|
+
// 50% Terminal-Bench, 61.4% OSWorld
|
|
776
|
+
security: 85,
|
|
777
|
+
performance: 85,
|
|
778
|
+
synthesis: 88,
|
|
779
|
+
speed: 70,
|
|
780
|
+
"context-length": 95
|
|
781
|
+
},
|
|
782
|
+
notes: "Best value: 77.2% SWE-bench at 1/5th Opus cost. Beats GPT-5 Codex."
|
|
783
|
+
},
|
|
784
|
+
"claude-haiku-4-5": {
|
|
785
|
+
model: "claude-haiku-4-5",
|
|
786
|
+
provider: "anthropic",
|
|
787
|
+
displayName: "Claude Haiku 4.5",
|
|
788
|
+
costPer1MTokens: 4,
|
|
789
|
+
// $0.80 in / $4 out → avg ~$2.4
|
|
790
|
+
contextWindow: 2e5,
|
|
791
|
+
skills: {
|
|
792
|
+
"code-generation": 75,
|
|
793
|
+
"code-review": 72,
|
|
794
|
+
debugging: 70,
|
|
795
|
+
planning: 65,
|
|
796
|
+
documentation: 75,
|
|
797
|
+
testing: 70,
|
|
798
|
+
security: 60,
|
|
799
|
+
performance: 65,
|
|
800
|
+
synthesis: 68,
|
|
801
|
+
speed: 95,
|
|
802
|
+
// Fastest Anthropic
|
|
803
|
+
"context-length": 95
|
|
804
|
+
},
|
|
805
|
+
notes: "Fast and cheap, good for simple tasks and exploration"
|
|
806
|
+
},
|
|
807
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
808
|
+
// OPENAI MODELS
|
|
809
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
810
|
+
"gpt-5.2-codex": {
|
|
811
|
+
model: "gpt-5.2-codex",
|
|
812
|
+
provider: "openai",
|
|
813
|
+
displayName: "GPT-5.2 Codex",
|
|
814
|
+
costPer1MTokens: 75,
|
|
815
|
+
// Premium tier ~$75/M
|
|
816
|
+
contextWindow: 128e3,
|
|
817
|
+
skills: {
|
|
818
|
+
"code-generation": 95,
|
|
819
|
+
// 80% SWE-bench Verified, 55.6% SWE-bench Pro
|
|
820
|
+
"code-review": 90,
|
|
821
|
+
debugging: 92,
|
|
822
|
+
// 92.4% GPQA Diamond
|
|
823
|
+
planning: 88,
|
|
824
|
+
documentation: 85,
|
|
825
|
+
testing: 90,
|
|
826
|
+
security: 85,
|
|
827
|
+
performance: 88,
|
|
828
|
+
// 52.9% ARC-AGI-2 (best reasoning)
|
|
829
|
+
synthesis: 88,
|
|
830
|
+
// 100% AIME 2025 without tools
|
|
831
|
+
speed: 55,
|
|
832
|
+
"context-length": 75
|
|
833
|
+
},
|
|
834
|
+
notes: "Premium coding: 80% SWE-bench. Best raw reasoning (52.9% ARC-AGI-2). Expensive."
|
|
835
|
+
},
|
|
836
|
+
"o3-deep-research": {
|
|
837
|
+
model: "o3-deep-research",
|
|
838
|
+
provider: "openai",
|
|
839
|
+
displayName: "O3 Deep Research",
|
|
840
|
+
costPer1MTokens: 100,
|
|
841
|
+
// Expensive reasoning model
|
|
842
|
+
contextWindow: 2e5,
|
|
843
|
+
skills: {
|
|
844
|
+
"code-generation": 85,
|
|
845
|
+
"code-review": 95,
|
|
846
|
+
debugging: 98,
|
|
847
|
+
// Best for debugging
|
|
848
|
+
planning: 95,
|
|
849
|
+
documentation: 88,
|
|
850
|
+
testing: 85,
|
|
851
|
+
security: 92,
|
|
852
|
+
performance: 92,
|
|
853
|
+
synthesis: 95,
|
|
854
|
+
speed: 20,
|
|
855
|
+
// Very slow (reasoning chains)
|
|
856
|
+
"context-length": 95
|
|
857
|
+
},
|
|
858
|
+
notes: "Deep reasoning model, excellent for complex debugging and analysis"
|
|
859
|
+
},
|
|
860
|
+
"gpt-4o": {
|
|
861
|
+
model: "gpt-4o",
|
|
862
|
+
provider: "openai",
|
|
863
|
+
displayName: "GPT-4o",
|
|
864
|
+
costPer1MTokens: 15,
|
|
865
|
+
// $5 in / $15 out
|
|
866
|
+
contextWindow: 128e3,
|
|
867
|
+
skills: {
|
|
868
|
+
"code-generation": 88,
|
|
869
|
+
"code-review": 85,
|
|
870
|
+
debugging: 85,
|
|
871
|
+
planning: 82,
|
|
872
|
+
documentation: 88,
|
|
873
|
+
testing: 82,
|
|
874
|
+
security: 78,
|
|
875
|
+
performance: 80,
|
|
876
|
+
synthesis: 85,
|
|
877
|
+
speed: 75,
|
|
878
|
+
"context-length": 75
|
|
879
|
+
},
|
|
880
|
+
notes: "Good all-rounder, competitive with Sonnet"
|
|
881
|
+
},
|
|
882
|
+
"gpt-4o-mini": {
|
|
883
|
+
model: "gpt-4o-mini",
|
|
884
|
+
provider: "openai",
|
|
885
|
+
displayName: "GPT-4o Mini",
|
|
886
|
+
costPer1MTokens: 1,
|
|
887
|
+
// Very cheap
|
|
888
|
+
contextWindow: 128e3,
|
|
889
|
+
skills: {
|
|
890
|
+
"code-generation": 72,
|
|
891
|
+
"code-review": 68,
|
|
892
|
+
debugging: 65,
|
|
893
|
+
planning: 60,
|
|
894
|
+
documentation: 70,
|
|
895
|
+
testing: 65,
|
|
896
|
+
security: 55,
|
|
897
|
+
performance: 60,
|
|
898
|
+
synthesis: 62,
|
|
899
|
+
speed: 92,
|
|
900
|
+
"context-length": 75
|
|
901
|
+
},
|
|
902
|
+
notes: "Budget option, good for simple tasks"
|
|
903
|
+
},
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
905
|
+
// GOOGLE MODELS
|
|
906
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
907
|
+
"gemini-3-pro-preview": {
|
|
908
|
+
model: "gemini-3-pro-preview",
|
|
909
|
+
provider: "google",
|
|
910
|
+
displayName: "Gemini 3 Pro",
|
|
911
|
+
costPer1MTokens: 12,
|
|
912
|
+
// $4.2 in / $18.9 out
|
|
913
|
+
contextWindow: 1e6,
|
|
914
|
+
// 1M context!
|
|
915
|
+
skills: {
|
|
916
|
+
"code-generation": 90,
|
|
917
|
+
// 2439 Elo LiveCodeBench Pro (first >1500 on LMArena)
|
|
918
|
+
"code-review": 88,
|
|
919
|
+
debugging: 85,
|
|
920
|
+
planning: 85,
|
|
921
|
+
documentation: 88,
|
|
922
|
+
testing: 85,
|
|
923
|
+
// ~95% AIME 2025
|
|
924
|
+
security: 78,
|
|
925
|
+
performance: 85,
|
|
926
|
+
// Strong multimodal
|
|
927
|
+
synthesis: 90,
|
|
928
|
+
// Best for combining large codebases
|
|
929
|
+
speed: 80,
|
|
930
|
+
"context-length": 100
|
|
931
|
+
// Best context - 1M tokens
|
|
932
|
+
},
|
|
933
|
+
notes: "First to exceed 1500 Elo on LMArena. Best for large codebase analysis with 1M context."
|
|
934
|
+
},
|
|
935
|
+
"gemini-3-flash-preview": {
|
|
936
|
+
model: "gemini-3-flash-preview",
|
|
937
|
+
provider: "google",
|
|
938
|
+
displayName: "Gemini 3 Flash",
|
|
939
|
+
costPer1MTokens: 0.5,
|
|
940
|
+
// Very cheap
|
|
941
|
+
contextWindow: 1e6,
|
|
942
|
+
skills: {
|
|
943
|
+
"code-generation": 75,
|
|
944
|
+
"code-review": 70,
|
|
945
|
+
debugging: 68,
|
|
946
|
+
planning: 62,
|
|
947
|
+
documentation: 72,
|
|
948
|
+
testing: 68,
|
|
949
|
+
security: 55,
|
|
950
|
+
performance: 65,
|
|
951
|
+
synthesis: 70,
|
|
952
|
+
speed: 98,
|
|
953
|
+
// Fastest overall
|
|
954
|
+
"context-length": 100
|
|
955
|
+
},
|
|
956
|
+
notes: "Extremely fast and cheap, huge context, great for exploration"
|
|
957
|
+
},
|
|
958
|
+
"gemini-2.5-pro": {
|
|
959
|
+
model: "gemini-2.5-pro",
|
|
960
|
+
provider: "google",
|
|
961
|
+
displayName: "Gemini 2.5 Pro",
|
|
962
|
+
costPer1MTokens: 12,
|
|
963
|
+
contextWindow: 1e6,
|
|
964
|
+
skills: {
|
|
965
|
+
"code-generation": 92,
|
|
966
|
+
"code-review": 90,
|
|
967
|
+
debugging: 88,
|
|
968
|
+
planning: 88,
|
|
969
|
+
documentation: 90,
|
|
970
|
+
testing: 87,
|
|
971
|
+
security: 82,
|
|
972
|
+
performance: 88,
|
|
973
|
+
synthesis: 92,
|
|
974
|
+
speed: 75,
|
|
975
|
+
"context-length": 100
|
|
976
|
+
},
|
|
977
|
+
notes: "Advanced reasoning and code capabilities with 1M context"
|
|
978
|
+
},
|
|
979
|
+
"gemini-2.5-flash": {
|
|
980
|
+
model: "gemini-2.5-flash",
|
|
981
|
+
provider: "google",
|
|
982
|
+
displayName: "Gemini 2.5 Flash",
|
|
983
|
+
costPer1MTokens: 0.6,
|
|
984
|
+
contextWindow: 1e6,
|
|
985
|
+
skills: {
|
|
986
|
+
"code-generation": 78,
|
|
987
|
+
"code-review": 73,
|
|
988
|
+
debugging: 70,
|
|
989
|
+
planning: 65,
|
|
990
|
+
documentation: 75,
|
|
991
|
+
testing: 70,
|
|
992
|
+
security: 58,
|
|
993
|
+
performance: 68,
|
|
994
|
+
synthesis: 73,
|
|
995
|
+
speed: 95,
|
|
996
|
+
"context-length": 100
|
|
997
|
+
},
|
|
998
|
+
notes: "Fast and efficient with large context support"
|
|
999
|
+
},
|
|
1000
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1001
|
+
// Z.AI MODELS
|
|
1002
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1003
|
+
"glm-4.7": {
|
|
1004
|
+
model: "glm-4.7",
|
|
1005
|
+
provider: "zai",
|
|
1006
|
+
displayName: "GLM 4.7",
|
|
1007
|
+
costPer1MTokens: 5,
|
|
1008
|
+
contextWindow: 2e5,
|
|
1009
|
+
// 200K context, 128K output
|
|
1010
|
+
skills: {
|
|
1011
|
+
"code-generation": 88,
|
|
1012
|
+
// 73.8% SWE-bench, 84.9 LiveCodeBench v6 (open-source SOTA)
|
|
1013
|
+
"code-review": 85,
|
|
1014
|
+
debugging: 85,
|
|
1015
|
+
// Strong debugging with Interleaved Thinking
|
|
1016
|
+
planning: 82,
|
|
1017
|
+
// 95.7% AIME 2025 (beats Gemini 3 & GPT-5.1)
|
|
1018
|
+
documentation: 80,
|
|
1019
|
+
testing: 82,
|
|
1020
|
+
// 87.4 τ²-Bench (SOTA for tool use)
|
|
1021
|
+
security: 72,
|
|
1022
|
+
performance: 78,
|
|
1023
|
+
synthesis: 85,
|
|
1024
|
+
// Preserved Thinking retains context across turns
|
|
1025
|
+
speed: 80,
|
|
1026
|
+
"context-length": 95
|
|
1027
|
+
// 200K context
|
|
1028
|
+
},
|
|
1029
|
+
notes: "Top open-source for agentic coding. 73.8% SWE-bench, best tool use. 400B params with Interleaved Thinking."
|
|
1030
|
+
},
|
|
1031
|
+
"glm-4.7-flash": {
|
|
1032
|
+
model: "glm-4.7-flash",
|
|
1033
|
+
provider: "zai",
|
|
1034
|
+
displayName: "GLM 4.7 Flash",
|
|
1035
|
+
costPer1MTokens: 1.5,
|
|
1036
|
+
contextWindow: 128e3,
|
|
1037
|
+
skills: {
|
|
1038
|
+
"code-generation": 72,
|
|
1039
|
+
"code-review": 68,
|
|
1040
|
+
debugging: 65,
|
|
1041
|
+
planning: 62,
|
|
1042
|
+
documentation: 70,
|
|
1043
|
+
testing: 65,
|
|
1044
|
+
security: 55,
|
|
1045
|
+
performance: 62,
|
|
1046
|
+
synthesis: 65,
|
|
1047
|
+
speed: 92,
|
|
1048
|
+
// Fast inference
|
|
1049
|
+
"context-length": 75
|
|
1050
|
+
},
|
|
1051
|
+
notes: "Fast and affordable. Good for quick iterations and exploration."
|
|
1052
|
+
},
|
|
1053
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1054
|
+
// KIMI MODELS
|
|
1055
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1056
|
+
"kimi-k2": {
|
|
1057
|
+
model: "kimi-k2",
|
|
1058
|
+
provider: "kimi",
|
|
1059
|
+
displayName: "Kimi K2",
|
|
1060
|
+
costPer1MTokens: 1.4,
|
|
1061
|
+
// $0.16 in / $2.63 out → very cheap
|
|
1062
|
+
contextWindow: 131e3,
|
|
1063
|
+
skills: {
|
|
1064
|
+
"code-generation": 82,
|
|
1065
|
+
// 65.8% SWE-bench (beats GPT-4.1 at 54.6%)
|
|
1066
|
+
"code-review": 80,
|
|
1067
|
+
debugging: 78,
|
|
1068
|
+
planning: 75,
|
|
1069
|
+
documentation: 80,
|
|
1070
|
+
testing: 75,
|
|
1071
|
+
security: 70,
|
|
1072
|
+
performance: 72,
|
|
1073
|
+
synthesis: 78,
|
|
1074
|
+
speed: 80,
|
|
1075
|
+
"context-length": 75
|
|
1076
|
+
},
|
|
1077
|
+
notes: "Strong value: 65.8% SWE-bench at very low cost. Good for routine tasks."
|
|
1078
|
+
},
|
|
1079
|
+
"kimi-k2.5": {
|
|
1080
|
+
model: "kimi-k2.5",
|
|
1081
|
+
provider: "kimi",
|
|
1082
|
+
displayName: "Kimi K2.5",
|
|
1083
|
+
costPer1MTokens: 8,
|
|
1084
|
+
// ~5.1x cheaper than GPT-5.2
|
|
1085
|
+
contextWindow: 256e3,
|
|
1086
|
+
skills: {
|
|
1087
|
+
"code-generation": 92,
|
|
1088
|
+
// 76.8% SWE-bench, 85 LiveCodeBench v6
|
|
1089
|
+
"code-review": 90,
|
|
1090
|
+
debugging: 90,
|
|
1091
|
+
// Strong analytical capabilities
|
|
1092
|
+
planning: 88,
|
|
1093
|
+
// User confirms "highly capable"
|
|
1094
|
+
documentation: 88,
|
|
1095
|
+
testing: 88,
|
|
1096
|
+
// 92% coding accuracy
|
|
1097
|
+
security: 82,
|
|
1098
|
+
performance: 85,
|
|
1099
|
+
synthesis: 92,
|
|
1100
|
+
// Can coordinate 100 sub-agents, 1500 tool calls
|
|
1101
|
+
speed: 75,
|
|
1102
|
+
// MoE: 1T total params, 32B active
|
|
1103
|
+
"context-length": 98
|
|
1104
|
+
// 256K context
|
|
1105
|
+
},
|
|
1106
|
+
notes: "Best open-source coding model. 5x cheaper than GPT-5.2. Excellent for frontend dev and multi-agent orchestration."
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
function getModelCapability(model) {
|
|
1110
|
+
return MODEL_CAPABILITIES[model];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/lib/smart-model-selector.ts
|
|
1114
|
+
var WORK_TYPE_REQUIREMENTS = {
|
|
1115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1116
|
+
// ISSUE AGENT PHASES
|
|
1117
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1118
|
+
"issue-agent:exploration": [
|
|
1119
|
+
{ skill: "speed", weight: 0.4 },
|
|
1120
|
+
// Need fast exploration
|
|
1121
|
+
{ skill: "context-length", weight: 0.3 },
|
|
1122
|
+
// Large codebases
|
|
1123
|
+
{ skill: "synthesis", weight: 0.3 }
|
|
1124
|
+
// Understanding structure
|
|
1125
|
+
],
|
|
1126
|
+
"issue-agent:planning": [
|
|
1127
|
+
{ skill: "planning", weight: 0.5 },
|
|
1128
|
+
// Primary skill
|
|
1129
|
+
{ skill: "code-review", weight: 0.2 },
|
|
1130
|
+
// Understanding existing code
|
|
1131
|
+
{ skill: "synthesis", weight: 0.3 }
|
|
1132
|
+
// Combining requirements
|
|
1133
|
+
],
|
|
1134
|
+
"issue-agent:implementation": [
|
|
1135
|
+
{ skill: "code-generation", weight: 0.6 },
|
|
1136
|
+
// Primary skill
|
|
1137
|
+
{ skill: "debugging", weight: 0.2 },
|
|
1138
|
+
// Avoiding bugs
|
|
1139
|
+
{ skill: "testing", weight: 0.2 }
|
|
1140
|
+
// Writing testable code
|
|
1141
|
+
],
|
|
1142
|
+
"issue-agent:testing": [
|
|
1143
|
+
{ skill: "testing", weight: 0.5 },
|
|
1144
|
+
// Primary skill
|
|
1145
|
+
{ skill: "code-generation", weight: 0.3 },
|
|
1146
|
+
// Writing test code
|
|
1147
|
+
{ skill: "debugging", weight: 0.2 }
|
|
1148
|
+
// Finding edge cases
|
|
1149
|
+
],
|
|
1150
|
+
"issue-agent:documentation": [
|
|
1151
|
+
{ skill: "documentation", weight: 0.6 },
|
|
1152
|
+
// Primary skill
|
|
1153
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1154
|
+
// Summarizing
|
|
1155
|
+
{ skill: "speed", weight: 0.1 }
|
|
1156
|
+
// Fast iteration
|
|
1157
|
+
],
|
|
1158
|
+
"issue-agent:review-response": [
|
|
1159
|
+
{ skill: "code-review", weight: 0.4 },
|
|
1160
|
+
// Understanding feedback
|
|
1161
|
+
{ skill: "code-generation", weight: 0.3 },
|
|
1162
|
+
// Making fixes
|
|
1163
|
+
{ skill: "debugging", weight: 0.3 }
|
|
1164
|
+
// Finding issues
|
|
1165
|
+
],
|
|
1166
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1167
|
+
// SPECIALIST AGENTS
|
|
1168
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1169
|
+
"specialist-review-agent": [
|
|
1170
|
+
{ skill: "code-review", weight: 0.5 },
|
|
1171
|
+
// Primary skill
|
|
1172
|
+
{ skill: "security", weight: 0.25 },
|
|
1173
|
+
// Security awareness
|
|
1174
|
+
{ skill: "performance", weight: 0.25 }
|
|
1175
|
+
// Performance awareness
|
|
1176
|
+
],
|
|
1177
|
+
"specialist-test-agent": [
|
|
1178
|
+
{ skill: "testing", weight: 0.5 },
|
|
1179
|
+
// Primary skill
|
|
1180
|
+
{ skill: "code-generation", weight: 0.3 },
|
|
1181
|
+
// Writing tests
|
|
1182
|
+
{ skill: "debugging", weight: 0.2 }
|
|
1183
|
+
// Finding issues
|
|
1184
|
+
],
|
|
1185
|
+
"specialist-merge-agent": [
|
|
1186
|
+
{ skill: "code-review", weight: 0.4 },
|
|
1187
|
+
// Understanding conflicts
|
|
1188
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1189
|
+
// Merging changes
|
|
1190
|
+
{ skill: "debugging", weight: 0.3 }
|
|
1191
|
+
// Resolving issues
|
|
1192
|
+
],
|
|
1193
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1194
|
+
// SUBAGENTS
|
|
1195
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1196
|
+
"subagent:explore": [
|
|
1197
|
+
{ skill: "speed", weight: 0.5 },
|
|
1198
|
+
// Need speed
|
|
1199
|
+
{ skill: "context-length", weight: 0.3 },
|
|
1200
|
+
// Large scope
|
|
1201
|
+
{ skill: "synthesis", weight: 0.2 }
|
|
1202
|
+
// Quick understanding
|
|
1203
|
+
],
|
|
1204
|
+
"subagent:plan": [
|
|
1205
|
+
{ skill: "planning", weight: 0.5 },
|
|
1206
|
+
// Primary skill
|
|
1207
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1208
|
+
// Combining info
|
|
1209
|
+
{ skill: "speed", weight: 0.2 }
|
|
1210
|
+
// Quick iteration
|
|
1211
|
+
],
|
|
1212
|
+
"subagent:bash": [
|
|
1213
|
+
{ skill: "speed", weight: 0.6 },
|
|
1214
|
+
// Fast execution
|
|
1215
|
+
{ skill: "code-generation", weight: 0.3 },
|
|
1216
|
+
// Command generation
|
|
1217
|
+
{ skill: "debugging", weight: 0.1 }
|
|
1218
|
+
// Error handling
|
|
1219
|
+
],
|
|
1220
|
+
"subagent:general-purpose": [
|
|
1221
|
+
{ skill: "speed", weight: 0.3 },
|
|
1222
|
+
// Balanced
|
|
1223
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1224
|
+
// General understanding
|
|
1225
|
+
{ skill: "code-generation", weight: 0.4 }
|
|
1226
|
+
// General tasks
|
|
1227
|
+
],
|
|
1228
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1229
|
+
// CONVOY MEMBERS
|
|
1230
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1231
|
+
"convoy:security-reviewer": [
|
|
1232
|
+
{ skill: "security", weight: 0.7 },
|
|
1233
|
+
// PRIMARY - never compromise
|
|
1234
|
+
{ skill: "code-review", weight: 0.2 },
|
|
1235
|
+
// Code understanding
|
|
1236
|
+
{ skill: "debugging", weight: 0.1 }
|
|
1237
|
+
// Finding vulnerabilities
|
|
1238
|
+
],
|
|
1239
|
+
"convoy:performance-reviewer": [
|
|
1240
|
+
{ skill: "performance", weight: 0.6 },
|
|
1241
|
+
// Primary skill
|
|
1242
|
+
{ skill: "code-review", weight: 0.3 },
|
|
1243
|
+
// Code understanding
|
|
1244
|
+
{ skill: "debugging", weight: 0.1 }
|
|
1245
|
+
// Finding bottlenecks
|
|
1246
|
+
],
|
|
1247
|
+
"convoy:correctness-reviewer": [
|
|
1248
|
+
{ skill: "code-review", weight: 0.4 },
|
|
1249
|
+
// Primary skill
|
|
1250
|
+
{ skill: "debugging", weight: 0.4 },
|
|
1251
|
+
// Finding bugs
|
|
1252
|
+
{ skill: "testing", weight: 0.2 }
|
|
1253
|
+
// Test coverage
|
|
1254
|
+
],
|
|
1255
|
+
"convoy:synthesis-agent": [
|
|
1256
|
+
{ skill: "synthesis", weight: 0.6 },
|
|
1257
|
+
// Primary skill
|
|
1258
|
+
{ skill: "documentation", weight: 0.2 },
|
|
1259
|
+
// Clear writing
|
|
1260
|
+
{ skill: "planning", weight: 0.2 }
|
|
1261
|
+
// Organizing findings
|
|
1262
|
+
],
|
|
1263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1264
|
+
// PRE-WORK AGENTS
|
|
1265
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1266
|
+
"prd-agent": [
|
|
1267
|
+
{ skill: "documentation", weight: 0.5 },
|
|
1268
|
+
// Primary skill
|
|
1269
|
+
{ skill: "planning", weight: 0.3 },
|
|
1270
|
+
// Structure
|
|
1271
|
+
{ skill: "synthesis", weight: 0.2 }
|
|
1272
|
+
// Combining requirements
|
|
1273
|
+
],
|
|
1274
|
+
"decomposition-agent": [
|
|
1275
|
+
{ skill: "planning", weight: 0.5 },
|
|
1276
|
+
// Primary skill
|
|
1277
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1278
|
+
// Breaking down
|
|
1279
|
+
{ skill: "documentation", weight: 0.2 }
|
|
1280
|
+
// Clear tasks
|
|
1281
|
+
],
|
|
1282
|
+
"triage-agent": [
|
|
1283
|
+
{ skill: "speed", weight: 0.4 },
|
|
1284
|
+
// Quick decisions
|
|
1285
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1286
|
+
// Understanding scope
|
|
1287
|
+
{ skill: "planning", weight: 0.3 }
|
|
1288
|
+
// Prioritization
|
|
1289
|
+
],
|
|
1290
|
+
"planning-agent": [
|
|
1291
|
+
{ skill: "planning", weight: 0.5 },
|
|
1292
|
+
// Primary skill
|
|
1293
|
+
{ skill: "code-review", weight: 0.3 },
|
|
1294
|
+
// Understanding codebase
|
|
1295
|
+
{ skill: "synthesis", weight: 0.2 }
|
|
1296
|
+
// Combining approaches
|
|
1297
|
+
],
|
|
1298
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1299
|
+
// CLI CONTEXTS
|
|
1300
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1301
|
+
"cli:interactive": [
|
|
1302
|
+
{ skill: "speed", weight: 0.4 },
|
|
1303
|
+
// Responsive
|
|
1304
|
+
{ skill: "synthesis", weight: 0.3 },
|
|
1305
|
+
// Understanding context
|
|
1306
|
+
{ skill: "code-generation", weight: 0.3 }
|
|
1307
|
+
// Quick code
|
|
1308
|
+
],
|
|
1309
|
+
"cli:quick-command": [
|
|
1310
|
+
{ skill: "speed", weight: 0.7 },
|
|
1311
|
+
// Must be fast
|
|
1312
|
+
{ skill: "code-generation", weight: 0.2 },
|
|
1313
|
+
// Simple generation
|
|
1314
|
+
{ skill: "synthesis", weight: 0.1 }
|
|
1315
|
+
// Quick understanding
|
|
1316
|
+
]
|
|
1317
|
+
};
|
|
1318
|
+
function calculateSkillScore(model, requirements) {
|
|
1319
|
+
const cap = getModelCapability(model);
|
|
1320
|
+
let totalScore = 0;
|
|
1321
|
+
let totalWeight = 0;
|
|
1322
|
+
for (const req of requirements) {
|
|
1323
|
+
totalScore += cap.skills[req.skill] * req.weight;
|
|
1324
|
+
totalWeight += req.weight;
|
|
1325
|
+
}
|
|
1326
|
+
return totalWeight > 0 ? totalScore / totalWeight : 0;
|
|
1327
|
+
}
|
|
1328
|
+
function calculateSelectionScore(model, skillScore) {
|
|
1329
|
+
return skillScore;
|
|
1330
|
+
}
|
|
1331
|
+
function selectModel(workType, availableModels, options = {}) {
|
|
1332
|
+
const { minCapability = 50, forceModel } = options;
|
|
1333
|
+
if (forceModel) {
|
|
1334
|
+
if (availableModels.includes(forceModel)) {
|
|
1335
|
+
return {
|
|
1336
|
+
model: forceModel,
|
|
1337
|
+
score: 100,
|
|
1338
|
+
reason: `Forced selection: ${forceModel}`,
|
|
1339
|
+
candidates: [{ model: forceModel, score: 100, available: true }]
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const requirements = WORK_TYPE_REQUIREMENTS[workType];
|
|
1344
|
+
const allModels = Object.keys(MODEL_CAPABILITIES);
|
|
1345
|
+
const candidates = allModels.map((model) => {
|
|
1346
|
+
const skillScore = calculateSkillScore(model, requirements);
|
|
1347
|
+
const selectionScore = calculateSelectionScore(model, skillScore);
|
|
1348
|
+
const available = availableModels.includes(model);
|
|
1349
|
+
return {
|
|
1350
|
+
model,
|
|
1351
|
+
skillScore,
|
|
1352
|
+
score: selectionScore,
|
|
1353
|
+
available
|
|
1354
|
+
};
|
|
1355
|
+
});
|
|
1356
|
+
const eligible = candidates.filter(
|
|
1357
|
+
(c) => c.available && c.skillScore >= minCapability
|
|
1358
|
+
);
|
|
1359
|
+
eligible.sort((a, b) => b.score - a.score);
|
|
1360
|
+
if (eligible.length === 0) {
|
|
1361
|
+
const fallback = candidates.filter((c) => c.available).sort((a, b) => b.score - a.score)[0];
|
|
1362
|
+
if (!fallback) {
|
|
1363
|
+
return {
|
|
1364
|
+
model: "claude-sonnet-4-5",
|
|
1365
|
+
score: 0,
|
|
1366
|
+
reason: "No models available, falling back to default",
|
|
1367
|
+
candidates: candidates.map((c) => ({
|
|
1368
|
+
model: c.model,
|
|
1369
|
+
score: c.score,
|
|
1370
|
+
available: c.available
|
|
1371
|
+
}))
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
model: fallback.model,
|
|
1376
|
+
score: fallback.score,
|
|
1377
|
+
reason: `Best available (below min threshold): ${fallback.model}`,
|
|
1378
|
+
candidates: candidates.map((c) => ({
|
|
1379
|
+
model: c.model,
|
|
1380
|
+
score: c.score,
|
|
1381
|
+
available: c.available
|
|
1382
|
+
}))
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
const selected = eligible[0];
|
|
1386
|
+
const cap = getModelCapability(selected.model);
|
|
1387
|
+
const topSkills = requirements.sort((a, b) => b.weight - a.weight).slice(0, 2).map((r) => r.skill);
|
|
1388
|
+
const reason = `Best for ${workType}: ${cap.displayName} (${topSkills.join(", ")}: ${Math.round(selected.skillScore)}, cost: $${cap.costPer1MTokens}/1M)`;
|
|
1389
|
+
return {
|
|
1390
|
+
model: selected.model,
|
|
1391
|
+
score: selected.score,
|
|
1392
|
+
reason,
|
|
1393
|
+
candidates: candidates.map((c) => ({
|
|
1394
|
+
model: c.model,
|
|
1395
|
+
score: c.score,
|
|
1396
|
+
available: c.available
|
|
1397
|
+
}))
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/lib/work-type-router.ts
|
|
1402
|
+
var WorkTypeRouter = class {
|
|
1403
|
+
config;
|
|
1404
|
+
availableModels = null;
|
|
1405
|
+
constructor(config) {
|
|
1406
|
+
this.config = config || loadConfig();
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Get list of available models based on enabled providers
|
|
1410
|
+
*/
|
|
1411
|
+
getAvailableModels() {
|
|
1412
|
+
if (this.availableModels) {
|
|
1413
|
+
return this.availableModels;
|
|
1414
|
+
}
|
|
1415
|
+
const available = [];
|
|
1416
|
+
for (const provider of this.config.enabledProviders) {
|
|
1417
|
+
available.push(...getModelsByProvider(provider));
|
|
1418
|
+
}
|
|
1419
|
+
this.availableModels = available;
|
|
1420
|
+
return available;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Get model for a specific work type
|
|
1424
|
+
*
|
|
1425
|
+
* Resolution order:
|
|
1426
|
+
* 1. Per-project/global override (if configured)
|
|
1427
|
+
* 2. Smart selection (capability-based)
|
|
1428
|
+
*/
|
|
1429
|
+
getModel(workTypeId) {
|
|
1430
|
+
validateWorkType(workTypeId);
|
|
1431
|
+
let model;
|
|
1432
|
+
let source;
|
|
1433
|
+
let originalModel;
|
|
1434
|
+
let selection;
|
|
1435
|
+
if (this.config.overrides[workTypeId]) {
|
|
1436
|
+
model = this.config.overrides[workTypeId];
|
|
1437
|
+
source = "override";
|
|
1438
|
+
selection = {
|
|
1439
|
+
score: 100,
|
|
1440
|
+
reason: `Explicit override: ${model}`
|
|
1441
|
+
};
|
|
1442
|
+
} else {
|
|
1443
|
+
const availableModels = this.getAvailableModels();
|
|
1444
|
+
const result = selectModel(workTypeId, availableModels);
|
|
1445
|
+
model = result.model;
|
|
1446
|
+
source = "smart";
|
|
1447
|
+
selection = {
|
|
1448
|
+
score: result.score,
|
|
1449
|
+
reason: result.reason
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
originalModel = model;
|
|
1453
|
+
model = applyFallback(model, this.config.enabledProviders);
|
|
1454
|
+
return {
|
|
1455
|
+
model,
|
|
1456
|
+
workType: workTypeId,
|
|
1457
|
+
source,
|
|
1458
|
+
usedFallback: model !== originalModel,
|
|
1459
|
+
originalModel: model !== originalModel ? originalModel : void 0,
|
|
1460
|
+
selection
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Get just the model ID for a work type (convenience method)
|
|
1465
|
+
*/
|
|
1466
|
+
getModelId(workTypeId) {
|
|
1467
|
+
return this.getModel(workTypeId).model;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Check if a work type has an override configured
|
|
1471
|
+
*/
|
|
1472
|
+
hasOverride(workTypeId) {
|
|
1473
|
+
return workTypeId in this.config.overrides;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Get the set of enabled providers
|
|
1477
|
+
*/
|
|
1478
|
+
getEnabledProviders() {
|
|
1479
|
+
return this.config.enabledProviders;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Get all configured overrides
|
|
1483
|
+
*/
|
|
1484
|
+
getOverrides() {
|
|
1485
|
+
return { ...this.config.overrides };
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Get API keys configuration
|
|
1489
|
+
*/
|
|
1490
|
+
getApiKeys() {
|
|
1491
|
+
return { ...this.config.apiKeys };
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Get Gemini thinking level
|
|
1495
|
+
*/
|
|
1496
|
+
getGeminiThinkingLevel() {
|
|
1497
|
+
return this.config.geminiThinkingLevel;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Reload configuration from disk
|
|
1501
|
+
*/
|
|
1502
|
+
reloadConfig() {
|
|
1503
|
+
this.config = loadConfig();
|
|
1504
|
+
this.availableModels = null;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Get debug information about current configuration
|
|
1508
|
+
*/
|
|
1509
|
+
getDebugInfo() {
|
|
1510
|
+
return {
|
|
1511
|
+
enabledProviders: Array.from(this.config.enabledProviders),
|
|
1512
|
+
availableModelCount: this.getAvailableModels().length,
|
|
1513
|
+
overrideCount: Object.keys(this.config.overrides).length,
|
|
1514
|
+
hasApiKeys: {
|
|
1515
|
+
openai: !!this.config.apiKeys.openai,
|
|
1516
|
+
google: !!this.config.apiKeys.google,
|
|
1517
|
+
zai: !!this.config.apiKeys.zai,
|
|
1518
|
+
kimi: !!this.config.apiKeys.kimi
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
var globalRouter = null;
|
|
1524
|
+
function getGlobalRouter() {
|
|
1525
|
+
if (!globalRouter) {
|
|
1526
|
+
globalRouter = new WorkTypeRouter();
|
|
1527
|
+
}
|
|
1528
|
+
return globalRouter;
|
|
1529
|
+
}
|
|
1530
|
+
function getModelId(workTypeId) {
|
|
1531
|
+
return getGlobalRouter().getModelId(workTypeId);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/lib/agents.ts
|
|
1535
|
+
var execAsync = promisify(exec);
|
|
1536
|
+
function getReadySignalPath(agentId) {
|
|
1537
|
+
return join4(getAgentDir(agentId), "ready.json");
|
|
1538
|
+
}
|
|
1539
|
+
function clearReadySignal(agentId) {
|
|
1540
|
+
const readyPath = getReadySignalPath(agentId);
|
|
1541
|
+
if (existsSync4(readyPath)) {
|
|
1542
|
+
try {
|
|
1543
|
+
unlinkSync2(readyPath);
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
async function waitForReadySignal(agentId, timeoutSeconds = 30) {
|
|
1549
|
+
const readyPath = getReadySignalPath(agentId);
|
|
1550
|
+
for (let i = 0; i < timeoutSeconds; i++) {
|
|
1551
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1552
|
+
if (existsSync4(readyPath)) {
|
|
1553
|
+
try {
|
|
1554
|
+
const content = readFileSync4(readyPath, "utf-8");
|
|
1555
|
+
const signal = JSON.parse(content);
|
|
1556
|
+
if (signal.ready === true) {
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
function getAgentDir(agentId) {
|
|
1566
|
+
return join4(AGENTS_DIR, agentId);
|
|
1567
|
+
}
|
|
1568
|
+
function getAgentState(agentId) {
|
|
1569
|
+
const stateFile = join4(getAgentDir(agentId), "state.json");
|
|
1570
|
+
if (!existsSync4(stateFile)) return null;
|
|
1571
|
+
const content = readFileSync4(stateFile, "utf8");
|
|
1572
|
+
return JSON.parse(content);
|
|
1573
|
+
}
|
|
1574
|
+
function saveAgentState(state) {
|
|
1575
|
+
const dir = getAgentDir(state.id);
|
|
1576
|
+
mkdirSync3(dir, { recursive: true });
|
|
1577
|
+
writeFileSync4(
|
|
1578
|
+
join4(dir, "state.json"),
|
|
1579
|
+
JSON.stringify(state, null, 2)
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
function getAgentRuntimeState(agentId) {
|
|
1583
|
+
const stateFile = join4(getAgentDir(agentId), "state.json");
|
|
1584
|
+
if (!existsSync4(stateFile)) {
|
|
1585
|
+
return {
|
|
1586
|
+
state: "uninitialized",
|
|
1587
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
try {
|
|
1591
|
+
const content = readFileSync4(stateFile, "utf8");
|
|
1592
|
+
return JSON.parse(content);
|
|
1593
|
+
} catch {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function saveAgentRuntimeState(agentId, state) {
|
|
1598
|
+
const dir = getAgentDir(agentId);
|
|
1599
|
+
mkdirSync3(dir, { recursive: true });
|
|
1600
|
+
const existing = getAgentRuntimeState(agentId);
|
|
1601
|
+
const merged = {
|
|
1602
|
+
...existing || { state: "uninitialized", lastActivity: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1603
|
+
...state
|
|
1604
|
+
};
|
|
1605
|
+
writeFileSync4(
|
|
1606
|
+
join4(dir, "state.json"),
|
|
1607
|
+
JSON.stringify(merged, null, 2)
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
function appendActivity(agentId, entry) {
|
|
1611
|
+
const dir = getAgentDir(agentId);
|
|
1612
|
+
mkdirSync3(dir, { recursive: true });
|
|
1613
|
+
const activityFile = join4(dir, "activity.jsonl");
|
|
1614
|
+
appendFileSync(activityFile, JSON.stringify(entry) + "\n");
|
|
1615
|
+
if (existsSync4(activityFile)) {
|
|
1616
|
+
try {
|
|
1617
|
+
const lines = readFileSync4(activityFile, "utf8").trim().split("\n");
|
|
1618
|
+
if (lines.length > 100) {
|
|
1619
|
+
const trimmed = lines.slice(-100);
|
|
1620
|
+
writeFileSync4(activityFile, trimmed.join("\n") + "\n");
|
|
1621
|
+
}
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function getActivity(agentId, limit = 100) {
|
|
1627
|
+
const activityFile = join4(getAgentDir(agentId), "activity.jsonl");
|
|
1628
|
+
if (!existsSync4(activityFile)) {
|
|
1629
|
+
return [];
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
const lines = readFileSync4(activityFile, "utf8").trim().split("\n");
|
|
1633
|
+
const entries = lines.filter((line) => line.trim()).map((line) => JSON.parse(line)).slice(-limit);
|
|
1634
|
+
return entries;
|
|
1635
|
+
} catch {
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
function saveSessionId(agentId, sessionId) {
|
|
1640
|
+
const dir = getAgentDir(agentId);
|
|
1641
|
+
mkdirSync3(dir, { recursive: true });
|
|
1642
|
+
writeFileSync4(join4(dir, "session.id"), sessionId);
|
|
1643
|
+
}
|
|
1644
|
+
function getSessionId(agentId) {
|
|
1645
|
+
const sessionFile = join4(getAgentDir(agentId), "session.id");
|
|
1646
|
+
if (!existsSync4(sessionFile)) {
|
|
1647
|
+
return null;
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
return readFileSync4(sessionFile, "utf8").trim();
|
|
1651
|
+
} catch {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function determineModel(options) {
|
|
1656
|
+
console.log(`[DEBUG] determineModel called with:`, { model: options.model, workType: options.workType, phase: options.phase, agentType: options.agentType, difficulty: options.difficulty });
|
|
1657
|
+
if (options.model) {
|
|
1658
|
+
console.log(`[DEBUG] Using explicit model: ${options.model}`);
|
|
1659
|
+
return options.model;
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
if (options.workType) {
|
|
1663
|
+
return getModelId(options.workType);
|
|
1664
|
+
}
|
|
1665
|
+
if (options.phase) {
|
|
1666
|
+
const workType = `issue-agent:${options.phase}`;
|
|
1667
|
+
return getModelId(workType);
|
|
1668
|
+
}
|
|
1669
|
+
if (options.agentType && options.agentType !== "work-agent") {
|
|
1670
|
+
if (options.agentType === "planning-agent") {
|
|
1671
|
+
return getModelId("planning-agent");
|
|
1672
|
+
}
|
|
1673
|
+
const workType = `specialist-${options.agentType}`;
|
|
1674
|
+
return getModelId(workType);
|
|
1675
|
+
}
|
|
1676
|
+
if (options.difficulty) {
|
|
1677
|
+
const settings = loadSettings();
|
|
1678
|
+
if (settings.models.complexity[options.difficulty]) {
|
|
1679
|
+
console.warn(`Using legacy complexity-based routing for ${options.difficulty}. Consider migrating to work types.`);
|
|
1680
|
+
return settings.models.complexity[options.difficulty];
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return "kimi-k2.5";
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
console.warn("Warning: Could not resolve model using work type router, using default");
|
|
1686
|
+
console.log(`[DEBUG] Catch block, options.model: ${options.model}, fallback: kimi-k2.5`);
|
|
1687
|
+
return options.model || "kimi-k2.5";
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
async function spawnAgent(options) {
|
|
1691
|
+
const agentId = `agent-${options.issueId.toLowerCase()}`;
|
|
1692
|
+
if (sessionExists(agentId)) {
|
|
1693
|
+
throw new Error(`Agent ${agentId} already running. Use 'pan work tell' to message it.`);
|
|
1694
|
+
}
|
|
1695
|
+
initHook(agentId);
|
|
1696
|
+
const selectedModel = determineModel(options);
|
|
1697
|
+
console.log(`[DEBUG] Selected model: ${selectedModel}`);
|
|
1698
|
+
const state = {
|
|
1699
|
+
id: agentId,
|
|
1700
|
+
issueId: options.issueId,
|
|
1701
|
+
workspace: options.workspace,
|
|
1702
|
+
runtime: options.runtime || "claude",
|
|
1703
|
+
model: selectedModel,
|
|
1704
|
+
status: "starting",
|
|
1705
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1706
|
+
// Initialize Phase 4 fields (legacy)
|
|
1707
|
+
complexity: options.difficulty,
|
|
1708
|
+
handoffCount: 0,
|
|
1709
|
+
costSoFar: 0,
|
|
1710
|
+
// Work type system (PAN-118)
|
|
1711
|
+
phase: options.phase,
|
|
1712
|
+
workType: options.workType
|
|
1713
|
+
};
|
|
1714
|
+
saveAgentState(state);
|
|
1715
|
+
let prompt = options.prompt || "";
|
|
1716
|
+
const { hasWork, items } = checkHook(agentId);
|
|
1717
|
+
if (hasWork) {
|
|
1718
|
+
const fixedPointPrompt = generateFixedPointPrompt(agentId);
|
|
1719
|
+
if (fixedPointPrompt) {
|
|
1720
|
+
prompt = fixedPointPrompt + "\n\n---\n\n" + prompt;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
const promptFile = join4(getAgentDir(agentId), "initial-prompt.md");
|
|
1724
|
+
if (prompt) {
|
|
1725
|
+
writeFileSync4(promptFile, prompt);
|
|
1726
|
+
}
|
|
1727
|
+
checkAndSetupHooks();
|
|
1728
|
+
writeTaskCache(agentId, options.issueId);
|
|
1729
|
+
clearReadySignal(agentId);
|
|
1730
|
+
const provider = getProviderForModel(selectedModel);
|
|
1731
|
+
const settings = loadSettings();
|
|
1732
|
+
let providerEnv = {};
|
|
1733
|
+
if (provider.name !== "anthropic") {
|
|
1734
|
+
const apiKey = settings.api_keys[provider.name];
|
|
1735
|
+
if (apiKey) {
|
|
1736
|
+
providerEnv = getProviderEnv(provider, apiKey);
|
|
1737
|
+
} else {
|
|
1738
|
+
console.warn(`Warning: No API key configured for ${provider.displayName}, falling back to Anthropic`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
let claudeCmd;
|
|
1742
|
+
if (prompt) {
|
|
1743
|
+
const launcherScript = join4(getAgentDir(agentId), "launcher.sh");
|
|
1744
|
+
const launcherContent = `#!/bin/bash
|
|
1745
|
+
prompt=$(cat "${promptFile}")
|
|
1746
|
+
exec claude --dangerously-skip-permissions --model ${state.model} "$prompt"
|
|
1747
|
+
`;
|
|
1748
|
+
writeFileSync4(launcherScript, launcherContent, { mode: 493 });
|
|
1749
|
+
claudeCmd = `bash "${launcherScript}"`;
|
|
1750
|
+
} else {
|
|
1751
|
+
claudeCmd = `claude --dangerously-skip-permissions --model ${state.model}`;
|
|
1752
|
+
}
|
|
1753
|
+
createSession(agentId, options.workspace, claudeCmd, {
|
|
1754
|
+
env: {
|
|
1755
|
+
PANOPTICON_AGENT_ID: agentId,
|
|
1756
|
+
...providerEnv
|
|
1757
|
+
// Add provider-specific env vars (BASE_URL, AUTH_TOKEN, etc.)
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
state.status = "running";
|
|
1761
|
+
saveAgentState(state);
|
|
1762
|
+
startWork(agentId, options.issueId);
|
|
1763
|
+
return state;
|
|
1764
|
+
}
|
|
1765
|
+
function listRunningAgents() {
|
|
1766
|
+
const tmuxSessions = getAgentSessions();
|
|
1767
|
+
const tmuxNames = new Set(tmuxSessions.map((s) => s.name));
|
|
1768
|
+
const agents = [];
|
|
1769
|
+
if (!existsSync4(AGENTS_DIR)) return agents;
|
|
1770
|
+
const dirs = readdirSync3(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
1771
|
+
for (const dir of dirs) {
|
|
1772
|
+
const state = getAgentState(dir.name);
|
|
1773
|
+
if (state) {
|
|
1774
|
+
agents.push({
|
|
1775
|
+
...state,
|
|
1776
|
+
tmuxActive: tmuxNames.has(state.id)
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return agents;
|
|
1781
|
+
}
|
|
1782
|
+
function stopAgent(agentId) {
|
|
1783
|
+
const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
|
|
1784
|
+
if (sessionExists(normalizedId)) {
|
|
1785
|
+
killSession(normalizedId);
|
|
1786
|
+
}
|
|
1787
|
+
const state = getAgentState(normalizedId);
|
|
1788
|
+
if (state) {
|
|
1789
|
+
state.status = "stopped";
|
|
1790
|
+
saveAgentState(state);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
async function messageAgent(agentId, message) {
|
|
1794
|
+
const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
|
|
1795
|
+
const runtimeState = getAgentRuntimeState(normalizedId);
|
|
1796
|
+
if (runtimeState?.state === "suspended") {
|
|
1797
|
+
console.log(`[agents] Auto-resuming suspended agent ${normalizedId} to deliver message`);
|
|
1798
|
+
const result = await resumeAgent(normalizedId, message);
|
|
1799
|
+
if (!result.success) {
|
|
1800
|
+
throw new Error(`Failed to auto-resume agent: ${result.error}`);
|
|
1801
|
+
}
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (!sessionExists(normalizedId)) {
|
|
1805
|
+
throw new Error(`Agent ${normalizedId} not running`);
|
|
1806
|
+
}
|
|
1807
|
+
sendKeys(normalizedId, message);
|
|
1808
|
+
const mailDir = join4(getAgentDir(normalizedId), "mail");
|
|
1809
|
+
mkdirSync3(mailDir, { recursive: true });
|
|
1810
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1811
|
+
writeFileSync4(
|
|
1812
|
+
join4(mailDir, `${timestamp}.md`),
|
|
1813
|
+
`# Message
|
|
1814
|
+
|
|
1815
|
+
${message}
|
|
1816
|
+
`
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
async function resumeAgent(agentId, message) {
|
|
1820
|
+
const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
|
|
1821
|
+
const runtimeState = getAgentRuntimeState(normalizedId);
|
|
1822
|
+
if (!runtimeState || runtimeState.state !== "suspended") {
|
|
1823
|
+
return {
|
|
1824
|
+
success: false,
|
|
1825
|
+
error: `Cannot resume agent in state: ${runtimeState?.state || "unknown"}`
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
const sessionId = getSessionId(normalizedId);
|
|
1829
|
+
if (!sessionId) {
|
|
1830
|
+
return {
|
|
1831
|
+
success: false,
|
|
1832
|
+
error: "No saved session ID found"
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
const agentState = getAgentState(normalizedId);
|
|
1836
|
+
if (!agentState) {
|
|
1837
|
+
return {
|
|
1838
|
+
success: false,
|
|
1839
|
+
error: "Agent state not found"
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
if (sessionExists(normalizedId)) {
|
|
1843
|
+
return {
|
|
1844
|
+
success: false,
|
|
1845
|
+
error: "Agent session already exists"
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
clearReadySignal(normalizedId);
|
|
1850
|
+
const claudeCmd = `claude --resume "${sessionId}" --dangerously-skip-permissions`;
|
|
1851
|
+
createSession(normalizedId, agentState.workspace, claudeCmd, {
|
|
1852
|
+
env: {
|
|
1853
|
+
PANOPTICON_AGENT_ID: normalizedId
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
if (message) {
|
|
1857
|
+
const ready = await waitForReadySignal(normalizedId, 30);
|
|
1858
|
+
if (ready) {
|
|
1859
|
+
sendKeys(normalizedId, message);
|
|
1860
|
+
} else {
|
|
1861
|
+
console.error("Claude SessionStart hook did not fire during resume, message not sent");
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
saveAgentRuntimeState(normalizedId, {
|
|
1865
|
+
state: "active",
|
|
1866
|
+
resumedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1867
|
+
});
|
|
1868
|
+
if (agentState) {
|
|
1869
|
+
agentState.status = "running";
|
|
1870
|
+
agentState.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1871
|
+
saveAgentState(agentState);
|
|
1872
|
+
}
|
|
1873
|
+
return { success: true };
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1876
|
+
return {
|
|
1877
|
+
success: false,
|
|
1878
|
+
error: `Failed to resume agent: ${msg}`
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function detectCrashedAgents() {
|
|
1883
|
+
const agents = listRunningAgents();
|
|
1884
|
+
return agents.filter(
|
|
1885
|
+
(agent) => agent.status === "running" && !agent.tmuxActive
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
function recoverAgent(agentId) {
|
|
1889
|
+
const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
|
|
1890
|
+
const state = getAgentState(normalizedId);
|
|
1891
|
+
if (!state) {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
if (sessionExists(normalizedId)) {
|
|
1895
|
+
return state;
|
|
1896
|
+
}
|
|
1897
|
+
const healthFile = join4(getAgentDir(normalizedId), "health.json");
|
|
1898
|
+
let health = { consecutiveFailures: 0, killCount: 0, recoveryCount: 0 };
|
|
1899
|
+
if (existsSync4(healthFile)) {
|
|
1900
|
+
try {
|
|
1901
|
+
health = { ...health, ...JSON.parse(readFileSync4(healthFile, "utf-8")) };
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
health.recoveryCount = (health.recoveryCount || 0) + 1;
|
|
1906
|
+
writeFileSync4(healthFile, JSON.stringify(health, null, 2));
|
|
1907
|
+
const recoveryPrompt = generateRecoveryPrompt(state);
|
|
1908
|
+
const claudeCmd = `claude --dangerously-skip-permissions --model ${state.model} "${recoveryPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
|
|
1909
|
+
createSession(normalizedId, state.workspace, claudeCmd);
|
|
1910
|
+
state.status = "running";
|
|
1911
|
+
state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1912
|
+
saveAgentState(state);
|
|
1913
|
+
return state;
|
|
1914
|
+
}
|
|
1915
|
+
function generateRecoveryPrompt(state) {
|
|
1916
|
+
const lines = [
|
|
1917
|
+
"# Agent Recovery",
|
|
1918
|
+
"",
|
|
1919
|
+
"\u26A0\uFE0F This agent session was recovered after a crash.",
|
|
1920
|
+
"",
|
|
1921
|
+
"## Previous Context",
|
|
1922
|
+
`- Issue: ${state.issueId}`,
|
|
1923
|
+
`- Workspace: ${state.workspace}`,
|
|
1924
|
+
`- Started: ${state.startedAt}`,
|
|
1925
|
+
"",
|
|
1926
|
+
"## Recovery Steps",
|
|
1927
|
+
"1. Check beads for context: `bd show " + state.issueId + "`",
|
|
1928
|
+
"2. Review recent git commits: `git log --oneline -10`",
|
|
1929
|
+
"3. Check hook for pending work: `pan work hook check`",
|
|
1930
|
+
"4. Resume from last known state",
|
|
1931
|
+
"",
|
|
1932
|
+
"## FPP Reminder",
|
|
1933
|
+
'> "Any runnable action is a fixed point and must resolve before the system can rest."',
|
|
1934
|
+
""
|
|
1935
|
+
];
|
|
1936
|
+
const { hasWork } = checkHook(state.id);
|
|
1937
|
+
if (hasWork) {
|
|
1938
|
+
const fixedPointPrompt = generateFixedPointPrompt(state.id);
|
|
1939
|
+
if (fixedPointPrompt) {
|
|
1940
|
+
lines.push("---");
|
|
1941
|
+
lines.push("");
|
|
1942
|
+
lines.push(fixedPointPrompt);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return lines.join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
function autoRecoverAgents() {
|
|
1948
|
+
const crashed = detectCrashedAgents();
|
|
1949
|
+
const recovered = [];
|
|
1950
|
+
const failed = [];
|
|
1951
|
+
for (const agent of crashed) {
|
|
1952
|
+
try {
|
|
1953
|
+
const result = recoverAgent(agent.id);
|
|
1954
|
+
if (result) {
|
|
1955
|
+
recovered.push(agent.id);
|
|
1956
|
+
} else {
|
|
1957
|
+
failed.push(agent.id);
|
|
1958
|
+
}
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
failed.push(agent.id);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return { recovered, failed };
|
|
1964
|
+
}
|
|
1965
|
+
function checkAndSetupHooks() {
|
|
1966
|
+
const settingsPath = join4(homedir2(), ".claude", "settings.json");
|
|
1967
|
+
const hookPath = join4(homedir2(), ".panopticon", "bin", "heartbeat-hook");
|
|
1968
|
+
if (existsSync4(settingsPath)) {
|
|
1969
|
+
try {
|
|
1970
|
+
const settingsContent = readFileSync4(settingsPath, "utf-8");
|
|
1971
|
+
const settings = JSON.parse(settingsContent);
|
|
1972
|
+
const postToolUse = settings?.hooks?.PostToolUse || [];
|
|
1973
|
+
const hookConfigured = postToolUse.some(
|
|
1974
|
+
(hookConfig) => hookConfig.hooks?.some(
|
|
1975
|
+
(hook) => hook.command === hookPath || hook.command?.includes("panopticon") || hook.command?.includes("heartbeat-hook")
|
|
1976
|
+
)
|
|
1977
|
+
);
|
|
1978
|
+
if (hookConfigured) {
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
console.log("Configuring Panopticon heartbeat hooks...");
|
|
1986
|
+
exec("pan setup hooks", (error) => {
|
|
1987
|
+
if (error) {
|
|
1988
|
+
console.warn("\u26A0 Failed to auto-configure hooks. Run `pan setup hooks` manually.");
|
|
1989
|
+
} else {
|
|
1990
|
+
console.log("\u2713 Heartbeat hooks configured");
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
console.warn("\u26A0 Failed to auto-configure hooks. Run `pan setup hooks` manually.");
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function writeTaskCache(agentId, issueId) {
|
|
1998
|
+
const cacheDir = join4(getAgentDir(agentId));
|
|
1999
|
+
mkdirSync3(cacheDir, { recursive: true });
|
|
2000
|
+
const cacheFile = join4(cacheDir, "current-task.json");
|
|
2001
|
+
writeFileSync4(
|
|
2002
|
+
cacheFile,
|
|
2003
|
+
JSON.stringify({
|
|
2004
|
+
id: issueId,
|
|
2005
|
+
title: `Working on ${issueId}`,
|
|
2006
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2007
|
+
}, null, 2)
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
export {
|
|
2012
|
+
sessionExists,
|
|
2013
|
+
createSession,
|
|
2014
|
+
killSession,
|
|
2015
|
+
sendKeys,
|
|
2016
|
+
pushToHook,
|
|
2017
|
+
checkHook,
|
|
2018
|
+
popFromHook,
|
|
2019
|
+
clearHook,
|
|
2020
|
+
sendMail,
|
|
2021
|
+
generateFixedPointPrompt,
|
|
2022
|
+
getAgentCV,
|
|
2023
|
+
getAgentRankings,
|
|
2024
|
+
formatCV,
|
|
2025
|
+
getModelId,
|
|
2026
|
+
getAgentDir,
|
|
2027
|
+
getAgentState,
|
|
2028
|
+
saveAgentState,
|
|
2029
|
+
getAgentRuntimeState,
|
|
2030
|
+
saveAgentRuntimeState,
|
|
2031
|
+
appendActivity,
|
|
2032
|
+
getActivity,
|
|
2033
|
+
saveSessionId,
|
|
2034
|
+
getSessionId,
|
|
2035
|
+
spawnAgent,
|
|
2036
|
+
listRunningAgents,
|
|
2037
|
+
stopAgent,
|
|
2038
|
+
messageAgent,
|
|
2039
|
+
resumeAgent,
|
|
2040
|
+
detectCrashedAgents,
|
|
2041
|
+
recoverAgent,
|
|
2042
|
+
autoRecoverAgents
|
|
2043
|
+
};
|
|
2044
|
+
//# sourceMappingURL=chunk-H45CLB7E.js.map
|