panopticon-cli 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cli/index.js +19 -19
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/dashboard/{event-store-BtuZCLHu.js → event-store-D7kLBd07.js} +1 -1
  4. package/dist/dashboard/{event-store-OS5jH3Eu.js → event-store-O9q0Gweh.js} +2 -2
  5. package/dist/dashboard/{event-store-OS5jH3Eu.js.map → event-store-O9q0Gweh.js.map} +1 -1
  6. package/dist/dashboard/{inspect-agent-CwT4mrvV.js → inspect-agent-B57kGDUV.js} +3 -3
  7. package/dist/dashboard/{inspect-agent-CwT4mrvV.js.map → inspect-agent-B57kGDUV.js.map} +1 -1
  8. package/dist/dashboard/{issue-service-singleton-z78bbRiO.js → issue-service-singleton-DQK42EqH.js} +1 -1
  9. package/dist/dashboard/{issue-service-singleton-0n9hcF71.js → issue-service-singleton-sb2HkB9f.js} +2 -2
  10. package/dist/dashboard/{issue-service-singleton-0n9hcF71.js.map → issue-service-singleton-sb2HkB9f.js.map} +1 -1
  11. package/dist/dashboard/{lifecycle-B6d3AE3n.js → lifecycle-ZTYdrr2O.js} +1 -1
  12. package/dist/dashboard/{merge-agent-DaIEvGJG.js → merge-agent-GLtMEsTu.js} +1 -1
  13. package/dist/dashboard/{merge-agent-CmqR1MFf.js → merge-agent-twroFuAh.js} +2 -2
  14. package/dist/dashboard/{merge-agent-CmqR1MFf.js.map → merge-agent-twroFuAh.js.map} +1 -1
  15. package/dist/dashboard/{projection-cache-Bkzs_90o.js → projection-cache-DQ9zegkK.js} +10 -10
  16. package/dist/dashboard/projection-cache-DQ9zegkK.js.map +1 -0
  17. package/dist/dashboard/public/assets/{dist-D-q87oB4.js → dist-C2sRcZJv.js} +1 -1
  18. package/dist/dashboard/public/assets/{index--G6_upSx.js → index-BCLmEMRf.js} +41 -41
  19. package/dist/dashboard/public/assets/index-BEdq7CFf.css +1 -0
  20. package/dist/dashboard/public/index.html +2 -2
  21. package/dist/dashboard/{review-status-DqJZDthU.js → review-status-CK3eBGyb.js} +1 -1
  22. package/dist/dashboard/{review-status-LQATWF6L.js → review-status-CV55Tl-n.js} +2 -2
  23. package/dist/dashboard/{review-status-LQATWF6L.js.map → review-status-CV55Tl-n.js.map} +1 -1
  24. package/dist/dashboard/server.js +85 -85
  25. package/dist/dashboard/server.js.map +1 -1
  26. package/dist/dashboard/{specialist-context-IX8ZZBxy.js → specialist-context-ColzlmGE.js} +2 -2
  27. package/dist/dashboard/{specialist-context-IX8ZZBxy.js.map → specialist-context-ColzlmGE.js.map} +1 -1
  28. package/dist/dashboard/{specialist-logs-BvOQ3XPt.js → specialist-logs-BhmDpFIq.js} +1 -1
  29. package/dist/dashboard/{specialists-C7Fyhq_j.js → specialists-C6s3U6tX.js} +21 -7
  30. package/dist/dashboard/specialists-C6s3U6tX.js.map +1 -0
  31. package/dist/dashboard/{specialists-B4aDa5xP.js → specialists-Cny632-T.js} +1 -1
  32. package/dist/dashboard/{test-agent-queue-C0WrVdrJ.js → test-agent-queue-tqI4VDsu.js} +3 -3
  33. package/dist/dashboard/{test-agent-queue-C0WrVdrJ.js.map → test-agent-queue-tqI4VDsu.js.map} +1 -1
  34. package/dist/dashboard/workflows-B2ARUpOa.js +2 -0
  35. package/dist/dashboard/{workflows-Cj6tzch6.js → workflows-N1UTipYl.js} +3 -3
  36. package/dist/dashboard/{workflows-Cj6tzch6.js.map → workflows-N1UTipYl.js.map} +1 -1
  37. package/dist/{merge-agent-BCPyotWG.js → merge-agent-VQH9z9t8.js} +2 -2
  38. package/dist/{merge-agent-BCPyotWG.js.map → merge-agent-VQH9z9t8.js.map} +1 -1
  39. package/dist/{review-status-p_HOugvo.js → review-status-2TdtHNcs.js} +1 -1
  40. package/dist/{review-status-BbY22dtx.js → review-status-Bm1bWNEa.js} +2 -2
  41. package/dist/{review-status-BbY22dtx.js.map → review-status-Bm1bWNEa.js.map} +1 -1
  42. package/dist/{specialist-context-CRBBW-z5.js → specialist-context-BdNFsfMG.js} +2 -2
  43. package/dist/{specialist-context-CRBBW-z5.js.map → specialist-context-BdNFsfMG.js.map} +1 -1
  44. package/dist/{specialist-logs-m0UvPm3F.js → specialist-logs-CLztE_bE.js} +1 -1
  45. package/dist/{specialists-ldNesMhg.js → specialists-DEKqgkxp.js} +21 -7
  46. package/dist/specialists-DEKqgkxp.js.map +1 -0
  47. package/dist/{specialists-DXDDLqoY.js → specialists-aUoUVWsN.js} +1 -1
  48. package/package.json +1 -1
  49. package/scripts/record-cost-event.js +15 -0
  50. package/scripts/record-cost-event.js.map +1 -1
  51. package/scripts/record-cost-event.ts +2 -0
  52. package/scripts/work-agent-stop-hook +26 -0
  53. package/dist/dashboard/projection-cache-Bkzs_90o.js.map +0 -1
  54. package/dist/dashboard/public/assets/index-CjpnhB4Q.css +0 -1
  55. package/dist/dashboard/specialists-C7Fyhq_j.js.map +0 -1
  56. package/dist/dashboard/workflows-BsUDQntr.js +0 -2
  57. package/dist/specialists-ldNesMhg.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"issue-service-singleton-0n9hcF71.js","names":[],"sources":["../../src/core/state-mapping.ts","../../src/dashboard/server/services/cache-service.ts","../../src/dashboard/server/services/issue-data-service.ts","../../src/dashboard/server/services/issue-service-singleton.ts"],"sourcesContent":["/**\n * Panopticon State Mapping System\n *\n * Maps Panopticon's canonical workflow states to various issue tracker states.\n * Supports auto-creation of missing states where possible, and label fallbacks.\n */\n\n// Panopticon's canonical workflow states\nexport type CanonicalState =\n | 'backlog'\n | 'todo'\n | 'in_progress'\n | 'in_review'\n | 'done'\n | 'canceled';\n\n// State type categories (Linear terminology)\nexport type StateType = 'backlog' | 'unstarted' | 'started' | 'completed' | 'canceled';\n\n// Canonical state definitions\nexport interface CanonicalStateDefinition {\n name: CanonicalState;\n type: StateType;\n description: string;\n color: string;\n}\n\nexport const CANONICAL_STATES: CanonicalStateDefinition[] = [\n { name: 'backlog', type: 'backlog', description: 'Ideas and future work', color: '#6b7280' },\n { name: 'todo', type: 'unstarted', description: 'Prioritized and ready', color: '#3b82f6' },\n { name: 'in_progress', type: 'started', description: 'Agent executing', color: '#eab308' },\n { name: 'in_review', type: 'started', description: 'PR awaiting review', color: '#ec4899' },\n { name: 'done', type: 'completed', description: 'Work complete', color: '#22c55e' },\n { name: 'canceled', type: 'canceled', description: \"Won't do\", color: '#71717a' },\n];\n\nexport const STATE_TYPE_MAP: Record<CanonicalState, StateType> = {\n backlog: 'backlog',\n todo: 'unstarted',\n in_progress: 'started',\n in_review: 'started',\n done: 'completed',\n canceled: 'canceled',\n};\n\n// Strategy for handling missing states\nexport type MissingStateStrategy = 'auto_create' | 'error';\n\n// Auto-create configuration for a specific state\nexport interface AutoCreateStateConfig {\n type: StateType;\n color: string;\n positionAfter?: string; // State name to position after\n}\n\n// Tracker-specific state mapping\nexport interface TrackerStateMapping {\n stateMap: Record<CanonicalState, string | { status: string; label?: string | null }>;\n missingStateStrategy: MissingStateStrategy;\n autoCreateConfig?: Record<string, AutoCreateStateConfig>;\n // Tracker-specific options\n projectBoard?: {\n enabled: boolean;\n name: string;\n columnMap: Record<CanonicalState, string>;\n };\n}\n\n// Supported trackers\nexport type SupportedTracker = 'linear' | 'github' | 'gitlab' | 'jira' | 'trello';\n\n// Full state mapping configuration\nexport interface StateMappingConfig {\n canonicalStates: CanonicalStateDefinition[];\n trackers: Record<SupportedTracker, TrackerStateMapping>;\n}\n\n// Default state mappings for supported trackers\nexport const DEFAULT_STATE_MAPPINGS: StateMappingConfig = {\n canonicalStates: CANONICAL_STATES,\n trackers: {\n linear: {\n stateMap: {\n backlog: 'Backlog',\n todo: 'Todo',\n in_progress: 'In Progress',\n in_review: 'In Review',\n done: 'Done',\n canceled: 'Canceled',\n },\n missingStateStrategy: 'auto_create',\n },\n\n github: {\n stateMap: {\n backlog: { status: 'open', label: null },\n todo: { status: 'open', label: null },\n in_progress: { status: 'open', label: 'in-progress' },\n in_review: { status: 'open', label: 'in-review' },\n done: { status: 'closed', label: null },\n canceled: { status: 'closed', label: 'wontfix' },\n },\n missingStateStrategy: 'error',\n projectBoard: {\n enabled: true,\n name: 'Panopticon',\n columnMap: {\n backlog: 'Backlog',\n todo: 'Todo',\n in_progress: 'In Progress',\n in_review: 'Review',\n done: 'Done',\n canceled: 'Done',\n },\n },\n },\n\n gitlab: {\n stateMap: {\n backlog: { status: 'opened', label: 'backlog' },\n todo: { status: 'opened', label: 'todo' },\n in_progress: { status: 'opened', label: 'in-progress' },\n in_review: { status: 'opened', label: 'in-review' },\n done: { status: 'closed', label: null },\n canceled: { status: 'closed', label: 'wontfix' },\n },\n missingStateStrategy: 'error',\n },\n\n jira: {\n stateMap: {\n backlog: 'Backlog',\n todo: 'To Do',\n in_progress: 'In Progress',\n in_review: 'In Review',\n done: 'Done',\n canceled: 'Canceled',\n },\n missingStateStrategy: 'error', // Can't auto-create in Jira\n },\n\n trello: {\n stateMap: {\n backlog: 'Backlog',\n todo: 'To Do',\n in_progress: 'Doing',\n in_review: 'Review',\n done: 'Done',\n canceled: 'Archived',\n },\n missingStateStrategy: 'auto_create', // Trello lists are easy to create\n },\n },\n};\n\n// Virtual state tracking for issues\nexport interface PanopticonIssueState {\n issueId: string;\n panopticonState: CanonicalState;\n trackerState: string;\n lastSyncedAt: string;\n syncStatus: 'synced' | 'pending' | 'conflict';\n fallbacksUsed: string[];\n}\n\n// State transition result\nexport interface StateTransitionResult {\n success: boolean;\n panopticonState: CanonicalState;\n trackerState: string;\n fallbacksUsed: string[];\n warnings: string[];\n error?: string;\n}\n\n// Tracker state check result\nexport interface TrackerStateCheckResult {\n tracker: SupportedTracker;\n team?: string;\n existingStates: string[];\n missingStates: CanonicalState[];\n recommendations: {\n state: CanonicalState;\n action: 'create' | 'skip';\n details: string;\n }[];\n}\n\n/**\n * Map a tracker state name to a canonical state\n */\nexport function trackerStateToCanonical(\n trackerState: string,\n tracker: SupportedTracker = 'linear'\n): CanonicalState {\n const mapping = DEFAULT_STATE_MAPPINGS.trackers[tracker];\n if (!mapping) return 'backlog';\n\n // Check direct state map\n for (const [canonical, mapped] of Object.entries(mapping.stateMap)) {\n if (typeof mapped === 'string') {\n if (mapped.toLowerCase() === trackerState.toLowerCase()) {\n return canonical as CanonicalState;\n }\n } else if (mapped.label === trackerState.toLowerCase()) {\n return canonical as CanonicalState;\n }\n }\n\n // Fallback heuristics\n const lower = trackerState.toLowerCase();\n if (lower.includes('backlog') || lower.includes('triage')) return 'backlog';\n if (lower.includes('todo') || lower.includes('ready') || lower.includes('unstarted')) return 'todo';\n if (lower.includes('progress') || lower.includes('started') || lower.includes('active')) return 'in_progress';\n if (lower.includes('review') || lower.includes('qa') || lower.includes('testing')) return 'in_review';\n if (lower.includes('done') || lower.includes('complete') || lower.includes('closed')) return 'done';\n if (lower.includes('cancel') || lower.includes('duplicate') || lower.includes('wontfix')) return 'canceled';\n\n return 'backlog';\n}\n\n/**\n * Get the tracker state name for a canonical state\n */\nexport function canonicalToTrackerState(\n canonicalState: CanonicalState,\n tracker: SupportedTracker = 'linear'\n): string {\n const mapping = DEFAULT_STATE_MAPPINGS.trackers[tracker];\n if (!mapping) return canonicalState;\n\n const mapped = mapping.stateMap[canonicalState];\n if (typeof mapped === 'string') {\n return mapped;\n } else {\n return mapped.label || mapped.status;\n }\n}\n\n/**\n * Workflow labels that should be removed during state transitions\n */\nexport const WORKFLOW_LABELS = [\n 'in-progress',\n 'in progress',\n 'in-review',\n 'in review',\n 'review-ready',\n 'review ready',\n 'planned',\n 'planning',\n 'closed-out',\n];\n\n/**\n * Get the target workflow label for a canonical state\n */\nexport function getStateLabel(state: CanonicalState): string | null {\n switch (state) {\n case 'in_progress':\n return 'in-progress';\n case 'in_review':\n return 'in-review';\n case 'done':\n return 'done';\n default:\n return null;\n }\n}\n\n/**\n * Map GitHub issue state + labels to canonical state.\n * This function handles the GitHub-specific mapping where issues have both\n * a state (open/closed) and workflow labels.\n *\n * @param state - GitHub issue state ('open' or 'closed')\n * @param labels - Array of label names on the issue\n * @returns Canonical state string\n */\nexport function mapGitHubStateToCanonical(state: string, labels: string[]): CanonicalState {\n // Handle both API lowercase and gh CLI uppercase\n const stateLower = state.toLowerCase();\n\n // Closed issues are always done (regardless of labels)\n if (stateLower === 'closed') {\n return 'done';\n }\n\n // For open issues, check labels for workflow state\n // Order matters: more progressed states take precedence\n const labelNames = labels.map(l => l.toLowerCase());\n\n // Most progressed states first\n // merged = postMergeLifecycle applied label; issue may still be open if auto-close failed\n // needs-close-out = merged work reopened for close-out ceremony\n // Both belong in the done column — awaiting close-out ceremony\n if (labelNames.some(l => l === 'merged' || l === 'needs-close-out')) {\n return 'done';\n }\n // \"done\" label on OPEN issues = work complete, pending merge/closure → in_review\n // (actual \"done\" status only for CLOSED issues, handled above)\n if (labelNames.some(l => l === 'done' || l.includes('completed'))) {\n return 'in_review';\n }\n if (labelNames.some(l => l.includes('in review') || l.includes('in-review') || l.includes('review') || l.includes('qa'))) {\n return 'in_review';\n }\n if (labelNames.some(l => l.includes('in progress') || l.includes('in-progress') || l.includes('wip'))) {\n return 'in_progress';\n }\n // Early workflow stages\n if (labelNames.some(l => l.includes('backlog') || l.includes('icebox'))) {\n return 'backlog';\n }\n if (labelNames.some(l => l.includes('todo') || l.includes('ready'))) {\n return 'todo';\n }\n\n // Default open issues to todo\n return 'todo';\n}\n\n/**\n * Get the target state name for a Linear team.\n * Uses the DEFAULT_STATE_MAPPINGS to find the Linear state name.\n *\n * @param canonicalState - The canonical state to map\n * @returns The Linear state name (e.g., 'In Review')\n */\nexport function getLinearStateName(canonicalState: CanonicalState): string {\n const mapping = DEFAULT_STATE_MAPPINGS.trackers.linear;\n const mapped = mapping.stateMap[canonicalState];\n return typeof mapped === 'string' ? mapped : canonicalState;\n}\n\n/**\n * Find a Linear workflow state by name in a team.\n * Returns null if not found.\n *\n * @param states - Array of Linear workflow states from the SDK\n * @param stateName - The state name to find\n * @returns The matching state or null\n */\nexport function findLinearStateByName(states: any[], stateName: string): any | null {\n // Try exact match first\n const exactMatch = states.find(s => s.name === stateName);\n if (exactMatch) return exactMatch;\n\n // Try case-insensitive match\n const lowerName = stateName.toLowerCase();\n return states.find(s => s.name.toLowerCase() === lowerName) || null;\n}\n\n/**\n * Clean up workflow labels during state transitions.\n * Removes all workflow labels, then adds the label matching the target state (if any).\n *\n * @param currentLabels - Array of current label names\n * @param targetState - The canonical state being transitioned to\n * @returns Array of label names after cleanup\n */\nexport function cleanupWorkflowLabels(\n currentLabels: string[],\n targetState: CanonicalState\n): string[] {\n // Remove all workflow labels\n const cleaned = currentLabels.filter(\n label => !WORKFLOW_LABELS.includes(label.toLowerCase())\n );\n\n // Add the label matching the target state (if applicable)\n const targetLabel = getStateLabel(targetState);\n if (targetLabel && !cleaned.includes(targetLabel)) {\n cleaned.push(targetLabel);\n }\n\n return cleaned;\n}\n","/**\n * CacheService — Two-layer cache for dashboard API responses\n *\n * L1: In-memory Map (hot, 10s TTL, 50 entries max)\n * L2: SQLite (persistent, survives restarts)\n *\n * Stores API responses per tracker with ETag support (GitHub REST 304s are FREE).\n * Tracks rate limits per tracker for adaptive backoff.\n */\n\nimport type Database from 'better-sqlite3';\nimport { createRequire } from 'module';\nimport { join } from 'path';\nimport { existsSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\n\ndeclare const Bun: unknown;\nconst _require = createRequire(import.meta.url);\n\nfunction openSqliteDb(dbPath: string): Database.Database {\n if (typeof Bun !== 'undefined') {\n const { Database: BunDatabase } = _require('bun:sqlite') as { Database: new (path: string) => any };\n const bunDb = new BunDatabase(dbPath);\n bunDb.pragma = function (sql: string, options?: { simple?: boolean }): any {\n if (options?.simple) {\n const key = sql.trim();\n const row = bunDb.query(`PRAGMA ${key}`).get() as Record<string, unknown> | null;\n return row?.[key] ?? null;\n }\n bunDb.exec(`PRAGMA ${sql}`);\n return undefined;\n };\n return bunDb as Database.Database;\n }\n const BetterSqlite3 = _require('better-sqlite3');\n return new BetterSqlite3(dbPath) as Database.Database;\n}\n\nconst PANOPTICON_HOME = process.env.PANOPTICON_HOME || join(homedir(), '.panopticon');\nconst CACHE_DB_PATH = join(PANOPTICON_HOME, 'cache.db');\n\n// Default TTLs per tracker (seconds)\nexport const DEFAULT_TTLS: Record<string, number> = {\n github: 60,\n linear: 30,\n rally: 120,\n};\n\n// L1 in-memory cache entry\ninterface L1Entry {\n data: any;\n etag?: string;\n lastModified?: string;\n lastFetchedAt: string;\n lastUpdatedAt: string;\n ttlSeconds: number;\n insertedAt: number; // Date.now()\n}\n\n// Rate limit info\nexport interface RateLimitInfo {\n remaining: number;\n total: number;\n resetAt: string;\n}\n\n// Cache entry returned from get()\nexport interface CacheEntry {\n data: any;\n etag?: string;\n lastModified?: string;\n lastFetchedAt: string;\n lastUpdatedAt: string;\n ttlSeconds: number;\n}\n\nexport class CacheService {\n private db: Database.Database;\n private l1: Map<string, L1Entry> = new Map();\n private readonly l1MaxEntries = 50;\n private readonly l1TtlMs = 10_000; // 10 seconds\n\n constructor() {\n if (!existsSync(PANOPTICON_HOME)) {\n mkdirSync(PANOPTICON_HOME, { recursive: true });\n }\n\n this.db = openSqliteDb(CACHE_DB_PATH);\n this.db.pragma('journal_mode = WAL');\n this.createSchema();\n }\n\n private createSchema(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS api_cache (\n tracker TEXT NOT NULL,\n cache_key TEXT NOT NULL,\n data TEXT NOT NULL,\n etag TEXT,\n last_modified TEXT,\n last_fetched_at TEXT NOT NULL,\n last_updated_at TEXT NOT NULL,\n ttl_seconds INTEGER NOT NULL,\n PRIMARY KEY (tracker, cache_key)\n );\n\n CREATE TABLE IF NOT EXISTS rate_limits (\n tracker TEXT PRIMARY KEY,\n remaining INTEGER,\n total INTEGER,\n reset_at TEXT,\n updated_at TEXT NOT NULL\n );\n `);\n }\n\n /**\n * Get a cached entry. Checks L1 first, then L2 (SQLite).\n * Returns null if no entry or if expired.\n */\n get(tracker: string, cacheKey: string): CacheEntry | null {\n const compositeKey = `${tracker}:${cacheKey}`;\n\n // L1 check\n const l1Entry = this.l1.get(compositeKey);\n if (l1Entry && Date.now() - l1Entry.insertedAt < this.l1TtlMs) {\n return {\n data: l1Entry.data,\n etag: l1Entry.etag,\n lastModified: l1Entry.lastModified,\n lastFetchedAt: l1Entry.lastFetchedAt,\n lastUpdatedAt: l1Entry.lastUpdatedAt,\n ttlSeconds: l1Entry.ttlSeconds,\n };\n }\n\n // L1 miss or expired — check L2\n const stmt = this.db.prepare(`\n SELECT data, etag, last_modified, last_fetched_at, last_updated_at, ttl_seconds\n FROM api_cache\n WHERE tracker = ? AND cache_key = ?\n `);\n\n const row = stmt.get(tracker, cacheKey) as any;\n if (!row) return null;\n\n const entry: CacheEntry = {\n data: JSON.parse(row.data),\n etag: row.etag || undefined,\n lastModified: row.last_modified || undefined,\n lastFetchedAt: row.last_fetched_at,\n lastUpdatedAt: row.last_updated_at,\n ttlSeconds: row.ttl_seconds,\n };\n\n // Promote to L1\n this.setL1(compositeKey, {\n ...entry,\n insertedAt: Date.now(),\n });\n\n return entry;\n }\n\n /**\n * Get cached entry even if stale (for serving while re-fetching).\n */\n getStale(tracker: string, cacheKey: string): CacheEntry | null {\n const compositeKey = `${tracker}:${cacheKey}`;\n\n // Check L1 (even if expired)\n const l1Entry = this.l1.get(compositeKey);\n if (l1Entry) {\n return {\n data: l1Entry.data,\n etag: l1Entry.etag,\n lastModified: l1Entry.lastModified,\n lastFetchedAt: l1Entry.lastFetchedAt,\n lastUpdatedAt: l1Entry.lastUpdatedAt,\n ttlSeconds: l1Entry.ttlSeconds,\n };\n }\n\n // L2\n const stmt = this.db.prepare(`\n SELECT data, etag, last_modified, last_fetched_at, last_updated_at, ttl_seconds\n FROM api_cache\n WHERE tracker = ? AND cache_key = ?\n `);\n\n const row = stmt.get(tracker, cacheKey) as any;\n if (!row) return null;\n\n return {\n data: JSON.parse(row.data),\n etag: row.etag || undefined,\n lastModified: row.last_modified || undefined,\n lastFetchedAt: row.last_fetched_at,\n lastUpdatedAt: row.last_updated_at,\n ttlSeconds: row.ttl_seconds,\n };\n }\n\n /**\n * Store data in both L1 and L2 cache.\n */\n set(\n tracker: string,\n cacheKey: string,\n data: any,\n options?: {\n etag?: string;\n lastModified?: string;\n lastUpdatedAt?: string;\n ttlSeconds?: number;\n }\n ): void {\n const now = new Date().toISOString();\n const ttl = options?.ttlSeconds ?? DEFAULT_TTLS[tracker] ?? 60;\n const lastUpdatedAt = options?.lastUpdatedAt ?? now;\n const compositeKey = `${tracker}:${cacheKey}`;\n\n // L2 (SQLite) — upsert\n const stmt = this.db.prepare(`\n INSERT INTO api_cache (tracker, cache_key, data, etag, last_modified, last_fetched_at, last_updated_at, ttl_seconds)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(tracker, cache_key) DO UPDATE SET\n data = excluded.data,\n etag = excluded.etag,\n last_modified = excluded.last_modified,\n last_fetched_at = excluded.last_fetched_at,\n last_updated_at = excluded.last_updated_at,\n ttl_seconds = excluded.ttl_seconds\n `);\n\n stmt.run(\n tracker,\n cacheKey,\n JSON.stringify(data),\n options?.etag ?? null,\n options?.lastModified ?? null,\n now,\n lastUpdatedAt,\n ttl\n );\n\n // L1\n this.setL1(compositeKey, {\n data,\n etag: options?.etag,\n lastModified: options?.lastModified,\n lastFetchedAt: now,\n lastUpdatedAt,\n ttlSeconds: ttl,\n insertedAt: Date.now(),\n });\n }\n\n /**\n * Check if a cache entry is stale (past its TTL).\n */\n isStale(tracker: string, cacheKey: string): boolean {\n const entry = this.get(tracker, cacheKey);\n if (!entry) return true;\n\n const fetchedAt = new Date(entry.lastFetchedAt).getTime();\n const age = (Date.now() - fetchedAt) / 1000;\n return age > entry.ttlSeconds;\n }\n\n /**\n * Get the stored ETag for a tracker/key combo (for conditional requests).\n */\n getEtag(tracker: string, cacheKey: string): string | undefined {\n // Check L1 first\n const compositeKey = `${tracker}:${cacheKey}`;\n const l1Entry = this.l1.get(compositeKey);\n if (l1Entry?.etag) return l1Entry.etag;\n\n // Check L2\n const stmt = this.db.prepare(`\n SELECT etag FROM api_cache WHERE tracker = ? AND cache_key = ?\n `);\n const row = stmt.get(tracker, cacheKey) as any;\n return row?.etag || undefined;\n }\n\n /**\n * Invalidate cache for a specific tracker (all keys).\n */\n invalidate(tracker: string): void {\n // L1 — remove all entries for this tracker\n for (const key of this.l1.keys()) {\n if (key.startsWith(`${tracker}:`)) {\n this.l1.delete(key);\n }\n }\n\n // L2\n this.db.prepare('DELETE FROM api_cache WHERE tracker = ?').run(tracker);\n }\n\n /**\n * Invalidate a specific cache key.\n */\n invalidateKey(tracker: string, cacheKey: string): void {\n this.l1.delete(`${tracker}:${cacheKey}`);\n this.db.prepare('DELETE FROM api_cache WHERE tracker = ? AND cache_key = ?').run(tracker, cacheKey);\n }\n\n // --- Rate limit tracking ---\n\n /**\n * Update rate limit info for a tracker.\n */\n updateRateLimit(tracker: string, info: RateLimitInfo): void {\n const stmt = this.db.prepare(`\n INSERT INTO rate_limits (tracker, remaining, total, reset_at, updated_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(tracker) DO UPDATE SET\n remaining = excluded.remaining,\n total = excluded.total,\n reset_at = excluded.reset_at,\n updated_at = excluded.updated_at\n `);\n stmt.run(tracker, info.remaining, info.total, info.resetAt, new Date().toISOString());\n }\n\n /**\n * Get rate limit info for a tracker.\n */\n getRateLimit(tracker: string): RateLimitInfo | null {\n const stmt = this.db.prepare(`\n SELECT remaining, total, reset_at FROM rate_limits WHERE tracker = ?\n `);\n const row = stmt.get(tracker) as any;\n if (!row) return null;\n\n return {\n remaining: row.remaining,\n total: row.total,\n resetAt: row.reset_at,\n };\n }\n\n /**\n * Check if we should back off requests for a tracker.\n * Returns true if remaining < 10% of total.\n */\n shouldBackoff(tracker: string): boolean {\n const limit = this.getRateLimit(tracker);\n if (!limit) return false;\n\n // If reset time has passed, no need to back off\n if (new Date(limit.resetAt).getTime() < Date.now()) return false;\n\n return limit.remaining < limit.total * 0.1;\n }\n\n /**\n * Calculate adaptive backoff delay in ms.\n * Returns 0 if no backoff needed.\n */\n getBackoffMs(tracker: string, baseIntervalMs: number): number {\n const limit = this.getRateLimit(tracker);\n if (!limit) return 0;\n\n // If reset time has passed, no backoff\n if (new Date(limit.resetAt).getTime() < Date.now()) return 0;\n\n const ratioRemaining = limit.remaining / limit.total;\n\n if (ratioRemaining > 0.5) return 0; // >50% remaining: no backoff\n if (ratioRemaining > 0.25) return baseIntervalMs; // 25-50%: 2x interval\n if (ratioRemaining > 0.1) return baseIntervalMs * 4; // 10-25%: 5x interval\n return baseIntervalMs * 9; // <10%: 10x interval\n }\n\n /**\n * Get cache status for all trackers (for diagnostics endpoint).\n */\n getStatus(): Record<string, {\n remaining: number | null;\n total: number | null;\n lastFetched: string | null;\n cacheKeys: number;\n }> {\n const result: Record<string, any> = {};\n\n for (const tracker of ['github', 'linear', 'rally']) {\n const limit = this.getRateLimit(tracker);\n const countStmt = this.db.prepare(\n 'SELECT COUNT(*) as cnt, MAX(last_fetched_at) as latest FROM api_cache WHERE tracker = ?'\n );\n const row = countStmt.get(tracker) as any;\n\n result[tracker] = {\n remaining: limit?.remaining ?? null,\n total: limit?.total ?? null,\n lastFetched: row?.latest ?? null,\n cacheKeys: row?.cnt ?? 0,\n };\n }\n\n return result;\n }\n\n /**\n * Close the database connection.\n */\n close(): void {\n this.l1.clear();\n this.db.close();\n }\n\n // --- L1 helpers ---\n\n private setL1(compositeKey: string, entry: L1Entry): void {\n // Evict oldest if at capacity\n if (this.l1.size >= this.l1MaxEntries && !this.l1.has(compositeKey)) {\n const oldestKey = this.l1.keys().next().value;\n if (oldestKey) this.l1.delete(oldestKey);\n }\n this.l1.set(compositeKey, entry);\n }\n}\n","/**\n * IssueDataService — Central orchestrator for issue data\n *\n * Replaces the inline fetching in /api/issues with:\n * - Background polling per tracker on independent timers\n * - GitHub REST + ETags (304s are FREE, don't count against rate limit)\n * - Linear incremental fetching via updatedAt filter\n * - Rally TTL-based caching\n * - Change detection + event store push (via onIssuesChanged callback)\n * - Adaptive backoff on rate limit pressure\n * - Instant serve from cache (sub-100ms dashboard loads)\n */\n\nimport { Octokit } from '@octokit/rest';\nimport { mapGitHubStateToCanonical } from '../../../core/state-mapping.js';\nimport { CacheService, DEFAULT_TTLS } from './cache-service.js';\nimport { getGitHubConfig, getLinearApiKey, getRallyConfig, validateRallyConfig } from './tracker-config.js';\nimport type { GitHubConfig, RallyConfig } from './tracker-config.js';\nimport { loadReviewStatuses } from '../../../lib/review-status.js';\n\n/**\n * Map a raw status string to its canonical state.\n * Exported for testing.\n */\nexport function getCanonicalStatus(status: string | undefined, stateType?: string): string {\n if (!status) return 'backlog';\n const normalized = status.toLowerCase();\n // Direct backlog mappings\n if (normalized === 'backlog' || normalized === 'triage' || normalized === 'unknown') {\n return 'backlog';\n }\n // Other canonical states\n if (normalized === 'todo' || normalized === 'to do' || normalized === 'ready' || normalized === 'unstarted') {\n return 'todo';\n }\n if (normalized === 'in progress' || normalized === 'started' || normalized === 'active' || normalized === 'in planning') {\n return 'in_progress';\n }\n if (normalized === 'in review' || normalized === 'review' || normalized === 'qa' || normalized === 'testing') {\n return 'in_review';\n }\n if (normalized === 'done' || normalized === 'completed' || normalized === 'closed') {\n return 'done';\n }\n if (normalized === 'canceled' || normalized === 'cancelled' || normalized === 'duplicate' || normalized === \"won't do\" || normalized === 'wontfix') {\n return 'canceled';\n }\n // Fallback: use Linear stateType if available (handles custom status names)\n if (stateType) {\n const typeMap: Record<string, string> = {\n backlog: 'backlog',\n unstarted: 'todo',\n started: 'in_progress',\n completed: 'done',\n canceled: 'canceled',\n cancelled: 'canceled',\n };\n if (typeMap[stateType]) return typeMap[stateType];\n }\n return 'backlog'; // Default fallback\n}\n\n// Poll intervals (ms)\nconst POLL_INTERVALS = {\n github: { default: 30_000, min: 15_000, max: 300_000 },\n linear: { default: 30_000, min: 15_000, max: 300_000 },\n rally: { default: 120_000, min: 60_000, max: 600_000 },\n};\n\n// Linear full refresh interval (safety net)\nconst LINEAR_FULL_REFRESH_MS = 5 * 60 * 1000; // 5 minutes\n\ninterface TrackerState {\n timer: ReturnType<typeof setTimeout> | null;\n currentInterval: number;\n lastFetchedIssues: any[];\n lastError: string | null;\n lastFetchedAt: string | null;\n}\n\n/**\n * Map normalized IssueState (open/in_progress/closed) to canonical dashboard status.\n * The Rally tracker already normalizes raw Rally states to IssueState in rally.ts.\n */\nfunction mapRallyStateToCanonical(issueState: string): string {\n if (!issueState) return 'todo';\n const stateLower = issueState.toLowerCase();\n if (stateLower === 'in_progress') return 'in_progress';\n if (stateLower === 'closed') return 'done';\n // 'open' and anything unrecognized → 'todo'\n return 'todo';\n}\n\nexport class IssueDataService {\n private cache: CacheService;\n private trackers: Record<string, TrackerState> = {};\n private linearLastFullRefresh = 0;\n private started = false;\n private shadowStateModule: any = null;\n private _onIssuesChanged: ((issues: unknown[]) => void) | null = null;\n\n /** Register a callback invoked whenever issue data changes (PAN-433). */\n onIssuesChanged(fn: (issues: unknown[]) => void): void {\n this._onIssuesChanged = fn;\n }\n\n constructor(cache: CacheService) {\n this.cache = cache;\n\n for (const tracker of ['github', 'linear', 'rally'] as const) {\n this.trackers[tracker] = {\n timer: null,\n currentInterval: POLL_INTERVALS[tracker].default,\n lastFetchedIssues: [],\n lastError: null,\n lastFetchedAt: null,\n };\n }\n }\n\n /**\n * Start background polling. Returns immediately after loading cached data.\n * API fetches run in the background and push incremental updates.\n */\n async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n\n // Pre-load shadow state module\n await this.ensureShadowStateLoaded();\n\n // Load any cached data from SQLite so getIssues() works instantly\n this.loadCachedData();\n\n // Push snapshot immediately with stale cached data so read model has\n // something to work with before the background fetches complete.\n this.pushSnapshot();\n\n // Kick off all tracker fetches in the background — do NOT await.\n // Each poll calls pushUpdated() when done → incremental client updates.\n void Promise.allSettled([\n this.pollGitHub(),\n this.pollLinear(),\n this.pollRally(),\n ]).then(() => {\n // Final snapshot push after all initial fetches complete\n this.pushSnapshot();\n // Start recurring timers (after first fetch completes)\n this.scheduleNext('github');\n this.scheduleNext('linear');\n this.scheduleNext('rally');\n });\n }\n\n /**\n * Stop all polling timers.\n */\n stop(): void {\n this.started = false;\n for (const state of Object.values(this.trackers)) {\n if (state.timer) {\n clearTimeout(state.timer);\n state.timer = null;\n }\n }\n }\n\n /**\n * Clear all cached issue data and trigger a fresh re-fetch from all trackers.\n */\n async clearCacheAndRefresh(): Promise<void> {\n // Clear SQLite + L1 cache for all trackers\n for (const tracker of ['github', 'linear', 'rally']) {\n this.cache.invalidate(tracker);\n this.trackers[tracker].lastFetchedIssues = [];\n this.trackers[tracker].lastFetchedAt = null;\n this.trackers[tracker].lastError = null;\n }\n console.log('[IssueDataService] Cache cleared — re-fetching all trackers');\n // Re-fetch all trackers\n await Promise.allSettled([\n this.pollGitHub(),\n this.pollLinear(),\n this.pollRally(),\n ]);\n this.pushSnapshot();\n }\n\n /**\n * Look up which tracker an issue belongs to by its identifier.\n * Returns 'github' | 'linear' | 'rally' | null.\n */\n getIssueSource(identifier: string): 'github' | 'linear' | 'rally' | null {\n const id = identifier.toLowerCase();\n for (const [trackerName, state] of Object.entries(this.trackers)) {\n for (const issue of state.lastFetchedIssues) {\n if ((issue.identifier || '').toLowerCase() === id) {\n return trackerName as 'github' | 'linear' | 'rally';\n }\n }\n }\n return null;\n }\n\n /**\n * Get all issues from cache. Applies shadow state and filtering.\n * This is the hot path — must be fast.\n */\n getIssues(options?: { cycle?: string; includeCompleted?: boolean }): any[] {\n let allIssues = [\n ...this.trackers.github.lastFetchedIssues,\n ...this.trackers.linear.lastFetchedIssues,\n ...this.trackers.rally.lastFetchedIssues,\n ];\n\n // Merge shadow state (module is pre-loaded by ensureShadowStateLoaded)\n try {\n if (this.shadowStateModule) {\n const shadowStates = this.shadowStateModule.listShadowedIssues();\n const shadowMap = new Map<string, any>();\n for (const state of shadowStates) {\n shadowMap.set(state.issueId.toLowerCase(), state);\n }\n\n allIssues = allIssues.map(issue => {\n const shadowState = shadowMap.get(issue.identifier.toLowerCase());\n if (shadowState) {\n return {\n ...issue,\n shadowStatus: shadowState.shadowStatus,\n targetCanonicalState: shadowState.targetCanonicalState,\n shadowedAt: shadowState.shadowedAt,\n shadowTrackerStatus: shadowState.trackerStatus,\n };\n }\n return { ...issue, shadowStatus: null, targetCanonicalState: null };\n });\n }\n } catch (e) {\n allIssues = allIssues.map(issue => ({ ...issue, shadowStatus: null }));\n }\n\n // Show all completed issues (label-based dismissal will be added later)\n\n // Apply cycle filter using canonical status mapping\n const cycle = options?.cycle ?? 'current';\n if (cycle === 'current') {\n // Current cycle: exclude Backlog and Canceled items, only show active cycle work\n allIssues = allIssues.filter(issue => {\n const canonical = getCanonicalStatus(issue.status);\n return canonical !== 'backlog' && canonical !== 'canceled';\n });\n } else if (cycle === 'backlog') {\n // Backlog view: only show Backlog items (including Triage, Unknown)\n allIssues = allIssues.filter(issue => {\n const canonical = getCanonicalStatus(issue.status, issue.stateType);\n return canonical === 'backlog';\n });\n } else if (cycle === 'canceled') {\n // Canceled view: only show Canceled items (Canceled, Duplicate, Won't Do)\n allIssues = allIssues.filter(issue => {\n const canonical = getCanonicalStatus(issue.status);\n return canonical === 'canceled';\n });\n }\n // cycle === 'all': no additional filtering, show everything\n\n // Augment with mergeStatus from review-status (used for MERGED badge)\n try {\n const reviewStatuses = loadReviewStatuses();\n allIssues = allIssues.map(issue => {\n const key = issue.identifier?.toUpperCase();\n const rs = key ? reviewStatuses[key] : null;\n if (rs?.mergeStatus) {\n return { ...issue, mergeStatus: rs.mergeStatus };\n }\n return issue;\n });\n } catch {\n // review-status.json may not exist yet\n }\n\n // Sort by updatedAt\n allIssues.sort((a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\n );\n\n return allIssues;\n }\n\n /**\n * Invalidate a tracker's cache and trigger immediate re-poll.\n * Called after mutations (move-status, label changes, etc.)\n */\n /**\n * Immediately patch a cached issue and push the update to all clients.\n * Use this after any state mutation so the dashboard reflects the change\n * instantly without waiting for the next poll cycle.\n *\n * @param identifier - Issue identifier (e.g. \"MIN-734\", \"PAN-302\")\n * @param patch - Fields to merge into the cached issue object\n */\n patchIssue(identifier: string, patch: Record<string, any>): void {\n const id = identifier.toLowerCase();\n for (const state of Object.values(this.trackers)) {\n const idx = state.lastFetchedIssues.findIndex(\n (i: any) => (i.identifier || '').toLowerCase() === id\n );\n if (idx !== -1) {\n state.lastFetchedIssues[idx] = { ...state.lastFetchedIssues[idx], ...patch };\n this.pushUpdated();\n return;\n }\n }\n // Issue not in cache yet (e.g. just created) — trigger a full refresh instead\n const source = patch.source || 'linear';\n this.invalidateTracker(source).catch(() => {});\n }\n\n async invalidateTracker(tracker: string): Promise<void> {\n this.cache.invalidate(tracker);\n\n // Force full refresh on next poll (not incremental) so new issues\n // added to the cycle externally are discovered immediately\n if (tracker === 'linear') {\n this.linearLastFullRefresh = 0;\n }\n\n // Cancel current timer and fetch immediately\n const state = this.trackers[tracker];\n if (state?.timer) {\n clearTimeout(state.timer);\n state.timer = null;\n }\n\n switch (tracker) {\n case 'github': await this.pollGitHub(); break;\n case 'linear': await this.pollLinear(); break;\n case 'rally': await this.pollRally(); break;\n }\n\n this.pushSnapshot();\n this.scheduleNext(tracker);\n }\n\n /**\n * Get diagnostics for /api/cache-status endpoint.\n */\n getDiagnostics(): Record<string, any> {\n const result: Record<string, any> = {};\n for (const [tracker, state] of Object.entries(this.trackers)) {\n const limit = this.cache.getRateLimit(tracker);\n result[tracker] = {\n remaining: limit?.remaining ?? null,\n total: limit?.total ?? null,\n pollInterval: state.currentInterval,\n lastFetched: state.lastFetchedAt,\n lastError: state.lastError,\n issueCount: state.lastFetchedIssues.length,\n };\n }\n return result;\n }\n\n // ---------------------------------------------------------------\n // Private: polling methods\n // ---------------------------------------------------------------\n\n private scheduleNext(tracker: string): void {\n const state = this.trackers[tracker];\n if (!this.started || !state) return;\n\n // Compute effective interval with backoff\n const intervals = POLL_INTERVALS[tracker as keyof typeof POLL_INTERVALS];\n if (!intervals) return;\n\n const backoffMs = this.cache.getBackoffMs(tracker, intervals.default);\n const effectiveInterval = Math.min(\n Math.max(intervals.default + backoffMs, intervals.min),\n intervals.max\n );\n state.currentInterval = effectiveInterval;\n\n state.timer = setTimeout(async () => {\n try {\n switch (tracker) {\n case 'github': await this.pollGitHub(); break;\n case 'linear': await this.pollLinear(); break;\n case 'rally': await this.pollRally(); break;\n }\n } catch (err: any) {\n console.error(`[IssueDataService] Error polling ${tracker}:`, err.message);\n state.lastError = err.message;\n }\n this.scheduleNext(tracker);\n }, effectiveInterval);\n }\n\n private async ensureShadowStateLoaded(): Promise<void> {\n if (this.shadowStateModule) return;\n try {\n this.shadowStateModule = await import('../../../lib/shadow-state.js');\n } catch {\n // Shadow state not available — issues will work without it\n }\n }\n\n private loadCachedData(): void {\n // Build a lookup of repo → prefix from current config for re-stamping stale identifiers\n const repoPrefixMap = new Map<string, string>();\n try {\n const ghConfig = getGitHubConfig();\n if (ghConfig) {\n for (const { owner, repo, prefix } of ghConfig.repos) {\n repoPrefixMap.set(`${owner}/${repo}`, prefix || repo.toUpperCase());\n }\n }\n } catch { /* ignore */ }\n\n for (const tracker of ['github', 'linear', 'rally']) {\n const cached = this.cache.getStale(tracker, 'issues');\n if (cached?.data) {\n // Sanitize stale Rally cache: rawTrackerState may be an object from pre-PAN-201 data\n if (tracker === 'rally') {\n let sanitizedCount = 0;\n cached.data = cached.data.map((issue: any) => {\n if (typeof issue.rawTrackerState === 'object' && issue.rawTrackerState !== null) {\n sanitizedCount++;\n return {\n ...issue,\n rawTrackerState: issue.rawTrackerState.Name || issue.rawTrackerState._refObjectName || 'Defined',\n };\n }\n return issue;\n });\n if (sanitizedCount > 0) {\n console.warn(`[IssueDataService] Rally cache: sanitized ${sanitizedCount} issues with object rawTrackerState (PAN-201)`);\n }\n }\n // Re-stamp GitHub identifiers in case prefix config changed since cache was written\n if (tracker === 'github') {\n cached.data = cached.data.map((issue: any) => {\n const repoKey = issue.sourceRepo;\n const prefix = repoKey ? repoPrefixMap.get(repoKey) : undefined;\n if (prefix) {\n // Extract issue number from id (github-owner-repo-NUMBER) or identifier (PREFIX-NUMBER)\n const issueNum = issue.id?.match(/-(\\d+)$/)?.[1] || issue.identifier?.match(/-(\\d+)$/)?.[1];\n if (issueNum) {\n const expectedId = `${prefix}-${issueNum}`;\n if (issue.identifier !== expectedId) {\n return { ...issue, identifier: expectedId };\n }\n }\n }\n return issue;\n });\n }\n this.trackers[tracker].lastFetchedIssues = cached.data;\n this.trackers[tracker].lastFetchedAt = cached.lastFetchedAt;\n }\n }\n }\n\n private pushSnapshot(): void {\n this._onIssuesChanged?.(this.getIssues());\n }\n\n private pushUpdated(): void {\n this._onIssuesChanged?.(this.getIssues());\n }\n\n private pushMeta(): void {\n // Diagnostics are served via GET /api/issues/diagnostics — no push needed\n }\n\n // ---------------------------------------------------------------\n // GitHub polling — uses Octokit REST + ETags (304 = FREE)\n // ---------------------------------------------------------------\n\n private async pollGitHub(): Promise<void> {\n const config = getGitHubConfig();\n if (!config) {\n this.trackers.github.lastFetchedIssues = [];\n return;\n }\n\n const allIssues: any[] = [];\n const octokit = new Octokit({ auth: config.token });\n\n for (const { owner, repo, prefix } of config.repos) {\n try {\n // Fetch open issues with ETag support\n const openIssues = await this.fetchGitHubRepoIssues(\n octokit, owner, repo, 'open', prefix || repo.toUpperCase().replace(/-CLI$/, '').replace(/-/g, ''),\n `github:open:${owner}/${repo}`\n );\n\n // Fetch recently closed issues\n const closedIssues = await this.fetchGitHubRepoIssues(\n octokit, owner, repo, 'closed', prefix || repo.toUpperCase().replace(/-CLI$/, '').replace(/-/g, ''),\n `github:closed:${owner}/${repo}`\n );\n\n allIssues.push(...openIssues, ...closedIssues);\n } catch (error: any) {\n console.error(`[IssueDataService] Error fetching GitHub issues for ${owner}/${repo}:`, error.message);\n this.trackers.github.lastError = error.message;\n }\n }\n\n // Check if data actually changed\n const oldData = this.trackers.github.lastFetchedIssues;\n const changed = JSON.stringify(allIssues) !== JSON.stringify(oldData);\n\n this.trackers.github.lastFetchedIssues = allIssues;\n this.trackers.github.lastFetchedAt = new Date().toISOString();\n this.trackers.github.lastError = null;\n\n // Persist to cache\n this.cache.set('github', 'issues', allIssues, { ttlSeconds: DEFAULT_TTLS.github });\n\n if (changed) {\n console.log(`[IssueDataService] GitHub: ${allIssues.length} issues (changed)`);\n this.pushUpdated();\n this.pushMeta();\n }\n }\n\n private async fetchGitHubRepoIssues(\n octokit: Octokit,\n owner: string,\n repo: string,\n state: 'open' | 'closed',\n issuePrefix: string,\n cacheKey: string,\n ): Promise<any[]> {\n // Get stored ETag for conditional request\n const cachedEtag = this.cache.getEtag('github', cacheKey);\n\n const requestParams: any = {\n owner,\n repo,\n state,\n per_page: 100,\n sort: 'updated' as const,\n direction: 'desc' as const,\n };\n\n // Only send If-None-Match when we have a cached ETag\n if (cachedEtag) {\n requestParams.headers = { 'If-None-Match': cachedEtag };\n }\n\n // Fetch ALL closed issues (no date filter) so Done column is complete after restarts\n if (state === 'closed') {\n requestParams.per_page = 100;\n }\n\n try {\n // Use paginate to fetch ALL pages (not just the first 100)\n let newEtag: string | undefined;\n const allData = await octokit.paginate(octokit.issues.listForRepo, requestParams, (response) => {\n // Extract rate limit from each response\n const remaining = parseInt(response.headers['x-ratelimit-remaining'] as string);\n const total = parseInt(response.headers['x-ratelimit-limit'] as string);\n const resetAt = new Date(parseInt(response.headers['x-ratelimit-reset'] as string) * 1000).toISOString();\n\n if (!isNaN(remaining) && !isNaN(total)) {\n this.cache.updateRateLimit('github', { remaining, total, resetAt });\n }\n\n // Store ETag from first page (used for conditional requests on next poll)\n if (!newEtag && response.headers.etag) {\n newEtag = response.headers.etag as string;\n }\n\n return response.data;\n });\n\n // Filter out PRs (they have pull_request key)\n const issues = allData.filter((issue: any) => !issue.pull_request);\n\n // Format issues to match dashboard schema\n const formatted = issues.map((issue: any) => {\n const labelNames = issue.labels?.map((l: any) => typeof l === 'string' ? l : l.name) || [];\n const canonicalStatus = mapGitHubStateToCanonical(issue.state || '', labelNames);\n const identifier = `${issuePrefix}-${issue.number}`;\n\n const firstAssignee = issue.assignees?.[0] || issue.assignee;\n\n return {\n id: `github-${owner}-${repo}-${issue.number}`,\n identifier,\n title: issue.title,\n description: issue.body || '',\n status: canonicalStatus === 'todo' ? 'Todo' :\n canonicalStatus === 'in_progress' ? 'In Progress' :\n canonicalStatus === 'in_review' ? 'In Review' :\n canonicalStatus === 'done' ? 'Done' :\n canonicalStatus === 'backlog' ? 'Backlog' : 'Todo',\n canonicalStatus,\n state: canonicalStatus,\n priority: labelNames.some((l: string) => l.includes('priority') && l.includes('high')) ? 2 :\n labelNames.some((l: string) => l.includes('priority') && l.includes('urgent')) ? 1 :\n labelNames.some((l: string) => l.includes('priority') && l.includes('low')) ? 4 : 3,\n assignee: firstAssignee ? {\n name: firstAssignee.login,\n email: `${firstAssignee.login}@github`,\n } : undefined,\n labels: labelNames,\n url: issue.html_url,\n createdAt: issue.created_at,\n updatedAt: issue.updated_at,\n completedAt: issue.closed_at,\n project: {\n id: `github-${owner}-${repo}`,\n name: `${owner}/${repo}`,\n color: '#333',\n icon: 'github',\n },\n source: 'github',\n sourceRepo: `${owner}/${repo}`,\n };\n });\n\n // Cache with ETag\n this.cache.set('github', cacheKey, formatted, {\n etag: newEtag,\n ttlSeconds: DEFAULT_TTLS.github,\n });\n\n return formatted;\n } catch (err: any) {\n // 304 Not Modified — return cached data (this is FREE, no rate limit cost)\n if (err.status === 304) {\n const cached = this.cache.getStale('github', cacheKey);\n if (!cached?.data) return [];\n // Re-stamp identifiers in case the prefix changed since the cache was written\n return cached.data.map((issue: any) => {\n const issueNum = issue.id?.match(/-(\\d+)$/)?.[1] || issue.identifier?.match(/-(\\d+)$/)?.[1];\n if (issueNum) {\n const expectedId = `${issuePrefix}-${issueNum}`;\n return issue.identifier === expectedId ? issue : { ...issue, identifier: expectedId };\n }\n return issue;\n });\n }\n throw err;\n }\n }\n\n // ---------------------------------------------------------------\n // Linear polling — TTL + incremental updatedAt\n // ---------------------------------------------------------------\n\n private async pollLinear(): Promise<void> {\n const apiKey = getLinearApiKey();\n if (!apiKey) {\n this.trackers.linear.lastFetchedIssues = [];\n return;\n }\n\n const now = Date.now();\n const needsFullRefresh = now - this.linearLastFullRefresh > LINEAR_FULL_REFRESH_MS;\n\n // Get the most recent updatedAt from cached issues for incremental fetch\n let sinceUpdatedAt: string | null = null;\n if (!needsFullRefresh && this.trackers.linear.lastFetchedIssues.length > 0) {\n const maxUpdated = this.trackers.linear.lastFetchedIssues.reduce((max: string, issue: any) => {\n return issue.updatedAt > max ? issue.updatedAt : max;\n }, '');\n if (maxUpdated) sinceUpdatedAt = maxUpdated;\n }\n\n try {\n const fetchedIssues = await this.fetchLinearIssues(apiKey, sinceUpdatedAt);\n\n let allIssues: any[];\n if (sinceUpdatedAt && fetchedIssues.length > 0) {\n // Incremental: merge new/updated issues into existing list\n const existingMap = new Map(\n this.trackers.linear.lastFetchedIssues.map((i: any) => [i.identifier, i])\n );\n for (const issue of fetchedIssues) {\n existingMap.set(issue.identifier, issue);\n }\n allIssues = Array.from(existingMap.values());\n } else if (needsFullRefresh || sinceUpdatedAt === null) {\n // Full refresh\n allIssues = fetchedIssues;\n this.linearLastFullRefresh = now;\n } else {\n // No new data from incremental fetch\n allIssues = this.trackers.linear.lastFetchedIssues;\n }\n\n const oldData = this.trackers.linear.lastFetchedIssues;\n const changed = JSON.stringify(allIssues) !== JSON.stringify(oldData);\n\n this.trackers.linear.lastFetchedIssues = allIssues;\n this.trackers.linear.lastFetchedAt = new Date().toISOString();\n this.trackers.linear.lastError = null;\n\n this.cache.set('linear', 'issues', allIssues, { ttlSeconds: DEFAULT_TTLS.linear });\n\n if (changed) {\n console.log(`[IssueDataService] Linear: ${allIssues.length} issues (changed)`);\n this.pushUpdated();\n this.pushMeta();\n }\n } catch (err: any) {\n console.error('[IssueDataService] Linear poll error:', err.message);\n this.trackers.linear.lastError = err.message;\n }\n }\n\n private async fetchLinearIssues(apiKey: string, sinceUpdatedAt: string | null): Promise<any[]> {\n const allIssues: any[] = [];\n let hasMore = true;\n let cursor: string | undefined;\n\n // Build filter conditions\n const filterConditions: string[] = [];\n // Scope to active cycle only — completed/canceled filtering is handled\n // by getIssues() post-filter, NOT here. The GraphQL query must fetch\n // completed issues so that: (1) the \"Include completed\" toggle works,\n // (2) incremental updates correctly reflect state transitions, and\n // (3) internal getIssues() callers can look up recently-completed issues.\n filterConditions.push('cycle: { isActive: { eq: true } }');\n\n // Incremental: only issues updated after sinceUpdatedAt\n if (sinceUpdatedAt) {\n filterConditions.push(`updatedAt: { gt: \"${sinceUpdatedAt}\" }`);\n }\n\n let filterClause = '';\n if (filterConditions.length === 1) {\n filterClause = `filter: { ${filterConditions[0]} }`;\n } else if (filterConditions.length > 1) {\n filterClause = `filter: { and: [${filterConditions.map(c => `{ ${c} }`).join(', ')}] }`;\n }\n\n while (hasMore) {\n const query = `\n query GetIssues($after: String) {\n issues(first: 100, after: $after, ${filterClause ? filterClause + ', ' : ''}orderBy: updatedAt) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n identifier\n title\n description\n priority\n url\n createdAt\n updatedAt\n completedAt\n state {\n name\n type\n }\n assignee {\n name\n email\n }\n labels {\n nodes {\n name\n }\n }\n project {\n id\n name\n color\n icon\n }\n team {\n id\n name\n color\n icon\n }\n cycle {\n id\n name\n number\n }\n }\n }\n }\n `;\n\n const response = await fetch('https://api.linear.app/graphql', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': apiKey,\n },\n body: JSON.stringify({ query, variables: { after: cursor } }),\n });\n\n const json = await response.json();\n\n if (json.errors) {\n throw new Error(json.errors[0]?.message || 'Linear GraphQL error');\n }\n\n const issues = json.data?.issues;\n if (!issues) break;\n\n allIssues.push(...issues.nodes);\n hasMore = issues.pageInfo.hasNextPage;\n cursor = issues.pageInfo.endCursor;\n\n if (allIssues.length > 1000) break;\n }\n\n // Build project lookup for deduplication\n const projectByName = new Map<string, { id: string; name: string; color?: string; icon?: string }>();\n for (const issue of allIssues) {\n if (issue.project && !projectByName.has(issue.project.name)) {\n projectByName.set(issue.project.name, {\n id: issue.project.id,\n name: issue.project.name,\n color: issue.project.color,\n icon: issue.project.icon,\n });\n }\n }\n\n // Format to dashboard schema\n return allIssues.map((issue: any) => {\n let project;\n if (issue.project) {\n project = {\n id: issue.project.id,\n name: issue.project.name,\n color: issue.project.color,\n icon: issue.project.icon,\n };\n } else if (issue.team) {\n const existing = projectByName.get(issue.team.name);\n project = existing || {\n id: issue.team.id,\n name: issue.team.name,\n color: issue.team.color,\n icon: issue.team.icon,\n };\n }\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n status: issue.state?.name || 'Backlog',\n stateType: issue.state?.type,\n priority: issue.priority,\n assignee: issue.assignee ? { name: issue.assignee.name, email: issue.assignee.email } : undefined,\n labels: issue.labels?.nodes?.map((l: any) => l.name) || [],\n url: issue.url,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n completedAt: issue.completedAt,\n project,\n cycle: issue.cycle ? {\n id: issue.cycle.id,\n name: issue.cycle.name,\n number: issue.cycle.number,\n } : undefined,\n source: 'linear',\n };\n });\n }\n\n // ---------------------------------------------------------------\n // Rally polling — TTL-based caching, per-project config support\n // ---------------------------------------------------------------\n\n /**\n * Format a raw Rally issue into the dashboard schema.\n */\n private formatRallyIssue(issue: any, projectInfo: { id: string; name: string; color: string; icon: string }): any {\n const canonicalStatus = mapRallyStateToCanonical(issue.state);\n const identifier = issue.ref || issue.id || 'unknown';\n if (typeof issue.rawState === 'object' && issue.rawState !== null) {\n console.warn(`[IssueDataService] Rally ${identifier}: rawState is object, normalizing (PAN-201)`);\n }\n return {\n id: `rally-${issue.id || identifier}`,\n identifier,\n title: issue.title || '',\n description: issue.description || '',\n status: canonicalStatus === 'todo' ? 'Todo' :\n canonicalStatus === 'in_progress' ? 'In Progress' :\n canonicalStatus === 'done' ? 'Done' : 'Todo',\n priority: issue.priority ?? 3,\n assignee: issue.assignee ? {\n name: issue.assignee,\n email: `${issue.assignee.replace(/\\s+/g, '.').toLowerCase()}@rally`,\n } : undefined,\n labels: Array.isArray(issue.labels) ? issue.labels.filter((l: any) => typeof l === 'string') : [],\n url: issue.url || '',\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n parentRef: issue.parentRef,\n artifactType: issue.artifactType,\n rawTrackerState: typeof issue.rawState === 'object' && issue.rawState !== null\n ? (issue.rawState.Name || issue.rawState._refObjectName || 'Defined')\n : issue.rawState,\n project: projectInfo,\n source: 'rally',\n };\n }\n\n /**\n * Compute derived feature status from child stories.\n * If ANY child is in progress, the feature is derived as 'in_progress'.\n * If ALL children are done, the feature is derived as 'closed'.\n * Attaches child counts for progress display.\n */\n private computeDerivedFeatureStatus(issues: any[]): any[] {\n // Build children-by-parent map (key: parent identifier, value: child issues)\n const childrenByParent = new Map<string, any[]>();\n for (const issue of issues) {\n if (issue.parentRef) {\n const existing = childrenByParent.get(issue.parentRef) || [];\n existing.push(issue);\n childrenByParent.set(issue.parentRef, existing);\n }\n }\n\n // For each Feature, compute derived status\n return issues.map(issue => {\n const isFeature = issue.artifactType?.includes('PortfolioItem');\n if (!isFeature) return issue;\n\n const children = childrenByParent.get(issue.identifier) || [];\n if (children.length === 0) return issue;\n\n const completedChildCount = children.filter(\n (c: any) => c.status === 'Done'\n ).length;\n const inProgressChildCount = children.filter(\n (c: any) => c.status === 'In Progress'\n ).length;\n const totalChildCount = children.length;\n\n let derivedStatus: string | undefined;\n if (completedChildCount === totalChildCount) {\n derivedStatus = 'closed';\n } else if (inProgressChildCount > 0) {\n derivedStatus = 'in_progress';\n }\n\n return {\n ...issue,\n derivedStatus,\n totalChildCount,\n completedChildCount,\n inProgressChildCount,\n };\n });\n }\n\n private async pollRally(): Promise<void> {\n const globalConfig = getRallyConfig();\n if (!globalConfig) {\n this.trackers.rally.lastFetchedIssues = [];\n return;\n }\n\n // Validate config on first poll and log warnings\n if (!this.trackers.rally.lastFetchedAt) {\n const validation = validateRallyConfig(globalConfig);\n if (validation.warnings.length > 0) {\n console.warn('[Rally] Configuration warnings:', validation.warnings.join('; '));\n }\n }\n\n // Only fetch if cache is stale\n if (!this.cache.isStale('rally', 'issues') && this.trackers.rally.lastFetchedIssues.length > 0) {\n return;\n }\n\n try {\n const { RallyTracker } = await import('../../../lib/tracker/rally.js');\n const { findProjectsByRallyProject } = await import('../../../lib/projects.js');\n\n const rallyProjects = findProjectsByRallyProject();\n let allFormatted: any[] = [];\n\n if (rallyProjects.length > 0) {\n // Per-project mode: create separate tracker per Rally project OID\n const projectQueries = rallyProjects.map(async ({ key, config: projConfig }) => {\n try {\n const tracker = new RallyTracker({\n apiKey: globalConfig.apiKey,\n server: globalConfig.server,\n workspace: globalConfig.workspace,\n project: projConfig.rally_project,\n });\n\n const issues = await tracker.listIssues({\n includeClosed: false,\n limit: 100,\n });\n\n const projectInfo = {\n id: `rally-${key}`,\n name: projConfig.name,\n color: '#00C7B1',\n icon: 'rally',\n };\n\n return issues.map((issue: any) => this.formatRallyIssue(issue, projectInfo));\n } catch (err: any) {\n console.error(`[IssueDataService] Rally poll error for project ${key}:`, err.message);\n return [];\n }\n });\n\n const results = await Promise.all(projectQueries);\n allFormatted = results.flat();\n } else {\n // Fallback: use global RALLY_PROJECT env (backward compat)\n const tracker = new RallyTracker({\n apiKey: globalConfig.apiKey,\n server: globalConfig.server,\n workspace: globalConfig.workspace,\n project: globalConfig.project,\n });\n\n const issues = await tracker.listIssues({\n includeClosed: false,\n limit: 100,\n });\n\n const projectInfo = {\n id: 'rally-project',\n name: 'Rally',\n color: '#00C7B1',\n icon: 'rally',\n };\n\n allFormatted = issues.map((issue: any) => this.formatRallyIssue(issue, projectInfo));\n }\n\n // Compute derived feature status from child stories\n allFormatted = this.computeDerivedFeatureStatus(allFormatted);\n\n const oldData = this.trackers.rally.lastFetchedIssues;\n const changed = JSON.stringify(allFormatted) !== JSON.stringify(oldData);\n\n this.trackers.rally.lastFetchedIssues = allFormatted;\n this.trackers.rally.lastFetchedAt = new Date().toISOString();\n this.trackers.rally.lastError = null;\n\n this.cache.set('rally', 'issues', allFormatted, { ttlSeconds: DEFAULT_TTLS.rally });\n\n if (changed) {\n console.log(`[IssueDataService] Rally: ${allFormatted.length} issues (changed)`);\n this.pushUpdated();\n this.pushMeta();\n }\n } catch (err: any) {\n const errorMsg = err.message?.includes('Could not parse')\n ? `${err.message} - Check Rally workspace/project configuration. Enable DEBUG=rally for query details.`\n : err.message;\n console.error('[IssueDataService] Rally poll error:', errorMsg);\n this.trackers.rally.lastError = errorMsg;\n }\n }\n}\n","/**\n * Shared IssueDataService singleton — used by read model (bootstrap),\n * route handlers (force-refresh, diagnostics), and event emission.\n */\nimport { IssueDataService } from './issue-data-service.js';\nimport { CacheService } from './cache-service.js';\n\nlet _service: IssueDataService | null = null;\nlet _startPromise: Promise<void> | null = null;\n\nexport function getSharedIssueService(): IssueDataService {\n if (!_service) {\n _service = new IssueDataService(new CacheService());\n }\n return _service;\n}\n\nexport function startSharedIssueService(): Promise<void> {\n if (_startPromise) return _startPromise;\n _startPromise = getSharedIssueService().start().catch((err: unknown) => {\n console.error('[issue-service-singleton] start failed:', err);\n });\n return _startPromise;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"issue-service-singleton-sb2HkB9f.js","names":[],"sources":["../../src/core/state-mapping.ts","../../src/dashboard/server/services/cache-service.ts","../../src/dashboard/server/services/issue-data-service.ts","../../src/dashboard/server/services/issue-service-singleton.ts"],"sourcesContent":["/**\n * Panopticon State Mapping System\n *\n * Maps Panopticon's canonical workflow states to various issue tracker states.\n * Supports auto-creation of missing states where possible, and label fallbacks.\n */\n\n// Panopticon's canonical workflow states\nexport type CanonicalState =\n | 'backlog'\n | 'todo'\n | 'in_progress'\n | 'in_review'\n | 'done'\n | 'canceled';\n\n// State type categories (Linear terminology)\nexport type StateType = 'backlog' | 'unstarted' | 'started' | 'completed' | 'canceled';\n\n// Canonical state definitions\nexport interface CanonicalStateDefinition {\n name: CanonicalState;\n type: StateType;\n description: string;\n color: string;\n}\n\nexport const CANONICAL_STATES: CanonicalStateDefinition[] = [\n { name: 'backlog', type: 'backlog', description: 'Ideas and future work', color: '#6b7280' },\n { name: 'todo', type: 'unstarted', description: 'Prioritized and ready', color: '#3b82f6' },\n { name: 'in_progress', type: 'started', description: 'Agent executing', color: '#eab308' },\n { name: 'in_review', type: 'started', description: 'PR awaiting review', color: '#ec4899' },\n { name: 'done', type: 'completed', description: 'Work complete', color: '#22c55e' },\n { name: 'canceled', type: 'canceled', description: \"Won't do\", color: '#71717a' },\n];\n\nexport const STATE_TYPE_MAP: Record<CanonicalState, StateType> = {\n backlog: 'backlog',\n todo: 'unstarted',\n in_progress: 'started',\n in_review: 'started',\n done: 'completed',\n canceled: 'canceled',\n};\n\n// Strategy for handling missing states\nexport type MissingStateStrategy = 'auto_create' | 'error';\n\n// Auto-create configuration for a specific state\nexport interface AutoCreateStateConfig {\n type: StateType;\n color: string;\n positionAfter?: string; // State name to position after\n}\n\n// Tracker-specific state mapping\nexport interface TrackerStateMapping {\n stateMap: Record<CanonicalState, string | { status: string; label?: string | null }>;\n missingStateStrategy: MissingStateStrategy;\n autoCreateConfig?: Record<string, AutoCreateStateConfig>;\n // Tracker-specific options\n projectBoard?: {\n enabled: boolean;\n name: string;\n columnMap: Record<CanonicalState, string>;\n };\n}\n\n// Supported trackers\nexport type SupportedTracker = 'linear' | 'github' | 'gitlab' | 'jira' | 'trello';\n\n// Full state mapping configuration\nexport interface StateMappingConfig {\n canonicalStates: CanonicalStateDefinition[];\n trackers: Record<SupportedTracker, TrackerStateMapping>;\n}\n\n// Default state mappings for supported trackers\nexport const DEFAULT_STATE_MAPPINGS: StateMappingConfig = {\n canonicalStates: CANONICAL_STATES,\n trackers: {\n linear: {\n stateMap: {\n backlog: 'Backlog',\n todo: 'Todo',\n in_progress: 'In Progress',\n in_review: 'In Review',\n done: 'Done',\n canceled: 'Canceled',\n },\n missingStateStrategy: 'auto_create',\n },\n\n github: {\n stateMap: {\n backlog: { status: 'open', label: null },\n todo: { status: 'open', label: null },\n in_progress: { status: 'open', label: 'in-progress' },\n in_review: { status: 'open', label: 'in-review' },\n done: { status: 'closed', label: null },\n canceled: { status: 'closed', label: 'wontfix' },\n },\n missingStateStrategy: 'error',\n projectBoard: {\n enabled: true,\n name: 'Panopticon',\n columnMap: {\n backlog: 'Backlog',\n todo: 'Todo',\n in_progress: 'In Progress',\n in_review: 'Review',\n done: 'Done',\n canceled: 'Done',\n },\n },\n },\n\n gitlab: {\n stateMap: {\n backlog: { status: 'opened', label: 'backlog' },\n todo: { status: 'opened', label: 'todo' },\n in_progress: { status: 'opened', label: 'in-progress' },\n in_review: { status: 'opened', label: 'in-review' },\n done: { status: 'closed', label: null },\n canceled: { status: 'closed', label: 'wontfix' },\n },\n missingStateStrategy: 'error',\n },\n\n jira: {\n stateMap: {\n backlog: 'Backlog',\n todo: 'To Do',\n in_progress: 'In Progress',\n in_review: 'In Review',\n done: 'Done',\n canceled: 'Canceled',\n },\n missingStateStrategy: 'error', // Can't auto-create in Jira\n },\n\n trello: {\n stateMap: {\n backlog: 'Backlog',\n todo: 'To Do',\n in_progress: 'Doing',\n in_review: 'Review',\n done: 'Done',\n canceled: 'Archived',\n },\n missingStateStrategy: 'auto_create', // Trello lists are easy to create\n },\n },\n};\n\n// Virtual state tracking for issues\nexport interface PanopticonIssueState {\n issueId: string;\n panopticonState: CanonicalState;\n trackerState: string;\n lastSyncedAt: string;\n syncStatus: 'synced' | 'pending' | 'conflict';\n fallbacksUsed: string[];\n}\n\n// State transition result\nexport interface StateTransitionResult {\n success: boolean;\n panopticonState: CanonicalState;\n trackerState: string;\n fallbacksUsed: string[];\n warnings: string[];\n error?: string;\n}\n\n// Tracker state check result\nexport interface TrackerStateCheckResult {\n tracker: SupportedTracker;\n team?: string;\n existingStates: string[];\n missingStates: CanonicalState[];\n recommendations: {\n state: CanonicalState;\n action: 'create' | 'skip';\n details: string;\n }[];\n}\n\n/**\n * Map a tracker state name to a canonical state\n */\nexport function trackerStateToCanonical(\n trackerState: string,\n tracker: SupportedTracker = 'linear'\n): CanonicalState {\n const mapping = DEFAULT_STATE_MAPPINGS.trackers[tracker];\n if (!mapping) return 'backlog';\n\n // Check direct state map\n for (const [canonical, mapped] of Object.entries(mapping.stateMap)) {\n if (typeof mapped === 'string') {\n if (mapped.toLowerCase() === trackerState.toLowerCase()) {\n return canonical as CanonicalState;\n }\n } else if (mapped.label === trackerState.toLowerCase()) {\n return canonical as CanonicalState;\n }\n }\n\n // Fallback heuristics\n const lower = trackerState.toLowerCase();\n if (lower.includes('backlog') || lower.includes('triage')) return 'backlog';\n if (lower.includes('todo') || lower.includes('ready') || lower.includes('unstarted')) return 'todo';\n if (lower.includes('progress') || lower.includes('started') || lower.includes('active')) return 'in_progress';\n if (lower.includes('review') || lower.includes('qa') || lower.includes('testing')) return 'in_review';\n if (lower.includes('done') || lower.includes('complete') || lower.includes('closed')) return 'done';\n if (lower.includes('cancel') || lower.includes('duplicate') || lower.includes('wontfix')) return 'canceled';\n\n return 'backlog';\n}\n\n/**\n * Get the tracker state name for a canonical state\n */\nexport function canonicalToTrackerState(\n canonicalState: CanonicalState,\n tracker: SupportedTracker = 'linear'\n): string {\n const mapping = DEFAULT_STATE_MAPPINGS.trackers[tracker];\n if (!mapping) return canonicalState;\n\n const mapped = mapping.stateMap[canonicalState];\n if (typeof mapped === 'string') {\n return mapped;\n } else {\n return mapped.label || mapped.status;\n }\n}\n\n/**\n * Workflow labels that should be removed during state transitions\n */\nexport const WORKFLOW_LABELS = [\n 'in-progress',\n 'in progress',\n 'in-review',\n 'in review',\n 'review-ready',\n 'review ready',\n 'planned',\n 'planning',\n 'closed-out',\n];\n\n/**\n * Get the target workflow label for a canonical state\n */\nexport function getStateLabel(state: CanonicalState): string | null {\n switch (state) {\n case 'in_progress':\n return 'in-progress';\n case 'in_review':\n return 'in-review';\n case 'done':\n return 'done';\n default:\n return null;\n }\n}\n\n/**\n * Map GitHub issue state + labels to canonical state.\n * This function handles the GitHub-specific mapping where issues have both\n * a state (open/closed) and workflow labels.\n *\n * @param state - GitHub issue state ('open' or 'closed')\n * @param labels - Array of label names on the issue\n * @returns Canonical state string\n */\nexport function mapGitHubStateToCanonical(state: string, labels: string[]): CanonicalState {\n // Handle both API lowercase and gh CLI uppercase\n const stateLower = state.toLowerCase();\n\n // Closed issues are always done (regardless of labels)\n if (stateLower === 'closed') {\n return 'done';\n }\n\n // For open issues, check labels for workflow state\n // Order matters: more progressed states take precedence\n const labelNames = labels.map(l => l.toLowerCase());\n\n // Most progressed states first\n // merged = postMergeLifecycle applied label; issue may still be open if auto-close failed\n // needs-close-out = merged work reopened for close-out ceremony\n // Both belong in the done column — awaiting close-out ceremony\n if (labelNames.some(l => l === 'merged' || l === 'needs-close-out')) {\n return 'done';\n }\n // \"done\" label on OPEN issues = work complete, pending merge/closure → in_review\n // (actual \"done\" status only for CLOSED issues, handled above)\n if (labelNames.some(l => l === 'done' || l.includes('completed'))) {\n return 'in_review';\n }\n if (labelNames.some(l => l.includes('in review') || l.includes('in-review') || l.includes('review') || l.includes('qa'))) {\n return 'in_review';\n }\n if (labelNames.some(l => l.includes('in progress') || l.includes('in-progress') || l.includes('wip'))) {\n return 'in_progress';\n }\n // Early workflow stages\n if (labelNames.some(l => l.includes('backlog') || l.includes('icebox'))) {\n return 'backlog';\n }\n if (labelNames.some(l => l.includes('todo') || l.includes('ready'))) {\n return 'todo';\n }\n\n // Default open issues to todo\n return 'todo';\n}\n\n/**\n * Get the target state name for a Linear team.\n * Uses the DEFAULT_STATE_MAPPINGS to find the Linear state name.\n *\n * @param canonicalState - The canonical state to map\n * @returns The Linear state name (e.g., 'In Review')\n */\nexport function getLinearStateName(canonicalState: CanonicalState): string {\n const mapping = DEFAULT_STATE_MAPPINGS.trackers.linear;\n const mapped = mapping.stateMap[canonicalState];\n return typeof mapped === 'string' ? mapped : canonicalState;\n}\n\n/**\n * Find a Linear workflow state by name in a team.\n * Returns null if not found.\n *\n * @param states - Array of Linear workflow states from the SDK\n * @param stateName - The state name to find\n * @returns The matching state or null\n */\nexport function findLinearStateByName(states: any[], stateName: string): any | null {\n // Try exact match first\n const exactMatch = states.find(s => s.name === stateName);\n if (exactMatch) return exactMatch;\n\n // Try case-insensitive match\n const lowerName = stateName.toLowerCase();\n return states.find(s => s.name.toLowerCase() === lowerName) || null;\n}\n\n/**\n * Clean up workflow labels during state transitions.\n * Removes all workflow labels, then adds the label matching the target state (if any).\n *\n * @param currentLabels - Array of current label names\n * @param targetState - The canonical state being transitioned to\n * @returns Array of label names after cleanup\n */\nexport function cleanupWorkflowLabels(\n currentLabels: string[],\n targetState: CanonicalState\n): string[] {\n // Remove all workflow labels\n const cleaned = currentLabels.filter(\n label => !WORKFLOW_LABELS.includes(label.toLowerCase())\n );\n\n // Add the label matching the target state (if applicable)\n const targetLabel = getStateLabel(targetState);\n if (targetLabel && !cleaned.includes(targetLabel)) {\n cleaned.push(targetLabel);\n }\n\n return cleaned;\n}\n","/**\n * CacheService — Two-layer cache for dashboard API responses\n *\n * L1: In-memory Map (hot, 10s TTL, 50 entries max)\n * L2: SQLite (persistent, survives restarts)\n *\n * Stores API responses per tracker with ETag support (GitHub REST 304s are FREE).\n * Tracks rate limits per tracker for adaptive backoff.\n */\n\nimport type Database from 'better-sqlite3';\nimport { createRequire } from 'module';\nimport { join } from 'path';\nimport { existsSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\n\ndeclare const Bun: unknown;\nconst _require = createRequire(import.meta.url);\n\nfunction openSqliteDb(dbPath: string): Database.Database {\n if (typeof Bun !== 'undefined') {\n const { Database: BunDatabase } = _require('bun:sqlite') as { Database: new (path: string) => any };\n const bunDb = new BunDatabase(dbPath);\n bunDb.pragma = function (sql: string, options?: { simple?: boolean }): any {\n if (options?.simple) {\n const key = sql.trim();\n const row = bunDb.query(`PRAGMA ${key}`).get() as Record<string, unknown> | null;\n return row?.[key] ?? null;\n }\n bunDb.exec(`PRAGMA ${sql}`);\n return undefined;\n };\n return bunDb as Database.Database;\n }\n const BetterSqlite3 = _require('better-sqlite3');\n return new BetterSqlite3(dbPath) as Database.Database;\n}\n\nconst PANOPTICON_HOME = process.env.PANOPTICON_HOME || join(homedir(), '.panopticon');\nconst CACHE_DB_PATH = join(PANOPTICON_HOME, 'cache.db');\n\n// Default TTLs per tracker (seconds)\nexport const DEFAULT_TTLS: Record<string, number> = {\n github: 60,\n linear: 30,\n rally: 120,\n};\n\n// L1 in-memory cache entry\ninterface L1Entry {\n data: any;\n etag?: string;\n lastModified?: string;\n lastFetchedAt: string;\n lastUpdatedAt: string;\n ttlSeconds: number;\n insertedAt: number; // Date.now()\n}\n\n// Rate limit info\nexport interface RateLimitInfo {\n remaining: number;\n total: number;\n resetAt: string;\n}\n\n// Cache entry returned from get()\nexport interface CacheEntry {\n data: any;\n etag?: string;\n lastModified?: string;\n lastFetchedAt: string;\n lastUpdatedAt: string;\n ttlSeconds: number;\n}\n\nexport class CacheService {\n private db: Database.Database;\n private l1: Map<string, L1Entry> = new Map();\n private readonly l1MaxEntries = 50;\n private readonly l1TtlMs = 10_000; // 10 seconds\n\n constructor() {\n if (!existsSync(PANOPTICON_HOME)) {\n mkdirSync(PANOPTICON_HOME, { recursive: true });\n }\n\n this.db = openSqliteDb(CACHE_DB_PATH);\n this.db.pragma('journal_mode = WAL');\n this.createSchema();\n }\n\n private createSchema(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS api_cache (\n tracker TEXT NOT NULL,\n cache_key TEXT NOT NULL,\n data TEXT NOT NULL,\n etag TEXT,\n last_modified TEXT,\n last_fetched_at TEXT NOT NULL,\n last_updated_at TEXT NOT NULL,\n ttl_seconds INTEGER NOT NULL,\n PRIMARY KEY (tracker, cache_key)\n );\n\n CREATE TABLE IF NOT EXISTS rate_limits (\n tracker TEXT PRIMARY KEY,\n remaining INTEGER,\n total INTEGER,\n reset_at TEXT,\n updated_at TEXT NOT NULL\n );\n `);\n }\n\n /**\n * Get a cached entry. Checks L1 first, then L2 (SQLite).\n * Returns null if no entry or if expired.\n */\n get(tracker: string, cacheKey: string): CacheEntry | null {\n const compositeKey = `${tracker}:${cacheKey}`;\n\n // L1 check\n const l1Entry = this.l1.get(compositeKey);\n if (l1Entry && Date.now() - l1Entry.insertedAt < this.l1TtlMs) {\n return {\n data: l1Entry.data,\n etag: l1Entry.etag,\n lastModified: l1Entry.lastModified,\n lastFetchedAt: l1Entry.lastFetchedAt,\n lastUpdatedAt: l1Entry.lastUpdatedAt,\n ttlSeconds: l1Entry.ttlSeconds,\n };\n }\n\n // L1 miss or expired — check L2\n const stmt = this.db.prepare(`\n SELECT data, etag, last_modified, last_fetched_at, last_updated_at, ttl_seconds\n FROM api_cache\n WHERE tracker = ? AND cache_key = ?\n `);\n\n const row = stmt.get(tracker, cacheKey) as any;\n if (!row) return null;\n\n const entry: CacheEntry = {\n data: JSON.parse(row.data),\n etag: row.etag || undefined,\n lastModified: row.last_modified || undefined,\n lastFetchedAt: row.last_fetched_at,\n lastUpdatedAt: row.last_updated_at,\n ttlSeconds: row.ttl_seconds,\n };\n\n // Promote to L1\n this.setL1(compositeKey, {\n ...entry,\n insertedAt: Date.now(),\n });\n\n return entry;\n }\n\n /**\n * Get cached entry even if stale (for serving while re-fetching).\n */\n getStale(tracker: string, cacheKey: string): CacheEntry | null {\n const compositeKey = `${tracker}:${cacheKey}`;\n\n // Check L1 (even if expired)\n const l1Entry = this.l1.get(compositeKey);\n if (l1Entry) {\n return {\n data: l1Entry.data,\n etag: l1Entry.etag,\n lastModified: l1Entry.lastModified,\n lastFetchedAt: l1Entry.lastFetchedAt,\n lastUpdatedAt: l1Entry.lastUpdatedAt,\n ttlSeconds: l1Entry.ttlSeconds,\n };\n }\n\n // L2\n const stmt = this.db.prepare(`\n SELECT data, etag, last_modified, last_fetched_at, last_updated_at, ttl_seconds\n FROM api_cache\n WHERE tracker = ? AND cache_key = ?\n `);\n\n const row = stmt.get(tracker, cacheKey) as any;\n if (!row) return null;\n\n return {\n data: JSON.parse(row.data),\n etag: row.etag || undefined,\n lastModified: row.last_modified || undefined,\n lastFetchedAt: row.last_fetched_at,\n lastUpdatedAt: row.last_updated_at,\n ttlSeconds: row.ttl_seconds,\n };\n }\n\n /**\n * Store data in both L1 and L2 cache.\n */\n set(\n tracker: string,\n cacheKey: string,\n data: any,\n options?: {\n etag?: string;\n lastModified?: string;\n lastUpdatedAt?: string;\n ttlSeconds?: number;\n }\n ): void {\n const now = new Date().toISOString();\n const ttl = options?.ttlSeconds ?? DEFAULT_TTLS[tracker] ?? 60;\n const lastUpdatedAt = options?.lastUpdatedAt ?? now;\n const compositeKey = `${tracker}:${cacheKey}`;\n\n // L2 (SQLite) — upsert\n const stmt = this.db.prepare(`\n INSERT INTO api_cache (tracker, cache_key, data, etag, last_modified, last_fetched_at, last_updated_at, ttl_seconds)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(tracker, cache_key) DO UPDATE SET\n data = excluded.data,\n etag = excluded.etag,\n last_modified = excluded.last_modified,\n last_fetched_at = excluded.last_fetched_at,\n last_updated_at = excluded.last_updated_at,\n ttl_seconds = excluded.ttl_seconds\n `);\n\n stmt.run(\n tracker,\n cacheKey,\n JSON.stringify(data),\n options?.etag ?? null,\n options?.lastModified ?? null,\n now,\n lastUpdatedAt,\n ttl\n );\n\n // L1\n this.setL1(compositeKey, {\n data,\n etag: options?.etag,\n lastModified: options?.lastModified,\n lastFetchedAt: now,\n lastUpdatedAt,\n ttlSeconds: ttl,\n insertedAt: Date.now(),\n });\n }\n\n /**\n * Check if a cache entry is stale (past its TTL).\n */\n isStale(tracker: string, cacheKey: string): boolean {\n const entry = this.get(tracker, cacheKey);\n if (!entry) return true;\n\n const fetchedAt = new Date(entry.lastFetchedAt).getTime();\n const age = (Date.now() - fetchedAt) / 1000;\n return age > entry.ttlSeconds;\n }\n\n /**\n * Get the stored ETag for a tracker/key combo (for conditional requests).\n */\n getEtag(tracker: string, cacheKey: string): string | undefined {\n // Check L1 first\n const compositeKey = `${tracker}:${cacheKey}`;\n const l1Entry = this.l1.get(compositeKey);\n if (l1Entry?.etag) return l1Entry.etag;\n\n // Check L2\n const stmt = this.db.prepare(`\n SELECT etag FROM api_cache WHERE tracker = ? AND cache_key = ?\n `);\n const row = stmt.get(tracker, cacheKey) as any;\n return row?.etag || undefined;\n }\n\n /**\n * Invalidate cache for a specific tracker (all keys).\n */\n invalidate(tracker: string): void {\n // L1 — remove all entries for this tracker\n for (const key of this.l1.keys()) {\n if (key.startsWith(`${tracker}:`)) {\n this.l1.delete(key);\n }\n }\n\n // L2\n this.db.prepare('DELETE FROM api_cache WHERE tracker = ?').run(tracker);\n }\n\n /**\n * Invalidate a specific cache key.\n */\n invalidateKey(tracker: string, cacheKey: string): void {\n this.l1.delete(`${tracker}:${cacheKey}`);\n this.db.prepare('DELETE FROM api_cache WHERE tracker = ? AND cache_key = ?').run(tracker, cacheKey);\n }\n\n // --- Rate limit tracking ---\n\n /**\n * Update rate limit info for a tracker.\n */\n updateRateLimit(tracker: string, info: RateLimitInfo): void {\n const stmt = this.db.prepare(`\n INSERT INTO rate_limits (tracker, remaining, total, reset_at, updated_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(tracker) DO UPDATE SET\n remaining = excluded.remaining,\n total = excluded.total,\n reset_at = excluded.reset_at,\n updated_at = excluded.updated_at\n `);\n stmt.run(tracker, info.remaining, info.total, info.resetAt, new Date().toISOString());\n }\n\n /**\n * Get rate limit info for a tracker.\n */\n getRateLimit(tracker: string): RateLimitInfo | null {\n const stmt = this.db.prepare(`\n SELECT remaining, total, reset_at FROM rate_limits WHERE tracker = ?\n `);\n const row = stmt.get(tracker) as any;\n if (!row) return null;\n\n return {\n remaining: row.remaining,\n total: row.total,\n resetAt: row.reset_at,\n };\n }\n\n /**\n * Check if we should back off requests for a tracker.\n * Returns true if remaining < 10% of total.\n */\n shouldBackoff(tracker: string): boolean {\n const limit = this.getRateLimit(tracker);\n if (!limit) return false;\n\n // If reset time has passed, no need to back off\n if (new Date(limit.resetAt).getTime() < Date.now()) return false;\n\n return limit.remaining < limit.total * 0.1;\n }\n\n /**\n * Calculate adaptive backoff delay in ms.\n * Returns 0 if no backoff needed.\n */\n getBackoffMs(tracker: string, baseIntervalMs: number): number {\n const limit = this.getRateLimit(tracker);\n if (!limit) return 0;\n\n // If reset time has passed, no backoff\n if (new Date(limit.resetAt).getTime() < Date.now()) return 0;\n\n const ratioRemaining = limit.remaining / limit.total;\n\n if (ratioRemaining > 0.5) return 0; // >50% remaining: no backoff\n if (ratioRemaining > 0.25) return baseIntervalMs; // 25-50%: 2x interval\n if (ratioRemaining > 0.1) return baseIntervalMs * 4; // 10-25%: 5x interval\n return baseIntervalMs * 9; // <10%: 10x interval\n }\n\n /**\n * Get cache status for all trackers (for diagnostics endpoint).\n */\n getStatus(): Record<string, {\n remaining: number | null;\n total: number | null;\n lastFetched: string | null;\n cacheKeys: number;\n }> {\n const result: Record<string, any> = {};\n\n for (const tracker of ['github', 'linear', 'rally']) {\n const limit = this.getRateLimit(tracker);\n const countStmt = this.db.prepare(\n 'SELECT COUNT(*) as cnt, MAX(last_fetched_at) as latest FROM api_cache WHERE tracker = ?'\n );\n const row = countStmt.get(tracker) as any;\n\n result[tracker] = {\n remaining: limit?.remaining ?? null,\n total: limit?.total ?? null,\n lastFetched: row?.latest ?? null,\n cacheKeys: row?.cnt ?? 0,\n };\n }\n\n return result;\n }\n\n /**\n * Close the database connection.\n */\n close(): void {\n this.l1.clear();\n this.db.close();\n }\n\n // --- L1 helpers ---\n\n private setL1(compositeKey: string, entry: L1Entry): void {\n // Evict oldest if at capacity\n if (this.l1.size >= this.l1MaxEntries && !this.l1.has(compositeKey)) {\n const oldestKey = this.l1.keys().next().value;\n if (oldestKey) this.l1.delete(oldestKey);\n }\n this.l1.set(compositeKey, entry);\n }\n}\n","/**\n * IssueDataService — Central orchestrator for issue data\n *\n * Replaces the inline fetching in /api/issues with:\n * - Background polling per tracker on independent timers\n * - GitHub REST + ETags (304s are FREE, don't count against rate limit)\n * - Linear incremental fetching via updatedAt filter\n * - Rally TTL-based caching\n * - Change detection + event store push (via onIssuesChanged callback)\n * - Adaptive backoff on rate limit pressure\n * - Instant serve from cache (sub-100ms dashboard loads)\n */\n\nimport { Octokit } from '@octokit/rest';\nimport { mapGitHubStateToCanonical } from '../../../core/state-mapping.js';\nimport { CacheService, DEFAULT_TTLS } from './cache-service.js';\nimport { getGitHubConfig, getLinearApiKey, getRallyConfig, validateRallyConfig } from './tracker-config.js';\nimport type { GitHubConfig, RallyConfig } from './tracker-config.js';\nimport { loadReviewStatuses } from '../../../lib/review-status.js';\n\n/**\n * Map a raw status string to its canonical state.\n * Exported for testing.\n */\nexport function getCanonicalStatus(status: string | undefined, stateType?: string): string {\n if (!status) return 'backlog';\n const normalized = status.toLowerCase();\n // Direct backlog mappings\n if (normalized === 'backlog' || normalized === 'triage' || normalized === 'unknown') {\n return 'backlog';\n }\n // Other canonical states\n if (normalized === 'todo' || normalized === 'to do' || normalized === 'ready' || normalized === 'unstarted') {\n return 'todo';\n }\n if (normalized === 'in progress' || normalized === 'started' || normalized === 'active' || normalized === 'in planning') {\n return 'in_progress';\n }\n if (normalized === 'in review' || normalized === 'review' || normalized === 'qa' || normalized === 'testing') {\n return 'in_review';\n }\n if (normalized === 'done' || normalized === 'completed' || normalized === 'closed') {\n return 'done';\n }\n if (normalized === 'canceled' || normalized === 'cancelled' || normalized === 'duplicate' || normalized === \"won't do\" || normalized === 'wontfix') {\n return 'canceled';\n }\n // Fallback: use Linear stateType if available (handles custom status names)\n if (stateType) {\n const typeMap: Record<string, string> = {\n backlog: 'backlog',\n unstarted: 'todo',\n started: 'in_progress',\n completed: 'done',\n canceled: 'canceled',\n cancelled: 'canceled',\n };\n if (typeMap[stateType]) return typeMap[stateType];\n }\n return 'backlog'; // Default fallback\n}\n\n// Poll intervals (ms)\nconst POLL_INTERVALS = {\n github: { default: 30_000, min: 15_000, max: 300_000 },\n linear: { default: 30_000, min: 15_000, max: 300_000 },\n rally: { default: 120_000, min: 60_000, max: 600_000 },\n};\n\n// Linear full refresh interval (safety net)\nconst LINEAR_FULL_REFRESH_MS = 5 * 60 * 1000; // 5 minutes\n\ninterface TrackerState {\n timer: ReturnType<typeof setTimeout> | null;\n currentInterval: number;\n lastFetchedIssues: any[];\n lastError: string | null;\n lastFetchedAt: string | null;\n}\n\n/**\n * Map normalized IssueState (open/in_progress/closed) to canonical dashboard status.\n * The Rally tracker already normalizes raw Rally states to IssueState in rally.ts.\n */\nfunction mapRallyStateToCanonical(issueState: string): string {\n if (!issueState) return 'todo';\n const stateLower = issueState.toLowerCase();\n if (stateLower === 'in_progress') return 'in_progress';\n if (stateLower === 'closed') return 'done';\n // 'open' and anything unrecognized → 'todo'\n return 'todo';\n}\n\nexport class IssueDataService {\n private cache: CacheService;\n private trackers: Record<string, TrackerState> = {};\n private linearLastFullRefresh = 0;\n private started = false;\n private shadowStateModule: any = null;\n private _onIssuesChanged: ((issues: unknown[]) => void) | null = null;\n\n /** Register a callback invoked whenever issue data changes (PAN-433). */\n onIssuesChanged(fn: (issues: unknown[]) => void): void {\n this._onIssuesChanged = fn;\n }\n\n constructor(cache: CacheService) {\n this.cache = cache;\n\n for (const tracker of ['github', 'linear', 'rally'] as const) {\n this.trackers[tracker] = {\n timer: null,\n currentInterval: POLL_INTERVALS[tracker].default,\n lastFetchedIssues: [],\n lastError: null,\n lastFetchedAt: null,\n };\n }\n }\n\n /**\n * Start background polling. Returns immediately after loading cached data.\n * API fetches run in the background and push incremental updates.\n */\n async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n\n // Pre-load shadow state module\n await this.ensureShadowStateLoaded();\n\n // Load any cached data from SQLite so getIssues() works instantly\n this.loadCachedData();\n\n // Push snapshot immediately with stale cached data so read model has\n // something to work with before the background fetches complete.\n this.pushSnapshot();\n\n // Kick off all tracker fetches in the background — do NOT await.\n // Each poll calls pushUpdated() when done → incremental client updates.\n void Promise.allSettled([\n this.pollGitHub(),\n this.pollLinear(),\n this.pollRally(),\n ]).then(() => {\n // Final snapshot push after all initial fetches complete\n this.pushSnapshot();\n // Start recurring timers (after first fetch completes)\n this.scheduleNext('github');\n this.scheduleNext('linear');\n this.scheduleNext('rally');\n });\n }\n\n /**\n * Stop all polling timers.\n */\n stop(): void {\n this.started = false;\n for (const state of Object.values(this.trackers)) {\n if (state.timer) {\n clearTimeout(state.timer);\n state.timer = null;\n }\n }\n }\n\n /**\n * Clear all cached issue data and trigger a fresh re-fetch from all trackers.\n */\n async clearCacheAndRefresh(): Promise<void> {\n // Clear SQLite + L1 cache for all trackers\n for (const tracker of ['github', 'linear', 'rally']) {\n this.cache.invalidate(tracker);\n this.trackers[tracker].lastFetchedIssues = [];\n this.trackers[tracker].lastFetchedAt = null;\n this.trackers[tracker].lastError = null;\n }\n console.log('[IssueDataService] Cache cleared — re-fetching all trackers');\n // Re-fetch all trackers\n await Promise.allSettled([\n this.pollGitHub(),\n this.pollLinear(),\n this.pollRally(),\n ]);\n this.pushSnapshot();\n }\n\n /**\n * Look up which tracker an issue belongs to by its identifier.\n * Returns 'github' | 'linear' | 'rally' | null.\n */\n getIssueSource(identifier: string): 'github' | 'linear' | 'rally' | null {\n const id = identifier.toLowerCase();\n for (const [trackerName, state] of Object.entries(this.trackers)) {\n for (const issue of state.lastFetchedIssues) {\n if ((issue.identifier || '').toLowerCase() === id) {\n return trackerName as 'github' | 'linear' | 'rally';\n }\n }\n }\n return null;\n }\n\n /**\n * Get all issues from cache. Applies shadow state and filtering.\n * This is the hot path — must be fast.\n */\n getIssues(options?: { cycle?: string; includeCompleted?: boolean }): any[] {\n let allIssues = [\n ...this.trackers.github.lastFetchedIssues,\n ...this.trackers.linear.lastFetchedIssues,\n ...this.trackers.rally.lastFetchedIssues,\n ];\n\n // Merge shadow state (module is pre-loaded by ensureShadowStateLoaded)\n try {\n if (this.shadowStateModule) {\n const shadowStates = this.shadowStateModule.listShadowedIssues();\n const shadowMap = new Map<string, any>();\n for (const state of shadowStates) {\n shadowMap.set(state.issueId.toLowerCase(), state);\n }\n\n allIssues = allIssues.map(issue => {\n const shadowState = shadowMap.get(issue.identifier.toLowerCase());\n if (shadowState) {\n return {\n ...issue,\n shadowStatus: shadowState.shadowStatus,\n targetCanonicalState: shadowState.targetCanonicalState,\n shadowedAt: shadowState.shadowedAt,\n shadowTrackerStatus: shadowState.trackerStatus,\n };\n }\n return { ...issue, shadowStatus: null, targetCanonicalState: null };\n });\n }\n } catch (e) {\n allIssues = allIssues.map(issue => ({ ...issue, shadowStatus: null }));\n }\n\n // Show all completed issues (label-based dismissal will be added later)\n\n // Apply cycle filter using canonical status mapping\n const cycle = options?.cycle ?? 'current';\n if (cycle === 'current') {\n // Current cycle: exclude Backlog and Canceled items, only show active cycle work\n allIssues = allIssues.filter(issue => {\n const canonical = getCanonicalStatus(issue.status);\n return canonical !== 'backlog' && canonical !== 'canceled';\n });\n } else if (cycle === 'backlog') {\n // Backlog view: only show Backlog items (including Triage, Unknown)\n allIssues = allIssues.filter(issue => {\n const canonical = getCanonicalStatus(issue.status, issue.stateType);\n return canonical === 'backlog';\n });\n } else if (cycle === 'canceled') {\n // Canceled view: only show Canceled items (Canceled, Duplicate, Won't Do)\n allIssues = allIssues.filter(issue => {\n const canonical = getCanonicalStatus(issue.status);\n return canonical === 'canceled';\n });\n }\n // cycle === 'all': no additional filtering, show everything\n\n // Augment with mergeStatus from review-status (used for MERGED badge)\n try {\n const reviewStatuses = loadReviewStatuses();\n allIssues = allIssues.map(issue => {\n const key = issue.identifier?.toUpperCase();\n const rs = key ? reviewStatuses[key] : null;\n if (rs?.mergeStatus) {\n return { ...issue, mergeStatus: rs.mergeStatus };\n }\n return issue;\n });\n } catch {\n // review-status.json may not exist yet\n }\n\n // Sort by updatedAt\n allIssues.sort((a, b) =>\n new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()\n );\n\n return allIssues;\n }\n\n /**\n * Invalidate a tracker's cache and trigger immediate re-poll.\n * Called after mutations (move-status, label changes, etc.)\n */\n /**\n * Immediately patch a cached issue and push the update to all clients.\n * Use this after any state mutation so the dashboard reflects the change\n * instantly without waiting for the next poll cycle.\n *\n * @param identifier - Issue identifier (e.g. \"MIN-734\", \"PAN-302\")\n * @param patch - Fields to merge into the cached issue object\n */\n patchIssue(identifier: string, patch: Record<string, any>): void {\n const id = identifier.toLowerCase();\n for (const state of Object.values(this.trackers)) {\n const idx = state.lastFetchedIssues.findIndex(\n (i: any) => (i.identifier || '').toLowerCase() === id\n );\n if (idx !== -1) {\n state.lastFetchedIssues[idx] = { ...state.lastFetchedIssues[idx], ...patch };\n this.pushUpdated();\n return;\n }\n }\n // Issue not in cache yet (e.g. just created) — trigger a full refresh instead\n const source = patch.source || 'linear';\n this.invalidateTracker(source).catch(() => {});\n }\n\n async invalidateTracker(tracker: string): Promise<void> {\n this.cache.invalidate(tracker);\n\n // Force full refresh on next poll (not incremental) so new issues\n // added to the cycle externally are discovered immediately\n if (tracker === 'linear') {\n this.linearLastFullRefresh = 0;\n }\n\n // Cancel current timer and fetch immediately\n const state = this.trackers[tracker];\n if (state?.timer) {\n clearTimeout(state.timer);\n state.timer = null;\n }\n\n switch (tracker) {\n case 'github': await this.pollGitHub(); break;\n case 'linear': await this.pollLinear(); break;\n case 'rally': await this.pollRally(); break;\n }\n\n this.pushSnapshot();\n this.scheduleNext(tracker);\n }\n\n /**\n * Get diagnostics for /api/cache-status endpoint.\n */\n getDiagnostics(): Record<string, any> {\n const result: Record<string, any> = {};\n for (const [tracker, state] of Object.entries(this.trackers)) {\n const limit = this.cache.getRateLimit(tracker);\n result[tracker] = {\n remaining: limit?.remaining ?? null,\n total: limit?.total ?? null,\n pollInterval: state.currentInterval,\n lastFetched: state.lastFetchedAt,\n lastError: state.lastError,\n issueCount: state.lastFetchedIssues.length,\n };\n }\n return result;\n }\n\n // ---------------------------------------------------------------\n // Private: polling methods\n // ---------------------------------------------------------------\n\n private scheduleNext(tracker: string): void {\n const state = this.trackers[tracker];\n if (!this.started || !state) return;\n\n // Compute effective interval with backoff\n const intervals = POLL_INTERVALS[tracker as keyof typeof POLL_INTERVALS];\n if (!intervals) return;\n\n const backoffMs = this.cache.getBackoffMs(tracker, intervals.default);\n const effectiveInterval = Math.min(\n Math.max(intervals.default + backoffMs, intervals.min),\n intervals.max\n );\n state.currentInterval = effectiveInterval;\n\n state.timer = setTimeout(async () => {\n try {\n switch (tracker) {\n case 'github': await this.pollGitHub(); break;\n case 'linear': await this.pollLinear(); break;\n case 'rally': await this.pollRally(); break;\n }\n } catch (err: any) {\n console.error(`[IssueDataService] Error polling ${tracker}:`, err.message);\n state.lastError = err.message;\n }\n this.scheduleNext(tracker);\n }, effectiveInterval);\n }\n\n private async ensureShadowStateLoaded(): Promise<void> {\n if (this.shadowStateModule) return;\n try {\n this.shadowStateModule = await import('../../../lib/shadow-state.js');\n } catch {\n // Shadow state not available — issues will work without it\n }\n }\n\n private loadCachedData(): void {\n // Build a lookup of repo → prefix from current config for re-stamping stale identifiers\n const repoPrefixMap = new Map<string, string>();\n try {\n const ghConfig = getGitHubConfig();\n if (ghConfig) {\n for (const { owner, repo, prefix } of ghConfig.repos) {\n repoPrefixMap.set(`${owner}/${repo}`, prefix || repo.toUpperCase());\n }\n }\n } catch { /* ignore */ }\n\n for (const tracker of ['github', 'linear', 'rally']) {\n const cached = this.cache.getStale(tracker, 'issues');\n if (cached?.data) {\n // Sanitize stale Rally cache: rawTrackerState may be an object from pre-PAN-201 data\n if (tracker === 'rally') {\n let sanitizedCount = 0;\n cached.data = cached.data.map((issue: any) => {\n if (typeof issue.rawTrackerState === 'object' && issue.rawTrackerState !== null) {\n sanitizedCount++;\n return {\n ...issue,\n rawTrackerState: issue.rawTrackerState.Name || issue.rawTrackerState._refObjectName || 'Defined',\n };\n }\n return issue;\n });\n if (sanitizedCount > 0) {\n console.warn(`[IssueDataService] Rally cache: sanitized ${sanitizedCount} issues with object rawTrackerState (PAN-201)`);\n }\n }\n // Re-stamp GitHub identifiers in case prefix config changed since cache was written\n if (tracker === 'github') {\n cached.data = cached.data.map((issue: any) => {\n const repoKey = issue.sourceRepo;\n const prefix = repoKey ? repoPrefixMap.get(repoKey) : undefined;\n if (prefix) {\n // Extract issue number from id (github-owner-repo-NUMBER) or identifier (PREFIX-NUMBER)\n const issueNum = issue.id?.match(/-(\\d+)$/)?.[1] || issue.identifier?.match(/-(\\d+)$/)?.[1];\n if (issueNum) {\n const expectedId = `${prefix}-${issueNum}`;\n if (issue.identifier !== expectedId) {\n return { ...issue, identifier: expectedId };\n }\n }\n }\n return issue;\n });\n }\n this.trackers[tracker].lastFetchedIssues = cached.data;\n this.trackers[tracker].lastFetchedAt = cached.lastFetchedAt;\n }\n }\n }\n\n private pushSnapshot(): void {\n this._onIssuesChanged?.(this.getIssues());\n }\n\n private pushUpdated(): void {\n this._onIssuesChanged?.(this.getIssues());\n }\n\n private pushMeta(): void {\n // Diagnostics are served via GET /api/issues/diagnostics — no push needed\n }\n\n // ---------------------------------------------------------------\n // GitHub polling — uses Octokit REST + ETags (304 = FREE)\n // ---------------------------------------------------------------\n\n private async pollGitHub(): Promise<void> {\n const config = getGitHubConfig();\n if (!config) {\n this.trackers.github.lastFetchedIssues = [];\n return;\n }\n\n const allIssues: any[] = [];\n const octokit = new Octokit({ auth: config.token });\n\n for (const { owner, repo, prefix } of config.repos) {\n try {\n // Fetch open issues with ETag support\n const openIssues = await this.fetchGitHubRepoIssues(\n octokit, owner, repo, 'open', prefix || repo.toUpperCase().replace(/-CLI$/, '').replace(/-/g, ''),\n `github:open:${owner}/${repo}`\n );\n\n // Fetch recently closed issues\n const closedIssues = await this.fetchGitHubRepoIssues(\n octokit, owner, repo, 'closed', prefix || repo.toUpperCase().replace(/-CLI$/, '').replace(/-/g, ''),\n `github:closed:${owner}/${repo}`\n );\n\n allIssues.push(...openIssues, ...closedIssues);\n } catch (error: any) {\n console.error(`[IssueDataService] Error fetching GitHub issues for ${owner}/${repo}:`, error.message);\n this.trackers.github.lastError = error.message;\n }\n }\n\n // Check if data actually changed\n const oldData = this.trackers.github.lastFetchedIssues;\n const changed = JSON.stringify(allIssues) !== JSON.stringify(oldData);\n\n this.trackers.github.lastFetchedIssues = allIssues;\n this.trackers.github.lastFetchedAt = new Date().toISOString();\n this.trackers.github.lastError = null;\n\n // Persist to cache\n this.cache.set('github', 'issues', allIssues, { ttlSeconds: DEFAULT_TTLS.github });\n\n if (changed) {\n console.log(`[IssueDataService] GitHub: ${allIssues.length} issues (changed)`);\n this.pushUpdated();\n this.pushMeta();\n }\n }\n\n private async fetchGitHubRepoIssues(\n octokit: Octokit,\n owner: string,\n repo: string,\n state: 'open' | 'closed',\n issuePrefix: string,\n cacheKey: string,\n ): Promise<any[]> {\n // Get stored ETag for conditional request\n const cachedEtag = this.cache.getEtag('github', cacheKey);\n\n const requestParams: any = {\n owner,\n repo,\n state,\n per_page: 100,\n sort: 'updated' as const,\n direction: 'desc' as const,\n };\n\n // Only send If-None-Match when we have a cached ETag\n if (cachedEtag) {\n requestParams.headers = { 'If-None-Match': cachedEtag };\n }\n\n // Fetch ALL closed issues (no date filter) so Done column is complete after restarts\n if (state === 'closed') {\n requestParams.per_page = 100;\n }\n\n try {\n // Use paginate to fetch ALL pages (not just the first 100)\n let newEtag: string | undefined;\n const allData = await octokit.paginate(octokit.issues.listForRepo, requestParams, (response) => {\n // Extract rate limit from each response\n const remaining = parseInt(response.headers['x-ratelimit-remaining'] as string);\n const total = parseInt(response.headers['x-ratelimit-limit'] as string);\n const resetAt = new Date(parseInt(response.headers['x-ratelimit-reset'] as string) * 1000).toISOString();\n\n if (!isNaN(remaining) && !isNaN(total)) {\n this.cache.updateRateLimit('github', { remaining, total, resetAt });\n }\n\n // Store ETag from first page (used for conditional requests on next poll)\n if (!newEtag && response.headers.etag) {\n newEtag = response.headers.etag as string;\n }\n\n return response.data;\n });\n\n // Filter out PRs (they have pull_request key)\n const issues = allData.filter((issue: any) => !issue.pull_request);\n\n // Format issues to match dashboard schema\n const formatted = issues.map((issue: any) => {\n const labelNames = issue.labels?.map((l: any) => typeof l === 'string' ? l : l.name) || [];\n const canonicalStatus = mapGitHubStateToCanonical(issue.state || '', labelNames);\n const identifier = `${issuePrefix}-${issue.number}`;\n\n const firstAssignee = issue.assignees?.[0] || issue.assignee;\n\n return {\n id: `github-${owner}-${repo}-${issue.number}`,\n identifier,\n title: issue.title,\n description: issue.body || '',\n status: canonicalStatus === 'todo' ? 'Todo' :\n canonicalStatus === 'in_progress' ? 'In Progress' :\n canonicalStatus === 'in_review' ? 'In Review' :\n canonicalStatus === 'done' ? 'Done' :\n canonicalStatus === 'backlog' ? 'Backlog' : 'Todo',\n canonicalStatus,\n state: canonicalStatus,\n priority: labelNames.some((l: string) => l.includes('priority') && l.includes('high')) ? 2 :\n labelNames.some((l: string) => l.includes('priority') && l.includes('urgent')) ? 1 :\n labelNames.some((l: string) => l.includes('priority') && l.includes('low')) ? 4 : 3,\n assignee: firstAssignee ? {\n name: firstAssignee.login,\n email: `${firstAssignee.login}@github`,\n } : undefined,\n labels: labelNames,\n url: issue.html_url,\n createdAt: issue.created_at,\n updatedAt: issue.updated_at,\n completedAt: issue.closed_at,\n project: {\n id: `github-${owner}-${repo}`,\n name: `${owner}/${repo}`,\n color: '#333',\n icon: 'github',\n },\n source: 'github',\n sourceRepo: `${owner}/${repo}`,\n };\n });\n\n // Cache with ETag\n this.cache.set('github', cacheKey, formatted, {\n etag: newEtag,\n ttlSeconds: DEFAULT_TTLS.github,\n });\n\n return formatted;\n } catch (err: any) {\n // 304 Not Modified — return cached data (this is FREE, no rate limit cost)\n if (err.status === 304) {\n const cached = this.cache.getStale('github', cacheKey);\n if (!cached?.data) return [];\n // Re-stamp identifiers in case the prefix changed since the cache was written\n return cached.data.map((issue: any) => {\n const issueNum = issue.id?.match(/-(\\d+)$/)?.[1] || issue.identifier?.match(/-(\\d+)$/)?.[1];\n if (issueNum) {\n const expectedId = `${issuePrefix}-${issueNum}`;\n return issue.identifier === expectedId ? issue : { ...issue, identifier: expectedId };\n }\n return issue;\n });\n }\n throw err;\n }\n }\n\n // ---------------------------------------------------------------\n // Linear polling — TTL + incremental updatedAt\n // ---------------------------------------------------------------\n\n private async pollLinear(): Promise<void> {\n const apiKey = getLinearApiKey();\n if (!apiKey) {\n this.trackers.linear.lastFetchedIssues = [];\n return;\n }\n\n const now = Date.now();\n const needsFullRefresh = now - this.linearLastFullRefresh > LINEAR_FULL_REFRESH_MS;\n\n // Get the most recent updatedAt from cached issues for incremental fetch\n let sinceUpdatedAt: string | null = null;\n if (!needsFullRefresh && this.trackers.linear.lastFetchedIssues.length > 0) {\n const maxUpdated = this.trackers.linear.lastFetchedIssues.reduce((max: string, issue: any) => {\n return issue.updatedAt > max ? issue.updatedAt : max;\n }, '');\n if (maxUpdated) sinceUpdatedAt = maxUpdated;\n }\n\n try {\n const fetchedIssues = await this.fetchLinearIssues(apiKey, sinceUpdatedAt);\n\n let allIssues: any[];\n if (sinceUpdatedAt && fetchedIssues.length > 0) {\n // Incremental: merge new/updated issues into existing list\n const existingMap = new Map(\n this.trackers.linear.lastFetchedIssues.map((i: any) => [i.identifier, i])\n );\n for (const issue of fetchedIssues) {\n existingMap.set(issue.identifier, issue);\n }\n allIssues = Array.from(existingMap.values());\n } else if (needsFullRefresh || sinceUpdatedAt === null) {\n // Full refresh\n allIssues = fetchedIssues;\n this.linearLastFullRefresh = now;\n } else {\n // No new data from incremental fetch\n allIssues = this.trackers.linear.lastFetchedIssues;\n }\n\n const oldData = this.trackers.linear.lastFetchedIssues;\n const changed = JSON.stringify(allIssues) !== JSON.stringify(oldData);\n\n this.trackers.linear.lastFetchedIssues = allIssues;\n this.trackers.linear.lastFetchedAt = new Date().toISOString();\n this.trackers.linear.lastError = null;\n\n this.cache.set('linear', 'issues', allIssues, { ttlSeconds: DEFAULT_TTLS.linear });\n\n if (changed) {\n console.log(`[IssueDataService] Linear: ${allIssues.length} issues (changed)`);\n this.pushUpdated();\n this.pushMeta();\n }\n } catch (err: any) {\n console.error('[IssueDataService] Linear poll error:', err.message);\n this.trackers.linear.lastError = err.message;\n }\n }\n\n private async fetchLinearIssues(apiKey: string, sinceUpdatedAt: string | null): Promise<any[]> {\n const allIssues: any[] = [];\n let hasMore = true;\n let cursor: string | undefined;\n\n // Build filter conditions\n const filterConditions: string[] = [];\n // Scope to active cycle only — completed/canceled filtering is handled\n // by getIssues() post-filter, NOT here. The GraphQL query must fetch\n // completed issues so that: (1) the \"Include completed\" toggle works,\n // (2) incremental updates correctly reflect state transitions, and\n // (3) internal getIssues() callers can look up recently-completed issues.\n filterConditions.push('cycle: { isActive: { eq: true } }');\n\n // Incremental: only issues updated after sinceUpdatedAt\n if (sinceUpdatedAt) {\n filterConditions.push(`updatedAt: { gt: \"${sinceUpdatedAt}\" }`);\n }\n\n let filterClause = '';\n if (filterConditions.length === 1) {\n filterClause = `filter: { ${filterConditions[0]} }`;\n } else if (filterConditions.length > 1) {\n filterClause = `filter: { and: [${filterConditions.map(c => `{ ${c} }`).join(', ')}] }`;\n }\n\n while (hasMore) {\n const query = `\n query GetIssues($after: String) {\n issues(first: 100, after: $after, ${filterClause ? filterClause + ', ' : ''}orderBy: updatedAt) {\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n id\n identifier\n title\n description\n priority\n url\n createdAt\n updatedAt\n completedAt\n state {\n name\n type\n }\n assignee {\n name\n email\n }\n labels {\n nodes {\n name\n }\n }\n project {\n id\n name\n color\n icon\n }\n team {\n id\n name\n color\n icon\n }\n cycle {\n id\n name\n number\n }\n }\n }\n }\n `;\n\n const response = await fetch('https://api.linear.app/graphql', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': apiKey,\n },\n body: JSON.stringify({ query, variables: { after: cursor } }),\n });\n\n const json = await response.json();\n\n if (json.errors) {\n throw new Error(json.errors[0]?.message || 'Linear GraphQL error');\n }\n\n const issues = json.data?.issues;\n if (!issues) break;\n\n allIssues.push(...issues.nodes);\n hasMore = issues.pageInfo.hasNextPage;\n cursor = issues.pageInfo.endCursor;\n\n if (allIssues.length > 1000) break;\n }\n\n // Build project lookup for deduplication\n const projectByName = new Map<string, { id: string; name: string; color?: string; icon?: string }>();\n for (const issue of allIssues) {\n if (issue.project && !projectByName.has(issue.project.name)) {\n projectByName.set(issue.project.name, {\n id: issue.project.id,\n name: issue.project.name,\n color: issue.project.color,\n icon: issue.project.icon,\n });\n }\n }\n\n // Format to dashboard schema\n return allIssues.map((issue: any) => {\n let project;\n if (issue.project) {\n project = {\n id: issue.project.id,\n name: issue.project.name,\n color: issue.project.color,\n icon: issue.project.icon,\n };\n } else if (issue.team) {\n const existing = projectByName.get(issue.team.name);\n project = existing || {\n id: issue.team.id,\n name: issue.team.name,\n color: issue.team.color,\n icon: issue.team.icon,\n };\n }\n\n return {\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n description: issue.description,\n status: issue.state?.name || 'Backlog',\n stateType: issue.state?.type,\n priority: issue.priority,\n assignee: issue.assignee ? { name: issue.assignee.name, email: issue.assignee.email } : undefined,\n labels: issue.labels?.nodes?.map((l: any) => l.name) || [],\n url: issue.url,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n completedAt: issue.completedAt,\n project,\n cycle: issue.cycle ? {\n id: issue.cycle.id,\n name: issue.cycle.name,\n number: issue.cycle.number,\n } : undefined,\n source: 'linear',\n };\n });\n }\n\n // ---------------------------------------------------------------\n // Rally polling — TTL-based caching, per-project config support\n // ---------------------------------------------------------------\n\n /**\n * Format a raw Rally issue into the dashboard schema.\n */\n private formatRallyIssue(issue: any, projectInfo: { id: string; name: string; color: string; icon: string }): any {\n const canonicalStatus = mapRallyStateToCanonical(issue.state);\n const identifier = issue.ref || issue.id || 'unknown';\n if (typeof issue.rawState === 'object' && issue.rawState !== null) {\n console.warn(`[IssueDataService] Rally ${identifier}: rawState is object, normalizing (PAN-201)`);\n }\n return {\n id: `rally-${issue.id || identifier}`,\n identifier,\n title: issue.title || '',\n description: issue.description || '',\n status: canonicalStatus === 'todo' ? 'Todo' :\n canonicalStatus === 'in_progress' ? 'In Progress' :\n canonicalStatus === 'done' ? 'Done' : 'Todo',\n priority: issue.priority ?? 3,\n assignee: issue.assignee ? {\n name: issue.assignee,\n email: `${issue.assignee.replace(/\\s+/g, '.').toLowerCase()}@rally`,\n } : undefined,\n labels: Array.isArray(issue.labels) ? issue.labels.filter((l: any) => typeof l === 'string') : [],\n url: issue.url || '',\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n parentRef: issue.parentRef,\n artifactType: issue.artifactType,\n rawTrackerState: typeof issue.rawState === 'object' && issue.rawState !== null\n ? (issue.rawState.Name || issue.rawState._refObjectName || 'Defined')\n : issue.rawState,\n project: projectInfo,\n source: 'rally',\n };\n }\n\n /**\n * Compute derived feature status from child stories.\n * If ANY child is in progress, the feature is derived as 'in_progress'.\n * If ALL children are done, the feature is derived as 'closed'.\n * Attaches child counts for progress display.\n */\n private computeDerivedFeatureStatus(issues: any[]): any[] {\n // Build children-by-parent map (key: parent identifier, value: child issues)\n const childrenByParent = new Map<string, any[]>();\n for (const issue of issues) {\n if (issue.parentRef) {\n const existing = childrenByParent.get(issue.parentRef) || [];\n existing.push(issue);\n childrenByParent.set(issue.parentRef, existing);\n }\n }\n\n // For each Feature, compute derived status\n return issues.map(issue => {\n const isFeature = issue.artifactType?.includes('PortfolioItem');\n if (!isFeature) return issue;\n\n const children = childrenByParent.get(issue.identifier) || [];\n if (children.length === 0) return issue;\n\n const completedChildCount = children.filter(\n (c: any) => c.status === 'Done'\n ).length;\n const inProgressChildCount = children.filter(\n (c: any) => c.status === 'In Progress'\n ).length;\n const totalChildCount = children.length;\n\n let derivedStatus: string | undefined;\n if (completedChildCount === totalChildCount) {\n derivedStatus = 'closed';\n } else if (inProgressChildCount > 0) {\n derivedStatus = 'in_progress';\n }\n\n return {\n ...issue,\n derivedStatus,\n totalChildCount,\n completedChildCount,\n inProgressChildCount,\n };\n });\n }\n\n private async pollRally(): Promise<void> {\n const globalConfig = getRallyConfig();\n if (!globalConfig) {\n this.trackers.rally.lastFetchedIssues = [];\n return;\n }\n\n // Validate config on first poll and log warnings\n if (!this.trackers.rally.lastFetchedAt) {\n const validation = validateRallyConfig(globalConfig);\n if (validation.warnings.length > 0) {\n console.warn('[Rally] Configuration warnings:', validation.warnings.join('; '));\n }\n }\n\n // Only fetch if cache is stale\n if (!this.cache.isStale('rally', 'issues') && this.trackers.rally.lastFetchedIssues.length > 0) {\n return;\n }\n\n try {\n const { RallyTracker } = await import('../../../lib/tracker/rally.js');\n const { findProjectsByRallyProject } = await import('../../../lib/projects.js');\n\n const rallyProjects = findProjectsByRallyProject();\n let allFormatted: any[] = [];\n\n if (rallyProjects.length > 0) {\n // Per-project mode: create separate tracker per Rally project OID\n const projectQueries = rallyProjects.map(async ({ key, config: projConfig }) => {\n try {\n const tracker = new RallyTracker({\n apiKey: globalConfig.apiKey,\n server: globalConfig.server,\n workspace: globalConfig.workspace,\n project: projConfig.rally_project,\n });\n\n const issues = await tracker.listIssues({\n includeClosed: false,\n limit: 100,\n });\n\n const projectInfo = {\n id: `rally-${key}`,\n name: projConfig.name,\n color: '#00C7B1',\n icon: 'rally',\n };\n\n return issues.map((issue: any) => this.formatRallyIssue(issue, projectInfo));\n } catch (err: any) {\n console.error(`[IssueDataService] Rally poll error for project ${key}:`, err.message);\n return [];\n }\n });\n\n const results = await Promise.all(projectQueries);\n allFormatted = results.flat();\n } else {\n // Fallback: use global RALLY_PROJECT env (backward compat)\n const tracker = new RallyTracker({\n apiKey: globalConfig.apiKey,\n server: globalConfig.server,\n workspace: globalConfig.workspace,\n project: globalConfig.project,\n });\n\n const issues = await tracker.listIssues({\n includeClosed: false,\n limit: 100,\n });\n\n const projectInfo = {\n id: 'rally-project',\n name: 'Rally',\n color: '#00C7B1',\n icon: 'rally',\n };\n\n allFormatted = issues.map((issue: any) => this.formatRallyIssue(issue, projectInfo));\n }\n\n // Compute derived feature status from child stories\n allFormatted = this.computeDerivedFeatureStatus(allFormatted);\n\n const oldData = this.trackers.rally.lastFetchedIssues;\n const changed = JSON.stringify(allFormatted) !== JSON.stringify(oldData);\n\n this.trackers.rally.lastFetchedIssues = allFormatted;\n this.trackers.rally.lastFetchedAt = new Date().toISOString();\n this.trackers.rally.lastError = null;\n\n this.cache.set('rally', 'issues', allFormatted, { ttlSeconds: DEFAULT_TTLS.rally });\n\n if (changed) {\n console.log(`[IssueDataService] Rally: ${allFormatted.length} issues (changed)`);\n this.pushUpdated();\n this.pushMeta();\n }\n } catch (err: any) {\n const errorMsg = err.message?.includes('Could not parse')\n ? `${err.message} - Check Rally workspace/project configuration. Enable DEBUG=rally for query details.`\n : err.message;\n console.error('[IssueDataService] Rally poll error:', errorMsg);\n this.trackers.rally.lastError = errorMsg;\n }\n }\n}\n","/**\n * Shared IssueDataService singleton — used by read model (bootstrap),\n * route handlers (force-refresh, diagnostics), and event emission.\n */\nimport { IssueDataService } from './issue-data-service.js';\nimport { CacheService } from './cache-service.js';\n\nlet _service: IssueDataService | null = null;\nlet _startPromise: Promise<void> | null = null;\n\nexport function getSharedIssueService(): IssueDataService {\n if (!_service) {\n _service = new IssueDataService(new CacheService());\n }\n return _service;\n}\n\nexport function startSharedIssueService(): Promise<void> {\n if (_startPromise) return _startPromise;\n _startPromise = getSharedIssueService().start().catch((err: unknown) => {\n console.error('[issue-service-singleton] start failed:', err);\n });\n return _startPromise;\n}\n"],"mappings":"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"}
@@ -3,5 +3,5 @@ import { t as archivePlanning } from "./archive-planning-h-hAjk0P.js";
3
3
  import { t as cleanPlanningArtifacts } from "./clean-planning-qafw99vY.js";
4
4
  import { t as closeIssue } from "./close-issue-DfIggeZD.js";
5
5
  import { t as compactBeads } from "./compact-beads-Dt0qTqsC.js";
6
- import { a as teardownWorkspace, i as deepWipe, n as close, r as closeOut, t as approve } from "./workflows-Cj6tzch6.js";
6
+ import { a as teardownWorkspace, i as deepWipe, n as close, r as closeOut, t as approve } from "./workflows-N1UTipYl.js";
7
7
  export { approve, archivePlanning, cleanPlanningArtifacts, close, closeIssue, closeOut, compactBeads, deepWipe, stepFailed, stepOk, stepSkipped, teardownWorkspace };
@@ -1,2 +1,2 @@
1
- import { a as scanForConflictMarkers, c as spawnRebaseAgentForBranch, i as runProjectQualityGates, l as syncMainIntoWorkspace, n as postMergeLifecycle, o as spawnMergeAgent, r as resetPostMergeState, s as spawnMergeAgentForBranches, t as notifyTldrDaemon } from "./merge-agent-CmqR1MFf.js";
1
+ import { a as scanForConflictMarkers, c as spawnRebaseAgentForBranch, i as runProjectQualityGates, l as syncMainIntoWorkspace, n as postMergeLifecycle, o as spawnMergeAgent, r as resetPostMergeState, s as spawnMergeAgentForBranches, t as notifyTldrDaemon } from "./merge-agent-twroFuAh.js";
2
2
  export { notifyTldrDaemon, postMergeLifecycle, resetPostMergeState, runProjectQualityGates, scanForConflictMarkers, spawnMergeAgent, spawnMergeAgentForBranches, spawnRebaseAgentForBranch, syncMainIntoWorkspace };
@@ -1,6 +1,6 @@
1
1
  import { g as init_paths, s as PANOPTICON_HOME } from "./paths-COdEvoXR.js";
2
2
  import { g as loadProjectsConfig, p as init_projects, s as getIssuePrefix, y as resolveProjectFromIssue } from "./projects-Cq3TWdPS.js";
3
- import { H as recordWake, I as isRunning, O as getTmuxSessionName, Y as spawnEphemeralSpecialist, j as init_specialists, rt as wakeSpecialist } from "./specialists-C7Fyhq_j.js";
3
+ import { H as recordWake, I as isRunning, O as getTmuxSessionName, Y as spawnEphemeralSpecialist, j as init_specialists, rt as wakeSpecialist } from "./specialists-C6s3U6tX.js";
4
4
  import { n as init_workspace_config, r as replacePlaceholders } from "./workspace-config-cmp5_ipD.js";
5
5
  import { l as sendKeysAsync, o as init_tmux, u as sessionExists } from "./tmux-IlN1Slv-.js";
6
6
  import { a as init_config, o as loadConfig } from "./config-CUREjHP7.js";
@@ -1991,4 +1991,4 @@ async function runProjectQualityGates(projectPath, phase) {
1991
1991
  //#endregion
1992
1992
  export { scanForConflictMarkers as a, spawnRebaseAgentForBranch as c, runQualityGates as d, resolveGitHubIssue as f, runProjectQualityGates as i, syncMainIntoWorkspace as l, postMergeLifecycle as n, spawnMergeAgent as o, resolveTrackerType as p, resetPostMergeState as r, spawnMergeAgentForBranches as s, notifyTldrDaemon as t, DEFAULT_GATES as u };
1993
1993
 
1994
- //# sourceMappingURL=merge-agent-CmqR1MFf.js.map
1994
+ //# sourceMappingURL=merge-agent-twroFuAh.js.map