panopticon-cli 0.5.8 → 0.5.10

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 (100) hide show
  1. package/README.md +29 -83
  2. package/dist/{agents-I6RAEGL5.js → agents-MOMDECON.js} +8 -6
  3. package/dist/{archive-planning-U3AZAKWI.js → archive-planning-54J6EP6A.js} +3 -3
  4. package/dist/{chunk-UKSGE6RH.js → chunk-3KYTNMSE.js} +1 -2
  5. package/dist/{chunk-UKSGE6RH.js.map → chunk-3KYTNMSE.js.map} +1 -1
  6. package/dist/{chunk-M6ZVVKZ3.js → chunk-4OQ4SXQZ.js} +219 -107
  7. package/dist/chunk-4OQ4SXQZ.js.map +1 -0
  8. package/dist/{chunk-ZMJFEHGF.js → chunk-7ZB5D46Y.js} +2 -2
  9. package/dist/{chunk-ZMJFEHGF.js.map → chunk-7ZB5D46Y.js.map} +1 -1
  10. package/dist/{chunk-BYWVPPAZ.js → chunk-BHRMW7BY.js} +31 -4
  11. package/dist/chunk-BHRMW7BY.js.map +1 -0
  12. package/dist/{chunk-WEQW3EAT.js → chunk-F4XS2FQN.js} +3 -2
  13. package/dist/chunk-F4XS2FQN.js.map +1 -0
  14. package/dist/{chunk-OJF4QS3S.js → chunk-GIW2TUWI.js} +2 -2
  15. package/dist/{chunk-SUM2WVPF.js → chunk-H7T35QDO.js} +30 -12
  16. package/dist/chunk-H7T35QDO.js.map +1 -0
  17. package/dist/{chunk-MJXYTGK5.js → chunk-JZWCL5S5.js} +2 -2
  18. package/dist/{chunk-ZN5RHWGR.js → chunk-PFA5XE2V.js} +5 -41
  19. package/dist/chunk-PFA5XE2V.js.map +1 -0
  20. package/dist/{chunk-6OYUJ4AJ.js → chunk-R47UJWF6.js} +2 -2
  21. package/dist/{chunk-NYOGHGIW.js → chunk-RCYJK3ZC.js} +10 -9
  22. package/dist/chunk-RCYJK3ZC.js.map +1 -0
  23. package/dist/{chunk-R4KPLLRB.js → chunk-SFX3BG6N.js} +1 -1
  24. package/dist/chunk-SFX3BG6N.js.map +1 -0
  25. package/dist/{chunk-IZIXJYXZ.js → chunk-TA5X4QYQ.js} +6 -2
  26. package/dist/{chunk-IZIXJYXZ.js.map → chunk-TA5X4QYQ.js.map} +1 -1
  27. package/dist/{chunk-43F4LDZ4.js → chunk-VVTAPQOI.js} +2 -2
  28. package/dist/{chunk-YAAT66RT.js → chunk-WP6ZLWU3.js} +28 -3
  29. package/dist/chunk-WP6ZLWU3.js.map +1 -0
  30. package/dist/clean-planning-V4SSVU26.js +9 -0
  31. package/dist/cli/index.js +1654 -1056
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/close-issue-5OMOP2FU.js +9 -0
  34. package/dist/compact-beads-YQDVF6FQ.js +9 -0
  35. package/dist/dashboard/prompts/inspect-agent.md +157 -0
  36. package/dist/dashboard/prompts/merge-agent.md +11 -0
  37. package/dist/dashboard/prompts/review-agent.md +9 -0
  38. package/dist/dashboard/prompts/test-agent.md +9 -0
  39. package/dist/dashboard/prompts/uat-agent.md +215 -0
  40. package/dist/dashboard/prompts/work-agent.md +53 -5
  41. package/dist/dashboard/public/assets/index-5hYjhhGn.js +826 -0
  42. package/dist/dashboard/public/assets/index-DIFh3T1V.css +32 -0
  43. package/dist/dashboard/public/index.html +3 -6
  44. package/dist/dashboard/server.js +3338 -2033
  45. package/dist/factory-KKT7324R.js +20 -0
  46. package/dist/{feedback-writer-T2WCT6EZ.js → feedback-writer-IPPIUPDX.js} +2 -2
  47. package/dist/feedback-writer-IPPIUPDX.js.map +1 -0
  48. package/dist/index.d.ts +8 -3
  49. package/dist/index.js +19 -19
  50. package/dist/{label-cleanup-4HJVX6NP.js → label-cleanup-4IVZIPGK.js} +2 -2
  51. package/dist/{merge-agent-ZITLVF2B.js → merge-agent-6YOMGQMX.js} +16 -16
  52. package/dist/{projects-3CRF57ZU.js → projects-BPGM6IFB.js} +2 -2
  53. package/dist/{remote-workspace-M4IULGFZ.js → remote-workspace-LKRDGYEB.js} +2 -2
  54. package/dist/{review-status-J2YJGL3E.js → review-status-E77PZZWG.js} +2 -2
  55. package/dist/{specialist-context-W25PPWM4.js → specialist-context-GVF4DV3M.js} +5 -5
  56. package/dist/{specialist-logs-KPC45SZN.js → specialist-logs-W47SAAIU.js} +5 -5
  57. package/dist/{specialists-H4LGYR7R.js → specialists-SIXRWCZ3.js} +5 -5
  58. package/dist/{traefik-QXLZ4PO2.js → traefik-X2IWTUHO.js} +3 -3
  59. package/dist/{workspace-manager-G6TTBPC3.js → workspace-manager-Z57ROWBQ.js} +2 -2
  60. package/dist/workspace-manager-Z57ROWBQ.js.map +1 -0
  61. package/package.json +1 -1
  62. package/scripts/inspect-on-bead-close +73 -0
  63. package/scripts/stop-hook +17 -0
  64. package/skills/pan-new-project/SKILL.md +1 -1
  65. package/skills/pan-oversee/SKILL.md +45 -10
  66. package/skills/plan/SKILL.md +336 -0
  67. package/dist/chunk-BYWVPPAZ.js.map +0 -1
  68. package/dist/chunk-M6ZVVKZ3.js.map +0 -1
  69. package/dist/chunk-NYOGHGIW.js.map +0 -1
  70. package/dist/chunk-R4KPLLRB.js.map +0 -1
  71. package/dist/chunk-SUM2WVPF.js.map +0 -1
  72. package/dist/chunk-WEQW3EAT.js.map +0 -1
  73. package/dist/chunk-YAAT66RT.js.map +0 -1
  74. package/dist/chunk-ZN5RHWGR.js.map +0 -1
  75. package/dist/clean-planning-7Z5YY64X.js +0 -9
  76. package/dist/close-issue-CTZK777I.js +0 -9
  77. package/dist/compact-beads-72SHALOL.js +0 -9
  78. package/dist/dashboard/public/assets/index-Bx4NCn9A.css +0 -32
  79. package/dist/dashboard/public/assets/index-C7hJ5-o1.js +0 -756
  80. package/dist/feedback-writer-T2WCT6EZ.js.map +0 -1
  81. package/skills/opus-plan/SKILL.md +0 -400
  82. /package/dist/{agents-I6RAEGL5.js.map → agents-MOMDECON.js.map} +0 -0
  83. /package/dist/{archive-planning-U3AZAKWI.js.map → archive-planning-54J6EP6A.js.map} +0 -0
  84. /package/dist/{chunk-OJF4QS3S.js.map → chunk-GIW2TUWI.js.map} +0 -0
  85. /package/dist/{chunk-MJXYTGK5.js.map → chunk-JZWCL5S5.js.map} +0 -0
  86. /package/dist/{chunk-6OYUJ4AJ.js.map → chunk-R47UJWF6.js.map} +0 -0
  87. /package/dist/{chunk-43F4LDZ4.js.map → chunk-VVTAPQOI.js.map} +0 -0
  88. /package/dist/{clean-planning-7Z5YY64X.js.map → clean-planning-V4SSVU26.js.map} +0 -0
  89. /package/dist/{close-issue-CTZK777I.js.map → close-issue-5OMOP2FU.js.map} +0 -0
  90. /package/dist/{compact-beads-72SHALOL.js.map → compact-beads-YQDVF6FQ.js.map} +0 -0
  91. /package/dist/{projects-3CRF57ZU.js.map → factory-KKT7324R.js.map} +0 -0
  92. /package/dist/{label-cleanup-4HJVX6NP.js.map → label-cleanup-4IVZIPGK.js.map} +0 -0
  93. /package/dist/{merge-agent-ZITLVF2B.js.map → merge-agent-6YOMGQMX.js.map} +0 -0
  94. /package/dist/{review-status-J2YJGL3E.js.map → projects-BPGM6IFB.js.map} +0 -0
  95. /package/dist/{remote-workspace-M4IULGFZ.js.map → remote-workspace-LKRDGYEB.js.map} +0 -0
  96. /package/dist/{specialist-logs-KPC45SZN.js.map → review-status-E77PZZWG.js.map} +0 -0
  97. /package/dist/{specialist-context-W25PPWM4.js.map → specialist-context-GVF4DV3M.js.map} +0 -0
  98. /package/dist/{specialists-H4LGYR7R.js.map → specialist-logs-W47SAAIU.js.map} +0 -0
  99. /package/dist/{traefik-QXLZ4PO2.js.map → specialists-SIXRWCZ3.js.map} +0 -0
  100. /package/dist/{workspace-manager-G6TTBPC3.js.map → traefik-X2IWTUHO.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/database/schema.ts","../src/lib/database/index.ts","../src/lib/database/review-status-db.ts","../src/lib/review-status.ts"],"sourcesContent":["/**\n * Panopticon Database Schema\n *\n * Defines the unified schema for panopticon.db.\n * All persistent application state lives here.\n */\n\nimport type Database from 'better-sqlite3';\n\n// Schema version — increment when making breaking schema changes\nexport const SCHEMA_VERSION = 3;\n\n/**\n * Initialize the complete database schema.\n * Idempotent — uses CREATE TABLE IF NOT EXISTS throughout.\n */\nexport function initSchema(db: Database.Database): void {\n db.exec(`\n -- ===== Cost Events =====\n CREATE TABLE IF NOT EXISTS cost_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n issue_id TEXT NOT NULL,\n session_type TEXT NOT NULL DEFAULT 'unknown',\n provider TEXT NOT NULL DEFAULT 'anthropic',\n model TEXT NOT NULL,\n input INTEGER NOT NULL DEFAULT 0,\n output INTEGER NOT NULL DEFAULT 0,\n cache_read INTEGER NOT NULL DEFAULT 0,\n cache_write INTEGER NOT NULL DEFAULT 0,\n cost REAL NOT NULL DEFAULT 0,\n request_id TEXT,\n session_id TEXT, -- Claude Code session UUID (for reconciler offset tracking)\n -- TLDR metrics\n tldr_interceptions INTEGER,\n tldr_bypasses INTEGER,\n tldr_tokens_saved INTEGER,\n tldr_bypass_reasons TEXT, -- JSON string\n -- WAL source tracking\n source_file TEXT -- path of WAL file this came from (for imports)\n );\n\n CREATE UNIQUE INDEX IF NOT EXISTS idx_cost_request_id\n ON cost_events(request_id) WHERE request_id IS NOT NULL;\n\n CREATE INDEX IF NOT EXISTS idx_cost_issue_id\n ON cost_events(issue_id, ts);\n\n CREATE INDEX IF NOT EXISTS idx_cost_agent_id\n ON cost_events(agent_id, ts);\n\n CREATE INDEX IF NOT EXISTS idx_cost_ts\n ON cost_events(ts);\n\n CREATE INDEX IF NOT EXISTS idx_cost_session_id\n ON cost_events(session_id) WHERE session_id IS NOT NULL;\n\n -- ===== Review Status =====\n CREATE TABLE IF NOT EXISTS review_status (\n issue_id TEXT PRIMARY KEY,\n review_status TEXT NOT NULL DEFAULT 'pending',\n test_status TEXT NOT NULL DEFAULT 'pending',\n merge_status TEXT,\n verification_status TEXT,\n verification_notes TEXT,\n verification_cycle_count INTEGER DEFAULT 0,\n verification_max_cycles INTEGER,\n review_notes TEXT,\n test_notes TEXT,\n merge_notes TEXT,\n updated_at TEXT NOT NULL,\n ready_for_merge INTEGER NOT NULL DEFAULT 0,\n auto_requeue_count INTEGER DEFAULT 0,\n pr_url TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_review_status_updated\n ON review_status(updated_at);\n\n -- ===== Status History =====\n CREATE TABLE IF NOT EXISTS status_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n issue_id TEXT NOT NULL,\n type TEXT NOT NULL, -- 'review', 'test', 'merge'\n status TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n notes TEXT,\n FOREIGN KEY (issue_id) REFERENCES review_status(issue_id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_status_history_issue\n ON status_history(issue_id, timestamp);\n\n -- UNIQUE constraint enables INSERT OR IGNORE deduplication in upsertReviewStatus\n CREATE UNIQUE INDEX IF NOT EXISTS idx_status_history_unique\n ON status_history(issue_id, type, status, timestamp);\n\n -- ===== Health Events =====\n CREATE TABLE IF NOT EXISTS health_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n agent_id TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n state TEXT NOT NULL,\n previous_state TEXT,\n source TEXT,\n metadata TEXT -- JSON string\n );\n\n CREATE INDEX IF NOT EXISTS idx_health_agent_timestamp\n ON health_events(agent_id, timestamp);\n\n CREATE INDEX IF NOT EXISTS idx_health_timestamp\n ON health_events(timestamp);\n\n -- ===== Processed Sessions (for reconciler offset tracking) =====\n CREATE TABLE IF NOT EXISTS processed_sessions (\n session_id TEXT PRIMARY KEY,\n agent_id TEXT,\n issue_id TEXT,\n transcript_path TEXT, -- full path to the .jsonl file\n byte_offset INTEGER NOT NULL DEFAULT 0, -- bytes consumed so far\n processed_at TEXT NOT NULL,\n event_count INTEGER NOT NULL DEFAULT 0\n );\n\n -- ===== API Cache =====\n CREATE TABLE IF NOT EXISTS api_cache (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL, -- JSON string\n expires_at TEXT,\n created_at TEXT NOT NULL\n );\n\n -- ===== Rate Limits =====\n CREATE TABLE IF NOT EXISTS rate_limits (\n service TEXT PRIMARY KEY,\n requests INTEGER NOT NULL DEFAULT 0,\n window_start TEXT NOT NULL,\n limit_per_window INTEGER NOT NULL DEFAULT 1000\n );\n `);\n\n // Record schema version\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n}\n\n/**\n * Run schema migrations if the database version is older than SCHEMA_VERSION.\n * This function handles upgrading from older schema versions.\n */\nexport function runMigrations(db: Database.Database): void {\n const currentVersion = db.pragma('user_version', { simple: true }) as number;\n\n if (currentVersion === SCHEMA_VERSION) {\n return; // Already at latest version\n }\n\n if (currentVersion === 0) {\n // Fresh database — just initialize the full schema\n initSchema(db);\n return;\n }\n\n // v1 → v2: add UNIQUE index on status_history for INSERT OR IGNORE dedup\n if (currentVersion < 2) {\n // Remove duplicate rows before adding the unique index (keep lowest id per unique key)\n db.exec(`\n DELETE FROM status_history\n WHERE id NOT IN (\n SELECT MIN(id)\n FROM status_history\n GROUP BY issue_id, type, status, timestamp\n );\n CREATE UNIQUE INDEX IF NOT EXISTS idx_status_history_unique\n ON status_history(issue_id, type, status, timestamp);\n `);\n }\n\n // v2 → v3: add session_id to cost_events, extend processed_sessions for reconciler\n if (currentVersion < 3) {\n // Add session_id column to cost_events (nullable, no data loss)\n try {\n db.exec(`ALTER TABLE cost_events ADD COLUMN session_id TEXT`);\n } catch {\n // Column may already exist if schema was manually applied\n }\n\n // Add index on session_id\n db.exec(`\n CREATE INDEX IF NOT EXISTS idx_cost_session_id\n ON cost_events(session_id) WHERE session_id IS NOT NULL;\n `);\n\n // Extend processed_sessions with new columns for reconciler\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN agent_id TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN issue_id TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN transcript_path TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN byte_offset INTEGER NOT NULL DEFAULT 0`);\n } catch { /* already exists */ }\n }\n\n // After all migrations, set the version\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n}\n","/**\n * Panopticon Unified Database\n *\n * Single panopticon.db at ~/.panopticon/panopticon.db.\n * Singleton pattern — one connection shared across the process.\n *\n * IMPORTANT: This module is safe to import in both server and CLI contexts.\n * Never use execSync here — this is synchronous SQLite, not a subprocess.\n */\n\nimport Database from 'better-sqlite3';\nimport { join } from 'path';\nimport { existsSync, mkdirSync } from 'fs';\nimport { getPanopticonHome } from '../paths.js';\nimport { runMigrations } from './schema.js';\n\nlet _db: Database.Database | null = null;\n\n/**\n * Get the path to panopticon.db (dynamic, respects PANOPTICON_HOME override for tests)\n */\nexport function getDatabasePath(): string {\n return join(getPanopticonHome(), 'panopticon.db');\n}\n\n/**\n * Initialize and return the singleton database connection.\n * Safe to call multiple times — returns the existing connection after first call.\n */\nexport function getDatabase(): Database.Database {\n if (_db) {\n return _db;\n }\n\n const home = getPanopticonHome();\n if (!existsSync(home)) {\n mkdirSync(home, { recursive: true });\n }\n\n const dbPath = getDatabasePath();\n _db = new Database(dbPath);\n\n // Enable WAL mode for concurrent readers + single writer\n _db.pragma('journal_mode = WAL');\n // Enforce foreign keys\n _db.pragma('foreign_keys = ON');\n // Write-ahead log synchronization — NORMAL is safe and fast\n _db.pragma('synchronous = NORMAL');\n\n // Initialize or migrate schema\n runMigrations(_db);\n\n return _db;\n}\n\n/**\n * Close the database connection and release the singleton.\n * Primarily used in tests to get a fresh connection.\n */\nexport function closeDatabase(): void {\n if (_db) {\n _db.close();\n _db = null;\n }\n}\n\n/**\n * Force re-initialization of the database connection.\n * Used in tests after PANOPTICON_HOME changes.\n */\nexport function resetDatabase(): void {\n closeDatabase();\n}\n","/**\n * Review Status SQLite Storage\n *\n * Provides SQLite-backed CRUD for ReviewStatus, matching the interface in\n * src/lib/review-status.ts. Atomic single-transaction writes eliminate the\n * TOCTOU race in the JSON-backed implementation.\n */\n\nimport { getDatabase } from './index.js';\nimport type { ReviewStatus, StatusHistoryEntry } from '../review-status.js';\n\n// ============== Write operations ==============\n\n/**\n * Upsert a review status record atomically.\n * Replaces the JSON read-modify-write cycle with a single transaction.\n */\nexport function upsertReviewStatus(status: ReviewStatus): void {\n const db = getDatabase();\n\n const upsert = db.transaction((s: ReviewStatus) => {\n // Upsert main record\n db.prepare(`\n INSERT INTO review_status (\n issue_id, review_status, test_status, merge_status,\n verification_status, verification_notes,\n verification_cycle_count, verification_max_cycles,\n review_notes, test_notes, merge_notes,\n updated_at, ready_for_merge, auto_requeue_count, pr_url\n ) VALUES (\n ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n )\n ON CONFLICT(issue_id) DO UPDATE SET\n review_status = excluded.review_status,\n test_status = excluded.test_status,\n merge_status = excluded.merge_status,\n verification_status = excluded.verification_status,\n verification_notes = excluded.verification_notes,\n verification_cycle_count = excluded.verification_cycle_count,\n verification_max_cycles = excluded.verification_max_cycles,\n review_notes = excluded.review_notes,\n test_notes = excluded.test_notes,\n merge_notes = excluded.merge_notes,\n updated_at = excluded.updated_at,\n ready_for_merge = excluded.ready_for_merge,\n auto_requeue_count = excluded.auto_requeue_count,\n pr_url = excluded.pr_url\n `).run(\n s.issueId,\n s.reviewStatus,\n s.testStatus,\n s.mergeStatus ?? null,\n s.verificationStatus ?? null,\n s.verificationNotes ?? null,\n s.verificationCycleCount ?? null,\n s.verificationMaxCycles ?? null,\n s.reviewNotes ?? null,\n s.testNotes ?? null,\n s.mergeNotes ?? null,\n s.updatedAt,\n s.readyForMerge ? 1 : 0,\n s.autoRequeueCount ?? null,\n s.prUrl ?? null,\n );\n\n // Append new history entries (deduplicate by timestamp to avoid re-inserting)\n if (s.history && s.history.length > 0) {\n const insertHistory = db.prepare(`\n INSERT OR IGNORE INTO status_history (issue_id, type, status, timestamp, notes)\n VALUES (?, ?, ?, ?, ?)\n `);\n for (const entry of s.history) {\n insertHistory.run(s.issueId, entry.type, entry.status, entry.timestamp, entry.notes ?? null);\n }\n }\n });\n\n upsert(status);\n}\n\n/**\n * Delete a review status record and its history.\n */\nexport function deleteReviewStatus(issueId: string): void {\n const db = getDatabase();\n db.prepare('DELETE FROM review_status WHERE issue_id = ?').run(issueId);\n}\n\n// ============== Read operations ==============\n\n/**\n * Get a single review status by issue ID.\n */\nexport function getReviewStatusFromDb(issueId: string): ReviewStatus | null {\n const db = getDatabase();\n\n const row = db.prepare(`\n SELECT * FROM review_status WHERE issue_id = ?\n `).get(issueId) as DbReviewStatusRow | undefined;\n\n if (!row) return null;\n\n const history = getHistoryFromDb(issueId);\n return rowToReviewStatus(row, history);\n}\n\n/**\n * Get all review statuses.\n */\nexport function getAllReviewStatusesFromDb(): Record<string, ReviewStatus> {\n const db = getDatabase();\n\n const rows = db.prepare('SELECT * FROM review_status ORDER BY updated_at DESC').all() as DbReviewStatusRow[];\n const result: Record<string, ReviewStatus> = {};\n\n for (const row of rows) {\n const history = getHistoryFromDb(row.issue_id);\n result[row.issue_id] = rowToReviewStatus(row, history);\n }\n\n return result;\n}\n\n/**\n * Get history entries for an issue.\n */\nfunction getHistoryFromDb(issueId: string): StatusHistoryEntry[] {\n const db = getDatabase();\n const rows = db.prepare(`\n SELECT type, status, timestamp, notes\n FROM status_history\n WHERE issue_id = ?\n ORDER BY timestamp ASC\n `).all(issueId) as Array<{ type: string; status: string; timestamp: string; notes: string | null }>;\n\n return rows.map(r => ({\n type: r.type as 'review' | 'test' | 'merge',\n status: r.status,\n timestamp: r.timestamp,\n ...(r.notes ? { notes: r.notes } : {}),\n }));\n}\n\n// ============== Row mapping ==============\n\ninterface DbReviewStatusRow {\n issue_id: string;\n review_status: string;\n test_status: string;\n merge_status: string | null;\n verification_status: string | null;\n verification_notes: string | null;\n verification_cycle_count: number | null;\n verification_max_cycles: number | null;\n review_notes: string | null;\n test_notes: string | null;\n merge_notes: string | null;\n updated_at: string;\n ready_for_merge: number;\n auto_requeue_count: number | null;\n pr_url: string | null;\n}\n\nfunction rowToReviewStatus(row: DbReviewStatusRow, history: StatusHistoryEntry[]): ReviewStatus {\n return {\n issueId: row.issue_id,\n reviewStatus: row.review_status as ReviewStatus['reviewStatus'],\n testStatus: row.test_status as ReviewStatus['testStatus'],\n mergeStatus: row.merge_status as ReviewStatus['mergeStatus'] ?? undefined,\n verificationStatus: row.verification_status as ReviewStatus['verificationStatus'] ?? undefined,\n verificationNotes: row.verification_notes ?? undefined,\n verificationCycleCount: row.verification_cycle_count ?? undefined,\n verificationMaxCycles: row.verification_max_cycles ?? undefined,\n reviewNotes: row.review_notes ?? undefined,\n testNotes: row.test_notes ?? undefined,\n mergeNotes: row.merge_notes ?? undefined,\n updatedAt: row.updated_at,\n readyForMerge: row.ready_for_merge === 1,\n autoRequeueCount: row.auto_requeue_count ?? undefined,\n prUrl: row.pr_url ?? undefined,\n history: history.length > 0 ? history : undefined,\n };\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { notifyPipeline } from './pipeline-notifier.js';\nimport {\n upsertReviewStatus as dbUpsert,\n deleteReviewStatus as dbDelete,\n getReviewStatusFromDb,\n getAllReviewStatusesFromDb,\n} from './database/review-status-db.js';\n\nexport interface StatusHistoryEntry {\n type: 'review' | 'test' | 'merge';\n status: string;\n timestamp: string;\n notes?: string;\n}\n\nexport interface ReviewStatus {\n issueId: string;\n reviewStatus: 'pending' | 'reviewing' | 'passed' | 'failed' | 'blocked';\n testStatus: 'pending' | 'testing' | 'passed' | 'failed' | 'skipped' | 'dispatch_failed';\n mergeStatus?: 'pending' | 'merging' | 'merged' | 'failed';\n verificationStatus?: 'pending' | 'running' | 'passed' | 'failed' | 'skipped';\n verificationNotes?: string;\n verificationCycleCount?: number;\n verificationMaxCycles?: number;\n reviewNotes?: string;\n testNotes?: string;\n mergeNotes?: string;\n updatedAt: string;\n readyForMerge: boolean;\n autoRequeueCount?: number;\n prUrl?: string;\n history?: StatusHistoryEntry[];\n}\n\nconst DEFAULT_STATUS_FILE = join(homedir(), '.panopticon', 'review-status.json');\n\nexport function loadReviewStatuses(filePath = DEFAULT_STATUS_FILE): Record<string, ReviewStatus> {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n return getAllReviewStatusesFromDb();\n } catch {\n // Fall through to JSON on DB error\n }\n }\n\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, 'utf-8'));\n }\n } catch (err) {\n console.error('Failed to load review statuses:', err);\n }\n return {};\n}\n\nexport function saveReviewStatuses(statuses: Record<string, ReviewStatus>, filePath = DEFAULT_STATUS_FILE): void {\n try {\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, JSON.stringify(statuses, null, 2));\n } catch (err) {\n console.error('Failed to save review statuses:', err);\n }\n}\n\nexport function setReviewStatus(\n issueId: string,\n update: Partial<ReviewStatus>,\n filePath = DEFAULT_STATUS_FILE,\n): ReviewStatus {\n const statuses = loadReviewStatuses(filePath);\n const existing = statuses[issueId] || {\n issueId,\n reviewStatus: 'pending' as const,\n testStatus: 'pending' as const,\n updatedAt: new Date().toISOString(),\n readyForMerge: false,\n };\n\n // Guard: reject reviewStatus regression from 'passed' to 'reviewing' unless the caller\n // is explicitly resetting the merge lifecycle (update includes mergeStatus).\n // This is belt-and-suspenders — endpoint-level guards should catch this first.\n if (update.reviewStatus === 'reviewing' && existing.reviewStatus === 'passed' && update.mergeStatus === undefined) {\n console.warn(`[review-status] Rejecting reviewStatus regression from 'passed' to 'reviewing' for ${issueId} (mergeStatus not being reset)`);\n return existing as ReviewStatus;\n }\n\n const merged = { ...existing, ...update };\n\n // Track status transitions in history (last 10 entries)\n const history = [...(existing.history || [])];\n const now = new Date().toISOString();\n if (update.reviewStatus && update.reviewStatus !== existing.reviewStatus) {\n history.push({ type: 'review', status: update.reviewStatus, timestamp: now, notes: update.reviewNotes });\n }\n if (update.testStatus && update.testStatus !== existing.testStatus) {\n history.push({ type: 'test', status: update.testStatus, timestamp: now, notes: update.testNotes });\n }\n if (update.mergeStatus && update.mergeStatus !== existing.mergeStatus) {\n history.push({ type: 'merge', status: update.mergeStatus, timestamp: now });\n }\n while (history.length > 10) history.shift();\n\n const readyForMerge = update.readyForMerge !== undefined\n ? update.readyForMerge\n : (merged.reviewStatus === 'passed' && merged.testStatus === 'passed' && merged.mergeStatus !== 'merged');\n\n const updated: ReviewStatus = {\n ...merged,\n issueId,\n updatedAt: now,\n readyForMerge,\n history,\n };\n\n // SQLite first — it is the authoritative store (reads prefer SQLite)\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbUpsert(updated);\n } catch (err) {\n console.error('[review-status] SQLite write failed (continuing with JSON):', err);\n }\n }\n\n // JSON second — legacy fallback for tools that read review-status.json directly\n statuses[issueId] = updated;\n saveReviewStatuses(statuses, filePath);\n\n notifyPipeline({ type: 'status_changed', issueId, status: updated });\n\n return updated;\n}\n\nexport function getReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): ReviewStatus | null {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n const fromDb = getReviewStatusFromDb(issueId);\n if (fromDb) return fromDb;\n } catch {\n // Fall through to JSON on DB error\n }\n }\n const statuses = loadReviewStatuses(filePath);\n return statuses[issueId] || null;\n}\n\nexport function clearReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): void {\n const statuses = loadReviewStatuses(filePath);\n delete statuses[issueId];\n saveReviewStatuses(statuses, filePath);\n\n // Dual-delete from SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbDelete(issueId);\n } catch (err) {\n console.error('[review-status] SQLite delete failed (continuing with JSON):', err);\n }\n }\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","join"]}
1
+ {"version":3,"sources":["../src/lib/database/schema.ts","../src/lib/database/index.ts","../src/lib/database/review-status-db.ts","../src/lib/review-status.ts"],"sourcesContent":["/**\n * Panopticon Database Schema\n *\n * Defines the unified schema for panopticon.db.\n * All persistent application state lives here.\n */\n\nimport type Database from 'better-sqlite3';\n\n// Schema version — increment when making breaking schema changes\nexport const SCHEMA_VERSION = 3;\n\n/**\n * Initialize the complete database schema.\n * Idempotent — uses CREATE TABLE IF NOT EXISTS throughout.\n */\nexport function initSchema(db: Database.Database): void {\n db.exec(`\n -- ===== Cost Events =====\n CREATE TABLE IF NOT EXISTS cost_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n issue_id TEXT NOT NULL,\n session_type TEXT NOT NULL DEFAULT 'unknown',\n provider TEXT NOT NULL DEFAULT 'anthropic',\n model TEXT NOT NULL,\n input INTEGER NOT NULL DEFAULT 0,\n output INTEGER NOT NULL DEFAULT 0,\n cache_read INTEGER NOT NULL DEFAULT 0,\n cache_write INTEGER NOT NULL DEFAULT 0,\n cost REAL NOT NULL DEFAULT 0,\n request_id TEXT,\n session_id TEXT, -- Claude Code session UUID (for reconciler offset tracking)\n -- TLDR metrics\n tldr_interceptions INTEGER,\n tldr_bypasses INTEGER,\n tldr_tokens_saved INTEGER,\n tldr_bypass_reasons TEXT, -- JSON string\n -- WAL source tracking\n source_file TEXT -- path of WAL file this came from (for imports)\n );\n\n CREATE UNIQUE INDEX IF NOT EXISTS idx_cost_request_id\n ON cost_events(request_id) WHERE request_id IS NOT NULL;\n\n CREATE INDEX IF NOT EXISTS idx_cost_issue_id\n ON cost_events(issue_id, ts);\n\n CREATE INDEX IF NOT EXISTS idx_cost_agent_id\n ON cost_events(agent_id, ts);\n\n CREATE INDEX IF NOT EXISTS idx_cost_ts\n ON cost_events(ts);\n\n CREATE INDEX IF NOT EXISTS idx_cost_session_id\n ON cost_events(session_id) WHERE session_id IS NOT NULL;\n\n -- ===== Review Status =====\n CREATE TABLE IF NOT EXISTS review_status (\n issue_id TEXT PRIMARY KEY,\n review_status TEXT NOT NULL DEFAULT 'pending',\n test_status TEXT NOT NULL DEFAULT 'pending',\n merge_status TEXT,\n verification_status TEXT,\n verification_notes TEXT,\n verification_cycle_count INTEGER DEFAULT 0,\n verification_max_cycles INTEGER,\n review_notes TEXT,\n test_notes TEXT,\n merge_notes TEXT,\n updated_at TEXT NOT NULL,\n ready_for_merge INTEGER NOT NULL DEFAULT 0,\n auto_requeue_count INTEGER DEFAULT 0,\n pr_url TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_review_status_updated\n ON review_status(updated_at);\n\n -- ===== Status History =====\n CREATE TABLE IF NOT EXISTS status_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n issue_id TEXT NOT NULL,\n type TEXT NOT NULL, -- 'review', 'test', 'merge'\n status TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n notes TEXT,\n FOREIGN KEY (issue_id) REFERENCES review_status(issue_id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_status_history_issue\n ON status_history(issue_id, timestamp);\n\n -- UNIQUE constraint enables INSERT OR IGNORE deduplication in upsertReviewStatus\n CREATE UNIQUE INDEX IF NOT EXISTS idx_status_history_unique\n ON status_history(issue_id, type, status, timestamp);\n\n -- ===== Health Events =====\n CREATE TABLE IF NOT EXISTS health_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n agent_id TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n state TEXT NOT NULL,\n previous_state TEXT,\n source TEXT,\n metadata TEXT -- JSON string\n );\n\n CREATE INDEX IF NOT EXISTS idx_health_agent_timestamp\n ON health_events(agent_id, timestamp);\n\n CREATE INDEX IF NOT EXISTS idx_health_timestamp\n ON health_events(timestamp);\n\n -- ===== Processed Sessions (for reconciler offset tracking) =====\n CREATE TABLE IF NOT EXISTS processed_sessions (\n session_id TEXT PRIMARY KEY,\n agent_id TEXT,\n issue_id TEXT,\n transcript_path TEXT, -- full path to the .jsonl file\n byte_offset INTEGER NOT NULL DEFAULT 0, -- bytes consumed so far\n processed_at TEXT NOT NULL,\n event_count INTEGER NOT NULL DEFAULT 0\n );\n\n -- ===== API Cache =====\n CREATE TABLE IF NOT EXISTS api_cache (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL, -- JSON string\n expires_at TEXT,\n created_at TEXT NOT NULL\n );\n\n -- ===== Rate Limits =====\n CREATE TABLE IF NOT EXISTS rate_limits (\n service TEXT PRIMARY KEY,\n requests INTEGER NOT NULL DEFAULT 0,\n window_start TEXT NOT NULL,\n limit_per_window INTEGER NOT NULL DEFAULT 1000\n );\n `);\n\n // Record schema version\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n}\n\n/**\n * Run schema migrations if the database version is older than SCHEMA_VERSION.\n * This function handles upgrading from older schema versions.\n */\nexport function runMigrations(db: Database.Database): void {\n const currentVersion = db.pragma('user_version', { simple: true }) as number;\n\n if (currentVersion === SCHEMA_VERSION) {\n return; // Already at latest version\n }\n\n if (currentVersion === 0) {\n // Fresh database — just initialize the full schema\n initSchema(db);\n return;\n }\n\n // v1 → v2: add UNIQUE index on status_history for INSERT OR IGNORE dedup\n if (currentVersion < 2) {\n // Remove duplicate rows before adding the unique index (keep lowest id per unique key)\n db.exec(`\n DELETE FROM status_history\n WHERE id NOT IN (\n SELECT MIN(id)\n FROM status_history\n GROUP BY issue_id, type, status, timestamp\n );\n CREATE UNIQUE INDEX IF NOT EXISTS idx_status_history_unique\n ON status_history(issue_id, type, status, timestamp);\n `);\n }\n\n // v2 → v3: add session_id to cost_events, extend processed_sessions for reconciler\n if (currentVersion < 3) {\n // Add session_id column to cost_events (nullable, no data loss)\n try {\n db.exec(`ALTER TABLE cost_events ADD COLUMN session_id TEXT`);\n } catch {\n // Column may already exist if schema was manually applied\n }\n\n // Add index on session_id\n db.exec(`\n CREATE INDEX IF NOT EXISTS idx_cost_session_id\n ON cost_events(session_id) WHERE session_id IS NOT NULL;\n `);\n\n // Extend processed_sessions with new columns for reconciler\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN agent_id TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN issue_id TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN transcript_path TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE processed_sessions ADD COLUMN byte_offset INTEGER NOT NULL DEFAULT 0`);\n } catch { /* already exists */ }\n }\n\n // After all migrations, set the version\n db.pragma(`user_version = ${SCHEMA_VERSION}`);\n}\n","/**\n * Panopticon Unified Database\n *\n * Single panopticon.db at ~/.panopticon/panopticon.db.\n * Singleton pattern — one connection shared across the process.\n *\n * IMPORTANT: This module is safe to import in both server and CLI contexts.\n * Never use execSync here — this is synchronous SQLite, not a subprocess.\n */\n\nimport Database from 'better-sqlite3';\nimport { join } from 'path';\nimport { existsSync, mkdirSync } from 'fs';\nimport { getPanopticonHome } from '../paths.js';\nimport { runMigrations } from './schema.js';\n\nlet _db: Database.Database | null = null;\n\n/**\n * Get the path to panopticon.db (dynamic, respects PANOPTICON_HOME override for tests)\n */\nexport function getDatabasePath(): string {\n return join(getPanopticonHome(), 'panopticon.db');\n}\n\n/**\n * Initialize and return the singleton database connection.\n * Safe to call multiple times — returns the existing connection after first call.\n */\nexport function getDatabase(): Database.Database {\n if (_db) {\n return _db;\n }\n\n const home = getPanopticonHome();\n if (!existsSync(home)) {\n mkdirSync(home, { recursive: true });\n }\n\n const dbPath = getDatabasePath();\n _db = new Database(dbPath);\n\n // Enable WAL mode for concurrent readers + single writer\n _db.pragma('journal_mode = WAL');\n // Enforce foreign keys\n _db.pragma('foreign_keys = ON');\n // Write-ahead log synchronization — NORMAL is safe and fast\n _db.pragma('synchronous = NORMAL');\n\n // Initialize or migrate schema\n runMigrations(_db);\n\n return _db;\n}\n\n/**\n * Close the database connection and release the singleton.\n * Primarily used in tests to get a fresh connection.\n */\nexport function closeDatabase(): void {\n if (_db) {\n _db.close();\n _db = null;\n }\n}\n\n/**\n * Force re-initialization of the database connection.\n * Used in tests after PANOPTICON_HOME changes.\n */\nexport function resetDatabase(): void {\n closeDatabase();\n}\n","/**\n * Review Status SQLite Storage\n *\n * Provides SQLite-backed CRUD for ReviewStatus, matching the interface in\n * src/lib/review-status.ts. Atomic single-transaction writes eliminate the\n * TOCTOU race in the JSON-backed implementation.\n */\n\nimport { getDatabase } from './index.js';\nimport type { ReviewStatus, StatusHistoryEntry } from '../review-status.js';\n\n// ============== Write operations ==============\n\n/**\n * Upsert a review status record atomically.\n * Replaces the JSON read-modify-write cycle with a single transaction.\n */\nexport function upsertReviewStatus(status: ReviewStatus): void {\n const db = getDatabase();\n\n const upsert = db.transaction((s: ReviewStatus) => {\n // Upsert main record\n db.prepare(`\n INSERT INTO review_status (\n issue_id, review_status, test_status, merge_status,\n verification_status, verification_notes,\n verification_cycle_count, verification_max_cycles,\n review_notes, test_notes, merge_notes,\n updated_at, ready_for_merge, auto_requeue_count, pr_url\n ) VALUES (\n ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n )\n ON CONFLICT(issue_id) DO UPDATE SET\n review_status = excluded.review_status,\n test_status = excluded.test_status,\n merge_status = excluded.merge_status,\n verification_status = excluded.verification_status,\n verification_notes = excluded.verification_notes,\n verification_cycle_count = excluded.verification_cycle_count,\n verification_max_cycles = excluded.verification_max_cycles,\n review_notes = excluded.review_notes,\n test_notes = excluded.test_notes,\n merge_notes = excluded.merge_notes,\n updated_at = excluded.updated_at,\n ready_for_merge = excluded.ready_for_merge,\n auto_requeue_count = excluded.auto_requeue_count,\n pr_url = excluded.pr_url\n `).run(\n s.issueId,\n s.reviewStatus,\n s.testStatus,\n s.mergeStatus ?? null,\n s.verificationStatus ?? null,\n s.verificationNotes ?? null,\n s.verificationCycleCount ?? null,\n s.verificationMaxCycles ?? null,\n s.reviewNotes ?? null,\n s.testNotes ?? null,\n s.mergeNotes ?? null,\n s.updatedAt,\n s.readyForMerge ? 1 : 0,\n s.autoRequeueCount ?? null,\n s.prUrl ?? null,\n );\n\n // Append new history entries (deduplicate by timestamp to avoid re-inserting)\n if (s.history && s.history.length > 0) {\n const insertHistory = db.prepare(`\n INSERT OR IGNORE INTO status_history (issue_id, type, status, timestamp, notes)\n VALUES (?, ?, ?, ?, ?)\n `);\n for (const entry of s.history) {\n insertHistory.run(s.issueId, entry.type, entry.status, entry.timestamp, entry.notes ?? null);\n }\n }\n });\n\n upsert(status);\n}\n\n/**\n * Delete a review status record and its history.\n */\nexport function deleteReviewStatus(issueId: string): void {\n const db = getDatabase();\n db.prepare('DELETE FROM review_status WHERE issue_id = ?').run(issueId);\n}\n\n// ============== Read operations ==============\n\n/**\n * Get a single review status by issue ID.\n */\nexport function getReviewStatusFromDb(issueId: string): ReviewStatus | null {\n const db = getDatabase();\n\n const row = db.prepare(`\n SELECT * FROM review_status WHERE issue_id = ?\n `).get(issueId) as DbReviewStatusRow | undefined;\n\n if (!row) return null;\n\n const history = getHistoryFromDb(issueId);\n return rowToReviewStatus(row, history);\n}\n\n/**\n * Get all review statuses.\n */\nexport function getAllReviewStatusesFromDb(): Record<string, ReviewStatus> {\n const db = getDatabase();\n\n const rows = db.prepare('SELECT * FROM review_status ORDER BY updated_at DESC').all() as DbReviewStatusRow[];\n const result: Record<string, ReviewStatus> = {};\n\n for (const row of rows) {\n const history = getHistoryFromDb(row.issue_id);\n result[row.issue_id] = rowToReviewStatus(row, history);\n }\n\n return result;\n}\n\n/**\n * Get history entries for an issue.\n */\nfunction getHistoryFromDb(issueId: string): StatusHistoryEntry[] {\n const db = getDatabase();\n const rows = db.prepare(`\n SELECT type, status, timestamp, notes\n FROM status_history\n WHERE issue_id = ?\n ORDER BY timestamp ASC\n `).all(issueId) as Array<{ type: string; status: string; timestamp: string; notes: string | null }>;\n\n return rows.map(r => ({\n type: r.type as 'review' | 'test' | 'merge',\n status: r.status,\n timestamp: r.timestamp,\n ...(r.notes ? { notes: r.notes } : {}),\n }));\n}\n\n// ============== Row mapping ==============\n\ninterface DbReviewStatusRow {\n issue_id: string;\n review_status: string;\n test_status: string;\n merge_status: string | null;\n verification_status: string | null;\n verification_notes: string | null;\n verification_cycle_count: number | null;\n verification_max_cycles: number | null;\n review_notes: string | null;\n test_notes: string | null;\n merge_notes: string | null;\n updated_at: string;\n ready_for_merge: number;\n auto_requeue_count: number | null;\n pr_url: string | null;\n}\n\nfunction rowToReviewStatus(row: DbReviewStatusRow, history: StatusHistoryEntry[]): ReviewStatus {\n return {\n issueId: row.issue_id,\n reviewStatus: row.review_status as ReviewStatus['reviewStatus'],\n testStatus: row.test_status as ReviewStatus['testStatus'],\n mergeStatus: row.merge_status as ReviewStatus['mergeStatus'] ?? undefined,\n verificationStatus: row.verification_status as ReviewStatus['verificationStatus'] ?? undefined,\n verificationNotes: row.verification_notes ?? undefined,\n verificationCycleCount: row.verification_cycle_count ?? undefined,\n verificationMaxCycles: row.verification_max_cycles ?? undefined,\n reviewNotes: row.review_notes ?? undefined,\n testNotes: row.test_notes ?? undefined,\n mergeNotes: row.merge_notes ?? undefined,\n updatedAt: row.updated_at,\n readyForMerge: row.ready_for_merge === 1,\n autoRequeueCount: row.auto_requeue_count ?? undefined,\n prUrl: row.pr_url ?? undefined,\n history: history.length > 0 ? history : undefined,\n };\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { notifyPipeline } from './pipeline-notifier.js';\nimport {\n upsertReviewStatus as dbUpsert,\n deleteReviewStatus as dbDelete,\n getReviewStatusFromDb,\n getAllReviewStatusesFromDb,\n} from './database/review-status-db.js';\n\nexport interface StatusHistoryEntry {\n type: 'review' | 'test' | 'merge' | 'inspect' | 'uat';\n status: string;\n timestamp: string;\n notes?: string;\n}\n\nexport interface ReviewStatus {\n issueId: string;\n reviewStatus: 'pending' | 'reviewing' | 'passed' | 'failed' | 'blocked';\n testStatus: 'pending' | 'testing' | 'passed' | 'failed' | 'skipped' | 'dispatch_failed';\n mergeStatus?: 'pending' | 'merging' | 'merged' | 'failed';\n inspectStatus?: 'pending' | 'inspecting' | 'passed' | 'failed';\n inspectNotes?: string;\n uatStatus?: 'pending' | 'testing' | 'passed' | 'failed';\n uatNotes?: string;\n verificationStatus?: 'pending' | 'running' | 'passed' | 'failed' | 'skipped';\n verificationNotes?: string;\n verificationCycleCount?: number;\n verificationMaxCycles?: number;\n reviewNotes?: string;\n testNotes?: string;\n mergeNotes?: string;\n updatedAt: string;\n readyForMerge: boolean;\n autoRequeueCount?: number;\n prUrl?: string;\n history?: StatusHistoryEntry[];\n}\n\nconst DEFAULT_STATUS_FILE = join(homedir(), '.panopticon', 'review-status.json');\n\nexport function loadReviewStatuses(filePath = DEFAULT_STATUS_FILE): Record<string, ReviewStatus> {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n return getAllReviewStatusesFromDb();\n } catch {\n // Fall through to JSON on DB error\n }\n }\n\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, 'utf-8'));\n }\n } catch (err) {\n console.error('Failed to load review statuses:', err);\n }\n return {};\n}\n\nexport function saveReviewStatuses(statuses: Record<string, ReviewStatus>, filePath = DEFAULT_STATUS_FILE): void {\n try {\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, JSON.stringify(statuses, null, 2));\n } catch (err) {\n console.error('Failed to save review statuses:', err);\n }\n}\n\nexport function setReviewStatus(\n issueId: string,\n update: Partial<ReviewStatus>,\n filePath = DEFAULT_STATUS_FILE,\n): ReviewStatus {\n const statuses = loadReviewStatuses(filePath);\n const existing = statuses[issueId] || {\n issueId,\n reviewStatus: 'pending' as const,\n testStatus: 'pending' as const,\n updatedAt: new Date().toISOString(),\n readyForMerge: false,\n };\n\n // Guard: reject reviewStatus regression from 'passed' to 'reviewing' unless the caller\n // is explicitly resetting the merge lifecycle (update includes mergeStatus).\n // This is belt-and-suspenders — endpoint-level guards should catch this first.\n if (update.reviewStatus === 'reviewing' && existing.reviewStatus === 'passed' && update.mergeStatus === undefined) {\n console.warn(`[review-status] Rejecting reviewStatus regression from 'passed' to 'reviewing' for ${issueId} (mergeStatus not being reset)`);\n return existing as ReviewStatus;\n }\n\n const merged = { ...existing, ...update };\n\n // Track status transitions in history (last 10 entries)\n const history = [...(existing.history || [])];\n const now = new Date().toISOString();\n if (update.reviewStatus && update.reviewStatus !== existing.reviewStatus) {\n history.push({ type: 'review', status: update.reviewStatus, timestamp: now, notes: update.reviewNotes });\n }\n if (update.testStatus && update.testStatus !== existing.testStatus) {\n history.push({ type: 'test', status: update.testStatus, timestamp: now, notes: update.testNotes });\n }\n if (update.uatStatus && update.uatStatus !== existing.uatStatus) {\n history.push({ type: 'uat', status: update.uatStatus, timestamp: now, notes: update.uatNotes });\n }\n if (update.mergeStatus && update.mergeStatus !== existing.mergeStatus) {\n history.push({ type: 'merge', status: update.mergeStatus, timestamp: now });\n }\n while (history.length > 10) history.shift();\n\n // readyForMerge is true when all required gates pass.\n // If uatStatus exists (UAT specialist has been involved), it must also be 'passed'.\n const readyForMerge = update.readyForMerge !== undefined\n ? update.readyForMerge\n : (\n merged.reviewStatus === 'passed' &&\n merged.testStatus === 'passed' &&\n merged.mergeStatus !== 'merged' &&\n // If UAT has been initiated, it must pass too\n (merged.uatStatus === undefined || merged.uatStatus === 'passed')\n );\n\n const updated: ReviewStatus = {\n ...merged,\n issueId,\n updatedAt: now,\n readyForMerge,\n history,\n };\n\n // SQLite first — it is the authoritative store (reads prefer SQLite)\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbUpsert(updated);\n } catch (err) {\n console.error('[review-status] SQLite write failed (continuing with JSON):', err);\n }\n }\n\n // JSON second — legacy fallback for tools that read review-status.json directly\n statuses[issueId] = updated;\n saveReviewStatuses(statuses, filePath);\n\n notifyPipeline({ type: 'status_changed', issueId, status: updated });\n\n return updated;\n}\n\nexport function getReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): ReviewStatus | null {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n const fromDb = getReviewStatusFromDb(issueId);\n if (fromDb) return fromDb;\n } catch {\n // Fall through to JSON on DB error\n }\n }\n const statuses = loadReviewStatuses(filePath);\n return statuses[issueId] || null;\n}\n\nexport function clearReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): void {\n const statuses = loadReviewStatuses(filePath);\n delete statuses[issueId];\n saveReviewStatuses(statuses, filePath);\n\n // Dual-delete from SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbDelete(issueId);\n } catch (err) {\n console.error('[review-status] SQLite delete failed (continuing with JSON):', err);\n }\n }\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","join"]}
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  init_projects,
7
7
  loadProjectsConfig
8
- } from "./chunk-ZMJFEHGF.js";
8
+ } from "./chunk-7ZB5D46Y.js";
9
9
  import {
10
10
  SOURCE_TRAEFIK_TEMPLATES,
11
11
  TRAEFIK_CERTS_DIR,
@@ -151,4 +151,4 @@ export {
151
151
  ensureProjectCerts,
152
152
  cleanupStaleTlsSections
153
153
  };
154
- //# sourceMappingURL=chunk-43F4LDZ4.js.map
154
+ //# sourceMappingURL=chunk-VVTAPQOI.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  init_projects,
3
3
  loadProjectsConfig
4
- } from "./chunk-ZMJFEHGF.js";
4
+ } from "./chunk-7ZB5D46Y.js";
5
5
  import {
6
6
  init_esm_shims
7
7
  } from "./chunk-ZHC57RCV.js";
@@ -62,9 +62,34 @@ function resolveGitHubIssue(issueId) {
62
62
  function isGitHubIssue(issueId) {
63
63
  return resolveGitHubIssue(issueId).isGitHub;
64
64
  }
65
+ function resolveTrackerType(issueId) {
66
+ if (resolveGitHubIssue(issueId).isGitHub) {
67
+ return "github";
68
+ }
69
+ const prefix = extractIssuePrefix(issueId);
70
+ try {
71
+ const { projects } = loadProjectsConfig();
72
+ for (const [key, project] of Object.entries(projects)) {
73
+ if (project.linear_team?.toUpperCase() === prefix) {
74
+ return "linear";
75
+ }
76
+ if (!project.linear_team) {
77
+ const derivedPrefix = key.toUpperCase().replace(/-/g, "");
78
+ if (derivedPrefix === prefix) {
79
+ if (project.rally_project && !project.github_repo) {
80
+ return "rally";
81
+ }
82
+ }
83
+ }
84
+ }
85
+ } catch {
86
+ }
87
+ return "linear";
88
+ }
65
89
 
66
90
  export {
67
91
  resolveGitHubIssue,
68
- isGitHubIssue
92
+ isGitHubIssue,
93
+ resolveTrackerType
69
94
  };
70
- //# sourceMappingURL=chunk-YAAT66RT.js.map
95
+ //# sourceMappingURL=chunk-WP6ZLWU3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/tracker-utils.ts"],"sourcesContent":["/**\n * Shared tracker utilities for resolving issue IDs to their tracker type\n * (GitHub or Linear) based on GITHUB_REPOS configuration.\n *\n * Eliminates hardcoded prefix checks like `issueId.startsWith('PAN-')`.\n */\n\nimport { readFileSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { loadProjectsConfig } from './projects.js';\n\nexport interface GitHubRepoConfig {\n owner: string;\n repo: string;\n prefix: string;\n}\n\nexport interface GitHubIssueResolution {\n isGitHub: true;\n owner: string;\n repo: string;\n prefix: string;\n number: number;\n}\n\nexport interface NonGitHubResolution {\n isGitHub: false;\n}\n\nexport type IssueResolution = GitHubIssueResolution | NonGitHubResolution;\n\n/**\n * Parse GitHub repos from GITHUB_REPOS env var and projects.yaml.\n * Priority: GITHUB_REPOS env var first, then auto-derive from projects.yaml.\n * Format for env var: \"owner/repo:PREFIX,owner2/repo2:PREFIX2\"\n */\nexport function parseGitHubRepos(): GitHubRepoConfig[] {\n const repos: GitHubRepoConfig[] = [];\n\n // 1. Check GITHUB_REPOS env var\n const envFile = join(homedir(), '.panopticon.env');\n if (existsSync(envFile)) {\n const content = readFileSync(envFile, 'utf-8');\n const reposMatch = content.match(/GITHUB_REPOS=(.+)/);\n if (reposMatch) {\n repos.push(...reposMatch[1].trim().split(',').map(r => {\n const [repoPath, prefix] = r.trim().split(':');\n const [owner, repo] = (repoPath || '').split('/');\n return { owner: owner || '', repo: repo || '', prefix: (prefix || '').toUpperCase() };\n }).filter(r => r.owner && r.repo && r.prefix));\n }\n }\n\n // 2. Auto-derive from projects.yaml (if no explicit GITHUB_REPOS)\n if (repos.length === 0) {\n try {\n const { projects } = loadProjectsConfig();\n for (const [key, project] of Object.entries(projects)) {\n if (project.github_repo) {\n const [owner, repo] = project.github_repo.split('/');\n // Derive prefix: linear_team if set, otherwise uppercase project key\n const prefix = project.linear_team || key.toUpperCase().replace(/-/g, '');\n if (owner && repo && prefix) {\n repos.push({ owner, repo, prefix: prefix.toUpperCase() });\n }\n }\n }\n } catch { /* ignore */ }\n }\n\n return repos;\n}\n\n/**\n * Extract the prefix from an issue ID (e.g., \"CLI\" from \"CLI-1\", \"PAN\" from \"PAN-42\").\n */\nexport function extractIssuePrefix(issueId: string): string {\n return issueId.split('-')[0].toUpperCase();\n}\n\n/**\n * Resolve an issue ID to its GitHub repo config, or determine it's not a GitHub issue.\n *\n * Checks the issue prefix against all prefixes configured in GITHUB_REPOS.\n * Returns the matching repo config with parsed issue number, or { isGitHub: false }.\n */\nexport function resolveGitHubIssue(issueId: string): IssueResolution {\n const prefix = extractIssuePrefix(issueId);\n const repos = parseGitHubRepos();\n\n for (const repoConfig of repos) {\n if (repoConfig.prefix === prefix) {\n const number = parseInt(issueId.split('-')[1], 10);\n if (!isNaN(number)) {\n return { isGitHub: true, ...repoConfig, number };\n }\n }\n }\n\n return { isGitHub: false };\n}\n\n/**\n * Check if an issue ID belongs to a GitHub-tracked project.\n */\nexport function isGitHubIssue(issueId: string): boolean {\n return resolveGitHubIssue(issueId).isGitHub;\n}\n\nexport type TrackerTypeResolution = 'github' | 'rally' | 'linear';\n\n/**\n * Resolve the tracker type for an issue ID by checking projects.yaml configuration.\n *\n * Resolution order:\n * 1. GitHub — prefix matches a configured github_repo project\n * 2. Rally — prefix matches a project with rally_project but no linear_team / github_repo\n * 3. Linear — fallback (matches linear_team or unknown prefix)\n */\nexport function resolveTrackerType(issueId: string): TrackerTypeResolution {\n // Check GitHub first (existing logic)\n if (resolveGitHubIssue(issueId).isGitHub) {\n return 'github';\n }\n\n // Check if the issue prefix matches a Rally-only project\n const prefix = extractIssuePrefix(issueId);\n try {\n const { projects } = loadProjectsConfig();\n for (const [key, project] of Object.entries(projects)) {\n // Match by linear_team first (even Rally projects may have linear_team for routing)\n if (project.linear_team?.toUpperCase() === prefix) {\n // Project has linear_team matching this prefix — it's a Linear project\n // (even if it also has rally_project for cross-tracking)\n return 'linear';\n }\n\n // For projects without linear_team, derive prefix from project key\n if (!project.linear_team) {\n const derivedPrefix = key.toUpperCase().replace(/-/g, '');\n if (derivedPrefix === prefix) {\n // Prefix matches — determine tracker by what's configured\n if (project.rally_project && !project.github_repo) {\n return 'rally';\n }\n // github_repo projects are already caught by resolveGitHubIssue above\n }\n }\n }\n } catch { /* ignore config errors */ }\n\n // Default to Linear for unknown prefixes\n return 'linear';\n}\n"],"mappings":";;;;;;;;;AAAA;AAUA;AAHA,SAAS,cAAc,kBAAkB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AA4BjB,SAAS,mBAAuC;AACrD,QAAM,QAA4B,CAAC;AAGnC,QAAM,UAAU,KAAK,QAAQ,GAAG,iBAAiB;AACjD,MAAI,WAAW,OAAO,GAAG;AACvB,UAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,UAAM,aAAa,QAAQ,MAAM,mBAAmB;AACpD,QAAI,YAAY;AACd,YAAM,KAAK,GAAG,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE,IAAI,OAAK;AACrD,cAAM,CAAC,UAAU,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG;AAC7C,cAAM,CAAC,OAAO,IAAI,KAAK,YAAY,IAAI,MAAM,GAAG;AAChD,eAAO,EAAE,OAAO,SAAS,IAAI,MAAM,QAAQ,IAAI,SAAS,UAAU,IAAI,YAAY,EAAE;AAAA,MACtF,CAAC,EAAE,OAAO,OAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC;AAAA,IAC/C;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,GAAG;AACtB,QAAI;AACF,YAAM,EAAE,SAAS,IAAI,mBAAmB;AACxC,iBAAW,CAAC,KAAK,OAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACrD,YAAI,QAAQ,aAAa;AACvB,gBAAM,CAAC,OAAO,IAAI,IAAI,QAAQ,YAAY,MAAM,GAAG;AAEnD,gBAAM,SAAS,QAAQ,eAAe,IAAI,YAAY,EAAE,QAAQ,MAAM,EAAE;AACxE,cAAI,SAAS,QAAQ,QAAQ;AAC3B,kBAAM,KAAK,EAAE,OAAO,MAAM,QAAQ,OAAO,YAAY,EAAE,CAAC;AAAA,UAC1D;AAAA,QACF;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAAe;AAAA,EACzB;AAEA,SAAO;AACT;AAKO,SAAS,mBAAmB,SAAyB;AAC1D,SAAO,QAAQ,MAAM,GAAG,EAAE,CAAC,EAAE,YAAY;AAC3C;AAQO,SAAS,mBAAmB,SAAkC;AACnE,QAAM,SAAS,mBAAmB,OAAO;AACzC,QAAM,QAAQ,iBAAiB;AAE/B,aAAW,cAAc,OAAO;AAC9B,QAAI,WAAW,WAAW,QAAQ;AAChC,YAAM,SAAS,SAAS,QAAQ,MAAM,GAAG,EAAE,CAAC,GAAG,EAAE;AACjD,UAAI,CAAC,MAAM,MAAM,GAAG;AAClB,eAAO,EAAE,UAAU,MAAM,GAAG,YAAY,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,MAAM;AAC3B;AAKO,SAAS,cAAc,SAA0B;AACtD,SAAO,mBAAmB,OAAO,EAAE;AACrC;AAYO,SAAS,mBAAmB,SAAwC;AAEzE,MAAI,mBAAmB,OAAO,EAAE,UAAU;AACxC,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,mBAAmB,OAAO;AACzC,MAAI;AACF,UAAM,EAAE,SAAS,IAAI,mBAAmB;AACxC,eAAW,CAAC,KAAK,OAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAErD,UAAI,QAAQ,aAAa,YAAY,MAAM,QAAQ;AAGjD,eAAO;AAAA,MACT;AAGA,UAAI,CAAC,QAAQ,aAAa;AACxB,cAAM,gBAAgB,IAAI,YAAY,EAAE,QAAQ,MAAM,EAAE;AACxD,YAAI,kBAAkB,QAAQ;AAE5B,cAAI,QAAQ,iBAAiB,CAAC,QAAQ,aAAa;AACjD,mBAAO;AAAA,UACT;AAAA,QAEF;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAA6B;AAGrC,SAAO;AACT;","names":[]}
@@ -0,0 +1,9 @@
1
+ import {
2
+ cleanPlanningArtifacts
3
+ } from "./chunk-F4XS2FQN.js";
4
+ import "./chunk-SFX3BG6N.js";
5
+ import "./chunk-ZHC57RCV.js";
6
+ export {
7
+ cleanPlanningArtifacts
8
+ };
9
+ //# sourceMappingURL=clean-planning-V4SSVU26.js.map