carson 3.22.0 → 3.23.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 +4 -4
- data/API.md +19 -20
- data/MANUAL.md +76 -65
- data/README.md +42 -50
- data/RELEASE.md +24 -1
- data/SKILL.md +1 -1
- data/VERSION +1 -1
- data/carson.gemspec +3 -4
- data/hooks/command-guard +1 -1
- data/hooks/pre-push +17 -20
- data/lib/carson/adapters/agent.rb +2 -2
- data/lib/carson/branch.rb +38 -0
- data/lib/carson/cli.rb +45 -30
- data/lib/carson/config.rb +80 -29
- data/lib/carson/delivery.rb +64 -0
- data/lib/carson/ledger.rb +305 -0
- data/lib/carson/repository.rb +47 -0
- data/lib/carson/revision.rb +30 -0
- data/lib/carson/runtime/audit.rb +43 -17
- data/lib/carson/runtime/deliver.rb +163 -149
- data/lib/carson/runtime/govern.rb +233 -357
- data/lib/carson/runtime/housekeep.rb +233 -27
- data/lib/carson/runtime/local/onboard.rb +29 -29
- data/lib/carson/runtime/local/prune.rb +120 -35
- data/lib/carson/runtime/local/sync.rb +29 -7
- data/lib/carson/runtime/local/template.rb +30 -12
- data/lib/carson/runtime/local/worktree.rb +37 -442
- data/lib/carson/runtime/review/gate_support.rb +144 -12
- data/lib/carson/runtime/review/sweep_support.rb +2 -2
- data/lib/carson/runtime/review/utility.rb +1 -1
- data/lib/carson/runtime/review.rb +21 -77
- data/lib/carson/runtime/setup.rb +25 -33
- data/lib/carson/runtime/status.rb +96 -212
- data/lib/carson/runtime.rb +39 -4
- data/lib/carson/worktree.rb +497 -0
- data/lib/carson.rb +6 -0
- metadata +37 -17
- data/.github/copilot-instructions.md +0 -1
- data/.github/pull_request_template.md +0 -12
- data/templates/.github/AGENTS.md +0 -1
- data/templates/.github/CLAUDE.md +0 -1
- data/templates/.github/carson.md +0 -47
- data/templates/.github/copilot-instructions.md +0 -1
- data/templates/.github/pull_request_template.md +0 -12
|
@@ -1,86 +1,118 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
# `carson deliver` pushes and creates the PR.
|
|
4
|
-
# `carson deliver --merge` also merges if CI passes or no checks are configured.
|
|
5
|
-
# `carson deliver --json` outputs structured result for agent consumption.
|
|
1
|
+
# Branch delivery lifecycle — push, create/update PR, and register Carson-owned delivery state.
|
|
2
|
+
# `carson deliver` is now the async handoff point. It does not wait for merge.
|
|
6
3
|
module Carson
|
|
7
4
|
class Runtime
|
|
8
5
|
module Deliver
|
|
9
6
|
# Entry point for `carson deliver`.
|
|
10
|
-
# Pushes current branch,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
result
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
# Pushes the current branch, ensures a PR exists, records delivery state, and returns.
|
|
8
|
+
def deliver!( title: nil, body_file: nil, json_output: false )
|
|
9
|
+
branch_name = current_branch
|
|
10
|
+
main_branch = config.main_branch
|
|
11
|
+
remote_name = config.git_remote
|
|
12
|
+
result = { command: "deliver", branch: branch_name }
|
|
13
|
+
|
|
14
|
+
if branch_name == main_branch
|
|
15
|
+
result[ :error ] = "cannot deliver from #{main_branch}"
|
|
16
|
+
result[ :recovery ] = "carson worktree create <name>"
|
|
17
|
+
return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
sync_exit, sync_diagnostics = deliver_template_sync
|
|
21
|
+
if sync_exit == EXIT_ERROR
|
|
22
|
+
result[ :error ] = sync_diagnostics.to_s.strip.empty? ? "template sync failed" : sync_diagnostics.strip
|
|
23
|
+
return deliver_finish( result: result, exit_code: sync_exit, json_output: json_output )
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
push_exit = push_branch!( branch: branch, remote: remote, result: result )
|
|
26
|
+
push_exit = push_branch!( branch: branch_name, remote: remote_name, result: result )
|
|
27
27
|
return deliver_finish( result: result, exit_code: push_exit, json_output: json_output ) unless push_exit == EXIT_OK
|
|
28
28
|
|
|
29
|
-
# Step 2: find or create the PR.
|
|
30
29
|
pr_number, pr_url = find_or_create_pr!(
|
|
31
|
-
branch:
|
|
30
|
+
branch: branch_name,
|
|
31
|
+
title: title,
|
|
32
|
+
body_file: body_file,
|
|
33
|
+
result: result
|
|
32
34
|
)
|
|
33
|
-
if pr_number.nil?
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
return deliver_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output ) if pr_number.nil?
|
|
36
|
+
|
|
37
|
+
branch = branch_record( name: branch_name )
|
|
38
|
+
delivery = ledger.upsert_delivery(
|
|
39
|
+
repository: repository_record,
|
|
40
|
+
branch_name: branch.name,
|
|
41
|
+
head: branch.head || current_head,
|
|
42
|
+
worktree_path: branch.worktree || repo_root,
|
|
43
|
+
authority: config.govern_authority,
|
|
44
|
+
pr_number: pr_number,
|
|
45
|
+
pr_url: pr_url,
|
|
46
|
+
status: "preparing",
|
|
47
|
+
summary: "delivery accepted",
|
|
48
|
+
cause: nil
|
|
49
|
+
)
|
|
50
|
+
delivery = assess_delivery!( delivery: delivery, branch_name: branch.name )
|
|
36
51
|
|
|
37
52
|
result[ :pr_number ] = pr_number
|
|
38
53
|
result[ :pr_url ] = pr_url
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
result[ :ci ] = check_pr_ci( number: pr_number ).to_s
|
|
55
|
+
result[ :delivery ] = delivery_payload( delivery: delivery )
|
|
56
|
+
result[ :summary ] = delivery.summary
|
|
57
|
+
result[ :next_step ] = "carson status"
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
result[ :ci ] = ci_status.to_s
|
|
47
|
-
|
|
48
|
-
case ci_status
|
|
49
|
-
when :pass, :none
|
|
50
|
-
# Continue to review gate. :none means no checks configured — nothing to wait for.
|
|
51
|
-
when :pending
|
|
52
|
-
result[ :recovery ] = "gh pr checks #{pr_number} --watch && carson deliver --merge"
|
|
53
|
-
return deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
|
|
54
|
-
when :fail
|
|
55
|
-
result[ :recovery ] = "gh pr checks #{pr_number} — fix failures, push, then `carson deliver --merge`"
|
|
56
|
-
return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Step 4: check review gate — block if changes are requested.
|
|
60
|
-
review = check_pr_review( number: pr_number )
|
|
61
|
-
result[ :review ] = review.to_s
|
|
62
|
-
if review == :changes_requested
|
|
63
|
-
result[ :error ] = "review changes requested on PR ##{pr_number}"
|
|
64
|
-
result[ :recovery ] = "address review comments, push, then `carson deliver --merge`"
|
|
65
|
-
return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
|
|
66
|
-
end
|
|
59
|
+
deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
|
|
60
|
+
end
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
merge_exit = merge_pr!( number: pr_number, result: result )
|
|
70
|
-
return deliver_finish( result: result, exit_code: merge_exit, json_output: json_output ) unless merge_exit == EXIT_OK
|
|
62
|
+
private
|
|
71
63
|
|
|
72
|
-
|
|
64
|
+
def deliver_template_sync
|
|
65
|
+
saved_output, saved_error = @output, @error
|
|
66
|
+
captured_out = StringIO.new
|
|
67
|
+
captured_err = StringIO.new
|
|
68
|
+
@output = captured_out
|
|
69
|
+
@error = captured_err
|
|
70
|
+
exit_code = template_apply!( push_prep: true )
|
|
71
|
+
[ exit_code, captured_out.string + captured_err.string ]
|
|
72
|
+
rescue StandardError => exception
|
|
73
|
+
[ EXIT_ERROR, "template sync error: #{exception.message}" ]
|
|
74
|
+
ensure
|
|
75
|
+
@output, @error = saved_output, saved_error
|
|
76
|
+
end
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
# Assesses delivery readiness and records Carson's current branch state.
|
|
79
|
+
def assess_delivery!( delivery:, branch_name: )
|
|
80
|
+
review = check_pr_review( number: delivery.pull_request_number, branch: branch_name, pr_url: delivery.pull_request_url )
|
|
81
|
+
ci = check_pr_ci( number: delivery.pull_request_number )
|
|
82
|
+
status, cause, summary = delivery_assessment( ci: ci, review: review )
|
|
83
|
+
|
|
84
|
+
ledger.update_delivery(
|
|
85
|
+
delivery: delivery,
|
|
86
|
+
status: status,
|
|
87
|
+
cause: cause,
|
|
88
|
+
summary: summary,
|
|
89
|
+
pr_number: delivery.pull_request_number,
|
|
90
|
+
pr_url: delivery.pull_request_url,
|
|
91
|
+
worktree_path: delivery.worktree_path
|
|
92
|
+
)
|
|
93
|
+
end
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
def delivery_assessment( ci:, review: )
|
|
96
|
+
return [ "gated", "ci", "waiting for CI checks" ] if ci == :pending
|
|
97
|
+
return [ "gated", "ci", "CI checks are failing" ] if ci == :fail
|
|
98
|
+
return [ "gated", "review", "review changes requested" ] if review.fetch( :review, :none ) == :changes_requested
|
|
99
|
+
return [ "gated", "review", "waiting for review" ] if review.fetch( :review, :none ) == :review_required
|
|
100
|
+
return [ "gated", "review", review.fetch( :detail ).to_s ] if review.fetch( :status, :pass ) == :fail
|
|
101
|
+
return [ "gated", "policy", "unable to assess review gate: #{review.fetch( :detail )}" ] if review.fetch( :status, :pass ) == :error
|
|
79
102
|
|
|
80
|
-
|
|
103
|
+
[ "queued", nil, "ready to integrate into #{config.main_branch}" ]
|
|
81
104
|
end
|
|
82
105
|
|
|
83
|
-
|
|
106
|
+
def delivery_payload( delivery: )
|
|
107
|
+
{
|
|
108
|
+
id: delivery.id,
|
|
109
|
+
status: delivery.status,
|
|
110
|
+
head: delivery.head,
|
|
111
|
+
worktree_path: delivery.worktree_path,
|
|
112
|
+
revision_count: delivery.revision_count,
|
|
113
|
+
cause: delivery.cause
|
|
114
|
+
}
|
|
115
|
+
end
|
|
84
116
|
|
|
85
117
|
# Outputs the final result — JSON or human-readable — and returns exit code.
|
|
86
118
|
def deliver_finish( result:, exit_code:, json_output: )
|
|
@@ -97,78 +129,76 @@ module Carson
|
|
|
97
129
|
|
|
98
130
|
# Human-readable output for deliver results.
|
|
99
131
|
def print_deliver_human( result: )
|
|
100
|
-
exit_code = result.fetch( :exit_code )
|
|
101
|
-
|
|
102
132
|
if result[ :error ]
|
|
103
|
-
puts_line
|
|
104
|
-
puts_line "
|
|
133
|
+
puts_line result[ :error ]
|
|
134
|
+
puts_line " → #{result[ :recovery ]}" if result[ :recovery ]
|
|
105
135
|
return
|
|
106
136
|
end
|
|
107
137
|
|
|
108
|
-
if result[ :pr_number ]
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if result[ :ci ]
|
|
113
|
-
ci = result[ :ci ]
|
|
114
|
-
case ci
|
|
115
|
-
when "pass"
|
|
116
|
-
puts_line "CI: pass"
|
|
117
|
-
when "none"
|
|
118
|
-
puts_line "CI: none — no checks configured, proceeding."
|
|
119
|
-
when "pending"
|
|
120
|
-
puts_line "CI: pending — merge when checks complete."
|
|
121
|
-
puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
|
|
122
|
-
when "fail"
|
|
123
|
-
puts_line "CI: failing — fix before merging."
|
|
124
|
-
puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
if result[ :merged ]
|
|
129
|
-
puts_line "Merged PR ##{result[ :pr_number ]} via #{result[ :merge_method ]}."
|
|
130
|
-
puts_line " Next: #{result[ :next_step ]}" if result[ :next_step ]
|
|
138
|
+
puts_line "PR: ##{result[ :pr_number ]} #{result[ :pr_url ]}" if result[ :pr_number ]
|
|
139
|
+
if result[ :delivery ]
|
|
140
|
+
puts_line "Delivery: #{result.dig( :delivery, :status )}"
|
|
131
141
|
end
|
|
142
|
+
puts_line "Summary: #{result[ :summary ]}" if result[ :summary ]
|
|
143
|
+
puts_line " Next: #{result[ :next_step ]}" if result[ :next_step ]
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
# Pushes the branch to the remote with tracking.
|
|
135
|
-
# Sets CARSON_PUSH=1 so the pre-push hook knows this is a Carson-managed push.
|
|
136
147
|
def push_branch!( branch:, remote:, result: )
|
|
137
|
-
_, push_stderr, push_success, =
|
|
138
|
-
|
|
148
|
+
_, push_stderr, push_success, = git_run( "push", "--no-verify", "-u", remote, branch )
|
|
149
|
+
|
|
150
|
+
if !push_success && push_stderr.to_s.include?( "non-fast-forward" )
|
|
151
|
+
return force_push_with_lease!( branch: branch, remote: remote, result: result )
|
|
139
152
|
end
|
|
153
|
+
|
|
140
154
|
unless push_success
|
|
141
155
|
error_text = push_stderr.to_s.strip
|
|
142
156
|
error_text = "push failed" if error_text.empty?
|
|
143
157
|
result[ :error ] = error_text
|
|
144
|
-
result[ :recovery ] = "git pull #{remote} #{branch} --rebase && git push -u #{remote} #{branch}"
|
|
145
158
|
return EXIT_ERROR
|
|
146
159
|
end
|
|
147
160
|
puts_verbose "pushed #{branch} to #{remote}"
|
|
148
161
|
EXIT_OK
|
|
149
162
|
end
|
|
150
163
|
|
|
164
|
+
# Retries push with --force-with-lease after a non-fast-forward rejection.
|
|
165
|
+
def force_push_with_lease!( branch:, remote:, result: )
|
|
166
|
+
puts_verbose "push rejected (non-fast-forward), retrying with --force-with-lease"
|
|
167
|
+
_, lease_stderr, lease_success, = git_run( "push", "--no-verify", "--force-with-lease", "-u", remote, branch )
|
|
168
|
+
|
|
169
|
+
if lease_success
|
|
170
|
+
puts_verbose "pushed #{branch} to #{remote} (force-with-lease)"
|
|
171
|
+
return EXIT_OK
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if lease_stderr.to_s.include?( "stale info" )
|
|
175
|
+
result[ :error ] = "force-with-lease rejected — another push landed on #{branch} since your last fetch"
|
|
176
|
+
result[ :recovery ] = "git fetch #{remote} #{branch} && carson deliver"
|
|
177
|
+
else
|
|
178
|
+
error_text = lease_stderr.to_s.strip
|
|
179
|
+
error_text = "push failed (force-with-lease)" if error_text.empty?
|
|
180
|
+
result[ :error ] = error_text
|
|
181
|
+
end
|
|
182
|
+
EXIT_ERROR
|
|
183
|
+
end
|
|
184
|
+
|
|
151
185
|
# Finds an existing PR for the branch, or creates a new one.
|
|
152
|
-
# Returns [number, url] or [nil, nil] on failure.
|
|
153
186
|
def find_or_create_pr!( branch:, title: nil, body_file: nil, result: )
|
|
154
|
-
# Check for existing PR.
|
|
155
187
|
existing = find_existing_pr( branch: branch )
|
|
156
188
|
return existing if existing.first
|
|
157
189
|
|
|
158
|
-
# Create a new PR.
|
|
159
190
|
create_pr!( branch: branch, title: title, body_file: body_file, result: result )
|
|
160
191
|
end
|
|
161
192
|
|
|
162
193
|
# Queries gh for an open PR on this branch.
|
|
163
|
-
# Returns [number, url] or [nil, nil].
|
|
164
194
|
def find_existing_pr( branch: )
|
|
165
195
|
stdout, _, success, = gh_run(
|
|
166
196
|
"pr", "view", branch,
|
|
167
|
-
"--json", "number,url"
|
|
197
|
+
"--json", "number,url,state"
|
|
168
198
|
)
|
|
169
199
|
if success
|
|
170
200
|
data = JSON.parse( stdout ) rescue nil
|
|
171
|
-
if data && data[ "number" ]
|
|
201
|
+
if data && data[ "number" ] && data[ "state" ] == "OPEN"
|
|
172
202
|
return [ data[ "number" ], data[ "url" ].to_s ]
|
|
173
203
|
end
|
|
174
204
|
end
|
|
@@ -176,11 +206,10 @@ module Carson
|
|
|
176
206
|
end
|
|
177
207
|
|
|
178
208
|
# Creates a PR via gh. Title defaults to branch name humanised.
|
|
179
|
-
# Returns [number, url] or [nil, nil] on failure.
|
|
180
209
|
def create_pr!( branch:, title: nil, body_file: nil, result: )
|
|
181
210
|
pr_title = title || default_pr_title( branch: branch )
|
|
182
|
-
|
|
183
211
|
args = [ "pr", "create", "--title", pr_title, "--head", branch ]
|
|
212
|
+
|
|
184
213
|
if body_file && File.exist?( body_file )
|
|
185
214
|
args.push( "--body-file", body_file )
|
|
186
215
|
else
|
|
@@ -196,24 +225,18 @@ module Carson
|
|
|
196
225
|
return [ nil, nil ]
|
|
197
226
|
end
|
|
198
227
|
|
|
199
|
-
# gh pr create prints the URL on success. Parse number from it.
|
|
200
228
|
pr_url = stdout.to_s.strip
|
|
201
229
|
pr_number = pr_url.split( "/" ).last.to_i
|
|
202
|
-
if pr_number > 0
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# Fallback: query the just-created PR.
|
|
206
|
-
find_existing_pr( branch: branch )
|
|
207
|
-
end
|
|
230
|
+
return [ pr_number, pr_url ] if pr_number > 0
|
|
231
|
+
|
|
232
|
+
find_existing_pr( branch: branch )
|
|
208
233
|
end
|
|
209
234
|
|
|
210
|
-
# Generates a default PR title from the branch name.
|
|
211
235
|
def default_pr_title( branch: )
|
|
212
|
-
branch.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) {
|
|
236
|
+
branch.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) { |character| character.upcase }
|
|
213
237
|
end
|
|
214
238
|
|
|
215
239
|
# Checks CI status on a PR. Returns :pass, :fail, :pending, or :none.
|
|
216
|
-
# Uses the `bucket` field (pass/fail/pending) from `gh pr checks --json`.
|
|
217
240
|
def check_pr_ci( number: )
|
|
218
241
|
stdout, _, success, = gh_run(
|
|
219
242
|
"pr", "checks", number.to_s,
|
|
@@ -224,35 +247,47 @@ module Carson
|
|
|
224
247
|
checks = JSON.parse( stdout ) rescue []
|
|
225
248
|
return :none if checks.empty?
|
|
226
249
|
|
|
227
|
-
buckets = checks.map {
|
|
250
|
+
buckets = checks.map { |entry| entry[ "bucket" ].to_s.downcase }
|
|
228
251
|
return :fail if buckets.include?( "fail" )
|
|
229
252
|
return :pending if buckets.include?( "pending" )
|
|
230
253
|
|
|
231
254
|
:pass
|
|
232
255
|
end
|
|
233
256
|
|
|
234
|
-
# Checks review
|
|
235
|
-
def check_pr_review( number: )
|
|
257
|
+
# Checks the full review gate on a PR. Returns a structured result hash.
|
|
258
|
+
def check_pr_review( number:, branch:, pr_url: nil )
|
|
259
|
+
owner, repo = repository_coordinates
|
|
260
|
+
report = review_gate_report_for_pr(
|
|
261
|
+
owner: owner,
|
|
262
|
+
repo: repo,
|
|
263
|
+
pr_number: number,
|
|
264
|
+
branch_name: branch,
|
|
265
|
+
pr_summary: {
|
|
266
|
+
number: number,
|
|
267
|
+
title: "",
|
|
268
|
+
url: pr_url.to_s,
|
|
269
|
+
state: "OPEN"
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
review_gate_result( report: report )
|
|
273
|
+
rescue StandardError => exception
|
|
274
|
+
{ status: :error, review: :error, detail: exception.message }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Returns the current PR state for govern reconciliation.
|
|
278
|
+
def pull_request_state( number: )
|
|
236
279
|
stdout, _, success, = gh_run(
|
|
237
280
|
"pr", "view", number.to_s,
|
|
238
|
-
"--json", "
|
|
281
|
+
"--json", "number,state,isDraft,url"
|
|
239
282
|
)
|
|
240
|
-
return
|
|
283
|
+
return nil unless success
|
|
241
284
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
when "APPROVED" then :approved
|
|
246
|
-
when "CHANGES_REQUESTED" then :changes_requested
|
|
247
|
-
when "REVIEW_REQUIRED" then :review_required
|
|
248
|
-
else :none
|
|
249
|
-
end
|
|
285
|
+
JSON.parse( stdout )
|
|
286
|
+
rescue JSON::ParserError
|
|
287
|
+
nil
|
|
250
288
|
end
|
|
251
289
|
|
|
252
|
-
# Merges the PR using the
|
|
253
|
-
# Deliberately omits --delete-branch: gh tries to switch the local
|
|
254
|
-
# checkout to main afterwards, which fails inside a worktree where
|
|
255
|
-
# main is already checked output. Branch cleanup deferred to `carson prune`.
|
|
290
|
+
# Merges the PR using the governed merge method.
|
|
256
291
|
def merge_pr!( number:, result: )
|
|
257
292
|
method = config.govern_merge_method
|
|
258
293
|
result[ :merge_method ] = method
|
|
@@ -274,9 +309,6 @@ module Carson
|
|
|
274
309
|
end
|
|
275
310
|
|
|
276
311
|
# Syncs main after a successful merge.
|
|
277
|
-
# Pulls into the main worktree directly — does not attempt checkout,
|
|
278
|
-
# because checkout would fail when running inside a feature worktree
|
|
279
|
-
# (main is already checked output in the main tree).
|
|
280
312
|
def sync_after_merge!( remote:, main:, result: )
|
|
281
313
|
main_root = main_worktree_root
|
|
282
314
|
_, pull_stderr, pull_success, = Open3.capture3(
|
|
@@ -291,24 +323,6 @@ module Carson
|
|
|
291
323
|
puts_verbose "sync failed: #{pull_stderr.to_s.strip}"
|
|
292
324
|
end
|
|
293
325
|
end
|
|
294
|
-
|
|
295
|
-
# Builds next-step guidance after a successful merge.
|
|
296
|
-
# Detects whether the agent is inside a worktree and suggests cleanup.
|
|
297
|
-
def compute_post_merge_next_step!( result: )
|
|
298
|
-
main_root = main_worktree_root
|
|
299
|
-
cwd = realpath_safe( Dir.pwd )
|
|
300
|
-
current_wt = worktree_list.select { it.fetch( :path ) != realpath_safe( main_root ) }
|
|
301
|
-
.find { cwd == it.fetch( :path ) || cwd.start_with?( File.join( it.fetch( :path ), "" ) ) }
|
|
302
|
-
|
|
303
|
-
if current_wt
|
|
304
|
-
wt_name = File.basename( current_wt.fetch( :path ) )
|
|
305
|
-
result[ :next_step ] = "cd #{main_root} && carson worktree remove #{wt_name}"
|
|
306
|
-
else
|
|
307
|
-
result[ :next_step ] = "carson prune"
|
|
308
|
-
end
|
|
309
|
-
rescue StandardError
|
|
310
|
-
# Best-effort — do not fail deliver because of next-step detection.
|
|
311
|
-
end
|
|
312
326
|
end
|
|
313
327
|
|
|
314
328
|
include Deliver
|