panopticon-cli 0.4.33 → 0.5.1

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 (71) hide show
  1. package/README.md +96 -210
  2. package/dist/{agents-VLK4BMVA.js → agents-5OPQKM5K.js} +6 -5
  3. package/dist/{chunk-OMNXYPXC.js → chunk-2V4NF7J2.js} +14 -1
  4. package/dist/chunk-2V4NF7J2.js.map +1 -0
  5. package/dist/{chunk-XKT5MHPT.js → chunk-4YSYJ4HM.js} +2 -2
  6. package/dist/{chunk-XFR2DLMR.js → chunk-76F6DSVS.js} +49 -10
  7. package/dist/chunk-76F6DSVS.js.map +1 -0
  8. package/dist/{chunk-PI7Y3PSN.js → chunk-F5555J3A.js} +42 -6
  9. package/dist/chunk-F5555J3A.js.map +1 -0
  10. package/dist/{chunk-KJ2TRXNK.js → chunk-FTCPTHIJ.js} +47 -420
  11. package/dist/chunk-FTCPTHIJ.js.map +1 -0
  12. package/dist/chunk-HJSM6E6U.js +1038 -0
  13. package/dist/chunk-HJSM6E6U.js.map +1 -0
  14. package/dist/{chunk-RBUO57TC.js → chunk-NLQRED36.js} +3 -3
  15. package/dist/chunk-NLQRED36.js.map +1 -0
  16. package/dist/{chunk-ASY7T35E.js → chunk-OWHXCGVO.js} +245 -90
  17. package/dist/chunk-OWHXCGVO.js.map +1 -0
  18. package/dist/{chunk-BKCWRMUX.js → chunk-VHKSS7QX.js} +106 -11
  19. package/dist/chunk-VHKSS7QX.js.map +1 -0
  20. package/dist/{chunk-GFP3PIPB.js → chunk-YGJ54GW2.js} +1 -1
  21. package/dist/chunk-YGJ54GW2.js.map +1 -0
  22. package/dist/cli/index.js +1521 -935
  23. package/dist/cli/index.js.map +1 -1
  24. package/dist/dashboard/prompts/work-agent.md +2 -0
  25. package/dist/dashboard/public/assets/index-Ce6q21Fm.js +743 -0
  26. package/dist/dashboard/public/assets/{index-UjZq6ykz.css → index-NzpI0ItZ.css} +1 -1
  27. package/dist/dashboard/public/index.html +2 -2
  28. package/dist/dashboard/server.js +4274 -2320
  29. package/dist/{feedback-writer-LVZ5TFYZ.js → feedback-writer-VRMMWWTW.js} +2 -2
  30. package/dist/git-utils-I2UDKNZH.js +131 -0
  31. package/dist/git-utils-I2UDKNZH.js.map +1 -0
  32. package/dist/index.d.ts +12 -1
  33. package/dist/index.js +5 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/{projects-JEIVIYC6.js → projects-CFX3RTDL.js} +4 -2
  36. package/dist/{remote-workspace-AHVHQEES.js → remote-workspace-7FPGF2RM.js} +2 -2
  37. package/dist/{review-status-EPFG4XM7.js → review-status-TDPSOU5J.js} +2 -2
  38. package/dist/{specialist-context-T3NBMCIE.js → specialist-context-WGUUYDWY.js} +5 -5
  39. package/dist/{specialist-logs-CVKD3YJ3.js → specialist-logs-XJB5TCKJ.js} +5 -5
  40. package/dist/{specialists-TKAP6T6Z.js → specialists-5LBRHYFA.js} +5 -5
  41. package/dist/{traefik-QX4ZV4YG.js → traefik-WFMQX2LY.js} +3 -3
  42. package/dist/{workspace-manager-KLHUCIZV.js → workspace-manager-E434Z45T.js} +2 -2
  43. package/package.json +1 -1
  44. package/scripts/record-cost-event.js +5 -5
  45. package/scripts/stop-hook +7 -0
  46. package/scripts/work-agent-stop-hook +137 -0
  47. package/skills/myn-standards/SKILL.md +351 -0
  48. package/skills/pan-new-project/SKILL.md +304 -0
  49. package/skills/write-spec/SKILL.md +138 -0
  50. package/dist/chunk-7XNJJBH6.js +0 -538
  51. package/dist/chunk-7XNJJBH6.js.map +0 -1
  52. package/dist/chunk-ASY7T35E.js.map +0 -1
  53. package/dist/chunk-BKCWRMUX.js.map +0 -1
  54. package/dist/chunk-GFP3PIPB.js.map +0 -1
  55. package/dist/chunk-KJ2TRXNK.js.map +0 -1
  56. package/dist/chunk-OMNXYPXC.js.map +0 -1
  57. package/dist/chunk-PI7Y3PSN.js.map +0 -1
  58. package/dist/chunk-RBUO57TC.js.map +0 -1
  59. package/dist/chunk-XFR2DLMR.js.map +0 -1
  60. package/dist/dashboard/public/assets/index-kAJqtLDO.js +0 -708
  61. /package/dist/{agents-VLK4BMVA.js.map → agents-5OPQKM5K.js.map} +0 -0
  62. /package/dist/{chunk-XKT5MHPT.js.map → chunk-4YSYJ4HM.js.map} +0 -0
  63. /package/dist/{feedback-writer-LVZ5TFYZ.js.map → feedback-writer-VRMMWWTW.js.map} +0 -0
  64. /package/dist/{projects-JEIVIYC6.js.map → projects-CFX3RTDL.js.map} +0 -0
  65. /package/dist/{remote-workspace-AHVHQEES.js.map → remote-workspace-7FPGF2RM.js.map} +0 -0
  66. /package/dist/{review-status-EPFG4XM7.js.map → review-status-TDPSOU5J.js.map} +0 -0
  67. /package/dist/{specialist-context-T3NBMCIE.js.map → specialist-context-WGUUYDWY.js.map} +0 -0
  68. /package/dist/{specialist-logs-CVKD3YJ3.js.map → specialist-logs-XJB5TCKJ.js.map} +0 -0
  69. /package/dist/{specialists-TKAP6T6Z.js.map → specialists-5LBRHYFA.js.map} +0 -0
  70. /package/dist/{traefik-QX4ZV4YG.js.map → traefik-WFMQX2LY.js.map} +0 -0
  71. /package/dist/{workspace-manager-KLHUCIZV.js.map → workspace-manager-E434Z45T.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/settings.ts","../src/lib/providers.ts","../src/lib/config-yaml.ts"],"sourcesContent":["import { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { SETTINGS_FILE } from './paths.js';\n\n// Model identifiers\nexport type AnthropicModel = 'claude-opus-4-6' | 'claude-sonnet-4-6' | 'claude-sonnet-4-5' | 'claude-haiku-4-5';\nexport type OpenAIModel = 'gpt-5.2-codex' | 'o3-deep-research' | 'gpt-4o' | 'gpt-4o-mini';\nexport type GoogleModel = 'gemini-3-pro-preview' | 'gemini-3-flash-preview' | 'gemini-2.5-pro' | 'gemini-2.5-flash';\nexport type ZAIModel = 'glm-4.7' | 'glm-4.7-flash';\nexport type KimiModel = 'kimi-k2' | 'kimi-k2.5';\nexport type ModelId = AnthropicModel | OpenAIModel | GoogleModel | ZAIModel | KimiModel;\n\n// Task complexity levels\nexport type ComplexityLevel = 'trivial' | 'simple' | 'medium' | 'complex' | 'expert';\n\n// Specialist agent types\nexport interface SpecialistModels {\n review_agent: ModelId;\n test_agent: ModelId;\n merge_agent: ModelId;\n}\n\n// Complexity-based model mapping\nexport type ComplexityModels = {\n [K in ComplexityLevel]: ModelId;\n};\n\n// All model configuration\nexport interface ModelsConfig {\n specialists: SpecialistModels;\n status_review: ModelId;\n complexity: ComplexityModels;\n}\n\n// API keys for external providers\nexport interface ApiKeysConfig {\n openai?: string;\n google?: string;\n zai?: string;\n kimi?: string;\n}\n\n// Complete settings structure\nexport interface SettingsConfig {\n models: ModelsConfig;\n api_keys: ApiKeysConfig;\n}\n\n// Default settings - match optimal defaults from settings-api.ts\nconst DEFAULT_SETTINGS: SettingsConfig = {\n models: {\n specialists: {\n review_agent: 'claude-opus-4-6',\n test_agent: 'claude-sonnet-4-6',\n merge_agent: 'claude-sonnet-4-6',\n },\n status_review: 'claude-opus-4-6',\n complexity: {\n trivial: 'claude-haiku-4-5',\n simple: 'claude-haiku-4-5',\n medium: 'kimi-k2.5',\n complex: 'kimi-k2.5',\n expert: 'claude-opus-4-6',\n },\n },\n api_keys: {},\n};\n\n/**\n * Deep merge utility that recursively merges objects.\n * - Recursively merges nested objects\n * - User values take precedence over defaults\n */\nfunction deepMerge<T extends object>(defaults: T, overrides: Partial<T>): T {\n const result = { ...defaults };\n\n for (const key of Object.keys(overrides) as (keyof T)[]) {\n const defaultVal = defaults[key];\n const overrideVal = overrides[key];\n\n // Skip undefined values in overrides\n if (overrideVal === undefined) continue;\n\n // Deep merge if both values are non-array objects\n if (\n typeof defaultVal === 'object' &&\n defaultVal !== null &&\n !Array.isArray(defaultVal) &&\n typeof overrideVal === 'object' &&\n overrideVal !== null &&\n !Array.isArray(overrideVal)\n ) {\n result[key] = deepMerge(defaultVal, overrideVal as any);\n } else {\n // For primitives or null - override wins\n result[key] = overrideVal as T[keyof T];\n }\n }\n\n return result;\n}\n\n/**\n * Load settings from ~/.panopticon/settings.json\n * Returns default settings if file doesn't exist or is invalid\n * Also loads API keys from environment variables as fallback\n */\nexport function loadSettings(): SettingsConfig {\n let settings: SettingsConfig;\n\n if (!existsSync(SETTINGS_FILE)) {\n settings = getDefaultSettings();\n } else {\n try {\n const content = readFileSync(SETTINGS_FILE, 'utf8');\n const parsed = JSON.parse(content) as Partial<SettingsConfig>;\n settings = deepMerge(DEFAULT_SETTINGS, parsed);\n } catch (error) {\n console.error('Warning: Failed to parse settings.json, using defaults');\n settings = getDefaultSettings();\n }\n }\n\n // Load API keys from environment variables as fallback\n // This allows using ~/.panopticon.env for API keys\n const envApiKeys: ApiKeysConfig = {};\n if (process.env.OPENAI_API_KEY) envApiKeys.openai = process.env.OPENAI_API_KEY;\n if (process.env.GOOGLE_API_KEY) envApiKeys.google = process.env.GOOGLE_API_KEY;\n if (process.env.ZAI_API_KEY) envApiKeys.zai = process.env.ZAI_API_KEY;\n if (process.env.KIMI_API_KEY) envApiKeys.kimi = process.env.KIMI_API_KEY;\n\n // Merge env vars as fallback (settings.json takes precedence)\n settings.api_keys = {\n ...envApiKeys,\n ...settings.api_keys,\n };\n\n return settings;\n}\n\n/**\n * Save settings to ~/.panopticon/settings.json\n * Writes with pretty formatting (2-space indent)\n */\nexport function saveSettings(settings: SettingsConfig): void {\n const content = JSON.stringify(settings, null, 2);\n writeFileSync(SETTINGS_FILE, content, 'utf8');\n}\n\n/**\n * Validate settings structure and model IDs\n * Returns error message if invalid, null if valid\n */\nexport function validateSettings(settings: SettingsConfig): string | null {\n // Validate models structure\n if (!settings.models) {\n return 'Missing models configuration';\n }\n\n // Validate specialists\n if (!settings.models.specialists) {\n return 'Missing specialists configuration';\n }\n const specialists = settings.models.specialists;\n if (!specialists.review_agent || !specialists.test_agent || !specialists.merge_agent) {\n return 'Missing specialist agent model configuration';\n }\n\n // Validate complexity levels\n if (!settings.models.complexity) {\n return 'Missing complexity configuration';\n }\n const complexity = settings.models.complexity;\n const requiredLevels: ComplexityLevel[] = ['trivial', 'simple', 'medium', 'complex', 'expert'];\n for (const level of requiredLevels) {\n if (!complexity[level]) {\n return `Missing complexity level: ${level}`;\n }\n }\n\n // Validate api_keys structure (optional keys)\n if (!settings.api_keys) {\n return 'Missing api_keys configuration';\n }\n\n return null;\n}\n\n/**\n * Get a deep copy of the default settings\n */\nexport function getDefaultSettings(): SettingsConfig {\n return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));\n}\n\n/**\n * Get available models for a provider based on configured API keys\n * Returns empty array if provider API key is not configured\n */\nexport function getAvailableModels(settings: SettingsConfig): {\n anthropic: AnthropicModel[];\n openai: OpenAIModel[];\n google: GoogleModel[];\n zai: ZAIModel[];\n kimi: KimiModel[];\n} {\n const anthropicModels: AnthropicModel[] = [\n 'claude-opus-4-6',\n 'claude-sonnet-4-6',\n 'claude-haiku-4-5',\n ];\n\n const openaiModels: OpenAIModel[] = settings.api_keys.openai\n ? ['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini']\n : [];\n\n const googleModels: GoogleModel[] = settings.api_keys.google\n ? ['gemini-3-pro-preview', 'gemini-3-flash-preview']\n : [];\n\n const zaiModels: ZAIModel[] = settings.api_keys.zai\n ? ['glm-4.7', 'glm-4.7-flash']\n : [];\n\n const kimiModels: KimiModel[] = settings.api_keys.kimi\n ? ['kimi-k2', 'kimi-k2.5']\n : [];\n\n return {\n anthropic: anthropicModels,\n openai: openaiModels,\n google: googleModels,\n zai: zaiModels,\n kimi: kimiModels,\n };\n}\n\n/**\n * Check if a model ID is an Anthropic model\n * Anthropic models can be run directly with `claude` CLI\n */\nexport function isAnthropicModel(modelId: ModelId | string): boolean {\n return modelId.startsWith('claude-');\n}\n\n/**\n * Get the Claude CLI model flag for an Anthropic model\n * Maps our model IDs to Claude's expected format\n */\nexport function getClaudeModelFlag(modelId: ModelId | string): string {\n const modelMap: Record<string, string> = {\n 'claude-opus-4-6': 'opus',\n 'claude-sonnet-4-6': 'sonnet',\n 'claude-sonnet-4-5': 'sonnet',\n 'claude-haiku-4-5': 'haiku',\n };\n return modelMap[modelId] || 'sonnet';\n}\n\n/**\n * Get the command to run an agent with a specific model\n * Returns 'claude' for Anthropic models, 'claude-code-router' for others\n */\nexport function getAgentCommand(modelId: ModelId | string): { command: string; args: string[] } {\n if (isAnthropicModel(modelId)) {\n return {\n command: 'claude',\n args: ['--model', getClaudeModelFlag(modelId)],\n };\n }\n // Non-Anthropic models require the router\n return {\n command: 'claude-code-router',\n args: [],\n };\n}\n","/**\r\n * Provider Configuration and Compatibility\r\n *\r\n * Defines which LLM providers are compatible with Claude Code's API format.\r\n * - Direct providers: Implement Anthropic-compatible API (no router needed)\r\n * - Router providers: Require claude-code-router for API translation\r\n */\r\n\r\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\r\nimport { join } from 'path';\r\nimport type { ModelId, AnthropicModel, OpenAIModel, GoogleModel, ZAIModel } from './settings.js';\r\n\r\nexport type ProviderName = 'anthropic' | 'kimi' | 'openai' | 'google' | 'zai';\r\n\r\n/**\r\n * Provider compatibility types\r\n * - direct: Anthropic-compatible API, use ANTHROPIC_BASE_URL directly\r\n * - router: Incompatible API, requires claude-code-router for translation\r\n */\r\nexport type ProviderCompatibility = 'direct' | 'router';\r\n\r\n/**\r\n * Provider configuration\r\n */\r\n/**\r\n * Auth type for direct providers:\r\n * - static: Use a long-lived API key passed via ANTHROPIC_AUTH_TOKEN (default)\r\n * - credential-file: Use apiKeyHelper to read a fresh token from a credential file.\r\n * Used for providers like Kimi Code Plan whose JWT tokens expire every ~15 minutes.\r\n */\r\nexport type ProviderAuthType = 'static' | 'credential-file';\r\n\r\nexport interface ProviderConfig {\r\n name: ProviderName;\r\n displayName: string;\r\n compatibility: ProviderCompatibility;\r\n baseUrl?: string; // For direct providers\r\n authType?: ProviderAuthType; // Defaults to 'static'\r\n credentialFile?: string; // Path to credential file (for 'credential-file' auth)\r\n credentialHelper?: string; // Script that reads credential file and prints token\r\n models: ModelId[];\r\n tested: boolean; // Whether compatibility has been verified\r\n description: string;\r\n}\r\n\r\n/**\r\n * All provider configurations\r\n */\r\nexport const PROVIDERS: Record<ProviderName, ProviderConfig> = {\r\n anthropic: {\r\n name: 'anthropic',\r\n displayName: 'Anthropic',\r\n compatibility: 'direct',\r\n models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5', 'claude-haiku-4-5'],\r\n tested: true,\r\n description: 'Native Claude API',\r\n },\r\n\r\n kimi: {\r\n name: 'kimi',\r\n displayName: 'Kimi (Moonshot AI)',\r\n compatibility: 'direct',\r\n baseUrl: 'https://api.kimi.com/coding/',\r\n authType: 'credential-file',\r\n credentialFile: '~/.kimi/credentials/kimi-code.json',\r\n credentialHelper: '~/.panopticon/bin/kimi-token-helper.sh',\r\n models: [], // Kimi uses same model names as Anthropic\r\n tested: true,\r\n description: 'Anthropic-compatible API via Kimi Code Plan (OAuth token refresh)',\r\n },\r\n\r\n zai: {\r\n name: 'zai',\r\n displayName: 'Z.AI (GLM)',\r\n compatibility: 'direct',\r\n baseUrl: 'https://api.z.ai/api/anthropic',\r\n models: ['glm-4.7', 'glm-4.7-flash'],\r\n tested: true,\r\n description: 'Anthropic-compatible API, tested 2026-01-28',\r\n },\r\n\r\n openai: {\r\n name: 'openai',\r\n displayName: 'OpenAI',\r\n compatibility: 'router',\r\n models: ['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini'],\r\n tested: false,\r\n description: 'Requires claude-code-router for API translation',\r\n },\r\n\r\n google: {\r\n name: 'google',\r\n displayName: 'Google (Gemini)',\r\n compatibility: 'router',\r\n models: ['gemini-3-pro-preview', 'gemini-3-flash-preview'],\r\n tested: false,\r\n description: 'Requires claude-code-router for API translation',\r\n },\r\n};\r\n\r\n/**\r\n * Get provider for a given model ID\r\n */\r\nexport function getProviderForModel(modelId: ModelId): ProviderConfig {\r\n // Check Anthropic models\r\n if (['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-sonnet-4-5', 'claude-haiku-4-5'].includes(modelId)) {\r\n return PROVIDERS.anthropic;\r\n }\r\n\r\n // Check OpenAI models\r\n if (['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini'].includes(modelId)) {\r\n return PROVIDERS.openai;\r\n }\r\n\r\n // Check Google models\r\n if (['gemini-3-pro-preview', 'gemini-3-flash-preview'].includes(modelId)) {\r\n return PROVIDERS.google;\r\n }\r\n\r\n // Check Z.AI models\r\n if (['glm-4.7', 'glm-4.7-flash'].includes(modelId)) {\r\n return PROVIDERS.zai;\r\n }\r\n\r\n // Check Kimi models\r\n if (['kimi-k2', 'kimi-k2.5'].includes(modelId)) {\r\n return PROVIDERS.kimi;\r\n }\r\n\r\n // Default to Anthropic if unknown\r\n return PROVIDERS.anthropic;\r\n}\r\n\r\n/**\r\n * Check if a provider requires claude-code-router\r\n */\r\nexport function requiresRouter(provider: ProviderName): boolean {\r\n return PROVIDERS[provider].compatibility === 'router';\r\n}\r\n\r\n/**\r\n * Get all providers that require router (have router compatibility)\r\n */\r\nexport function getRouterProviders(): ProviderConfig[] {\r\n return Object.values(PROVIDERS).filter(p => p.compatibility === 'router');\r\n}\r\n\r\n/**\r\n * Get all direct-compatible providers\r\n */\r\nexport function getDirectProviders(): ProviderConfig[] {\r\n return Object.values(PROVIDERS).filter(p => p.compatibility === 'direct');\r\n}\r\n\r\n/**\r\n * Check if any configured providers require router\r\n * Used to determine if router installation is needed\r\n */\r\nexport function needsRouter(apiKeys: { openai?: string; google?: string; zai?: string }): boolean {\r\n return !!(apiKeys.openai || apiKeys.google);\r\n}\r\n\r\n/**\r\n * Get environment variables for spawning agent with specific provider\r\n */\r\nexport function getProviderEnv(\r\n provider: ProviderConfig,\r\n apiKey: string\r\n): Record<string, string> {\r\n if (provider.compatibility === 'direct') {\r\n // Direct providers use ANTHROPIC_BASE_URL\r\n const env: Record<string, string> = {};\r\n\r\n if (provider.baseUrl) {\r\n env.ANTHROPIC_BASE_URL = provider.baseUrl;\r\n }\r\n\r\n if (provider.name !== 'anthropic') {\r\n if (provider.authType === 'credential-file') {\r\n // Credential-file providers use apiKeyHelper for dynamic token refresh.\r\n // We still need an initial ANTHROPIC_AUTH_TOKEN for the first request,\r\n // but apiKeyHelper (configured via setupCredentialFileAuth) will keep it fresh.\r\n env.ANTHROPIC_AUTH_TOKEN = apiKey;\r\n // Refresh token every 60 seconds (kimi-cli refreshes credential file automatically)\r\n env.CLAUDE_CODE_API_KEY_HELPER_TTL_MS = '60000';\r\n } else {\r\n // Static providers use a long-lived API key\r\n env.ANTHROPIC_AUTH_TOKEN = apiKey;\r\n }\r\n }\r\n\r\n // Z.AI recommends longer timeout\r\n if (provider.name === 'zai') {\r\n env.API_TIMEOUT_MS = '300000';\r\n }\r\n\r\n return env;\r\n } else {\r\n // Router providers use local router proxy\r\n return {\r\n ANTHROPIC_BASE_URL: 'http://localhost:3456',\r\n ANTHROPIC_AUTH_TOKEN: 'router-managed',\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * For credential-file providers (e.g. Kimi Code Plan), configure Claude Code's\r\n * apiKeyHelper in the workspace settings so tokens are refreshed dynamically.\r\n *\r\n * This writes to .claude/settings.local.json in the workspace directory.\r\n * Must be called before spawning the agent.\r\n */\r\nexport function setupCredentialFileAuth(provider: ProviderConfig, workspacePath: string): void {\r\n if (provider.authType !== 'credential-file' || !provider.credentialHelper) return;\r\n\r\n const helperPath = provider.credentialHelper.replace('~', process.env.HOME || '');\r\n const claudeDir = join(workspacePath, '.claude');\r\n const settingsPath = join(claudeDir, 'settings.local.json');\r\n\r\n if (!existsSync(claudeDir)) {\r\n mkdirSync(claudeDir, { recursive: true });\r\n }\r\n\r\n // Read existing settings or start fresh\r\n let settings: Record<string, unknown> = {};\r\n if (existsSync(settingsPath)) {\r\n try {\r\n settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));\r\n } catch { /* start fresh */ }\r\n }\r\n\r\n // Set the apiKeyHelper to our token reader script\r\n settings.apiKeyHelper = helperPath;\r\n\r\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\\n');\r\n}\r\n","/**\n * YAML Configuration Loader\n *\n * Loads and merges configuration from:\n * 1. Global config: ~/.panopticon/config.yaml\n * 2. Per-project config: .panopticon.yaml (project root)\n *\n * Uses smart (capability-based) model selection - no legacy presets.\n */\n\nimport { readFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport yaml from 'js-yaml';\nimport { WorkTypeId } from './work-types.js';\nimport { ModelId } from './settings.js';\nimport { ModelProvider } from './model-fallback.js';\n\n/**\n * Provider configuration (enable/disable + API keys)\n */\nexport interface ProviderConfig {\n /** Whether this provider is enabled */\n enabled: boolean;\n /** API key (optional, can use env var) */\n api_key?: string;\n}\n\n/**\n * Shadow mode configuration\n */\nexport interface ShadowConfig {\n /** Global shadow mode default */\n enabled?: boolean;\n\n /** Per-tracker overrides */\n trackers?: {\n linear?: boolean;\n github?: boolean;\n gitlab?: boolean;\n rally?: boolean;\n };\n}\n\n/**\n * Complete configuration structure (YAML schema)\n */\nexport interface YamlConfig {\n /** Model configuration */\n models?: {\n /** Provider enable/disable and API keys */\n providers?: {\n anthropic?: ProviderConfig | boolean;\n openai?: ProviderConfig | boolean;\n google?: ProviderConfig | boolean;\n zai?: ProviderConfig | boolean;\n kimi?: ProviderConfig | boolean;\n };\n\n /** Per-work-type overrides (explicit model for specific tasks) */\n overrides?: Partial<Record<WorkTypeId, ModelId>>;\n\n /** Gemini thinking level (1-4) */\n gemini_thinking_level?: 1 | 2 | 3 | 4;\n };\n\n /** Legacy API keys (for backward compatibility) */\n api_keys?: {\n openai?: string;\n google?: string;\n zai?: string;\n kimi?: string;\n };\n\n /** Tracker API keys (override environment variables) */\n tracker_keys?: {\n linear?: string;\n github?: string;\n gitlab?: string;\n rally?: string;\n };\n\n /** Shadow mode configuration */\n shadow?: ShadowConfig;\n}\n\n/**\n * Normalized shadow configuration\n */\nexport interface NormalizedShadowConfig {\n /** Global shadow mode enabled */\n enabled: boolean;\n\n /** Per-tracker overrides */\n trackers: {\n linear: boolean;\n github: boolean;\n gitlab: boolean;\n rally: boolean;\n };\n}\n\n/**\n * Normalized configuration (after loading and merging)\n */\nexport interface NormalizedConfig {\n /** Enabled providers */\n enabledProviders: Set<ModelProvider>;\n\n /** API keys by provider */\n apiKeys: {\n openai?: string;\n google?: string;\n zai?: string;\n kimi?: string;\n };\n\n /** Per-work-type overrides */\n overrides: Partial<Record<WorkTypeId, ModelId>>;\n\n /** Gemini thinking level */\n geminiThinkingLevel: 1 | 2 | 3 | 4;\n\n /** Tracker API keys */\n trackerKeys: {\n linear?: string;\n github?: string;\n gitlab?: string;\n rally?: string;\n };\n\n /** Shadow mode configuration */\n shadow: NormalizedShadowConfig;\n}\n\n/**\n * Default configuration (used when no config files exist)\n */\nconst DEFAULT_CONFIG: NormalizedConfig = {\n enabledProviders: new Set(['anthropic']), // Only Anthropic by default\n apiKeys: {},\n overrides: {},\n geminiThinkingLevel: 3,\n trackerKeys: {},\n shadow: {\n enabled: false,\n trackers: {\n linear: false,\n github: false,\n gitlab: false,\n rally: false,\n },\n },\n};\n\n/**\n * Path to global config file\n */\nconst GLOBAL_CONFIG_PATH = join(homedir(), '.panopticon', 'config.yaml');\n\n/**\n * Normalize a provider config (handle both boolean and object forms)\n */\nfunction normalizeProviderConfig(\n providerConfig: ProviderConfig | boolean | undefined,\n fallbackKey?: string\n): { enabled: boolean; api_key?: string } {\n if (providerConfig === undefined) {\n return { enabled: false };\n }\n\n if (typeof providerConfig === 'boolean') {\n return { enabled: providerConfig, api_key: fallbackKey };\n }\n\n return {\n enabled: providerConfig.enabled,\n api_key: providerConfig.api_key || fallbackKey,\n };\n}\n\n/**\n * Resolve environment variables in config values.\n * If the env var is not set, returns the original reference (e.g., \"$OPENAI_API_KEY\")\n * so the UI can show that it's configured via env var but not resolved.\n */\nfunction resolveEnvVar(value: string | undefined): string | undefined {\n if (!value) return undefined;\n\n // Replace $VAR_NAME or ${VAR_NAME} with environment variable\n // If env var is not set, keep the original reference\n return value.replace(/\\$\\{?([A-Z_][A-Z0-9_]*)\\}?/g, (match, varName) => {\n const envValue = process.env[varName];\n return envValue !== undefined ? envValue : match; // Keep $VAR_NAME if not set\n });\n}\n\n/**\n * Load and parse a YAML config file\n */\nfunction loadYamlFile(filePath: string): YamlConfig | null {\n if (!existsSync(filePath)) {\n return null;\n }\n\n try {\n const content = readFileSync(filePath, 'utf-8');\n const parsed = yaml.load(content) as YamlConfig;\n return parsed || {};\n } catch (error) {\n console.error(`Error loading YAML config from ${filePath}:`, error);\n return null;\n }\n}\n\n/**\n * Find project root by looking for .git directory\n */\nfunction findProjectRoot(startDir: string = process.cwd()): string | null {\n let currentDir = startDir;\n\n while (currentDir !== '/') {\n if (existsSync(join(currentDir, '.git'))) {\n return currentDir;\n }\n currentDir = join(currentDir, '..');\n }\n\n return null;\n}\n\n/**\n * Load per-project config (.panopticon.yaml in project root)\n */\nfunction loadProjectConfig(): YamlConfig | null {\n const projectRoot = findProjectRoot();\n if (!projectRoot) {\n return null;\n }\n\n const projectConfigPath = join(projectRoot, '.panopticon.yaml');\n return loadYamlFile(projectConfigPath);\n}\n\n/**\n * Load global config (~/.panopticon/config.yaml)\n */\nfunction loadGlobalConfig(): YamlConfig | null {\n return loadYamlFile(GLOBAL_CONFIG_PATH);\n}\n\n/**\n * Merge shadow configuration from multiple sources\n */\nfunction mergeShadowConfig(\n result: NormalizedShadowConfig,\n config: YamlConfig | null\n): void {\n if (!config?.shadow) return;\n\n // Merge global enabled flag\n if (config.shadow.enabled !== undefined) {\n result.enabled = config.shadow.enabled;\n }\n\n // Merge per-tracker overrides\n if (config.shadow.trackers) {\n if (config.shadow.trackers.linear !== undefined) {\n result.trackers.linear = config.shadow.trackers.linear;\n }\n if (config.shadow.trackers.github !== undefined) {\n result.trackers.github = config.shadow.trackers.github;\n }\n if (config.shadow.trackers.gitlab !== undefined) {\n result.trackers.gitlab = config.shadow.trackers.gitlab;\n }\n if (config.shadow.trackers.rally !== undefined) {\n result.trackers.rally = config.shadow.trackers.rally;\n }\n }\n}\n\n/**\n * Merge multiple configs with precedence: project > global > defaults\n */\nfunction mergeConfigs(...configs: (YamlConfig | null)[]): NormalizedConfig {\n const result: NormalizedConfig = {\n ...DEFAULT_CONFIG,\n enabledProviders: new Set(DEFAULT_CONFIG.enabledProviders),\n shadow: {\n enabled: DEFAULT_CONFIG.shadow.enabled,\n trackers: { ...DEFAULT_CONFIG.shadow.trackers },\n },\n };\n\n // Filter out null configs\n const validConfigs = configs.filter((c): c is YamlConfig => c !== null);\n\n // Merge in reverse order (lowest precedence first)\n for (const config of validConfigs.reverse()) {\n // Merge providers\n if (config.models?.providers) {\n const providers = config.models.providers;\n const legacyKeys = config.api_keys || {};\n\n // Anthropic (always enabled)\n result.enabledProviders.add('anthropic');\n\n // OpenAI\n const openai = normalizeProviderConfig(providers.openai, legacyKeys.openai);\n if (openai.enabled) {\n result.enabledProviders.add('openai');\n if (openai.api_key) {\n result.apiKeys.openai = resolveEnvVar(openai.api_key);\n }\n }\n\n // Google\n const google = normalizeProviderConfig(providers.google, legacyKeys.google);\n if (google.enabled) {\n result.enabledProviders.add('google');\n if (google.api_key) {\n result.apiKeys.google = resolveEnvVar(google.api_key);\n }\n }\n\n // Z.AI\n const zai = normalizeProviderConfig(providers.zai, legacyKeys.zai);\n if (zai.enabled) {\n result.enabledProviders.add('zai');\n if (zai.api_key) {\n result.apiKeys.zai = resolveEnvVar(zai.api_key);\n }\n }\n\n // Kimi\n const kimi = normalizeProviderConfig(providers.kimi, legacyKeys.kimi);\n if (kimi.enabled) {\n result.enabledProviders.add('kimi');\n if (kimi.api_key) {\n result.apiKeys.kimi = resolveEnvVar(kimi.api_key);\n }\n }\n }\n\n // Merge legacy API keys (for backward compatibility)\n if (config.api_keys) {\n if (config.api_keys.openai) {\n result.apiKeys.openai = resolveEnvVar(config.api_keys.openai);\n result.enabledProviders.add('openai');\n }\n if (config.api_keys.google) {\n result.apiKeys.google = resolveEnvVar(config.api_keys.google);\n result.enabledProviders.add('google');\n }\n if (config.api_keys.zai) {\n result.apiKeys.zai = resolveEnvVar(config.api_keys.zai);\n result.enabledProviders.add('zai');\n }\n if (config.api_keys.kimi) {\n result.apiKeys.kimi = resolveEnvVar(config.api_keys.kimi);\n result.enabledProviders.add('kimi');\n }\n }\n\n // Merge overrides\n if (config.models?.overrides) {\n result.overrides = {\n ...result.overrides,\n ...config.models.overrides,\n };\n }\n\n // Merge Gemini thinking level\n if (config.models?.gemini_thinking_level) {\n result.geminiThinkingLevel = config.models.gemini_thinking_level;\n }\n\n // Merge tracker keys\n if (config.tracker_keys) {\n if (config.tracker_keys.linear) {\n result.trackerKeys.linear = resolveEnvVar(config.tracker_keys.linear);\n }\n if (config.tracker_keys.github) {\n result.trackerKeys.github = resolveEnvVar(config.tracker_keys.github);\n }\n if (config.tracker_keys.gitlab) {\n result.trackerKeys.gitlab = resolveEnvVar(config.tracker_keys.gitlab);\n }\n if (config.tracker_keys.rally) {\n result.trackerKeys.rally = resolveEnvVar(config.tracker_keys.rally);\n }\n }\n\n // Merge shadow configuration\n mergeShadowConfig(result.shadow, config);\n }\n\n return result;\n}\n\n/**\n * Load complete configuration (global + project + defaults)\n * Also loads API keys from environment variables as fallback\n */\nexport function loadConfig(): NormalizedConfig {\n const globalConfig = loadGlobalConfig();\n const projectConfig = loadProjectConfig();\n const config = mergeConfigs(projectConfig, globalConfig);\n\n // Load API keys from environment variables as fallback\n // This allows using ~/.panopticon.env for API keys\n if (process.env.OPENAI_API_KEY && !config.apiKeys.openai) {\n config.apiKeys.openai = process.env.OPENAI_API_KEY;\n config.enabledProviders.add('openai');\n }\n if (process.env.GOOGLE_API_KEY && !config.apiKeys.google) {\n config.apiKeys.google = process.env.GOOGLE_API_KEY;\n config.enabledProviders.add('google');\n }\n if (process.env.ZAI_API_KEY && !config.apiKeys.zai) {\n config.apiKeys.zai = process.env.ZAI_API_KEY;\n config.enabledProviders.add('zai');\n }\n if (process.env.KIMI_API_KEY && !config.apiKeys.kimi) {\n config.apiKeys.kimi = process.env.KIMI_API_KEY;\n config.enabledProviders.add('kimi');\n }\n\n // Load tracker API keys from environment variables as fallback\n if (process.env.LINEAR_API_KEY && !config.trackerKeys.linear) {\n config.trackerKeys.linear = process.env.LINEAR_API_KEY;\n }\n if (process.env.GITHUB_TOKEN && !config.trackerKeys.github) {\n config.trackerKeys.github = process.env.GITHUB_TOKEN;\n }\n if (process.env.GITLAB_TOKEN && !config.trackerKeys.gitlab) {\n config.trackerKeys.gitlab = process.env.GITLAB_TOKEN;\n }\n if (process.env.RALLY_API_KEY && !config.trackerKeys.rally) {\n config.trackerKeys.rally = process.env.RALLY_API_KEY;\n }\n\n // Load shadow mode from environment as fallback\n // Environment variable takes precedence over config file\n if (process.env.SHADOW_MODE !== undefined) {\n const envShadowMode = ['true', '1', 'yes'].includes(process.env.SHADOW_MODE.toLowerCase());\n config.shadow.enabled = envShadowMode;\n }\n\n return config;\n}\n\n/**\n * Check if a project-level config exists\n */\nexport function hasProjectConfig(): boolean {\n const projectRoot = findProjectRoot();\n if (!projectRoot) return false;\n return existsSync(join(projectRoot, '.panopticon.yaml'));\n}\n\n/**\n * Check if global config exists\n */\nexport function hasGlobalConfig(): boolean {\n return existsSync(GLOBAL_CONFIG_PATH);\n}\n\n/**\n * Get path to global config file\n */\nexport function getGlobalConfigPath(): string {\n return GLOBAL_CONFIG_PATH;\n}\n\n/**\n * Get path to project config file (null if not in a project)\n */\nexport function getProjectConfigPath(): string | null {\n const projectRoot = findProjectRoot();\n if (!projectRoot) return null;\n return join(projectRoot, '.panopticon.yaml');\n}\n"],"mappings":"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","names":["existsSync","readFileSync","writeFileSync","readFileSync","existsSync","join"]}
@@ -1 +0,0 @@
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\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.5', 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.5')\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.5 → 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.5';\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.5\" (normalized name)\n * - Multiple models: \"claude-sonnet-4.5 → 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.5 → claude-opus-4.6\"\n // For single-model sessions: \"claude-sonnet-4.5\"\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 } from '../providers.js';\nimport { sendKeysAsync } from '../tmux.js';\nimport { notifyPipeline } from '../pipeline-notifier.js';\n\nconst execAsync = promisify(exec);\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 *\n * @param name - Specialist name\n * @returns Path to session file\n */\nexport function getSessionFilePath(name: SpecialistType): string {\n return join(SPECIALISTS_DIR, `${name}.session`);\n}\n\n/**\n * Read session ID from file\n *\n * @param name - Specialist name\n * @returns Session ID or null if not found\n */\nexport function getSessionId(name: SpecialistType): string | null {\n const sessionFile = getSessionFilePath(name);\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}:`, 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 */\nexport function setSessionId(name: SpecialistType, sessionId: string): void {\n initSpecialistsDirectory();\n\n const sessionFile = getSessionFilePath(name);\n\n try {\n writeFileSync(sessionFile, sessionId.trim(), 'utf-8');\n } catch (error) {\n console.error(`Failed to write session file for ${name}:`, error);\n throw error;\n }\n}\n\n/**\n * Delete session file\n *\n * @param name - Specialist name\n * @returns True if file was deleted, false if it didn't exist\n */\nexport function clearSessionId(name: SpecialistType): boolean {\n const sessionFile = getSessionFilePath(name);\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}:`, 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 }\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\n const taskPrompt = await buildTaskPrompt(projectKey, specialistType, task, contextDigest);\n\n // Spawn tmux session\n const tmuxSession = getTmuxSessionName(specialistType, projectKey);\n const cwd = process.env.HOME || '/home/exedev';\n\n try {\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 const providerConfig = getProviderForModel(model as ModelId);\n if (providerConfig.authType === 'credential-file') {\n setupCredentialFileAuth(providerConfig, 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 prompt += `Your task:\n1. Fetch the latest main branch\n2. Attempt to merge ${task.branch} into main\n3. Resolve conflicts intelligently if needed\n4. Run tests to verify merge is clean\n5. Complete merge if tests pass`;\n break;\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 return true;\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);\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 const providerCfg = getProviderForModel(model as ModelId);\n if (providerCfg.authType === 'credential-file') {\n setupCredentialFileAuth(providerCfg, 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\n await execAsync(`tmux send-keys -t \"${tmuxSession}\" C-c`, { encoding: 'utf-8' });\n await new Promise(resolve => setTimeout(resolve, 200));\n\n // 2. Reset working directory using centralized sendKeys\n await sendKeysAsync(tmuxSession, 'cd ~');\n await new Promise(resolve => setTimeout(resolve, 200));\n\n // 3. Clear the prompt buffer with Ctrl+U\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 } = {}\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 // 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 const provCfg = getProviderForModel(model as ModelId);\n if (provCfg.authType === 'credential-file') {\n setupCredentialFileAuth(provCfg, 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 // Wait for Claude to be ready\n await new Promise(resolve => setTimeout(resolve, 3000));\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 // 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 if (isLargePrompt) {\n // Ensure tasks directory exists\n if (!existsSync(TASKS_DIR)) {\n mkdirSync(TASKS_DIR, { recursive: true });\n }\n\n // Write task to file with timestamp\n const taskFile = join(TASKS_DIR, `${name}-${Date.now()}.md`);\n writeFileSync(taskFile, taskPrompt, 'utf-8');\n\n // Send a short message pointing to the task file\n // Use centralized sendKeys which handles Enter correctly\n const shortMessage = `Read and execute the task in: ${taskFile}`;\n await sendKeysAsync(tmuxSession, shortMessage);\n } else {\n // For short prompts, send directly via tmux\n // Use centralized sendKeys which handles Enter correctly\n await sendKeysAsync(tmuxSession, taskPrompt);\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): 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 prompt = `New merge task for ${task.issueId}:\n\nBranch: ${task.branch || 'unknown'}\nWorkspace: ${task.workspace || 'unknown'}\n${task.prUrl ? `PR URL: ${task.prUrl}` : ''}\n\nYour task:\n1. Fetch the latest main branch\n2. Attempt to merge ${task.branch} into main\n3. If conflicts arise, resolve them intelligently based on context\n4. Run the test suite to verify the merge is clean\n5. If tests pass, complete the merge and push\n6. If tests fail, analyze the failures and either fix them or report back\n\nWhen done, provide feedback on:\n- Any conflicts encountered and how you resolved them\n- Test results\n- Any patterns you notice that future agents should be aware of\n\nUse the send-feedback-to-agent skill to report findings back to the issue agent.`;\n break;\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 directory — polyrepo workspaces have .git in subdirectories, not at root\n let gitDirs: string[] = [];\n if (workspace !== 'unknown') {\n if (existsSync(join(workspace, '.git'))) {\n // Monorepo or single-repo workspace — .git at root\n gitDirs = [workspace];\n } else {\n // Polyrepo — find subdirectories that contain .git\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 // 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 prompt = `New test task for ${task.issueId}:\n\nBranch: ${task.branch || 'unknown'}\nWorkspace: ${task.workspace || 'unknown'}\n\nYour task:\n1. Run the full test suite — 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.**\n\n## Step 1: Run Feature Branch Tests\n\n\\`\\`\\`bash\ncd ${task.workspace || 'unknown'} && npm test 2>&1 > /tmp/test-feature.txt; echo \"EXIT_CODE: $?\"\n# Use timeout: 300000 for this command\ntail -20 /tmp/test-feature.txt\n\\`\\`\\`\n\n## Step 2: Check Results\n\n- If ALL tests pass (exit code 0) → skip baseline, go to \"Update Status\"\n- If failures exist → continue to Step 3\n\n## Step 3: Baseline Comparison (ONLY if failures found)\n\n\\`\\`\\`bash\ncd ${task.context?.workspace ? task.context.workspace.replace(/workspaces\\/feature-[^/]+/, '') : 'unknown'} && npm test 2>&1 > /tmp/test-main.txt; echo \"EXIT_CODE: $?\"\n# Use timeout: 300000 for this command\ntail -20 /tmp/test-main.txt\n\\`\\`\\`\n\nThen compare failures (targeted, NOT full output):\n\\`\\`\\`bash\ngrep -E \"FAIL|✗|Error|failed\" /tmp/test-feature.txt | head -30\ngrep -E \"FAIL|✗|Error|failed\" /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]\"}' | 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]\"}' | 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 default:\n prompt = `Task for ${task.issueId}: Please process this task and report findings.`;\n }\n\n return wakeSpecialist(name, prompt, { issueId: task.issueId });\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);\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":["join","provider","pricing","existsSync","readFileSync","join","readFileSync","existsSync","mkdirSync","readdirSync","appendFileSync","join","basename","homedir","createRunLog","finalizeRunLog","cleanupOldLogs","existsSync","mkdirSync","writeFileSync","appendFileSync","readFileSync","readdirSync","statSync","unlinkSync","join","basename","listProjectsWithSpecialists"]}