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.
@@ -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