panopticon-cli 0.6.8 → 0.6.9

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 (89) hide show
  1. package/dist/{agents-D_2oRFVf.js → agents-BQOqo27C.js} +1 -1
  2. package/dist/{agents-CfFDs52G.js → agents-DezveQ1x.js} +4 -4
  3. package/dist/{agents-CfFDs52G.js.map → agents-DezveQ1x.js.map} +1 -1
  4. package/dist/cli/index.js +34 -34
  5. package/dist/{config-yaml-DGbLSMCa.js → config-yaml-BHD2Qdd8.js} +22 -1
  6. package/dist/config-yaml-BHD2Qdd8.js.map +1 -0
  7. package/dist/{config-yaml-Dqt4FWQH.js → config-yaml-IlSnFzJQ.js} +1 -1
  8. package/dist/dashboard/{agent-enrichment-DdO7ZqjI.js → agent-enrichment-BKZjVvlL.js} +3 -3
  9. package/dist/dashboard/{agent-enrichment-DdO7ZqjI.js.map → agent-enrichment-BKZjVvlL.js.map} +1 -1
  10. package/dist/dashboard/{agent-enrichment-dLeGE1fX.js → agent-enrichment-iY3_PylI.js} +1 -1
  11. package/dist/dashboard/{agents-DCpQQ_W5.js → agents-BQWA-Vps.js} +4 -4
  12. package/dist/dashboard/{agents-DCpQQ_W5.js.map → agents-BQWA-Vps.js.map} +1 -1
  13. package/dist/dashboard/{agents-Dgh2TjSp.js → agents-Dinc9j_8.js} +1 -1
  14. package/dist/dashboard/{config-yaml-DkresmrS.js → config-yaml-CNNnB4Mu.js} +1 -1
  15. package/dist/dashboard/{config-yaml-DSfYpzN6.js → config-yaml-DUu0JI25.js} +22 -1
  16. package/dist/dashboard/{config-yaml-DSfYpzN6.js.map → config-yaml-DUu0JI25.js.map} +1 -1
  17. package/dist/dashboard/{factory-C8nhLGHB.js → factory-CBY0WWeE.js} +2 -2
  18. package/dist/dashboard/{factory-C8nhLGHB.js.map → factory-CBY0WWeE.js.map} +1 -1
  19. package/dist/dashboard/{inspect-agent-7eour7EA.js → inspect-agent-KKOeNR7E.js} +3 -3
  20. package/dist/dashboard/{inspect-agent-7eour7EA.js.map → inspect-agent-KKOeNR7E.js.map} +1 -1
  21. package/dist/dashboard/{issue-service-singleton-Wv4xBm3y.js → issue-service-singleton-BCZ62hLj.js} +3 -3
  22. package/dist/dashboard/{issue-service-singleton-Wv4xBm3y.js.map → issue-service-singleton-BCZ62hLj.js.map} +1 -1
  23. package/dist/dashboard/{issue-service-singleton-Co__-6kL.js → issue-service-singleton-BGKf0A95.js} +1 -1
  24. package/dist/dashboard/{lifecycle-BcUmtkR4.js → lifecycle-Dpgg-IeP.js} +1 -1
  25. package/dist/dashboard/{merge-agent-CGN3TT0a.js → merge-agent-CqvQu-n_.js} +1 -1
  26. package/dist/dashboard/{merge-agent-yudQOPZc.js → merge-agent-Dxxc4JEE.js} +5 -5
  27. package/dist/dashboard/{merge-agent-yudQOPZc.js.map → merge-agent-Dxxc4JEE.js.map} +1 -1
  28. package/dist/dashboard/public/assets/{dist-C-wcq54x.js → dist-DS1gmhe1.js} +1 -1
  29. package/dist/dashboard/public/assets/index-DjGsaJLv.js +212 -0
  30. package/dist/dashboard/public/index.html +1 -1
  31. package/dist/dashboard/{review-status-BtXqWBhS.js → review-status-Dww2OKUX.js} +1 -1
  32. package/dist/dashboard/{review-status-Bymwzh2i.js → review-status-d_wOE-XQ.js} +3 -3
  33. package/dist/dashboard/{review-status-Bymwzh2i.js.map → review-status-d_wOE-XQ.js.map} +1 -1
  34. package/dist/dashboard/server.js +97 -97
  35. package/dist/dashboard/settings-BHlDG7TK.js.map +1 -1
  36. package/dist/dashboard/{spawn-planning-session-D5hrVdWM.js → spawn-planning-session-D5uEpHzf.js} +1 -1
  37. package/dist/dashboard/{spawn-planning-session-33Jf-d5T.js → spawn-planning-session-DtbNfA2Q.js} +3 -3
  38. package/dist/dashboard/{spawn-planning-session-33Jf-d5T.js.map → spawn-planning-session-DtbNfA2Q.js.map} +1 -1
  39. package/dist/dashboard/{specialist-context-DGukHSn8.js → specialist-context-CEKqWqyF.js} +4 -4
  40. package/dist/dashboard/{specialist-context-DGukHSn8.js.map → specialist-context-CEKqWqyF.js.map} +1 -1
  41. package/dist/dashboard/{specialist-logs-CIw4qfTy.js → specialist-logs-CBGVRoQF.js} +1 -1
  42. package/dist/dashboard/{specialists-Cp-PgspS.js → specialists-sIFlMd3s.js} +1 -1
  43. package/dist/dashboard/{specialists-B_zrayaP.js → specialists-saEYE0-z.js} +20 -20
  44. package/dist/dashboard/{specialists-B_zrayaP.js.map → specialists-saEYE0-z.js.map} +1 -1
  45. package/dist/dashboard/{test-agent-queue-ypF_ecHo.js → test-agent-queue-7jXB2KkN.js} +3 -3
  46. package/dist/dashboard/{test-agent-queue-ypF_ecHo.js.map → test-agent-queue-7jXB2KkN.js.map} +1 -1
  47. package/dist/dashboard/{tracker-config-BP59uH4V.js → tracker-config-BX6ijWOc.js} +1 -1
  48. package/dist/dashboard/{tracker-config-e7ph1QqT.js → tracker-config-tD22z5sv.js} +2 -2
  49. package/dist/dashboard/{tracker-config-e7ph1QqT.js.map → tracker-config-tD22z5sv.js.map} +1 -1
  50. package/dist/dashboard/{work-agent-prompt-fCg67nyo.js → work-agent-prompt-D3tPzPvb.js} +2 -2
  51. package/dist/dashboard/{work-agent-prompt-fCg67nyo.js.map → work-agent-prompt-D3tPzPvb.js.map} +1 -1
  52. package/dist/dashboard/{work-type-router-CWVW2Wk_.js → work-type-router-7kwLSwrP.js} +4 -2
  53. package/dist/dashboard/work-type-router-7kwLSwrP.js.map +1 -0
  54. package/dist/dashboard/{work-type-router-Di5gCQwh.js → work-type-router-ByOOudGz.js} +1 -1
  55. package/dist/dashboard/workflows-BDpPjK18.js +2 -0
  56. package/dist/dashboard/{workflows-BSMipN07.js → workflows-DcEeDkbS.js} +3 -3
  57. package/dist/dashboard/{workflows-BSMipN07.js.map → workflows-DcEeDkbS.js.map} +1 -1
  58. package/dist/{factory-BRBGw6OB.js → factory-BR48tuUR.js} +1 -1
  59. package/dist/{factory-DzsOiZVc.js → factory-D6LJaZ__.js} +2 -2
  60. package/dist/{factory-DzsOiZVc.js.map → factory-D6LJaZ__.js.map} +1 -1
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.js +3 -3
  63. package/dist/{merge-agent-DlUiUanN.js → merge-agent-BBwHwpn2.js} +3 -3
  64. package/dist/{merge-agent-DlUiUanN.js.map → merge-agent-BBwHwpn2.js.map} +1 -1
  65. package/dist/{review-status-DEDvCKMP.js → review-status-Ba6llgCb.js} +3 -3
  66. package/dist/{review-status-DEDvCKMP.js.map → review-status-Ba6llgCb.js.map} +1 -1
  67. package/dist/{review-status-D6H2WOw8.js → review-status-Chxzuwn2.js} +1 -1
  68. package/dist/{settings-BcWPTrua.js → settings-A-CWz_ph.js} +6 -2
  69. package/dist/{settings-BcWPTrua.js.map → settings-A-CWz_ph.js.map} +1 -1
  70. package/dist/{specialist-context-BAUWL1Fl.js → specialist-context-B3lknlwi.js} +4 -4
  71. package/dist/{specialist-context-BAUWL1Fl.js.map → specialist-context-B3lknlwi.js.map} +1 -1
  72. package/dist/{specialist-logs-DQKKQV9B.js → specialist-logs-DDyY4xqo.js} +1 -1
  73. package/dist/{specialists-D7Kj5o6s.js → specialists-DvTYu1VZ.js} +20 -20
  74. package/dist/{specialists-D7Kj5o6s.js.map → specialists-DvTYu1VZ.js.map} +1 -1
  75. package/dist/{specialists-Bfb9ATzw.js → specialists-DyB4IRlM.js} +1 -1
  76. package/dist/sync-CLVqiGl4.js +2 -0
  77. package/dist/{sync-DMfgd389.js → sync-DTHFlEc-.js} +2 -2
  78. package/dist/{sync-DMfgd389.js.map → sync-DTHFlEc-.js.map} +1 -1
  79. package/dist/{tracker-BhYYvU3p.js → tracker-CYpb7oUa.js} +2 -2
  80. package/dist/{tracker-BhYYvU3p.js.map → tracker-CYpb7oUa.js.map} +1 -1
  81. package/dist/{work-type-router-CHjciPyS.js → work-type-router-oCgTPXsP.js} +4 -2
  82. package/dist/work-type-router-oCgTPXsP.js.map +1 -0
  83. package/package.json +1 -1
  84. package/dist/config-yaml-DGbLSMCa.js.map +0 -1
  85. package/dist/dashboard/public/assets/index-DKlrFY1k.js +0 -212
  86. package/dist/dashboard/work-type-router-CWVW2Wk_.js.map +0 -1
  87. package/dist/dashboard/workflows-DaYWQIS2.js +0 -2
  88. package/dist/sync-TL6y-8K6.js +0 -2
  89. package/dist/work-type-router-CHjciPyS.js.map +0 -1
@@ -585,7 +585,7 @@ function setReviewStatus(issueId, update, filePath = DEFAULT_STATUS_FILE) {
585
585
  });
586
586
  if (update.reviewStatus === "passed" && existing.reviewStatus !== "passed" && existing.testStatus === "pending") (async () => {
587
587
  try {
588
- const { submitToSpecialistQueue } = await import("./specialists-Bfb9ATzw.js");
588
+ const { submitToSpecialistQueue } = await import("./specialists-DyB4IRlM.js");
589
589
  const workAgentId = `agent-${issueId.toLowerCase()}`;
590
590
  const workStateFile = join(homedir(), ".panopticon", "agents", workAgentId, "state.json");
591
591
  let workspace;
@@ -615,7 +615,7 @@ function setReviewStatus(issueId, update, filePath = DEFAULT_STATUS_FILE) {
615
615
  if (!sessionExists(agentSession)) return;
616
616
  const statusType = update.reviewStatus === "blocked" ? "REVIEW BLOCKED" : "TESTS FAILED";
617
617
  const msg = `SPECIALIST FEEDBACK: ${statusType} for ${issueId}.\n\n${update.reviewNotes || update.testNotes || "No details provided."}\n\nFix the issues, then run: pan work done ${issueId}`;
618
- const { messageAgent } = await import("./agents-D_2oRFVf.js");
618
+ const { messageAgent } = await import("./agents-BQOqo27C.js");
619
619
  await messageAgent(agentSession, msg);
620
620
  console.log(`[review-status] Auto-delivered ${statusType} feedback to ${agentSession}`);
621
621
  } catch (err) {
@@ -651,4 +651,4 @@ var init_review_status = __esmMin((() => {
651
651
  //#endregion
652
652
  export { saveReviewStatuses as a, getDatabase as c, loadReviewStatuses as i, init_database as l, getReviewStatus as n, setReviewStatus as o, init_review_status as r, closeDatabase as s, clearReviewStatus as t };
653
653
 
654
- //# sourceMappingURL=review-status-DEDvCKMP.js.map
654
+ //# sourceMappingURL=review-status-Ba6llgCb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"review-status-DEDvCKMP.js","names":[],"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 = 13;\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 -- ===== Domain Events (PAN-428: push-first architecture) =====\n CREATE TABLE IF NOT EXISTS events (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n payload TEXT NOT NULL -- JSON\n );\n\n CREATE INDEX IF NOT EXISTS idx_events_type\n ON events(type);\n\n CREATE INDEX IF NOT EXISTS idx_events_timestamp\n ON events(timestamp);\n\n -- ===== Projection Cache (PAN-437: instant dashboard startup) =====\n CREATE TABLE IF NOT EXISTS projection_cache (\n key TEXT PRIMARY KEY,\n data TEXT NOT NULL, -- JSON-serialized DashboardSnapshot\n sequence INTEGER NOT NULL, -- Last event sequence applied\n updated_at TEXT NOT NULL -- ISO timestamp\n );\n\n -- ===== Conversations (PAN-416: Mission Control conversation launcher) =====\n CREATE TABLE IF NOT EXISTS conversations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n tmux_session TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'active', -- 'active', 'ended'\n cwd TEXT NOT NULL,\n issue_id TEXT, -- optional cost attribution\n created_at TEXT NOT NULL,\n ended_at TEXT,\n last_attached_at TEXT,\n session_file TEXT, -- path to Claude Code JSONL session file (PAN-451)\n title TEXT, -- human-readable title, auto-set from first message\n title_source TEXT, -- 'auto', 'ai', or 'manual'\n title_seed TEXT, -- original auto-generated title for replacement check\n total_cost REAL DEFAULT 0, -- cached total cost in USD\n archived_at TEXT, -- ISO timestamp when archived, null = active\n model TEXT, -- model used to spawn conversation (e.g. 'minimax-m2.7-highspeed')\n effort TEXT -- effort level (e.g. 'low', 'medium', 'high')\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_status\n ON conversations(status);\n\n CREATE INDEX IF NOT EXISTS idx_conversations_created_at\n ON conversations(created_at);\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 // v3 → v4: add events table for push-first architecture (PAN-428)\n if (currentVersion < 4) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS events (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n payload TEXT NOT NULL -- JSON\n );\n\n CREATE INDEX IF NOT EXISTS idx_events_type\n ON events(type);\n\n CREATE INDEX IF NOT EXISTS idx_events_timestamp\n ON events(timestamp);\n `);\n }\n\n // v4 → v5: add projection_cache table (PAN-437: instant dashboard startup)\n if (currentVersion < 5) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS projection_cache (\n key TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n updated_at TEXT NOT NULL\n );\n `);\n }\n\n // v5 → v6: add conversations table (PAN-416)\n if (currentVersion < 6) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n tmux_session TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'active',\n cwd TEXT NOT NULL,\n issue_id TEXT,\n created_at TEXT NOT NULL,\n ended_at TEXT,\n last_attached_at TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_status\n ON conversations(status);\n\n CREATE INDEX IF NOT EXISTS idx_conversations_created_at\n ON conversations(created_at);\n `);\n }\n\n // v6 → v7: add session_file column to conversations (PAN-451)\n if (currentVersion < 7) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN session_file TEXT`);\n } catch { /* already exists */ }\n }\n\n // v7 → v8: add title column to conversations (auto-set from first message)\n if (currentVersion < 8) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN title TEXT`);\n } catch { /* already exists */ }\n }\n\n // v8 → v9: add title_source and title_seed columns to conversations\n // title_source tracks how the title was set: 'auto' (truncated first message),\n // 'ai' (Claude-generated), or 'manual' (user renamed). Used for T3Code-style\n // canReplaceThreadTitle logic — only auto-generated titles get AI replacement.\n // title_seed stores the original truncated message for replacement eligibility.\n if (currentVersion < 9) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN title_source TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN title_seed TEXT`);\n } catch { /* already exists */ }\n }\n\n // v9 → v10: add total_cost column to conversations (cached cost in USD)\n if (currentVersion < 10) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN total_cost REAL DEFAULT 0`);\n } catch { /* already exists */ }\n }\n\n // v10 → v11: expression index for UPPER(issue_id) on cost_events\n // The N+1 queries in getCostsByIssueFromDb use UPPER(issue_id) which defeats\n // the existing idx_cost_issue_id index. This expression index fixes that.\n if (currentVersion < 11) {\n try {\n db.exec(`CREATE INDEX IF NOT EXISTS idx_cost_issue_upper ON cost_events(UPPER(issue_id))`);\n } catch { /* already exists */ }\n }\n\n // v11 → v12: archived_at column + index for conversations (T3Code pattern)\n if (currentVersion < 12) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN archived_at TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_archived ON conversations(archived_at)`);\n } catch { /* already exists */ }\n }\n\n // v12 → v13: add model + effort columns to conversations (preserve model on resume)\n if (currentVersion < 13) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN model TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN effort TEXT`);\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 * Dual-runtime (PAN-428):\n * - Bun: uses bun:sqlite (better-sqlite3 is a native addon — ERR_DLOPEN_FAILED in Bun)\n * - Node: uses better-sqlite3\n * In both cases the external API is identical: pragma(), exec(), prepare(), close().\n */\n\nimport type Database from 'better-sqlite3';\nimport { createRequire } from 'module';\nimport { join } from 'path';\nimport { existsSync, mkdirSync } from 'fs';\nimport { getPanopticonHome } from '../paths.js';\nimport { runMigrations } from './schema.js';\n\ndeclare const Bun: unknown;\n\nfunction isBunRuntime(): boolean {\n return typeof Bun !== 'undefined';\n}\n\n// createRequire allows synchronous require() in ESM — works in both Bun and Node\nconst _require = createRequire(import.meta.url);\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\n if (isBunRuntime()) {\n // better-sqlite3 is a native Node.js addon that fails in Bun with ERR_DLOPEN_FAILED.\n // Use bun:sqlite instead, with a pragma() shim for API compatibility.\n const { Database: BunDatabase } = _require('bun:sqlite') as { Database: new (path: string) => any };\n const bunDb = new BunDatabase(dbPath);\n\n // bun:sqlite has no pragma() method — shim it using exec() and query().get()\n bunDb.pragma = function (sql: string, options?: { simple?: boolean }): any {\n if (options?.simple) {\n // Read-only: return the scalar value directly (e.g. db.pragma('user_version', { simple: true }))\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 // Set or no-return pragma (e.g. 'journal_mode = WAL', 'foreign_keys = ON')\n bunDb.exec(`PRAGMA ${sql}`);\n return undefined;\n };\n\n _db = bunDb as Database.Database;\n } else {\n // Node.js path: load better-sqlite3 lazily (avoids import-time native addon load)\n const BetterSqlite3 = _require('better-sqlite3');\n _db = new BetterSqlite3(dbPath) as Database.Database;\n }\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 /** HEAD commit SHA at the time review passed — used to detect new commits after review */\n reviewedAtCommit?: string;\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 // verificationStatus must not be 'failed' — verification catches pre-existing test breakage\n // that scoped test runs (e2e/dashboard) may miss.\n const readyForMerge = update.readyForMerge !== undefined\n ? update.readyForMerge\n : (\n merged.reviewStatus === 'passed' &&\n merged.testStatus === 'passed' &&\n merged.verificationStatus !== 'failed' &&\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 // Report commit statuses to GitHub when readyForMerge transitions to true (PAN-536)\n if (readyForMerge && !existing.readyForMerge && updated.prUrl) {\n (async () => {\n try {\n const { isGitHubAppConfigured, reportCommitStatus } = await import('./github-app.js');\n if (!isGitHubAppConfigured()) return;\n const prMatch = updated.prUrl!.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull/);\n if (!prMatch) return;\n const [, owner, repo] = prMatch;\n // Get HEAD SHA of the PR branch\n const { exec } = await import('child_process');\n const { promisify } = await import('util');\n const execAsync = promisify(exec);\n const { stdout } = await execAsync(\n `gh pr view ${updated.prUrl!.match(/\\/pull\\/(\\d+)/)?.[1]} --json headRefOid --jq .headRefOid`,\n { encoding: 'utf-8', timeout: 10000 }\n );\n const sha = stdout.trim();\n if (sha) {\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/review', 'Review passed');\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/test', 'Tests passed');\n console.log(`[review-status] Reported commit statuses for ${issueId} (${sha.slice(0, 8)})`);\n }\n } catch (err: any) {\n console.warn(`[review-status] Failed to report commit status: ${err.message}`);\n }\n })();\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 // Queue test-agent when review transitions to 'passed'.\n // This fires regardless of how setReviewStatus() is called (API or direct import),\n // ensuring test-agent is queued even when review-agent bypasses the specialist\n // dispatch endpoint. Idempotent — if test-agent is already queued, pushToHook\n // deduplicates by issueId.\n if (\n update.reviewStatus === 'passed' &&\n existing.reviewStatus !== 'passed' &&\n existing.testStatus === 'pending'\n ) {\n (async () => {\n try {\n const { submitToSpecialistQueue } = await import('./cloister/specialists.js');\n const workAgentId = `agent-${issueId.toLowerCase()}`;\n const workStateFile = join(homedir(), '.panopticon', 'agents', workAgentId, 'state.json');\n let workspace: string | undefined;\n let branch: string | undefined;\n if (existsSync(workStateFile)) {\n try {\n const workState = JSON.parse(readFileSync(workStateFile, 'utf-8'));\n workspace = workState.workspace;\n branch = workState.branch || `feature/${issueId.toLowerCase()}`;\n } catch {}\n }\n submitToSpecialistQueue('test-agent', {\n priority: 'high',\n source: 'review-agent-auto',\n issueId,\n workspace,\n branch,\n });\n console.log(`[review-status] Queued test-agent for ${issueId} after review passed`);\n } catch (err: any) {\n console.warn(`[review-status] Failed to queue test-agent for ${issueId}: ${err.message}`);\n }\n })();\n }\n\n // Auto-deliver feedback to work agent when review blocks or tests fail.\n // This ensures feedback reaches the agent regardless of whether status was\n // set via the dashboard API or directly (e.g., bun -e import). See PAN-586.\n if (\n (update.reviewStatus === 'blocked' || update.testStatus === 'failed') &&\n (update.reviewStatus !== existing.reviewStatus || update.testStatus !== existing.testStatus)\n ) {\n const agentSession = `agent-${issueId.toLowerCase()}`;\n (async () => {\n try {\n const { sessionExists } = await import('./tmux.js');\n if (!sessionExists(agentSession)) return;\n\n const statusType = update.reviewStatus === 'blocked' ? 'REVIEW BLOCKED' : 'TESTS FAILED';\n const notes = update.reviewNotes || update.testNotes || 'No details provided.';\n const msg = `SPECIALIST FEEDBACK: ${statusType} for ${issueId}.\\n\\n${notes}\\n\\nFix the issues, then run: pan work done ${issueId}`;\n\n const { messageAgent } = await import('./agents.js');\n await messageAgent(agentSession, msg);\n console.log(`[review-status] Auto-delivered ${statusType} feedback to ${agentSession}`);\n } catch (err: any) {\n console.warn(`[review-status] Failed to auto-deliver feedback to ${agentSession}: ${err.message}`);\n }\n })();\n }\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\n/**\n * On server startup, clear any mergeStatus stuck at 'merging'.\n * Pending merge operations are in-memory only — they don't survive a restart.\n * Any 'merging' status after boot is definitionally stuck (PAN-490).\n */\nexport function clearStuckMergeStatuses(): void {\n const statuses = loadReviewStatuses();\n const stuck = Object.values(statuses).filter(s => s.mergeStatus === 'merging');\n if (stuck.length === 0) return;\n console.log(`[review-status] Clearing ${stuck.length} stuck 'merging' status(es) on startup`);\n for (const s of stuck) {\n setReviewStatus(s.issueId, { mergeStatus: 'pending' });\n }\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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"}
1
+ {"version":3,"file":"review-status-Ba6llgCb.js","names":[],"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 = 13;\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 -- ===== Domain Events (PAN-428: push-first architecture) =====\n CREATE TABLE IF NOT EXISTS events (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n payload TEXT NOT NULL -- JSON\n );\n\n CREATE INDEX IF NOT EXISTS idx_events_type\n ON events(type);\n\n CREATE INDEX IF NOT EXISTS idx_events_timestamp\n ON events(timestamp);\n\n -- ===== Projection Cache (PAN-437: instant dashboard startup) =====\n CREATE TABLE IF NOT EXISTS projection_cache (\n key TEXT PRIMARY KEY,\n data TEXT NOT NULL, -- JSON-serialized DashboardSnapshot\n sequence INTEGER NOT NULL, -- Last event sequence applied\n updated_at TEXT NOT NULL -- ISO timestamp\n );\n\n -- ===== Conversations (PAN-416: Mission Control conversation launcher) =====\n CREATE TABLE IF NOT EXISTS conversations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n tmux_session TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'active', -- 'active', 'ended'\n cwd TEXT NOT NULL,\n issue_id TEXT, -- optional cost attribution\n created_at TEXT NOT NULL,\n ended_at TEXT,\n last_attached_at TEXT,\n session_file TEXT, -- path to Claude Code JSONL session file (PAN-451)\n title TEXT, -- human-readable title, auto-set from first message\n title_source TEXT, -- 'auto', 'ai', or 'manual'\n title_seed TEXT, -- original auto-generated title for replacement check\n total_cost REAL DEFAULT 0, -- cached total cost in USD\n archived_at TEXT, -- ISO timestamp when archived, null = active\n model TEXT, -- model used to spawn conversation (e.g. 'minimax-m2.7-highspeed')\n effort TEXT -- effort level (e.g. 'low', 'medium', 'high')\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_status\n ON conversations(status);\n\n CREATE INDEX IF NOT EXISTS idx_conversations_created_at\n ON conversations(created_at);\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 // v3 → v4: add events table for push-first architecture (PAN-428)\n if (currentVersion < 4) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS events (\n sequence INTEGER PRIMARY KEY AUTOINCREMENT,\n type TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n payload TEXT NOT NULL -- JSON\n );\n\n CREATE INDEX IF NOT EXISTS idx_events_type\n ON events(type);\n\n CREATE INDEX IF NOT EXISTS idx_events_timestamp\n ON events(timestamp);\n `);\n }\n\n // v4 → v5: add projection_cache table (PAN-437: instant dashboard startup)\n if (currentVersion < 5) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS projection_cache (\n key TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n sequence INTEGER NOT NULL,\n updated_at TEXT NOT NULL\n );\n `);\n }\n\n // v5 → v6: add conversations table (PAN-416)\n if (currentVersion < 6) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n tmux_session TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'active',\n cwd TEXT NOT NULL,\n issue_id TEXT,\n created_at TEXT NOT NULL,\n ended_at TEXT,\n last_attached_at TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_status\n ON conversations(status);\n\n CREATE INDEX IF NOT EXISTS idx_conversations_created_at\n ON conversations(created_at);\n `);\n }\n\n // v6 → v7: add session_file column to conversations (PAN-451)\n if (currentVersion < 7) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN session_file TEXT`);\n } catch { /* already exists */ }\n }\n\n // v7 → v8: add title column to conversations (auto-set from first message)\n if (currentVersion < 8) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN title TEXT`);\n } catch { /* already exists */ }\n }\n\n // v8 → v9: add title_source and title_seed columns to conversations\n // title_source tracks how the title was set: 'auto' (truncated first message),\n // 'ai' (Claude-generated), or 'manual' (user renamed). Used for T3Code-style\n // canReplaceThreadTitle logic — only auto-generated titles get AI replacement.\n // title_seed stores the original truncated message for replacement eligibility.\n if (currentVersion < 9) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN title_source TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN title_seed TEXT`);\n } catch { /* already exists */ }\n }\n\n // v9 → v10: add total_cost column to conversations (cached cost in USD)\n if (currentVersion < 10) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN total_cost REAL DEFAULT 0`);\n } catch { /* already exists */ }\n }\n\n // v10 → v11: expression index for UPPER(issue_id) on cost_events\n // The N+1 queries in getCostsByIssueFromDb use UPPER(issue_id) which defeats\n // the existing idx_cost_issue_id index. This expression index fixes that.\n if (currentVersion < 11) {\n try {\n db.exec(`CREATE INDEX IF NOT EXISTS idx_cost_issue_upper ON cost_events(UPPER(issue_id))`);\n } catch { /* already exists */ }\n }\n\n // v11 → v12: archived_at column + index for conversations (T3Code pattern)\n if (currentVersion < 12) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN archived_at TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_archived ON conversations(archived_at)`);\n } catch { /* already exists */ }\n }\n\n // v12 → v13: add model + effort columns to conversations (preserve model on resume)\n if (currentVersion < 13) {\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN model TEXT`);\n } catch { /* already exists */ }\n try {\n db.exec(`ALTER TABLE conversations ADD COLUMN effort TEXT`);\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 * Dual-runtime (PAN-428):\n * - Bun: uses bun:sqlite (better-sqlite3 is a native addon — ERR_DLOPEN_FAILED in Bun)\n * - Node: uses better-sqlite3\n * In both cases the external API is identical: pragma(), exec(), prepare(), close().\n */\n\nimport type Database from 'better-sqlite3';\nimport { createRequire } from 'module';\nimport { join } from 'path';\nimport { existsSync, mkdirSync } from 'fs';\nimport { getPanopticonHome } from '../paths.js';\nimport { runMigrations } from './schema.js';\n\ndeclare const Bun: unknown;\n\nfunction isBunRuntime(): boolean {\n return typeof Bun !== 'undefined';\n}\n\n// createRequire allows synchronous require() in ESM — works in both Bun and Node\nconst _require = createRequire(import.meta.url);\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\n if (isBunRuntime()) {\n // better-sqlite3 is a native Node.js addon that fails in Bun with ERR_DLOPEN_FAILED.\n // Use bun:sqlite instead, with a pragma() shim for API compatibility.\n const { Database: BunDatabase } = _require('bun:sqlite') as { Database: new (path: string) => any };\n const bunDb = new BunDatabase(dbPath);\n\n // bun:sqlite has no pragma() method — shim it using exec() and query().get()\n bunDb.pragma = function (sql: string, options?: { simple?: boolean }): any {\n if (options?.simple) {\n // Read-only: return the scalar value directly (e.g. db.pragma('user_version', { simple: true }))\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 // Set or no-return pragma (e.g. 'journal_mode = WAL', 'foreign_keys = ON')\n bunDb.exec(`PRAGMA ${sql}`);\n return undefined;\n };\n\n _db = bunDb as Database.Database;\n } else {\n // Node.js path: load better-sqlite3 lazily (avoids import-time native addon load)\n const BetterSqlite3 = _require('better-sqlite3');\n _db = new BetterSqlite3(dbPath) as Database.Database;\n }\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 /** HEAD commit SHA at the time review passed — used to detect new commits after review */\n reviewedAtCommit?: string;\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 // verificationStatus must not be 'failed' — verification catches pre-existing test breakage\n // that scoped test runs (e2e/dashboard) may miss.\n const readyForMerge = update.readyForMerge !== undefined\n ? update.readyForMerge\n : (\n merged.reviewStatus === 'passed' &&\n merged.testStatus === 'passed' &&\n merged.verificationStatus !== 'failed' &&\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 // Report commit statuses to GitHub when readyForMerge transitions to true (PAN-536)\n if (readyForMerge && !existing.readyForMerge && updated.prUrl) {\n (async () => {\n try {\n const { isGitHubAppConfigured, reportCommitStatus } = await import('./github-app.js');\n if (!isGitHubAppConfigured()) return;\n const prMatch = updated.prUrl!.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull/);\n if (!prMatch) return;\n const [, owner, repo] = prMatch;\n // Get HEAD SHA of the PR branch\n const { exec } = await import('child_process');\n const { promisify } = await import('util');\n const execAsync = promisify(exec);\n const { stdout } = await execAsync(\n `gh pr view ${updated.prUrl!.match(/\\/pull\\/(\\d+)/)?.[1]} --json headRefOid --jq .headRefOid`,\n { encoding: 'utf-8', timeout: 10000 }\n );\n const sha = stdout.trim();\n if (sha) {\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/review', 'Review passed');\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/test', 'Tests passed');\n console.log(`[review-status] Reported commit statuses for ${issueId} (${sha.slice(0, 8)})`);\n }\n } catch (err: any) {\n console.warn(`[review-status] Failed to report commit status: ${err.message}`);\n }\n })();\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 // Queue test-agent when review transitions to 'passed'.\n // This fires regardless of how setReviewStatus() is called (API or direct import),\n // ensuring test-agent is queued even when review-agent bypasses the specialist\n // dispatch endpoint. Idempotent — if test-agent is already queued, pushToHook\n // deduplicates by issueId.\n if (\n update.reviewStatus === 'passed' &&\n existing.reviewStatus !== 'passed' &&\n existing.testStatus === 'pending'\n ) {\n (async () => {\n try {\n const { submitToSpecialistQueue } = await import('./cloister/specialists.js');\n const workAgentId = `agent-${issueId.toLowerCase()}`;\n const workStateFile = join(homedir(), '.panopticon', 'agents', workAgentId, 'state.json');\n let workspace: string | undefined;\n let branch: string | undefined;\n if (existsSync(workStateFile)) {\n try {\n const workState = JSON.parse(readFileSync(workStateFile, 'utf-8'));\n workspace = workState.workspace;\n branch = workState.branch || `feature/${issueId.toLowerCase()}`;\n } catch {}\n }\n submitToSpecialistQueue('test-agent', {\n priority: 'high',\n source: 'review-agent-auto',\n issueId,\n workspace,\n branch,\n });\n console.log(`[review-status] Queued test-agent for ${issueId} after review passed`);\n } catch (err: any) {\n console.warn(`[review-status] Failed to queue test-agent for ${issueId}: ${err.message}`);\n }\n })();\n }\n\n // Auto-deliver feedback to work agent when review blocks or tests fail.\n // This ensures feedback reaches the agent regardless of whether status was\n // set via the dashboard API or directly (e.g., bun -e import). See PAN-586.\n if (\n (update.reviewStatus === 'blocked' || update.testStatus === 'failed') &&\n (update.reviewStatus !== existing.reviewStatus || update.testStatus !== existing.testStatus)\n ) {\n const agentSession = `agent-${issueId.toLowerCase()}`;\n (async () => {\n try {\n const { sessionExists } = await import('./tmux.js');\n if (!sessionExists(agentSession)) return;\n\n const statusType = update.reviewStatus === 'blocked' ? 'REVIEW BLOCKED' : 'TESTS FAILED';\n const notes = update.reviewNotes || update.testNotes || 'No details provided.';\n const msg = `SPECIALIST FEEDBACK: ${statusType} for ${issueId}.\\n\\n${notes}\\n\\nFix the issues, then run: pan work done ${issueId}`;\n\n const { messageAgent } = await import('./agents.js');\n await messageAgent(agentSession, msg);\n console.log(`[review-status] Auto-delivered ${statusType} feedback to ${agentSession}`);\n } catch (err: any) {\n console.warn(`[review-status] Failed to auto-deliver feedback to ${agentSession}: ${err.message}`);\n }\n })();\n }\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\n/**\n * On server startup, clear any mergeStatus stuck at 'merging'.\n * Pending merge operations are in-memory only — they don't survive a restart.\n * Any 'merging' status after boot is definitionally stuck (PAN-490).\n */\nexport function clearStuckMergeStatuses(): void {\n const statuses = loadReviewStatuses();\n const stuck = Object.values(statuses).filter(s => s.mergeStatus === 'merging');\n if (stuck.length === 0) return;\n console.log(`[review-status] Clearing ${stuck.length} stuck 'merging' status(es) on startup`);\n for (const s of stuck) {\n setReviewStatus(s.issueId, { mergeStatus: 'pending' });\n }\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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"}
@@ -1,3 +1,3 @@
1
- import { i as loadReviewStatuses, n as getReviewStatus, o as setReviewStatus, r as init_review_status, t as clearReviewStatus } from "./review-status-DEDvCKMP.js";
1
+ import { i as loadReviewStatuses, n as getReviewStatus, o as setReviewStatus, r as init_review_status, t as clearReviewStatus } from "./review-status-Ba6llgCb.js";
2
2
  init_review_status();
3
3
  export { clearReviewStatus, getReviewStatus, loadReviewStatuses, setReviewStatus };
@@ -570,7 +570,11 @@ function getAvailableModels(settings) {
570
570
  "gpt-4o-mini"
571
571
  ] : [],
572
572
  google: settings.api_keys.google ? ["gemini-3-pro-preview", "gemini-3-flash-preview"] : [],
573
- zai: settings.api_keys.zai ? ["glm-4.7-flash"] : [],
573
+ zai: settings.api_keys.zai ? [
574
+ "glm-4.7",
575
+ "glm-4.7-flash",
576
+ "glm-5.1"
577
+ ] : [],
574
578
  kimi: settings.api_keys.kimi ? ["kimi-k2", "kimi-k2.5"] : []
575
579
  };
576
580
  }
@@ -611,4 +615,4 @@ function getAgentCommand(modelId) {
611
615
  //#endregion
612
616
  export { cleanOldBackups as _, isAnthropicModel as a, listBackups as b, validateSettings as c, migrateStalePersonalContent as d, planHooksSync as f, syncStatusline as g, syncHooks as h, getDefaultSettings as i, executeSync as l, refreshCache as m, getAvailableModels as n, loadSettings as o, planSync as p, getClaudeModelFlag as r, saveSettings as s, getAgentCommand as t, isPanopticonSymlink as u, createBackup as v, restoreBackup as x, createBackupTimestamp as y };
613
617
 
614
- //# sourceMappingURL=settings-BcWPTrua.js.map
618
+ //# sourceMappingURL=settings-A-CWz_ph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-BcWPTrua.js","names":[],"sources":["../src/lib/backup.ts","../src/lib/sync.ts","../src/lib/settings.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, cpSync, rmSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { BACKUPS_DIR } from './paths.js';\n\nexport interface BackupInfo {\n timestamp: string;\n path: string;\n targets: string[];\n}\n\nexport function createBackupTimestamp(): string {\n return new Date().toISOString().replace(/[:.]/g, '-');\n}\n\nexport function createBackup(sourceDirs: string[]): BackupInfo {\n const timestamp = createBackupTimestamp();\n const backupPath = join(BACKUPS_DIR, timestamp);\n\n mkdirSync(backupPath, { recursive: true });\n\n const targets: string[] = [];\n\n for (const sourceDir of sourceDirs) {\n if (!existsSync(sourceDir)) continue;\n\n const targetName = basename(sourceDir);\n const targetPath = join(backupPath, targetName);\n\n // Use filter to skip symlinks — sync targets (e.g. ~/.claude/skills/)\n // contain symlinks back into ~/.panopticon/skills/ which causes cpSync\n // to fail with \"cannot copy to a subdirectory of self\".\n cpSync(sourceDir, targetPath, {\n recursive: true,\n filter: (src) => !lstatSync(src).isSymbolicLink(),\n });\n targets.push(targetName);\n }\n\n return {\n timestamp,\n path: backupPath,\n targets,\n };\n}\n\nexport function listBackups(): BackupInfo[] {\n if (!existsSync(BACKUPS_DIR)) return [];\n\n const entries = readdirSync(BACKUPS_DIR, { withFileTypes: true });\n\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => {\n const backupPath = join(BACKUPS_DIR, e.name);\n const contents = readdirSync(backupPath);\n\n return {\n timestamp: e.name,\n path: backupPath,\n targets: contents,\n };\n })\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n}\n\nexport function restoreBackup(timestamp: string, targetDirs: Record<string, string>): void {\n const backupPath = join(BACKUPS_DIR, timestamp);\n\n if (!existsSync(backupPath)) {\n throw new Error(`Backup not found: ${timestamp}`);\n }\n\n const contents = readdirSync(backupPath, { withFileTypes: true });\n\n for (const entry of contents) {\n if (!entry.isDirectory()) continue;\n\n const sourcePath = join(backupPath, entry.name);\n const targetPath = targetDirs[entry.name];\n\n if (!targetPath) continue;\n\n // Remove existing and restore from backup\n if (existsSync(targetPath)) {\n rmSync(targetPath, { recursive: true });\n }\n\n cpSync(sourcePath, targetPath, { recursive: true });\n }\n}\n\nexport function cleanOldBackups(keepCount: number = 10): number {\n const backups = listBackups();\n\n if (backups.length <= keepCount) return 0;\n\n const toRemove = backups.slice(keepCount);\n let removed = 0;\n\n for (const backup of toRemove) {\n rmSync(backup.path, { recursive: true });\n removed++;\n }\n\n return removed;\n}\n","import { existsSync, mkdirSync, readdirSync, symlinkSync, unlinkSync, lstatSync, readlinkSync, rmSync, copyFileSync, chmodSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename, dirname, relative } from 'path';\nimport { homedir } from 'os';\nimport {\n SKILLS_DIR, COMMANDS_DIR, AGENTS_DIR, BIN_DIR,\n SOURCE_SCRIPTS_DIR, SOURCE_DEV_SKILLS_DIR, SOURCE_SKILLS_DIR, SOURCE_AGENTS_DIR, SOURCE_RULES_DIR,\n CACHE_AGENTS_DIR, CACHE_RULES_DIR, CACHE_MANIFEST,\n SYNC_TARGET, isDevMode,\n} from './paths.js';\nimport {\n buildManifestFromDirectory, writeManifest, readManifest, hashFile,\n setManifestEntry, collectSourceFiles,\n type Manifest, type FileStatus,\n compareFileToManifest,\n} from './manifest.js';\nimport { getDevrootPath } from './config.js';\n\nexport interface SyncItem {\n name: string;\n sourcePath: string;\n targetPath: string;\n status: 'new' | 'exists' | 'conflict' | 'symlink';\n}\n\nexport interface SyncPlan {\n skills: SyncItem[];\n commands: SyncItem[];\n agents: SyncItem[];\n rules: SyncItem[];\n devSkills: SyncItem[]; // Developer-only skills (only synced in dev mode)\n}\n\n/**\n * Remove a file, symlink, or directory safely\n */\nfunction removeTarget(targetPath: string): void {\n const stats = lstatSync(targetPath);\n if (stats.isDirectory() && !stats.isSymbolicLink()) {\n // It's a real directory, remove recursively\n rmSync(targetPath, { recursive: true, force: true });\n } else {\n // It's a file or symlink\n unlinkSync(targetPath);\n }\n}\n\n/**\n * Check if a path is a Panopticon-managed symlink\n */\nexport function isPanopticonSymlink(targetPath: string): boolean {\n if (!existsSync(targetPath)) return false;\n\n try {\n const stats = lstatSync(targetPath);\n if (!stats.isSymbolicLink()) return false;\n\n const linkTarget = readlinkSync(targetPath);\n // It's ours if it points to our skills/commands dir\n return linkTarget.includes('.panopticon');\n } catch {\n return false;\n }\n}\n\nexport interface MigrationResult {\n removedSymlinks: string[];\n preservedUserContent: string[];\n errors: string[];\n}\n\n/**\n * One-time migration: remove Panopticon-managed symlinks from ~/.claude/.\n *\n * Detects symlinks in ~/.claude/skills/ and ~/.claude/agents/ that point to\n * .panopticon directories. Removes only those symlinks, preserving any\n * user-created content (real files/directories).\n *\n * This is safe to run multiple times — it's a no-op if nothing remains to clean up.\n *\n * Removes two kinds of stale Panopticon content from ~/.claude/:\n * 1. Symlinks pointing to .panopticon or panopticon-cli (legacy sync method)\n * 2. Plain directories that also exist in the devroot (stale copies from before\n * the devroot migration — these cause duplicate skill listings)\n */\nexport function migrateStalePersonalContent(): MigrationResult {\n const claudeDir = join(homedir(), '.claude');\n const result: MigrationResult = {\n removedSymlinks: [],\n preservedUserContent: [],\n errors: [],\n };\n\n // Build a set of skill/agent/command names that exist in the devroot\n // so we can identify stale copies in ~/.claude/\n const devrootNames = new Set<string>();\n const devroot = getDevrootPath();\n if (devroot) {\n for (const subdir of ['skills', 'commands', 'agents']) {\n const devrootDir = join(devroot, '.claude', subdir);\n if (existsSync(devrootDir)) {\n try {\n for (const entry of readdirSync(devrootDir)) {\n devrootNames.add(`${subdir}/${entry}`);\n }\n } catch {\n // Ignore read errors on devroot\n }\n }\n }\n }\n\n for (const subdir of ['skills', 'commands', 'agents']) {\n const dir = join(claudeDir, subdir);\n if (!existsSync(dir)) continue;\n\n try {\n const entries = readdirSync(dir);\n for (const entry of entries) {\n const entryPath = join(dir, entry);\n try {\n const stats = lstatSync(entryPath);\n if (stats.isSymbolicLink()) {\n const linkTarget = readlinkSync(entryPath);\n if (linkTarget.includes('.panopticon') || linkTarget.includes('panopticon-cli')) {\n unlinkSync(entryPath);\n result.removedSymlinks.push(`${subdir}/${entry}`);\n } else {\n // Symlink to somewhere else — leave it\n result.preservedUserContent.push(`${subdir}/${entry}`);\n }\n } else if (stats.isDirectory() && devrootNames.has(`${subdir}/${entry}`)) {\n // Plain directory that also exists in devroot — stale Panopticon copy.\n // The devroot copy is the canonical one; this personal copy causes\n // duplicate listings and violates principle #4 (never touch ~/.claude/).\n rmSync(entryPath, { recursive: true, force: true });\n result.removedSymlinks.push(`${subdir}/${entry} (stale copy)`);\n } else {\n // Real file/directory with no devroot counterpart — user content, never touch\n result.preservedUserContent.push(`${subdir}/${entry}`);\n }\n } catch (err: any) {\n result.errors.push(`${subdir}/${entry}: ${err.message}`);\n }\n }\n } catch (err: any) {\n result.errors.push(`${subdir}: ${err.message}`);\n }\n }\n\n return result;\n}\n\nexport interface RefreshCacheResult {\n skills: { copied: number; total: number };\n agents: { copied: number; total: number };\n rules: { copied: number; total: number };\n}\n\n/**\n * Recursively copy a directory, overwriting existing files.\n */\nfunction copyDirectoryRecursive(source: string, dest: string): number {\n if (!existsSync(source)) return 0;\n\n mkdirSync(dest, { recursive: true });\n let count = 0;\n\n const entries = readdirSync(source, { withFileTypes: true });\n for (const entry of entries) {\n const srcPath = join(source, entry.name);\n const dstPath = join(dest, entry.name);\n if (entry.isDirectory()) {\n count += copyDirectoryRecursive(srcPath, dstPath);\n } else if (entry.isFile()) {\n copyFileSync(srcPath, dstPath);\n count++;\n }\n }\n return count;\n}\n\n/**\n * Refresh the ~/.panopticon/ cache from the repo source.\n *\n * Always copies (overwrites) skills, agents, and rules from the package's\n * source directories to the cache. Generates ~/.panopticon/.manifest.json\n * tracking all cached files.\n *\n * This replaces the old \"skip if exists\" behavior in `pan install`.\n */\nexport function refreshCache(): RefreshCacheResult {\n const result: RefreshCacheResult = {\n skills: { copied: 0, total: 0 },\n agents: { copied: 0, total: 0 },\n rules: { copied: 0, total: 0 },\n };\n\n // Copy skills from repo to cache (always overwrite)\n if (existsSync(SOURCE_SKILLS_DIR)) {\n const skillDirs = readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true })\n .filter((d) => d.isDirectory());\n\n result.skills.total = skillDirs.length;\n for (const skillDir of skillDirs) {\n const src = join(SOURCE_SKILLS_DIR, skillDir.name);\n const dst = join(SKILLS_DIR, skillDir.name);\n copyDirectoryRecursive(src, dst);\n result.skills.copied++;\n }\n }\n\n // Copy dev-skills to cache too (in dev mode only)\n if (isDevMode() && existsSync(SOURCE_DEV_SKILLS_DIR)) {\n const devSkillDirs = readdirSync(SOURCE_DEV_SKILLS_DIR, { withFileTypes: true })\n .filter((d) => d.isDirectory());\n\n for (const skillDir of devSkillDirs) {\n const src = join(SOURCE_DEV_SKILLS_DIR, skillDir.name);\n const dst = join(SKILLS_DIR, skillDir.name);\n copyDirectoryRecursive(src, dst);\n result.skills.copied++;\n result.skills.total++;\n }\n }\n\n // Copy agent definitions from repo to cache\n if (existsSync(SOURCE_AGENTS_DIR)) {\n mkdirSync(CACHE_AGENTS_DIR, { recursive: true });\n const agents = readdirSync(SOURCE_AGENTS_DIR, { withFileTypes: true })\n .filter((entry) => entry.isFile() && entry.name.endsWith('.md'));\n\n result.agents.total = agents.length;\n for (const agent of agents) {\n copyFileSync(join(SOURCE_AGENTS_DIR, agent.name), join(CACHE_AGENTS_DIR, agent.name));\n result.agents.copied++;\n }\n }\n\n // Copy rules from repo to cache (directory may not exist yet)\n if (existsSync(SOURCE_RULES_DIR)) {\n const ruleFiles = readdirSync(SOURCE_RULES_DIR, { withFileTypes: true })\n .filter((entry) => entry.isFile());\n\n result.rules.total = ruleFiles.length;\n for (const rule of ruleFiles) {\n mkdirSync(CACHE_RULES_DIR, { recursive: true });\n copyFileSync(join(SOURCE_RULES_DIR, rule.name), join(CACHE_RULES_DIR, rule.name));\n result.rules.copied++;\n }\n }\n\n // Generate cache manifest\n const manifest = buildManifestFromDirectory(\n join(SKILLS_DIR, '..'), // ~/.panopticon/\n ['skills', 'agent-definitions', 'rules'],\n 'panopticon',\n );\n writeManifest(CACHE_MANIFEST, manifest);\n\n return result;\n}\n\n/**\n * Devroot sync item — represents a single file to distribute.\n */\nexport interface DevrootSyncItem {\n /** Relative path from .claude/ (e.g., \"skills/beads/SKILL.md\") */\n relativePath: string;\n /** Absolute path to source file in cache */\n sourcePath: string;\n /** Absolute path to target file at devroot */\n targetPath: string;\n /** What action to take */\n status: FileStatus;\n}\n\n/**\n * Plan what would be synced to devroot (dry run).\n * Reads from cache, targets <devroot>/.claude/, uses manifest comparison.\n */\nexport function planSync(): SyncPlan {\n const plan: SyncPlan = {\n skills: [],\n commands: [],\n agents: [],\n rules: [],\n devSkills: [],\n };\n\n const devrootPath = getDevrootPath();\n if (!devrootPath) return plan;\n\n const targetBase = join(devrootPath, '.claude');\n const manifestPath = join(targetBase, '.panopticon-manifest.json');\n const manifest = readManifest(manifestPath);\n\n // Plan skills\n const skillFiles = collectSourceFiles(SKILLS_DIR, 'skills/');\n for (const file of skillFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n const skillName = file.relativePath.split('/')[1] || file.relativePath;\n\n let syncStatus: SyncItem['status'] = 'new';\n if (status.action === 'update') syncStatus = 'symlink'; // reusing 'symlink' for \"managed, safe to update\"\n else if (status.action === 'modified') syncStatus = 'conflict';\n else if (status.action === 'user-owned') syncStatus = 'conflict';\n\n plan.skills.push({\n name: file.relativePath,\n sourcePath: file.absolutePath,\n targetPath: targetFile,\n status: syncStatus,\n });\n }\n\n // Plan agents\n const agentFiles = collectSourceFiles(CACHE_AGENTS_DIR, 'agents/');\n for (const file of agentFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n\n let syncStatus: SyncItem['status'] = 'new';\n if (status.action === 'update') syncStatus = 'symlink';\n else if (status.action === 'modified') syncStatus = 'conflict';\n else if (status.action === 'user-owned') syncStatus = 'conflict';\n\n plan.agents.push({\n name: file.relativePath,\n sourcePath: file.absolutePath,\n targetPath: targetFile,\n status: syncStatus,\n });\n }\n\n // Plan rules\n const ruleFiles = collectSourceFiles(CACHE_RULES_DIR, 'rules/');\n for (const file of ruleFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n\n let syncStatus: SyncItem['status'] = 'new';\n if (status.action === 'update') syncStatus = 'symlink';\n else if (status.action === 'modified') syncStatus = 'conflict';\n else if (status.action === 'user-owned') syncStatus = 'conflict';\n\n plan.rules.push({\n name: file.relativePath,\n sourcePath: file.absolutePath,\n targetPath: targetFile,\n status: syncStatus,\n });\n }\n\n return plan;\n}\n\nexport interface SyncOptions {\n force?: boolean;\n diff?: boolean;\n dryRun?: boolean;\n}\n\nexport interface SyncResult {\n created: string[];\n updated: string[];\n skipped: string[];\n conflicts: string[];\n diffs: Array<{ path: string; sourceContent: string; targetContent: string }>;\n}\n\n/**\n * Execute sync to devroot: copy from cache to <devroot>/.claude/.\n * Uses manifest-based conflict resolution. NEVER touches ~/.claude/.\n */\nexport function executeSync(options: SyncOptions = {}): SyncResult {\n const result: SyncResult = {\n created: [],\n updated: [],\n skipped: [],\n conflicts: [],\n diffs: [],\n };\n\n const devrootPath = getDevrootPath();\n if (!devrootPath) {\n return result;\n }\n\n const targetBase = join(devrootPath, '.claude');\n const manifestPath = join(targetBase, '.panopticon-manifest.json');\n const manifest = readManifest(manifestPath);\n\n // Collect all source files from cache\n const allFiles = [\n ...collectSourceFiles(SKILLS_DIR, 'skills/'),\n ...collectSourceFiles(CACHE_AGENTS_DIR, 'agents/'),\n ...collectSourceFiles(CACHE_RULES_DIR, 'rules/'),\n ];\n\n for (const file of allFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n\n switch (status.action) {\n case 'new': {\n // File doesn't exist at target — copy it\n mkdirSync(dirname(targetFile), { recursive: true });\n copyFileSync(file.absolutePath, targetFile);\n const hash = hashFile(targetFile);\n setManifestEntry(manifest, file.relativePath, hash, 'panopticon');\n result.created.push(file.relativePath);\n break;\n }\n\n case 'update': {\n // File exists, hash matches manifest — safe to overwrite (user didn't modify)\n mkdirSync(dirname(targetFile), { recursive: true });\n copyFileSync(file.absolutePath, targetFile);\n const hash = hashFile(targetFile);\n setManifestEntry(manifest, file.relativePath, hash, 'panopticon');\n result.updated.push(file.relativePath);\n break;\n }\n\n case 'modified': {\n // File was modified since we placed it\n if (options.diff) {\n result.diffs.push({\n path: file.relativePath,\n sourceContent: readFileSync(file.absolutePath, 'utf-8'),\n targetContent: readFileSync(targetFile, 'utf-8'),\n });\n }\n\n if (options.force) {\n mkdirSync(dirname(targetFile), { recursive: true });\n copyFileSync(file.absolutePath, targetFile);\n const hash = hashFile(targetFile);\n setManifestEntry(manifest, file.relativePath, hash, 'panopticon');\n result.updated.push(file.relativePath);\n } else {\n result.conflicts.push(file.relativePath);\n }\n break;\n }\n\n case 'user-owned': {\n // User placed this file, never touch it\n result.skipped.push(file.relativePath);\n break;\n }\n }\n }\n\n // Write updated manifest\n writeManifest(manifestPath, manifest);\n\n return result;\n}\n\n/**\n * Hook item for sync planning\n */\nexport interface HookItem {\n name: string;\n sourcePath: string;\n targetPath: string;\n status: 'new' | 'updated' | 'current';\n}\n\n/**\n * Plan hooks sync (checks what would be updated)\n */\nexport function planHooksSync(): HookItem[] {\n const hooks: HookItem[] = [];\n\n if (!existsSync(SOURCE_SCRIPTS_DIR)) {\n return hooks;\n }\n\n // Sync hook scripts (no extension) and bundled JS scripts (.js)\n // Skip source files (.ts), shell helpers (.sh), and other non-hook files (.mjs)\n const scripts = readdirSync(SOURCE_SCRIPTS_DIR, { withFileTypes: true })\n .filter((entry) => entry.isFile() && !entry.name.startsWith('.')\n && (!entry.name.includes('.') || entry.name.endsWith('.js')));\n\n for (const script of scripts) {\n const sourcePath = join(SOURCE_SCRIPTS_DIR, script.name);\n const targetPath = join(BIN_DIR, script.name);\n\n let status: HookItem['status'] = 'new';\n\n if (existsSync(targetPath)) {\n // Could compare file contents/timestamps here for 'current' vs 'updated'\n // For now, always update to ensure latest version\n status = 'updated';\n }\n\n hooks.push({ name: script.name, sourcePath, targetPath, status });\n }\n\n return hooks;\n}\n\n/**\n * Sync hooks (copy scripts to ~/.panopticon/bin/)\n */\nexport function syncHooks(): { synced: string[]; errors: string[] } {\n const result = { synced: [] as string[], errors: [] as string[] };\n\n // Ensure bin directory exists\n mkdirSync(BIN_DIR, { recursive: true });\n\n const hooks = planHooksSync();\n\n for (const hook of hooks) {\n try {\n copyFileSync(hook.sourcePath, hook.targetPath);\n chmodSync(hook.targetPath, 0o755); // Make executable\n result.synced.push(hook.name);\n } catch (error) {\n result.errors.push(`${hook.name}: ${error}`);\n }\n }\n\n return result;\n}\n\n/**\n * Runtime-specific statusline configurations\n * Maps runtime to: config dir, statusline filename, settings file\n */\nconst STATUSLINE_TARGETS: Record<string, { configDir: string; scriptName: string; settingsFile: string }> = {\n claude: {\n configDir: join(homedir(), '.claude'),\n scriptName: 'statusline-command.sh',\n settingsFile: join(homedir(), '.claude', 'settings.json'),\n },\n // Other runtimes can be added as they support statusline\n};\n\n/**\n * Sync statusline script to all supported runtimes\n * Copies the canonical statusline.sh from panopticon scripts to each runtime's config dir\n * and ensures the runtime's settings.json references it.\n */\nexport function syncStatusline(): { synced: string[]; errors: string[] } {\n const result = { synced: [] as string[], errors: [] as string[] };\n\n const sourceScript = join(SOURCE_SCRIPTS_DIR, 'statusline.sh');\n if (!existsSync(sourceScript)) {\n return result;\n }\n\n for (const [runtime, target] of Object.entries(STATUSLINE_TARGETS)) {\n try {\n // Ensure config dir exists\n mkdirSync(target.configDir, { recursive: true });\n\n // Copy statusline script\n const targetScript = join(target.configDir, target.scriptName);\n copyFileSync(sourceScript, targetScript);\n chmodSync(targetScript, 0o755);\n\n // Update settings.json to reference the statusline\n updateSettingsStatusline(target.settingsFile, targetScript);\n\n result.synced.push(runtime);\n } catch (error) {\n result.errors.push(`${runtime}: ${error}`);\n }\n }\n\n return result;\n}\n\n/**\n * Update a settings.json file to include the statusLine configuration\n * Preserves all existing settings (hooks, etc.)\n */\nfunction updateSettingsStatusline(settingsFile: string, scriptPath: string): void {\n let settings: Record<string, any> = {};\n\n if (existsSync(settingsFile)) {\n try {\n settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));\n } catch {\n // If settings file is corrupt, start fresh but preserve the file\n settings = {};\n }\n }\n\n // Only update if statusLine is missing or points to a different script\n const currentCommand = settings.statusLine?.command;\n if (currentCommand === scriptPath && settings.statusLine?.type === 'command') {\n return; // Already configured correctly\n }\n\n settings.statusLine = {\n type: 'command',\n command: scriptPath,\n padding: 0,\n };\n\n mkdirSync(dirname(settingsFile), { recursive: true });\n writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\\n', 'utf-8');\n}\n","import { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { SETTINGS_FILE } from './paths.js';\n\n// Model identifiers\nexport type AnthropicModel = 'claude-opus-4-6' | 'claude-sonnet-4-6' | 'claude-sonnet-4-5' | 'claude-haiku-4-5';\nexport type OpenAIModel = 'gpt-5.2-codex' | 'o3-deep-research' | 'gpt-4o' | 'gpt-4o-mini';\nexport type GoogleModel = 'gemini-3-pro-preview' | 'gemini-3-flash-preview' | 'gemini-2.5-pro' | 'gemini-2.5-flash';\nexport type ZAIModel = 'glm-4.7-flash';\nexport type KimiModel = 'kimi-k2' | 'kimi-k2.5';\nexport type MiniMaxModel = 'minimax-m2.7' | 'minimax-m2.7-highspeed';\nexport type ModelId = AnthropicModel | OpenAIModel | GoogleModel | ZAIModel | KimiModel | MiniMaxModel;\n\n// Task complexity levels\nexport type ComplexityLevel = 'trivial' | 'simple' | 'medium' | 'complex' | 'expert';\n\n// Specialist agent types\nexport interface SpecialistModels {\n review_agent: ModelId;\n test_agent: ModelId;\n merge_agent: ModelId;\n}\n\n// Complexity-based model mapping\nexport type ComplexityModels = {\n [K in ComplexityLevel]: ModelId;\n};\n\n// All model configuration\nexport interface ModelsConfig {\n specialists: SpecialistModels;\n status_review: ModelId;\n complexity: ComplexityModels;\n}\n\n// API keys for external providers\nexport interface ApiKeysConfig {\n openai?: string;\n google?: string;\n zai?: string;\n kimi?: string;\n}\n\n// Complete settings structure\nexport interface SettingsConfig {\n models: ModelsConfig;\n api_keys: ApiKeysConfig;\n}\n\n// Default settings - match optimal defaults from settings-api.ts\nconst DEFAULT_SETTINGS: SettingsConfig = {\n models: {\n specialists: {\n review_agent: 'claude-opus-4-6',\n test_agent: 'claude-sonnet-4-6',\n merge_agent: 'claude-sonnet-4-6',\n },\n status_review: 'claude-opus-4-6',\n complexity: {\n trivial: 'claude-haiku-4-5',\n simple: 'claude-haiku-4-5',\n medium: 'kimi-k2.5',\n complex: 'kimi-k2.5',\n expert: 'claude-opus-4-6',\n },\n },\n api_keys: {},\n};\n\n/**\n * Deep merge utility that recursively merges objects.\n * - Recursively merges nested objects\n * - User values take precedence over defaults\n */\nfunction deepMerge<T extends object>(defaults: T, overrides: Partial<T>): T {\n const result = { ...defaults };\n\n for (const key of Object.keys(overrides) as (keyof T)[]) {\n const defaultVal = defaults[key];\n const overrideVal = overrides[key];\n\n // Skip undefined values in overrides\n if (overrideVal === undefined) continue;\n\n // Deep merge if both values are non-array objects\n if (\n typeof defaultVal === 'object' &&\n defaultVal !== null &&\n !Array.isArray(defaultVal) &&\n typeof overrideVal === 'object' &&\n overrideVal !== null &&\n !Array.isArray(overrideVal)\n ) {\n result[key] = deepMerge(defaultVal, overrideVal as any);\n } else {\n // For primitives or null - override wins\n result[key] = overrideVal as T[keyof T];\n }\n }\n\n return result;\n}\n\n/**\n * Load settings from ~/.panopticon/settings.json\n * Returns default settings if file doesn't exist or is invalid\n * Also loads API keys from environment variables as fallback\n */\nexport function loadSettings(): SettingsConfig {\n let settings: SettingsConfig;\n\n if (!existsSync(SETTINGS_FILE)) {\n settings = getDefaultSettings();\n } else {\n try {\n const content = readFileSync(SETTINGS_FILE, 'utf8');\n const parsed = JSON.parse(content) as Partial<SettingsConfig>;\n settings = deepMerge(DEFAULT_SETTINGS, parsed);\n } catch (error) {\n console.error('Warning: Failed to parse settings.json, using defaults');\n settings = getDefaultSettings();\n }\n }\n\n // Load API keys from environment variables as fallback\n // This allows using ~/.panopticon.env for API keys\n const envApiKeys: ApiKeysConfig = {};\n if (process.env.OPENAI_API_KEY) envApiKeys.openai = process.env.OPENAI_API_KEY;\n if (process.env.GOOGLE_API_KEY) envApiKeys.google = process.env.GOOGLE_API_KEY;\n if (process.env.ZAI_API_KEY) envApiKeys.zai = process.env.ZAI_API_KEY;\n if (process.env.KIMI_API_KEY) envApiKeys.kimi = process.env.KIMI_API_KEY;\n\n // Merge env vars as fallback (settings.json takes precedence)\n settings.api_keys = {\n ...envApiKeys,\n ...settings.api_keys,\n };\n\n return settings;\n}\n\n/**\n * Save settings to ~/.panopticon/settings.json\n * Writes with pretty formatting (2-space indent)\n */\nexport function saveSettings(settings: SettingsConfig): void {\n const content = JSON.stringify(settings, null, 2);\n writeFileSync(SETTINGS_FILE, content, 'utf8');\n}\n\n/**\n * Validate settings structure and model IDs\n * Returns error message if invalid, null if valid\n */\nexport function validateSettings(settings: SettingsConfig): string | null {\n // Validate models structure\n if (!settings.models) {\n return 'Missing models configuration';\n }\n\n // Validate specialists\n if (!settings.models.specialists) {\n return 'Missing specialists configuration';\n }\n const specialists = settings.models.specialists;\n if (!specialists.review_agent || !specialists.test_agent || !specialists.merge_agent) {\n return 'Missing specialist agent model configuration';\n }\n\n // Validate complexity levels\n if (!settings.models.complexity) {\n return 'Missing complexity configuration';\n }\n const complexity = settings.models.complexity;\n const requiredLevels: ComplexityLevel[] = ['trivial', 'simple', 'medium', 'complex', 'expert'];\n for (const level of requiredLevels) {\n if (!complexity[level]) {\n return `Missing complexity level: ${level}`;\n }\n }\n\n // Validate api_keys structure (optional keys)\n if (!settings.api_keys) {\n return 'Missing api_keys configuration';\n }\n\n return null;\n}\n\n/**\n * Get a deep copy of the default settings\n */\nexport function getDefaultSettings(): SettingsConfig {\n return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));\n}\n\n/**\n * Get available models for a provider based on configured API keys\n * Returns empty array if provider API key is not configured\n */\nexport function getAvailableModels(settings: SettingsConfig): {\n anthropic: AnthropicModel[];\n openai: OpenAIModel[];\n google: GoogleModel[];\n zai: ZAIModel[];\n kimi: KimiModel[];\n} {\n const anthropicModels: AnthropicModel[] = [\n 'claude-opus-4-6',\n 'claude-sonnet-4-6',\n 'claude-haiku-4-5',\n ];\n\n const openaiModels: OpenAIModel[] = settings.api_keys.openai\n ? ['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini']\n : [];\n\n const googleModels: GoogleModel[] = settings.api_keys.google\n ? ['gemini-3-pro-preview', 'gemini-3-flash-preview']\n : [];\n\n const zaiModels: ZAIModel[] = settings.api_keys.zai\n ? ['glm-4.7-flash']\n : [];\n\n const kimiModels: KimiModel[] = settings.api_keys.kimi\n ? ['kimi-k2', 'kimi-k2.5']\n : [];\n\n return {\n anthropic: anthropicModels,\n openai: openaiModels,\n google: googleModels,\n zai: zaiModels,\n kimi: kimiModels,\n };\n}\n\n/**\n * Check if a model ID is an Anthropic model\n * Anthropic models can be run directly with `claude` CLI\n */\nexport function isAnthropicModel(modelId: ModelId | string): boolean {\n return modelId.startsWith('claude-');\n}\n\n/**\n * Get the Claude CLI model flag for an Anthropic model\n * Maps our model IDs to Claude's expected format\n */\nexport function getClaudeModelFlag(modelId: ModelId | string): string {\n const modelMap: Record<string, string> = {\n 'claude-opus-4-6': 'opus',\n 'claude-sonnet-4-6': 'sonnet',\n 'claude-sonnet-4-5': 'sonnet',\n 'claude-haiku-4-5': 'haiku',\n };\n return modelMap[modelId] || 'sonnet';\n}\n\n/**\n * Get the command to run an agent with a specific model\n * Always uses 'claude' CLI — non-Anthropic models work via ANTHROPIC_BASE_URL env var\n * pointing to their Anthropic-compatible endpoint.\n */\nexport function getAgentCommand(modelId: ModelId | string): { command: string; args: string[] } {\n if (isAnthropicModel(modelId)) {\n return {\n command: 'claude',\n args: ['--model', getClaudeModelFlag(modelId)],\n };\n }\n // Non-Anthropic direct providers: use claude CLI with the model name as-is.\n // The caller must set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN env vars.\n return {\n command: 'claude',\n args: ['--model', modelId],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"settings-A-CWz_ph.js","names":[],"sources":["../src/lib/backup.ts","../src/lib/sync.ts","../src/lib/settings.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, cpSync, rmSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { BACKUPS_DIR } from './paths.js';\n\nexport interface BackupInfo {\n timestamp: string;\n path: string;\n targets: string[];\n}\n\nexport function createBackupTimestamp(): string {\n return new Date().toISOString().replace(/[:.]/g, '-');\n}\n\nexport function createBackup(sourceDirs: string[]): BackupInfo {\n const timestamp = createBackupTimestamp();\n const backupPath = join(BACKUPS_DIR, timestamp);\n\n mkdirSync(backupPath, { recursive: true });\n\n const targets: string[] = [];\n\n for (const sourceDir of sourceDirs) {\n if (!existsSync(sourceDir)) continue;\n\n const targetName = basename(sourceDir);\n const targetPath = join(backupPath, targetName);\n\n // Use filter to skip symlinks — sync targets (e.g. ~/.claude/skills/)\n // contain symlinks back into ~/.panopticon/skills/ which causes cpSync\n // to fail with \"cannot copy to a subdirectory of self\".\n cpSync(sourceDir, targetPath, {\n recursive: true,\n filter: (src) => !lstatSync(src).isSymbolicLink(),\n });\n targets.push(targetName);\n }\n\n return {\n timestamp,\n path: backupPath,\n targets,\n };\n}\n\nexport function listBackups(): BackupInfo[] {\n if (!existsSync(BACKUPS_DIR)) return [];\n\n const entries = readdirSync(BACKUPS_DIR, { withFileTypes: true });\n\n return entries\n .filter((e) => e.isDirectory())\n .map((e) => {\n const backupPath = join(BACKUPS_DIR, e.name);\n const contents = readdirSync(backupPath);\n\n return {\n timestamp: e.name,\n path: backupPath,\n targets: contents,\n };\n })\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n}\n\nexport function restoreBackup(timestamp: string, targetDirs: Record<string, string>): void {\n const backupPath = join(BACKUPS_DIR, timestamp);\n\n if (!existsSync(backupPath)) {\n throw new Error(`Backup not found: ${timestamp}`);\n }\n\n const contents = readdirSync(backupPath, { withFileTypes: true });\n\n for (const entry of contents) {\n if (!entry.isDirectory()) continue;\n\n const sourcePath = join(backupPath, entry.name);\n const targetPath = targetDirs[entry.name];\n\n if (!targetPath) continue;\n\n // Remove existing and restore from backup\n if (existsSync(targetPath)) {\n rmSync(targetPath, { recursive: true });\n }\n\n cpSync(sourcePath, targetPath, { recursive: true });\n }\n}\n\nexport function cleanOldBackups(keepCount: number = 10): number {\n const backups = listBackups();\n\n if (backups.length <= keepCount) return 0;\n\n const toRemove = backups.slice(keepCount);\n let removed = 0;\n\n for (const backup of toRemove) {\n rmSync(backup.path, { recursive: true });\n removed++;\n }\n\n return removed;\n}\n","import { existsSync, mkdirSync, readdirSync, symlinkSync, unlinkSync, lstatSync, readlinkSync, rmSync, copyFileSync, chmodSync, readFileSync, writeFileSync } from 'fs';\nimport { join, basename, dirname, relative } from 'path';\nimport { homedir } from 'os';\nimport {\n SKILLS_DIR, COMMANDS_DIR, AGENTS_DIR, BIN_DIR,\n SOURCE_SCRIPTS_DIR, SOURCE_DEV_SKILLS_DIR, SOURCE_SKILLS_DIR, SOURCE_AGENTS_DIR, SOURCE_RULES_DIR,\n CACHE_AGENTS_DIR, CACHE_RULES_DIR, CACHE_MANIFEST,\n SYNC_TARGET, isDevMode,\n} from './paths.js';\nimport {\n buildManifestFromDirectory, writeManifest, readManifest, hashFile,\n setManifestEntry, collectSourceFiles,\n type Manifest, type FileStatus,\n compareFileToManifest,\n} from './manifest.js';\nimport { getDevrootPath } from './config.js';\n\nexport interface SyncItem {\n name: string;\n sourcePath: string;\n targetPath: string;\n status: 'new' | 'exists' | 'conflict' | 'symlink';\n}\n\nexport interface SyncPlan {\n skills: SyncItem[];\n commands: SyncItem[];\n agents: SyncItem[];\n rules: SyncItem[];\n devSkills: SyncItem[]; // Developer-only skills (only synced in dev mode)\n}\n\n/**\n * Remove a file, symlink, or directory safely\n */\nfunction removeTarget(targetPath: string): void {\n const stats = lstatSync(targetPath);\n if (stats.isDirectory() && !stats.isSymbolicLink()) {\n // It's a real directory, remove recursively\n rmSync(targetPath, { recursive: true, force: true });\n } else {\n // It's a file or symlink\n unlinkSync(targetPath);\n }\n}\n\n/**\n * Check if a path is a Panopticon-managed symlink\n */\nexport function isPanopticonSymlink(targetPath: string): boolean {\n if (!existsSync(targetPath)) return false;\n\n try {\n const stats = lstatSync(targetPath);\n if (!stats.isSymbolicLink()) return false;\n\n const linkTarget = readlinkSync(targetPath);\n // It's ours if it points to our skills/commands dir\n return linkTarget.includes('.panopticon');\n } catch {\n return false;\n }\n}\n\nexport interface MigrationResult {\n removedSymlinks: string[];\n preservedUserContent: string[];\n errors: string[];\n}\n\n/**\n * One-time migration: remove Panopticon-managed symlinks from ~/.claude/.\n *\n * Detects symlinks in ~/.claude/skills/ and ~/.claude/agents/ that point to\n * .panopticon directories. Removes only those symlinks, preserving any\n * user-created content (real files/directories).\n *\n * This is safe to run multiple times — it's a no-op if nothing remains to clean up.\n *\n * Removes two kinds of stale Panopticon content from ~/.claude/:\n * 1. Symlinks pointing to .panopticon or panopticon-cli (legacy sync method)\n * 2. Plain directories that also exist in the devroot (stale copies from before\n * the devroot migration — these cause duplicate skill listings)\n */\nexport function migrateStalePersonalContent(): MigrationResult {\n const claudeDir = join(homedir(), '.claude');\n const result: MigrationResult = {\n removedSymlinks: [],\n preservedUserContent: [],\n errors: [],\n };\n\n // Build a set of skill/agent/command names that exist in the devroot\n // so we can identify stale copies in ~/.claude/\n const devrootNames = new Set<string>();\n const devroot = getDevrootPath();\n if (devroot) {\n for (const subdir of ['skills', 'commands', 'agents']) {\n const devrootDir = join(devroot, '.claude', subdir);\n if (existsSync(devrootDir)) {\n try {\n for (const entry of readdirSync(devrootDir)) {\n devrootNames.add(`${subdir}/${entry}`);\n }\n } catch {\n // Ignore read errors on devroot\n }\n }\n }\n }\n\n for (const subdir of ['skills', 'commands', 'agents']) {\n const dir = join(claudeDir, subdir);\n if (!existsSync(dir)) continue;\n\n try {\n const entries = readdirSync(dir);\n for (const entry of entries) {\n const entryPath = join(dir, entry);\n try {\n const stats = lstatSync(entryPath);\n if (stats.isSymbolicLink()) {\n const linkTarget = readlinkSync(entryPath);\n if (linkTarget.includes('.panopticon') || linkTarget.includes('panopticon-cli')) {\n unlinkSync(entryPath);\n result.removedSymlinks.push(`${subdir}/${entry}`);\n } else {\n // Symlink to somewhere else — leave it\n result.preservedUserContent.push(`${subdir}/${entry}`);\n }\n } else if (stats.isDirectory() && devrootNames.has(`${subdir}/${entry}`)) {\n // Plain directory that also exists in devroot — stale Panopticon copy.\n // The devroot copy is the canonical one; this personal copy causes\n // duplicate listings and violates principle #4 (never touch ~/.claude/).\n rmSync(entryPath, { recursive: true, force: true });\n result.removedSymlinks.push(`${subdir}/${entry} (stale copy)`);\n } else {\n // Real file/directory with no devroot counterpart — user content, never touch\n result.preservedUserContent.push(`${subdir}/${entry}`);\n }\n } catch (err: any) {\n result.errors.push(`${subdir}/${entry}: ${err.message}`);\n }\n }\n } catch (err: any) {\n result.errors.push(`${subdir}: ${err.message}`);\n }\n }\n\n return result;\n}\n\nexport interface RefreshCacheResult {\n skills: { copied: number; total: number };\n agents: { copied: number; total: number };\n rules: { copied: number; total: number };\n}\n\n/**\n * Recursively copy a directory, overwriting existing files.\n */\nfunction copyDirectoryRecursive(source: string, dest: string): number {\n if (!existsSync(source)) return 0;\n\n mkdirSync(dest, { recursive: true });\n let count = 0;\n\n const entries = readdirSync(source, { withFileTypes: true });\n for (const entry of entries) {\n const srcPath = join(source, entry.name);\n const dstPath = join(dest, entry.name);\n if (entry.isDirectory()) {\n count += copyDirectoryRecursive(srcPath, dstPath);\n } else if (entry.isFile()) {\n copyFileSync(srcPath, dstPath);\n count++;\n }\n }\n return count;\n}\n\n/**\n * Refresh the ~/.panopticon/ cache from the repo source.\n *\n * Always copies (overwrites) skills, agents, and rules from the package's\n * source directories to the cache. Generates ~/.panopticon/.manifest.json\n * tracking all cached files.\n *\n * This replaces the old \"skip if exists\" behavior in `pan install`.\n */\nexport function refreshCache(): RefreshCacheResult {\n const result: RefreshCacheResult = {\n skills: { copied: 0, total: 0 },\n agents: { copied: 0, total: 0 },\n rules: { copied: 0, total: 0 },\n };\n\n // Copy skills from repo to cache (always overwrite)\n if (existsSync(SOURCE_SKILLS_DIR)) {\n const skillDirs = readdirSync(SOURCE_SKILLS_DIR, { withFileTypes: true })\n .filter((d) => d.isDirectory());\n\n result.skills.total = skillDirs.length;\n for (const skillDir of skillDirs) {\n const src = join(SOURCE_SKILLS_DIR, skillDir.name);\n const dst = join(SKILLS_DIR, skillDir.name);\n copyDirectoryRecursive(src, dst);\n result.skills.copied++;\n }\n }\n\n // Copy dev-skills to cache too (in dev mode only)\n if (isDevMode() && existsSync(SOURCE_DEV_SKILLS_DIR)) {\n const devSkillDirs = readdirSync(SOURCE_DEV_SKILLS_DIR, { withFileTypes: true })\n .filter((d) => d.isDirectory());\n\n for (const skillDir of devSkillDirs) {\n const src = join(SOURCE_DEV_SKILLS_DIR, skillDir.name);\n const dst = join(SKILLS_DIR, skillDir.name);\n copyDirectoryRecursive(src, dst);\n result.skills.copied++;\n result.skills.total++;\n }\n }\n\n // Copy agent definitions from repo to cache\n if (existsSync(SOURCE_AGENTS_DIR)) {\n mkdirSync(CACHE_AGENTS_DIR, { recursive: true });\n const agents = readdirSync(SOURCE_AGENTS_DIR, { withFileTypes: true })\n .filter((entry) => entry.isFile() && entry.name.endsWith('.md'));\n\n result.agents.total = agents.length;\n for (const agent of agents) {\n copyFileSync(join(SOURCE_AGENTS_DIR, agent.name), join(CACHE_AGENTS_DIR, agent.name));\n result.agents.copied++;\n }\n }\n\n // Copy rules from repo to cache (directory may not exist yet)\n if (existsSync(SOURCE_RULES_DIR)) {\n const ruleFiles = readdirSync(SOURCE_RULES_DIR, { withFileTypes: true })\n .filter((entry) => entry.isFile());\n\n result.rules.total = ruleFiles.length;\n for (const rule of ruleFiles) {\n mkdirSync(CACHE_RULES_DIR, { recursive: true });\n copyFileSync(join(SOURCE_RULES_DIR, rule.name), join(CACHE_RULES_DIR, rule.name));\n result.rules.copied++;\n }\n }\n\n // Generate cache manifest\n const manifest = buildManifestFromDirectory(\n join(SKILLS_DIR, '..'), // ~/.panopticon/\n ['skills', 'agent-definitions', 'rules'],\n 'panopticon',\n );\n writeManifest(CACHE_MANIFEST, manifest);\n\n return result;\n}\n\n/**\n * Devroot sync item — represents a single file to distribute.\n */\nexport interface DevrootSyncItem {\n /** Relative path from .claude/ (e.g., \"skills/beads/SKILL.md\") */\n relativePath: string;\n /** Absolute path to source file in cache */\n sourcePath: string;\n /** Absolute path to target file at devroot */\n targetPath: string;\n /** What action to take */\n status: FileStatus;\n}\n\n/**\n * Plan what would be synced to devroot (dry run).\n * Reads from cache, targets <devroot>/.claude/, uses manifest comparison.\n */\nexport function planSync(): SyncPlan {\n const plan: SyncPlan = {\n skills: [],\n commands: [],\n agents: [],\n rules: [],\n devSkills: [],\n };\n\n const devrootPath = getDevrootPath();\n if (!devrootPath) return plan;\n\n const targetBase = join(devrootPath, '.claude');\n const manifestPath = join(targetBase, '.panopticon-manifest.json');\n const manifest = readManifest(manifestPath);\n\n // Plan skills\n const skillFiles = collectSourceFiles(SKILLS_DIR, 'skills/');\n for (const file of skillFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n const skillName = file.relativePath.split('/')[1] || file.relativePath;\n\n let syncStatus: SyncItem['status'] = 'new';\n if (status.action === 'update') syncStatus = 'symlink'; // reusing 'symlink' for \"managed, safe to update\"\n else if (status.action === 'modified') syncStatus = 'conflict';\n else if (status.action === 'user-owned') syncStatus = 'conflict';\n\n plan.skills.push({\n name: file.relativePath,\n sourcePath: file.absolutePath,\n targetPath: targetFile,\n status: syncStatus,\n });\n }\n\n // Plan agents\n const agentFiles = collectSourceFiles(CACHE_AGENTS_DIR, 'agents/');\n for (const file of agentFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n\n let syncStatus: SyncItem['status'] = 'new';\n if (status.action === 'update') syncStatus = 'symlink';\n else if (status.action === 'modified') syncStatus = 'conflict';\n else if (status.action === 'user-owned') syncStatus = 'conflict';\n\n plan.agents.push({\n name: file.relativePath,\n sourcePath: file.absolutePath,\n targetPath: targetFile,\n status: syncStatus,\n });\n }\n\n // Plan rules\n const ruleFiles = collectSourceFiles(CACHE_RULES_DIR, 'rules/');\n for (const file of ruleFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n\n let syncStatus: SyncItem['status'] = 'new';\n if (status.action === 'update') syncStatus = 'symlink';\n else if (status.action === 'modified') syncStatus = 'conflict';\n else if (status.action === 'user-owned') syncStatus = 'conflict';\n\n plan.rules.push({\n name: file.relativePath,\n sourcePath: file.absolutePath,\n targetPath: targetFile,\n status: syncStatus,\n });\n }\n\n return plan;\n}\n\nexport interface SyncOptions {\n force?: boolean;\n diff?: boolean;\n dryRun?: boolean;\n}\n\nexport interface SyncResult {\n created: string[];\n updated: string[];\n skipped: string[];\n conflicts: string[];\n diffs: Array<{ path: string; sourceContent: string; targetContent: string }>;\n}\n\n/**\n * Execute sync to devroot: copy from cache to <devroot>/.claude/.\n * Uses manifest-based conflict resolution. NEVER touches ~/.claude/.\n */\nexport function executeSync(options: SyncOptions = {}): SyncResult {\n const result: SyncResult = {\n created: [],\n updated: [],\n skipped: [],\n conflicts: [],\n diffs: [],\n };\n\n const devrootPath = getDevrootPath();\n if (!devrootPath) {\n return result;\n }\n\n const targetBase = join(devrootPath, '.claude');\n const manifestPath = join(targetBase, '.panopticon-manifest.json');\n const manifest = readManifest(manifestPath);\n\n // Collect all source files from cache\n const allFiles = [\n ...collectSourceFiles(SKILLS_DIR, 'skills/'),\n ...collectSourceFiles(CACHE_AGENTS_DIR, 'agents/'),\n ...collectSourceFiles(CACHE_RULES_DIR, 'rules/'),\n ];\n\n for (const file of allFiles) {\n const targetFile = join(targetBase, file.relativePath);\n const status = compareFileToManifest(targetFile, file.relativePath, manifest);\n\n switch (status.action) {\n case 'new': {\n // File doesn't exist at target — copy it\n mkdirSync(dirname(targetFile), { recursive: true });\n copyFileSync(file.absolutePath, targetFile);\n const hash = hashFile(targetFile);\n setManifestEntry(manifest, file.relativePath, hash, 'panopticon');\n result.created.push(file.relativePath);\n break;\n }\n\n case 'update': {\n // File exists, hash matches manifest — safe to overwrite (user didn't modify)\n mkdirSync(dirname(targetFile), { recursive: true });\n copyFileSync(file.absolutePath, targetFile);\n const hash = hashFile(targetFile);\n setManifestEntry(manifest, file.relativePath, hash, 'panopticon');\n result.updated.push(file.relativePath);\n break;\n }\n\n case 'modified': {\n // File was modified since we placed it\n if (options.diff) {\n result.diffs.push({\n path: file.relativePath,\n sourceContent: readFileSync(file.absolutePath, 'utf-8'),\n targetContent: readFileSync(targetFile, 'utf-8'),\n });\n }\n\n if (options.force) {\n mkdirSync(dirname(targetFile), { recursive: true });\n copyFileSync(file.absolutePath, targetFile);\n const hash = hashFile(targetFile);\n setManifestEntry(manifest, file.relativePath, hash, 'panopticon');\n result.updated.push(file.relativePath);\n } else {\n result.conflicts.push(file.relativePath);\n }\n break;\n }\n\n case 'user-owned': {\n // User placed this file, never touch it\n result.skipped.push(file.relativePath);\n break;\n }\n }\n }\n\n // Write updated manifest\n writeManifest(manifestPath, manifest);\n\n return result;\n}\n\n/**\n * Hook item for sync planning\n */\nexport interface HookItem {\n name: string;\n sourcePath: string;\n targetPath: string;\n status: 'new' | 'updated' | 'current';\n}\n\n/**\n * Plan hooks sync (checks what would be updated)\n */\nexport function planHooksSync(): HookItem[] {\n const hooks: HookItem[] = [];\n\n if (!existsSync(SOURCE_SCRIPTS_DIR)) {\n return hooks;\n }\n\n // Sync hook scripts (no extension) and bundled JS scripts (.js)\n // Skip source files (.ts), shell helpers (.sh), and other non-hook files (.mjs)\n const scripts = readdirSync(SOURCE_SCRIPTS_DIR, { withFileTypes: true })\n .filter((entry) => entry.isFile() && !entry.name.startsWith('.')\n && (!entry.name.includes('.') || entry.name.endsWith('.js')));\n\n for (const script of scripts) {\n const sourcePath = join(SOURCE_SCRIPTS_DIR, script.name);\n const targetPath = join(BIN_DIR, script.name);\n\n let status: HookItem['status'] = 'new';\n\n if (existsSync(targetPath)) {\n // Could compare file contents/timestamps here for 'current' vs 'updated'\n // For now, always update to ensure latest version\n status = 'updated';\n }\n\n hooks.push({ name: script.name, sourcePath, targetPath, status });\n }\n\n return hooks;\n}\n\n/**\n * Sync hooks (copy scripts to ~/.panopticon/bin/)\n */\nexport function syncHooks(): { synced: string[]; errors: string[] } {\n const result = { synced: [] as string[], errors: [] as string[] };\n\n // Ensure bin directory exists\n mkdirSync(BIN_DIR, { recursive: true });\n\n const hooks = planHooksSync();\n\n for (const hook of hooks) {\n try {\n copyFileSync(hook.sourcePath, hook.targetPath);\n chmodSync(hook.targetPath, 0o755); // Make executable\n result.synced.push(hook.name);\n } catch (error) {\n result.errors.push(`${hook.name}: ${error}`);\n }\n }\n\n return result;\n}\n\n/**\n * Runtime-specific statusline configurations\n * Maps runtime to: config dir, statusline filename, settings file\n */\nconst STATUSLINE_TARGETS: Record<string, { configDir: string; scriptName: string; settingsFile: string }> = {\n claude: {\n configDir: join(homedir(), '.claude'),\n scriptName: 'statusline-command.sh',\n settingsFile: join(homedir(), '.claude', 'settings.json'),\n },\n // Other runtimes can be added as they support statusline\n};\n\n/**\n * Sync statusline script to all supported runtimes\n * Copies the canonical statusline.sh from panopticon scripts to each runtime's config dir\n * and ensures the runtime's settings.json references it.\n */\nexport function syncStatusline(): { synced: string[]; errors: string[] } {\n const result = { synced: [] as string[], errors: [] as string[] };\n\n const sourceScript = join(SOURCE_SCRIPTS_DIR, 'statusline.sh');\n if (!existsSync(sourceScript)) {\n return result;\n }\n\n for (const [runtime, target] of Object.entries(STATUSLINE_TARGETS)) {\n try {\n // Ensure config dir exists\n mkdirSync(target.configDir, { recursive: true });\n\n // Copy statusline script\n const targetScript = join(target.configDir, target.scriptName);\n copyFileSync(sourceScript, targetScript);\n chmodSync(targetScript, 0o755);\n\n // Update settings.json to reference the statusline\n updateSettingsStatusline(target.settingsFile, targetScript);\n\n result.synced.push(runtime);\n } catch (error) {\n result.errors.push(`${runtime}: ${error}`);\n }\n }\n\n return result;\n}\n\n/**\n * Update a settings.json file to include the statusLine configuration\n * Preserves all existing settings (hooks, etc.)\n */\nfunction updateSettingsStatusline(settingsFile: string, scriptPath: string): void {\n let settings: Record<string, any> = {};\n\n if (existsSync(settingsFile)) {\n try {\n settings = JSON.parse(readFileSync(settingsFile, 'utf-8'));\n } catch {\n // If settings file is corrupt, start fresh but preserve the file\n settings = {};\n }\n }\n\n // Only update if statusLine is missing or points to a different script\n const currentCommand = settings.statusLine?.command;\n if (currentCommand === scriptPath && settings.statusLine?.type === 'command') {\n return; // Already configured correctly\n }\n\n settings.statusLine = {\n type: 'command',\n command: scriptPath,\n padding: 0,\n };\n\n mkdirSync(dirname(settingsFile), { recursive: true });\n writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\\n', 'utf-8');\n}\n","import { readFileSync, writeFileSync, existsSync } from 'fs';\nimport { SETTINGS_FILE } from './paths.js';\n\n// Model identifiers\nexport type AnthropicModel = 'claude-opus-4-6' | 'claude-sonnet-4-6' | 'claude-sonnet-4-5' | 'claude-haiku-4-5';\nexport type OpenAIModel = 'gpt-5.2-codex' | 'o3-deep-research' | 'gpt-4o' | 'gpt-4o-mini';\nexport type GoogleModel = 'gemini-3-pro-preview' | 'gemini-3-flash-preview' | 'gemini-2.5-pro' | 'gemini-2.5-flash';\nexport type ZAIModel = 'glm-4.7' | 'glm-4.7-flash' | 'glm-5.1';\nexport type KimiModel = 'kimi-k2' | 'kimi-k2.5';\nexport type MiniMaxModel = 'minimax-m2.7' | 'minimax-m2.7-highspeed';\nexport type ModelId = AnthropicModel | OpenAIModel | GoogleModel | ZAIModel | KimiModel | MiniMaxModel;\n\n// Task complexity levels\nexport type ComplexityLevel = 'trivial' | 'simple' | 'medium' | 'complex' | 'expert';\n\n// Specialist agent types\nexport interface SpecialistModels {\n review_agent: ModelId;\n test_agent: ModelId;\n merge_agent: ModelId;\n}\n\n// Complexity-based model mapping\nexport type ComplexityModels = {\n [K in ComplexityLevel]: ModelId;\n};\n\n// All model configuration\nexport interface ModelsConfig {\n specialists: SpecialistModels;\n status_review: ModelId;\n complexity: ComplexityModels;\n}\n\n// API keys for external providers\nexport interface ApiKeysConfig {\n openai?: string;\n google?: string;\n zai?: string;\n kimi?: string;\n}\n\n// Complete settings structure\nexport interface SettingsConfig {\n models: ModelsConfig;\n api_keys: ApiKeysConfig;\n}\n\n// Default settings - match optimal defaults from settings-api.ts\nconst DEFAULT_SETTINGS: SettingsConfig = {\n models: {\n specialists: {\n review_agent: 'claude-opus-4-6',\n test_agent: 'claude-sonnet-4-6',\n merge_agent: 'claude-sonnet-4-6',\n },\n status_review: 'claude-opus-4-6',\n complexity: {\n trivial: 'claude-haiku-4-5',\n simple: 'claude-haiku-4-5',\n medium: 'kimi-k2.5',\n complex: 'kimi-k2.5',\n expert: 'claude-opus-4-6',\n },\n },\n api_keys: {},\n};\n\n/**\n * Deep merge utility that recursively merges objects.\n * - Recursively merges nested objects\n * - User values take precedence over defaults\n */\nfunction deepMerge<T extends object>(defaults: T, overrides: Partial<T>): T {\n const result = { ...defaults };\n\n for (const key of Object.keys(overrides) as (keyof T)[]) {\n const defaultVal = defaults[key];\n const overrideVal = overrides[key];\n\n // Skip undefined values in overrides\n if (overrideVal === undefined) continue;\n\n // Deep merge if both values are non-array objects\n if (\n typeof defaultVal === 'object' &&\n defaultVal !== null &&\n !Array.isArray(defaultVal) &&\n typeof overrideVal === 'object' &&\n overrideVal !== null &&\n !Array.isArray(overrideVal)\n ) {\n result[key] = deepMerge(defaultVal, overrideVal as any);\n } else {\n // For primitives or null - override wins\n result[key] = overrideVal as T[keyof T];\n }\n }\n\n return result;\n}\n\n/**\n * Load settings from ~/.panopticon/settings.json\n * Returns default settings if file doesn't exist or is invalid\n * Also loads API keys from environment variables as fallback\n */\nexport function loadSettings(): SettingsConfig {\n let settings: SettingsConfig;\n\n if (!existsSync(SETTINGS_FILE)) {\n settings = getDefaultSettings();\n } else {\n try {\n const content = readFileSync(SETTINGS_FILE, 'utf8');\n const parsed = JSON.parse(content) as Partial<SettingsConfig>;\n settings = deepMerge(DEFAULT_SETTINGS, parsed);\n } catch (error) {\n console.error('Warning: Failed to parse settings.json, using defaults');\n settings = getDefaultSettings();\n }\n }\n\n // Load API keys from environment variables as fallback\n // This allows using ~/.panopticon.env for API keys\n const envApiKeys: ApiKeysConfig = {};\n if (process.env.OPENAI_API_KEY) envApiKeys.openai = process.env.OPENAI_API_KEY;\n if (process.env.GOOGLE_API_KEY) envApiKeys.google = process.env.GOOGLE_API_KEY;\n if (process.env.ZAI_API_KEY) envApiKeys.zai = process.env.ZAI_API_KEY;\n if (process.env.KIMI_API_KEY) envApiKeys.kimi = process.env.KIMI_API_KEY;\n\n // Merge env vars as fallback (settings.json takes precedence)\n settings.api_keys = {\n ...envApiKeys,\n ...settings.api_keys,\n };\n\n return settings;\n}\n\n/**\n * Save settings to ~/.panopticon/settings.json\n * Writes with pretty formatting (2-space indent)\n */\nexport function saveSettings(settings: SettingsConfig): void {\n const content = JSON.stringify(settings, null, 2);\n writeFileSync(SETTINGS_FILE, content, 'utf8');\n}\n\n/**\n * Validate settings structure and model IDs\n * Returns error message if invalid, null if valid\n */\nexport function validateSettings(settings: SettingsConfig): string | null {\n // Validate models structure\n if (!settings.models) {\n return 'Missing models configuration';\n }\n\n // Validate specialists\n if (!settings.models.specialists) {\n return 'Missing specialists configuration';\n }\n const specialists = settings.models.specialists;\n if (!specialists.review_agent || !specialists.test_agent || !specialists.merge_agent) {\n return 'Missing specialist agent model configuration';\n }\n\n // Validate complexity levels\n if (!settings.models.complexity) {\n return 'Missing complexity configuration';\n }\n const complexity = settings.models.complexity;\n const requiredLevels: ComplexityLevel[] = ['trivial', 'simple', 'medium', 'complex', 'expert'];\n for (const level of requiredLevels) {\n if (!complexity[level]) {\n return `Missing complexity level: ${level}`;\n }\n }\n\n // Validate api_keys structure (optional keys)\n if (!settings.api_keys) {\n return 'Missing api_keys configuration';\n }\n\n return null;\n}\n\n/**\n * Get a deep copy of the default settings\n */\nexport function getDefaultSettings(): SettingsConfig {\n return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));\n}\n\n/**\n * Get available models for a provider based on configured API keys\n * Returns empty array if provider API key is not configured\n */\nexport function getAvailableModels(settings: SettingsConfig): {\n anthropic: AnthropicModel[];\n openai: OpenAIModel[];\n google: GoogleModel[];\n zai: ZAIModel[];\n kimi: KimiModel[];\n} {\n const anthropicModels: AnthropicModel[] = [\n 'claude-opus-4-6',\n 'claude-sonnet-4-6',\n 'claude-haiku-4-5',\n ];\n\n const openaiModels: OpenAIModel[] = settings.api_keys.openai\n ? ['gpt-5.2-codex', 'o3-deep-research', 'gpt-4o', 'gpt-4o-mini']\n : [];\n\n const googleModels: GoogleModel[] = settings.api_keys.google\n ? ['gemini-3-pro-preview', 'gemini-3-flash-preview']\n : [];\n\n const zaiModels: ZAIModel[] = settings.api_keys.zai\n ? ['glm-4.7', 'glm-4.7-flash', 'glm-5.1']\n : [];\n\n const kimiModels: KimiModel[] = settings.api_keys.kimi\n ? ['kimi-k2', 'kimi-k2.5']\n : [];\n\n return {\n anthropic: anthropicModels,\n openai: openaiModels,\n google: googleModels,\n zai: zaiModels,\n kimi: kimiModels,\n };\n}\n\n/**\n * Check if a model ID is an Anthropic model\n * Anthropic models can be run directly with `claude` CLI\n */\nexport function isAnthropicModel(modelId: ModelId | string): boolean {\n return modelId.startsWith('claude-');\n}\n\n/**\n * Get the Claude CLI model flag for an Anthropic model\n * Maps our model IDs to Claude's expected format\n */\nexport function getClaudeModelFlag(modelId: ModelId | string): string {\n const modelMap: Record<string, string> = {\n 'claude-opus-4-6': 'opus',\n 'claude-sonnet-4-6': 'sonnet',\n 'claude-sonnet-4-5': 'sonnet',\n 'claude-haiku-4-5': 'haiku',\n };\n return modelMap[modelId] || 'sonnet';\n}\n\n/**\n * Get the command to run an agent with a specific model\n * Always uses 'claude' CLI — non-Anthropic models work via ANTHROPIC_BASE_URL env var\n * pointing to their Anthropic-compatible endpoint.\n */\nexport function getAgentCommand(modelId: ModelId | string): { command: string; args: string[] } {\n if (isAnthropicModel(modelId)) {\n return {\n command: 'claude',\n args: ['--model', getClaudeModelFlag(modelId)],\n };\n }\n // Non-Anthropic direct providers: use claude CLI with the model name as-is.\n // The caller must set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN env vars.\n return {\n command: 'claude',\n args: ['--model', modelId],\n };\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { t as __esmMin } from "./chunk-ruWRV7i3.js";
2
2
  import { G as init_paths, W as getPanopticonHome } from "./paths-CDJ_HsbN.js";
3
3
  import { c as getProject, p as init_projects } from "./projects-Bk-5QhFQ.js";
4
- import { n as init_work_type_router, t as getModelId } from "./work-type-router-CHjciPyS.js";
5
- import { mt as getRecentRunLogs, yt as init_specialist_logs } from "./specialists-D7Kj5o6s.js";
4
+ import { n as init_work_type_router, t as getModelId } from "./work-type-router-oCgTPXsP.js";
5
+ import { mt as getRecentRunLogs, yt as init_specialist_logs } from "./specialists-DvTYu1VZ.js";
6
6
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
7
7
  import { join } from "path";
8
8
  import { exec } from "child_process";
@@ -112,7 +112,7 @@ async function generateContextDigest(projectKey, specialistType, options = {}) {
112
112
  if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
113
113
  const promptFile = join(tempDir, `digest-prompt-${Date.now()}.md`);
114
114
  writeFileSync(promptFile, prompt, "utf-8");
115
- const { getProviderEnvForModel } = await import("./agents-D_2oRFVf.js");
115
+ const { getProviderEnvForModel } = await import("./agents-BQOqo27C.js");
116
116
  const providerEnv = getProviderEnvForModel(model);
117
117
  const envPrefix = Object.entries(providerEnv).map(([k, v]) => `${k}="${v}"`).join(" ");
118
118
  const { stdout, stderr } = await execAsync(`${envPrefix ? envPrefix + " " : ""}claude --dangerously-skip-permissions --model ${model} "$(cat '${promptFile}')"`, {
@@ -232,4 +232,4 @@ __esmMin((() => {
232
232
  }))();
233
233
  export { loadContextDigest, scheduleDigestGeneration };
234
234
 
235
- //# sourceMappingURL=specialist-context-BAUWL1Fl.js.map
235
+ //# sourceMappingURL=specialist-context-B3lknlwi.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"specialist-context-BAUWL1Fl.js","names":[],"sources":["../src/lib/cloister/specialist-context.ts"],"sourcesContent":["/**\n * Specialist Context Management\n *\n * Generates and manages AI-powered context digests from recent specialist runs.\n * These digests seed new specialist sessions with learned patterns and expertise.\n *\n * Directory structure:\n * ~/.panopticon/specialists/{projectKey}/{specialistType}/context/latest-digest.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { getPanopticonHome } from '../paths.js';\nimport { getRecentRunLogs, type RunLogEntry } from './specialist-logs.js';\nimport { getProject } from '../projects.js';\nimport { getModelId } from '../work-type-router.js';\n\nconst execAsync = promisify(exec);\n\n/** Get specialists directory (lazy to support test env overrides) */\nfunction getSpecialistsDir(): string {\n return join(getPanopticonHome(), 'specialists');\n}\n\n/**\n * Get the context directory for a project's specialist\n */\nexport function getContextDirectory(projectKey: string, specialistType: string): string {\n return join(getSpecialistsDir(), projectKey, specialistType, 'context');\n}\n\n/**\n * Get the path to the latest context digest file\n */\nexport function getContextDigestPath(projectKey: string, specialistType: string): string {\n const contextDir = getContextDirectory(projectKey, specialistType);\n return join(contextDir, 'latest-digest.md');\n}\n\n/**\n * Ensure context directory exists for a project's specialist\n */\nfunction ensureContextDirectory(projectKey: string, specialistType: string): void {\n const contextDir = getContextDirectory(projectKey, specialistType);\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Load the context digest for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Context digest content or null if not found\n */\nexport function loadContextDigest(projectKey: string, specialistType: string): string | null {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return null;\n }\n\n try {\n return readFileSync(digestPath, 'utf-8');\n } catch (error) {\n console.error(`[specialist-context] Failed to load digest for ${projectKey}/${specialistType}:`, error);\n return null;\n }\n}\n\n/**\n * Get the number of recent runs to include in context\n *\n * Reads from project config or uses default.\n *\n * @param projectKey - Project identifier\n * @returns Number of runs to include (default: 5)\n */\nfunction getContextRunsCount(projectKey: string): number {\n const project = getProject(projectKey);\n return project?.specialists?.context_runs ?? 5;\n}\n\n/**\n * Get the model to use for digest generation\n *\n * Reads from project config or uses the same model as the specialist.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Model ID to use\n */\nfunction getDigestModel(projectKey: string, specialistType: string): string {\n const project = getProject(projectKey);\n\n // Check for explicit digest model in project config\n if (project?.specialists?.digest_model) {\n return project.specialists.digest_model;\n }\n\n // Fall back to specialist's model\n try {\n const workTypeId = `specialist-${specialistType}` as any;\n return getModelId(workTypeId);\n } catch (error) {\n // Default to Sonnet if can't resolve\n return 'claude-sonnet-4-6';\n }\n}\n\n/**\n * Generate a context digest from recent runs using AI\n *\n * Creates an AI-generated summary of recent specialist runs to provide\n * context for the next run. This includes patterns, learnings, and common issues.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param options - Generation options\n * @returns Generated digest or null if generation failed\n */\nexport async function generateContextDigest(\n projectKey: string,\n specialistType: string,\n options: {\n runCount?: number;\n model?: string;\n force?: boolean; // Generate even if no recent runs\n } = {}\n): Promise<string | null> {\n ensureContextDirectory(projectKey, specialistType);\n\n // Get recent runs\n const runCount = options.runCount ?? getContextRunsCount(projectKey);\n const recentRuns = getRecentRunLogs(projectKey, specialistType, runCount);\n\n if (recentRuns.length === 0 && !options.force) {\n console.log(`[specialist-context] No recent runs for ${projectKey}/${specialistType}, skipping digest generation`);\n return null;\n }\n\n // Build prompt for digest generation\n const prompt = buildDigestPrompt(projectKey, specialistType, recentRuns);\n const model = options.model ?? getDigestModel(projectKey, specialistType);\n\n try {\n console.log(`[specialist-context] Generating digest for ${projectKey}/${specialistType} using ${model}...`);\n\n // Use Claude Code CLI to generate digest\n // Write prompt to temp file to avoid shell escaping issues\n const tempDir = join(getPanopticonHome(), 'tmp');\n if (!existsSync(tempDir)) {\n mkdirSync(tempDir, { recursive: true });\n }\n\n const promptFile = join(tempDir, `digest-prompt-${Date.now()}.md`);\n writeFileSync(promptFile, prompt, 'utf-8');\n\n // Run Claude Code with the prompt (include provider env vars for non-Anthropic models)\n const { getProviderEnvForModel } = await import('../agents.js');\n const providerEnv = getProviderEnvForModel(model);\n const envPrefix = Object.entries(providerEnv).map(([k, v]) => `${k}=\"${v}\"`).join(' ');\n const { stdout, stderr } = await execAsync(\n `${envPrefix ? envPrefix + ' ' : ''}claude --dangerously-skip-permissions --model ${model} \"$(cat '${promptFile}')\"`,\n {\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024, // 10MB buffer\n timeout: 60000, // 60 second timeout\n }\n );\n\n // Clean up temp file\n try {\n unlinkSync(promptFile);\n } catch {\n // Ignore cleanup errors\n }\n\n if (stderr && !stderr.includes('warning')) {\n console.error(`[specialist-context] Claude stderr:`, stderr);\n }\n\n const digest = stdout.trim();\n\n if (!digest) {\n console.error(`[specialist-context] Empty digest generated`);\n return null;\n }\n\n // Save digest\n const digestPath = getContextDigestPath(projectKey, specialistType);\n writeFileSync(digestPath, digest, 'utf-8');\n\n console.log(`[specialist-context] Generated digest (${digest.length} chars)`);\n return digest;\n } catch (error: any) {\n console.error(`[specialist-context] Failed to generate digest:`, error.message);\n // Degrade gracefully - return null so specialist can continue without context\n return null;\n }\n}\n\n/**\n * Build the prompt for digest generation\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param recentRuns - Recent run logs\n * @returns Prompt for Claude\n */\nfunction buildDigestPrompt(\n projectKey: string,\n specialistType: string,\n recentRuns: RunLogEntry[]\n): string {\n const project = getProject(projectKey);\n const projectName = project?.name || projectKey;\n\n let prompt = `You are analyzing the recent history of a ${specialistType} specialist for the ${projectName} project.\n\nYour task is to generate a concise context digest that will be provided to the specialist at the start of their next run. This digest should help them understand:\n- Common patterns and practices observed in recent runs\n- Recurring issues or failure modes\n- Successful approaches and best practices\n- Any project-specific context that would be helpful\n\nGenerate a digest in markdown format. Keep it focused and actionable - aim for 200-400 words total.\n\n## Recent Runs\n\n`;\n\n if (recentRuns.length === 0) {\n prompt += `No recent runs available yet. This is the specialist's first run.\\n\\n`;\n prompt += `Generate a brief introduction for the specialist explaining their role and what to expect.\\n`;\n } else {\n recentRuns.forEach((run, index) => {\n prompt += `### Run ${index + 1}: ${run.metadata.issueId} (${run.metadata.status || 'unknown'})\\n`;\n prompt += `Started: ${run.metadata.startedAt}\\n`;\n if (run.metadata.finishedAt) {\n prompt += `Finished: ${run.metadata.finishedAt}\\n`;\n }\n if (run.metadata.duration) {\n const durationSec = Math.floor(run.metadata.duration / 1000);\n const minutes = Math.floor(durationSec / 60);\n const seconds = durationSec % 60;\n prompt += `Duration: ${minutes}m ${seconds}s\\n`;\n }\n if (run.metadata.notes) {\n prompt += `Notes: ${run.metadata.notes}\\n`;\n }\n\n // Include snippets from the log if available\n try {\n const logContent = readFileSync(run.filePath, 'utf-8');\n // Extract key sections (limit to avoid overwhelming the prompt)\n const maxChars = 500;\n const transcriptMatch = logContent.match(/## Session Transcript\\n([\\s\\S]+?)(?=\\n## |$)/);\n if (transcriptMatch) {\n let transcript = transcriptMatch[1].trim();\n if (transcript.length > maxChars) {\n transcript = transcript.substring(0, maxChars) + '... [truncated]';\n }\n prompt += `\\nTranscript excerpt:\\n${transcript}\\n`;\n }\n } catch (error) {\n // If we can't read the log, skip the excerpt\n }\n\n prompt += `\\n`;\n });\n }\n\n prompt += `\\n## Your Task\n\nGenerate a context digest that summarizes the key insights from these runs. Format it as:\n\n# Recent ${specialistType} History for ${projectName}\n\n## Summary\n[2-3 sentence overview of patterns and trends]\n\n## Common Patterns\n[Bulleted list of observed patterns]\n\n## Recent Notable Runs\n[Brief highlights of 2-3 most interesting runs]\n\n## Recommendations\n[Specific guidance for the next run based on this history]\n\nKeep it concise, actionable, and focused on helping the specialist be more effective.`;\n\n return prompt;\n}\n\n/**\n * Regenerate the context digest\n *\n * Forces regeneration even if a digest already exists.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Generated digest or null if generation failed\n */\nexport async function regenerateContextDigest(\n projectKey: string,\n specialistType: string\n): Promise<string | null> {\n return generateContextDigest(projectKey, specialistType, { force: true });\n}\n\n/**\n * Generate digest after a run completes (async, fire-and-forget)\n *\n * This is called after a specialist finishes a run to update the context\n * for the next run. It runs asynchronously and failures are logged but not thrown.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport function scheduleDigestGeneration(projectKey: string, specialistType: string): void {\n // Run async without awaiting\n generateContextDigest(projectKey, specialistType).catch((error) => {\n console.error(\n `[specialist-context] Background digest generation failed for ${projectKey}/${specialistType}:`,\n error\n );\n });\n}\n\n/**\n * Check if a context digest exists\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest file exists\n */\nexport function hasContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n return existsSync(digestPath);\n}\n\n/**\n * Delete the context digest\n *\n * Useful for forcing a fresh start or clearing stale context.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest was deleted, false if it didn't exist\n */\nexport function deleteContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return false;\n }\n\n try {\n unlinkSync(digestPath);\n return true;\n } catch (error) {\n console.error(`[specialist-context] Failed to delete digest:`, error);\n return false;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"specialist-context-B3lknlwi.js","names":[],"sources":["../src/lib/cloister/specialist-context.ts"],"sourcesContent":["/**\n * Specialist Context Management\n *\n * Generates and manages AI-powered context digests from recent specialist runs.\n * These digests seed new specialist sessions with learned patterns and expertise.\n *\n * Directory structure:\n * ~/.panopticon/specialists/{projectKey}/{specialistType}/context/latest-digest.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { getPanopticonHome } from '../paths.js';\nimport { getRecentRunLogs, type RunLogEntry } from './specialist-logs.js';\nimport { getProject } from '../projects.js';\nimport { getModelId } from '../work-type-router.js';\n\nconst execAsync = promisify(exec);\n\n/** Get specialists directory (lazy to support test env overrides) */\nfunction getSpecialistsDir(): string {\n return join(getPanopticonHome(), 'specialists');\n}\n\n/**\n * Get the context directory for a project's specialist\n */\nexport function getContextDirectory(projectKey: string, specialistType: string): string {\n return join(getSpecialistsDir(), projectKey, specialistType, 'context');\n}\n\n/**\n * Get the path to the latest context digest file\n */\nexport function getContextDigestPath(projectKey: string, specialistType: string): string {\n const contextDir = getContextDirectory(projectKey, specialistType);\n return join(contextDir, 'latest-digest.md');\n}\n\n/**\n * Ensure context directory exists for a project's specialist\n */\nfunction ensureContextDirectory(projectKey: string, specialistType: string): void {\n const contextDir = getContextDirectory(projectKey, specialistType);\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Load the context digest for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Context digest content or null if not found\n */\nexport function loadContextDigest(projectKey: string, specialistType: string): string | null {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return null;\n }\n\n try {\n return readFileSync(digestPath, 'utf-8');\n } catch (error) {\n console.error(`[specialist-context] Failed to load digest for ${projectKey}/${specialistType}:`, error);\n return null;\n }\n}\n\n/**\n * Get the number of recent runs to include in context\n *\n * Reads from project config or uses default.\n *\n * @param projectKey - Project identifier\n * @returns Number of runs to include (default: 5)\n */\nfunction getContextRunsCount(projectKey: string): number {\n const project = getProject(projectKey);\n return project?.specialists?.context_runs ?? 5;\n}\n\n/**\n * Get the model to use for digest generation\n *\n * Reads from project config or uses the same model as the specialist.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Model ID to use\n */\nfunction getDigestModel(projectKey: string, specialistType: string): string {\n const project = getProject(projectKey);\n\n // Check for explicit digest model in project config\n if (project?.specialists?.digest_model) {\n return project.specialists.digest_model;\n }\n\n // Fall back to specialist's model\n try {\n const workTypeId = `specialist-${specialistType}` as any;\n return getModelId(workTypeId);\n } catch (error) {\n // Default to Sonnet if can't resolve\n return 'claude-sonnet-4-6';\n }\n}\n\n/**\n * Generate a context digest from recent runs using AI\n *\n * Creates an AI-generated summary of recent specialist runs to provide\n * context for the next run. This includes patterns, learnings, and common issues.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param options - Generation options\n * @returns Generated digest or null if generation failed\n */\nexport async function generateContextDigest(\n projectKey: string,\n specialistType: string,\n options: {\n runCount?: number;\n model?: string;\n force?: boolean; // Generate even if no recent runs\n } = {}\n): Promise<string | null> {\n ensureContextDirectory(projectKey, specialistType);\n\n // Get recent runs\n const runCount = options.runCount ?? getContextRunsCount(projectKey);\n const recentRuns = getRecentRunLogs(projectKey, specialistType, runCount);\n\n if (recentRuns.length === 0 && !options.force) {\n console.log(`[specialist-context] No recent runs for ${projectKey}/${specialistType}, skipping digest generation`);\n return null;\n }\n\n // Build prompt for digest generation\n const prompt = buildDigestPrompt(projectKey, specialistType, recentRuns);\n const model = options.model ?? getDigestModel(projectKey, specialistType);\n\n try {\n console.log(`[specialist-context] Generating digest for ${projectKey}/${specialistType} using ${model}...`);\n\n // Use Claude Code CLI to generate digest\n // Write prompt to temp file to avoid shell escaping issues\n const tempDir = join(getPanopticonHome(), 'tmp');\n if (!existsSync(tempDir)) {\n mkdirSync(tempDir, { recursive: true });\n }\n\n const promptFile = join(tempDir, `digest-prompt-${Date.now()}.md`);\n writeFileSync(promptFile, prompt, 'utf-8');\n\n // Run Claude Code with the prompt (include provider env vars for non-Anthropic models)\n const { getProviderEnvForModel } = await import('../agents.js');\n const providerEnv = getProviderEnvForModel(model);\n const envPrefix = Object.entries(providerEnv).map(([k, v]) => `${k}=\"${v}\"`).join(' ');\n const { stdout, stderr } = await execAsync(\n `${envPrefix ? envPrefix + ' ' : ''}claude --dangerously-skip-permissions --model ${model} \"$(cat '${promptFile}')\"`,\n {\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024, // 10MB buffer\n timeout: 60000, // 60 second timeout\n }\n );\n\n // Clean up temp file\n try {\n unlinkSync(promptFile);\n } catch {\n // Ignore cleanup errors\n }\n\n if (stderr && !stderr.includes('warning')) {\n console.error(`[specialist-context] Claude stderr:`, stderr);\n }\n\n const digest = stdout.trim();\n\n if (!digest) {\n console.error(`[specialist-context] Empty digest generated`);\n return null;\n }\n\n // Save digest\n const digestPath = getContextDigestPath(projectKey, specialistType);\n writeFileSync(digestPath, digest, 'utf-8');\n\n console.log(`[specialist-context] Generated digest (${digest.length} chars)`);\n return digest;\n } catch (error: any) {\n console.error(`[specialist-context] Failed to generate digest:`, error.message);\n // Degrade gracefully - return null so specialist can continue without context\n return null;\n }\n}\n\n/**\n * Build the prompt for digest generation\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param recentRuns - Recent run logs\n * @returns Prompt for Claude\n */\nfunction buildDigestPrompt(\n projectKey: string,\n specialistType: string,\n recentRuns: RunLogEntry[]\n): string {\n const project = getProject(projectKey);\n const projectName = project?.name || projectKey;\n\n let prompt = `You are analyzing the recent history of a ${specialistType} specialist for the ${projectName} project.\n\nYour task is to generate a concise context digest that will be provided to the specialist at the start of their next run. This digest should help them understand:\n- Common patterns and practices observed in recent runs\n- Recurring issues or failure modes\n- Successful approaches and best practices\n- Any project-specific context that would be helpful\n\nGenerate a digest in markdown format. Keep it focused and actionable - aim for 200-400 words total.\n\n## Recent Runs\n\n`;\n\n if (recentRuns.length === 0) {\n prompt += `No recent runs available yet. This is the specialist's first run.\\n\\n`;\n prompt += `Generate a brief introduction for the specialist explaining their role and what to expect.\\n`;\n } else {\n recentRuns.forEach((run, index) => {\n prompt += `### Run ${index + 1}: ${run.metadata.issueId} (${run.metadata.status || 'unknown'})\\n`;\n prompt += `Started: ${run.metadata.startedAt}\\n`;\n if (run.metadata.finishedAt) {\n prompt += `Finished: ${run.metadata.finishedAt}\\n`;\n }\n if (run.metadata.duration) {\n const durationSec = Math.floor(run.metadata.duration / 1000);\n const minutes = Math.floor(durationSec / 60);\n const seconds = durationSec % 60;\n prompt += `Duration: ${minutes}m ${seconds}s\\n`;\n }\n if (run.metadata.notes) {\n prompt += `Notes: ${run.metadata.notes}\\n`;\n }\n\n // Include snippets from the log if available\n try {\n const logContent = readFileSync(run.filePath, 'utf-8');\n // Extract key sections (limit to avoid overwhelming the prompt)\n const maxChars = 500;\n const transcriptMatch = logContent.match(/## Session Transcript\\n([\\s\\S]+?)(?=\\n## |$)/);\n if (transcriptMatch) {\n let transcript = transcriptMatch[1].trim();\n if (transcript.length > maxChars) {\n transcript = transcript.substring(0, maxChars) + '... [truncated]';\n }\n prompt += `\\nTranscript excerpt:\\n${transcript}\\n`;\n }\n } catch (error) {\n // If we can't read the log, skip the excerpt\n }\n\n prompt += `\\n`;\n });\n }\n\n prompt += `\\n## Your Task\n\nGenerate a context digest that summarizes the key insights from these runs. Format it as:\n\n# Recent ${specialistType} History for ${projectName}\n\n## Summary\n[2-3 sentence overview of patterns and trends]\n\n## Common Patterns\n[Bulleted list of observed patterns]\n\n## Recent Notable Runs\n[Brief highlights of 2-3 most interesting runs]\n\n## Recommendations\n[Specific guidance for the next run based on this history]\n\nKeep it concise, actionable, and focused on helping the specialist be more effective.`;\n\n return prompt;\n}\n\n/**\n * Regenerate the context digest\n *\n * Forces regeneration even if a digest already exists.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Generated digest or null if generation failed\n */\nexport async function regenerateContextDigest(\n projectKey: string,\n specialistType: string\n): Promise<string | null> {\n return generateContextDigest(projectKey, specialistType, { force: true });\n}\n\n/**\n * Generate digest after a run completes (async, fire-and-forget)\n *\n * This is called after a specialist finishes a run to update the context\n * for the next run. It runs asynchronously and failures are logged but not thrown.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport function scheduleDigestGeneration(projectKey: string, specialistType: string): void {\n // Run async without awaiting\n generateContextDigest(projectKey, specialistType).catch((error) => {\n console.error(\n `[specialist-context] Background digest generation failed for ${projectKey}/${specialistType}:`,\n error\n );\n });\n}\n\n/**\n * Check if a context digest exists\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest file exists\n */\nexport function hasContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n return existsSync(digestPath);\n}\n\n/**\n * Delete the context digest\n *\n * Useful for forcing a fresh start or clearing stale context.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest was deleted, false if it didn't exist\n */\nexport function deleteContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return false;\n }\n\n try {\n unlinkSync(digestPath);\n return true;\n } catch (error) {\n console.error(`[specialist-context] Failed to delete digest:`, error);\n return false;\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { St as parseLogMetadata, dt as createRunLog, ft as finalizeRunLog, gt as getRunLogPath, ht as getRunLog, lt as cleanupAllLogs, ut as cleanupOldLogs, xt as listRunLogs, yt as init_specialist_logs } from "./specialists-D7Kj5o6s.js";
1
+ import { St as parseLogMetadata, dt as createRunLog, ft as finalizeRunLog, gt as getRunLogPath, ht as getRunLog, lt as cleanupAllLogs, ut as cleanupOldLogs, xt as listRunLogs, yt as init_specialist_logs } from "./specialists-DvTYu1VZ.js";
2
2
  init_specialist_logs();
3
3
  export { cleanupAllLogs, cleanupOldLogs, createRunLog, finalizeRunLog, getRunLog, getRunLogPath, listRunLogs, parseLogMetadata };