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
@@ -18,7 +18,7 @@
18
18
  }
19
19
  })();
20
20
  </script>
21
- <script type="module" crossorigin src="/assets/index-DKlrFY1k.js"></script>
21
+ <script type="module" crossorigin src="/assets/index-DjGsaJLv.js"></script>
22
22
  <link rel="modulepreload" crossorigin href="/assets/chunk-BEqpzyXh.js">
23
23
  <link rel="stylesheet" crossorigin href="/assets/index-OEEbThNN.css">
24
24
  </head>
@@ -1,3 +1,3 @@
1
- import { a as loadReviewStatuses, i as init_review_status, n as clearStuckMergeStatuses, o as saveReviewStatuses, r as getReviewStatus, s as setReviewStatus, t as clearReviewStatus } from "./review-status-Bymwzh2i.js";
1
+ import { a as loadReviewStatuses, i as init_review_status, n as clearStuckMergeStatuses, o as saveReviewStatuses, r as getReviewStatus, s as setReviewStatus, t as clearReviewStatus } from "./review-status-d_wOE-XQ.js";
2
2
  init_review_status();
3
3
  export { clearReviewStatus, clearStuckMergeStatuses, getReviewStatus, loadReviewStatuses, saveReviewStatuses, setReviewStatus };
@@ -224,7 +224,7 @@ function setReviewStatus(issueId, update, filePath = DEFAULT_STATUS_FILE) {
224
224
  });
225
225
  if (update.reviewStatus === "passed" && existing.reviewStatus !== "passed" && existing.testStatus === "pending") (async () => {
226
226
  try {
227
- const { submitToSpecialistQueue } = await import("./specialists-Cp-PgspS.js");
227
+ const { submitToSpecialistQueue } = await import("./specialists-sIFlMd3s.js");
228
228
  const workAgentId = `agent-${issueId.toLowerCase()}`;
229
229
  const workStateFile = join(homedir(), ".panopticon", "agents", workAgentId, "state.json");
230
230
  let workspace;
@@ -254,7 +254,7 @@ function setReviewStatus(issueId, update, filePath = DEFAULT_STATUS_FILE) {
254
254
  if (!sessionExists(agentSession)) return;
255
255
  const statusType = update.reviewStatus === "blocked" ? "REVIEW BLOCKED" : "TESTS FAILED";
256
256
  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}`;
257
- const { messageAgent } = await import("./agents-Dgh2TjSp.js");
257
+ const { messageAgent } = await import("./agents-Dinc9j_8.js");
258
258
  await messageAgent(agentSession, msg);
259
259
  console.log(`[review-status] Auto-delivered ${statusType} feedback to ${agentSession}`);
260
260
  } catch (err) {
@@ -302,4 +302,4 @@ var init_review_status = __esmMin((() => {
302
302
  //#endregion
303
303
  export { loadReviewStatuses as a, init_review_status as i, clearStuckMergeStatuses as n, saveReviewStatuses as o, getReviewStatus as r, setReviewStatus as s, clearReviewStatus as t };
304
304
 
305
- //# sourceMappingURL=review-status-Bymwzh2i.js.map
305
+ //# sourceMappingURL=review-status-d_wOE-XQ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"review-status-Bymwzh2i.js","names":[],"sources":["../../src/lib/database/review-status-db.ts","../../src/lib/review-status.ts"],"sourcesContent":["/**\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-d_wOE-XQ.js","names":[],"sources":["../../src/lib/database/review-status-db.ts","../../src/lib/review-status.ts"],"sourcesContent":["/**\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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"}