@dreki-gg/pi-code-reviewer 0.1.0 → 0.2.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 +4 -4
- package/extensions/code-reviewer/commands/review-tool.ts +14 -2
- package/extensions/code-reviewer/commands/review.ts +31 -6
- package/extensions/code-reviewer/reviewer.ts +48 -25
- package/extensions/code-reviewer/types.ts +2 -0
- package/package.json +1 -1
- package/skills/code-review/SKILL.md +3 -3
- package/skills/code-review/lenses/accessibility.md +20 -0
- package/skills/code-review/lenses/product-vision.md +11 -11
- package/skills/code-review/lenses/ux-design.md +0 -20
package/README.md
CHANGED
|
@@ -50,8 +50,8 @@ Evaluates changes for correctness and adherence to project standards.
|
|
|
50
50
|
- Does the change follow naming conventions?
|
|
51
51
|
|
|
52
52
|
## Tools
|
|
53
|
-
- `
|
|
54
|
-
- `
|
|
53
|
+
- `npm run typecheck`
|
|
54
|
+
- `npm run lint`
|
|
55
55
|
|
|
56
56
|
## Severity
|
|
57
57
|
- blocker: Type errors, unresolved imports
|
|
@@ -67,8 +67,8 @@ The package ships with four example lenses:
|
|
|
67
67
|
| --- | --- |
|
|
68
68
|
| `code-quality` | Correctness, lint, types, dead code |
|
|
69
69
|
| `maintainability` | Coupling, complexity, readability |
|
|
70
|
-
| `product-vision` |
|
|
71
|
-
| `
|
|
70
|
+
| `product-vision` | Traces changes back to their originating issue or design doc, checks goal alignment |
|
|
71
|
+
| `accessibility` | Semantic HTML, keyboard navigation, ARIA, screen reader compatibility |
|
|
72
72
|
|
|
73
73
|
Run `/review-init` to scaffold these (customized for your project's tools) into `.code-review/lenses/`.
|
|
74
74
|
|
|
@@ -38,7 +38,7 @@ export function registerReviewTool(pi: ExtensionAPI) {
|
|
|
38
38
|
),
|
|
39
39
|
}),
|
|
40
40
|
|
|
41
|
-
async execute(_toolCallId, params, signal,
|
|
41
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
42
42
|
const cwd = ctx.cwd;
|
|
43
43
|
const config = loadConfig(cwd);
|
|
44
44
|
const lensDir = getLensDir(cwd, config);
|
|
@@ -58,12 +58,14 @@ export function registerReviewTool(pi: ExtensionAPI) {
|
|
|
58
58
|
|
|
59
59
|
const lensNames = resolveLensNames(params.lenses, config, available);
|
|
60
60
|
|
|
61
|
+
ctx.ui.setStatus('code-review', '🔍 Collecting diff...');
|
|
61
62
|
const diff = await collectDiff(pi, cwd, {
|
|
62
63
|
base: params.base,
|
|
63
64
|
staged: params.staged,
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
if (!diff.diff.trim()) {
|
|
68
|
+
ctx.ui.setStatus('code-review', undefined);
|
|
67
69
|
return {
|
|
68
70
|
content: [{ type: 'text', text: 'No changes to review.' }],
|
|
69
71
|
details: {},
|
|
@@ -71,15 +73,25 @@ export function registerReviewTool(pi: ExtensionAPI) {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const results: LensResult[] = [];
|
|
74
|
-
for (
|
|
76
|
+
for (let i = 0; i < lensNames.length; i++) {
|
|
75
77
|
if (signal?.aborted) break;
|
|
76
78
|
|
|
79
|
+
const name = lensNames[i];
|
|
80
|
+
const progressMsg = `Lens ${i + 1}/${lensNames.length}: ${name}`;
|
|
81
|
+
ctx.ui.setStatus('code-review', `🔍 ${progressMsg}`);
|
|
82
|
+
onUpdate?.({
|
|
83
|
+
content: [{ type: 'text', text: progressMsg }],
|
|
84
|
+
details: { currentLens: name, lensIndex: i + 1, totalLenses: lensNames.length },
|
|
85
|
+
});
|
|
86
|
+
|
|
77
87
|
const lens = available.get(name)!;
|
|
78
88
|
const content = getLensContent(lensDir, name) ?? '';
|
|
79
89
|
const result = await reviewWithLens(pi, ctx, cwd, lens, content, diff, signal);
|
|
80
90
|
results.push(result);
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
ctx.ui.setStatus('code-review', undefined);
|
|
94
|
+
|
|
83
95
|
const report: ReviewReport = {
|
|
84
96
|
diff: diff.diff,
|
|
85
97
|
diffStat: diff.stat,
|
|
@@ -3,7 +3,7 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
|
3
3
|
import { loadConfig, getLensDir } from '../config';
|
|
4
4
|
import { collectDiff } from '../diff';
|
|
5
5
|
import { discoverLenses, getLensContent } from '../lenses';
|
|
6
|
-
import { reviewWithLens } from '../reviewer';
|
|
6
|
+
import { reviewWithLens, buildDiffSection } from '../reviewer';
|
|
7
7
|
import { parseReviewArgs } from '../parse-args';
|
|
8
8
|
|
|
9
9
|
export function registerReviewCommand(pi: ExtensionAPI) {
|
|
@@ -34,36 +34,61 @@ export function registerReviewCommand(pi: ExtensionAPI) {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
ctx.ui.setStatus('code-review', '🔍 Collecting diff...');
|
|
37
38
|
const diff = await collectDiff(pi, cwd, {
|
|
38
39
|
base: parsed.base,
|
|
39
40
|
staged: parsed.staged,
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
if (!diff.diff.trim()) {
|
|
44
|
+
ctx.ui.setStatus('code-review', undefined);
|
|
43
45
|
ctx.ui.notify('No changes to review', 'info');
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
ctx.ui.notify(`Reviewing ${diff.label} through ${lensNames.length} lens(es)...`, 'info');
|
|
50
|
+
ctx.ui.setStatus('code-review', `🔍 Reviewing (0/${lensNames.length})...`);
|
|
51
|
+
|
|
52
|
+
const lensSections: string[] = [];
|
|
53
|
+
for (let i = 0; i < lensNames.length; i++) {
|
|
54
|
+
const name = lensNames[i];
|
|
55
|
+
ctx.ui.setStatus('code-review', `🔍 Lens ${i + 1}/${lensNames.length}: ${name}`);
|
|
48
56
|
|
|
49
|
-
const lensPrompts: string[] = [];
|
|
50
|
-
for (const name of lensNames) {
|
|
51
57
|
const lens = available.get(name)!;
|
|
52
58
|
const content = getLensContent(lensDir, name) ?? '';
|
|
53
59
|
const result = await reviewWithLens(pi, ctx, cwd, lens, content, diff);
|
|
54
60
|
|
|
55
|
-
if (result.
|
|
56
|
-
|
|
61
|
+
if (result._lensSection) {
|
|
62
|
+
lensSections.push(result._lensSection);
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
ctx.ui.setStatus('code-review', undefined);
|
|
67
|
+
|
|
60
68
|
const combinedPrompt = [
|
|
61
69
|
`Review the following changes through ${lensNames.length} lens(es): ${lensNames.join(', ')}.`,
|
|
62
70
|
'',
|
|
63
71
|
'For each lens, evaluate the diff against its criteria and produce findings.',
|
|
64
72
|
'Output your review as a structured report with sections per lens.',
|
|
65
73
|
'',
|
|
66
|
-
|
|
74
|
+
buildDiffSection(diff),
|
|
75
|
+
'',
|
|
76
|
+
'## Lenses',
|
|
77
|
+
'',
|
|
78
|
+
...lensSections,
|
|
79
|
+
'',
|
|
80
|
+
'## Instructions',
|
|
81
|
+
'',
|
|
82
|
+
'For each lens above, review the diff and output a JSON array of findings:',
|
|
83
|
+
'',
|
|
84
|
+
'```json',
|
|
85
|
+
'[',
|
|
86
|
+
' { "file": "path/to/file.ts", "line": 42, "severity": "warning", "message": "Description" }',
|
|
87
|
+
']',
|
|
88
|
+
'```',
|
|
89
|
+
'',
|
|
90
|
+
'After each lens JSON array, write a 2-3 sentence summary.',
|
|
91
|
+
'If there are no findings for a lens, return an empty array `[]` and note the code looks good.',
|
|
67
92
|
].join('\n');
|
|
68
93
|
|
|
69
94
|
pi.sendUserMessage(combinedPrompt, { deliverAs: 'followUp' });
|
|
@@ -33,23 +33,13 @@ async function runLensTools(
|
|
|
33
33
|
return outputs;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** Build the review prompt
|
|
37
|
-
function
|
|
38
|
-
lens: LensConfig,
|
|
39
|
-
lensContent: string,
|
|
40
|
-
diff: DiffSource,
|
|
41
|
-
toolOutputs: Record<string, string>,
|
|
42
|
-
): string {
|
|
36
|
+
/** Build the shared diff section of the review prompt (included once). */
|
|
37
|
+
export function buildDiffSection(diff: DiffSource): string {
|
|
43
38
|
const parts: string[] = [];
|
|
44
|
-
|
|
45
|
-
parts.push(`You are reviewing code changes through the "${lens.name}" lens.`);
|
|
46
|
-
parts.push('');
|
|
47
|
-
parts.push('## Lens Definition');
|
|
48
|
-
parts.push(lensContent);
|
|
49
|
-
parts.push('');
|
|
50
|
-
parts.push(`## Diff (${diff.label})`);
|
|
51
39
|
const maxDiffLen = 50_000;
|
|
52
40
|
const diffTruncated = diff.diff.length > maxDiffLen;
|
|
41
|
+
|
|
42
|
+
parts.push(`## Diff (${diff.label})`);
|
|
53
43
|
parts.push('```diff');
|
|
54
44
|
parts.push(diff.diff.slice(0, maxDiffLen));
|
|
55
45
|
parts.push('```');
|
|
@@ -64,21 +54,60 @@ function buildReviewPrompt(
|
|
|
64
54
|
parts.push(diff.stat);
|
|
65
55
|
parts.push('```');
|
|
66
56
|
|
|
57
|
+
return parts.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Build the lens-specific section of the review prompt (no diff duplication). */
|
|
61
|
+
function buildLensSection(
|
|
62
|
+
lens: LensConfig,
|
|
63
|
+
lensContent: string,
|
|
64
|
+
toolOutputs: Record<string, string>,
|
|
65
|
+
): string {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
|
|
68
|
+
parts.push(`### Lens: ${lens.name}`);
|
|
69
|
+
parts.push('');
|
|
70
|
+
parts.push('#### Lens Definition');
|
|
71
|
+
parts.push(lensContent);
|
|
72
|
+
|
|
67
73
|
if (Object.keys(toolOutputs).length > 0) {
|
|
68
74
|
parts.push('');
|
|
69
|
-
parts.push('
|
|
75
|
+
parts.push('#### Tool Outputs');
|
|
70
76
|
for (const [cmd, output] of Object.entries(toolOutputs)) {
|
|
71
|
-
parts.push(
|
|
77
|
+
parts.push(`##### \`${cmd}\``);
|
|
72
78
|
parts.push('```');
|
|
73
79
|
parts.push(output.slice(0, 20_000));
|
|
74
80
|
parts.push('```');
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
parts.push('');
|
|
85
|
+
parts.push('#### Severity levels');
|
|
86
|
+
if (lens.severityRules.blocker) parts.push(`- **blocker**: ${lens.severityRules.blocker}`);
|
|
87
|
+
if (lens.severityRules.warning) parts.push(`- **warning**: ${lens.severityRules.warning}`);
|
|
88
|
+
if (lens.severityRules.note) parts.push(`- **note**: ${lens.severityRules.note}`);
|
|
89
|
+
|
|
90
|
+
return parts.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Build the full review prompt for a single lens (includes diff — used by the tool path). */
|
|
94
|
+
function buildReviewPrompt(
|
|
95
|
+
lens: LensConfig,
|
|
96
|
+
lensContent: string,
|
|
97
|
+
diff: DiffSource,
|
|
98
|
+
toolOutputs: Record<string, string>,
|
|
99
|
+
): string {
|
|
100
|
+
const parts: string[] = [];
|
|
101
|
+
|
|
102
|
+
parts.push(`You are reviewing code changes through the "${lens.name}" lens.`);
|
|
103
|
+
parts.push('');
|
|
104
|
+
parts.push(buildDiffSection(diff));
|
|
105
|
+
parts.push('');
|
|
106
|
+
parts.push(buildLensSection(lens, lensContent, toolOutputs));
|
|
78
107
|
parts.push('');
|
|
79
108
|
parts.push('## Instructions');
|
|
80
109
|
parts.push('');
|
|
81
|
-
parts.push('Review the diff through this lens. For each finding, output a JSON array:');
|
|
110
|
+
parts.push('Review the diff above through this lens. For each finding, output a JSON array:');
|
|
82
111
|
parts.push('');
|
|
83
112
|
parts.push('```json');
|
|
84
113
|
parts.push('[');
|
|
@@ -88,11 +117,6 @@ function buildReviewPrompt(
|
|
|
88
117
|
parts.push(']');
|
|
89
118
|
parts.push('```');
|
|
90
119
|
parts.push('');
|
|
91
|
-
parts.push('Severity levels:');
|
|
92
|
-
if (lens.severityRules.blocker) parts.push(`- **blocker**: ${lens.severityRules.blocker}`);
|
|
93
|
-
if (lens.severityRules.warning) parts.push(`- **warning**: ${lens.severityRules.warning}`);
|
|
94
|
-
if (lens.severityRules.note) parts.push(`- **note**: ${lens.severityRules.note}`);
|
|
95
|
-
parts.push('');
|
|
96
120
|
parts.push(
|
|
97
121
|
'After the JSON array, write a 2-3 sentence summary of your review through this lens.',
|
|
98
122
|
);
|
|
@@ -116,15 +140,14 @@ export async function reviewWithLens(
|
|
|
116
140
|
|
|
117
141
|
// Build the prompt
|
|
118
142
|
const prompt = buildReviewPrompt(lens, lensContent, diff, toolOutputs);
|
|
143
|
+
const lensSection = buildLensSection(lens, lensContent, toolOutputs);
|
|
119
144
|
|
|
120
|
-
// Use sendMessage to delegate to the agent for review
|
|
121
|
-
// For now, we return the prompt and let the command handler
|
|
122
|
-
// pass it to a subagent via the subagent tool
|
|
123
145
|
return {
|
|
124
146
|
lens: lens.name,
|
|
125
147
|
findings: [],
|
|
126
148
|
summary: '',
|
|
127
149
|
toolOutputs,
|
|
128
150
|
_prompt: prompt,
|
|
151
|
+
_lensSection: lensSection,
|
|
129
152
|
};
|
|
130
153
|
}
|
|
@@ -22,6 +22,8 @@ export type LensResult = {
|
|
|
22
22
|
toolOutputs?: Record<string, string>;
|
|
23
23
|
/** Review prompt built for this lens, used internally to delegate to the agent. */
|
|
24
24
|
_prompt?: string;
|
|
25
|
+
/** Lens-specific section (without diff), used by /review command to avoid diff duplication. */
|
|
26
|
+
_lensSection?: string;
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export type ReviewConfig = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: code-review
|
|
3
|
-
description: Multi-lens code review against working directory changes. Evaluates diffs through configurable criteria like code-quality, maintainability, product-vision, and
|
|
3
|
+
description: Multi-lens code review against working directory changes. Evaluates diffs through configurable criteria like code-quality, maintainability, product-vision, and accessibility. Use when user says "review my changes", "review this code", "check before committing", or "code review".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Code Review
|
|
@@ -36,8 +36,8 @@ Description of what this lens evaluates.
|
|
|
36
36
|
- Evaluation point 2
|
|
37
37
|
|
|
38
38
|
## Tools
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
39
|
+
- `npm run typecheck`
|
|
40
|
+
- `npm run lint`
|
|
41
41
|
|
|
42
42
|
## Severity
|
|
43
43
|
- blocker: What constitutes a blocking issue
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
Evaluates UI changes for accessibility compliance — semantic markup, keyboard support, screen reader compatibility, and inclusive design patterns.
|
|
4
|
+
|
|
5
|
+
## Criteria
|
|
6
|
+
- Are interactive elements accessible via keyboard (tab order, Enter/Space activation, Escape to dismiss)?
|
|
7
|
+
- Do custom components use appropriate ARIA roles, states, and properties?
|
|
8
|
+
- Are images and icons accompanied by meaningful alt text or aria-label?
|
|
9
|
+
- Are form inputs associated with visible labels (not just placeholder text)?
|
|
10
|
+
- Does the change use semantic HTML elements (button, nav, main, dialog) instead of generic divs with click handlers?
|
|
11
|
+
- Is focus managed correctly after dynamic content changes (modals, route transitions, toast notifications)?
|
|
12
|
+
- Are error and validation messages announced to screen readers (aria-live, role="alert")?
|
|
13
|
+
- Does the color usage rely solely on color to convey meaning, or is there a secondary indicator (icon, text)?
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
## Severity
|
|
18
|
+
- blocker: Interactive elements unreachable by keyboard, missing labels on form inputs, click handlers on non-interactive elements without ARIA roles
|
|
19
|
+
- warning: Missing alt text, placeholder-only labels, missing aria-live on dynamic content, focus not managed after modals
|
|
20
|
+
- note: Semantic HTML improvements, ARIA attribute refinements, color contrast suggestions
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Product Vision
|
|
2
2
|
|
|
3
|
-
Evaluates whether changes
|
|
3
|
+
Evaluates whether changes fulfill the intent behind the work — traces the diff back to its originating issue, PR description, or design doc and checks alignment.
|
|
4
4
|
|
|
5
5
|
## Criteria
|
|
6
|
-
-
|
|
7
|
-
- Does
|
|
8
|
-
-
|
|
9
|
-
- Does
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
6
|
+
- Is there a linked issue, PR description, or design document that explains why this change was requested?
|
|
7
|
+
- Does the diff actually accomplish the stated goal, or does it drift into unrelated concerns?
|
|
8
|
+
- Are there parts of the requirement left unaddressed by the change?
|
|
9
|
+
- Does the change introduce scope creep beyond what was requested?
|
|
10
|
+
- Does it follow the project's domain language (check CONTEXT.md, glossary, or naming conventions)?
|
|
11
|
+
- Are user-facing strings consistent with the existing product vocabulary?
|
|
12
|
+
- If the change modifies a public API or user flow, is that justified by the original request?
|
|
13
13
|
|
|
14
14
|
## Tools
|
|
15
15
|
|
|
16
16
|
## Severity
|
|
17
|
-
- blocker:
|
|
18
|
-
- warning:
|
|
19
|
-
- note: Opportunities to better align with
|
|
17
|
+
- blocker: Change does not accomplish the stated goal, or contradicts the originating requirement
|
|
18
|
+
- warning: Scope creep beyond the request, unaddressed requirements, inconsistent domain language
|
|
19
|
+
- note: Opportunities to better align naming or structure with the stated intent
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# UX Design
|
|
2
|
-
|
|
3
|
-
Evaluates the user experience quality of UI changes — accessibility, responsiveness, and interaction design.
|
|
4
|
-
|
|
5
|
-
## Criteria
|
|
6
|
-
- Are interactive elements accessible (keyboard navigation, ARIA labels, focus management)?
|
|
7
|
-
- Are loading and error states handled visually?
|
|
8
|
-
- Is the layout responsive or does it break at common viewport sizes?
|
|
9
|
-
- Are animations purposeful and not distracting (100-200ms, no bouncy effects)?
|
|
10
|
-
- Is text readable (minimum 12px body, sufficient contrast)?
|
|
11
|
-
- Are form inputs properly labeled with clear validation feedback?
|
|
12
|
-
- Does the change follow the project's existing component patterns?
|
|
13
|
-
- Are click targets large enough for touch (minimum 44px)?
|
|
14
|
-
|
|
15
|
-
## Tools
|
|
16
|
-
|
|
17
|
-
## Severity
|
|
18
|
-
- blocker: Missing keyboard accessibility on interactive elements, broken layout, no error states on data-fetching UI
|
|
19
|
-
- warning: Missing ARIA labels, tiny text (<12px), missing loading states, non-responsive layout
|
|
20
|
-
- note: Animation polish, spacing consistency, component reuse opportunities
|