@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
|
@@ -14,6 +14,7 @@ class RepoSettingsPage {
|
|
|
14
14
|
this.providers = {};
|
|
15
15
|
this.selectedProvider = null;
|
|
16
16
|
this.councils = [];
|
|
17
|
+
this.worktreeData = null;
|
|
17
18
|
|
|
18
19
|
this.init();
|
|
19
20
|
}
|
|
@@ -39,6 +40,9 @@ class RepoSettingsPage {
|
|
|
39
40
|
|
|
40
41
|
// Load settings
|
|
41
42
|
await this.loadSettings();
|
|
43
|
+
|
|
44
|
+
// Load worktrees (non-blocking, section stays hidden on error)
|
|
45
|
+
await this.loadWorktrees();
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
/**
|
|
@@ -210,6 +214,16 @@ class RepoSettingsPage {
|
|
|
210
214
|
});
|
|
211
215
|
}
|
|
212
216
|
|
|
217
|
+
// Load skills select
|
|
218
|
+
const loadSkillsSelect = document.getElementById('load-skills-select');
|
|
219
|
+
if (loadSkillsSelect) {
|
|
220
|
+
loadSkillsSelect.addEventListener('change', () => {
|
|
221
|
+
const val = loadSkillsSelect.value;
|
|
222
|
+
this.currentSettings.load_skills = val === '' ? null : parseInt(val, 10);
|
|
223
|
+
this.checkForChanges();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
213
227
|
// Analysis mode segmented control
|
|
214
228
|
const modeToggle = document.getElementById('analysis-mode-toggle');
|
|
215
229
|
if (modeToggle) {
|
|
@@ -249,6 +263,22 @@ class RepoSettingsPage {
|
|
|
249
263
|
clearLocalPathBtn.addEventListener('click', () => this.handleClearLocalPath());
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
// Worktree actions (delegated)
|
|
267
|
+
const worktreesContent = document.getElementById('worktrees-content');
|
|
268
|
+
if (worktreesContent) {
|
|
269
|
+
worktreesContent.addEventListener('click', (e) => {
|
|
270
|
+
const deleteBtn = e.target.closest('.worktree-delete-btn');
|
|
271
|
+
if (deleteBtn) {
|
|
272
|
+
this.deleteWorktree(deleteBtn.dataset.worktreeId);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const deleteAllBtn = e.target.closest('.worktree-delete-all-btn');
|
|
276
|
+
if (deleteAllBtn) {
|
|
277
|
+
this.deleteAllWorktrees();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
252
282
|
// Warn before leaving with unsaved changes
|
|
253
283
|
window.addEventListener('beforeunload', (e) => {
|
|
254
284
|
if (this.hasUnsavedChanges) {
|
|
@@ -825,7 +855,10 @@ class RepoSettingsPage {
|
|
|
825
855
|
default_council_id: settings.default_council_id || null,
|
|
826
856
|
default_instructions: settings.default_instructions || '',
|
|
827
857
|
local_path: settings.local_path || null,
|
|
828
|
-
default_chat_instructions: settings.default_chat_instructions || ''
|
|
858
|
+
default_chat_instructions: settings.default_chat_instructions || '',
|
|
859
|
+
pool_size: settings.pool_size ?? null,
|
|
860
|
+
pool_fetch_interval_minutes: settings.pool_fetch_interval_minutes ?? null,
|
|
861
|
+
load_skills: settings.load_skills ?? null
|
|
829
862
|
};
|
|
830
863
|
|
|
831
864
|
// Set current settings
|
|
@@ -844,13 +877,279 @@ class RepoSettingsPage {
|
|
|
844
877
|
default_council_id: null,
|
|
845
878
|
default_instructions: '',
|
|
846
879
|
local_path: null,
|
|
847
|
-
default_chat_instructions: ''
|
|
880
|
+
default_chat_instructions: '',
|
|
881
|
+
pool_size: null,
|
|
882
|
+
pool_fetch_interval_minutes: null,
|
|
883
|
+
load_skills: null
|
|
848
884
|
};
|
|
849
885
|
this.currentSettings = { ...this.originalSettings };
|
|
850
886
|
this.updateUI();
|
|
851
887
|
}
|
|
852
888
|
}
|
|
853
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Load worktree data for the current repository
|
|
892
|
+
*/
|
|
893
|
+
async loadWorktrees() {
|
|
894
|
+
if (!this.owner || !this.repo) return;
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/worktrees`);
|
|
898
|
+
if (!response.ok) {
|
|
899
|
+
throw new Error('Failed to load worktrees');
|
|
900
|
+
}
|
|
901
|
+
this.worktreeData = await response.json();
|
|
902
|
+
|
|
903
|
+
// Seed pool settings from resolved config values when DB has no override
|
|
904
|
+
const pool = this.worktreeData.pool || {};
|
|
905
|
+
if (this.originalSettings.pool_size == null && pool.size) {
|
|
906
|
+
this.originalSettings.pool_size = pool.size;
|
|
907
|
+
this.currentSettings.pool_size = pool.size;
|
|
908
|
+
}
|
|
909
|
+
if (this.originalSettings.pool_fetch_interval_minutes == null && pool.fetch_interval_minutes) {
|
|
910
|
+
this.originalSettings.pool_fetch_interval_minutes = pool.fetch_interval_minutes;
|
|
911
|
+
this.currentSettings.pool_fetch_interval_minutes = pool.fetch_interval_minutes;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
this.renderWorktrees();
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error('Error loading worktrees:', error);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Render the worktrees section content
|
|
922
|
+
*/
|
|
923
|
+
renderWorktrees() {
|
|
924
|
+
const section = document.getElementById('worktrees-section');
|
|
925
|
+
const content = document.getElementById('worktrees-content');
|
|
926
|
+
if (!section || !content) return;
|
|
927
|
+
|
|
928
|
+
if (!this.worktreeData) return;
|
|
929
|
+
|
|
930
|
+
section.style.display = '';
|
|
931
|
+
|
|
932
|
+
const pool = this.worktreeData.pool || {};
|
|
933
|
+
const worktrees = this.worktreeData.worktrees || [];
|
|
934
|
+
let html = '';
|
|
935
|
+
|
|
936
|
+
// Pool settings (editable)
|
|
937
|
+
const poolSizeValue = this.currentSettings.pool_size ?? '';
|
|
938
|
+
const fetchIntervalValue = this.currentSettings.pool_fetch_interval_minutes ?? '';
|
|
939
|
+
const currentCount = pool.current_count || 0;
|
|
940
|
+
const countNote = poolSizeValue ? ` (${currentCount} active)` : '';
|
|
941
|
+
|
|
942
|
+
html += `<div class="worktree-pool-config">
|
|
943
|
+
<div class="worktree-pool-config-items">
|
|
944
|
+
<div class="worktree-pool-config-item">
|
|
945
|
+
<label class="worktree-pool-config-label" for="pool-size-input">Pool Size</label>
|
|
946
|
+
<div class="worktree-pool-input-group">
|
|
947
|
+
<input type="number" id="pool-size-input" class="worktree-pool-input"
|
|
948
|
+
min="0" max="20" step="1" placeholder="0"
|
|
949
|
+
value="${this.escapeHtml(String(poolSizeValue))}">
|
|
950
|
+
<span class="worktree-pool-input-note">${this.escapeHtml(countNote)}</span>
|
|
951
|
+
</div>
|
|
952
|
+
</div>
|
|
953
|
+
<div class="worktree-pool-config-item">
|
|
954
|
+
<label class="worktree-pool-config-label" for="pool-fetch-interval-input">Fetch Interval</label>
|
|
955
|
+
<div class="worktree-pool-input-group">
|
|
956
|
+
<input type="number" id="pool-fetch-interval-input" class="worktree-pool-input"
|
|
957
|
+
min="0" max="1440" step="1" placeholder="Off"
|
|
958
|
+
value="${this.escapeHtml(String(fetchIntervalValue))}">
|
|
959
|
+
<span class="worktree-pool-input-note">minutes</span>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
<p class="worktree-pool-hint">
|
|
964
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"/></svg>
|
|
965
|
+
Pre-warms worktrees so PR reviews start instantly. Set size to 0 to disable.
|
|
966
|
+
</p>
|
|
967
|
+
</div>`;
|
|
968
|
+
|
|
969
|
+
// Worktree list
|
|
970
|
+
if (worktrees.length > 0) {
|
|
971
|
+
html += '<div class="worktree-list">';
|
|
972
|
+
for (const wt of worktrees) {
|
|
973
|
+
const badgeHtml = wt.is_pool
|
|
974
|
+
? '<span class="worktree-pool-badge">Pool</span>'
|
|
975
|
+
: '<span class="worktree-adhoc-badge">Ad-hoc</span>';
|
|
976
|
+
const prInfo = wt.pr_number ? '#' + wt.pr_number : 'Unassigned';
|
|
977
|
+
const branchInfo = wt.branch ? ' · ' + this.escapeHtml(wt.branch) : '';
|
|
978
|
+
const fullPath = wt.path || '';
|
|
979
|
+
const diskWarning = !wt.disk_exists
|
|
980
|
+
? '<span class="worktree-disk-warning">Missing from disk</span>'
|
|
981
|
+
: '';
|
|
982
|
+
const statusIcon = this.getWorktreeStatusIcon(wt.status);
|
|
983
|
+
const statusLabel = this.getWorktreeStatusLabel(wt.status);
|
|
984
|
+
const fetchedHtml = wt.last_fetched_at
|
|
985
|
+
? `<span class="worktree-timestamp">Fetched ${this.formatRelativeTime(wt.last_fetched_at)}</span>`
|
|
986
|
+
: '';
|
|
987
|
+
|
|
988
|
+
html += `<div class="worktree-item">
|
|
989
|
+
<div class="worktree-item-top">
|
|
990
|
+
<div class="worktree-item-left">
|
|
991
|
+
${badgeHtml}
|
|
992
|
+
<span class="worktree-pr-info">${prInfo}${branchInfo}</span>
|
|
993
|
+
${diskWarning}
|
|
994
|
+
</div>
|
|
995
|
+
<div class="worktree-item-right">
|
|
996
|
+
<span class="worktree-status worktree-status--${this.escapeHtml(wt.status || 'unknown')}">
|
|
997
|
+
${statusIcon} ${statusLabel}
|
|
998
|
+
</span>
|
|
999
|
+
${fetchedHtml}
|
|
1000
|
+
<button class="worktree-delete-btn" data-worktree-id="${this.escapeHtml(wt.id)}" title="Delete worktree">
|
|
1001
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1002
|
+
<path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19a1.75 1.75 0 001.741-1.575l.66-6.6a.75.75 0 00-1.492-.15l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"/>
|
|
1003
|
+
</svg>
|
|
1004
|
+
</button>
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div class="worktree-item-bottom">
|
|
1008
|
+
<span class="worktree-path">${this.escapeHtml(fullPath)}</span>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>`;
|
|
1011
|
+
}
|
|
1012
|
+
html += '</div>';
|
|
1013
|
+
|
|
1014
|
+
// Delete all button
|
|
1015
|
+
html += `<div class="worktree-actions">
|
|
1016
|
+
<button class="worktree-delete-all-btn">
|
|
1017
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1018
|
+
<path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19a1.75 1.75 0 001.741-1.575l.66-6.6a.75.75 0 00-1.492-.15l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"/>
|
|
1019
|
+
</svg>
|
|
1020
|
+
Delete All Worktrees
|
|
1021
|
+
</button>
|
|
1022
|
+
</div>`;
|
|
1023
|
+
} else {
|
|
1024
|
+
html += '<div class="worktree-empty">No worktrees found for this repository.</div>';
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
content.innerHTML = html;
|
|
1028
|
+
|
|
1029
|
+
// Wire up pool setting inputs
|
|
1030
|
+
const poolSizeInput = document.getElementById('pool-size-input');
|
|
1031
|
+
if (poolSizeInput) {
|
|
1032
|
+
poolSizeInput.addEventListener('input', () => {
|
|
1033
|
+
const val = poolSizeInput.value.trim();
|
|
1034
|
+
this.currentSettings.pool_size = val === '' ? null : parseInt(val, 10);
|
|
1035
|
+
this.checkForChanges();
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
const poolFetchInput = document.getElementById('pool-fetch-interval-input');
|
|
1039
|
+
if (poolFetchInput) {
|
|
1040
|
+
poolFetchInput.addEventListener('input', () => {
|
|
1041
|
+
const val = poolFetchInput.value.trim();
|
|
1042
|
+
this.currentSettings.pool_fetch_interval_minutes = val === '' ? null : parseInt(val, 10);
|
|
1043
|
+
this.checkForChanges();
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Get the SVG icon for a worktree status
|
|
1050
|
+
* @param {string} status
|
|
1051
|
+
* @returns {string} Inline SVG string
|
|
1052
|
+
*/
|
|
1053
|
+
getWorktreeStatusIcon(status) {
|
|
1054
|
+
switch (status) {
|
|
1055
|
+
case 'available':
|
|
1056
|
+
return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="4" fill="#2da44e"/></svg>';
|
|
1057
|
+
case 'in_use':
|
|
1058
|
+
return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"/></svg>';
|
|
1059
|
+
case 'switching':
|
|
1060
|
+
return '<svg class="worktree-icon-spin" width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2.5a5.5 5.5 0 00-5.23 3.79.75.75 0 01-1.42-.48A7.001 7.001 0 0115 8a7 7 0 01-13.65 2.19.75.75 0 011.42-.48A5.5 5.5 0 108 2.5z"/></svg>';
|
|
1061
|
+
case 'creating':
|
|
1062
|
+
return '<svg class="worktree-icon-spin" width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2.5a5.5 5.5 0 00-5.23 3.79.75.75 0 01-1.42-.48A7.001 7.001 0 0115 8a7 7 0 01-13.65 2.19.75.75 0 011.42-.48A5.5 5.5 0 108 2.5z"/></svg>';
|
|
1063
|
+
case 'active':
|
|
1064
|
+
return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="4" fill="#0969da"/></svg>';
|
|
1065
|
+
default:
|
|
1066
|
+
return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="4" fill="#8b949e"/></svg>';
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Get the display label for a worktree status
|
|
1072
|
+
* @param {string} status
|
|
1073
|
+
* @returns {string}
|
|
1074
|
+
*/
|
|
1075
|
+
getWorktreeStatusLabel(status) {
|
|
1076
|
+
switch (status) {
|
|
1077
|
+
case 'available': return 'Available';
|
|
1078
|
+
case 'in_use': return 'In use';
|
|
1079
|
+
case 'switching': return 'Switching';
|
|
1080
|
+
case 'creating': return 'Creating';
|
|
1081
|
+
case 'active': return 'Active';
|
|
1082
|
+
default: return status || 'Unknown';
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Format an ISO timestamp as a human-readable relative time string
|
|
1090
|
+
* @param {string} isoString
|
|
1091
|
+
* @returns {string}
|
|
1092
|
+
*/
|
|
1093
|
+
formatRelativeTime(isoString) {
|
|
1094
|
+
if (!isoString) return '';
|
|
1095
|
+
const seconds = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000);
|
|
1096
|
+
if (seconds < 60) return 'just now';
|
|
1097
|
+
const minutes = Math.floor(seconds / 60);
|
|
1098
|
+
if (minutes < 60) return minutes + 'm ago';
|
|
1099
|
+
const hours = Math.floor(minutes / 60);
|
|
1100
|
+
if (hours < 24) return hours + 'h ago';
|
|
1101
|
+
const days = Math.floor(hours / 24);
|
|
1102
|
+
if (days === 1) return 'yesterday';
|
|
1103
|
+
return days + 'd ago';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Delete a single worktree by ID
|
|
1108
|
+
* @param {string} worktreeId
|
|
1109
|
+
*/
|
|
1110
|
+
async deleteWorktree(worktreeId) {
|
|
1111
|
+
if (!confirm('Delete this worktree? This removes it from disk and cannot be undone.')) return;
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/worktrees/${worktreeId}`, {
|
|
1115
|
+
method: 'DELETE'
|
|
1116
|
+
});
|
|
1117
|
+
if (!response.ok) {
|
|
1118
|
+
const data = await response.json().catch(() => ({}));
|
|
1119
|
+
throw new Error(data.error || 'Failed to delete worktree');
|
|
1120
|
+
}
|
|
1121
|
+
this.showToast('success', 'Worktree deleted');
|
|
1122
|
+
await this.loadWorktrees();
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
this.showToast('error', 'Failed to delete worktree: ' + error.message);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Delete all worktrees for the current repository
|
|
1130
|
+
*/
|
|
1131
|
+
async deleteAllWorktrees() {
|
|
1132
|
+
const count = this.worktreeData && this.worktreeData.worktrees
|
|
1133
|
+
? this.worktreeData.worktrees.length
|
|
1134
|
+
: 0;
|
|
1135
|
+
if (!confirm(`Delete all ${count} worktree(s)? This removes them from disk and cannot be undone.`)) return;
|
|
1136
|
+
|
|
1137
|
+
try {
|
|
1138
|
+
const response = await fetch(`/api/repos/${this.owner}/${this.repo}/worktrees`, {
|
|
1139
|
+
method: 'DELETE'
|
|
1140
|
+
});
|
|
1141
|
+
if (!response.ok) {
|
|
1142
|
+
const data = await response.json().catch(() => ({}));
|
|
1143
|
+
throw new Error(data.error || 'Failed to delete worktrees');
|
|
1144
|
+
}
|
|
1145
|
+
const data = await response.json();
|
|
1146
|
+
this.showToast('success', `Deleted ${data.deleted} worktree(s)`);
|
|
1147
|
+
await this.loadWorktrees();
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
this.showToast('error', 'Failed to delete worktrees: ' + error.message);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
854
1153
|
updateUI() {
|
|
855
1154
|
// Update provider selection - validate provider exists before selecting
|
|
856
1155
|
let providerId = this.currentSettings.default_provider;
|
|
@@ -908,6 +1207,23 @@ class RepoSettingsPage {
|
|
|
908
1207
|
|
|
909
1208
|
// Update local path display
|
|
910
1209
|
this.updateLocalPathDisplay();
|
|
1210
|
+
|
|
1211
|
+
// Update pool setting inputs
|
|
1212
|
+
const poolSizeInput = document.getElementById('pool-size-input');
|
|
1213
|
+
if (poolSizeInput) {
|
|
1214
|
+
poolSizeInput.value = this.currentSettings.pool_size ?? '';
|
|
1215
|
+
}
|
|
1216
|
+
const poolFetchInput = document.getElementById('pool-fetch-interval-input');
|
|
1217
|
+
if (poolFetchInput) {
|
|
1218
|
+
poolFetchInput.value = this.currentSettings.pool_fetch_interval_minutes ?? '';
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Update load skills select
|
|
1222
|
+
const loadSkillsSelect = document.getElementById('load-skills-select');
|
|
1223
|
+
if (loadSkillsSelect) {
|
|
1224
|
+
const val = this.currentSettings.load_skills;
|
|
1225
|
+
loadSkillsSelect.value = val === null || val === undefined ? '' : String(val);
|
|
1226
|
+
}
|
|
911
1227
|
}
|
|
912
1228
|
|
|
913
1229
|
/**
|
|
@@ -957,8 +1273,11 @@ class RepoSettingsPage {
|
|
|
957
1273
|
const councilChanged = (this.currentSettings.default_council_id ?? null) !== (this.originalSettings.default_council_id ?? null);
|
|
958
1274
|
const instructionsChanged = (this.currentSettings.default_instructions ?? '') !== (this.originalSettings.default_instructions ?? '');
|
|
959
1275
|
const chatInstructionsChanged = (this.currentSettings.default_chat_instructions ?? '') !== (this.originalSettings.default_chat_instructions ?? '');
|
|
1276
|
+
const poolSizeChanged = (this.currentSettings.pool_size ?? null) !== (this.originalSettings.pool_size ?? null);
|
|
1277
|
+
const poolFetchChanged = (this.currentSettings.pool_fetch_interval_minutes ?? null) !== (this.originalSettings.pool_fetch_interval_minutes ?? null);
|
|
1278
|
+
const loadSkillsChanged = (this.currentSettings.load_skills ?? null) !== (this.originalSettings.load_skills ?? null);
|
|
960
1279
|
|
|
961
|
-
this.hasUnsavedChanges = providerChanged || modelChanged || tabChanged || councilChanged || instructionsChanged || chatInstructionsChanged;
|
|
1280
|
+
this.hasUnsavedChanges = providerChanged || modelChanged || tabChanged || councilChanged || instructionsChanged || chatInstructionsChanged || poolSizeChanged || poolFetchChanged || loadSkillsChanged;
|
|
962
1281
|
|
|
963
1282
|
// Show/hide action bar
|
|
964
1283
|
const actionBar = document.getElementById('action-bar');
|
|
@@ -993,7 +1312,10 @@ class RepoSettingsPage {
|
|
|
993
1312
|
default_tab: this.currentSettings.default_tab,
|
|
994
1313
|
default_council_id: this.currentSettings.default_council_id,
|
|
995
1314
|
default_instructions: this.currentSettings.default_instructions,
|
|
996
|
-
default_chat_instructions: this.currentSettings.default_chat_instructions
|
|
1315
|
+
default_chat_instructions: this.currentSettings.default_chat_instructions,
|
|
1316
|
+
pool_size: this.currentSettings.pool_size,
|
|
1317
|
+
pool_fetch_interval_minutes: this.currentSettings.pool_fetch_interval_minutes,
|
|
1318
|
+
load_skills: this.currentSettings.load_skills
|
|
997
1319
|
})
|
|
998
1320
|
});
|
|
999
1321
|
|
|
@@ -1068,7 +1390,10 @@ class RepoSettingsPage {
|
|
|
1068
1390
|
default_council_id: null,
|
|
1069
1391
|
default_instructions: '',
|
|
1070
1392
|
local_path: null,
|
|
1071
|
-
default_chat_instructions: ''
|
|
1393
|
+
default_chat_instructions: '',
|
|
1394
|
+
pool_size: null,
|
|
1395
|
+
pool_fetch_interval_minutes: null,
|
|
1396
|
+
load_skills: null
|
|
1072
1397
|
})
|
|
1073
1398
|
});
|
|
1074
1399
|
|
|
@@ -1084,7 +1409,10 @@ class RepoSettingsPage {
|
|
|
1084
1409
|
default_council_id: null,
|
|
1085
1410
|
default_instructions: '',
|
|
1086
1411
|
local_path: null,
|
|
1087
|
-
default_chat_instructions: ''
|
|
1412
|
+
default_chat_instructions: '',
|
|
1413
|
+
pool_size: null,
|
|
1414
|
+
pool_fetch_interval_minutes: null,
|
|
1415
|
+
load_skills: null
|
|
1088
1416
|
};
|
|
1089
1417
|
this.currentSettings = { ...this.originalSettings };
|
|
1090
1418
|
this.hasUnsavedChanges = false;
|
|
@@ -198,6 +198,22 @@ Examples:
|
|
|
198
198
|
</div>
|
|
199
199
|
</section>
|
|
200
200
|
|
|
201
|
+
<!-- Provider Skill Discovery Section -->
|
|
202
|
+
<section class="settings-section">
|
|
203
|
+
<div class="section-header">
|
|
204
|
+
<h2>Provider Skill Discovery</h2>
|
|
205
|
+
<p class="section-description">Controls whether AI providers load environment-level skills automatically. Currently applies to Pi's skill auto-discovery. When disabled, Pi runs with <code>--no-skills</code>.</p>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="form-group" style="max-width: 300px;">
|
|
208
|
+
<label for="load-skills-select" class="settings-label">Load Skills</label>
|
|
209
|
+
<select class="settings-select" id="load-skills-select">
|
|
210
|
+
<option value="">Default (inherit from config)</option>
|
|
211
|
+
<option value="1">Enabled</option>
|
|
212
|
+
<option value="0">Disabled</option>
|
|
213
|
+
</select>
|
|
214
|
+
</div>
|
|
215
|
+
</section>
|
|
216
|
+
|
|
201
217
|
<!-- Repository Location Section -->
|
|
202
218
|
<section class="settings-section" id="local-path-section">
|
|
203
219
|
<div class="section-header">
|
|
@@ -227,6 +243,19 @@ Examples:
|
|
|
227
243
|
</div>
|
|
228
244
|
</section>
|
|
229
245
|
|
|
246
|
+
<!-- Worktrees Section -->
|
|
247
|
+
<section class="settings-section" id="worktrees-section" style="display: none;">
|
|
248
|
+
<div class="section-header">
|
|
249
|
+
<h2>Worktrees</h2>
|
|
250
|
+
<p class="section-description">
|
|
251
|
+
Manage worktree directories used for reviewing pull requests in this repository.
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
<div id="worktrees-content">
|
|
255
|
+
<!-- Rendered dynamically by JS -->
|
|
256
|
+
</div>
|
|
257
|
+
</section>
|
|
258
|
+
|
|
230
259
|
<!-- Danger Zone -->
|
|
231
260
|
<section class="settings-section danger-zone">
|
|
232
261
|
<div class="section-header">
|
package/src/ai/analyzer.js
CHANGED
|
@@ -73,9 +73,10 @@ async function captureDiffSnapshot(analyzer, worktreePath, prMetadata, logPrefix
|
|
|
73
73
|
* @param {Object|null} instructions - Instructions object { repoInstructions, requestInstructions }
|
|
74
74
|
* @param {Function|null} progressCallback - Parent progress callback to wrap
|
|
75
75
|
* @param {Object} db - Database instance
|
|
76
|
+
* @param {Object} providerOverrides - Per-call config overrides passed to createProvider (optional)
|
|
76
77
|
* @returns {Object} { voiceAnalyzer, voiceKey, reviewerLabel, voiceRequestInstructions, voiceProgressCallback, voiceTier, voiceTimeout }
|
|
77
78
|
*/
|
|
78
|
-
function buildVoiceContext(voice, idx, instructions, progressCallback, db) {
|
|
79
|
+
function buildVoiceContext(voice, idx, instructions, progressCallback, db, providerOverrides = {}, providerOverridesMap = null) {
|
|
79
80
|
const voiceKey = `${voice.provider}-${voice.model}${idx > 0 ? `-${idx}` : ''}`;
|
|
80
81
|
const reviewerLabel = buildReviewerLabel(idx, voice);
|
|
81
82
|
|
|
@@ -87,13 +88,16 @@ function buildVoiceContext(voice, idx, instructions, progressCallback, db) {
|
|
|
87
88
|
: voice.customInstructions;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// Resolve per-voice overrides: prefer provider-specific from map, fall back to shared overrides
|
|
92
|
+
const effectiveOverrides = providerOverridesMap?.[voice.provider] || providerOverrides;
|
|
93
|
+
|
|
90
94
|
const ProviderClass = getProviderClass(voice.provider);
|
|
91
95
|
const isExecutable = ProviderClass?.isExecutable || false;
|
|
92
96
|
|
|
93
97
|
// Only create Analyzer for native voices
|
|
94
|
-
const voiceAnalyzer = isExecutable ? null : new Analyzer(db, voice.model, voice.provider);
|
|
98
|
+
const voiceAnalyzer = isExecutable ? null : new Analyzer(db, voice.model, voice.provider, effectiveOverrides);
|
|
95
99
|
// Create provider instance for executable voices (used directly)
|
|
96
|
-
const voiceProvider = isExecutable ? createProvider(voice.provider, voice.model) : null;
|
|
100
|
+
const voiceProvider = isExecutable ? createProvider(voice.provider, voice.model, effectiveOverrides) : null;
|
|
97
101
|
|
|
98
102
|
const voiceTier = voice.tier || 'balanced';
|
|
99
103
|
const voiceTimeout = voice.timeout || ProviderClass?.defaultTimeout || 600000;
|
|
@@ -272,12 +276,16 @@ class Analyzer {
|
|
|
272
276
|
* @param {Object} database - Database instance
|
|
273
277
|
* @param {string} model - Model to use (e.g., 'opus', 'gemini-2.5-pro')
|
|
274
278
|
* @param {string} provider - Provider ID (e.g., 'claude', 'gemini'). Defaults to 'claude'.
|
|
279
|
+
* @param {Object} providerOverrides - Per-call config overrides passed to createProvider (optional)
|
|
280
|
+
* @param {Object|null} providerOverridesMap - Per-provider overrides map for council mode (provider ID → overrides)
|
|
275
281
|
*/
|
|
276
|
-
constructor(database, model = 'opus', provider = 'claude') {
|
|
282
|
+
constructor(database, model = 'opus', provider = 'claude', providerOverrides = {}, providerOverridesMap = null) {
|
|
277
283
|
// Store model and provider for creating provider instances per level
|
|
278
284
|
this.model = model;
|
|
279
285
|
this.provider = provider;
|
|
280
286
|
this.db = database;
|
|
287
|
+
this.providerOverrides = providerOverrides;
|
|
288
|
+
this.providerOverridesMap = providerOverridesMap;
|
|
281
289
|
this.testContextCache = new Map(); // Cache test detection results per worktree
|
|
282
290
|
this._worktreeManager = null; // Lazy-initialized for sparse-checkout queries
|
|
283
291
|
}
|
|
@@ -1015,7 +1023,7 @@ Or simply ignore any changes to files matching these patterns in your analysis.
|
|
|
1015
1023
|
}
|
|
1016
1024
|
|
|
1017
1025
|
// Create provider instance for this level
|
|
1018
|
-
const aiProvider = createProvider(this.provider, this.model);
|
|
1026
|
+
const aiProvider = createProvider(this.provider, this.model, this.providerOverrides);
|
|
1019
1027
|
|
|
1020
1028
|
const updateProgress = (step) => {
|
|
1021
1029
|
const progress = `${lp}[Level 1] ${step}...`;
|
|
@@ -1975,7 +1983,7 @@ If you are unsure, use "NEW" - it is correct for the vast majority of suggestion
|
|
|
1975
1983
|
}
|
|
1976
1984
|
|
|
1977
1985
|
// Create provider instance for this level
|
|
1978
|
-
const aiProvider = createProvider(this.provider, this.model);
|
|
1986
|
+
const aiProvider = createProvider(this.provider, this.model, this.providerOverrides);
|
|
1979
1987
|
|
|
1980
1988
|
const updateProgress = (step) => {
|
|
1981
1989
|
const progress = `${lp}[Level 2] ${step}...`;
|
|
@@ -2085,7 +2093,7 @@ If you are unsure, use "NEW" - it is correct for the vast majority of suggestion
|
|
|
2085
2093
|
}
|
|
2086
2094
|
|
|
2087
2095
|
// Create provider instance for this level
|
|
2088
|
-
const aiProvider = createProvider(this.provider, this.model);
|
|
2096
|
+
const aiProvider = createProvider(this.provider, this.model, this.providerOverrides);
|
|
2089
2097
|
|
|
2090
2098
|
const updateProgress = (step) => {
|
|
2091
2099
|
const progress = `${lp}[Level 3] ${step}...`;
|
|
@@ -2655,7 +2663,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2655
2663
|
}
|
|
2656
2664
|
|
|
2657
2665
|
// Create provider instance for consolidation (use overrides if provided)
|
|
2658
|
-
const aiProvider = createProvider(providerOverride || this.provider, modelOverride || this.model);
|
|
2666
|
+
const aiProvider = createProvider(providerOverride || this.provider, modelOverride || this.model, this.providerOverrides);
|
|
2659
2667
|
|
|
2660
2668
|
// Build the consolidation prompt
|
|
2661
2669
|
const prompt = this.buildOrchestrationPrompt(allSuggestions, prMetadata, customInstructions, worktreePath, tier, lp, { excludePrevious, dedupContext });
|
|
@@ -2932,7 +2940,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2932
2940
|
if (voices.length === 1) {
|
|
2933
2941
|
const voice = voices[0];
|
|
2934
2942
|
const { voiceAnalyzer, voiceProvider, isExecutable, voiceKey, reviewerLabel, voiceRequestInstructions, voiceProgressCallback, voiceTier, voiceTimeout } =
|
|
2935
|
-
buildVoiceContext(voice, 0, instructions, progressCallback, this.db);
|
|
2943
|
+
buildVoiceContext(voice, 0, instructions, progressCallback, this.db, this.providerOverrides, this.providerOverridesMap);
|
|
2936
2944
|
logger.info(`[ReviewerCouncil] Single reviewer (${reviewerLabel}) — running directly on parent run, no child run`);
|
|
2937
2945
|
|
|
2938
2946
|
// Report voice-centric progress structure
|
|
@@ -3034,7 +3042,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3034
3042
|
const commentRepo = new CommentRepository(this.db);
|
|
3035
3043
|
const voicePromises = voices.map(async (voice, idx) => {
|
|
3036
3044
|
const { voiceAnalyzer, voiceProvider, isExecutable, voiceKey, reviewerLabel, voiceRequestInstructions, voiceProgressCallback, voiceTier, voiceTimeout } =
|
|
3037
|
-
buildVoiceContext(voice, idx, instructions, progressCallback, this.db);
|
|
3045
|
+
buildVoiceContext(voice, idx, instructions, progressCallback, this.db, this.providerOverrides, this.providerOverridesMap);
|
|
3038
3046
|
const childRunId = uuidv4();
|
|
3039
3047
|
|
|
3040
3048
|
// Create child analysis run record
|
|
@@ -3271,7 +3279,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3271
3279
|
|
|
3272
3280
|
const consolidated = await this._crossVoiceConsolidate(
|
|
3273
3281
|
voiceReviews, prMetadata, consolInstructions, worktreePath,
|
|
3274
|
-
{ provider: consolProvider, model: consolModel, tier: consolTier, timeout: consolConfig.timeout, analysisId, progressCallback, excludePrevious, dedupContext }
|
|
3282
|
+
{ provider: consolProvider, model: consolModel, tier: consolTier, timeout: consolConfig.timeout, analysisId, progressCallback, excludePrevious, dedupContext, providerOverrides: this.providerOverrides }
|
|
3275
3283
|
);
|
|
3276
3284
|
|
|
3277
3285
|
const finalSuggestions = this.validateAndFinalizeSuggestions(
|
|
@@ -3410,7 +3418,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3410
3418
|
tier,
|
|
3411
3419
|
timeout: voice.timeout || VoiceProviderClass?.defaultTimeout || 600000,
|
|
3412
3420
|
customInstructions: voiceInstructions,
|
|
3413
|
-
voiceCustomInstructions: voice.customInstructions || null
|
|
3421
|
+
voiceCustomInstructions: voice.customInstructions || null,
|
|
3422
|
+
providerOverrides: this.providerOverridesMap?.[voice.provider] || this.providerOverrides
|
|
3414
3423
|
});
|
|
3415
3424
|
}
|
|
3416
3425
|
}
|
|
@@ -3575,7 +3584,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3575
3584
|
}));
|
|
3576
3585
|
const consolidated = await this._intraLevelConsolidate(
|
|
3577
3586
|
level, voiceGroups, prMetadata, orchInstructions, worktreePath,
|
|
3578
|
-
{ provider: orchProvider, model: orchModel, tier: orchTier, timeout: orchConfig.timeout, analysisId, progressCallback, reviewerCount: successfulVoicesForLevel.length }
|
|
3587
|
+
{ provider: orchProvider, model: orchModel, tier: orchTier, timeout: orchConfig.timeout, analysisId, progressCallback, reviewerCount: successfulVoicesForLevel.length, providerOverrides: this.providerOverrides }
|
|
3579
3588
|
);
|
|
3580
3589
|
consolidatedPerLevel[level] = consolidated;
|
|
3581
3590
|
// Report intra-level consolidation step as completed
|
|
@@ -3661,7 +3670,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3661
3670
|
* @private
|
|
3662
3671
|
*/
|
|
3663
3672
|
async _executeCouncilVoice(task, context) {
|
|
3664
|
-
const { voiceId, reviewerLabel, reviewerLogPrefix, level, provider, model, tier, timeout = 600000, customInstructions } = task;
|
|
3673
|
+
const { voiceId, reviewerLabel, reviewerLogPrefix, level, provider, model, tier, timeout = 600000, customInstructions, providerOverrides } = task;
|
|
3665
3674
|
const { reviewId, runId, worktreePath, prMetadata, generatedPatterns, validFiles, analysisId, progressCallback } = context;
|
|
3666
3675
|
const displayLabel = reviewerLabel || voiceId;
|
|
3667
3676
|
|
|
@@ -3672,7 +3681,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3672
3681
|
}
|
|
3673
3682
|
|
|
3674
3683
|
// Create provider instance for this voice
|
|
3675
|
-
const aiProvider = createProvider(provider, model);
|
|
3684
|
+
const aiProvider = createProvider(provider, model, providerOverrides || {});
|
|
3676
3685
|
|
|
3677
3686
|
// Build prompt based on level
|
|
3678
3687
|
let prompt;
|
|
@@ -3748,9 +3757,9 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3748
3757
|
* @private
|
|
3749
3758
|
*/
|
|
3750
3759
|
async _intraLevelConsolidate(level, voiceGroups, prMetadata, customInstructions, worktreePath, orchConfig) {
|
|
3751
|
-
const { provider, model, tier, timeout, analysisId, progressCallback, reviewerCount } = orchConfig;
|
|
3760
|
+
const { provider, model, tier, timeout, analysisId, progressCallback, reviewerCount, providerOverrides } = orchConfig;
|
|
3752
3761
|
|
|
3753
|
-
const aiProvider = createProvider(provider, model);
|
|
3762
|
+
const aiProvider = createProvider(provider, model, providerOverrides || {});
|
|
3754
3763
|
|
|
3755
3764
|
const isLocal = prMetadata.reviewType === 'local';
|
|
3756
3765
|
const reviewDescription = isLocal
|
|
@@ -3916,9 +3925,9 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3916
3925
|
* @private
|
|
3917
3926
|
*/
|
|
3918
3927
|
async _crossVoiceConsolidate(voiceReviews, prMetadata, customInstructions, worktreePath, config) {
|
|
3919
|
-
const { provider, model, tier, timeout, analysisId, progressCallback, excludePrevious, dedupContext } = config;
|
|
3928
|
+
const { provider, model, tier, timeout, analysisId, progressCallback, excludePrevious, dedupContext, providerOverrides } = config;
|
|
3920
3929
|
|
|
3921
|
-
const aiProvider = createProvider(provider, model);
|
|
3930
|
+
const aiProvider = createProvider(provider, model, providerOverrides || {});
|
|
3922
3931
|
|
|
3923
3932
|
const voiceDescriptions = voiceReviews.map(v => {
|
|
3924
3933
|
let desc = `### Reviewer: ${v.voiceKey}`;
|
package/src/ai/claude-cli.js
CHANGED
|
@@ -53,6 +53,8 @@ class ClaudeCLI {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
const pid = claude.pid;
|
|
56
|
+
const fullCommand = this.useShell ? this.command : `${this.command} ${this.args.join(' ')}`;
|
|
57
|
+
logger.debug(`${levelPrefix} Claude CLI command: ${fullCommand}`);
|
|
56
58
|
logger.info(`${levelPrefix} Spawned Claude CLI process: PID ${pid}`);
|
|
57
59
|
|
|
58
60
|
let stdout = '';
|
|
@@ -257,7 +257,8 @@ class ClaudeProvider extends AIProvider {
|
|
|
257
257
|
logger.info(`${levelPrefix} Executing Claude CLI...`);
|
|
258
258
|
logger.info(`${levelPrefix} Writing prompt: ${prompt.length} bytes`);
|
|
259
259
|
|
|
260
|
-
const
|
|
260
|
+
const spawnArgs = [...this.args];
|
|
261
|
+
const claude = spawn(this.command, spawnArgs, {
|
|
261
262
|
cwd,
|
|
262
263
|
env: {
|
|
263
264
|
...process.env,
|
|
@@ -268,6 +269,8 @@ class ClaudeProvider extends AIProvider {
|
|
|
268
269
|
});
|
|
269
270
|
|
|
270
271
|
const pid = claude.pid;
|
|
272
|
+
const fullCommand = this.useShell ? this.command : `${this.command} ${spawnArgs.join(' ')}`;
|
|
273
|
+
logger.debug(`${levelPrefix} Claude CLI command: ${fullCommand}`);
|
|
271
274
|
logger.info(`${levelPrefix} Spawned Claude CLI process: PID ${pid}`);
|
|
272
275
|
|
|
273
276
|
// Register process for cancellation tracking if analysisId provided
|