panopticon-cli 0.5.1 → 0.5.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.
Files changed (120) hide show
  1. package/dist/{agents-5OPQKM5K.js → agents-HNMF52RM.js} +7 -6
  2. package/dist/{chunk-F5555J3A.js → chunk-4HST45MO.js} +13 -27
  3. package/dist/chunk-4HST45MO.js.map +1 -0
  4. package/dist/{chunk-YLPSQAM2.js → chunk-565HZ6VV.js} +2 -2
  5. package/dist/chunk-6N2KBSJA.js +452 -0
  6. package/dist/chunk-6N2KBSJA.js.map +1 -0
  7. package/dist/{chunk-4YSYJ4HM.js → chunk-DFNVHK3N.js} +2 -2
  8. package/dist/{chunk-7SN4L4PH.js → chunk-HOGYHJ2G.js} +2 -2
  9. package/dist/{chunk-FTCPTHIJ.js → chunk-HRU7S4TA.js} +24 -7
  10. package/dist/chunk-HRU7S4TA.js.map +1 -0
  11. package/dist/{chunk-OWHXCGVO.js → chunk-ID4OYXVH.js} +378 -101
  12. package/dist/chunk-ID4OYXVH.js.map +1 -0
  13. package/dist/{chunk-NLQRED36.js → chunk-KBHRXV5T.js} +3 -3
  14. package/dist/chunk-KBHRXV5T.js.map +1 -0
  15. package/dist/{chunk-VHKSS7QX.js → chunk-KY2E2Q3T.js} +25 -19
  16. package/dist/chunk-KY2E2Q3T.js.map +1 -0
  17. package/dist/{chunk-CWELWPWQ.js → chunk-MOPGR3CL.js} +1 -1
  18. package/dist/chunk-MOPGR3CL.js.map +1 -0
  19. package/dist/{chunk-2V4NF7J2.js → chunk-RLZQB7HS.js} +2 -2
  20. package/dist/chunk-RLZQB7HS.js.map +1 -0
  21. package/dist/{chunk-76F6DSVS.js → chunk-T7BBPDEJ.js} +11 -7
  22. package/dist/chunk-T7BBPDEJ.js.map +1 -0
  23. package/dist/chunk-USYP2SBE.js +317 -0
  24. package/dist/chunk-USYP2SBE.js.map +1 -0
  25. package/dist/{chunk-JM6V62LT.js → chunk-ZDNQFWR5.js} +2 -2
  26. package/dist/{chunk-JM6V62LT.js.map → chunk-ZDNQFWR5.js.map} +1 -1
  27. package/dist/{chunk-HJSM6E6U.js → chunk-ZP6EWSZV.js} +29 -322
  28. package/dist/chunk-ZP6EWSZV.js.map +1 -0
  29. package/dist/{chunk-PELXV435.js → chunk-ZTYHZMEC.js} +2 -2
  30. package/dist/chunk-ZTYHZMEC.js.map +1 -0
  31. package/dist/cli/index.js +1390 -777
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/config-yaml-OVZLKFMA.js +18 -0
  34. package/dist/dashboard/prompts/merge-agent.md +7 -5
  35. package/dist/dashboard/prompts/review-agent.md +12 -1
  36. package/dist/dashboard/prompts/test-agent.md +3 -1
  37. package/dist/dashboard/public/assets/index-DA6pnizT.js +767 -0
  38. package/dist/dashboard/public/assets/index-DSvt5pPn.css +32 -0
  39. package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  40. package/dist/dashboard/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  41. package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  42. package/dist/dashboard/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  43. package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  44. package/dist/dashboard/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  45. package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  46. package/dist/dashboard/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  47. package/dist/dashboard/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  48. package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  49. package/dist/dashboard/public/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  50. package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-BflQw4A9.woff +0 -0
  51. package/dist/dashboard/public/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
  52. package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  53. package/dist/dashboard/public/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  54. package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
  55. package/dist/dashboard/public/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
  56. package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-DxxdqCpr.woff2 +0 -0
  57. package/dist/dashboard/public/assets/space-grotesk-latin-ext-600-normal-VcznFIpX.woff +0 -0
  58. package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  59. package/dist/dashboard/public/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  60. package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
  61. package/dist/dashboard/public/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
  62. package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-D6zpsUhD.woff +0 -0
  63. package/dist/dashboard/public/assets/space-grotesk-vietnamese-600-normal-DUi7WF5p.woff2 +0 -0
  64. package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  65. package/dist/dashboard/public/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  66. package/dist/dashboard/public/index.html +5 -3
  67. package/dist/dashboard/server.js +4728 -2767
  68. package/dist/{feedback-writer-VRMMWWTW.js → feedback-writer-T43PI5S2.js} +2 -2
  69. package/dist/{hume-WMAUBBV2.js → hume-CKJJ3OUU.js} +3 -3
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/{projects-CFX3RTDL.js → projects-KVM3MN3Y.js} +2 -2
  73. package/dist/{remote-agents-TFSMW7GN.js → remote-agents-ULPD6C5U.js} +3 -3
  74. package/dist/{remote-workspace-7FPGF2RM.js → remote-workspace-XX6ARE6I.js} +3 -3
  75. package/dist/{review-status-TDPSOU5J.js → review-status-XKUKZF6J.js} +3 -2
  76. package/dist/{specialist-context-WGUUYDWY.js → specialist-context-C66TEMXS.js} +6 -5
  77. package/dist/{specialist-context-WGUUYDWY.js.map → specialist-context-C66TEMXS.js.map} +1 -1
  78. package/dist/{specialist-logs-XJB5TCKJ.js → specialist-logs-CJKXM3SR.js} +6 -5
  79. package/dist/{specialists-5LBRHYFA.js → specialists-NXYD4Z62.js} +6 -5
  80. package/dist/{traefik-WFMQX2LY.js → traefik-5GL3Q7DJ.js} +3 -3
  81. package/dist/{tunnel-W2GZBLEV.js → tunnel-BKC7KLBX.js} +3 -3
  82. package/dist/{workspace-manager-E434Z45T.js → workspace-manager-ALBR62AS.js} +5 -5
  83. package/dist/workspace-manager-ALBR62AS.js.map +1 -0
  84. package/package.json +1 -1
  85. package/scripts/record-cost-event.js +8424 -42
  86. package/scripts/recover-costs-deep.mjs +209 -0
  87. package/scripts/recover-costs-proportional.mjs +206 -0
  88. package/scripts/recover-costs.mjs +169 -0
  89. package/scripts/work-agent-stop-hook +221 -24
  90. package/dist/chunk-2V4NF7J2.js.map +0 -1
  91. package/dist/chunk-76F6DSVS.js.map +0 -1
  92. package/dist/chunk-CWELWPWQ.js.map +0 -1
  93. package/dist/chunk-F5555J3A.js.map +0 -1
  94. package/dist/chunk-FTCPTHIJ.js.map +0 -1
  95. package/dist/chunk-HJSM6E6U.js.map +0 -1
  96. package/dist/chunk-NLQRED36.js.map +0 -1
  97. package/dist/chunk-OWHXCGVO.js.map +0 -1
  98. package/dist/chunk-PELXV435.js.map +0 -1
  99. package/dist/chunk-VHKSS7QX.js.map +0 -1
  100. package/dist/chunk-YGJ54GW2.js +0 -96
  101. package/dist/chunk-YGJ54GW2.js.map +0 -1
  102. package/dist/dashboard/public/assets/index-Ce6q21Fm.js +0 -743
  103. package/dist/dashboard/public/assets/index-NzpI0ItZ.css +0 -32
  104. package/dist/git-utils-I2UDKNZH.js +0 -131
  105. package/dist/git-utils-I2UDKNZH.js.map +0 -1
  106. /package/dist/{agents-5OPQKM5K.js.map → agents-HNMF52RM.js.map} +0 -0
  107. /package/dist/{chunk-YLPSQAM2.js.map → chunk-565HZ6VV.js.map} +0 -0
  108. /package/dist/{chunk-4YSYJ4HM.js.map → chunk-DFNVHK3N.js.map} +0 -0
  109. /package/dist/{chunk-7SN4L4PH.js.map → chunk-HOGYHJ2G.js.map} +0 -0
  110. /package/dist/{hume-WMAUBBV2.js.map → config-yaml-OVZLKFMA.js.map} +0 -0
  111. /package/dist/{feedback-writer-VRMMWWTW.js.map → feedback-writer-T43PI5S2.js.map} +0 -0
  112. /package/dist/{projects-CFX3RTDL.js.map → hume-CKJJ3OUU.js.map} +0 -0
  113. /package/dist/{remote-agents-TFSMW7GN.js.map → projects-KVM3MN3Y.js.map} +0 -0
  114. /package/dist/{review-status-TDPSOU5J.js.map → remote-agents-ULPD6C5U.js.map} +0 -0
  115. /package/dist/{remote-workspace-7FPGF2RM.js.map → remote-workspace-XX6ARE6I.js.map} +0 -0
  116. /package/dist/{specialist-logs-XJB5TCKJ.js.map → review-status-XKUKZF6J.js.map} +0 -0
  117. /package/dist/{specialists-5LBRHYFA.js.map → specialist-logs-CJKXM3SR.js.map} +0 -0
  118. /package/dist/{traefik-WFMQX2LY.js.map → specialists-NXYD4Z62.js.map} +0 -0
  119. /package/dist/{tunnel-W2GZBLEV.js.map → traefik-5GL3Q7DJ.js.map} +0 -0
  120. /package/dist/{workspace-manager-E434Z45T.js.map → tunnel-BKC7KLBX.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/cost.ts","../src/lib/cost-parsers/jsonl-parser.ts","../src/lib/cloister/specialist-handoff-logger.ts","../src/lib/cloister/specialists.ts","../src/lib/cloister/specialist-logs.ts"],"sourcesContent":["/**\n * Cost Tracking System\n *\n * Track AI usage costs per feature, issue, and project.\n * Supports multiple AI providers with configurable pricing.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { COSTS_DIR } from './paths.js';\n\n// ============== Types ==============\n\nexport type AIProvider = 'anthropic' | 'openai' | 'google' | 'custom';\n\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n cacheTTL?: '5m' | '1h'; // Cache write TTL (default: '5m')\n}\n\nexport interface CostEntry {\n id: string;\n timestamp: string;\n provider: AIProvider;\n model: string;\n issueId?: string;\n featureId?: string;\n agentId?: string;\n operation: string;\n usage: TokenUsage;\n cost: number;\n currency: string;\n metadata?: Record<string, any>;\n}\n\nexport interface CostSummary {\n totalCost: number;\n currency: string;\n period: {\n start: string;\n end: string;\n };\n byProvider: Record<AIProvider, number>;\n byModel: Record<string, number>;\n byIssue: Record<string, number>;\n byFeature: Record<string, number>;\n entryCount: number;\n totalTokens: {\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n total: number;\n };\n}\n\nexport interface CostBudget {\n id: string;\n name: string;\n type: 'issue' | 'feature' | 'project' | 'daily' | 'monthly';\n limit: number;\n currency: string;\n spent: number;\n alertThreshold: number; // e.g., 0.8 = alert at 80%\n enabled: boolean;\n}\n\nexport interface ModelPricing {\n provider: AIProvider;\n model: string;\n inputPer1k: number;\n outputPer1k: number;\n cacheReadPer1k?: number;\n cacheWrite5mPer1k?: number; // 5-minute TTL (default)\n cacheWrite1hPer1k?: number; // 1-hour TTL\n currency: string;\n}\n\n// ============== Pricing Data ==============\n\nexport const DEFAULT_PRICING: ModelPricing[] = [\n // Anthropic - 4.6 series (API IDs use dashes: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5)\n { provider: 'anthropic', model: 'claude-opus-4-6', inputPer1k: 0.005, outputPer1k: 0.025, cacheReadPer1k: 0.0005, cacheWrite5mPer1k: 0.00625, cacheWrite1hPer1k: 0.01, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-sonnet-4-6', inputPer1k: 0.003, outputPer1k: 0.015, cacheReadPer1k: 0.0003, cacheWrite5mPer1k: 0.00375, cacheWrite1hPer1k: 0.006, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-haiku-4-5', inputPer1k: 0.001, outputPer1k: 0.005, cacheReadPer1k: 0.0001, cacheWrite5mPer1k: 0.00125, cacheWrite1hPer1k: 0.002, currency: 'USD' },\n // Anthropic - 4.x series\n { provider: 'anthropic', model: 'claude-opus-4-1', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 0.0015, cacheWrite5mPer1k: 0.01875, cacheWrite1hPer1k: 0.03, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-opus-4', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 0.0015, cacheWrite5mPer1k: 0.01875, cacheWrite1hPer1k: 0.03, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-sonnet-4', inputPer1k: 0.003, outputPer1k: 0.015, cacheReadPer1k: 0.0003, cacheWrite5mPer1k: 0.00375, cacheWrite1hPer1k: 0.006, currency: 'USD' },\n // Anthropic - Legacy\n { provider: 'anthropic', model: 'claude-haiku-3', inputPer1k: 0.00025, outputPer1k: 0.00125, cacheReadPer1k: 0.00003, cacheWrite5mPer1k: 0.0003, cacheWrite1hPer1k: 0.0005, currency: 'USD' },\n // OpenAI\n { provider: 'openai', model: 'gpt-4-turbo', inputPer1k: 0.01, outputPer1k: 0.03, currency: 'USD' },\n { provider: 'openai', model: 'gpt-4o', inputPer1k: 0.005, outputPer1k: 0.015, currency: 'USD' },\n { provider: 'openai', model: 'gpt-4o-mini', inputPer1k: 0.00015, outputPer1k: 0.0006, currency: 'USD' },\n // Google\n { provider: 'google', model: 'gemini-1.5-pro', inputPer1k: 0.00125, outputPer1k: 0.005, currency: 'USD' },\n { provider: 'google', model: 'gemini-1.5-flash', inputPer1k: 0.000075, outputPer1k: 0.0003, currency: 'USD' },\n // Moonshot AI (Kimi)\n { provider: 'custom', model: 'kimi-for-coding', inputPer1k: 0.0006, outputPer1k: 0.002, cacheReadPer1k: 0.00006, cacheWrite5mPer1k: 0.00075, currency: 'USD' },\n { provider: 'custom', model: 'kimi-k2.5', inputPer1k: 0.0006, outputPer1k: 0.002, cacheReadPer1k: 0.00006, cacheWrite5mPer1k: 0.00075, currency: 'USD' },\n];\n\n// ============== Cost Calculation ==============\n\n/**\n * Calculate cost for token usage\n */\nexport function calculateCost(usage: TokenUsage, pricing: ModelPricing): number {\n let cost = 0;\n let inputMultiplier = 1;\n let outputMultiplier = 1;\n\n // Long-context pricing for Sonnet 4/4.5 (>200K total input tokens)\n // Total input includes: inputTokens + cacheReadTokens + cacheWriteTokens\n const totalInputTokens = usage.inputTokens\n + (usage.cacheReadTokens || 0)\n + (usage.cacheWriteTokens || 0);\n\n if ((pricing.model === 'claude-sonnet-4' || pricing.model === 'claude-sonnet-4-6')\n && totalInputTokens > 200000) {\n inputMultiplier = 2; // $6/MTok vs $3/MTok\n outputMultiplier = 1.5; // $22.50/MTok vs $15/MTok\n }\n\n // Input tokens\n cost += (usage.inputTokens / 1000) * pricing.inputPer1k * inputMultiplier;\n\n // Output tokens\n cost += (usage.outputTokens / 1000) * pricing.outputPer1k * outputMultiplier;\n\n // Cache read tokens (not affected by long-context multiplier)\n if (usage.cacheReadTokens && pricing.cacheReadPer1k) {\n cost += (usage.cacheReadTokens / 1000) * pricing.cacheReadPer1k;\n }\n\n // Cache write tokens - use TTL-appropriate pricing\n if (usage.cacheWriteTokens) {\n const ttl = usage.cacheTTL || '5m';\n const cacheWritePrice = ttl === '1h'\n ? pricing.cacheWrite1hPer1k\n : pricing.cacheWrite5mPer1k;\n if (cacheWritePrice) {\n cost += (usage.cacheWriteTokens / 1000) * cacheWritePrice;\n }\n }\n\n return Math.round(cost * 1000000) / 1000000; // Round to 6 decimal places\n}\n\n/**\n * Get pricing for a model\n */\nexport function getPricing(provider: AIProvider, model: string): ModelPricing | null {\n // Try exact match first\n let pricing = DEFAULT_PRICING.find(\n p => p.provider === provider && p.model === model\n );\n\n if (!pricing) {\n // Try partial match (e.g., \"claude-sonnet-4-20250101\" matches \"claude-sonnet-4\")\n pricing = DEFAULT_PRICING.find(\n p => p.provider === provider && model.startsWith(p.model)\n );\n }\n\n return pricing || null;\n}\n\n// ============== Cost Logging ==============\n\nfunction getCostFile(date: string): string {\n return join(COSTS_DIR, `costs-${date}.jsonl`);\n}\n\nfunction getCurrentDateString(): string {\n return new Date().toISOString().split('T')[0];\n}\n\n/**\n * Log a cost entry\n */\nexport function logCost(entry: Omit<CostEntry, 'id' | 'timestamp'>): CostEntry {\n mkdirSync(COSTS_DIR, { recursive: true });\n\n const fullEntry: CostEntry = {\n ...entry,\n id: `cost-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: new Date().toISOString(),\n };\n\n const costFile = getCostFile(getCurrentDateString());\n appendFileSync(costFile, JSON.stringify(fullEntry) + '\\n');\n\n return fullEntry;\n}\n\n/**\n * Log cost from token usage\n */\nexport function logUsage(\n provider: AIProvider,\n model: string,\n usage: TokenUsage,\n options: {\n issueId?: string;\n featureId?: string;\n agentId?: string;\n operation?: string;\n metadata?: Record<string, any>;\n } = {}\n): CostEntry | null {\n const pricing = getPricing(provider, model);\n if (!pricing) {\n console.warn(`No pricing found for ${provider}/${model}`);\n return null;\n }\n\n const cost = calculateCost(usage, pricing);\n\n return logCost({\n provider,\n model,\n usage,\n cost,\n currency: pricing.currency,\n operation: options.operation || 'api_call',\n issueId: options.issueId,\n featureId: options.featureId,\n agentId: options.agentId,\n metadata: options.metadata,\n });\n}\n\n// ============== Cost Reading ==============\n\n/**\n * Read cost entries for a date range\n */\nexport function readCosts(startDate: string, endDate: string): CostEntry[] {\n const entries: CostEntry[] = [];\n\n const start = new Date(startDate);\n const end = new Date(endDate);\n\n for (let date = start; date <= end; date.setDate(date.getDate() + 1)) {\n const dateStr = date.toISOString().split('T')[0];\n const costFile = getCostFile(dateStr);\n\n if (existsSync(costFile)) {\n const content = readFileSync(costFile, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n for (const line of lines) {\n try {\n entries.push(JSON.parse(line));\n } catch {\n // Skip invalid entries\n }\n }\n }\n }\n\n return entries;\n}\n\n/**\n * Read costs for today\n */\nexport function readTodayCosts(): CostEntry[] {\n const today = getCurrentDateString();\n return readCosts(today, today);\n}\n\n/**\n * Read costs for an issue\n */\nexport function readIssueCosts(issueId: string, days: number = 30): CostEntry[] {\n const end = new Date();\n const start = new Date();\n start.setDate(start.getDate() - days);\n\n const allCosts = readCosts(\n start.toISOString().split('T')[0],\n end.toISOString().split('T')[0]\n );\n\n return allCosts.filter(entry => entry.issueId === issueId);\n}\n\n// ============== Cost Aggregation ==============\n\n/**\n * Calculate cost summary for a set of entries\n */\nexport function summarizeCosts(entries: CostEntry[]): CostSummary {\n const summary: CostSummary = {\n totalCost: 0,\n currency: 'USD',\n period: {\n start: entries[0]?.timestamp || new Date().toISOString(),\n end: entries[entries.length - 1]?.timestamp || new Date().toISOString(),\n },\n byProvider: {} as Record<AIProvider, number>,\n byModel: {},\n byIssue: {},\n byFeature: {},\n entryCount: entries.length,\n totalTokens: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n\n for (const entry of entries) {\n summary.totalCost += entry.cost;\n\n // By provider\n summary.byProvider[entry.provider] =\n (summary.byProvider[entry.provider] || 0) + entry.cost;\n\n // By model\n summary.byModel[entry.model] =\n (summary.byModel[entry.model] || 0) + entry.cost;\n\n // By issue\n if (entry.issueId) {\n summary.byIssue[entry.issueId] =\n (summary.byIssue[entry.issueId] || 0) + entry.cost;\n }\n\n // By feature\n if (entry.featureId) {\n summary.byFeature[entry.featureId] =\n (summary.byFeature[entry.featureId] || 0) + entry.cost;\n }\n\n // Tokens\n summary.totalTokens.input += entry.usage.inputTokens;\n summary.totalTokens.output += entry.usage.outputTokens;\n summary.totalTokens.cacheRead += entry.usage.cacheReadTokens || 0;\n summary.totalTokens.cacheWrite += entry.usage.cacheWriteTokens || 0;\n }\n\n // Total includes all token types\n summary.totalTokens.total = summary.totalTokens.input\n + summary.totalTokens.output\n + summary.totalTokens.cacheRead\n + summary.totalTokens.cacheWrite;\n summary.totalCost = Math.round(summary.totalCost * 100) / 100;\n\n return summary;\n}\n\n/**\n * Get daily cost summary\n */\nexport function getDailySummary(date?: string): CostSummary {\n const targetDate = date || getCurrentDateString();\n const entries = readCosts(targetDate, targetDate);\n return summarizeCosts(entries);\n}\n\n/**\n * Get weekly cost summary\n */\nexport function getWeeklySummary(): CostSummary {\n const end = new Date();\n const start = new Date();\n start.setDate(start.getDate() - 7);\n\n const entries = readCosts(\n start.toISOString().split('T')[0],\n end.toISOString().split('T')[0]\n );\n\n return summarizeCosts(entries);\n}\n\n/**\n * Get monthly cost summary\n */\nexport function getMonthlySummary(): CostSummary {\n const end = new Date();\n const start = new Date();\n start.setDate(start.getDate() - 30);\n\n const entries = readCosts(\n start.toISOString().split('T')[0],\n end.toISOString().split('T')[0]\n );\n\n return summarizeCosts(entries);\n}\n\n// ============== Cost Budgets ==============\n\nconst BUDGETS_FILE = join(COSTS_DIR, 'budgets.json');\n\nfunction loadBudgets(): CostBudget[] {\n if (!existsSync(BUDGETS_FILE)) {\n return [];\n }\n\n try {\n const content = readFileSync(BUDGETS_FILE, 'utf-8');\n return JSON.parse(content);\n } catch {\n return [];\n }\n}\n\nfunction saveBudgets(budgets: CostBudget[]): void {\n mkdirSync(COSTS_DIR, { recursive: true });\n writeFileSync(BUDGETS_FILE, JSON.stringify(budgets, null, 2));\n}\n\n/**\n * Create a cost budget\n */\nexport function createBudget(budget: Omit<CostBudget, 'id' | 'spent'>): CostBudget {\n const budgets = loadBudgets();\n\n const newBudget: CostBudget = {\n ...budget,\n id: `budget-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n spent: 0,\n };\n\n budgets.push(newBudget);\n saveBudgets(budgets);\n\n return newBudget;\n}\n\n/**\n * Get a budget by ID\n */\nexport function getBudget(id: string): CostBudget | null {\n const budgets = loadBudgets();\n return budgets.find(b => b.id === id) || null;\n}\n\n/**\n * Get all budgets\n */\nexport function getAllBudgets(): CostBudget[] {\n return loadBudgets();\n}\n\n/**\n * Update budget spent amount\n */\nexport function updateBudgetSpent(id: string, spent: number): boolean {\n const budgets = loadBudgets();\n const budget = budgets.find(b => b.id === id);\n\n if (!budget) return false;\n\n budget.spent = spent;\n saveBudgets(budgets);\n\n return true;\n}\n\n/**\n * Check budget status\n */\nexport function checkBudget(id: string): {\n budget: CostBudget | null;\n remaining: number;\n percentUsed: number;\n exceeded: boolean;\n alert: boolean;\n} {\n const budget = getBudget(id);\n\n if (!budget) {\n return {\n budget: null,\n remaining: 0,\n percentUsed: 0,\n exceeded: false,\n alert: false,\n };\n }\n\n const remaining = budget.limit - budget.spent;\n const percentUsed = budget.spent / budget.limit;\n\n return {\n budget,\n remaining,\n percentUsed,\n exceeded: percentUsed >= 1,\n alert: percentUsed >= budget.alertThreshold,\n };\n}\n\n/**\n * Delete a budget\n */\nexport function deleteBudget(id: string): boolean {\n const budgets = loadBudgets();\n const index = budgets.findIndex(b => b.id === id);\n\n if (index === -1) return false;\n\n budgets.splice(index, 1);\n saveBudgets(budgets);\n\n return true;\n}\n\n// ============== Reports ==============\n\n/**\n * Generate a cost report\n */\nexport function generateReport(startDate: string, endDate: string): string {\n const entries = readCosts(startDate, endDate);\n const summary = summarizeCosts(entries);\n\n const lines: string[] = [\n '# Cost Report',\n '',\n `**Period:** ${startDate} to ${endDate}`,\n '',\n '## Summary',\n '',\n `- **Total Cost:** $${summary.totalCost.toFixed(2)}`,\n `- **Total Entries:** ${summary.entryCount}`,\n `- **Total Tokens:** ${summary.totalTokens.total.toLocaleString()}`,\n ` - Input: ${summary.totalTokens.input.toLocaleString()}`,\n ` - Output: ${summary.totalTokens.output.toLocaleString()}`,\n '',\n '## By Provider',\n '',\n ];\n\n for (const [provider, cost] of Object.entries(summary.byProvider)) {\n lines.push(`- **${provider}:** $${cost.toFixed(2)}`);\n }\n\n lines.push('');\n lines.push('## By Model');\n lines.push('');\n\n for (const [model, cost] of Object.entries(summary.byModel)) {\n lines.push(`- **${model}:** $${cost.toFixed(2)}`);\n }\n\n if (Object.keys(summary.byIssue).length > 0) {\n lines.push('');\n lines.push('## By Issue');\n lines.push('');\n\n const sortedIssues = Object.entries(summary.byIssue)\n .sort(([, a], [, b]) => b - a);\n\n for (const [issue, cost] of sortedIssues.slice(0, 10)) {\n lines.push(`- **${issue}:** $${cost.toFixed(2)}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format cost for display\n */\nexport function formatCost(cost: number, currency: string = 'USD'): string {\n if (currency === 'USD') {\n return `$${cost.toFixed(4)}`;\n }\n return `${cost.toFixed(4)} ${currency}`;\n}\n","/**\n * Claude Code JSONL Parser\n *\n * Parse token usage from Claude Code session files.\n * Session files are stored at: ~/.claude/projects/<project-path-hash>/<session-id>.jsonl\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { TokenUsage, calculateCost, getPricing, AIProvider, logCost, CostEntry } from '../cost.js';\n\n// Claude Code JSONL message format\nexport interface ClaudeMessage {\n sessionId?: string;\n timestamp?: string;\n parentMessageId?: string;\n message?: {\n id?: string;\n role?: 'user' | 'assistant';\n model?: string;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n };\n };\n // Some messages have usage at top level\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n };\n model?: string;\n}\n\n// Parsed session usage\nexport interface SessionUsage {\n sessionId: string;\n sessionFile: string;\n startTime: string;\n endTime: string;\n model: string; // Display name (normalized). Shows \"sonnet-4-6 → opus-4-6\" for upgrades\n usage: TokenUsage; // Total tokens across all models\n cost: number; // DEPRECATED: Uses first-model pricing (kept for backward compatibility)\n cost_v2?: number; // NEW: Accurate per-message pricing\n messageCount: number;\n modelBreakdown?: Record<string, { // NEW: Cost/token breakdown by exact model ID\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n }>;\n}\n\n// Claude projects directory\nconst CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');\n\n/**\n * Get all Claude Code project directories\n */\nexport function getProjectDirs(): string[] {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n return [];\n }\n\n return readdirSync(CLAUDE_PROJECTS_DIR)\n .map(name => join(CLAUDE_PROJECTS_DIR, name))\n .filter(path => {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Get session JSONL files for a project directory\n */\nexport function getSessionFiles(projectDir: string): string[] {\n if (!existsSync(projectDir)) {\n return [];\n }\n\n return readdirSync(projectDir)\n .filter(name => name.endsWith('.jsonl'))\n .map(name => join(projectDir, name))\n .sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Get all session files across all projects\n */\nexport function getAllSessionFiles(): string[] {\n const files: string[] = [];\n\n for (const projectDir of getProjectDirs()) {\n files.push(...getSessionFiles(projectDir));\n }\n\n return files.sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Normalize model name for pricing lookup\n */\nexport function normalizeModelName(model: string): { provider: AIProvider; model: string } {\n // Claude models\n if (model.includes('claude')) {\n let normalizedModel = model;\n\n // Map full model IDs to pricing model names\n // Order matters - check more specific patterns first\n\n // Opus models\n if (model.includes('opus-4-6') || model.includes('opus-4.6')) {\n normalizedModel = 'claude-opus-4-6';\n } else if (model.includes('opus-4-1') || model.includes('opus-4.1')) {\n normalizedModel = 'claude-opus-4-1';\n } else if (model.includes('opus-4') || model.includes('opus')) {\n normalizedModel = 'claude-opus-4';\n }\n\n // Sonnet models\n if (model.includes('sonnet-4-5') || model.includes('sonnet-4.5')) {\n normalizedModel = 'claude-sonnet-4-6';\n } else if (model.includes('sonnet-4') || model.includes('sonnet')) {\n normalizedModel = 'claude-sonnet-4';\n }\n\n // Haiku models - default to 4.5 (current), support 3 for legacy\n if (model.includes('haiku-4-5') || model.includes('haiku-4.5')) {\n normalizedModel = 'claude-haiku-4-5';\n } else if (model.includes('haiku-3')) {\n normalizedModel = 'claude-haiku-3';\n } else if (model.includes('haiku')) {\n normalizedModel = 'claude-haiku-4-5'; // Default to current model\n }\n\n return { provider: 'anthropic', model: normalizedModel };\n }\n\n // OpenAI models\n if (model.includes('gpt')) {\n return { provider: 'openai', model };\n }\n\n // Google models\n if (model.includes('gemini')) {\n return { provider: 'google', model };\n }\n\n // Default to anthropic/claude\n return { provider: 'anthropic', model: 'claude-sonnet-4' };\n}\n\n/**\n * Parse a Claude Code session JSONL file and extract usage with per-message cost calculation\n *\n * This function calculates costs accurately for sessions that span multiple models\n * (e.g., when Claude Max auto-upgrades from Sonnet to Opus mid-conversation).\n *\n * Cost Calculation:\n * - `cost_v2`: Accurate per-message pricing. Each message is costed using its own model's pricing.\n * - `cost`: DEPRECATED. Uses first model's pricing for all messages (kept for backward compatibility).\n *\n * Model Display:\n * - Single model: \"claude-sonnet-4-6\" (normalized name)\n * - Multiple models: \"claude-sonnet-4-6 → claude-opus-4-6\" (progression of normalized names)\n *\n * Model Breakdown:\n * - Keys are exact model IDs (e.g., \"claude-sonnet-4-5-20250929\")\n * - Values contain per-model cost, token counts, and message count\n *\n * @param sessionFile - Path to the .jsonl session file\n * @returns Session usage summary with accurate multi-model costing, or null if no usage found\n */\nexport function parseClaudeSession(sessionFile: string): SessionUsage | null {\n if (!existsSync(sessionFile)) {\n return null;\n }\n\n const content = readFileSync(sessionFile, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n let sessionId = '';\n let startTime = '';\n let endTime = '';\n let primaryModel = '';\n let messageCount = 0;\n\n const totalUsage: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n };\n\n // NEW: Per-message cost tracking\n const modelBreakdown: Record<string, {\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n }> = {};\n let totalCostV2 = 0;\n\n for (const line of lines) {\n try {\n const msg: ClaudeMessage = JSON.parse(line);\n\n // Extract session ID from first message\n if (msg.sessionId && !sessionId) {\n sessionId = msg.sessionId;\n }\n\n // Track timestamps\n if (msg.timestamp) {\n if (!startTime || msg.timestamp < startTime) {\n startTime = msg.timestamp;\n }\n if (!endTime || msg.timestamp > endTime) {\n endTime = msg.timestamp;\n }\n }\n\n // Extract usage - can be in message.usage or top-level usage\n const usage = msg.message?.usage || msg.usage;\n const modelId = msg.message?.model || msg.model; // Exact model ID\n\n if (usage) {\n // Accumulate total tokens (existing behavior)\n totalUsage.inputTokens += usage.input_tokens || 0;\n totalUsage.outputTokens += usage.output_tokens || 0;\n totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens || 0) + (usage.cache_read_input_tokens || 0);\n totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens || 0) + (usage.cache_creation_input_tokens || 0);\n messageCount++;\n\n // NEW: Calculate cost for THIS message using THIS message's model\n if (modelId) {\n // Normalize model name for pricing lookup\n const { provider, model: normalizedModel } = normalizeModelName(modelId);\n const pricing = getPricing(provider, normalizedModel);\n\n if (pricing) {\n // Create message-specific usage object\n const msgUsage: TokenUsage = {\n inputTokens: usage.input_tokens || 0,\n outputTokens: usage.output_tokens || 0,\n cacheReadTokens: usage.cache_read_input_tokens || 0,\n cacheWriteTokens: usage.cache_creation_input_tokens || 0,\n };\n\n // Calculate cost for this message\n const msgCost = calculateCost(msgUsage, pricing);\n totalCostV2 += msgCost;\n\n // Track breakdown by exact model ID\n if (!modelBreakdown[modelId]) {\n modelBreakdown[modelId] = {\n cost: 0,\n inputTokens: 0,\n outputTokens: 0,\n messageCount: 0,\n };\n }\n modelBreakdown[modelId].cost += msgCost;\n modelBreakdown[modelId].inputTokens += msgUsage.inputTokens;\n modelBreakdown[modelId].outputTokens += msgUsage.outputTokens;\n modelBreakdown[modelId].messageCount++;\n }\n }\n }\n\n // Track primary model (first model found - for backward compatibility)\n if (modelId && !primaryModel) {\n primaryModel = modelId;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // If no usage found, return null\n if (totalUsage.inputTokens === 0 && totalUsage.outputTokens === 0) {\n return null;\n }\n\n // Use filename as session ID if not found in messages\n if (!sessionId) {\n sessionId = basename(sessionFile, '.jsonl');\n }\n\n // Default model if not found\n if (!primaryModel) {\n primaryModel = 'claude-sonnet-4';\n }\n\n // Generate model display string (normalized names)\n // For multi-model sessions: \"claude-sonnet-4-6 → claude-opus-4-6\"\n // For single-model sessions: \"claude-sonnet-4-6\"\n const normalizedModels = Object.keys(modelBreakdown)\n .map(id => normalizeModelName(id).model);\n const modelDisplay = normalizedModels.length > 0\n ? (normalizedModels.length > 1\n ? normalizedModels.join(' → ')\n : normalizedModels[0])\n : normalizeModelName(primaryModel).model;\n\n // DEPRECATED: Calculate cost using first model (for backward compatibility)\n const { provider, model } = normalizeModelName(primaryModel);\n const pricing = getPricing(provider, model);\n const cost = pricing ? calculateCost(totalUsage, pricing) : 0;\n\n return {\n sessionId,\n sessionFile,\n startTime: startTime || new Date().toISOString(),\n endTime: endTime || new Date().toISOString(),\n model: modelDisplay,\n usage: totalUsage,\n cost, // DEPRECATED: First-model pricing\n cost_v2: totalCostV2 > 0 ? totalCostV2 : undefined, // NEW: Accurate per-message pricing\n messageCount,\n modelBreakdown: Object.keys(modelBreakdown).length > 0 ? modelBreakdown : undefined, // NEW: Cost breakdown by model\n };\n}\n\n/**\n * Parse all sessions and return usage summaries\n */\nexport function parseAllSessions(maxAge?: number): SessionUsage[] {\n const sessions: SessionUsage[] = [];\n const cutoffTime = maxAge ? Date.now() - maxAge : 0;\n\n for (const file of getAllSessionFiles()) {\n try {\n const stat = statSync(file);\n if (cutoffTime && stat.mtime.getTime() < cutoffTime) {\n continue;\n }\n\n const usage = parseClaudeSession(file);\n if (usage) {\n sessions.push(usage);\n }\n } catch {\n // Skip files that can't be read\n }\n }\n\n return sessions;\n}\n\n/**\n * Get recent sessions (last N days)\n */\nexport function getRecentSessions(days: number = 7): SessionUsage[] {\n const maxAge = days * 24 * 60 * 60 * 1000;\n return parseAllSessions(maxAge);\n}\n\n/**\n * Get the active session model for a workspace\n * Returns the full model ID (e.g., \"claude-sonnet-4-5-20250929\") from the most recent session file\n *\n * NOTE: Claude Max can auto-upgrade models mid-session (e.g., Sonnet → Opus).\n * We read from the END of the file to get the CURRENT model, not the initial one.\n */\nexport function getActiveSessionModel(workspacePath: string): string | null {\n try {\n // Convert workspace path to Claude project dir name\n // e.g., /home/user/projects/myn/workspaces/feature-min-664\n // -> -home-user-projects-myn-workspaces-feature-min-664\n // NOTE: The directory name KEEPS the leading dash\n const projectDirName = workspacePath.replace(/\\//g, '-');\n const projectDir = join(CLAUDE_PROJECTS_DIR, projectDirName);\n\n // Find most recently modified session file\n const sessions = getSessionFiles(projectDir);\n if (sessions.length === 0) {\n return null;\n }\n\n // Parse the most recent session file to find model\n const mostRecentSession = sessions[0]; // Already sorted by mtime\n const content = readFileSync(mostRecentSession, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n // Read from END of file to get CURRENT model (may have been auto-upgraded by Claude Max)\n // Look at last 100 lines to find the most recent model entry\n const searchLines = lines.slice(-100);\n for (let i = searchLines.length - 1; i >= 0; i--) {\n try {\n const msg: ClaudeMessage = JSON.parse(searchLines[i]);\n const model = msg.message?.model || msg.model;\n // Skip synthetic/placeholder model values\n if (model && model !== '<synthetic>') {\n return model; // Return full model ID\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // Fallback: check first few lines if nothing found at end\n for (let i = 0; i < Math.min(lines.length, 10); i++) {\n try {\n const msg: ClaudeMessage = JSON.parse(lines[i]);\n const model = msg.message?.model || msg.model;\n if (model && model !== '<synthetic>') {\n return model;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return null;\n } catch (error) {\n console.warn('Failed to get active session model:', error);\n return null;\n }\n}\n\n/**\n * Import session usage to cost log\n */\nexport function importSessionToCostLog(\n session: SessionUsage,\n options: {\n issueId?: string;\n agentId?: string;\n operation?: string;\n } = {}\n): CostEntry | null {\n const { provider, model } = normalizeModelName(session.model);\n const pricing = getPricing(provider, model);\n\n if (!pricing) {\n console.warn(`No pricing found for ${session.model}`);\n return null;\n }\n\n return logCost({\n provider,\n model,\n usage: session.usage,\n cost: session.cost,\n currency: 'USD',\n operation: options.operation || 'claude_session',\n issueId: options.issueId,\n agentId: options.agentId,\n metadata: {\n sessionId: session.sessionId,\n sessionFile: session.sessionFile,\n startTime: session.startTime,\n endTime: session.endTime,\n messageCount: session.messageCount,\n },\n });\n}\n","/**\n * Specialist Handoff Event Logger\n *\n * Logs specialist handoff events (work passing between specialist agents)\n * to JSONL file for tracking and analysis in the dashboard.\n */\n\nimport { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { PANOPTICON_HOME } from '../paths.js';\n\n/**\n * Specialist handoff event structure\n */\nexport interface SpecialistHandoff {\n id: string;\n timestamp: string; // ISO 8601\n issueId: string;\n fromSpecialist: string; // e.g., \"review-agent\"\n toSpecialist: string; // e.g., \"test-agent\"\n status: 'queued' | 'processing' | 'completed' | 'failed';\n priority: 'urgent' | 'high' | 'normal' | 'low';\n completedAt?: string; // ISO 8601\n result?: 'success' | 'failure';\n context?: {\n workspace?: string;\n branch?: string;\n prUrl?: string;\n source?: string;\n };\n}\n\n/**\n * Specialist handoff log file path\n */\nconst SPECIALIST_HANDOFF_LOG_FILE = join(PANOPTICON_HOME, 'logs', 'specialist-handoffs.jsonl');\n\n/**\n * Ensure log directory exists\n */\nfunction ensureLogDir(): void {\n const logDir = join(PANOPTICON_HOME, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n}\n\n/**\n * Log a specialist handoff event\n *\n * @param event - Specialist handoff event to log\n */\nexport function logSpecialistHandoff(event: SpecialistHandoff): void {\n ensureLogDir();\n\n const line = JSON.stringify(event) + '\\n';\n appendFileSync(SPECIALIST_HANDOFF_LOG_FILE, line, 'utf-8');\n}\n\n/**\n * Create a specialist handoff event (queued status)\n *\n * @param fromSpecialist - Source specialist (or 'issue-agent')\n * @param toSpecialist - Target specialist\n * @param issueId - Issue ID\n * @param priority - Task priority\n * @param context - Additional context\n * @returns Specialist handoff event\n */\nexport function createSpecialistHandoff(\n fromSpecialist: string,\n toSpecialist: string,\n issueId: string,\n priority: 'urgent' | 'high' | 'normal' | 'low',\n context?: {\n workspace?: string;\n branch?: string;\n prUrl?: string;\n source?: string;\n }\n): SpecialistHandoff {\n return {\n id: `${toSpecialist}-${issueId}-${Date.now()}`,\n timestamp: new Date().toISOString(),\n issueId,\n fromSpecialist,\n toSpecialist,\n status: 'queued',\n priority,\n context,\n };\n}\n\n/**\n * Read all specialist handoff events from log\n *\n * @param limit - Maximum number of events to return (most recent first)\n * @returns Array of specialist handoff events\n */\nexport function readSpecialistHandoffs(limit?: number): SpecialistHandoff[] {\n ensureLogDir();\n\n if (!existsSync(SPECIALIST_HANDOFF_LOG_FILE)) {\n return [];\n }\n\n const content = readFileSync(SPECIALIST_HANDOFF_LOG_FILE, 'utf-8');\n const lines = content.trim().split('\\n').filter(line => line.trim());\n\n const events = lines.map(line => JSON.parse(line) as SpecialistHandoff);\n\n // Return most recent first\n events.reverse();\n\n if (limit) {\n return events.slice(0, limit);\n }\n\n return events;\n}\n\n/**\n * Read specialist handoff events for a specific issue\n *\n * @param issueId - Issue ID\n * @returns Array of specialist handoff events for the issue\n */\nexport function readIssueSpecialistHandoffs(issueId: string): SpecialistHandoff[] {\n const allEvents = readSpecialistHandoffs();\n return allEvents.filter(e => e.issueId === issueId);\n}\n\n/**\n * Get specialist handoff statistics\n *\n * @returns Specialist handoff statistics\n */\nexport function getSpecialistHandoffStats(): {\n totalHandoffs: number;\n todayCount: number;\n bySpecialist: Record<string, { sent: number; received: number }>;\n byStatus: Record<string, number>;\n successRate: number;\n queueDepth: number; // Current items with 'queued' or 'processing' status\n} {\n const events = readSpecialistHandoffs();\n const today = new Date().toISOString().split('T')[0];\n\n const stats = {\n totalHandoffs: events.length,\n todayCount: 0,\n bySpecialist: {} as Record<string, { sent: number; received: number }>,\n byStatus: {} as Record<string, number>,\n successRate: 0,\n queueDepth: 0,\n };\n\n let completedCount = 0;\n let successCount = 0;\n\n for (const event of events) {\n // Count today's handoffs\n if (event.timestamp.startsWith(today)) {\n stats.todayCount++;\n }\n\n // Count by specialist (from)\n if (!stats.bySpecialist[event.fromSpecialist]) {\n stats.bySpecialist[event.fromSpecialist] = { sent: 0, received: 0 };\n }\n stats.bySpecialist[event.fromSpecialist].sent++;\n\n // Count by specialist (to)\n if (!stats.bySpecialist[event.toSpecialist]) {\n stats.bySpecialist[event.toSpecialist] = { sent: 0, received: 0 };\n }\n stats.bySpecialist[event.toSpecialist].received++;\n\n // Count by status\n stats.byStatus[event.status] = (stats.byStatus[event.status] || 0) + 1;\n\n // Count for success rate (only completed items)\n if (event.status === 'completed' || event.status === 'failed') {\n completedCount++;\n if (event.result === 'success') {\n successCount++;\n }\n }\n\n // Count queue depth (queued or processing)\n if (event.status === 'queued' || event.status === 'processing') {\n stats.queueDepth++;\n }\n }\n\n // Calculate success rate\n stats.successRate = completedCount > 0 ? successCount / completedCount : 0;\n\n return stats;\n}\n\n/**\n * Get handoffs from today\n *\n * @returns Array of specialist handoff events from today\n */\nexport function getTodaySpecialistHandoffs(): SpecialistHandoff[] {\n const events = readSpecialistHandoffs();\n const today = new Date().toISOString().split('T')[0];\n return events.filter(e => e.timestamp.startsWith(today));\n}\n","/**\n * Cloister Specialist Agents\n *\n * Manages long-running specialist agents that can be woken up on demand.\n * Specialists maintain context across invocations via session files.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, appendFileSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { randomUUID } from 'crypto';\nimport { PANOPTICON_HOME } from '../paths.js';\nimport { getAllSessionFiles, parseClaudeSession } from '../cost-parsers/jsonl-parser.js';\nimport { createSpecialistHandoff, logSpecialistHandoff } from './specialist-handoff-logger.js';\nimport { loadSettings, type ModelId } from '../settings.js';\nimport { getModelId, WorkTypeId } from '../work-type-router.js';\nimport { getProviderForModel, getProviderEnv, setupCredentialFileAuth, clearCredentialFileAuth } from '../providers.js';\nimport { sendKeysAsync, capturePaneAsync, waitForClaudePrompt, confirmDelivery } from '../tmux.js';\nimport { notifyPipeline } from '../pipeline-notifier.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Resolve git directories and branch name from a workspace path.\n * Handles both monorepo (single .git at root) and polyrepo (multiple .git in subdirs).\n * When task.branch is missing, detects it from the checked-out branch in git repos.\n */\nasync function resolveWorkspaceGitInfo(workspace: string | undefined, taskBranch: string | undefined): Promise<{\n gitDirs: string[];\n branch: string;\n isPolyrepo: boolean;\n}> {\n const gitDirs: string[] = [];\n let branch = taskBranch || 'unknown';\n\n if (!workspace || workspace === 'unknown') {\n return { gitDirs, branch, isPolyrepo: false };\n }\n\n // Detect git directories\n if (existsSync(join(workspace, '.git'))) {\n gitDirs.push(workspace);\n } else {\n try {\n const entries = readdirSync(workspace, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && existsSync(join(workspace, entry.name, '.git'))) {\n gitDirs.push(join(workspace, entry.name));\n }\n }\n } catch {}\n }\n\n // Auto-resolve branch from git when not provided\n if (branch === 'unknown' && gitDirs.length > 0) {\n try {\n const { stdout } = await execAsync(\n `cd \"${gitDirs[0]}\" && git branch --show-current`,\n { encoding: 'utf-8', timeout: 5000 }\n );\n const detected = stdout.trim();\n if (detected) {\n branch = detected;\n }\n } catch {}\n }\n\n return { gitDirs, branch, isPolyrepo: gitDirs.length > 1 };\n}\n\n/**\n * Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for a model.\n * For non-Anthropic providers (Kimi, Z.AI, etc.), returns env vars needed\n * to redirect Claude Code API calls to the correct endpoint.\n */\nfunction getProviderEnvForModel(model: string): Record<string, string> {\n const provider = getProviderForModel(model as ModelId);\n if (provider.name === 'anthropic') return {};\n\n const settings = loadSettings();\n const apiKey = settings.api_keys?.[provider.name as keyof typeof settings.api_keys];\n if (apiKey) {\n return getProviderEnv(provider, apiKey);\n }\n console.warn(`[specialist] No API key for ${provider.displayName}, falling back to Anthropic`);\n return {};\n}\n\n/**\n * Build tmux -e flags for environment variables\n */\nfunction buildTmuxEnvFlags(env: Record<string, string>): string {\n let flags = '';\n for (const [key, value] of Object.entries(env)) {\n flags += ` -e ${key}=\"${value.replace(/\"/g, '\\\\\"')}\"`;\n }\n return flags;\n}\n\nconst SPECIALISTS_DIR = join(PANOPTICON_HOME, 'specialists');\nconst REGISTRY_FILE = join(SPECIALISTS_DIR, 'registry.json');\nconst TASKS_DIR = join(SPECIALISTS_DIR, 'tasks');\n\n/**\n * Supported specialist types\n */\nexport type SpecialistType = 'merge-agent' | 'review-agent' | 'test-agent';\n\n/**\n * Specialist state\n */\nexport type SpecialistState = 'sleeping' | 'active' | 'uninitialized';\n\n/**\n * Specialist metadata\n */\nexport interface SpecialistMetadata {\n name: SpecialistType;\n displayName: string;\n description: string;\n enabled: boolean;\n autoWake: boolean;\n sessionId?: string;\n lastWake?: string; // ISO 8601 timestamp\n contextTokens?: number;\n}\n\n/**\n * Specialist status including runtime state\n */\nexport interface SpecialistStatus extends SpecialistMetadata {\n state: SpecialistState;\n isRunning: boolean;\n tmuxSession?: string;\n currentIssue?: string; // Issue ID currently being worked on\n}\n\n/**\n * Per-project specialist metadata\n */\nexport interface ProjectSpecialistMetadata {\n runCount: number;\n lastRunAt: string | null;\n lastRunStatus: 'passed' | 'failed' | 'blocked' | null;\n currentRun: string | null; // Run ID if active\n sessionId?: string; // Legacy session ID for transition period\n}\n\n/**\n * Registry of all specialist agents (per-project structure)\n */\nexport interface SpecialistRegistry {\n version: string;\n // Global defaults for specialist configuration\n defaults: {\n contextRuns: number;\n digestModel: string | null;\n retention: { maxDays: number; maxRuns: number };\n };\n // Per-project specialist metadata\n projects: {\n [projectKey: string]: {\n [specialistType: string]: ProjectSpecialistMetadata;\n };\n };\n // Legacy: Global specialists list (for backward compatibility)\n specialists?: SpecialistMetadata[];\n lastUpdated: string; // ISO 8601 timestamp\n}\n\n/**\n * Default specialist definitions\n */\nconst DEFAULT_SPECIALISTS: SpecialistMetadata[] = [\n {\n name: 'merge-agent',\n displayName: 'Merge Agent',\n description: 'PR merging and conflict resolution',\n enabled: true,\n autoWake: true,\n },\n {\n name: 'review-agent',\n displayName: 'Review Agent',\n description: 'Code review and quality checks',\n enabled: true,\n autoWake: true,\n },\n {\n name: 'test-agent',\n displayName: 'Test Agent',\n description: 'Test execution and analysis',\n enabled: true,\n autoWake: true,\n },\n];\n\n/**\n * Initialize specialists directory and registry\n *\n * Creates directory structure and default registry.json if needed.\n * Safe to call multiple times - idempotent.\n */\nexport function initSpecialistsDirectory(): void {\n // Ensure specialists directory exists\n if (!existsSync(SPECIALISTS_DIR)) {\n mkdirSync(SPECIALISTS_DIR, { recursive: true });\n }\n\n // Create default registry if it doesn't exist\n if (!existsSync(REGISTRY_FILE)) {\n const registry: SpecialistRegistry = {\n version: '2.0', // Updated for per-project structure\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: {\n maxDays: 30,\n maxRuns: 50,\n },\n },\n projects: {},\n // Keep legacy specialists for backward compatibility during transition\n specialists: DEFAULT_SPECIALISTS,\n lastUpdated: new Date().toISOString(),\n };\n saveRegistry(registry);\n } else {\n // Migrate old registry if needed\n migrateRegistryIfNeeded();\n }\n}\n\n/**\n * Migrate old registry format to new per-project structure\n */\nfunction migrateRegistryIfNeeded(): void {\n try {\n const content = readFileSync(REGISTRY_FILE, 'utf-8');\n const registry = JSON.parse(content) as SpecialistRegistry;\n\n // Check if already migrated\n if (registry.version === '2.0' || registry.projects) {\n return;\n }\n\n // Migrate to new structure\n console.log('[specialists] Migrating registry to per-project structure...');\n\n const migratedRegistry: SpecialistRegistry = {\n version: '2.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: {\n maxDays: 30,\n maxRuns: 50,\n },\n },\n projects: {},\n specialists: registry.specialists, // Keep for backward compat\n lastUpdated: new Date().toISOString(),\n };\n\n saveRegistry(migratedRegistry);\n console.log('[specialists] Registry migration complete');\n } catch (error) {\n console.error('[specialists] Failed to migrate registry:', error);\n }\n}\n\n/**\n * Load the specialist registry\n *\n * @returns Specialist registry\n */\nexport function loadRegistry(): SpecialistRegistry {\n initSpecialistsDirectory();\n\n try {\n const content = readFileSync(REGISTRY_FILE, 'utf-8');\n return JSON.parse(content);\n } catch (error) {\n console.error('Failed to load specialist registry:', error);\n // Return default registry\n return {\n version: '1.0',\n defaults: {\n contextRuns: 5,\n digestModel: null,\n retention: { maxDays: 30, maxRuns: 50 },\n },\n projects: {},\n specialists: DEFAULT_SPECIALISTS,\n lastUpdated: new Date().toISOString(),\n };\n }\n}\n\n/**\n * Save the specialist registry\n *\n * @param registry - Registry to save\n */\nexport function saveRegistry(registry: SpecialistRegistry): void {\n // Only ensure directory exists, don't call initSpecialistsDirectory to avoid recursion\n if (!existsSync(SPECIALISTS_DIR)) {\n mkdirSync(SPECIALISTS_DIR, { recursive: true });\n }\n\n registry.lastUpdated = new Date().toISOString();\n\n try {\n const content = JSON.stringify(registry, null, 2);\n writeFileSync(REGISTRY_FILE, content, 'utf-8');\n } catch (error) {\n console.error('Failed to save specialist registry:', error);\n throw error;\n }\n}\n\n/**\n * Get session file path for a specialist.\n * Per-project specialists use a project-scoped subdirectory to prevent\n * session ID collision when multiple projects share the same specialist type.\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key (per-project specialists only)\n * @returns Path to session file\n */\nexport function getSessionFilePath(name: SpecialistType, projectKey?: string): string {\n if (projectKey) {\n return join(SPECIALISTS_DIR, 'projects', projectKey, `${name}.session`);\n }\n return join(SPECIALISTS_DIR, `${name}.session`);\n}\n\n/**\n * Read session ID from file\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key (per-project specialists only)\n * @returns Session ID or null if not found\n */\nexport function getSessionId(name: SpecialistType, projectKey?: string): string | null {\n const sessionFile = getSessionFilePath(name, projectKey);\n\n if (!existsSync(sessionFile)) {\n return null;\n }\n\n try {\n return readFileSync(sessionFile, 'utf-8').trim();\n } catch (error) {\n console.error(`Failed to read session file for ${name} (${projectKey ?? 'global'}):`, error);\n return null;\n }\n}\n\n/**\n * Write session ID to file\n *\n * @param name - Specialist name\n * @param sessionId - Session ID to store\n * @param projectKey - Optional project key (per-project specialists only)\n */\nexport function setSessionId(name: SpecialistType, sessionId: string, projectKey?: string): void {\n const sessionFile = getSessionFilePath(name, projectKey);\n const dir = projectKey\n ? join(SPECIALISTS_DIR, 'projects', projectKey)\n : SPECIALISTS_DIR;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n try {\n writeFileSync(sessionFile, sessionId.trim(), 'utf-8');\n } catch (error) {\n console.error(`Failed to write session file for ${name} (${projectKey ?? 'global'}):`, error);\n throw error;\n }\n}\n\n/**\n * Delete session file\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key (per-project specialists only)\n * @returns True if file was deleted, false if it didn't exist\n */\nexport function clearSessionId(name: SpecialistType, projectKey?: string): boolean {\n const sessionFile = getSessionFilePath(name, projectKey);\n\n if (!existsSync(sessionFile)) {\n return false;\n }\n\n try {\n unlinkSync(sessionFile);\n return true;\n } catch (error) {\n console.error(`Failed to delete session file for ${name} (${projectKey ?? 'global'}):`, error);\n throw error;\n }\n}\n\n/**\n * Get metadata for a specific specialist\n *\n * @param name - Specialist name\n * @returns Specialist metadata or null if not found\n */\nexport function getSpecialistMetadata(name: SpecialistType): SpecialistMetadata | null {\n const registry = loadRegistry();\n return (registry.specialists ?? []).find((s) => s.name === name) || null;\n}\n\n/**\n * Update specialist metadata\n *\n * @param name - Specialist name\n * @param updates - Partial metadata to update\n */\nexport function updateSpecialistMetadata(\n name: SpecialistType,\n updates: Partial<SpecialistMetadata>\n): void {\n const registry = loadRegistry();\n\n const specialists = registry.specialists ?? [];\n const index = specialists.findIndex((s) => s.name === name);\n\n if (index === -1) {\n throw new Error(`Specialist ${name} not found in registry`);\n }\n\n specialists[index] = {\n ...specialists[index],\n ...updates,\n name, // Ensure name doesn't change\n };\n registry.specialists = specialists;\n\n saveRegistry(registry);\n}\n\n/**\n * Get all specialist metadata\n *\n * @returns Array of all specialists\n */\nexport function getAllSpecialists(): SpecialistMetadata[] {\n const registry = loadRegistry();\n return registry.specialists ?? [];\n}\n\n/**\n * Check if a specialist is initialized (has session file)\n *\n * @param name - Specialist name\n * @returns True if specialist has a session file\n */\nexport function isInitialized(name: SpecialistType): boolean {\n return getSessionId(name) !== null;\n}\n\n/**\n * Get the state of a specialist based on session file\n *\n * Note: This only checks if session exists, not if it's actually running.\n * Use getSpecialistStatus() for runtime state.\n *\n * @param name - Specialist name\n * @returns Specialist state\n */\nexport function getSpecialistState(name: SpecialistType): Exclude<SpecialistState, 'active'> {\n return isInitialized(name) ? 'sleeping' : 'uninitialized';\n}\n\n/**\n * Get tmux session name for a specialist\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Expected tmux session name\n */\nexport function getTmuxSessionName(name: SpecialistType, projectKey?: string): string {\n if (projectKey) {\n return `specialist-${projectKey}-${name}`;\n }\n // Legacy format for backward compatibility\n return `specialist-${name}`;\n}\n\n/**\n * Record wake event in metadata\n *\n * @param name - Specialist name\n * @param sessionId - New session ID (if changed)\n */\nexport function recordWake(name: SpecialistType, sessionId?: string): void {\n const updates: Partial<SpecialistMetadata> = {\n lastWake: new Date().toISOString(),\n };\n\n if (sessionId) {\n updates.sessionId = sessionId;\n }\n\n updateSpecialistMetadata(name, updates);\n}\n\n/**\n * ===========================================================================\n * Ephemeral Lifecycle Management\n * ===========================================================================\n */\n\n/**\n * Grace period state for a specialist\n */\nexport interface GracePeriodState {\n active: boolean;\n startedAt: string;\n duration: number; // milliseconds\n paused: boolean;\n pausedAt?: string;\n remainingTime?: number; // milliseconds when paused\n}\n\nconst gracePeriodStates = new Map<string, GracePeriodState>();\n\n/**\n * Spawn an ephemeral specialist for a project\n *\n * Creates a new specialist session that will run for this task and then terminate.\n * The specialist is seeded with context from recent runs.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param task - Task details\n * @returns Spawn result with run ID and session info\n */\nexport async function spawnEphemeralSpecialist(\n projectKey: string,\n specialistType: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n promptOverride?: string; // Use this prompt instead of building from template\n }\n): Promise<{\n success: boolean;\n runId?: string;\n tmuxSession?: string;\n message: string;\n error?: string;\n}> {\n // Ensure project specialist directory exists\n ensureProjectSpecialistDir(projectKey, specialistType);\n\n // Load context digest\n const { loadContextDigest } = await import('./specialist-context.js');\n const contextDigest = loadContextDigest(projectKey, specialistType);\n\n // Create run log\n const { createRunLog } = await import('./specialist-logs.js');\n const { runId, filePath: logFilePath } = createRunLog(\n projectKey,\n specialistType,\n task.issueId,\n contextDigest || undefined\n );\n\n // Update metadata\n setCurrentRun(projectKey, specialistType, runId);\n incrementProjectRunCount(projectKey, specialistType);\n\n // Build task prompt (use override if provided, otherwise build from template)\n const taskPrompt = task.promptOverride ?? await buildTaskPrompt(projectKey, specialistType, task, contextDigest);\n\n if (task.promptOverride) {\n console.log(`[specialist] Using promptOverride for ${projectKey}/${task.issueId} (${taskPrompt.length} chars)`);\n }\n\n // Spawn tmux session\n const tmuxSession = getTmuxSessionName(specialistType, projectKey);\n const cwd = homedir();\n\n try {\n // Check if session already exists (stale from previous run)\n try {\n const { stdout: sessions } = await execAsync('tmux list-sessions -F \"#{session_name}\" 2>/dev/null || echo \"\"', { encoding: 'utf-8' });\n if (sessions.split('\\n').map(s => s.trim()).includes(tmuxSession)) {\n const { getAgentRuntimeState } = await import('../agents.js');\n const existingState = getAgentRuntimeState(tmuxSession);\n if (existingState?.state === 'active') {\n return {\n success: false,\n message: `Specialist ${specialistType} (${projectKey}) is already running task ${existingState.currentIssue ?? 'unknown'}`,\n error: 'specialist_busy',\n };\n }\n // Stale session — kill it before spawning fresh\n console.log(`[specialist] Killing stale ${tmuxSession} session before respawn`);\n await execAsync(`tmux kill-session -t \"${tmuxSession}\"`, { encoding: 'utf-8' }).catch(() => {});\n }\n } catch {\n // Non-fatal: session check failure shouldn't block spawn\n }\n // Determine model for this specialist\n let model = 'claude-sonnet-4-6'; // default\n try {\n const workTypeId: WorkTypeId = `specialist-${specialistType}` as WorkTypeId;\n model = getModelId(workTypeId);\n } catch (error) {\n console.warn(`Warning: Could not resolve model for ${specialistType}, using default`);\n }\n\n // Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for non-Anthropic models\n const providerEnv = getProviderEnvForModel(model);\n const envFlags = buildTmuxEnvFlags(providerEnv);\n\n // For credential-file providers (e.g. Kimi), configure apiKeyHelper for token refresh.\n // For all other providers, clear stale apiKeyHelper from previous runs.\n const providerConfig = getProviderForModel(model as ModelId);\n if (providerConfig.authType === 'credential-file') {\n setupCredentialFileAuth(providerConfig, cwd);\n } else {\n clearCredentialFileAuth(cwd);\n }\n\n // Permission flags based on specialist type\n const permissionFlags = specialistType === 'merge-agent'\n ? '--dangerously-skip-permissions --permission-mode bypassPermissions'\n : '--dangerously-skip-permissions';\n\n // Write task prompt to file to avoid shell escaping issues\n const agentDir = join(homedir(), '.panopticon', 'agents', tmuxSession);\n await execAsync(`mkdir -p \"${agentDir}\"`, { encoding: 'utf-8' });\n\n const promptFile = join(agentDir, 'task-prompt.md');\n writeFileSync(promptFile, taskPrompt);\n\n // Create launcher script that pipes output to log file\n const launcherScript = join(agentDir, 'launcher.sh');\n writeFileSync(launcherScript, `#!/bin/bash\ncd \"${cwd}\"\nprompt=$(cat \"${promptFile}\")\n\n# Run Claude and tee output to log file\nclaude ${permissionFlags} --model ${model} \"$prompt\" 2>&1 | tee -a \"${logFilePath}\"\n\n# Signal completion\necho \"\"\necho \"## Specialist completed task\"\n`, { mode: 0o755 });\n\n // Spawn Claude Code via launcher script (with provider env vars)\n await execAsync(\n `tmux new-session -d -s \"${tmuxSession}\"${envFlags} \"bash '${launcherScript}'\"`,\n { encoding: 'utf-8' }\n );\n\n // Set state to active\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'active',\n lastActivity: new Date().toISOString(),\n currentIssue: task.issueId,\n });\n\n console.log(`[specialist] Spawned ephemeral ${specialistType} for ${projectKey}/${task.issueId} (run: ${runId})`);\n\n return {\n success: true,\n runId,\n tmuxSession,\n message: `Spawned specialist ${specialistType} for ${task.issueId}`,\n };\n } catch (error: any) {\n console.error(`[specialist] Failed to spawn ${specialistType}:`, error);\n\n // Clean up metadata\n setCurrentRun(projectKey, specialistType, null);\n\n return {\n success: false,\n message: `Failed to spawn specialist: ${error.message}`,\n error: error.message,\n };\n }\n}\n\n/**\n * Build task prompt for a specialist\n */\nasync function buildTaskPrompt(\n projectKey: string,\n specialistType: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n },\n contextDigest: string | null\n): Promise<string> {\n const { getSpecialistPromptOverride } = await import('../projects.js');\n const customPrompt = getSpecialistPromptOverride(projectKey, specialistType);\n\n let prompt = `# ${specialistType} Task - ${task.issueId}\\n\\n`;\n\n // Add context digest if available\n if (contextDigest) {\n prompt += `## Context from Recent Runs\\n\\n${contextDigest}\\n\\n`;\n }\n\n // Add custom project-specific prompt if configured\n if (customPrompt) {\n prompt += `## Project-Specific Guidelines\\n\\n${customPrompt}\\n\\n`;\n }\n\n // Add task details\n prompt += `## Current Task\\n\\n`;\n prompt += `Issue: ${task.issueId}\\n`;\n if (task.branch) prompt += `Branch: ${task.branch}\\n`;\n if (task.workspace) prompt += `Workspace: ${task.workspace}\\n`;\n if (task.prUrl) prompt += `PR URL: ${task.prUrl}\\n`;\n prompt += `\\n`;\n\n // Add specialist-specific instructions\n switch (specialistType) {\n case 'review-agent':\n prompt += `Your task:\n0. FIRST: Check if branch has any changes vs main (git diff --name-only main...HEAD)\n - If 0 files changed: mark as passed with note \"branch identical to main\" and STOP\n1. Review all changes in the branch\n2. Check for code quality issues, security concerns, and best practices\n3. Verify test FILES exist for new code (DO NOT run tests)\n4. Provide specific, actionable feedback\n5. Update status via API when done\n\nIMPORTANT: DO NOT run tests. You are the REVIEW agent.\n\nUpdate status via API:\n- If no changes (stale branch): POST to /api/workspaces/${task.issueId}/review-status with {\"reviewStatus\":\"passed\",\"reviewNotes\":\"No changes — branch identical to main\"}\n- If issues found: POST to /api/workspaces/${task.issueId}/review-status with {\"reviewStatus\":\"blocked\",\"reviewNotes\":\"...\"}\n- If review passes: POST with {\"reviewStatus\":\"passed\"} then queue test-agent`;\n break;\n\n case 'test-agent':\n prompt += `Your task:\n1. Run the full test suite\n2. Analyze any failures in detail\n3. Identify root causes\n4. Update status via API when done\n\nUpdate status via API:\n- If tests pass: POST to /api/workspaces/${task.issueId}/review-status with {\"testStatus\":\"passed\"}\n- If tests fail: POST with {\"testStatus\":\"failed\",\"testNotes\":\"...\"}`;\n break;\n\n case 'merge-agent': {\n const bInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n if (bInfo.isPolyrepo) {\n prompt += `This is a POLYREPO project with ${bInfo.gitDirs.length} repos: ${bInfo.gitDirs.map(d => basename(d)).join(', ')}.\nYou must merge each repo separately.\\n\\n`;\n }\n prompt += `Your task:\n1. Fetch the latest main branch\n2. Attempt to merge ${bInfo.branch} into main\n3. Resolve conflicts intelligently if needed\n4. Run tests to verify merge is clean\n5. Complete merge if tests pass\n6. NEVER use git push --force`;\n break;\n }\n }\n\n prompt += `\\n\\nWhen you complete your task, report your findings and status.`;\n\n return prompt;\n}\n\n/**\n * Start grace period for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param duration - Grace period duration in milliseconds (default: 60000)\n */\nexport function startGracePeriod(\n projectKey: string,\n specialistType: SpecialistType,\n duration: number = 60000\n): void {\n const key = `${projectKey}-${specialistType}`;\n\n gracePeriodStates.set(key, {\n active: true,\n startedAt: new Date().toISOString(),\n duration,\n paused: false,\n });\n\n console.log(`[specialist] Grace period started for ${projectKey}/${specialistType} (${duration}ms)`);\n\n // Schedule termination after grace period\n setTimeout(() => {\n const state = gracePeriodStates.get(key);\n if (state && state.active && !state.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, duration);\n}\n\n/**\n * Pause grace period countdown\n */\nexport function pauseGracePeriod(projectKey: string, specialistType: SpecialistType): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active) {\n return false;\n }\n\n const elapsed = Date.now() - new Date(state.startedAt).getTime();\n const remaining = state.duration - elapsed;\n\n state.paused = true;\n state.pausedAt = new Date().toISOString();\n state.remainingTime = remaining;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period paused for ${projectKey}/${specialistType}`);\n\n return true;\n}\n\n/**\n * Resume grace period countdown\n */\nexport function resumeGracePeriod(projectKey: string, specialistType: SpecialistType): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active || !state.paused) {\n return false;\n }\n\n state.paused = false;\n state.startedAt = new Date().toISOString();\n state.pausedAt = undefined;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period resumed for ${projectKey}/${specialistType}`);\n\n // Schedule termination for remaining time\n setTimeout(() => {\n const currentState = gracePeriodStates.get(key);\n if (currentState && currentState.active && !currentState.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, state.remainingTime || 0);\n\n return true;\n}\n\n/**\n * Exit grace period immediately (terminate now)\n */\nexport function exitGracePeriod(projectKey: string, specialistType: SpecialistType): void {\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n terminateSpecialist(projectKey, specialistType);\n}\n\n/**\n * Get grace period state\n */\nexport function getGracePeriodState(\n projectKey: string,\n specialistType: SpecialistType\n): GracePeriodState | null {\n const key = `${projectKey}-${specialistType}`;\n return gracePeriodStates.get(key) || null;\n}\n\n/**\n * Signal that a specialist has completed its task\n *\n * This should be called when the specialist finishes its work.\n * It updates the run status and starts the grace period.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param result - Task result\n */\nexport function signalSpecialistCompletion(\n projectKey: string,\n specialistType: SpecialistType,\n result: {\n status: 'passed' | 'failed' | 'blocked';\n notes?: string;\n }\n): void {\n const metadata = getProjectSpecialistMetadata(projectKey, specialistType);\n\n // Update status\n updateRunStatus(projectKey, specialistType, result.status);\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n const { finalizeRunLog } = require('./specialist-logs.js');\n\n try {\n finalizeRunLog(projectKey, specialistType, metadata.currentRun, {\n status: result.status,\n notes: result.notes,\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n }\n\n // Start grace period (60 seconds)\n startGracePeriod(projectKey, specialistType, 60000);\n\n console.log(`[specialist] ${specialistType} completed for ${projectKey} (status: ${result.status})`);\n}\n\n/**\n * Terminate a specialist session\n *\n * Kills the tmux session, finalizes logs, and schedules digest generation.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport async function terminateSpecialist(\n projectKey: string,\n specialistType: SpecialistType\n): Promise<void> {\n const tmuxSession = getTmuxSessionName(specialistType, projectKey);\n const metadata = getProjectSpecialistMetadata(projectKey, specialistType);\n\n try {\n // Kill tmux session\n await execAsync(`tmux kill-session -t \"${tmuxSession}\"`);\n console.log(`[specialist] Terminated ${projectKey}/${specialistType}`);\n } catch (error) {\n console.error(`[specialist] Failed to kill tmux session ${tmuxSession}:`, error);\n }\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n const { finalizeRunLog } = await import('./specialist-logs.js');\n\n try {\n finalizeRunLog(projectKey, specialistType, metadata.currentRun, {\n status: metadata.lastRunStatus || 'incomplete',\n notes: 'Specialist terminated',\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n\n // Clear current run\n setCurrentRun(projectKey, specialistType, null);\n }\n\n // Clear grace period state\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n // Update runtime state\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'suspended',\n lastActivity: new Date().toISOString(),\n });\n\n // Schedule digest generation (async, fire-and-forget)\n const { scheduleDigestGeneration } = await import('./specialist-context.js');\n scheduleDigestGeneration(projectKey, specialistType);\n\n // Run log cleanup for this project/specialist (async, fire-and-forget)\n scheduleLogCleanup(projectKey, specialistType);\n}\n\n/**\n * Schedule log cleanup for a project's specialist (async, fire-and-forget)\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nfunction scheduleLogCleanup(projectKey: string, specialistType: SpecialistType): void {\n // Run async without awaiting\n Promise.resolve().then(async () => {\n try {\n const { cleanupOldLogs } = await import('./specialist-logs.js');\n const { getSpecialistRetention } = await import('../projects.js');\n\n const retention = getSpecialistRetention(projectKey);\n const deleted = cleanupOldLogs(projectKey, specialistType, { maxDays: retention.max_days, maxRuns: retention.max_runs });\n\n if (deleted > 0) {\n console.log(`[specialist] Cleaned up ${deleted} old logs for ${projectKey}/${specialistType}`);\n }\n } catch (error) {\n console.error(`[specialist] Log cleanup failed for ${projectKey}/${specialistType}:`, error);\n }\n });\n}\n\n/**\n * ===========================================================================\n * Per-Project Specialist Functions\n * ===========================================================================\n */\n\n/**\n * Get the directory for a project's specialist\n */\nexport function getProjectSpecialistDir(projectKey: string, specialistType: SpecialistType): string {\n return join(SPECIALISTS_DIR, projectKey, specialistType);\n}\n\n/**\n * Ensure per-project specialist directory structure exists\n */\nexport function ensureProjectSpecialistDir(projectKey: string, specialistType: SpecialistType): void {\n const specialistDir = getProjectSpecialistDir(projectKey, specialistType);\n const runsDir = join(specialistDir, 'runs');\n const contextDir = join(specialistDir, 'context');\n\n if (!existsSync(runsDir)) {\n mkdirSync(runsDir, { recursive: true });\n }\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Get per-project specialist metadata\n */\nexport function getProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistType\n): ProjectSpecialistMetadata {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][specialistType]) {\n // Initialize with defaults\n registry.projects[projectKey][specialistType] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n saveRegistry(registry);\n }\n\n return registry.projects[projectKey][specialistType];\n}\n\n/**\n * Update per-project specialist metadata\n */\nexport function updateProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistType,\n updates: Partial<ProjectSpecialistMetadata>\n): void {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][specialistType]) {\n registry.projects[projectKey][specialistType] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n }\n\n registry.projects[projectKey][specialistType] = {\n ...registry.projects[projectKey][specialistType],\n ...updates,\n };\n\n saveRegistry(registry);\n}\n\n/**\n * Increment run count for a project's specialist\n */\nexport function incrementProjectRunCount(projectKey: string, specialistType: SpecialistType): void {\n const metadata = getProjectSpecialistMetadata(projectKey, specialistType);\n updateProjectSpecialistMetadata(projectKey, specialistType, {\n runCount: metadata.runCount + 1,\n lastRunAt: new Date().toISOString(),\n });\n}\n\n/**\n * Set current run for a project's specialist\n */\nexport function setCurrentRun(\n projectKey: string,\n specialistType: SpecialistType,\n runId: string | null\n): void {\n updateProjectSpecialistMetadata(projectKey, specialistType, { currentRun: runId });\n}\n\n/**\n * Update run status for a project's specialist\n */\nexport function updateRunStatus(\n projectKey: string,\n specialistType: SpecialistType,\n status: 'passed' | 'failed' | 'blocked' | null\n): void {\n updateProjectSpecialistMetadata(projectKey, specialistType, { lastRunStatus: status });\n}\n\n/**\n * List all projects that have specialists configured\n */\nexport function listProjectsWithSpecialists(): string[] {\n const registry = loadRegistry();\n return Object.keys(registry.projects);\n}\n\n/**\n * List all specialist types for a project\n */\nexport function listSpecialistsForProject(projectKey: string): SpecialistType[] {\n const registry = loadRegistry();\n const project = registry.projects[projectKey];\n\n if (!project) {\n return [];\n }\n\n return Object.keys(project) as SpecialistType[];\n}\n\n/**\n * Get all per-project specialist statuses\n */\nexport async function getAllProjectSpecialistStatuses(): Promise<Array<{\n projectKey: string;\n specialistType: SpecialistType;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n}>> {\n const registry = loadRegistry();\n const results: Array<{\n projectKey: string;\n specialistType: SpecialistType;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n }> = [];\n\n for (const [projectKey, specialists] of Object.entries(registry.projects)) {\n for (const [specialistType, metadata] of Object.entries(specialists)) {\n const tmuxSession = getTmuxSessionName(specialistType as SpecialistType, projectKey);\n const running = await isRunning(specialistType as SpecialistType, projectKey);\n\n results.push({\n projectKey,\n specialistType: specialistType as SpecialistType,\n metadata,\n isRunning: running,\n tmuxSession,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Update context token count for a specialist\n *\n * @param name - Specialist name\n * @param tokens - Total context tokens\n */\nexport function updateContextTokens(name: SpecialistType, tokens: number): void {\n updateSpecialistMetadata(name, { contextTokens: tokens });\n}\n\n/**\n * List all session files in the specialists directory\n *\n * @returns Array of specialist names that have session files\n */\nexport function listSessionFiles(): SpecialistType[] {\n initSpecialistsDirectory();\n\n try {\n const files = readdirSync(SPECIALISTS_DIR);\n const sessionFiles = files.filter((f) => f.endsWith('.session'));\n\n return sessionFiles.map((f) => f.replace('.session', '') as SpecialistType);\n } catch (error) {\n console.error('Failed to list session files:', error);\n return [];\n }\n}\n\n/**\n * Enable a specialist\n *\n * @param name - Specialist name\n */\nexport function enableSpecialist(name: SpecialistType): void {\n updateSpecialistMetadata(name, { enabled: true });\n}\n\n/**\n * Disable a specialist\n *\n * @param name - Specialist name\n */\nexport function disableSpecialist(name: SpecialistType): void {\n updateSpecialistMetadata(name, { enabled: false });\n}\n\n/**\n * Check if a specialist is enabled\n *\n * @param name - Specialist name\n * @returns True if specialist is enabled\n */\nexport function isEnabled(name: SpecialistType): boolean {\n const metadata = getSpecialistMetadata(name);\n return metadata?.enabled ?? false;\n}\n\n/**\n * Get all enabled specialists\n *\n * @returns Array of enabled specialists\n */\nexport function getEnabledSpecialists(): SpecialistMetadata[] {\n return getAllSpecialists().filter((s) => s.enabled);\n}\n\n/**\n * Find JSONL file for a session ID\n *\n * Searches through Claude Code project directories to find the JSONL file.\n *\n * @param sessionId - Session ID to find\n * @returns Path to JSONL file or null if not found\n */\nexport function findSessionFile(sessionId: string): string | null {\n try {\n const allFiles = getAllSessionFiles();\n\n for (const file of allFiles) {\n const fileSessionId = basename(file, '.jsonl');\n if (fileSessionId === sessionId) {\n return file;\n }\n }\n } catch {\n // Session files not available\n }\n\n return null;\n}\n\n/**\n * Count context tokens for a specialist session\n *\n * Reads the JSONL file for the specialist's session and sums all token usage.\n * This gives an approximate count of context size.\n *\n * @param name - Specialist name\n * @returns Total token count or null if session not found\n */\nexport function countContextTokens(name: SpecialistType): number | null {\n const sessionId = getSessionId(name);\n\n if (!sessionId) {\n return null;\n }\n\n const sessionFile = findSessionFile(sessionId);\n\n if (!sessionFile) {\n return null;\n }\n\n const sessionUsage = parseClaudeSession(sessionFile);\n\n if (!sessionUsage) {\n return null;\n }\n\n // Sum all token types for total context\n return (\n sessionUsage.usage.inputTokens +\n sessionUsage.usage.outputTokens +\n (sessionUsage.usage.cacheReadTokens || 0) +\n (sessionUsage.usage.cacheWriteTokens || 0)\n );\n}\n\n/**\n * Check if a specialist is currently running in tmux\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns True if specialist has an active tmux session\n */\nexport async function isRunning(name: SpecialistType, projectKey?: string): Promise<boolean> {\n const tmuxSession = getTmuxSessionName(name, projectKey);\n\n try {\n await execAsync(`tmux has-session -t ${tmuxSession}`);\n // Session exists — but check if the pane actually has a running process.\n // When Claude Code crashes, the pane's process exits but the tmux session persists,\n // making has-session return success even though nothing is running.\n const { stdout } = await execAsync(\n `tmux list-panes -t ${tmuxSession} -F \"#{pane_pid}\" 2>/dev/null`,\n { encoding: 'utf-8' }\n );\n const panePid = stdout.trim();\n if (!panePid) return false;\n // Check if the pane's process has any child processes (Claude Code / bash)\n const { stdout: children } = await execAsync(\n `ps --ppid ${panePid} --no-headers 2>/dev/null || echo \"\"`,\n { encoding: 'utf-8' }\n );\n return children.trim().length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Get complete status for a specialist\n *\n * Combines metadata, session info, and runtime state (PAN-80: uses hook-based state).\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Complete specialist status\n */\nexport async function getSpecialistStatus(\n name: SpecialistType,\n projectKey?: string\n): Promise<SpecialistStatus> {\n const metadata = getSpecialistMetadata(name) || {\n name,\n displayName: name,\n description: '',\n enabled: false,\n autoWake: false,\n };\n\n const sessionId = getSessionId(name, projectKey);\n const running = await isRunning(name, projectKey);\n const contextTokens = countContextTokens(name);\n\n // Determine state from hook-based runtime state (PAN-80)\n const { getAgentRuntimeState } = await import('../agents.js');\n const tmuxSession = getTmuxSessionName(name, projectKey);\n const runtimeState = getAgentRuntimeState(tmuxSession);\n\n let state: SpecialistState;\n if (runtimeState) {\n // Map runtime state to specialist state\n switch (runtimeState.state) {\n case 'active':\n state = 'active';\n break;\n case 'idle':\n state = 'sleeping'; // Idle = at prompt waiting\n break;\n case 'suspended':\n state = 'sleeping'; // Suspended = session saved, not running\n break;\n case 'uninitialized':\n default:\n state = 'uninitialized';\n break;\n }\n } else {\n // Fallback if no runtime state available\n if (running && sessionId) {\n state = 'sleeping';\n } else if (sessionId) {\n state = 'sleeping';\n } else {\n state = 'uninitialized';\n }\n }\n\n return {\n ...metadata,\n sessionId: sessionId || undefined,\n contextTokens: contextTokens || undefined,\n state,\n isRunning: running,\n tmuxSession: getTmuxSessionName(name, projectKey),\n currentIssue: runtimeState?.currentIssue,\n };\n}\n\n/**\n * Get status for all specialists\n *\n * @returns Array of specialist statuses\n */\nexport async function getAllSpecialistStatus(): Promise<SpecialistStatus[]> {\n const specialists = getAllSpecialists();\n return Promise.all(specialists.map((metadata) => getSpecialistStatus(metadata.name)));\n}\n\n/**\n * Initialize a specialist agent\n *\n * Creates a tmux session and starts Claude Code with an identity prompt.\n * This is for first-time initialization of specialists that don't have session files.\n *\n * @param name - Specialist name\n * @returns Promise with initialization result\n */\nexport async function initializeSpecialist(name: SpecialistType): Promise<{\n success: boolean;\n message: string;\n tmuxSession?: string;\n error?: string;\n}> {\n // Check if already running\n if (await isRunning(name)) {\n return {\n success: false,\n message: `Specialist ${name} is already running`,\n error: 'already_running',\n };\n }\n\n // Check if already initialized\n if (getSessionId(name)) {\n return {\n success: false,\n message: `Specialist ${name} is already initialized. Use wake to start it.`,\n error: 'already_initialized',\n };\n }\n\n const tmuxSession = getTmuxSessionName(name);\n const cwd = process.env.HOME || '/home/eltmon';\n\n // Determine model for this specialist using work type router\n let model = 'claude-sonnet-4-6'; // default fallback\n try {\n // Map specialist name to work type ID\n const workTypeId: WorkTypeId = `specialist-${name}` as WorkTypeId;\n model = getModelId(workTypeId);\n } catch (error) {\n console.warn(`Warning: Could not resolve model for ${name}, using default model`);\n }\n\n // Create identity prompt for the specialist\n const identityPrompt = `You are the ${name} specialist agent for Panopticon.\nYour role: ${name === 'merge-agent' ? 'Resolve merge conflicts and ensure clean integrations' :\n name === 'review-agent' ? 'Review code changes and provide quality feedback' :\n name === 'test-agent' ? 'Execute and analyze test results' : 'Assist with development tasks'}\n\nYou will be woken up when your services are needed. For now, acknowledge your initialization and wait.\nSay: \"I am the ${name} specialist, ready and waiting for tasks.\"`;\n\n try {\n // Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for non-Anthropic models\n const providerEnv = getProviderEnvForModel(model);\n const envFlags = buildTmuxEnvFlags(providerEnv);\n\n // For credential-file providers (e.g. Kimi), configure apiKeyHelper for token refresh.\n // For all other providers, clear stale apiKeyHelper from previous runs.\n const providerCfg = getProviderForModel(model as ModelId);\n if (providerCfg.authType === 'credential-file') {\n setupCredentialFileAuth(providerCfg, cwd);\n } else {\n clearCredentialFileAuth(cwd);\n }\n\n // Write identity prompt and launcher script to avoid shell escaping issues\n const agentDir = join(homedir(), '.panopticon', 'agents', tmuxSession);\n await execAsync(`mkdir -p \"${agentDir}\"`, { encoding: 'utf-8' });\n\n const promptFile = join(agentDir, 'identity-prompt.md');\n const launcherScript = join(agentDir, 'launcher.sh');\n\n writeFileSync(promptFile, identityPrompt);\n const newSessionId = randomUUID();\n writeFileSync(launcherScript, `#!/bin/bash\ncd \"${cwd}\"\nprompt=$(cat \"${promptFile}\")\nexec claude --dangerously-skip-permissions --session-id \"${newSessionId}\" --model ${model} \"$prompt\"\n`, { mode: 0o755 });\n setSessionId(name, newSessionId);\n\n // Spawn Claude Code via launcher script (with provider env vars)\n await execAsync(\n `tmux new-session -d -s \"${tmuxSession}\"${envFlags} \"bash '${launcherScript}'\"`,\n { encoding: 'utf-8' }\n );\n\n // Record wake event\n recordWake(name);\n\n return {\n success: true,\n message: `Specialist ${name} initialized and started`,\n tmuxSession,\n };\n } catch (error: unknown) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n message: `Failed to initialize specialist ${name}: ${errorMessage}`,\n error: errorMessage,\n };\n }\n}\n\n/**\n * Initialize all enabled but uninitialized specialists\n *\n * Called during Cloister startup to ensure specialists are ready.\n *\n * @returns Promise with array of initialization results\n */\nexport async function initializeEnabledSpecialists(): Promise<Array<{\n name: SpecialistType;\n success: boolean;\n message: string;\n}>> {\n const enabled = getEnabledSpecialists();\n const results: Array<{ name: SpecialistType; success: boolean; message: string }> = [];\n\n for (const specialist of enabled) {\n const sessionId = getSessionId(specialist.name);\n\n if (!sessionId) {\n // Specialist is enabled but not initialized\n console.log(` → Auto-initializing specialist: ${specialist.name}`);\n const result = await initializeSpecialist(specialist.name);\n results.push({\n name: specialist.name,\n success: result.success,\n message: result.message,\n });\n\n // Small delay between initializations to avoid overwhelming the system\n if (results.length < enabled.length) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n } else {\n results.push({\n name: specialist.name,\n success: true,\n message: `Already initialized with session ${sessionId.substring(0, 8)}...`,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Reset specialist state before sending a new task\n *\n * Clears stale state from previous tasks:\n * 1. Sends Ctrl+C to cancel any pending command\n * 2. Runs 'cd ~' to reset working directory\n * 3. Sends Ctrl+U to clear the prompt buffer\n *\n * @param name - Specialist name\n */\nasync function resetSpecialist(name: SpecialistType): Promise<void> {\n const tmuxSession = getTmuxSessionName(name);\n\n try {\n // 1. Cancel any pending command with Ctrl+C and wait for Claude to return to idle.\n // Do NOT send 'cd ~' here — that triggers LLM inference (2-5s) and creates a race:\n // the task message arrives while Claude is still processing the cd command and gets lost.\n await execAsync(`tmux send-keys -t \"${tmuxSession}\" C-c`, { encoding: 'utf-8' });\n await new Promise(resolve => setTimeout(resolve, 500));\n\n // 2. Clear any partial input on the prompt line\n await execAsync(`tmux send-keys -t \"${tmuxSession}\" C-u`, { encoding: 'utf-8' });\n await new Promise(resolve => setTimeout(resolve, 100));\n } catch (error) {\n console.error(`[specialist] Failed to reset ${name}:`, error);\n // Non-fatal - continue with wake\n }\n}\n\n/**\n * Wake a specialist to process a task\n *\n * Sends a task prompt to a running specialist. If the specialist isn't running,\n * starts it first (with --resume if it has a session).\n *\n * @param name - Specialist name\n * @param taskPrompt - The task prompt to send to the specialist\n * @param options - Additional options\n * @returns Promise with wake result\n */\nexport async function wakeSpecialist(\n name: SpecialistType,\n taskPrompt: string,\n options: {\n waitForReady?: boolean; // Wait for agent to be ready before sending prompt (default: true)\n startIfNotRunning?: boolean; // Start the agent if not running (default: true)\n issueId?: string; // Issue ID being worked on (for tracking)\n skipBusyGuard?: boolean; // Skip busy check (caller already verified idle + set active)\n } = {}\n): Promise<{\n success: boolean;\n message: string;\n tmuxSession?: string;\n wasAlreadyRunning: boolean;\n error?: string;\n}> {\n const { waitForReady = true, startIfNotRunning = true, issueId } = options;\n const tmuxSession = getTmuxSessionName(name);\n const sessionId = getSessionId(name);\n const wasAlreadyRunning = await isRunning(name);\n\n // Guard: if specialist is running and busy, refuse to send a new task.\n // Sending a message to a busy Claude session causes \"Interrupted\" behavior —\n // the running tool is cancelled and the previous task is abandoned mid-flight.\n // Callers should use wakeSpecialistOrQueue() for automatic busy handling.\n // Skip this guard when called from wakeSpecialistOrQueue (skipBusyGuard),\n // since the caller already verified idle state and pre-set active to prevent races.\n if (wasAlreadyRunning && !options.skipBusyGuard) {\n const { getAgentRuntimeState } = await import('../agents.js');\n const runtimeState = getAgentRuntimeState(tmuxSession);\n if (runtimeState?.state === 'active') {\n console.warn(`[specialist] ${name} is busy (working on ${runtimeState.currentIssue}), refusing to interrupt`);\n return {\n success: false,\n message: `Specialist ${name} is busy (working on ${runtimeState.currentIssue}). Use wakeSpecialistOrQueue() instead.`,\n tmuxSession,\n wasAlreadyRunning: true,\n error: 'specialist_busy',\n };\n }\n }\n\n // If not running, start it first\n if (!wasAlreadyRunning) {\n if (!startIfNotRunning) {\n return {\n success: false,\n message: `Specialist ${name} is not running`,\n wasAlreadyRunning: false,\n error: 'not_running',\n };\n }\n\n const cwd = process.env.HOME || '/home/eltmon';\n\n try {\n // Resolve model from work type router (respects config.yaml overrides)\n let model = 'claude-sonnet-4-6'; // default fallback\n try {\n const workTypeId: WorkTypeId = `specialist-${name}` as WorkTypeId;\n model = getModelId(workTypeId);\n } catch (error) {\n console.warn(`[specialist] Could not resolve model for ${name}, using default`);\n }\n const modelFlag = `--model ${model}`;\n\n // Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for non-Anthropic models\n const providerEnv = getProviderEnvForModel(model);\n const envFlags = buildTmuxEnvFlags(providerEnv);\n\n // For credential-file providers (e.g. Kimi), configure apiKeyHelper for token refresh.\n // For all other providers, clear stale apiKeyHelper from previous runs.\n const provCfg = getProviderForModel(model as ModelId);\n if (provCfg.authType === 'credential-file') {\n setupCredentialFileAuth(provCfg, cwd);\n } else {\n clearCredentialFileAuth(cwd);\n }\n\n // merge-agent needs full bypass to handle git stash drop, reset, etc.\n const permissionFlags = name === 'merge-agent'\n ? '--dangerously-skip-permissions --permission-mode bypassPermissions'\n : '--dangerously-skip-permissions';\n\n // Start with --resume if we have a session, otherwise generate a new session ID\n let claudeCmd: string;\n if (sessionId) {\n claudeCmd = `claude --resume \"${sessionId}\" ${modelFlag} ${permissionFlags}`;\n } else {\n const newSessionId = randomUUID();\n claudeCmd = `claude --session-id \"${newSessionId}\" ${modelFlag} ${permissionFlags}`;\n setSessionId(name, newSessionId);\n }\n\n await execAsync(\n `tmux new-session -d -s \"${tmuxSession}\" -c \"${cwd}\"${envFlags} \"${claudeCmd}\"`,\n { encoding: 'utf-8' }\n );\n\n if (waitForReady) {\n // Poll for Claude's interactive prompt instead of fixed sleep.\n // Fresh starts can take 5-10s; 15s timeout covers slow models.\n const ready = await waitForClaudePrompt(tmuxSession, 15000);\n if (!ready) {\n console.warn(`[specialist] ${name}: prompt not detected within 15s, proceeding anyway`);\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n message: `Failed to start specialist ${name}: ${msg}`,\n wasAlreadyRunning: false,\n error: msg,\n };\n }\n }\n\n // Reset specialist state to clear stale context from previous tasks\n await resetSpecialist(name);\n\n // Wait for Claude to be at its interactive prompt before sending the task.\n // For already-running specialists this should be near-instant; for freshly-started\n // ones the waitForReady above already waited, but resetSpecialist's C-c may have\n // interrupted something so we re-confirm here.\n const promptReady = await waitForClaudePrompt(tmuxSession, wasAlreadyRunning ? 5000 : 15000);\n if (!promptReady) {\n console.warn(`[specialist] ${name}: prompt not detected after reset, proceeding anyway`);\n }\n\n // Send the task prompt\n try {\n // For large prompts (>500 chars or multiline), write to file to avoid tmux paste issues\n // Tmux send-keys with large text shows as \"[Pasted text #1 +N lines]\" which Claude doesn't process\n const isLargePrompt = taskPrompt.length > 500 || taskPrompt.includes('\\n');\n\n // Prepare the message to send\n let messageToSend: string;\n if (isLargePrompt) {\n if (!existsSync(TASKS_DIR)) {\n mkdirSync(TASKS_DIR, { recursive: true });\n }\n const taskFile = join(TASKS_DIR, `${name}-${Date.now()}.md`);\n writeFileSync(taskFile, taskPrompt, 'utf-8');\n messageToSend = `Read and execute the task in: ${taskFile}`;\n } else {\n messageToSend = taskPrompt;\n }\n\n // Snapshot tmux output BEFORE sending so we can detect new activity\n const outputBefore = await capturePaneAsync(tmuxSession, 50);\n\n // Send the task message\n await sendKeysAsync(tmuxSession, messageToSend);\n\n // Verify Claude received the message by watching for new output (tool calls, responses).\n // This catches silent delivery failures — the structural root cause of lost tasks.\n const delivered = await confirmDelivery(tmuxSession, outputBefore, 10000);\n if (!delivered) {\n console.warn(`[specialist] ${name}: no activity detected after task send, retrying...`);\n // Re-snapshot and retry once\n const retryBefore = await capturePaneAsync(tmuxSession, 50);\n await sendKeysAsync(tmuxSession, messageToSend);\n const retryDelivered = await confirmDelivery(tmuxSession, retryBefore, 10000);\n if (!retryDelivered) {\n return {\n success: false,\n message: `Task message not received by specialist ${name} after retry`,\n tmuxSession,\n wasAlreadyRunning,\n error: 'delivery_failed',\n };\n }\n }\n\n // Record wake event\n recordWake(name, sessionId || undefined);\n\n // Set state to active immediately (PAN-80: spinner should show right away)\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'active',\n lastActivity: new Date().toISOString(),\n currentIssue: issueId,\n });\n\n return {\n success: true,\n message: wasAlreadyRunning\n ? `Sent task to running specialist ${name}`\n : `Started specialist ${name} and sent task`,\n tmuxSession,\n wasAlreadyRunning,\n };\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n message: `Failed to send task to specialist ${name}: ${msg}`,\n tmuxSession,\n wasAlreadyRunning,\n error: msg,\n };\n }\n}\n\n/**\n * Wake specialist with a task from the queue\n *\n * Convenience wrapper that formats task details into a prompt.\n *\n * @param name - Specialist name\n * @param task - Task from the queue\n * @returns Promise with wake result\n */\nexport async function wakeSpecialistWithTask(\n name: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n },\n options: { skipBusyGuard?: boolean } = {}\n): Promise<ReturnType<typeof wakeSpecialist>> {\n // Build context-aware prompt based on specialist type and task\n const apiPort = process.env.API_PORT || process.env.PORT || '3011';\n const apiUrl = process.env.DASHBOARD_URL || `http://localhost:${apiPort}`;\n let prompt: string;\n\n switch (name) {\n case 'merge-agent': {\n const mergeWorkspace = task.workspace || 'unknown';\n const mergeInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const mergeBranch = mergeInfo.branch;\n\n const mergeRepoInstructions = mergeInfo.isPolyrepo\n ? `\\nIMPORTANT: This is a POLYREPO project. There are ${mergeInfo.gitDirs.length} separate git repositories to merge:\n${mergeInfo.gitDirs.map((d, i) => `${i + 1}. ${basename(d)}: ${d}`).join('\\n')}\n\nThe workspace root is NOT a git repo. You must cd into each subdirectory to run git commands.\nYou MUST complete the merge for ALL repos.\\n`\n : '';\n\n prompt = `New merge task for ${task.issueId}:\n\nBranch: ${mergeBranch}\nWorkspace: ${mergeWorkspace}\n${mergeInfo.isPolyrepo ? `Polyrepo: git repos in subdirectories: ${mergeInfo.gitDirs.map(d => basename(d)).join(', ')}` : ''}\n${task.prUrl ? `PR URL: ${task.prUrl}` : ''}\n${mergeRepoInstructions}\nFor ${mergeInfo.isPolyrepo ? 'EACH repo' : 'the repo'}, perform these steps:\n\nPHASE 1 — SYNC & BASELINE (before merge):\n1. ${mergeInfo.isPolyrepo ? 'cd into the repo directory' : `cd ${mergeWorkspace}`}\n2. git checkout main\n3. git fetch origin main\n4. Sync local main with origin/main:\n Run: git rev-list --left-right --count main...origin/main\n If REMOTE_AHEAD > 0: git rebase origin/main\n If rebase conflicts: abort and report failure.\n5. Run tests on main to establish a baseline. Record BASELINE_PASS and BASELINE_FAIL.\n\nPHASE 2 — MERGE:\n6. git merge ${mergeBranch} --no-edit\n7. If conflicts: resolve them intelligently, then git add and git commit\n8. If clean merge: the merge commit is auto-created (or fast-forward)\n\nPHASE 3 — VERIFY:\n9. Run tests again. Record MERGE_PASS and MERGE_FAIL.\n\nPHASE 4 — DECIDE:\n10. Compare results:\n - If MERGE_FAIL > BASELINE_FAIL (NEW test failures): ROLLBACK with git reset --hard ORIG_HEAD\n - If MERGE_FAIL <= BASELINE_FAIL (no new failures): PUSH with git push origin main\n - Pre-existing failures on main are NOT a reason to rollback\n\nPHASE 5 — REPORT:\n11. Call the Panopticon API to report results:\n curl -s -X POST ${apiUrl}/api/specialists/done \\\\\n -H \"Content-Type: application/json\" \\\\\n -d '{\"specialist\":\"merge\",\"issueId\":\"${task.issueId}\",\"status\":\"passed|failed\",\"notes\":\"<summary>\"}'\n\nCRITICAL: You MUST call the /api/specialists/done endpoint whether you succeed or fail.\nCRITICAL: NEVER use git push --force.\nCRITICAL: Do NOT delete the feature branch.`;\n break;\n }\n\n case 'review-agent': {\n // Pre-check: detect stale branch (0 diff from main) before waking the agent\n const workspace = task.workspace || 'unknown';\n\n // Resolve git directories and branch from workspace\n const reviewGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const gitDirs = reviewGitInfo.gitDirs;\n // Use first git dir for pre-check (primary repo), fall back to workspace root\n const gitDir = gitDirs[0] || workspace;\n\n let staleBranch = false;\n if (workspace !== 'unknown' && gitDirs.length > 0) {\n try {\n // For polyrepos, check all git dirs — if ANY has changes, it's not stale\n let totalChangedFiles = 0;\n for (const dir of gitDirs) {\n const { stdout: dirDiff } = await execAsync(\n `cd \"${dir}\" && git fetch origin main 2>/dev/null; git diff --name-only main...HEAD 2>/dev/null`,\n { encoding: 'utf-8', timeout: 15000 }\n );\n totalChangedFiles += dirDiff.trim().split('\\n').filter((f: string) => f.length > 0).length;\n }\n if (totalChangedFiles === 0) {\n staleBranch = true;\n console.log(`[specialist] review-agent: stale branch detected for ${task.issueId} — 0 files changed vs main`);\n\n // Auto-complete the review: set reviewStatus to passed\n const { setReviewStatus } = await import('../review-status.js');\n setReviewStatus(task.issueId.toUpperCase(), {\n reviewStatus: 'passed',\n reviewNotes: 'No changes to review — branch identical to main (already merged or stale)',\n });\n console.log(`[specialist] review-agent: auto-passed ${task.issueId} (stale branch)`);\n\n // Also try to signal via the specialists/done path for idle state management\n const tmuxSession = getTmuxSessionName('review-agent');\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n });\n\n return { success: true, message: `Stale branch auto-passed for ${task.issueId}`, wasAlreadyRunning: false, error: undefined };\n }\n } catch (err) {\n // If pre-check fails, fall through to normal wake — agent will handle it\n console.warn(`[specialist] review-agent: stale branch pre-check failed for ${task.issueId}:`, err);\n }\n }\n\n // Build git commands for the prompt — polyrepo workspaces need git commands in subdirectories\n const isPolyrepo = gitDirs.length > 1;\n const gitDiffCommands = gitDirs.length > 0\n ? gitDirs.map(d => `cd \"${d}\" && git diff --name-only main...HEAD`).join('\\n')\n : `cd \"${workspace}\" && git diff --name-only main...HEAD`;\n const gitDiffFileCmd = gitDirs.length > 0\n ? `cd \"${gitDir}\" && git diff main...HEAD -- <file>`\n : `cd \"${workspace}\" && git diff main...HEAD -- <file>`;\n\n prompt = `New review task for ${task.issueId}:\n\nBranch: ${task.branch || 'unknown'}\nWorkspace: ${workspace}\n${isPolyrepo ? `Polyrepo: git repos in subdirectories: ${gitDirs.map(d => basename(d)).join(', ')}` : ''}\n${task.prUrl ? `PR URL: ${task.prUrl}` : ''}\n\nYour task:\n1. Review all changes in the branch compared to main\n2. Check for code quality issues, security concerns, and best practices\n3. Verify test FILES exist for new code (DO NOT run tests - test-agent does that)\n4. Provide specific, actionable feedback\n\nIMPORTANT: DO NOT run tests (npm test). You are the REVIEW agent - you only review code.\nThe TEST agent will run tests in the next step.\n\n## How to Review Changes\n\n**Step 0 (CRITICAL):** First check if there are ANY changes to review:\n${isPolyrepo ? `This is a polyrepo — run git diff in each repo subdirectory:` : ''}\n\\`\\`\\`bash\n${gitDiffCommands}\n\\`\\`\\`\n\n**If the diff is EMPTY (0 files changed across all repos):** The branch is stale or already merged into main. In this case:\n1. Do NOT attempt a full review\n2. Update status as passed immediately:\n\\`\\`\\`bash\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"reviewStatus\":\"passed\",\"reviewNotes\":\"No changes to review — branch identical to main (already merged or stale)\"}' | jq .\n\\`\\`\\`\n3. Tell the issue agent:\n\\`\\`\\`bash\npan work tell ${task.issueId} \"Review complete: branch has 0 diff from main — already merged or stale. Marking as passed.\"\n\\`\\`\\`\n4. Stop here — you are done.\n\n**Step 1:** Get the list of changed files:\n\\`\\`\\`bash\n${gitDiffCommands}\n\\`\\`\\`\n\n**Step 2:** Read the CURRENT version of each changed file using the Read tool.\nReview the actual file contents — do NOT rely solely on diff output.\n\n**Step 3:** If you need to see what specifically changed, use:\n\\`\\`\\`bash\n${gitDiffFileCmd}\n\\`\\`\\`\n\n## Avoiding False Positives\n\n**CRITICAL:** When reviewing diffs, understand that:\n- Lines starting with \\`+\\` are ADDITIONS (new code)\n- Lines starting with \\`-\\` are DELETIONS (removed code)\n- Lines without prefix are CONTEXT (unchanged surrounding code)\n- The SAME content may appear in both \\`-\\` and \\`+\\` sections when code is moved or reformatted — this is NOT duplication\n- A section shown in diff context does NOT mean it appears twice in the actual file\n- **Always read the actual file** to verify before claiming duplicate or redundant content\n\nDo NOT flag:\n- Code that appears in both removed and added hunks (it was moved, not duplicated)\n- Diff context lines as \"duplicate sections\" — they exist once in the real file\n- Reformatted/restructured code as \"duplicated\"\n\n## REQUIRED: Update Status via API\n\nYou MUST execute these curl commands and verify they succeed. Do NOT just describe them - actually RUN them with Bash.\n\nIf issues found:\n\\`\\`\\`bash\n# EXECUTE THIS - verify you see JSON response with reviewStatus\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"reviewStatus\":\"blocked\",\"reviewNotes\":\"[describe issues]\"}' | jq .\n\\`\\`\\`\nThen use send-feedback-to-agent skill to notify issue agent.\n\nIf review passes:\n\\`\\`\\`bash\n# EXECUTE THIS FIRST - verify you see JSON response with reviewStatus:\"passed\"\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"reviewStatus\":\"passed\"}' | jq .\n\n# THEN EXECUTE THIS - verify you see JSON response with queued task\ncurl -s -X POST ${apiUrl}/api/specialists/test-agent/queue -H \"Content-Type: application/json\" -d '{\"issueId\":\"${task.issueId}\",\"workspace\":\"${task.workspace}\",\"branch\":\"${task.branch}\"}' | jq .\n\\`\\`\\`\n\n⚠️ VERIFICATION: After running each curl, confirm you see valid JSON output. If you get an error, report it.`;\n break;\n }\n\n case 'test-agent': {\n // Resolve polyrepo structure and project test config\n const testWorkspace = task.workspace || 'unknown';\n const testGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const testIsPolyrepo = testGitInfo.isPolyrepo;\n\n // Look up project test config from projects.yaml\n const { extractTeamPrefix, findProjectByTeam } = await import('../projects.js');\n const testTeamPrefix = extractTeamPrefix(task.issueId);\n const testProjectConfig = testTeamPrefix ? findProjectByTeam(testTeamPrefix) : null;\n const testConfigs = testProjectConfig?.tests;\n\n // Build per-repo test commands from projects.yaml config\n let testCommands = '';\n let baselineCommands = '';\n const featureName = task.issueId.toLowerCase();\n // Derive main workspace path for baseline comparison\n const mainWorkspacePath = testWorkspace.replace(/workspaces\\/feature-[^/]+/, 'workspaces/main');\n const projectRootPath = testProjectConfig?.path || testWorkspace.replace(/\\/workspaces\\/.*/, '');\n\n if (testConfigs && Object.keys(testConfigs).length > 0) {\n // Use projects.yaml test config — each entry may target a different repo subdirectory\n const testEntries = Object.entries(testConfigs);\n const testSuites: string[] = [];\n const baselineSuites: string[] = [];\n for (const [name, cfg] of testEntries) {\n const testDir = testIsPolyrepo\n ? `${testWorkspace}/${cfg.path}`\n : (cfg.path === '.' ? testWorkspace : `${testWorkspace}/${cfg.path}`);\n const baseDir = testIsPolyrepo\n ? `${mainWorkspacePath}/${cfg.path}`\n : (cfg.path === '.' ? mainWorkspacePath : `${mainWorkspacePath}/${cfg.path}`);\n // Fall back to project root for monorepo baseline if main workspace doesn't exist\n const fallbackDir = cfg.path === '.' ? projectRootPath : `${projectRootPath}/${cfg.path}`;\n testSuites.push(`echo \"\\\\n=== Test suite: ${name} (${cfg.type}) ===\" && cd \"${testDir}\" && ${cfg.command} 2>&1; echo \"EXIT_CODE_${name}: $?\"`);\n baselineSuites.push(`echo \"\\\\n=== Baseline: ${name} (${cfg.type}) ===\" && cd \"${baseDir}\" 2>/dev/null && ${cfg.command} 2>&1 || (cd \"${fallbackDir}\" 2>/dev/null && ${cfg.command} 2>&1) || echo \"BASELINE_SKIP_${name}: could not run baseline\"; echo \"EXIT_CODE_${name}: $?\"`);\n }\n testCommands = testSuites.map((cmd, i) => `# Suite ${i + 1}\\n${cmd}`).join('\\n');\n baselineCommands = baselineSuites.map((cmd, i) => `# Suite ${i + 1}\\n${cmd}`).join('\\n');\n } else if (testIsPolyrepo) {\n // No projects.yaml config but detected polyrepo — discover test commands per repo\n const testSuites: string[] = [];\n const baselineSuites: string[] = [];\n for (const gitDir of testGitInfo.gitDirs) {\n const repoName = basename(gitDir);\n // Auto-detect test runner in each repo\n testSuites.push(`echo \"\\\\n=== ${repoName} ===\" && cd \"${gitDir}\" && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo \"No test runner found\"; fi; echo \"EXIT_CODE_${repoName}: $?\"`);\n const baseDir = `${mainWorkspacePath}/${repoName}`;\n baselineSuites.push(`echo \"\\\\n=== Baseline: ${repoName} ===\" && cd \"${baseDir}\" 2>/dev/null && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo \"No test runner found\"; fi; echo \"EXIT_CODE_${repoName}: $?\"`);\n }\n testCommands = testSuites.join('\\n');\n baselineCommands = baselineSuites.join('\\n');\n } else {\n // Monorepo fallback — single test command\n testCommands = `cd \"${testWorkspace}\" && npm test 2>&1; echo \"EXIT_CODE: $?\"`;\n baselineCommands = `cd \"${mainWorkspacePath}\" 2>/dev/null && npm test 2>&1 || (cd \"${projectRootPath}\" && npm test 2>&1); echo \"EXIT_CODE: $?\"`;\n }\n\n // Build test suite summary for the prompt\n const testConfigSummary = testConfigs\n ? Object.entries(testConfigs).map(([name, cfg]) => `- **${name}** (${cfg.type}): \\`${cfg.command}\\` in \\`${cfg.path}/\\``).join('\\n')\n : testIsPolyrepo\n ? testGitInfo.gitDirs.map(d => `- **${basename(d)}**: auto-detected`).join('\\n')\n : '- Single test suite at workspace root';\n\n prompt = `New test task for ${task.issueId}:\n\nBranch: ${task.branch || 'unknown'}\nWorkspace: ${testWorkspace}\n${testIsPolyrepo ? `Polyrepo: git repos in subdirectories: ${testGitInfo.gitDirs.map(d => basename(d)).join(', ')}` : ''}\n\n## Test Suites\n\n${testConfigSummary}\n\nYour task:\n1. Run ALL test suites — redirect output to file, read only summaries\n2. If ALL pass, skip baseline and report PASS\n3. If failures, run baseline on main and compare\n4. Only fail for NEW regressions (not pre-existing)\n5. Update status via API when done\n\n## CRITICAL: Context Management — Output Redirection\n\n**NEVER let full test output flow into your context.** Always redirect to file and read only summaries.\nRaw test output from large suites (1000+ tests) WILL fill your context and cause compaction, losing your task.\n\n## CRITICAL: Bash Timeout for Test Commands\n\n**ALWAYS use timeout: 300000 (5 minutes) when running test commands.**\nFor Maven/Spring Boot tests, use timeout: 600000 (10 minutes) — they take longer.\n\n## Step 1: Run Feature Branch Tests\n\n${testIsPolyrepo || (testConfigs && Object.keys(testConfigs).length > 1)\n ? `**Run ALL test suites** — each suite is a separate repo/runner. Redirect ALL output to one file.`\n : ''}\n\n\\`\\`\\`bash\n(\n${testCommands}\n) > /tmp/test-feature.txt 2>&1\n# Use timeout: ${testConfigs && Object.values(testConfigs).some(c => c.type === 'maven') ? '600000' : '300000'} for this command\necho \"--- Feature test output tail ---\"\ntail -40 /tmp/test-feature.txt\ngrep \"EXIT_CODE\" /tmp/test-feature.txt\n\\`\\`\\`\n\n## Step 2: Check Results\n\n- If ALL exit codes are 0 → skip baseline, go to \"Update Status\"\n- If any failures → continue to Step 3\n\n## Step 3: Baseline Comparison (ONLY if failures found)\n\n\\`\\`\\`bash\n(\n${baselineCommands}\n) > /tmp/test-main.txt 2>&1\n# Use timeout: ${testConfigs && Object.values(testConfigs).some(c => c.type === 'maven') ? '600000' : '300000'} for this command\necho \"--- Baseline test output tail ---\"\ntail -40 /tmp/test-main.txt\ngrep \"EXIT_CODE\" /tmp/test-main.txt\n\\`\\`\\`\n\nThen compare failures (targeted, NOT full output):\n\\`\\`\\`bash\ngrep -E \"FAIL|✗|Error|failed|BUILD FAILURE\" /tmp/test-feature.txt | head -30\ngrep -E \"FAIL|✗|Error|failed|BUILD FAILURE\" /tmp/test-main.txt | head -30\n\\`\\`\\`\n\nTests that fail on BOTH = pre-existing (don't block). Tests that fail ONLY on feature = NEW regression (block).\n\n**Pass criteria:** Feature branch introduces ZERO new test failures vs main.\n**Fail criteria:** Feature branch introduces NEW failures not present on main.\n\n## REQUIRED: Update Status via API\n\nYou MUST execute the appropriate curl command and verify it succeeds. Do NOT just describe it - actually RUN it with Bash.\n\nIf NO new regressions (tests PASS):\n\\`\\`\\`bash\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"testStatus\":\"passed\",\"testNotes\":\"[summary including pre-existing failures if any, and which suites were tested]\"}' | jq .\n\\`\\`\\`\n\nIf NEW regressions found (tests FAIL):\n\\`\\`\\`bash\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"testStatus\":\"failed\",\"testNotes\":\"[describe NEW failures only — specify which suite/repo]\"}' | jq .\n\\`\\`\\`\nThen use send-feedback-to-agent skill to notify issue agent of NEW failures only.\n\n⚠️ VERIFICATION: After running curl, confirm you see valid JSON output with the updated status. If you get an error or empty response, the update FAILED - report this.\n\n**NEVER run test commands without redirecting to a file.** This is not optional.\n\nIMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.`;\n break;\n }\n\n default:\n prompt = `Task for ${task.issueId}: Please process this task and report findings.`;\n }\n\n return wakeSpecialist(name, prompt, { issueId: task.issueId, skipBusyGuard: options.skipBusyGuard });\n}\n\n/**\n * Task context interface for handoffs and specialist tasks\n */\nexport interface TaskContext {\n prUrl?: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n reason?: string;\n targetModel?: string;\n additionalInstructions?: string;\n [key: string]: string | string[] | undefined;\n}\n\n/**\n * Wake a specialist or queue the task if busy\n *\n * This wrapper checks if the specialist is busy before waking.\n * If the specialist is running but not idle, the task is queued instead.\n *\n * @param name - Specialist name\n * @param task - Task details\n * @param priority - Task priority (default: 'normal')\n * @param source - Source of the task (default: 'handoff')\n * @returns Promise with result indicating whether task was queued or executed\n */\nexport async function wakeSpecialistOrQueue(\n name: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n },\n options: {\n priority?: 'urgent' | 'high' | 'normal' | 'low';\n source?: string;\n } = {}\n): Promise<{\n success: boolean;\n queued: boolean;\n message: string;\n error?: string;\n}> {\n const { priority = 'normal', source = 'handoff' } = options;\n\n // Check if specialist is running and get state (PAN-80)\n const running = await isRunning(name);\n const { getAgentRuntimeState } = await import('../agents.js');\n const tmuxSession = getTmuxSessionName(name);\n const runtimeState = getAgentRuntimeState(tmuxSession);\n const idle = runtimeState?.state === 'idle' || runtimeState?.state === 'suspended';\n\n // If running and busy (active), queue the task\n if (running && !idle) {\n try {\n submitToSpecialistQueue(name, {\n priority,\n source,\n issueId: task.issueId,\n workspace: task.workspace,\n branch: task.branch,\n prUrl: task.prUrl,\n context: task.context,\n });\n\n console.log(`[specialist] ${name} busy, queued task for ${task.issueId} (priority: ${priority})`);\n\n return {\n success: true,\n queued: true,\n message: `Specialist ${name} is busy. Task queued with ${priority} priority.`,\n };\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n queued: false,\n message: `Failed to queue task for ${name}: ${msg}`,\n error: msg,\n };\n }\n }\n\n // Otherwise, wake the specialist directly\n // PAN-88: Set state to 'active' IMMEDIATELY to prevent race conditions\n // This must happen BEFORE the actual wake to block concurrent requests\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'active',\n lastActivity: new Date().toISOString(),\n currentIssue: task.issueId,\n });\n console.log(`[specialist] ${name} marked active (preventing concurrent wakes)`);\n\n try {\n const wakeResult = await wakeSpecialistWithTask(name, task, { skipBusyGuard: true });\n\n if (!wakeResult.success) {\n // Wake failed - revert state to idle and clear currentIssue\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n }\n\n return {\n success: wakeResult.success,\n queued: false,\n message: wakeResult.message,\n error: wakeResult.error,\n };\n } catch (error: unknown) {\n // Exception - revert state to idle and clear currentIssue\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n queued: false,\n message: `Failed to wake specialist ${name}: ${msg}`,\n error: msg,\n };\n }\n}\n\n/**\n * ===========================================================================\n * Specialist Queue Helpers\n * ===========================================================================\n */\n\nimport { HookItem, pushToHook, checkHook, popFromHook } from '../hooks.js';\n\n/**\n * Specialist queue item - extends HookItem with specialist-specific payload\n */\nexport interface SpecialistQueueItem extends HookItem {\n type: 'task';\n payload: {\n prUrl?: string;\n issueId: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n context?: TaskContext;\n };\n}\n\n/**\n * Submit a task to a specialist's queue\n *\n * @param specialistName - Name of the specialist (e.g., 'review-agent', 'merge-agent')\n * @param task - Task details\n * @returns The created queue item\n */\nexport function submitToSpecialistQueue(\n specialistName: SpecialistType,\n task: {\n priority: 'urgent' | 'high' | 'normal' | 'low';\n source: string;\n prUrl?: string;\n issueId: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n context?: TaskContext;\n }\n): HookItem {\n // Put specialist-specific fields into context to match HookItem type\n const item: Omit<HookItem, 'id' | 'createdAt'> = {\n type: 'task',\n priority: task.priority,\n source: task.source,\n payload: {\n issueId: task.issueId,\n context: {\n ...task.context,\n prUrl: task.prUrl,\n workspace: task.workspace,\n branch: task.branch,\n filesChanged: task.filesChanged,\n },\n },\n };\n\n const queueItem = pushToHook(specialistName, item);\n\n notifyPipeline({ type: 'task_queued', specialist: specialistName, issueId: task.issueId });\n\n // Log specialist handoff event\n const handoffEvent = createSpecialistHandoff(\n task.source, // From (e.g., 'review-agent' or 'issue-agent')\n specialistName, // To specialist\n task.issueId,\n task.priority,\n {\n workspace: task.workspace,\n branch: task.branch,\n prUrl: task.prUrl,\n source: task.source,\n }\n );\n logSpecialistHandoff(handoffEvent);\n\n return queueItem;\n}\n\n/**\n * Check if a specialist has pending work in their queue\n *\n * @param specialistName - Name of the specialist\n * @returns Queue status\n */\nexport function checkSpecialistQueue(specialistName: SpecialistType): {\n hasWork: boolean;\n urgentCount: number;\n items: HookItem[];\n} {\n return checkHook(specialistName);\n}\n\n/**\n * Remove a completed task from a specialist's queue\n *\n * @param specialistName - Name of the specialist\n * @param itemId - ID of the completed task\n * @returns True if item was removed\n */\nexport function completeSpecialistTask(specialistName: SpecialistType, itemId: string): boolean {\n return popFromHook(specialistName, itemId);\n}\n\n/**\n * Get the next task from a specialist's queue (highest priority)\n *\n * Does NOT remove the task - use completeSpecialistTask() after execution.\n *\n * @param specialistName - Name of the specialist\n * @returns The next task or null if queue is empty\n */\nexport function getNextSpecialistTask(specialistName: SpecialistType): HookItem | null {\n const { items } = checkSpecialistQueue(specialistName);\n return items.length > 0 ? items[0] : null;\n}\n\n/**\n * ===========================================================================\n * Specialist Feedback System\n * ===========================================================================\n *\n * Specialists accumulate context and expertise. This system allows them to\n * share learnings back to issue agents, creating a feedback loop that\n * improves the overall system over time.\n */\n\n/**\n * Feedback from a specialist to an issue agent\n */\nexport interface SpecialistFeedback {\n id: string;\n timestamp: string;\n fromSpecialist: SpecialistType;\n toIssueId: string;\n feedbackType: 'success' | 'failure' | 'warning' | 'insight';\n category: 'merge' | 'test' | 'review' | 'general';\n summary: string;\n details: string;\n actionItems?: string[];\n patterns?: string[]; // Patterns the specialist noticed\n suggestions?: string[]; // Suggestions for the issue agent\n}\n\nconst FEEDBACK_DIR = join(PANOPTICON_HOME, 'specialists', 'feedback');\nconst FEEDBACK_LOG = join(FEEDBACK_DIR, 'feedback.jsonl');\n\n/**\n * Send feedback from a specialist to an issue agent\n *\n * This is the key mechanism for specialists to share their accumulated\n * expertise back to the issue agents that spawned the work.\n *\n * @param feedback - The feedback to send\n * @returns True if feedback was sent successfully\n */\nexport async function sendFeedbackToAgent(\n feedback: Omit<SpecialistFeedback, 'id' | 'timestamp'>\n): Promise<boolean> {\n const { fromSpecialist, toIssueId, summary, details } = feedback;\n\n // Ensure feedback directory exists\n if (!existsSync(FEEDBACK_DIR)) {\n mkdirSync(FEEDBACK_DIR, { recursive: true });\n }\n\n // Create full feedback record\n const fullFeedback: SpecialistFeedback = {\n ...feedback,\n id: `feedback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n timestamp: new Date().toISOString(),\n };\n\n // Log feedback to JSONL\n try {\n const line = JSON.stringify(fullFeedback) + '\\n';\n appendFileSync(FEEDBACK_LOG, line, 'utf-8');\n } catch (error) {\n console.error(`[specialist] Failed to log feedback:`, error);\n }\n\n // Try to send feedback to the issue agent\n const agentSession = `agent-${toIssueId.toLowerCase()}`;\n\n // Format feedback message for the agent\n const feedbackMessage = formatFeedbackForAgent(fullFeedback);\n\n // Write feedback to workspace file\n const { writeFeedbackFile } = await import('./feedback-writer.js');\n const specialistMap: Record<string, 'review-agent' | 'test-agent' | 'merge-agent'> = {\n 'review-agent': 'review-agent',\n 'test-agent': 'test-agent',\n 'merge-agent': 'merge-agent',\n };\n const specialist = specialistMap[fromSpecialist] || 'review-agent';\n const outcome = feedback.feedbackType === 'success' ? 'approved' : feedback.feedbackType === 'failure' ? 'failed' : feedback.feedbackType;\n\n const fileResult = await writeFeedbackFile({\n issueId: toIssueId,\n specialist,\n outcome,\n summary: summary.slice(0, 100),\n markdownBody: feedbackMessage,\n });\n\n if (!fileResult.success) {\n console.error(`[specialist] Failed to write feedback file for ${toIssueId}: ${fileResult.error}`);\n return false;\n }\n\n // Send short reference pointing to the file\n try {\n const { messageAgent } = await import('../agents.js');\n const msg = `SPECIALIST FEEDBACK: ${fromSpecialist} reported ${feedback.feedbackType.toUpperCase()} for ${toIssueId}.\\nRead and address: ${fileResult.relativePath}`;\n await messageAgent(agentSession, msg);\n console.log(`[specialist] Sent feedback from ${fromSpecialist} to ${agentSession} (file: ${fileResult.relativePath})`);\n return true;\n } catch (err) {\n // Agent may be gone — feedback file is still in the workspace for crash recovery\n console.log(`[specialist] Could not send reference to ${agentSession} (file written): ${err}`);\n return true; // File was written successfully, that's the important part\n }\n}\n\n/**\n * Format feedback for display to an agent\n */\nfunction formatFeedbackForAgent(feedback: SpecialistFeedback): string {\n const { fromSpecialist, feedbackType, category, summary, details, actionItems, patterns, suggestions } = feedback;\n\n const typeEmoji = {\n success: '✅',\n failure: '❌',\n warning: '⚠️',\n insight: '💡',\n }[feedbackType];\n\n let message = `\\n${typeEmoji} **Feedback from ${fromSpecialist}** (${category})\\n\\n`;\n message += `**Summary:** ${summary}\\n\\n`;\n message += `**Details:**\\n${details}\\n`;\n\n if (actionItems?.length) {\n message += `\\n**Action Items:**\\n`;\n actionItems.forEach((item, i) => {\n message += `${i + 1}. ${item}\\n`;\n });\n }\n\n if (patterns?.length) {\n message += `\\n**Patterns Noticed:**\\n`;\n patterns.forEach(pattern => {\n message += `- ${pattern}\\n`;\n });\n }\n\n if (suggestions?.length) {\n message += `\\n**Suggestions:**\\n`;\n suggestions.forEach(suggestion => {\n message += `- ${suggestion}\\n`;\n });\n }\n\n return message;\n}\n\n/**\n * Get pending feedback for an issue that hasn't been delivered yet\n *\n * @param issueId - Issue ID to get feedback for\n * @returns Array of feedback records\n */\nexport function getPendingFeedback(issueId: string): SpecialistFeedback[] {\n if (!existsSync(FEEDBACK_LOG)) {\n return [];\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n const allFeedback = lines.map(line => JSON.parse(line) as SpecialistFeedback);\n\n // Filter to this issue\n return allFeedback.filter(f => f.toIssueId.toLowerCase() === issueId.toLowerCase());\n } catch (error) {\n console.error(`[specialist] Failed to read feedback log:`, error);\n return [];\n }\n}\n\n/**\n * Get feedback statistics for all specialists\n *\n * @returns Feedback stats by specialist and type\n */\nexport function getFeedbackStats(): {\n bySpecialist: Record<SpecialistType, number>;\n byType: Record<string, number>;\n total: number;\n} {\n const stats = {\n bySpecialist: {\n 'merge-agent': 0,\n 'review-agent': 0,\n 'test-agent': 0,\n } as Record<SpecialistType, number>,\n byType: {} as Record<string, number>,\n total: 0,\n };\n\n if (!existsSync(FEEDBACK_LOG)) {\n return stats;\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n\n for (const line of lines) {\n const feedback = JSON.parse(line) as SpecialistFeedback;\n stats.bySpecialist[feedback.fromSpecialist] = (stats.bySpecialist[feedback.fromSpecialist] || 0) + 1;\n stats.byType[feedback.feedbackType] = (stats.byType[feedback.feedbackType] || 0) + 1;\n stats.total++;\n }\n } catch (error) {\n console.error(`[specialist] Failed to read feedback stats:`, error);\n }\n\n return stats;\n}\n","/**\n * Specialist Log Management\n *\n * Manages persistent log files for specialist agent runs.\n * Each run produces a structured log file with metadata, context, and full transcript.\n *\n * Directory structure:\n * ~/.panopticon/specialists/{projectKey}/{specialistType}/runs/{timestamp}-{issueId}.log\n */\n\nimport { existsSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';\nimport { join, basename } from 'path';\nimport { getPanopticonHome } from '../paths.js';\n\n/** Get specialists directory (lazy to support test env overrides) */\nfunction getSpecialistsDir(): string {\n return join(getPanopticonHome(), 'specialists');\n}\n\n/**\n * Log file metadata\n */\nexport interface RunLogMetadata {\n runId: string;\n project: string;\n specialistType: string;\n issueId: string;\n startedAt: string;\n finishedAt?: string;\n status?: 'passed' | 'failed' | 'blocked' | 'incomplete';\n duration?: number; // in milliseconds\n notes?: string;\n}\n\n/**\n * Run log entry for listing\n */\nexport interface RunLogEntry {\n runId: string;\n filePath: string;\n metadata: RunLogMetadata;\n fileSize: number;\n createdAt: Date;\n}\n\n/**\n * Get the runs directory for a project's specialist\n */\nexport function getRunsDirectory(projectKey: string, specialistType: string): string {\n return join(getSpecialistsDir(), projectKey, specialistType, 'runs');\n}\n\n/**\n * Generate a run ID from timestamp and issue ID\n */\nexport function generateRunId(issueId: string): string {\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);\n return `${timestamp}-${issueId}`;\n}\n\n/**\n * Get the log file path for a run\n */\nexport function getRunLogPath(projectKey: string, specialistType: string, runId: string): string {\n const runsDir = getRunsDirectory(projectKey, specialistType);\n return join(runsDir, `${runId}.log`);\n}\n\n/**\n * Ensure runs directory exists for a project's specialist\n */\nfunction ensureRunsDirectory(projectKey: string, specialistType: string): void {\n const runsDir = getRunsDirectory(projectKey, specialistType);\n if (!existsSync(runsDir)) {\n mkdirSync(runsDir, { recursive: true });\n }\n}\n\n/**\n * Create a new run log file\n *\n * Initializes a log file with metadata header.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type (review-agent, test-agent, merge-agent)\n * @param issueId - Issue ID being worked on\n * @param contextSeed - Optional context digest that was provided to the specialist\n * @returns Run ID and file path\n */\nexport function createRunLog(\n projectKey: string,\n specialistType: string,\n issueId: string,\n contextSeed?: string\n): { runId: string; filePath: string } {\n ensureRunsDirectory(projectKey, specialistType);\n\n const runId = generateRunId(issueId);\n const filePath = getRunLogPath(projectKey, specialistType, runId);\n const startedAt = new Date().toISOString();\n\n // Create log header\n const header = `# ${specialistType} Run - ${issueId}\nProject: ${projectKey}\nStarted: ${startedAt}\nIssue: ${issueId}\nRun ID: ${runId}\n\n## Context Seed\n${contextSeed ? contextSeed : '[No context digest available]'}\n\n## Session Transcript\n`;\n\n writeFileSync(filePath, header, 'utf-8');\n\n return { runId, filePath };\n}\n\n/**\n * Append content to a run log\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param runId - Run identifier\n * @param content - Content to append\n */\nexport function appendToRunLog(\n projectKey: string,\n specialistType: string,\n runId: string,\n content: string\n): void {\n const filePath = getRunLogPath(projectKey, specialistType, runId);\n\n if (!existsSync(filePath)) {\n throw new Error(`Run log not found: ${filePath}`);\n }\n\n appendFileSync(filePath, content, 'utf-8');\n}\n\n/**\n * Finalize a run log with result metadata\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param runId - Run identifier\n * @param result - Run result\n */\nexport function finalizeRunLog(\n projectKey: string,\n specialistType: string,\n runId: string,\n result: {\n status: 'passed' | 'failed' | 'blocked' | 'incomplete';\n notes?: string;\n }\n): void {\n const filePath = getRunLogPath(projectKey, specialistType, runId);\n\n if (!existsSync(filePath)) {\n throw new Error(`Run log not found: ${filePath}`);\n }\n\n // Read the log to extract start time\n const content = readFileSync(filePath, 'utf-8');\n const startMatch = content.match(/^Started: (.+)$/m);\n const startedAt = startMatch ? new Date(startMatch[1]) : new Date();\n const finishedAt = new Date();\n const duration = finishedAt.getTime() - startedAt.getTime();\n\n // Format duration\n const durationSeconds = Math.floor(duration / 1000);\n const minutes = Math.floor(durationSeconds / 60);\n const seconds = durationSeconds % 60;\n const durationStr = `${minutes}m ${seconds}s`;\n\n // Append result section\n const resultSection = `\n\n## Result\nStatus: ${result.status}\n${result.notes ? `Notes: ${result.notes}` : ''}\nDuration: ${durationStr}\nFinished: ${finishedAt.toISOString()}\n`;\n\n appendFileSync(filePath, resultSection, 'utf-8');\n}\n\n/**\n * Read a run log file\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param runId - Run identifier\n * @returns Log content or null if not found\n */\nexport function getRunLog(\n projectKey: string,\n specialistType: string,\n runId: string\n): string | null {\n const filePath = getRunLogPath(projectKey, specialistType, runId);\n\n if (!existsSync(filePath)) {\n return null;\n }\n\n try {\n return readFileSync(filePath, 'utf-8');\n } catch (error) {\n console.error(`Failed to read run log ${runId}:`, error);\n return null;\n }\n}\n\n/**\n * Parse metadata from a log file\n *\n * @param logContent - Log file content\n * @returns Parsed metadata\n */\nexport function parseLogMetadata(logContent: string): Partial<RunLogMetadata> {\n const metadata: Partial<RunLogMetadata> = {};\n\n // Extract metadata from header\n const projectMatch = logContent.match(/^Project: (.+)$/m);\n const startedMatch = logContent.match(/^Started: (.+)$/m);\n const issueMatch = logContent.match(/^Issue: (.+)$/m);\n const runIdMatch = logContent.match(/^Run ID: (.+)$/m);\n const statusMatch = logContent.match(/^Status: (.+)$/m);\n const notesMatch = logContent.match(/^Notes: (.+)$/m);\n const finishedMatch = logContent.match(/^Finished: (.+)$/m);\n const durationMatch = logContent.match(/^Duration: (.+)$/m);\n\n if (projectMatch) metadata.project = projectMatch[1].trim();\n if (startedMatch) metadata.startedAt = startedMatch[1].trim();\n if (issueMatch) metadata.issueId = issueMatch[1].trim();\n if (runIdMatch) metadata.runId = runIdMatch[1].trim();\n if (statusMatch) metadata.status = statusMatch[1].trim() as RunLogMetadata['status'];\n if (notesMatch) metadata.notes = notesMatch[1].trim();\n if (finishedMatch) metadata.finishedAt = finishedMatch[1].trim();\n\n // Parse duration if available\n if (durationMatch) {\n const durationStr = durationMatch[1].trim();\n const minutesMatch = durationStr.match(/(\\d+)m/);\n const secondsMatch = durationStr.match(/(\\d+)s/);\n const minutes = minutesMatch ? parseInt(minutesMatch[1], 10) : 0;\n const seconds = secondsMatch ? parseInt(secondsMatch[1], 10) : 0;\n metadata.duration = (minutes * 60 + seconds) * 1000;\n }\n\n return metadata;\n}\n\n/**\n * List all run logs for a project's specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param options - Listing options\n * @returns Array of run log entries, sorted by most recent first\n */\nexport function listRunLogs(\n projectKey: string,\n specialistType: string,\n options: {\n limit?: number;\n offset?: number;\n } = {}\n): RunLogEntry[] {\n const runsDir = getRunsDirectory(projectKey, specialistType);\n\n if (!existsSync(runsDir)) {\n return [];\n }\n\n try {\n const files = readdirSync(runsDir)\n .filter(f => f.endsWith('.log'))\n .map(f => {\n const filePath = join(runsDir, f);\n const stats = statSync(filePath);\n const runId = basename(f, '.log');\n\n // Read file to extract metadata\n const content = readFileSync(filePath, 'utf-8');\n const metadata = parseLogMetadata(content);\n\n return {\n runId,\n filePath,\n metadata: {\n runId,\n project: projectKey,\n specialistType,\n issueId: metadata.issueId || 'unknown',\n startedAt: metadata.startedAt || stats.birthtime.toISOString(),\n finishedAt: metadata.finishedAt,\n status: metadata.status,\n duration: metadata.duration,\n notes: metadata.notes,\n },\n fileSize: stats.size,\n createdAt: stats.birthtime,\n };\n });\n\n // Sort by most recent first, with runId as tiebreaker for stable ordering\n files.sort((a, b) => {\n const timeDiff = b.createdAt.getTime() - a.createdAt.getTime();\n if (timeDiff !== 0) return timeDiff;\n return b.runId.localeCompare(a.runId);\n });\n\n // Apply pagination\n const { limit, offset = 0 } = options;\n if (limit !== undefined) {\n return files.slice(offset, offset + limit);\n }\n\n return files.slice(offset);\n } catch (error) {\n console.error(`Failed to list run logs for ${projectKey}/${specialistType}:`, error);\n return [];\n }\n}\n\n/**\n * Get the most recent N run logs\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param count - Number of recent runs to retrieve\n * @returns Array of recent run log entries\n */\nexport function getRecentRunLogs(\n projectKey: string,\n specialistType: string,\n count: number\n): RunLogEntry[] {\n return listRunLogs(projectKey, specialistType, { limit: count });\n}\n\n/**\n * Clean up old run logs based on retention policy\n *\n * Keeps logs that match either criteria (whichever is more permissive):\n * - Within maxDays\n * - Within the last maxRuns count\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param retention - Retention policy\n * @returns Number of logs deleted\n */\nexport function cleanupOldLogs(\n projectKey: string,\n specialistType: string,\n retention: { maxDays: number; maxRuns: number }\n): number {\n const { maxDays, maxRuns } = retention;\n\n // Compute cutoff BEFORE reading file stats. This ensures all files that existed\n // when cleanup was invoked have birthtimes <= cutoffDate when maxDays=0, avoiding\n // a race where a file created in the same millisecond as cutoffDate would be\n // incorrectly retained by a >= comparison.\n const now = new Date();\n const cutoffDate = new Date(now.getTime() - maxDays * 24 * 60 * 60 * 1000);\n\n const allLogs = listRunLogs(projectKey, specialistType);\n\n if (allLogs.length === 0) {\n return 0;\n }\n\n let deletedCount = 0;\n\n allLogs.forEach((log, index) => {\n // Keep if within maxRuns (most recent N runs)\n if (index < maxRuns) {\n return;\n }\n\n // Keep if within maxDays. Skip the age check entirely when maxDays=0, because\n // \"within 0 days\" means no age-based protection — only maxRuns applies.\n // This avoids a timing/rounding issue: Node.js converts nanosecond birthtime\n // to milliseconds using standard rounding, so a file created at 431.6ms gets\n // birthtime 432ms. If cutoff is 431ms (same wall-clock millisecond), the file\n // incorrectly appears newer than the cutoff and gets retained.\n if (maxDays > 0 && log.createdAt >= cutoffDate) {\n return;\n }\n\n // Delete this log\n try {\n unlinkSync(log.filePath);\n deletedCount++;\n console.log(`[specialist-logs] Deleted old log: ${log.runId}`);\n } catch (error) {\n console.error(`[specialist-logs] Failed to delete ${log.runId}:`, error);\n }\n });\n\n return deletedCount;\n}\n\n/**\n * Check if a run log is still active (not finalized)\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param runId - Run identifier\n * @returns True if log exists but has no result section yet\n */\nexport function isRunLogActive(\n projectKey: string,\n specialistType: string,\n runId: string\n): boolean {\n const content = getRunLog(projectKey, specialistType, runId);\n\n if (!content) {\n return false;\n }\n\n // Check if Result section exists\n return !content.includes('## Result');\n}\n\n/**\n * Get file size of a run log (useful for truncation check)\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param runId - Run identifier\n * @returns File size in bytes or null if not found\n */\nexport function getRunLogSize(\n projectKey: string,\n specialistType: string,\n runId: string\n): number | null {\n const filePath = getRunLogPath(projectKey, specialistType, runId);\n\n if (!existsSync(filePath)) {\n return null;\n }\n\n try {\n const stats = statSync(filePath);\n return stats.size;\n } catch (error) {\n return null;\n }\n}\n\n/**\n * Maximum log file size (10MB) before truncation warning\n */\nexport const MAX_LOG_SIZE = 10 * 1024 * 1024;\n\n/**\n * Check if a log file is approaching or exceeding size limits\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param runId - Run identifier\n * @returns Warning info or null if size is OK\n */\nexport function checkLogSizeLimit(\n projectKey: string,\n specialistType: string,\n runId: string\n): { exceeded: boolean; size: number; limit: number } | null {\n const size = getRunLogSize(projectKey, specialistType, runId);\n\n if (size === null) {\n return null;\n }\n\n if (size >= MAX_LOG_SIZE) {\n return {\n exceeded: true,\n size,\n limit: MAX_LOG_SIZE,\n };\n }\n\n return null;\n}\n\n/**\n * Clean up old logs for all projects and specialists\n *\n * Runs cleanup based on retention policies configured in projects.yaml.\n * This should be called periodically (e.g., daily cron job).\n *\n * @returns Summary of cleanup results\n */\nexport function cleanupAllLogs(): {\n totalDeleted: number;\n byProject: Record<string, Record<string, number>>;\n} {\n const { listProjectsWithSpecialists } = require('./specialists.js');\n const { getSpecialistRetention } = require('../projects.js');\n\n const results = {\n totalDeleted: 0,\n byProject: {} as Record<string, Record<string, number>>,\n };\n\n const projects = listProjectsWithSpecialists();\n\n for (const projectKey of projects) {\n results.byProject[projectKey] = {};\n\n // Get retention policy for this project\n const retention = getSpecialistRetention(projectKey);\n\n // Clean up each specialist type\n const specialistTypes = ['review-agent', 'test-agent', 'merge-agent'];\n\n for (const specialistType of specialistTypes) {\n const deleted = cleanupOldLogs(projectKey, specialistType, retention);\n\n if (deleted > 0) {\n results.byProject[projectKey][specialistType] = deleted;\n results.totalDeleted += deleted;\n }\n }\n }\n\n console.log(`[specialist-logs] Cleanup complete: deleted ${results.totalDeleted} old logs`);\n\n return results;\n}\n"],"mappings":"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","names":["existsSync","readFileSync","readdirSync","join","provider","pricing","existsSync","mkdirSync","appendFileSync","readFileSync","join","readFileSync","writeFileSync","existsSync","mkdirSync","readdirSync","appendFileSync","join","basename","homedir","createRunLog","finalizeRunLog","cleanupOldLogs","name","existsSync","mkdirSync","writeFileSync","appendFileSync","readFileSync","readdirSync","statSync","unlinkSync","join","basename","listProjectsWithSpecialists"]}
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  init_projects,
7
7
  loadProjectsConfig
8
- } from "./chunk-2V4NF7J2.js";
8
+ } from "./chunk-RLZQB7HS.js";
9
9
  import {
10
10
  SOURCE_TRAEFIK_TEMPLATES,
11
11
  TRAEFIK_CERTS_DIR,
@@ -30,7 +30,7 @@ function generatePanopticonTraefikConfig() {
30
30
  if (!existsSync(templatePath)) {
31
31
  return false;
32
32
  }
33
- const { config } = loadConfig();
33
+ const config = loadConfig();
34
34
  const placeholders = {
35
35
  TRAEFIK_DOMAIN: config.traefik?.domain || "pan.localhost",
36
36
  DASHBOARD_PORT: String(config.dashboard.port),
@@ -151,4 +151,4 @@ export {
151
151
  ensureProjectCerts,
152
152
  cleanupStaleTlsSections
153
153
  };
154
- //# sourceMappingURL=chunk-NLQRED36.js.map
154
+ //# sourceMappingURL=chunk-KBHRXV5T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/traefik.ts"],"sourcesContent":["/**\n * Traefik Configuration Generator\n *\n * Generates the Panopticon dashboard Traefik routing config\n * from a template, substituting values from config.toml.\n * Also generates TLS certificate configuration from discovered certs.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync } from 'fs';\nimport { join, basename } from 'path';\nimport { execSync } from 'child_process';\nimport { TRAEFIK_DYNAMIC_DIR, TRAEFIK_CERTS_DIR, TRAEFIK_DIR, SOURCE_TRAEFIK_TEMPLATES } from './paths.js';\nimport { loadConfig } from './config.js';\nimport { loadProjectsConfig } from './projects.js';\n\n/**\n * Generate panopticon.yml from template using current config values.\n * Safe to call multiple times (idempotent).\n * Returns true if file was written, false if template not found.\n */\nexport function generatePanopticonTraefikConfig(): boolean {\n const templatePath = join(SOURCE_TRAEFIK_TEMPLATES, 'dynamic', 'panopticon.yml.template');\n if (!existsSync(templatePath)) {\n return false;\n }\n\n const config = loadConfig();\n const placeholders: Record<string, string> = {\n TRAEFIK_DOMAIN: config.traefik?.domain || 'pan.localhost',\n DASHBOARD_PORT: String(config.dashboard.port),\n DASHBOARD_API_PORT: String(config.dashboard.api_port),\n };\n\n let content = readFileSync(templatePath, 'utf-8');\n for (const [key, value] of Object.entries(placeholders)) {\n content = content.replace(new RegExp(`\\\\{\\\\{${key}\\\\}\\\\}`, 'g'), value);\n }\n\n mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });\n const outputPath = join(TRAEFIK_DYNAMIC_DIR, 'panopticon.yml');\n writeFileSync(outputPath, content, 'utf-8');\n return true;\n}\n\n/**\n * Remove any accidentally-copied .template files from the runtime Traefik dir.\n * Called after copyDirectoryRecursive in pan install.\n */\nexport function cleanupTemplateFiles(): void {\n const copiedTemplate = join(TRAEFIK_DYNAMIC_DIR, 'panopticon.yml.template');\n if (existsSync(copiedTemplate)) {\n unlinkSync(copiedTemplate);\n }\n}\n\n/**\n * Generate tls.yml from all discovered certificate files in the certs directory.\n *\n * Traefik v3 ignores `tls:` sections when they appear in the same dynamic config\n * file as `http:` routers/services. This function creates a dedicated tls.yml file\n * that Traefik's file provider will pick up separately.\n *\n * The first cert found (pan.localhost) is used as the default certificate.\n * All certs are listed in the certificates array for SNI matching.\n *\n * Safe to call multiple times (idempotent).\n * Returns true if file was written, false if no certs found.\n */\nexport function generateTlsConfig(): boolean {\n if (!existsSync(TRAEFIK_CERTS_DIR)) {\n return false;\n }\n\n // Scan for cert files (exclude -key.pem files)\n const files = readdirSync(TRAEFIK_CERTS_DIR);\n const certFiles = files.filter(f => f.endsWith('.pem') && !f.endsWith('-key.pem'));\n\n if (certFiles.length === 0) {\n return false;\n }\n\n // Pair each cert with its key file\n const certPairs: Array<{ certFile: string; keyFile: string }> = [];\n for (const certFile of certFiles) {\n const keyFile = certFile.replace('.pem', '-key.pem');\n if (files.includes(keyFile)) {\n certPairs.push({\n certFile: `/etc/traefik/certs/${certFile}`,\n keyFile: `/etc/traefik/certs/${keyFile}`,\n });\n }\n }\n\n if (certPairs.length === 0) {\n return false;\n }\n\n // Use the pan.localhost cert as default, fall back to first cert\n const defaultCert = certPairs.find(p => p.certFile.includes('pan.localhost')) || certPairs[0];\n\n // Build YAML content\n let yaml = '# Auto-generated TLS configuration — do not edit manually\\n';\n yaml += '# Generated by: pan up / pan install\\n';\n yaml += '# Traefik v3 requires TLS config in a separate dynamic config file\\n\\n';\n yaml += 'tls:\\n';\n yaml += ' stores:\\n';\n yaml += ' default:\\n';\n yaml += ' defaultCertificate:\\n';\n yaml += ` certFile: ${defaultCert.certFile}\\n`;\n yaml += ` keyFile: ${defaultCert.keyFile}\\n`;\n yaml += ' certificates:\\n';\n for (const pair of certPairs) {\n yaml += ` - certFile: ${pair.certFile}\\n`;\n yaml += ` keyFile: ${pair.keyFile}\\n`;\n }\n\n mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });\n const outputPath = join(TRAEFIK_DYNAMIC_DIR, 'tls.yml');\n writeFileSync(outputPath, yaml, 'utf-8');\n return true;\n}\n\n/**\n * Ensure wildcard certificates exist for all registered projects that have DNS domains.\n *\n * Scans projects.yaml for projects with workspace.dns.domain, and generates\n * mkcert wildcard certs for any that don't already have certs in the Traefik\n * certs directory.\n *\n * Returns array of domains that had certs generated.\n */\nexport function ensureProjectCerts(): string[] {\n // Check mkcert is available\n try {\n execSync('which mkcert', { stdio: 'pipe' });\n } catch {\n return [];\n }\n\n const projectsConfig = loadProjectsConfig();\n const generated: string[] = [];\n\n for (const [, project] of Object.entries(projectsConfig.projects)) {\n const domain = project.workspace?.dns?.domain;\n if (!domain) continue;\n\n const certFile = join(TRAEFIK_CERTS_DIR, `_wildcard.${domain}.pem`);\n const keyFile = join(TRAEFIK_CERTS_DIR, `_wildcard.${domain}-key.pem`);\n\n if (existsSync(certFile) && existsSync(keyFile)) {\n continue;\n }\n\n // Generate cert for this project's domain\n mkdirSync(TRAEFIK_CERTS_DIR, { recursive: true });\n try {\n execSync(\n `mkcert -cert-file \"${certFile}\" -key-file \"${keyFile}\" \"${domain}\" \"*.${domain}\" 2>/dev/null`,\n { stdio: 'pipe' }\n );\n generated.push(domain);\n } catch {\n // mkcert failed — skip this domain\n }\n }\n\n return generated;\n}\n\n/**\n * Remove stale `tls:` sections from runtime config files.\n *\n * Traefik v3 ignores tls: in static config (traefik.yml) and in dynamic\n * config files that also contain http: routers. This function strips those\n * dead sections to avoid confusion.\n *\n * Called during `pan up` to clean up configs from older Panopticon versions.\n */\nexport function cleanupStaleTlsSections(): void {\n // Clean static config (traefik.yml)\n const staticConfig = join(TRAEFIK_DIR, 'traefik.yml');\n if (existsSync(staticConfig)) {\n const content = readFileSync(staticConfig, 'utf-8');\n // Remove tls: section at the end of the file\n const cleaned = content.replace(/\\n# TLS Configuration\\ntls:\\n(?: .*\\n)*/g, '\\n');\n if (cleaned !== content) {\n writeFileSync(staticConfig, cleaned, 'utf-8');\n }\n }\n\n // Clean dynamic panopticon.yml (regenerated from template, but also clean runtime copy)\n const dynamicConfig = join(TRAEFIK_DYNAMIC_DIR, 'panopticon.yml');\n if (existsSync(dynamicConfig)) {\n const content = readFileSync(dynamicConfig, 'utf-8');\n // Remove standalone tls: section (not nested under http: routers)\n const cleaned = content.replace(/\\ntls:\\n (?:stores|certificates):\\n(?: .*\\n)*/g, '\\n');\n if (cleaned !== content) {\n writeFileSync(dynamicConfig, cleaned, 'utf-8');\n }\n }\n}\n"],"mappings":"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","names":[]}