@in-the-loop-labs/pair-review 3.2.2 → 3.3.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 (46) hide show
  1. package/README.md +7 -6
  2. package/package.json +5 -4
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  5. package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -1
  6. package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +8 -1
  7. package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +8 -7
  8. package/public/css/repo-settings.css +347 -0
  9. package/public/index.html +46 -9
  10. package/public/js/components/AIPanel.js +79 -37
  11. package/public/js/components/DiffOptionsDropdown.js +84 -1
  12. package/public/js/index.js +31 -6
  13. package/public/js/modules/analysis-history.js +11 -7
  14. package/public/js/pr.js +22 -0
  15. package/public/js/repo-settings.js +334 -6
  16. package/public/repo-settings.html +29 -0
  17. package/src/ai/analyzer.js +28 -19
  18. package/src/ai/claude-cli.js +2 -0
  19. package/src/ai/claude-provider.js +4 -1
  20. package/src/ai/prompts/baseline/consolidation/balanced.js +6 -4
  21. package/src/ai/prompts/baseline/consolidation/fast.js +6 -2
  22. package/src/ai/prompts/baseline/consolidation/thorough.js +7 -6
  23. package/src/ai/prompts/baseline/orchestration/balanced.js +13 -1
  24. package/src/ai/prompts/baseline/orchestration/fast.js +12 -1
  25. package/src/ai/prompts/baseline/orchestration/thorough.js +8 -7
  26. package/src/ai/provider.js +7 -6
  27. package/src/chat/session-manager.js +6 -3
  28. package/src/config.js +230 -38
  29. package/src/database.js +766 -38
  30. package/src/git/worktree-pool-lifecycle.js +674 -0
  31. package/src/git/worktree-pool-usage.js +216 -0
  32. package/src/git/worktree.js +46 -13
  33. package/src/main.js +185 -26
  34. package/src/routes/analyses.js +48 -26
  35. package/src/routes/chat.js +27 -3
  36. package/src/routes/config.js +17 -5
  37. package/src/routes/executable-analysis.js +38 -19
  38. package/src/routes/local.js +19 -6
  39. package/src/routes/mcp.js +13 -2
  40. package/src/routes/pr.js +72 -29
  41. package/src/routes/setup.js +41 -4
  42. package/src/routes/stack-analysis.js +29 -10
  43. package/src/routes/worktrees.js +294 -9
  44. package/src/server.js +20 -3
  45. package/src/setup/pr-setup.js +161 -27
  46. package/src/ws/server.js +51 -1
@@ -123,9 +123,11 @@ Assess severity based on the evidence and reasoning across all reviewers. When r
123
123
 
124
124
  <section name="summary-synthesis" required="true">
125
125
  ## Summary Synthesis
126
- The summary field should synthesize the findings, not list them.
127
- - Synthesize the key findings into a single cohesive paragraph
128
- - **Draw on reviewer summaries**: Use these as evidence for your own synthesis integrate their insights into a cohesive conclusion rather than listing them individually
126
+ The summary field should synthesize the findings, but it should not be one big paragraph.
127
+ - Start with 1-2 sentences describing the overall assessment
128
+ - Then include a markdown bullet list using "- " bullets for the key specific points
129
+ - **Draw on reviewer summaries**: Use these as evidence for your own synthesis — integrate their insights into the overview and bullets rather than listing reviewer-by-reviewer conclusions
130
+ - After the bullets, add extra sentences or short paragraphs only when needed for requested context or follow-up detail
129
131
  - Write as if a single reviewer produced this analysis — do not mention consolidation, merging, or multiple reviewers
130
132
  </section>
131
133
 
@@ -160,7 +162,7 @@ Output JSON with this structure:
160
162
  "confidence": 0.0-1.0,
161
163
  "reasoning": ["Step-by-step reasoning explaining why this issue was flagged (optional)"]
162
164
  }],
163
- "summary": "Single cohesive paragraph summarizing key findings. Write as a single reviewer."
165
+ "summary": "Formatted markdown summary following the Summary Synthesis guidance above."
164
166
  }
165
167
 
166
168
  ### GitHub Suggestion Syntax
@@ -83,7 +83,11 @@ Deduplicate, don't concatenate: merge duplicate findings, preserve distinct ones
83
83
  </section>
84
84
 
85
85
  <section name="summary-synthesis" required="true" tier="fast">
86
- Draw on reviewer summaries as evidence for your own synthesis. Write one cohesive paragraph no mention of consolidation/merging.
86
+ Draw on reviewer summaries as evidence for your own synthesis. The summary field should be markdown, not one big paragraph.
87
+ - Start with 1-2 sentences describing the overall assessment
88
+ - Then include a markdown bullet list using "- " bullets for the key specific points
89
+ - After the bullets, add extra sentences or short paragraphs only when needed for requested context or follow-up detail
90
+ - Write as a single reviewer with no mention of consolidation or merging
87
91
  </section>
88
92
 
89
93
  <section name="output-schema" locked="true">
@@ -109,7 +113,7 @@ Draw on reviewer summaries as evidence for your own synthesis. Write one cohesiv
109
113
  "suggestion": "How to address the file-level concern (omit for praise items)",
110
114
  "confidence": 0.0-1.0
111
115
  }],
112
- "summary": "Single cohesive paragraph of key findings. Write as single reviewer."
116
+ "summary": "Formatted markdown summary following the summary guidance above."
113
117
  }
114
118
 
115
119
  ### GitHub Suggestion Syntax
@@ -167,13 +167,14 @@ Note: Confidence is about certainty of value, not severity.
167
167
 
168
168
  <section name="summary-synthesis" required="true" tier="thorough">
169
169
  ## Summary Synthesis Guidance
170
- The summary field should synthesize the findings, not list them.
170
+ The summary field should synthesize the findings, but it should not be one big paragraph.
171
171
 
172
172
  **Effective Summary Approach:**
173
- - **Lead with the most important insight**: What should the reviewer focus on first?
174
- - **Connect the dots**: How do individual findings relate to each other?
175
- - **Calibrate severity**: Is this code fundamentally sound with minor issues, or are there structural problems?
176
- - **Draw on reviewer summaries**: Use these as evidence for your own synthesis — integrate their insights into a cohesive conclusion rather than listing them individually.
173
+ - **Start with 1-2 sentences of overall assessment**: Lead with the most important insight and calibrate the overall severity
174
+ - **Then use a markdown bullet list with "- " bullets**: Capture the key specific points the reviewer should keep in view
175
+ - **Connect the dots**: Make the overview and bullets feel like one coherent review, not disconnected fragments
176
+ - **Draw on reviewer summaries**: Use these as evidence for your own synthesis — integrate their insights into the overview and bullets rather than listing reviewer-by-reviewer conclusions.
177
+ - **After the bullets, add extra sentences or short paragraphs only when needed**: Use them for requested context, caveats, or follow-up detail
177
178
  - **Write as a single reviewer**: Do not mention consolidation, merging, or multiple reviewers -- unless specifically requested
178
179
  </section>
179
180
 
@@ -208,7 +209,7 @@ Output JSON with this structure:
208
209
  "confidence": 0.0-1.0,
209
210
  "reasoning": ["Step-by-step reasoning explaining why this issue was flagged"]
210
211
  }],
211
- "summary": "Single cohesive paragraph synthesizing key findings and their significance. Write as a single reviewer — do not mention consolidation, merging, or multiple reviewers."
212
+ "summary": "Formatted markdown summary following the Summary Synthesis Guidance above."
212
213
  }
213
214
 
214
215
  ### GitHub Suggestion Syntax
@@ -121,6 +121,16 @@ Prioritize suggestions in this order:
121
121
  - **Provide context** for why each suggestion matters to the reviewer
122
122
  </section>
123
123
 
124
+ <section name="summary-synthesis" required="true">
125
+ ## Summary Structure
126
+ The summary field should synthesize the findings, but it should not be one big paragraph.
127
+ - Start with 1-2 sentences describing the overall assessment
128
+ - Then include a markdown bullet list using "- " bullets for the key specific points
129
+ - After the bullets, add extra sentences or short paragraphs only when needed for requested context or follow-up detail
130
+ - Focus on WHAT was found, not HOW it was found
131
+ - Write as if a single reviewer produced this analysis
132
+ </section>
133
+
124
134
  <section name="output-schema" locked="true">
125
135
  ## Output Format
126
136
 
@@ -151,7 +161,7 @@ Output JSON with this structure:
151
161
  "confidence": 0.0-1.0,
152
162
  "reasoning": ["Step-by-step reasoning explaining why this issue was flagged (optional)"]
153
163
  }],
154
- "summary": "Brief summary of the key findings and their significance to the reviewer. Focus on WHAT was found, not HOW it was found. Do NOT mention 'orchestration', 'levels', 'merged from Level 1/2/3' etc. Write as if a single reviewer produced this analysis."
164
+ "summary": "Formatted markdown summary following the Summary Structure guidance above."
155
165
  }
156
166
 
157
167
  ### GitHub Suggestion Syntax
@@ -218,6 +228,7 @@ const sections = [
218
228
  { name: 'priority-curation', required: true },
219
229
  { name: 'balanced-output', required: true },
220
230
  { name: 'human-centric-framing', required: true },
231
+ { name: 'summary-synthesis', required: true },
221
232
  { name: 'output-schema', locked: true },
222
233
  { name: 'diff-instructions', required: true },
223
234
  { name: 'file-level-guidance', optional: true, tier: ['balanced', 'thorough'] },
@@ -240,6 +251,7 @@ const defaultOrder = [
240
251
  'priority-curation',
241
252
  'balanced-output',
242
253
  'human-centric-framing',
254
+ 'summary-synthesis',
243
255
  'output-schema',
244
256
  'diff-instructions',
245
257
  'file-level-guidance',
@@ -103,6 +103,15 @@ Max 2-3 praise items. Prefer line-level over file-level. Include actionable sugg
103
103
  Use "Consider...", "Worth noting..." - guidance not mandates.
104
104
  </section>
105
105
 
106
+ <section name="summary-synthesis" required="true" tier="fast">
107
+ ### Summary
108
+ The summary field should be markdown, not one big paragraph.
109
+ - Start with 1-2 sentences describing the overall assessment
110
+ - Then include a markdown bullet list using "- " bullets for the key specific points
111
+ - After the bullets, add extra sentences or short paragraphs only when needed for requested context or follow-up detail
112
+ - Write as a single reviewer; do not mention levels or orchestration
113
+ </section>
114
+
106
115
  <section name="output-schema" locked="true">
107
116
  ## JSON Schema
108
117
  {
@@ -127,7 +136,7 @@ Use "Consider...", "Worth noting..." - guidance not mandates.
127
136
  "suggestion": "How to fix (omit for praise)",
128
137
  "confidence": 0.0-1.0
129
138
  }],
130
- "summary": "Key findings as if from single reviewer (no mention of levels/orchestration)"
139
+ "summary": "Formatted markdown summary following the Summary guidance above."
131
140
  }
132
141
 
133
142
  ### GitHub Suggestion Syntax
@@ -167,6 +176,7 @@ const sections = [
167
176
  { name: 'priority-curation', required: true, tier: ['fast'] },
168
177
  { name: 'balanced-output', required: true, tier: ['fast'] },
169
178
  { name: 'human-centric-framing', required: true, tier: ['fast'] },
179
+ { name: 'summary-synthesis', required: true, tier: ['fast'] },
170
180
  { name: 'output-schema', locked: true },
171
181
  { name: 'diff-instructions', required: true, tier: ['fast'] },
172
182
  { name: 'guidelines', required: true, tier: ['fast'] }
@@ -189,6 +199,7 @@ const defaultOrder = [
189
199
  'priority-curation',
190
200
  'balanced-output',
191
201
  'human-centric-framing',
202
+ 'summary-synthesis',
192
203
  'output-schema',
193
204
  'diff-instructions',
194
205
  'guidelines'
@@ -248,20 +248,21 @@ Note: Confidence is about certainty of value, not severity. A minor improvement
248
248
 
249
249
  <section name="summary-synthesis" required="true" tier="thorough">
250
250
  ## Summary Synthesis Guidance
251
- The summary field is not a list of findings - it's a synthesis that helps the reviewer see the forest, not just the trees.
251
+ The summary field should help the reviewer see the forest, not just the trees, but it should not be one big paragraph.
252
252
 
253
253
  **Effective Summary Approach:**
254
- - **Synthesize, don't summarize**: Identify the overarching narrative of this PR's quality and concerns
255
- - **Lead with the most important insight**: What single thing should the reviewer understand first?
256
- - **Connect the dots**: How do individual findings relate to each other or to a common theme?
257
- - **Calibrate severity**: Is this PR fundamentally sound with minor issues, or does it have structural problems?
254
+ - **Start with 1-2 sentences of overall assessment**: Identify the overarching narrative of this PR's quality and concerns
255
+ - **Then use a markdown bullet list with "- " bullets**: Capture the key specific points the reviewer should track
256
+ - **Connect the dots**: Make the overview and bullets feel like one coherent review
257
+ - **Calibrate severity**: Make clear whether this PR is fundamentally sound with minor issues or has structural problems
258
+ - **After the bullets, add extra sentences or short paragraphs only when needed**: Use them for requested context, caveats, or follow-up detail
258
259
  - **Respect reviewer time**: A good summary lets the reviewer decide where to focus attention
259
260
 
260
261
  **Summary Anti-patterns to Avoid:**
261
262
  - Listing findings ("Found 3 bugs, 2 improvements, 1 praise...")
262
263
  - Implementation details ("Merged Level 1 and Level 2 suggestions...")
263
264
  - Vague platitudes ("This PR has some issues to consider...")
264
- - Excessive length (2-3 sentences is ideal)
265
+ - A single unbroken paragraph with no bullets
265
266
  </section>
266
267
 
267
268
  <section name="output-schema" locked="true">
@@ -294,7 +295,7 @@ Output JSON with this structure:
294
295
  "confidence": 0.0-1.0,
295
296
  "reasoning": ["Step-by-step reasoning explaining why this issue was flagged"]
296
297
  }],
297
- "summary": "Brief summary of the key findings and their significance to the reviewer. Focus on WHAT was found, not HOW it was found. Do NOT mention 'orchestration', 'levels', 'merged from Level 1/2/3' etc. Write as if a single reviewer produced this analysis."
298
+ "summary": "Formatted markdown summary following the Summary Synthesis Guidance above."
298
299
  }
299
300
 
300
301
  ### GitHub Suggestion Syntax
@@ -671,10 +671,11 @@ function getAllProvidersInfo() {
671
671
  * Create a provider instance
672
672
  * @param {string} providerId - Provider ID (e.g., 'claude', 'gemini')
673
673
  * @param {string} model - Model to use (optional, uses default if not specified)
674
+ * @param {Object} overrides - Per-call config overrides that supersede global providerConfigOverrides (optional)
674
675
  * @returns {AIProvider}
675
676
  * @throws {Error} If provider is not registered
676
677
  */
677
- function createProvider(providerId, model = null) {
678
+ function createProvider(providerId, model = null, overrides = {}) {
678
679
  const ProviderClass = providerRegistry.get(providerId);
679
680
 
680
681
  if (!ProviderClass) {
@@ -683,7 +684,7 @@ function createProvider(providerId, model = null) {
683
684
  }
684
685
 
685
686
  // Get config overrides for this provider
686
- const overrides = providerConfigOverrides.get(providerId);
687
+ const configOverrides = providerConfigOverrides.get(providerId);
687
688
 
688
689
  // Determine the actual model to use
689
690
  let actualModel = model;
@@ -691,8 +692,8 @@ function createProvider(providerId, model = null) {
691
692
  // Resolve default from merged models (config + built-in).
692
693
  // Checks both sources because some providers (e.g., Pi) define built-in
693
694
  // modes with default:true that aren't in config overrides.
694
- if (overrides?.models || ProviderClass.getModels().length > 0) {
695
- actualModel = resolveDefaultModel(mergeModels(ProviderClass.getModels(), overrides?.models));
695
+ if (configOverrides?.models || ProviderClass.getModels().length > 0) {
696
+ actualModel = resolveDefaultModel(mergeModels(ProviderClass.getModels(), configOverrides?.models));
696
697
  }
697
698
  // Fall back to provider's built-in default
698
699
  if (!actualModel) {
@@ -700,8 +701,8 @@ function createProvider(providerId, model = null) {
700
701
  }
701
702
  }
702
703
 
703
- // Create provider instance with config overrides
704
- return new ProviderClass(actualModel, { ...(overrides || {}), yolo: yoloMode });
704
+ // Create provider instance with config overrides, per-call overrides, and yolo mode
705
+ return new ProviderClass(actualModel, { ...(configOverrides || {}), ...overrides, yolo: yoloMode });
705
706
  }
706
707
 
707
708
  /**
@@ -43,7 +43,7 @@ class ChatSessionManager {
43
43
  * @param {string} [options.initialContext] - Initial context to prepend to the first user message
44
44
  * @returns {Promise<{id: number, status: string}>}
45
45
  */
46
- async createSession({ provider, model, reviewId, contextCommentId, systemPrompt, cwd, initialContext }) {
46
+ async createSession({ provider, model, reviewId, contextCommentId, systemPrompt, cwd, initialContext, loadSkills }) {
47
47
  // Resolve provider definition once — used for model fallback and bridge construction
48
48
  const providerDef = getChatProvider(provider);
49
49
 
@@ -72,6 +72,7 @@ class ChatSessionManager {
72
72
  model: resolvedModel,
73
73
  cwd,
74
74
  systemPrompt,
75
+ loadSkills,
75
76
  }, providerDef);
76
77
 
77
78
  const listeners = {
@@ -413,9 +414,10 @@ class ChatSessionManager {
413
414
  * @param {Object} options
414
415
  * @param {string} [options.systemPrompt] - System prompt text
415
416
  * @param {string} [options.cwd] - Working directory for agent
417
+ * @param {boolean} [options.loadSkills] - Resolved load_skills override for the session
416
418
  * @returns {Promise<{id: number, status: string}>}
417
419
  */
418
- async resumeSession(sessionId, { systemPrompt, cwd } = {}) {
420
+ async resumeSession(sessionId, { systemPrompt, cwd, loadSkills } = {}) {
419
421
  // Already active — return immediately
420
422
  if (this._sessions.has(sessionId)) {
421
423
  return { id: sessionId, status: 'active' };
@@ -459,6 +461,7 @@ class ChatSessionManager {
459
461
  model: row.model,
460
462
  cwd,
461
463
  systemPrompt,
464
+ loadSkills,
462
465
  ...resumeOptions,
463
466
  });
464
467
 
@@ -588,7 +591,7 @@ class ChatSessionManager {
588
591
  useShell: def?.useShell,
589
592
  tools: CHAT_TOOLS,
590
593
  extensions: appExtensions ? [taskExtensionDir] : [],
591
- loadSkills: def?.load_skills,
594
+ loadSkills: options.loadSkills ?? def?.load_skills,
592
595
  });
593
596
  }
594
597
 
package/src/config.js CHANGED
@@ -34,7 +34,7 @@ const DEFAULT_CONFIG = {
34
34
  chat: { enable_shortcuts: true, enter_to_send: true }, // Chat panel settings (enable_shortcuts: show action shortcut buttons, enter_to_send: Enter sends message instead of newline)
35
35
  providers: {}, // Custom AI analysis provider configurations (overrides built-in defaults)
36
36
  chat_providers: {}, // Custom chat provider configurations (overrides built-in defaults)
37
- monorepos: {}, // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
37
+ repos: {}, // Repository configurations: { "owner/repo": { path: "~/path/to/clone" } }
38
38
  assisted_by_url: "https://github.com/in-the-loop-labs/pair-review", // URL for "Review assisted by" footer link
39
39
  hooks: {}, // Hook commands per event: { "review.started": { "my_hook": { "command": "..." } } }
40
40
  enable_graphite: false, // When true, shows Graphite links alongside GitHub links
@@ -221,6 +221,20 @@ async function loadConfig() {
221
221
  }
222
222
  }
223
223
 
224
+ // Normalize legacy monorepos key into repos (monorepos values are overridden by repos)
225
+ if (mergedConfig.monorepos) {
226
+ mergedConfig.repos = deepMerge(mergedConfig.monorepos, mergedConfig.repos);
227
+ }
228
+
229
+ // Normalize repo keys to lowercase to match the database's COLLATE NOCASE identity
230
+ if (mergedConfig.repos) {
231
+ const normalized = {};
232
+ for (const [key, value] of Object.entries(mergedConfig.repos)) {
233
+ normalized[key.toLowerCase()] = value;
234
+ }
235
+ mergedConfig.repos = normalized;
236
+ }
237
+
224
238
  // Validate port
225
239
  if (!validatePort(mergedConfig.port)) {
226
240
  console.error(`Invalid port number ${mergedConfig.port}`);
@@ -393,83 +407,248 @@ function expandPath(p) {
393
407
  }
394
408
 
395
409
  /**
396
- * Gets the configured monorepo path for a repository
410
+ * Get repository configuration, checking `repos` key first, falling back to `monorepos`.
411
+ * @param {object} config
412
+ * @param {string} repository - owner/repo
413
+ * @returns {object|null}
414
+ */
415
+ function getRepoConfig(config, repository) {
416
+ const reposSection = config.repos || {};
417
+ const entry = reposSection[repository];
418
+ if (entry) return entry;
419
+
420
+ const legacySection = config.monorepos || {};
421
+ return legacySection[repository] || null;
422
+ }
423
+
424
+ /**
425
+ * Gets the configured repository path
397
426
  * @param {Object} config - Configuration object from loadConfig()
398
427
  * @param {string} repository - Repository in "owner/repo" format
399
428
  * @returns {string|null} - Expanded path or null if not configured
400
429
  */
401
- function getMonorepoPath(config, repository) {
402
- const monorepoConfig = config.monorepos?.[repository];
403
- if (monorepoConfig?.path) {
404
- return expandPath(monorepoConfig.path);
430
+ function getRepoPath(config, repository) {
431
+ const repoConfig = getRepoConfig(config, repository);
432
+ if (repoConfig?.path) {
433
+ return expandPath(repoConfig.path);
405
434
  }
406
435
  return null;
407
436
  }
408
437
 
409
438
  /**
410
- * Gets the configured checkout script for a monorepo repository
439
+ * Gets the configured checkout script for a repository
411
440
  * @param {Object} config - Configuration object from loadConfig()
412
441
  * @param {string} repository - Repository in "owner/repo" format
413
442
  * @returns {string|null} - Checkout script path or null if not configured
414
443
  */
415
- function getMonorepoCheckoutScript(config, repository) {
416
- const monorepoConfig = config.monorepos?.[repository];
417
- return monorepoConfig?.checkout_script || null;
444
+ function getRepoCheckoutScript(config, repository) {
445
+ const repoConfig = getRepoConfig(config, repository);
446
+ return repoConfig?.checkout_script || null;
418
447
  }
419
448
 
420
449
  /**
421
- * Gets the configured worktree directory for a monorepo repository
450
+ * Gets the configured worktree directory for a repository
422
451
  * @param {Object} config - Configuration object from loadConfig()
423
452
  * @param {string} repository - Repository in "owner/repo" format
424
453
  * @returns {string|null} - Expanded worktree directory path or null if not configured
425
454
  */
426
- function getMonorepoWorktreeDirectory(config, repository) {
427
- const monorepoConfig = config.monorepos?.[repository];
428
- if (monorepoConfig?.worktree_directory) {
429
- return expandPath(monorepoConfig.worktree_directory);
455
+ function getRepoWorktreeDirectory(config, repository) {
456
+ const repoConfig = getRepoConfig(config, repository);
457
+ if (repoConfig?.worktree_directory) {
458
+ return expandPath(repoConfig.worktree_directory);
430
459
  }
431
460
  return null;
432
461
  }
433
462
 
434
463
  /**
435
- * Gets the configured worktree name template for a monorepo repository
464
+ * Gets the configured worktree name template for a repository
436
465
  * @param {Object} config - Configuration object from loadConfig()
437
466
  * @param {string} repository - Repository in "owner/repo" format
438
467
  * @returns {string|null} - Template string or null if not configured
439
468
  */
440
- function getMonorepoWorktreeNameTemplate(config, repository) {
441
- const monorepoConfig = config.monorepos?.[repository];
442
- return monorepoConfig?.worktree_name_template || null;
469
+ function getRepoWorktreeNameTemplate(config, repository) {
470
+ const repoConfig = getRepoConfig(config, repository);
471
+ return repoConfig?.worktree_name_template || null;
443
472
  }
444
473
 
445
474
  /**
446
- * Gets the configured checkout script timeout for a monorepo repository
475
+ * Computes the display name for a worktree path by deriving the relative
476
+ * path from the configured (or default) worktree base directory.
477
+ * Falls back to the basename when the path lies outside the base directory.
478
+ *
479
+ * @param {string} worktreePath - Absolute path to the worktree
480
+ * @param {Object} config - Configuration object from loadConfig()
481
+ * @param {string} repository - Repository in "owner/repo" format
482
+ * @returns {string|null} - Relative display name (e.g. "abc123/src") or basename fallback
483
+ */
484
+ function getWorktreeDisplayName(worktreePath, config, repository) {
485
+ if (!worktreePath) return null;
486
+ const worktreeBaseDir = getRepoWorktreeDirectory(config, repository)
487
+ || path.join(getConfigDir(), 'worktrees');
488
+ const relativePath = path.relative(worktreeBaseDir, worktreePath);
489
+ if (relativePath.startsWith('..')) {
490
+ return path.basename(worktreePath);
491
+ }
492
+ return relativePath;
493
+ }
494
+
495
+ /**
496
+ * Gets the configured checkout script timeout for a repository
447
497
  * @param {Object} config - Configuration object from loadConfig()
448
498
  * @param {string} repository - Repository in "owner/repo" format
449
499
  * @returns {number} - Timeout in milliseconds (default: 300000 = 5 minutes)
450
500
  */
451
- function getMonorepoCheckoutTimeout(config, repository) {
452
- const monorepoConfig = config.monorepos?.[repository];
453
- if (monorepoConfig?.checkout_timeout_seconds > 0) {
454
- return monorepoConfig.checkout_timeout_seconds * 1000;
501
+ function getRepoCheckoutTimeout(config, repository) {
502
+ const repoConfig = getRepoConfig(config, repository);
503
+ if (repoConfig?.checkout_timeout_seconds > 0) {
504
+ return repoConfig.checkout_timeout_seconds * 1000;
455
505
  }
456
506
  return DEFAULT_CHECKOUT_TIMEOUT_MS; // 5 minutes default
457
507
  }
458
508
 
459
509
  /**
460
- * Resolves all monorepo worktree options for a repository into a single object.
510
+ * Gets the configured reset script for a repository
511
+ * @param {Object} config - Configuration object from loadConfig()
512
+ * @param {string} repository - Repository in "owner/repo" format
513
+ * @returns {string|null} - Reset script path or null if not configured
514
+ */
515
+ function getRepoResetScript(config, repository) {
516
+ const repoConfig = getRepoConfig(config, repository);
517
+ return repoConfig?.reset_script || null;
518
+ }
519
+
520
+ /**
521
+ * Gets the configured pool size for a repository from file config only.
522
+ * Prefer resolvePoolConfig() when DB repo_settings are available.
523
+ * @param {Object} config - Configuration object from loadConfig()
524
+ * @param {string} repository - Repository in "owner/repo" format
525
+ * @returns {number} - Pool size (0 if not configured or invalid)
526
+ */
527
+ function getRepoPoolSize(config, repository) {
528
+ const repoConfig = getRepoConfig(config, repository);
529
+ const size = repoConfig?.pool_size;
530
+ return (typeof size === 'number' && size > 0) ? size : 0;
531
+ }
532
+
533
+ /**
534
+ * Gets the configured pool fetch interval for a repository from file config only.
535
+ * Prefer resolvePoolConfig() when DB repo_settings are available.
536
+ * @param {Object} config - Configuration object from loadConfig()
537
+ * @param {string} repository - Repository in "owner/repo" format
538
+ * @returns {number|null} - Interval in minutes or null if not configured
539
+ */
540
+ function getRepoPoolFetchInterval(config, repository) {
541
+ const repoConfig = getRepoConfig(config, repository);
542
+ const minutes = repoConfig?.pool_fetch_interval_minutes;
543
+ return (typeof minutes === 'number' && minutes > 0) ? minutes : null;
544
+ }
545
+
546
+ /**
547
+ * Gets the configured load_skills setting for a repository from file config.
548
+ * @param {Object} config - Configuration object from loadConfig()
549
+ * @param {string} repository - Repository in "owner/repo" format
550
+ * @returns {boolean|null} - true/false if set, null if not configured
551
+ */
552
+ function getRepoLoadSkills(config, repository) {
553
+ const repoConfig = getRepoConfig(config, repository);
554
+ const val = repoConfig?.load_skills;
555
+ return typeof val === 'boolean' ? val : null;
556
+ }
557
+
558
+ /**
559
+ * Resolves the load_skills setting for a repository, checking DB repo_settings first,
560
+ * then repo JSON config, then provider config. Returns a boolean suitable for passing
561
+ * directly to provider constructors (which check `!== false`).
562
+ *
563
+ * @param {Object} config - Configuration object from loadConfig()
564
+ * @param {string} repository - Repository in "owner/repo" format
565
+ * @param {Object|null} repoSettings - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
566
+ * @param {boolean} [providerLoadSkills] - Provider-level load_skills from config.providers
567
+ * @returns {boolean} - Resolved load_skills value
568
+ */
569
+ function resolveLoadSkills(config, repository, repoSettings, providerLoadSkills) {
570
+ // Tier 1: DB repo settings (1 = true, 0 = false, null = not set)
571
+ const dbVal = repoSettings?.load_skills;
572
+ if (typeof dbVal === 'number' && (dbVal === 0 || dbVal === 1)) {
573
+ return dbVal === 1;
574
+ }
575
+
576
+ // Tier 2: Repo JSON config (config.repos["owner/repo"].load_skills)
577
+ const repoVal = getRepoLoadSkills(config, repository);
578
+ if (repoVal !== null) {
579
+ return repoVal;
580
+ }
581
+
582
+ // Tier 3: Provider-level config
583
+ if (typeof providerLoadSkills === 'boolean') {
584
+ return providerLoadSkills;
585
+ }
586
+
587
+ // Tier 4: Default
588
+ return true;
589
+ }
590
+
591
+ /**
592
+ * Builds council-mode provider overrides: a shared (tier 1+2) base and a per-provider
593
+ * map that includes tier 3 resolution for each configured provider.
594
+ *
595
+ * @param {Object} config - Configuration object from loadConfig()
596
+ * @param {string} repository - Repository in "owner/repo" format
597
+ * @param {Object|null} repoSettings - DB repo_settings row
598
+ * @returns {{ providerOverrides: Object, providerOverridesMap: Object }}
599
+ */
600
+ function buildCouncilProviderOverrides(config, repository, repoSettings) {
601
+ const baseLoadSkills = resolveLoadSkills(config, repository, repoSettings);
602
+ const providerOverrides = { load_skills: baseLoadSkills };
603
+ const providerOverridesMap = {};
604
+ if (config.providers) {
605
+ for (const [pid, pconf] of Object.entries(config.providers)) {
606
+ providerOverridesMap[pid] = {
607
+ load_skills: resolveLoadSkills(config, repository, repoSettings, pconf?.load_skills)
608
+ };
609
+ }
610
+ }
611
+ return { providerOverrides, providerOverridesMap };
612
+ }
613
+
614
+ /**
615
+ * Resolves pool configuration for a repository, checking DB repo_settings first,
616
+ * then falling back to file config. DB values take precedence when set (non-null).
617
+ * @param {Object} config - Configuration object from loadConfig()
618
+ * @param {string} repository - Repository in "owner/repo" format
619
+ * @param {Object|null} repoSettings - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
620
+ * @returns {{ poolSize: number, poolFetchIntervalMinutes: number|null }}
621
+ */
622
+ function resolvePoolConfig(config, repository, repoSettings) {
623
+ const dbPoolSize = repoSettings?.pool_size;
624
+ const dbFetchInterval = repoSettings?.pool_fetch_interval_minutes;
625
+
626
+ const poolSize = (typeof dbPoolSize === 'number' && dbPoolSize >= 0)
627
+ ? dbPoolSize
628
+ : getRepoPoolSize(config, repository);
629
+
630
+ const poolFetchIntervalMinutes = (typeof dbFetchInterval === 'number' && dbFetchInterval >= 0)
631
+ ? (dbFetchInterval > 0 ? dbFetchInterval : null)
632
+ : getRepoPoolFetchInterval(config, repository);
633
+
634
+ return { poolSize, poolFetchIntervalMinutes };
635
+ }
636
+
637
+ /**
638
+ * Resolves all repository worktree options into a single object.
461
639
  * Composite helper that combines the individual getters into the shape expected
462
640
  * by GitWorktreeManager and createWorktreeForPR.
463
641
  *
464
642
  * @param {Object} config - Configuration object from loadConfig()
465
643
  * @param {string} repository - Repository in "owner/repo" format
466
- * @returns {{ checkoutScript: string|null, checkoutTimeout: number, worktreeConfig: Object|null }}
644
+ * @param {Object|null} [repoSettings=null] - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
645
+ * @returns {{ checkoutScript: string|null, checkoutTimeout: number, worktreeConfig: Object|null, resetScript: string|null, poolSize: number, poolFetchIntervalMinutes: number|null }}
467
646
  */
468
- function resolveMonorepoOptions(config, repository) {
469
- const checkoutScript = getMonorepoCheckoutScript(config, repository);
470
- const checkoutTimeout = getMonorepoCheckoutTimeout(config, repository);
471
- const worktreeDirectory = getMonorepoWorktreeDirectory(config, repository);
472
- const nameTemplate = getMonorepoWorktreeNameTemplate(config, repository);
647
+ function resolveRepoOptions(config, repository, repoSettings = null) {
648
+ const checkoutScript = getRepoCheckoutScript(config, repository);
649
+ const checkoutTimeout = getRepoCheckoutTimeout(config, repository);
650
+ const worktreeDirectory = getRepoWorktreeDirectory(config, repository);
651
+ const nameTemplate = getRepoWorktreeNameTemplate(config, repository);
473
652
 
474
653
  let worktreeConfig = null;
475
654
  if (worktreeDirectory || nameTemplate) {
@@ -478,7 +657,10 @@ function resolveMonorepoOptions(config, repository) {
478
657
  if (nameTemplate) worktreeConfig.nameTemplate = nameTemplate;
479
658
  }
480
659
 
481
- return { checkoutScript, checkoutTimeout, worktreeConfig };
660
+ const resetScript = getRepoResetScript(config, repository);
661
+ const { poolSize, poolFetchIntervalMinutes } = resolvePoolConfig(config, repository, repoSettings);
662
+
663
+ return { checkoutScript, checkoutTimeout, worktreeConfig, resetScript, poolSize, poolFetchIntervalMinutes };
482
664
  }
483
665
 
484
666
  /**
@@ -558,12 +740,22 @@ module.exports = {
558
740
  isRunningViaNpx,
559
741
  showWelcomeMessage,
560
742
  expandPath,
561
- getMonorepoPath,
562
- getMonorepoCheckoutScript,
563
- getMonorepoWorktreeDirectory,
564
- getMonorepoWorktreeNameTemplate,
565
- getMonorepoCheckoutTimeout,
566
- resolveMonorepoOptions,
743
+ // New repo-prefixed names
744
+ getRepoConfig,
745
+ getRepoPath,
746
+ getRepoCheckoutScript,
747
+ getRepoWorktreeDirectory,
748
+ getRepoWorktreeNameTemplate,
749
+ getWorktreeDisplayName,
750
+ getRepoCheckoutTimeout,
751
+ resolveRepoOptions,
752
+ getRepoResetScript,
753
+ getRepoPoolSize,
754
+ getRepoPoolFetchInterval,
755
+ getRepoLoadSkills,
756
+ resolvePoolConfig,
757
+ resolveLoadSkills,
758
+ buildCouncilProviderOverrides,
567
759
  resolveDbName,
568
760
  warnIfDevModeWithoutDbName,
569
761
  shouldSkipUpdateNotifier,