@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.
- package/README.md +7 -6
- package/package.json +5 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +8 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +8 -7
- package/public/css/repo-settings.css +347 -0
- package/public/index.html +46 -9
- package/public/js/components/AIPanel.js +79 -37
- package/public/js/components/DiffOptionsDropdown.js +84 -1
- package/public/js/index.js +31 -6
- package/public/js/modules/analysis-history.js +11 -7
- package/public/js/pr.js +22 -0
- package/public/js/repo-settings.js +334 -6
- package/public/repo-settings.html +29 -0
- package/src/ai/analyzer.js +28 -19
- package/src/ai/claude-cli.js +2 -0
- package/src/ai/claude-provider.js +4 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +6 -4
- package/src/ai/prompts/baseline/consolidation/fast.js +6 -2
- package/src/ai/prompts/baseline/consolidation/thorough.js +7 -6
- package/src/ai/prompts/baseline/orchestration/balanced.js +13 -1
- package/src/ai/prompts/baseline/orchestration/fast.js +12 -1
- package/src/ai/prompts/baseline/orchestration/thorough.js +8 -7
- package/src/ai/provider.js +7 -6
- package/src/chat/session-manager.js +6 -3
- package/src/config.js +230 -38
- package/src/database.js +766 -38
- package/src/git/worktree-pool-lifecycle.js +674 -0
- package/src/git/worktree-pool-usage.js +216 -0
- package/src/git/worktree.js +46 -13
- package/src/main.js +185 -26
- package/src/routes/analyses.js +48 -26
- package/src/routes/chat.js +27 -3
- package/src/routes/config.js +17 -5
- package/src/routes/executable-analysis.js +38 -19
- package/src/routes/local.js +19 -6
- package/src/routes/mcp.js +13 -2
- package/src/routes/pr.js +72 -29
- package/src/routes/setup.js +41 -4
- package/src/routes/stack-analysis.js +29 -10
- package/src/routes/worktrees.js +294 -9
- package/src/server.js +20 -3
- package/src/setup/pr-setup.js +161 -27
- 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
|
|
127
|
-
-
|
|
128
|
-
-
|
|
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": "
|
|
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.
|
|
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": "
|
|
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
|
|
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
|
|
174
|
-
- **
|
|
175
|
-
- **
|
|
176
|
-
- **Draw on reviewer summaries**: Use these as evidence for your own synthesis — integrate their insights into
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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
|
-
- **
|
|
255
|
-
- **
|
|
256
|
-
- **Connect the dots**:
|
|
257
|
-
- **Calibrate severity**:
|
|
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
|
-
-
|
|
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": "
|
|
298
|
+
"summary": "Formatted markdown summary following the Summary Synthesis Guidance above."
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
### GitHub Suggestion Syntax
|
package/src/ai/provider.js
CHANGED
|
@@ -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
|
|
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 (
|
|
695
|
-
actualModel = resolveDefaultModel(mergeModels(ProviderClass.getModels(),
|
|
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, { ...(
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
404
|
-
return expandPath(
|
|
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
|
|
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
|
|
416
|
-
const
|
|
417
|
-
return
|
|
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
|
|
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
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
return expandPath(
|
|
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
|
|
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
|
|
441
|
-
const
|
|
442
|
-
return
|
|
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
|
-
*
|
|
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
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
454
|
-
return
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
469
|
-
const checkoutScript =
|
|
470
|
-
const checkoutTimeout =
|
|
471
|
-
const worktreeDirectory =
|
|
472
|
-
const nameTemplate =
|
|
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
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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,
|