panopticon-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3439 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AGENTS_DIR,
4
+ CLAUDE_DIR,
5
+ CLAUDE_MD_TEMPLATES,
6
+ COMMANDS_DIR,
7
+ CONFIG_FILE,
8
+ INIT_DIRS,
9
+ PANOPTICON_HOME,
10
+ SKILLS_DIR,
11
+ SYNC_TARGETS,
12
+ __require,
13
+ addAlias,
14
+ createBackup,
15
+ detectShell,
16
+ executeSync,
17
+ getAliasInstructions,
18
+ getDefaultConfig,
19
+ getShellRcFile,
20
+ listBackups,
21
+ loadConfig,
22
+ planSync,
23
+ restoreBackup,
24
+ saveConfig
25
+ } from "../chunk-FR2P66GU.js";
26
+
27
+ // src/cli/index.ts
28
+ import { Command } from "commander";
29
+ import chalk23 from "chalk";
30
+
31
+ // src/cli/commands/init.ts
32
+ import { existsSync, mkdirSync } from "fs";
33
+ import chalk from "chalk";
34
+ import ora from "ora";
35
+ async function initCommand() {
36
+ const spinner = ora("Initializing Panopticon...").start();
37
+ if (existsSync(CONFIG_FILE)) {
38
+ spinner.info("Panopticon already initialized");
39
+ console.log(chalk.dim(` Config: ${CONFIG_FILE}`));
40
+ console.log(chalk.dim(` Home: ${PANOPTICON_HOME}`));
41
+ return;
42
+ }
43
+ try {
44
+ for (const dir of INIT_DIRS) {
45
+ if (!existsSync(dir)) {
46
+ mkdirSync(dir, { recursive: true });
47
+ }
48
+ }
49
+ spinner.text = "Created directories...";
50
+ const config = getDefaultConfig();
51
+ saveConfig(config);
52
+ spinner.text = "Created config...";
53
+ const shell = detectShell();
54
+ const rcFile = getShellRcFile(shell);
55
+ if (rcFile && existsSync(rcFile)) {
56
+ addAlias(rcFile);
57
+ spinner.succeed("Panopticon initialized!");
58
+ console.log("");
59
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
60
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
61
+ console.log(chalk.green("\u2713") + " " + getAliasInstructions(shell));
62
+ } else {
63
+ spinner.succeed("Panopticon initialized!");
64
+ console.log("");
65
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(PANOPTICON_HOME));
66
+ console.log(chalk.green("\u2713") + " Created " + chalk.cyan(CONFIG_FILE));
67
+ console.log(chalk.yellow("!") + " Could not detect shell. Add alias manually:");
68
+ console.log(chalk.dim(' alias pan="panopticon"'));
69
+ }
70
+ console.log("");
71
+ console.log("Next steps:");
72
+ console.log(chalk.dim(" 1. Add skills to ~/.panopticon/skills/"));
73
+ console.log(chalk.dim(" 2. Run: pan sync"));
74
+ } catch (error) {
75
+ spinner.fail("Failed to initialize");
76
+ console.error(chalk.red(error.message));
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ // src/cli/commands/sync.ts
82
+ import chalk2 from "chalk";
83
+ import ora2 from "ora";
84
+ async function syncCommand(options) {
85
+ const config = loadConfig();
86
+ const targets = config.sync.targets;
87
+ if (targets.length === 0) {
88
+ console.log(chalk2.yellow("No sync targets configured."));
89
+ console.log(chalk2.dim("Edit ~/.panopticon/config.toml to add targets."));
90
+ return;
91
+ }
92
+ if (options.dryRun) {
93
+ console.log(chalk2.bold("Sync Plan (dry run):\n"));
94
+ for (const runtime of targets) {
95
+ const plan = planSync(runtime);
96
+ console.log(chalk2.cyan(`${runtime}:`));
97
+ if (plan.skills.length === 0 && plan.commands.length === 0) {
98
+ console.log(chalk2.dim(" (nothing to sync)"));
99
+ continue;
100
+ }
101
+ for (const item of plan.skills) {
102
+ const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
103
+ const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
104
+ console.log(` ${icon} skill/${item.name} ${status}`);
105
+ }
106
+ for (const item of plan.commands) {
107
+ const icon = item.status === "conflict" ? chalk2.yellow("!") : chalk2.green("+");
108
+ const status = item.status === "conflict" ? chalk2.yellow("[conflict]") : "";
109
+ console.log(` ${icon} command/${item.name} ${status}`);
110
+ }
111
+ console.log("");
112
+ }
113
+ console.log(chalk2.dim("Run without --dry-run to apply changes."));
114
+ return;
115
+ }
116
+ if (config.sync.backup_before_sync) {
117
+ const spinner2 = ora2("Creating backup...").start();
118
+ const backupDirs = targets.flatMap((r) => [
119
+ SYNC_TARGETS[r].skills,
120
+ SYNC_TARGETS[r].commands
121
+ ]);
122
+ const backup = createBackup(backupDirs);
123
+ if (backup.targets.length > 0) {
124
+ spinner2.succeed(`Backup created: ${backup.timestamp}`);
125
+ } else {
126
+ spinner2.info("No existing content to backup");
127
+ }
128
+ if (options.backupOnly) {
129
+ return;
130
+ }
131
+ }
132
+ const spinner = ora2("Syncing...").start();
133
+ let totalCreated = 0;
134
+ let totalConflicts = 0;
135
+ for (const runtime of targets) {
136
+ spinner.text = `Syncing to ${runtime}...`;
137
+ const result = executeSync(runtime, { force: options.force });
138
+ totalCreated += result.created.length;
139
+ totalConflicts += result.conflicts.length;
140
+ if (result.conflicts.length > 0 && !options.force) {
141
+ console.log("");
142
+ console.log(chalk2.yellow(`Conflicts in ${runtime}:`));
143
+ for (const name of result.conflicts) {
144
+ console.log(chalk2.dim(` - ${name} (use --force to overwrite)`));
145
+ }
146
+ }
147
+ }
148
+ if (totalConflicts > 0 && !options.force) {
149
+ spinner.warn(`Synced ${totalCreated} items, ${totalConflicts} conflicts`);
150
+ console.log("");
151
+ console.log(chalk2.dim("Use --force to overwrite conflicting items."));
152
+ } else {
153
+ spinner.succeed(`Synced ${totalCreated} items to ${targets.join(", ")}`);
154
+ }
155
+ }
156
+
157
+ // src/cli/commands/restore.ts
158
+ import chalk3 from "chalk";
159
+ import ora3 from "ora";
160
+ import inquirer from "inquirer";
161
+ async function restoreCommand(timestamp) {
162
+ const backups = listBackups();
163
+ if (backups.length === 0) {
164
+ console.log(chalk3.yellow("No backups found."));
165
+ return;
166
+ }
167
+ if (!timestamp) {
168
+ console.log(chalk3.bold("Available backups:\n"));
169
+ for (const backup of backups.slice(0, 10)) {
170
+ console.log(` ${chalk3.cyan(backup.timestamp)} - ${backup.targets.join(", ")}`);
171
+ }
172
+ if (backups.length > 10) {
173
+ console.log(chalk3.dim(` ... and ${backups.length - 10} more`));
174
+ }
175
+ console.log("");
176
+ const { selected } = await inquirer.prompt([
177
+ {
178
+ type: "list",
179
+ name: "selected",
180
+ message: "Select backup to restore:",
181
+ choices: backups.slice(0, 10).map((b) => ({
182
+ name: `${b.timestamp} (${b.targets.join(", ")})`,
183
+ value: b.timestamp
184
+ }))
185
+ }
186
+ ]);
187
+ timestamp = selected;
188
+ }
189
+ const { confirm } = await inquirer.prompt([
190
+ {
191
+ type: "confirm",
192
+ name: "confirm",
193
+ message: `Restore backup ${timestamp}? This will overwrite current files.`,
194
+ default: false
195
+ }
196
+ ]);
197
+ if (!confirm) {
198
+ console.log(chalk3.dim("Restore cancelled."));
199
+ return;
200
+ }
201
+ const spinner = ora3("Restoring backup...").start();
202
+ try {
203
+ const config = loadConfig();
204
+ const targets = config.sync.targets;
205
+ const targetDirs = {};
206
+ for (const runtime of targets) {
207
+ targetDirs[`${runtime}-skills`] = SYNC_TARGETS[runtime].skills;
208
+ targetDirs[`${runtime}-commands`] = SYNC_TARGETS[runtime].commands;
209
+ targetDirs["skills"] = SYNC_TARGETS[runtime].skills;
210
+ targetDirs["commands"] = SYNC_TARGETS[runtime].commands;
211
+ }
212
+ restoreBackup(timestamp, targetDirs);
213
+ spinner.succeed(`Restored backup: ${timestamp}`);
214
+ } catch (error) {
215
+ spinner.fail("Failed to restore");
216
+ console.error(chalk3.red(error.message));
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ // src/cli/commands/skills.ts
222
+ import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
223
+ import { join } from "path";
224
+ import chalk4 from "chalk";
225
+ function parseSkillFrontmatter(content) {
226
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
227
+ if (!match) return {};
228
+ const frontmatter = match[1];
229
+ const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim();
230
+ const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
231
+ return { name, description };
232
+ }
233
+ function listSkills() {
234
+ if (!existsSync2(SKILLS_DIR)) return [];
235
+ const skills = [];
236
+ const dirs = readdirSync(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
237
+ for (const dir of dirs) {
238
+ const skillFile = join(SKILLS_DIR, dir.name, "SKILL.md");
239
+ if (!existsSync2(skillFile)) continue;
240
+ const content = readFileSync(skillFile, "utf8");
241
+ const { name, description } = parseSkillFrontmatter(content);
242
+ skills.push({
243
+ name: name || dir.name,
244
+ description: description || "(no description)",
245
+ path: skillFile
246
+ });
247
+ }
248
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
249
+ }
250
+ async function skillsCommand(options) {
251
+ const skills = listSkills();
252
+ if (options.json) {
253
+ console.log(JSON.stringify(skills, null, 2));
254
+ return;
255
+ }
256
+ console.log(chalk4.bold(`
257
+ Panopticon Skills (${skills.length})
258
+ `));
259
+ if (skills.length === 0) {
260
+ console.log(chalk4.yellow("No skills found."));
261
+ console.log(chalk4.dim("Skills should be in ~/.panopticon/skills/<name>/SKILL.md"));
262
+ return;
263
+ }
264
+ for (const skill of skills) {
265
+ console.log(chalk4.cyan(skill.name));
266
+ console.log(chalk4.dim(` ${skill.description}`));
267
+ }
268
+ console.log(`
269
+ ${chalk4.dim('Run "pan sync" to sync skills to Claude Code')}`);
270
+ }
271
+
272
+ // src/cli/commands/work/issue.ts
273
+ import chalk5 from "chalk";
274
+ import ora4 from "ora";
275
+
276
+ // src/lib/agents.ts
277
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "fs";
278
+ import { join as join4 } from "path";
279
+
280
+ // src/lib/tmux.ts
281
+ import { execSync } from "child_process";
282
+ function listSessions() {
283
+ try {
284
+ const output = execSync('tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}|#{session_windows}"', {
285
+ encoding: "utf8"
286
+ });
287
+ return output.trim().split("\n").filter(Boolean).map((line) => {
288
+ const [name, created, attached, windows] = line.split("|");
289
+ return {
290
+ name,
291
+ created: new Date(parseInt(created) * 1e3),
292
+ attached: attached === "1",
293
+ windows: parseInt(windows)
294
+ };
295
+ });
296
+ } catch {
297
+ return [];
298
+ }
299
+ }
300
+ function sessionExists(name) {
301
+ try {
302
+ execSync(`tmux has-session -t ${name} 2>/dev/null`);
303
+ return true;
304
+ } catch {
305
+ return false;
306
+ }
307
+ }
308
+ function createSession(name, cwd, initialCommand) {
309
+ const escapedCwd = cwd.replace(/"/g, '\\"');
310
+ const cmd = initialCommand ? `tmux new-session -d -s ${name} -c "${escapedCwd}" "${initialCommand.replace(/"/g, '\\"')}"` : `tmux new-session -d -s ${name} -c "${escapedCwd}"`;
311
+ execSync(cmd);
312
+ }
313
+ function killSession(name) {
314
+ execSync(`tmux kill-session -t ${name}`);
315
+ }
316
+ function sendKeys(sessionName, keys) {
317
+ const escapedKeys = keys.replace(/"/g, '\\"');
318
+ execSync(`tmux send-keys -t ${sessionName} "${escapedKeys}"`);
319
+ execSync(`tmux send-keys -t ${sessionName} Enter`);
320
+ }
321
+ function getAgentSessions() {
322
+ return listSessions().filter((s) => s.name.startsWith("agent-"));
323
+ }
324
+
325
+ // src/lib/hooks.ts
326
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, readdirSync as readdirSync2, unlinkSync } from "fs";
327
+ import { join as join2 } from "path";
328
+ function getHookDir(agentId) {
329
+ return join2(AGENTS_DIR, agentId);
330
+ }
331
+ function getHookFile(agentId) {
332
+ return join2(getHookDir(agentId), "hook.json");
333
+ }
334
+ function getMailDir(agentId) {
335
+ return join2(getHookDir(agentId), "mail");
336
+ }
337
+ function initHook(agentId) {
338
+ const hookDir = getHookDir(agentId);
339
+ const mailDir = getMailDir(agentId);
340
+ mkdirSync2(hookDir, { recursive: true });
341
+ mkdirSync2(mailDir, { recursive: true });
342
+ const hookFile = getHookFile(agentId);
343
+ if (!existsSync3(hookFile)) {
344
+ const hook = {
345
+ agentId,
346
+ items: []
347
+ };
348
+ writeFileSync(hookFile, JSON.stringify(hook, null, 2));
349
+ }
350
+ }
351
+ function getHook(agentId) {
352
+ const hookFile = getHookFile(agentId);
353
+ if (!existsSync3(hookFile)) {
354
+ return null;
355
+ }
356
+ try {
357
+ const content = readFileSync2(hookFile, "utf-8");
358
+ return JSON.parse(content);
359
+ } catch {
360
+ return null;
361
+ }
362
+ }
363
+ function pushToHook(agentId, item) {
364
+ initHook(agentId);
365
+ const hook = getHook(agentId) || { agentId, items: [] };
366
+ const newItem = {
367
+ ...item,
368
+ id: `hook-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
369
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
370
+ };
371
+ hook.items.push(newItem);
372
+ writeFileSync(getHookFile(agentId), JSON.stringify(hook, null, 2));
373
+ return newItem;
374
+ }
375
+ function checkHook(agentId) {
376
+ const hook = getHook(agentId);
377
+ if (!hook || hook.items.length === 0) {
378
+ const mailDir = getMailDir(agentId);
379
+ if (existsSync3(mailDir)) {
380
+ const mails = readdirSync2(mailDir).filter((f) => f.endsWith(".json"));
381
+ if (mails.length > 0) {
382
+ const mailItems = mails.map((file) => {
383
+ try {
384
+ const content = readFileSync2(join2(mailDir, file), "utf-8");
385
+ return JSON.parse(content);
386
+ } catch {
387
+ return null;
388
+ }
389
+ }).filter(Boolean);
390
+ return {
391
+ hasWork: mailItems.length > 0,
392
+ urgentCount: mailItems.filter((i) => i.priority === "urgent").length,
393
+ items: mailItems
394
+ };
395
+ }
396
+ }
397
+ return { hasWork: false, urgentCount: 0, items: [] };
398
+ }
399
+ const now = /* @__PURE__ */ new Date();
400
+ const activeItems = hook.items.filter((item) => {
401
+ if (item.expiresAt) {
402
+ return new Date(item.expiresAt) > now;
403
+ }
404
+ return true;
405
+ });
406
+ const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
407
+ activeItems.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
408
+ return {
409
+ hasWork: activeItems.length > 0,
410
+ urgentCount: activeItems.filter((i) => i.priority === "urgent").length,
411
+ items: activeItems
412
+ };
413
+ }
414
+ function popFromHook(agentId, itemId) {
415
+ const hook = getHook(agentId);
416
+ if (!hook) return false;
417
+ const index = hook.items.findIndex((i) => i.id === itemId);
418
+ if (index === -1) return false;
419
+ hook.items.splice(index, 1);
420
+ hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
421
+ writeFileSync(getHookFile(agentId), JSON.stringify(hook, null, 2));
422
+ return true;
423
+ }
424
+ function clearHook(agentId) {
425
+ const hook = getHook(agentId);
426
+ if (!hook) return;
427
+ hook.items = [];
428
+ hook.lastChecked = (/* @__PURE__ */ new Date()).toISOString();
429
+ writeFileSync(getHookFile(agentId), JSON.stringify(hook, null, 2));
430
+ }
431
+ function sendMail(toAgentId, from, message, priority = "normal") {
432
+ initHook(toAgentId);
433
+ const mailDir = getMailDir(toAgentId);
434
+ const mailItem = {
435
+ id: `mail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
436
+ type: "message",
437
+ priority,
438
+ source: from,
439
+ payload: { message },
440
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
441
+ };
442
+ writeFileSync(
443
+ join2(mailDir, `${mailItem.id}.json`),
444
+ JSON.stringify(mailItem, null, 2)
445
+ );
446
+ }
447
+ function generateGUPPPrompt(agentId) {
448
+ const { hasWork, urgentCount, items } = checkHook(agentId);
449
+ if (!hasWork) return null;
450
+ const lines = [
451
+ "# GUPP: Work Found on Your Hook",
452
+ "",
453
+ '> "If there is work on your Hook, YOU MUST RUN IT."',
454
+ ""
455
+ ];
456
+ if (urgentCount > 0) {
457
+ lines.push(`\u26A0\uFE0F **${urgentCount} URGENT item(s) require immediate attention**`);
458
+ lines.push("");
459
+ }
460
+ lines.push(`## Pending Work Items (${items.length})`);
461
+ lines.push("");
462
+ for (const item of items) {
463
+ const priorityEmoji = {
464
+ urgent: "\u{1F534}",
465
+ high: "\u{1F7E0}",
466
+ normal: "\u{1F7E2}",
467
+ low: "\u26AA"
468
+ }[item.priority];
469
+ lines.push(`### ${priorityEmoji} ${item.type.toUpperCase()}: ${item.id}`);
470
+ lines.push(`- Source: ${item.source}`);
471
+ lines.push(`- Created: ${item.createdAt}`);
472
+ if (item.payload.issueId) {
473
+ lines.push(`- Issue: ${item.payload.issueId}`);
474
+ }
475
+ if (item.payload.message) {
476
+ lines.push(`- Message: ${item.payload.message}`);
477
+ }
478
+ if (item.payload.action) {
479
+ lines.push(`- Action: ${item.payload.action}`);
480
+ }
481
+ lines.push("");
482
+ }
483
+ lines.push("---");
484
+ lines.push("");
485
+ lines.push("Execute these items in priority order. Use `bd hook pop <id>` after completing each item.");
486
+ return lines.join("\n");
487
+ }
488
+
489
+ // src/lib/cv.ts
490
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3 } from "fs";
491
+ import { join as join3 } from "path";
492
+ function getCVFile(agentId) {
493
+ return join3(AGENTS_DIR, agentId, "cv.json");
494
+ }
495
+ function getAgentCV(agentId) {
496
+ const cvFile = getCVFile(agentId);
497
+ if (existsSync4(cvFile)) {
498
+ try {
499
+ return JSON.parse(readFileSync3(cvFile, "utf-8"));
500
+ } catch {
501
+ }
502
+ }
503
+ const cv = {
504
+ agentId,
505
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
506
+ lastActive: (/* @__PURE__ */ new Date()).toISOString(),
507
+ runtime: "claude",
508
+ model: "sonnet",
509
+ stats: {
510
+ totalIssues: 0,
511
+ successCount: 0,
512
+ failureCount: 0,
513
+ abandonedCount: 0,
514
+ avgDuration: 0,
515
+ successRate: 0
516
+ },
517
+ skillsUsed: [],
518
+ recentWork: []
519
+ };
520
+ saveAgentCV(cv);
521
+ return cv;
522
+ }
523
+ function saveAgentCV(cv) {
524
+ const dir = join3(AGENTS_DIR, cv.agentId);
525
+ mkdirSync3(dir, { recursive: true });
526
+ writeFileSync2(getCVFile(cv.agentId), JSON.stringify(cv, null, 2));
527
+ }
528
+ function startWork(agentId, issueId, skills) {
529
+ const cv = getAgentCV(agentId);
530
+ const entry = {
531
+ issueId,
532
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
533
+ outcome: "in_progress",
534
+ skills
535
+ };
536
+ cv.recentWork.unshift(entry);
537
+ cv.stats.totalIssues++;
538
+ cv.lastActive = (/* @__PURE__ */ new Date()).toISOString();
539
+ if (skills) {
540
+ for (const skill of skills) {
541
+ if (!cv.skillsUsed.includes(skill)) {
542
+ cv.skillsUsed.push(skill);
543
+ }
544
+ }
545
+ }
546
+ if (cv.recentWork.length > 50) {
547
+ cv.recentWork = cv.recentWork.slice(0, 50);
548
+ }
549
+ saveAgentCV(cv);
550
+ }
551
+ function getAgentRankings() {
552
+ const rankings = [];
553
+ if (!existsSync4(AGENTS_DIR)) return rankings;
554
+ const dirs = readdirSync3(AGENTS_DIR, { withFileTypes: true }).filter(
555
+ (d) => d.isDirectory()
556
+ );
557
+ for (const dir of dirs) {
558
+ const cv = getAgentCV(dir.name);
559
+ if (cv.stats.totalIssues > 0) {
560
+ rankings.push({
561
+ agentId: dir.name,
562
+ successRate: cv.stats.successRate,
563
+ totalIssues: cv.stats.totalIssues,
564
+ avgDuration: cv.stats.avgDuration
565
+ });
566
+ }
567
+ }
568
+ rankings.sort((a, b) => {
569
+ if (b.successRate !== a.successRate) {
570
+ return b.successRate - a.successRate;
571
+ }
572
+ return b.totalIssues - a.totalIssues;
573
+ });
574
+ return rankings;
575
+ }
576
+ function formatCV(cv) {
577
+ const lines = [
578
+ `# Agent CV: ${cv.agentId}`,
579
+ "",
580
+ `Runtime: ${cv.runtime} (${cv.model})`,
581
+ `Created: ${cv.createdAt}`,
582
+ `Last Active: ${cv.lastActive}`,
583
+ "",
584
+ "## Statistics",
585
+ "",
586
+ `- Total Issues: ${cv.stats.totalIssues}`,
587
+ `- Success Rate: ${(cv.stats.successRate * 100).toFixed(1)}%`,
588
+ `- Successes: ${cv.stats.successCount}`,
589
+ `- Failures: ${cv.stats.failureCount}`,
590
+ `- Abandoned: ${cv.stats.abandonedCount}`,
591
+ `- Avg Duration: ${cv.stats.avgDuration} minutes`,
592
+ ""
593
+ ];
594
+ if (cv.skillsUsed.length > 0) {
595
+ lines.push("## Skills Used");
596
+ lines.push("");
597
+ lines.push(cv.skillsUsed.join(", "));
598
+ lines.push("");
599
+ }
600
+ if (cv.recentWork.length > 0) {
601
+ lines.push("## Recent Work");
602
+ lines.push("");
603
+ for (const work of cv.recentWork.slice(0, 10)) {
604
+ const statusIcon = {
605
+ success: "\u2713",
606
+ failed: "\u2717",
607
+ abandoned: "\u2298",
608
+ in_progress: "\u25CF"
609
+ }[work.outcome];
610
+ const duration = work.duration ? ` (${work.duration}m)` : "";
611
+ lines.push(`${statusIcon} ${work.issueId}${duration}`);
612
+ if (work.failureReason) {
613
+ lines.push(` Reason: ${work.failureReason}`);
614
+ }
615
+ }
616
+ lines.push("");
617
+ }
618
+ return lines.join("\n");
619
+ }
620
+
621
+ // src/lib/agents.ts
622
+ function getAgentDir(agentId) {
623
+ return join4(AGENTS_DIR, agentId);
624
+ }
625
+ function getAgentState(agentId) {
626
+ const stateFile = join4(getAgentDir(agentId), "state.json");
627
+ if (!existsSync5(stateFile)) return null;
628
+ const content = readFileSync4(stateFile, "utf8");
629
+ return JSON.parse(content);
630
+ }
631
+ function saveAgentState(state) {
632
+ const dir = getAgentDir(state.id);
633
+ mkdirSync4(dir, { recursive: true });
634
+ writeFileSync3(
635
+ join4(dir, "state.json"),
636
+ JSON.stringify(state, null, 2)
637
+ );
638
+ }
639
+ function spawnAgent(options) {
640
+ const agentId = `agent-${options.issueId.toLowerCase()}`;
641
+ if (sessionExists(agentId)) {
642
+ throw new Error(`Agent ${agentId} already running. Use 'pan work tell' to message it.`);
643
+ }
644
+ initHook(agentId);
645
+ const state = {
646
+ id: agentId,
647
+ issueId: options.issueId,
648
+ workspace: options.workspace,
649
+ runtime: options.runtime || "claude",
650
+ model: options.model || "sonnet",
651
+ status: "starting",
652
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
653
+ };
654
+ saveAgentState(state);
655
+ let prompt = options.prompt || "";
656
+ const { hasWork, items } = checkHook(agentId);
657
+ if (hasWork) {
658
+ const guppPrompt = generateGUPPPrompt(agentId);
659
+ if (guppPrompt) {
660
+ prompt = guppPrompt + "\n\n---\n\n" + prompt;
661
+ }
662
+ }
663
+ const claudeCmd = prompt ? `claude --model ${state.model} "${prompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"` : `claude --model ${state.model}`;
664
+ createSession(agentId, options.workspace, claudeCmd);
665
+ state.status = "running";
666
+ saveAgentState(state);
667
+ startWork(agentId, options.issueId);
668
+ return state;
669
+ }
670
+ function listRunningAgents() {
671
+ const tmuxSessions = getAgentSessions();
672
+ const tmuxNames = new Set(tmuxSessions.map((s) => s.name));
673
+ const agents = [];
674
+ if (!existsSync5(AGENTS_DIR)) return agents;
675
+ const dirs = readdirSync4(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
676
+ for (const dir of dirs) {
677
+ const state = getAgentState(dir.name);
678
+ if (state) {
679
+ agents.push({
680
+ ...state,
681
+ tmuxActive: tmuxNames.has(state.id)
682
+ });
683
+ }
684
+ }
685
+ return agents;
686
+ }
687
+ function stopAgent(agentId) {
688
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
689
+ if (sessionExists(normalizedId)) {
690
+ killSession(normalizedId);
691
+ }
692
+ const state = getAgentState(normalizedId);
693
+ if (state) {
694
+ state.status = "stopped";
695
+ saveAgentState(state);
696
+ }
697
+ }
698
+ function messageAgent(agentId, message) {
699
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
700
+ if (!sessionExists(normalizedId)) {
701
+ throw new Error(`Agent ${normalizedId} not running`);
702
+ }
703
+ sendKeys(normalizedId, message);
704
+ const mailDir = join4(getAgentDir(normalizedId), "mail");
705
+ mkdirSync4(mailDir, { recursive: true });
706
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
707
+ writeFileSync3(
708
+ join4(mailDir, `${timestamp}.md`),
709
+ `# Message
710
+
711
+ ${message}
712
+ `
713
+ );
714
+ }
715
+ function detectCrashedAgents() {
716
+ const agents = listRunningAgents();
717
+ return agents.filter(
718
+ (agent) => agent.status === "running" && !agent.tmuxActive
719
+ );
720
+ }
721
+ function recoverAgent(agentId) {
722
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
723
+ const state = getAgentState(normalizedId);
724
+ if (!state) {
725
+ return null;
726
+ }
727
+ if (sessionExists(normalizedId)) {
728
+ return state;
729
+ }
730
+ const healthFile = join4(getAgentDir(normalizedId), "health.json");
731
+ let health = { consecutiveFailures: 0, killCount: 0, recoveryCount: 0 };
732
+ if (existsSync5(healthFile)) {
733
+ try {
734
+ health = { ...health, ...JSON.parse(readFileSync4(healthFile, "utf-8")) };
735
+ } catch {
736
+ }
737
+ }
738
+ health.recoveryCount = (health.recoveryCount || 0) + 1;
739
+ writeFileSync3(healthFile, JSON.stringify(health, null, 2));
740
+ const recoveryPrompt = generateRecoveryPrompt(state);
741
+ const claudeCmd = `claude --model ${state.model} "${recoveryPrompt.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
742
+ createSession(normalizedId, state.workspace, claudeCmd);
743
+ state.status = "running";
744
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
745
+ saveAgentState(state);
746
+ return state;
747
+ }
748
+ function generateRecoveryPrompt(state) {
749
+ const lines = [
750
+ "# Agent Recovery",
751
+ "",
752
+ "\u26A0\uFE0F This agent session was recovered after a crash.",
753
+ "",
754
+ "## Previous Context",
755
+ `- Issue: ${state.issueId}`,
756
+ `- Workspace: ${state.workspace}`,
757
+ `- Started: ${state.startedAt}`,
758
+ "",
759
+ "## Recovery Steps",
760
+ "1. Check beads for context: `bd show " + state.issueId + "`",
761
+ "2. Review recent git commits: `git log --oneline -10`",
762
+ "3. Check hook for pending work: `pan work hook check`",
763
+ "4. Resume from last known state",
764
+ "",
765
+ "## GUPP Reminder",
766
+ '> "If there is work on your Hook, YOU MUST RUN IT."',
767
+ ""
768
+ ];
769
+ const { hasWork } = checkHook(state.id);
770
+ if (hasWork) {
771
+ const guppPrompt = generateGUPPPrompt(state.id);
772
+ if (guppPrompt) {
773
+ lines.push("---");
774
+ lines.push("");
775
+ lines.push(guppPrompt);
776
+ }
777
+ }
778
+ return lines.join("\n");
779
+ }
780
+ function autoRecoverAgents() {
781
+ const crashed = detectCrashedAgents();
782
+ const recovered = [];
783
+ const failed = [];
784
+ for (const agent of crashed) {
785
+ try {
786
+ const result = recoverAgent(agent.id);
787
+ if (result) {
788
+ recovered.push(agent.id);
789
+ } else {
790
+ failed.push(agent.id);
791
+ }
792
+ } catch (error) {
793
+ failed.push(agent.id);
794
+ }
795
+ }
796
+ return { recovered, failed };
797
+ }
798
+
799
+ // src/cli/commands/work/issue.ts
800
+ async function issueCommand(id, options) {
801
+ const spinner = ora4(`Preparing workspace for ${id}...`).start();
802
+ try {
803
+ const normalizedId = id.toLowerCase();
804
+ const workspace = process.cwd();
805
+ if (options.dryRun) {
806
+ spinner.info("Dry run mode");
807
+ console.log("");
808
+ console.log(chalk5.bold("Would create:"));
809
+ console.log(` Agent ID: agent-${normalizedId}`);
810
+ console.log(` Workspace: ${workspace}`);
811
+ console.log(` Runtime: ${options.runtime}`);
812
+ console.log(` Model: ${options.model}`);
813
+ return;
814
+ }
815
+ spinner.text = "Spawning agent...";
816
+ const agent = spawnAgent({
817
+ issueId: id,
818
+ workspace,
819
+ runtime: options.runtime,
820
+ model: options.model,
821
+ prompt: `You are working on issue ${id}. Check beads for context: bd show ${id}`
822
+ });
823
+ spinner.succeed(`Agent spawned: ${agent.id}`);
824
+ console.log("");
825
+ console.log(chalk5.bold("Agent Details:"));
826
+ console.log(` Session: ${chalk5.cyan(agent.id)}`);
827
+ console.log(` Workspace: ${workspace}`);
828
+ console.log(` Runtime: ${agent.runtime} (${agent.model})`);
829
+ console.log("");
830
+ console.log(chalk5.dim("Commands:"));
831
+ console.log(` Attach: tmux attach -t ${agent.id}`);
832
+ console.log(` Message: pan work tell ${id} "your message"`);
833
+ console.log(` Kill: pan work kill ${id}`);
834
+ } catch (error) {
835
+ spinner.fail(error.message);
836
+ process.exit(1);
837
+ }
838
+ }
839
+
840
+ // src/cli/commands/work/status.ts
841
+ import chalk6 from "chalk";
842
+ async function statusCommand(options) {
843
+ const agents = listRunningAgents();
844
+ if (options.json) {
845
+ console.log(JSON.stringify(agents, null, 2));
846
+ return;
847
+ }
848
+ if (agents.length === 0) {
849
+ console.log(chalk6.dim("No running agents."));
850
+ console.log(chalk6.dim('Use "pan work issue <id>" to spawn one.'));
851
+ return;
852
+ }
853
+ console.log(chalk6.bold("\nRunning Agents\n"));
854
+ for (const agent of agents) {
855
+ const statusColor = agent.tmuxActive ? chalk6.green : chalk6.red;
856
+ const status = agent.tmuxActive ? "running" : "stopped";
857
+ const startedAt = new Date(agent.startedAt);
858
+ const duration = Math.floor((Date.now() - startedAt.getTime()) / 1e3 / 60);
859
+ console.log(`${chalk6.cyan(agent.id)}`);
860
+ console.log(` Issue: ${agent.issueId}`);
861
+ console.log(` Status: ${statusColor(status)}`);
862
+ console.log(` Runtime: ${agent.runtime} (${agent.model})`);
863
+ console.log(` Duration: ${duration} min`);
864
+ console.log(` Workspace: ${chalk6.dim(agent.workspace)}`);
865
+ console.log("");
866
+ }
867
+ }
868
+
869
+ // src/cli/commands/work/tell.ts
870
+ import chalk7 from "chalk";
871
+ async function tellCommand(id, message) {
872
+ const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
873
+ try {
874
+ messageAgent(agentId, message);
875
+ console.log(chalk7.green("Message sent to " + agentId));
876
+ console.log(chalk7.dim(` "${message}"`));
877
+ } catch (error) {
878
+ console.error(chalk7.red("Error: " + error.message));
879
+ process.exit(1);
880
+ }
881
+ }
882
+
883
+ // src/cli/commands/work/kill.ts
884
+ import chalk8 from "chalk";
885
+ async function killCommand(id, options) {
886
+ const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
887
+ const state = getAgentState(agentId);
888
+ const isRunning = sessionExists(agentId);
889
+ if (!state && !isRunning) {
890
+ console.log(chalk8.yellow(`Agent ${agentId} not found.`));
891
+ return;
892
+ }
893
+ if (!options.force && isRunning) {
894
+ }
895
+ try {
896
+ stopAgent(agentId);
897
+ console.log(chalk8.green(`Killed agent: ${agentId}`));
898
+ } catch (error) {
899
+ console.error(chalk8.red("Error: " + error.message));
900
+ process.exit(1);
901
+ }
902
+ }
903
+
904
+ // src/cli/commands/work/pending.ts
905
+ import chalk9 from "chalk";
906
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
907
+ import { join as join5 } from "path";
908
+ async function pendingCommand() {
909
+ const agents = listRunningAgents().filter((a) => !a.tmuxActive && a.status !== "error");
910
+ if (agents.length === 0) {
911
+ console.log(chalk9.dim("No pending reviews."));
912
+ console.log(chalk9.dim("Agents will appear here when they complete work."));
913
+ return;
914
+ }
915
+ console.log(chalk9.bold("\nPending Reviews\n"));
916
+ for (const agent of agents) {
917
+ console.log(`${chalk9.cyan(agent.issueId)}`);
918
+ console.log(` Agent: ${agent.id}`);
919
+ console.log(` Workspace: ${chalk9.dim(agent.workspace)}`);
920
+ const completionFile = join5(AGENTS_DIR, agent.id, "completion.md");
921
+ if (existsSync6(completionFile)) {
922
+ const content = readFileSync5(completionFile, "utf8");
923
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
924
+ if (firstLine) {
925
+ console.log(` Summary: ${chalk9.dim(firstLine.trim())}`);
926
+ }
927
+ }
928
+ console.log("");
929
+ }
930
+ console.log(chalk9.dim('Run "pan work approve <id>" to approve and merge.'));
931
+ }
932
+
933
+ // src/cli/commands/work/approve.ts
934
+ import chalk10 from "chalk";
935
+ import ora5 from "ora";
936
+ import { existsSync as existsSync7, writeFileSync as writeFileSync4, readFileSync as readFileSync6 } from "fs";
937
+ import { join as join6 } from "path";
938
+ import { homedir } from "os";
939
+ import { execSync as execSync2 } from "child_process";
940
+ function getLinearApiKey() {
941
+ const envFile = join6(homedir(), ".panopticon.env");
942
+ if (existsSync7(envFile)) {
943
+ const content = readFileSync6(envFile, "utf-8");
944
+ const match = content.match(/LINEAR_API_KEY=(.+)/);
945
+ if (match) return match[1].trim();
946
+ }
947
+ return process.env.LINEAR_API_KEY || null;
948
+ }
949
+ function checkGhCli() {
950
+ try {
951
+ execSync2("which gh", { stdio: "pipe" });
952
+ return true;
953
+ } catch {
954
+ return false;
955
+ }
956
+ }
957
+ function findPRForBranch(workspace) {
958
+ try {
959
+ const branch = execSync2("git rev-parse --abbrev-ref HEAD", {
960
+ cwd: workspace,
961
+ encoding: "utf-8",
962
+ stdio: ["pipe", "pipe", "pipe"]
963
+ }).trim();
964
+ const prJson = execSync2(`gh pr list --head "${branch}" --json number,url --limit 1`, {
965
+ cwd: workspace,
966
+ encoding: "utf-8",
967
+ stdio: ["pipe", "pipe", "pipe"]
968
+ });
969
+ const prs = JSON.parse(prJson);
970
+ if (prs.length > 0) {
971
+ return { number: prs[0].number, url: prs[0].url };
972
+ }
973
+ return null;
974
+ } catch {
975
+ return null;
976
+ }
977
+ }
978
+ function mergePR(workspace, prNumber) {
979
+ try {
980
+ execSync2(`gh pr merge ${prNumber} --squash --delete-branch`, {
981
+ cwd: workspace,
982
+ encoding: "utf-8",
983
+ stdio: ["pipe", "pipe", "pipe"]
984
+ });
985
+ return { success: true };
986
+ } catch (error) {
987
+ return { success: false, error: error.message };
988
+ }
989
+ }
990
+ async function updateLinearStatus(apiKey, issueIdentifier) {
991
+ try {
992
+ const { LinearClient } = await import("@linear/sdk");
993
+ const client = new LinearClient({ apiKey });
994
+ const me = await client.viewer;
995
+ const teams = await me.teams();
996
+ const team = teams.nodes[0];
997
+ if (!team) return false;
998
+ const issues = await team.issues({ first: 100 });
999
+ const issue = issues.nodes.find(
1000
+ (i) => i.identifier.toUpperCase() === issueIdentifier.toUpperCase()
1001
+ );
1002
+ if (!issue) return false;
1003
+ const states = await team.states();
1004
+ const doneState = states.nodes.find((s) => s.type === "completed" && s.name === "Done");
1005
+ if (!doneState) return false;
1006
+ await issue.update({ stateId: doneState.id });
1007
+ return true;
1008
+ } catch {
1009
+ return false;
1010
+ }
1011
+ }
1012
+ async function approveCommand(id, options = {}) {
1013
+ const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
1014
+ const state = getAgentState(agentId);
1015
+ if (!state) {
1016
+ console.log(chalk10.yellow(`Agent ${agentId} not found.`));
1017
+ console.log(chalk10.dim('Run "pan work status" to see running agents.'));
1018
+ return;
1019
+ }
1020
+ const spinner = ora5("Approving work...").start();
1021
+ try {
1022
+ const workspace = state.workspace;
1023
+ let prMerged = false;
1024
+ let linearUpdated = false;
1025
+ if (options.merge !== false) {
1026
+ if (!checkGhCli()) {
1027
+ spinner.warn("gh CLI not found - skipping PR merge");
1028
+ console.log(chalk10.dim(" Install: https://cli.github.com/"));
1029
+ } else {
1030
+ spinner.text = "Looking for PR...";
1031
+ const pr = findPRForBranch(workspace);
1032
+ if (pr) {
1033
+ spinner.text = `Merging PR #${pr.number}...`;
1034
+ const result = mergePR(workspace, pr.number);
1035
+ if (result.success) {
1036
+ prMerged = true;
1037
+ console.log(chalk10.green(` \u2713 Merged PR #${pr.number}`));
1038
+ } else {
1039
+ console.log(chalk10.yellow(` \u26A0 Failed to merge: ${result.error}`));
1040
+ console.log(chalk10.dim(` Merge manually: gh pr merge ${pr.number} --squash`));
1041
+ }
1042
+ } else {
1043
+ console.log(chalk10.dim(" No PR found for this branch"));
1044
+ }
1045
+ }
1046
+ }
1047
+ if (options.noLinear !== true) {
1048
+ const apiKey = getLinearApiKey();
1049
+ if (apiKey) {
1050
+ spinner.text = "Updating Linear status...";
1051
+ linearUpdated = await updateLinearStatus(apiKey, state.issueId);
1052
+ if (linearUpdated) {
1053
+ console.log(chalk10.green(` \u2713 Updated ${state.issueId} to Done`));
1054
+ } else {
1055
+ console.log(chalk10.yellow(` \u26A0 Failed to update Linear status`));
1056
+ }
1057
+ } else {
1058
+ console.log(chalk10.dim(" LINEAR_API_KEY not set - skipping status update"));
1059
+ }
1060
+ }
1061
+ state.status = "stopped";
1062
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1063
+ saveAgentState(state);
1064
+ const approvedFile = join6(AGENTS_DIR, agentId, "approved");
1065
+ writeFileSync4(approvedFile, JSON.stringify({
1066
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1067
+ prMerged,
1068
+ linearUpdated
1069
+ }));
1070
+ spinner.succeed(`Approved: ${state.issueId}`);
1071
+ console.log("");
1072
+ console.log(chalk10.bold("Summary:"));
1073
+ console.log(` Issue: ${chalk10.cyan(state.issueId)}`);
1074
+ console.log(` PR: ${prMerged ? chalk10.green("Merged") : chalk10.dim("Not merged")}`);
1075
+ console.log(` Linear: ${linearUpdated ? chalk10.green("Updated to Done") : chalk10.dim("Not updated")}`);
1076
+ console.log("");
1077
+ console.log(chalk10.dim("Workspace can be cleaned up with:"));
1078
+ console.log(chalk10.dim(` pan workspace destroy ${state.issueId}`));
1079
+ } catch (error) {
1080
+ spinner.fail(error.message);
1081
+ process.exit(1);
1082
+ }
1083
+ }
1084
+
1085
+ // src/cli/commands/work/plan.ts
1086
+ import chalk11 from "chalk";
1087
+ import ora6 from "ora";
1088
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "fs";
1089
+ import { join as join7 } from "path";
1090
+ import { homedir as homedir2 } from "os";
1091
+ import { execSync as execSync3 } from "child_process";
1092
+ function getLinearApiKey2() {
1093
+ const envFile = join7(homedir2(), ".panopticon.env");
1094
+ if (existsSync8(envFile)) {
1095
+ const content = readFileSync7(envFile, "utf-8");
1096
+ const match = content.match(/LINEAR_API_KEY=(.+)/);
1097
+ if (match) return match[1].trim();
1098
+ }
1099
+ return process.env.LINEAR_API_KEY || null;
1100
+ }
1101
+ function findPRDFiles(issueId) {
1102
+ const found = [];
1103
+ const cwd = process.cwd();
1104
+ const searchPaths = [
1105
+ "docs/prds",
1106
+ "docs/prd",
1107
+ "prds",
1108
+ "docs"
1109
+ ];
1110
+ const issueIdLower = issueId.toLowerCase();
1111
+ for (const searchPath of searchPaths) {
1112
+ const fullPath = join7(cwd, searchPath);
1113
+ if (!existsSync8(fullPath)) continue;
1114
+ try {
1115
+ const result = execSync3(
1116
+ `find "${fullPath}" -type f -name "*.md" 2>/dev/null | xargs grep -l -i "${issueIdLower}" 2>/dev/null || true`,
1117
+ { encoding: "utf-8" }
1118
+ );
1119
+ const files = result.trim().split("\n").filter((f) => f);
1120
+ found.push(...files);
1121
+ } catch {
1122
+ }
1123
+ }
1124
+ return [...new Set(found)];
1125
+ }
1126
+ function generatePlan(issue, prdFiles) {
1127
+ const sections = [];
1128
+ sections.push(`# Execution Plan: ${issue.identifier}`);
1129
+ sections.push("");
1130
+ sections.push(`**Title:** ${issue.title}`);
1131
+ sections.push(`**Status:** ${issue.state.name}`);
1132
+ if (issue.project) {
1133
+ sections.push(`**Project:** ${issue.project.name}`);
1134
+ }
1135
+ sections.push(`**Linear:** ${issue.url}`);
1136
+ sections.push("");
1137
+ if (issue.description) {
1138
+ sections.push("## Issue Description");
1139
+ sections.push("");
1140
+ sections.push(issue.description);
1141
+ sections.push("");
1142
+ }
1143
+ if (prdFiles.length > 0) {
1144
+ sections.push("## Related PRDs");
1145
+ sections.push("");
1146
+ for (const prd of prdFiles) {
1147
+ sections.push(`- [${prd.replace(process.cwd() + "/", "")}](${prd})`);
1148
+ }
1149
+ sections.push("");
1150
+ sections.push("> **IMPORTANT:** Review the PRD before starting implementation.");
1151
+ sections.push("");
1152
+ }
1153
+ sections.push("## Implementation Steps");
1154
+ sections.push("");
1155
+ sections.push("<!-- Edit these steps based on the issue requirements -->");
1156
+ sections.push("");
1157
+ sections.push("- [ ] Understand requirements and existing code");
1158
+ sections.push("- [ ] Design approach (document in comments if complex)");
1159
+ sections.push("- [ ] Implement core functionality");
1160
+ sections.push("- [ ] Add tests");
1161
+ sections.push("- [ ] Verify linting/type checks pass");
1162
+ sections.push("- [ ] Manual testing");
1163
+ sections.push("- [ ] Update documentation if needed");
1164
+ sections.push("");
1165
+ sections.push("## Files to Modify");
1166
+ sections.push("");
1167
+ sections.push("<!-- List files that will need changes -->");
1168
+ sections.push("");
1169
+ sections.push("- TBD after codebase exploration");
1170
+ sections.push("");
1171
+ sections.push("## Test Strategy");
1172
+ sections.push("");
1173
+ sections.push("<!-- Define how this will be tested -->");
1174
+ sections.push("");
1175
+ sections.push("- Unit tests: TBD");
1176
+ sections.push("- Integration tests: TBD");
1177
+ sections.push("- E2E tests: TBD");
1178
+ sections.push("");
1179
+ sections.push("## Acceptance Criteria");
1180
+ sections.push("");
1181
+ sections.push("<!-- What must be true for this to be complete? -->");
1182
+ sections.push("");
1183
+ sections.push("- [ ] Feature works as described");
1184
+ sections.push("- [ ] Tests pass");
1185
+ sections.push("- [ ] No regressions");
1186
+ sections.push("");
1187
+ sections.push("## Notes for Agent");
1188
+ sections.push("");
1189
+ sections.push("<!-- Add any special instructions or context -->");
1190
+ sections.push("");
1191
+ sections.push("- Review this plan before starting");
1192
+ sections.push("- Ask clarifying questions if requirements are unclear");
1193
+ sections.push("- Commit frequently with descriptive messages");
1194
+ sections.push("");
1195
+ return sections.join("\n");
1196
+ }
1197
+ async function planCommand(id, options = {}) {
1198
+ const spinner = ora6(`Creating execution plan for ${id}...`).start();
1199
+ try {
1200
+ const apiKey = getLinearApiKey2();
1201
+ if (!apiKey) {
1202
+ spinner.fail("LINEAR_API_KEY not found");
1203
+ console.log("");
1204
+ console.log(chalk11.dim("Set it in ~/.panopticon.env:"));
1205
+ console.log(" LINEAR_API_KEY=lin_api_xxxxx");
1206
+ process.exit(1);
1207
+ }
1208
+ spinner.text = "Fetching issue from Linear...";
1209
+ const { LinearClient } = await import("@linear/sdk");
1210
+ const client = new LinearClient({ apiKey });
1211
+ const me = await client.viewer;
1212
+ const teams = await me.teams();
1213
+ const team = teams.nodes[0];
1214
+ if (!team) {
1215
+ spinner.fail("No Linear team found");
1216
+ process.exit(1);
1217
+ }
1218
+ const searchResult = await team.issues({
1219
+ first: 100
1220
+ });
1221
+ const issue = searchResult.nodes.find(
1222
+ (i) => i.identifier.toUpperCase() === id.toUpperCase()
1223
+ );
1224
+ if (!issue) {
1225
+ spinner.fail(`Issue not found: ${id}`);
1226
+ process.exit(1);
1227
+ }
1228
+ const state = await issue.state;
1229
+ const assignee = await issue.assignee;
1230
+ const project2 = await issue.project;
1231
+ const labels = await issue.labels();
1232
+ const issueData = {
1233
+ id: issue.id,
1234
+ identifier: issue.identifier,
1235
+ title: issue.title,
1236
+ description: issue.description || void 0,
1237
+ state: { name: state?.name || "Unknown" },
1238
+ priority: issue.priority,
1239
+ url: issue.url,
1240
+ labels: labels.nodes.map((l) => ({ name: l.name })),
1241
+ assignee: assignee ? { name: assignee.name } : void 0,
1242
+ project: project2 ? { name: project2.name } : void 0
1243
+ };
1244
+ spinner.text = "Searching for related PRDs...";
1245
+ const prdFiles = findPRDFiles(id);
1246
+ spinner.text = "Generating execution plan...";
1247
+ const plan = generatePlan(issueData, prdFiles);
1248
+ if (options.json) {
1249
+ spinner.stop();
1250
+ console.log(JSON.stringify({
1251
+ issue: issueData,
1252
+ prdFiles,
1253
+ plan
1254
+ }, null, 2));
1255
+ return;
1256
+ }
1257
+ const outputPath = options.output || `PLAN-${issue.identifier}.md`;
1258
+ writeFileSync5(outputPath, plan);
1259
+ spinner.succeed(`Execution plan created: ${outputPath}`);
1260
+ console.log("");
1261
+ console.log(chalk11.bold("Issue Details:"));
1262
+ console.log(` ${chalk11.cyan(issue.identifier)} ${issue.title}`);
1263
+ console.log(` Status: ${state?.name}`);
1264
+ if (prdFiles.length > 0) {
1265
+ console.log(` PRDs found: ${chalk11.green(prdFiles.length)}`);
1266
+ }
1267
+ console.log("");
1268
+ console.log(chalk11.bold("Next steps:"));
1269
+ console.log(` 1. Review and edit ${chalk11.cyan(outputPath)}`);
1270
+ console.log(` 2. Run ${chalk11.cyan(`pan work issue ${id}`)} to spawn agent`);
1271
+ console.log("");
1272
+ if (prdFiles.length > 0) {
1273
+ console.log(chalk11.yellow("PRD files found - agent will reference these:"));
1274
+ for (const prd of prdFiles) {
1275
+ console.log(` ${chalk11.dim(prd.replace(process.cwd() + "/", ""))}`);
1276
+ }
1277
+ console.log("");
1278
+ }
1279
+ } catch (error) {
1280
+ spinner.fail(error.message);
1281
+ process.exit(1);
1282
+ }
1283
+ }
1284
+
1285
+ // src/cli/commands/work/list.ts
1286
+ import chalk12 from "chalk";
1287
+ import ora7 from "ora";
1288
+ import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
1289
+ import { join as join8 } from "path";
1290
+ import { homedir as homedir3 } from "os";
1291
+ function getLinearApiKey3() {
1292
+ const envFile = join8(homedir3(), ".panopticon.env");
1293
+ if (existsSync9(envFile)) {
1294
+ const content = readFileSync8(envFile, "utf-8");
1295
+ const match = content.match(/LINEAR_API_KEY=(.+)/);
1296
+ if (match) return match[1].trim();
1297
+ }
1298
+ return process.env.LINEAR_API_KEY || null;
1299
+ }
1300
+ var PRIORITY_LABELS = {
1301
+ 0: chalk12.dim("None"),
1302
+ 1: chalk12.red("Urgent"),
1303
+ 2: chalk12.yellow("High"),
1304
+ 3: chalk12.blue("Medium"),
1305
+ 4: chalk12.dim("Low")
1306
+ };
1307
+ var STATE_COLORS = {
1308
+ "Backlog": chalk12.dim,
1309
+ "Todo": chalk12.white,
1310
+ "In Progress": chalk12.yellow,
1311
+ "In Review": chalk12.magenta,
1312
+ "Done": chalk12.green,
1313
+ "Canceled": chalk12.strikethrough
1314
+ };
1315
+ async function listCommand(options) {
1316
+ const spinner = ora7("Fetching issues from Linear...").start();
1317
+ try {
1318
+ const apiKey = getLinearApiKey3();
1319
+ if (!apiKey) {
1320
+ spinner.fail("LINEAR_API_KEY not found");
1321
+ console.log("");
1322
+ console.log(chalk12.dim("Set it in ~/.panopticon.env:"));
1323
+ console.log(" LINEAR_API_KEY=lin_api_xxxxx");
1324
+ process.exit(1);
1325
+ }
1326
+ const { LinearClient } = await import("@linear/sdk");
1327
+ const client = new LinearClient({ apiKey });
1328
+ const me = await client.viewer;
1329
+ const teams = await me.teams();
1330
+ const team = teams.nodes[0];
1331
+ if (!team) {
1332
+ spinner.fail("No Linear team found");
1333
+ process.exit(1);
1334
+ }
1335
+ spinner.text = `Fetching issues from ${team.name}...`;
1336
+ let issues;
1337
+ if (options.mine) {
1338
+ const assignedIssues = await me.assignedIssues({
1339
+ first: 50,
1340
+ filter: options.all ? {} : { state: { type: { neq: "completed" } } }
1341
+ });
1342
+ issues = assignedIssues.nodes;
1343
+ } else {
1344
+ const teamIssues = await team.issues({
1345
+ first: 50,
1346
+ filter: options.all ? {} : { state: { type: { neq: "completed" } } }
1347
+ });
1348
+ issues = teamIssues.nodes;
1349
+ }
1350
+ spinner.stop();
1351
+ if (options.json) {
1352
+ const formatted = await Promise.all(issues.map(async (issue) => {
1353
+ const state = await issue.state;
1354
+ const assignee = await issue.assignee;
1355
+ return {
1356
+ id: issue.id,
1357
+ identifier: issue.identifier,
1358
+ title: issue.title,
1359
+ state: state?.name,
1360
+ priority: issue.priority,
1361
+ assignee: assignee?.name,
1362
+ url: issue.url
1363
+ };
1364
+ }));
1365
+ console.log(JSON.stringify(formatted, null, 2));
1366
+ return;
1367
+ }
1368
+ if (issues.length === 0) {
1369
+ console.log(chalk12.dim("No issues found."));
1370
+ return;
1371
+ }
1372
+ console.log(chalk12.bold(`
1373
+ ${team.name} Issues
1374
+ `));
1375
+ const byState = {};
1376
+ for (const issue of issues) {
1377
+ const state = await issue.state;
1378
+ const stateName = state?.name || "Unknown";
1379
+ if (!byState[stateName]) byState[stateName] = [];
1380
+ byState[stateName].push(issue);
1381
+ }
1382
+ const stateOrder = ["In Progress", "In Review", "Todo", "Backlog", "Done", "Canceled"];
1383
+ for (const stateName of stateOrder) {
1384
+ const stateIssues = byState[stateName];
1385
+ if (!stateIssues || stateIssues.length === 0) continue;
1386
+ const colorFn = STATE_COLORS[stateName] || chalk12.white;
1387
+ console.log(colorFn(`\u2500\u2500 ${stateName} (${stateIssues.length}) \u2500\u2500`));
1388
+ console.log("");
1389
+ for (const issue of stateIssues) {
1390
+ const assignee = await issue.assignee;
1391
+ const priorityLabel = PRIORITY_LABELS[issue.priority] || "";
1392
+ const assigneeStr = assignee ? chalk12.dim(` @${assignee.name.split(" ")[0]}`) : "";
1393
+ console.log(` ${chalk12.cyan(issue.identifier)} ${issue.title}${assigneeStr}`);
1394
+ if (issue.priority > 0 && issue.priority < 3) {
1395
+ console.log(` ${priorityLabel}`);
1396
+ }
1397
+ }
1398
+ console.log("");
1399
+ }
1400
+ console.log(chalk12.dim(`Showing ${issues.length} issues. Use --all to include completed.`));
1401
+ } catch (error) {
1402
+ spinner.fail(error.message);
1403
+ process.exit(1);
1404
+ }
1405
+ }
1406
+
1407
+ // src/cli/commands/work/triage.ts
1408
+ import chalk13 from "chalk";
1409
+ import ora8 from "ora";
1410
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "fs";
1411
+ import { join as join9 } from "path";
1412
+ import { homedir as homedir4 } from "os";
1413
+ function getConfig() {
1414
+ const envFile = join9(homedir4(), ".panopticon.env");
1415
+ const config = {};
1416
+ if (existsSync10(envFile)) {
1417
+ const content = readFileSync9(envFile, "utf-8");
1418
+ const ghMatch = content.match(/GITHUB_TOKEN=(.+)/);
1419
+ if (ghMatch) config.githubToken = ghMatch[1].trim();
1420
+ const repoMatch = content.match(/GITHUB_REPO=(.+)/);
1421
+ if (repoMatch) config.githubRepo = repoMatch[1].trim();
1422
+ const linearMatch = content.match(/LINEAR_API_KEY=(.+)/);
1423
+ if (linearMatch) config.linearApiKey = linearMatch[1].trim();
1424
+ }
1425
+ config.githubToken = config.githubToken || process.env.GITHUB_TOKEN;
1426
+ config.githubRepo = config.githubRepo || process.env.GITHUB_REPO;
1427
+ config.linearApiKey = config.linearApiKey || process.env.LINEAR_API_KEY;
1428
+ return config;
1429
+ }
1430
+ function getTriageStatePath() {
1431
+ return join9(homedir4(), ".panopticon", "triage-state.json");
1432
+ }
1433
+ function loadTriageState() {
1434
+ const path = getTriageStatePath();
1435
+ if (existsSync10(path)) {
1436
+ return JSON.parse(readFileSync9(path, "utf-8"));
1437
+ }
1438
+ return { dismissed: [], created: {} };
1439
+ }
1440
+ function saveTriageState(state) {
1441
+ const path = getTriageStatePath();
1442
+ writeFileSync6(path, JSON.stringify(state, null, 2));
1443
+ }
1444
+ async function fetchGitHubIssues(token, repo) {
1445
+ const response = await fetch(`https://api.github.com/repos/${repo}/issues?state=open&per_page=50`, {
1446
+ headers: {
1447
+ Authorization: `Bearer ${token}`,
1448
+ Accept: "application/vnd.github.v3+json"
1449
+ }
1450
+ });
1451
+ if (!response.ok) {
1452
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
1453
+ }
1454
+ const issues = await response.json();
1455
+ return issues.filter((i) => !("pull_request" in i));
1456
+ }
1457
+ async function createLinearIssue(apiKey, title, description, githubUrl) {
1458
+ const { LinearClient } = await import("@linear/sdk");
1459
+ const client = new LinearClient({ apiKey });
1460
+ const me = await client.viewer;
1461
+ const teams = await me.teams();
1462
+ const team = teams.nodes[0];
1463
+ if (!team) {
1464
+ throw new Error("No Linear team found");
1465
+ }
1466
+ const fullDescription = `${description}
1467
+
1468
+ ---
1469
+
1470
+ **From GitHub:** ${githubUrl}`;
1471
+ const result = await client.createIssue({
1472
+ teamId: team.id,
1473
+ title,
1474
+ description: fullDescription
1475
+ });
1476
+ const issue = await result.issue;
1477
+ return issue?.identifier || "unknown";
1478
+ }
1479
+ async function triageCommand(id, options = {}) {
1480
+ const spinner = ora8("Loading triage queue...").start();
1481
+ try {
1482
+ const config = getConfig();
1483
+ if (!config.githubToken || !config.githubRepo) {
1484
+ spinner.info("GitHub integration not configured");
1485
+ console.log("");
1486
+ console.log(chalk13.bold("Setup Instructions:"));
1487
+ console.log("");
1488
+ console.log("Add to ~/.panopticon.env:");
1489
+ console.log(chalk13.dim(" GITHUB_TOKEN=ghp_xxxxx"));
1490
+ console.log(chalk13.dim(" GITHUB_REPO=owner/repo"));
1491
+ console.log("");
1492
+ console.log(chalk13.dim("Get a token at: https://github.com/settings/tokens"));
1493
+ console.log(chalk13.dim("Required scopes: repo (for private repos) or public_repo"));
1494
+ return;
1495
+ }
1496
+ const triageState = loadTriageState();
1497
+ if (id) {
1498
+ const issueNumber = parseInt(id.replace("#", ""), 10);
1499
+ if (options.dismiss) {
1500
+ if (!triageState.dismissed.includes(issueNumber)) {
1501
+ triageState.dismissed.push(issueNumber);
1502
+ saveTriageState(triageState);
1503
+ }
1504
+ spinner.succeed(`Dismissed #${issueNumber}: ${options.dismiss}`);
1505
+ return;
1506
+ }
1507
+ if (options.create) {
1508
+ if (!config.linearApiKey) {
1509
+ spinner.fail("LINEAR_API_KEY not configured");
1510
+ return;
1511
+ }
1512
+ spinner.text = `Fetching GitHub issue #${issueNumber}...`;
1513
+ const response = await fetch(
1514
+ `https://api.github.com/repos/${config.githubRepo}/issues/${issueNumber}`,
1515
+ {
1516
+ headers: {
1517
+ Authorization: `Bearer ${config.githubToken}`,
1518
+ Accept: "application/vnd.github.v3+json"
1519
+ }
1520
+ }
1521
+ );
1522
+ if (!response.ok) {
1523
+ spinner.fail(`GitHub issue #${issueNumber} not found`);
1524
+ return;
1525
+ }
1526
+ const ghIssue = await response.json();
1527
+ spinner.text = "Creating Linear issue...";
1528
+ const linearId = await createLinearIssue(
1529
+ config.linearApiKey,
1530
+ ghIssue.title,
1531
+ ghIssue.body || "",
1532
+ ghIssue.html_url
1533
+ );
1534
+ triageState.created[issueNumber] = linearId;
1535
+ saveTriageState(triageState);
1536
+ spinner.succeed(`Created ${linearId} from GitHub #${issueNumber}`);
1537
+ console.log("");
1538
+ console.log(` GitHub: ${chalk13.dim(ghIssue.html_url)}`);
1539
+ console.log(` Linear: ${chalk13.cyan(linearId)}`);
1540
+ return;
1541
+ }
1542
+ }
1543
+ spinner.text = "Fetching GitHub issues...";
1544
+ const issues = await fetchGitHubIssues(config.githubToken, config.githubRepo);
1545
+ const pending = issues.filter(
1546
+ (i) => !triageState.dismissed.includes(i.number) && !triageState.created[i.number]
1547
+ );
1548
+ spinner.stop();
1549
+ if (pending.length === 0) {
1550
+ console.log(chalk13.green("No issues pending triage."));
1551
+ console.log(chalk13.dim(`${issues.length} total open, ${triageState.dismissed.length} dismissed, ${Object.keys(triageState.created).length} created`));
1552
+ return;
1553
+ }
1554
+ console.log(chalk13.bold(`
1555
+ GitHub Issues Pending Triage (${pending.length})
1556
+ `));
1557
+ for (const issue of pending) {
1558
+ const labels = issue.labels.map((l) => chalk13.dim(`[${l.name}]`)).join(" ");
1559
+ console.log(` ${chalk13.cyan(`#${issue.number}`)} ${issue.title} ${labels}`);
1560
+ console.log(` ${chalk13.dim(issue.html_url)}`);
1561
+ }
1562
+ console.log("");
1563
+ console.log(chalk13.bold("Commands:"));
1564
+ console.log(` ${chalk13.dim("Create Linear issue:")} pan work triage <number> --create`);
1565
+ console.log(` ${chalk13.dim("Dismiss from queue:")} pan work triage <number> --dismiss "reason"`);
1566
+ console.log("");
1567
+ } catch (error) {
1568
+ spinner.fail(error.message);
1569
+ process.exit(1);
1570
+ }
1571
+ }
1572
+
1573
+ // src/cli/commands/work/hook.ts
1574
+ import chalk14 from "chalk";
1575
+ async function hookCommand(action, idOrMessage, options = {}) {
1576
+ const agentId = process.env.PANOPTICON_AGENT_ID || "default";
1577
+ switch (action) {
1578
+ case "check": {
1579
+ const result = checkHook(idOrMessage || agentId);
1580
+ if (options.json) {
1581
+ console.log(JSON.stringify(result, null, 2));
1582
+ return;
1583
+ }
1584
+ if (!result.hasWork) {
1585
+ console.log(chalk14.green("\u2713 No pending work on hook"));
1586
+ return;
1587
+ }
1588
+ console.log(chalk14.yellow(`\u26A0 ${result.items.length} item(s) on hook`));
1589
+ if (result.urgentCount > 0) {
1590
+ console.log(chalk14.red(` ${result.urgentCount} URGENT`));
1591
+ }
1592
+ console.log("");
1593
+ for (const item of result.items) {
1594
+ const priorityColor = {
1595
+ urgent: chalk14.red,
1596
+ high: chalk14.yellow,
1597
+ normal: chalk14.white,
1598
+ low: chalk14.dim
1599
+ }[item.priority];
1600
+ console.log(`${priorityColor(`[${item.priority.toUpperCase()}]`)} ${item.id}`);
1601
+ console.log(` Type: ${item.type}`);
1602
+ console.log(` From: ${item.source}`);
1603
+ if (item.payload.message) {
1604
+ console.log(` Message: ${item.payload.message}`);
1605
+ }
1606
+ console.log("");
1607
+ }
1608
+ break;
1609
+ }
1610
+ case "push": {
1611
+ if (!idOrMessage) {
1612
+ console.log(chalk14.red("Usage: pan work hook push <agent-id> <message>"));
1613
+ process.exit(1);
1614
+ }
1615
+ const [targetAgent, ...messageParts] = idOrMessage.split(" ");
1616
+ const message = messageParts.join(" ");
1617
+ if (!message) {
1618
+ console.log(chalk14.red("Message required"));
1619
+ process.exit(1);
1620
+ }
1621
+ const item = pushToHook(targetAgent.startsWith("agent-") ? targetAgent : `agent-${targetAgent}`, {
1622
+ type: "task",
1623
+ priority: "normal",
1624
+ source: "cli",
1625
+ payload: { message }
1626
+ });
1627
+ console.log(chalk14.green(`\u2713 Pushed to hook: ${item.id}`));
1628
+ break;
1629
+ }
1630
+ case "pop": {
1631
+ if (!idOrMessage) {
1632
+ console.log(chalk14.red("Usage: pan work hook pop <item-id>"));
1633
+ process.exit(1);
1634
+ }
1635
+ const success = popFromHook(agentId, idOrMessage);
1636
+ if (success) {
1637
+ console.log(chalk14.green(`\u2713 Popped: ${idOrMessage}`));
1638
+ } else {
1639
+ console.log(chalk14.yellow(`Item not found: ${idOrMessage}`));
1640
+ }
1641
+ break;
1642
+ }
1643
+ case "clear": {
1644
+ clearHook(idOrMessage || agentId);
1645
+ console.log(chalk14.green("\u2713 Hook cleared"));
1646
+ break;
1647
+ }
1648
+ case "mail": {
1649
+ if (!idOrMessage) {
1650
+ console.log(chalk14.red("Usage: pan work hook mail <agent-id> <message>"));
1651
+ process.exit(1);
1652
+ }
1653
+ const [targetAgent, ...messageParts] = idOrMessage.split(" ");
1654
+ const message = messageParts.join(" ");
1655
+ if (!message) {
1656
+ console.log(chalk14.red("Message required"));
1657
+ process.exit(1);
1658
+ }
1659
+ sendMail(
1660
+ targetAgent.startsWith("agent-") ? targetAgent : `agent-${targetAgent}`,
1661
+ "cli",
1662
+ message
1663
+ );
1664
+ console.log(chalk14.green(`\u2713 Mail sent to ${targetAgent}`));
1665
+ break;
1666
+ }
1667
+ case "gupp": {
1668
+ const prompt = generateGUPPPrompt(idOrMessage || agentId);
1669
+ if (!prompt) {
1670
+ console.log(chalk14.green("No GUPP work found"));
1671
+ return;
1672
+ }
1673
+ console.log(prompt);
1674
+ break;
1675
+ }
1676
+ default:
1677
+ console.log(chalk14.bold("Hook Commands:"));
1678
+ console.log("");
1679
+ console.log(` ${chalk14.cyan("pan work hook check [agent-id]")} - Check for pending work`);
1680
+ console.log(` ${chalk14.cyan("pan work hook push <agent-id> <msg>")} - Push task to hook`);
1681
+ console.log(` ${chalk14.cyan("pan work hook pop <item-id>")} - Remove completed item`);
1682
+ console.log(` ${chalk14.cyan("pan work hook clear [agent-id]")} - Clear all hook items`);
1683
+ console.log(` ${chalk14.cyan("pan work hook mail <agent-id> <msg>")} - Send mail to agent`);
1684
+ console.log(` ${chalk14.cyan("pan work hook gupp [agent-id]")} - Generate GUPP prompt`);
1685
+ }
1686
+ }
1687
+
1688
+ // src/cli/commands/work/recover.ts
1689
+ import chalk15 from "chalk";
1690
+ import ora9 from "ora";
1691
+ async function recoverCommand(id, options = {}) {
1692
+ const spinner = ora9("Checking for crashed agents...").start();
1693
+ try {
1694
+ if (options.all || !id) {
1695
+ const crashed = detectCrashedAgents();
1696
+ if (crashed.length === 0) {
1697
+ spinner.succeed("No crashed agents found");
1698
+ return;
1699
+ }
1700
+ if (options.json) {
1701
+ spinner.stop();
1702
+ console.log(JSON.stringify({ crashed: crashed.map((a) => a.id) }, null, 2));
1703
+ if (!options.all) {
1704
+ console.log(chalk15.dim("\nUse --all to auto-recover all crashed agents"));
1705
+ return;
1706
+ }
1707
+ }
1708
+ if (!options.all) {
1709
+ spinner.info(`Found ${crashed.length} crashed agent(s)`);
1710
+ console.log("");
1711
+ for (const agent of crashed) {
1712
+ console.log(` ${chalk15.red("\u25CF")} ${chalk15.cyan(agent.id)}`);
1713
+ console.log(` Issue: ${agent.issueId}`);
1714
+ console.log(` Started: ${agent.startedAt}`);
1715
+ console.log("");
1716
+ }
1717
+ console.log(chalk15.dim("Use --all to auto-recover, or specify an agent ID"));
1718
+ return;
1719
+ }
1720
+ spinner.text = "Auto-recovering agents...";
1721
+ const result = autoRecoverAgents();
1722
+ spinner.stop();
1723
+ if (options.json) {
1724
+ console.log(JSON.stringify(result, null, 2));
1725
+ return;
1726
+ }
1727
+ if (result.recovered.length > 0) {
1728
+ console.log(chalk15.green(`\u2713 Recovered ${result.recovered.length} agent(s):`));
1729
+ for (const agentId2 of result.recovered) {
1730
+ console.log(` ${chalk15.cyan(agentId2)}`);
1731
+ }
1732
+ }
1733
+ if (result.failed.length > 0) {
1734
+ console.log(chalk15.red(`\u2717 Failed to recover ${result.failed.length} agent(s):`));
1735
+ for (const agentId2 of result.failed) {
1736
+ console.log(` ${chalk15.dim(agentId2)}`);
1737
+ }
1738
+ }
1739
+ return;
1740
+ }
1741
+ const agentId = id.startsWith("agent-") ? id : `agent-${id.toLowerCase()}`;
1742
+ spinner.text = `Recovering ${agentId}...`;
1743
+ const state = recoverAgent(agentId);
1744
+ if (!state) {
1745
+ spinner.fail(`Agent not found: ${agentId}`);
1746
+ process.exit(1);
1747
+ }
1748
+ spinner.succeed(`Recovered: ${agentId}`);
1749
+ console.log("");
1750
+ console.log(chalk15.bold("Agent Details:"));
1751
+ console.log(` Issue: ${chalk15.cyan(state.issueId)}`);
1752
+ console.log(` Workspace: ${chalk15.dim(state.workspace)}`);
1753
+ console.log(` Model: ${state.model}`);
1754
+ console.log("");
1755
+ console.log(chalk15.dim("Commands:"));
1756
+ console.log(` Attach: tmux attach -t ${state.id}`);
1757
+ console.log(` Message: pan work tell ${state.issueId} "your message"`);
1758
+ } catch (error) {
1759
+ spinner.fail(error.message);
1760
+ process.exit(1);
1761
+ }
1762
+ }
1763
+
1764
+ // src/cli/commands/work/cv.ts
1765
+ import chalk16 from "chalk";
1766
+ async function cvCommand(agentId, options = {}) {
1767
+ if (options.rankings || !agentId) {
1768
+ const rankings = getAgentRankings();
1769
+ if (options.json) {
1770
+ console.log(JSON.stringify(rankings, null, 2));
1771
+ return;
1772
+ }
1773
+ if (rankings.length === 0) {
1774
+ console.log(chalk16.dim("No agent work history yet."));
1775
+ console.log(chalk16.dim("CVs are created as agents complete work."));
1776
+ return;
1777
+ }
1778
+ console.log(chalk16.bold("\nAgent Rankings\n"));
1779
+ console.log(
1780
+ `${"Agent".padEnd(25)} ${"Success".padStart(8)} ${"Total".padStart(6)} ${"Avg Time".padStart(10)}`
1781
+ );
1782
+ console.log(chalk16.dim("\u2500".repeat(52)));
1783
+ for (let i = 0; i < rankings.length; i++) {
1784
+ const r = rankings[i];
1785
+ const medal = i === 0 ? "\u{1F947}" : i === 1 ? "\u{1F948}" : i === 2 ? "\u{1F949}" : " ";
1786
+ const successPct = `${(r.successRate * 100).toFixed(0)}%`;
1787
+ const avgTime = r.avgDuration > 0 ? `${r.avgDuration}m` : "-";
1788
+ console.log(
1789
+ `${medal} ${r.agentId.padEnd(22)} ${successPct.padStart(8)} ${r.totalIssues.toString().padStart(6)} ${avgTime.padStart(10)}`
1790
+ );
1791
+ }
1792
+ console.log("");
1793
+ console.log(chalk16.dim(`Use: pan work cv <agent-id> for details`));
1794
+ return;
1795
+ }
1796
+ const normalizedId = agentId.startsWith("agent-") ? agentId : `agent-${agentId.toLowerCase()}`;
1797
+ const cv = getAgentCV(normalizedId);
1798
+ if (options.json) {
1799
+ console.log(JSON.stringify(cv, null, 2));
1800
+ return;
1801
+ }
1802
+ console.log("");
1803
+ console.log(formatCV(cv));
1804
+ }
1805
+
1806
+ // src/cli/commands/work/context.ts
1807
+ import chalk17 from "chalk";
1808
+
1809
+ // src/lib/context.ts
1810
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync7, appendFileSync, readdirSync as readdirSync6 } from "fs";
1811
+ import { join as join10 } from "path";
1812
+ function getStateFile(agentId) {
1813
+ return join10(AGENTS_DIR, agentId, "STATE.md");
1814
+ }
1815
+ function readAgentState(agentId) {
1816
+ const stateFile = getStateFile(agentId);
1817
+ if (!existsSync11(stateFile)) return null;
1818
+ try {
1819
+ const content = readFileSync10(stateFile, "utf-8");
1820
+ return parseStateMd(content);
1821
+ } catch {
1822
+ return null;
1823
+ }
1824
+ }
1825
+ function writeAgentState(agentId, state) {
1826
+ const dir = join10(AGENTS_DIR, agentId);
1827
+ mkdirSync5(dir, { recursive: true });
1828
+ const content = generateStateMd(state);
1829
+ writeFileSync7(getStateFile(agentId), content);
1830
+ }
1831
+ function updateCheckpoint(agentId, checkpoint, resumePoint) {
1832
+ const state = readAgentState(agentId);
1833
+ if (!state) return;
1834
+ state.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
1835
+ state.lastCheckpoint = checkpoint;
1836
+ if (resumePoint) {
1837
+ state.resumePoint = resumePoint;
1838
+ }
1839
+ writeAgentState(agentId, state);
1840
+ }
1841
+ function generateStateMd(state) {
1842
+ const lines = [
1843
+ `# Agent State: ${state.issueId}`,
1844
+ "",
1845
+ "## Current Position",
1846
+ "",
1847
+ `Issue: ${state.issueId}`,
1848
+ `Status: ${state.status}`,
1849
+ `Last activity: ${state.lastActivity}`,
1850
+ ""
1851
+ ];
1852
+ if (state.lastCheckpoint) {
1853
+ lines.push("## Session Continuity");
1854
+ lines.push("");
1855
+ lines.push(`Last checkpoint: "${state.lastCheckpoint}"`);
1856
+ if (state.resumePoint) {
1857
+ lines.push(`Resume point: "${state.resumePoint}"`);
1858
+ }
1859
+ lines.push("");
1860
+ }
1861
+ if (state.contextRefs.workspace || state.contextRefs.prd || state.contextRefs.beads) {
1862
+ lines.push("## Context References");
1863
+ lines.push("");
1864
+ if (state.contextRefs.workspace) {
1865
+ lines.push(`- Workspace: ${state.contextRefs.workspace}`);
1866
+ }
1867
+ if (state.contextRefs.prd) {
1868
+ lines.push(`- PRD: ${state.contextRefs.prd}`);
1869
+ }
1870
+ if (state.contextRefs.beads) {
1871
+ lines.push(`- Beads: ${state.contextRefs.beads}`);
1872
+ }
1873
+ lines.push("");
1874
+ }
1875
+ return lines.join("\n");
1876
+ }
1877
+ function parseStateMd(content) {
1878
+ const state = {
1879
+ issueId: "",
1880
+ status: "",
1881
+ lastActivity: "",
1882
+ contextRefs: {}
1883
+ };
1884
+ const titleMatch = content.match(/# Agent State: (.+)/);
1885
+ if (titleMatch) state.issueId = titleMatch[1].trim();
1886
+ const statusMatch = content.match(/Status: (.+)/);
1887
+ if (statusMatch) state.status = statusMatch[1].trim();
1888
+ const activityMatch = content.match(/Last activity: (.+)/);
1889
+ if (activityMatch) state.lastActivity = activityMatch[1].trim();
1890
+ const checkpointMatch = content.match(/Last checkpoint: "(.+)"/);
1891
+ if (checkpointMatch) state.lastCheckpoint = checkpointMatch[1];
1892
+ const resumeMatch = content.match(/Resume point: "(.+)"/);
1893
+ if (resumeMatch) state.resumePoint = resumeMatch[1];
1894
+ const workspaceMatch = content.match(/- Workspace: (.+)/);
1895
+ if (workspaceMatch) state.contextRefs.workspace = workspaceMatch[1].trim();
1896
+ const prdMatch = content.match(/- PRD: (.+)/);
1897
+ if (prdMatch) state.contextRefs.prd = prdMatch[1].trim();
1898
+ const beadsMatch = content.match(/- Beads: (.+)/);
1899
+ if (beadsMatch) state.contextRefs.beads = beadsMatch[1].trim();
1900
+ return state;
1901
+ }
1902
+ function getSummaryFile(agentId) {
1903
+ return join10(AGENTS_DIR, agentId, "SUMMARY.md");
1904
+ }
1905
+ function appendSummary(agentId, summary) {
1906
+ const dir = join10(AGENTS_DIR, agentId);
1907
+ mkdirSync5(dir, { recursive: true });
1908
+ const summaryFile = getSummaryFile(agentId);
1909
+ const content = generateSummaryEntry(summary);
1910
+ if (existsSync11(summaryFile)) {
1911
+ appendFileSync(summaryFile, "\n---\n\n" + content);
1912
+ } else {
1913
+ writeFileSync7(summaryFile, "# Work Summaries\n\n" + content);
1914
+ }
1915
+ }
1916
+ function generateSummaryEntry(summary) {
1917
+ const lines = [
1918
+ `## ${summary.title}`,
1919
+ "",
1920
+ `**Completed:** ${summary.completedAt}`
1921
+ ];
1922
+ if (summary.duration) {
1923
+ lines.push(`**Duration:** ${summary.duration} minutes`);
1924
+ }
1925
+ lines.push("");
1926
+ lines.push("### What Was Done");
1927
+ lines.push("");
1928
+ for (let i = 0; i < summary.whatWasDone.length; i++) {
1929
+ lines.push(`${i + 1}. ${summary.whatWasDone[i]}`);
1930
+ }
1931
+ if (summary.keyInsights && summary.keyInsights.length > 0) {
1932
+ lines.push("");
1933
+ lines.push("### Key Insights");
1934
+ lines.push("");
1935
+ for (let i = 0; i < summary.keyInsights.length; i++) {
1936
+ lines.push(`${i + 1}. ${summary.keyInsights[i]}`);
1937
+ }
1938
+ }
1939
+ if (summary.filesModified && summary.filesModified.length > 0) {
1940
+ lines.push("");
1941
+ lines.push("### Files Modified");
1942
+ lines.push("");
1943
+ for (const file of summary.filesModified) {
1944
+ lines.push(`- ${file}`);
1945
+ }
1946
+ }
1947
+ lines.push("");
1948
+ return lines.join("\n");
1949
+ }
1950
+ function getHistoryDir(agentId) {
1951
+ return join10(AGENTS_DIR, agentId, "history");
1952
+ }
1953
+ function logHistory(agentId, action, details) {
1954
+ const historyDir = getHistoryDir(agentId);
1955
+ mkdirSync5(historyDir, { recursive: true });
1956
+ const date = /* @__PURE__ */ new Date();
1957
+ const dateStr = date.toISOString().split("T")[0];
1958
+ const historyFile = join10(historyDir, `${dateStr}.log`);
1959
+ const timestamp = date.toISOString();
1960
+ const detailsStr = details ? ` ${JSON.stringify(details)}` : "";
1961
+ const logLine = `[${timestamp}] ${action}${detailsStr}
1962
+ `;
1963
+ appendFileSync(historyFile, logLine);
1964
+ }
1965
+ function searchHistory(agentId, pattern) {
1966
+ const historyDir = getHistoryDir(agentId);
1967
+ if (!existsSync11(historyDir)) return [];
1968
+ const results = [];
1969
+ const regex = new RegExp(pattern, "i");
1970
+ const files = readdirSync6(historyDir).filter((f) => f.endsWith(".log"));
1971
+ files.sort().reverse();
1972
+ for (const file of files) {
1973
+ const content = readFileSync10(join10(historyDir, file), "utf-8");
1974
+ const lines = content.split("\n");
1975
+ for (const line of lines) {
1976
+ if (regex.test(line)) {
1977
+ results.push(line);
1978
+ }
1979
+ }
1980
+ }
1981
+ return results;
1982
+ }
1983
+ function getRecentHistory(agentId, limit = 20) {
1984
+ const historyDir = getHistoryDir(agentId);
1985
+ if (!existsSync11(historyDir)) return [];
1986
+ const results = [];
1987
+ const files = readdirSync6(historyDir).filter((f) => f.endsWith(".log"));
1988
+ files.sort().reverse();
1989
+ for (const file of files) {
1990
+ if (results.length >= limit) break;
1991
+ const content = readFileSync10(join10(historyDir, file), "utf-8");
1992
+ const lines = content.split("\n").filter((l) => l.trim());
1993
+ for (const line of lines.reverse()) {
1994
+ if (results.length >= limit) break;
1995
+ results.push(line);
1996
+ }
1997
+ }
1998
+ return results;
1999
+ }
2000
+ function estimateTokens(text) {
2001
+ return Math.ceil(text.length / 4);
2002
+ }
2003
+ function getMaterializedDir(agentId) {
2004
+ return join10(AGENTS_DIR, agentId, "materialized");
2005
+ }
2006
+ function listMaterialized(agentId) {
2007
+ const dir = getMaterializedDir(agentId);
2008
+ if (!existsSync11(dir)) return [];
2009
+ return readdirSync6(dir).filter((f) => f.endsWith(".md")).map((f) => {
2010
+ const match = f.match(/^(.+)-(\d+)\.md$/);
2011
+ if (!match) return null;
2012
+ return {
2013
+ tool: match[1],
2014
+ timestamp: parseInt(match[2], 10),
2015
+ file: join10(dir, f)
2016
+ };
2017
+ }).filter(Boolean);
2018
+ }
2019
+ function readMaterialized(filepath) {
2020
+ if (!existsSync11(filepath)) return null;
2021
+ return readFileSync10(filepath, "utf-8");
2022
+ }
2023
+
2024
+ // src/cli/commands/work/context.ts
2025
+ import { readFileSync as readFileSync11, existsSync as existsSync12 } from "fs";
2026
+ async function contextCommand(action, arg1, arg2, options = {}) {
2027
+ const agentId = process.env.PANOPTICON_AGENT_ID || arg1 || "default";
2028
+ switch (action) {
2029
+ case "state": {
2030
+ const state = readAgentState(agentId);
2031
+ if (options.json) {
2032
+ console.log(JSON.stringify(state, null, 2));
2033
+ return;
2034
+ }
2035
+ if (!state) {
2036
+ console.log(chalk17.dim("No state found for agent."));
2037
+ console.log(chalk17.dim("Initialize with: pan work context init <agent-id> <issue-id>"));
2038
+ return;
2039
+ }
2040
+ console.log(chalk17.bold(`
2041
+ Agent State: ${state.issueId}
2042
+ `));
2043
+ console.log(`Status: ${chalk17.cyan(state.status)}`);
2044
+ console.log(`Last Activity: ${chalk17.dim(state.lastActivity)}`);
2045
+ if (state.lastCheckpoint) {
2046
+ console.log("");
2047
+ console.log(chalk17.bold("Session Continuity:"));
2048
+ console.log(` Checkpoint: ${chalk17.yellow(state.lastCheckpoint)}`);
2049
+ if (state.resumePoint) {
2050
+ console.log(` Resume: ${chalk17.green(state.resumePoint)}`);
2051
+ }
2052
+ }
2053
+ if (state.contextRefs.workspace || state.contextRefs.prd) {
2054
+ console.log("");
2055
+ console.log(chalk17.bold("Context References:"));
2056
+ if (state.contextRefs.workspace) {
2057
+ console.log(` Workspace: ${chalk17.dim(state.contextRefs.workspace)}`);
2058
+ }
2059
+ if (state.contextRefs.prd) {
2060
+ console.log(` PRD: ${chalk17.dim(state.contextRefs.prd)}`);
2061
+ }
2062
+ if (state.contextRefs.beads) {
2063
+ console.log(` Beads: ${chalk17.dim(state.contextRefs.beads)}`);
2064
+ }
2065
+ }
2066
+ console.log("");
2067
+ break;
2068
+ }
2069
+ case "init": {
2070
+ const issueId = arg2 || arg1 || "UNKNOWN";
2071
+ const targetAgent = arg2 ? arg1 : agentId;
2072
+ const state = {
2073
+ issueId: issueId.toUpperCase(),
2074
+ status: "In Progress",
2075
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
2076
+ contextRefs: {}
2077
+ };
2078
+ writeAgentState(targetAgent, state);
2079
+ logHistory(targetAgent, "context:init", { issueId });
2080
+ console.log(chalk17.green(`\u2713 Initialized state for ${targetAgent}`));
2081
+ break;
2082
+ }
2083
+ case "checkpoint": {
2084
+ const checkpoint = arg1;
2085
+ const resume = arg2;
2086
+ if (!checkpoint) {
2087
+ console.log(chalk17.red("Checkpoint message required"));
2088
+ console.log(chalk17.dim('Usage: pan work context checkpoint "message" ["resume point"]'));
2089
+ return;
2090
+ }
2091
+ updateCheckpoint(agentId, checkpoint, resume);
2092
+ logHistory(agentId, "context:checkpoint", { checkpoint, resume });
2093
+ console.log(chalk17.green(`\u2713 Checkpoint saved: "${checkpoint}"`));
2094
+ if (resume) {
2095
+ console.log(chalk17.dim(` Resume point: "${resume}"`));
2096
+ }
2097
+ break;
2098
+ }
2099
+ case "summary": {
2100
+ const title = arg1 || "Work Session";
2101
+ const summary = {
2102
+ title,
2103
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2104
+ whatWasDone: ["Completed assigned work"]
2105
+ };
2106
+ appendSummary(agentId, summary);
2107
+ logHistory(agentId, "context:summary", { title });
2108
+ console.log(chalk17.green(`\u2713 Summary added: "${title}"`));
2109
+ break;
2110
+ }
2111
+ case "history": {
2112
+ const pattern = arg1;
2113
+ if (pattern) {
2114
+ const results = searchHistory(agentId, pattern);
2115
+ if (results.length === 0) {
2116
+ console.log(chalk17.dim("No matches found."));
2117
+ return;
2118
+ }
2119
+ console.log(chalk17.bold(`
2120
+ History matches for "${pattern}":
2121
+ `));
2122
+ for (const line of results.slice(0, 50)) {
2123
+ console.log(line);
2124
+ }
2125
+ } else {
2126
+ const recent = getRecentHistory(agentId, 20);
2127
+ if (recent.length === 0) {
2128
+ console.log(chalk17.dim("No history yet."));
2129
+ return;
2130
+ }
2131
+ console.log(chalk17.bold("\nRecent History:\n"));
2132
+ for (const line of recent) {
2133
+ console.log(line);
2134
+ }
2135
+ }
2136
+ console.log("");
2137
+ break;
2138
+ }
2139
+ case "materialize": {
2140
+ const filepath = arg1;
2141
+ if (filepath && existsSync12(filepath)) {
2142
+ const content = readMaterialized(filepath);
2143
+ if (content) {
2144
+ console.log(content);
2145
+ }
2146
+ return;
2147
+ }
2148
+ const outputs = listMaterialized(agentId);
2149
+ if (outputs.length === 0) {
2150
+ console.log(chalk17.dim("No materialized outputs."));
2151
+ return;
2152
+ }
2153
+ console.log(chalk17.bold("\nMaterialized Outputs:\n"));
2154
+ for (const out of outputs) {
2155
+ const date = new Date(out.timestamp).toLocaleString();
2156
+ console.log(` ${chalk17.cyan(out.tool)} ${chalk17.dim(date)}`);
2157
+ console.log(` ${chalk17.dim(out.file)}`);
2158
+ }
2159
+ console.log("");
2160
+ break;
2161
+ }
2162
+ case "tokens": {
2163
+ const target = arg1;
2164
+ if (!target) {
2165
+ console.log(chalk17.dim("Usage: pan work context tokens <file-or-text>"));
2166
+ return;
2167
+ }
2168
+ let text = target;
2169
+ if (existsSync12(target)) {
2170
+ text = readFileSync11(target, "utf-8");
2171
+ }
2172
+ const tokens = estimateTokens(text);
2173
+ console.log(`Estimated tokens: ${chalk17.cyan(tokens.toLocaleString())}`);
2174
+ break;
2175
+ }
2176
+ default:
2177
+ console.log(chalk17.bold("Context Commands:"));
2178
+ console.log("");
2179
+ console.log(` ${chalk17.cyan("pan work context state [agent-id]")} - Show current state`);
2180
+ console.log(` ${chalk17.cyan("pan work context init <agent> <issue>")} - Initialize state`);
2181
+ console.log(` ${chalk17.cyan('pan work context checkpoint "msg"')} - Save checkpoint`);
2182
+ console.log(` ${chalk17.cyan("pan work context summary [title]")} - Add work summary`);
2183
+ console.log(` ${chalk17.cyan("pan work context history [pattern]")} - Search history`);
2184
+ console.log(` ${chalk17.cyan("pan work context materialize [file]")} - List/read outputs`);
2185
+ console.log(` ${chalk17.cyan("pan work context tokens <file>")} - Estimate token count`);
2186
+ console.log("");
2187
+ }
2188
+ }
2189
+
2190
+ // src/cli/commands/work/health.ts
2191
+ import chalk18 from "chalk";
2192
+
2193
+ // src/lib/health.ts
2194
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
2195
+ import { join as join11 } from "path";
2196
+ import { execSync as execSync4 } from "child_process";
2197
+ var DEFAULT_PING_TIMEOUT_MS = 30 * 1e3;
2198
+ var DEFAULT_CONSECUTIVE_FAILURES = 3;
2199
+ var DEFAULT_COOLDOWN_MS = 5 * 60 * 1e3;
2200
+ var DEFAULT_CHECK_INTERVAL_MS = 30 * 1e3;
2201
+ function getHealthFile(agentId) {
2202
+ return join11(AGENTS_DIR, agentId, "health.json");
2203
+ }
2204
+ function getAgentHealth(agentId) {
2205
+ const healthFile = getHealthFile(agentId);
2206
+ const defaultHealth = {
2207
+ agentId,
2208
+ status: "healthy",
2209
+ consecutiveFailures: 0,
2210
+ forceKillCount: 0,
2211
+ recoveryCount: 0,
2212
+ inCooldown: false
2213
+ };
2214
+ if (existsSync13(healthFile)) {
2215
+ try {
2216
+ const stored = JSON.parse(readFileSync12(healthFile, "utf-8"));
2217
+ return { ...defaultHealth, ...stored };
2218
+ } catch {
2219
+ }
2220
+ }
2221
+ return defaultHealth;
2222
+ }
2223
+ function saveAgentHealth(health) {
2224
+ const dir = join11(AGENTS_DIR, health.agentId);
2225
+ mkdirSync6(dir, { recursive: true });
2226
+ writeFileSync8(getHealthFile(health.agentId), JSON.stringify(health, null, 2));
2227
+ }
2228
+ function isAgentAlive(agentId) {
2229
+ try {
2230
+ execSync4(`tmux has-session -t "${agentId}" 2>/dev/null`, { encoding: "utf-8" });
2231
+ return true;
2232
+ } catch {
2233
+ return false;
2234
+ }
2235
+ }
2236
+ function pingAgent(agentId, config = {
2237
+ pingTimeoutMs: DEFAULT_PING_TIMEOUT_MS,
2238
+ consecutiveFailures: DEFAULT_CONSECUTIVE_FAILURES,
2239
+ cooldownMs: DEFAULT_COOLDOWN_MS,
2240
+ checkIntervalMs: DEFAULT_CHECK_INTERVAL_MS
2241
+ }) {
2242
+ const health = getAgentHealth(agentId);
2243
+ health.lastPing = (/* @__PURE__ */ new Date()).toISOString();
2244
+ const alive = isAgentAlive(agentId);
2245
+ if (!alive) {
2246
+ health.status = "dead";
2247
+ health.consecutiveFailures++;
2248
+ } else {
2249
+ const state = getAgentState(agentId);
2250
+ const lastActivity = state?.lastActivity ? new Date(state.lastActivity) : null;
2251
+ if (lastActivity) {
2252
+ const ageMs = Date.now() - lastActivity.getTime();
2253
+ const ageMinutes = ageMs / (1e3 * 60);
2254
+ if (ageMinutes > 30) {
2255
+ health.status = "stuck";
2256
+ health.consecutiveFailures++;
2257
+ } else if (ageMinutes > 15) {
2258
+ health.status = "warning";
2259
+ } else {
2260
+ health.status = "healthy";
2261
+ health.consecutiveFailures = 0;
2262
+ }
2263
+ } else {
2264
+ health.status = "healthy";
2265
+ health.consecutiveFailures = 0;
2266
+ }
2267
+ health.lastPingResponse = (/* @__PURE__ */ new Date()).toISOString();
2268
+ }
2269
+ if (health.lastForceKill) {
2270
+ const timeSinceKill = Date.now() - new Date(health.lastForceKill).getTime();
2271
+ health.inCooldown = timeSinceKill < config.cooldownMs;
2272
+ } else {
2273
+ health.inCooldown = false;
2274
+ }
2275
+ saveAgentHealth(health);
2276
+ return health;
2277
+ }
2278
+ async function handleStuckAgent(agentId, config = {
2279
+ pingTimeoutMs: DEFAULT_PING_TIMEOUT_MS,
2280
+ consecutiveFailures: DEFAULT_CONSECUTIVE_FAILURES,
2281
+ cooldownMs: DEFAULT_COOLDOWN_MS,
2282
+ checkIntervalMs: DEFAULT_CHECK_INTERVAL_MS
2283
+ }) {
2284
+ const health = getAgentHealth(agentId);
2285
+ if (health.consecutiveFailures < config.consecutiveFailures) {
2286
+ return {
2287
+ action: "skipped",
2288
+ reason: `Only ${health.consecutiveFailures} failures (need ${config.consecutiveFailures})`
2289
+ };
2290
+ }
2291
+ if (health.lastForceKill) {
2292
+ const timeSinceKill = Date.now() - new Date(health.lastForceKill).getTime();
2293
+ if (timeSinceKill < config.cooldownMs) {
2294
+ const remainingMs = config.cooldownMs - timeSinceKill;
2295
+ const remainingMin = Math.ceil(remainingMs / (1e3 * 60));
2296
+ return {
2297
+ action: "cooldown",
2298
+ reason: `In cooldown (${remainingMin}m remaining)`
2299
+ };
2300
+ }
2301
+ }
2302
+ try {
2303
+ stopAgent(agentId);
2304
+ } catch {
2305
+ }
2306
+ health.lastForceKill = (/* @__PURE__ */ new Date()).toISOString();
2307
+ health.forceKillCount++;
2308
+ health.consecutiveFailures = 0;
2309
+ health.status = "dead";
2310
+ health.inCooldown = true;
2311
+ saveAgentHealth(health);
2312
+ try {
2313
+ const recovered = recoverAgent(agentId);
2314
+ if (recovered) {
2315
+ health.status = "healthy";
2316
+ health.recoveryCount++;
2317
+ saveAgentHealth(health);
2318
+ return { action: "recovered", reason: "Force killed and respawned" };
2319
+ }
2320
+ } catch {
2321
+ }
2322
+ return { action: "recovered", reason: "Force killed (respawn failed)" };
2323
+ }
2324
+ async function runHealthCheck(config = {
2325
+ pingTimeoutMs: DEFAULT_PING_TIMEOUT_MS,
2326
+ consecutiveFailures: DEFAULT_CONSECUTIVE_FAILURES,
2327
+ cooldownMs: DEFAULT_COOLDOWN_MS,
2328
+ checkIntervalMs: DEFAULT_CHECK_INTERVAL_MS
2329
+ }) {
2330
+ const results = {
2331
+ checked: 0,
2332
+ healthy: 0,
2333
+ warning: 0,
2334
+ stuck: 0,
2335
+ dead: 0,
2336
+ recovered: []
2337
+ };
2338
+ let sessions = [];
2339
+ try {
2340
+ const output = execSync4(
2341
+ 'tmux list-sessions -F "#{session_name}" 2>/dev/null || true',
2342
+ { encoding: "utf-8" }
2343
+ );
2344
+ sessions = output.trim().split("\n").filter((s) => s.startsWith("agent-"));
2345
+ } catch {
2346
+ }
2347
+ if (existsSync13(AGENTS_DIR)) {
2348
+ const { readdirSync: readdirSync10 } = await import("fs");
2349
+ const dirs = readdirSync10(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-")).map((d) => d.name);
2350
+ for (const dir of dirs) {
2351
+ if (!sessions.includes(dir)) {
2352
+ sessions.push(dir);
2353
+ }
2354
+ }
2355
+ }
2356
+ for (const agentId of sessions) {
2357
+ results.checked++;
2358
+ const health = pingAgent(agentId, config);
2359
+ switch (health.status) {
2360
+ case "healthy":
2361
+ results.healthy++;
2362
+ break;
2363
+ case "warning":
2364
+ results.warning++;
2365
+ break;
2366
+ case "stuck":
2367
+ results.stuck++;
2368
+ const result = await handleStuckAgent(agentId, config);
2369
+ if (result.action === "recovered") {
2370
+ results.recovered.push(agentId);
2371
+ }
2372
+ break;
2373
+ case "dead":
2374
+ results.dead++;
2375
+ const deadResult = await handleStuckAgent(agentId, config);
2376
+ if (deadResult.action === "recovered") {
2377
+ results.recovered.push(agentId);
2378
+ }
2379
+ break;
2380
+ }
2381
+ }
2382
+ return results;
2383
+ }
2384
+ function startHealthDaemon(config = {
2385
+ pingTimeoutMs: DEFAULT_PING_TIMEOUT_MS,
2386
+ consecutiveFailures: DEFAULT_CONSECUTIVE_FAILURES,
2387
+ cooldownMs: DEFAULT_COOLDOWN_MS,
2388
+ checkIntervalMs: DEFAULT_CHECK_INTERVAL_MS
2389
+ }, onCheck) {
2390
+ let running = true;
2391
+ const runLoop = async () => {
2392
+ while (running) {
2393
+ try {
2394
+ const results = await runHealthCheck(config);
2395
+ if (onCheck) {
2396
+ onCheck(results);
2397
+ }
2398
+ } catch (error) {
2399
+ console.error("Health check error:", error);
2400
+ }
2401
+ await new Promise((resolve2) => setTimeout(resolve2, config.checkIntervalMs));
2402
+ }
2403
+ };
2404
+ runLoop();
2405
+ return () => {
2406
+ running = false;
2407
+ };
2408
+ }
2409
+ function formatHealthStatus(health) {
2410
+ const statusIcons = {
2411
+ healthy: "\u2705",
2412
+ warning: "\u26A0\uFE0F",
2413
+ stuck: "\u{1F7E0}",
2414
+ dead: "\u274C"
2415
+ };
2416
+ const lines = [
2417
+ `${statusIcons[health.status]} ${health.agentId}: ${health.status.toUpperCase()}`
2418
+ ];
2419
+ if (health.lastPing) {
2420
+ lines.push(` Last ping: ${health.lastPing}`);
2421
+ }
2422
+ if (health.consecutiveFailures > 0) {
2423
+ lines.push(` Consecutive failures: ${health.consecutiveFailures}`);
2424
+ }
2425
+ if (health.forceKillCount > 0) {
2426
+ lines.push(` Force kills: ${health.forceKillCount}`);
2427
+ }
2428
+ if (health.recoveryCount > 0) {
2429
+ lines.push(` Recoveries: ${health.recoveryCount}`);
2430
+ }
2431
+ if (health.inCooldown) {
2432
+ lines.push(` Status: IN COOLDOWN`);
2433
+ }
2434
+ return lines.join("\n");
2435
+ }
2436
+
2437
+ // src/cli/commands/work/health.ts
2438
+ async function healthCommand(action, arg, options = {}) {
2439
+ const config = {
2440
+ pingTimeoutMs: DEFAULT_PING_TIMEOUT_MS,
2441
+ consecutiveFailures: DEFAULT_CONSECUTIVE_FAILURES,
2442
+ cooldownMs: DEFAULT_COOLDOWN_MS,
2443
+ checkIntervalMs: options.interval ? options.interval * 1e3 : DEFAULT_CHECK_INTERVAL_MS
2444
+ };
2445
+ switch (action) {
2446
+ case "check": {
2447
+ console.log(chalk18.bold("Running health check...\n"));
2448
+ const results = await runHealthCheck(config);
2449
+ if (options.json) {
2450
+ console.log(JSON.stringify(results, null, 2));
2451
+ return;
2452
+ }
2453
+ console.log(`Checked: ${results.checked} agents`);
2454
+ console.log(` ${chalk18.green("\u2705 Healthy:")} ${results.healthy}`);
2455
+ console.log(` ${chalk18.yellow("\u26A0\uFE0F Warning:")} ${results.warning}`);
2456
+ console.log(` ${chalk18.hex("#FFA500")("\u{1F7E0} Stuck:")} ${results.stuck}`);
2457
+ console.log(` ${chalk18.red("\u274C Dead:")} ${results.dead}`);
2458
+ if (results.recovered.length > 0) {
2459
+ console.log("");
2460
+ console.log(chalk18.green("Recovered agents:"));
2461
+ for (const agentId of results.recovered) {
2462
+ console.log(` - ${agentId}`);
2463
+ }
2464
+ }
2465
+ break;
2466
+ }
2467
+ case "status": {
2468
+ const agents = listRunningAgents();
2469
+ if (agents.length === 0) {
2470
+ console.log(chalk18.dim("No agents found."));
2471
+ return;
2472
+ }
2473
+ const healthData = agents.map((agent) => {
2474
+ const health = getAgentHealth(agent.id);
2475
+ return { agent, health };
2476
+ });
2477
+ if (options.json) {
2478
+ console.log(JSON.stringify(healthData.map((d) => d.health), null, 2));
2479
+ return;
2480
+ }
2481
+ console.log(chalk18.bold("Agent Health Status:\n"));
2482
+ for (const { health } of healthData) {
2483
+ console.log(formatHealthStatus(health));
2484
+ console.log("");
2485
+ }
2486
+ break;
2487
+ }
2488
+ case "ping": {
2489
+ if (!arg) {
2490
+ console.log(chalk18.red("Agent ID required"));
2491
+ console.log(chalk18.dim("Usage: pan work health ping <agent-id>"));
2492
+ return;
2493
+ }
2494
+ const agentId = arg.startsWith("agent-") ? arg : `agent-${arg.toLowerCase()}`;
2495
+ console.log(chalk18.dim(`Pinging ${agentId}...`));
2496
+ const health = pingAgent(agentId, config);
2497
+ if (options.json) {
2498
+ console.log(JSON.stringify(health, null, 2));
2499
+ return;
2500
+ }
2501
+ console.log("");
2502
+ console.log(formatHealthStatus(health));
2503
+ break;
2504
+ }
2505
+ case "recover": {
2506
+ if (!arg) {
2507
+ console.log(chalk18.red("Agent ID required"));
2508
+ console.log(chalk18.dim("Usage: pan work health recover <agent-id>"));
2509
+ return;
2510
+ }
2511
+ const agentId = arg.startsWith("agent-") ? arg : `agent-${arg.toLowerCase()}`;
2512
+ console.log(chalk18.dim(`Attempting recovery of ${agentId}...`));
2513
+ const forceConfig = { ...config, consecutiveFailures: 0 };
2514
+ const result = await handleStuckAgent(agentId, forceConfig);
2515
+ if (options.json) {
2516
+ console.log(JSON.stringify(result, null, 2));
2517
+ return;
2518
+ }
2519
+ if (result.action === "recovered") {
2520
+ console.log(chalk18.green(`\u2705 ${result.reason}`));
2521
+ } else if (result.action === "cooldown") {
2522
+ console.log(chalk18.yellow(`\u26A0\uFE0F ${result.reason}`));
2523
+ } else {
2524
+ console.log(chalk18.dim(result.reason));
2525
+ }
2526
+ break;
2527
+ }
2528
+ case "daemon": {
2529
+ console.log(chalk18.bold("Starting Panopticon Health Daemon"));
2530
+ console.log(chalk18.dim(`Check interval: ${config.checkIntervalMs / 1e3}s`));
2531
+ console.log(chalk18.dim(`Failure threshold: ${config.consecutiveFailures}`));
2532
+ console.log(chalk18.dim(`Cooldown: ${config.cooldownMs / (1e3 * 60)}m`));
2533
+ console.log("");
2534
+ console.log(chalk18.dim("Press Ctrl+C to stop...\n"));
2535
+ const stop = startHealthDaemon(config, (results) => {
2536
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
2537
+ const statusParts = [
2538
+ `[${timestamp}]`,
2539
+ `\u2705${results.healthy}`,
2540
+ `\u26A0\uFE0F${results.warning}`,
2541
+ `\u{1F7E0}${results.stuck}`,
2542
+ `\u274C${results.dead}`
2543
+ ];
2544
+ if (results.recovered.length > 0) {
2545
+ statusParts.push(chalk18.green(`+${results.recovered.length} recovered`));
2546
+ }
2547
+ console.log(statusParts.join(" "));
2548
+ });
2549
+ process.on("SIGINT", () => {
2550
+ console.log("\n" + chalk18.dim("Stopping health daemon..."));
2551
+ stop();
2552
+ process.exit(0);
2553
+ });
2554
+ await new Promise(() => {
2555
+ });
2556
+ break;
2557
+ }
2558
+ default:
2559
+ console.log(chalk18.bold("Health Monitoring Commands:"));
2560
+ console.log("");
2561
+ console.log(` ${chalk18.cyan("pan work health check")} - Run single health check`);
2562
+ console.log(` ${chalk18.cyan("pan work health status")} - Show all agent health`);
2563
+ console.log(` ${chalk18.cyan("pan work health ping <id>")} - Ping specific agent`);
2564
+ console.log(` ${chalk18.cyan("pan work health recover <id>")} - Force recover agent`);
2565
+ console.log(` ${chalk18.cyan("pan work health daemon")} - Start health daemon`);
2566
+ console.log("");
2567
+ console.log(chalk18.bold("Options:"));
2568
+ console.log(` ${chalk18.dim("--json")} Output as JSON`);
2569
+ console.log(` ${chalk18.dim("--interval <sec>")} Check interval for daemon (default: 30)`);
2570
+ console.log("");
2571
+ console.log(chalk18.bold("Deacon Pattern Defaults:"));
2572
+ console.log(` Ping timeout: ${DEFAULT_PING_TIMEOUT_MS / 1e3}s`);
2573
+ console.log(` Consecutive failures: ${DEFAULT_CONSECUTIVE_FAILURES}`);
2574
+ console.log(` Cooldown after kill: ${DEFAULT_COOLDOWN_MS / (1e3 * 60)}m`);
2575
+ console.log("");
2576
+ }
2577
+ }
2578
+
2579
+ // src/cli/commands/work/index.ts
2580
+ function registerWorkCommands(program2) {
2581
+ const work = program2.command("work").description("Agent and work management");
2582
+ work.command("issue <id>").description("Spawn agent for Linear issue").option("--model <model>", "Claude model (sonnet/opus/haiku)", "sonnet").option("--runtime <runtime>", "AI runtime (claude/codex)", "claude").option("--dry-run", "Show what would be created").action(issueCommand);
2583
+ work.command("status").description("Show all running agents").option("--json", "Output as JSON").action(statusCommand);
2584
+ work.command("tell <id> <message>").description("Send message to running agent").action(tellCommand);
2585
+ work.command("kill <id>").description("Kill an agent").option("--force", "Kill without confirmation").action(killCommand);
2586
+ work.command("pending").description("Show completed work awaiting review").action(pendingCommand);
2587
+ work.command("approve <id>").description("Approve agent work, merge MR, update Linear").option("--no-merge", "Skip MR merge").option("--no-linear", "Skip Linear status update").action(approveCommand);
2588
+ work.command("plan <id>").description("Create execution plan before spawning").option("-o, --output <path>", "Output file path").option("--json", "Output as JSON").action(planCommand);
2589
+ work.command("list").description("List Linear issues").option("--all", "Include completed issues").option("--mine", "Show only my assigned issues").option("--json", "Output as JSON").action(listCommand);
2590
+ work.command("triage [id]").description("Triage secondary tracker issues").option("--create", "Create primary issue from secondary").option("--dismiss <reason>", "Dismiss from triage").action(triageCommand);
2591
+ work.command("hook [action] [idOrMessage...]").description("GUPP hooks: check, push, pop, clear, mail, gupp").option("--json", "Output as JSON").action((action, idOrMessage, options) => {
2592
+ hookCommand(action || "help", idOrMessage?.join(" "), options);
2593
+ });
2594
+ work.command("recover [id]").description("Recover crashed agents").option("--all", "Auto-recover all crashed agents").option("--json", "Output as JSON").action(recoverCommand);
2595
+ work.command("cv [agentId]").description("View agent CVs (work history) and rankings").option("--json", "Output as JSON").option("--rankings", "Show agent rankings").action(cvCommand);
2596
+ work.command("context [action] [arg1] [arg2]").description("Context engineering: state, checkpoint, history, materialize").option("--json", "Output as JSON").action((action, arg1, arg2, options) => {
2597
+ contextCommand(action || "help", arg1, arg2, options);
2598
+ });
2599
+ work.command("health [action] [id]").description("Health monitoring: check, status, ping, recover, daemon").option("--json", "Output as JSON").option("--interval <seconds>", "Daemon check interval", "30").action((action, id, options) => {
2600
+ healthCommand(action || "help", id, {
2601
+ json: options.json,
2602
+ interval: parseInt(options.interval, 10)
2603
+ });
2604
+ });
2605
+ }
2606
+
2607
+ // src/cli/commands/workspace.ts
2608
+ import chalk19 from "chalk";
2609
+ import ora10 from "ora";
2610
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
2611
+ import { join as join14, basename as basename2 } from "path";
2612
+
2613
+ // src/lib/worktree.ts
2614
+ import { execSync as execSync5 } from "child_process";
2615
+ import { mkdirSync as mkdirSync7 } from "fs";
2616
+ import { dirname } from "path";
2617
+ function listWorktrees(repoPath) {
2618
+ const output = execSync5("git worktree list --porcelain", {
2619
+ cwd: repoPath,
2620
+ encoding: "utf8"
2621
+ });
2622
+ const worktrees = [];
2623
+ let current = {};
2624
+ for (const line of output.split("\n")) {
2625
+ if (line.startsWith("worktree ")) {
2626
+ if (current.path) worktrees.push(current);
2627
+ current = { path: line.slice(9), prunable: false };
2628
+ } else if (line.startsWith("HEAD ")) {
2629
+ current.head = line.slice(5);
2630
+ } else if (line.startsWith("branch ")) {
2631
+ current.branch = line.slice(7).replace("refs/heads/", "");
2632
+ } else if (line === "prunable") {
2633
+ current.prunable = true;
2634
+ }
2635
+ }
2636
+ if (current.path) worktrees.push(current);
2637
+ return worktrees;
2638
+ }
2639
+ function createWorktree(repoPath, targetPath, branchName) {
2640
+ mkdirSync7(dirname(targetPath), { recursive: true });
2641
+ try {
2642
+ execSync5(`git show-ref --verify --quiet refs/heads/${branchName}`, {
2643
+ cwd: repoPath
2644
+ });
2645
+ execSync5(`git worktree add "${targetPath}" "${branchName}"`, {
2646
+ cwd: repoPath,
2647
+ stdio: "pipe"
2648
+ });
2649
+ } catch {
2650
+ execSync5(`git worktree add -b "${branchName}" "${targetPath}"`, {
2651
+ cwd: repoPath,
2652
+ stdio: "pipe"
2653
+ });
2654
+ }
2655
+ }
2656
+ function removeWorktree(repoPath, worktreePath) {
2657
+ execSync5(`git worktree remove "${worktreePath}" --force`, {
2658
+ cwd: repoPath,
2659
+ stdio: "pipe"
2660
+ });
2661
+ }
2662
+
2663
+ // src/lib/template.ts
2664
+ import { readFileSync as readFileSync13, existsSync as existsSync14, readdirSync as readdirSync7 } from "fs";
2665
+ import { join as join12 } from "path";
2666
+ function loadTemplate(templatePath) {
2667
+ if (!existsSync14(templatePath)) {
2668
+ throw new Error(`Template not found: ${templatePath}`);
2669
+ }
2670
+ return readFileSync13(templatePath, "utf8");
2671
+ }
2672
+ function substituteVariables(template, variables) {
2673
+ let result = template;
2674
+ for (const [key, value] of Object.entries(variables)) {
2675
+ if (value !== void 0) {
2676
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
2677
+ result = result.replace(new RegExp(`\\$\\{${key}\\}`, "g"), value);
2678
+ }
2679
+ }
2680
+ return result;
2681
+ }
2682
+ function generateClaudeMd(projectPath, variables) {
2683
+ const sections = [];
2684
+ const defaultOrder = [
2685
+ "workspace-info.md",
2686
+ "beads.md",
2687
+ "commands-skills.md",
2688
+ "warnings.md"
2689
+ ];
2690
+ for (const section of defaultOrder) {
2691
+ const sectionPath = join12(CLAUDE_MD_TEMPLATES, section);
2692
+ if (existsSync14(sectionPath)) {
2693
+ const content = loadTemplate(sectionPath);
2694
+ sections.push(substituteVariables(content, variables));
2695
+ }
2696
+ }
2697
+ const projectSections = join12(projectPath, ".panopticon", "claude-md", "sections");
2698
+ if (existsSync14(projectSections)) {
2699
+ const projectFiles = readdirSync7(projectSections).filter((f) => f.endsWith(".md")).sort();
2700
+ for (const file of projectFiles) {
2701
+ const content = loadTemplate(join12(projectSections, file));
2702
+ sections.push(substituteVariables(content, variables));
2703
+ }
2704
+ }
2705
+ if (sections.length === 0) {
2706
+ return `# Workspace: ${variables.FEATURE_FOLDER}
2707
+
2708
+ **Issue:** ${variables.ISSUE_ID}
2709
+ **Branch:** ${variables.BRANCH_NAME}
2710
+ **Path:** ${variables.WORKSPACE_PATH}
2711
+
2712
+ ## Getting Started
2713
+
2714
+ This workspace was created by Panopticon. Use \`bd\` commands to track your work.
2715
+ `;
2716
+ }
2717
+ return sections.join("\n\n---\n\n");
2718
+ }
2719
+
2720
+ // src/lib/skills-merge.ts
2721
+ import {
2722
+ existsSync as existsSync15,
2723
+ readdirSync as readdirSync8,
2724
+ lstatSync,
2725
+ readlinkSync,
2726
+ symlinkSync,
2727
+ mkdirSync as mkdirSync8,
2728
+ appendFileSync as appendFileSync2
2729
+ } from "fs";
2730
+ import { join as join13 } from "path";
2731
+ import { execSync as execSync6 } from "child_process";
2732
+ function detectContentOrigin(path, repoPath) {
2733
+ try {
2734
+ const stat = lstatSync(path);
2735
+ if (stat.isSymbolicLink()) {
2736
+ const target = readlinkSync(path);
2737
+ if (target.includes(".panopticon")) {
2738
+ return "panopticon";
2739
+ }
2740
+ }
2741
+ try {
2742
+ execSync6(`git ls-files --error-unmatch "${path}" 2>/dev/null`, {
2743
+ cwd: repoPath,
2744
+ stdio: "pipe"
2745
+ });
2746
+ return "git-tracked";
2747
+ } catch {
2748
+ return "user-untracked";
2749
+ }
2750
+ } catch {
2751
+ return "user-untracked";
2752
+ }
2753
+ }
2754
+ function mergeSkillsIntoWorkspace(workspacePath) {
2755
+ const skillsTarget = join13(workspacePath, ".claude", "skills");
2756
+ const added = [];
2757
+ const skipped = [];
2758
+ mkdirSync8(skillsTarget, { recursive: true });
2759
+ const existingSkills = /* @__PURE__ */ new Set();
2760
+ if (existsSync15(skillsTarget)) {
2761
+ for (const item of readdirSync8(skillsTarget)) {
2762
+ existingSkills.add(item);
2763
+ }
2764
+ }
2765
+ if (!existsSync15(SKILLS_DIR)) return { added, skipped };
2766
+ const panopticonSkills = readdirSync8(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2767
+ for (const skill of panopticonSkills) {
2768
+ const targetPath = join13(skillsTarget, skill);
2769
+ const sourcePath = join13(SKILLS_DIR, skill);
2770
+ if (existingSkills.has(skill)) {
2771
+ const origin = detectContentOrigin(targetPath, workspacePath);
2772
+ if (origin === "git-tracked") {
2773
+ skipped.push(`${skill} (git-tracked)`);
2774
+ continue;
2775
+ }
2776
+ if (origin === "panopticon") {
2777
+ continue;
2778
+ }
2779
+ }
2780
+ try {
2781
+ symlinkSync(sourcePath, targetPath);
2782
+ added.push(skill);
2783
+ } catch (error) {
2784
+ if (error.code !== "EEXIST") {
2785
+ skipped.push(`${skill} (exists)`);
2786
+ }
2787
+ }
2788
+ }
2789
+ if (added.length > 0) {
2790
+ updateGitignore(skillsTarget, added);
2791
+ }
2792
+ return { added, skipped };
2793
+ }
2794
+ function updateGitignore(skillsDir, skills) {
2795
+ const gitignorePath = join13(skillsDir, ".gitignore");
2796
+ const content = `# Panopticon-managed symlinks (not committed)
2797
+ ${skills.join("\n")}
2798
+ `;
2799
+ try {
2800
+ appendFileSync2(gitignorePath, content);
2801
+ } catch {
2802
+ }
2803
+ }
2804
+
2805
+ // src/cli/commands/workspace.ts
2806
+ function registerWorkspaceCommands(program2) {
2807
+ const workspace = program2.command("workspace").description("Workspace management");
2808
+ workspace.command("create <issueId>").description("Create workspace for issue").option("--dry-run", "Show what would be created").option("--no-skills", "Skip skills symlink setup").action(createCommand);
2809
+ workspace.command("list").description("List all workspaces").option("--json", "Output as JSON").action(listCommand2);
2810
+ workspace.command("destroy <issueId>").description("Destroy workspace").option("--force", "Force removal even with uncommitted changes").action(destroyCommand);
2811
+ }
2812
+ async function createCommand(issueId, options) {
2813
+ const spinner = ora10("Creating workspace...").start();
2814
+ try {
2815
+ const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
2816
+ const branchName = `feature/${normalizedId}`;
2817
+ const folderName = `feature-${normalizedId}`;
2818
+ const projectRoot = process.cwd();
2819
+ const workspacesDir = join14(projectRoot, "workspaces");
2820
+ const workspacePath = join14(workspacesDir, folderName);
2821
+ if (options.dryRun) {
2822
+ spinner.info("Dry run mode");
2823
+ console.log("");
2824
+ console.log(chalk19.bold("Would create:"));
2825
+ console.log(` Workspace: ${chalk19.cyan(workspacePath)}`);
2826
+ console.log(` Branch: ${chalk19.cyan(branchName)}`);
2827
+ console.log(` CLAUDE.md: ${chalk19.dim(join14(workspacePath, "CLAUDE.md"))}`);
2828
+ if (options.skills !== false) {
2829
+ console.log(` Skills: ${chalk19.dim(join14(workspacePath, ".claude", "skills"))}`);
2830
+ }
2831
+ return;
2832
+ }
2833
+ if (existsSync16(workspacePath)) {
2834
+ spinner.fail(`Workspace already exists: ${workspacePath}`);
2835
+ process.exit(1);
2836
+ }
2837
+ if (!existsSync16(join14(projectRoot, ".git"))) {
2838
+ spinner.fail("Not a git repository. Run this from the project root.");
2839
+ process.exit(1);
2840
+ }
2841
+ spinner.text = "Creating git worktree...";
2842
+ createWorktree(projectRoot, workspacePath, branchName);
2843
+ spinner.text = "Generating CLAUDE.md...";
2844
+ const variables = {
2845
+ FEATURE_FOLDER: folderName,
2846
+ BRANCH_NAME: branchName,
2847
+ ISSUE_ID: issueId.toUpperCase(),
2848
+ WORKSPACE_PATH: workspacePath,
2849
+ FRONTEND_URL: `https://${folderName}.localhost:3000`,
2850
+ API_URL: `https://api-${folderName}.localhost:8080`
2851
+ };
2852
+ const claudeMd = generateClaudeMd(projectRoot, variables);
2853
+ writeFileSync9(join14(workspacePath, "CLAUDE.md"), claudeMd);
2854
+ let skillsResult = { added: [], skipped: [] };
2855
+ if (options.skills !== false) {
2856
+ spinner.text = "Merging skills...";
2857
+ mkdirSync9(join14(workspacePath, ".claude", "skills"), { recursive: true });
2858
+ skillsResult = mergeSkillsIntoWorkspace(workspacePath);
2859
+ }
2860
+ spinner.succeed("Workspace created!");
2861
+ console.log("");
2862
+ console.log(chalk19.bold("Workspace Details:"));
2863
+ console.log(` Path: ${chalk19.cyan(workspacePath)}`);
2864
+ console.log(` Branch: ${chalk19.dim(branchName)}`);
2865
+ console.log("");
2866
+ if (options.skills !== false) {
2867
+ console.log(chalk19.bold("Skills:"));
2868
+ console.log(` Added: ${skillsResult.added.length} Panopticon skills`);
2869
+ if (skillsResult.skipped.length > 0) {
2870
+ console.log(` Skipped: ${chalk19.dim(skillsResult.skipped.join(", "))}`);
2871
+ }
2872
+ console.log("");
2873
+ }
2874
+ console.log(chalk19.dim(`Next: cd ${workspacePath}`));
2875
+ } catch (error) {
2876
+ spinner.fail(error.message);
2877
+ process.exit(1);
2878
+ }
2879
+ }
2880
+ async function listCommand2(options) {
2881
+ const projectRoot = process.cwd();
2882
+ if (!existsSync16(join14(projectRoot, ".git"))) {
2883
+ console.error(chalk19.red("Not a git repository."));
2884
+ process.exit(1);
2885
+ }
2886
+ const worktrees = listWorktrees(projectRoot);
2887
+ const workspaces = worktrees.filter(
2888
+ (w) => w.path.includes("/workspaces/") || w.path.includes("\\workspaces\\")
2889
+ );
2890
+ if (options.json) {
2891
+ console.log(JSON.stringify(workspaces, null, 2));
2892
+ return;
2893
+ }
2894
+ if (workspaces.length === 0) {
2895
+ console.log(chalk19.dim("No workspaces found."));
2896
+ console.log(chalk19.dim("Create one with: pan workspace create <issue-id>"));
2897
+ return;
2898
+ }
2899
+ console.log(chalk19.bold("\nWorkspaces\n"));
2900
+ for (const ws of workspaces) {
2901
+ const name = basename2(ws.path);
2902
+ const status = ws.prunable ? chalk19.yellow(" (prunable)") : "";
2903
+ console.log(`${chalk19.cyan(name)}${status}`);
2904
+ console.log(` Branch: ${ws.branch || chalk19.dim("(detached)")}`);
2905
+ console.log(` Path: ${chalk19.dim(ws.path)}`);
2906
+ console.log("");
2907
+ }
2908
+ }
2909
+ async function destroyCommand(issueId, options) {
2910
+ const spinner = ora10("Destroying workspace...").start();
2911
+ try {
2912
+ const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
2913
+ const folderName = `feature-${normalizedId}`;
2914
+ const projectRoot = process.cwd();
2915
+ const workspacePath = join14(projectRoot, "workspaces", folderName);
2916
+ if (!existsSync16(workspacePath)) {
2917
+ spinner.fail(`Workspace not found: ${workspacePath}`);
2918
+ process.exit(1);
2919
+ }
2920
+ spinner.text = "Removing git worktree...";
2921
+ removeWorktree(projectRoot, workspacePath);
2922
+ spinner.succeed(`Workspace destroyed: ${folderName}`);
2923
+ } catch (error) {
2924
+ spinner.fail(error.message);
2925
+ if (!options.force) {
2926
+ console.log(chalk19.dim("Tip: Use --force to remove even with uncommitted changes"));
2927
+ }
2928
+ process.exit(1);
2929
+ }
2930
+ }
2931
+
2932
+ // src/cli/commands/install.ts
2933
+ import chalk20 from "chalk";
2934
+ import ora11 from "ora";
2935
+ import { execSync as execSync7 } from "child_process";
2936
+ import { existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10, readFileSync as readFileSync14 } from "fs";
2937
+ import { join as join15 } from "path";
2938
+ import { platform } from "os";
2939
+ function registerInstallCommand(program2) {
2940
+ program2.command("install").description("Install Panopticon prerequisites").option("--check", "Check prerequisites only").option("--minimal", "Skip Traefik and mkcert (use port-based routing)").option("--skip-mkcert", "Skip mkcert/HTTPS setup").option("--skip-docker", "Skip Docker network setup").action(installCommand);
2941
+ }
2942
+ function detectPlatform() {
2943
+ const os = platform();
2944
+ if (os === "linux") {
2945
+ try {
2946
+ const release = readFileSync14("/proc/version", "utf8").toLowerCase();
2947
+ if (release.includes("microsoft") || release.includes("wsl")) {
2948
+ return "wsl";
2949
+ }
2950
+ } catch {
2951
+ }
2952
+ return "linux";
2953
+ }
2954
+ return os;
2955
+ }
2956
+ function checkCommand(cmd) {
2957
+ try {
2958
+ execSync7(`which ${cmd}`, { stdio: "pipe" });
2959
+ return true;
2960
+ } catch {
2961
+ return false;
2962
+ }
2963
+ }
2964
+ function checkPrerequisites() {
2965
+ const results = [];
2966
+ const nodeVersion = process.version;
2967
+ const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0]);
2968
+ results.push({
2969
+ name: "Node.js",
2970
+ passed: nodeMajor >= 18,
2971
+ message: nodeMajor >= 18 ? `v${nodeVersion}` : `v${nodeVersion} (need v18+)`,
2972
+ fix: "Install Node.js 18+ from https://nodejs.org"
2973
+ });
2974
+ const hasGit = checkCommand("git");
2975
+ results.push({
2976
+ name: "Git",
2977
+ passed: hasGit,
2978
+ message: hasGit ? "installed" : "not found",
2979
+ fix: "Install git from your package manager"
2980
+ });
2981
+ const hasDocker = checkCommand("docker");
2982
+ let dockerRunning = false;
2983
+ if (hasDocker) {
2984
+ try {
2985
+ execSync7("docker info", { stdio: "pipe" });
2986
+ dockerRunning = true;
2987
+ } catch {
2988
+ }
2989
+ }
2990
+ results.push({
2991
+ name: "Docker",
2992
+ passed: dockerRunning,
2993
+ message: dockerRunning ? "running" : hasDocker ? "not running" : "not found",
2994
+ fix: hasDocker ? "Start Docker Desktop or docker service" : "Install Docker"
2995
+ });
2996
+ const hasTmux = checkCommand("tmux");
2997
+ results.push({
2998
+ name: "tmux",
2999
+ passed: hasTmux,
3000
+ message: hasTmux ? "installed" : "not found",
3001
+ fix: "apt install tmux / brew install tmux"
3002
+ });
3003
+ const hasMkcert = checkCommand("mkcert");
3004
+ results.push({
3005
+ name: "mkcert",
3006
+ passed: hasMkcert,
3007
+ message: hasMkcert ? "installed" : "not found (optional)",
3008
+ fix: "brew install mkcert / apt install mkcert"
3009
+ });
3010
+ const hasBeads = checkCommand("bd");
3011
+ results.push({
3012
+ name: "Beads CLI (bd)",
3013
+ passed: hasBeads,
3014
+ message: hasBeads ? "installed" : "not found",
3015
+ fix: "cargo install beads-cli"
3016
+ });
3017
+ return {
3018
+ results,
3019
+ allPassed: results.filter((r) => r.name !== "mkcert").every((r) => r.passed)
3020
+ };
3021
+ }
3022
+ function printPrereqStatus(prereqs) {
3023
+ console.log(chalk20.bold("Prerequisites:\n"));
3024
+ for (const result of prereqs.results) {
3025
+ const icon = result.passed ? chalk20.green("\u2713") : chalk20.red("\u2717");
3026
+ const msg = result.passed ? chalk20.dim(result.message) : chalk20.yellow(result.message);
3027
+ console.log(` ${icon} ${result.name}: ${msg}`);
3028
+ if (!result.passed && result.fix) {
3029
+ console.log(` ${chalk20.dim("\u2192 " + result.fix)}`);
3030
+ }
3031
+ }
3032
+ console.log("");
3033
+ }
3034
+ async function installCommand(options) {
3035
+ console.log(chalk20.bold("\nPanopticon Installation\n"));
3036
+ const plat = detectPlatform();
3037
+ console.log(`Platform: ${chalk20.cyan(plat)}
3038
+ `);
3039
+ const prereqs = checkPrerequisites();
3040
+ if (options.check) {
3041
+ printPrereqStatus(prereqs);
3042
+ process.exit(prereqs.allPassed ? 0 : 1);
3043
+ }
3044
+ printPrereqStatus(prereqs);
3045
+ if (!prereqs.allPassed) {
3046
+ console.log(chalk20.red("Fix prerequisites above before continuing."));
3047
+ console.log(chalk20.dim("Tip: Run with --minimal to skip optional components"));
3048
+ process.exit(1);
3049
+ }
3050
+ const spinner = ora11("Initializing Panopticon directories...").start();
3051
+ for (const dir of INIT_DIRS) {
3052
+ mkdirSync10(dir, { recursive: true });
3053
+ }
3054
+ spinner.succeed("Directories initialized");
3055
+ if (!options.skipDocker) {
3056
+ spinner.start("Creating Docker network...");
3057
+ try {
3058
+ execSync7("docker network create panopticon 2>/dev/null || true", { stdio: "pipe" });
3059
+ spinner.succeed("Docker network ready");
3060
+ } catch (error) {
3061
+ spinner.warn("Docker network setup failed (may already exist)");
3062
+ }
3063
+ }
3064
+ if (!options.skipMkcert && !options.minimal) {
3065
+ const hasMkcert = checkCommand("mkcert");
3066
+ if (hasMkcert) {
3067
+ spinner.start("Setting up mkcert CA...");
3068
+ try {
3069
+ execSync7("mkcert -install", { stdio: "pipe" });
3070
+ const certsDir = join15(PANOPTICON_HOME, "certs");
3071
+ mkdirSync10(certsDir, { recursive: true });
3072
+ execSync7(
3073
+ `mkcert -cert-file "${join15(certsDir, "localhost.pem")}" -key-file "${join15(certsDir, "localhost-key.pem")}" localhost "*.localhost" 127.0.0.1 ::1`,
3074
+ { stdio: "pipe" }
3075
+ );
3076
+ spinner.succeed("mkcert certificates generated");
3077
+ } catch (error) {
3078
+ spinner.warn("mkcert setup failed (HTTPS may not work)");
3079
+ }
3080
+ } else {
3081
+ spinner.info("Skipping mkcert (not installed)");
3082
+ }
3083
+ }
3084
+ const configFile = join15(PANOPTICON_HOME, "config.toml");
3085
+ if (!existsSync17(configFile)) {
3086
+ spinner.start("Creating default config...");
3087
+ writeFileSync10(
3088
+ configFile,
3089
+ `# Panopticon configuration
3090
+ [panopticon]
3091
+ version = "1.0.0"
3092
+ default_runtime = "claude"
3093
+
3094
+ [dashboard]
3095
+ port = 3001
3096
+ api_port = 3002
3097
+
3098
+ [sync]
3099
+ auto_sync = true
3100
+ strategy = "symlink"
3101
+
3102
+ [health]
3103
+ ping_timeout = "30s"
3104
+ consecutive_failures = 3
3105
+ `
3106
+ );
3107
+ spinner.succeed("Config created");
3108
+ }
3109
+ console.log("");
3110
+ console.log(chalk20.green.bold("Installation complete!"));
3111
+ console.log("");
3112
+ console.log(chalk20.bold("Next steps:"));
3113
+ console.log(` 1. Run ${chalk20.cyan("pan sync")} to sync skills to ~/.claude/`);
3114
+ console.log(` 2. Run ${chalk20.cyan("pan up")} to start the dashboard`);
3115
+ console.log(` 3. Create a workspace with ${chalk20.cyan("pan workspace create <issue-id>")}`);
3116
+ console.log("");
3117
+ }
3118
+
3119
+ // src/cli/commands/project.ts
3120
+ import chalk21 from "chalk";
3121
+ import { existsSync as existsSync18, readFileSync as readFileSync15, writeFileSync as writeFileSync11, mkdirSync as mkdirSync11 } from "fs";
3122
+ import { join as join16, resolve } from "path";
3123
+ var PROJECTS_FILE = join16(PANOPTICON_HOME, "projects.json");
3124
+ function loadProjects() {
3125
+ if (!existsSync18(PROJECTS_FILE)) {
3126
+ return [];
3127
+ }
3128
+ try {
3129
+ return JSON.parse(readFileSync15(PROJECTS_FILE, "utf-8"));
3130
+ } catch {
3131
+ return [];
3132
+ }
3133
+ }
3134
+ function saveProjects(projects) {
3135
+ mkdirSync11(PANOPTICON_HOME, { recursive: true });
3136
+ writeFileSync11(PROJECTS_FILE, JSON.stringify(projects, null, 2));
3137
+ }
3138
+ async function projectAddCommand(projectPath, options = {}) {
3139
+ const fullPath = resolve(projectPath);
3140
+ if (!existsSync18(fullPath)) {
3141
+ console.log(chalk21.red(`Path does not exist: ${fullPath}`));
3142
+ return;
3143
+ }
3144
+ const projects = loadProjects();
3145
+ const existing = projects.find((p) => p.path === fullPath);
3146
+ if (existing) {
3147
+ console.log(chalk21.yellow(`Project already registered: ${existing.name}`));
3148
+ return;
3149
+ }
3150
+ const name = options.name || fullPath.split("/").pop() || "unknown";
3151
+ let linearTeam = options.linearTeam;
3152
+ if (!linearTeam) {
3153
+ const projectToml = join16(fullPath, ".panopticon", "project.toml");
3154
+ if (existsSync18(projectToml)) {
3155
+ const content = readFileSync15(projectToml, "utf-8");
3156
+ const match = content.match(/team\s*=\s*"([^"]+)"/);
3157
+ if (match) linearTeam = match[1];
3158
+ }
3159
+ }
3160
+ const project2 = {
3161
+ name,
3162
+ path: fullPath,
3163
+ type: options.type || "standalone",
3164
+ linearTeam,
3165
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
3166
+ };
3167
+ projects.push(project2);
3168
+ saveProjects(projects);
3169
+ console.log(chalk21.green(`\u2713 Added project: ${name}`));
3170
+ console.log(chalk21.dim(` Path: ${fullPath}`));
3171
+ if (linearTeam) {
3172
+ console.log(chalk21.dim(` Linear team: ${linearTeam}`));
3173
+ }
3174
+ }
3175
+ async function projectListCommand(options = {}) {
3176
+ const projects = loadProjects();
3177
+ if (projects.length === 0) {
3178
+ console.log(chalk21.dim("No projects registered."));
3179
+ console.log(chalk21.dim("Add one with: pan project add <path>"));
3180
+ return;
3181
+ }
3182
+ if (options.json) {
3183
+ console.log(JSON.stringify(projects, null, 2));
3184
+ return;
3185
+ }
3186
+ console.log(chalk21.bold("\nRegistered Projects:\n"));
3187
+ for (const project2 of projects) {
3188
+ const exists = existsSync18(project2.path);
3189
+ const statusIcon = exists ? chalk21.green("\u2713") : chalk21.red("\u2717");
3190
+ console.log(`${statusIcon} ${chalk21.bold(project2.name)}`);
3191
+ console.log(` ${chalk21.dim(project2.path)}`);
3192
+ if (project2.linearTeam) {
3193
+ console.log(` ${chalk21.cyan(`Linear: ${project2.linearTeam}`)}`);
3194
+ }
3195
+ console.log(` ${chalk21.dim(`Type: ${project2.type}`)}`);
3196
+ console.log("");
3197
+ }
3198
+ }
3199
+ async function projectRemoveCommand(nameOrPath) {
3200
+ const projects = loadProjects();
3201
+ const index = projects.findIndex(
3202
+ (p) => p.name === nameOrPath || p.path === resolve(nameOrPath)
3203
+ );
3204
+ if (index === -1) {
3205
+ console.log(chalk21.red(`Project not found: ${nameOrPath}`));
3206
+ return;
3207
+ }
3208
+ const removed = projects.splice(index, 1)[0];
3209
+ saveProjects(projects);
3210
+ console.log(chalk21.green(`\u2713 Removed project: ${removed.name}`));
3211
+ }
3212
+
3213
+ // src/cli/commands/doctor.ts
3214
+ import chalk22 from "chalk";
3215
+ import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
3216
+ import { execSync as execSync8 } from "child_process";
3217
+ import { homedir as homedir6 } from "os";
3218
+ import { join as join17 } from "path";
3219
+ function checkCommand2(cmd) {
3220
+ try {
3221
+ execSync8(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
3222
+ return true;
3223
+ } catch {
3224
+ return false;
3225
+ }
3226
+ }
3227
+ function checkDirectory(path) {
3228
+ return existsSync19(path);
3229
+ }
3230
+ function countItems(path) {
3231
+ if (!existsSync19(path)) return 0;
3232
+ try {
3233
+ return readdirSync9(path).length;
3234
+ } catch {
3235
+ return 0;
3236
+ }
3237
+ }
3238
+ async function doctorCommand() {
3239
+ console.log(chalk22.bold("\nPanopticon Doctor\n"));
3240
+ console.log(chalk22.dim("Checking system health...\n"));
3241
+ const checks = [];
3242
+ const requiredCommands = [
3243
+ { cmd: "git", name: "Git", fix: "Install git" },
3244
+ { cmd: "tmux", name: "tmux", fix: "Install tmux: apt install tmux / brew install tmux" },
3245
+ { cmd: "node", name: "Node.js", fix: "Install Node.js 18+" },
3246
+ { cmd: "claude", name: "Claude CLI", fix: "Install: npm install -g @anthropic-ai/claude-code" }
3247
+ ];
3248
+ for (const { cmd, name, fix } of requiredCommands) {
3249
+ if (checkCommand2(cmd)) {
3250
+ checks.push({ name, status: "ok", message: "Installed" });
3251
+ } else {
3252
+ checks.push({ name, status: "error", message: "Not found", fix });
3253
+ }
3254
+ }
3255
+ const optionalCommands = [
3256
+ { cmd: "gh", name: "GitHub CLI", fix: "Install: gh auth login" },
3257
+ { cmd: "bd", name: "Beads CLI", fix: "Install beads for task tracking" },
3258
+ { cmd: "docker", name: "Docker", fix: "Install Docker for workspace containers" }
3259
+ ];
3260
+ for (const { cmd, name, fix } of optionalCommands) {
3261
+ if (checkCommand2(cmd)) {
3262
+ checks.push({ name, status: "ok", message: "Installed" });
3263
+ } else {
3264
+ checks.push({ name, status: "warn", message: "Not installed (optional)", fix });
3265
+ }
3266
+ }
3267
+ const directories = [
3268
+ { path: PANOPTICON_HOME, name: "Panopticon Home", fix: "Run: pan init" },
3269
+ { path: SKILLS_DIR, name: "Skills Directory", fix: "Run: pan init" },
3270
+ { path: COMMANDS_DIR, name: "Commands Directory", fix: "Run: pan init" },
3271
+ { path: AGENTS_DIR, name: "Agents Directory", fix: "Run: pan init" }
3272
+ ];
3273
+ for (const { path, name, fix } of directories) {
3274
+ if (checkDirectory(path)) {
3275
+ const count = countItems(path);
3276
+ checks.push({ name, status: "ok", message: `Exists (${count} items)` });
3277
+ } else {
3278
+ checks.push({ name, status: "error", message: "Missing", fix });
3279
+ }
3280
+ }
3281
+ if (checkDirectory(CLAUDE_DIR)) {
3282
+ const skillsCount = countItems(join17(CLAUDE_DIR, "skills"));
3283
+ const commandsCount = countItems(join17(CLAUDE_DIR, "commands"));
3284
+ checks.push({
3285
+ name: "Claude Code Skills",
3286
+ status: skillsCount > 0 ? "ok" : "warn",
3287
+ message: `${skillsCount} skills`,
3288
+ fix: skillsCount === 0 ? "Run: pan sync" : void 0
3289
+ });
3290
+ checks.push({
3291
+ name: "Claude Code Commands",
3292
+ status: commandsCount > 0 ? "ok" : "warn",
3293
+ message: `${commandsCount} commands`,
3294
+ fix: commandsCount === 0 ? "Run: pan sync" : void 0
3295
+ });
3296
+ } else {
3297
+ checks.push({
3298
+ name: "Claude Code Directory",
3299
+ status: "warn",
3300
+ message: "Not found",
3301
+ fix: "Install Claude Code first"
3302
+ });
3303
+ }
3304
+ const envFile = join17(homedir6(), ".panopticon.env");
3305
+ if (existsSync19(envFile)) {
3306
+ checks.push({ name: "Config File", status: "ok", message: "~/.panopticon.env exists" });
3307
+ } else {
3308
+ checks.push({
3309
+ name: "Config File",
3310
+ status: "warn",
3311
+ message: "~/.panopticon.env not found",
3312
+ fix: "Create ~/.panopticon.env with LINEAR_API_KEY=..."
3313
+ });
3314
+ }
3315
+ if (process.env.LINEAR_API_KEY) {
3316
+ checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in environment" });
3317
+ } else if (existsSync19(envFile)) {
3318
+ const content = __require("fs").readFileSync(envFile, "utf-8");
3319
+ if (content.includes("LINEAR_API_KEY")) {
3320
+ checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in config file" });
3321
+ } else {
3322
+ checks.push({
3323
+ name: "LINEAR_API_KEY",
3324
+ status: "warn",
3325
+ message: "Not configured",
3326
+ fix: "Add LINEAR_API_KEY to ~/.panopticon.env"
3327
+ });
3328
+ }
3329
+ } else {
3330
+ checks.push({
3331
+ name: "LINEAR_API_KEY",
3332
+ status: "warn",
3333
+ message: "Not configured",
3334
+ fix: "Set LINEAR_API_KEY environment variable or add to ~/.panopticon.env"
3335
+ });
3336
+ }
3337
+ try {
3338
+ const sessions = execSync8("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
3339
+ const agentSessions = sessions.split("\n").filter((s) => s.includes("agent-")).length;
3340
+ checks.push({
3341
+ name: "Running Agents",
3342
+ status: "ok",
3343
+ message: `${agentSessions} agent sessions`
3344
+ });
3345
+ } catch {
3346
+ checks.push({
3347
+ name: "Running Agents",
3348
+ status: "ok",
3349
+ message: "0 agent sessions"
3350
+ });
3351
+ }
3352
+ const icons = {
3353
+ ok: chalk22.green("\u2713"),
3354
+ warn: chalk22.yellow("\u26A0"),
3355
+ error: chalk22.red("\u2717")
3356
+ };
3357
+ let hasErrors = false;
3358
+ let hasWarnings = false;
3359
+ for (const check of checks) {
3360
+ const icon = icons[check.status];
3361
+ const message = check.status === "error" ? chalk22.red(check.message) : check.status === "warn" ? chalk22.yellow(check.message) : chalk22.dim(check.message);
3362
+ console.log(`${icon} ${check.name}: ${message}`);
3363
+ if (check.fix && check.status !== "ok") {
3364
+ console.log(chalk22.dim(` Fix: ${check.fix}`));
3365
+ }
3366
+ if (check.status === "error") hasErrors = true;
3367
+ if (check.status === "warn") hasWarnings = true;
3368
+ }
3369
+ console.log("");
3370
+ if (hasErrors) {
3371
+ console.log(chalk22.red("Some required components are missing."));
3372
+ console.log(chalk22.dim("Fix the errors above before using Panopticon."));
3373
+ } else if (hasWarnings) {
3374
+ console.log(chalk22.yellow("System is functional with some optional features missing."));
3375
+ } else {
3376
+ console.log(chalk22.green("All systems operational!"));
3377
+ }
3378
+ console.log("");
3379
+ }
3380
+
3381
+ // src/cli/index.ts
3382
+ var program = new Command();
3383
+ program.name("pan").description("Multi-agent orchestration for AI coding assistants").version("0.1.0");
3384
+ program.command("init").description("Initialize Panopticon (~/.panopticon/)").action(initCommand);
3385
+ program.command("sync").description("Sync skills/commands to AI tools").option("--dry-run", "Show what would be synced").option("--force", "Overwrite without prompts").option("--backup-only", "Only create backup").action(syncCommand);
3386
+ program.command("restore [timestamp]").description("Restore from backup").action(restoreCommand);
3387
+ program.command("skills").description("List and manage skills").option("--json", "Output as JSON").action(skillsCommand);
3388
+ registerWorkCommands(program);
3389
+ registerWorkspaceCommands(program);
3390
+ registerInstallCommand(program);
3391
+ program.command("status").description("Show running agents (shorthand for work status)").option("--json", "Output as JSON").action(statusCommand);
3392
+ program.command("up").description("Start dashboard").option("--detach", "Run in background").action(async (options) => {
3393
+ const { spawn, execSync: execSync9 } = await import("child_process");
3394
+ const { join: join18, dirname: dirname2 } = await import("path");
3395
+ const { fileURLToPath } = await import("url");
3396
+ const __dirname2 = dirname2(fileURLToPath(import.meta.url));
3397
+ const dashboardDir = join18(__dirname2, "..", "dashboard");
3398
+ console.log(chalk23.bold("Starting Panopticon dashboard...\n"));
3399
+ if (options.detach) {
3400
+ const child = spawn("npm", ["run", "dev"], {
3401
+ cwd: dashboardDir,
3402
+ detached: true,
3403
+ stdio: "ignore"
3404
+ });
3405
+ child.unref();
3406
+ console.log(chalk23.green("Dashboard started in background"));
3407
+ console.log(`Frontend: ${chalk23.cyan("http://localhost:3001")}`);
3408
+ console.log(`API: ${chalk23.cyan("http://localhost:3002")}`);
3409
+ } else {
3410
+ console.log(`Frontend: ${chalk23.cyan("http://localhost:3001")}`);
3411
+ console.log(`API: ${chalk23.cyan("http://localhost:3002")}`);
3412
+ console.log(chalk23.dim("\nPress Ctrl+C to stop\n"));
3413
+ const child = spawn("npm", ["run", "dev"], {
3414
+ cwd: dashboardDir,
3415
+ stdio: "inherit"
3416
+ });
3417
+ child.on("error", (err) => {
3418
+ console.error(chalk23.red("Failed to start dashboard:"), err.message);
3419
+ process.exit(1);
3420
+ });
3421
+ }
3422
+ });
3423
+ program.command("down").description("Stop dashboard").action(async () => {
3424
+ const { execSync: execSync9 } = await import("child_process");
3425
+ try {
3426
+ execSync9("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
3427
+ execSync9("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
3428
+ console.log(chalk23.green("Dashboard stopped"));
3429
+ } catch {
3430
+ console.log(chalk23.dim("No dashboard processes found"));
3431
+ }
3432
+ });
3433
+ var project = program.command("project").description("Project management");
3434
+ project.command("add <path>").description("Register a project with Panopticon").option("--name <name>", "Project name").option("--type <type>", "Project type (standalone/monorepo)", "standalone").option("--linear-team <team>", "Linear team prefix").action(projectAddCommand);
3435
+ project.command("list").description("List all managed projects").option("--json", "Output as JSON").action(projectListCommand);
3436
+ project.command("remove <nameOrPath>").description("Remove a project from Panopticon").action(projectRemoveCommand);
3437
+ program.command("doctor").description("Check system health and dependencies").action(doctorCommand);
3438
+ program.parse();
3439
+ //# sourceMappingURL=index.js.map