@in-the-loop-labs/pair-review 1.4.3 → 1.5.0

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 (48) hide show
  1. package/package.json +1 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin/skills/review-requests/SKILL.md +54 -0
  4. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  5. package/public/css/pr.css +1081 -54
  6. package/public/css/repo-settings.css +452 -140
  7. package/public/js/components/AdvancedConfigTab.js +1364 -0
  8. package/public/js/components/AnalysisConfigModal.js +488 -112
  9. package/public/js/components/CouncilProgressModal.js +1416 -0
  10. package/public/js/components/TextInputDialog.js +231 -0
  11. package/public/js/components/TimeoutSelect.js +367 -0
  12. package/public/js/components/VoiceCentricConfigTab.js +1334 -0
  13. package/public/js/local.js +162 -83
  14. package/public/js/modules/analysis-history.js +185 -11
  15. package/public/js/modules/comment-manager.js +13 -0
  16. package/public/js/modules/file-comment-manager.js +28 -0
  17. package/public/js/pr.js +233 -115
  18. package/public/js/repo-settings.js +575 -106
  19. package/public/local.html +11 -1
  20. package/public/pr.html +6 -1
  21. package/public/repo-settings.html +28 -21
  22. package/public/setup.html +8 -2
  23. package/src/ai/analyzer.js +1262 -111
  24. package/src/ai/claude-cli.js +2 -2
  25. package/src/ai/claude-provider.js +6 -6
  26. package/src/ai/codex-provider.js +6 -6
  27. package/src/ai/copilot-provider.js +3 -3
  28. package/src/ai/cursor-agent-provider.js +6 -6
  29. package/src/ai/gemini-provider.js +6 -6
  30. package/src/ai/opencode-provider.js +6 -6
  31. package/src/ai/pi-provider.js +6 -6
  32. package/src/ai/prompts/baseline/consolidation/balanced.js +208 -0
  33. package/src/ai/prompts/baseline/consolidation/fast.js +175 -0
  34. package/src/ai/prompts/baseline/consolidation/thorough.js +283 -0
  35. package/src/ai/prompts/config.js +1 -1
  36. package/src/ai/prompts/index.js +26 -2
  37. package/src/ai/provider.js +4 -2
  38. package/src/database.js +417 -14
  39. package/src/main.js +1 -1
  40. package/src/routes/analysis.js +495 -10
  41. package/src/routes/config.js +36 -15
  42. package/src/routes/councils.js +351 -0
  43. package/src/routes/local.js +33 -11
  44. package/src/routes/mcp.js +9 -2
  45. package/src/routes/setup.js +12 -2
  46. package/src/routes/shared.js +126 -13
  47. package/src/server.js +34 -4
  48. package/src/utils/stats-calculator.js +2 -0
package/src/database.js CHANGED
@@ -9,7 +9,7 @@ const DB_PATH = path.join(getConfigDir(), 'database.db');
9
9
  /**
10
10
  * Current schema version - increment this when adding new migrations
11
11
  */
12
- const CURRENT_SCHEMA_VERSION = 14;
12
+ const CURRENT_SCHEMA_VERSION = 18;
13
13
 
14
14
  /**
15
15
  * Database schema SQL statements
@@ -75,6 +75,9 @@ const SCHEMA_SQL = {
75
75
  parent_id INTEGER,
76
76
  is_file_level INTEGER DEFAULT 0,
77
77
 
78
+ voice_id TEXT,
79
+ is_raw INTEGER DEFAULT 0,
80
+
78
81
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
79
82
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
80
83
 
@@ -121,6 +124,8 @@ const SCHEMA_SQL = {
121
124
  default_instructions TEXT,
122
125
  default_provider TEXT,
123
126
  default_model TEXT,
127
+ default_council_id TEXT,
128
+ default_tab TEXT,
124
129
  local_path TEXT,
125
130
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
126
131
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP
@@ -143,6 +148,9 @@ const SCHEMA_SQL = {
143
148
  files_analyzed INTEGER DEFAULT 0,
144
149
  started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
150
  completed_at TIMESTAMP,
151
+ parent_run_id TEXT,
152
+ config_type TEXT DEFAULT 'single',
153
+ levels_config TEXT,
146
154
  FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
147
155
  )
148
156
  `,
@@ -171,6 +179,18 @@ const SCHEMA_SQL = {
171
179
  github_url TEXT,
172
180
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
173
181
  )
182
+ `,
183
+
184
+ councils: `
185
+ CREATE TABLE IF NOT EXISTS councils (
186
+ id TEXT PRIMARY KEY,
187
+ name TEXT NOT NULL,
188
+ type TEXT DEFAULT 'advanced',
189
+ config JSON NOT NULL,
190
+ last_used_at DATETIME,
191
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
192
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
193
+ )
174
194
  `
175
195
  };
176
196
 
@@ -193,9 +213,15 @@ const INDEX_SQL = [
193
213
  // Analysis runs indexes
194
214
  'CREATE INDEX IF NOT EXISTS idx_analysis_runs_review_id ON analysis_runs(review_id, started_at DESC)',
195
215
  'CREATE INDEX IF NOT EXISTS idx_analysis_runs_status ON analysis_runs(status)',
216
+ 'CREATE INDEX IF NOT EXISTS idx_analysis_runs_parent ON analysis_runs(parent_run_id)',
196
217
  // GitHub reviews indexes
197
218
  'CREATE INDEX IF NOT EXISTS idx_github_reviews_review_id ON github_reviews(review_id)',
198
- 'CREATE INDEX IF NOT EXISTS idx_github_reviews_state ON github_reviews(state)'
219
+ 'CREATE INDEX IF NOT EXISTS idx_github_reviews_state ON github_reviews(state)',
220
+ // Council indexes
221
+ 'CREATE INDEX IF NOT EXISTS idx_councils_name ON councils(name)',
222
+ // Voice tracking indexes
223
+ 'CREATE INDEX IF NOT EXISTS idx_comments_voice ON comments(voice_id)',
224
+ 'CREATE INDEX IF NOT EXISTS idx_comments_is_raw ON comments(is_raw)'
199
225
  ];
200
226
 
201
227
  /**
@@ -683,6 +709,205 @@ const MIGRATIONS = {
683
709
  console.log(' Created index idx_reviews_type_updated');
684
710
 
685
711
  console.log('Migration to schema version 14 complete');
712
+ },
713
+
714
+ // Migration to version 15: adds councils table and voice tracking columns to comments
715
+ 15: (db) => {
716
+ console.log('Running migration to schema version 15...');
717
+
718
+ // Create councils table if it doesn't exist
719
+ if (!tableExists(db, 'councils')) {
720
+ db.exec(`
721
+ CREATE TABLE councils (
722
+ id TEXT PRIMARY KEY,
723
+ name TEXT NOT NULL,
724
+ config JSON NOT NULL,
725
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
726
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
727
+ )
728
+ `);
729
+ db.exec('CREATE INDEX IF NOT EXISTS idx_councils_name ON councils(name)');
730
+ console.log(' Created councils table');
731
+ } else {
732
+ console.log(' Table councils already exists');
733
+ }
734
+
735
+ // Add voice_id column to comments if it doesn't exist
736
+ const hasVoiceId = columnExists(db, 'comments', 'voice_id');
737
+ if (!hasVoiceId) {
738
+ try {
739
+ db.prepare(`ALTER TABLE comments ADD COLUMN voice_id TEXT`).run();
740
+ db.exec('CREATE INDEX IF NOT EXISTS idx_comments_voice ON comments(voice_id)');
741
+ console.log(' Added voice_id column to comments');
742
+ } catch (error) {
743
+ if (!error.message.includes('duplicate column name')) {
744
+ throw error;
745
+ }
746
+ console.log(' Column voice_id already exists (race condition)');
747
+ }
748
+ } else {
749
+ console.log(' Column voice_id already exists');
750
+ }
751
+
752
+ // Add is_raw column to comments if it doesn't exist
753
+ const hasIsRaw = columnExists(db, 'comments', 'is_raw');
754
+ if (!hasIsRaw) {
755
+ try {
756
+ db.prepare(`ALTER TABLE comments ADD COLUMN is_raw INTEGER DEFAULT 0`).run();
757
+ db.exec('CREATE INDEX IF NOT EXISTS idx_comments_is_raw ON comments(is_raw)');
758
+ console.log(' Added is_raw column to comments');
759
+ } catch (error) {
760
+ if (!error.message.includes('duplicate column name')) {
761
+ throw error;
762
+ }
763
+ console.log(' Column is_raw already exists (race condition)');
764
+ }
765
+ } else {
766
+ console.log(' Column is_raw already exists');
767
+ }
768
+
769
+ console.log('Migration to schema version 15 complete');
770
+ },
771
+
772
+ // Migration to version 16: Add council MRU tracking and repo default council
773
+ 16: (db) => {
774
+ console.log('Running migration to schema version 16...');
775
+
776
+ // Add last_used_at column to councils for MRU ordering
777
+ const hasLastUsedAt = columnExists(db, 'councils', 'last_used_at');
778
+ if (!hasLastUsedAt) {
779
+ try {
780
+ db.prepare(`ALTER TABLE councils ADD COLUMN last_used_at DATETIME`).run();
781
+ console.log(' Added last_used_at column to councils');
782
+ } catch (error) {
783
+ if (!error.message.includes('duplicate column name')) {
784
+ throw error;
785
+ }
786
+ console.log(' Column last_used_at already exists (race condition)');
787
+ }
788
+ } else {
789
+ console.log(' Column last_used_at already exists');
790
+ }
791
+
792
+ // Add default_council_id column to repo_settings
793
+ const hasDefaultCouncilId = columnExists(db, 'repo_settings', 'default_council_id');
794
+ if (!hasDefaultCouncilId) {
795
+ try {
796
+ db.prepare(`ALTER TABLE repo_settings ADD COLUMN default_council_id TEXT`).run();
797
+ console.log(' Added default_council_id column to repo_settings');
798
+ } catch (error) {
799
+ if (!error.message.includes('duplicate column name')) {
800
+ throw error;
801
+ }
802
+ console.log(' Column default_council_id already exists (race condition)');
803
+ }
804
+ } else {
805
+ console.log(' Column default_council_id already exists');
806
+ }
807
+
808
+ console.log('Migration to schema version 16 complete');
809
+ },
810
+
811
+ // Migration to version 17: Add voice-centric council columns and repo default_tab
812
+ 17: (db) => {
813
+ console.log('Running migration to schema version 17...');
814
+
815
+ // Add parent_run_id to analysis_runs for child voice runs
816
+ const hasParentRunId = columnExists(db, 'analysis_runs', 'parent_run_id');
817
+ if (!hasParentRunId) {
818
+ try {
819
+ db.prepare(`ALTER TABLE analysis_runs ADD COLUMN parent_run_id TEXT`).run();
820
+ console.log(' Added parent_run_id column to analysis_runs');
821
+ } catch (error) {
822
+ if (!error.message.includes('duplicate column name')) {
823
+ throw error;
824
+ }
825
+ console.log(' Column parent_run_id already exists (race condition)');
826
+ }
827
+ } else {
828
+ console.log(' Column parent_run_id already exists');
829
+ }
830
+
831
+ // Add config_type to analysis_runs
832
+ const hasConfigType = columnExists(db, 'analysis_runs', 'config_type');
833
+ if (!hasConfigType) {
834
+ try {
835
+ db.prepare(`ALTER TABLE analysis_runs ADD COLUMN config_type TEXT DEFAULT 'single'`).run();
836
+ console.log(' Added config_type column to analysis_runs');
837
+ } catch (error) {
838
+ if (!error.message.includes('duplicate column name')) {
839
+ throw error;
840
+ }
841
+ console.log(' Column config_type already exists (race condition)');
842
+ }
843
+ } else {
844
+ console.log(' Column config_type already exists');
845
+ }
846
+
847
+ // Add levels_config to analysis_runs
848
+ const hasLevelsConfig = columnExists(db, 'analysis_runs', 'levels_config');
849
+ if (!hasLevelsConfig) {
850
+ try {
851
+ db.prepare(`ALTER TABLE analysis_runs ADD COLUMN levels_config TEXT`).run();
852
+ console.log(' Added levels_config column to analysis_runs');
853
+ } catch (error) {
854
+ if (!error.message.includes('duplicate column name')) {
855
+ throw error;
856
+ }
857
+ console.log(' Column levels_config already exists (race condition)');
858
+ }
859
+ } else {
860
+ console.log(' Column levels_config already exists');
861
+ }
862
+
863
+ // Add default_tab to repo_settings
864
+ const hasDefaultTab = columnExists(db, 'repo_settings', 'default_tab');
865
+ if (!hasDefaultTab) {
866
+ try {
867
+ db.prepare(`ALTER TABLE repo_settings ADD COLUMN default_tab TEXT`).run();
868
+ console.log(' Added default_tab column to repo_settings');
869
+ } catch (error) {
870
+ if (!error.message.includes('duplicate column name')) {
871
+ throw error;
872
+ }
873
+ console.log(' Column default_tab already exists (race condition)');
874
+ }
875
+ } else {
876
+ console.log(' Column default_tab already exists');
877
+ }
878
+
879
+ // Add index for parent_run_id lookups
880
+ try {
881
+ db.prepare(`CREATE INDEX IF NOT EXISTS idx_analysis_runs_parent ON analysis_runs(parent_run_id)`).run();
882
+ console.log(' Created idx_analysis_runs_parent index');
883
+ } catch (error) {
884
+ console.log(' Index idx_analysis_runs_parent already exists');
885
+ }
886
+
887
+ console.log('Migration to schema version 17 complete');
888
+ },
889
+
890
+ // Migration to version 18: Add type column to councils table
891
+ 18: (db) => {
892
+ console.log('Running migration to schema version 18...');
893
+
894
+ // Add type column to councils for distinguishing 'council' (voice-centric) from 'advanced' (level-centric)
895
+ const hasType = columnExists(db, 'councils', 'type');
896
+ if (!hasType) {
897
+ try {
898
+ db.prepare(`ALTER TABLE councils ADD COLUMN type TEXT DEFAULT 'advanced'`).run();
899
+ console.log(' Added type column to councils');
900
+ } catch (error) {
901
+ if (!error.message.includes('duplicate column name')) {
902
+ throw error;
903
+ }
904
+ console.log(' Column type already exists (race condition)');
905
+ }
906
+ } else {
907
+ console.log(' Column type already exists');
908
+ }
909
+
910
+ console.log('Migration to schema version 18 complete');
686
911
  }
687
912
  };
688
913
 
@@ -1179,7 +1404,7 @@ class RepoSettingsRepository {
1179
1404
  */
1180
1405
  async getRepoSettings(repository) {
1181
1406
  const row = await queryOne(this.db, `
1182
- SELECT id, repository, default_instructions, default_provider, default_model, local_path, created_at, updated_at
1407
+ SELECT id, repository, default_instructions, default_provider, default_model, default_council_id, default_tab, local_path, created_at, updated_at
1183
1408
  FROM repo_settings
1184
1409
  WHERE repository = ? COLLATE NOCASE
1185
1410
  `, [repository]);
@@ -1236,7 +1461,7 @@ class RepoSettingsRepository {
1236
1461
  * @returns {Promise<Object>} Saved settings object
1237
1462
  */
1238
1463
  async saveRepoSettings(repository, settings) {
1239
- const { default_instructions, default_provider, default_model, local_path } = settings;
1464
+ const { default_instructions, default_provider, default_model, default_council_id, default_tab, local_path } = settings;
1240
1465
  const now = new Date().toISOString();
1241
1466
 
1242
1467
  // Check if settings already exist
@@ -1249,6 +1474,8 @@ class RepoSettingsRepository {
1249
1474
  SET default_instructions = ?,
1250
1475
  default_provider = ?,
1251
1476
  default_model = ?,
1477
+ default_council_id = ?,
1478
+ default_tab = ?,
1252
1479
  local_path = ?,
1253
1480
  updated_at = ?
1254
1481
  WHERE repository = ? COLLATE NOCASE
@@ -1256,6 +1483,8 @@ class RepoSettingsRepository {
1256
1483
  default_instructions !== undefined ? default_instructions : existing.default_instructions,
1257
1484
  default_provider !== undefined ? default_provider : existing.default_provider,
1258
1485
  default_model !== undefined ? default_model : existing.default_model,
1486
+ default_council_id !== undefined ? default_council_id : existing.default_council_id,
1487
+ default_tab !== undefined ? default_tab : existing.default_tab,
1259
1488
  local_path !== undefined ? local_path : existing.local_path,
1260
1489
  now,
1261
1490
  repository
@@ -1266,15 +1495,17 @@ class RepoSettingsRepository {
1266
1495
  default_instructions: default_instructions !== undefined ? default_instructions : existing.default_instructions,
1267
1496
  default_provider: default_provider !== undefined ? default_provider : existing.default_provider,
1268
1497
  default_model: default_model !== undefined ? default_model : existing.default_model,
1498
+ default_council_id: default_council_id !== undefined ? default_council_id : existing.default_council_id,
1499
+ default_tab: default_tab !== undefined ? default_tab : existing.default_tab,
1269
1500
  local_path: local_path !== undefined ? local_path : existing.local_path,
1270
1501
  updated_at: now
1271
1502
  };
1272
1503
  } else {
1273
1504
  // Insert new settings
1274
1505
  const result = await run(this.db, `
1275
- INSERT INTO repo_settings (repository, default_instructions, default_provider, default_model, local_path, created_at, updated_at)
1276
- VALUES (?, ?, ?, ?, ?, ?, ?)
1277
- `, [repository, default_instructions || null, default_provider || null, default_model || null, local_path || null, now, now]);
1506
+ INSERT INTO repo_settings (repository, default_instructions, default_provider, default_model, default_council_id, default_tab, local_path, created_at, updated_at)
1507
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1508
+ `, [repository, default_instructions || null, default_provider || null, default_model || null, default_council_id || null, default_tab || null, local_path || null, now, now]);
1278
1509
 
1279
1510
  return {
1280
1511
  id: result.lastID,
@@ -1282,6 +1513,8 @@ class RepoSettingsRepository {
1282
1513
  default_instructions: default_instructions || null,
1283
1514
  default_provider: default_provider || null,
1284
1515
  default_model: default_model || null,
1516
+ default_council_id: default_council_id || null,
1517
+ default_tab: default_tab || null,
1285
1518
  local_path: local_path || null,
1286
1519
  created_at: now,
1287
1520
  updated_at: now
@@ -2357,13 +2590,14 @@ class AnalysisRunRepository {
2357
2590
  * @param {string} [runInfo.status='running'] - Initial status (default 'running'; pass 'completed' for externally-produced results)
2358
2591
  * @returns {Promise<Object>} Created analysis run record
2359
2592
  */
2360
- async create({ id, reviewId, provider = null, model = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, status = 'running' }) {
2593
+ async create({ id, reviewId, provider = null, model = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null }) {
2361
2594
  const isTerminal = ['completed', 'failed', 'cancelled'].includes(status);
2362
2595
  const completedAt = isTerminal ? 'CURRENT_TIMESTAMP' : 'NULL';
2596
+ const levelsConfigJson = levelsConfig ? JSON.stringify(levelsConfig) : null;
2363
2597
  await run(this.db, `
2364
- INSERT INTO analysis_runs (id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions, head_sha, status, completed_at)
2365
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt})
2366
- `, [id, reviewId, provider, model, customInstructions, repoInstructions, requestInstructions, headSha, status]);
2598
+ INSERT INTO analysis_runs (id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions, head_sha, status, completed_at, parent_run_id, config_type, levels_config)
2599
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?)
2600
+ `, [id, reviewId, provider, model, customInstructions, repoInstructions, requestInstructions, headSha, status, parentRunId, configType, levelsConfigJson]);
2367
2601
 
2368
2602
  // Query back the inserted row to return actual database values (including timestamps)
2369
2603
  return await this.getById(id);
@@ -2431,7 +2665,8 @@ class AnalysisRunRepository {
2431
2665
  async getById(id) {
2432
2666
  const row = await queryOne(this.db, `
2433
2667
  SELECT id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions,
2434
- head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at
2668
+ head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
2669
+ parent_run_id, config_type, levels_config
2435
2670
  FROM analysis_runs
2436
2671
  WHERE id = ?
2437
2672
  `, [id]);
@@ -2451,10 +2686,11 @@ class AnalysisRunRepository {
2451
2686
  const params = [reviewId];
2452
2687
  let sql = `
2453
2688
  SELECT id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions,
2454
- head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at
2689
+ head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
2690
+ parent_run_id, config_type, levels_config
2455
2691
  FROM analysis_runs
2456
2692
  WHERE review_id = ?
2457
- ORDER BY started_at DESC, id DESC`;
2693
+ ORDER BY COALESCE(completed_at, started_at) DESC, CASE WHEN parent_run_id IS NULL THEN 0 ELSE 1 END, started_at DESC, id DESC`;
2458
2694
  if (limit) {
2459
2695
  sql += `\n LIMIT ?`;
2460
2696
  params.push(limit);
@@ -2472,6 +2708,22 @@ class AnalysisRunRepository {
2472
2708
  return rows.length > 0 ? rows[0] : null;
2473
2709
  }
2474
2710
 
2711
+ /**
2712
+ * Get child runs for a parent council run, ordered by start time ascending
2713
+ * @param {string} parentRunId - Parent analysis run ID
2714
+ * @returns {Promise<Array<Object>>} Array of child analysis run records
2715
+ */
2716
+ async getChildRuns(parentRunId) {
2717
+ return query(this.db, `
2718
+ SELECT id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions,
2719
+ head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
2720
+ parent_run_id, config_type, levels_config
2721
+ FROM analysis_runs
2722
+ WHERE parent_run_id = ?
2723
+ ORDER BY started_at ASC
2724
+ `, [parentRunId]);
2725
+ }
2726
+
2475
2727
  /**
2476
2728
  * Delete an analysis run by ID
2477
2729
  * @param {string} id - Analysis run ID
@@ -2677,6 +2929,156 @@ class GitHubReviewRepository {
2677
2929
  }
2678
2930
  }
2679
2931
 
2932
+ /**
2933
+ * CouncilRepository class for managing council configurations
2934
+ */
2935
+ class CouncilRepository {
2936
+ /**
2937
+ * Create a new CouncilRepository instance
2938
+ * @param {Database} db - Database instance
2939
+ */
2940
+ constructor(db) {
2941
+ this.db = db;
2942
+ }
2943
+
2944
+ /**
2945
+ * Create a new council
2946
+ * @param {Object} councilData - Council data
2947
+ * @param {string} councilData.id - Unique ID (UUID)
2948
+ * @param {string} councilData.name - Council name
2949
+ * @param {Object} councilData.config - Council configuration JSON
2950
+ * @param {string} [councilData.type='advanced'] - Council type ('council' for voice-centric, 'advanced' for level-centric)
2951
+ * @returns {Promise<Object>} Created council record
2952
+ */
2953
+ async create({ id, name, config, type = 'advanced' }) {
2954
+ if (!id || !name || !config) {
2955
+ throw new Error('Missing required fields: id, name, config');
2956
+ }
2957
+
2958
+ const configJson = typeof config === 'string' ? config : JSON.stringify(config);
2959
+
2960
+ await run(this.db, `
2961
+ INSERT INTO councils (id, name, type, config)
2962
+ VALUES (?, ?, ?, ?)
2963
+ `, [id, name, type, configJson]);
2964
+
2965
+ return this.getById(id);
2966
+ }
2967
+
2968
+ /**
2969
+ * Get a council by ID
2970
+ * @param {string} id - Council ID
2971
+ * @returns {Promise<Object|null>} Council record with parsed config, or null
2972
+ */
2973
+ async getById(id) {
2974
+ const row = await queryOne(this.db, `
2975
+ SELECT id, name, type, config, last_used_at, created_at, updated_at
2976
+ FROM councils
2977
+ WHERE id = ?
2978
+ `, [id]);
2979
+
2980
+ if (!row) return null;
2981
+ return this._parseRow(row);
2982
+ }
2983
+
2984
+ /**
2985
+ * List all councils
2986
+ * @returns {Promise<Array<Object>>} Array of council records with parsed configs
2987
+ */
2988
+ async list() {
2989
+ const rows = await query(this.db, `
2990
+ SELECT id, name, type, config, last_used_at, created_at, updated_at
2991
+ FROM councils
2992
+ ORDER BY last_used_at DESC NULLS LAST, updated_at DESC
2993
+ `);
2994
+
2995
+ return rows.map(row => this._parseRow(row));
2996
+ }
2997
+
2998
+ /**
2999
+ * Update the last_used_at timestamp for a council (for MRU tracking)
3000
+ * @param {string} id - Council ID
3001
+ * @returns {Promise<boolean>} True if record was updated (council exists)
3002
+ */
3003
+ async touchLastUsedAt(id) {
3004
+ const result = await run(this.db, `
3005
+ UPDATE councils SET last_used_at = CURRENT_TIMESTAMP WHERE id = ?
3006
+ `, [id]);
3007
+
3008
+ return result.changes > 0;
3009
+ }
3010
+
3011
+ /**
3012
+ * Update a council
3013
+ * @param {string} id - Council ID
3014
+ * @param {Object} updates - Fields to update
3015
+ * @param {string} [updates.name] - New name
3016
+ * @param {Object} [updates.config] - New configuration
3017
+ * @param {string} [updates.type] - New type ('council' or 'advanced')
3018
+ * @returns {Promise<boolean>} True if record was updated
3019
+ */
3020
+ async update(id, updates) {
3021
+ const setClauses = ['updated_at = CURRENT_TIMESTAMP'];
3022
+ const params = [];
3023
+
3024
+ if (updates.name !== undefined) {
3025
+ setClauses.push('name = ?');
3026
+ params.push(updates.name);
3027
+ }
3028
+
3029
+ if (updates.type !== undefined) {
3030
+ setClauses.push('type = ?');
3031
+ params.push(updates.type);
3032
+ }
3033
+
3034
+ if (updates.config !== undefined) {
3035
+ setClauses.push('config = ?');
3036
+ const configJson = typeof updates.config === 'string' ? updates.config : JSON.stringify(updates.config);
3037
+ params.push(configJson);
3038
+ }
3039
+
3040
+ params.push(id);
3041
+
3042
+ const result = await run(this.db, `
3043
+ UPDATE councils
3044
+ SET ${setClauses.join(', ')}
3045
+ WHERE id = ?
3046
+ `, params);
3047
+
3048
+ return result.changes > 0;
3049
+ }
3050
+
3051
+ /**
3052
+ * Delete a council
3053
+ * @param {string} id - Council ID
3054
+ * @returns {Promise<boolean>} True if record was deleted
3055
+ */
3056
+ async delete(id) {
3057
+ const result = await run(this.db, `
3058
+ DELETE FROM councils WHERE id = ?
3059
+ `, [id]);
3060
+
3061
+ return result.changes > 0;
3062
+ }
3063
+
3064
+ /**
3065
+ * Parse a database row, converting JSON config string to object
3066
+ * @param {Object} row - Raw database row
3067
+ * @returns {Object} Row with parsed config
3068
+ * @private
3069
+ */
3070
+ _parseRow(row) {
3071
+ try {
3072
+ return {
3073
+ ...row,
3074
+ config: typeof row.config === 'string' ? JSON.parse(row.config) : row.config
3075
+ };
3076
+ } catch (e) {
3077
+ return { ...row, config: {} };
3078
+ }
3079
+ }
3080
+ }
3081
+
2680
3082
  module.exports = {
2681
3083
  initializeDatabase,
2682
3084
  closeDatabase,
@@ -2698,6 +3100,7 @@ module.exports = {
2698
3100
  PRMetadataRepository,
2699
3101
  AnalysisRunRepository,
2700
3102
  GitHubReviewRepository,
3103
+ CouncilRepository,
2701
3104
  generateWorktreeId,
2702
3105
  migrateExistingWorktrees
2703
3106
  };
package/src/main.js CHANGED
@@ -881,7 +881,7 @@ async function performHeadlessReview(args, config, db, flags, options) {
881
881
  title,
882
882
  type
883
883
  FROM comments
884
- WHERE review_id = ? AND source = 'ai' AND ai_level IS NULL AND status = 'active'
884
+ WHERE review_id = ? AND source = 'ai' AND ai_level IS NULL AND (is_raw = 0 OR is_raw IS NULL) AND status = 'active'
885
885
  ORDER BY file, line_start
886
886
  `, [review.id]);
887
887