Multi-perspective AI-powered code review with Linear integration
AI-powered code review with parallel multi-perspective analysis. Run before PRs to catch bugs, security issues, and architectural problems from expert viewpoints.
/plugin marketplace add duongdev/ccpm/plugin install ccpm@duongdev-ccpm-marketplace[--staged] [--branch=X] [--file=X] [--severity=X] [--multi]AI-powered code review that analyzes changes from multiple perspectives for comprehensive feedback.
With --multi flag, review runs parallel analysis from different expert viewpoints:
| Perspective | Agent | Focus Areas |
|---|---|---|
| Code Quality | ccpm:code-reviewer | Bugs, style, complexity, maintainability |
| Security | ccpm:security-auditor | OWASP Top 10, injection, auth flaws |
| Architecture | ccpm:backend-architect | Patterns, coupling, scalability |
| UX/Accessibility | ccpm:frontend-developer | A11y, UX patterns, responsive design |
Benefits:
This command uses specialized agents for comprehensive review:
ccpm:code-reviewer - Primary review agent (quality, bugs, style)ccpm:security-auditor - Security vulnerability detection (always in --multi, or --security flag)ccpm:backend-architect - Architecture review (in --multi mode)ccpm:frontend-developer - UX/accessibility review (in --multi mode for UI files)ccpm:linear-operations - Post findings to Linear (with --post-to-linear)# Review staged changes
/ccpm:review --staged
# Review current branch against main
/ccpm:review
# Multi-perspective review (RECOMMENDED for PRs)
/ccpm:review --multi
# Review specific branch
/ccpm:review --branch=feature/psn-29-auth
# Review specific file
/ccpm:review --file=src/auth/jwt.ts
# Set severity threshold (info, warning, error)
/ccpm:review --severity=warning
# Review and post to Linear
/ccpm:review --post-to-linear
# Full comprehensive review before PR
/ccpm:review --multi --post-to-linear
let options = {
staged: false,
branch: null,
file: null,
severity: 'info', // info, warning, error
postToLinear: false,
multi: false // Multi-perspective review
};
for (const arg of args) {
if (arg === '--staged') {
options.staged = true;
} else if (arg.startsWith('--branch=')) {
options.branch = arg.replace('--branch=', '');
} else if (arg.startsWith('--file=')) {
options.file = arg.replace('--file=', '');
} else if (arg.startsWith('--severity=')) {
options.severity = arg.replace('--severity=', '');
} else if (arg === '--post-to-linear') {
options.postToLinear = true;
} else if (arg === '--multi') {
options.multi = true;
}
}
// Default: review current branch against main
if (!options.staged && !options.branch && !options.file) {
options.branch = await Bash('git rev-parse --abbrev-ref HEAD');
}
let changes = [];
let diffOutput = '';
if (options.staged) {
// Staged changes only
diffOutput = await Bash('git diff --cached --name-only');
} else if (options.file) {
// Specific file
diffOutput = options.file;
} else if (options.branch) {
// Branch comparison
const baseBranch = 'main';
diffOutput = await Bash(`git diff --name-only ${baseBranch}...${options.branch.trim()}`);
}
const changedFiles = diffOutput.trim().split('\n').filter(f => f.length > 0);
if (changedFiles.length === 0) {
console.log('ā
No changes to review.\n');
console.log('š” Try:');
console.log(' /ccpm:review --staged - Review staged changes');
console.log(' /ccpm:review --file=path/to/file.ts');
return;
}
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
console.log('š Code Review');
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
console.log(`š ${changedFiles.length} file(s) to review:\n`);
changedFiles.forEach(f => console.log(` ⢠${f}`));
console.log('');
When --multi flag is set, run parallel reviews from multiple expert agents:
if (options.multi) {
console.log('\nšÆ Multi-Perspective Review Mode');
console.log(' Running parallel analysis from 4 expert viewpoints...\n');
// Determine which perspectives to use based on file types
const hasBackendFiles = changedFiles.some(f =>
f.match(/\.(ts|js|go|py|java|rb|php)$/) &&
!f.match(/\.(tsx|jsx)$/) &&
(f.includes('api') || f.includes('service') || f.includes('controller') || f.includes('resolver'))
);
const hasFrontendFiles = changedFiles.some(f =>
f.match(/\.(tsx|jsx|css|scss|vue|svelte)$/) ||
f.includes('component') || f.includes('page') || f.includes('ui')
);
// Build parallel review tasks
const reviewTasks = [];
// Always include code quality review
reviewTasks.push({
perspective: 'Code Quality',
agent: 'ccpm:code-reviewer',
icon: 'š',
focus: 'bugs, style, complexity, maintainability, error handling'
});
// Always include security review
reviewTasks.push({
perspective: 'Security',
agent: 'ccpm:security-auditor',
icon: 'š',
focus: 'OWASP Top 10, injection, authentication, authorization, data exposure'
});
// Architecture review for backend files
if (hasBackendFiles) {
reviewTasks.push({
perspective: 'Architecture',
agent: 'ccpm:backend-architect',
icon: 'šļø',
focus: 'patterns, coupling, scalability, SOLID principles, API design'
});
}
// UX/Accessibility review for frontend files
if (hasFrontendFiles) {
reviewTasks.push({
perspective: 'UX/Accessibility',
agent: 'ccpm:frontend-developer',
icon: 'āæ',
focus: 'accessibility (a11y), UX patterns, responsive design, component reuse'
});
}
console.log(` Perspectives: ${reviewTasks.map(t => t.perspective).join(', ')}\n`);
// Prepare combined diff for all files
const allDiffs = [];
for (const file of changedFiles.slice(0, 10)) {
let diff;
if (options.staged) {
diff = await Bash(`git diff --cached -- "${file}"`);
} else if (options.branch) {
diff = await Bash(`git diff main...${options.branch.trim()} -- "${file}"`);
} else {
diff = await Bash(`git diff HEAD -- "${file}"`);
}
allDiffs.push({ file, diff });
}
const combinedDiff = allDiffs.map(d => `### ${d.file}\n\`\`\`diff\n${d.diff}\n\`\`\``).join('\n\n');
// Launch parallel reviews using Task tool
// IMPORTANT: Call all Task tools in a SINGLE message for true parallelism
const parallelResults = await Promise.all(reviewTasks.map(async (task) => {
console.log(` ${task.icon} Starting ${task.perspective} review...`);
const result = await Task({
subagent_type: task.agent,
prompt: `
## ${task.perspective} Code Review
You are reviewing code changes as a **${task.perspective} expert**.
### Your Focus Areas
${task.focus}
### Files Changed
${changedFiles.map(f => `- ${f}`).join('\n')}
### Diffs
${combinedDiff.substring(0, 15000)} // Limit to 15k chars
### Output Format
Return findings as JSON array with perspective tag:
\`\`\`json
[
{
"perspective": "${task.perspective}",
"file": "path/to/file.ts",
"line": 42,
"severity": "error|warning|info",
"category": "specific-category",
"message": "Description of the issue",
"suggestion": "How to fix it"
}
]
\`\`\`
If no issues found in your expertise area, return: []
`
});
return { task, result };
}));
// Collect all findings from all perspectives
const allFindings = [];
for (const { task, result } of parallelResults) {
try {
const perspectiveFindings = JSON.parse(result.match(/\[[\s\S]*\]/)?.[0] || '[]');
perspectiveFindings.forEach(f => {
f.perspective = task.perspective;
f.icon = task.icon;
allFindings.push(f);
});
console.log(` ${task.icon} ${task.perspective}: ${perspectiveFindings.length} finding(s)`);
} catch (e) {
console.log(` ${task.icon} ${task.perspective}: Could not parse findings`);
}
}
// Continue to Step 4 with allFindings
findings.push(...allFindings);
} else {
// Standard single-perspective review (existing logic)
for (const file of changedFiles.slice(0, 10)) { // Limit to 10 files
console.log(`\nš Reviewing: ${file}...`);
// Get file extension for language detection
const ext = file.split('.').pop();
const language = getLanguage(ext);
// Get the diff for this file
let diff;
if (options.staged) {
diff = await Bash(`git diff --cached -- "${file}"`);
} else if (options.branch) {
diff = await Bash(`git diff main...${options.branch.trim()} -- "${file}"`);
} else {
diff = await Bash(`git diff HEAD -- "${file}"`);
}
// Get full file content for context
const content = await Read(file).catch(() => null);
// Use code-reviewer agent for analysis
const reviewResult = await Task({
subagent_type: 'ccpm:code-reviewer',
prompt: `
## Code Review Request
**File**: ${file}
**Language**: ${language}
### Diff
\`\`\`diff
${diff}
\`\`\`
### Full File (for context)
\`\`\`${language}
${content?.substring(0, 5000) || 'File not readable'}
\`\`\`
### Review Criteria
Please analyze for:
1. **Security Issues** (severity: error)
- SQL injection, XSS, command injection
- Hardcoded secrets, insecure defaults
- Missing input validation
2. **Bugs** (severity: error)
- Logic errors, null pointer risks
- Race conditions, memory leaks
- Unhandled exceptions
3. **Code Quality** (severity: warning)
- Code duplication
- Complex functions (cyclomatic complexity)
- Missing error handling
- Unclear naming
4. **Best Practices** (severity: info)
- TypeScript type safety
- Missing documentation for public APIs
- Deprecated APIs usage
- Performance concerns
### Output Format
Return findings as JSON array:
\`\`\`json
[
{
"line": 42,
"severity": "error|warning|info",
"category": "security|bug|quality|practice",
"message": "Description of the issue",
"suggestion": "How to fix it"
}
]
\`\`\`
If no issues found, return empty array: []
`
});
// Parse findings
try {
const fileFindings = JSON.parse(reviewResult.match(/\[[\s\S]*\]/)?.[0] || '[]');
fileFindings.forEach(f => {
f.file = file;
findings.push(f);
});
} catch (e) {
// Agent returned prose instead of JSON - extract key points
console.log(` ā ļø Could not parse structured findings`);
}
}
// Filter by severity threshold
const severityOrder = { 'error': 3, 'warning': 2, 'info': 1 };
const thresholdLevel = severityOrder[options.severity] || 1;
const filteredFindings = findings.filter(f =>
severityOrder[f.severity] >= thresholdLevel
);
// Group by file
const byFile = {};
filteredFindings.forEach(f => {
if (!byFile[f.file]) byFile[f.file] = [];
byFile[f.file].push(f);
});
// Count by severity
const counts = {
error: filteredFindings.filter(f => f.severity === 'error').length,
warning: filteredFindings.filter(f => f.severity === 'warning').length,
info: filteredFindings.filter(f => f.severity === 'info').length
};
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
console.log('š Review Summary');
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
// Summary bar
if (counts.error > 0) {
console.log(`š“ ${counts.error} error(s)`);
}
if (counts.warning > 0) {
console.log(`š” ${counts.warning} warning(s)`);
}
if (counts.info > 0) {
console.log(`šµ ${counts.info} info`);
}
if (filteredFindings.length === 0) {
console.log('ā
No issues found!\n');
console.log('Great job! Your code passes all checks.');
return;
}
console.log('');
// Display findings by file
for (const [file, fileFindings] of Object.entries(byFile)) {
console.log(`\nš ${file}`);
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
fileFindings.forEach(f => {
const icon = f.severity === 'error' ? 'š“' : f.severity === 'warning' ? 'š”' : 'šµ';
const category = `[${f.category.toUpperCase()}]`;
console.log(`\n${icon} Line ${f.line}: ${category}`);
console.log(` ${f.message}`);
if (f.suggestion) {
console.log(` š” ${f.suggestion}`);
}
});
}
// Overall assessment
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
console.log('š Assessment');
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
if (counts.error > 0) {
console.log('ā Review Status: NEEDS WORK');
console.log(' Errors must be fixed before merging.');
} else if (counts.warning > 5) {
console.log('ā ļø Review Status: NEEDS ATTENTION');
console.log(' Consider addressing warnings before merging.');
} else {
console.log('ā
Review Status: APPROVED');
console.log(' Minor suggestions can be addressed if time permits.');
}
if (options.postToLinear && filteredFindings.length > 0) {
// Detect issue from branch
const branch = options.branch || await Bash('git rev-parse --abbrev-ref HEAD');
const issueMatch = branch.match(/([A-Z]+-\d+)/);
if (issueMatch) {
const issueId = issueMatch[1];
console.log(`\nš¤ Posting review to Linear issue ${issueId}...`);
// Format findings for Linear
const findingsMarkdown = Object.entries(byFile).map(([file, fileFindings]) => {
const items = fileFindings.map(f => {
const icon = f.severity === 'error' ? 'š“' : f.severity === 'warning' ? 'š”' : 'šµ';
return `- ${icon} Line ${f.line}: ${f.message}`;
}).join('\n');
return `**${file}**\n${items}`;
}).join('\n\n');
await Task({
subagent_type: 'ccpm:linear-operations',
prompt: `operation: create_comment
params:
issueId: ${issueId}
body: |
š **Code Review** | ${changedFiles.length} files
**Summary**: ${counts.error} errors, ${counts.warning} warnings, ${counts.info} info
+++ š Detailed Findings
${findingsMarkdown}
+++
context:
command: review`
});
console.log(`ā
Review posted to ${issueId}`);
} else {
console.log('\nā ļø Could not detect issue ID from branch. Use --post-to-linear with an issue branch.');
}
}
if (counts.error > 0) {
const answer = await AskUserQuestion({
questions: [{
question: "Would you like AI to help fix the errors?",
header: "Fix Errors",
multiSelect: false,
options: [
{ label: "Yes, fix all errors", description: "AI will apply fixes automatically" },
{ label: "Fix one at a time", description: "Review each fix before applying" },
{ label: "No, I'll fix manually", description: "Just show the issues" }
]
}]
});
if (answer === "Yes, fix all errors" || answer === "Fix one at a time") {
const errorFindings = filteredFindings.filter(f => f.severity === 'error');
const fixOneAtATime = (answer === "Fix one at a time");
for (const finding of errorFindings) {
console.log(`\nš§ Fixing: ${finding.file}:${finding.line}`);
console.log(` ${finding.message}`);
if (fixOneAtATime) {
const confirm = await AskUserQuestion({
questions: [{
question: `Apply fix: ${finding.suggestion}?`,
header: "Confirm Fix",
multiSelect: false,
options: [
{ label: "Apply", description: "Apply this fix" },
{ label: "Skip", description: "Skip this fix" }
]
}]
});
if (confirm === "Skip") continue;
}
// Apply fix via specialized agent
await Task({
subagent_type: 'general-purpose',
prompt: `
Fix this code issue in ${finding.file} at line ${finding.line}:
Issue: ${finding.message}
Suggestion: ${finding.suggestion}
Apply the fix directly using the Edit tool.
`
});
console.log(` ā
Fixed`);
}
console.log('\nā
All selected fixes applied.');
console.log('š” Run /ccpm:review again to verify.');
}
}
function getLanguage(ext) {
const map = {
'ts': 'typescript',
'tsx': 'typescript',
'js': 'javascript',
'jsx': 'javascript',
'py': 'python',
'rb': 'ruby',
'go': 'go',
'rs': 'rust',
'java': 'java',
'kt': 'kotlin',
'swift': 'swift',
'php': 'php',
'cs': 'csharp',
'cpp': 'cpp',
'c': 'c',
'md': 'markdown',
'json': 'json',
'yaml': 'yaml',
'yml': 'yaml'
};
return map[ext] || 'text';
}
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š Code Review
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š 3 file(s) to review:
⢠src/auth/jwt.ts
⢠src/auth/middleware.ts
⢠src/routes/auth.ts
š Reviewing: src/auth/jwt.ts...
š Reviewing: src/auth/middleware.ts...
š Reviewing: src/routes/auth.ts...
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š Review Summary
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š“ 2 error(s)
š” 3 warning(s)
šµ 5 info
š src/auth/jwt.ts
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š“ Line 42: [SECURITY]
JWT secret is hardcoded in source code
š” Use environment variable: process.env.JWT_SECRET
š” Line 58: [QUALITY]
Token expiry is very short (5 minutes)
š” Consider 15-30 minutes for better UX
š src/routes/auth.ts
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š“ Line 23: [BUG]
Missing null check on user.email
š” Add: if (!user?.email) return res.status(400)...
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š Assessment
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Review Status: NEEDS WORK
Errors must be fixed before merging.
Would you like AI to help fix the errors?
> Yes, fix all errors
Fix one at a time
No, I'll fix manually
| Category | Severity | Examples |
|---|---|---|
| Security | error | SQL injection, XSS, hardcoded secrets |
| Bug | error | Null pointer, logic error, race condition |
| Quality | warning | Duplication, complexity, missing error handling |
| Practice | info | Type safety, documentation, deprecation |
code-reviewer agent for deep analysis+++ collapsible syntax/ccpm:verify workflow