panopticon-cli 0.4.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -16,11 +16,12 @@ import {
16
16
  restoreBackup,
17
17
  saveConfig,
18
18
  syncHooks
19
- } from "../chunk-7BGFIAWQ.js";
19
+ } from "../chunk-ITI4IC5A.js";
20
20
  import {
21
21
  autoRecoverAgents,
22
22
  checkHook,
23
23
  clearHook,
24
+ createSession,
24
25
  detectCrashedAgents,
25
26
  formatCV,
26
27
  generateFixedPointPrompt,
@@ -29,6 +30,7 @@ import {
29
30
  getAgentRankings,
30
31
  getAgentRuntimeState,
31
32
  getAgentState,
33
+ getModelId,
32
34
  killSession,
33
35
  listRunningAgents,
34
36
  messageAgent,
@@ -43,7 +45,7 @@ import {
43
45
  sessionExists,
44
46
  spawnAgent,
45
47
  stopAgent
46
- } from "../chunk-4BIZ4OVN.js";
48
+ } from "../chunk-H45CLB7E.js";
47
49
  import {
48
50
  AGENTS_DIR,
49
51
  CERTS_DIR,
@@ -60,15 +62,17 @@ import {
60
62
  SYNC_TARGETS,
61
63
  TRAEFIK_CERTS_DIR,
62
64
  TRAEFIK_DIR,
63
- isDevMode
64
- } from "../chunk-U4LCHEVU.js";
65
- import {
66
- __require
67
- } from "../chunk-DGUM43GV.js";
65
+ __require,
66
+ isDevMode,
67
+ loadSettings
68
+ } from "../chunk-7HHDVXBM.js";
68
69
 
69
70
  // src/cli/index.ts
71
+ import { readFileSync as readFileSync40, existsSync as existsSync47 } from "fs";
72
+ import { join as join48 } from "path";
73
+ import { homedir as homedir20 } from "os";
70
74
  import { Command } from "commander";
71
- import chalk42 from "chalk";
75
+ import chalk47 from "chalk";
72
76
 
73
77
  // src/cli/commands/init.ts
74
78
  import { existsSync, mkdirSync, readdirSync, cpSync } from "fs";
@@ -181,6 +185,7 @@ async function initCommand() {
181
185
  // src/cli/commands/sync.ts
182
186
  import chalk2 from "chalk";
183
187
  import ora2 from "ora";
188
+ import { execSync } from "child_process";
184
189
  import { existsSync as existsSync3, readdirSync as readdirSync2, statSync, symlinkSync, mkdirSync as mkdirSync3 } from "fs";
185
190
  import { join as join3, dirname as dirname2 } from "path";
186
191
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -208,8 +213,8 @@ function saveProjectsConfig(config2) {
208
213
  if (!existsSync2(dir)) {
209
214
  mkdirSync2(dir, { recursive: true });
210
215
  }
211
- const yaml = stringifyYaml(config2, { indent: 2 });
212
- writeFileSync(PROJECTS_CONFIG_FILE, yaml, "utf-8");
216
+ const yaml2 = stringifyYaml(config2, { indent: 2 });
217
+ writeFileSync(PROJECTS_CONFIG_FILE, yaml2, "utf-8");
213
218
  }
214
219
  function listProjects() {
215
220
  const config2 = loadProjectsConfig();
@@ -333,6 +338,14 @@ projects:
333
338
  var __filename3 = fileURLToPath2(import.meta.url);
334
339
  var __dirname3 = dirname2(__filename3);
335
340
  var BUNDLED_GIT_HOOKS_DIR = join3(__dirname3, "..", "..", "scripts", "git-hooks");
341
+ function checkCommand(cmd) {
342
+ try {
343
+ execSync(`which ${cmd}`, { stdio: "pipe" });
344
+ return true;
345
+ } catch {
346
+ return false;
347
+ }
348
+ }
336
349
  async function syncCommand(options) {
337
350
  const config2 = loadConfig();
338
351
  const targets = config2.sync?.targets;
@@ -442,6 +455,19 @@ async function syncCommand(options) {
442
455
  } else {
443
456
  hooksSpinner.info("No hooks to sync");
444
457
  }
458
+ const hasRouter = checkCommand("claude-code-router");
459
+ if (!hasRouter) {
460
+ const routerSpinner = ora2("Installing claude-code-router...").start();
461
+ try {
462
+ execSync("npm install -g @musistudio/claude-code-router", {
463
+ stdio: "pipe",
464
+ timeout: 12e4
465
+ });
466
+ routerSpinner.succeed("claude-code-router installed");
467
+ } catch (error) {
468
+ routerSpinner.warn("Failed to install claude-code-router - run: npm install -g @musistudio/claude-code-router");
469
+ }
470
+ }
445
471
  const projects = listProjects();
446
472
  if (projects.length > 0 && existsSync3(BUNDLED_GIT_HOOKS_DIR)) {
447
473
  const gitHooksSpinner = ora2("Installing git hooks in registered projects...").start();
@@ -483,9 +509,9 @@ async function syncCommand(options) {
483
509
  if (readlinkSync2(target) === source) continue;
484
510
  } catch {
485
511
  }
486
- const { renameSync } = await import("fs");
512
+ const { renameSync: renameSync2 } = await import("fs");
487
513
  try {
488
- renameSync(target, `${target}.backup`);
514
+ renameSync2(target, `${target}.backup`);
489
515
  } catch {
490
516
  }
491
517
  }
@@ -687,7 +713,7 @@ async function updateLinearToInProgress(apiKey, issueIdentifier) {
687
713
  (s) => s.name === "In Progress" || s.type === "started"
688
714
  );
689
715
  if (!inProgressState) return false;
690
- await issue.update({ stateId: inProgressState.id });
716
+ await client.updateIssue(issue.id, { stateId: inProgressState.id });
691
717
  return true;
692
718
  } catch (error) {
693
719
  return false;
@@ -752,6 +778,31 @@ function readPlanningContext(workspacePath) {
752
778
  }
753
779
  return null;
754
780
  }
781
+ function validateAndCleanStateFile(workspacePath, issueId) {
782
+ const statePath = join5(workspacePath, ".planning", "STATE.md");
783
+ if (!existsSync5(statePath)) {
784
+ return { valid: true, removed: false };
785
+ }
786
+ try {
787
+ const content = readFileSync3(statePath, "utf-8");
788
+ const firstLine = content.split("\n")[0] || "";
789
+ const issueMatch = firstLine.match(/^#\s*([A-Z]+-\d+)/i);
790
+ if (issueMatch) {
791
+ const stateIssueId = issueMatch[1].toUpperCase();
792
+ const currentIssueId = issueId.toUpperCase();
793
+ if (stateIssueId !== currentIssueId) {
794
+ const { unlinkSync: unlinkSync5 } = __require("fs");
795
+ unlinkSync5(statePath);
796
+ console.warn(chalk6.yellow(`\u26A0\uFE0F Removed stale STATE.md (was for ${stateIssueId}, not ${currentIssueId})`));
797
+ console.warn(chalk6.dim(" This can happen when branches are merged. The agent will start fresh."));
798
+ return { valid: false, removed: true, wrongIssue: stateIssueId };
799
+ }
800
+ }
801
+ return { valid: true, removed: false };
802
+ } catch (error) {
803
+ return { valid: true, removed: false };
804
+ }
805
+ }
755
806
  function extractStitchDesigns(stateContent) {
756
807
  if (!stateContent) return null;
757
808
  const stitchPatterns = [
@@ -830,6 +881,33 @@ function readBeadsTasks(workspacePath, projectRoot, issueId) {
830
881
  }
831
882
  return tasks;
832
883
  }
884
+ function buildPolyrepoContext(issueId, workspacePath) {
885
+ const teamPrefix = extractTeamPrefix(issueId);
886
+ const projectConfig = teamPrefix ? findProjectByTeam(teamPrefix) : null;
887
+ if (!projectConfig?.workspace?.type || projectConfig.workspace.type !== "polyrepo" || !projectConfig.workspace.repos) {
888
+ return "";
889
+ }
890
+ const repos = projectConfig.workspace.repos;
891
+ const lines = [
892
+ "## Project Structure (Polyrepo)",
893
+ "",
894
+ "**IMPORTANT:** This project uses a **polyrepo** structure. The workspace root is NOT a git repository.",
895
+ "Each subdirectory is a separate git worktree:",
896
+ "",
897
+ "| Directory | Purpose |",
898
+ "|-----------|---------|"
899
+ ];
900
+ for (const repo of repos) {
901
+ lines.push(`| \`${repo.name}/\` | Git worktree for ${repo.path} |`);
902
+ }
903
+ lines.push("");
904
+ lines.push("**Git operations:**");
905
+ lines.push("- Run `git status`, `git log`, etc. INSIDE the subdirectories (e.g., `cd fe && git status`)");
906
+ lines.push(`- The workspace root (\`${workspacePath}\`) has no \`.git\` directory`);
907
+ lines.push(`- Each subdirectory has its own branch: \`${repos[0]?.branch_prefix || "feature/"}${issueId.toLowerCase()}\``);
908
+ lines.push("");
909
+ return lines.join("\n");
910
+ }
833
911
  function buildAgentPrompt(issueId, workspacePath, projectRoot) {
834
912
  const lines = [
835
913
  `# Working on Issue: ${issueId}`,
@@ -837,6 +915,10 @@ function buildAgentPrompt(issueId, workspacePath, projectRoot) {
837
915
  `**Workspace:** ${workspacePath}`,
838
916
  ""
839
917
  ];
918
+ const polyrepoContext = buildPolyrepoContext(issueId, workspacePath);
919
+ if (polyrepoContext) {
920
+ lines.push(polyrepoContext);
921
+ }
840
922
  const hasStateFile = existsSync5(join5(workspacePath, ".planning", "STATE.md"));
841
923
  const hasClaudeMd = existsSync5(join5(workspacePath, "CLAUDE.md"));
842
924
  const hasProjectClaudeMd = existsSync5(join5(projectRoot, "CLAUDE.md"));
@@ -979,6 +1061,11 @@ async function issueCommand(id, options) {
979
1061
  console.log(` Beads: ${beadsTasks2.length} tasks`);
980
1062
  return;
981
1063
  }
1064
+ spinner.text = "Validating workspace state...";
1065
+ const stateValidation = validateAndCleanStateFile(workspace, id);
1066
+ if (stateValidation.removed) {
1067
+ spinner.warn(`Cleaned stale planning state from ${stateValidation.wrongIssue}`);
1068
+ }
982
1069
  spinner.text = "Building agent prompt with planning context...";
983
1070
  const prompt = buildAgentPrompt(id, workspace, projectRoot);
984
1071
  spinner.text = "Spawning agent...";
@@ -1122,7 +1209,7 @@ import ora5 from "ora";
1122
1209
  import { existsSync as existsSync7, writeFileSync as writeFileSync2, readFileSync as readFileSync5 } from "fs";
1123
1210
  import { join as join7 } from "path";
1124
1211
  import { homedir as homedir2 } from "os";
1125
- import { execSync } from "child_process";
1212
+ import { execSync as execSync2 } from "child_process";
1126
1213
  function getLinearApiKey2() {
1127
1214
  const envFile = join7(homedir2(), ".panopticon.env");
1128
1215
  if (existsSync7(envFile)) {
@@ -1134,7 +1221,7 @@ function getLinearApiKey2() {
1134
1221
  }
1135
1222
  function checkGhCli() {
1136
1223
  try {
1137
- execSync("which gh", { stdio: "pipe" });
1224
+ execSync2("which gh", { stdio: "pipe" });
1138
1225
  return true;
1139
1226
  } catch {
1140
1227
  return false;
@@ -1142,12 +1229,12 @@ function checkGhCli() {
1142
1229
  }
1143
1230
  function findPRForBranch(workspace) {
1144
1231
  try {
1145
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1232
+ const branch = execSync2("git rev-parse --abbrev-ref HEAD", {
1146
1233
  cwd: workspace,
1147
1234
  encoding: "utf-8",
1148
1235
  stdio: ["pipe", "pipe", "pipe"]
1149
1236
  }).trim();
1150
- const prJson = execSync(`gh pr list --head "${branch}" --json number,url --limit 1`, {
1237
+ const prJson = execSync2(`gh pr list --head "${branch}" --json number,url --limit 1`, {
1151
1238
  cwd: workspace,
1152
1239
  encoding: "utf-8",
1153
1240
  stdio: ["pipe", "pipe", "pipe"]
@@ -1163,7 +1250,7 @@ function findPRForBranch(workspace) {
1163
1250
  }
1164
1251
  function mergePR(workspace, prNumber) {
1165
1252
  try {
1166
- execSync(`gh pr merge ${prNumber} --squash`, {
1253
+ execSync2(`gh pr merge ${prNumber} --squash`, {
1167
1254
  cwd: workspace,
1168
1255
  encoding: "utf-8",
1169
1256
  stdio: ["pipe", "pipe", "pipe"]
@@ -1389,6 +1476,65 @@ async function doneCommand(id, options = {}) {
1389
1476
  console.log("");
1390
1477
  console.log(chalk12.dim("Ready for review. When approved, run:"));
1391
1478
  console.log(chalk12.dim(` pan work approve ${issueId}`));
1479
+ console.log("");
1480
+ try {
1481
+ const dashboardUrl = "http://localhost:3011";
1482
+ const http = await import("http");
1483
+ const checkDashboard = () => new Promise((resolve2) => {
1484
+ const req = http.request(`${dashboardUrl}/health`, { method: "GET", timeout: 1e3 }, (res) => {
1485
+ resolve2(res.statusCode === 200);
1486
+ });
1487
+ req.on("error", () => resolve2(false));
1488
+ req.on("timeout", () => {
1489
+ req.destroy();
1490
+ resolve2(false);
1491
+ });
1492
+ req.end();
1493
+ });
1494
+ const dashboardRunning = await checkDashboard();
1495
+ if (dashboardRunning) {
1496
+ console.log(chalk12.dim("Auto-triggering review & test..."));
1497
+ const reviewReq = () => new Promise((resolve2, reject) => {
1498
+ const postData = JSON.stringify({});
1499
+ const req = http.request(
1500
+ `${dashboardUrl}/api/workspaces/${issueId}/review`,
1501
+ { method: "POST", headers: { "Content-Type": "application/json" }, timeout: 5e3 },
1502
+ (res) => {
1503
+ let data = "";
1504
+ res.on("data", (chunk) => data += chunk);
1505
+ res.on("end", () => {
1506
+ try {
1507
+ resolve2(JSON.parse(data));
1508
+ } catch {
1509
+ resolve2({ success: false, error: "Invalid response" });
1510
+ }
1511
+ });
1512
+ }
1513
+ );
1514
+ req.on("error", reject);
1515
+ req.on("timeout", () => {
1516
+ req.destroy();
1517
+ reject(new Error("Timeout"));
1518
+ });
1519
+ req.write(postData);
1520
+ req.end();
1521
+ });
1522
+ const result = await reviewReq();
1523
+ if (result.success) {
1524
+ console.log(chalk12.green(` \u2713 Review & test ${result.queued ? "queued" : "started"} automatically`));
1525
+ } else {
1526
+ console.log(chalk12.yellow(` \u26A0 Auto-review not triggered: ${result.error || result.message || "Unknown error"}`));
1527
+ if (result.alreadyReviewed) {
1528
+ console.log(chalk12.dim(` Manual review needed - click "Review and Test" in dashboard`));
1529
+ }
1530
+ }
1531
+ } else {
1532
+ console.log(chalk12.dim(" Dashboard not running - skipping auto-review"));
1533
+ console.log(chalk12.dim(" Start dashboard with: pan up"));
1534
+ }
1535
+ } catch (error) {
1536
+ console.log(chalk12.dim(` Could not auto-trigger review: ${error.message}`));
1537
+ }
1392
1538
  } catch (error) {
1393
1539
  spinner.fail(error.message);
1394
1540
  process.exit(1);
@@ -3098,8 +3244,8 @@ async function runHealthCheck(config2 = {
3098
3244
  } catch {
3099
3245
  }
3100
3246
  if (existsSync13(AGENTS_DIR)) {
3101
- const { readdirSync: readdirSync16 } = await import("fs");
3102
- const dirs = readdirSync16(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-")).map((d) => d.name);
3247
+ const { readdirSync: readdirSync17 } = await import("fs");
3248
+ const dirs = readdirSync17(AGENTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("agent-")).map((d) => d.name);
3103
3249
  for (const dir of dirs) {
3104
3250
  if (!sessions.includes(dir)) {
3105
3251
  sessions.push(dir);
@@ -3611,8 +3757,8 @@ async function wipeCommand(issueId, options) {
3611
3757
  const projectsYamlPath = join14(homedir7(), ".panopticon", "projects.yaml");
3612
3758
  if (existsSync15(projectsYamlPath)) {
3613
3759
  try {
3614
- const yaml = await import("../js-yaml-DLUPUHNL.js");
3615
- const projectsConfig = yaml.load(readFileSync13(projectsYamlPath, "utf-8"));
3760
+ const yaml2 = await import("js-yaml");
3761
+ const projectsConfig = yaml2.load(readFileSync13(projectsYamlPath, "utf-8"));
3616
3762
  for (const [, config2] of Object.entries(projectsConfig.projects || {})) {
3617
3763
  const projConfig = config2;
3618
3764
  if (projConfig.linear_team?.toUpperCase() === prefix) {
@@ -3698,7 +3844,7 @@ async function wipeCommand(issueId, options) {
3698
3844
  // src/cli/commands/work/index.ts
3699
3845
  function registerWorkCommands(program2) {
3700
3846
  const work = program2.command("work").description("Agent and work management");
3701
- 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);
3847
+ work.command("issue <id>").description("Spawn agent for Linear issue").option("--model <model>", "Model to use (sonnet/opus/haiku/kimi-k2.5/etc) - defaults to settings.json or kimi-k2.5").option("--runtime <runtime>", "AI runtime (claude/codex)", "claude").option("--dry-run", "Show what would be created").action(issueCommand);
3702
3848
  work.command("status").description("Show all running agents").option("--json", "Output as JSON").action(statusCommand);
3703
3849
  work.command("tell <id> <message>").description("Send message to running agent").action(tellCommand);
3704
3850
  work.command("kill <id>").description("Kill an agent").option("--force", "Kill without confirmation").action(killCommand);
@@ -3734,11 +3880,11 @@ import { existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as
3734
3880
  import { join as join18, basename as basename3 } from "path";
3735
3881
 
3736
3882
  // src/lib/worktree.ts
3737
- import { execSync as execSync2 } from "child_process";
3883
+ import { execSync as execSync3 } from "child_process";
3738
3884
  import { mkdirSync as mkdirSync8 } from "fs";
3739
3885
  import { dirname as dirname5 } from "path";
3740
3886
  function listWorktrees(repoPath) {
3741
- const output = execSync2("git worktree list --porcelain", {
3887
+ const output = execSync3("git worktree list --porcelain", {
3742
3888
  cwd: repoPath,
3743
3889
  encoding: "utf8"
3744
3890
  });
@@ -3762,22 +3908,22 @@ function listWorktrees(repoPath) {
3762
3908
  function createWorktree(repoPath, targetPath, branchName) {
3763
3909
  mkdirSync8(dirname5(targetPath), { recursive: true });
3764
3910
  try {
3765
- execSync2(`git show-ref --verify --quiet refs/heads/${branchName}`, {
3911
+ execSync3(`git show-ref --verify --quiet refs/heads/${branchName}`, {
3766
3912
  cwd: repoPath
3767
3913
  });
3768
- execSync2(`git worktree add "${targetPath}" "${branchName}"`, {
3914
+ execSync3(`git worktree add "${targetPath}" "${branchName}"`, {
3769
3915
  cwd: repoPath,
3770
3916
  stdio: "pipe"
3771
3917
  });
3772
3918
  } catch {
3773
- execSync2(`git worktree add -b "${branchName}" "${targetPath}"`, {
3919
+ execSync3(`git worktree add -b "${branchName}" "${targetPath}"`, {
3774
3920
  cwd: repoPath,
3775
3921
  stdio: "pipe"
3776
3922
  });
3777
3923
  }
3778
3924
  }
3779
3925
  function removeWorktree(repoPath, worktreePath) {
3780
- execSync2(`git worktree remove "${worktreePath}" --force`, {
3926
+ execSync3(`git worktree remove "${worktreePath}" --force`, {
3781
3927
  cwd: repoPath,
3782
3928
  stdio: "pipe"
3783
3929
  });
@@ -3852,7 +3998,7 @@ import {
3852
3998
  writeFileSync as writeFileSync8
3853
3999
  } from "fs";
3854
4000
  import { join as join16 } from "path";
3855
- import { execSync as execSync3 } from "child_process";
4001
+ import { execSync as execSync4 } from "child_process";
3856
4002
  function detectContentOrigin(path, repoPath) {
3857
4003
  try {
3858
4004
  const stat = lstatSync(path);
@@ -3863,7 +4009,7 @@ function detectContentOrigin(path, repoPath) {
3863
4009
  }
3864
4010
  }
3865
4011
  try {
3866
- execSync3(`git ls-files --error-unmatch "${path}" 2>/dev/null`, {
4012
+ execSync4(`git ls-files --error-unmatch "${path}" 2>/dev/null`, {
3867
4013
  cwd: repoPath,
3868
4014
  stdio: "pipe"
3869
4015
  });
@@ -5260,12 +5406,12 @@ ${config2.name || key}`));
5260
5406
  // src/cli/commands/install.ts
5261
5407
  import chalk26 from "chalk";
5262
5408
  import ora14 from "ora";
5263
- import { execSync as execSync4 } from "child_process";
5409
+ import { execSync as execSync5 } from "child_process";
5264
5410
  import { existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync12, readFileSync as readFileSync17, copyFileSync as copyFileSync2, readdirSync as readdirSync10, statSync as statSync2 } from "fs";
5265
5411
  import { join as join20 } from "path";
5266
5412
  import { homedir as homedir10, platform } from "os";
5267
5413
  function registerInstallCommand(program2) {
5268
- 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").option("--skip-beads", "Skip beads CLI installation").action(installCommand);
5414
+ 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").option("--skip-beads", "Skip beads CLI installation").option("--skip-router", "Skip claude-code-router installation").action(installCommand);
5269
5415
  }
5270
5416
  function detectPlatform() {
5271
5417
  const os = platform();
@@ -5298,9 +5444,9 @@ function copyDirectoryRecursive(source, dest) {
5298
5444
  }
5299
5445
  }
5300
5446
  }
5301
- function checkCommand(cmd) {
5447
+ function checkCommand2(cmd) {
5302
5448
  try {
5303
- execSync4(`which ${cmd}`, { stdio: "pipe" });
5449
+ execSync5(`which ${cmd}`, { stdio: "pipe" });
5304
5450
  return true;
5305
5451
  } catch {
5306
5452
  return false;
@@ -5316,18 +5462,18 @@ function checkPrerequisites() {
5316
5462
  message: nodeMajor >= 18 ? `v${nodeVersion}` : `v${nodeVersion} (need v18+)`,
5317
5463
  fix: "Install Node.js 18+ from https://nodejs.org"
5318
5464
  });
5319
- const hasGit = checkCommand("git");
5465
+ const hasGit = checkCommand2("git");
5320
5466
  results.push({
5321
5467
  name: "Git",
5322
5468
  passed: hasGit,
5323
5469
  message: hasGit ? "installed" : "not found",
5324
5470
  fix: "Install git from your package manager"
5325
5471
  });
5326
- const hasDocker = checkCommand("docker");
5472
+ const hasDocker = checkCommand2("docker");
5327
5473
  let dockerRunning = false;
5328
5474
  if (hasDocker) {
5329
5475
  try {
5330
- execSync4("docker info", { stdio: "pipe" });
5476
+ execSync5("docker info", { stdio: "pipe" });
5331
5477
  dockerRunning = true;
5332
5478
  } catch {
5333
5479
  }
@@ -5338,25 +5484,25 @@ function checkPrerequisites() {
5338
5484
  message: dockerRunning ? "running" : hasDocker ? "not running" : "not found",
5339
5485
  fix: hasDocker ? "Start Docker Desktop or docker service" : "Install Docker"
5340
5486
  });
5341
- const hasTmux = checkCommand("tmux");
5487
+ const hasTmux = checkCommand2("tmux");
5342
5488
  results.push({
5343
5489
  name: "tmux",
5344
5490
  passed: hasTmux,
5345
5491
  message: hasTmux ? "installed" : "not found",
5346
5492
  fix: "apt install tmux / brew install tmux"
5347
5493
  });
5348
- const hasMkcert = checkCommand("mkcert");
5494
+ const hasMkcert = checkCommand2("mkcert");
5349
5495
  results.push({
5350
5496
  name: "mkcert",
5351
5497
  passed: hasMkcert,
5352
5498
  message: hasMkcert ? "installed" : "not found (optional)",
5353
5499
  fix: "brew install mkcert / apt install mkcert"
5354
5500
  });
5355
- const hasBeads = checkCommand("bd");
5501
+ const hasBeads = checkCommand2("bd");
5356
5502
  let beadsVersion = "";
5357
5503
  if (hasBeads) {
5358
5504
  try {
5359
- const output = execSync4("bd --version", { encoding: "utf-8" }).trim();
5505
+ const output = execSync5("bd --version", { encoding: "utf-8" }).trim();
5360
5506
  const match = output.match(/(\d+\.\d+\.\d+)/);
5361
5507
  beadsVersion = match ? match[1] : "unknown";
5362
5508
  } catch {
@@ -5368,7 +5514,14 @@ function checkPrerequisites() {
5368
5514
  message: hasBeads ? `v${beadsVersion}` : "not found (will auto-install)",
5369
5515
  fix: "curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"
5370
5516
  });
5371
- const hasTtyd = checkCommand("ttyd") || existsSync21(join20(homedir10(), "bin", "ttyd"));
5517
+ const hasRouter = checkCommand2("claude-code-router");
5518
+ results.push({
5519
+ name: "claude-code-router",
5520
+ passed: hasRouter,
5521
+ message: hasRouter ? "installed" : "not found (will auto-install)",
5522
+ fix: "npm install -g @musistudio/claude-code-router"
5523
+ });
5524
+ const hasTtyd = checkCommand2("ttyd") || existsSync21(join20(homedir10(), "bin", "ttyd"));
5372
5525
  results.push({
5373
5526
  name: "ttyd",
5374
5527
  passed: hasTtyd,
@@ -5377,8 +5530,8 @@ function checkPrerequisites() {
5377
5530
  });
5378
5531
  return {
5379
5532
  results,
5380
- // mkcert, ttyd, and beads are optional (will be auto-installed or skipped)
5381
- allPassed: results.filter((r) => r.name !== "mkcert" && r.name !== "ttyd" && r.name !== "Beads CLI (bd)").every((r) => r.passed)
5533
+ // mkcert, ttyd, beads, and claude-code-router are optional (will be auto-installed or skipped)
5534
+ allPassed: results.filter((r) => r.name !== "mkcert" && r.name !== "ttyd" && r.name !== "Beads CLI (bd)" && r.name !== "claude-code-router").every((r) => r.passed)
5382
5535
  };
5383
5536
  }
5384
5537
  function printPrereqStatus(prereqs) {
@@ -5442,23 +5595,23 @@ async function installCommand(options) {
5442
5595
  if (!options.skipDocker) {
5443
5596
  spinner.start("Creating Docker network...");
5444
5597
  try {
5445
- execSync4("docker network create panopticon 2>/dev/null || true", { stdio: "pipe" });
5598
+ execSync5("docker network create panopticon 2>/dev/null || true", { stdio: "pipe" });
5446
5599
  spinner.succeed("Docker network ready");
5447
5600
  } catch (error) {
5448
5601
  spinner.warn("Docker network setup failed (may already exist)");
5449
5602
  }
5450
5603
  }
5451
5604
  if (!options.skipMkcert && !options.minimal) {
5452
- const hasMkcert = checkCommand("mkcert");
5605
+ const hasMkcert = checkCommand2("mkcert");
5453
5606
  if (hasMkcert) {
5454
5607
  spinner.start("Setting up mkcert CA...");
5455
5608
  try {
5456
- execSync4("mkcert -install", { stdio: "pipe" });
5609
+ execSync5("mkcert -install", { stdio: "pipe" });
5457
5610
  spinner.succeed("mkcert CA installed");
5458
5611
  spinner.start("Generating wildcard certificates...");
5459
5612
  const traefikCertFile = join20(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost.pem");
5460
5613
  const traefikKeyFile = join20(TRAEFIK_CERTS_DIR, "_wildcard.pan.localhost-key.pem");
5461
- execSync4(
5614
+ execSync5(
5462
5615
  `mkcert -cert-file "${traefikCertFile}" -key-file "${traefikKeyFile}" "*.pan.localhost" "*.localhost" localhost 127.0.0.1 ::1`,
5463
5616
  { stdio: "pipe" }
5464
5617
  );
@@ -5474,7 +5627,7 @@ async function installCommand(options) {
5474
5627
  spinner.info("Skipping mkcert (not installed)");
5475
5628
  }
5476
5629
  }
5477
- const hasTtyd = checkCommand("ttyd") || existsSync21(join20(homedir10(), "bin", "ttyd"));
5630
+ const hasTtyd = checkCommand2("ttyd") || existsSync21(join20(homedir10(), "bin", "ttyd"));
5478
5631
  if (!hasTtyd) {
5479
5632
  spinner.start("Installing ttyd (web terminal)...");
5480
5633
  try {
@@ -5485,7 +5638,7 @@ async function installCommand(options) {
5485
5638
  let downloadUrl = "";
5486
5639
  if (plat2 === "darwin") {
5487
5640
  try {
5488
- execSync4("brew install ttyd", { stdio: "pipe" });
5641
+ execSync5("brew install ttyd", { stdio: "pipe" });
5489
5642
  spinner.succeed("ttyd installed via Homebrew");
5490
5643
  } catch {
5491
5644
  spinner.warn("ttyd installation failed - install manually: brew install ttyd");
@@ -5493,7 +5646,7 @@ async function installCommand(options) {
5493
5646
  } else {
5494
5647
  downloadUrl = "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64";
5495
5648
  try {
5496
- execSync4(`curl -sL "${downloadUrl}" -o "${ttydPath}" && chmod +x "${ttydPath}"`, {
5649
+ execSync5(`curl -sL "${downloadUrl}" -o "${ttydPath}" && chmod +x "${ttydPath}"`, {
5497
5650
  stdio: "pipe",
5498
5651
  timeout: 6e4
5499
5652
  });
@@ -5511,18 +5664,18 @@ async function installCommand(options) {
5511
5664
  if (options.skipBeads) {
5512
5665
  spinner.info("Skipping beads installation (--skip-beads)");
5513
5666
  } else {
5514
- const hasBeadsNow = checkCommand("bd");
5667
+ const hasBeadsNow = checkCommand2("bd");
5515
5668
  if (!hasBeadsNow) {
5516
5669
  spinner.start("Installing beads CLI (bd)...");
5517
5670
  try {
5518
5671
  const plat2 = detectPlatform();
5519
5672
  if (plat2 === "darwin") {
5520
5673
  try {
5521
- execSync4("brew install steveyegge/beads/bd", { stdio: "pipe", timeout: 12e4 });
5674
+ execSync5("brew install steveyegge/beads/bd", { stdio: "pipe", timeout: 12e4 });
5522
5675
  spinner.succeed("beads installed via Homebrew");
5523
5676
  } catch {
5524
5677
  try {
5525
- execSync4("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
5678
+ execSync5("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
5526
5679
  stdio: "pipe",
5527
5680
  timeout: 12e4
5528
5681
  });
@@ -5533,7 +5686,7 @@ async function installCommand(options) {
5533
5686
  }
5534
5687
  } else {
5535
5688
  try {
5536
- execSync4("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
5689
+ execSync5("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
5537
5690
  stdio: "pipe",
5538
5691
  timeout: 12e4
5539
5692
  });
@@ -5547,7 +5700,7 @@ async function installCommand(options) {
5547
5700
  }
5548
5701
  } else {
5549
5702
  try {
5550
- const output = execSync4("bd --version", { encoding: "utf-8" }).trim();
5703
+ const output = execSync5("bd --version", { encoding: "utf-8" }).trim();
5551
5704
  const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
5552
5705
  if (match) {
5553
5706
  const [, major, minor, patch] = match.map(Number);
@@ -5566,6 +5719,25 @@ async function installCommand(options) {
5566
5719
  }
5567
5720
  }
5568
5721
  }
5722
+ if (options.skipRouter) {
5723
+ spinner.info("Skipping claude-code-router installation (--skip-router)");
5724
+ } else {
5725
+ const hasRouterNow = checkCommand2("claude-code-router");
5726
+ if (!hasRouterNow) {
5727
+ spinner.start("Installing claude-code-router...");
5728
+ try {
5729
+ execSync5("npm install -g @musistudio/claude-code-router", {
5730
+ stdio: "pipe",
5731
+ timeout: 12e4
5732
+ });
5733
+ spinner.succeed("claude-code-router installed via npm");
5734
+ } catch (error) {
5735
+ spinner.warn("claude-code-router installation failed - install manually: npm install -g @musistudio/claude-code-router");
5736
+ }
5737
+ } else {
5738
+ spinner.info("claude-code-router already installed");
5739
+ }
5740
+ }
5569
5741
  if (!options.minimal) {
5570
5742
  spinner.start("Setting up Traefik configuration...");
5571
5743
  try {
@@ -5621,7 +5793,7 @@ async function installCommand(options) {
5621
5793
  console.log(` 4. Access dashboard at ${chalk26.cyan("https://pan.localhost")}`);
5622
5794
  } else {
5623
5795
  console.log(` 2. Run ${chalk26.cyan("pan up")} to start the dashboard`);
5624
- console.log(` 3. Access dashboard at ${chalk26.cyan("http://localhost:3001")}`);
5796
+ console.log(` 3. Access dashboard at ${chalk26.cyan("http://localhost:3011")}`);
5625
5797
  }
5626
5798
  console.log(` ${!options.minimal ? "5" : "4"}. Create a workspace with ${chalk26.cyan("pan workspace create <issue-id>")}`);
5627
5799
  console.log("");
@@ -6111,6 +6283,8 @@ function parseClaudeSession(sessionFile) {
6111
6283
  cacheReadTokens: 0,
6112
6284
  cacheWriteTokens: 0
6113
6285
  };
6286
+ const modelBreakdown = {};
6287
+ let totalCostV2 = 0;
6114
6288
  for (const line of lines) {
6115
6289
  try {
6116
6290
  const msg = JSON.parse(line);
@@ -6126,16 +6300,42 @@ function parseClaudeSession(sessionFile) {
6126
6300
  }
6127
6301
  }
6128
6302
  const usage = msg.message?.usage || msg.usage;
6129
- const model2 = msg.message?.model || msg.model;
6303
+ const modelId = msg.message?.model || msg.model;
6130
6304
  if (usage) {
6131
6305
  totalUsage.inputTokens += usage.input_tokens || 0;
6132
6306
  totalUsage.outputTokens += usage.output_tokens || 0;
6133
6307
  totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens || 0) + (usage.cache_read_input_tokens || 0);
6134
6308
  totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens || 0) + (usage.cache_creation_input_tokens || 0);
6135
6309
  messageCount++;
6310
+ if (modelId) {
6311
+ const { provider: provider2, model: normalizedModel } = normalizeModelName(modelId);
6312
+ const pricing2 = getPricing(provider2, normalizedModel);
6313
+ if (pricing2) {
6314
+ const msgUsage = {
6315
+ inputTokens: usage.input_tokens || 0,
6316
+ outputTokens: usage.output_tokens || 0,
6317
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
6318
+ cacheWriteTokens: usage.cache_creation_input_tokens || 0
6319
+ };
6320
+ const msgCost = calculateCost(msgUsage, pricing2);
6321
+ totalCostV2 += msgCost;
6322
+ if (!modelBreakdown[modelId]) {
6323
+ modelBreakdown[modelId] = {
6324
+ cost: 0,
6325
+ inputTokens: 0,
6326
+ outputTokens: 0,
6327
+ messageCount: 0
6328
+ };
6329
+ }
6330
+ modelBreakdown[modelId].cost += msgCost;
6331
+ modelBreakdown[modelId].inputTokens += msgUsage.inputTokens;
6332
+ modelBreakdown[modelId].outputTokens += msgUsage.outputTokens;
6333
+ modelBreakdown[modelId].messageCount++;
6334
+ }
6335
+ }
6136
6336
  }
6137
- if (model2 && !primaryModel) {
6138
- primaryModel = model2;
6337
+ if (modelId && !primaryModel) {
6338
+ primaryModel = modelId;
6139
6339
  }
6140
6340
  } catch {
6141
6341
  }
@@ -6149,6 +6349,8 @@ function parseClaudeSession(sessionFile) {
6149
6349
  if (!primaryModel) {
6150
6350
  primaryModel = "claude-sonnet-4";
6151
6351
  }
6352
+ const normalizedModels = Object.keys(modelBreakdown).map((id) => normalizeModelName(id).model);
6353
+ const modelDisplay = normalizedModels.length > 0 ? normalizedModels.length > 1 ? normalizedModels.join(" \u2192 ") : normalizedModels[0] : normalizeModelName(primaryModel).model;
6152
6354
  const { provider, model } = normalizeModelName(primaryModel);
6153
6355
  const pricing = getPricing(provider, model);
6154
6356
  const cost = pricing ? calculateCost(totalUsage, pricing) : 0;
@@ -6157,10 +6359,15 @@ function parseClaudeSession(sessionFile) {
6157
6359
  sessionFile,
6158
6360
  startTime: startTime || (/* @__PURE__ */ new Date()).toISOString(),
6159
6361
  endTime: endTime || (/* @__PURE__ */ new Date()).toISOString(),
6160
- model: primaryModel,
6362
+ model: modelDisplay,
6161
6363
  usage: totalUsage,
6162
6364
  cost,
6163
- messageCount
6365
+ // DEPRECATED: First-model pricing
6366
+ cost_v2: totalCostV2 > 0 ? totalCostV2 : void 0,
6367
+ // NEW: Accurate per-message pricing
6368
+ messageCount,
6369
+ modelBreakdown: Object.keys(modelBreakdown).length > 0 ? modelBreakdown : void 0
6370
+ // NEW: Cost breakdown by model
6164
6371
  };
6165
6372
  }
6166
6373
 
@@ -6373,7 +6580,7 @@ async function getSpecialistStatus(name) {
6373
6580
  const sessionId = getSessionId(name);
6374
6581
  const running = await isRunning(name);
6375
6582
  const contextTokens = countContextTokens(name);
6376
- const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-RCAPFJG7.js");
6583
+ const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
6377
6584
  const tmuxSession = getTmuxSessionName(name);
6378
6585
  const runtimeState = getAgentRuntimeState2(tmuxSession);
6379
6586
  let state;
@@ -6433,6 +6640,13 @@ async function initializeSpecialist(name) {
6433
6640
  }
6434
6641
  const tmuxSession = getTmuxSessionName(name);
6435
6642
  const cwd = process.env.HOME || "/home/eltmon";
6643
+ let model = "claude-sonnet-4-5";
6644
+ try {
6645
+ const workTypeId = `specialist-${name}`;
6646
+ model = getModelId(workTypeId);
6647
+ } catch (error) {
6648
+ console.warn(`Warning: Could not resolve model for ${name}, using default model`);
6649
+ }
6436
6650
  const identityPrompt = `You are the ${name} specialist agent for Panopticon.
6437
6651
  Your role: ${name === "merge-agent" ? "Resolve merge conflicts and ensure clean integrations" : name === "review-agent" ? "Review code changes and provide quality feedback" : name === "test-agent" ? "Execute and analyze test results" : "Assist with development tasks"}
6438
6652
 
@@ -6447,7 +6661,7 @@ Say: "I am the ${name} specialist, ready and waiting for tasks."`;
6447
6661
  writeFileSync14(launcherScript, `#!/bin/bash
6448
6662
  cd "${cwd}"
6449
6663
  prompt=$(cat "${promptFile}")
6450
- exec claude --dangerously-skip-permissions "$prompt"
6664
+ exec claude --dangerously-skip-permissions --model ${model} "$prompt"
6451
6665
  `, { mode: 493 });
6452
6666
  await execAsync7(
6453
6667
  `tmux new-session -d -s "${tmuxSession}" "bash '${launcherScript}'"`,
@@ -6565,7 +6779,7 @@ async function wakeSpecialist(name, taskPrompt, options = {}) {
6565
6779
  await execAsync7(`tmux send-keys -t "${tmuxSession}" C-m`, { encoding: "utf-8" });
6566
6780
  }
6567
6781
  recordWake(name, sessionId || void 0);
6568
- const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-RCAPFJG7.js");
6782
+ const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
6569
6783
  saveAgentRuntimeState2(tmuxSession, {
6570
6784
  state: "active",
6571
6785
  lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6666,7 +6880,7 @@ IMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.
6666
6880
  async function wakeSpecialistOrQueue(name, task, options = {}) {
6667
6881
  const { priority = "normal", source = "handoff" } = options;
6668
6882
  const running = await isRunning(name);
6669
- const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-RCAPFJG7.js");
6883
+ const { getAgentRuntimeState: getAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
6670
6884
  const tmuxSession = getTmuxSessionName(name);
6671
6885
  const runtimeState = getAgentRuntimeState2(tmuxSession);
6672
6886
  const idle = runtimeState?.state === "idle" || runtimeState?.state === "suspended";
@@ -6697,10 +6911,11 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
6697
6911
  };
6698
6912
  }
6699
6913
  }
6700
- const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-RCAPFJG7.js");
6914
+ const { saveAgentRuntimeState: saveAgentRuntimeState2 } = await import("../agents-B5NRTVHK.js");
6701
6915
  saveAgentRuntimeState2(tmuxSession, {
6702
6916
  state: "active",
6703
- lastActivity: (/* @__PURE__ */ new Date()).toISOString()
6917
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
6918
+ currentIssue: task.issueId
6704
6919
  });
6705
6920
  console.log(`[specialist] ${name} marked active (preventing concurrent wakes)`);
6706
6921
  try {
@@ -6708,7 +6923,8 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
6708
6923
  if (!wakeResult.success) {
6709
6924
  saveAgentRuntimeState2(tmuxSession, {
6710
6925
  state: "idle",
6711
- lastActivity: (/* @__PURE__ */ new Date()).toISOString()
6926
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
6927
+ currentIssue: void 0
6712
6928
  });
6713
6929
  }
6714
6930
  return {
@@ -6720,7 +6936,8 @@ async function wakeSpecialistOrQueue(name, task, options = {}) {
6720
6936
  } catch (error) {
6721
6937
  saveAgentRuntimeState2(tmuxSession, {
6722
6938
  state: "idle",
6723
- lastActivity: (/* @__PURE__ */ new Date()).toISOString()
6939
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
6940
+ currentIssue: void 0
6724
6941
  });
6725
6942
  const msg = error instanceof Error ? error.message : String(error);
6726
6943
  return {
@@ -8061,7 +8278,7 @@ function getCostSummary() {
8061
8278
  // src/lib/cloister/session-rotation.ts
8062
8279
  import { writeFileSync as writeFileSync20 } from "fs";
8063
8280
  import { join as join34 } from "path";
8064
- import { execSync as execSync5 } from "child_process";
8281
+ import { execSync as execSync6 } from "child_process";
8065
8282
  var SESSION_ROTATION_THRESHOLD = 1e5;
8066
8283
  var DEFAULT_MEMORY_TIERS = {
8067
8284
  recent_summary: 100,
@@ -8083,7 +8300,7 @@ function buildMergeAgentMemory(workingDir, tiers = DEFAULT_MEMORY_TIERS) {
8083
8300
  const merges = [];
8084
8301
  try {
8085
8302
  const totalMerges = Math.max(tiers.recent_summary, tiers.recent_detailed, tiers.recent_full);
8086
- const gitLog = execSync5(
8303
+ const gitLog = execSync6(
8087
8304
  `git log --merges --format="%H|%s|%an|%ad|%D" -n ${totalMerges}`,
8088
8305
  { cwd: workingDir, encoding: "utf-8" }
8089
8306
  );
@@ -8118,13 +8335,13 @@ function buildMergeAgentMemory(workingDir, tiers = DEFAULT_MEMORY_TIERS) {
8118
8335
  if (merge.branch) memory += `- Branch: ${merge.branch}
8119
8336
  `;
8120
8337
  try {
8121
- const files = execSync5(`git show --name-only --format= ${merge.hash}`, {
8338
+ const files = execSync6(`git show --name-only --format= ${merge.hash}`, {
8122
8339
  cwd: workingDir,
8123
8340
  encoding: "utf-8"
8124
8341
  }).trim().split("\n").filter((f) => f);
8125
8342
  memory += `- Files changed: ${files.length}
8126
8343
  `;
8127
- const diff = execSync5(`git show ${merge.hash} --stat`, {
8344
+ const diff = execSync6(`git show ${merge.hash} --stat`, {
8128
8345
  cwd: workingDir,
8129
8346
  encoding: "utf-8",
8130
8347
  maxBuffer: 10 * 1024 * 1024
@@ -8201,7 +8418,7 @@ async function rotateSpecialistSession(specialistName, workingDir) {
8201
8418
  }
8202
8419
  const tmuxSession = getTmuxSessionName(specialistName);
8203
8420
  try {
8204
- execSync5(`tmux kill-session -t "${tmuxSession}"`, { encoding: "utf-8" });
8421
+ execSync6(`tmux kill-session -t "${tmuxSession}"`, { encoding: "utf-8" });
8205
8422
  console.log(`Killed session: ${tmuxSession}`);
8206
8423
  } catch (error) {
8207
8424
  console.log(`Session ${tmuxSession} not found or already killed`);
@@ -8481,6 +8698,17 @@ async function checkAndSuspendIdleAgents() {
8481
8698
  continue;
8482
8699
  }
8483
8700
  const runtimeState = getAgentRuntimeState(agent.id);
8701
+ if (runtimeState && runtimeState.lastActivity) {
8702
+ const state = getAgentState(agent.id);
8703
+ if (state) {
8704
+ const runtimeLastActivity = runtimeState.lastActivity;
8705
+ const stateLastActivity = state.lastActivity;
8706
+ if (!stateLastActivity || new Date(runtimeLastActivity) > new Date(stateLastActivity)) {
8707
+ state.lastActivity = runtimeLastActivity;
8708
+ saveAgentState(state);
8709
+ }
8710
+ }
8711
+ }
8484
8712
  if (!runtimeState || runtimeState.state !== "idle") {
8485
8713
  continue;
8486
8714
  }
@@ -8489,6 +8717,13 @@ async function checkAndSuspendIdleAgents() {
8489
8717
  const idleMinutes = idleMs / (1e3 * 60);
8490
8718
  const isSpecialist = specialistNames.has(agent.id);
8491
8719
  const timeoutMinutes = isSpecialist ? 5 : 10;
8720
+ const isWorkAgent = agent.id.startsWith("agent-") && !isSpecialist;
8721
+ if (isWorkAgent) {
8722
+ const completedFile = join35(getAgentDir(agent.id), "completed");
8723
+ if (existsSync35(completedFile)) {
8724
+ continue;
8725
+ }
8726
+ }
8492
8727
  if (idleMinutes > timeoutMinutes) {
8493
8728
  console.log(`[deacon] Auto-suspending ${agent.id} (idle for ${Math.round(idleMinutes)} minutes)`);
8494
8729
  try {
@@ -8701,7 +8936,7 @@ async function runPatrol() {
8701
8936
  if (nextTask) {
8702
8937
  console.log(`[deacon] Auto-resuming suspended ${specialist.name} for queued task: ${nextTask.payload.issueId}`);
8703
8938
  try {
8704
- const { resumeAgent } = await import("../agents-RCAPFJG7.js");
8939
+ const { resumeAgent } = await import("../agents-B5NRTVHK.js");
8705
8940
  const message = `# Queued Work
8706
8941
 
8707
8942
  Processing queued task: ${nextTask.payload.issueId}`;
@@ -9580,11 +9815,11 @@ function registerCloisterCommands(program2) {
9580
9815
  import chalk30 from "chalk";
9581
9816
  import { readFileSync as readFileSync30, writeFileSync as writeFileSync23, existsSync as existsSync37, mkdirSync as mkdirSync24, copyFileSync as copyFileSync3, chmodSync as chmodSync2 } from "fs";
9582
9817
  import { join as join37 } from "path";
9583
- import { execSync as execSync6 } from "child_process";
9818
+ import { execSync as execSync7 } from "child_process";
9584
9819
  import { homedir as homedir15 } from "os";
9585
9820
  function checkJqInstalled() {
9586
9821
  try {
9587
- execSync6("which jq", { stdio: "pipe" });
9822
+ execSync7("which jq", { stdio: "pipe" });
9588
9823
  return true;
9589
9824
  } catch {
9590
9825
  return false;
@@ -9596,8 +9831,8 @@ function installJq() {
9596
9831
  const platform3 = process.platform;
9597
9832
  if (platform3 === "darwin") {
9598
9833
  try {
9599
- execSync6("brew --version", { stdio: "pipe" });
9600
- execSync6("brew install jq", { stdio: "inherit" });
9834
+ execSync7("brew --version", { stdio: "pipe" });
9835
+ execSync7("brew install jq", { stdio: "inherit" });
9601
9836
  console.log(chalk30.green("\u2713 jq installed via Homebrew"));
9602
9837
  return true;
9603
9838
  } catch {
@@ -9605,14 +9840,14 @@ function installJq() {
9605
9840
  }
9606
9841
  } else if (platform3 === "linux") {
9607
9842
  try {
9608
- execSync6("apt-get --version", { stdio: "pipe" });
9609
- execSync6("sudo apt-get update && sudo apt-get install -y jq", { stdio: "inherit" });
9843
+ execSync7("apt-get --version", { stdio: "pipe" });
9844
+ execSync7("sudo apt-get update && sudo apt-get install -y jq", { stdio: "inherit" });
9610
9845
  console.log(chalk30.green("\u2713 jq installed via apt"));
9611
9846
  return true;
9612
9847
  } catch {
9613
9848
  try {
9614
- execSync6("yum --version", { stdio: "pipe" });
9615
- execSync6("sudo yum install -y jq", { stdio: "inherit" });
9849
+ execSync7("yum --version", { stdio: "pipe" });
9850
+ execSync7("sudo yum install -y jq", { stdio: "inherit" });
9616
9851
  console.log(chalk30.green("\u2713 jq installed via yum"));
9617
9852
  return true;
9618
9853
  } catch {
@@ -10347,7 +10582,7 @@ async function doneCommand2(specialist, issueId, options) {
10347
10582
  }
10348
10583
  break;
10349
10584
  case "merge":
10350
- status.mergeStatus = options.status;
10585
+ status.mergeStatus = options.status === "passed" ? "merged" : "failed";
10351
10586
  if (options.status === "passed") {
10352
10587
  console.log(chalk36.green(`\u2713 Merge completed for ${normalizedIssueId}`));
10353
10588
  status.readyForMerge = false;
@@ -10399,32 +10634,652 @@ function registerSpecialistsCommands(program2) {
10399
10634
  specialists.command("done <type> <issueId>").description("Signal specialist completion (deterministic status update)").requiredOption("--status <status>", "Result status: passed or failed").option("--notes <notes>", "Optional notes about the result").action(doneCommand2);
10400
10635
  }
10401
10636
 
10402
- // src/cli/commands/project.ts
10637
+ // src/cli/commands/convoy/start.ts
10403
10638
  import chalk37 from "chalk";
10404
- import { existsSync as existsSync41, readFileSync as readFileSync33, symlinkSync as symlinkSync4, mkdirSync as mkdirSync26, readdirSync as readdirSync14, statSync as statSync5 } from "fs";
10405
- import { join as join41, resolve, dirname as dirname9 } from "path";
10639
+ import ora15 from "ora";
10640
+
10641
+ // src/lib/convoy.ts
10642
+ import { existsSync as existsSync41, mkdirSync as mkdirSync26, writeFileSync as writeFileSync27, readFileSync as readFileSync33, readdirSync as readdirSync14 } from "fs";
10643
+ import { join as join41 } from "path";
10644
+ import { homedir as homedir17 } from "os";
10645
+ import { exec as exec13 } from "child_process";
10646
+ import { promisify as promisify13 } from "util";
10647
+ import { parse as parseYaml2 } from "yaml";
10648
+
10649
+ // src/lib/convoy-templates.ts
10650
+ var CODE_REVIEW_TEMPLATE = {
10651
+ name: "code-review",
10652
+ description: "Parallel code review with automatic synthesis",
10653
+ agents: [
10654
+ {
10655
+ role: "correctness",
10656
+ subagent: "code-review-correctness",
10657
+ parallel: true
10658
+ },
10659
+ {
10660
+ role: "security",
10661
+ subagent: "code-review-security",
10662
+ parallel: true
10663
+ },
10664
+ {
10665
+ role: "performance",
10666
+ subagent: "code-review-performance",
10667
+ parallel: true
10668
+ },
10669
+ {
10670
+ role: "synthesis",
10671
+ subagent: "code-review-synthesis",
10672
+ parallel: false,
10673
+ dependsOn: ["correctness", "security", "performance"]
10674
+ }
10675
+ ],
10676
+ config: {
10677
+ outputDir: ".claude/reviews",
10678
+ maxParallel: 3,
10679
+ // Limit parallel reviewers
10680
+ timeout: 6e5
10681
+ // 10 minutes per agent
10682
+ }
10683
+ };
10684
+ var PLANNING_TEMPLATE = {
10685
+ name: "planning",
10686
+ description: "Codebase exploration and planning",
10687
+ agents: [
10688
+ {
10689
+ role: "planner",
10690
+ subagent: "planning-agent",
10691
+ parallel: false
10692
+ }
10693
+ ],
10694
+ config: {
10695
+ outputDir: ".claude/planning",
10696
+ timeout: 9e5
10697
+ // 15 minutes
10698
+ }
10699
+ };
10700
+ var TRIAGE_TEMPLATE = {
10701
+ name: "triage",
10702
+ description: "Parallel issue triage and categorization",
10703
+ agents: [
10704
+ // Agents are dynamically added based on issues to triage
10705
+ // This is a placeholder template; actual agents created at runtime
10706
+ ],
10707
+ config: {
10708
+ outputDir: ".panopticon/triage",
10709
+ maxParallel: 5
10710
+ // Limit concurrent triage agents
10711
+ }
10712
+ };
10713
+ var HEALTH_MONITOR_TEMPLATE = {
10714
+ name: "health-monitor",
10715
+ description: "Monitor health of running agents",
10716
+ agents: [
10717
+ {
10718
+ role: "monitor",
10719
+ subagent: "health-monitor",
10720
+ parallel: false
10721
+ }
10722
+ ],
10723
+ config: {
10724
+ outputDir: ".panopticon/health"
10725
+ }
10726
+ };
10727
+ var CONVOY_TEMPLATES = {
10728
+ "code-review": CODE_REVIEW_TEMPLATE,
10729
+ "planning": PLANNING_TEMPLATE,
10730
+ "triage": TRIAGE_TEMPLATE,
10731
+ "health-monitor": HEALTH_MONITOR_TEMPLATE
10732
+ };
10733
+ function getConvoyTemplate(name) {
10734
+ return CONVOY_TEMPLATES[name];
10735
+ }
10736
+ function getExecutionOrder(template) {
10737
+ const agents = [...template.agents];
10738
+ const phases = [];
10739
+ const completed = /* @__PURE__ */ new Set();
10740
+ while (agents.length > 0) {
10741
+ const ready = agents.filter((agent) => {
10742
+ const deps = agent.dependsOn || [];
10743
+ return deps.every((dep) => completed.has(dep));
10744
+ });
10745
+ if (ready.length === 0) {
10746
+ throw new Error("Cannot determine execution order: circular dependency or invalid template");
10747
+ }
10748
+ const parallel = ready.filter((a) => a.parallel);
10749
+ const sequential = ready.filter((a) => !a.parallel);
10750
+ if (parallel.length > 0) {
10751
+ phases.push(parallel);
10752
+ parallel.forEach((a) => completed.add(a.role));
10753
+ agents.splice(agents.indexOf(parallel[0]), parallel.length);
10754
+ }
10755
+ for (const agent of sequential) {
10756
+ phases.push([agent]);
10757
+ completed.add(agent.role);
10758
+ agents.splice(agents.indexOf(agent), 1);
10759
+ }
10760
+ }
10761
+ return phases;
10762
+ }
10763
+
10764
+ // src/lib/convoy.ts
10765
+ var execAsync13 = promisify13(exec13);
10766
+ var CONVOY_DIR = join41(homedir17(), ".panopticon", "convoys");
10767
+ function getConvoyStateFile(convoyId) {
10768
+ return join41(CONVOY_DIR, `${convoyId}.json`);
10769
+ }
10770
+ function getConvoyOutputDir(convoyId, template) {
10771
+ const baseDir = template.config?.outputDir || ".panopticon/convoy-output";
10772
+ return join41(process.cwd(), baseDir, convoyId);
10773
+ }
10774
+ function saveConvoyState(state) {
10775
+ mkdirSync26(CONVOY_DIR, { recursive: true });
10776
+ writeFileSync27(getConvoyStateFile(state.id), JSON.stringify(state, null, 2));
10777
+ }
10778
+ function loadConvoyState(convoyId) {
10779
+ const stateFile = getConvoyStateFile(convoyId);
10780
+ if (!existsSync41(stateFile)) {
10781
+ return void 0;
10782
+ }
10783
+ try {
10784
+ const content = readFileSync33(stateFile, "utf-8");
10785
+ return JSON.parse(content);
10786
+ } catch {
10787
+ return void 0;
10788
+ }
10789
+ }
10790
+ function getConvoyStatus(convoyId) {
10791
+ return loadConvoyState(convoyId);
10792
+ }
10793
+ function listConvoys(filter) {
10794
+ if (!existsSync41(CONVOY_DIR)) {
10795
+ return [];
10796
+ }
10797
+ const files = readdirSync14(CONVOY_DIR).filter((f) => f.endsWith(".json"));
10798
+ const convoys = [];
10799
+ for (const file of files) {
10800
+ const convoyId = file.replace(".json", "");
10801
+ const state = loadConvoyState(convoyId);
10802
+ if (state) {
10803
+ if (!filter?.status || state.status === filter.status) {
10804
+ convoys.push(state);
10805
+ }
10806
+ }
10807
+ }
10808
+ return convoys.sort(
10809
+ (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
10810
+ );
10811
+ }
10812
+ function parseAgentTemplate(templatePath) {
10813
+ if (!existsSync41(templatePath)) {
10814
+ throw new Error(`Agent template not found: ${templatePath}`);
10815
+ }
10816
+ const content = readFileSync33(templatePath, "utf-8");
10817
+ const frontmatterMatch = content.match(/^---\n([\s\S]+?)\n---\n([\s\S]*)$/);
10818
+ if (!frontmatterMatch) {
10819
+ throw new Error(`Invalid agent template format (missing frontmatter): ${templatePath}`);
10820
+ }
10821
+ const frontmatter = parseYaml2(frontmatterMatch[1]);
10822
+ const promptContent = frontmatterMatch[2].trim();
10823
+ return {
10824
+ name: frontmatter.name || "unknown",
10825
+ description: frontmatter.description || "",
10826
+ model: frontmatter.model || "sonnet",
10827
+ tools: frontmatter.tools || [],
10828
+ content: promptContent
10829
+ };
10830
+ }
10831
+ function mapConvoyRoleToWorkType(role) {
10832
+ const roleMap = {
10833
+ "security": "convoy:security-reviewer",
10834
+ "performance": "convoy:performance-reviewer",
10835
+ "correctness": "convoy:correctness-reviewer",
10836
+ "synthesis": "convoy:synthesis-agent"
10837
+ };
10838
+ return roleMap[role] || null;
10839
+ }
10840
+ async function spawnConvoyAgent(convoy, agent, agentState, context) {
10841
+ const { role, subagent } = agent;
10842
+ const templatePath = join41(AGENTS_DIR, `${subagent}.md`);
10843
+ const template = parseAgentTemplate(templatePath);
10844
+ let model = template.model;
10845
+ try {
10846
+ const workTypeId = mapConvoyRoleToWorkType(role);
10847
+ if (workTypeId) {
10848
+ model = getModelId(workTypeId);
10849
+ }
10850
+ } catch (error) {
10851
+ console.warn(`Warning: Could not resolve model for convoy role ${role}, using template default`);
10852
+ }
10853
+ const agentContext = {
10854
+ ...context,
10855
+ convoy: {
10856
+ id: convoy.id,
10857
+ template: convoy.template,
10858
+ role,
10859
+ outputDir: convoy.outputDir
10860
+ }
10861
+ };
10862
+ let prompt = template.content;
10863
+ const contextInstructions = `
10864
+ # Convoy Context
10865
+
10866
+ You are part of a convoy: **${convoy.template}**
10867
+ Your role: **${role}**
10868
+
10869
+ **Output Directory**: ${convoy.outputDir}
10870
+ **Output File**: ${agentState.outputFile || "Not specified"}
10871
+
10872
+ ${context.files ? `**Files to review**: ${context.files.join(", ")}` : ""}
10873
+ ${context.prUrl ? `**Pull Request**: ${context.prUrl}` : ""}
10874
+ ${context.issueId ? `**Issue ID**: ${context.issueId}` : ""}
10875
+
10876
+ ---
10877
+
10878
+ `;
10879
+ prompt = contextInstructions + prompt;
10880
+ mkdirSync26(convoy.outputDir, { recursive: true });
10881
+ const promptFile = join41(convoy.outputDir, `${role}-prompt.md`);
10882
+ writeFileSync27(promptFile, prompt);
10883
+ const claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;
10884
+ createSession(agentState.tmuxSession, convoy.context.projectPath, claudeCmd, {
10885
+ env: {
10886
+ PANOPTICON_CONVOY_ID: convoy.id,
10887
+ PANOPTICON_CONVOY_ROLE: role
10888
+ }
10889
+ });
10890
+ await new Promise((resolve2) => setTimeout(resolve2, 1500));
10891
+ await execAsync13(`tmux load-buffer "${promptFile}"`);
10892
+ await execAsync13(`tmux paste-buffer -t ${agentState.tmuxSession}`);
10893
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
10894
+ await execAsync13(`tmux send-keys -t ${agentState.tmuxSession} Enter`);
10895
+ agentState.status = "running";
10896
+ agentState.startedAt = (/* @__PURE__ */ new Date()).toISOString();
10897
+ saveConvoyState(convoy);
10898
+ }
10899
+ async function startConvoy(templateName, context) {
10900
+ const template = getConvoyTemplate(templateName);
10901
+ if (!template) {
10902
+ throw new Error(`Unknown convoy template: ${templateName}`);
10903
+ }
10904
+ const timestamp = Date.now();
10905
+ const convoyId = `convoy-${templateName}-${timestamp}`;
10906
+ const outputDir = getConvoyOutputDir(convoyId, template);
10907
+ mkdirSync26(outputDir, { recursive: true });
10908
+ const state = {
10909
+ id: convoyId,
10910
+ template: templateName,
10911
+ status: "running",
10912
+ agents: [],
10913
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
10914
+ outputDir,
10915
+ context
10916
+ };
10917
+ for (const agent of template.agents) {
10918
+ const tmuxSession = `${convoyId}-${agent.role}`;
10919
+ const outputFile = join41(outputDir, `${agent.role}.md`);
10920
+ state.agents.push({
10921
+ role: agent.role,
10922
+ subagent: agent.subagent,
10923
+ tmuxSession,
10924
+ status: "pending",
10925
+ outputFile
10926
+ });
10927
+ }
10928
+ saveConvoyState(state);
10929
+ const phases = getExecutionOrder(template);
10930
+ if (phases.length > 0) {
10931
+ await executePhase(state, template, phases[0], context);
10932
+ }
10933
+ startPhaseMonitor(state.id, template, phases, context);
10934
+ return state;
10935
+ }
10936
+ async function executePhase(convoy, template, phaseAgents, context) {
10937
+ const spawnPromises = [];
10938
+ for (const agent of phaseAgents) {
10939
+ const agentState = convoy.agents.find((a) => a.role === agent.role);
10940
+ if (!agentState) {
10941
+ throw new Error(`Agent state not found for role: ${agent.role}`);
10942
+ }
10943
+ const deps = agent.dependsOn || [];
10944
+ const allDepsCompleted = deps.every((depRole) => {
10945
+ const depAgent = convoy.agents.find((a) => a.role === depRole);
10946
+ return depAgent?.status === "completed";
10947
+ });
10948
+ if (!allDepsCompleted) {
10949
+ throw new Error(`Dependencies not met for agent ${agent.role}`);
10950
+ }
10951
+ const agentContext = { ...context };
10952
+ for (const depRole of deps) {
10953
+ const depAgent = convoy.agents.find((a) => a.role === depRole);
10954
+ if (depAgent?.outputFile && existsSync41(depAgent.outputFile)) {
10955
+ agentContext[`${depRole}_output`] = readFileSync33(depAgent.outputFile, "utf-8");
10956
+ }
10957
+ }
10958
+ spawnPromises.push(spawnConvoyAgent(convoy, agent, agentState, agentContext));
10959
+ }
10960
+ await Promise.all(spawnPromises);
10961
+ }
10962
+ function startPhaseMonitor(convoyId, template, phases, context) {
10963
+ const monitorLoop = async () => {
10964
+ let currentPhaseIndex = 1;
10965
+ while (currentPhaseIndex < phases.length) {
10966
+ await new Promise((resolve2) => setTimeout(resolve2, 5e3));
10967
+ const state = loadConvoyState(convoyId);
10968
+ if (!state || state.status !== "running") {
10969
+ break;
10970
+ }
10971
+ const prevPhase = phases[currentPhaseIndex - 1];
10972
+ const allCompleted = prevPhase.every((agent) => {
10973
+ const agentState = state.agents.find((a) => a.role === agent.role);
10974
+ return agentState?.status === "completed" || agentState?.status === "failed";
10975
+ });
10976
+ if (allCompleted) {
10977
+ const anyFailed = prevPhase.some((agent) => {
10978
+ const agentState = state.agents.find((a) => a.role === agent.role);
10979
+ return agentState?.status === "failed";
10980
+ });
10981
+ if (anyFailed) {
10982
+ state.status = "partial";
10983
+ saveConvoyState(state);
10984
+ console.log(`[convoy] Phase ${currentPhaseIndex - 1} had failures. Stopping convoy.`);
10985
+ break;
10986
+ }
10987
+ console.log(`[convoy] Starting phase ${currentPhaseIndex}`);
10988
+ await executePhase(state, template, phases[currentPhaseIndex], context);
10989
+ currentPhaseIndex++;
10990
+ }
10991
+ updateAgentStatuses(state);
10992
+ }
10993
+ const finalState = loadConvoyState(convoyId);
10994
+ if (finalState) {
10995
+ const allDone = finalState.agents.every(
10996
+ (a) => a.status === "completed" || a.status === "failed"
10997
+ );
10998
+ if (allDone) {
10999
+ const anyFailed = finalState.agents.some((a) => a.status === "failed");
11000
+ finalState.status = anyFailed ? "partial" : "completed";
11001
+ finalState.completedAt = (/* @__PURE__ */ new Date()).toISOString();
11002
+ saveConvoyState(finalState);
11003
+ console.log(`[convoy] Convoy ${convoyId} ${finalState.status}`);
11004
+ }
11005
+ }
11006
+ };
11007
+ monitorLoop().catch((err) => {
11008
+ console.error(`[convoy] Monitor error for ${convoyId}:`, err);
11009
+ });
11010
+ }
11011
+ function updateAgentStatuses(convoy) {
11012
+ let updated = false;
11013
+ for (const agent of convoy.agents) {
11014
+ if (agent.status === "running" && !sessionExists(agent.tmuxSession)) {
11015
+ agent.status = "completed";
11016
+ agent.completedAt = (/* @__PURE__ */ new Date()).toISOString();
11017
+ updated = true;
11018
+ if (agent.outputFile && existsSync41(agent.outputFile)) {
11019
+ agent.exitCode = 0;
11020
+ } else {
11021
+ agent.exitCode = 1;
11022
+ agent.status = "failed";
11023
+ }
11024
+ }
11025
+ }
11026
+ if (updated) {
11027
+ saveConvoyState(convoy);
11028
+ }
11029
+ }
11030
+ async function stopConvoy(convoyId) {
11031
+ const state = loadConvoyState(convoyId);
11032
+ if (!state) {
11033
+ throw new Error(`Convoy not found: ${convoyId}`);
11034
+ }
11035
+ for (const agent of state.agents) {
11036
+ if (sessionExists(agent.tmuxSession)) {
11037
+ killSession(agent.tmuxSession);
11038
+ }
11039
+ }
11040
+ state.status = "failed";
11041
+ state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
11042
+ saveConvoyState(state);
11043
+ }
11044
+
11045
+ // src/cli/commands/convoy/start.ts
11046
+ async function startCommand2(templateName, options) {
11047
+ const spinner = ora15("Starting convoy...").start();
11048
+ try {
11049
+ const template = getConvoyTemplate(templateName);
11050
+ if (!template) {
11051
+ spinner.fail(chalk37.red(`Unknown template: ${templateName}`));
11052
+ console.log(chalk37.dim("\nAvailable templates: code-review, planning, triage, health-monitor"));
11053
+ process.exit(1);
11054
+ }
11055
+ const context = {
11056
+ projectPath: options.projectPath || process.cwd()
11057
+ };
11058
+ if (options.files) {
11059
+ context.files = [options.files];
11060
+ }
11061
+ if (options.prUrl) {
11062
+ context.prUrl = options.prUrl;
11063
+ }
11064
+ if (options.issueId) {
11065
+ context.issueId = options.issueId;
11066
+ }
11067
+ spinner.text = `Starting convoy: ${template.name}`;
11068
+ const convoy = await startConvoy(templateName, context);
11069
+ spinner.succeed(chalk37.green("Convoy started"));
11070
+ console.log("");
11071
+ console.log(chalk37.bold("Convoy Details:"));
11072
+ console.log(chalk37.dim(" ID: ") + convoy.id);
11073
+ console.log(chalk37.dim(" Template: ") + convoy.template);
11074
+ console.log(chalk37.dim(" Agents: ") + convoy.agents.length);
11075
+ console.log(chalk37.dim(" Output: ") + convoy.outputDir);
11076
+ console.log("");
11077
+ console.log(chalk37.bold("Agents:"));
11078
+ for (const agent of convoy.agents) {
11079
+ const statusColor = agent.status === "running" ? chalk37.green : chalk37.dim;
11080
+ console.log(` ${statusColor(`\u2022 ${agent.role}`)} (${agent.subagent}) - ${agent.status}`);
11081
+ }
11082
+ console.log("");
11083
+ console.log(chalk37.dim("Monitor status with: ") + chalk37.cyan(`pan convoy status ${convoy.id}`));
11084
+ console.log(chalk37.dim("Attach to agent: ") + chalk37.cyan(`tmux attach -t ${convoy.agents[0]?.tmuxSession}`));
11085
+ } catch (error) {
11086
+ spinner.fail(chalk37.red("Failed to start convoy"));
11087
+ if (error instanceof Error) {
11088
+ console.error(chalk37.red("\n" + error.message));
11089
+ }
11090
+ process.exit(1);
11091
+ }
11092
+ }
11093
+
11094
+ // src/cli/commands/convoy/status.ts
11095
+ import chalk38 from "chalk";
11096
+ function statusCommand3(convoyId, options) {
11097
+ let id = convoyId;
11098
+ if (!id) {
11099
+ const running = listConvoys({ status: "running" });
11100
+ if (running.length === 0) {
11101
+ console.log(chalk38.yellow("No running convoys"));
11102
+ return;
11103
+ }
11104
+ id = running[0].id;
11105
+ }
11106
+ const convoy = getConvoyStatus(id);
11107
+ if (!convoy) {
11108
+ console.error(chalk38.red(`Convoy not found: ${id}`));
11109
+ process.exit(1);
11110
+ }
11111
+ if (options.json) {
11112
+ console.log(JSON.stringify(convoy, null, 2));
11113
+ return;
11114
+ }
11115
+ const statusColor = convoy.status === "completed" ? chalk38.green : convoy.status === "running" ? chalk38.cyan : convoy.status === "failed" ? chalk38.red : chalk38.yellow;
11116
+ console.log("");
11117
+ console.log(chalk38.bold("Convoy Status"));
11118
+ console.log(chalk38.dim("\u2500".repeat(60)));
11119
+ console.log(chalk38.dim(" ID: ") + convoy.id);
11120
+ console.log(chalk38.dim(" Template: ") + convoy.template);
11121
+ console.log(chalk38.dim(" Status: ") + statusColor(convoy.status));
11122
+ console.log(chalk38.dim(" Started: ") + new Date(convoy.startedAt).toLocaleString());
11123
+ if (convoy.completedAt) {
11124
+ console.log(chalk38.dim(" Completed:") + new Date(convoy.completedAt).toLocaleString());
11125
+ }
11126
+ console.log(chalk38.dim(" Output: ") + convoy.outputDir);
11127
+ console.log("");
11128
+ console.log(chalk38.bold("Agents:"));
11129
+ for (const agent of convoy.agents) {
11130
+ const statusColor2 = agent.status === "completed" ? chalk38.green : agent.status === "running" ? chalk38.cyan : agent.status === "failed" ? chalk38.red : chalk38.dim;
11131
+ const statusIcon = agent.status === "completed" ? "\u2713" : agent.status === "running" ? "\u27F3" : agent.status === "failed" ? "\u2717" : "\u25CB";
11132
+ console.log(` ${statusColor2(statusIcon)} ${chalk38.bold(agent.role)} (${agent.subagent})`);
11133
+ console.log(` ${chalk38.dim("Status:")} ${statusColor2(agent.status)}`);
11134
+ console.log(` ${chalk38.dim("Tmux:")} ${agent.tmuxSession}`);
11135
+ if (agent.outputFile) {
11136
+ console.log(` ${chalk38.dim("Output:")} ${agent.outputFile}`);
11137
+ }
11138
+ if (agent.startedAt) {
11139
+ console.log(` ${chalk38.dim("Started:")} ${new Date(agent.startedAt).toLocaleString()}`);
11140
+ }
11141
+ if (agent.completedAt) {
11142
+ console.log(` ${chalk38.dim("Completed:")} ${new Date(agent.completedAt).toLocaleString()}`);
11143
+ }
11144
+ console.log("");
11145
+ }
11146
+ if (convoy.status === "running") {
11147
+ console.log(chalk38.dim("Commands:"));
11148
+ const runningAgent = convoy.agents.find((a) => a.status === "running");
11149
+ if (runningAgent) {
11150
+ console.log(chalk38.dim(" Attach to agent: ") + chalk38.cyan(`tmux attach -t ${runningAgent.tmuxSession}`));
11151
+ }
11152
+ console.log(chalk38.dim(" Stop convoy: ") + chalk38.cyan(`pan convoy stop ${convoy.id}`));
11153
+ }
11154
+ if (convoy.status === "completed") {
11155
+ console.log(chalk38.dim("View outputs in: ") + chalk38.cyan(convoy.outputDir));
11156
+ }
11157
+ console.log("");
11158
+ }
11159
+
11160
+ // src/cli/commands/convoy/list.ts
11161
+ import chalk39 from "chalk";
11162
+ function listCommand4(options) {
11163
+ const filter = options.status ? { status: options.status } : void 0;
11164
+ const convoys = listConvoys(filter);
11165
+ if (convoys.length === 0) {
11166
+ console.log(chalk39.yellow("No convoys found"));
11167
+ return;
11168
+ }
11169
+ if (options.json) {
11170
+ console.log(JSON.stringify(convoys, null, 2));
11171
+ return;
11172
+ }
11173
+ console.log("");
11174
+ console.log(chalk39.bold("Convoys"));
11175
+ console.log(chalk39.dim("\u2500".repeat(80)));
11176
+ console.log("");
11177
+ for (const convoy of convoys) {
11178
+ const statusColor = convoy.status === "completed" ? chalk39.green : convoy.status === "running" ? chalk39.cyan : convoy.status === "failed" ? chalk39.red : chalk39.yellow;
11179
+ const statusIcon = convoy.status === "completed" ? "\u2713" : convoy.status === "running" ? "\u27F3" : convoy.status === "failed" ? "\u2717" : "\u25CB";
11180
+ console.log(chalk39.bold(`${statusColor(statusIcon)} ${convoy.id}`));
11181
+ console.log(` ${chalk39.dim("Template:")} ${convoy.template}`);
11182
+ console.log(` ${chalk39.dim("Status:")} ${statusColor(convoy.status)}`);
11183
+ console.log(` ${chalk39.dim("Started:")} ${new Date(convoy.startedAt).toLocaleString()}`);
11184
+ if (convoy.completedAt) {
11185
+ console.log(` ${chalk39.dim("Completed:")} ${new Date(convoy.completedAt).toLocaleString()}`);
11186
+ }
11187
+ const agentCounts = convoy.agents.reduce((acc, agent) => {
11188
+ acc[agent.status] = (acc[agent.status] || 0) + 1;
11189
+ return acc;
11190
+ }, {});
11191
+ const summary = Object.entries(agentCounts).map(([status, count]) => `${count} ${status}`).join(", ");
11192
+ console.log(` ${chalk39.dim("Agents:")} ${summary}`);
11193
+ console.log("");
11194
+ }
11195
+ console.log(chalk39.dim(`Total: ${convoys.length} convoy(s)`));
11196
+ console.log("");
11197
+ console.log(chalk39.dim("Use ") + chalk39.cyan("pan convoy status <id>") + chalk39.dim(" to see details"));
11198
+ console.log("");
11199
+ }
11200
+
11201
+ // src/cli/commands/convoy/stop.ts
11202
+ import chalk40 from "chalk";
11203
+ import ora16 from "ora";
11204
+ import inquirer4 from "inquirer";
11205
+ async function stopCommand2(convoyId, options) {
11206
+ const convoy = getConvoyStatus(convoyId);
11207
+ if (!convoy) {
11208
+ console.error(chalk40.red(`Convoy not found: ${convoyId}`));
11209
+ process.exit(1);
11210
+ }
11211
+ if (convoy.status !== "running") {
11212
+ console.log(chalk40.yellow(`Convoy is not running (status: ${convoy.status})`));
11213
+ return;
11214
+ }
11215
+ if (!options.force) {
11216
+ const runningAgents = convoy.agents.filter((a) => a.status === "running");
11217
+ console.log("");
11218
+ console.log(chalk40.bold(`Stopping convoy: ${convoy.id}`));
11219
+ console.log(chalk40.dim(` Template: ${convoy.template}`));
11220
+ console.log(chalk40.dim(` Running agents: ${runningAgents.length}`));
11221
+ console.log("");
11222
+ const { confirmed } = await inquirer4.prompt([
11223
+ {
11224
+ type: "confirm",
11225
+ name: "confirmed",
11226
+ message: "Are you sure you want to stop this convoy?",
11227
+ default: false
11228
+ }
11229
+ ]);
11230
+ if (!confirmed) {
11231
+ console.log(chalk40.dim("Cancelled"));
11232
+ return;
11233
+ }
11234
+ }
11235
+ const spinner = ora16("Stopping convoy...").start();
11236
+ try {
11237
+ await stopConvoy(convoyId);
11238
+ spinner.succeed(chalk40.green("Convoy stopped"));
11239
+ } catch (error) {
11240
+ spinner.fail(chalk40.red("Failed to stop convoy"));
11241
+ if (error instanceof Error) {
11242
+ console.error(chalk40.red("\n" + error.message));
11243
+ }
11244
+ process.exit(1);
11245
+ }
11246
+ }
11247
+
11248
+ // src/cli/commands/convoy/index.ts
11249
+ function registerConvoyCommands(program2) {
11250
+ const convoy = program2.command("convoy").description("Multi-agent convoy orchestration");
11251
+ convoy.command("start <template>").description("Start a new convoy").option("--files <pattern>", 'File pattern (e.g., "src/**/*.ts")').option("--pr-url <url>", "Pull request URL").option("--issue-id <id>", "Issue ID").option("--project-path <path>", "Project path (defaults to cwd)").action(startCommand2);
11252
+ convoy.command("status [convoy-id]").description("Show convoy status (current convoy if no ID specified)").option("--json", "Output as JSON").action(statusCommand3);
11253
+ convoy.command("list").description("List all convoys").option("--status <status>", "Filter by status (running|completed|failed|partial)").option("--json", "Output as JSON").action(listCommand4);
11254
+ convoy.command("stop <convoy-id>").description("Stop a running convoy").option("--force", "Skip confirmation").action(stopCommand2);
11255
+ }
11256
+
11257
+ // src/cli/commands/project.ts
11258
+ import chalk41 from "chalk";
11259
+ import { existsSync as existsSync42, readFileSync as readFileSync34, symlinkSync as symlinkSync4, mkdirSync as mkdirSync27, readdirSync as readdirSync15, statSync as statSync5 } from "fs";
11260
+ import { join as join42, resolve, dirname as dirname9 } from "path";
10406
11261
  import { fileURLToPath as fileURLToPath3 } from "url";
10407
11262
  var __filename4 = fileURLToPath3(import.meta.url);
10408
11263
  var __dirname4 = dirname9(__filename4);
10409
- var BUNDLED_HOOKS_DIR = join41(__dirname4, "..", "..", "scripts", "git-hooks");
11264
+ var BUNDLED_HOOKS_DIR = join42(__dirname4, "..", "..", "scripts", "git-hooks");
10410
11265
  function installGitHooks(gitDir) {
10411
- const hooksTarget = join41(gitDir, "hooks");
11266
+ const hooksTarget = join42(gitDir, "hooks");
10412
11267
  let installed = 0;
10413
- if (!existsSync41(hooksTarget)) {
10414
- mkdirSync26(hooksTarget, { recursive: true });
11268
+ if (!existsSync42(hooksTarget)) {
11269
+ mkdirSync27(hooksTarget, { recursive: true });
10415
11270
  }
10416
- if (!existsSync41(BUNDLED_HOOKS_DIR)) {
11271
+ if (!existsSync42(BUNDLED_HOOKS_DIR)) {
10417
11272
  return 0;
10418
11273
  }
10419
11274
  try {
10420
- const hooks = readdirSync14(BUNDLED_HOOKS_DIR).filter((f) => {
10421
- const p = join41(BUNDLED_HOOKS_DIR, f);
10422
- return existsSync41(p) && statSync5(p).isFile();
11275
+ const hooks = readdirSync15(BUNDLED_HOOKS_DIR).filter((f) => {
11276
+ const p = join42(BUNDLED_HOOKS_DIR, f);
11277
+ return existsSync42(p) && statSync5(p).isFile();
10423
11278
  });
10424
11279
  for (const hook of hooks) {
10425
- const source = join41(BUNDLED_HOOKS_DIR, hook);
10426
- const target = join41(hooksTarget, hook);
10427
- if (existsSync41(target)) {
11280
+ const source = join42(BUNDLED_HOOKS_DIR, hook);
11281
+ const target = join42(hooksTarget, hook);
11282
+ if (existsSync42(target)) {
10428
11283
  try {
10429
11284
  const { readlinkSync: readlinkSync2 } = __require("fs");
10430
11285
  if (readlinkSync2(target) === source) {
@@ -10433,9 +11288,9 @@ function installGitHooks(gitDir) {
10433
11288
  } catch {
10434
11289
  }
10435
11290
  }
10436
- if (existsSync41(target)) {
10437
- const { renameSync } = __require("fs");
10438
- renameSync(target, `${target}.backup`);
11291
+ if (existsSync42(target)) {
11292
+ const { renameSync: renameSync2 } = __require("fs");
11293
+ renameSync2(target, `${target}.backup`);
10439
11294
  }
10440
11295
  symlinkSync4(source, target);
10441
11296
  installed++;
@@ -10446,24 +11301,24 @@ function installGitHooks(gitDir) {
10446
11301
  }
10447
11302
  async function projectAddCommand(projectPath, options = {}) {
10448
11303
  const fullPath = resolve(projectPath);
10449
- if (!existsSync41(fullPath)) {
10450
- console.log(chalk37.red(`Path does not exist: ${fullPath}`));
11304
+ if (!existsSync42(fullPath)) {
11305
+ console.log(chalk41.red(`Path does not exist: ${fullPath}`));
10451
11306
  return;
10452
11307
  }
10453
11308
  const name = options.name || fullPath.split("/").pop() || "unknown";
10454
11309
  const key = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
10455
11310
  const existing = getProject(key);
10456
11311
  if (existing) {
10457
- console.log(chalk37.yellow(`Project already registered with key: ${key}`));
10458
- console.log(chalk37.dim(`Existing path: ${existing.path}`));
10459
- console.log(chalk37.dim(`To update, first run: pan project remove ${key}`));
11312
+ console.log(chalk41.yellow(`Project already registered with key: ${key}`));
11313
+ console.log(chalk41.dim(`Existing path: ${existing.path}`));
11314
+ console.log(chalk41.dim(`To update, first run: pan project remove ${key}`));
10460
11315
  return;
10461
11316
  }
10462
11317
  let linearTeam = options.linearTeam;
10463
11318
  if (!linearTeam) {
10464
- const projectToml = join41(fullPath, ".panopticon", "project.toml");
10465
- if (existsSync41(projectToml)) {
10466
- const content = readFileSync33(projectToml, "utf-8");
11319
+ const projectToml = join42(fullPath, ".panopticon", "project.toml");
11320
+ if (existsSync42(projectToml)) {
11321
+ const content = readFileSync34(projectToml, "utf-8");
10467
11322
  const match = content.match(/team\s*=\s*"([^"]+)"/);
10468
11323
  if (match) linearTeam = match[1];
10469
11324
  }
@@ -10476,26 +11331,26 @@ async function projectAddCommand(projectPath, options = {}) {
10476
11331
  projectConfig.linear_team = linearTeam.toUpperCase();
10477
11332
  }
10478
11333
  registerProject(key, projectConfig);
10479
- console.log(chalk37.green(`\u2713 Added project: ${name}`));
10480
- console.log(chalk37.dim(` Key: ${key}`));
10481
- console.log(chalk37.dim(` Path: ${fullPath}`));
11334
+ console.log(chalk41.green(`\u2713 Added project: ${name}`));
11335
+ console.log(chalk41.dim(` Key: ${key}`));
11336
+ console.log(chalk41.dim(` Path: ${fullPath}`));
10482
11337
  if (linearTeam) {
10483
- console.log(chalk37.dim(` Linear team: ${linearTeam}`));
11338
+ console.log(chalk41.dim(` Linear team: ${linearTeam}`));
10484
11339
  }
10485
11340
  console.log("");
10486
- const hasDevcontainer = existsSync41(join41(fullPath, ".devcontainer"));
10487
- const hasInfra = existsSync41(join41(fullPath, "infra"));
10488
- const hasDevcontainerTemplate = existsSync41(join41(fullPath, "infra", ".devcontainer-template")) || existsSync41(join41(fullPath, ".devcontainer-template"));
10489
- const hasRootGit = existsSync41(join41(fullPath, ".git"));
11341
+ const hasDevcontainer = existsSync42(join42(fullPath, ".devcontainer"));
11342
+ const hasInfra = existsSync42(join42(fullPath, "infra"));
11343
+ const hasDevcontainerTemplate = existsSync42(join42(fullPath, "infra", ".devcontainer-template")) || existsSync42(join42(fullPath, ".devcontainer-template"));
11344
+ const hasRootGit = existsSync42(join42(fullPath, ".git"));
10490
11345
  const subRepos = [];
10491
11346
  if (!hasRootGit) {
10492
- const { readdirSync: readdirSync16, statSync: statSync7 } = await import("fs");
11347
+ const { readdirSync: readdirSync17, statSync: statSync7 } = await import("fs");
10493
11348
  try {
10494
- const entries = readdirSync16(fullPath);
11349
+ const entries = readdirSync17(fullPath);
10495
11350
  for (const entry of entries) {
10496
- const entryPath = join41(fullPath, entry);
11351
+ const entryPath = join42(fullPath, entry);
10497
11352
  try {
10498
- if (statSync7(entryPath).isDirectory() && existsSync41(join41(entryPath, ".git"))) {
11353
+ if (statSync7(entryPath).isDirectory() && existsSync42(join42(entryPath, ".git"))) {
10499
11354
  subRepos.push(entry);
10500
11355
  }
10501
11356
  } catch {
@@ -10507,72 +11362,72 @@ async function projectAddCommand(projectPath, options = {}) {
10507
11362
  const isPolyrepo = !hasRootGit && subRepos.length > 0;
10508
11363
  let hooksInstalled = 0;
10509
11364
  if (hasRootGit) {
10510
- hooksInstalled = installGitHooks(join41(fullPath, ".git"));
11365
+ hooksInstalled = installGitHooks(join42(fullPath, ".git"));
10511
11366
  if (hooksInstalled > 0) {
10512
- console.log(chalk37.green(`\u2713 Installed ${hooksInstalled} git hook(s) for branch protection`));
11367
+ console.log(chalk41.green(`\u2713 Installed ${hooksInstalled} git hook(s) for branch protection`));
10513
11368
  }
10514
11369
  } else if (isPolyrepo) {
10515
11370
  for (const repo of subRepos) {
10516
- const count = installGitHooks(join41(fullPath, repo, ".git"));
11371
+ const count = installGitHooks(join42(fullPath, repo, ".git"));
10517
11372
  hooksInstalled += count;
10518
11373
  }
10519
11374
  if (hooksInstalled > 0) {
10520
- console.log(chalk37.green(`\u2713 Installed git hooks in ${subRepos.length} repositories`));
11375
+ console.log(chalk41.green(`\u2713 Installed git hooks in ${subRepos.length} repositories`));
10521
11376
  }
10522
11377
  }
10523
11378
  if (hooksInstalled > 0) {
10524
- console.log(chalk37.dim(" (Prevents agents from checking out branches in main project)"));
11379
+ console.log(chalk41.dim(" (Prevents agents from checking out branches in main project)"));
10525
11380
  console.log("");
10526
11381
  }
10527
- console.log(chalk37.bold("Next Steps:\n"));
11382
+ console.log(chalk41.bold("Next Steps:\n"));
10528
11383
  if (isPolyrepo) {
10529
- console.log(chalk37.yellow.bold("\u26A0\uFE0F POLYREPO DETECTED"));
10530
- console.log(chalk37.yellow(` Found ${subRepos.length} git repositories: ${subRepos.join(", ")}`));
11384
+ console.log(chalk41.yellow.bold("\u26A0\uFE0F POLYREPO DETECTED"));
11385
+ console.log(chalk41.yellow(` Found ${subRepos.length} git repositories: ${subRepos.join(", ")}`));
10531
11386
  console.log("");
10532
- console.log(chalk37.cyan("0. Configure as polyrepo"));
10533
- console.log(chalk37.dim(` Edit ${PROJECTS_CONFIG_FILE} and add:`));
11387
+ console.log(chalk41.cyan("0. Configure as polyrepo"));
11388
+ console.log(chalk41.dim(` Edit ${PROJECTS_CONFIG_FILE} and add:`));
10534
11389
  console.log("");
10535
- console.log(chalk37.dim(" workspace:"));
10536
- console.log(chalk37.dim(" type: polyrepo"));
10537
- console.log(chalk37.dim(" workspaces_dir: workspaces"));
10538
- console.log(chalk37.dim(" default_branch: main"));
10539
- console.log(chalk37.dim(" repos:"));
11390
+ console.log(chalk41.dim(" workspace:"));
11391
+ console.log(chalk41.dim(" type: polyrepo"));
11392
+ console.log(chalk41.dim(" workspaces_dir: workspaces"));
11393
+ console.log(chalk41.dim(" default_branch: main"));
11394
+ console.log(chalk41.dim(" repos:"));
10540
11395
  for (const repo of subRepos) {
10541
- console.log(chalk37.dim(` - name: ${repo}`));
10542
- console.log(chalk37.dim(` path: ${repo}`));
10543
- console.log(chalk37.dim(` branch_prefix: "feature/"`));
11396
+ console.log(chalk41.dim(` - name: ${repo}`));
11397
+ console.log(chalk41.dim(` path: ${repo}`));
11398
+ console.log(chalk41.dim(` branch_prefix: "feature/"`));
10544
11399
  }
10545
11400
  console.log("");
10546
- console.log(chalk37.dim(' See README "Polyrepo Workspace Configuration" for full example.'));
11401
+ console.log(chalk41.dim(' See README "Polyrepo Workspace Configuration" for full example.'));
10547
11402
  console.log("");
10548
11403
  }
10549
- console.log(chalk37.cyan(`${isPolyrepo ? "1" : "1"}. Configure workspace settings`));
10550
- console.log(chalk37.dim(` Edit ${PROJECTS_CONFIG_FILE}`));
10551
- console.log(chalk37.dim(" Add workspace, dns, docker, and service configuration"));
11404
+ console.log(chalk41.cyan(`${isPolyrepo ? "1" : "1"}. Configure workspace settings`));
11405
+ console.log(chalk41.dim(` Edit ${PROJECTS_CONFIG_FILE}`));
11406
+ console.log(chalk41.dim(" Add workspace, dns, docker, and service configuration"));
10552
11407
  console.log("");
10553
11408
  if (!hasDevcontainerTemplate && !hasDevcontainer) {
10554
- console.log(chalk37.cyan("2. Create workspace templates (for Docker-based workspaces)"));
10555
- console.log(chalk37.dim(" Your project needs:"));
10556
- console.log(chalk37.dim(" \u2022 infra/.devcontainer-template/docker-compose.devcontainer.yml.template"));
10557
- console.log(chalk37.dim(" \u2022 infra/.devcontainer-template/Dockerfile"));
10558
- console.log(chalk37.dim(' See README "What Your Project Needs to Provide" section'));
11409
+ console.log(chalk41.cyan("2. Create workspace templates (for Docker-based workspaces)"));
11410
+ console.log(chalk41.dim(" Your project needs:"));
11411
+ console.log(chalk41.dim(" \u2022 infra/.devcontainer-template/docker-compose.devcontainer.yml.template"));
11412
+ console.log(chalk41.dim(" \u2022 infra/.devcontainer-template/Dockerfile"));
11413
+ console.log(chalk41.dim(' See README "What Your Project Needs to Provide" section'));
10559
11414
  console.log("");
10560
11415
  } else {
10561
- console.log(chalk37.green("\u2713 Found existing container templates"));
11416
+ console.log(chalk41.green("\u2713 Found existing container templates"));
10562
11417
  console.log("");
10563
11418
  }
10564
- console.log(chalk37.cyan(`${hasDevcontainerTemplate || hasDevcontainer ? "2" : "3"}. Test workspace creation`));
10565
- console.log(chalk37.dim(" pan workspace create TEST-123"));
10566
- console.log(chalk37.dim(" pan workspace destroy TEST-123"));
11419
+ console.log(chalk41.cyan(`${hasDevcontainerTemplate || hasDevcontainer ? "2" : "3"}. Test workspace creation`));
11420
+ console.log(chalk41.dim(" pan workspace create TEST-123"));
11421
+ console.log(chalk41.dim(" pan workspace destroy TEST-123"));
10567
11422
  console.log("");
10568
- console.log(chalk37.dim("Documentation: https://github.com/eltmon/panopticon#what-your-project-needs-to-provide"));
11423
+ console.log(chalk41.dim("Documentation: https://github.com/eltmon/panopticon#what-your-project-needs-to-provide"));
10569
11424
  }
10570
11425
  async function projectListCommand(options = {}) {
10571
11426
  const projects = listProjects();
10572
11427
  if (projects.length === 0) {
10573
- console.log(chalk37.dim("No projects registered."));
10574
- console.log(chalk37.dim("Add one with: pan project add <path> --linear-team <TEAM>"));
10575
- console.log(chalk37.dim(`Or edit: ${PROJECTS_CONFIG_FILE}`));
11428
+ console.log(chalk41.dim("No projects registered."));
11429
+ console.log(chalk41.dim("Add one with: pan project add <path> --linear-team <TEAM>"));
11430
+ console.log(chalk41.dim(`Or edit: ${PROJECTS_CONFIG_FILE}`));
10576
11431
  return;
10577
11432
  }
10578
11433
  if (options.json) {
@@ -10583,51 +11438,51 @@ async function projectListCommand(options = {}) {
10583
11438
  console.log(JSON.stringify(output, null, 2));
10584
11439
  return;
10585
11440
  }
10586
- console.log(chalk37.bold("\nRegistered Projects:\n"));
11441
+ console.log(chalk41.bold("\nRegistered Projects:\n"));
10587
11442
  for (const { key, config: config2 } of projects) {
10588
- const exists = existsSync41(config2.path);
10589
- const statusIcon = exists ? chalk37.green("\u2713") : chalk37.red("\u2717");
10590
- console.log(`${statusIcon} ${chalk37.bold(config2.name)} ${chalk37.dim(`(${key})`)}`);
10591
- console.log(` ${chalk37.dim(config2.path)}`);
11443
+ const exists = existsSync42(config2.path);
11444
+ const statusIcon = exists ? chalk41.green("\u2713") : chalk41.red("\u2717");
11445
+ console.log(`${statusIcon} ${chalk41.bold(config2.name)} ${chalk41.dim(`(${key})`)}`);
11446
+ console.log(` ${chalk41.dim(config2.path)}`);
10592
11447
  if (config2.linear_team) {
10593
- console.log(` ${chalk37.cyan(`Linear: ${config2.linear_team}`)}`);
11448
+ console.log(` ${chalk41.cyan(`Linear: ${config2.linear_team}`)}`);
10594
11449
  }
10595
11450
  if (config2.issue_routing && config2.issue_routing.length > 0) {
10596
- console.log(` ${chalk37.dim(`Routes: ${config2.issue_routing.length} rules`)}`);
11451
+ console.log(` ${chalk41.dim(`Routes: ${config2.issue_routing.length} rules`)}`);
10597
11452
  }
10598
11453
  console.log("");
10599
11454
  }
10600
- console.log(chalk37.dim(`Config: ${PROJECTS_CONFIG_FILE}`));
11455
+ console.log(chalk41.dim(`Config: ${PROJECTS_CONFIG_FILE}`));
10601
11456
  }
10602
11457
  async function projectRemoveCommand(nameOrPath) {
10603
11458
  const projects = listProjects();
10604
11459
  if (unregisterProject(nameOrPath)) {
10605
- console.log(chalk37.green(`\u2713 Removed project: ${nameOrPath}`));
11460
+ console.log(chalk41.green(`\u2713 Removed project: ${nameOrPath}`));
10606
11461
  return;
10607
11462
  }
10608
11463
  for (const { key, config: config2 } of projects) {
10609
11464
  if (config2.name === nameOrPath || config2.path === resolve(nameOrPath)) {
10610
11465
  unregisterProject(key);
10611
- console.log(chalk37.green(`\u2713 Removed project: ${config2.name}`));
11466
+ console.log(chalk41.green(`\u2713 Removed project: ${config2.name}`));
10612
11467
  return;
10613
11468
  }
10614
11469
  }
10615
- console.log(chalk37.red(`Project not found: ${nameOrPath}`));
10616
- console.log(chalk37.dim(`Use 'pan project list' to see registered projects.`));
11470
+ console.log(chalk41.red(`Project not found: ${nameOrPath}`));
11471
+ console.log(chalk41.dim(`Use 'pan project list' to see registered projects.`));
10617
11472
  }
10618
11473
  async function projectInitCommand() {
10619
- if (existsSync41(PROJECTS_CONFIG_FILE)) {
10620
- console.log(chalk37.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
11474
+ if (existsSync42(PROJECTS_CONFIG_FILE)) {
11475
+ console.log(chalk41.yellow(`Config already exists: ${PROJECTS_CONFIG_FILE}`));
10621
11476
  return;
10622
11477
  }
10623
11478
  initializeProjectsConfig();
10624
- console.log(chalk37.green("\u2713 Projects config initialized"));
11479
+ console.log(chalk41.green("\u2713 Projects config initialized"));
10625
11480
  console.log("");
10626
- console.log(chalk37.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
11481
+ console.log(chalk41.dim(`Edit ${PROJECTS_CONFIG_FILE} to add your projects.`));
10627
11482
  console.log("");
10628
- console.log(chalk37.bold("Quick start:"));
11483
+ console.log(chalk41.bold("Quick start:"));
10629
11484
  console.log(
10630
- chalk37.dim(
11485
+ chalk41.dim(
10631
11486
  ' pan project add /path/to/project --name "My Project" --linear-team MIN'
10632
11487
  )
10633
11488
  );
@@ -10646,13 +11501,13 @@ async function projectShowCommand(keyOrName) {
10646
11501
  }
10647
11502
  }
10648
11503
  if (!found) {
10649
- console.error(chalk37.red(`Project not found: ${keyOrName}`));
10650
- console.log(chalk37.dim(`Use 'pan project list' to see registered projects.`));
11504
+ console.error(chalk41.red(`Project not found: ${keyOrName}`));
11505
+ console.log(chalk41.dim(`Use 'pan project list' to see registered projects.`));
10651
11506
  process.exit(1);
10652
11507
  }
10653
- const pathExists = existsSync41(found.path);
10654
- const pathStatus = pathExists ? chalk37.green("\u2713") : chalk37.red("\u2717");
10655
- console.log(chalk37.bold(`
11508
+ const pathExists = existsSync42(found.path);
11509
+ const pathStatus = pathExists ? chalk41.green("\u2713") : chalk41.red("\u2717");
11510
+ console.log(chalk41.bold(`
10656
11511
  Project: ${foundKey}
10657
11512
  `));
10658
11513
  console.log(` Name: ${found.name}`);
@@ -10661,7 +11516,7 @@ Project: ${foundKey}
10661
11516
  console.log(` Team: ${found.linear_team}`);
10662
11517
  }
10663
11518
  if (found.issue_routing && found.issue_routing.length > 0) {
10664
- console.log("\n " + chalk37.bold("Routing Rules:"));
11519
+ console.log("\n " + chalk41.bold("Routing Rules:"));
10665
11520
  for (const rule of found.issue_routing) {
10666
11521
  if (rule.labels) {
10667
11522
  console.log(` Labels: ${rule.labels.join(", ")}`);
@@ -10676,33 +11531,33 @@ Project: ${foundKey}
10676
11531
  }
10677
11532
 
10678
11533
  // src/cli/commands/doctor.ts
10679
- import chalk38 from "chalk";
10680
- import { existsSync as existsSync42, readdirSync as readdirSync15, readFileSync as readFileSync34 } from "fs";
10681
- import { execSync as execSync7 } from "child_process";
10682
- import { homedir as homedir17 } from "os";
10683
- import { join as join42 } from "path";
10684
- function checkCommand2(cmd) {
11534
+ import chalk42 from "chalk";
11535
+ import { existsSync as existsSync43, readdirSync as readdirSync16, readFileSync as readFileSync35 } from "fs";
11536
+ import { execSync as execSync8 } from "child_process";
11537
+ import { homedir as homedir18 } from "os";
11538
+ import { join as join43 } from "path";
11539
+ function checkCommand3(cmd) {
10685
11540
  try {
10686
- execSync7(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
11541
+ execSync8(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
10687
11542
  return true;
10688
11543
  } catch {
10689
11544
  return false;
10690
11545
  }
10691
11546
  }
10692
11547
  function checkDirectory(path) {
10693
- return existsSync42(path);
11548
+ return existsSync43(path);
10694
11549
  }
10695
11550
  function countItems(path) {
10696
- if (!existsSync42(path)) return 0;
11551
+ if (!existsSync43(path)) return 0;
10697
11552
  try {
10698
- return readdirSync15(path).length;
11553
+ return readdirSync16(path).length;
10699
11554
  } catch {
10700
11555
  return 0;
10701
11556
  }
10702
11557
  }
10703
11558
  async function doctorCommand() {
10704
- console.log(chalk38.bold("\nPanopticon Doctor\n"));
10705
- console.log(chalk38.dim("Checking system health...\n"));
11559
+ console.log(chalk42.bold("\nPanopticon Doctor\n"));
11560
+ console.log(chalk42.dim("Checking system health...\n"));
10706
11561
  const checks = [];
10707
11562
  const requiredCommands = [
10708
11563
  { cmd: "git", name: "Git", fix: "Install git" },
@@ -10711,7 +11566,7 @@ async function doctorCommand() {
10711
11566
  { cmd: "claude", name: "Claude CLI", fix: "Install: npm install -g @anthropic-ai/claude-code" }
10712
11567
  ];
10713
11568
  for (const { cmd, name, fix } of requiredCommands) {
10714
- if (checkCommand2(cmd)) {
11569
+ if (checkCommand3(cmd)) {
10715
11570
  checks.push({ name, status: "ok", message: "Installed" });
10716
11571
  } else {
10717
11572
  checks.push({ name, status: "error", message: "Not found", fix });
@@ -10723,7 +11578,7 @@ async function doctorCommand() {
10723
11578
  { cmd: "docker", name: "Docker", fix: "Install Docker for workspace containers" }
10724
11579
  ];
10725
11580
  for (const { cmd, name, fix } of optionalCommands) {
10726
- if (checkCommand2(cmd)) {
11581
+ if (checkCommand3(cmd)) {
10727
11582
  checks.push({ name, status: "ok", message: "Installed" });
10728
11583
  } else {
10729
11584
  checks.push({ name, status: "warn", message: "Not installed (optional)", fix });
@@ -10744,8 +11599,8 @@ async function doctorCommand() {
10744
11599
  }
10745
11600
  }
10746
11601
  if (checkDirectory(CLAUDE_DIR)) {
10747
- const skillsCount = countItems(join42(CLAUDE_DIR, "skills"));
10748
- const commandsCount = countItems(join42(CLAUDE_DIR, "commands"));
11602
+ const skillsCount = countItems(join43(CLAUDE_DIR, "skills"));
11603
+ const commandsCount = countItems(join43(CLAUDE_DIR, "commands"));
10749
11604
  checks.push({
10750
11605
  name: "Claude Code Skills",
10751
11606
  status: skillsCount > 0 ? "ok" : "warn",
@@ -10766,8 +11621,8 @@ async function doctorCommand() {
10766
11621
  fix: "Install Claude Code first"
10767
11622
  });
10768
11623
  }
10769
- const envFile = join42(homedir17(), ".panopticon.env");
10770
- if (existsSync42(envFile)) {
11624
+ const envFile = join43(homedir18(), ".panopticon.env");
11625
+ if (existsSync43(envFile)) {
10771
11626
  checks.push({ name: "Config File", status: "ok", message: "~/.panopticon.env exists" });
10772
11627
  } else {
10773
11628
  checks.push({
@@ -10779,8 +11634,8 @@ async function doctorCommand() {
10779
11634
  }
10780
11635
  if (process.env.LINEAR_API_KEY) {
10781
11636
  checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in environment" });
10782
- } else if (existsSync42(envFile)) {
10783
- const content = readFileSync34(envFile, "utf-8");
11637
+ } else if (existsSync43(envFile)) {
11638
+ const content = readFileSync35(envFile, "utf-8");
10784
11639
  if (content.includes("LINEAR_API_KEY")) {
10785
11640
  checks.push({ name: "LINEAR_API_KEY", status: "ok", message: "Set in config file" });
10786
11641
  } else {
@@ -10800,7 +11655,7 @@ async function doctorCommand() {
10800
11655
  });
10801
11656
  }
10802
11657
  try {
10803
- const sessions = execSync7("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
11658
+ const sessions = execSync8("tmux list-sessions 2>/dev/null || true", { encoding: "utf-8" });
10804
11659
  const agentSessions = sessions.split("\n").filter((s) => s.includes("agent-")).length;
10805
11660
  checks.push({
10806
11661
  name: "Running Agents",
@@ -10815,46 +11670,46 @@ async function doctorCommand() {
10815
11670
  });
10816
11671
  }
10817
11672
  const icons = {
10818
- ok: chalk38.green("\u2713"),
10819
- warn: chalk38.yellow("\u26A0"),
10820
- error: chalk38.red("\u2717")
11673
+ ok: chalk42.green("\u2713"),
11674
+ warn: chalk42.yellow("\u26A0"),
11675
+ error: chalk42.red("\u2717")
10821
11676
  };
10822
11677
  let hasErrors = false;
10823
11678
  let hasWarnings = false;
10824
11679
  for (const check of checks) {
10825
11680
  const icon = icons[check.status];
10826
- const message = check.status === "error" ? chalk38.red(check.message) : check.status === "warn" ? chalk38.yellow(check.message) : chalk38.dim(check.message);
11681
+ const message = check.status === "error" ? chalk42.red(check.message) : check.status === "warn" ? chalk42.yellow(check.message) : chalk42.dim(check.message);
10827
11682
  console.log(`${icon} ${check.name}: ${message}`);
10828
11683
  if (check.fix && check.status !== "ok") {
10829
- console.log(chalk38.dim(` Fix: ${check.fix}`));
11684
+ console.log(chalk42.dim(` Fix: ${check.fix}`));
10830
11685
  }
10831
11686
  if (check.status === "error") hasErrors = true;
10832
11687
  if (check.status === "warn") hasWarnings = true;
10833
11688
  }
10834
11689
  console.log("");
10835
11690
  if (hasErrors) {
10836
- console.log(chalk38.red("Some required components are missing."));
10837
- console.log(chalk38.dim("Fix the errors above before using Panopticon."));
11691
+ console.log(chalk42.red("Some required components are missing."));
11692
+ console.log(chalk42.dim("Fix the errors above before using Panopticon."));
10838
11693
  } else if (hasWarnings) {
10839
- console.log(chalk38.yellow("System is functional with some optional features missing."));
11694
+ console.log(chalk42.yellow("System is functional with some optional features missing."));
10840
11695
  } else {
10841
- console.log(chalk38.green("All systems operational!"));
11696
+ console.log(chalk42.green("All systems operational!"));
10842
11697
  }
10843
11698
  console.log("");
10844
11699
  }
10845
11700
 
10846
11701
  // src/cli/commands/update.ts
10847
- import { execSync as execSync8 } from "child_process";
10848
- import chalk39 from "chalk";
10849
- import { readFileSync as readFileSync35 } from "fs";
11702
+ import { execSync as execSync9 } from "child_process";
11703
+ import chalk43 from "chalk";
11704
+ import { readFileSync as readFileSync36 } from "fs";
10850
11705
  import { fileURLToPath as fileURLToPath4 } from "url";
10851
- import { dirname as dirname10, join as join43 } from "path";
11706
+ import { dirname as dirname10, join as join44 } from "path";
10852
11707
  function getCurrentVersion() {
10853
11708
  try {
10854
11709
  const __filename5 = fileURLToPath4(import.meta.url);
10855
11710
  const __dirname5 = dirname10(__filename5);
10856
- const pkgPath = join43(__dirname5, "..", "..", "..", "package.json");
10857
- const pkg = JSON.parse(readFileSync35(pkgPath, "utf-8"));
11711
+ const pkgPath = join44(__dirname5, "..", "..", "..", "package.json");
11712
+ const pkg = JSON.parse(readFileSync36(pkgPath, "utf-8"));
10858
11713
  return pkg.version;
10859
11714
  } catch {
10860
11715
  return "unknown";
@@ -10862,7 +11717,7 @@ function getCurrentVersion() {
10862
11717
  }
10863
11718
  async function getLatestVersion() {
10864
11719
  try {
10865
- const result = execSync8("npm view panopticon-cli version", {
11720
+ const result = execSync9("npm view panopticon-cli version", {
10866
11721
  encoding: "utf8",
10867
11722
  stdio: ["pipe", "pipe", "pipe"]
10868
11723
  });
@@ -10887,62 +11742,62 @@ function isNewer(latest, current) {
10887
11742
  return l.patch > c.patch;
10888
11743
  }
10889
11744
  async function updateCommand(options) {
10890
- console.log(chalk39.bold("Panopticon Update\n"));
11745
+ console.log(chalk43.bold("Panopticon Update\n"));
10891
11746
  const currentVersion = getCurrentVersion();
10892
- console.log(`Current version: ${chalk39.cyan(currentVersion)}`);
11747
+ console.log(`Current version: ${chalk43.cyan(currentVersion)}`);
10893
11748
  let latestVersion;
10894
11749
  try {
10895
- console.log(chalk39.dim("Checking npm for latest version..."));
11750
+ console.log(chalk43.dim("Checking npm for latest version..."));
10896
11751
  latestVersion = await getLatestVersion();
10897
- console.log(`Latest version: ${chalk39.cyan(latestVersion)}`);
11752
+ console.log(`Latest version: ${chalk43.cyan(latestVersion)}`);
10898
11753
  } catch (error) {
10899
- console.error(chalk39.red("Failed to check for updates"));
10900
- console.error(chalk39.dim("Make sure you have internet connectivity"));
11754
+ console.error(chalk43.red("Failed to check for updates"));
11755
+ console.error(chalk43.dim("Make sure you have internet connectivity"));
10901
11756
  process.exit(1);
10902
11757
  }
10903
11758
  const needsUpdate = isNewer(latestVersion, currentVersion);
10904
11759
  if (!needsUpdate) {
10905
- console.log(chalk39.green("\n\u2713 You are on the latest version"));
11760
+ console.log(chalk43.green("\n\u2713 You are on the latest version"));
10906
11761
  return;
10907
11762
  }
10908
11763
  console.log(
10909
- chalk39.yellow(`
11764
+ chalk43.yellow(`
10910
11765
  \u2191 Update available: ${currentVersion} \u2192 ${latestVersion}`)
10911
11766
  );
10912
11767
  if (options.check) {
10913
- console.log(chalk39.dim("\nRun `pan update` to install"));
11768
+ console.log(chalk43.dim("\nRun `pan update` to install"));
10914
11769
  return;
10915
11770
  }
10916
- console.log(chalk39.dim("\nUpdating Panopticon..."));
11771
+ console.log(chalk43.dim("\nUpdating Panopticon..."));
10917
11772
  try {
10918
- execSync8("npm install -g panopticon-cli@latest", {
11773
+ execSync9("npm install -g panopticon-cli@latest", {
10919
11774
  stdio: "inherit"
10920
11775
  });
10921
- console.log(chalk39.green(`
11776
+ console.log(chalk43.green(`
10922
11777
  \u2713 Updated to ${latestVersion}`));
10923
11778
  const config2 = loadConfig();
10924
11779
  if (config2.sync.auto_sync) {
10925
- console.log(chalk39.dim("\nRunning auto-sync..."));
11780
+ console.log(chalk43.dim("\nRunning auto-sync..."));
10926
11781
  await syncCommand({});
10927
11782
  }
10928
- console.log(chalk39.dim("\nRestart any running agents to use the new version."));
11783
+ console.log(chalk43.dim("\nRestart any running agents to use the new version."));
10929
11784
  } catch (error) {
10930
- console.error(chalk39.red("\nUpdate failed"));
11785
+ console.error(chalk43.red("\nUpdate failed"));
10931
11786
  console.error(
10932
- chalk39.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
11787
+ chalk43.dim("Try running with sudo: sudo npm install -g panopticon-cli@latest")
10933
11788
  );
10934
11789
  process.exit(1);
10935
11790
  }
10936
11791
  }
10937
11792
 
10938
11793
  // src/cli/commands/db.ts
10939
- import chalk40 from "chalk";
10940
- import ora15 from "ora";
10941
- import { existsSync as existsSync43, readFileSync as readFileSync36, writeFileSync as writeFileSync27, mkdirSync as mkdirSync27, statSync as statSync6 } from "fs";
10942
- import { join as join44, dirname as dirname11 } from "path";
10943
- import { exec as exec13 } from "child_process";
10944
- import { promisify as promisify13 } from "util";
10945
- var execAsync13 = promisify13(exec13);
11794
+ import chalk44 from "chalk";
11795
+ import ora17 from "ora";
11796
+ import { existsSync as existsSync44, readFileSync as readFileSync37, writeFileSync as writeFileSync28, mkdirSync as mkdirSync28, statSync as statSync6 } from "fs";
11797
+ import { join as join45, dirname as dirname11 } from "path";
11798
+ import { exec as exec14 } from "child_process";
11799
+ import { promisify as promisify14 } from "util";
11800
+ var execAsync14 = promisify14(exec14);
10946
11801
  function loadFullProjects() {
10947
11802
  const config2 = loadProjectsConfig();
10948
11803
  const projects = config2.projects;
@@ -10961,12 +11816,12 @@ function registerDbCommands(program2) {
10961
11816
  const db2 = program2.command("db").description("Database seeding and management");
10962
11817
  db2.command("snapshot").description("Create a database snapshot from an external source").option("--project <key>", "Project key (e.g., myn)").option("--output <path>", "Output file path").option("--sanitize", "Run sanitization script after snapshot").action(snapshotCommand);
10963
11818
  db2.command("seed <workspaceOrIssue>").description("Seed a workspace database with the configured seed file").option("--force", "Force reseed even if already initialized").option("--file <path>", "Override seed file path").action(seedCommand);
10964
- db2.command("status").description("Check database status for a workspace").argument("[workspaceOrIssue]", "Workspace folder or issue ID").action(statusCommand3);
11819
+ db2.command("status").description("Check database status for a workspace").argument("[workspaceOrIssue]", "Workspace folder or issue ID").action(statusCommand4);
10965
11820
  db2.command("clean <file>").description("Clean kubectl/stderr garbage from a pg_dump file").option("--output <path>", "Output file (default: overwrite input)").option("--dry-run", "Show what would be cleaned without modifying").action(cleanCommand);
10966
11821
  db2.command("config").description("Show database configuration for a project").argument("[project]", "Project key").action(configCommand);
10967
11822
  }
10968
11823
  async function snapshotCommand(options) {
10969
- const spinner = ora15("Creating database snapshot...").start();
11824
+ const spinner = ora17("Creating database snapshot...").start();
10970
11825
  try {
10971
11826
  let projectConfig;
10972
11827
  if (options.project) {
@@ -10986,8 +11841,8 @@ async function snapshotCommand(options) {
10986
11841
  const dbConfig = projectConfig.workspace?.database;
10987
11842
  if (!dbConfig?.snapshot_command && !dbConfig?.external_db) {
10988
11843
  spinner.fail(`No snapshot configuration for project ${projectConfig.key}`);
10989
- console.log(chalk40.dim("\nAdd database config to projects.yaml:"));
10990
- console.log(chalk40.dim(`
11844
+ console.log(chalk44.dim("\nAdd database config to projects.yaml:"));
11845
+ console.log(chalk44.dim(`
10991
11846
  ${projectConfig.key}:
10992
11847
  workspace:
10993
11848
  database:
@@ -11001,10 +11856,10 @@ async function snapshotCommand(options) {
11001
11856
  `));
11002
11857
  return;
11003
11858
  }
11004
- const outputPath = options.output || dbConfig.seed_file || join44(projectConfig.path, "infra", "seed", "seed.sql");
11859
+ const outputPath = options.output || dbConfig.seed_file || join45(projectConfig.path, "infra", "seed", "seed.sql");
11005
11860
  const outputDir = dirname11(outputPath);
11006
- if (!existsSync43(outputDir)) {
11007
- mkdirSync27(outputDir, { recursive: true });
11861
+ if (!existsSync44(outputDir)) {
11862
+ mkdirSync28(outputDir, { recursive: true });
11008
11863
  }
11009
11864
  spinner.text = "Running snapshot command...";
11010
11865
  let snapshotCmd;
@@ -11024,13 +11879,13 @@ async function snapshotCommand(options) {
11024
11879
  }
11025
11880
  const fullCmd = `${snapshotCmd} > "${outputPath}" 2>&1`;
11026
11881
  try {
11027
- await execAsync13(fullCmd, { timeout: 3e5 });
11882
+ await execAsync14(fullCmd, { timeout: 3e5 });
11028
11883
  } catch (error) {
11029
- if (existsSync43(outputPath)) {
11030
- const content2 = readFileSync36(outputPath, "utf-8");
11884
+ if (existsSync44(outputPath)) {
11885
+ const content2 = readFileSync37(outputPath, "utf-8");
11031
11886
  if (content2.includes("PostgreSQL database dump")) {
11032
11887
  spinner.warn("Snapshot completed with warnings (stderr captured)");
11033
- console.log(chalk40.dim(" Run `pan db clean` to remove stderr noise from the file"));
11888
+ console.log(chalk44.dim(" Run `pan db clean` to remove stderr noise from the file"));
11034
11889
  } else {
11035
11890
  spinner.fail(`Snapshot failed: ${error.message}`);
11036
11891
  return;
@@ -11040,7 +11895,7 @@ async function snapshotCommand(options) {
11040
11895
  return;
11041
11896
  }
11042
11897
  }
11043
- const content = readFileSync36(outputPath, "utf-8");
11898
+ const content = readFileSync37(outputPath, "utf-8");
11044
11899
  if (content.includes("Defaulted container") || content.includes("Unable to use a TTY")) {
11045
11900
  spinner.text = "Cleaning kubectl output from snapshot...";
11046
11901
  await cleanFile(outputPath);
@@ -11048,7 +11903,7 @@ async function snapshotCommand(options) {
11048
11903
  if (options.sanitize && dbConfig.seed_command) {
11049
11904
  spinner.text = "Running sanitization...";
11050
11905
  try {
11051
- await execAsync13(dbConfig.seed_command, { cwd: projectConfig.path });
11906
+ await execAsync14(dbConfig.seed_command, { cwd: projectConfig.path });
11052
11907
  } catch (error) {
11053
11908
  spinner.warn(`Sanitization warning: ${error.message}`);
11054
11909
  }
@@ -11056,13 +11911,13 @@ async function snapshotCommand(options) {
11056
11911
  spinner.succeed(`Snapshot saved to ${outputPath}`);
11057
11912
  const stats = statSync6(outputPath);
11058
11913
  const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
11059
- console.log(chalk40.dim(` Size: ${sizeMB} MB`));
11914
+ console.log(chalk44.dim(` Size: ${sizeMB} MB`));
11060
11915
  } catch (error) {
11061
11916
  spinner.fail(`Snapshot failed: ${error.message}`);
11062
11917
  }
11063
11918
  }
11064
11919
  async function seedCommand(workspaceOrIssue, options) {
11065
- const spinner = ora15("Seeding database...").start();
11920
+ const spinner = ora17("Seeding database...").start();
11066
11921
  try {
11067
11922
  const normalizedId = workspaceOrIssue.toLowerCase().replace(/[^a-z0-9-]/g, "-");
11068
11923
  const folderName = normalizedId.startsWith("feature-") ? normalizedId : `feature-${normalizedId}`;
@@ -11072,27 +11927,27 @@ async function seedCommand(workspaceOrIssue, options) {
11072
11927
  spinner.fail("Could not find project workspace configuration");
11073
11928
  return;
11074
11929
  }
11075
- const workspacePath = join44(projectConfig.path, projectConfig.workspace.workspaces_dir || "workspaces", folderName);
11076
- if (!existsSync43(workspacePath)) {
11930
+ const workspacePath = join45(projectConfig.path, projectConfig.workspace.workspaces_dir || "workspaces", folderName);
11931
+ if (!existsSync44(workspacePath)) {
11077
11932
  spinner.fail(`Workspace not found: ${workspacePath}`);
11078
11933
  return;
11079
11934
  }
11080
11935
  const dbConfig = projectConfig.workspace.database;
11081
11936
  const seedFile = options.file || dbConfig?.seed_file;
11082
- if (!seedFile || !existsSync43(seedFile)) {
11937
+ if (!seedFile || !existsSync44(seedFile)) {
11083
11938
  spinner.fail(`Seed file not found: ${seedFile || "(not configured)"}`);
11084
- console.log(chalk40.dim("\nConfigure seed_file in projects.yaml or use --file"));
11939
+ console.log(chalk44.dim("\nConfigure seed_file in projects.yaml or use --file"));
11085
11940
  return;
11086
11941
  }
11087
11942
  const projectName = `${projectConfig.name?.toLowerCase().replace(/\s+/g, "-")}-${folderName}`;
11088
11943
  const containerName = dbConfig?.container_name?.replace("{{PROJECT}}", projectName) || `${projectName}-postgres-1`;
11089
11944
  spinner.text = `Finding database container ${containerName}...`;
11090
11945
  try {
11091
- const { stdout } = await execAsync13(`docker ps --filter "name=${containerName}" --format "{{.Names}}"`);
11946
+ const { stdout } = await execAsync14(`docker ps --filter "name=${containerName}" --format "{{.Names}}"`);
11092
11947
  if (!stdout.trim()) {
11093
11948
  spinner.fail(`Database container not running: ${containerName}`);
11094
- console.log(chalk40.dim("\nStart the workspace containers first:"));
11095
- console.log(chalk40.dim(` pan workspace create ${workspaceOrIssue} --docker`));
11949
+ console.log(chalk44.dim("\nStart the workspace containers first:"));
11950
+ console.log(chalk44.dim(` pan workspace create ${workspaceOrIssue} --docker`));
11096
11951
  return;
11097
11952
  }
11098
11953
  } catch {
@@ -11101,7 +11956,7 @@ async function seedCommand(workspaceOrIssue, options) {
11101
11956
  }
11102
11957
  if (!options.force) {
11103
11958
  try {
11104
- const { stdout } = await execAsync13(
11959
+ const { stdout } = await execAsync14(
11105
11960
  `docker exec ${containerName} psql -U postgres -d myn -c "SELECT count(*) FROM flyway_schema_history" -t 2>/dev/null`
11106
11961
  );
11107
11962
  const count = parseInt(stdout.trim(), 10);
@@ -11115,7 +11970,7 @@ async function seedCommand(workspaceOrIssue, options) {
11115
11970
  if (options.force) {
11116
11971
  spinner.text = "Dropping existing database...";
11117
11972
  try {
11118
- await execAsync13(
11973
+ await execAsync14(
11119
11974
  `docker exec ${containerName} psql -U postgres -c "DROP DATABASE IF EXISTS myn; CREATE DATABASE myn;"`
11120
11975
  );
11121
11976
  } catch (error) {
@@ -11123,10 +11978,10 @@ async function seedCommand(workspaceOrIssue, options) {
11123
11978
  }
11124
11979
  }
11125
11980
  spinner.text = "Copying seed file to container...";
11126
- await execAsync13(`docker cp "${seedFile}" ${containerName}:/tmp/seed.sql`);
11981
+ await execAsync14(`docker cp "${seedFile}" ${containerName}:/tmp/seed.sql`);
11127
11982
  spinner.text = "Executing seed...";
11128
11983
  try {
11129
- await execAsync13(`docker exec ${containerName} psql -U postgres -d myn -f /tmp/seed.sql`, {
11984
+ await execAsync14(`docker exec ${containerName} psql -U postgres -d myn -f /tmp/seed.sql`, {
11130
11985
  timeout: 6e5
11131
11986
  // 10 minute timeout for large seeds
11132
11987
  });
@@ -11136,22 +11991,22 @@ async function seedCommand(workspaceOrIssue, options) {
11136
11991
  return;
11137
11992
  }
11138
11993
  }
11139
- await execAsync13(`docker exec ${containerName} rm /tmp/seed.sql`);
11994
+ await execAsync14(`docker exec ${containerName} rm /tmp/seed.sql`);
11140
11995
  spinner.succeed("Database seeded successfully");
11141
11996
  try {
11142
- const { stdout } = await execAsync13(
11997
+ const { stdout } = await execAsync14(
11143
11998
  `docker exec ${containerName} psql -U postgres -d myn -c "SELECT version, description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 3" -t`
11144
11999
  );
11145
- console.log(chalk40.dim("\nRecent migrations:"));
11146
- stdout.trim().split("\n").forEach((line) => console.log(chalk40.dim(` ${line.trim()}`)));
12000
+ console.log(chalk44.dim("\nRecent migrations:"));
12001
+ stdout.trim().split("\n").forEach((line) => console.log(chalk44.dim(` ${line.trim()}`)));
11147
12002
  } catch {
11148
12003
  }
11149
12004
  } catch (error) {
11150
12005
  spinner.fail(`Seed failed: ${error.message}`);
11151
12006
  }
11152
12007
  }
11153
- async function statusCommand3(workspaceOrIssue) {
11154
- const spinner = ora15("Checking database status...").start();
12008
+ async function statusCommand4(workspaceOrIssue) {
12009
+ const spinner = ora17("Checking database status...").start();
11155
12010
  try {
11156
12011
  let containerName;
11157
12012
  let projectConfig;
@@ -11169,7 +12024,7 @@ async function statusCommand3(workspaceOrIssue) {
11169
12024
  const projects = loadFullProjects();
11170
12025
  projectConfig = projects.find((p) => cwd.startsWith(p.path));
11171
12026
  if (projectConfig) {
11172
- const { stdout } = await execAsync13(
12027
+ const { stdout } = await execAsync14(
11173
12028
  `docker ps --filter "name=${projectConfig.name?.toLowerCase().replace(/\s+/g, "-")}" --filter "name=postgres" --format "{{.Names}}" | head -1`
11174
12029
  );
11175
12030
  containerName = stdout.trim();
@@ -11177,12 +12032,12 @@ async function statusCommand3(workspaceOrIssue) {
11177
12032
  }
11178
12033
  if (!containerName) {
11179
12034
  spinner.fail("Could not determine database container");
11180
- console.log(chalk40.dim("\nUsage: pan db status <issue-id>"));
11181
- console.log(chalk40.dim(" pan db status MIN-123"));
12035
+ console.log(chalk44.dim("\nUsage: pan db status <issue-id>"));
12036
+ console.log(chalk44.dim(" pan db status MIN-123"));
11182
12037
  return;
11183
12038
  }
11184
12039
  spinner.text = `Checking container ${containerName}...`;
11185
- const { stdout: containerStatus } = await execAsync13(
12040
+ const { stdout: containerStatus } = await execAsync14(
11186
12041
  `docker ps --filter "name=${containerName}" --format "{{.Status}}"`
11187
12042
  );
11188
12043
  if (!containerStatus.trim()) {
@@ -11190,28 +12045,28 @@ async function statusCommand3(workspaceOrIssue) {
11190
12045
  return;
11191
12046
  }
11192
12047
  spinner.succeed(`Container: ${containerName}`);
11193
- console.log(chalk40.dim(` Status: ${containerStatus.trim()}`));
12048
+ console.log(chalk44.dim(` Status: ${containerStatus.trim()}`));
11194
12049
  try {
11195
- const { stdout: version } = await execAsync13(
12050
+ const { stdout: version } = await execAsync14(
11196
12051
  `docker exec ${containerName} psql -U postgres -d myn -c "SELECT version, description FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 1" -t`
11197
12052
  );
11198
12053
  const [ver, desc] = version.trim().split("|").map((s) => s.trim());
11199
- console.log(chalk40.green(` Flyway: V${ver} - ${desc}`));
12054
+ console.log(chalk44.green(` Flyway: V${ver} - ${desc}`));
11200
12055
  } catch {
11201
- console.log(chalk40.yellow(" Flyway: Not initialized"));
12056
+ console.log(chalk44.yellow(" Flyway: Not initialized"));
11202
12057
  }
11203
12058
  try {
11204
- const { stdout: tableCount } = await execAsync13(
12059
+ const { stdout: tableCount } = await execAsync14(
11205
12060
  `docker exec ${containerName} psql -U postgres -d myn -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" -t`
11206
12061
  );
11207
- console.log(chalk40.dim(` Tables: ${tableCount.trim()}`));
12062
+ console.log(chalk44.dim(` Tables: ${tableCount.trim()}`));
11208
12063
  } catch {
11209
12064
  }
11210
12065
  try {
11211
- const { stdout: dbSize } = await execAsync13(
12066
+ const { stdout: dbSize } = await execAsync14(
11212
12067
  `docker exec ${containerName} psql -U postgres -d myn -c "SELECT pg_size_pretty(pg_database_size('myn'))" -t`
11213
12068
  );
11214
- console.log(chalk40.dim(` Size: ${dbSize.trim()}`));
12069
+ console.log(chalk44.dim(` Size: ${dbSize.trim()}`));
11215
12070
  } catch {
11216
12071
  }
11217
12072
  } catch (error) {
@@ -11219,13 +12074,13 @@ async function statusCommand3(workspaceOrIssue) {
11219
12074
  }
11220
12075
  }
11221
12076
  async function cleanCommand(file, options) {
11222
- const spinner = ora15("Cleaning database dump file...").start();
12077
+ const spinner = ora17("Cleaning database dump file...").start();
11223
12078
  try {
11224
- if (!existsSync43(file)) {
12079
+ if (!existsSync44(file)) {
11225
12080
  spinner.fail(`File not found: ${file}`);
11226
12081
  return;
11227
12082
  }
11228
- const content = readFileSync36(file, "utf-8");
12083
+ const content = readFileSync37(file, "utf-8");
11229
12084
  const lines = content.split("\n");
11230
12085
  const patternsToRemove = [
11231
12086
  /^Defaulted container/,
@@ -11278,24 +12133,24 @@ async function cleanCommand(file, options) {
11278
12133
  if (options.dryRun) {
11279
12134
  spinner.info(`Would remove ${removedLines} lines`);
11280
12135
  if (removedLines > 0) {
11281
- console.log(chalk40.dim("\nLines to remove (sample):"));
12136
+ console.log(chalk44.dim("\nLines to remove (sample):"));
11282
12137
  const removed = lines.filter((line) => patternsToRemove.some((p) => p.test(line))).slice(0, 5);
11283
- removed.forEach((line) => console.log(chalk40.red(` - ${line.slice(0, 80)}...`)));
12138
+ removed.forEach((line) => console.log(chalk44.red(` - ${line.slice(0, 80)}...`)));
11284
12139
  }
11285
12140
  return;
11286
12141
  }
11287
12142
  const outputPath = options.output || file;
11288
- writeFileSync27(outputPath, cleanedContent);
12143
+ writeFileSync28(outputPath, cleanedContent);
11289
12144
  spinner.succeed(`Cleaned ${removedLines} lines`);
11290
- console.log(chalk40.dim(` Output: ${outputPath}`));
11291
- console.log(chalk40.dim(` Original: ${lines.length} lines`));
11292
- console.log(chalk40.dim(` Cleaned: ${cleanedLines.length} lines`));
12145
+ console.log(chalk44.dim(` Output: ${outputPath}`));
12146
+ console.log(chalk44.dim(` Original: ${lines.length} lines`));
12147
+ console.log(chalk44.dim(` Cleaned: ${cleanedLines.length} lines`));
11293
12148
  } catch (error) {
11294
12149
  spinner.fail(`Clean failed: ${error.message}`);
11295
12150
  }
11296
12151
  }
11297
12152
  async function cleanFile(filePath) {
11298
- const content = readFileSync36(filePath, "utf-8");
12153
+ const content = readFileSync37(filePath, "utf-8");
11299
12154
  const lines = content.split("\n");
11300
12155
  let startIndex = 0;
11301
12156
  for (let i = 0; i < lines.length; i++) {
@@ -11315,7 +12170,7 @@ async function cleanFile(filePath) {
11315
12170
  /^error: timed out waiting/
11316
12171
  ];
11317
12172
  const cleanedLines = lines.slice(startIndex).filter((line) => !patternsToRemove.some((p) => p.test(line)));
11318
- writeFileSync27(filePath, cleanedLines.join("\n"));
12173
+ writeFileSync28(filePath, cleanedLines.join("\n"));
11319
12174
  }
11320
12175
  async function configCommand(project2) {
11321
12176
  const projects = loadFullProjects();
@@ -11329,18 +12184,18 @@ async function configCommand(project2) {
11329
12184
  projectConfig = projects.find((p) => cwd.startsWith(p.path));
11330
12185
  }
11331
12186
  if (!projectConfig) {
11332
- console.log(chalk40.red("Project not found"));
11333
- console.log(chalk40.dim("\nAvailable projects:"));
11334
- projects.forEach((p) => console.log(chalk40.dim(` ${p.key} (${p.linear_team || "no team"})`)));
12187
+ console.log(chalk44.red("Project not found"));
12188
+ console.log(chalk44.dim("\nAvailable projects:"));
12189
+ projects.forEach((p) => console.log(chalk44.dim(` ${p.key} (${p.linear_team || "no team"})`)));
11335
12190
  return;
11336
12191
  }
11337
- console.log(chalk40.bold(`Database Configuration: ${projectConfig.key}`));
12192
+ console.log(chalk44.bold(`Database Configuration: ${projectConfig.key}`));
11338
12193
  console.log("");
11339
12194
  const dbConfig = projectConfig.workspace?.database;
11340
12195
  if (!dbConfig) {
11341
- console.log(chalk40.yellow("No database configuration found"));
11342
- console.log(chalk40.dim("\nAdd to projects.yaml under workspace:"));
11343
- console.log(chalk40.dim(`
12196
+ console.log(chalk44.yellow("No database configuration found"));
12197
+ console.log(chalk44.dim("\nAdd to projects.yaml under workspace:"));
12198
+ console.log(chalk44.dim(`
11344
12199
  database:
11345
12200
  seed_file: /path/to/seed.sql
11346
12201
  snapshot_command: "kubectl exec -n prod pod/postgres -- pg_dump -U app db"
@@ -11349,12 +12204,12 @@ async function configCommand(project2) {
11349
12204
  return;
11350
12205
  }
11351
12206
  if (dbConfig.seed_file) {
11352
- const exists = existsSync43(dbConfig.seed_file);
12207
+ const exists = existsSync44(dbConfig.seed_file);
11353
12208
  console.log(` Seed file: ${dbConfig.seed_file}`);
11354
- console.log(chalk40.dim(` Status: ${exists ? chalk40.green("exists") : chalk40.red("not found")}`));
12209
+ console.log(chalk44.dim(` Status: ${exists ? chalk44.green("exists") : chalk44.red("not found")}`));
11355
12210
  if (exists) {
11356
12211
  const stats = statSync6(dbConfig.seed_file);
11357
- console.log(chalk40.dim(` Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`));
12212
+ console.log(chalk44.dim(` Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`));
11358
12213
  }
11359
12214
  }
11360
12215
  if (dbConfig.snapshot_command) {
@@ -11369,25 +12224,25 @@ async function configCommand(project2) {
11369
12224
  if (dbConfig.migrations) {
11370
12225
  console.log(` Migrations: ${dbConfig.migrations.type}`);
11371
12226
  if (dbConfig.migrations.path) {
11372
- console.log(chalk40.dim(` Path: ${dbConfig.migrations.path}`));
12227
+ console.log(chalk44.dim(` Path: ${dbConfig.migrations.path}`));
11373
12228
  }
11374
12229
  }
11375
12230
  }
11376
12231
 
11377
12232
  // src/cli/commands/beads.ts
11378
- import chalk41 from "chalk";
11379
- import ora16 from "ora";
11380
- import { existsSync as existsSync44, readFileSync as readFileSync37 } from "fs";
11381
- import { join as join45 } from "path";
11382
- import { exec as exec14, execSync as execSync9 } from "child_process";
11383
- import { promisify as promisify14 } from "util";
12233
+ import chalk45 from "chalk";
12234
+ import ora18 from "ora";
12235
+ import { existsSync as existsSync45, readFileSync as readFileSync38 } from "fs";
12236
+ import { join as join46 } from "path";
12237
+ import { exec as exec15, execSync as execSync10 } from "child_process";
12238
+ import { promisify as promisify15 } from "util";
11384
12239
  import { platform as platform2 } from "os";
11385
- var execAsync14 = promisify14(exec14);
12240
+ var execAsync15 = promisify15(exec15);
11386
12241
  function detectPlatform2() {
11387
12242
  const os = platform2();
11388
12243
  if (os === "linux") {
11389
12244
  try {
11390
- const release = readFileSync37("/proc/version", "utf8").toLowerCase();
12245
+ const release = readFileSync38("/proc/version", "utf8").toLowerCase();
11391
12246
  if (release.includes("microsoft") || release.includes("wsl")) {
11392
12247
  return "wsl";
11393
12248
  }
@@ -11399,7 +12254,7 @@ function detectPlatform2() {
11399
12254
  }
11400
12255
  async function isBdAvailable() {
11401
12256
  try {
11402
- await execAsync14("which bd", { encoding: "utf-8" });
12257
+ await execAsync15("which bd", { encoding: "utf-8" });
11403
12258
  return true;
11404
12259
  } catch {
11405
12260
  return false;
@@ -11408,7 +12263,7 @@ async function isBdAvailable() {
11408
12263
  async function getOldClosedCount(cwd, days) {
11409
12264
  try {
11410
12265
  const seconds = days * 24 * 60 * 60;
11411
- const { stdout } = await execAsync14(
12266
+ const { stdout } = await execAsync15(
11412
12267
  `bd list --status closed --json 2>/dev/null | jq '[.[] | select(.closed_at != null) | select((now - (.closed_at | fromdateiso8601)) > ${seconds})] | length' 2>/dev/null || echo "0"`,
11413
12268
  { cwd, encoding: "utf-8" }
11414
12269
  );
@@ -11421,17 +12276,17 @@ async function compactCommand(options) {
11421
12276
  const days = options.days || 30;
11422
12277
  const cwd = process.cwd();
11423
12278
  if (!await isBdAvailable()) {
11424
- console.error(chalk41.red("Error: bd (beads) CLI not found in PATH"));
11425
- console.log(chalk41.dim("Install beads: https://github.com/steveyegge/beads"));
12279
+ console.error(chalk45.red("Error: bd (beads) CLI not found in PATH"));
12280
+ console.log(chalk45.dim("Install beads: https://github.com/steveyegge/beads"));
11426
12281
  process.exit(1);
11427
12282
  }
11428
- const beadsDir = join45(cwd, ".beads");
11429
- if (!existsSync44(beadsDir)) {
11430
- console.error(chalk41.red("Error: No .beads directory found in current directory"));
11431
- console.log(chalk41.dim("Run bd init to initialize beads"));
12283
+ const beadsDir = join46(cwd, ".beads");
12284
+ if (!existsSync45(beadsDir)) {
12285
+ console.error(chalk45.red("Error: No .beads directory found in current directory"));
12286
+ console.log(chalk45.dim("Run bd init to initialize beads"));
11432
12287
  process.exit(1);
11433
12288
  }
11434
- const spinner = ora16("Checking for old closed beads...").start();
12289
+ const spinner = ora18("Checking for old closed beads...").start();
11435
12290
  try {
11436
12291
  const count = await getOldClosedCount(cwd, days);
11437
12292
  if (count === 0) {
@@ -11442,28 +12297,28 @@ async function compactCommand(options) {
11442
12297
  if (options.dryRun) {
11443
12298
  spinner.info(`Dry run: Would compact ${count} beads (use without --dry-run to execute)`);
11444
12299
  console.log("");
11445
- console.log(chalk41.bold("Beads that would be compacted:"));
12300
+ console.log(chalk45.bold("Beads that would be compacted:"));
11446
12301
  try {
11447
- const { stdout: beadsList } = await execAsync14(
12302
+ const { stdout: beadsList } = await execAsync15(
11448
12303
  `bd list --status closed --json 2>/dev/null | jq -r '.[] | select(.closed_at != null) | select((now - (.closed_at | fromdateiso8601)) > ${days * 24 * 60 * 60}) | " - \\(.id): \\(.title)"' 2>/dev/null`,
11449
12304
  { cwd, encoding: "utf-8" }
11450
12305
  );
11451
12306
  console.log(beadsList || " (none)");
11452
12307
  } catch {
11453
- console.log(chalk41.dim(" (could not list beads)"));
12308
+ console.log(chalk45.dim(" (could not list beads)"));
11454
12309
  }
11455
12310
  return;
11456
12311
  }
11457
12312
  spinner.text = "Running compaction...";
11458
- await execAsync14(`bd admin compact --days ${days}`, { cwd, encoding: "utf-8" });
12313
+ await execAsync15(`bd admin compact --days ${days}`, { cwd, encoding: "utf-8" });
11459
12314
  spinner.succeed(`Compacted ${count} beads older than ${days} days`);
11460
12315
  try {
11461
- await execAsync14(`git diff --quiet .beads/`, { cwd, encoding: "utf-8" });
11462
- console.log(chalk41.dim("No changes to commit (beads already up to date)"));
12316
+ await execAsync15(`git diff --quiet .beads/`, { cwd, encoding: "utf-8" });
12317
+ console.log(chalk45.dim("No changes to commit (beads already up to date)"));
11463
12318
  } catch {
11464
12319
  console.log("");
11465
- console.log(chalk41.bold("Changes detected in .beads/"));
11466
- console.log(chalk41.dim("To commit the compacted beads:"));
12320
+ console.log(chalk45.bold("Changes detected in .beads/"));
12321
+ console.log(chalk45.dim("To commit the compacted beads:"));
11467
12322
  console.log("");
11468
12323
  console.log(" git add .beads/");
11469
12324
  console.log(' git commit -m "chore: compact beads (remove closed issues > ' + days + ' days)"');
@@ -11475,34 +12330,34 @@ async function compactCommand(options) {
11475
12330
  }
11476
12331
  } catch (error) {
11477
12332
  spinner.fail("Compaction failed");
11478
- console.error(chalk41.red(error.message));
12333
+ console.error(chalk45.red(error.message));
11479
12334
  process.exit(1);
11480
12335
  }
11481
12336
  }
11482
12337
  async function statsCommand() {
11483
12338
  const cwd = process.cwd();
11484
12339
  if (!await isBdAvailable()) {
11485
- console.error(chalk41.red("Error: bd (beads) CLI not found"));
12340
+ console.error(chalk45.red("Error: bd (beads) CLI not found"));
11486
12341
  process.exit(1);
11487
12342
  }
11488
- const beadsDir = join45(cwd, ".beads");
11489
- if (!existsSync44(beadsDir)) {
11490
- console.error(chalk41.red("Error: No .beads directory found"));
12343
+ const beadsDir = join46(cwd, ".beads");
12344
+ if (!existsSync45(beadsDir)) {
12345
+ console.error(chalk45.red("Error: No .beads directory found"));
11491
12346
  process.exit(1);
11492
12347
  }
11493
- const spinner = ora16("Gathering beads statistics...").start();
12348
+ const spinner = ora18("Gathering beads statistics...").start();
11494
12349
  try {
11495
- const { stdout: totalRaw } = await execAsync14(`bd list --limit 0 --json 2>/dev/null | jq 'length'`, {
12350
+ const { stdout: totalRaw } = await execAsync15(`bd list --limit 0 --json 2>/dev/null | jq 'length'`, {
11496
12351
  cwd,
11497
12352
  encoding: "utf-8"
11498
12353
  });
11499
12354
  const total = parseInt(totalRaw.trim(), 10) || 0;
11500
- const { stdout: openRaw } = await execAsync14(`bd list --status open --limit 0 --json 2>/dev/null | jq 'length'`, {
12355
+ const { stdout: openRaw } = await execAsync15(`bd list --status open --limit 0 --json 2>/dev/null | jq 'length'`, {
11501
12356
  cwd,
11502
12357
  encoding: "utf-8"
11503
12358
  });
11504
12359
  const open = parseInt(openRaw.trim(), 10) || 0;
11505
- const { stdout: closedRaw } = await execAsync14(`bd list --status closed --limit 0 --json 2>/dev/null | jq 'length'`, {
12360
+ const { stdout: closedRaw } = await execAsync15(`bd list --status closed --limit 0 --json 2>/dev/null | jq 'length'`, {
11506
12361
  cwd,
11507
12362
  encoding: "utf-8"
11508
12363
  });
@@ -11510,20 +12365,20 @@ async function statsCommand() {
11510
12365
  const oldClosed = await getOldClosedCount(cwd, 30);
11511
12366
  spinner.stop();
11512
12367
  console.log("");
11513
- console.log(chalk41.bold("Beads Statistics"));
12368
+ console.log(chalk45.bold("Beads Statistics"));
11514
12369
  console.log("");
11515
- console.log(` Total: ${chalk41.cyan(total)}`);
11516
- console.log(` Open: ${chalk41.green(open)}`);
11517
- console.log(` Closed: ${chalk41.dim(closed)}`);
11518
- console.log(` Old (>30d): ${oldClosed > 0 ? chalk41.yellow(oldClosed) : chalk41.dim(oldClosed)}`);
12370
+ console.log(` Total: ${chalk45.cyan(total)}`);
12371
+ console.log(` Open: ${chalk45.green(open)}`);
12372
+ console.log(` Closed: ${chalk45.dim(closed)}`);
12373
+ console.log(` Old (>30d): ${oldClosed > 0 ? chalk45.yellow(oldClosed) : chalk45.dim(oldClosed)}`);
11519
12374
  console.log("");
11520
12375
  if (oldClosed > 0) {
11521
- console.log(chalk41.dim(`Tip: Run 'pan beads compact' to remove old closed beads`));
12376
+ console.log(chalk45.dim(`Tip: Run 'pan beads compact' to remove old closed beads`));
11522
12377
  console.log("");
11523
12378
  }
11524
12379
  } catch (error) {
11525
12380
  spinner.fail("Failed to get statistics");
11526
- console.error(chalk41.red(error.message));
12381
+ console.error(chalk45.red(error.message));
11527
12382
  process.exit(1);
11528
12383
  }
11529
12384
  }
@@ -11544,10 +12399,10 @@ function registerBeadsCommands(program2) {
11544
12399
  });
11545
12400
  }
11546
12401
  async function upgradeCommand(checkOnly = false) {
11547
- console.log(chalk41.dim("Checking beads version..."));
12402
+ console.log(chalk45.dim("Checking beads version..."));
11548
12403
  let currentVersion = "not installed";
11549
12404
  try {
11550
- const { stdout } = await execAsync14("bd --version", { encoding: "utf-8" });
12405
+ const { stdout } = await execAsync15("bd --version", { encoding: "utf-8" });
11551
12406
  const match = stdout.match(/(\d+\.\d+\.\d+)/);
11552
12407
  if (match) {
11553
12408
  currentVersion = match[1];
@@ -11556,7 +12411,7 @@ async function upgradeCommand(checkOnly = false) {
11556
12411
  }
11557
12412
  let latestVersion = "unknown";
11558
12413
  try {
11559
- const { stdout } = await execAsync14(
12414
+ const { stdout } = await execAsync15(
11560
12415
  "curl -sL https://api.github.com/repos/steveyegge/beads/releases/latest | jq -r .tag_name",
11561
12416
  { encoding: "utf-8" }
11562
12417
  );
@@ -11564,66 +12419,295 @@ async function upgradeCommand(checkOnly = false) {
11564
12419
  } catch {
11565
12420
  }
11566
12421
  console.log("");
11567
- console.log(chalk41.bold("Beads CLI Version"));
12422
+ console.log(chalk45.bold("Beads CLI Version"));
11568
12423
  console.log("");
11569
- console.log(` Current: ${currentVersion === "not installed" ? chalk41.red(currentVersion) : chalk41.cyan(currentVersion)}`);
11570
- console.log(` Latest: ${chalk41.green(latestVersion)}`);
12424
+ console.log(` Current: ${currentVersion === "not installed" ? chalk45.red(currentVersion) : chalk45.cyan(currentVersion)}`);
12425
+ console.log(` Latest: ${chalk45.green(latestVersion)}`);
11571
12426
  console.log("");
11572
12427
  if (currentVersion === latestVersion) {
11573
- console.log(chalk41.green("\u2713 Already on latest version"));
12428
+ console.log(chalk45.green("\u2713 Already on latest version"));
11574
12429
  return;
11575
12430
  }
11576
12431
  if (checkOnly) {
11577
12432
  if (currentVersion !== latestVersion && currentVersion !== "not installed") {
11578
- console.log(chalk41.yellow(`Update available: ${currentVersion} \u2192 ${latestVersion}`));
11579
- console.log(chalk41.dim(`Run 'pan beads upgrade' to install`));
12433
+ console.log(chalk45.yellow(`Update available: ${currentVersion} \u2192 ${latestVersion}`));
12434
+ console.log(chalk45.dim(`Run 'pan beads upgrade' to install`));
11580
12435
  }
11581
12436
  return;
11582
12437
  }
11583
- const spinner = ora16("Upgrading beads...").start();
12438
+ const spinner = ora18("Upgrading beads...").start();
11584
12439
  const plat = detectPlatform2();
11585
12440
  try {
11586
12441
  if (plat === "darwin") {
11587
12442
  try {
11588
- execSync9("brew upgrade steveyegge/beads/bd 2>/dev/null || brew install steveyegge/beads/bd", {
12443
+ execSync10("brew upgrade steveyegge/beads/bd 2>/dev/null || brew install steveyegge/beads/bd", {
11589
12444
  stdio: "pipe",
11590
12445
  timeout: 12e4
11591
12446
  });
11592
12447
  spinner.succeed("beads upgraded via Homebrew");
11593
12448
  } catch {
11594
- execSync9("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
12449
+ execSync10("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
11595
12450
  stdio: "pipe",
11596
12451
  timeout: 12e4
11597
12452
  });
11598
12453
  spinner.succeed("beads upgraded via install script");
11599
12454
  }
11600
12455
  } else {
11601
- execSync9("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
12456
+ execSync10("curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash", {
11602
12457
  stdio: "pipe",
11603
12458
  timeout: 12e4
11604
12459
  });
11605
12460
  spinner.succeed("beads upgraded via install script");
11606
12461
  }
11607
12462
  try {
11608
- const { stdout } = await execAsync14("bd --version", { encoding: "utf-8" });
12463
+ const { stdout } = await execAsync15("bd --version", { encoding: "utf-8" });
11609
12464
  const match = stdout.match(/(\d+\.\d+\.\d+)/);
11610
12465
  if (match) {
11611
- console.log(chalk41.green(`
12466
+ console.log(chalk45.green(`
11612
12467
  \u2713 Now running beads v${match[1]}`));
11613
12468
  }
11614
12469
  } catch {
11615
12470
  }
11616
12471
  } catch (error) {
11617
12472
  spinner.fail("Upgrade failed");
11618
- console.error(chalk41.red(error.message));
12473
+ console.error(chalk45.red(error.message));
12474
+ console.log("");
12475
+ console.log(chalk45.dim("Manual upgrade:"));
12476
+ console.log(chalk45.dim(" curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"));
12477
+ process.exit(1);
12478
+ }
12479
+ }
12480
+
12481
+ // src/cli/commands/migrate-config.ts
12482
+ import chalk46 from "chalk";
12483
+ import ora19 from "ora";
12484
+ import inquirer5 from "inquirer";
12485
+
12486
+ // src/lib/config-migration.ts
12487
+ import { readFileSync as readFileSync39, writeFileSync as writeFileSync29, existsSync as existsSync46, renameSync } from "fs";
12488
+ import { join as join47 } from "path";
12489
+ import { homedir as homedir19 } from "os";
12490
+ import yaml from "js-yaml";
12491
+ var LEGACY_SETTINGS_PATH = join47(homedir19(), ".panopticon", "settings.json");
12492
+ var NEW_CONFIG_PATH = join47(homedir19(), ".panopticon", "config.yaml");
12493
+ var BACKUP_SETTINGS_PATH = join47(homedir19(), ".panopticon", "settings.json.backup");
12494
+ function needsMigration() {
12495
+ return existsSync46(LEGACY_SETTINGS_PATH) && !existsSync46(NEW_CONFIG_PATH);
12496
+ }
12497
+ function hasLegacySettings() {
12498
+ return existsSync46(LEGACY_SETTINGS_PATH);
12499
+ }
12500
+ function detectEnabledProviders(settings) {
12501
+ return {
12502
+ anthropic: true,
12503
+ // Always enabled
12504
+ openai: !!settings.api_keys.openai,
12505
+ google: !!settings.api_keys.google,
12506
+ zai: !!settings.api_keys.zai,
12507
+ kimi: false
12508
+ // Legacy settings don't have Kimi
12509
+ };
12510
+ }
12511
+ function convertToYamlConfig(settings) {
12512
+ const providers = detectEnabledProviders(settings);
12513
+ const config2 = {
12514
+ models: {
12515
+ providers,
12516
+ overrides: {},
12517
+ // No overrides from legacy
12518
+ gemini_thinking_level: 3
12519
+ },
12520
+ api_keys: settings.api_keys
12521
+ };
12522
+ return config2;
12523
+ }
12524
+ function migrateConfig(options = {}) {
12525
+ const { backup: backup2 = true, deleteLegacy = false, dryRun = false } = options;
12526
+ try {
12527
+ if (!needsMigration()) {
12528
+ if (existsSync46(NEW_CONFIG_PATH)) {
12529
+ return {
12530
+ success: true,
12531
+ overridesCount: 0,
12532
+ providersEnabled: ["anthropic"],
12533
+ message: "Config already migrated (config.yaml exists)"
12534
+ };
12535
+ }
12536
+ return {
12537
+ success: false,
12538
+ overridesCount: 0,
12539
+ providersEnabled: [],
12540
+ message: "No legacy settings.json found to migrate"
12541
+ };
12542
+ }
12543
+ const settings = loadSettings();
12544
+ const yamlConfig = convertToYamlConfig(settings);
12545
+ const yamlContent = yaml.dump(yamlConfig, {
12546
+ indent: 2,
12547
+ lineWidth: 120,
12548
+ noRefs: true
12549
+ });
12550
+ if (dryRun) {
12551
+ const providersEnabled2 = Object.entries(yamlConfig.models?.providers || {}).filter(([_, enabled]) => enabled).map(([name]) => name);
12552
+ return {
12553
+ success: true,
12554
+ overridesCount: Object.keys(yamlConfig.models?.overrides || {}).length,
12555
+ providersEnabled: providersEnabled2,
12556
+ message: `Would migrate to smart selection with ${providersEnabled2.length} providers enabled`
12557
+ };
12558
+ }
12559
+ writeFileSync29(NEW_CONFIG_PATH, yamlContent, "utf-8");
12560
+ if (backup2) {
12561
+ const legacyContent = readFileSync39(LEGACY_SETTINGS_PATH, "utf-8");
12562
+ writeFileSync29(BACKUP_SETTINGS_PATH, legacyContent, "utf-8");
12563
+ }
12564
+ if (deleteLegacy) {
12565
+ renameSync(LEGACY_SETTINGS_PATH, `${LEGACY_SETTINGS_PATH}.migrated`);
12566
+ }
12567
+ const providersEnabled = Object.entries(yamlConfig.models?.providers || {}).filter(([_, enabled]) => enabled).map(([name]) => name);
12568
+ return {
12569
+ success: true,
12570
+ overridesCount: Object.keys(yamlConfig.models?.overrides || {}).length,
12571
+ providersEnabled,
12572
+ message: `Successfully migrated to smart selection with ${providersEnabled.length} providers`
12573
+ };
12574
+ } catch (error) {
12575
+ return {
12576
+ success: false,
12577
+ overridesCount: 0,
12578
+ providersEnabled: [],
12579
+ message: "Migration failed",
12580
+ error: error.message
12581
+ };
12582
+ }
12583
+ }
12584
+
12585
+ // src/cli/commands/migrate-config.ts
12586
+ async function migrateConfigCommand(options = {}) {
12587
+ console.log("");
12588
+ console.log(chalk46.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
12589
+ console.log(chalk46.bold.cyan(" CONFIGURATION MIGRATION"));
12590
+ console.log(chalk46.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
12591
+ console.log("");
12592
+ if (!hasLegacySettings()) {
12593
+ console.log(chalk46.yellow("\u2713 No legacy settings.json found"));
12594
+ console.log(chalk46.dim(" You are already using the new config.yaml format."));
12595
+ console.log("");
12596
+ return;
12597
+ }
12598
+ if (!needsMigration() && !options.force) {
12599
+ console.log(chalk46.green("\u2713 Already migrated to config.yaml"));
12600
+ console.log(chalk46.dim(" Use --force to regenerate config.yaml from settings.json"));
12601
+ console.log("");
12602
+ return;
12603
+ }
12604
+ if (options.preview) {
12605
+ const spinner2 = ora19("Generating migration preview...").start();
12606
+ const preview = migrateConfig({ dryRun: true });
12607
+ if (!preview.success) {
12608
+ spinner2.fail("Preview failed");
12609
+ console.error(chalk46.red(`Error: ${preview.error || preview.message}`));
12610
+ return;
12611
+ }
12612
+ spinner2.succeed("Migration preview generated");
11619
12613
  console.log("");
11620
- console.log(chalk41.dim("Manual upgrade:"));
11621
- console.log(chalk41.dim(" curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash"));
12614
+ console.log(chalk46.bold("Migration Summary:"));
12615
+ console.log(` Selection: ${chalk46.cyan("Smart (capability-based)")}`);
12616
+ console.log(` Overrides: ${chalk46.cyan(preview.overridesCount)} work types`);
12617
+ console.log(` Providers: ${chalk46.cyan(preview.providersEnabled.join(", "))}`);
12618
+ console.log("");
12619
+ console.log(chalk46.dim("Note: Legacy presets have been replaced with smart selection."));
12620
+ console.log(chalk46.dim("The system now automatically picks the best model for each task."));
12621
+ console.log("");
12622
+ return;
12623
+ }
12624
+ if (!options.force) {
12625
+ const preview = migrateConfig({ dryRun: true });
12626
+ if (preview.success) {
12627
+ console.log(chalk46.bold("Migration will:"));
12628
+ console.log(` \u2022 Create config.yaml with ${chalk46.cyan("smart (capability-based)")} selection`);
12629
+ console.log(` \u2022 Apply ${chalk46.cyan(preview.overridesCount)} work type overrides`);
12630
+ console.log(` \u2022 Enable providers: ${chalk46.cyan(preview.providersEnabled.join(", "))}`);
12631
+ if (options.backup !== false) {
12632
+ console.log(" \u2022 Back up settings.json to settings.json.backup");
12633
+ }
12634
+ if (options.deleteLegacy) {
12635
+ console.log(" \u2022 Rename settings.json to settings.json.migrated");
12636
+ }
12637
+ console.log("");
12638
+ console.log(chalk46.yellow("Note: Legacy presets (Premium/Balanced/Budget) have been removed."));
12639
+ console.log(chalk46.yellow("The new system automatically selects the best model for each task."));
12640
+ console.log("");
12641
+ }
12642
+ const { confirm: confirm3 } = await inquirer5.prompt([
12643
+ {
12644
+ type: "confirm",
12645
+ name: "confirm",
12646
+ message: "Proceed with migration?",
12647
+ default: true
12648
+ }
12649
+ ]);
12650
+ if (!confirm3) {
12651
+ console.log(chalk46.yellow("Migration cancelled"));
12652
+ return;
12653
+ }
12654
+ }
12655
+ const spinner = ora19("Migrating configuration...").start();
12656
+ const migrationOptions = {
12657
+ backup: options.backup !== false,
12658
+ // Default to true
12659
+ deleteLegacy: options.deleteLegacy || false
12660
+ };
12661
+ const result = migrateConfig(migrationOptions);
12662
+ if (!result.success) {
12663
+ spinner.fail("Migration failed");
12664
+ console.error(chalk46.red(`Error: ${result.error || result.message}`));
11622
12665
  process.exit(1);
11623
12666
  }
12667
+ spinner.succeed("Migration complete!");
12668
+ console.log("");
12669
+ console.log(chalk46.bold.green("\u2713 Configuration migrated successfully"));
12670
+ console.log("");
12671
+ console.log(chalk46.bold("Details:"));
12672
+ console.log(` ${chalk46.dim("Selection:")} ${chalk46.cyan("Smart (capability-based)")}`);
12673
+ console.log(` ${chalk46.dim("Work type overrides:")} ${chalk46.cyan(result.overridesCount)}`);
12674
+ console.log(` ${chalk46.dim("Enabled providers:")} ${chalk46.cyan(result.providersEnabled.join(", "))}`);
12675
+ console.log("");
12676
+ if (migrationOptions.backup) {
12677
+ console.log(chalk46.dim(" Legacy settings.json backed up to settings.json.backup"));
12678
+ }
12679
+ if (migrationOptions.deleteLegacy) {
12680
+ console.log(chalk46.dim(" Legacy settings.json renamed to settings.json.migrated"));
12681
+ }
12682
+ console.log("");
12683
+ console.log(chalk46.bold("Next steps:"));
12684
+ console.log(" 1. Review your new config: " + chalk46.cyan("~/.panopticon/config.yaml"));
12685
+ console.log(" 2. Enable additional providers for more model options");
12686
+ console.log(" 3. Add work type overrides if you prefer specific models for tasks");
12687
+ console.log(" 4. Documentation: " + chalk46.cyan("docs/CONFIGURATION.md"));
12688
+ console.log("");
11624
12689
  }
11625
12690
 
11626
12691
  // src/cli/index.ts
12692
+ var PANOPTICON_ENV_FILE = join48(homedir20(), ".panopticon.env");
12693
+ if (existsSync47(PANOPTICON_ENV_FILE)) {
12694
+ try {
12695
+ const envContent = readFileSync40(PANOPTICON_ENV_FILE, "utf-8");
12696
+ for (const line of envContent.split("\n")) {
12697
+ const trimmed = line.trim();
12698
+ if (!trimmed || trimmed.startsWith("#")) continue;
12699
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
12700
+ if (match) {
12701
+ const [, key, value] = match;
12702
+ if (process.env[key] === void 0) {
12703
+ process.env[key] = value.trim();
12704
+ }
12705
+ }
12706
+ }
12707
+ } catch (error) {
12708
+ console.warn("Warning: Failed to load ~/.panopticon.env:", error.message);
12709
+ }
12710
+ }
11627
12711
  var program = new Command();
11628
12712
  program.name("pan").description("Multi-agent orchestration for AI coding assistants").version("0.1.3");
11629
12713
  program.command("init").description("Initialize Panopticon (~/.panopticon/)").action(initCommand);
@@ -11638,90 +12722,93 @@ registerWorkspaceCommands(program);
11638
12722
  registerTestCommands(program);
11639
12723
  registerCloisterCommands(program);
11640
12724
  registerSpecialistsCommands(program);
12725
+ registerConvoyCommands(program);
11641
12726
  registerSetupCommands(program);
11642
12727
  registerInstallCommand(program);
11643
12728
  registerDbCommands(program);
11644
12729
  registerBeadsCommands(program);
12730
+ program.command("migrate-config").description("Migrate from settings.json to config.yaml").option("--force", "Force migration even if config.yaml exists").option("--preview", "Preview migration without applying changes").option("--no-backup", "Do not back up settings.json").option("--delete-legacy", "Delete settings.json after migration").action(migrateConfigCommand);
11645
12731
  program.command("status").description("Show running agents (shorthand for work status)").option("--json", "Output as JSON").action(statusCommand);
11646
12732
  program.command("up").description("Start dashboard (and Traefik if enabled)").option("--detach", "Run in background").option("--skip-traefik", "Skip Traefik startup").action(async (options) => {
11647
- const { spawn, execSync: execSync10 } = await import("child_process");
11648
- const { join: join46, dirname: dirname12 } = await import("path");
12733
+ const { spawn, execSync: execSync11 } = await import("child_process");
12734
+ const { join: join49, dirname: dirname12 } = await import("path");
11649
12735
  const { fileURLToPath: fileURLToPath5 } = await import("url");
11650
- const { readFileSync: readFileSync38, existsSync: existsSync45 } = await import("fs");
12736
+ const { readFileSync: readFileSync41, existsSync: existsSync48 } = await import("fs");
11651
12737
  const { parse: parse2 } = await import("@iarna/toml");
11652
12738
  const __dirname5 = dirname12(fileURLToPath5(import.meta.url));
11653
- const bundledServer = join46(__dirname5, "..", "dashboard", "server.js");
11654
- const srcDashboard = join46(__dirname5, "..", "..", "src", "dashboard");
11655
- const configFile = join46(process.env.HOME || "", ".panopticon", "config.toml");
12739
+ const bundledServer = join49(__dirname5, "..", "dashboard", "server.js");
12740
+ const srcDashboard = join49(__dirname5, "..", "..", "src", "dashboard");
12741
+ const configFile = join49(process.env.HOME || "", ".panopticon", "config.toml");
11656
12742
  let traefikEnabled = false;
11657
12743
  let traefikDomain = "pan.localhost";
11658
- if (existsSync45(configFile)) {
12744
+ if (existsSync48(configFile)) {
11659
12745
  try {
11660
- const configContent = readFileSync38(configFile, "utf-8");
12746
+ const configContent = readFileSync41(configFile, "utf-8");
11661
12747
  const config2 = parse2(configContent);
11662
12748
  traefikEnabled = config2.traefik?.enabled === true;
11663
12749
  traefikDomain = config2.traefik?.domain || "pan.localhost";
11664
12750
  } catch (error) {
11665
- console.log(chalk42.yellow("Warning: Could not read config.toml"));
12751
+ console.log(chalk47.yellow("Warning: Could not read config.toml"));
11666
12752
  }
11667
12753
  }
11668
- console.log(chalk42.bold("Starting Panopticon...\n"));
12754
+ console.log(chalk47.bold("Starting Panopticon...\n"));
11669
12755
  if (traefikEnabled && !options.skipTraefik) {
11670
- const traefikDir = join46(process.env.HOME || "", ".panopticon", "traefik");
11671
- if (existsSync45(traefikDir)) {
12756
+ const traefikDir = join49(process.env.HOME || "", ".panopticon", "traefik");
12757
+ if (existsSync48(traefikDir)) {
11672
12758
  try {
11673
- const composeFile = join46(traefikDir, "docker-compose.yml");
11674
- if (existsSync45(composeFile)) {
11675
- const content = readFileSync38(composeFile, "utf-8");
12759
+ const composeFile = join49(traefikDir, "docker-compose.yml");
12760
+ if (existsSync48(composeFile)) {
12761
+ const content = readFileSync41(composeFile, "utf-8");
11676
12762
  if (!content.includes("external: true") && content.includes("panopticon:")) {
11677
12763
  const patched = content.replace(
11678
12764
  /networks:\s*\n\s*panopticon:\s*\n\s*name: panopticon\s*\n\s*driver: bridge/,
11679
12765
  "networks:\n panopticon:\n name: panopticon\n external: true # Network created by 'pan install'"
11680
12766
  );
11681
- const { writeFileSync: writeFileSync28 } = await import("fs");
11682
- writeFileSync28(composeFile, patched);
11683
- console.log(chalk42.dim(" (migrated network config)"));
12767
+ const { writeFileSync: writeFileSync30 } = await import("fs");
12768
+ writeFileSync30(composeFile, patched);
12769
+ console.log(chalk47.dim(" (migrated network config)"));
11684
12770
  }
11685
12771
  }
11686
- console.log(chalk42.dim("Starting Traefik..."));
11687
- execSync10("docker-compose up -d", {
12772
+ console.log(chalk47.dim("Starting Traefik..."));
12773
+ execSync11("docker-compose up -d", {
11688
12774
  cwd: traefikDir,
11689
12775
  stdio: "pipe"
11690
12776
  });
11691
- console.log(chalk42.green("\u2713 Traefik started"));
11692
- console.log(chalk42.dim(` Dashboard: https://traefik.${traefikDomain}:8080
12777
+ console.log(chalk47.green("\u2713 Traefik started"));
12778
+ console.log(chalk47.dim(` Dashboard: https://traefik.${traefikDomain}:8080
11693
12779
  `));
11694
12780
  } catch (error) {
11695
- console.log(chalk42.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
11696
- console.log(chalk42.dim(" Run with --skip-traefik to suppress this message\n"));
12781
+ console.log(chalk47.yellow("\u26A0 Failed to start Traefik (continuing anyway)"));
12782
+ console.log(chalk47.dim(" Run with --skip-traefik to suppress this message\n"));
11697
12783
  }
11698
12784
  }
11699
12785
  }
11700
- const isProduction = existsSync45(bundledServer);
11701
- const isDevelopment = existsSync45(srcDashboard);
12786
+ const isProduction = existsSync48(bundledServer);
12787
+ const isDevelopment = existsSync48(srcDashboard);
11702
12788
  if (!isProduction && !isDevelopment) {
11703
- console.error(chalk42.red("Error: Dashboard not found"));
11704
- console.error(chalk42.dim("This may be a corrupted installation. Try reinstalling panopticon-cli."));
12789
+ console.error(chalk47.red("Error: Dashboard not found"));
12790
+ console.error(chalk47.dim("This may be a corrupted installation. Try reinstalling panopticon-cli."));
11705
12791
  process.exit(1);
11706
12792
  }
11707
12793
  if (isDevelopment && !isProduction) {
11708
12794
  try {
11709
- execSync10("npm --version", { stdio: "pipe" });
12795
+ execSync11("npm --version", { stdio: "pipe" });
11710
12796
  } catch {
11711
- console.error(chalk42.red("Error: npm not found in PATH"));
11712
- console.error(chalk42.dim("Make sure Node.js and npm are installed and in your PATH"));
12797
+ console.error(chalk47.red("Error: npm not found in PATH"));
12798
+ console.error(chalk47.dim("Make sure Node.js and npm are installed and in your PATH"));
11713
12799
  process.exit(1);
11714
12800
  }
11715
12801
  }
11716
12802
  if (isProduction) {
11717
- console.log(chalk42.dim("Starting dashboard (bundled mode)..."));
12803
+ console.log(chalk47.dim("Starting dashboard (bundled mode)..."));
11718
12804
  } else {
11719
- console.log(chalk42.dim("Starting dashboard (development mode)..."));
12805
+ console.log(chalk47.dim("Starting dashboard (development mode)..."));
11720
12806
  }
11721
12807
  if (options.detach) {
11722
12808
  const child = isProduction ? spawn("node", [bundledServer], {
11723
12809
  detached: true,
11724
- stdio: "ignore"
12810
+ stdio: "ignore",
12811
+ env: { ...process.env, DASHBOARD_PORT: "3010" }
11725
12812
  }) : spawn("npm", ["run", "dev"], {
11726
12813
  cwd: srcDashboard,
11727
12814
  detached: true,
@@ -11731,7 +12818,7 @@ program.command("up").description("Start dashboard (and Traefik if enabled)").op
11731
12818
  let hasError = false;
11732
12819
  child.on("error", (err) => {
11733
12820
  hasError = true;
11734
- console.error(chalk42.red("Failed to start dashboard in background:"), err.message);
12821
+ console.error(chalk47.red("Failed to start dashboard in background:"), err.message);
11735
12822
  process.exit(1);
11736
12823
  });
11737
12824
  setTimeout(() => {
@@ -11739,72 +12826,78 @@ program.command("up").description("Start dashboard (and Traefik if enabled)").op
11739
12826
  child.unref();
11740
12827
  }
11741
12828
  }, 100);
11742
- console.log(chalk42.green("\u2713 Dashboard started in background"));
12829
+ console.log(chalk47.green("\u2713 Dashboard started in background"));
11743
12830
  if (traefikEnabled) {
11744
- console.log(` Frontend: ${chalk42.cyan(`https://${traefikDomain}`)}`);
11745
- console.log(` API: ${chalk42.cyan(`https://${traefikDomain}/api`)}`);
12831
+ console.log(` Frontend: ${chalk47.cyan(`https://${traefikDomain}`)}`);
12832
+ console.log(` API: ${chalk47.cyan(`https://${traefikDomain}/api`)}`);
12833
+ } else if (isProduction) {
12834
+ console.log(` URL: ${chalk47.cyan("http://localhost:3010")}`);
11746
12835
  } else {
11747
- console.log(` Frontend: ${chalk42.cyan("http://localhost:3001")}`);
11748
- console.log(` API: ${chalk42.cyan("http://localhost:3002")}`);
12836
+ console.log(` Frontend: ${chalk47.cyan("http://localhost:3001")}`);
12837
+ console.log(` API: ${chalk47.cyan("http://localhost:3002")}`);
11749
12838
  }
11750
12839
  } else {
11751
12840
  if (traefikEnabled) {
11752
- console.log(` Frontend: ${chalk42.cyan(`https://${traefikDomain}`)}`);
11753
- console.log(` API: ${chalk42.cyan(`https://${traefikDomain}/api`)}`);
12841
+ console.log(` Frontend: ${chalk47.cyan(`https://${traefikDomain}`)}`);
12842
+ console.log(` API: ${chalk47.cyan(`https://${traefikDomain}/api`)}`);
12843
+ } else if (isProduction) {
12844
+ console.log(` URL: ${chalk47.cyan("http://localhost:3010")}`);
11754
12845
  } else {
11755
- console.log(` Frontend: ${chalk42.cyan("http://localhost:3001")}`);
11756
- console.log(` API: ${chalk42.cyan("http://localhost:3002")}`);
12846
+ console.log(` Frontend: ${chalk47.cyan("http://localhost:3001")}`);
12847
+ console.log(` API: ${chalk47.cyan("http://localhost:3002")}`);
11757
12848
  }
11758
- console.log(chalk42.dim("\nPress Ctrl+C to stop\n"));
12849
+ console.log(chalk47.dim("\nPress Ctrl+C to stop\n"));
11759
12850
  const child = isProduction ? spawn("node", [bundledServer], {
11760
- stdio: "inherit"
12851
+ stdio: "inherit",
12852
+ env: { ...process.env, DASHBOARD_PORT: "3010" }
11761
12853
  }) : spawn("npm", ["run", "dev"], {
11762
12854
  cwd: srcDashboard,
11763
12855
  stdio: "inherit",
11764
12856
  shell: true
11765
12857
  });
11766
12858
  child.on("error", (err) => {
11767
- console.error(chalk42.red("Failed to start dashboard:"), err.message);
12859
+ console.error(chalk47.red("Failed to start dashboard:"), err.message);
11768
12860
  process.exit(1);
11769
12861
  });
11770
12862
  }
11771
12863
  });
11772
12864
  program.command("down").description("Stop dashboard (and Traefik if enabled)").option("--skip-traefik", "Skip Traefik shutdown").action(async (options) => {
11773
- const { execSync: execSync10 } = await import("child_process");
11774
- const { join: join46 } = await import("path");
11775
- const { readFileSync: readFileSync38, existsSync: existsSync45 } = await import("fs");
12865
+ const { execSync: execSync11 } = await import("child_process");
12866
+ const { join: join49 } = await import("path");
12867
+ const { readFileSync: readFileSync41, existsSync: existsSync48 } = await import("fs");
11776
12868
  const { parse: parse2 } = await import("@iarna/toml");
11777
- console.log(chalk42.bold("Stopping Panopticon...\n"));
11778
- console.log(chalk42.dim("Stopping dashboard..."));
12869
+ console.log(chalk47.bold("Stopping Panopticon...\n"));
12870
+ console.log(chalk47.dim("Stopping dashboard..."));
11779
12871
  try {
11780
- execSync10("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
11781
- execSync10("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
11782
- console.log(chalk42.green("\u2713 Dashboard stopped"));
12872
+ execSync11("lsof -ti:3001 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
12873
+ execSync11("lsof -ti:3002 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
12874
+ execSync11("lsof -ti:3010 | xargs kill -9 2>/dev/null || true", { stdio: "pipe" });
12875
+ console.log(chalk47.green("\u2713 Dashboard stopped"));
11783
12876
  } catch {
11784
- console.log(chalk42.dim(" No dashboard processes found"));
12877
+ console.log(chalk47.dim(" No dashboard processes found"));
11785
12878
  }
11786
- const configFile = join46(process.env.HOME || "", ".panopticon", "config.toml");
12879
+ const configFile = join49(process.env.HOME || "", ".panopticon", "config.toml");
11787
12880
  let traefikEnabled = false;
11788
- if (existsSync45(configFile)) {
12881
+ if (existsSync48(configFile)) {
11789
12882
  try {
11790
- const configContent = readFileSync38(configFile, "utf-8");
12883
+ const configContent = readFileSync41(configFile, "utf-8");
11791
12884
  const config2 = parse2(configContent);
11792
12885
  traefikEnabled = config2.traefik?.enabled === true;
11793
12886
  } catch (error) {
11794
12887
  }
11795
12888
  }
11796
12889
  if (traefikEnabled && !options.skipTraefik) {
11797
- const traefikDir = join46(process.env.HOME || "", ".panopticon", "traefik");
11798
- if (existsSync45(traefikDir)) {
11799
- console.log(chalk42.dim("Stopping Traefik..."));
12890
+ const traefikDir = join49(process.env.HOME || "", ".panopticon", "traefik");
12891
+ if (existsSync48(traefikDir)) {
12892
+ console.log(chalk47.dim("Stopping Traefik..."));
11800
12893
  try {
11801
- execSync10("docker-compose down", {
12894
+ execSync11("docker-compose down", {
11802
12895
  cwd: traefikDir,
11803
12896
  stdio: "pipe"
11804
12897
  });
11805
- console.log(chalk42.green("\u2713 Traefik stopped"));
12898
+ console.log(chalk47.green("\u2713 Traefik stopped"));
11806
12899
  } catch (error) {
11807
- console.log(chalk42.yellow("\u26A0 Failed to stop Traefik"));
12900
+ console.log(chalk47.yellow("\u26A0 Failed to stop Traefik"));
11808
12901
  }
11809
12902
  }
11810
12903
  }