@aaronshaf/ger 0.1.0 → 0.1.3

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/src/cli/index.ts CHANGED
@@ -25,7 +25,8 @@ import { Command } from 'commander'
25
25
  import { Effect } from 'effect'
26
26
  import { GerritApiServiceLive } from '@/api/gerrit'
27
27
  import { ConfigServiceLive } from '@/services/config'
28
- import { AiServiceLive } from '@/services/ai-enhanced'
28
+ import { ReviewStrategyServiceLive } from '@/services/review-strategy'
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'
@@ -367,10 +368,15 @@ program
367
368
  .option('-y, --yes', 'Skip confirmation prompts when posting comments')
368
369
  .option('--debug', 'Show debug output including AI responses')
369
370
  .option('--prompt <file>', 'Path to custom review prompt file (e.g., ~/prompts/review.md)')
371
+ .option(
372
+ '--provider <provider>',
373
+ 'Preferred AI provider (claude-sdk, claude, gemini, codex, opencode)',
374
+ )
375
+ .option('--system-prompt <prompt>', 'Custom system prompt for the AI')
370
376
  .addHelpText(
371
377
  'after',
372
378
  `
373
- This command uses AI (claude, llm, or opencode CLI) to review a Gerrit change.
379
+ This command uses AI (Claude SDK, claude CLI, gemini CLI, codex CLI, or opencode CLI) to review a Gerrit change.
374
380
  It performs a two-stage review process:
375
381
 
376
382
  1. Generates inline comments for specific code issues
@@ -381,7 +387,7 @@ Use --comment to post the review to Gerrit (with confirmation prompts).
381
387
  Use --comment --yes to post without confirmation.
382
388
 
383
389
  Requirements:
384
- - One of these AI tools must be installed: claude, llm, or opencode
390
+ - One of these AI tools must be available: Claude SDK (ANTHROPIC_API_KEY), claude CLI, gemini CLI, codex CLI, or opencode CLI
385
391
  - Gerrit credentials must be configured (run 'ger setup' first)
386
392
 
387
393
  Examples:
@@ -405,10 +411,13 @@ Examples:
405
411
  yes: options.yes,
406
412
  debug: options.debug,
407
413
  prompt: options.prompt,
414
+ provider: options.provider,
415
+ systemPrompt: options.systemPrompt,
408
416
  }).pipe(
409
- Effect.provide(AiServiceLive),
417
+ Effect.provide(ReviewStrategyServiceLive),
410
418
  Effect.provide(GerritApiServiceLive),
411
419
  Effect.provide(ConfigServiceLive),
420
+ Effect.provide(GitWorktreeServiceLive),
412
421
  )
413
422
  await Effect.runPromise(effect)
414
423
  } 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
@@ -3,7 +3,7 @@
3
3
  **YOUR ENTIRE OUTPUT MUST BE WRAPPED IN <response></response> TAGS.**
4
4
  **NEVER USE BACKTICKS ANYWHERE IN YOUR RESPONSE - they cause shell execution errors.**
5
5
 
6
- Output ONLY a JSON array wrapped in response tags. No other text before or after the tags.
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.
7
7
 
8
8
  ## JSON Structure for Inline Comments
9
9
 
@@ -59,30 +59,55 @@ Line numbers refer to the final file (REVISION), not the diff.
59
59
 
60
60
  ## Priority Guidelines for Inline Comments
61
61
 
62
- ### ALWAYS Comment On
63
- - Security vulnerabilities (injection, auth bypass, data exposure)
64
- - Data corruption or loss risks
65
- - Logic errors that produce wrong results
66
- - Resource leaks (memory, connections, handles)
67
- - Race conditions and concurrency bugs
68
-
69
- ### USUALLY Comment On
70
- - Missing error handling for likely failure cases
71
- - Performance problems (N+1 queries, unbounded loops)
72
- - Type safety issues and invalid casts
73
- - Missing input validation
74
- - Incorrect API usage
75
-
76
- ### RARELY Comment On
77
- - Style preferences (unless egregious)
78
- - Minor optimizations without measurement
79
- - Alternative approaches that are equivalent
80
- - Issues in unchanged code
81
- - Formatting (unless it obscures logic)
62
+ ### ALWAYS Comment On (Real Problems Only)
63
+ - **Bugs**: Logic errors, null pointer risks, incorrect algorithms
64
+ - **Security**: Injection vulnerabilities, auth bypasses, data leaks
65
+ - **Crashes**: Unhandled exceptions, resource exhaustion, infinite loops
66
+ - **Data loss**: Operations that corrupt or lose user data
67
+
68
+ ### SOMETIMES Comment On (If Significant)
69
+ - **Performance**: N+1 queries, memory leaks, algorithmic complexity issues
70
+ - **Error handling**: Missing try/catch for operations that commonly fail
71
+ - **Type safety**: Dangerous casts, missing validation for external input
72
+
73
+ ### NEVER Comment On (Time Wasters)
74
+ - **Style/formatting**: Let automated tools handle this
75
+ - **Working code**: If it functions correctly, leave it alone
76
+ - **Personal preferences**: "I would have done this differently"
77
+ - **Nitpicks**: Variable names, spacing, minor wording
78
+ - **Compliments**: Don't waste time praising obvious competence
79
+
80
+ ## GIT REPOSITORY ACCESS
81
+
82
+ You are running in a git repository with full access to:
83
+ - git diff, git show, git log for understanding changes and context
84
+ - git blame for code ownership and history
85
+ - All project files for architectural understanding
86
+ - Use these commands to provide comprehensive, accurate reviews
82
87
 
83
88
  ## FINAL REMINDER
84
89
 
85
- Your ENTIRE output must be a JSON array wrapped in <response></response> tags.
86
- Every message must start with "🤖 ".
87
- Never use backticks in your response.
88
- Focus on substantial technical issues, not preferences.
90
+ **CRITICAL: Your ENTIRE output must be a JSON array wrapped in <response></response> tags.**
91
+
92
+ Example formats:
93
+ ```
94
+ <response>
95
+ []
96
+ </response>
97
+ ```
98
+ (Empty array for clean code - this is GOOD!)
99
+
100
+ ```
101
+ <response>
102
+ [{"file": "auth.js", "line": 42, "message": "🤖 SQL injection vulnerability: query uses string concatenation"}]
103
+ </response>
104
+ ```
105
+ (Only comment on real problems)
106
+
107
+ **REQUIREMENTS**:
108
+ - Every message must start with "🤖 "
109
+ - Never use backticks in your response
110
+ - Empty arrays are encouraged for clean code
111
+ - Focus on bugs, security, crashes - ignore style preferences
112
+ - Use git commands to understand context before commenting
113
+ - NO TEXT OUTSIDE THE <response></response> TAGS
@@ -41,11 +41,10 @@ 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
- The review content inside the response tags should start with "🤖 [Your Tool Name] ([Your Model])" followed by your analysis. For example:
44
+ Start with "🤖 [Your Tool Name] ([Your Model])" then provide a **CONCISE** engineering assessment. Examples:
45
45
  - If you are Claude Sonnet 4: "🤖 Claude (Sonnet 4)"
46
- - If you are GPT-4: "🤖 OpenAI (GPT-4)"
47
- - If you are Llama: "🤖 Llama (70B)"
48
- - etc.
46
+ - For clean code: "No significant issues found. Change is ready for merge."
47
+ - For problematic code: Focus only on critical/important issues, skip minor concerns
49
48
 
50
49
  ## Example Output Format
51
50
 
@@ -139,14 +138,40 @@ The security issues are blocking and must be fixed. The performance concerns sho
139
138
  - Skip trivial issues unless they indicate patterns
140
139
  - Include concrete fix suggestions
141
140
 
141
+ ## GIT REPOSITORY ACCESS
142
+
143
+ You are running in a git repository with full access to:
144
+ - git diff, git show, git log for understanding changes and context
145
+ - git blame for code ownership and history
146
+ - All project files for architectural understanding
147
+ - Use these commands to explore the codebase and provide comprehensive reviews
148
+
142
149
  ## FINAL REMINDER
143
150
 
144
- Your ENTIRE output must be wrapped in <response></response> tags.
145
- Start with "🤖 [Your Tool Name] ([Your Model])" then proceed with your analysis.
146
- Use Gerrit's limited markdown format - NO backticks, NO markdown bold/italic.
151
+ **CRITICAL: Your ENTIRE output must be wrapped in <response></response> tags.**
152
+
153
+ Example format:
154
+ ```
155
+ <response>
156
+ 🤖 Claude (Sonnet 4)
157
+
158
+ OVERALL ASSESSMENT
159
+
160
+ Your review content here...
161
+ </response>
162
+ ```
163
+
164
+ MANDATORY REQUIREMENTS:
165
+ - Start with "🤖 [Your Tool Name] ([Your Model])"
166
+ - Be CONCISE - engineers value brevity over verbosity
167
+ - For clean code, simply state "No significant issues found"
168
+ - Focus on material problems, skip style preferences and compliments
169
+ - Use Gerrit's limited markdown format - NO backticks, NO markdown bold/italic
170
+ - Use git commands to understand context before writing review
171
+ - NO TEXT OUTSIDE THE <response></response> TAGS
147
172
 
148
173
  CRITICAL FORMATTING RULES:
149
174
  - Add blank lines between sections and before/after code blocks
150
- - Use exactly 4 spaces to start each line of code blocks
175
+ - Use exactly 4 spaces to start each line of code blocks
151
176
  - Keep code blocks simple and readable
152
177
  - Add proper spacing for readability
@@ -0,0 +1,297 @@
1
+ import { Effect, Console, pipe, Layer, Context } from 'effect'
2
+ import { Schema } from 'effect'
3
+ import * as os from 'node:os'
4
+ import * as path from 'node:path'
5
+ import * as fs from 'node:fs/promises'
6
+ import { spawn } from 'node:child_process'
7
+
8
+ // Error types
9
+ export class WorktreeCreationError extends Schema.TaggedError<WorktreeCreationError>()(
10
+ 'WorktreeCreationError',
11
+ {
12
+ message: Schema.String,
13
+ cause: Schema.optional(Schema.Unknown),
14
+ },
15
+ ) {}
16
+
17
+ export class PatchsetFetchError extends Schema.TaggedError<PatchsetFetchError>()(
18
+ 'PatchsetFetchError',
19
+ {
20
+ message: Schema.String,
21
+ cause: Schema.optional(Schema.Unknown),
22
+ },
23
+ ) {}
24
+
25
+ export class DirtyRepoError extends Schema.TaggedError<DirtyRepoError>()('DirtyRepoError', {
26
+ message: Schema.String,
27
+ }) {}
28
+
29
+ export class NotGitRepoError extends Schema.TaggedError<NotGitRepoError>()('NotGitRepoError', {
30
+ message: Schema.String,
31
+ }) {}
32
+
33
+ export type GitError = WorktreeCreationError | PatchsetFetchError | DirtyRepoError | NotGitRepoError
34
+
35
+ // Worktree info
36
+ export interface WorktreeInfo {
37
+ path: string
38
+ changeId: string
39
+ originalCwd: string
40
+ timestamp: number
41
+ pid: number
42
+ }
43
+
44
+ // Git command runner with Effect
45
+ const runGitCommand = (
46
+ args: string[],
47
+ options: { cwd?: string } = {},
48
+ ): Effect.Effect<string, GitError, never> =>
49
+ Effect.async<string, GitError, never>((resume) => {
50
+ const child = spawn('git', args, {
51
+ cwd: options.cwd || process.cwd(),
52
+ stdio: ['ignore', 'pipe', 'pipe'],
53
+ })
54
+
55
+ let stdout = ''
56
+ let stderr = ''
57
+
58
+ child.stdout?.on('data', (data) => {
59
+ stdout += data.toString()
60
+ })
61
+
62
+ child.stderr?.on('data', (data) => {
63
+ stderr += data.toString()
64
+ })
65
+
66
+ child.on('close', (code) => {
67
+ if (code === 0) {
68
+ resume(Effect.succeed(stdout.trim()))
69
+ } else {
70
+ const errorMessage = `Git command failed: git ${args.join(' ')}\nStderr: ${stderr}`
71
+
72
+ // Classify error based on command and output
73
+ if (args[0] === 'worktree' && args[1] === 'add') {
74
+ resume(Effect.fail(new WorktreeCreationError({ message: errorMessage })))
75
+ } else if (args[0] === 'fetch' || args[0] === 'checkout') {
76
+ resume(Effect.fail(new PatchsetFetchError({ message: errorMessage })))
77
+ } else {
78
+ resume(Effect.fail(new WorktreeCreationError({ message: errorMessage })))
79
+ }
80
+ }
81
+ })
82
+
83
+ child.on('error', (error) => {
84
+ resume(
85
+ Effect.fail(
86
+ new WorktreeCreationError({
87
+ message: `Failed to spawn git: ${error.message}`,
88
+ cause: error,
89
+ }),
90
+ ),
91
+ )
92
+ })
93
+ })
94
+
95
+ // Check if current directory is a git repository
96
+ const validateGitRepo = (): Effect.Effect<void, NotGitRepoError, never> =>
97
+ pipe(
98
+ runGitCommand(['rev-parse', '--git-dir']),
99
+ Effect.mapError(
100
+ () => new NotGitRepoError({ message: 'Current directory is not a git repository' }),
101
+ ),
102
+ Effect.map(() => undefined),
103
+ )
104
+
105
+ // Check if working directory is clean
106
+ const validateCleanRepo = (): Effect.Effect<void, DirtyRepoError, never> =>
107
+ pipe(
108
+ runGitCommand(['status', '--porcelain']),
109
+ Effect.mapError(() => new DirtyRepoError({ message: 'Failed to check repository status' })),
110
+ Effect.flatMap((output) =>
111
+ output.trim() === ''
112
+ ? Effect.succeed(undefined)
113
+ : Effect.fail(
114
+ new DirtyRepoError({
115
+ message:
116
+ 'Working directory has uncommitted changes. Please commit or stash changes before review.',
117
+ }),
118
+ ),
119
+ ),
120
+ )
121
+
122
+ // Generate unique worktree path
123
+ const generateWorktreePath = (changeId: string): string => {
124
+ const timestamp = Date.now()
125
+ const pid = process.pid
126
+ const uniqueId = `${changeId}-${timestamp}-${pid}`
127
+ return path.join(os.homedir(), '.ger', 'worktrees', uniqueId)
128
+ }
129
+
130
+ // Ensure .ger directory exists
131
+ const ensureGerDirectory = (): Effect.Effect<void, never, never> =>
132
+ Effect.tryPromise({
133
+ try: async () => {
134
+ const gerDir = path.join(os.homedir(), '.ger', 'worktrees')
135
+ await fs.mkdir(gerDir, { recursive: true })
136
+ },
137
+ catch: () => undefined, // Ignore errors, will fail later if directory can't be created
138
+ }).pipe(Effect.catchAll(() => Effect.succeed(undefined)))
139
+
140
+ // Build Gerrit refspec for change
141
+ const buildRefspec = (changeNumber: string, patchsetNumber: number = 1): string => {
142
+ // Extract change number from changeId if it contains non-numeric characters
143
+ const numericChangeNumber = changeNumber.replace(/\D/g, '')
144
+ return `refs/changes/${numericChangeNumber.slice(-2)}/${numericChangeNumber}/${patchsetNumber}`
145
+ }
146
+
147
+ // Get the current HEAD commit hash to avoid branch conflicts
148
+ const getCurrentCommit = (): Effect.Effect<string, GitError, never> =>
149
+ pipe(
150
+ runGitCommand(['rev-parse', 'HEAD']),
151
+ Effect.map((output) => output.trim()),
152
+ Effect.catchAll(() =>
153
+ // Fallback: try to get commit from default branch
154
+ pipe(
155
+ runGitCommand(['rev-parse', 'origin/main']),
156
+ Effect.catchAll(() => runGitCommand(['rev-parse', 'origin/master'])),
157
+ Effect.catchAll(() => Effect.succeed('HEAD')),
158
+ ),
159
+ ),
160
+ )
161
+
162
+ // Get latest patchset number for a change
163
+ const getLatestPatchsetNumber = (
164
+ changeId: string,
165
+ ): Effect.Effect<number, PatchsetFetchError, never> =>
166
+ pipe(
167
+ runGitCommand(['ls-remote', 'origin', `refs/changes/*/${changeId.replace(/\D/g, '')}/*`]),
168
+ Effect.mapError(
169
+ (error) =>
170
+ new PatchsetFetchError({ message: `Failed to get patchset info: ${error.message}` }),
171
+ ),
172
+ Effect.map((output) => {
173
+ const lines = output.split('\n').filter((line) => line.trim())
174
+ if (lines.length === 0) {
175
+ return 1 // Default to patchset 1 if no refs found
176
+ }
177
+
178
+ // Extract patchset numbers and return the highest
179
+ const patchsetNumbers = lines
180
+ .map((line) => {
181
+ const match = line.match(/refs\/changes\/\d+\/\d+\/(\d+)$/)
182
+ return match ? parseInt(match[1], 10) : 0
183
+ })
184
+ .filter((num) => num > 0)
185
+
186
+ return patchsetNumbers.length > 0 ? Math.max(...patchsetNumbers) : 1
187
+ }),
188
+ )
189
+
190
+ // GitWorktreeService implementation
191
+ export interface GitWorktreeServiceImpl {
192
+ validatePreconditions: () => Effect.Effect<void, GitError, never>
193
+ createWorktree: (changeId: string) => Effect.Effect<WorktreeInfo, GitError, never>
194
+ fetchAndCheckoutPatchset: (worktreeInfo: WorktreeInfo) => Effect.Effect<void, GitError, never>
195
+ cleanup: (worktreeInfo: WorktreeInfo) => Effect.Effect<void, never, never>
196
+ getChangedFiles: () => Effect.Effect<string[], GitError, never>
197
+ }
198
+
199
+ const GitWorktreeServiceImplLive: GitWorktreeServiceImpl = {
200
+ validatePreconditions: () =>
201
+ Effect.gen(function* () {
202
+ yield* validateGitRepo()
203
+ yield* validateCleanRepo()
204
+ yield* Console.log('✓ Git repository validation passed')
205
+ }),
206
+
207
+ createWorktree: (changeId: string) =>
208
+ Effect.gen(function* () {
209
+ yield* Console.log(`→ Creating worktree for change ${changeId}...`)
210
+
211
+ // Get current commit hash to avoid branch conflicts
212
+ const currentCommit = yield* getCurrentCommit()
213
+ yield* Console.log(`→ Using base commit: ${currentCommit.substring(0, 7)}`)
214
+
215
+ // Ensure .ger directory exists
216
+ yield* ensureGerDirectory()
217
+
218
+ // Generate unique path
219
+ const worktreePath = generateWorktreePath(changeId)
220
+ const originalCwd = process.cwd()
221
+
222
+ // Create worktree using commit hash (no branch conflicts)
223
+ yield* runGitCommand(['worktree', 'add', '--detach', worktreePath, currentCommit])
224
+
225
+ const worktreeInfo: WorktreeInfo = {
226
+ path: worktreePath,
227
+ changeId,
228
+ originalCwd,
229
+ timestamp: Date.now(),
230
+ pid: process.pid,
231
+ }
232
+
233
+ yield* Console.log(`✓ Worktree created at ${worktreePath}`)
234
+ return worktreeInfo
235
+ }),
236
+
237
+ fetchAndCheckoutPatchset: (worktreeInfo: WorktreeInfo) =>
238
+ Effect.gen(function* () {
239
+ yield* Console.log(`→ Fetching and checking out patchset for ${worktreeInfo.changeId}...`)
240
+
241
+ // Get latest patchset number
242
+ const patchsetNumber = yield* getLatestPatchsetNumber(worktreeInfo.changeId)
243
+ const refspec = buildRefspec(worktreeInfo.changeId, patchsetNumber)
244
+
245
+ yield* Console.log(`→ Using refspec: ${refspec}`)
246
+
247
+ // Fetch the change
248
+ yield* runGitCommand(['fetch', 'origin', refspec], { cwd: worktreeInfo.path })
249
+
250
+ // Checkout FETCH_HEAD
251
+ yield* runGitCommand(['checkout', 'FETCH_HEAD'], { cwd: worktreeInfo.path })
252
+
253
+ yield* Console.log(`✓ Checked out patchset ${patchsetNumber} for ${worktreeInfo.changeId}`)
254
+ }),
255
+
256
+ cleanup: (worktreeInfo: WorktreeInfo) =>
257
+ Effect.gen(function* () {
258
+ yield* Console.log(`→ Cleaning up worktree for ${worktreeInfo.changeId}...`)
259
+
260
+ // Always restore original working directory first
261
+ try {
262
+ process.chdir(worktreeInfo.originalCwd)
263
+ } catch (error) {
264
+ yield* Console.warn(`Warning: Could not restore original directory: ${error}`)
265
+ }
266
+
267
+ // Attempt to remove worktree (don't fail if this doesn't work)
268
+ yield* pipe(
269
+ runGitCommand(['worktree', 'remove', '--force', worktreeInfo.path]),
270
+ Effect.catchAll((error) =>
271
+ Effect.gen(function* () {
272
+ yield* Console.warn(`Warning: Could not remove worktree: ${error.message}`)
273
+ yield* Console.warn(`Manual cleanup may be required: ${worktreeInfo.path}`)
274
+ }),
275
+ ),
276
+ )
277
+
278
+ yield* Console.log(`✓ Cleanup completed for ${worktreeInfo.changeId}`)
279
+ }),
280
+
281
+ getChangedFiles: () =>
282
+ Effect.gen(function* () {
283
+ // Get list of changed files in current worktree
284
+ const output = yield* runGitCommand(['diff', '--name-only', 'HEAD~1'])
285
+ const files = output.split('\n').filter((file) => file.trim())
286
+ return files
287
+ }),
288
+ }
289
+
290
+ // Export service tag for dependency injection
291
+ export class GitWorktreeService extends Context.Tag('GitWorktreeService')<
292
+ GitWorktreeService,
293
+ GitWorktreeServiceImpl
294
+ >() {}
295
+
296
+ // Export service layer
297
+ export const GitWorktreeServiceLive = Layer.succeed(GitWorktreeService, GitWorktreeServiceImplLive)