@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- // Helper to get change data and format as XML string
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
- // Flatten comments from all files
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(` <change>`)
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
- // Comments section
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
- // Flatten comments from all files
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
- // Check for AI tool availability first
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 via --prompt option
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
- // Combine user prompt with system prompts for each stage
408
- const inlinePrompt = `${userReviewPrompt}\n\n${prompts.inlineReviewSystemPrompt}`
409
- const overallPrompt = `${userReviewPrompt}\n\n${prompts.overallReviewSystemPrompt}`
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
- yield* Console.log(`→ Fetching change data for ${changeId}...`)
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
- // Stage 1: Generate inline comments
414
- yield* Console.log(`→ Generating inline comments for change ${changeId}...`)
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
- // Get change data in XML format for inline review
417
- const xmlData = yield* getChangeDataAsXml(changeId)
443
+ if (originalCount > validCount) {
444
+ yield* Console.log(
445
+ `→ Filtered ${originalCount - validCount} invalid comments, ${validCount} remain`,
446
+ )
447
+ }
418
448
 
419
- if (options.debug) {
420
- yield* Console.log('[DEBUG] Running AI for inline comments...')
421
- }
449
+ // Handle inline comments output/posting
450
+ yield* handleInlineComments(inlineComments, changeId, options)
422
451
 
423
- // Run inline review
424
- const inlineResponse = yield* aiService.runPrompt(inlinePrompt, xmlData).pipe(
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
- if (options.debug) {
439
- yield* Console.log(`[DEBUG] Inline response:\n${inlineResponse}`)
440
- }
455
+ const overallPrompt = yield* buildEnhancedPrompt(
456
+ userReviewPrompt,
457
+ prompts.overallReviewSystemPrompt,
458
+ changeId,
459
+ changedFiles,
460
+ )
441
461
 
442
- // Parse JSON array from response using Effect
443
- const inlineCommentsArray = yield* Effect.tryPromise({
444
- try: () => Promise.resolve(JSON.parse(inlineResponse)),
445
- catch: (error) => new Error(`Invalid JSON response: ${error}`),
446
- }).pipe(
447
- Effect.catchAll((error) =>
448
- Effect.gen(function* () {
449
- yield* Console.error(`✗ Failed to parse inline comments JSON: ${error}`)
450
- if (!options.debug) {
451
- yield* Console.error('Run with --debug to see raw AI output')
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
- // Validate and fix inline comments
470
- const originalCount = inlineCommentsArray.length
471
- const inlineComments = yield* validateAndFixInlineComments(inlineCommentsArray, availableFiles)
472
- const validCount = inlineComments.length
499
+ yield* Console.log(`✓ Review complete for ${changeId}`)
500
+ }),
473
501
 
474
- if (originalCount > validCount) {
475
- yield* Console.log(
476
- `→ Filtered ${originalCount - validCount} invalid comments, ${validCount} remain`,
477
- )
478
- }
502
+ // Release: Always cleanup worktree
503
+ (worktreeInfo) => gitService.cleanup(worktreeInfo),
504
+ )
505
+ })
479
506
 
480
- // If not in comment mode, just output the inline comments
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
- // In comment mode, handle posting
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
- // Ask for confirmation unless --yes is provided
502
- const shouldPost = options.yes
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
- if (inlineComments.length === 0) {
508
- yield* Console.log('→ No valid comments to post after validation')
509
- } else {
510
- // Post inline comments using the new direct input method
511
- yield* pipe(
512
- commentCommandWithInput(changeId, JSON.stringify(inlineComments), { batch: true }),
513
- Effect.catchAll((error) =>
514
- Effect.gen(function* () {
515
- yield* Console.error(`✗ Failed to post inline comments: ${error}`)
516
- return yield* Effect.fail(error)
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
- // Stage 2: Generate overall review comment
531
- yield* Console.log(`→ Generating overall review comment for change ${changeId}...`)
532
-
533
- // Get change data in regular format for overall review
534
- const prettyData = yield* getChangeDataAsPretty(changeId)
535
-
536
- if (options.debug) {
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
- // In comment mode, handle posting
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
- // Ask for confirmation unless --yes is provided
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 Guidelines
1
+ # Engineering Code Review - Signal Over Noise
2
2
 
3
- You are reviewing a Gerrit change set. Provide thorough, constructive feedback focused on technical excellence and maintainability.
3
+ You are conducting a technical code review for experienced engineers. **PRIORITY: Find actual problems, not generate busy work.**
4
4
 
5
- ## Review Philosophy
5
+ ## Core Principles
6
6
 
7
- 1. **Understand First, Critique Second**
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
- 2. **Be Direct and Constructive**
15
- - Focus on substantive technical concerns
16
- - Explain WHY something is problematic, not just what
17
- - Provide actionable suggestions when identifying issues
18
- - Assume the author has domain expertise
19
- - Ask clarifying questions when intent is unclear
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
- - **Already Fixed**: Issues resolved in the current patchset
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
- ## Context Requirements
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
- Before commenting, verify:
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
- ## Inline Comment Guidelines
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
- - Start each comment with "🤖 " (robot emoji with space)
70
- - Be specific about file paths and line numbers
71
- - Group related issues when they share a root cause
72
- - Provide concrete examples or corrections when helpful
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
- ## Remember
81
+ ## Success Metrics
76
82
 
77
- - The goal is to improve code quality while respecting the author's time
78
- - Focus on issues that matter for correctness, security, and maintainability
79
- - Your review should help ship better code, not perfect code
80
- - When in doubt, phrase feedback as a question rather than a mandate
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