@in-the-loop-labs/pair-review 3.0.5 → 3.1.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/package.json +2 -1
- 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/level1-balanced.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
- package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
- package/public/css/analysis-config.css +83 -0
- package/public/css/pr.css +191 -4
- package/public/index.html +20 -0
- package/public/js/components/AIPanel.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +83 -8
- package/public/js/components/AnalysisConfigModal.js +155 -5
- package/public/js/components/ChatPanel.js +22 -5
- package/public/js/components/CouncilProgressModal.js +239 -22
- package/public/js/components/TimeoutSelect.js +2 -0
- package/public/js/components/VoiceCentricConfigTab.js +179 -12
- package/public/js/index.js +119 -1
- package/public/js/local.js +141 -47
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +71 -12
- package/public/js/repo-settings.js +2 -2
- package/public/local.html +32 -11
- package/public/pr.html +2 -0
- package/src/ai/analyzer.js +371 -111
- package/src/ai/claude-provider.js +2 -0
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +2 -0
- package/src/ai/executable-provider.js +534 -0
- package/src/ai/gemini-provider.js +2 -0
- package/src/ai/index.js +9 -1
- package/src/ai/pi-provider.js +10 -8
- package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
- package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
- package/src/ai/prompts/baseline/level1/balanced.js +12 -0
- package/src/ai/prompts/baseline/level1/fast.js +11 -0
- package/src/ai/prompts/baseline/level1/thorough.js +12 -0
- package/src/ai/prompts/baseline/level2/balanced.js +13 -0
- package/src/ai/prompts/baseline/level2/fast.js +12 -0
- package/src/ai/prompts/baseline/level2/thorough.js +13 -0
- package/src/ai/prompts/baseline/level3/balanced.js +13 -0
- package/src/ai/prompts/baseline/level3/fast.js +12 -0
- package/src/ai/prompts/baseline/level3/thorough.js +13 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
- package/src/ai/prompts/render-for-skill.js +3 -0
- package/src/ai/prompts/shared/output-schema.js +8 -0
- package/src/ai/provider.js +89 -4
- package/src/chat/prompt-builder.js +17 -1
- package/src/chat/session-manager.js +32 -28
- package/src/config.js +15 -2
- package/src/database.js +59 -15
- package/src/git/base-branch.js +133 -52
- package/src/local-review.js +15 -9
- package/src/main.js +3 -2
- package/src/routes/analyses.js +34 -8
- package/src/routes/chat.js +15 -8
- package/src/routes/config.js +3 -120
- package/src/routes/councils.js +15 -6
- package/src/routes/executable-analysis.js +494 -0
- package/src/routes/local.js +160 -26
- package/src/routes/mcp.js +9 -4
- package/src/routes/pr.js +166 -29
- package/src/routes/reviews.js +31 -5
- package/src/routes/shared.js +72 -5
- package/src/routes/worktrees.js +4 -2
- package/src/utils/comment-formatter.js +28 -11
- package/src/utils/instructions.js +22 -8
- package/src/utils/logger.js +20 -10
package/src/config.js
CHANGED
|
@@ -31,7 +31,7 @@ const DEFAULT_CONFIG = {
|
|
|
31
31
|
enable_chat: true, // When true, enables the chat panel feature (uses chat_provider)
|
|
32
32
|
chat_provider: "pi", // Chat provider: 'pi', 'copilot-acp', 'gemini-acp', 'opencode-acp', 'cursor-acp', 'codex'
|
|
33
33
|
comment_format: "legacy", // Comment format preset or custom template for adopted suggestions
|
|
34
|
-
chat: { enable_shortcuts: true }, // Chat panel settings (enable_shortcuts: show action shortcut buttons)
|
|
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
|
monorepos: {}, // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
|
|
@@ -227,6 +227,20 @@ async function loadConfig() {
|
|
|
227
227
|
process.exit(1);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// Load global instructions from ~/.pair-review/global-instructions.md
|
|
231
|
+
const globalInstructionsPath = path.join(CONFIG_DIR, 'global-instructions.md');
|
|
232
|
+
try {
|
|
233
|
+
const content = await fs.readFile(globalInstructionsPath, 'utf-8');
|
|
234
|
+
const trimmed = content.trim();
|
|
235
|
+
if (trimmed) {
|
|
236
|
+
mergedConfig.globalInstructions = trimmed;
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error.code !== 'ENOENT') {
|
|
240
|
+
logger.warn(`Could not read global instructions from ${globalInstructionsPath}: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
230
244
|
return { config: mergedConfig, isFirstRun };
|
|
231
245
|
}
|
|
232
246
|
|
|
@@ -535,7 +549,6 @@ function shouldSkipUpdateNotifier() {
|
|
|
535
549
|
module.exports = {
|
|
536
550
|
deepMerge,
|
|
537
551
|
loadConfig,
|
|
538
|
-
saveConfig,
|
|
539
552
|
getConfigDir,
|
|
540
553
|
validatePort,
|
|
541
554
|
getGitHubToken,
|
package/src/database.js
CHANGED
|
@@ -20,7 +20,7 @@ function getDbPath() {
|
|
|
20
20
|
/**
|
|
21
21
|
* Current schema version - increment this when adding new migrations
|
|
22
22
|
*/
|
|
23
|
-
const CURRENT_SCHEMA_VERSION =
|
|
23
|
+
const CURRENT_SCHEMA_VERSION = 35;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Database schema SQL statements
|
|
@@ -96,6 +96,8 @@ const SCHEMA_SQL = {
|
|
|
96
96
|
voice_id TEXT,
|
|
97
97
|
is_raw INTEGER DEFAULT 0,
|
|
98
98
|
|
|
99
|
+
severity TEXT,
|
|
100
|
+
|
|
99
101
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
100
102
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
101
103
|
|
|
@@ -162,6 +164,7 @@ const SCHEMA_SQL = {
|
|
|
162
164
|
model TEXT,
|
|
163
165
|
tier TEXT,
|
|
164
166
|
custom_instructions TEXT,
|
|
167
|
+
global_instructions TEXT,
|
|
165
168
|
repo_instructions TEXT,
|
|
166
169
|
request_instructions TEXT,
|
|
167
170
|
head_sha TEXT,
|
|
@@ -1564,6 +1567,44 @@ const MIGRATIONS = {
|
|
|
1564
1567
|
|
|
1565
1568
|
console.log(' Recreated comments with ON DELETE CASCADE for review_id');
|
|
1566
1569
|
console.log('Migration to schema version 33 complete');
|
|
1570
|
+
},
|
|
1571
|
+
|
|
1572
|
+
34: (db) => {
|
|
1573
|
+
console.log('Migrating to schema version 34: Add global_instructions to analysis_runs');
|
|
1574
|
+
|
|
1575
|
+
if (!columnExists(db, 'analysis_runs', 'global_instructions')) {
|
|
1576
|
+
try {
|
|
1577
|
+
db.prepare('ALTER TABLE analysis_runs ADD COLUMN global_instructions TEXT').run();
|
|
1578
|
+
console.log(' Added global_instructions column to analysis_runs');
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
if (!error.message.includes('duplicate column name')) {
|
|
1581
|
+
throw error;
|
|
1582
|
+
}
|
|
1583
|
+
console.log(' Column global_instructions already exists (race condition)');
|
|
1584
|
+
}
|
|
1585
|
+
} else {
|
|
1586
|
+
console.log(' Column global_instructions already exists');
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
console.log('Migration to schema version 34 complete');
|
|
1590
|
+
},
|
|
1591
|
+
|
|
1592
|
+
// Migration to version 35: Add severity column to comments table
|
|
1593
|
+
35: (db) => {
|
|
1594
|
+
console.log('Running migration to schema version 35...');
|
|
1595
|
+
|
|
1596
|
+
const addColumnIfNotExists = (table, column, definition) => {
|
|
1597
|
+
const tableInfo = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
1598
|
+
const columnExists = tableInfo.some(col => col.name === column);
|
|
1599
|
+
if (!columnExists) {
|
|
1600
|
+
db.prepare(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`).run();
|
|
1601
|
+
console.log(` Added column ${column} to ${table}`);
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
addColumnIfNotExists('comments', 'severity', 'TEXT');
|
|
1606
|
+
|
|
1607
|
+
console.log('Migration to schema version 35 complete');
|
|
1567
1608
|
}
|
|
1568
1609
|
};
|
|
1569
1610
|
|
|
@@ -2567,6 +2608,7 @@ class CommentRepository {
|
|
|
2567
2608
|
status,
|
|
2568
2609
|
parent_id,
|
|
2569
2610
|
is_file_level,
|
|
2611
|
+
severity,
|
|
2570
2612
|
created_at,
|
|
2571
2613
|
updated_at
|
|
2572
2614
|
FROM comments
|
|
@@ -2634,8 +2676,8 @@ class CommentRepository {
|
|
|
2634
2676
|
await run(this.db, `
|
|
2635
2677
|
INSERT INTO comments (
|
|
2636
2678
|
review_id, source, author, ai_run_id, ai_level, ai_confidence,
|
|
2637
|
-
file, line_start, line_end, side, type, title, body, suggestion_text, reasoning, status, is_file_level
|
|
2638
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2679
|
+
file, line_start, line_end, side, type, title, body, suggestion_text, reasoning, status, is_file_level, severity
|
|
2680
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2639
2681
|
`, [
|
|
2640
2682
|
reviewId,
|
|
2641
2683
|
'ai',
|
|
@@ -2653,7 +2695,8 @@ class CommentRepository {
|
|
|
2653
2695
|
suggestionText,
|
|
2654
2696
|
suggestion.reasoning ? JSON.stringify(suggestion.reasoning) : null,
|
|
2655
2697
|
'active',
|
|
2656
|
-
isFileLevel
|
|
2698
|
+
isFileLevel,
|
|
2699
|
+
suggestion.severity ?? null
|
|
2657
2700
|
]);
|
|
2658
2701
|
}
|
|
2659
2702
|
}
|
|
@@ -3482,6 +3525,7 @@ class AnalysisRunRepository {
|
|
|
3482
3525
|
* @param {string} [runInfo.provider] - AI provider (claude, gemini, etc.)
|
|
3483
3526
|
* @param {string} [runInfo.model] - AI model name
|
|
3484
3527
|
* @param {string} [runInfo.customInstructions] - Merged custom instructions (kept for backward compatibility)
|
|
3528
|
+
* @param {string} [runInfo.globalInstructions] - Global instructions from ~/.pair-review/global-instructions.md
|
|
3485
3529
|
* @param {string} [runInfo.repoInstructions] - Repository-level instructions from repo_settings
|
|
3486
3530
|
* @param {string} [runInfo.requestInstructions] - Request-level instructions from the analyze request
|
|
3487
3531
|
* @param {string} [runInfo.headSha] - Git HEAD SHA at the time of analysis (PR head commit or local HEAD)
|
|
@@ -3489,14 +3533,14 @@ class AnalysisRunRepository {
|
|
|
3489
3533
|
* @param {string} [runInfo.status='running'] - Initial status (default 'running'; pass 'completed' for externally-produced results)
|
|
3490
3534
|
* @returns {Promise<Object>} Created analysis run record
|
|
3491
3535
|
*/
|
|
3492
|
-
async create({ id, reviewId, provider = null, model = null, tier = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, diff = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null, scopeStart = null, scopeEnd = null }) {
|
|
3536
|
+
async create({ id, reviewId, provider = null, model = null, tier = null, customInstructions = null, globalInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, diff = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null, scopeStart = null, scopeEnd = null }) {
|
|
3493
3537
|
const isTerminal = ['completed', 'failed', 'cancelled'].includes(status);
|
|
3494
3538
|
const completedAt = isTerminal ? 'CURRENT_TIMESTAMP' : 'NULL';
|
|
3495
3539
|
const levelsConfigJson = levelsConfig ? JSON.stringify(levelsConfig) : null;
|
|
3496
3540
|
await run(this.db, `
|
|
3497
|
-
INSERT INTO analysis_runs (id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions, head_sha, diff, status, completed_at, parent_run_id, config_type, levels_config, scope_start, scope_end)
|
|
3498
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?, ?, ?)
|
|
3499
|
-
`, [id, reviewId, provider, model, tier, customInstructions, repoInstructions, requestInstructions, headSha, diff, status, parentRunId, configType, levelsConfigJson, scopeStart, scopeEnd]);
|
|
3541
|
+
INSERT INTO analysis_runs (id, review_id, provider, model, tier, custom_instructions, global_instructions, repo_instructions, request_instructions, head_sha, diff, status, completed_at, parent_run_id, config_type, levels_config, scope_start, scope_end)
|
|
3542
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?, ?, ?)
|
|
3543
|
+
`, [id, reviewId, provider, model, tier, customInstructions, globalInstructions, repoInstructions, requestInstructions, headSha, diff, status, parentRunId, configType, levelsConfigJson, scopeStart, scopeEnd]);
|
|
3500
3544
|
|
|
3501
3545
|
// Query back the inserted row to return actual database values (including timestamps)
|
|
3502
3546
|
return await this.getById(id);
|
|
@@ -3579,12 +3623,12 @@ class AnalysisRunRepository {
|
|
|
3579
3623
|
*/
|
|
3580
3624
|
async getById(id, { includeDiff = false } = {}) {
|
|
3581
3625
|
const columns = [
|
|
3582
|
-
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'repo_instructions', 'request_instructions',
|
|
3626
|
+
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'global_instructions', 'repo_instructions', 'request_instructions',
|
|
3583
3627
|
'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
|
|
3584
3628
|
'parent_run_id', 'config_type', 'levels_config'
|
|
3585
3629
|
];
|
|
3586
3630
|
if (includeDiff) {
|
|
3587
|
-
columns.splice(
|
|
3631
|
+
columns.splice(columns.indexOf('head_sha') + 1, 0, 'diff'); // Insert diff after head_sha
|
|
3588
3632
|
}
|
|
3589
3633
|
const row = await queryOne(this.db, `
|
|
3590
3634
|
SELECT ${columns.join(', ')}
|
|
@@ -3607,12 +3651,12 @@ class AnalysisRunRepository {
|
|
|
3607
3651
|
async getByReviewId(reviewId, { limit, includeDiff = false } = {}) {
|
|
3608
3652
|
const params = [reviewId];
|
|
3609
3653
|
const columns = [
|
|
3610
|
-
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'repo_instructions', 'request_instructions',
|
|
3654
|
+
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'global_instructions', 'repo_instructions', 'request_instructions',
|
|
3611
3655
|
'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
|
|
3612
3656
|
'parent_run_id', 'config_type', 'levels_config'
|
|
3613
3657
|
];
|
|
3614
3658
|
if (includeDiff) {
|
|
3615
|
-
columns.splice(
|
|
3659
|
+
columns.splice(columns.indexOf('head_sha') + 1, 0, 'diff'); // Insert diff after head_sha
|
|
3616
3660
|
}
|
|
3617
3661
|
let sql = `
|
|
3618
3662
|
SELECT ${columns.join(', ')}
|
|
@@ -3645,12 +3689,12 @@ class AnalysisRunRepository {
|
|
|
3645
3689
|
*/
|
|
3646
3690
|
async getLatestCompletedByReviewId(reviewId, { includeDiff = false } = {}) {
|
|
3647
3691
|
const columns = [
|
|
3648
|
-
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'repo_instructions', 'request_instructions',
|
|
3692
|
+
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'global_instructions', 'repo_instructions', 'request_instructions',
|
|
3649
3693
|
'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
|
|
3650
3694
|
'parent_run_id', 'config_type', 'levels_config'
|
|
3651
3695
|
];
|
|
3652
3696
|
if (includeDiff) {
|
|
3653
|
-
columns.splice(
|
|
3697
|
+
columns.splice(columns.indexOf('head_sha') + 1, 0, 'diff'); // Insert diff after head_sha
|
|
3654
3698
|
}
|
|
3655
3699
|
const row = await queryOne(this.db, `
|
|
3656
3700
|
SELECT ${columns.join(', ')}
|
|
@@ -3672,7 +3716,7 @@ class AnalysisRunRepository {
|
|
|
3672
3716
|
// Note: diff column is intentionally omitted - child runs share the same diff as parent
|
|
3673
3717
|
// to avoid data duplication. Use the parent run's diff when needed.
|
|
3674
3718
|
return query(this.db, `
|
|
3675
|
-
SELECT id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions,
|
|
3719
|
+
SELECT id, review_id, provider, model, tier, custom_instructions, global_instructions, repo_instructions, request_instructions,
|
|
3676
3720
|
head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
|
|
3677
3721
|
parent_run_id, config_type, levels_config
|
|
3678
3722
|
FROM analysis_runs
|
package/src/git/base-branch.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
const { execSync } = require('child_process');
|
|
3
|
+
const { readFileSync } = require('fs');
|
|
4
|
+
const path = require('path');
|
|
3
5
|
const logger = require('../utils/logger');
|
|
4
6
|
|
|
5
7
|
const defaults = {
|
|
6
8
|
execSync,
|
|
9
|
+
readFileSync,
|
|
7
10
|
// Callers should pass a resolved token via _deps.getGitHubToken.
|
|
8
11
|
// This default returns empty so GitHub lookup is silently skipped
|
|
9
12
|
// when no token is provided — never re-resolve config internally.
|
|
@@ -18,7 +21,7 @@ const defaults = {
|
|
|
18
21
|
* Detect the base branch for the current branch.
|
|
19
22
|
*
|
|
20
23
|
* Priority:
|
|
21
|
-
* 1. Graphite — `gt
|
|
24
|
+
* 1. Graphite — `gt state` (single call for trunk, parent, and stack)
|
|
22
25
|
* 2. GitHub PR — look up an open PR for this branch
|
|
23
26
|
* 3. Default branch — `git remote show origin` or local main/master
|
|
24
27
|
*
|
|
@@ -28,7 +31,7 @@ const defaults = {
|
|
|
28
31
|
* @param {string} [options.repository] - owner/repo string (needed for GitHub lookup)
|
|
29
32
|
* @param {boolean} [options.enableGraphite] - When true, try Graphite CLI for parent branch
|
|
30
33
|
* @param {Object} [options._deps] - Dependency overrides for testing
|
|
31
|
-
* @returns {Promise<{baseBranch: string, source: string, prNumber?: number}|null>}
|
|
34
|
+
* @returns {Promise<{baseBranch: string, source: string, prNumber?: number, stack?: Array}|null>}
|
|
32
35
|
*/
|
|
33
36
|
async function detectBaseBranch(repoPath, currentBranch, options = {}) {
|
|
34
37
|
const deps = { ...defaults, ...options._deps };
|
|
@@ -40,7 +43,7 @@ async function detectBaseBranch(repoPath, currentBranch, options = {}) {
|
|
|
40
43
|
|
|
41
44
|
// 1. Graphite (only when enabled via config)
|
|
42
45
|
if (options.enableGraphite) {
|
|
43
|
-
const graphiteResult =
|
|
46
|
+
const graphiteResult = tryGraphiteState(repoPath, currentBranch, deps);
|
|
44
47
|
if (graphiteResult) return graphiteResult;
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -56,46 +59,127 @@ async function detectBaseBranch(repoPath, currentBranch, options = {}) {
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
|
-
* Try Graphite CLI to find the parent branch.
|
|
62
|
+
* Try Graphite CLI `gt state` to find the parent branch and build the stack.
|
|
63
|
+
* Single execSync call replaces the previous 3 serial calls.
|
|
60
64
|
*/
|
|
61
|
-
function
|
|
65
|
+
function tryGraphiteState(repoPath, currentBranch, deps) {
|
|
62
66
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
const raw = deps.execSync('gt state', {
|
|
68
|
+
cwd: repoPath,
|
|
65
69
|
encoding: 'utf8',
|
|
66
70
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
67
|
-
timeout:
|
|
71
|
+
timeout: 5000
|
|
68
72
|
});
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
const trunk =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const state = JSON.parse(raw);
|
|
75
|
+
const trunk = Object.entries(state).find(([, v]) => v.trunk)?.[0];
|
|
76
|
+
const entry = state[currentBranch];
|
|
77
|
+
const parent = entry?.parents?.[0]?.ref;
|
|
78
|
+
|
|
79
|
+
if (!entry || !parent || parent === currentBranch) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stack = buildStack(state, currentBranch, trunk);
|
|
84
|
+
return { baseBranch: parent, source: 'graphite', stack };
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.debug(`Graphite state failed: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Walk from currentBranch up via parents[0].ref to build the stack,
|
|
94
|
+
* ordered trunk-first. Includes cycle protection.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} state - Parsed `gt state` output
|
|
97
|
+
* @param {string} currentBranch - Current branch name
|
|
98
|
+
* @param {string|undefined} trunk - Trunk branch name
|
|
99
|
+
* @returns {Array<{branch: string, parentBranch: string|null, parentSha: string|null, isTrunk: boolean}>}
|
|
100
|
+
*/
|
|
101
|
+
function buildStack(state, currentBranch, trunk) {
|
|
102
|
+
const entries = [];
|
|
103
|
+
const visited = new Set();
|
|
104
|
+
let branch = currentBranch;
|
|
105
|
+
|
|
106
|
+
while (branch && !visited.has(branch)) {
|
|
107
|
+
visited.add(branch);
|
|
108
|
+
const info = state[branch];
|
|
109
|
+
if (!info) break;
|
|
110
|
+
|
|
111
|
+
const parentRef = info.parents?.[0]?.ref || null;
|
|
112
|
+
const parentSha = info.parents?.[0]?.sha || null;
|
|
113
|
+
entries.push({
|
|
114
|
+
branch,
|
|
115
|
+
parentBranch: parentRef,
|
|
116
|
+
parentSha,
|
|
117
|
+
isTrunk: !!info.trunk
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
branch = parentRef;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
entries.reverse();
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
// If the walk terminated before reaching the trunk, prepend it
|
|
126
|
+
if (trunk && !visited.has(trunk) && state[trunk]) {
|
|
127
|
+
entries.unshift({
|
|
128
|
+
branch: trunk,
|
|
129
|
+
parentBranch: null,
|
|
130
|
+
parentSha: null,
|
|
131
|
+
isTrunk: true
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return entries;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read Graphite PR info from the `.graphite_pr_info` file in the git dir.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} repoPath - Absolute path to the repository
|
|
142
|
+
* @param {Object} deps - Dependencies (execSync, readFileSync)
|
|
143
|
+
* @returns {Object|null} Parsed PR info object with `prInfos` array, or null
|
|
144
|
+
*/
|
|
145
|
+
function readGraphitePRInfo(repoPath, deps) {
|
|
146
|
+
try {
|
|
147
|
+
const gitCommonDir = deps.execSync('git rev-parse --git-common-dir', {
|
|
80
148
|
cwd: repoPath,
|
|
81
149
|
encoding: 'utf8',
|
|
82
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
83
|
-
timeout: 3000
|
|
150
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
84
151
|
}).trim();
|
|
85
152
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
const prInfoPath = path.resolve(repoPath, gitCommonDir, '.graphite_pr_info');
|
|
154
|
+
const raw = deps.readFileSync(prInfoPath, 'utf8');
|
|
155
|
+
return JSON.parse(raw);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.debug(`Graphite PR info read failed: ${error.message}`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
89
161
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Enrich stack entries with PR numbers from Graphite PR info.
|
|
164
|
+
*
|
|
165
|
+
* @param {Array} stack - Stack entries from buildStack
|
|
166
|
+
* @param {Array} prInfos - Array of PR info objects with headRefName and prNumber
|
|
167
|
+
* @returns {Array} New array of stack entries, each with optional prNumber
|
|
168
|
+
*/
|
|
169
|
+
function enrichStackWithPRInfo(stack, prInfos) {
|
|
170
|
+
if (!prInfos || !Array.isArray(prInfos)) return stack;
|
|
171
|
+
|
|
172
|
+
const prMap = new Map();
|
|
173
|
+
for (const info of prInfos) {
|
|
174
|
+
if (info.headRefName) {
|
|
175
|
+
prMap.set(info.headRefName, info.prNumber);
|
|
93
176
|
}
|
|
94
|
-
} catch {
|
|
95
|
-
// Graphite not installed or failed — fall through silently
|
|
96
177
|
}
|
|
97
178
|
|
|
98
|
-
return
|
|
179
|
+
return stack.map(entry => {
|
|
180
|
+
const prNumber = prMap.get(entry.branch);
|
|
181
|
+
return prNumber != null ? { ...entry, prNumber } : { ...entry };
|
|
182
|
+
});
|
|
99
183
|
}
|
|
100
184
|
|
|
101
185
|
/**
|
|
@@ -171,46 +255,43 @@ function tryDefaultBranch(repoPath, currentBranch, deps) {
|
|
|
171
255
|
}
|
|
172
256
|
|
|
173
257
|
/**
|
|
174
|
-
* Synchronously detect the default branch for a repository
|
|
258
|
+
* Synchronously detect the default branch for a repository using only
|
|
259
|
+
* local refs (no network I/O).
|
|
175
260
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
261
|
+
* Priority:
|
|
262
|
+
* 1. `git symbolic-ref refs/remotes/origin/HEAD` — reads the local ref
|
|
263
|
+
* that `git clone` sets automatically.
|
|
264
|
+
* 2. Check whether `refs/heads/main` or `refs/heads/master` exist locally.
|
|
179
265
|
*
|
|
180
|
-
* @param {string}
|
|
266
|
+
* @param {string} localPath - Absolute path to the repository
|
|
181
267
|
* @param {Object} [_deps] - Dependency overrides for testing
|
|
182
268
|
* @returns {string|null} Default branch name, or null if it cannot be determined
|
|
183
269
|
*/
|
|
184
|
-
function getDefaultBranch(
|
|
270
|
+
function getDefaultBranch(localPath, _deps) {
|
|
271
|
+
if (!localPath) return null;
|
|
185
272
|
const deps = { ...defaults, ..._deps };
|
|
186
273
|
|
|
187
|
-
// Try
|
|
274
|
+
// Try symbolic-ref (set by git clone)
|
|
188
275
|
try {
|
|
189
|
-
const
|
|
190
|
-
cwd:
|
|
276
|
+
const ref = deps.execSync('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
277
|
+
cwd: localPath,
|
|
191
278
|
encoding: 'utf8',
|
|
192
279
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (match) {
|
|
198
|
-
const branch = match[1].trim();
|
|
199
|
-
if (branch && branch !== '(unknown)') {
|
|
200
|
-
return branch;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
280
|
+
}).trim();
|
|
281
|
+
// ref looks like "refs/remotes/origin/main"
|
|
282
|
+
const branch = ref.replace(/^refs\/remotes\/origin\//, '');
|
|
283
|
+
if (branch && branch !== ref) return branch;
|
|
203
284
|
} catch {
|
|
204
|
-
//
|
|
285
|
+
// origin/HEAD not set — fall through to local check
|
|
205
286
|
}
|
|
206
287
|
|
|
207
288
|
// Fallback: check if main or master exists locally
|
|
208
289
|
for (const candidate of ['main', 'master']) {
|
|
209
290
|
try {
|
|
210
|
-
deps.execSync(`git rev-parse --verify
|
|
211
|
-
cwd:
|
|
291
|
+
deps.execSync(`git rev-parse --verify refs/heads/${candidate}`, {
|
|
292
|
+
cwd: localPath,
|
|
212
293
|
encoding: 'utf8',
|
|
213
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
294
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
214
295
|
});
|
|
215
296
|
return candidate;
|
|
216
297
|
} catch {
|
|
@@ -221,4 +302,4 @@ function getDefaultBranch(repoPath, _deps) {
|
|
|
221
302
|
return null;
|
|
222
303
|
}
|
|
223
304
|
|
|
224
|
-
module.exports = { detectBaseBranch, getDefaultBranch };
|
|
305
|
+
module.exports = { detectBaseBranch, getDefaultBranch, tryGraphiteState, buildStack, readGraphitePRInfo, enrichStackWithPRInfo };
|
package/src/local-review.js
CHANGED
|
@@ -394,9 +394,11 @@ async function findMergeBase(repoPath, baseBranch) {
|
|
|
394
394
|
* @param {string} repoPath - Path to the git repository
|
|
395
395
|
* @param {Array} untrackedFiles - Array from getUntrackedFiles()
|
|
396
396
|
* @param {string} wFlag - Whitespace flag (e.g. ' -w' or '')
|
|
397
|
+
* @param {string} [contextFlag=''] - Unified context flag (e.g. ' --unified=3')
|
|
398
|
+
* @param {string} [extraArgsStr=''] - Additional git diff flags (e.g. ' --patience')
|
|
397
399
|
* @returns {string} Combined diff text for untracked files
|
|
398
400
|
*/
|
|
399
|
-
function generateUntrackedDiffs(repoPath, untrackedFiles, wFlag) {
|
|
401
|
+
function generateUntrackedDiffs(repoPath, untrackedFiles, wFlag, contextFlag = '', extraArgsStr = '') {
|
|
400
402
|
let diff = '';
|
|
401
403
|
for (const untracked of untrackedFiles) {
|
|
402
404
|
if (!untracked.skipped) {
|
|
@@ -404,7 +406,7 @@ function generateUntrackedDiffs(repoPath, untrackedFiles, wFlag) {
|
|
|
404
406
|
const filePath = path.join(repoPath, untracked.file);
|
|
405
407
|
let fileDiff;
|
|
406
408
|
try {
|
|
407
|
-
fileDiff = execSync(`git diff --no-index ${GIT_DIFF_FLAGS}${wFlag} -- /dev/null "${filePath}"`, {
|
|
409
|
+
fileDiff = execSync(`git diff --no-index ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag} -- /dev/null "${filePath}"`, {
|
|
408
410
|
cwd: repoPath,
|
|
409
411
|
encoding: 'utf8',
|
|
410
412
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -450,10 +452,14 @@ function generateUntrackedDiffs(repoPath, untrackedFiles, wFlag) {
|
|
|
450
452
|
* @param {string} [baseBranch] - Base branch name (required when branch is in scope)
|
|
451
453
|
* @param {Object} [options]
|
|
452
454
|
* @param {boolean} [options.hideWhitespace] - Whether to hide whitespace changes
|
|
455
|
+
* @param {number} [options.contextLines=25] - Number of unified context lines (--unified=N). Defaults to 25.
|
|
456
|
+
* @param {string[]} [options.extraArgs=[]] - Additional git diff flags appended to each diff command
|
|
453
457
|
* @returns {Promise<{diff: string, stats: Object, mergeBaseSha: string|null}>}
|
|
454
458
|
*/
|
|
455
459
|
async function generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch, options = {}) {
|
|
456
460
|
const wFlag = options.hideWhitespace ? ' -w' : '';
|
|
461
|
+
const contextFlag = ` --unified=${options.contextLines ?? 25}`;
|
|
462
|
+
const extraArgsStr = (options.extraArgs || []).length > 0 ? ' ' + options.extraArgs.join(' ') : '';
|
|
457
463
|
const stats = {
|
|
458
464
|
trackedChanges: 0,
|
|
459
465
|
untrackedFiles: 0,
|
|
@@ -481,37 +487,37 @@ async function generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch, op
|
|
|
481
487
|
try {
|
|
482
488
|
if (hasBranch && !hasStaged && !hasUnstaged) {
|
|
483
489
|
// Branch only → committed changes since merge-base
|
|
484
|
-
diff = execSync(`git diff ${mergeBaseSha}..HEAD ${GIT_DIFF_FLAGS}
|
|
490
|
+
diff = execSync(`git diff ${mergeBaseSha}..HEAD ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag}`, {
|
|
485
491
|
cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
486
492
|
maxBuffer: 50 * 1024 * 1024
|
|
487
493
|
});
|
|
488
494
|
} else if (hasBranch && hasStaged && !hasUnstaged) {
|
|
489
495
|
// Branch–Staged → staged changes relative to merge-base
|
|
490
|
-
diff = execSync(`git diff --cached ${mergeBaseSha} ${GIT_DIFF_FLAGS}
|
|
496
|
+
diff = execSync(`git diff --cached ${mergeBaseSha} ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag}`, {
|
|
491
497
|
cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
492
498
|
maxBuffer: 50 * 1024 * 1024
|
|
493
499
|
});
|
|
494
500
|
} else if (hasBranch && hasUnstaged) {
|
|
495
501
|
// Branch–Unstaged (or Branch–Untracked) → working tree vs merge-base
|
|
496
|
-
diff = execSync(`git diff ${mergeBaseSha} ${GIT_DIFF_FLAGS}
|
|
502
|
+
diff = execSync(`git diff ${mergeBaseSha} ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag}`, {
|
|
497
503
|
cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
498
504
|
maxBuffer: 50 * 1024 * 1024
|
|
499
505
|
});
|
|
500
506
|
} else if (hasStaged && !hasUnstaged) {
|
|
501
507
|
// Staged only → cached changes
|
|
502
|
-
diff = execSync(`git diff --cached ${GIT_DIFF_FLAGS}
|
|
508
|
+
diff = execSync(`git diff --cached ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag}`, {
|
|
503
509
|
cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
504
510
|
maxBuffer: 50 * 1024 * 1024
|
|
505
511
|
});
|
|
506
512
|
} else if (hasStaged && hasUnstaged) {
|
|
507
513
|
// Staged–Unstaged (or Staged–Untracked) → all changes vs HEAD
|
|
508
|
-
diff = execSync(`git diff HEAD ${GIT_DIFF_FLAGS}
|
|
514
|
+
diff = execSync(`git diff HEAD ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag}`, {
|
|
509
515
|
cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
510
516
|
maxBuffer: 50 * 1024 * 1024
|
|
511
517
|
});
|
|
512
518
|
} else if (hasUnstaged) {
|
|
513
519
|
// Unstaged only or Unstaged–Untracked → working tree changes
|
|
514
|
-
diff = execSync(`git diff ${GIT_DIFF_FLAGS}
|
|
520
|
+
diff = execSync(`git diff ${GIT_DIFF_FLAGS}${contextFlag}${extraArgsStr}${wFlag}`, {
|
|
515
521
|
cwd: repoPath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
516
522
|
maxBuffer: 50 * 1024 * 1024
|
|
517
523
|
});
|
|
@@ -555,7 +561,7 @@ async function generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch, op
|
|
|
555
561
|
const untrackedFiles = await getUntrackedFiles(repoPath);
|
|
556
562
|
stats.untrackedFiles = untrackedFiles.length;
|
|
557
563
|
|
|
558
|
-
const untrackedDiff = generateUntrackedDiffs(repoPath, untrackedFiles, wFlag);
|
|
564
|
+
const untrackedDiff = generateUntrackedDiffs(repoPath, untrackedFiles, wFlag, contextFlag, extraArgsStr);
|
|
559
565
|
if (untrackedDiff) {
|
|
560
566
|
if (diff) diff += '\n';
|
|
561
567
|
diff += untrackedDiff;
|
package/src/main.js
CHANGED
|
@@ -829,6 +829,7 @@ async function performHeadlessReview(args, config, db, flags, options) {
|
|
|
829
829
|
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
830
830
|
const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
|
|
831
831
|
const repoInstructions = repoSettings?.default_instructions || null;
|
|
832
|
+
const globalInstructions = config.globalInstructions || null;
|
|
832
833
|
|
|
833
834
|
// Run AI analysis
|
|
834
835
|
console.log('Running AI analysis (all 3 levels)...');
|
|
@@ -837,8 +838,8 @@ async function performHeadlessReview(args, config, db, flags, options) {
|
|
|
837
838
|
|
|
838
839
|
let analysisSummary = null;
|
|
839
840
|
try {
|
|
840
|
-
// Pass
|
|
841
|
-
const analysisResult = await analyzer.analyzeAllLevels(review.id, worktreePath, storedPRData, null, { repoInstructions });
|
|
841
|
+
// Pass all instruction levels to ensure they're captured in the analysis run
|
|
842
|
+
const analysisResult = await analyzer.analyzeAllLevels(review.id, worktreePath, storedPRData, null, { globalInstructions, repoInstructions });
|
|
842
843
|
analysisSummary = analysisResult.summary;
|
|
843
844
|
console.log('AI analysis completed successfully');
|
|
844
845
|
} catch (analysisError) {
|