@aaronshaf/ger 0.1.5 → 0.1.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -89,7 +89,7 @@ interface ReviewOptions {
89
89
  comment?: boolean
90
90
  yes?: boolean
91
91
  prompt?: string
92
- provider?: string
92
+ tool?: string
93
93
  systemPrompt?: string
94
94
  }
95
95
 
@@ -336,7 +336,7 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
336
336
  }
337
337
 
338
338
  // Select strategy based on user preference
339
- const selectedStrategy = yield* reviewStrategy.selectStrategy(options.provider)
339
+ const selectedStrategy = yield* reviewStrategy.selectStrategy(options.tool)
340
340
  yield* Console.log(`✓ Using AI tool: ${selectedStrategy.name}`)
341
341
 
342
342
  // Load custom review prompt if provided
package/src/cli/index.ts CHANGED
@@ -368,12 +368,12 @@ program
368
368
  .option('-y, --yes', 'Skip confirmation prompts when posting comments')
369
369
  .option('--debug', 'Show debug output including AI responses')
370
370
  .option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
371
- .option('--provider <provider>', 'Preferred AI provider (claude-sdk, claude, gemini, opencode)')
371
+ .option('--tool <tool>', 'Preferred AI tool (claude, gemini, opencode)')
372
372
  .option('--system-prompt <prompt>', 'Custom system prompt for the AI')
373
373
  .addHelpText(
374
374
  'after',
375
375
  `
376
- This command uses AI (Claude SDK, claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
376
+ This command uses AI (claude CLI, gemini CLI, or opencode CLI) to review a Gerrit change.
377
377
  It performs a two-stage review process:
378
378
 
379
379
  1. Generates inline comments for specific code issues
@@ -384,7 +384,7 @@ Use --comment to post the review to Gerrit (with confirmation prompts).
384
384
  Use --comment --yes to post without confirmation.
385
385
 
386
386
  Requirements:
387
- - One of these AI tools must be available: Claude SDK (ANTHROPIC_API_KEY), claude CLI, gemini CLI, or opencode CLI
387
+ - One of these AI tools must be available: claude CLI, gemini CLI, or opencode CLI
388
388
  - Gerrit credentials must be configured (run 'ger setup' first)
389
389
 
390
390
  Examples:
@@ -397,6 +397,9 @@ Examples:
397
397
  # Review and auto-post comments without prompting
398
398
  $ ger review 12345 --comment --yes
399
399
 
400
+ # Use specific AI tool
401
+ $ ger review 12345 --tool gemini
402
+
400
403
  # Show debug output to troubleshoot issues
401
404
  $ ger review 12345 --debug
402
405
  `,
@@ -408,7 +411,7 @@ Examples:
408
411
  yes: options.yes,
409
412
  debug: options.debug,
410
413
  prompt: options.prompt,
411
- provider: options.provider,
414
+ tool: options.tool,
412
415
  systemPrompt: options.systemPrompt,
413
416
  }).pipe(
414
417
  Effect.provide(ReviewStrategyServiceLive),
@@ -1,9 +1,15 @@
1
- ## CRITICAL OUTPUT REQUIREMENT
1
+ ## IMMEDIATE TASK: ANALYZE CODE AND GENERATE INLINE COMMENTS
2
2
 
3
- **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
4
- **NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors.**
3
+ **YOU MUST ANALYZE THE PROVIDED CODE CHANGES RIGHT NOW AND GENERATE INLINE COMMENTS.**
5
4
 
6
- Output ONLY a JSON array wrapped in response tags. **EMPTY ARRAY IS PERFECTLY VALID** for clean code without issues. No other text before or after the tags.
5
+ **CRITICAL OUTPUT REQUIREMENT:**
6
+ - YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS
7
+ - NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors
8
+ - Output ONLY a JSON array wrapped in response tags
9
+ - **EMPTY ARRAY IS PERFECTLY VALID** for clean code without issues
10
+ - No other text before or after the tags
11
+
12
+ **START YOUR ANALYSIS NOW. DO NOT ASK QUESTIONS. DO NOT WAIT FOR MORE INPUT.**
7
13
 
8
14
  ## JSON Structure for Inline Comments
9
15
 
@@ -21,12 +27,17 @@ The JSON array must contain inline comment objects with these fields:
21
27
  - "start_character": Optional column start (integer)
22
28
  - "end_character": Optional column end (integer)
23
29
 
24
- **IMPORTANT**: Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
30
+ **CRITICAL LINE NUMBER RULES:**
31
+ 1. **ALWAYS use final file line numbers, NEVER diff line numbers**
32
+ 2. Line numbers must match the NEW version of the file after all changes
33
+ 3. Use `git show HEAD:path/to/file` or examine the final file to get correct line numbers
34
+ 4. If you see "+50" in a diff, the actual line number is NOT 50 - check the final file
35
+ 5. Every comment MUST have either "line" OR "range". Comments without valid line numbers will be rejected.
25
36
 
26
37
  ### Optional Fields
27
38
  - "side": "REVISION" (new code, default) or "PARENT" (original code)
28
39
 
29
- Line numbers refer to the final file (REVISION), not the diff.
40
+ **VERIFICATION STEP**: Before adding any comment, verify the line number by checking the final file content to ensure your line number points to the exact code you're commenting on.
30
41
 
31
42
  ## Comment Quality Guidelines
32
43
 
@@ -85,11 +96,11 @@ You are running in a git repository with full access to:
85
96
  - All project files for architectural understanding
86
97
  - Use these commands to provide comprehensive, accurate reviews
87
98
 
88
- ## FINAL REMINDER
99
+ ## FINAL TASK INSTRUCTION
89
100
 
90
- **CRITICAL: Your ENTIRE output must be a JSON array wrapped in <response></response> tags.**
101
+ **ANALYZE THE CODE CHANGES NOW AND OUTPUT YOUR INLINE COMMENTS IMMEDIATELY.**
91
102
 
92
- Example formats:
103
+ Your output format must be:
93
104
  ```
94
105
  <response>
95
106
  []
@@ -97,6 +108,8 @@ Example formats:
97
108
  ```
98
109
  (Empty array for clean code - this is GOOD!)
99
110
 
111
+ OR:
112
+
100
113
  ```
101
114
  <response>
102
115
  [{"file": "auth.js", "line": 42, "message": "🤖 SQL injection vulnerability: query uses string concatenation"}]
@@ -104,10 +117,12 @@ Example formats:
104
117
  ```
105
118
  (Only comment on real problems)
106
119
 
107
- **REQUIREMENTS**:
120
+ **CRITICAL REQUIREMENTS**:
108
121
  - Every message must start with "🤖 "
109
122
  - Never use backticks in your response
110
123
  - Empty arrays are encouraged for clean code
111
124
  - Focus on bugs, security, crashes - ignore style preferences
112
125
  - Use git commands to understand context before commenting
113
- - NO TEXT OUTSIDE THE <response></response> TAGS
126
+ - NO TEXT OUTSIDE THE <response></response> TAGS
127
+
128
+ **DO YOUR ANALYSIS NOW. STOP ASKING QUESTIONS. GENERATE THE REVIEW.**
@@ -41,11 +41,16 @@ Gerrit uses a LIMITED markdown subset. Follow these rules EXACTLY:
41
41
 
42
42
  **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
43
43
 
44
+ **IMMEDIATE TASK**: Analyze the code changes provided below and write a comprehensive engineering review.
45
+
44
46
  Start with "🤖 [Your Tool Name] ([Your Model])" then provide a **CONCISE** engineering assessment. Examples:
45
47
  - If you are Claude Sonnet 4: "🤖 Claude (Sonnet 4)"
48
+ - If you are Gemini: "🤖 Gemini (1.5 Pro)" or "🤖 Gemini (1.5 Flash)"
46
49
  - For clean code: "No significant issues found. Change is ready for merge."
47
50
  - For problematic code: Focus only on critical/important issues, skip minor concerns
48
51
 
52
+ **YOU MUST ANALYZE THE PROVIDED CODE CHANGES AND WRITE A REVIEW NOW.**
53
+
49
54
  ## Example Output Format
50
55
 
51
56
  <response>
@@ -175,3 +180,9 @@ CRITICAL FORMATTING RULES:
175
180
  - Use exactly 4 spaces to start each line of code blocks
176
181
  - Keep code blocks simple and readable
177
182
  - Add proper spacing for readability
183
+
184
+ ## TASK SUMMARY
185
+
186
+ **ANALYZE THE CODE CHANGES PROVIDED ABOVE AND WRITE YOUR ENGINEERING REVIEW IMMEDIATELY.**
187
+
188
+ Do not ask for clarification. Do not wait for more input. Analyze the provided code changes, git history, and changed files, then write your review following the format requirements above.
@@ -4,6 +4,13 @@ import { promisify } from 'node:util'
4
4
 
5
5
  const execAsync = promisify(exec)
6
6
 
7
+ // Shared response extraction logic for all AI tools
8
+ const extractResponse = (stdout: string): string => {
9
+ // Extract response from <response> tags or use full output
10
+ const responseMatch = stdout.match(/<response>([\s\S]*?)<\/response>/i)
11
+ return responseMatch ? responseMatch[1].trim() : stdout.trim()
12
+ }
13
+
7
14
  // Simple strategy focused only on review needs
8
15
  export class ReviewStrategyError extends Data.TaggedError('ReviewStrategyError')<{
9
16
  message: string
@@ -75,9 +82,7 @@ export const claudeCliStrategy: ReviewStrategy = {
75
82
  }),
76
83
  })
77
84
 
78
- // Extract response from <response> tags or use full output
79
- const responseMatch = result.stdout.match(/<response>([\s\S]*?)<\/response>/i)
80
- return responseMatch ? responseMatch[1].trim() : result.stdout.trim()
85
+ return extractResponse(result.stdout)
81
86
  }),
82
87
  }
83
88
 
@@ -135,7 +140,7 @@ export const geminiCliStrategy: ReviewStrategy = {
135
140
  }),
136
141
  })
137
142
 
138
- return result.stdout.trim()
143
+ return extractResponse(result.stdout)
139
144
  }),
140
145
  }
141
146
 
@@ -193,7 +198,7 @@ export const openCodeCliStrategy: ReviewStrategy = {
193
198
  }),
194
199
  })
195
200
 
196
- return result.stdout.trim()
201
+ return extractResponse(result.stdout)
197
202
  }),
198
203
  }
199
204
 
@@ -7,11 +7,6 @@ interface MockDeps {
7
7
  spawn: (command: string, options: any) => any
8
8
  }
9
9
 
10
- // Mock for Claude SDK
11
- interface MockClaudeSDK {
12
- query: any // Will be a Bun mock function
13
- }
14
-
15
10
  // Test implementation that mirrors the real strategy structure
16
11
  const createTestStrategy = (name: string, command: string, flags: string[], deps: MockDeps) => ({
17
12
  name,
@@ -74,47 +69,6 @@ const createTestStrategy = (name: string, command: string, flags: string[], deps
74
69
  }),
75
70
  })
76
71
 
77
- // Test implementation for Claude SDK strategy
78
- const createSDKStrategy = (
79
- name: string,
80
- deps: { sdk: MockClaudeSDK | null; hasApiKey: boolean },
81
- ) => ({
82
- name,
83
- isAvailable: () =>
84
- Effect.gen(function* () {
85
- return Boolean(deps.sdk && deps.hasApiKey)
86
- }),
87
- executeReview: (prompt: string, options: { cwd?: string; systemPrompt?: string } = {}) =>
88
- Effect.gen(function* () {
89
- if (!deps.sdk) {
90
- return yield* Effect.fail(new Error(`${name} not available`))
91
- }
92
-
93
- const result = yield* Effect.tryPromise({
94
- try: async () => {
95
- for await (const message of deps.sdk!.query({
96
- prompt,
97
- options: {
98
- maxTurns: 3,
99
- customSystemPrompt: options.systemPrompt || 'You are a code review expert.',
100
- allowedTools: ['Read', 'Grep', 'Glob'],
101
- cwd: options.cwd,
102
- },
103
- })) {
104
- if (message.type === 'result' && message.subtype === 'success' && message.result) {
105
- return message.result
106
- }
107
- }
108
- throw new Error('No result received')
109
- },
110
- catch: (error) =>
111
- new Error(`${name} failed: ${error instanceof Error ? error.message : String(error)}`),
112
- })
113
-
114
- return result
115
- }),
116
- })
117
-
118
72
  describe('Review Strategy', () => {
119
73
  let mockExecAsync: any
120
74
  let mockSpawn: any
@@ -266,6 +220,14 @@ describe('Review Strategy', () => {
266
220
  expect(response).toBe('Gemini response')
267
221
  expect(mockSpawn).toHaveBeenCalledWith('gemini -p', expect.any(Object))
268
222
  })
223
+
224
+ it('should extract response from tags', async () => {
225
+ setupSuccessfulExecution('<response>Gemini tagged content</response>')
226
+
227
+ const response = await Effect.runPromise(geminiStrategy.executeReview('Test prompt'))
228
+
229
+ expect(response).toBe('Gemini tagged content')
230
+ })
269
231
  })
270
232
 
271
233
  describe('OpenCode CLI Strategy', () => {
@@ -295,120 +257,13 @@ describe('Review Strategy', () => {
295
257
  expect(response).toBe('OpenCode response')
296
258
  expect(mockSpawn).toHaveBeenCalledWith('opencode -p', expect.any(Object))
297
259
  })
298
- })
299
260
 
300
- describe('Claude SDK Strategy', () => {
301
- let mockSDK: MockClaudeSDK
302
- let claudeSDKStrategy: any
303
-
304
- beforeEach(() => {
305
- mockSDK = {
306
- query: mock(),
307
- }
308
-
309
- claudeSDKStrategy = createSDKStrategy('Claude SDK', {
310
- sdk: mockSDK,
311
- hasApiKey: true,
312
- })
313
- })
314
-
315
- it('should check availability when SDK and API key are present', async () => {
316
- const available = await Effect.runPromise(claudeSDKStrategy.isAvailable())
317
-
318
- expect(available).toBe(true)
319
- })
320
-
321
- it('should check availability when SDK is missing', async () => {
322
- const strategy = createSDKStrategy('Claude SDK', {
323
- sdk: null,
324
- hasApiKey: true,
325
- })
326
-
327
- const available = await Effect.runPromise(strategy.isAvailable())
328
-
329
- expect(available).toBe(false)
330
- })
331
-
332
- it('should check availability when API key is missing', async () => {
333
- const strategy = createSDKStrategy('Claude SDK', {
334
- sdk: mockSDK,
335
- hasApiKey: false,
336
- })
337
-
338
- const available = await Effect.runPromise(strategy.isAvailable())
339
-
340
- expect(available).toBe(false)
341
- })
342
-
343
- it('should execute review successfully', async () => {
344
- mockSDK.query = mock(async function* () {
345
- yield { type: 'message', content: 'Thinking...' }
346
- yield { type: 'result', subtype: 'success', result: 'Claude SDK response' }
347
- })
348
-
349
- const response = await Effect.runPromise(
350
- claudeSDKStrategy.executeReview('Test prompt', { cwd: '/tmp' }),
351
- )
352
-
353
- expect(response).toBe('Claude SDK response')
354
- expect(mockSDK.query).toHaveBeenCalledWith({
355
- prompt: 'Test prompt',
356
- options: {
357
- maxTurns: 3,
358
- customSystemPrompt: 'You are a code review expert.',
359
- allowedTools: ['Read', 'Grep', 'Glob'],
360
- cwd: '/tmp',
361
- },
362
- })
363
- })
364
-
365
- it('should use custom system prompt', async () => {
366
- mockSDK.query = mock(async function* () {
367
- yield { type: 'result', subtype: 'success', result: 'Custom prompt response' }
368
- })
369
-
370
- const response = await Effect.runPromise(
371
- claudeSDKStrategy.executeReview('Test prompt', {
372
- systemPrompt: 'Custom review prompt',
373
- }),
374
- )
375
-
376
- expect(response).toBe('Custom prompt response')
377
- expect(mockSDK.query).toHaveBeenCalledWith({
378
- prompt: 'Test prompt',
379
- options: {
380
- maxTurns: 3,
381
- customSystemPrompt: 'Custom review prompt',
382
- allowedTools: ['Read', 'Grep', 'Glob'],
383
- cwd: undefined,
384
- },
385
- })
386
- })
387
-
388
- it('should handle SDK failure', async () => {
389
- mockSDK.query = mock(async function* () {
390
- throw new Error('SDK error')
391
- })
392
-
393
- try {
394
- await Effect.runPromise(claudeSDKStrategy.executeReview('Test prompt'))
395
- expect(false).toBe(true) // Should not reach here
396
- } catch (error: any) {
397
- expect(error.message).toContain('Claude SDK failed')
398
- }
399
- })
261
+ it('should extract response from tags', async () => {
262
+ setupSuccessfulExecution('<response>OpenCode tagged content</response>')
400
263
 
401
- it('should handle no result received', async () => {
402
- mockSDK.query = mock(async function* () {
403
- yield { type: 'message', content: 'No result' }
404
- })
264
+ const response = await Effect.runPromise(opencodeStrategy.executeReview('Test prompt'))
405
265
 
406
- try {
407
- await Effect.runPromise(claudeSDKStrategy.executeReview('Test prompt'))
408
- expect(false).toBe(true) // Should not reach here
409
- } catch (error: any) {
410
- expect(error.message).toContain('No result received')
411
- }
266
+ expect(response).toBe('OpenCode tagged content')
412
267
  })
413
268
  })
414
269