ace-git-worktree 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dd11e367c00bf01b67787574f51768d79adee5ca4fbdd2f0e31e2c68f15a56a
4
- data.tar.gz: a98a57a36e0f520e19337f97dba830e273d5e9855f06badcf2493301c6218ce7
3
+ metadata.gz: 440da5b336b58817b1e6bbc1b8d57de396e78848b9e9c289d1492ca85fc7065a
4
+ data.tar.gz: 432c570346044a2351c72355b729501cef4723507fb64f0298307f7790edf289
5
5
  SHA512:
6
- metadata.gz: e060226c29bdf088b46b9f83315e321ea52ad9b421766a5da3b369b6ffcbcbcfe8d6fbc5a3637d4e038d749f44c41867cbbc51f8e5b5a6636e9ab942654391ee
7
- data.tar.gz: 8f0c1ad4f09380ce2cf8edf2a8944ac2a3267a1fe40e39111239dd5075f562f34c57bd2251f8cfb0520f4b7fc786584fd669d9b6b1522b81cbb4ad4ecbefd32b
6
+ metadata.gz: c4c1f7d225354653e5d6d4c3ff8e88c0c8b4cee1142707e5d51181569b961b58e81fd1f64bca355a78a6675205429654420ed9776481470204342f70e7bfc9a5
7
+ data.tar.gz: 46e6f7112657db3d867a69db98dacbe429883672beb37b83fa35bda5b059d071a8a318fe5d26adc90835da05a3b4f74d105b096244be92beb61a904007652358
data/CHANGELOG.md CHANGED
@@ -7,7 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] - 2026-09-28
10
11
 
12
+ ### Added
13
+ - Forge-neutral PR worktree checkout: the exact declared source
14
+ repository/ref is fetched and its head SHA verified against normalized PR
15
+ evidence before any worktree is created (fork and canonical sources are
16
+ equally valid).
17
+ - `--server` / `--default-server` selection for PR creation, task-PR
18
+ creation, and cleanup; `--remote` remains a local Git remote name only.
19
+ - Task PR creation proves the pushed branch head SHA to the provider
20
+ (`expected_head`); PR identity is never inferred from branch names.
21
+ - Cleanup report digests now bind the resolved server identity and each
22
+ item's provider proof (status, PR, proven head, merge commit); `--apply`
23
+ recomputes the full report immediately before executing and refuses any
24
+ drift from the approved digest.
25
+
26
+ ### Removed
27
+ - Direct GitHub coupling from worktree correctness paths: `ace/git/github`
28
+ requires, `Github::PrFetcher` usage, `gh` CLI checks and troubleshooting,
29
+ the raw gh-based `PrCreator` molecule, and the `ace-git-github` gemspec
30
+ dependency. Provider execution lives behind the neutral ace-git provider
31
+ contract.
32
+
33
+
34
+
35
+ ### Added
36
+ - Forge-neutral PR worktree checkout: the exact declared source
37
+ repository/ref is fetched and its head SHA verified against normalized PR
38
+ evidence before any worktree is created (fork and canonical sources are
39
+ equally valid).
40
+ - `--server` / `--default-server` selection for PR creation, task-PR
41
+ creation, and cleanup; `--remote` remains a local Git remote name only.
42
+ - Task PR creation proves the pushed branch head SHA to the provider
43
+ (`expected_head`); PR identity is never inferred from branch names.
44
+ - Cleanup report digests now bind the resolved server identity and each
45
+ item's provider proof (status, PR, proven head, merge commit); `--apply`
46
+ recomputes the full report immediately before executing and refuses any
47
+ drift from the approved digest.
48
+
49
+ ### Removed
50
+ - Direct GitHub coupling from worktree correctness paths: `ace/git/github`
51
+ requires, `Github::PrFetcher` usage, `gh` CLI checks and troubleshooting,
52
+ the raw gh-based `PrCreator` molecule, and the `ace-git-github` gemspec
53
+ dependency. Provider execution lives behind the neutral ace-git provider
54
+ contract.
55
+
56
+
57
+
58
+ ## [0.23.0] - 2026-09-27
59
+
60
+ ### Changed
61
+ - PR lookups now go through the `ace-git-github` provider (`Ace::Git::Github::PrFetcher`) instead of the `PrMetadataFetcher` molecule removed from the ace-git 0.24 core; provider failures surface as classified `Provider*Error`s instead of the removed `Gh*Error` classes.
62
+ - Raised the `ace-git` dependency floor to `~> 0.24` and added `ace-git-github ~> 0.1.1` (cross-repository PR argument fix). Resolves the require-time `LoadError` (`ace/git/molecules/pr_metadata_fetcher`) that made released 0.22.1 incompatible with ace-git 0.24 and blocked adopting ace-review 0.55.0.
11
63
  ## [0.22.1] - 2026-09-02
12
64
 
13
65
  ### Technical
data/docs/usage.md CHANGED
@@ -76,12 +76,18 @@ Created task worktree and branch for 081
76
76
 
77
77
  ### Scenario 2: Review a Pull Request in Its Own Worktree
78
78
 
79
- **Goal:** Create a dedicated workspace for PR review or patching.
79
+ **Goal:** Create a dedicated workspace for PR review or patching, on any
80
+ configured forge (GitHub, default Forgejo, or a named Forgejo).
80
81
 
81
82
  **Commands:**
82
83
 
83
84
  ```bash
84
- ace-git-worktree create --pr 26
85
+ # Preview provenance first (no mutation): source/base repositories, exact head
86
+ ace-git-worktree create --pr 26 --server forgejo-lab --dry-run
87
+
88
+ # Create the worktree; the declared source ref is fetched and its SHA is
89
+ # verified against the PR evidence before anything is created
90
+ ace-git-worktree create --pr 26 --server forgejo-lab
85
91
  ace-git-worktree list
86
92
  ```
87
93
 
@@ -187,6 +193,8 @@ ace-git-worktree create --task 081 --dry-run
187
193
 
188
194
  * `--task` - Task ID for a task-aware worktree
189
195
  * `--pr`, `--pull-request` - PR number for a PR-aware worktree
196
+ * `--server <name>` - Named configured forge server for PR and task-PR operations
197
+ * `--default-server` - Use the configured default forge server
190
198
  * `--from`, `-b` - Create from a specific local or remote branch
191
199
  * `--path` - Override the destination worktree path
192
200
  * `--source` - Override the git ref used as the start point
@@ -195,7 +203,8 @@ ace-git-worktree create --task 081 --dry-run
195
203
  * `--no-commit` - Skip committing task metadata changes
196
204
  * `--no-push` - Skip pushing task changes
197
205
  * `--no-upstream` - Skip pushing the new branch with upstream tracking
198
- * `--no-pr` - Skip automatic draft-PR creation when enabled
206
+ * `--no-pr` - Skip automatic draft-PR creation; keeps task creation fully
207
+ local and provider-free
199
208
  * `--push-remote` - Override the remote used for task-related pushes
200
209
  * `--no-auto-navigate` - Stay in the current directory after creation
201
210
  * `--commit-message` - Use a custom commit message for task metadata updates
@@ -295,6 +304,42 @@ ace-git-worktree prune --cleanup-directories
295
304
  * `-v`, `--verbose` - Show verbose output
296
305
  * `-d`, `--debug` - Show debug output
297
306
 
307
+ ### `ace-git-worktree cleanup`
308
+
309
+ Build a complete, deterministic, report-only cleanup inventory for merged
310
+ worktrees and refs. Local ancestry is the first safe proof; provider PR
311
+ evidence (from the selected forge server) proves candidates whose local
312
+ ancestry is unproven. Unavailable or failed provider proof is never relabeled
313
+ as merged.
314
+
315
+ ```bash
316
+ ace-git-worktree cleanup --target main
317
+ ace-git-worktree cleanup --target main --format json
318
+ ace-git-worktree cleanup --target main --server forgejo-lab
319
+ ace-git-worktree cleanup --target main --offline
320
+ ```
321
+
322
+ **Options:**
323
+
324
+ * `--target <ref>` - Target ref for ancestry proof (required)
325
+ * `--remote <name>` - Local Git remote name (default: `origin`); never forge selection
326
+ * `--server <name>` - Named configured forge server for PR proof
327
+ * `--default-server` - Use the configured default forge server
328
+ * `--offline` - Skip remote refresh and provider proof
329
+ * `--format <type>` - Output format: `table` (default), `json`
330
+ * `--apply` - Apply a reviewed plan (see below)
331
+ * `--approved-digest <sha256>` - Digest of the reviewed plan; required with `--apply`
332
+ * `--require-only-target` - Fail if the final rescan retains non-target state
333
+
334
+ **Approval binding:** the report carries a canonical SHA-256 digest over the
335
+ inventory, decisions, the resolved server identity, and each item's provider
336
+ proof (status, PR number, proven head, merge commit). `--apply` recomputes
337
+ the full report immediately before executing and refuses anything whose
338
+ digest differs from the approved one — changed repo state, changed server
339
+ configuration, or changed PR/head proof all invalidate consent. Only
340
+ confirmed merged evidence permits remote-merge cleanup; `--force` does not
341
+ exist and dirty, locked, and in-use worktrees are always retained.
342
+
298
343
  ### `ace-git-worktree bootstrap [IDENTIFIER]`
299
344
 
300
345
  Rerun preparation phases (toolchain trust and bootstrap policy) for an existing worktree in place.
@@ -399,16 +444,23 @@ data is available.
399
444
 
400
445
  ### Problem: PR worktree creation fails
401
446
 
402
- **Symptom:** `create --pr` reports a GitHub or authentication error.
447
+ **Symptom:** `create --pr` reports a classified provider error (missing
448
+ provider CLI, unauthenticated, unreachable, or identity mismatch).
403
449
 
404
450
  **Solution:**
405
451
 
406
- ```bash
407
- gh auth status
408
- gh auth login
409
- ```
410
-
411
- PR-based creation depends on `gh` being installed and authenticated.
452
+ 1. Configure the forge server in `git.servers` (see `ace-git` docs) and
453
+ select it with `--server` / `--default-server`, or make the repository
454
+ remote resolvable.
455
+ 2. Install and authenticate the provider CLI for that server's provider
456
+ type (`gh` for GitHub, `fj` for Forgejo; check `gh auth status` or
457
+ `fj auth list`).
458
+ 3. Preview with `--dry-run` to inspect the exact source/base provenance
459
+ before creating anything.
460
+
461
+ PR creation is forge-neutral: the exact declared source repository/ref is
462
+ fetched and its head SHA is verified against the PR evidence. Failures never
463
+ fall back to another provider.
412
464
 
413
465
  ### Problem: `switch` returns nothing useful
414
466
 
@@ -30,28 +30,16 @@ module Ace
30
30
 
31
31
  validate_options(options)
32
32
 
33
- reporter = Molecules::CleanupReporter.new(
34
- target: options[:target],
35
- remote: options[:remote],
36
- offline: options[:offline]
37
- )
38
-
39
- result = reporter.report
33
+ result = build_reporter(options).report
40
34
 
41
35
  if result[:success]
42
36
  if options[:apply]
43
- applier = Molecules::CleanupApplier.new(result, options[:approved_digest])
44
- apply_result = applier.apply
37
+ apply_result = apply_fresh_report(options)
45
38
 
46
39
  if apply_result[:success]
47
- # Strict rescan
48
- final_reporter = Molecules::CleanupReporter.new(
49
- target: options[:target],
50
- remote: options[:remote],
51
- offline: options[:offline]
52
- )
53
- final_result = final_reporter.report
54
-
40
+ # Strict rescan with the same selection
41
+ final_result = build_reporter(options).report
42
+
55
43
  if final_result[:success]
56
44
  # Check require_only_target if needed
57
45
  if options[:require_only_target] && has_non_target_state?(final_result)
@@ -59,7 +47,7 @@ module Ace
59
47
  display_result(final_result, options)
60
48
  return 1
61
49
  end
62
-
50
+
63
51
  display_apply_result(apply_result, final_result, options)
64
52
  return 0
65
53
  else
@@ -93,6 +81,35 @@ module Ace
93
81
  1
94
82
  end
95
83
 
84
+ private
85
+
86
+ # Build a reporter with the command's server selection; --remote
87
+ # stays a local Git remote name and is never forge selection.
88
+ def build_reporter(options)
89
+ Molecules::CleanupReporter.new(
90
+ target: options[:target],
91
+ remote: options[:remote],
92
+ offline: options[:offline],
93
+ server_name: options[:server],
94
+ use_default: options[:default_server] == true,
95
+ remote_name: options[:remote]
96
+ )
97
+ end
98
+
99
+ # Recompute the complete report immediately before apply; approval
100
+ # is bound to the reviewed digest over the exact evidence. Any
101
+ # drift (repo state, server identity, PR/head proof) invalidates
102
+ # the approved digest and refuses the apply.
103
+ def apply_fresh_report(options)
104
+ fresh = build_reporter(options).report
105
+ unless fresh[:success]
106
+ return {success: false, error: "Report recomputation failed before apply: #{fresh[:error]}", ledger: [], partial_failure: false}
107
+ end
108
+
109
+ applier = Molecules::CleanupApplier.new(fresh, options[:approved_digest])
110
+ applier.apply
111
+ end
112
+
96
113
  # Show help
97
114
  #
98
115
  # @return [Integer] Exit code
@@ -103,8 +120,10 @@ module Ace
103
120
 
104
121
  OPTIONS
105
122
  --target <ref> Target ref for ancestry proof (required)
106
- --remote <name> Remote name (default: origin)
107
- --offline Skip remote evidence refresh
123
+ --remote <name> Local Git remote name (default: origin); never forge selection
124
+ --server <name> Named configured forge server for PR proof
125
+ --default-server Use the configured default forge server
126
+ --offline Skip remote evidence refresh and provider proof
108
127
  --apply Apply a reviewed plan
109
128
  --approved-digest <sha256> Approved plan digest to apply
110
129
  --require-only-target Fail if final rescan has retained non-target state
@@ -128,12 +147,11 @@ module Ace
128
147
  0
129
148
  end
130
149
 
131
- private
132
-
133
150
  def parse_arguments(args)
134
151
  options = {
135
152
  target: nil, remote: "origin", offline: false, format: "table", help: false,
136
- apply: false, approved_digest: nil, require_only_target: false
153
+ apply: false, approved_digest: nil, require_only_target: false,
154
+ server: nil, default_server: false
137
155
  }
138
156
  i = 0
139
157
  while i < args.length
@@ -146,6 +164,11 @@ module Ace
146
164
  options[:remote] = args[i]
147
165
  when "--offline"
148
166
  options[:offline] = true
167
+ when "--server"
168
+ i += 1
169
+ options[:server] = args[i]
170
+ when "--default-server"
171
+ options[:default_server] = true
149
172
  when "--apply"
150
173
  options[:apply] = true
151
174
  when "--approved-digest"
@@ -169,16 +192,17 @@ module Ace
169
192
  def validate_options(options)
170
193
  raise ArgumentError, "--target is required" unless options[:target]
171
194
  raise ArgumentError, "--remote is required" unless options[:remote]
195
+ raise ArgumentError, "--server and --default-server are mutually exclusive" if options[:server] && options[:default_server]
172
196
 
173
197
  valid_formats = %w[table json]
174
198
  unless valid_formats.include?(options[:format])
175
199
  raise ArgumentError, "Invalid format '#{options[:format]}'. Use: #{valid_formats.join(", ")}"
176
200
  end
177
-
201
+
178
202
  if options[:apply] && !options[:approved_digest]
179
203
  raise ArgumentError, "--approved-digest is required when --apply is used"
180
204
  end
181
-
205
+
182
206
  if options[:approved_digest] && !options[:apply]
183
207
  raise ArgumentError, "--apply is required when --approved-digest is used"
184
208
  end
@@ -55,9 +55,10 @@ module Ace
55
55
  puts
56
56
  show_help
57
57
  1
58
- rescue Ace::Git::PrNotFoundError,
59
- Ace::Git::GhAuthenticationError,
60
- Ace::Git::GhNotInstalledError,
58
+ rescue Ace::Git::ProviderObjectNotFoundError,
59
+ Ace::Git::ProviderAuthenticationError,
60
+ Ace::Git::ProviderCliMissingError,
61
+ Ace::Git::ProviderIdentityMismatchError,
61
62
  Ace::Git::TimeoutError => e
62
63
  puts "Error: #{e.message}"
63
64
  1
@@ -86,9 +87,12 @@ module Ace
86
87
  Task ID formats: 081, task.081, v.0.9.0+081
87
88
 
88
89
  PR-AWARE CREATION:
89
- --pr <number> Create worktree for a GitHub pull request
90
+ --pr <number> Create worktree for a pull request
90
91
  --pull-request <number> (alias for --pr)
91
- Requires gh CLI to be installed and authenticated
92
+ Requires a configured forge server and its
93
+ provider package; fetches the exact declared
94
+ source repository/ref and verifies the head
95
+ SHA against the PR evidence.
92
96
 
93
97
  BRANCH-AWARE CREATION:
94
98
  -b <branch> Create worktree from a branch (local or remote)
@@ -100,12 +104,14 @@ module Ace
100
104
  --path <path> Custom worktree path (default: from config)
101
105
  --source <ref> Git ref to use as branch start-point (default: current branch)
102
106
  Examples: main, origin/develop, HEAD~3, commit-sha
107
+ --server <name> Use the named configured forge server (PR mode)
108
+ --default-server Use the configured default forge server
103
109
  --dry-run Show what would be created without creating
104
110
  --no-status-update Skip marking task as in-progress (task mode only)
105
111
  --no-commit Skip committing task changes (task mode only)
106
112
  --no-push Skip pushing task changes to remote (task mode only)
107
113
  --no-upstream Skip pushing worktree branch with upstream tracking (task mode only)
108
- --no-pr Skip creating draft PR (task mode only)
114
+ --no-pr Skip creating draft PR; keeps task creation local and provider-free
109
115
  --no-bootstrap Skip worktree bootstrap preparation
110
116
  --push-remote <name> Remote to push to (default: origin) (task mode only)
111
117
  --no-auto-navigate Stay in current directory (default: navigate to worktree)
@@ -120,8 +126,8 @@ module Ace
120
126
  # Create task worktree based on main instead of current branch
121
127
  ace-git-worktree create --task 081 --source main
122
128
 
123
- # Create PR worktree
124
- ace-git-worktree create --pr 26
129
+ # Create PR worktree on a named forge server (no mutation)
130
+ ace-git-worktree create --pr 26 --server forgejo-lab --dry-run
125
131
 
126
132
  # Create worktree from remote branch
127
133
  ace-git-worktree create -b origin/feature/auth
@@ -148,9 +154,10 @@ module Ace
148
154
  - hooks.after_create: Commands to run after worktree creation
149
155
 
150
156
  REQUIREMENTS:
151
- PR-aware creation requires GitHub CLI (gh):
152
- - Install: brew install gh
153
- - Authenticate: gh auth login
157
+ PR-aware creation requires a configured forge server
158
+ (git.servers) and the provider package for its provider
159
+ type. Task and branch creation are fully local and never
160
+ touch a forge.
154
161
  HELP
155
162
  0
156
163
  end
@@ -168,6 +175,8 @@ module Ace
168
175
  branch: nil,
169
176
  path: nil,
170
177
  source: nil,
178
+ server: nil,
179
+ default_server: false,
171
180
  dry_run: false,
172
181
  no_status_update: false,
173
182
  no_commit: false,
@@ -203,6 +212,11 @@ module Ace
203
212
  when "--source"
204
213
  i += 1
205
214
  options[:source] = args[i]
215
+ when "--server"
216
+ i += 1
217
+ options[:server] = args[i]
218
+ when "--default-server"
219
+ options[:default_server] = true
206
220
  when "--dry-run"
207
221
  options[:dry_run] = true
208
222
  when "--no-status-update"
@@ -375,6 +389,8 @@ module Ace
375
389
  push_remote: options[:push_remote],
376
390
  commit_message: options[:commit_message],
377
391
  target_branch: options[:target_branch],
392
+ server: options[:server],
393
+ default_server: options[:default_server],
378
394
  no_mise_trust: options[:no_mise_trust],
379
395
  force: options[:force]
380
396
  }.compact
@@ -408,7 +424,7 @@ module Ace
408
424
  end
409
425
  end
410
426
 
411
- # Create a PR worktree
427
+ # Create a PR worktree from normalized provider evidence.
412
428
  #
413
429
  # @param options [Hash] Command options
414
430
  # @return [Integer] Exit code
@@ -432,47 +448,28 @@ module Ace
432
448
 
433
449
  puts "Creating worktree for PR ##{pr_number}..."
434
450
 
435
- # Check gh CLI availability using ace-git's PrMetadataFetcher
436
- unless Ace::Git::Molecules::PrMetadataFetcher.gh_installed?
437
- puts "Error: gh CLI is required for PR worktree creation."
438
- puts
439
- puts gh_not_available_message
440
- return 1
441
- end
442
-
443
- unless Ace::Git::Molecules::PrMetadataFetcher.gh_authenticated?
444
- puts "Error: gh CLI is not authenticated."
445
- puts
446
- puts "Authenticate with: gh auth login"
447
- return 1
448
- end
449
-
450
- # Fetch PR data
451
- puts "Fetching PR information..."
452
451
  begin
453
- result = Ace::Git::Molecules::PrMetadataFetcher.fetch_metadata(pr_number.to_s)
454
-
455
- unless result[:success]
456
- puts "Error: #{result[:error]}"
457
- return 1
458
- end
459
-
460
- metadata = result[:metadata]
461
-
462
- # Convert to pr_data format expected by worktree creator
463
- pr_data = pr_data_from_metadata(metadata)
464
-
465
- # Show PR details
466
- puts "PR ##{pr_data[:number]}: #{pr_data[:title]}"
467
- puts "Branch: #{pr_data[:head_branch]} -> #{pr_data[:base_branch]}"
468
-
469
- # Warn about fork PRs
470
- if pr_data[:is_cross_repository]
471
- puts
472
- puts "Warning: This PR is from a fork (#{pr_data[:head_repository_owner]})."
473
- puts "You will not be able to push to the PR branch directly."
452
+ # Fetch normalized evidence through the selected forge server.
453
+ puts "Fetching PR information..."
454
+ resolver = Molecules::PullRequestEvidenceResolver.new(
455
+ server_name: options[:server],
456
+ use_default: options[:default_server] == true
457
+ )
458
+ resolved = resolver.resolve(pr_number)
459
+ evidence = resolved[:evidence]
460
+
461
+ display_pr_evidence(evidence)
462
+
463
+ # Fetch and verify the exact declared source (skipped for dry
464
+ # runs, which may read evidence but never create anything).
465
+ checkout = nil
466
+ unless options[:dry_run]
467
+ checkout = Molecules::PullRequestCheckoutPreparer.new.prepare(evidence)
468
+ unless checkout[:success]
469
+ puts "Error: #{checkout[:error]}"
470
+ return 1
471
+ end
474
472
  end
475
- puts
476
473
 
477
474
  # Prepare creation options
478
475
  creation_options = {
@@ -483,7 +480,7 @@ module Ace
483
480
  }.compact
484
481
 
485
482
  # Create the worktree
486
- result = @manager.create_pr(pr_number, pr_data, creation_options)
483
+ result = @manager.create_pr(evidence, checkout, creation_options)
487
484
 
488
485
  if result[:success]
489
486
  display_pr_creation_result(result, options[:dry_run])
@@ -493,14 +490,30 @@ module Ace
493
490
  display_warnings(result[:warnings]) if result[:warnings]
494
491
  1
495
492
  end
496
- rescue Ace::Git::PrNotFoundError, Ace::Git::GhAuthenticationError,
497
- Ace::Git::GhNotInstalledError => e
498
- # Let specific ace-git errors be handled by handle_pr_fetch_error
499
- # Other errors will bubble up to the top-level rescue in run()
500
- handle_pr_fetch_error(e, pr_number)
493
+ rescue Ace::Git::Error => e
494
+ puts "Error: #{e.message}"
495
+ puts "Debug: #{e.class}" if ENV["DEBUG"]
496
+ 1
501
497
  end
502
498
  end
503
499
 
500
+ # Display normalized PR evidence before checkout.
501
+ #
502
+ # @param evidence [Ace::Git::ProviderPullRequest] normalized evidence
503
+ def display_pr_evidence(evidence)
504
+ puts "PR ##{evidence.number}: #{evidence.title}"
505
+ puts "Branch: #{evidence.head_ref} -> #{evidence.base_ref}"
506
+ puts "Source: #{evidence.head_repository_url}@#{evidence.head_ref}" \
507
+ " (head #{evidence.head_sha.to_s[0, 8] || "unknown"})"
508
+ puts "Server: #{evidence.server_name}"
509
+
510
+ return unless evidence.head_repository_url != evidence.base_repository_url
511
+
512
+ puts
513
+ puts "Warning: This PR is from a fork (#{evidence.head_repository_url})."
514
+ puts "You will not be able to push to the PR branch directly."
515
+ end
516
+
504
517
  # Create a branch worktree
505
518
  #
506
519
  # @param options [Hash] Command options
@@ -863,104 +876,6 @@ module Ace
863
876
  system("which ace-tmux > /dev/null 2>&1")
864
877
  end
865
878
 
866
- # Convert PR metadata from gh CLI to internal pr_data format
867
- #
868
- # Anti-corruption layer that translates gh CLI JSON output to internal format.
869
- # This isolates worktree creation from gh CLI output structure changes.
870
- #
871
- # Expected metadata schema from ace-git PrMetadataFetcher (via gh pr view --json):
872
- # {
873
- # "number" => Integer,
874
- # "title" => String,
875
- # "headRefName" => String (PR source branch),
876
- # "baseRefName" => String (PR target branch),
877
- # "isCrossRepository" => Boolean (true for fork PRs),
878
- # "headRepositoryOwner" => { "login" => String } (fork owner info)
879
- # }
880
- #
881
- # @param metadata [Hash] PR metadata from gh CLI
882
- # @return [Hash] pr_data format expected by worktree creator
883
- def pr_data_from_metadata(metadata)
884
- {
885
- number: metadata["number"],
886
- title: metadata["title"],
887
- head_branch: metadata["headRefName"],
888
- base_branch: metadata["baseRefName"],
889
- is_cross_repository: metadata["isCrossRepository"] || false,
890
- head_repository_owner: metadata.dig("headRepositoryOwner", "login") || "unknown"
891
- }
892
- end
893
-
894
- # Handle errors during PR metadata fetch
895
- #
896
- # @param error [Exception] The error that occurred
897
- # @param pr_number [Integer] PR number for context in error messages
898
- # @return [Integer] Exit code (always 1 for errors)
899
- def handle_pr_fetch_error(error, pr_number)
900
- case error
901
- when Ace::Git::PrNotFoundError
902
- handle_pr_not_found(error, pr_number)
903
- when Ace::Git::GhAuthenticationError
904
- handle_gh_auth_error(error)
905
- when Ace::Git::GhNotInstalledError
906
- handle_gh_not_installed(error)
907
- else
908
- handle_unknown_error(error)
909
- end
910
- 1
911
- end
912
-
913
- # Handle PR not found error with helpful suggestions
914
- def handle_pr_not_found(error, pr_number)
915
- puts "Error: #{error.message}"
916
- puts
917
- puts "Suggestions:"
918
- puts " 1. Verify the PR number is correct"
919
- puts " 2. Check if the PR exists: gh pr view #{pr_number}"
920
- puts " 3. Ensure you're in the correct repository"
921
- end
922
-
923
- # Handle GitHub authentication error with troubleshooting steps
924
- def handle_gh_auth_error(error)
925
- puts "Error: #{error.message}"
926
- puts
927
- puts "Troubleshooting:"
928
- puts " 1. Verify GitHub authentication: gh auth status"
929
- puts " 2. Re-authenticate if needed: gh auth login"
930
- puts " 3. Check repository access permissions"
931
- end
932
-
933
- # Handle gh CLI not installed error with installation guidance
934
- def handle_gh_not_installed(error)
935
- puts "Error: #{error.message}"
936
- puts
937
- puts gh_not_available_message
938
- end
939
-
940
- # Handle unknown/unexpected errors with debug info
941
- def handle_unknown_error(error)
942
- puts "Error: #{error.message}"
943
- if ENV["DEBUG"]
944
- puts "Debug: #{error.class}"
945
- puts "Debug: #{error.backtrace&.first}" if error.backtrace
946
- end
947
- end
948
-
949
- # Get helpful error message when gh CLI is unavailable
950
- #
951
- # @return [String] User-friendly error message with installation guidance
952
- def gh_not_available_message
953
- <<~MESSAGE
954
- gh CLI is required for PR worktrees but is not installed.
955
-
956
- Install gh CLI:
957
- - macOS: brew install gh
958
- - Linux: See https://github.com/cli/cli#installation
959
- - Windows: See https://github.com/cli/cli#installation
960
-
961
- After installation, authenticate with: gh auth login
962
- MESSAGE
963
- end
964
879
  end
965
880
  end
966
881
  end