@aaronshaf/ger 0.1.0 → 0.1.1
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 +1 -1
- package/src/cli/commands/review.ts +203 -203
- package/src/cli/index.ts +2 -0
- package/src/prompts/default-review.md +45 -39
- package/src/prompts/system-inline-review.md +50 -25
- package/src/prompts/system-overall-review.md +33 -8
- package/src/services/ai-enhanced.ts +3 -2
- package/src/services/ai.ts +12 -4
- package/src/services/git-worktree.ts +297 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +111 -0
- package/tests/ai-service.test.ts +3 -3
- package/tests/review.test.ts +94 -628
- package/tests/unit/git-branch-detection.test.ts +83 -0
- package/tests/unit/git-worktree.test.ts +54 -0
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, pipe, Schema } from 'effect'
|
|
1
|
+
import { Effect, pipe, Schema, Layer } from 'effect'
|
|
2
2
|
import { AiService } from '@/services/ai'
|
|
3
3
|
import { commentCommandWithInput } from './comment'
|
|
4
4
|
import { Console } from 'effect'
|
|
@@ -7,6 +7,13 @@ import type { CommentInfo } from '@/schemas/gerrit'
|
|
|
7
7
|
import { sanitizeCDATA, escapeXML } from '@/utils/shell-safety'
|
|
8
8
|
import { formatDiffPretty } from '@/utils/diff-formatters'
|
|
9
9
|
import { formatDate } from '@/utils/formatters'
|
|
10
|
+
import {
|
|
11
|
+
formatChangeAsXML,
|
|
12
|
+
formatCommentsAsXML,
|
|
13
|
+
formatMessagesAsXML,
|
|
14
|
+
flattenComments,
|
|
15
|
+
} from '@/utils/review-formatters'
|
|
16
|
+
import { buildEnhancedPrompt } from '@/utils/review-prompt-builder'
|
|
10
17
|
import * as fs from 'node:fs/promises'
|
|
11
18
|
import * as fsSync from 'node:fs'
|
|
12
19
|
import * as os from 'node:os'
|
|
@@ -14,6 +21,7 @@ import * as path from 'node:path'
|
|
|
14
21
|
import { fileURLToPath } from 'node:url'
|
|
15
22
|
import { dirname } from 'node:path'
|
|
16
23
|
import * as readline from 'node:readline'
|
|
24
|
+
import { GitWorktreeService, GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
17
25
|
|
|
18
26
|
// Get the directory of this module
|
|
19
27
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -178,7 +186,7 @@ const validateAndFixInlineComments = (
|
|
|
178
186
|
return validComments
|
|
179
187
|
})
|
|
180
188
|
|
|
181
|
-
//
|
|
189
|
+
// Legacy helper for backward compatibility (will be removed)
|
|
182
190
|
const getChangeDataAsXml = (changeId: string): Effect.Effect<string, ApiError, GerritApiService> =>
|
|
183
191
|
Effect.gen(function* () {
|
|
184
192
|
const gerritApi = yield* GerritApiService
|
|
@@ -190,83 +198,17 @@ const getChangeDataAsXml = (changeId: string): Effect.Effect<string, ApiError, G
|
|
|
190
198
|
const commentsMap = yield* gerritApi.getComments(changeId)
|
|
191
199
|
const messages = yield* gerritApi.getMessages(changeId)
|
|
192
200
|
|
|
193
|
-
|
|
194
|
-
const comments: CommentInfo[] = []
|
|
195
|
-
for (const [path, fileComments] of Object.entries(commentsMap)) {
|
|
196
|
-
for (const comment of fileComments) {
|
|
197
|
-
comments.push({ ...comment, path })
|
|
198
|
-
}
|
|
199
|
-
}
|
|
201
|
+
const comments = flattenComments(commentsMap)
|
|
200
202
|
|
|
201
|
-
// Build XML string
|
|
203
|
+
// Build XML string using helper functions
|
|
202
204
|
const xmlLines: string[] = []
|
|
203
205
|
xmlLines.push(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
204
206
|
xmlLines.push(`<show_result>`)
|
|
205
207
|
xmlLines.push(` <status>success</status>`)
|
|
206
|
-
xmlLines.push(
|
|
207
|
-
xmlLines.push(` <id>${escapeXML(change.change_id)}</id>`)
|
|
208
|
-
xmlLines.push(` <number>${change._number}</number>`)
|
|
209
|
-
xmlLines.push(` <subject><![CDATA[${sanitizeCDATA(change.subject)}]]></subject>`)
|
|
210
|
-
xmlLines.push(` <status>${escapeXML(change.status)}</status>`)
|
|
211
|
-
xmlLines.push(` <project>${escapeXML(change.project)}</project>`)
|
|
212
|
-
xmlLines.push(` <branch>${escapeXML(change.branch)}</branch>`)
|
|
213
|
-
xmlLines.push(` <owner>`)
|
|
214
|
-
if (change.owner?.name) {
|
|
215
|
-
xmlLines.push(` <name><![CDATA[${sanitizeCDATA(change.owner.name)}]]></name>`)
|
|
216
|
-
}
|
|
217
|
-
if (change.owner?.email) {
|
|
218
|
-
xmlLines.push(` <email>${escapeXML(change.owner.email)}</email>`)
|
|
219
|
-
}
|
|
220
|
-
xmlLines.push(` </owner>`)
|
|
221
|
-
xmlLines.push(` <created>${escapeXML(change.created || '')}</created>`)
|
|
222
|
-
xmlLines.push(` <updated>${escapeXML(change.updated || '')}</updated>`)
|
|
223
|
-
xmlLines.push(` </change>`)
|
|
208
|
+
xmlLines.push(...formatChangeAsXML(change))
|
|
224
209
|
xmlLines.push(` <diff><![CDATA[${sanitizeCDATA(diff)}]]></diff>`)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
xmlLines.push(` <comments>`)
|
|
228
|
-
xmlLines.push(` <count>${comments.length}</count>`)
|
|
229
|
-
for (const comment of comments) {
|
|
230
|
-
xmlLines.push(` <comment>`)
|
|
231
|
-
if (comment.id) xmlLines.push(` <id>${escapeXML(comment.id)}</id>`)
|
|
232
|
-
if (comment.path)
|
|
233
|
-
xmlLines.push(` <path><![CDATA[${sanitizeCDATA(comment.path)}]]></path>`)
|
|
234
|
-
if (comment.line) xmlLines.push(` <line>${comment.line}</line>`)
|
|
235
|
-
if (comment.author?.name) {
|
|
236
|
-
xmlLines.push(` <author><![CDATA[${sanitizeCDATA(comment.author.name)}]]></author>`)
|
|
237
|
-
}
|
|
238
|
-
if (comment.updated) xmlLines.push(` <updated>${escapeXML(comment.updated)}</updated>`)
|
|
239
|
-
if (comment.message) {
|
|
240
|
-
xmlLines.push(` <message><![CDATA[${sanitizeCDATA(comment.message)}]]></message>`)
|
|
241
|
-
}
|
|
242
|
-
if (comment.unresolved) xmlLines.push(` <unresolved>true</unresolved>`)
|
|
243
|
-
xmlLines.push(` </comment>`)
|
|
244
|
-
}
|
|
245
|
-
xmlLines.push(` </comments>`)
|
|
246
|
-
|
|
247
|
-
// Messages section
|
|
248
|
-
xmlLines.push(` <messages>`)
|
|
249
|
-
xmlLines.push(` <count>${messages.length}</count>`)
|
|
250
|
-
for (const message of messages) {
|
|
251
|
-
xmlLines.push(` <message>`)
|
|
252
|
-
xmlLines.push(` <id>${escapeXML(message.id)}</id>`)
|
|
253
|
-
if (message.author?.name) {
|
|
254
|
-
xmlLines.push(` <author><![CDATA[${sanitizeCDATA(message.author.name)}]]></author>`)
|
|
255
|
-
}
|
|
256
|
-
if (message.author?._account_id) {
|
|
257
|
-
xmlLines.push(` <author_id>${message.author._account_id}</author_id>`)
|
|
258
|
-
}
|
|
259
|
-
xmlLines.push(` <date>${escapeXML(message.date)}</date>`)
|
|
260
|
-
if (message._revision_number) {
|
|
261
|
-
xmlLines.push(` <revision>${message._revision_number}</revision>`)
|
|
262
|
-
}
|
|
263
|
-
if (message.tag) {
|
|
264
|
-
xmlLines.push(` <tag>${escapeXML(message.tag)}</tag>`)
|
|
265
|
-
}
|
|
266
|
-
xmlLines.push(` <message><![CDATA[${sanitizeCDATA(message.message)}]]></message>`)
|
|
267
|
-
xmlLines.push(` </message>`)
|
|
268
|
-
}
|
|
269
|
-
xmlLines.push(` </messages>`)
|
|
210
|
+
xmlLines.push(...formatCommentsAsXML(comments))
|
|
211
|
+
xmlLines.push(...formatMessagesAsXML(messages))
|
|
270
212
|
xmlLines.push(`</show_result>`)
|
|
271
213
|
|
|
272
214
|
return xmlLines.join('\n')
|
|
@@ -286,13 +228,7 @@ const getChangeDataAsPretty = (
|
|
|
286
228
|
const commentsMap = yield* gerritApi.getComments(changeId)
|
|
287
229
|
const messages = yield* gerritApi.getMessages(changeId)
|
|
288
230
|
|
|
289
|
-
|
|
290
|
-
const comments: CommentInfo[] = []
|
|
291
|
-
for (const [path, fileComments] of Object.entries(commentsMap)) {
|
|
292
|
-
for (const comment of fileComments) {
|
|
293
|
-
comments.push({ ...comment, path })
|
|
294
|
-
}
|
|
295
|
-
}
|
|
231
|
+
const comments = flattenComments(commentsMap)
|
|
296
232
|
|
|
297
233
|
// Build pretty string
|
|
298
234
|
const lines: string[] = []
|
|
@@ -379,18 +315,22 @@ const promptUser = (message: string): Effect.Effect<boolean, never> =>
|
|
|
379
315
|
export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
380
316
|
Effect.gen(function* () {
|
|
381
317
|
const aiService = yield* AiService
|
|
318
|
+
const gitService = yield* GitWorktreeService
|
|
382
319
|
|
|
383
320
|
// Load default prompts first
|
|
384
321
|
const prompts = yield* loadDefaultPrompts
|
|
385
322
|
|
|
386
|
-
//
|
|
323
|
+
// Validate preconditions
|
|
324
|
+
yield* gitService.validatePreconditions()
|
|
325
|
+
|
|
326
|
+
// Check for AI tool availability
|
|
387
327
|
yield* Console.log('→ Checking for AI tool availability...')
|
|
388
328
|
const aiTool = yield* aiService
|
|
389
329
|
.detectAiTool()
|
|
390
330
|
.pipe(Effect.catchTag('NoAiToolFoundError', (error) => Effect.fail(new Error(error.message))))
|
|
391
331
|
yield* Console.log(`✓ Found AI tool: ${aiTool}`)
|
|
392
332
|
|
|
393
|
-
// Load custom review prompt if provided
|
|
333
|
+
// Load custom review prompt if provided
|
|
394
334
|
let userReviewPrompt = prompts.defaultReviewPrompt
|
|
395
335
|
|
|
396
336
|
if (options.prompt) {
|
|
@@ -404,81 +344,175 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
404
344
|
}
|
|
405
345
|
}
|
|
406
346
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
347
|
+
// Use Effect's resource management for worktree lifecycle
|
|
348
|
+
yield* Effect.acquireUseRelease(
|
|
349
|
+
// Acquire: Create worktree and setup
|
|
350
|
+
Effect.gen(function* () {
|
|
351
|
+
const worktreeInfo = yield* gitService.createWorktree(changeId)
|
|
352
|
+
yield* gitService.fetchAndCheckoutPatchset(worktreeInfo)
|
|
353
|
+
return worktreeInfo
|
|
354
|
+
}),
|
|
355
|
+
|
|
356
|
+
// Use: Run the enhanced review process
|
|
357
|
+
(worktreeInfo) =>
|
|
358
|
+
Effect.gen(function* () {
|
|
359
|
+
// Switch to worktree directory
|
|
360
|
+
const originalCwd = process.cwd()
|
|
361
|
+
process.chdir(worktreeInfo.path)
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Get changed files from git
|
|
365
|
+
const changedFiles = yield* gitService.getChangedFiles()
|
|
366
|
+
|
|
367
|
+
yield* Console.log(`→ Found ${changedFiles.length} changed files`)
|
|
368
|
+
if (options.debug) {
|
|
369
|
+
yield* Console.log(`[DEBUG] Changed files: ${changedFiles.join(', ')}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Stage 1: Generate inline comments
|
|
373
|
+
yield* Console.log(`→ Generating inline comments for change ${changeId}...`)
|
|
374
|
+
|
|
375
|
+
const inlinePrompt = yield* buildEnhancedPrompt(
|
|
376
|
+
userReviewPrompt,
|
|
377
|
+
prompts.inlineReviewSystemPrompt,
|
|
378
|
+
changeId,
|
|
379
|
+
changedFiles,
|
|
380
|
+
)
|
|
410
381
|
|
|
411
|
-
|
|
382
|
+
if (options.debug) {
|
|
383
|
+
yield* Console.log('[DEBUG] Running AI for inline comments...')
|
|
384
|
+
yield* Console.log(`[DEBUG] Working directory: ${worktreeInfo.path}`)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Run inline review with worktree as working directory
|
|
388
|
+
const inlineResponse = yield* aiService
|
|
389
|
+
.runPrompt(inlinePrompt, '', { cwd: worktreeInfo.path })
|
|
390
|
+
.pipe(
|
|
391
|
+
Effect.catchTag('AiResponseParseError', (error) =>
|
|
392
|
+
Effect.gen(function* () {
|
|
393
|
+
yield* Console.error(`✗ Failed to parse AI response: ${error.message}`)
|
|
394
|
+
yield* Console.error('Raw AI output:')
|
|
395
|
+
yield* Console.error('-'.repeat(80))
|
|
396
|
+
yield* Console.error(error.rawOutput || 'No output captured')
|
|
397
|
+
yield* Console.error('-'.repeat(80))
|
|
398
|
+
return yield* Effect.fail(error)
|
|
399
|
+
}),
|
|
400
|
+
),
|
|
401
|
+
Effect.catchTag('AiServiceError', (error) =>
|
|
402
|
+
Effect.die(new Error(`AI service error: ${error.message}`)),
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if (options.debug) {
|
|
407
|
+
yield* Console.log(`[DEBUG] Inline response:\n${inlineResponse}`)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Response is already extracted by runPrompt
|
|
411
|
+
const extractedInlineResponse = inlineResponse
|
|
412
|
+
|
|
413
|
+
// Parse JSON array from response
|
|
414
|
+
const inlineCommentsArray = yield* Effect.tryPromise({
|
|
415
|
+
try: () => Promise.resolve(JSON.parse(extractedInlineResponse)),
|
|
416
|
+
catch: (error) => new Error(`Invalid JSON response: ${error}`),
|
|
417
|
+
}).pipe(
|
|
418
|
+
Effect.catchAll((error) =>
|
|
419
|
+
Effect.gen(function* () {
|
|
420
|
+
yield* Console.error(`✗ Failed to parse inline comments JSON: ${error}`)
|
|
421
|
+
if (!options.debug) {
|
|
422
|
+
yield* Console.error('Run with --debug to see raw AI output')
|
|
423
|
+
}
|
|
424
|
+
return yield* Effect.fail(error)
|
|
425
|
+
}),
|
|
426
|
+
),
|
|
427
|
+
)
|
|
412
428
|
|
|
413
|
-
|
|
414
|
-
|
|
429
|
+
// Validate that the response is an array
|
|
430
|
+
if (!Array.isArray(inlineCommentsArray)) {
|
|
431
|
+
yield* Console.error('✗ AI response is not an array of comments')
|
|
432
|
+
return yield* Effect.fail(new Error('Invalid inline comments format'))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Validate and fix inline comments
|
|
436
|
+
const originalCount = inlineCommentsArray.length
|
|
437
|
+
const inlineComments = yield* validateAndFixInlineComments(
|
|
438
|
+
inlineCommentsArray,
|
|
439
|
+
changedFiles,
|
|
440
|
+
)
|
|
441
|
+
const validCount = inlineComments.length
|
|
415
442
|
|
|
416
|
-
|
|
417
|
-
|
|
443
|
+
if (originalCount > validCount) {
|
|
444
|
+
yield* Console.log(
|
|
445
|
+
`→ Filtered ${originalCount - validCount} invalid comments, ${validCount} remain`,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
418
448
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
449
|
+
// Handle inline comments output/posting
|
|
450
|
+
yield* handleInlineComments(inlineComments, changeId, options)
|
|
422
451
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
Effect.catchTag('AiResponseParseError', (error) =>
|
|
426
|
-
Effect.gen(function* () {
|
|
427
|
-
if (options.debug) {
|
|
428
|
-
yield* Console.error(`[DEBUG] AI output:\n${error.rawOutput}`)
|
|
429
|
-
}
|
|
430
|
-
return yield* Effect.fail(error)
|
|
431
|
-
}),
|
|
432
|
-
),
|
|
433
|
-
Effect.catchTag('AiServiceError', (error) =>
|
|
434
|
-
Effect.die(new Error(`AI service error: ${error.message}`)),
|
|
435
|
-
),
|
|
436
|
-
)
|
|
452
|
+
// Stage 2: Generate overall review comment
|
|
453
|
+
yield* Console.log(`→ Generating overall review comment for change ${changeId}...`)
|
|
437
454
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
455
|
+
const overallPrompt = yield* buildEnhancedPrompt(
|
|
456
|
+
userReviewPrompt,
|
|
457
|
+
prompts.overallReviewSystemPrompt,
|
|
458
|
+
changeId,
|
|
459
|
+
changedFiles,
|
|
460
|
+
)
|
|
441
461
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
462
|
+
if (options.debug) {
|
|
463
|
+
yield* Console.log('[DEBUG] Running AI for overall review...')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Run overall review
|
|
467
|
+
const overallResponse = yield* aiService
|
|
468
|
+
.runPrompt(overallPrompt, '', { cwd: worktreeInfo.path })
|
|
469
|
+
.pipe(
|
|
470
|
+
Effect.catchTag('AiResponseParseError', (error) =>
|
|
471
|
+
Effect.gen(function* () {
|
|
472
|
+
yield* Console.error(`✗ Failed to parse AI response: ${error.message}`)
|
|
473
|
+
yield* Console.error('Raw AI output:')
|
|
474
|
+
yield* Console.error('-'.repeat(80))
|
|
475
|
+
yield* Console.error(error.rawOutput || 'No output captured')
|
|
476
|
+
yield* Console.error('-'.repeat(80))
|
|
477
|
+
return yield* Effect.fail(error)
|
|
478
|
+
}),
|
|
479
|
+
),
|
|
480
|
+
Effect.catchTag('AiServiceError', (error) =>
|
|
481
|
+
Effect.die(new Error(`AI service error: ${error.message}`)),
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if (options.debug) {
|
|
486
|
+
yield* Console.log(`[DEBUG] Overall response:\n${overallResponse}`)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Response is already extracted by runPrompt
|
|
490
|
+
const extractedOverallResponse = overallResponse
|
|
491
|
+
|
|
492
|
+
// Handle overall review output/posting
|
|
493
|
+
yield* handleOverallReview(extractedOverallResponse, changeId, options)
|
|
494
|
+
} finally {
|
|
495
|
+
// Always restore original working directory
|
|
496
|
+
process.chdir(originalCwd)
|
|
452
497
|
}
|
|
453
|
-
return yield* Effect.fail(error)
|
|
454
|
-
}),
|
|
455
|
-
),
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
// Validate that the response is an array
|
|
459
|
-
if (!Array.isArray(inlineCommentsArray)) {
|
|
460
|
-
yield* Console.error('✗ AI response is not an array of comments')
|
|
461
|
-
return yield* Effect.fail(new Error('Invalid inline comments format'))
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Get available files for validation
|
|
465
|
-
const gerritApi = yield* GerritApiService
|
|
466
|
-
const files = yield* gerritApi.getFiles(changeId)
|
|
467
|
-
const availableFiles = Object.keys(files)
|
|
468
498
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const inlineComments = yield* validateAndFixInlineComments(inlineCommentsArray, availableFiles)
|
|
472
|
-
const validCount = inlineComments.length
|
|
499
|
+
yield* Console.log(`✓ Review complete for ${changeId}`)
|
|
500
|
+
}),
|
|
473
501
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
502
|
+
// Release: Always cleanup worktree
|
|
503
|
+
(worktreeInfo) => gitService.cleanup(worktreeInfo),
|
|
504
|
+
)
|
|
505
|
+
})
|
|
479
506
|
|
|
480
|
-
|
|
507
|
+
// Helper function to handle inline comments output/posting
|
|
508
|
+
const handleInlineComments = (
|
|
509
|
+
inlineComments: InlineComment[],
|
|
510
|
+
changeId: string,
|
|
511
|
+
options: ReviewOptions,
|
|
512
|
+
): Effect.Effect<void, Error, GerritApiService> =>
|
|
513
|
+
Effect.gen(function* () {
|
|
481
514
|
if (!options.comment) {
|
|
515
|
+
// Display mode
|
|
482
516
|
if (inlineComments.length > 0) {
|
|
483
517
|
yield* Console.log('\n━━━━━━ INLINE COMMENTS ━━━━━━')
|
|
484
518
|
for (const comment of inlineComments) {
|
|
@@ -489,7 +523,7 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
489
523
|
yield* Console.log('\n→ No inline comments')
|
|
490
524
|
}
|
|
491
525
|
} else {
|
|
492
|
-
//
|
|
526
|
+
// Comment posting mode
|
|
493
527
|
if (inlineComments.length > 0) {
|
|
494
528
|
yield* Console.log('\n━━━━━━ INLINE COMMENTS TO POST ━━━━━━')
|
|
495
529
|
for (const comment of inlineComments) {
|
|
@@ -498,27 +532,20 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
498
532
|
}
|
|
499
533
|
yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
500
534
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
? true
|
|
504
|
-
: yield* promptUser('\nPost these inline comments to Gerrit?')
|
|
535
|
+
const shouldPost =
|
|
536
|
+
options.yes || (yield* promptUser('\nPost these inline comments to Gerrit?'))
|
|
505
537
|
|
|
506
538
|
if (shouldPost) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}),
|
|
518
|
-
),
|
|
519
|
-
)
|
|
520
|
-
yield* Console.log(`✓ Inline comments posted for ${changeId}`)
|
|
521
|
-
}
|
|
539
|
+
yield* pipe(
|
|
540
|
+
commentCommandWithInput(changeId, JSON.stringify(inlineComments), { batch: true }),
|
|
541
|
+
Effect.catchAll((error) =>
|
|
542
|
+
Effect.gen(function* () {
|
|
543
|
+
yield* Console.error(`✗ Failed to post inline comments: ${error}`)
|
|
544
|
+
return yield* Effect.fail(error)
|
|
545
|
+
}),
|
|
546
|
+
),
|
|
547
|
+
)
|
|
548
|
+
yield* Console.log(`✓ Inline comments posted for ${changeId}`)
|
|
522
549
|
} else {
|
|
523
550
|
yield* Console.log('→ Inline comments not posted')
|
|
524
551
|
}
|
|
@@ -526,54 +553,29 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
526
553
|
yield* Console.log('\n→ No valid inline comments to post')
|
|
527
554
|
}
|
|
528
555
|
}
|
|
556
|
+
})
|
|
529
557
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
yield* Console.log('[DEBUG] Running AI for overall review...')
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Run overall review
|
|
541
|
-
const overallResponse = yield* aiService.runPrompt(overallPrompt, prettyData).pipe(
|
|
542
|
-
Effect.catchTag('AiResponseParseError', (error) =>
|
|
543
|
-
Effect.gen(function* () {
|
|
544
|
-
if (options.debug) {
|
|
545
|
-
yield* Console.error(`[DEBUG] AI output:\n${error.rawOutput}`)
|
|
546
|
-
}
|
|
547
|
-
return yield* Effect.fail(error)
|
|
548
|
-
}),
|
|
549
|
-
),
|
|
550
|
-
Effect.catchTag('AiServiceError', (error) =>
|
|
551
|
-
Effect.die(new Error(`AI service error: ${error.message}`)),
|
|
552
|
-
),
|
|
553
|
-
)
|
|
554
|
-
|
|
555
|
-
if (options.debug) {
|
|
556
|
-
yield* Console.log(`[DEBUG] Overall response:\n${overallResponse}`)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// If not in comment mode, just output the review
|
|
558
|
+
// Helper function to handle overall review output/posting
|
|
559
|
+
const handleOverallReview = (
|
|
560
|
+
overallResponse: string,
|
|
561
|
+
changeId: string,
|
|
562
|
+
options: ReviewOptions,
|
|
563
|
+
): Effect.Effect<void, Error, GerritApiService> =>
|
|
564
|
+
Effect.gen(function* () {
|
|
560
565
|
if (!options.comment) {
|
|
566
|
+
// Display mode
|
|
561
567
|
yield* Console.log('\n━━━━━━ OVERALL REVIEW ━━━━━━')
|
|
562
568
|
yield* Console.log(overallResponse)
|
|
563
569
|
yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
564
570
|
} else {
|
|
565
|
-
//
|
|
571
|
+
// Comment posting mode
|
|
566
572
|
yield* Console.log('\n━━━━━━ OVERALL REVIEW TO POST ━━━━━━')
|
|
567
573
|
yield* Console.log(overallResponse)
|
|
568
574
|
yield* Console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
569
575
|
|
|
570
|
-
|
|
571
|
-
const shouldPost = options.yes
|
|
572
|
-
? true
|
|
573
|
-
: yield* promptUser('\nPost this overall review to Gerrit?')
|
|
576
|
+
const shouldPost = options.yes || (yield* promptUser('\nPost this overall review to Gerrit?'))
|
|
574
577
|
|
|
575
578
|
if (shouldPost) {
|
|
576
|
-
// Post overall comment using the new direct input method
|
|
577
579
|
yield* pipe(
|
|
578
580
|
commentCommandWithInput(changeId, overallResponse, {}),
|
|
579
581
|
Effect.catchAll((error) =>
|
|
@@ -588,6 +590,4 @@ export const reviewCommand = (changeId: string, options: ReviewOptions = {}) =>
|
|
|
588
590
|
yield* Console.log('→ Overall review not posted')
|
|
589
591
|
}
|
|
590
592
|
}
|
|
591
|
-
|
|
592
|
-
yield* Console.log(`✓ Review complete for ${changeId}`)
|
|
593
593
|
})
|
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { Effect } from 'effect'
|
|
|
26
26
|
import { GerritApiServiceLive } from '@/api/gerrit'
|
|
27
27
|
import { ConfigServiceLive } from '@/services/config'
|
|
28
28
|
import { AiServiceLive } from '@/services/ai-enhanced'
|
|
29
|
+
import { GitWorktreeServiceLive } from '@/services/git-worktree'
|
|
29
30
|
import { abandonCommand } from './commands/abandon'
|
|
30
31
|
import { commentCommand } from './commands/comment'
|
|
31
32
|
import { commentsCommand } from './commands/comments'
|
|
@@ -409,6 +410,7 @@ Examples:
|
|
|
409
410
|
Effect.provide(AiServiceLive),
|
|
410
411
|
Effect.provide(GerritApiServiceLive),
|
|
411
412
|
Effect.provide(ConfigServiceLive),
|
|
413
|
+
Effect.provide(GitWorktreeServiceLive),
|
|
412
414
|
)
|
|
413
415
|
await Effect.runPromise(effect)
|
|
414
416
|
} catch (error) {
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
# Code Review
|
|
1
|
+
# Engineering Code Review - Signal Over Noise
|
|
2
2
|
|
|
3
|
-
You are
|
|
3
|
+
You are conducting a technical code review for experienced engineers. **PRIORITY: Find actual problems, not generate busy work.**
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Core Principles
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Fully comprehend the author's intent before identifying issues
|
|
9
|
-
- Read COMPLETE files, not just diffs
|
|
10
|
-
- Check if apparent issues are handled elsewhere in the change
|
|
11
|
-
- Consider the broader architectural context
|
|
12
|
-
- Verify you're reviewing the LATEST patchset version
|
|
7
|
+
**SIGNAL > NOISE**: Only comment on issues that materially impact correctness, security, performance, or maintainability. Silence is better than noise.
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
**NO PRAISE NEEDED**: Don't compliment good code. Engineers expect competent code by default.
|
|
10
|
+
|
|
11
|
+
**EMPTY RESPONSES ARE VALID**: Small changes without issues should result in empty inline comments. The overall review can simply note "No significant issues found."
|
|
12
|
+
|
|
13
|
+
**FOCUS ON REAL PROBLEMS**:
|
|
14
|
+
- Bugs that will cause runtime failures
|
|
15
|
+
- Security vulnerabilities
|
|
16
|
+
- Performance bottlenecks
|
|
17
|
+
- Architectural mistakes
|
|
18
|
+
- Missing error handling
|
|
19
|
+
|
|
20
|
+
**ASSUME COMPETENCE**: The author is an experienced engineer who made intentional decisions. Question only when you see genuine problems.
|
|
20
21
|
|
|
21
22
|
## Review Categories (Priority Order)
|
|
22
23
|
|
|
@@ -47,34 +48,39 @@ You are reviewing a Gerrit change set. Provide thorough, constructive feedback f
|
|
|
47
48
|
- **Clarity**: Code that works but could be more readable
|
|
48
49
|
- **Future-Proofing**: Anticipating likely future requirements
|
|
49
50
|
|
|
50
|
-
## What NOT to Review
|
|
51
|
+
## What NOT to Review (Common Time Wasters)
|
|
52
|
+
|
|
53
|
+
- **Code style/formatting**: Handled by automated tools
|
|
54
|
+
- **Personal preferences**: Different != wrong
|
|
55
|
+
- **Compliments**: "Looks good!" wastes everyone's time
|
|
56
|
+
- **Nitpicks**: Minor wording, variable names, spacing
|
|
57
|
+
- **Micro-optimizations**: Unless there's a proven performance problem
|
|
58
|
+
- **Already working code**: If it works and isn't broken, don't fix it
|
|
59
|
+
- **Suggestions for "better" approaches**: Only if current approach has concrete problems
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
- **Style Preferences**: Formatting that doesn't impact readability
|
|
54
|
-
- **Micro-Optimizations**: Unless performance is a stated goal
|
|
55
|
-
- **Personal Preferences**: Unless they violate team standards
|
|
56
|
-
- **Out of Scope**: Issues in unchanged code (unless directly relevant)
|
|
61
|
+
## Before Commenting, Ask Yourself
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
1. **Will this cause a runtime failure?** → Critical issue, comment required
|
|
64
|
+
2. **Will this create a security vulnerability?** → Critical issue, comment required
|
|
65
|
+
3. **Will this significantly harm performance?** → Important issue, comment required
|
|
66
|
+
4. **Will this make the code unmaintainable?** → Consider commenting
|
|
67
|
+
5. **Is this just a different way to solve the same problem?** → Skip it
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
1. The issue still exists in the current patchset
|
|
62
|
-
2. The fix wouldn't break other functionality
|
|
63
|
-
3. Your understanding of the code's purpose is correct
|
|
64
|
-
4. The issue isn't intentional or documented
|
|
65
|
-
5. The concern is worth the author's time to address
|
|
69
|
+
## Output Guidelines
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
**INLINE COMMENTS**: Only for specific line-level issues. Empty array is perfectly valid.
|
|
72
|
+
- Start with "🤖 "
|
|
73
|
+
- Be direct: "This will cause X bug" not "Consider maybe perhaps changing this"
|
|
74
|
+
- Provide specific fixes when possible
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Use questions for clarification, statements for clear issues
|
|
76
|
+
**OVERALL REVIEW**: Required even if no inline comments.
|
|
77
|
+
- For clean code: "No significant issues found. Change is ready."
|
|
78
|
+
- For problematic code: Focus on the most important issues only
|
|
79
|
+
- Skip the pleasantries, get to the point
|
|
74
80
|
|
|
75
|
-
##
|
|
81
|
+
## Success Metrics
|
|
76
82
|
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
83
|
+
- **Good review**: Finds 1-3 real issues that would cause problems
|
|
84
|
+
- **Great review**: Catches a critical bug before production
|
|
85
|
+
- **Bad review**: 10+ nitpicky comments about style preferences
|
|
86
|
+
- **Terrible review**: "Great job! LGTM!" with zero value added
|