carson 3.20.0 → 3.21.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.
data/lib/carson/config.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # Loads and validates Carson configuration from global config and environment overrides.
1
2
  require "json"
2
3
 
3
4
  module Carson
@@ -85,8 +86,8 @@ module Carson
85
86
  parsed = JSON.parse( raw )
86
87
  raise ConfigError, "global config must be a JSON object at #{path}" unless parsed.is_a?( Hash )
87
88
  parsed
88
- rescue JSON::ParserError => e
89
- raise ConfigError, "invalid global config JSON at #{path} (#{e.message})"
89
+ rescue JSON::ParserError => exception
90
+ raise ConfigError, "invalid global config JSON at #{path} (#{exception.message})"
90
91
  end
91
92
 
92
93
  def self.global_config_path( repo_root: )
@@ -212,7 +213,7 @@ module Carson
212
213
  @audit_advisory_check_names = fetch_optional_string_array( hash: audit_hash, key: "advisory_check_names" )
213
214
 
214
215
  govern_hash = fetch_hash( hash: data, key: "govern" )
215
- @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |p| safe_expand_path( p ) }
216
+ @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
216
217
  @govern_auto_merge = fetch_optional_boolean( hash: govern_hash, key: "auto_merge", default: true, key_path: "govern.auto_merge" )
217
218
  @govern_merge_method = fetch_string( hash: govern_hash, key: "merge_method" ).downcase
218
219
  govern_agent_hash = fetch_hash( hash: govern_hash, key: "agent" )
@@ -11,7 +11,7 @@ module Carson
11
11
  return fingerprint_status unless fingerprint_status.nil?
12
12
  unless head_exists?
13
13
  if json_output
14
- out.puts JSON.pretty_generate( { command: "audit", status: "skipped", reason: "no commits yet", exit_code: EXIT_OK } )
14
+ output.puts JSON.pretty_generate( { command: "audit", status: "skipped", reason: "no commits yet", exit_code: EXIT_OK } )
15
15
  else
16
16
  puts_line "No commits yet — audit skipped for initial commit."
17
17
  end
@@ -69,24 +69,24 @@ module Carson
69
69
  audit_state = "attention" if audit_state == "ok" && !%w[ok skipped].include?( monitor_report.fetch( :status ) )
70
70
  if monitor_report.fetch( :status ) == "attention"
71
71
  checks = monitor_report.fetch( :checks )
72
- fail_n = checks.fetch( :failing_count )
73
- pend_n = checks.fetch( :pending_count )
72
+ failing_count = checks.fetch( :failing_count )
73
+ pending_count = checks.fetch( :pending_count )
74
74
  total = checks.fetch( :required_total )
75
75
  fail_names = checks.fetch( :failing ).map { it.fetch( :name ) }.join( ", " )
76
- if fail_n.positive? && pend_n.positive?
77
- audit_concise_problems << "Checks: #{fail_n} failing (#{fail_names}), #{pend_n} pending of #{total} required."
78
- elsif fail_n.positive?
79
- audit_concise_problems << "Checks: #{fail_n} of #{total} failing (#{fail_names})."
80
- elsif pend_n.positive?
81
- audit_concise_problems << "Checks: pending (#{total - pend_n} of #{total} complete)."
76
+ if failing_count.positive? && pending_count.positive?
77
+ audit_concise_problems << "Checks: #{failing_count} failing (#{fail_names}), #{pending_count} pending of #{total} required."
78
+ elsif failing_count.positive?
79
+ audit_concise_problems << "Checks: #{failing_count} of #{total} failing (#{fail_names})."
80
+ elsif pending_count.positive?
81
+ audit_concise_problems << "Checks: pending (#{total - pending_count} of #{total} complete)."
82
82
  end
83
83
  end
84
84
  puts_verbose ""
85
85
  puts_verbose "[Default Branch CI Baseline (gh)]"
86
86
  default_branch_baseline = default_branch_ci_baseline_report
87
87
  audit_state = "attention" if audit_state == "ok" && !%w[ok skipped].include?( default_branch_baseline.fetch( :status ) )
88
- baseline_st = default_branch_baseline.fetch( :status )
89
- if baseline_st == "block"
88
+ baseline_status = default_branch_baseline.fetch( :status )
89
+ if baseline_status == "block"
90
90
  parts = []
91
91
  if default_branch_baseline.fetch( :failing_count ).positive?
92
92
  names = default_branch_baseline.fetch( :failing ).map { it.fetch( :name ) }.join( ", " )
@@ -98,7 +98,7 @@ module Carson
98
98
  end
99
99
  parts << "no check-runs for active workflows" if default_branch_baseline.fetch( :no_check_evidence )
100
100
  audit_concise_problems << "Baseline (#{default_branch_baseline.fetch( :default_branch, config.main_branch )}): #{parts.join( ', ' )} — fix before merge."
101
- elsif baseline_st == "attention"
101
+ elsif baseline_status == "attention"
102
102
  parts = []
103
103
  if default_branch_baseline.fetch( :advisory_failing_count ).positive?
104
104
  names = default_branch_baseline.fetch( :advisory_failing ).map { it.fetch( :name ) }.join( ", " )
@@ -143,7 +143,7 @@ module Carson
143
143
  problems: audit_concise_problems,
144
144
  exit_code: exit_code
145
145
  }
146
- out.puts JSON.pretty_generate( result )
146
+ output.puts JSON.pretty_generate( result )
147
147
  else
148
148
  puts_verbose ""
149
149
  puts_verbose "[Audit Result]"
@@ -182,8 +182,8 @@ module Carson
182
182
  end
183
183
 
184
184
  begin
185
- rt = build_scoped_runtime( repo_path: repo_path )
186
- status = rt.audit!
185
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
186
+ status = scoped_runtime.audit!
187
187
  case status
188
188
  when EXIT_OK
189
189
  puts_line "#{repo_name}: ok" unless verbose?
@@ -197,9 +197,9 @@ module Carson
197
197
  record_batch_skip( command: "audit", repo_path: repo_path, reason: "audit failed" )
198
198
  failed += 1
199
199
  end
200
- rescue StandardError => e
201
- puts_line "#{repo_name}: FAIL (#{e.message})"
202
- record_batch_skip( command: "audit", repo_path: repo_path, reason: e.message )
200
+ rescue StandardError => exception
201
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
202
+ record_batch_skip( command: "audit", repo_path: repo_path, reason: exception.message )
203
203
  failed += 1
204
204
  end
205
205
  end
@@ -277,9 +277,9 @@ module Carson
277
277
  report.dig( :checks, :pending ).each { |entry| puts_verbose "check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
278
278
  report[ :status ] = "attention" if report.dig( :checks, :failing_count ).positive? || report.dig( :checks, :pending_count ).positive?
279
279
  report
280
- rescue JSON::ParserError => e
280
+ rescue JSON::ParserError => exception
281
281
  report[ :status ] = "skipped"
282
- report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
282
+ report[ :skip_reason ] = "invalid gh JSON response (#{exception.message})"
283
283
  puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
284
284
  report
285
285
  end
@@ -374,14 +374,14 @@ module Carson
374
374
  puts_verbose "ACTION: default branch has workflow files but no check-runs; align workflow triggers and branch protection check names."
375
375
  end
376
376
  report
377
- rescue JSON::ParserError => e
377
+ rescue JSON::ParserError => exception
378
378
  report[ :status ] = "skipped"
379
- report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
379
+ report[ :skip_reason ] = "invalid gh JSON response (#{exception.message})"
380
380
  puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
381
381
  report
382
- rescue StandardError => e
382
+ rescue StandardError => exception
383
383
  report[ :status ] = "skipped"
384
- report[ :skip_reason ] = e.message
384
+ report[ :skip_reason ] = exception.message
385
385
  puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
386
386
  report
387
387
  end
@@ -443,7 +443,7 @@ module Carson
443
443
  end
444
444
 
445
445
  # Returns true when a required-check entry is in a non-passing, non-pending state.
446
- # Cancelled, errored, timed-out, and any unknown bucket all count as failing.
446
+ # Cancelled, errored, timed-output, and any unknown bucket all count as failing.
447
447
  def check_entry_failing?( entry: )
448
448
  !%w[pass pending].include?( entry[ "bucket" ].to_s )
449
449
  end
@@ -487,8 +487,8 @@ module Carson
487
487
  markdown_path, json_path = write_pr_monitor_report( report: report )
488
488
  puts_verbose "report_markdown: #{markdown_path}"
489
489
  puts_verbose "report_json: #{json_path}"
490
- rescue StandardError => e
491
- puts_verbose "report_write: SKIP (#{e.message})"
490
+ rescue StandardError => exception
491
+ puts_verbose "report_write: SKIP (#{exception.message})"
492
492
  end
493
493
 
494
494
  # Persists report in both machine-readable JSON and human-readable Markdown.
@@ -90,7 +90,7 @@ module Carson
90
90
  result[ :exit_code ] = exit_code
91
91
 
92
92
  if json_output
93
- out.puts JSON.pretty_generate( result )
93
+ output.puts JSON.pretty_generate( result )
94
94
  else
95
95
  print_deliver_human( result: result )
96
96
  end
@@ -136,8 +136,11 @@ module Carson
136
136
  end
137
137
 
138
138
  # Pushes the branch to the remote with tracking.
139
+ # Sets CARSON_PUSH=1 so the pre-push hook knows this is a Carson-managed push.
139
140
  def push_branch!( branch:, remote:, result: )
140
- _, push_stderr, push_success, = git_run( "push", "-u", remote, branch )
141
+ _, push_stderr, push_success, = with_env_var( "CARSON_PUSH", "1" ) do
142
+ git_run( "push", "-u", remote, branch )
143
+ end
141
144
  unless push_success
142
145
  error_text = push_stderr.to_s.strip
143
146
  error_text = "push failed" if error_text.empty?
@@ -253,7 +256,7 @@ module Carson
253
256
  # Merges the PR using the configured merge method.
254
257
  # Deliberately omits --delete-branch: gh tries to switch the local
255
258
  # checkout to main afterwards, which fails inside a worktree where
256
- # main is already checked out. Branch cleanup deferred to `carson prune`.
259
+ # main is already checked output. Branch cleanup deferred to `carson prune`.
257
260
  def merge_pr!( number:, result: )
258
261
  method = config.govern_merge_method
259
262
  result[ :merge_method ] = method
@@ -277,7 +280,7 @@ module Carson
277
280
  # Syncs main after a successful merge.
278
281
  # Pulls into the main worktree directly — does not attempt checkout,
279
282
  # because checkout would fail when running inside a feature worktree
280
- # (main is already checked out in the main tree).
283
+ # (main is already checked output in the main tree).
281
284
  def sync_after_merge!( remote:, main:, result: )
282
285
  main_root = main_worktree_root
283
286
  _, pull_stderr, pull_success, = Open3.capture3(
@@ -55,8 +55,8 @@ module Carson
55
55
  end
56
56
 
57
57
  EXIT_OK
58
- rescue StandardError => e
59
- puts_line "ERROR: govern failed — #{e.message}"
58
+ rescue StandardError => exception
59
+ puts_line "ERROR: govern failed — #{exception.message}"
60
60
  EXIT_ERROR
61
61
  end
62
62
 
@@ -69,8 +69,8 @@ module Carson
69
69
  puts_line "── cycle #{cycle_count} at #{Time.now.utc.strftime( "%Y-%m-%d %H:%M:%S UTC" )} ──"
70
70
  begin
71
71
  govern_cycle!( dry_run: dry_run, json_output: json_output )
72
- rescue StandardError => e
73
- puts_line "ERROR: cycle #{cycle_count} failed — #{e.message}"
72
+ rescue StandardError => exception
73
+ puts_line "ERROR: cycle #{cycle_count} failed — #{exception.message}"
74
74
  end
75
75
  puts_line "sleeping #{loop_seconds}s until next cycle…"
76
76
  sleep loop_seconds
@@ -145,8 +145,8 @@ module Carson
145
145
  return nil
146
146
  end
147
147
  JSON.parse( stdout_text )
148
- rescue JSON::ParserError => e
149
- puts_line "gh pr list returned invalid JSON: #{e.message}"
148
+ rescue JSON::ParserError => exception
149
+ puts_line "gh pr list returned invalid JSON: #{exception.message}"
150
150
  nil
151
151
  end
152
152
 
@@ -345,13 +345,13 @@ module Carson
345
345
 
346
346
  # Runs sync + prune in the given repo after a successful merge.
347
347
  def housekeep_repo!( repo_path: )
348
- rt = if repo_path == self.repo_root
348
+ scoped_runtime = if repo_path == self.repo_root
349
349
  self
350
350
  else
351
- Runtime.new( repo_root: repo_path, tool_root: tool_root, out: out, err: err )
351
+ Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error )
352
352
  end
353
- sync_status = rt.sync!
354
- rt.prune! if sync_status == EXIT_OK
353
+ sync_status = scoped_runtime.sync!
354
+ scoped_runtime.prune! if sync_status == EXIT_OK
355
355
  end
356
356
 
357
357
  # Selects which agent provider to use based on config and availability.
@@ -410,18 +410,18 @@ module Carson
410
410
 
411
411
  # Evidence gathering — builds structured context Hash for agent work orders.
412
412
  def evidence( pr:, repo_path:, objective: )
413
- ctx = { title: pr.fetch( "title", "" ) }
413
+ context = { title: pr.fetch( "title", "" ) }
414
414
  case objective
415
415
  when "fix_ci"
416
- ctx.merge!( ci_evidence( pr: pr, repo_path: repo_path ) )
416
+ context.merge!( ci_evidence( pr: pr, repo_path: repo_path ) )
417
417
  when "address_review"
418
- ctx.merge!( review_evidence( pr: pr, repo_path: repo_path ) )
418
+ context.merge!( review_evidence( pr: pr, repo_path: repo_path ) )
419
419
  end
420
420
  prior = prior_attempt( pr: pr, repo_path: repo_path )
421
- ctx[ :prior_attempt ] = prior if prior
422
- ctx
423
- rescue StandardError => e
424
- puts_line " evidence gathering failed: #{e.message}"
421
+ context[ :prior_attempt ] = prior if prior
422
+ context
423
+ rescue StandardError => exception
424
+ puts_line " evidence gathering failed: #{exception.message}"
425
425
  { title: pr.fetch( "title", "" ) }
426
426
  end
427
427
 
@@ -452,8 +452,8 @@ module Carson
452
452
  return { ci_run_url: run_url } unless log_status.success?
453
453
 
454
454
  { ci_logs: truncate_log( text: log_stdout ), ci_run_url: run_url }
455
- rescue StandardError => e
456
- puts_line " ci_evidence failed: #{e.message}"
455
+ rescue StandardError => exception
456
+ puts_line " ci_evidence failed: #{exception.message}"
457
457
  {}
458
458
  end
459
459
 
@@ -464,13 +464,13 @@ module Carson
464
464
  end
465
465
 
466
466
  def review_evidence( pr:, repo_path: )
467
- rt = scoped_runtime( repo_path: repo_path )
468
- owner, repo = rt.send( :repository_coordinates )
467
+ scoped_runtime = scoped_runtime( repo_path: repo_path )
468
+ owner, repo = scoped_runtime.send( :repository_coordinates )
469
469
  pr_number = pr[ "number" ]
470
- details = rt.send( :pull_request_details, owner: owner, repo: repo, pr_number: pr_number )
470
+ details = scoped_runtime.send( :pull_request_details, owner: owner, repo: repo, pr_number: pr_number )
471
471
  pr_author = details.dig( :author, :login ).to_s
472
- threads = rt.send( :unresolved_thread_entries, details: details )
473
- top_level = rt.send( :actionable_top_level_items, details: details, pr_author: pr_author )
472
+ threads = scoped_runtime.send( :unresolved_thread_entries, details: details )
473
+ top_level = scoped_runtime.send( :actionable_top_level_items, details: details, pr_author: pr_author )
474
474
 
475
475
  findings = []
476
476
  threads.each do |entry|
@@ -483,14 +483,14 @@ module Carson
483
483
  end
484
484
 
485
485
  { review_findings: findings }
486
- rescue StandardError => e
487
- puts_line " review_evidence failed: #{e.message}"
486
+ rescue StandardError => exception
487
+ puts_line " review_evidence failed: #{exception.message}"
488
488
  {}
489
489
  end
490
490
 
491
491
  def scoped_runtime( repo_path: )
492
492
  return self if repo_path == self.repo_root
493
- Runtime.new( repo_root: repo_path, tool_root: tool_root, out: out, err: err )
493
+ Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error )
494
494
  end
495
495
 
496
496
  def prior_attempt( pr:, repo_path: )
@@ -64,9 +64,9 @@ module Carson
64
64
  return unless gh_available?
65
65
 
66
66
  main_root = main_worktree_root
67
- worktree_list.each do |wt|
68
- path = wt.fetch( :path )
69
- branch = wt.fetch( :branch, nil )
67
+ worktree_list.each do |worktree|
68
+ path = worktree.fetch( :path )
69
+ branch = worktree.fetch( :branch, nil )
70
70
  next if path == main_root
71
71
  next unless branch
72
72
  next if cwd_inside_worktree?( worktree_path: path )
@@ -121,26 +121,26 @@ module Carson
121
121
  return { name: repo_name, path: repo_path, status: "error", error: "path not found" }
122
122
  end
123
123
 
124
- buf = verbose? ? out : StringIO.new
125
- err_buf = verbose? ? err : StringIO.new
126
- rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? )
124
+ buffer = verbose? ? output : StringIO.new
125
+ error_buffer = verbose? ? error : StringIO.new
126
+ scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? )
127
127
 
128
- sync_status = rt.sync!
128
+ sync_status = scoped_runtime.sync!
129
129
  if sync_status == EXIT_OK
130
- rt.reap_dead_worktrees!
131
- prune_status = rt.prune!
130
+ scoped_runtime.reap_dead_worktrees!
131
+ prune_status = scoped_runtime.prune!
132
132
  end
133
133
 
134
134
  ok = sync_status == EXIT_OK && prune_status == EXIT_OK
135
135
  unless verbose? || silent
136
- summary = strip_badge( buf.string.lines.last.to_s.strip )
136
+ summary = strip_badge( buffer.string.lines.last.to_s.strip )
137
137
  puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
138
138
  end
139
139
 
140
140
  { name: repo_name, path: repo_path, status: ok ? "ok" : "error" }
141
- rescue StandardError => e
142
- puts_line "#{repo_name}: FAIL (#{e.message})" unless silent
143
- { name: repo_name, path: repo_path, status: "error", error: e.message }
141
+ rescue StandardError => exception
142
+ puts_line "#{repo_name}: FAIL (#{exception.message})" unless silent
143
+ { name: repo_name, path: repo_path, status: "error", error: exception.message }
144
144
  end
145
145
 
146
146
  # Strips the Carson badge prefix from a message to avoid double-badging.
@@ -156,7 +156,7 @@ module Carson
156
156
  return expanded if repos.include?( expanded )
157
157
 
158
158
  downcased = File.basename( target ).downcase
159
- repos.find { |r| File.basename( r ).downcase == downcased }
159
+ repos.find { |repo_path| File.basename( repo_path ).downcase == downcased }
160
160
  end
161
161
 
162
162
  # Unified output — JSON or human-readable.
@@ -164,7 +164,7 @@ module Carson
164
164
  result[ :exit_code ] = exit_code
165
165
 
166
166
  if json_output
167
- out.puts JSON.pretty_generate( result )
167
+ output.puts JSON.pretty_generate( result )
168
168
  else
169
169
  if results && ( succeeded || failed )
170
170
  total = ( succeeded || 0 ) + ( failed || 0 )
@@ -1,3 +1,4 @@
1
+ # Installs, validates, and reports on managed git hooks for governed repositories.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Local
@@ -30,11 +31,30 @@ module Carson
30
31
  end
31
32
  git_system!( "config", "core.hooksPath", hooks_dir )
32
33
  File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
34
+ install_command_guard!
33
35
  puts_verbose "configured_hooks_path: #{hooks_dir}"
34
36
  puts_line "Hooks installed (#{config.managed_hooks.count} hooks)."
35
37
  EXIT_OK
36
38
  end
37
39
 
40
+ # Installs the command guard hook to a stable (non-versioned) path.
41
+ # Claude Code's PreToolUse hook references this path — it must not change across upgrades.
42
+ def install_command_guard!
43
+ source = hook_template_path( hook_name: "command-guard" )
44
+ return unless File.file?( source )
45
+
46
+ target = command_guard_path
47
+ FileUtils.mkdir_p( File.dirname( target ) )
48
+ FileUtils.cp( source, target )
49
+ FileUtils.chmod( 0o755, target )
50
+ puts_verbose "command_guard: #{target}"
51
+ end
52
+
53
+ # Stable path for the command guard script — not versioned so external references survive upgrades.
54
+ def command_guard_path
55
+ File.expand_path( File.join( config.hooks_path, "command-guard" ) )
56
+ end
57
+
38
58
  # Canonical hook template location inside Carson repository.
39
59
  def hook_template_path( hook_name: )
40
60
  File.join( tool_root, "hooks", hook_name )
@@ -164,12 +164,12 @@ module Carson
164
164
  end
165
165
 
166
166
  begin
167
- buf = verbose? ? out : StringIO.new
168
- err_buf = verbose? ? err : StringIO.new
169
- rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? )
170
- status = rt.prune!
167
+ buffer = verbose? ? output : StringIO.new
168
+ error_buffer = verbose? ? error : StringIO.new
169
+ scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? )
170
+ status = scoped_runtime.prune!
171
171
  unless verbose?
172
- summary = buf.string.lines.last.to_s.strip
172
+ summary = buffer.string.lines.last.to_s.strip
173
173
  puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
174
174
  end
175
175
  if status == EXIT_ERROR
@@ -179,9 +179,9 @@ module Carson
179
179
  clear_batch_success( command: "prune", repo_path: repo_path )
180
180
  succeeded += 1
181
181
  end
182
- rescue StandardError => e
183
- puts_line "#{repo_name}: FAIL (#{e.message})"
184
- record_batch_skip( command: "prune", repo_path: repo_path, reason: e.message )
182
+ rescue StandardError => exception
183
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
184
+ record_batch_skip( command: "prune", repo_path: repo_path, reason: exception.message )
185
185
  failed += 1
186
186
  end
187
187
  end
@@ -294,7 +294,7 @@ module Carson
294
294
  def onboard_run_audit!
295
295
  audit_error = nil
296
296
  audit_status = with_captured_output { audit! }
297
- rescue StandardError => e
297
+ rescue StandardError => exception
298
298
  audit_error = e
299
299
  audit_status = EXIT_OK
300
300
  ensure
@@ -339,17 +339,17 @@ module Carson
339
339
  # Refreshes a single governed repository using a scoped Runtime.
340
340
  def refresh_single_repo( repo_path:, repo_name: )
341
341
  if verbose?
342
- rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: out, err: err, verbose: true )
342
+ scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: output, error: error, verbose: true )
343
343
  else
344
- rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: StringIO.new, err: StringIO.new )
344
+ scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: StringIO.new, error: StringIO.new )
345
345
  end
346
- status = rt.refresh!
346
+ status = scoped_runtime.refresh!
347
347
  label = refresh_status_label( status: status )
348
- sync_suffix = refresh_sync_suffix( result: rt.template_sync_result )
348
+ sync_suffix = refresh_sync_suffix( result: scoped_runtime.template_sync_result )
349
349
  puts_line "#{repo_name}: #{label}#{sync_suffix}"
350
350
  status
351
- rescue StandardError => e
352
- puts_line "#{repo_name}: FAIL (#{e.message})"
351
+ rescue StandardError => exception
352
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
353
353
  EXIT_ERROR
354
354
  end
355
355
 
@@ -376,8 +376,8 @@ module Carson
376
376
  git_system!( "config", "--unset", "core.hooksPath" )
377
377
  puts_verbose "hooks_path_unset: core.hooksPath"
378
378
  EXIT_OK
379
- rescue StandardError => e
380
- puts_line "ERROR: unable to update core.hooksPath (#{e.message})"
379
+ rescue StandardError => exception
380
+ puts_line "ERROR: unable to update core.hooksPath (#{exception.message})"
381
381
  EXIT_ERROR
382
382
  end
383
383
 
@@ -8,7 +8,7 @@ module Carson
8
8
  fingerprint_status = block_if_outsider_fingerprints!
9
9
  unless fingerprint_status.nil?
10
10
  if json_output
11
- out.puts JSON.pretty_generate( {
11
+ output.puts JSON.pretty_generate( {
12
12
  command: "prune", status: "block",
13
13
  error: "Carson-owned artefacts detected in host repository",
14
14
  recovery: "remove Carson-owned files (.carson.yml, bin/carson, .tools/carson) then retry",
@@ -51,7 +51,7 @@ module Carson
51
51
  result[ :exit_code ] = exit_code
52
52
 
53
53
  if json_output
54
- out.puts JSON.pretty_generate( result )
54
+ output.puts JSON.pretty_generate( result )
55
55
  else
56
56
  print_prune_human( counters: counters )
57
57
  end
@@ -116,7 +116,7 @@ module Carson
116
116
  end
117
117
 
118
118
  def prune_skip_stale_branch( type:, branch:, upstream: )
119
- reason = { protected: "protected branch", current: "current branch", cwd_worktree: "checked out in CWD worktree" }.fetch( type, type.to_s )
119
+ reason = { protected: "protected branch", current: "current branch", cwd_worktree: "checked output in CWD worktree" }.fetch( type, type.to_s )
120
120
  status = { protected: "skip_protected_branch", current: "skip_current_branch", cwd_worktree: "skip_cwd_worktree_branch" }.fetch( type, "skip_#{type}" )
121
121
  puts_verbose "#{status}: #{branch} (upstream=#{upstream})"
122
122
  { action: :skipped, branch: branch, upstream: upstream, type: "stale", reason: reason }
@@ -135,7 +135,7 @@ module Carson
135
135
  end
136
136
 
137
137
  def prune_safe_delete_success( branch:, upstream:, stdout_text: )
138
- out.print stdout_text if verbose? && !stdout_text.empty?
138
+ output.print stdout_text if verbose? && !stdout_text.empty?
139
139
  puts_verbose "deleted_local_branch: #{branch} (upstream=#{upstream})"
140
140
  { action: :deleted, branch: branch, upstream: upstream, type: "stale", reason: "upstream gone" }
141
141
  end
@@ -154,7 +154,7 @@ module Carson
154
154
  end
155
155
 
156
156
  def prune_force_delete_success( branch:, upstream:, merged_pr:, force_stdout: )
157
- out.print force_stdout if verbose? && !force_stdout.empty?
157
+ output.print force_stdout if verbose? && !force_stdout.empty?
158
158
  puts_verbose "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
159
159
  { action: :deleted, branch: branch, upstream: upstream, type: "stale", reason: "force deleted with PR evidence" }
160
160
  end
@@ -195,9 +195,9 @@ module Carson
195
195
  error_text.to_s.downcase.include?( "used by worktree" )
196
196
  end
197
197
 
198
- # Returns the worktree path for a branch, or nil if not checked out in any worktree.
198
+ # Returns the worktree path for a branch, or nil if not checked output in any worktree.
199
199
  def worktree_path_for_branch( branch: )
200
- entry = worktree_list.find { |wt| wt.fetch( :branch, nil ) == branch }
200
+ entry = worktree_list.find { |worktree| worktree.fetch( :branch, nil ) == branch }
201
201
  entry&.fetch( :path, nil )
202
202
  end
203
203
 
@@ -307,7 +307,7 @@ module Carson
307
307
  return { action: :skipped, branch: branch, upstream: upstream, type: "absorbed", reason: error_text }
308
308
  end
309
309
 
310
- out.print force_stdout if verbose? && !force_stdout.empty?
310
+ output.print force_stdout if verbose? && !force_stdout.empty?
311
311
 
312
312
  remote_branch = upstream.sub( "#{config.git_remote}/", "" )
313
313
  git_run( "push", config.git_remote, "--delete", remote_branch )
@@ -372,7 +372,7 @@ module Carson
372
372
 
373
373
  force_stdout, force_stderr, force_success = force_delete_local_branch( branch: branch )
374
374
  if force_success
375
- out.print force_stdout if verbose? && !force_stdout.empty?
375
+ output.print force_stdout if verbose? && !force_stdout.empty?
376
376
  puts_verbose "deleted_orphan_branch: #{branch} merged_pr=#{merged_pr.fetch( :url )}"
377
377
  return { action: :deleted, branch: branch, upstream: "", type: "orphan", reason: "merged PR evidence found" }
378
378
  end
@@ -486,10 +486,10 @@ module Carson
486
486
  return [ nil, "no merged PR evidence for branch tip #{branch_tip_sha} into #{config.main_branch}" ] if latest.nil?
487
487
 
488
488
  [ latest, nil ]
489
- rescue JSON::ParserError => e
490
- [ nil, "invalid gh JSON response (#{e.message})" ]
491
- rescue StandardError => e
492
- [ nil, e.message ]
489
+ rescue JSON::ParserError => exception
490
+ [ nil, "invalid gh JSON response (#{exception.message})" ]
491
+ rescue StandardError => exception
492
+ [ nil, exception.message ]
493
493
  end
494
494
  end
495
495
  end
@@ -66,8 +66,8 @@ module Carson
66
66
  end
67
67
 
68
68
  begin
69
- rt = build_scoped_runtime( repo_path: repo_path )
70
- status = rt.sync!
69
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
70
+ status = scoped_runtime.sync!
71
71
  if status == EXIT_OK
72
72
  puts_line "#{repo_name}: ok" unless verbose?
73
73
  clear_batch_success( command: "sync", repo_path: repo_path )
@@ -77,9 +77,9 @@ module Carson
77
77
  record_batch_skip( command: "sync", repo_path: repo_path, reason: "sync failed" )
78
78
  failed += 1
79
79
  end
80
- rescue StandardError => e
81
- puts_line "#{repo_name}: FAIL (#{e.message})"
82
- record_batch_skip( command: "sync", repo_path: repo_path, reason: e.message )
80
+ rescue StandardError => exception
81
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
82
+ record_batch_skip( command: "sync", repo_path: repo_path, reason: exception.message )
83
83
  failed += 1
84
84
  end
85
85
  end
@@ -106,7 +106,7 @@ module Carson
106
106
  result[ :exit_code ] = exit_code
107
107
 
108
108
  if json_output
109
- out.puts JSON.pretty_generate( result )
109
+ output.puts JSON.pretty_generate( result )
110
110
  else
111
111
  print_sync_human( result: result )
112
112
  end