panopticon-cli 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +36 -27
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/{event-store-BtuZCLHu.js → event-store-D7kLBd07.js} +1 -1
- package/dist/dashboard/{event-store-OS5jH3Eu.js → event-store-O9q0Gweh.js} +2 -2
- package/dist/dashboard/{event-store-OS5jH3Eu.js.map → event-store-O9q0Gweh.js.map} +1 -1
- package/dist/dashboard/{inspect-agent-CwT4mrvV.js → inspect-agent-B57kGDUV.js} +3 -3
- package/dist/dashboard/{inspect-agent-CwT4mrvV.js.map → inspect-agent-B57kGDUV.js.map} +1 -1
- package/dist/dashboard/{issue-service-singleton-z78bbRiO.js → issue-service-singleton-DQK42EqH.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-0n9hcF71.js → issue-service-singleton-sb2HkB9f.js} +2 -2
- package/dist/dashboard/{issue-service-singleton-0n9hcF71.js.map → issue-service-singleton-sb2HkB9f.js.map} +1 -1
- package/dist/dashboard/{lifecycle-B6d3AE3n.js → lifecycle-ZTYdrr2O.js} +1 -1
- package/dist/dashboard/{merge-agent-DaIEvGJG.js → merge-agent-GLtMEsTu.js} +1 -1
- package/dist/dashboard/{merge-agent-CmqR1MFf.js → merge-agent-twroFuAh.js} +2 -2
- package/dist/dashboard/{merge-agent-CmqR1MFf.js.map → merge-agent-twroFuAh.js.map} +1 -1
- package/dist/dashboard/{projection-cache-Bkzs_90o.js → projection-cache-DQ9zegkK.js} +10 -10
- package/dist/dashboard/projection-cache-DQ9zegkK.js.map +1 -0
- package/dist/dashboard/public/assets/{dist-D-q87oB4.js → dist-C2sRcZJv.js} +1 -1
- package/dist/dashboard/public/assets/{index--G6_upSx.js → index-BCLmEMRf.js} +41 -41
- package/dist/dashboard/public/assets/index-BEdq7CFf.css +1 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/{review-status-DqJZDthU.js → review-status-CK3eBGyb.js} +1 -1
- package/dist/dashboard/{review-status-LQATWF6L.js → review-status-CV55Tl-n.js} +2 -2
- package/dist/dashboard/{review-status-LQATWF6L.js.map → review-status-CV55Tl-n.js.map} +1 -1
- package/dist/dashboard/server.js +85 -85
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/{specialist-context-IX8ZZBxy.js → specialist-context-ColzlmGE.js} +2 -2
- package/dist/dashboard/{specialist-context-IX8ZZBxy.js.map → specialist-context-ColzlmGE.js.map} +1 -1
- package/dist/dashboard/{specialist-logs-BvOQ3XPt.js → specialist-logs-BhmDpFIq.js} +1 -1
- package/dist/dashboard/{specialists-C7Fyhq_j.js → specialists-C6s3U6tX.js} +21 -7
- package/dist/dashboard/specialists-C6s3U6tX.js.map +1 -0
- package/dist/dashboard/{specialists-B4aDa5xP.js → specialists-Cny632-T.js} +1 -1
- package/dist/dashboard/{test-agent-queue-C0WrVdrJ.js → test-agent-queue-tqI4VDsu.js} +3 -3
- package/dist/dashboard/{test-agent-queue-C0WrVdrJ.js.map → test-agent-queue-tqI4VDsu.js.map} +1 -1
- package/dist/dashboard/workflows-B2ARUpOa.js +2 -0
- package/dist/dashboard/{workflows-Cj6tzch6.js → workflows-N1UTipYl.js} +3 -3
- package/dist/dashboard/{workflows-Cj6tzch6.js.map → workflows-N1UTipYl.js.map} +1 -1
- package/dist/{merge-agent-BCPyotWG.js → merge-agent-VQH9z9t8.js} +2 -2
- package/dist/{merge-agent-BCPyotWG.js.map → merge-agent-VQH9z9t8.js.map} +1 -1
- package/dist/{review-status-p_HOugvo.js → review-status-2TdtHNcs.js} +1 -1
- package/dist/{review-status-BbY22dtx.js → review-status-Bm1bWNEa.js} +2 -2
- package/dist/{review-status-BbY22dtx.js.map → review-status-Bm1bWNEa.js.map} +1 -1
- package/dist/{specialist-context-CRBBW-z5.js → specialist-context-BdNFsfMG.js} +2 -2
- package/dist/{specialist-context-CRBBW-z5.js.map → specialist-context-BdNFsfMG.js.map} +1 -1
- package/dist/{specialist-logs-m0UvPm3F.js → specialist-logs-CLztE_bE.js} +1 -1
- package/dist/{specialists-ldNesMhg.js → specialists-DEKqgkxp.js} +21 -7
- package/dist/specialists-DEKqgkxp.js.map +1 -0
- package/dist/{specialists-DXDDLqoY.js → specialists-aUoUVWsN.js} +1 -1
- package/package.json +1 -1
- package/scripts/record-cost-event.js +15 -0
- package/scripts/record-cost-event.js.map +1 -1
- package/scripts/record-cost-event.ts +2 -0
- package/scripts/work-agent-stop-hook +26 -0
- package/dist/dashboard/projection-cache-Bkzs_90o.js.map +0 -1
- package/dist/dashboard/public/assets/index-CjpnhB4Q.css +0 -1
- package/dist/dashboard/specialists-C7Fyhq_j.js.map +0 -1
- package/dist/dashboard/workflows-BsUDQntr.js +0 -2
- package/dist/specialists-ldNesMhg.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specialists-C6s3U6tX.js","names":["loadYamlConfig"],"sources":["../../src/lib/cost.ts","../../src/lib/cost-parsers/jsonl-parser.ts","../../src/lib/cloister/specialist-handoff-logger.ts","../../src/lib/cloister/task-readiness.ts","../../src/lib/cloister/specialist-logs.ts","../../src/lib/cloister/specialists.ts"],"sourcesContent":["/**\n * Cost Tracking System\n *\n * Track AI usage costs per feature, issue, and project.\n * Supports multiple AI providers with configurable pricing.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { COSTS_DIR } from './paths.js';\n\n// ============== Types ==============\n\nexport type AIProvider = 'anthropic' | 'openai' | 'google' | 'custom';\n\nexport interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n cacheTTL?: '5m' | '1h'; // Cache write TTL (default: '5m')\n}\n\nexport interface CostEntry {\n id: string;\n timestamp: string;\n provider: AIProvider;\n model: string;\n issueId?: string;\n featureId?: string;\n agentId?: string;\n operation: string;\n usage: TokenUsage;\n cost: number;\n currency: string;\n metadata?: Record<string, any>;\n}\n\nexport interface CostSummary {\n totalCost: number;\n currency: string;\n period: {\n start: string;\n end: string;\n };\n byProvider: Record<AIProvider, number>;\n byModel: Record<string, number>;\n byIssue: Record<string, number>;\n byFeature: Record<string, number>;\n entryCount: number;\n totalTokens: {\n input: number;\n output: number;\n cacheRead: number;\n cacheWrite: number;\n total: number;\n };\n}\n\nexport interface CostBudget {\n id: string;\n name: string;\n type: 'issue' | 'feature' | 'project' | 'daily' | 'monthly';\n limit: number;\n currency: string;\n spent: number;\n alertThreshold: number; // e.g., 0.8 = alert at 80%\n enabled: boolean;\n}\n\nexport interface ModelPricing {\n provider: AIProvider;\n model: string;\n inputPer1k: number;\n outputPer1k: number;\n cacheReadPer1k?: number;\n cacheWrite5mPer1k?: number; // 5-minute TTL (default)\n cacheWrite1hPer1k?: number; // 1-hour TTL\n currency: string;\n}\n\n// ============== Pricing Data ==============\n\nexport const DEFAULT_PRICING: ModelPricing[] = [\n // Anthropic - 4.6 series (API IDs use dashes: claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5)\n { provider: 'anthropic', model: 'claude-opus-4-6', inputPer1k: 0.005, outputPer1k: 0.025, cacheReadPer1k: 0.0005, cacheWrite5mPer1k: 0.00625, cacheWrite1hPer1k: 0.01, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-sonnet-4-6', inputPer1k: 0.003, outputPer1k: 0.015, cacheReadPer1k: 0.0003, cacheWrite5mPer1k: 0.00375, cacheWrite1hPer1k: 0.006, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-haiku-4-5', inputPer1k: 0.001, outputPer1k: 0.005, cacheReadPer1k: 0.0001, cacheWrite5mPer1k: 0.00125, cacheWrite1hPer1k: 0.002, currency: 'USD' },\n // Anthropic - 4.x series\n { provider: 'anthropic', model: 'claude-opus-4-1', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 0.0015, cacheWrite5mPer1k: 0.01875, cacheWrite1hPer1k: 0.03, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-opus-4', inputPer1k: 0.015, outputPer1k: 0.075, cacheReadPer1k: 0.0015, cacheWrite5mPer1k: 0.01875, cacheWrite1hPer1k: 0.03, currency: 'USD' },\n { provider: 'anthropic', model: 'claude-sonnet-4', inputPer1k: 0.003, outputPer1k: 0.015, cacheReadPer1k: 0.0003, cacheWrite5mPer1k: 0.00375, cacheWrite1hPer1k: 0.006, currency: 'USD' },\n // Anthropic - Legacy\n { provider: 'anthropic', model: 'claude-haiku-3', inputPer1k: 0.00025, outputPer1k: 0.00125, cacheReadPer1k: 0.00003, cacheWrite5mPer1k: 0.0003, cacheWrite1hPer1k: 0.0005, currency: 'USD' },\n // OpenAI\n { provider: 'openai', model: 'gpt-4-turbo', inputPer1k: 0.01, outputPer1k: 0.03, currency: 'USD' },\n { provider: 'openai', model: 'gpt-4o', inputPer1k: 0.005, outputPer1k: 0.015, currency: 'USD' },\n { provider: 'openai', model: 'gpt-4o-mini', inputPer1k: 0.00015, outputPer1k: 0.0006, currency: 'USD' },\n // Google\n { provider: 'google', model: 'gemini-1.5-pro', inputPer1k: 0.00125, outputPer1k: 0.005, currency: 'USD' },\n { provider: 'google', model: 'gemini-1.5-flash', inputPer1k: 0.000075, outputPer1k: 0.0003, currency: 'USD' },\n // Moonshot AI (Kimi)\n { provider: 'custom', model: 'kimi-for-coding', inputPer1k: 0.0006, outputPer1k: 0.002, cacheReadPer1k: 0.00006, cacheWrite5mPer1k: 0.00075, currency: 'USD' },\n { provider: 'custom', model: 'kimi-k2.5', inputPer1k: 0.0006, outputPer1k: 0.002, cacheReadPer1k: 0.00006, cacheWrite5mPer1k: 0.00075, currency: 'USD' },\n // MiniMax ($0.30/M input, $1.20/M output)\n { provider: 'custom', model: 'minimax-m2.7', inputPer1k: 0.0003, outputPer1k: 0.0012, currency: 'USD' },\n { provider: 'custom', model: 'minimax-m2.7-highspeed', inputPer1k: 0.0003, outputPer1k: 0.0012, currency: 'USD' },\n { provider: 'custom', model: 'MiniMax-M2.7', inputPer1k: 0.0003, outputPer1k: 0.0012, currency: 'USD' },\n { provider: 'custom', model: 'MiniMax-M2.7-highspeed', inputPer1k: 0.0003, outputPer1k: 0.0012, currency: 'USD' },\n];\n\n// ============== Cost Calculation ==============\n\n/**\n * Calculate cost for token usage\n */\nexport function calculateCost(usage: TokenUsage, pricing: ModelPricing): number {\n let cost = 0;\n let inputMultiplier = 1;\n let outputMultiplier = 1;\n\n // Long-context pricing for Sonnet 4/4.5 (>200K total input tokens)\n // Total input includes: inputTokens + cacheReadTokens + cacheWriteTokens\n const totalInputTokens = usage.inputTokens\n + (usage.cacheReadTokens || 0)\n + (usage.cacheWriteTokens || 0);\n\n if ((pricing.model === 'claude-sonnet-4' || pricing.model === 'claude-sonnet-4-6')\n && totalInputTokens > 200000) {\n inputMultiplier = 2; // $6/MTok vs $3/MTok\n outputMultiplier = 1.5; // $22.50/MTok vs $15/MTok\n }\n\n // Input tokens\n cost += (usage.inputTokens / 1000) * pricing.inputPer1k * inputMultiplier;\n\n // Output tokens\n cost += (usage.outputTokens / 1000) * pricing.outputPer1k * outputMultiplier;\n\n // Cache read tokens (not affected by long-context multiplier)\n if (usage.cacheReadTokens && pricing.cacheReadPer1k) {\n cost += (usage.cacheReadTokens / 1000) * pricing.cacheReadPer1k;\n }\n\n // Cache write tokens - use TTL-appropriate pricing\n if (usage.cacheWriteTokens) {\n const ttl = usage.cacheTTL || '5m';\n const cacheWritePrice = ttl === '1h'\n ? pricing.cacheWrite1hPer1k\n : pricing.cacheWrite5mPer1k;\n if (cacheWritePrice) {\n cost += (usage.cacheWriteTokens / 1000) * cacheWritePrice;\n }\n }\n\n return Math.round(cost * 1000000) / 1000000; // Round to 6 decimal places\n}\n\n/**\n * Get pricing for a model\n */\nexport function getPricing(provider: AIProvider, model: string): ModelPricing | null {\n // Try exact match first\n let pricing = DEFAULT_PRICING.find(\n p => p.provider === provider && p.model === model\n );\n\n if (!pricing) {\n // Try partial match (e.g., \"claude-sonnet-4-20250101\" matches \"claude-sonnet-4\")\n pricing = DEFAULT_PRICING.find(\n p => p.provider === provider && model.startsWith(p.model)\n );\n }\n\n return pricing || null;\n}\n\n// ============== Cost Logging ==============\n\nfunction getCostFile(date: string): string {\n return join(COSTS_DIR, `costs-${date}.jsonl`);\n}\n\nfunction getCurrentDateString(): string {\n return new Date().toISOString().split('T')[0];\n}\n\n/**\n * Log a cost entry\n */\nexport function logCost(entry: Omit<CostEntry, 'id' | 'timestamp'>): CostEntry {\n mkdirSync(COSTS_DIR, { recursive: true });\n\n const fullEntry: CostEntry = {\n ...entry,\n id: `cost-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n timestamp: new Date().toISOString(),\n };\n\n const costFile = getCostFile(getCurrentDateString());\n appendFileSync(costFile, JSON.stringify(fullEntry) + '\\n');\n\n return fullEntry;\n}\n\n/**\n * Log cost from token usage\n */\nexport function logUsage(\n provider: AIProvider,\n model: string,\n usage: TokenUsage,\n options: {\n issueId?: string;\n featureId?: string;\n agentId?: string;\n operation?: string;\n metadata?: Record<string, any>;\n } = {}\n): CostEntry | null {\n const pricing = getPricing(provider, model);\n if (!pricing) {\n console.warn(`No pricing found for ${provider}/${model}`);\n return null;\n }\n\n const cost = calculateCost(usage, pricing);\n\n return logCost({\n provider,\n model,\n usage,\n cost,\n currency: pricing.currency,\n operation: options.operation || 'api_call',\n issueId: options.issueId,\n featureId: options.featureId,\n agentId: options.agentId,\n metadata: options.metadata,\n });\n}\n\n// ============== Cost Reading ==============\n\n/**\n * Read cost entries for a date range\n */\nexport function readCosts(startDate: string, endDate: string): CostEntry[] {\n const entries: CostEntry[] = [];\n\n const start = new Date(startDate);\n const end = new Date(endDate);\n\n for (let date = start; date <= end; date.setDate(date.getDate() + 1)) {\n const dateStr = date.toISOString().split('T')[0];\n const costFile = getCostFile(dateStr);\n\n if (existsSync(costFile)) {\n const content = readFileSync(costFile, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n for (const line of lines) {\n try {\n entries.push(JSON.parse(line));\n } catch {\n // Skip invalid entries\n }\n }\n }\n }\n\n return entries;\n}\n\n/**\n * Read costs for today\n */\nexport function readTodayCosts(): CostEntry[] {\n const today = getCurrentDateString();\n return readCosts(today, today);\n}\n\n/**\n * Read costs for an issue\n */\nexport function readIssueCosts(issueId: string, days: number = 30): CostEntry[] {\n const end = new Date();\n const start = new Date();\n start.setDate(start.getDate() - days);\n\n const allCosts = readCosts(\n start.toISOString().split('T')[0],\n end.toISOString().split('T')[0]\n );\n\n return allCosts.filter(entry => entry.issueId === issueId);\n}\n\n// ============== Cost Aggregation ==============\n\n/**\n * Calculate cost summary for a set of entries\n */\nexport function summarizeCosts(entries: CostEntry[]): CostSummary {\n const summary: CostSummary = {\n totalCost: 0,\n currency: 'USD',\n period: {\n start: entries[0]?.timestamp || new Date().toISOString(),\n end: entries[entries.length - 1]?.timestamp || new Date().toISOString(),\n },\n byProvider: {} as Record<AIProvider, number>,\n byModel: {},\n byIssue: {},\n byFeature: {},\n entryCount: entries.length,\n totalTokens: {\n input: 0,\n output: 0,\n cacheRead: 0,\n cacheWrite: 0,\n total: 0,\n },\n };\n\n for (const entry of entries) {\n summary.totalCost += entry.cost;\n\n // By provider\n summary.byProvider[entry.provider] =\n (summary.byProvider[entry.provider] || 0) + entry.cost;\n\n // By model\n summary.byModel[entry.model] =\n (summary.byModel[entry.model] || 0) + entry.cost;\n\n // By issue\n if (entry.issueId) {\n summary.byIssue[entry.issueId] =\n (summary.byIssue[entry.issueId] || 0) + entry.cost;\n }\n\n // By feature\n if (entry.featureId) {\n summary.byFeature[entry.featureId] =\n (summary.byFeature[entry.featureId] || 0) + entry.cost;\n }\n\n // Tokens\n summary.totalTokens.input += entry.usage.inputTokens;\n summary.totalTokens.output += entry.usage.outputTokens;\n summary.totalTokens.cacheRead += entry.usage.cacheReadTokens || 0;\n summary.totalTokens.cacheWrite += entry.usage.cacheWriteTokens || 0;\n }\n\n // Total includes all token types\n summary.totalTokens.total = summary.totalTokens.input\n + summary.totalTokens.output\n + summary.totalTokens.cacheRead\n + summary.totalTokens.cacheWrite;\n summary.totalCost = Math.round(summary.totalCost * 100) / 100;\n\n return summary;\n}\n\n/**\n * Get daily cost summary\n */\nexport function getDailySummary(date?: string): CostSummary {\n const targetDate = date || getCurrentDateString();\n const entries = readCosts(targetDate, targetDate);\n return summarizeCosts(entries);\n}\n\n/**\n * Get weekly cost summary\n */\nexport function getWeeklySummary(): CostSummary {\n const end = new Date();\n const start = new Date();\n start.setDate(start.getDate() - 7);\n\n const entries = readCosts(\n start.toISOString().split('T')[0],\n end.toISOString().split('T')[0]\n );\n\n return summarizeCosts(entries);\n}\n\n/**\n * Get monthly cost summary\n */\nexport function getMonthlySummary(): CostSummary {\n const end = new Date();\n const start = new Date();\n start.setDate(start.getDate() - 30);\n\n const entries = readCosts(\n start.toISOString().split('T')[0],\n end.toISOString().split('T')[0]\n );\n\n return summarizeCosts(entries);\n}\n\n// ============== Cost Budgets ==============\n\nconst BUDGETS_FILE = join(COSTS_DIR, 'budgets.json');\n\nfunction loadBudgets(): CostBudget[] {\n if (!existsSync(BUDGETS_FILE)) {\n return [];\n }\n\n try {\n const content = readFileSync(BUDGETS_FILE, 'utf-8');\n return JSON.parse(content);\n } catch {\n return [];\n }\n}\n\nfunction saveBudgets(budgets: CostBudget[]): void {\n mkdirSync(COSTS_DIR, { recursive: true });\n writeFileSync(BUDGETS_FILE, JSON.stringify(budgets, null, 2));\n}\n\n/**\n * Create a cost budget\n */\nexport function createBudget(budget: Omit<CostBudget, 'id' | 'spent'>): CostBudget {\n const budgets = loadBudgets();\n\n const newBudget: CostBudget = {\n ...budget,\n id: `budget-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n spent: 0,\n };\n\n budgets.push(newBudget);\n saveBudgets(budgets);\n\n return newBudget;\n}\n\n/**\n * Get a budget by ID\n */\nexport function getBudget(id: string): CostBudget | null {\n const budgets = loadBudgets();\n return budgets.find(b => b.id === id) || null;\n}\n\n/**\n * Get all budgets\n */\nexport function getAllBudgets(): CostBudget[] {\n return loadBudgets();\n}\n\n/**\n * Update budget spent amount\n */\nexport function updateBudgetSpent(id: string, spent: number): boolean {\n const budgets = loadBudgets();\n const budget = budgets.find(b => b.id === id);\n\n if (!budget) return false;\n\n budget.spent = spent;\n saveBudgets(budgets);\n\n return true;\n}\n\n/**\n * Check budget status\n */\nexport function checkBudget(id: string): {\n budget: CostBudget | null;\n remaining: number;\n percentUsed: number;\n exceeded: boolean;\n alert: boolean;\n} {\n const budget = getBudget(id);\n\n if (!budget) {\n return {\n budget: null,\n remaining: 0,\n percentUsed: 0,\n exceeded: false,\n alert: false,\n };\n }\n\n const remaining = budget.limit - budget.spent;\n const percentUsed = budget.spent / budget.limit;\n\n return {\n budget,\n remaining,\n percentUsed,\n exceeded: percentUsed >= 1,\n alert: percentUsed >= budget.alertThreshold,\n };\n}\n\n/**\n * Delete a budget\n */\nexport function deleteBudget(id: string): boolean {\n const budgets = loadBudgets();\n const index = budgets.findIndex(b => b.id === id);\n\n if (index === -1) return false;\n\n budgets.splice(index, 1);\n saveBudgets(budgets);\n\n return true;\n}\n\n// ============== Reports ==============\n\n/**\n * Generate a cost report\n */\nexport function generateReport(startDate: string, endDate: string): string {\n const entries = readCosts(startDate, endDate);\n const summary = summarizeCosts(entries);\n\n const lines: string[] = [\n '# Cost Report',\n '',\n `**Period:** ${startDate} to ${endDate}`,\n '',\n '## Summary',\n '',\n `- **Total Cost:** $${summary.totalCost.toFixed(2)}`,\n `- **Total Entries:** ${summary.entryCount}`,\n `- **Total Tokens:** ${summary.totalTokens.total.toLocaleString()}`,\n ` - Input: ${summary.totalTokens.input.toLocaleString()}`,\n ` - Output: ${summary.totalTokens.output.toLocaleString()}`,\n '',\n '## By Provider',\n '',\n ];\n\n for (const [provider, cost] of Object.entries(summary.byProvider)) {\n lines.push(`- **${provider}:** $${cost.toFixed(2)}`);\n }\n\n lines.push('');\n lines.push('## By Model');\n lines.push('');\n\n for (const [model, cost] of Object.entries(summary.byModel)) {\n lines.push(`- **${model}:** $${cost.toFixed(2)}`);\n }\n\n if (Object.keys(summary.byIssue).length > 0) {\n lines.push('');\n lines.push('## By Issue');\n lines.push('');\n\n const sortedIssues = Object.entries(summary.byIssue)\n .sort(([, a], [, b]) => b - a);\n\n for (const [issue, cost] of sortedIssues.slice(0, 10)) {\n lines.push(`- **${issue}:** $${cost.toFixed(2)}`);\n }\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Format cost for display\n */\nexport function formatCost(cost: number, currency: string = 'USD'): string {\n if (currency === 'USD') {\n return `$${cost.toFixed(4)}`;\n }\n return `${cost.toFixed(4)} ${currency}`;\n}\n","/**\n * Claude Code JSONL Parser\n *\n * Parse token usage from Claude Code session files.\n * Session files are stored at: ~/.claude/projects/<project-path-hash>/<session-id>.jsonl\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from 'fs';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { TokenUsage, calculateCost, getPricing, AIProvider, logCost, CostEntry } from '../cost.js';\n\n// Claude Code JSONL message format\nexport interface ClaudeMessage {\n sessionId?: string;\n timestamp?: string;\n parentMessageId?: string;\n message?: {\n id?: string;\n role?: 'user' | 'assistant';\n model?: string;\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n };\n };\n // Some messages have usage at top level\n usage?: {\n input_tokens?: number;\n output_tokens?: number;\n cache_creation_input_tokens?: number;\n cache_read_input_tokens?: number;\n };\n model?: string;\n}\n\n// Parsed session usage\nexport interface SessionUsage {\n sessionId: string;\n sessionFile: string;\n startTime: string;\n endTime: string;\n model: string; // Display name (normalized). Shows \"sonnet-4-6 → opus-4-6\" for upgrades\n usage: TokenUsage; // Total tokens across all models\n cost: number; // DEPRECATED: Uses first-model pricing (kept for backward compatibility)\n cost_v2?: number; // NEW: Accurate per-message pricing\n messageCount: number;\n modelBreakdown?: Record<string, { // NEW: Cost/token breakdown by exact model ID\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n }>;\n}\n\n// Claude projects directory\nconst CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');\n\n/**\n * Get all Claude Code project directories\n */\nexport function getProjectDirs(): string[] {\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n return [];\n }\n\n return readdirSync(CLAUDE_PROJECTS_DIR)\n .map(name => join(CLAUDE_PROJECTS_DIR, name))\n .filter(path => {\n try {\n return statSync(path).isDirectory();\n } catch {\n return false;\n }\n });\n}\n\n/**\n * Get session JSONL files for a project directory\n */\nexport function getSessionFiles(projectDir: string): string[] {\n if (!existsSync(projectDir)) {\n return [];\n }\n\n return readdirSync(projectDir)\n .filter(name => name.endsWith('.jsonl'))\n .map(name => join(projectDir, name))\n .sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Get all session files across all projects\n */\nexport function getAllSessionFiles(): string[] {\n const files: string[] = [];\n\n for (const projectDir of getProjectDirs()) {\n files.push(...getSessionFiles(projectDir));\n }\n\n return files.sort((a, b) => {\n try {\n return statSync(b).mtime.getTime() - statSync(a).mtime.getTime();\n } catch {\n return 0;\n }\n });\n}\n\n/**\n * Normalize model name for pricing lookup\n */\nexport function normalizeModelName(model: string): { provider: AIProvider; model: string } {\n // Claude models\n if (model.includes('claude')) {\n let normalizedModel = model;\n\n // Map full model IDs to pricing model names\n // Order matters - check more specific patterns first\n\n // Opus models\n if (model.includes('opus-4-6') || model.includes('opus-4.6')) {\n normalizedModel = 'claude-opus-4-6';\n } else if (model.includes('opus-4-1') || model.includes('opus-4.1')) {\n normalizedModel = 'claude-opus-4-1';\n } else if (model.includes('opus-4') || model.includes('opus')) {\n normalizedModel = 'claude-opus-4';\n }\n\n // Sonnet models\n if (model.includes('sonnet-4-5') || model.includes('sonnet-4.5')) {\n normalizedModel = 'claude-sonnet-4-6';\n } else if (model.includes('sonnet-4') || model.includes('sonnet')) {\n normalizedModel = 'claude-sonnet-4';\n }\n\n // Haiku models - default to 4.5 (current), support 3 for legacy\n if (model.includes('haiku-4-5') || model.includes('haiku-4.5')) {\n normalizedModel = 'claude-haiku-4-5';\n } else if (model.includes('haiku-3')) {\n normalizedModel = 'claude-haiku-3';\n } else if (model.includes('haiku')) {\n normalizedModel = 'claude-haiku-4-5'; // Default to current model\n }\n\n return { provider: 'anthropic', model: normalizedModel };\n }\n\n // OpenAI models\n if (model.includes('gpt')) {\n return { provider: 'openai', model };\n }\n\n // Google models\n if (model.includes('gemini')) {\n return { provider: 'google', model };\n }\n\n // MiniMax models\n if (model.includes('minimax')) {\n return { provider: 'custom', model };\n }\n\n // Default to anthropic/claude\n return { provider: 'anthropic', model: 'claude-sonnet-4' };\n}\n\n/**\n * Parse a Claude Code session JSONL file and extract usage with per-message cost calculation\n *\n * This function calculates costs accurately for sessions that span multiple models\n * (e.g., when Claude Max auto-upgrades from Sonnet to Opus mid-conversation).\n *\n * Cost Calculation:\n * - `cost_v2`: Accurate per-message pricing. Each message is costed using its own model's pricing.\n * - `cost`: DEPRECATED. Uses first model's pricing for all messages (kept for backward compatibility).\n *\n * Model Display:\n * - Single model: \"claude-sonnet-4-6\" (normalized name)\n * - Multiple models: \"claude-sonnet-4-6 → claude-opus-4-6\" (progression of normalized names)\n *\n * Model Breakdown:\n * - Keys are exact model IDs (e.g., \"claude-sonnet-4-5-20250929\")\n * - Values contain per-model cost, token counts, and message count\n *\n * @param sessionFile - Path to the .jsonl session file\n * @returns Session usage summary with accurate multi-model costing, or null if no usage found\n */\nexport function parseClaudeSession(sessionFile: string): SessionUsage | null {\n if (!existsSync(sessionFile)) {\n return null;\n }\n\n const content = readFileSync(sessionFile, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n let sessionId = '';\n let startTime = '';\n let endTime = '';\n let primaryModel = '';\n let messageCount = 0;\n\n const totalUsage: TokenUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n };\n\n // NEW: Per-message cost tracking\n const modelBreakdown: Record<string, {\n cost: number;\n inputTokens: number;\n outputTokens: number;\n messageCount: number;\n }> = {};\n let totalCostV2 = 0;\n\n for (const line of lines) {\n try {\n const msg: ClaudeMessage = JSON.parse(line);\n\n // Extract session ID from first message\n if (msg.sessionId && !sessionId) {\n sessionId = msg.sessionId;\n }\n\n // Track timestamps\n if (msg.timestamp) {\n if (!startTime || msg.timestamp < startTime) {\n startTime = msg.timestamp;\n }\n if (!endTime || msg.timestamp > endTime) {\n endTime = msg.timestamp;\n }\n }\n\n // Extract usage - can be in message.usage or top-level usage\n const usage = msg.message?.usage || msg.usage;\n const modelId = msg.message?.model || msg.model; // Exact model ID\n\n if (usage) {\n // Accumulate total tokens (existing behavior)\n totalUsage.inputTokens += usage.input_tokens || 0;\n totalUsage.outputTokens += usage.output_tokens || 0;\n totalUsage.cacheReadTokens = (totalUsage.cacheReadTokens || 0) + (usage.cache_read_input_tokens || 0);\n totalUsage.cacheWriteTokens = (totalUsage.cacheWriteTokens || 0) + (usage.cache_creation_input_tokens || 0);\n messageCount++;\n\n // NEW: Calculate cost for THIS message using THIS message's model\n if (modelId) {\n // Normalize model name for pricing lookup\n const { provider, model: normalizedModel } = normalizeModelName(modelId);\n const pricing = getPricing(provider, normalizedModel);\n\n if (pricing) {\n // Create message-specific usage object\n const msgUsage: TokenUsage = {\n inputTokens: usage.input_tokens || 0,\n outputTokens: usage.output_tokens || 0,\n cacheReadTokens: usage.cache_read_input_tokens || 0,\n cacheWriteTokens: usage.cache_creation_input_tokens || 0,\n };\n\n // Calculate cost for this message\n const msgCost = calculateCost(msgUsage, pricing);\n totalCostV2 += msgCost;\n\n // Track breakdown by exact model ID\n if (!modelBreakdown[modelId]) {\n modelBreakdown[modelId] = {\n cost: 0,\n inputTokens: 0,\n outputTokens: 0,\n messageCount: 0,\n };\n }\n modelBreakdown[modelId].cost += msgCost;\n modelBreakdown[modelId].inputTokens += msgUsage.inputTokens;\n modelBreakdown[modelId].outputTokens += msgUsage.outputTokens;\n modelBreakdown[modelId].messageCount++;\n }\n }\n }\n\n // Track primary model (first model found - for backward compatibility)\n if (modelId && !primaryModel) {\n primaryModel = modelId;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // If no usage found, return null\n if (totalUsage.inputTokens === 0 && totalUsage.outputTokens === 0) {\n return null;\n }\n\n // Use filename as session ID if not found in messages\n if (!sessionId) {\n sessionId = basename(sessionFile, '.jsonl');\n }\n\n // Default model if not found\n if (!primaryModel) {\n primaryModel = 'claude-sonnet-4';\n }\n\n // Generate model display string (normalized names)\n // For multi-model sessions: \"claude-sonnet-4-6 → claude-opus-4-6\"\n // For single-model sessions: \"claude-sonnet-4-6\"\n const normalizedModels = Object.keys(modelBreakdown)\n .map(id => normalizeModelName(id).model);\n const modelDisplay = normalizedModels.length > 0\n ? (normalizedModels.length > 1\n ? normalizedModels.join(' → ')\n : normalizedModels[0])\n : normalizeModelName(primaryModel).model;\n\n // DEPRECATED: Calculate cost using first model (for backward compatibility)\n const { provider, model } = normalizeModelName(primaryModel);\n const pricing = getPricing(provider, model);\n const cost = pricing ? calculateCost(totalUsage, pricing) : 0;\n\n return {\n sessionId,\n sessionFile,\n startTime: startTime || new Date().toISOString(),\n endTime: endTime || new Date().toISOString(),\n model: modelDisplay,\n usage: totalUsage,\n cost, // DEPRECATED: First-model pricing\n cost_v2: totalCostV2 > 0 ? totalCostV2 : undefined, // NEW: Accurate per-message pricing\n messageCount,\n modelBreakdown: Object.keys(modelBreakdown).length > 0 ? modelBreakdown : undefined, // NEW: Cost breakdown by model\n };\n}\n\n/**\n * Parse all sessions and return usage summaries\n */\nexport function parseAllSessions(maxAge?: number): SessionUsage[] {\n const sessions: SessionUsage[] = [];\n const cutoffTime = maxAge ? Date.now() - maxAge : 0;\n\n for (const file of getAllSessionFiles()) {\n try {\n const stat = statSync(file);\n if (cutoffTime && stat.mtime.getTime() < cutoffTime) {\n continue;\n }\n\n const usage = parseClaudeSession(file);\n if (usage) {\n sessions.push(usage);\n }\n } catch {\n // Skip files that can't be read\n }\n }\n\n return sessions;\n}\n\n/**\n * Get recent sessions (last N days)\n */\nexport function getRecentSessions(days: number = 7): SessionUsage[] {\n const maxAge = days * 24 * 60 * 60 * 1000;\n return parseAllSessions(maxAge);\n}\n\n/**\n * Get the active session model for a workspace\n * Returns the full model ID (e.g., \"claude-sonnet-4-5-20250929\") from the most recent session file\n *\n * NOTE: Claude Max can auto-upgrade models mid-session (e.g., Sonnet → Opus).\n * We read from the END of the file to get the CURRENT model, not the initial one.\n */\nexport function getActiveSessionModel(workspacePath: string): string | null {\n try {\n // Convert workspace path to Claude project dir name\n // e.g., /home/user/projects/myn/workspaces/feature-min-664\n // -> -home-user-projects-myn-workspaces-feature-min-664\n // NOTE: The directory name KEEPS the leading dash\n const projectDirName = workspacePath.replace(/\\//g, '-');\n const projectDir = join(CLAUDE_PROJECTS_DIR, projectDirName);\n\n // Find most recently modified session file\n const sessions = getSessionFiles(projectDir);\n if (sessions.length === 0) {\n return null;\n }\n\n // Parse the most recent session file to find model\n const mostRecentSession = sessions[0]; // Already sorted by mtime\n const content = readFileSync(mostRecentSession, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim());\n\n // Read from END of file to get CURRENT model (may have been auto-upgraded by Claude Max)\n // Look at last 100 lines to find the most recent model entry\n const searchLines = lines.slice(-100);\n for (let i = searchLines.length - 1; i >= 0; i--) {\n try {\n const msg: ClaudeMessage = JSON.parse(searchLines[i]);\n const model = msg.message?.model || msg.model;\n // Skip synthetic/placeholder model values\n if (model && model !== '<synthetic>') {\n return model; // Return full model ID\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n // Fallback: check first few lines if nothing found at end\n for (let i = 0; i < Math.min(lines.length, 10); i++) {\n try {\n const msg: ClaudeMessage = JSON.parse(lines[i]);\n const model = msg.message?.model || msg.model;\n if (model && model !== '<synthetic>') {\n return model;\n }\n } catch {\n // Skip invalid JSON lines\n }\n }\n\n return null;\n } catch (error) {\n console.warn('Failed to get active session model:', error);\n return null;\n }\n}\n\n/**\n * Import session usage to cost log\n */\nexport function importSessionToCostLog(\n session: SessionUsage,\n options: {\n issueId?: string;\n agentId?: string;\n operation?: string;\n } = {}\n): CostEntry | null {\n const { provider, model } = normalizeModelName(session.model);\n const pricing = getPricing(provider, model);\n\n if (!pricing) {\n console.warn(`No pricing found for ${session.model}`);\n return null;\n }\n\n return logCost({\n provider,\n model,\n usage: session.usage,\n cost: session.cost,\n currency: 'USD',\n operation: options.operation || 'claude_session',\n issueId: options.issueId,\n agentId: options.agentId,\n metadata: {\n sessionId: session.sessionId,\n sessionFile: session.sessionFile,\n startTime: session.startTime,\n endTime: session.endTime,\n messageCount: session.messageCount,\n },\n });\n}\n","/**\n * Specialist Handoff Event Logger\n *\n * Logs specialist handoff events (work passing between specialist agents)\n * to JSONL file for tracking and analysis in the dashboard.\n */\n\nimport { existsSync, mkdirSync, appendFileSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { PANOPTICON_HOME } from '../paths.js';\n\n/**\n * Specialist handoff event structure\n */\nexport interface SpecialistHandoff {\n id: string;\n timestamp: string; // ISO 8601\n issueId: string;\n fromSpecialist: string; // e.g., \"review-agent\"\n toSpecialist: string; // e.g., \"test-agent\"\n status: 'queued' | 'processing' | 'completed' | 'failed';\n priority: 'urgent' | 'high' | 'normal' | 'low';\n completedAt?: string; // ISO 8601\n result?: 'success' | 'failure';\n context?: {\n workspace?: string;\n branch?: string;\n prUrl?: string;\n source?: string;\n };\n}\n\n/**\n * Specialist handoff log file path\n */\nconst SPECIALIST_HANDOFF_LOG_FILE = join(PANOPTICON_HOME, 'logs', 'specialist-handoffs.jsonl');\n\n/**\n * Ensure log directory exists\n */\nfunction ensureLogDir(): void {\n const logDir = join(PANOPTICON_HOME, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n}\n\n/**\n * Log a specialist handoff event\n *\n * @param event - Specialist handoff event to log\n */\nexport function logSpecialistHandoff(event: SpecialistHandoff): void {\n ensureLogDir();\n\n const line = JSON.stringify(event) + '\\n';\n appendFileSync(SPECIALIST_HANDOFF_LOG_FILE, line, 'utf-8');\n}\n\n/**\n * Create a specialist handoff event (queued status)\n *\n * @param fromSpecialist - Source specialist (or 'issue-agent')\n * @param toSpecialist - Target specialist\n * @param issueId - Issue ID\n * @param priority - Task priority\n * @param context - Additional context\n * @returns Specialist handoff event\n */\nexport function createSpecialistHandoff(\n fromSpecialist: string,\n toSpecialist: string,\n issueId: string,\n priority: 'urgent' | 'high' | 'normal' | 'low',\n context?: {\n workspace?: string;\n branch?: string;\n prUrl?: string;\n source?: string;\n }\n): SpecialistHandoff {\n return {\n id: `${toSpecialist}-${issueId}-${Date.now()}`,\n timestamp: new Date().toISOString(),\n issueId,\n fromSpecialist,\n toSpecialist,\n status: 'queued',\n priority,\n context,\n };\n}\n\n/**\n * Read all specialist handoff events from log\n *\n * @param limit - Maximum number of events to return (most recent first)\n * @returns Array of specialist handoff events\n */\nexport function readSpecialistHandoffs(limit?: number): SpecialistHandoff[] {\n ensureLogDir();\n\n if (!existsSync(SPECIALIST_HANDOFF_LOG_FILE)) {\n return [];\n }\n\n const content = readFileSync(SPECIALIST_HANDOFF_LOG_FILE, 'utf-8');\n const lines = content.trim().split('\\n').filter(line => line.trim());\n\n const events = lines.map(line => JSON.parse(line) as SpecialistHandoff);\n\n // Return most recent first\n events.reverse();\n\n if (limit) {\n return events.slice(0, limit);\n }\n\n return events;\n}\n\n/**\n * Read specialist handoff events for a specific issue\n *\n * @param issueId - Issue ID\n * @returns Array of specialist handoff events for the issue\n */\nexport function readIssueSpecialistHandoffs(issueId: string): SpecialistHandoff[] {\n const allEvents = readSpecialistHandoffs();\n return allEvents.filter(e => e.issueId === issueId);\n}\n\n/**\n * Get specialist handoff statistics\n *\n * @returns Specialist handoff statistics\n */\nexport function getSpecialistHandoffStats(): {\n totalHandoffs: number;\n todayCount: number;\n bySpecialist: Record<string, { sent: number; received: number }>;\n byStatus: Record<string, number>;\n successRate: number;\n queueDepth: number; // Current items with 'queued' or 'processing' status\n} {\n const events = readSpecialistHandoffs();\n const today = new Date().toISOString().split('T')[0];\n\n const stats = {\n totalHandoffs: events.length,\n todayCount: 0,\n bySpecialist: {} as Record<string, { sent: number; received: number }>,\n byStatus: {} as Record<string, number>,\n successRate: 0,\n queueDepth: 0,\n };\n\n let completedCount = 0;\n let successCount = 0;\n\n for (const event of events) {\n // Count today's handoffs\n if (event.timestamp.startsWith(today)) {\n stats.todayCount++;\n }\n\n // Count by specialist (from)\n if (!stats.bySpecialist[event.fromSpecialist]) {\n stats.bySpecialist[event.fromSpecialist] = { sent: 0, received: 0 };\n }\n stats.bySpecialist[event.fromSpecialist].sent++;\n\n // Count by specialist (to)\n if (!stats.bySpecialist[event.toSpecialist]) {\n stats.bySpecialist[event.toSpecialist] = { sent: 0, received: 0 };\n }\n stats.bySpecialist[event.toSpecialist].received++;\n\n // Count by status\n stats.byStatus[event.status] = (stats.byStatus[event.status] || 0) + 1;\n\n // Count for success rate (only completed items)\n if (event.status === 'completed' || event.status === 'failed') {\n completedCount++;\n if (event.result === 'success') {\n successCount++;\n }\n }\n\n // Count queue depth (queued or processing)\n if (event.status === 'queued' || event.status === 'processing') {\n stats.queueDepth++;\n }\n }\n\n // Calculate success rate\n stats.successRate = completedCount > 0 ? successCount / completedCount : 0;\n\n return stats;\n}\n\n/**\n * Get handoffs from today\n *\n * @returns Array of specialist handoff events from today\n */\nexport function getTodaySpecialistHandoffs(): SpecialistHandoff[] {\n const events = readSpecialistHandoffs();\n const today = new Date().toISOString().split('T')[0];\n return events.filter(e => e.timestamp.startsWith(today));\n}\n","/**\n * DAG-aware task readiness module for Cloister scheduling (PAN-388 Phase 4)\n *\n * Provides:\n * - isTaskReady(): checks if a vBRIEF item's hard dependencies are all done\n * - getUnblockedItems(): returns items newly unblocked after a task completes\n *\n * Gracefully degrades when no plan.vbrief.json exists — all tasks are ready.\n */\n\nimport { readWorkspacePlan } from '../vbrief/io.js';\nimport type { VBriefItemStatus } from '../vbrief/types.js';\n\nconst TERMINAL_STATUSES: VBriefItemStatus[] = ['completed', 'cancelled'];\n\n/**\n * Returns true if the given vBRIEF item can be worked on — i.e., all items\n * that block it have reached a terminal status (completed or cancelled).\n *\n * When no plan exists for the workspace, returns true (no-op for legacy flows).\n */\nexport function isTaskReady(itemId: string, workspacePath: string): boolean {\n const doc = readWorkspacePlan(workspacePath);\n if (!doc) return true; // No plan → all tasks ready\n\n // If item doesn't exist in this plan, don't block it (e.g., legacy bead not in vBRIEF)\n const itemExists = doc.plan.items.some(i => i.id === itemId);\n if (!itemExists) return true;\n\n // Find all items that block this task (type: 'blocks', edge.to === itemId)\n const blockerIds = doc.plan.edges\n .filter(e => e.type === 'blocks' && e.to === itemId)\n .map(e => e.from);\n\n if (blockerIds.length === 0) return true; // No blockers\n\n const itemById = new Map(doc.plan.items.map(i => [i.id, i]));\n\n return blockerIds.every(blockerId => {\n const blocker = itemById.get(blockerId);\n if (!blocker) return true; // Unknown blocker — don't block\n return TERMINAL_STATUSES.includes(blocker.status);\n });\n}\n\n/**\n * Returns the list of item IDs that are newly unblocked after `justCompletedId`\n * reaches a terminal status.\n *\n * \"Newly unblocked\" means: the item was not ready before (had at least one\n * non-terminal blocker) and is now ready after this completion.\n *\n * Returns [] when no plan exists.\n */\nexport function getUnblockedItems(workspacePath: string, justCompletedId: string): string[] {\n const doc = readWorkspacePlan(workspacePath);\n if (!doc) return [];\n\n const itemById = new Map(doc.plan.items.map(i => [i.id, i]));\n\n // Find items that this completed item directly blocks\n const directlyUnblocking = doc.plan.edges\n .filter(e => e.type === 'blocks' && e.from === justCompletedId)\n .map(e => e.to);\n\n const unblocked: string[] = [];\n\n for (const candidateId of directlyUnblocking) {\n const candidate = itemById.get(candidateId);\n if (!candidate) continue;\n if (TERMINAL_STATUSES.includes(candidate.status)) continue; // Already done\n\n // Check all blockers of this candidate (excluding the just-completed one)\n const blockers = doc.plan.edges\n .filter(e => e.type === 'blocks' && e.to === candidateId)\n .map(e => e.from);\n\n const allBlockersTerminal = blockers.every(blockerId => {\n const blocker = itemById.get(blockerId);\n if (!blocker) return true;\n // The just-completed item counts as terminal even if status not yet written\n if (blockerId === justCompletedId) return true;\n return TERMINAL_STATUSES.includes(blocker.status);\n });\n\n if (allBlockersTerminal) {\n unblocked.push(candidateId);\n }\n }\n\n return unblocked;\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","/**\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, createHash } from 'crypto';\nimport { PANOPTICON_HOME } from '../paths.js';\nimport { getDevrootPath } from '../config.js';\nimport { getProject } from '../projects.js';\nimport { getAllSessionFiles, parseClaudeSession } from '../cost-parsers/jsonl-parser.js';\nimport { createSpecialistHandoff, logSpecialistHandoff } from './specialist-handoff-logger.js';\nimport type { ModelId } from '../settings.js';\nimport { loadConfig as loadYamlConfig } from '../config-yaml.js';\nimport { getModelId, WorkTypeId } from '../work-type-router.js';\nimport { getProviderForModel, getProviderEnv, setupCredentialFileAuth, clearCredentialFileAuth } from '../providers.js';\nimport { sendKeysAsync, capturePaneAsync, waitForClaudePrompt, confirmDelivery } from '../tmux.js';\nimport { notifyPipeline } from '../pipeline-notifier.js';\nimport { isTaskReady } from './task-readiness.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Resolve git directories and branch name from a workspace path.\n * Handles both monorepo (single .git at root) and polyrepo (multiple .git in subdirs).\n * When task.branch is missing, detects it from the checked-out branch in git repos.\n */\nasync function resolveWorkspaceGitInfo(workspace: string | undefined, taskBranch: string | undefined): Promise<{\n gitDirs: string[];\n branch: string;\n isPolyrepo: boolean;\n}> {\n const gitDirs: string[] = [];\n let branch = taskBranch || 'unknown';\n\n if (!workspace || workspace === 'unknown') {\n return { gitDirs, branch, isPolyrepo: false };\n }\n\n // Detect git directories\n if (existsSync(join(workspace, '.git'))) {\n gitDirs.push(workspace);\n } else {\n try {\n const entries = readdirSync(workspace, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && existsSync(join(workspace, entry.name, '.git'))) {\n gitDirs.push(join(workspace, entry.name));\n }\n }\n } catch {}\n }\n\n // Auto-resolve branch from git when not provided\n if (branch === 'unknown' && gitDirs.length > 0) {\n try {\n const { stdout } = await execAsync(\n `cd \"${gitDirs[0]}\" && git branch --show-current`,\n { encoding: 'utf-8', timeout: 5000 }\n );\n const detected = stdout.trim();\n if (detected) {\n branch = detected;\n }\n } catch {}\n }\n\n return { gitDirs, branch, isPolyrepo: gitDirs.length > 1 };\n}\n\n/**\n * Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for a model.\n * For non-Anthropic providers (Kimi, Z.AI, etc.), returns env vars needed\n * to redirect Claude Code API calls to the correct endpoint.\n */\nfunction getProviderEnvForModel(model: string): Record<string, string> {\n const provider = getProviderForModel(model as ModelId);\n if (provider.name === 'anthropic') return {};\n\n // OpenRouter has its own key path\n if (provider.name === 'openrouter') {\n const { config } = loadYamlConfig();\n const apiKey = config.apiKeys.openrouter;\n if (apiKey) return getProviderEnv(provider, apiKey);\n throw new Error(`OpenRouter API key not configured. Add your key in Settings before using model \"${model}\".`);\n }\n\n const { config } = loadYamlConfig();\n const apiKey = config.apiKeys[provider.name as keyof typeof config.apiKeys];\n if (apiKey) {\n return getProviderEnv(provider, apiKey);\n }\n throw new Error(`No API key configured for ${provider.displayName}. Configure it in Settings before using model \"${model}\".`);\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' | 'inspect-agent' | 'uat-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 * Generate a deterministic UUID from a string.\n * Uses SHA-256 hash formatted as a UUID v4-compatible string.\n * This ensures the same specialist+project always gets the same session ID\n * while satisfying Claude Code's UUID format requirement.\n */\nfunction deterministicUUID(input: string): string {\n const hash = createHash('sha256').update(input).digest('hex');\n // Format as UUID: 8-4-4-4-12\n return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;\n}\n\n/**\n * Get session file path for a specialist.\n * Per-project specialists use a project-scoped subdirectory to prevent\n * session ID collision when multiple projects share the same specialist type.\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key (per-project specialists only)\n * @returns Path to session file\n */\nexport function getSessionFilePath(name: SpecialistType, projectKey?: string): string {\n if (projectKey) {\n return join(SPECIALISTS_DIR, 'projects', projectKey, `${name}.session`);\n }\n return join(SPECIALISTS_DIR, `${name}.session`);\n}\n\n/**\n * Read session ID from file\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key (per-project specialists only)\n * @returns Session ID or null if not found\n */\nexport function getSessionId(name: SpecialistType, projectKey?: string): string | null {\n const sessionFile = getSessionFilePath(name, projectKey);\n\n if (!existsSync(sessionFile)) {\n return null;\n }\n\n try {\n const sessionId = readFileSync(sessionFile, 'utf-8').trim();\n // Validate UUID format — Claude Code requires valid UUIDs for --resume and --session-id.\n // Old deterministic IDs (e.g., \"specialist-mind-your-now-review-agent\") are not valid UUIDs.\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n if (!uuidRegex.test(sessionId)) {\n console.warn(`[specialist] Invalid session ID format for ${name} (${projectKey ?? 'global'}): ${sessionId} — discarding`);\n unlinkSync(sessionFile);\n return null;\n }\n return sessionId;\n } catch (error) {\n console.error(`Failed to read session file for ${name} (${projectKey ?? 'global'}):`, error);\n return null;\n }\n}\n\n/**\n * Write session ID to file\n *\n * @param name - Specialist name\n * @param sessionId - Session ID to store\n * @param projectKey - Optional project key (per-project specialists only)\n */\n/**\n * Get the current session generation (for rotating session IDs).\n * Returns 0 if no generation file exists.\n */\nexport function getSessionGeneration(name: SpecialistType, projectKey?: string): number {\n const genFile = getSessionFilePath(name, projectKey) + '.gen';\n if (!existsSync(genFile)) return 0;\n try {\n return parseInt(readFileSync(genFile, 'utf-8').trim(), 10) || 0;\n } catch { return 0; }\n}\n\n/**\n * Bump the session generation — next dispatch will use a new session ID.\n * Old JSONL files are preserved (not deleted).\n */\nexport function bumpSessionGeneration(name: SpecialistType, projectKey?: string): number {\n const genFile = getSessionFilePath(name, projectKey) + '.gen';\n const dir = projectKey\n ? join(SPECIALISTS_DIR, 'projects', projectKey)\n : SPECIALISTS_DIR;\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n const newGen = getSessionGeneration(name, projectKey) + 1;\n writeFileSync(genFile, String(newGen));\n return newGen;\n}\n\nexport function setSessionId(name: SpecialistType, sessionId: string, projectKey?: string): void {\n const sessionFile = getSessionFilePath(name, projectKey);\n const dir = projectKey\n ? join(SPECIALISTS_DIR, 'projects', projectKey)\n : SPECIALISTS_DIR;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n try {\n writeFileSync(sessionFile, sessionId.trim(), 'utf-8');\n } catch (error) {\n console.error(`Failed to write session file for ${name} (${projectKey ?? 'global'}):`, error);\n throw error;\n }\n}\n\n/**\n * Delete session file\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key (per-project specialists only)\n * @returns True if file was deleted, false if it didn't exist\n */\nexport function clearSessionId(name: SpecialistType, projectKey?: string): boolean {\n const sessionFile = getSessionFilePath(name, projectKey);\n\n if (!existsSync(sessionFile)) {\n return false;\n }\n\n try {\n unlinkSync(sessionFile);\n return true;\n } catch (error) {\n console.error(`Failed to delete session file for ${name} (${projectKey ?? 'global'}):`, error);\n throw error;\n }\n}\n\n/**\n * Get metadata for a specific specialist\n *\n * @param name - Specialist name\n * @returns Specialist metadata or null if not found\n */\nexport function getSpecialistMetadata(name: SpecialistType): SpecialistMetadata | null {\n const registry = loadRegistry();\n return (registry.specialists ?? []).find((s) => s.name === name) || null;\n}\n\n/**\n * Update specialist metadata\n *\n * @param name - Specialist name\n * @param updates - Partial metadata to update\n */\nexport function updateSpecialistMetadata(\n name: SpecialistType,\n updates: Partial<SpecialistMetadata>\n): void {\n const registry = loadRegistry();\n\n const specialists = registry.specialists ?? [];\n const index = specialists.findIndex((s) => s.name === name);\n\n if (index === -1) {\n throw new Error(`Specialist ${name} not found in registry`);\n }\n\n specialists[index] = {\n ...specialists[index],\n ...updates,\n name, // Ensure name doesn't change\n };\n registry.specialists = specialists;\n\n saveRegistry(registry);\n}\n\n/**\n * Get all specialist metadata\n *\n * @returns Array of all specialists\n */\nexport function getAllSpecialists(): SpecialistMetadata[] {\n const registry = loadRegistry();\n return registry.specialists ?? [];\n}\n\n/**\n * Check if a specialist is initialized (has session file)\n *\n * @param name - Specialist name\n * @returns True if specialist has a session file\n */\nexport function isInitialized(name: SpecialistType): boolean {\n return getSessionId(name) !== null;\n}\n\n/**\n * Get the state of a specialist based on session file\n *\n * Note: This only checks if session exists, not if it's actually running.\n * Use getSpecialistStatus() for runtime state.\n *\n * @param name - Specialist name\n * @returns Specialist state\n */\nexport function getSpecialistState(name: SpecialistType): Exclude<SpecialistState, 'active'> {\n return isInitialized(name) ? 'sleeping' : 'uninitialized';\n}\n\n/**\n * Get tmux session name for a specialist\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Expected tmux session name\n */\nexport function getTmuxSessionName(name: SpecialistType, projectKey?: string): string {\n if (projectKey) {\n return `specialist-${projectKey}-${name}`;\n }\n // Legacy format for backward compatibility\n return `specialist-${name}`;\n}\n\n/**\n * Record wake event in metadata\n *\n * @param name - Specialist name\n * @param sessionId - New session ID (if changed)\n */\nexport function recordWake(name: SpecialistType, sessionId?: string): void {\n const updates: Partial<SpecialistMetadata> = {\n lastWake: new Date().toISOString(),\n };\n\n if (sessionId) {\n updates.sessionId = sessionId;\n }\n\n updateSpecialistMetadata(name, updates);\n}\n\n/**\n * ===========================================================================\n * Ephemeral Lifecycle Management\n * ===========================================================================\n */\n\n/**\n * Grace period state for a specialist\n */\nexport interface GracePeriodState {\n active: boolean;\n startedAt: string;\n duration: number; // milliseconds\n paused: boolean;\n pausedAt?: string;\n remainingTime?: number; // milliseconds when paused\n}\n\nconst gracePeriodStates = new Map<string, GracePeriodState>();\n\n/**\n * Spawn an ephemeral specialist for a project\n *\n * Creates a new specialist session that will run for this task and then terminate.\n * The specialist is seeded with context from recent runs.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param task - Task details\n * @returns Spawn result with run ID and session info\n */\nexport async function spawnEphemeralSpecialist(\n projectKey: string,\n specialistType: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n promptOverride?: string; // Use this prompt instead of building from template\n }\n): Promise<{\n success: boolean;\n runId?: string;\n tmuxSession?: string;\n message: string;\n error?: string;\n}> {\n // Ensure project specialist directory exists\n ensureProjectSpecialistDir(projectKey, specialistType);\n\n // Load context digest\n const { loadContextDigest } = await import('./specialist-context.js');\n const contextDigest = loadContextDigest(projectKey, specialistType);\n\n // Create run log\n const { createRunLog } = await import('./specialist-logs.js');\n const { runId, filePath: logFilePath } = createRunLog(\n projectKey,\n specialistType,\n task.issueId,\n contextDigest || undefined\n );\n\n // Update metadata\n setCurrentRun(projectKey, specialistType, runId);\n incrementProjectRunCount(projectKey, specialistType);\n\n // Build task prompt (use override if provided, otherwise build from template)\n const basePrompt = task.promptOverride ?? await buildTaskPrompt(projectKey, specialistType, task, contextDigest);\n\n if (task.promptOverride) {\n console.log(`[specialist] Using promptOverride for ${projectKey}/${task.issueId} (${basePrompt.length} chars)`);\n }\n\n // Prepend session-aware preamble: specialists accumulate context via --resume,\n // so they may have seen this issue before. They MUST re-execute fresh every time.\n const taskPrompt = `IMPORTANT: This is a NEW task dispatch. You may have context from prior runs in this session — that is useful background knowledge, but you MUST execute this task fresh RIGHT NOW. Do NOT skip steps or report cached results. Read the code, run the commands, and call the status update APIs as instructed below. Prior results are stale — the code may have changed.\n\n${basePrompt}`;\n\n // Spawn tmux session — use project path so specialist has correct context\n const tmuxSession = getTmuxSessionName(specialistType, projectKey);\n const project = getProject(projectKey);\n const cwd = project?.path || getDevrootPath() || homedir();\n\n // Pre-trust cwd so specialists don't hit the trust prompt\n try {\n const { preTrustDirectory } = await import('../workspace-manager.js') as { preTrustDirectory: (dir: string) => void };\n preTrustDirectory(cwd);\n } catch { /* non-fatal */ }\n\n try {\n // Check if session already exists (stale from previous run)\n try {\n const { stdout: sessions } = await execAsync('tmux list-sessions -F \"#{session_name}\" 2>/dev/null || echo \"\"', { encoding: 'utf-8' });\n if (sessions.split('\\n').map(s => s.trim()).includes(tmuxSession)) {\n const { getAgentRuntimeState } = await import('../agents.js');\n const existingState = getAgentRuntimeState(tmuxSession);\n if (existingState?.state === 'active') {\n // PAN-511: verify the session is actually running before treating it as busy.\n // If state says 'active' but the process isn't alive (e.g. Claude Code crashed),\n // the runtime.json is stale. Kill the dead session and spawn fresh instead of\n // returning specialist_busy which would permanently block new dispatches.\n const actuallyRunning = await isRunning(specialistType, projectKey);\n if (actuallyRunning) {\n return {\n success: false,\n message: `Specialist ${specialistType} (${projectKey}) is already running task ${existingState.currentIssue ?? 'unknown'}`,\n error: 'specialist_busy',\n };\n }\n console.log(`[specialist] ${tmuxSession} state=active but not running — clearing stale state`);\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n }\n // Stale session — kill it before spawning fresh\n console.log(`[specialist] Killing stale ${tmuxSession} session before respawn`);\n await execAsync(`tmux kill-session -t \"${tmuxSession}\"`, { encoding: 'utf-8' }).catch(() => {});\n }\n } catch {\n // Non-fatal: session check failure shouldn't block spawn\n }\n // Determine model for this specialist\n let model = 'claude-sonnet-4-6'; // default\n try {\n const workTypeId: WorkTypeId = `specialist-${specialistType}` as WorkTypeId;\n model = getModelId(workTypeId);\n } catch (error) {\n console.warn(`Warning: Could not resolve model for ${specialistType}, using default`);\n }\n\n // Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for non-Anthropic models\n const providerEnv = getProviderEnvForModel(model);\n // Add Panopticon cost attribution env vars so heartbeat hook records correct stage/issue\n const sessionTypeLabel = specialistType.replace('-agent', ''); // review-agent → review\n const panopticonEnv: Record<string, string> = {\n PANOPTICON_AGENT_ID: tmuxSession,\n PANOPTICON_ISSUE_ID: task.issueId,\n PANOPTICON_SESSION_TYPE: sessionTypeLabel,\n };\n const envFlags = buildTmuxEnvFlags({ ...providerEnv, ...panopticonEnv });\n\n // For credential-file providers (e.g. Kimi), configure apiKeyHelper for token refresh.\n // For all other providers, clear stale apiKeyHelper from previous runs.\n const providerConfig = getProviderForModel(model as ModelId);\n if (providerConfig.authType === 'credential-file') {\n setupCredentialFileAuth(providerConfig, cwd);\n } else {\n clearCredentialFileAuth(cwd);\n }\n\n // Permission flags based on specialist type\n const permissionFlags = specialistType === 'merge-agent'\n ? '--dangerously-skip-permissions --permission-mode bypassPermissions'\n : '--dangerously-skip-permissions';\n\n // Write task prompt to file to avoid shell escaping issues\n const agentDir = join(homedir(), '.panopticon', 'agents', tmuxSession);\n await execAsync(`mkdir -p \"${agentDir}\"`, { encoding: 'utf-8' });\n\n const promptFile = join(agentDir, 'task-prompt.md');\n writeFileSync(promptFile, taskPrompt);\n\n // Deterministic session ID: same specialist + project + generation gets the same UUID.\n // Bumping the generation (via API) rotates to a fresh session without deleting old JONLs.\n // --resume is always the default (session exists from prior runs).\n // On very first cold start, --resume fails and the launcher falls back to --session-id.\n const gen = getSessionGeneration(specialistType, projectKey);\n const sessionName = `specialist-${projectKey}-${specialistType}-gen${gen}`;\n const sessionId = deterministicUUID(sessionName);\n\n // Write session file for informational purposes (pan specialists list)\n setSessionId(specialistType, sessionId, projectKey);\n\n console.log(`[specialist] Dispatching ${specialistType} for ${projectKey}/${task.issueId} (session: ${sessionId.slice(0, 8)}...)`);\n\n // Single launcher script: always try --resume first (normal case).\n // Falls back to --session-id only on first cold start (session not in Claude's storage).\n // Prompt is always passed as CLI argument — no tmux key delivery needed.\n // Inner script runs Claude; outer launcher wraps with script(1) for real-time PTY output\n // so tmux capture-pane (God View) can see output while also logging to file.\n const launcherScript = join(agentDir, 'launcher.sh');\n const innerScript = join(agentDir, 'run-claude.sh');\n\n // Inner script: the actual Claude invocation.\n // test-agent NEVER resumes — each test run is stateless and must start fresh to avoid\n // reporting cached analysis from prior runs (accumulated history caused repeated false-FAILs\n // even after the underlying bug was fixed). All other specialists accumulate context.\n const useResume = specialistType !== 'test-agent';\n writeFileSync(innerScript, `#!/bin/bash\nset -o pipefail\ncd \"${cwd}\"\nexport PANOPTICON_AGENT_ID=\"${tmuxSession}\"\nexport PANOPTICON_ISSUE_ID=\"${task.issueId}\"\nexport PANOPTICON_SESSION_TYPE=\"${sessionTypeLabel}\"\nprompt=$(cat \"${promptFile}\")\n\n${useResume ? `# Resume existing session (accumulates context over time)\nclaude ${permissionFlags} --resume \"${sessionId}\" --model ${model} \"$prompt\"\nexit_code=$?\n\n# First cold start: session doesn't exist yet in Claude's storage\nif [ $exit_code -ne 0 ]; then\n echo \"[launcher] First run — creating session\"\n claude ${permissionFlags} --session-id \"${sessionId}\" --model ${model} \"$prompt\"\nfi` : `# test-agent: always fresh session — no --resume to prevent stale result reporting\nclaude ${permissionFlags} --model ${model} \"$prompt\"`}\n\n# Signal completion\necho \"\"\necho \"## Specialist completed task\"\n`, { mode: 0o755 });\n\n // Outer launcher: wraps inner script with script(1) for PTY + tee for log file.\n // script -qfec forces a PTY so Claude outputs in real time (visible in tmux pane + God View).\n writeFileSync(launcherScript, `#!/bin/bash\nscript -qfec \"bash '${innerScript}'\" /dev/null 2>&1 | tee -a \"${logFilePath}\"\n`, { mode: 0o755 });\n\n // Spawn Claude Code via launcher script (with provider env vars)\n // -c sets tmux session working directory to project path (prevents trust prompt — PAN-384)\n // Kill stale session first to prevent \"duplicate session\" error (PAN-430)\n await execAsync(`tmux kill-session -t \"${tmuxSession}\" 2>/dev/null || true`, { encoding: 'utf-8' });\n await execAsync(\n `tmux new-session -d -s \"${tmuxSession}\" -c \"${cwd}\"${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 ${specialistType} for ${projectKey}/${task.issueId} (run: ${runId})`);\n\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 * Shared test-agent prompt builder — used by both buildTaskPrompt (ephemeral spawn)\n * and wakeSpecialistWithTask (queue-based wake). Extracted to avoid the bug where\n * ephemeral test specialists got empty prompts (PAN-511).\n */\nexport async function buildTestAgentPromptContent(task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n}): Promise<string> {\n const apiPort = process.env.API_PORT || process.env.PORT || '3011';\n const apiUrl = process.env.DASHBOARD_URL || `http://localhost:${apiPort}`;\n const testWorkspace = task.workspace || 'unknown';\n const testGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const testIsPolyrepo = testGitInfo.isPolyrepo;\n\n const { extractTeamPrefix, findProjectByTeam } = await import('../projects.js');\n const testTeamPrefix = extractTeamPrefix(task.issueId);\n const testProjectConfig = testTeamPrefix ? findProjectByTeam(testTeamPrefix) : null;\n const testConfigs = testProjectConfig?.tests;\n\n let testCommands = '';\n let baselineCommands = '';\n const featureName = task.issueId.toLowerCase();\n const mainWorkspacePath = testWorkspace.replace(/workspaces\\/feature-[^/]+/, 'workspaces/main');\n const projectRootPath = testProjectConfig?.path || testWorkspace.replace(/\\/workspaces\\/.*/, '');\n\n if (testConfigs && Object.keys(testConfigs).length > 0) {\n const testEntries = Object.entries(testConfigs);\n const testSuites: string[] = [];\n const baselineSuites: string[] = [];\n for (const [name, cfg] of testEntries) {\n const testDir = testIsPolyrepo\n ? `${testWorkspace}/${cfg.path}`\n : (cfg.path === '.' ? testWorkspace : `${testWorkspace}/${cfg.path}`);\n const baseDir = testIsPolyrepo\n ? `${mainWorkspacePath}/${cfg.path}`\n : (cfg.path === '.' ? mainWorkspacePath : `${mainWorkspacePath}/${cfg.path}`);\n const fallbackDir = cfg.path === '.' ? projectRootPath : `${projectRootPath}/${cfg.path}`;\n testSuites.push(`echo \"\\\\n=== Test suite: ${name} (${cfg.type}) ===\" && cd \"${testDir}\" && ${cfg.command} 2>&1; echo \"EXIT_CODE_${name}: $?\"`);\n baselineSuites.push(`echo \"\\\\n=== Baseline: ${name} (${cfg.type}) ===\" && cd \"${baseDir}\" 2>/dev/null && ${cfg.command} 2>&1 || (cd \"${fallbackDir}\" 2>/dev/null && ${cfg.command} 2>&1) || echo \"BASELINE_SKIP_${name}: could not run baseline\"; echo \"EXIT_CODE_${name}: $?\"`);\n }\n testCommands = testSuites.map((cmd, i) => `# Suite ${i + 1}\\n${cmd}`).join('\\n');\n baselineCommands = baselineSuites.map((cmd, i) => `# Suite ${i + 1}\\n${cmd}`).join('\\n');\n } else if (testIsPolyrepo) {\n const testSuites: string[] = [];\n const baselineSuites: string[] = [];\n for (const gitDir of testGitInfo.gitDirs) {\n const repoName = basename(gitDir);\n testSuites.push(`echo \"\\\\n=== ${repoName} ===\" && cd \"${gitDir}\" && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo \"No test runner found\"; fi; echo \"EXIT_CODE_${repoName}: $?\"`);\n const baseDir = `${mainWorkspacePath}/${repoName}`;\n baselineSuites.push(`echo \"\\\\n=== Baseline: ${repoName} ===\" && cd \"${baseDir}\" 2>/dev/null && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo \"No test runner found\"; fi; echo \"EXIT_CODE_${repoName}: $?\"`);\n }\n testCommands = testSuites.join('\\n');\n baselineCommands = baselineSuites.join('\\n');\n } else {\n testCommands = `cd \"${testWorkspace}\" && npm test 2>&1; echo \"EXIT_CODE: $?\"`;\n baselineCommands = `cd \"${mainWorkspacePath}\" 2>/dev/null && npm test 2>&1 || (cd \"${projectRootPath}\" && npm test 2>&1); echo \"EXIT_CODE: $?\"`;\n }\n\n const testConfigSummary = testConfigs\n ? Object.entries(testConfigs).map(([name, cfg]) => `- **${name}** (${cfg.type}): \\`${cfg.command}\\` in \\`${cfg.path}/\\``).join('\\n')\n : testIsPolyrepo\n ? testGitInfo.gitDirs.map(d => `- **${basename(d)}**: auto-detected`).join('\\n')\n : '- Single test suite at workspace root';\n\n const timeoutMs = testConfigs && Object.values(testConfigs).some(c => c.type === 'maven') ? '600000' : '300000';\n\n return `Your task:\n1. Run ALL test suites — redirect output to file, read only summaries\n2. If ALL pass, skip baseline and report PASS\n3. If failures, run baseline on main and compare\n4. Only fail for NEW regressions (not pre-existing)\n5. Update status via API when done\n\n## Test Suites\n\n${testConfigSummary}\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.\n\n## CRITICAL: Bash Timeout for Test Commands\n\n**ALWAYS use timeout: ${timeoutMs} when running test commands.**\n\n## Step 1: Run Feature Branch Tests\n\n\\`\\`\\`bash\n(\n${testCommands}\n) > /tmp/test-feature.txt 2>&1\n# Use timeout: ${timeoutMs} for this command\necho \"--- Feature test output tail ---\"\ntail -40 /tmp/test-feature.txt\ngrep \"EXIT_CODE\" /tmp/test-feature.txt\n\\`\\`\\`\n\n## Step 2: Check Results\n\n- If ALL exit codes are 0 → skip baseline, go to \"Update Status\"\n- If any failures → continue to Step 3\n\n## Step 3: Baseline Comparison (ONLY if failures found)\n\n\\`\\`\\`bash\n(\n${baselineCommands}\n) > /tmp/test-main.txt 2>&1\n# Use timeout: ${timeoutMs} for this command\necho \"--- Baseline test output tail ---\"\ntail -40 /tmp/test-main.txt\ngrep \"EXIT_CODE\" /tmp/test-main.txt\n\\`\\`\\`\n\nThen compare failures:\n\\`\\`\\`bash\ngrep -E \"FAIL|✗|Error|failed|BUILD FAILURE\" /tmp/test-feature.txt | head -30\ngrep -E \"FAIL|✗|Error|failed|BUILD FAILURE\" /tmp/test-main.txt | head -30\n\\`\\`\\`\n\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.\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]\"}' | 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]\"}' | jq .\n\\`\\`\\`\n\n## Container Smoke Test\n\nAfter unit tests pass, verify Docker workspace frontend is accessible if containers are running:\n\\`\\`\\`bash\ndocker ps --filter \"name=${featureName}\" --format \"{{.Names}} {{.Status}}\" 2>/dev/null\n\\`\\`\\`\n\nIMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.`;\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 // Delegate to shared test-agent prompt builder\n const testPrompt = await buildTestAgentPromptContent(task);\n prompt += testPrompt;\n break;\n }\n\n case 'merge-agent': {\n const bInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n if (bInfo.isPolyrepo) {\n prompt += `This is a POLYREPO project with ${bInfo.gitDirs.length} repos: ${bInfo.gitDirs.map(d => basename(d)).join(', ')}.\nYou must merge each repo separately.\\n\\n`;\n }\n prompt += `Your task:\n1. Fetch the latest main branch\n2. Attempt to merge ${bInfo.branch} into main\n3. Resolve conflicts intelligently if needed\n4. Run tests to verify merge is clean\n5. Complete merge if tests pass\n6. NEVER use git push --force`;\n break;\n }\n }\n\n prompt += `\\n\\nWhen you complete your task, report your findings and status.`;\n\n return prompt;\n}\n\n/**\n * Start grace period for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param duration - Grace period duration in milliseconds (default: 60000)\n */\nexport function startGracePeriod(\n projectKey: string,\n specialistType: SpecialistType,\n duration: number = 60000\n): void {\n const key = `${projectKey}-${specialistType}`;\n\n gracePeriodStates.set(key, {\n active: true,\n startedAt: new Date().toISOString(),\n duration,\n paused: false,\n });\n\n console.log(`[specialist] Grace period started for ${projectKey}/${specialistType} (${duration}ms)`);\n\n // Schedule termination after grace period\n setTimeout(() => {\n const state = gracePeriodStates.get(key);\n if (state && state.active && !state.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, duration);\n}\n\n/**\n * Pause grace period countdown\n */\nexport function pauseGracePeriod(projectKey: string, specialistType: SpecialistType): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active) {\n return false;\n }\n\n const elapsed = Date.now() - new Date(state.startedAt).getTime();\n const remaining = state.duration - elapsed;\n\n state.paused = true;\n state.pausedAt = new Date().toISOString();\n state.remainingTime = remaining;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period paused for ${projectKey}/${specialistType}`);\n\n return true;\n}\n\n/**\n * Resume grace period countdown\n */\nexport function resumeGracePeriod(projectKey: string, specialistType: SpecialistType): boolean {\n const key = `${projectKey}-${specialistType}`;\n const state = gracePeriodStates.get(key);\n\n if (!state || !state.active || !state.paused) {\n return false;\n }\n\n state.paused = false;\n state.startedAt = new Date().toISOString();\n state.pausedAt = undefined;\n\n gracePeriodStates.set(key, state);\n console.log(`[specialist] Grace period resumed for ${projectKey}/${specialistType}`);\n\n // Schedule termination for remaining time\n setTimeout(() => {\n const currentState = gracePeriodStates.get(key);\n if (currentState && currentState.active && !currentState.paused) {\n terminateSpecialist(projectKey, specialistType);\n }\n }, state.remainingTime || 0);\n\n return true;\n}\n\n/**\n * Exit grace period immediately (terminate now)\n */\nexport function exitGracePeriod(projectKey: string, specialistType: SpecialistType): void {\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n terminateSpecialist(projectKey, specialistType);\n}\n\n/**\n * Get grace period state\n */\nexport function getGracePeriodState(\n projectKey: string,\n specialistType: SpecialistType\n): GracePeriodState | null {\n const key = `${projectKey}-${specialistType}`;\n return gracePeriodStates.get(key) || null;\n}\n\n/**\n * Signal that a specialist has completed its task\n *\n * This should be called when the specialist finishes its work.\n * It updates the run status and starts the grace period.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param result - Task result\n */\nexport function signalSpecialistCompletion(\n projectKey: string,\n specialistType: SpecialistType,\n result: {\n status: 'passed' | 'failed' | 'blocked';\n notes?: string;\n }\n): void {\n const metadata = getProjectSpecialistMetadata(projectKey, specialistType);\n\n // Update status\n updateRunStatus(projectKey, specialistType, result.status);\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n const { finalizeRunLog } = require('./specialist-logs.js');\n\n try {\n finalizeRunLog(projectKey, specialistType, metadata.currentRun, {\n status: result.status,\n notes: result.notes,\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n }\n\n // Start grace period (60 seconds)\n startGracePeriod(projectKey, specialistType, 60000);\n\n console.log(`[specialist] ${specialistType} completed for ${projectKey} (status: ${result.status})`);\n}\n\n/**\n * Terminate a specialist session\n *\n * Kills the tmux session, finalizes logs, and schedules digest generation.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport async function terminateSpecialist(\n projectKey: string,\n specialistType: SpecialistType\n): Promise<void> {\n const tmuxSession = getTmuxSessionName(specialistType, projectKey);\n const metadata = getProjectSpecialistMetadata(projectKey, specialistType);\n\n try {\n // Kill tmux session\n await execAsync(`tmux kill-session -t \"${tmuxSession}\"`);\n console.log(`[specialist] Terminated ${projectKey}/${specialistType}`);\n } catch (error) {\n console.error(`[specialist] Failed to kill tmux session ${tmuxSession}:`, error);\n }\n\n // Finalize log if there's a current run\n if (metadata.currentRun) {\n const { finalizeRunLog } = await import('./specialist-logs.js');\n\n try {\n finalizeRunLog(projectKey, specialistType, metadata.currentRun, {\n status: metadata.lastRunStatus || 'incomplete',\n notes: 'Specialist terminated',\n });\n } catch (error) {\n console.error(`[specialist] Failed to finalize log:`, error);\n }\n\n // Clear current run\n setCurrentRun(projectKey, specialistType, null);\n }\n\n // Clear grace period state\n const key = `${projectKey}-${specialistType}`;\n gracePeriodStates.delete(key);\n\n // Update runtime state\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'suspended',\n lastActivity: new Date().toISOString(),\n });\n\n // Schedule digest generation (async, fire-and-forget)\n const { scheduleDigestGeneration } = await import('./specialist-context.js');\n scheduleDigestGeneration(projectKey, specialistType);\n\n // Run log cleanup for this project/specialist (async, fire-and-forget)\n scheduleLogCleanup(projectKey, specialistType);\n}\n\n/**\n * Schedule log cleanup for a project's specialist (async, fire-and-forget)\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nfunction scheduleLogCleanup(projectKey: string, specialistType: SpecialistType): void {\n // Run async without awaiting\n Promise.resolve().then(async () => {\n try {\n const { cleanupOldLogs } = await import('./specialist-logs.js');\n const { getSpecialistRetention } = await import('../projects.js');\n\n const retention = getSpecialistRetention(projectKey);\n const deleted = cleanupOldLogs(projectKey, specialistType, { maxDays: retention.max_days, maxRuns: retention.max_runs });\n\n if (deleted > 0) {\n console.log(`[specialist] Cleaned up ${deleted} old logs for ${projectKey}/${specialistType}`);\n }\n } catch (error) {\n console.error(`[specialist] Log cleanup failed for ${projectKey}/${specialistType}:`, error);\n }\n });\n}\n\n/**\n * ===========================================================================\n * Per-Project Specialist Functions\n * ===========================================================================\n */\n\n/**\n * Get the directory for a project's specialist\n */\nexport function getProjectSpecialistDir(projectKey: string, specialistType: SpecialistType): string {\n return join(SPECIALISTS_DIR, projectKey, specialistType);\n}\n\n/**\n * Ensure per-project specialist directory structure exists\n */\nexport function ensureProjectSpecialistDir(projectKey: string, specialistType: SpecialistType): void {\n const specialistDir = getProjectSpecialistDir(projectKey, specialistType);\n const runsDir = join(specialistDir, 'runs');\n const contextDir = join(specialistDir, 'context');\n\n if (!existsSync(runsDir)) {\n mkdirSync(runsDir, { recursive: true });\n }\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Get per-project specialist metadata\n */\nexport function getProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistType\n): ProjectSpecialistMetadata {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][specialistType]) {\n // Initialize with defaults\n registry.projects[projectKey][specialistType] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n saveRegistry(registry);\n }\n\n return registry.projects[projectKey][specialistType];\n}\n\n/**\n * Update per-project specialist metadata\n */\nexport function updateProjectSpecialistMetadata(\n projectKey: string,\n specialistType: SpecialistType,\n updates: Partial<ProjectSpecialistMetadata>\n): void {\n const registry = loadRegistry();\n\n if (!registry.projects[projectKey]) {\n registry.projects[projectKey] = {};\n }\n\n if (!registry.projects[projectKey][specialistType]) {\n registry.projects[projectKey][specialistType] = {\n runCount: 0,\n lastRunAt: null,\n lastRunStatus: null,\n currentRun: null,\n };\n }\n\n registry.projects[projectKey][specialistType] = {\n ...registry.projects[projectKey][specialistType],\n ...updates,\n };\n\n saveRegistry(registry);\n}\n\n/**\n * Increment run count for a project's specialist\n */\nexport function incrementProjectRunCount(projectKey: string, specialistType: SpecialistType): void {\n const metadata = getProjectSpecialistMetadata(projectKey, specialistType);\n updateProjectSpecialistMetadata(projectKey, specialistType, {\n runCount: metadata.runCount + 1,\n lastRunAt: new Date().toISOString(),\n });\n}\n\n/**\n * Set current run for a project's specialist\n */\nexport function setCurrentRun(\n projectKey: string,\n specialistType: SpecialistType,\n runId: string | null\n): void {\n updateProjectSpecialistMetadata(projectKey, specialistType, { currentRun: runId });\n}\n\n/**\n * Update run status for a project's specialist\n */\nexport function updateRunStatus(\n projectKey: string,\n specialistType: SpecialistType,\n status: 'passed' | 'failed' | 'blocked' | null\n): void {\n updateProjectSpecialistMetadata(projectKey, specialistType, { lastRunStatus: status });\n}\n\n/**\n * List all projects that have specialists configured\n */\nexport function listProjectsWithSpecialists(): string[] {\n const registry = loadRegistry();\n return Object.keys(registry.projects);\n}\n\n/**\n * List all specialist types for a project\n */\nexport function listSpecialistsForProject(projectKey: string): SpecialistType[] {\n const registry = loadRegistry();\n const project = registry.projects[projectKey];\n\n if (!project) {\n return [];\n }\n\n return Object.keys(project) as SpecialistType[];\n}\n\n/**\n * Get all per-project specialist statuses\n */\nexport async function getAllProjectSpecialistStatuses(): Promise<Array<{\n projectKey: string;\n specialistType: SpecialistType;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n}>> {\n const registry = loadRegistry();\n const results: Array<{\n projectKey: string;\n specialistType: SpecialistType;\n metadata: ProjectSpecialistMetadata;\n isRunning: boolean;\n tmuxSession: string;\n }> = [];\n\n for (const [projectKey, specialists] of Object.entries(registry.projects)) {\n for (const [specialistType, metadata] of Object.entries(specialists)) {\n const tmuxSession = getTmuxSessionName(specialistType as SpecialistType, projectKey);\n const running = await isRunning(specialistType as SpecialistType, projectKey);\n\n results.push({\n projectKey,\n specialistType: specialistType as SpecialistType,\n metadata,\n isRunning: running,\n tmuxSession,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Update context token count for a specialist\n *\n * @param name - Specialist name\n * @param tokens - Total context tokens\n */\nexport function updateContextTokens(name: SpecialistType, tokens: number): void {\n updateSpecialistMetadata(name, { contextTokens: tokens });\n}\n\n/**\n * List all session files in the specialists directory\n *\n * @returns Array of specialist names that have session files\n */\nexport function listSessionFiles(): SpecialistType[] {\n initSpecialistsDirectory();\n\n try {\n const files = readdirSync(SPECIALISTS_DIR);\n const sessionFiles = files.filter((f) => f.endsWith('.session'));\n\n return sessionFiles.map((f) => f.replace('.session', '') as SpecialistType);\n } catch (error) {\n console.error('Failed to list session files:', error);\n return [];\n }\n}\n\n/**\n * Enable a specialist\n *\n * @param name - Specialist name\n */\nexport function enableSpecialist(name: SpecialistType): void {\n updateSpecialistMetadata(name, { enabled: true });\n}\n\n/**\n * Disable a specialist\n *\n * @param name - Specialist name\n */\nexport function disableSpecialist(name: SpecialistType): void {\n updateSpecialistMetadata(name, { enabled: false });\n}\n\n/**\n * Check if a specialist is enabled\n *\n * @param name - Specialist name\n * @returns True if specialist is enabled\n */\nexport function isEnabled(name: SpecialistType): boolean {\n const metadata = getSpecialistMetadata(name);\n return metadata?.enabled ?? false;\n}\n\n/**\n * Get all enabled specialists\n *\n * @returns Array of enabled specialists\n */\nexport function getEnabledSpecialists(): SpecialistMetadata[] {\n return getAllSpecialists().filter((s) => s.enabled);\n}\n\n/**\n * Find JSONL file for a session ID\n *\n * Searches through Claude Code project directories to find the JSONL file.\n *\n * @param sessionId - Session ID to find\n * @returns Path to JSONL file or null if not found\n */\nexport function findSessionFile(sessionId: string): string | null {\n try {\n const allFiles = getAllSessionFiles();\n\n for (const file of allFiles) {\n const fileSessionId = basename(file, '.jsonl');\n if (fileSessionId === sessionId) {\n return file;\n }\n }\n } catch {\n // Session files not available\n }\n\n return null;\n}\n\n/**\n * Count context tokens for a specialist session\n *\n * Reads the JSONL file for the specialist's session and sums all token usage.\n * This gives an approximate count of context size.\n *\n * @param name - Specialist name\n * @returns Total token count or null if session not found\n */\nexport function countContextTokens(name: SpecialistType): number | null {\n const sessionId = getSessionId(name);\n\n if (!sessionId) {\n return null;\n }\n\n const sessionFile = findSessionFile(sessionId);\n\n if (!sessionFile) {\n return null;\n }\n\n const sessionUsage = parseClaudeSession(sessionFile);\n\n if (!sessionUsage) {\n return null;\n }\n\n // Sum all token types for total context\n return (\n sessionUsage.usage.inputTokens +\n sessionUsage.usage.outputTokens +\n (sessionUsage.usage.cacheReadTokens || 0) +\n (sessionUsage.usage.cacheWriteTokens || 0)\n );\n}\n\n/**\n * Check if a specialist is currently running in tmux\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns True if specialist has an active tmux session\n */\nexport async function isRunning(name: SpecialistType, projectKey?: string): Promise<boolean> {\n const tmuxSession = getTmuxSessionName(name, projectKey);\n\n try {\n await execAsync(`tmux has-session -t ${tmuxSession}`);\n // Session exists — but check if the pane actually has a running process.\n // When Claude Code crashes, the pane's process exits but the tmux session persists,\n // making has-session return success even though nothing is running.\n const { stdout } = await execAsync(\n `tmux list-panes -t ${tmuxSession} -F \"#{pane_pid}\" 2>/dev/null`,\n { encoding: 'utf-8' }\n );\n const panePid = stdout.trim();\n if (!panePid) return false;\n // Check if the pane's process has any child processes (Claude Code / bash)\n const { stdout: children } = await execAsync(\n `ps --ppid ${panePid} --no-headers 2>/dev/null || echo \"\"`,\n { encoding: 'utf-8' }\n );\n return children.trim().length > 0;\n } catch {\n return false;\n }\n}\n\n/**\n * Get complete status for a specialist\n *\n * Combines metadata, session info, and runtime state (PAN-80: uses hook-based state).\n *\n * @param name - Specialist name\n * @param projectKey - Optional project key for per-project specialists\n * @returns Complete specialist status\n */\nexport async function getSpecialistStatus(\n name: SpecialistType,\n projectKey?: string\n): Promise<SpecialistStatus> {\n const metadata = getSpecialistMetadata(name) || {\n name,\n displayName: name,\n description: '',\n enabled: false,\n autoWake: false,\n };\n\n const sessionId = getSessionId(name, projectKey);\n const running = await isRunning(name, projectKey);\n const contextTokens = countContextTokens(name);\n\n // Determine state from hook-based runtime state (PAN-80)\n const { getAgentRuntimeState } = await import('../agents.js');\n const tmuxSession = getTmuxSessionName(name, projectKey);\n const runtimeState = getAgentRuntimeState(tmuxSession);\n\n let state: SpecialistState;\n if (runtimeState) {\n // Map runtime state to specialist state\n switch (runtimeState.state) {\n case 'active':\n state = 'active';\n break;\n case 'idle':\n state = 'sleeping'; // Idle = at prompt waiting\n break;\n case 'suspended':\n state = 'sleeping'; // Suspended = session saved, not running\n break;\n case 'uninitialized':\n default:\n state = 'uninitialized';\n break;\n }\n } else {\n // Fallback if no runtime state available\n if (running && sessionId) {\n state = 'sleeping';\n } else if (sessionId) {\n state = 'sleeping';\n } else {\n state = 'uninitialized';\n }\n }\n\n return {\n ...metadata,\n sessionId: sessionId || undefined,\n contextTokens: contextTokens || undefined,\n state,\n isRunning: running,\n tmuxSession: getTmuxSessionName(name, projectKey),\n currentIssue: runtimeState?.currentIssue,\n };\n}\n\n/**\n * Get status for all specialists\n *\n * @returns Array of specialist statuses\n */\nexport async function getAllSpecialistStatus(): Promise<SpecialistStatus[]> {\n const specialists = getAllSpecialists();\n return Promise.all(specialists.map((metadata) => getSpecialistStatus(metadata.name)));\n}\n\n/**\n * Initialize a specialist agent\n *\n * Creates a tmux session and starts Claude Code with an identity prompt.\n * This is for first-time initialization of specialists that don't have session files.\n *\n * @param name - Specialist name\n * @returns Promise with initialization result\n */\nexport async function initializeSpecialist(name: SpecialistType): Promise<{\n success: boolean;\n message: string;\n tmuxSession?: string;\n error?: string;\n}> {\n // Check if already running\n if (await isRunning(name)) {\n return {\n success: false,\n message: `Specialist ${name} is already running`,\n error: 'already_running',\n };\n }\n\n // Check if already initialized\n if (getSessionId(name)) {\n return {\n success: false,\n message: `Specialist ${name} is already initialized. Use wake to start it.`,\n error: 'already_initialized',\n };\n }\n\n const tmuxSession = getTmuxSessionName(name);\n const cwd = getDevrootPath() || homedir();\n\n // Determine model for this specialist using work type router\n let model = 'claude-sonnet-4-6'; // default fallback\n try {\n // Map specialist name to work type ID\n const workTypeId: WorkTypeId = `specialist-${name}` as WorkTypeId;\n model = getModelId(workTypeId);\n } catch (error) {\n console.warn(`Warning: Could not resolve model for ${name}, using default model`);\n }\n\n // Create identity prompt for the specialist\n const identityPrompt = `You are the ${name} specialist agent for Panopticon.\nYour role: ${name === 'merge-agent' ? 'Resolve merge conflicts and ensure clean integrations' :\n name === 'review-agent' ? 'Review code changes and provide quality feedback' :\n name === 'test-agent' ? 'Execute and analyze test results' : 'Assist with development tasks'}\n\nYou will be woken up when your services are needed. For now, acknowledge your initialization and wait.\nSay: \"I am the ${name} specialist, ready and waiting for tasks.\"`;\n\n try {\n // Get provider-specific env vars (BASE_URL, AUTH_TOKEN) for non-Anthropic models\n const providerEnv = getProviderEnvForModel(model);\n const envFlags = buildTmuxEnvFlags(providerEnv);\n\n // For credential-file providers (e.g. Kimi), configure apiKeyHelper for token refresh.\n // For all other providers, clear stale apiKeyHelper from previous runs.\n const providerCfg = getProviderForModel(model as ModelId);\n if (providerCfg.authType === 'credential-file') {\n setupCredentialFileAuth(providerCfg, cwd);\n } else {\n clearCredentialFileAuth(cwd);\n }\n\n // Write identity prompt and launcher script to avoid shell escaping issues\n const agentDir = join(homedir(), '.panopticon', 'agents', tmuxSession);\n await execAsync(`mkdir -p \"${agentDir}\"`, { encoding: 'utf-8' });\n\n const promptFile = join(agentDir, 'identity-prompt.md');\n const launcherScript = join(agentDir, 'launcher.sh');\n\n writeFileSync(promptFile, identityPrompt);\n const newSessionId = randomUUID();\n writeFileSync(launcherScript, `#!/bin/bash\ncd \"${cwd}\"\nprompt=$(cat \"${promptFile}\")\nexec claude --dangerously-skip-permissions --session-id \"${newSessionId}\" --model ${model} \"$prompt\"\n`, { mode: 0o755 });\n setSessionId(name, newSessionId);\n\n // Pre-trust cwd so specialists don't hit the trust prompt (same as spawnSpecialist)\n try {\n const { preTrustDirectory } = await import('../workspace-manager.js') as { preTrustDirectory: (dir: string) => void };\n preTrustDirectory(cwd);\n } catch { /* non-fatal */ }\n\n // Spawn Claude Code via launcher script (with provider env vars)\n // -c sets tmux session working directory to project path (prevents trust prompt)\n // Kill stale session first to prevent \"duplicate session\" error (PAN-430)\n await execAsync(`tmux kill-session -t \"${tmuxSession}\" 2>/dev/null || true`, { encoding: 'utf-8' });\n await execAsync(\n `tmux new-session -d -s \"${tmuxSession}\" -c \"${cwd}\"${envFlags} \"bash '${launcherScript}'\"`,\n { encoding: 'utf-8' }\n );\n\n // Record wake event\n recordWake(name);\n\n return {\n success: true,\n message: `Specialist ${name} initialized and started`,\n tmuxSession,\n };\n } catch (error: unknown) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n message: `Failed to initialize specialist ${name}: ${errorMessage}`,\n error: errorMessage,\n };\n }\n}\n\n/**\n * Initialize all enabled but uninitialized specialists\n *\n * Called during Cloister startup to ensure specialists are ready.\n *\n * @returns Promise with array of initialization results\n */\nexport async function initializeEnabledSpecialists(): Promise<Array<{\n name: SpecialistType;\n success: boolean;\n message: string;\n}>> {\n const enabled = getEnabledSpecialists();\n const results: Array<{ name: SpecialistType; success: boolean; message: string }> = [];\n\n for (const specialist of enabled) {\n const sessionId = getSessionId(specialist.name);\n\n if (!sessionId) {\n // Specialist is enabled but not initialized\n console.log(` → Auto-initializing specialist: ${specialist.name}`);\n const result = await initializeSpecialist(specialist.name);\n results.push({\n name: specialist.name,\n success: result.success,\n message: result.message,\n });\n\n // Small delay between initializations to avoid overwhelming the system\n if (results.length < enabled.length) {\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n } else {\n results.push({\n name: specialist.name,\n success: true,\n message: `Already initialized with session ${sessionId.substring(0, 8)}...`,\n });\n }\n }\n\n return results;\n}\n\n/**\n * Reset specialist state before sending a new task\n *\n * Clears stale state from previous tasks:\n * 1. Sends Ctrl+C to cancel any pending command\n * 2. Runs 'cd ~' to reset working directory\n * 3. Sends Ctrl+U to clear the prompt buffer\n *\n * @param name - Specialist name\n */\nasync function resetSpecialist(name: SpecialistType): Promise<void> {\n const tmuxSession = getTmuxSessionName(name);\n\n try {\n // 1. Cancel any pending command with Ctrl+C and wait for Claude to return to idle.\n // Do NOT send 'cd ~' here — that triggers LLM inference (2-5s) and creates a race:\n // the task message arrives while Claude is still processing the cd command and gets lost.\n await execAsync(`tmux send-keys -t \"${tmuxSession}\" C-c`, { encoding: 'utf-8' });\n await new Promise(resolve => setTimeout(resolve, 500));\n\n // 2. Clear any partial input on the prompt line\n await execAsync(`tmux send-keys -t \"${tmuxSession}\" C-u`, { encoding: 'utf-8' });\n await new Promise(resolve => setTimeout(resolve, 100));\n } catch (error) {\n console.error(`[specialist] Failed to reset ${name}:`, error);\n // Non-fatal - continue with wake\n }\n}\n\n/**\n * Wake a specialist to process a task\n *\n * Sends a task prompt to a running specialist. If the specialist isn't running,\n * starts it first (with --resume if it has a session).\n *\n * @param name - Specialist name\n * @param taskPrompt - The task prompt to send to the specialist\n * @param options - Additional options\n * @returns Promise with wake result\n */\nexport async function wakeSpecialist(\n name: SpecialistType,\n taskPrompt: string,\n options: {\n waitForReady?: boolean; // Wait for agent to be ready before sending prompt (default: true)\n startIfNotRunning?: boolean; // Start the agent if not running (default: true)\n issueId?: string; // Issue ID being worked on (for tracking)\n skipBusyGuard?: boolean; // Skip busy check (caller already verified idle + set active)\n } = {}\n): Promise<{\n success: boolean;\n message: string;\n tmuxSession?: string;\n wasAlreadyRunning: boolean;\n error?: string;\n}> {\n const { waitForReady = true, startIfNotRunning = true, issueId } = options;\n const tmuxSession = getTmuxSessionName(name);\n const sessionId = getSessionId(name);\n const wasAlreadyRunning = await isRunning(name);\n\n // Guard: if specialist is running and busy, refuse to send a new task.\n // Sending a message to a busy Claude session causes \"Interrupted\" behavior —\n // the running tool is cancelled and the previous task is abandoned mid-flight.\n // Callers should use wakeSpecialistOrQueue() for automatic busy handling.\n // Skip this guard when called from wakeSpecialistOrQueue (skipBusyGuard),\n // since the caller already verified idle state and pre-set active to prevent races.\n if (wasAlreadyRunning && !options.skipBusyGuard) {\n const { getAgentRuntimeState } = await import('../agents.js');\n const runtimeState = getAgentRuntimeState(tmuxSession);\n if (runtimeState?.state === 'active') {\n console.warn(`[specialist] ${name} is busy (working on ${runtimeState.currentIssue}), refusing to interrupt`);\n return {\n success: false,\n message: `Specialist ${name} is busy (working on ${runtimeState.currentIssue}). Use wakeSpecialistOrQueue() instead.`,\n tmuxSession,\n wasAlreadyRunning: true,\n error: 'specialist_busy',\n };\n }\n }\n\n // If not running, start it first\n if (!wasAlreadyRunning) {\n if (!startIfNotRunning) {\n return {\n success: false,\n message: `Specialist ${name} is not running`,\n wasAlreadyRunning: false,\n error: 'not_running',\n };\n }\n\n // Use devroot (~/Projects) — already trusted in Claude Code\n const cwd = getDevrootPath() || join(process.env.HOME || '/home/eltmon', 'Projects');\n\n // Pre-trust cwd so specialists don't hit the trust prompt\n try {\n const { preTrustDirectory } = await import('../workspace-manager.js') as { preTrustDirectory: (dir: string) => void };\n preTrustDirectory(cwd);\n } catch { /* non-fatal */ }\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 // Add Panopticon cost attribution env vars\n const wakeSessionType = name.replace('-agent', ''); // review-agent → review\n const wakePanEnv: Record<string, string> = {\n PANOPTICON_AGENT_ID: tmuxSession,\n PANOPTICON_SESSION_TYPE: wakeSessionType,\n };\n if (issueId) {\n wakePanEnv.PANOPTICON_ISSUE_ID = issueId;\n }\n const envFlags = buildTmuxEnvFlags({ ...providerEnv, ...wakePanEnv });\n\n // For credential-file providers (e.g. Kimi), configure apiKeyHelper for token refresh.\n // For all other providers, clear stale apiKeyHelper from previous runs.\n const provCfg = getProviderForModel(model as ModelId);\n if (provCfg.authType === 'credential-file') {\n setupCredentialFileAuth(provCfg, cwd);\n } else {\n clearCredentialFileAuth(cwd);\n }\n\n // merge-agent needs full bypass to handle git stash drop, reset, etc.\n const permissionFlags = name === 'merge-agent'\n ? '--dangerously-skip-permissions --permission-mode bypassPermissions'\n : '--dangerously-skip-permissions';\n\n // Start with --resume if we have a session, otherwise generate a new session ID\n let claudeCmd: string;\n if (sessionId) {\n claudeCmd = `claude --resume \"${sessionId}\" ${modelFlag} ${permissionFlags}`;\n } else {\n const newSessionId = randomUUID();\n claudeCmd = `claude --session-id \"${newSessionId}\" ${modelFlag} ${permissionFlags}`;\n setSessionId(name, newSessionId);\n }\n\n // Kill stale session first to prevent \"duplicate session\" error (PAN-430)\n await execAsync(`tmux kill-session -t \"${tmuxSession}\" 2>/dev/null || true`, { encoding: 'utf-8' });\n await execAsync(\n `tmux new-session -d -s \"${tmuxSession}\" -c \"${cwd}\"${envFlags} \"${claudeCmd}\"`,\n { encoding: 'utf-8' }\n );\n\n if (waitForReady) {\n // Poll for Claude's interactive prompt instead of fixed sleep.\n // Fresh starts can take 5-10s; 15s timeout covers slow models.\n const ready = await waitForClaudePrompt(tmuxSession, 15000);\n if (!ready) {\n console.warn(`[specialist] ${name}: prompt not detected within 15s, proceeding anyway`);\n }\n }\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n message: `Failed to start specialist ${name}: ${msg}`,\n wasAlreadyRunning: false,\n error: msg,\n };\n }\n }\n\n // Reset specialist state to clear stale context from previous tasks\n await resetSpecialist(name);\n\n // Wait for Claude to be at its interactive prompt before sending the task.\n // For already-running specialists this should be near-instant; for freshly-started\n // ones the waitForReady above already waited, but resetSpecialist's C-c may have\n // interrupted something so we re-confirm here.\n const promptReady = await waitForClaudePrompt(tmuxSession, wasAlreadyRunning ? 5000 : 15000);\n if (!promptReady) {\n console.warn(`[specialist] ${name}: prompt not detected after reset, proceeding anyway`);\n }\n\n // Send the task prompt\n try {\n // For large prompts (>500 chars or multiline), write to file to avoid tmux paste issues\n // Tmux send-keys with large text shows as \"[Pasted text #1 +N lines]\" which Claude doesn't process\n const isLargePrompt = taskPrompt.length > 500 || taskPrompt.includes('\\n');\n\n // Prepare the message to send\n let messageToSend: string;\n if (isLargePrompt) {\n if (!existsSync(TASKS_DIR)) {\n mkdirSync(TASKS_DIR, { recursive: true });\n }\n const taskFile = join(TASKS_DIR, `${name}-${Date.now()}.md`);\n writeFileSync(taskFile, taskPrompt, 'utf-8');\n messageToSend = `Read and execute the task in: ${taskFile}`;\n } else {\n messageToSend = taskPrompt;\n }\n\n // Snapshot tmux output BEFORE sending so we can detect new activity\n const outputBefore = await capturePaneAsync(tmuxSession, 50);\n\n // Send the task message\n await sendKeysAsync(tmuxSession, messageToSend);\n\n // Verify Claude received the message by watching for new output (tool calls, responses).\n // This catches silent delivery failures — the structural root cause of lost tasks.\n const delivered = await confirmDelivery(tmuxSession, outputBefore, 10000);\n if (!delivered) {\n console.warn(`[specialist] ${name}: no activity detected after task send, retrying...`);\n // Re-snapshot and retry once\n const retryBefore = await capturePaneAsync(tmuxSession, 50);\n await sendKeysAsync(tmuxSession, messageToSend);\n const retryDelivered = await confirmDelivery(tmuxSession, retryBefore, 10000);\n if (!retryDelivered) {\n return {\n success: false,\n message: `Task message not received by specialist ${name} after retry`,\n tmuxSession,\n wasAlreadyRunning,\n error: 'delivery_failed',\n };\n }\n }\n\n // Record wake event\n recordWake(name, sessionId || undefined);\n\n // Set state to active immediately (PAN-80: spinner should show right away)\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'active',\n lastActivity: new Date().toISOString(),\n currentIssue: issueId,\n });\n\n return {\n success: true,\n message: wasAlreadyRunning\n ? `Sent task to running specialist ${name}`\n : `Started specialist ${name} and sent task`,\n tmuxSession,\n wasAlreadyRunning,\n };\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n message: `Failed to send task to specialist ${name}: ${msg}`,\n tmuxSession,\n wasAlreadyRunning,\n error: msg,\n };\n }\n}\n\n/**\n * Wake specialist with a task from the queue\n *\n * Convenience wrapper that formats task details into a prompt.\n *\n * @param name - Specialist name\n * @param task - Task from the queue\n * @returns Promise with wake result\n */\nexport async function wakeSpecialistWithTask(\n name: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n },\n options: { skipBusyGuard?: boolean } = {}\n): Promise<ReturnType<typeof wakeSpecialist>> {\n // Build context-aware prompt based on specialist type and task\n const apiPort = process.env.API_PORT || process.env.PORT || '3011';\n const apiUrl = process.env.DASHBOARD_URL || `http://localhost:${apiPort}`;\n let prompt: string;\n\n switch (name) {\n case 'merge-agent': {\n const mergeWorkspace = task.workspace || 'unknown';\n const mergeInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const mergeBranch = mergeInfo.branch;\n\n const mergeRepoInstructions = mergeInfo.isPolyrepo\n ? `\\nIMPORTANT: This is a POLYREPO project. There are ${mergeInfo.gitDirs.length} separate git repositories to merge:\n${mergeInfo.gitDirs.map((d, i) => `${i + 1}. ${basename(d)}: ${d}`).join('\\n')}\n\nThe workspace root is NOT a git repo. You must cd into each subdirectory to run git commands.\nYou MUST complete the merge for ALL repos.\\n`\n : '';\n\n prompt = `New merge task for ${task.issueId}:\n\nBranch: ${mergeBranch}\nWorkspace: ${mergeWorkspace}\n${mergeInfo.isPolyrepo ? `Polyrepo: git repos in subdirectories: ${mergeInfo.gitDirs.map(d => basename(d)).join(', ')}` : ''}\n${task.prUrl ? `PR URL: ${task.prUrl}` : ''}\n${mergeRepoInstructions}\nFor ${mergeInfo.isPolyrepo ? 'EACH repo' : 'the repo'}, perform these steps:\n\nPHASE 1 — SYNC & BASELINE (before merge):\n1. ${mergeInfo.isPolyrepo ? 'cd into the repo directory' : `cd ${mergeWorkspace}`}\n2. git checkout main\n3. git fetch origin main\n4. Sync local main with origin/main:\n Run: git rev-list --left-right --count main...origin/main\n If REMOTE_AHEAD > 0: git rebase origin/main\n If rebase conflicts: abort and report failure.\n5. Run tests on main to establish a baseline. Record BASELINE_PASS and BASELINE_FAIL.\n\nPHASE 2 — MERGE (dry run):\n6. git merge ${mergeBranch} --no-edit\n7. If conflicts: resolve them intelligently, then git add and git commit\n8. If clean merge: the merge commit is auto-created (or fast-forward)\n\nPHASE 3 — VERIFY:\n9. Run tests again. Record MERGE_PASS and MERGE_FAIL.\n\nPHASE 4 — DECIDE:\n10. Compare results:\n - If MERGE_FAIL > BASELINE_FAIL (NEW test failures): ROLLBACK with git reset --hard ORIG_HEAD and report FAILED\n - If MERGE_FAIL <= BASELINE_FAIL (no new failures): Report PASSED (merge is validated)\n - Pre-existing failures on main are NOT a reason to rollback\n\nCRITICAL: Do NOT push to main. Do NOT run git push origin main.\nThe merge validation stays LOCAL. A human will click Merge in the dashboard to push.\n\nPHASE 5 — REPORT:\n11. Call the Panopticon API to report results:\n curl -s -X POST ${apiUrl}/api/specialists/done \\\\\n -H \"Content-Type: application/json\" \\\\\n -d '{\"specialist\":\"merge\",\"issueId\":\"${task.issueId}\",\"status\":\"passed|failed\",\"notes\":\"<summary>\"}'\n\nCRITICAL: You MUST call the /api/specialists/done endpoint whether you succeed or fail.\nCRITICAL: NEVER push to main — only humans merge. Your job is to VALIDATE the merge, not execute it.\nCRITICAL: NEVER use git push --force.\nCRITICAL: Do NOT delete the feature branch.`;\n break;\n }\n\n case 'review-agent': {\n // Pre-check: detect stale branch (0 diff from main) before waking the agent\n const workspace = task.workspace || 'unknown';\n\n // Resolve git directories and branch from workspace\n const reviewGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const gitDirs = reviewGitInfo.gitDirs;\n // Use first git dir for pre-check (primary repo), fall back to workspace root\n const gitDir = gitDirs[0] || workspace;\n\n let staleBranch = false;\n if (workspace !== 'unknown' && gitDirs.length > 0) {\n try {\n // For polyrepos, check all git dirs — if ANY has changes, it's not stale\n let totalChangedFiles = 0;\n for (const dir of gitDirs) {\n const { stdout: dirDiff } = await execAsync(\n `cd \"${dir}\" && git fetch origin main 2>/dev/null; git diff --name-only main...HEAD 2>/dev/null`,\n { encoding: 'utf-8', timeout: 15000 }\n );\n totalChangedFiles += dirDiff.trim().split('\\n').filter((f: string) => f.length > 0).length;\n }\n if (totalChangedFiles === 0) {\n staleBranch = true;\n console.log(`[specialist] review-agent: stale branch detected for ${task.issueId} — 0 files changed vs main`);\n\n // Auto-complete the review: set reviewStatus to passed\n const { setReviewStatus } = await import('../review-status.js');\n setReviewStatus(task.issueId.toUpperCase(), {\n reviewStatus: 'passed',\n reviewNotes: 'No changes to review — branch identical to main (already merged or stale)',\n });\n console.log(`[specialist] review-agent: auto-passed ${task.issueId} (stale branch)`);\n\n // Also try to signal via the specialists/done path for idle state management\n const tmuxSession = getTmuxSessionName('review-agent');\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n });\n\n return { success: true, message: `Stale branch auto-passed for ${task.issueId}`, wasAlreadyRunning: false, error: undefined };\n }\n } catch (err) {\n // If pre-check fails, fall through to normal wake — agent will handle it\n console.warn(`[specialist] review-agent: stale branch pre-check failed for ${task.issueId}:`, err);\n }\n }\n\n // Build git commands for the prompt — polyrepo workspaces need git commands in subdirectories\n const isPolyrepo = gitDirs.length > 1;\n const gitDiffCommands = gitDirs.length > 0\n ? gitDirs.map(d => `cd \"${d}\" && git diff --name-only main...HEAD`).join('\\n')\n : `cd \"${workspace}\" && git diff --name-only main...HEAD`;\n const gitDiffFileCmd = gitDirs.length > 0\n ? `cd \"${gitDir}\" && git diff main...HEAD -- <file>`\n : `cd \"${workspace}\" && git diff main...HEAD -- <file>`;\n\n prompt = `New review task for ${task.issueId}:\n\nBranch: ${task.branch || 'unknown'}\nWorkspace: ${workspace}\n${isPolyrepo ? `Polyrepo: git repos in subdirectories: ${gitDirs.map(d => basename(d)).join(', ')}` : ''}\n${task.prUrl ? `PR URL: ${task.prUrl}` : ''}\n\nYour task:\n1. Review all changes in the branch compared to main\n2. Check for code quality issues, security concerns, and best practices\n3. Verify test FILES exist for new code (DO NOT run tests - test-agent does that)\n4. Provide specific, actionable feedback\n\nIMPORTANT: DO NOT run tests (npm test). You are the REVIEW agent - you only review code.\nThe TEST agent will run tests in the next step.\n\n## How to Review Changes\n\n**Step 0 (CRITICAL):** First check if there are ANY changes to review:\n${isPolyrepo ? `This is a polyrepo — run git diff in each repo subdirectory:` : ''}\n\\`\\`\\`bash\n${gitDiffCommands}\n\\`\\`\\`\n\n**If the diff is EMPTY (0 files changed across all repos):** The branch is stale or already merged into main. In this case:\n1. Do NOT attempt a full review\n2. Update status as passed immediately:\n\\`\\`\\`bash\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"reviewStatus\":\"passed\",\"reviewNotes\":\"No changes to review — branch identical to main (already merged or stale)\"}' | jq .\n\\`\\`\\`\n3. Tell the issue agent:\n\\`\\`\\`bash\npan work tell ${task.issueId} \"Review complete: branch has 0 diff from main — already merged or stale. Marking as passed.\"\n\\`\\`\\`\n4. Stop here — you are done.\n\n**Step 1:** Get the list of changed files:\n\\`\\`\\`bash\n${gitDiffCommands}\n\\`\\`\\`\n\n**Step 2:** Read the CURRENT version of each changed file using the Read tool.\nReview the actual file contents — do NOT rely solely on diff output.\n\n**Step 3:** If you need to see what specifically changed, use:\n\\`\\`\\`bash\n${gitDiffFileCmd}\n\\`\\`\\`\n\n## Avoiding False Positives\n\n**CRITICAL:** When reviewing diffs, understand that:\n- Lines starting with \\`+\\` are ADDITIONS (new code)\n- Lines starting with \\`-\\` are DELETIONS (removed code)\n- Lines without prefix are CONTEXT (unchanged surrounding code)\n- The SAME content may appear in both \\`-\\` and \\`+\\` sections when code is moved or reformatted — this is NOT duplication\n- A section shown in diff context does NOT mean it appears twice in the actual file\n- **Always read the actual file** to verify before claiming duplicate or redundant content\n\nDo NOT flag:\n- Code that appears in both removed and added hunks (it was moved, not duplicated)\n- Diff context lines as \"duplicate sections\" — they exist once in the real file\n- Reformatted/restructured code as \"duplicated\"\n\n## REQUIRED: Update Status via API\n\nYou MUST execute these curl commands and verify they succeed. Do NOT just describe them - actually RUN them with Bash.\n\nIf issues found:\n\\`\\`\\`bash\n# EXECUTE THIS - verify you see JSON response with reviewStatus\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"reviewStatus\":\"blocked\",\"reviewNotes\":\"[describe issues]\"}' | jq .\n\\`\\`\\`\nThen use send-feedback-to-agent skill to notify issue agent.\n\nIf review passes:\n\\`\\`\\`bash\n# EXECUTE THIS FIRST - verify you see JSON response with reviewStatus:\"passed\"\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"reviewStatus\":\"passed\"}' | jq .\n\n# THEN EXECUTE THIS - verify you see JSON response with queued task\ncurl -s -X POST ${apiUrl}/api/specialists/test-agent/queue -H \"Content-Type: application/json\" -d '{\"issueId\":\"${task.issueId}\",\"workspace\":\"${task.workspace}\",\"branch\":\"${task.branch}\"}' | jq .\n\\`\\`\\`\n\n⚠️ VERIFICATION: After running each curl, confirm you see valid JSON output. If you get an error, report it.`;\n break;\n }\n\n case 'test-agent': {\n // Resolve polyrepo structure and project test config\n const testWorkspace = task.workspace || 'unknown';\n const testGitInfo = await resolveWorkspaceGitInfo(task.workspace, task.branch);\n const testIsPolyrepo = testGitInfo.isPolyrepo;\n\n // Look up project test config from projects.yaml\n const { extractTeamPrefix, findProjectByTeam } = await import('../projects.js');\n const testTeamPrefix = extractTeamPrefix(task.issueId);\n const testProjectConfig = testTeamPrefix ? findProjectByTeam(testTeamPrefix) : null;\n const testConfigs = testProjectConfig?.tests;\n\n // Build per-repo test commands from projects.yaml config\n let testCommands = '';\n let baselineCommands = '';\n const featureName = task.issueId.toLowerCase();\n // Derive main workspace path for baseline comparison\n const mainWorkspacePath = testWorkspace.replace(/workspaces\\/feature-[^/]+/, 'workspaces/main');\n const projectRootPath = testProjectConfig?.path || testWorkspace.replace(/\\/workspaces\\/.*/, '');\n\n if (testConfigs && Object.keys(testConfigs).length > 0) {\n // Use projects.yaml test config — each entry may target a different repo subdirectory\n const testEntries = Object.entries(testConfigs);\n const testSuites: string[] = [];\n const baselineSuites: string[] = [];\n for (const [name, cfg] of testEntries) {\n const testDir = testIsPolyrepo\n ? `${testWorkspace}/${cfg.path}`\n : (cfg.path === '.' ? testWorkspace : `${testWorkspace}/${cfg.path}`);\n const baseDir = testIsPolyrepo\n ? `${mainWorkspacePath}/${cfg.path}`\n : (cfg.path === '.' ? mainWorkspacePath : `${mainWorkspacePath}/${cfg.path}`);\n // Fall back to project root for monorepo baseline if main workspace doesn't exist\n const fallbackDir = cfg.path === '.' ? projectRootPath : `${projectRootPath}/${cfg.path}`;\n testSuites.push(`echo \"\\\\n=== Test suite: ${name} (${cfg.type}) ===\" && cd \"${testDir}\" && ${cfg.command} 2>&1; echo \"EXIT_CODE_${name}: $?\"`);\n baselineSuites.push(`echo \"\\\\n=== Baseline: ${name} (${cfg.type}) ===\" && cd \"${baseDir}\" 2>/dev/null && ${cfg.command} 2>&1 || (cd \"${fallbackDir}\" 2>/dev/null && ${cfg.command} 2>&1) || echo \"BASELINE_SKIP_${name}: could not run baseline\"; echo \"EXIT_CODE_${name}: $?\"`);\n }\n testCommands = testSuites.map((cmd, i) => `# Suite ${i + 1}\\n${cmd}`).join('\\n');\n baselineCommands = baselineSuites.map((cmd, i) => `# Suite ${i + 1}\\n${cmd}`).join('\\n');\n } else if (testIsPolyrepo) {\n // No projects.yaml config but detected polyrepo — discover test commands per repo\n const testSuites: string[] = [];\n const baselineSuites: string[] = [];\n for (const gitDir of testGitInfo.gitDirs) {\n const repoName = basename(gitDir);\n // Auto-detect test runner in each repo\n testSuites.push(`echo \"\\\\n=== ${repoName} ===\" && cd \"${gitDir}\" && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo \"No test runner found\"; fi; echo \"EXIT_CODE_${repoName}: $?\"`);\n const baseDir = `${mainWorkspacePath}/${repoName}`;\n baselineSuites.push(`echo \"\\\\n=== Baseline: ${repoName} ===\" && cd \"${baseDir}\" 2>/dev/null && if [ -f pom.xml ]; then ./mvnw test 2>&1; elif [ -f package.json ]; then npm test 2>&1; else echo \"No test runner found\"; fi; echo \"EXIT_CODE_${repoName}: $?\"`);\n }\n testCommands = testSuites.join('\\n');\n baselineCommands = baselineSuites.join('\\n');\n } else {\n // Monorepo fallback — single test command\n testCommands = `cd \"${testWorkspace}\" && npm test 2>&1; echo \"EXIT_CODE: $?\"`;\n baselineCommands = `cd \"${mainWorkspacePath}\" 2>/dev/null && npm test 2>&1 || (cd \"${projectRootPath}\" && npm test 2>&1); echo \"EXIT_CODE: $?\"`;\n }\n\n // Build test suite summary for the prompt\n const testConfigSummary = testConfigs\n ? Object.entries(testConfigs).map(([name, cfg]) => `- **${name}** (${cfg.type}): \\`${cfg.command}\\` in \\`${cfg.path}/\\``).join('\\n')\n : testIsPolyrepo\n ? testGitInfo.gitDirs.map(d => `- **${basename(d)}**: auto-detected`).join('\\n')\n : '- Single test suite at workspace root';\n\n prompt = `New test task for ${task.issueId}:\n\nBranch: ${task.branch || 'unknown'}\nWorkspace: ${testWorkspace}\n${testIsPolyrepo ? `Polyrepo: git repos in subdirectories: ${testGitInfo.gitDirs.map(d => basename(d)).join(', ')}` : ''}\n\n## Test Suites\n\n${testConfigSummary}\n\nYour task:\n1. Run ALL test suites — redirect output to file, read only summaries\n2. If ALL pass, skip baseline and report PASS\n3. If failures, run baseline on main and compare\n4. Only fail for NEW regressions (not pre-existing)\n5. Update status via API when done\n\n## CRITICAL: Context Management — Output Redirection\n\n**NEVER let full test output flow into your context.** Always redirect to file and read only summaries.\nRaw test output from large suites (1000+ tests) WILL fill your context and cause compaction, losing your task.\n\n## CRITICAL: Bash Timeout for Test Commands\n\n**ALWAYS use timeout: 300000 (5 minutes) when running test commands.**\nFor Maven/Spring Boot tests, use timeout: 600000 (10 minutes) — they take longer.\n\n## Step 1: Run Feature Branch Tests\n\n${testIsPolyrepo || (testConfigs && Object.keys(testConfigs).length > 1)\n ? `**Run ALL test suites** — each suite is a separate repo/runner. Redirect ALL output to one file.`\n : ''}\n\n\\`\\`\\`bash\n(\n${testCommands}\n) > /tmp/test-feature.txt 2>&1\n# Use timeout: ${testConfigs && Object.values(testConfigs).some(c => c.type === 'maven') ? '600000' : '300000'} for this command\necho \"--- Feature test output tail ---\"\ntail -40 /tmp/test-feature.txt\ngrep \"EXIT_CODE\" /tmp/test-feature.txt\n\\`\\`\\`\n\n## Step 2: Check Results\n\n- If ALL exit codes are 0 → skip baseline, go to \"Update Status\"\n- If any failures → continue to Step 3\n\n## Step 3: Baseline Comparison (ONLY if failures found)\n\n\\`\\`\\`bash\n(\n${baselineCommands}\n) > /tmp/test-main.txt 2>&1\n# Use timeout: ${testConfigs && Object.values(testConfigs).some(c => c.type === 'maven') ? '600000' : '300000'} for this command\necho \"--- Baseline test output tail ---\"\ntail -40 /tmp/test-main.txt\ngrep \"EXIT_CODE\" /tmp/test-main.txt\n\\`\\`\\`\n\nThen compare failures (targeted, NOT full output):\n\\`\\`\\`bash\ngrep -E \"FAIL|✗|Error|failed|BUILD FAILURE\" /tmp/test-feature.txt | head -30\ngrep -E \"FAIL|✗|Error|failed|BUILD FAILURE\" /tmp/test-main.txt | head -30\n\\`\\`\\`\n\nTests that fail on BOTH = pre-existing (don't block). Tests that fail ONLY on feature = NEW regression (block).\n\n**Pass criteria:** Feature branch introduces ZERO new test failures vs main.\n**Fail criteria:** Feature branch introduces NEW failures not present on main.\n\n## REQUIRED: Update Status via API\n\nYou MUST execute the appropriate curl command and verify it succeeds. Do NOT just describe it - actually RUN it with Bash.\n\nIf NO new regressions (tests PASS):\n\\`\\`\\`bash\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"testStatus\":\"passed\",\"testNotes\":\"[summary including pre-existing failures if any, and which suites were tested]\"}' | jq .\n\\`\\`\\`\n\nIf NEW regressions found (tests FAIL):\n\\`\\`\\`bash\ncurl -s -X POST ${apiUrl}/api/workspaces/${task.issueId}/review-status -H \"Content-Type: application/json\" -d '{\"testStatus\":\"failed\",\"testNotes\":\"[describe NEW failures only — specify which suite/repo]\"}' | jq .\n\\`\\`\\`\nThen use send-feedback-to-agent skill to notify issue agent of NEW failures only.\n\n⚠️ VERIFICATION: After running curl, confirm you see valid JSON output with the updated status. If you get an error or empty response, the update FAILED - report this.\n\n**NEVER run test commands without redirecting to a file.** This is not optional.\n\n## REQUIRED: Container Smoke Test\n\nAfter unit tests pass, verify the Docker workspace frontend is accessible.\nThis is NOT optional — UI changes that pass unit tests but break in containers must be caught.\n\n\\`\\`\\`bash\n# Check if containers are running for this workspace\ndocker ps --filter \"name=${featureName}\" --format \"{{.Names}} {{.Status}}\" 2>/dev/null\n\\`\\`\\`\n\nIf containers are running, test these URLs:\n- **Frontend**: \\`curl -sk https://feature-${featureName}.${testProjectConfig?.workspace?.dns?.domain || 'pan.localhost'}/ | head -5\\`\n- **API proxy**: \\`curl -sk https://feature-${featureName}.${testProjectConfig?.workspace?.dns?.domain || 'pan.localhost'}/api/health\\`\n- **API issues**: \\`curl -sk https://feature-${featureName}.${testProjectConfig?.workspace?.dns?.domain || 'pan.localhost'}/api/issues | head -100\\`\n\n**Pass criteria:**\n1. Frontend returns HTML containing \\`<div id=\"root\">\\`\n2. \\`/api/health\\` returns JSON with \\`\"status\":\"ok\"\\`\n3. \\`/api/issues\\` returns JSON array (not an error)\n\n**If ANY of these fail, the test FAILS** — report via the API with details about which check failed.\nIf containers are NOT running, note it but don't fail (containers may not be configured for this project).\n\nIMPORTANT: Do NOT hand off to merge-agent. Human clicks Merge button when ready.`;\n break;\n }\n\n default:\n prompt = `Task for ${task.issueId}: Please process this task and report findings.`;\n }\n\n return wakeSpecialist(name, prompt, { issueId: task.issueId, skipBusyGuard: options.skipBusyGuard });\n}\n\n/**\n * Task context interface for handoffs and specialist tasks\n */\nexport interface TaskContext {\n prUrl?: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n reason?: string;\n targetModel?: string;\n additionalInstructions?: string;\n [key: string]: string | string[] | undefined;\n}\n\n/**\n * Wake a specialist or queue the task if busy\n *\n * This wrapper checks if the specialist is busy before waking.\n * If the specialist is running but not idle, the task is queued instead.\n *\n * @param name - Specialist name\n * @param task - Task details\n * @param priority - Task priority (default: 'normal')\n * @param source - Source of the task (default: 'handoff')\n * @returns Promise with result indicating whether task was queued or executed\n */\nexport async function wakeSpecialistOrQueue(\n name: SpecialistType,\n task: {\n issueId: string;\n branch?: string;\n workspace?: string;\n prUrl?: string;\n context?: TaskContext;\n },\n options: {\n priority?: 'urgent' | 'high' | 'normal' | 'low';\n source?: string;\n } = {}\n): Promise<{\n success: boolean;\n queued: boolean;\n message: string;\n error?: string;\n}> {\n const { priority = 'normal', source = 'handoff' } = options;\n\n // DAG-aware readiness gate: if a vBRIEF item ID is provided in context,\n // check that all its blocking dependencies are completed before scheduling.\n // This prevents scheduling work whose dependencies aren't done yet.\n const vbriefItemId = task.context?.vbriefItemId as string | undefined;\n const workspacePath = task.workspace || (task.context?.workspace as string | undefined);\n if (vbriefItemId && workspacePath) {\n try {\n if (!isTaskReady(vbriefItemId, workspacePath)) {\n return {\n success: false,\n queued: false,\n message: `Task \"${vbriefItemId}\" has incomplete blocking dependencies — not ready to schedule`,\n };\n }\n } catch (readinessErr: any) {\n // Non-fatal: proceed if readiness check fails\n console.warn(`[specialist] Task readiness check failed for ${vbriefItemId}: ${readinessErr.message}`);\n }\n }\n\n // Check if specialist is running and get state (PAN-80)\n const running = await isRunning(name);\n const { getAgentRuntimeState } = await import('../agents.js');\n const tmuxSession = getTmuxSessionName(name);\n const runtimeState = getAgentRuntimeState(tmuxSession);\n const idle = runtimeState?.state === 'idle' || runtimeState?.state === 'suspended';\n\n // If running and busy (active), queue the task\n if (running && !idle) {\n try {\n submitToSpecialistQueue(name, {\n priority,\n source,\n issueId: task.issueId,\n workspace: task.workspace,\n branch: task.branch,\n prUrl: task.prUrl,\n context: task.context,\n });\n\n console.log(`[specialist] ${name} busy, queued task for ${task.issueId} (priority: ${priority})`);\n\n return {\n success: true,\n queued: true,\n message: `Specialist ${name} is busy. Task queued with ${priority} priority.`,\n };\n } catch (error: unknown) {\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n queued: false,\n message: `Failed to queue task for ${name}: ${msg}`,\n error: msg,\n };\n }\n }\n\n // Otherwise, wake the specialist directly\n // PAN-88: Set state to 'active' IMMEDIATELY to prevent race conditions\n // This must happen BEFORE the actual wake to block concurrent requests\n const { saveAgentRuntimeState } = await import('../agents.js');\n saveAgentRuntimeState(tmuxSession, {\n state: 'active',\n lastActivity: new Date().toISOString(),\n currentIssue: task.issueId,\n });\n console.log(`[specialist] ${name} marked active (preventing concurrent wakes)`);\n\n try {\n const wakeResult = await wakeSpecialistWithTask(name, task, { skipBusyGuard: true });\n\n if (!wakeResult.success) {\n // Wake failed - revert state to idle and clear currentIssue\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n }\n\n return {\n success: wakeResult.success,\n queued: false,\n message: wakeResult.message,\n error: wakeResult.error,\n };\n } catch (error: unknown) {\n // Exception - revert state to idle and clear currentIssue\n saveAgentRuntimeState(tmuxSession, {\n state: 'idle',\n lastActivity: new Date().toISOString(),\n currentIssue: undefined,\n });\n\n const msg = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n queued: false,\n message: `Failed to wake specialist ${name}: ${msg}`,\n error: msg,\n };\n }\n}\n\n/**\n * ===========================================================================\n * Specialist Queue Helpers\n * ===========================================================================\n */\n\nimport { HookItem, pushToHook, checkHook, popFromHook } from '../hooks.js';\n\n/**\n * Specialist queue item - extends HookItem with specialist-specific payload\n */\nexport interface SpecialistQueueItem extends HookItem {\n type: 'task';\n payload: {\n prUrl?: string;\n issueId: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n context?: TaskContext;\n };\n}\n\n/**\n * Submit a task to a specialist's queue\n *\n * @param specialistName - Name of the specialist (e.g., 'review-agent', 'merge-agent')\n * @param task - Task details\n * @returns The created queue item\n */\nexport function submitToSpecialistQueue(\n specialistName: SpecialistType,\n task: {\n priority: 'urgent' | 'high' | 'normal' | 'low';\n source: string;\n prUrl?: string;\n issueId: string;\n workspace?: string;\n branch?: string;\n filesChanged?: string[];\n context?: TaskContext;\n }\n): HookItem {\n // Put specialist-specific fields into context to match HookItem type\n const item: Omit<HookItem, 'id' | 'createdAt'> = {\n type: 'task',\n priority: task.priority,\n source: task.source,\n payload: {\n issueId: task.issueId,\n context: {\n ...task.context,\n prUrl: task.prUrl,\n workspace: task.workspace,\n branch: task.branch,\n filesChanged: task.filesChanged,\n },\n },\n };\n\n const queueItem = pushToHook(specialistName, item);\n\n notifyPipeline({ type: 'task_queued', specialist: specialistName, issueId: task.issueId });\n\n // Log specialist handoff event\n const handoffEvent = createSpecialistHandoff(\n task.source, // From (e.g., 'review-agent' or 'issue-agent')\n specialistName, // To specialist\n task.issueId,\n task.priority,\n {\n workspace: task.workspace,\n branch: task.branch,\n prUrl: task.prUrl,\n source: task.source,\n }\n );\n logSpecialistHandoff(handoffEvent);\n\n return queueItem;\n}\n\n/**\n * Check if a specialist has pending work in their queue\n *\n * @param specialistName - Name of the specialist\n * @returns Queue status\n */\nexport function checkSpecialistQueue(specialistName: SpecialistType): {\n hasWork: boolean;\n urgentCount: number;\n items: HookItem[];\n} {\n return checkHook(specialistName);\n}\n\n/**\n * Remove a completed task from a specialist's queue\n *\n * @param specialistName - Name of the specialist\n * @param itemId - ID of the completed task\n * @returns True if item was removed\n */\nexport function completeSpecialistTask(specialistName: SpecialistType, itemId: string): boolean {\n return popFromHook(specialistName, itemId);\n}\n\n/**\n * Get the next task from a specialist's queue (highest priority)\n *\n * Does NOT remove the task - use completeSpecialistTask() after execution.\n *\n * @param specialistName - Name of the specialist\n * @returns The next task or null if queue is empty\n */\nexport function getNextSpecialistTask(specialistName: SpecialistType): HookItem | null {\n const { items } = checkSpecialistQueue(specialistName);\n return items.length > 0 ? items[0] : null;\n}\n\n/**\n * ===========================================================================\n * Specialist Feedback System\n * ===========================================================================\n *\n * Specialists accumulate context and expertise. This system allows them to\n * share learnings back to issue agents, creating a feedback loop that\n * improves the overall system over time.\n */\n\n/**\n * Feedback from a specialist to an issue agent\n */\nexport interface SpecialistFeedback {\n id: string;\n timestamp: string;\n fromSpecialist: SpecialistType;\n toIssueId: string;\n feedbackType: 'success' | 'failure' | 'warning' | 'insight';\n category: 'merge' | 'test' | 'review' | 'general';\n summary: string;\n details: string;\n actionItems?: string[];\n patterns?: string[]; // Patterns the specialist noticed\n suggestions?: string[]; // Suggestions for the issue agent\n}\n\nconst FEEDBACK_DIR = join(PANOPTICON_HOME, 'specialists', 'feedback');\nconst FEEDBACK_LOG = join(FEEDBACK_DIR, 'feedback.jsonl');\n\n/**\n * Send feedback from a specialist to an issue agent\n *\n * This is the key mechanism for specialists to share their accumulated\n * expertise back to the issue agents that spawned the work.\n *\n * @param feedback - The feedback to send\n * @returns True if feedback was sent successfully\n */\nexport async function sendFeedbackToAgent(\n feedback: Omit<SpecialistFeedback, 'id' | 'timestamp'>\n): Promise<boolean> {\n const { fromSpecialist, toIssueId, summary, details } = feedback;\n\n // Ensure feedback directory exists\n if (!existsSync(FEEDBACK_DIR)) {\n mkdirSync(FEEDBACK_DIR, { recursive: true });\n }\n\n // Create full feedback record\n const fullFeedback: SpecialistFeedback = {\n ...feedback,\n id: `feedback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n timestamp: new Date().toISOString(),\n };\n\n // Log feedback to JSONL\n try {\n const line = JSON.stringify(fullFeedback) + '\\n';\n appendFileSync(FEEDBACK_LOG, line, 'utf-8');\n } catch (error) {\n console.error(`[specialist] Failed to log feedback:`, error);\n }\n\n // Try to send feedback to the issue agent\n const agentSession = `agent-${toIssueId.toLowerCase()}`;\n\n // Format feedback message for the agent\n const feedbackMessage = formatFeedbackForAgent(fullFeedback);\n\n // Write feedback to workspace file\n const { writeFeedbackFile } = await import('./feedback-writer.js');\n const specialistMap: Record<string, 'review-agent' | 'test-agent' | 'merge-agent'> = {\n 'review-agent': 'review-agent',\n 'test-agent': 'test-agent',\n 'merge-agent': 'merge-agent',\n };\n const specialist = specialistMap[fromSpecialist] || 'review-agent';\n const outcome = feedback.feedbackType === 'success' ? 'approved' : feedback.feedbackType === 'failure' ? 'failed' : feedback.feedbackType;\n\n const fileResult = await writeFeedbackFile({\n issueId: toIssueId,\n specialist,\n outcome,\n summary: summary.slice(0, 100),\n markdownBody: feedbackMessage,\n });\n\n if (!fileResult.success) {\n console.error(`[specialist] Failed to write feedback file for ${toIssueId}: ${fileResult.error}`);\n return false;\n }\n\n // Send short reference pointing to the file\n try {\n const { messageAgent } = await import('../agents.js');\n const msg = `SPECIALIST FEEDBACK: ${fromSpecialist} reported ${feedback.feedbackType.toUpperCase()} for ${toIssueId}.\\nRead and address: ${fileResult.relativePath}`;\n await messageAgent(agentSession, msg);\n console.log(`[specialist] Sent feedback from ${fromSpecialist} to ${agentSession} (file: ${fileResult.relativePath})`);\n return true;\n } catch (err) {\n // Agent may be gone — feedback file is still in the workspace for crash recovery\n console.log(`[specialist] Could not send reference to ${agentSession} (file written): ${err}`);\n return true; // File was written successfully, that's the important part\n }\n}\n\n/**\n * Format feedback for display to an agent\n */\nfunction formatFeedbackForAgent(feedback: SpecialistFeedback): string {\n const { fromSpecialist, feedbackType, category, summary, details, actionItems, patterns, suggestions } = feedback;\n\n const typeEmoji = {\n success: '✅',\n failure: '❌',\n warning: '⚠️',\n insight: '💡',\n }[feedbackType];\n\n let message = `\\n${typeEmoji} **Feedback from ${fromSpecialist}** (${category})\\n\\n`;\n message += `**Summary:** ${summary}\\n\\n`;\n message += `**Details:**\\n${details}\\n`;\n\n if (actionItems?.length) {\n message += `\\n**Action Items:**\\n`;\n actionItems.forEach((item, i) => {\n message += `${i + 1}. ${item}\\n`;\n });\n }\n\n if (patterns?.length) {\n message += `\\n**Patterns Noticed:**\\n`;\n patterns.forEach(pattern => {\n message += `- ${pattern}\\n`;\n });\n }\n\n if (suggestions?.length) {\n message += `\\n**Suggestions:**\\n`;\n suggestions.forEach(suggestion => {\n message += `- ${suggestion}\\n`;\n });\n }\n\n return message;\n}\n\n/**\n * Get pending feedback for an issue that hasn't been delivered yet\n *\n * @param issueId - Issue ID to get feedback for\n * @returns Array of feedback records\n */\nexport function getPendingFeedback(issueId: string): SpecialistFeedback[] {\n if (!existsSync(FEEDBACK_LOG)) {\n return [];\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n const allFeedback = lines.map(line => JSON.parse(line) as SpecialistFeedback);\n\n // Filter to this issue\n return allFeedback.filter(f => f.toIssueId.toLowerCase() === issueId.toLowerCase());\n } catch (error) {\n console.error(`[specialist] Failed to read feedback log:`, error);\n return [];\n }\n}\n\n/**\n * Get feedback statistics for all specialists\n *\n * @returns Feedback stats by specialist and type\n */\nexport function getFeedbackStats(): {\n bySpecialist: Record<SpecialistType, number>;\n byType: Record<string, number>;\n total: number;\n} {\n const stats = {\n bySpecialist: {\n 'merge-agent': 0,\n 'review-agent': 0,\n 'test-agent': 0,\n } as Record<SpecialistType, number>,\n byType: {} as Record<string, number>,\n total: 0,\n };\n\n if (!existsSync(FEEDBACK_LOG)) {\n return stats;\n }\n\n try {\n const content = readFileSync(FEEDBACK_LOG, 'utf-8');\n const lines = content.trim().split('\\n').filter(l => l.length > 0);\n\n for (const line of lines) {\n const feedback = JSON.parse(line) as SpecialistFeedback;\n stats.bySpecialist[feedback.fromSpecialist] = (stats.bySpecialist[feedback.fromSpecialist] || 0) + 1;\n stats.byType[feedback.feedbackType] = (stats.byType[feedback.feedbackType] || 0) + 1;\n stats.total++;\n }\n } catch (error) {\n console.error(`[specialist] Failed to read feedback stats:`, error);\n }\n\n return stats;\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { $ as updateContextTokens, A as initSpecialistsDirectory, B as loadRegistry, C as getSessionGeneration, D as getSpecialistStatus, E as getSpecialistState, F as isInitialized, G as sendFeedbackToAgent, H as recordWake, I as isRunning, J as signalSpecialistCompletion, K as setCurrentRun, L as listProjectsWithSpecialists, M as initializeEnabledSpecialists, N as initializeSpecialist, O as getTmuxSessionName, P as isEnabled, Q as terminateSpecialist, R as listSessionFiles, S as getSessionFilePath, T as getSpecialistMetadata, U as resumeGracePeriod, V as pauseGracePeriod, W as saveRegistry, X as startGracePeriod, Y as spawnEphemeralSpecialist, Z as submitToSpecialistQueue, _ as getGracePeriodState, a as completeSpecialistTask, at as wakeSpecialistWithTask, b as getProjectSpecialistDir, c as enableSpecialist, d as findSessionFile, et as updateProjectSpecialistMetadata, f as getAllProjectSpecialistStatuses, g as getFeedbackStats, h as getEnabledSpecialists, i as clearSessionId, it as wakeSpecialistOrQueue, j as init_specialists, k as incrementProjectRunCount, l as ensureProjectSpecialistDir, m as getAllSpecialists, n as bumpSessionGeneration, nt as updateSpecialistMetadata, o as countContextTokens, p as getAllSpecialistStatus, q as setSessionId, r as checkSpecialistQueue, rt as wakeSpecialist, s as disableSpecialist, t as buildTestAgentPromptContent, tt as updateRunStatus, u as exitGracePeriod, v as getNextSpecialistTask, w as getSessionId, x as getProjectSpecialistMetadata, y as getPendingFeedback, z as listSpecialistsForProject } from "./specialists-
|
|
1
|
+
import { $ as updateContextTokens, A as initSpecialistsDirectory, B as loadRegistry, C as getSessionGeneration, D as getSpecialistStatus, E as getSpecialistState, F as isInitialized, G as sendFeedbackToAgent, H as recordWake, I as isRunning, J as signalSpecialistCompletion, K as setCurrentRun, L as listProjectsWithSpecialists, M as initializeEnabledSpecialists, N as initializeSpecialist, O as getTmuxSessionName, P as isEnabled, Q as terminateSpecialist, R as listSessionFiles, S as getSessionFilePath, T as getSpecialistMetadata, U as resumeGracePeriod, V as pauseGracePeriod, W as saveRegistry, X as startGracePeriod, Y as spawnEphemeralSpecialist, Z as submitToSpecialistQueue, _ as getGracePeriodState, a as completeSpecialistTask, at as wakeSpecialistWithTask, b as getProjectSpecialistDir, c as enableSpecialist, d as findSessionFile, et as updateProjectSpecialistMetadata, f as getAllProjectSpecialistStatuses, g as getFeedbackStats, h as getEnabledSpecialists, i as clearSessionId, it as wakeSpecialistOrQueue, j as init_specialists, k as incrementProjectRunCount, l as ensureProjectSpecialistDir, m as getAllSpecialists, n as bumpSessionGeneration, nt as updateSpecialistMetadata, o as countContextTokens, p as getAllSpecialistStatus, q as setSessionId, r as checkSpecialistQueue, rt as wakeSpecialist, s as disableSpecialist, t as buildTestAgentPromptContent, tt as updateRunStatus, u as exitGracePeriod, v as getNextSpecialistTask, w as getSessionId, x as getProjectSpecialistMetadata, y as getPendingFeedback, z as listSpecialistsForProject } from "./specialists-C6s3U6tX.js";
|
|
2
2
|
init_specialists();
|
|
3
3
|
export { buildTestAgentPromptContent, bumpSessionGeneration, checkSpecialistQueue, clearSessionId, completeSpecialistTask, countContextTokens, disableSpecialist, enableSpecialist, ensureProjectSpecialistDir, exitGracePeriod, findSessionFile, getAllProjectSpecialistStatuses, getAllSpecialistStatus, getAllSpecialists, getEnabledSpecialists, getFeedbackStats, getGracePeriodState, getNextSpecialistTask, getPendingFeedback, getProjectSpecialistDir, getProjectSpecialistMetadata, getSessionFilePath, getSessionGeneration, getSessionId, getSpecialistMetadata, getSpecialistState, getSpecialistStatus, getTmuxSessionName, incrementProjectRunCount, initSpecialistsDirectory, initializeEnabledSpecialists, initializeSpecialist, isEnabled, isInitialized, isRunning, listProjectsWithSpecialists, listSessionFiles, listSpecialistsForProject, loadRegistry, pauseGracePeriod, recordWake, resumeGracePeriod, saveRegistry, sendFeedbackToAgent, setCurrentRun, setSessionId, signalSpecialistCompletion, spawnEphemeralSpecialist, startGracePeriod, submitToSpecialistQueue, terminateSpecialist, updateContextTokens, updateProjectSpecialistMetadata, updateRunStatus, updateSpecialistMetadata, wakeSpecialist, wakeSpecialistOrQueue, wakeSpecialistWithTask };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { p as init_projects, y as resolveProjectFromIssue } from "./projects-Cq3TWdPS.js";
|
|
2
|
-
import { i as init_review_status, s as setReviewStatus } from "./review-status-
|
|
3
|
-
import { Y as spawnEphemeralSpecialist, Z as submitToSpecialistQueue, j as init_specialists } from "./specialists-
|
|
2
|
+
import { i as init_review_status, s as setReviewStatus } from "./review-status-CV55Tl-n.js";
|
|
3
|
+
import { Y as spawnEphemeralSpecialist, Z as submitToSpecialistQueue, j as init_specialists } from "./specialists-C6s3U6tX.js";
|
|
4
4
|
//#region ../../lib/cloister/test-agent-queue.ts
|
|
5
5
|
init_review_status();
|
|
6
6
|
init_specialists();
|
|
@@ -98,4 +98,4 @@ async function autoQueueTestAgentAndNotify(issueId, workspace, branch, notifyAge
|
|
|
98
98
|
//#endregion
|
|
99
99
|
export { autoQueueTestAgentAndNotify };
|
|
100
100
|
|
|
101
|
-
//# sourceMappingURL=test-agent-queue-
|
|
101
|
+
//# sourceMappingURL=test-agent-queue-tqI4VDsu.js.map
|