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.
@@ -1,3 +1,4 @@
1
+ # Detects template drift, applies canonical files, and propagates changes via PR.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Local
@@ -9,7 +10,7 @@ module Carson
9
10
  ".github/.mega-linter.yml"
10
11
  ].freeze
11
12
 
12
- # Read-only template drift check; returns block when managed files are out of sync.
13
+ # Read-only template drift check; returns block when managed files are output of sync.
13
14
  def template_check!
14
15
  fingerprint_status = block_if_outsider_fingerprints!
15
16
  return fingerprint_status unless fingerprint_status.nil?
@@ -68,8 +69,8 @@ module Carson
68
69
  end
69
70
 
70
71
  begin
71
- rt = build_scoped_runtime( repo_path: repo_path )
72
- status = rt.template_check!
72
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
73
+ status = scoped_runtime.template_check!
73
74
  if status == EXIT_OK
74
75
  puts_line "#{repo_name}: in sync" unless verbose?
75
76
  clear_batch_success( command: "template_check", repo_path: repo_path )
@@ -78,9 +79,9 @@ module Carson
78
79
  puts_line "#{repo_name}: DRIFT" unless verbose?
79
80
  drifted += 1
80
81
  end
81
- rescue StandardError => e
82
- puts_line "#{repo_name}: FAIL (#{e.message})"
83
- record_batch_skip( command: "template_check", repo_path: repo_path, reason: e.message )
82
+ rescue StandardError => exception
83
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
84
+ record_batch_skip( command: "template_check", repo_path: repo_path, reason: exception.message )
84
85
  failed += 1
85
86
  end
86
87
  end
@@ -171,9 +172,9 @@ module Carson
171
172
  result = template_propagate_deliver!( worktree_dir: worktree_dir )
172
173
  template_propagate_report!( result: result )
173
174
  result
174
- rescue StandardError => e
175
- puts_verbose "template_propagate: error (#{e.message})"
176
- { status: :error, reason: e.message }
175
+ rescue StandardError => exception
176
+ puts_verbose "template_propagate: error (#{exception.message})"
177
+ { status: :error, reason: exception.message }
177
178
  ensure
178
179
  template_propagate_cleanup!( worktree_dir: worktree_dir ) if worktree_dir
179
180
  end
@@ -181,12 +182,12 @@ module Carson
181
182
 
182
183
  def template_propagate_create_worktree!
183
184
  worktree_dir = File.join( Dir.tmpdir, "carson-template-sync-#{Process.pid}-#{Time.now.to_i}" )
184
- wt_git = Adapters::Git.new( repo_root: worktree_dir )
185
+ worktree_git = Adapters::Git.new( repo_root: worktree_dir )
185
186
 
186
187
  git_system!( "fetch", config.git_remote, config.main_branch )
187
188
  git_system!( "worktree", "add", "--detach", worktree_dir, "#{config.git_remote}/#{config.main_branch}" )
188
- wt_git.run( "checkout", "-B", TEMPLATE_SYNC_BRANCH )
189
- wt_git.run( "config", "core.hooksPath", "/dev/null" )
189
+ worktree_git.run( "checkout", "-B", TEMPLATE_SYNC_BRANCH )
190
+ worktree_git.run( "config", "core.hooksPath", "/dev/null" )
190
191
  puts_verbose "template_propagate: worktree created at #{worktree_dir}"
191
192
  worktree_dir
192
193
  end
@@ -211,13 +212,13 @@ module Carson
211
212
  end
212
213
 
213
214
  def template_propagate_commit!( worktree_dir: )
214
- wt_git = Adapters::Git.new( repo_root: worktree_dir )
215
- wt_git.run( "add", "--all" )
215
+ worktree_git = Adapters::Git.new( repo_root: worktree_dir )
216
+ worktree_git.run( "add", "--all" )
216
217
 
217
- _, _, no_diff, = wt_git.run( "diff", "--cached", "--quiet" )
218
+ _, _, no_diff, = worktree_git.run( "diff", "--cached", "--quiet" )
218
219
  return false if no_diff
219
220
 
220
- wt_git.run( "commit", "-m", "chore: sync Carson #{Carson::VERSION} managed templates" )
221
+ worktree_git.run( "commit", "-m", "chore: sync Carson #{Carson::VERSION} managed templates" )
221
222
  puts_verbose "template_propagate: committed"
222
223
  true
223
224
  end
@@ -231,8 +232,8 @@ module Carson
231
232
  end
232
233
 
233
234
  def template_propagate_deliver_trunk!( worktree_dir: )
234
- wt_git = Adapters::Git.new( repo_root: worktree_dir )
235
- stdout_text, stderr_text, success, = wt_git.run( "push", config.git_remote, "HEAD:refs/heads/#{config.main_branch}" )
235
+ worktree_git = Adapters::Git.new( repo_root: worktree_dir )
236
+ stdout_text, stderr_text, success, = worktree_git.run( "push", config.git_remote, "HEAD:refs/heads/#{config.main_branch}" )
236
237
  unless success
237
238
  error_text = stderr_text.to_s.strip
238
239
  error_text = "push to #{config.main_branch} failed" if error_text.empty?
@@ -243,8 +244,8 @@ module Carson
243
244
  end
244
245
 
245
246
  def template_propagate_deliver_branch!( worktree_dir: )
246
- wt_git = Adapters::Git.new( repo_root: worktree_dir )
247
- stdout_text, stderr_text, success, = wt_git.run( "push", "--force-with-lease", config.git_remote, "#{TEMPLATE_SYNC_BRANCH}:#{TEMPLATE_SYNC_BRANCH}" )
247
+ worktree_git = Adapters::Git.new( repo_root: worktree_dir )
248
+ stdout_text, stderr_text, success, = worktree_git.run( "push", "--force-with-lease", config.git_remote, "#{TEMPLATE_SYNC_BRANCH}:#{TEMPLATE_SYNC_BRANCH}" )
248
249
  unless success
249
250
  error_text = stderr_text.to_s.strip
250
251
  error_text = "push #{TEMPLATE_SYNC_BRANCH} failed" if error_text.empty?
@@ -296,8 +297,8 @@ module Carson
296
297
  git_run( "worktree", "remove", "--force", worktree_dir ) unless safe_success
297
298
  git_run( "branch", "-D", TEMPLATE_SYNC_BRANCH )
298
299
  puts_verbose "template_propagate: worktree and local branch cleaned up"
299
- rescue StandardError => e
300
- puts_verbose "template_propagate: cleanup warning (#{e.message})"
300
+ rescue StandardError => exception
301
+ puts_verbose "template_propagate: cleanup warning (#{exception.message})"
301
302
  end
302
303
 
303
304
  def template_propagate_report!( result: )
@@ -381,14 +382,14 @@ module Carson
381
382
  def managed_dirty_paths
382
383
  template_paths = config.template_managed_files + SUPERSEDED
383
384
  linters_glob = Dir.glob( File.join( repo_root, ".github/linters/**/*" ) )
384
- .select { |p| File.file?( p ) }
385
- .map { |p| p.delete_prefix( "#{repo_root}/" ) }
385
+ .select { |path| File.file?( path ) }
386
+ .map { |path| path.delete_prefix( "#{repo_root}/" ) }
386
387
  candidates = ( template_paths + linters_glob ).uniq
387
388
  return [] if candidates.empty?
388
389
 
389
390
  stdout_text, = git_capture_soft( "status", "--porcelain", "--", *candidates )
390
391
  stdout_text.to_s.lines
391
- .map { |l| l[ 3.. ].strip }
392
+ .map { |line| line[ 3.. ].strip }
392
393
  .reject( &:empty? )
393
394
  end
394
395
  end
@@ -15,11 +15,11 @@ module Carson
15
15
  # Uses main_worktree_root so this works even when called from inside a worktree.
16
16
  def worktree_create!( name:, json_output: false )
17
17
  worktrees_dir = File.join( main_worktree_root, ".claude", "worktrees" )
18
- wt_path = File.join( worktrees_dir, name )
18
+ worktree_path = File.join( worktrees_dir, name )
19
19
 
20
- if Dir.exist?( wt_path )
20
+ if Dir.exist?( worktree_path )
21
21
  return worktree_finish(
22
- result: { command: "worktree create", status: "error", name: name, path: wt_path,
22
+ result: { command: "worktree create", status: "error", name: name, path: worktree_path,
23
23
  error: "worktree already exists: #{name}",
24
24
  recovery: "carson worktree remove #{name}, then retry" },
25
25
  exit_code: EXIT_ERROR, json_output: json_output
@@ -42,9 +42,9 @@ module Carson
42
42
 
43
43
  # Create the worktree with a new branch based on the main branch.
44
44
  FileUtils.mkdir_p( worktrees_dir )
45
- _, wt_stderr, wt_success, = git_run( "worktree", "add", wt_path, "-b", name, base )
46
- unless wt_success
47
- error_text = wt_stderr.to_s.strip
45
+ _, worktree_stderr, worktree_success, = git_run( "worktree", "add", worktree_path, "-b", name, base )
46
+ unless worktree_success
47
+ error_text = worktree_stderr.to_s.strip
48
48
  error_text = "unable to create worktree" if error_text.empty?
49
49
  return worktree_finish(
50
50
  result: { command: "worktree create", status: "error", name: name,
@@ -54,7 +54,7 @@ module Carson
54
54
  end
55
55
 
56
56
  worktree_finish(
57
- result: { command: "worktree create", status: "ok", name: name, path: wt_path, branch: name },
57
+ result: { command: "worktree create", status: "ok", name: name, path: worktree_path, branch: name },
58
58
  exit_code: EXIT_OK, json_output: json_output
59
59
  )
60
60
  end
@@ -66,7 +66,7 @@ module Carson
66
66
  fingerprint_status = block_if_outsider_fingerprints!
67
67
  unless fingerprint_status.nil?
68
68
  if json_output
69
- out.puts JSON.pretty_generate( {
69
+ output.puts JSON.pretty_generate( {
70
70
  command: "worktree remove", status: "block",
71
71
  error: "Carson-owned artefacts detected in host repository",
72
72
  recovery: "remove Carson-owned files (.carson.yml, bin/carson, .tools/carson) then retry",
@@ -202,9 +202,9 @@ module Carson
202
202
  end
203
203
  return if agent_prefixes.empty?
204
204
 
205
- worktrees.each do |wt|
206
- path = wt.fetch( :path )
207
- branch = wt.fetch( :branch, nil )
205
+ worktrees.each do |worktree|
206
+ path = worktree.fetch( :path )
207
+ branch = worktree.fetch( :branch, nil )
208
208
  next unless branch
209
209
  next unless agent_prefixes.any? { |prefix| path.start_with?( prefix ) }
210
210
  next if cwd_inside_worktree?( worktree_path: path )
@@ -270,7 +270,7 @@ module Carson
270
270
  result[ :exit_code ] = exit_code
271
271
 
272
272
  if json_output
273
- out.puts JSON.pretty_generate( result )
273
+ output.puts JSON.pretty_generate( result )
274
274
  else
275
275
  print_worktree_human( result: result )
276
276
  end
@@ -310,9 +310,9 @@ module Carson
310
310
  # Uses realpath on both sides to handle symlink differences (e.g. /tmp vs /private/tmp).
311
311
  def cwd_inside_worktree?( worktree_path: )
312
312
  cwd = realpath_safe( Dir.pwd )
313
- wt = realpath_safe( worktree_path )
314
- normalised_wt = File.join( wt, "" )
315
- cwd == wt || cwd.start_with?( normalised_wt )
313
+ worktree = realpath_safe( worktree_path )
314
+ normalised_wt = File.join( worktree, "" )
315
+ cwd == worktree || cwd.start_with?( normalised_wt )
316
316
  rescue StandardError
317
317
  false
318
318
  end
@@ -378,7 +378,7 @@ module Carson
378
378
  nil
379
379
  end
380
380
 
381
- # Returns the branch checked out in the worktree that contains the process CWD,
381
+ # Returns the branch checked output in the worktree that contains the process CWD,
382
382
  # or nil if CWD is not inside any worktree. Used by prune to proactively
383
383
  # protect the CWD worktree's branch from deletion.
384
384
  # Matches the longest (most specific) path because worktree directories
@@ -387,12 +387,12 @@ module Carson
387
387
  cwd = realpath_safe( Dir.pwd )
388
388
  best_branch = nil
389
389
  best_length = -1
390
- worktree_list.each do |wt|
391
- wt_path = wt.fetch( :path )
392
- normalised = File.join( wt_path, "" )
393
- if ( cwd == wt_path || cwd.start_with?( normalised ) ) && wt_path.length > best_length
394
- best_branch = wt.fetch( :branch, nil )
395
- best_length = wt_path.length
390
+ worktree_list.each do |worktree|
391
+ worktree_path = worktree.fetch( :path )
392
+ normalised = File.join( worktree_path, "" )
393
+ if ( cwd == worktree_path || cwd.start_with?( normalised ) ) && worktree_path.length > best_length
394
+ best_branch = worktree.fetch( :branch, nil )
395
+ best_length = worktree_path.length
396
396
  end
397
397
  end
398
398
  best_branch
@@ -426,7 +426,7 @@ module Carson
426
426
  existing = File.exist?( exclude_path ) ? File.read( exclude_path ) : ""
427
427
  return if existing.lines.any? { |line| line.strip == ".claude/" }
428
428
 
429
- File.open( exclude_path, "a" ) { |f| f.puts ".claude/" }
429
+ File.open( exclude_path, "a" ) { |file| file.puts ".claude/" }
430
430
  rescue StandardError
431
431
  # Best-effort — do not block worktree creation if exclude fails.
432
432
  end
@@ -451,14 +451,14 @@ module Carson
451
451
  # Compares using realpath to handle symlink differences.
452
452
  def worktree_registered?( path: )
453
453
  canonical = realpath_safe( path )
454
- worktree_list.any? { |wt| wt.fetch( :path ) == canonical }
454
+ worktree_list.any? { |worktree| worktree.fetch( :path ) == canonical }
455
455
  end
456
456
 
457
- # Returns the branch name checked out in a worktree, or nil.
457
+ # Returns the branch name checked output in a worktree, or nil.
458
458
  # Compares using realpath to handle symlink differences.
459
459
  def worktree_branch( path: )
460
460
  canonical = realpath_safe( path )
461
- entry = worktree_list.find { |wt| wt.fetch( :path ) == canonical }
461
+ entry = worktree_list.find { |worktree| worktree.fetch( :path ) == canonical }
462
462
  entry&.fetch( :branch, nil )
463
463
  end
464
464
 
@@ -1,3 +1,4 @@
1
+ # Aggregates local repository operation modules (sync, prune, hooks, worktree, template).
1
2
  require_relative "local/sync"
2
3
  require_relative "local/prune"
3
4
  require_relative "local/template"
@@ -9,7 +9,7 @@ module Carson
9
9
  repos = config.govern_repos
10
10
 
11
11
  if json_output
12
- out.puts JSON.pretty_generate( { command: "repos", repos: repos } )
12
+ output.puts JSON.pretty_generate( { command: "repos", repos: repos } )
13
13
  else
14
14
  if repos.empty?
15
15
  puts_line "No governed repositories."
@@ -176,8 +176,8 @@ module Carson
176
176
  )
177
177
  puts_verbose "review_gate_report_markdown: #{markdown_path}"
178
178
  puts_verbose "review_gate_report_json: #{json_path}"
179
- rescue StandardError => e
180
- puts_verbose "review_gate_report_write: SKIP (#{e.message})"
179
+ rescue StandardError => exception
180
+ puts_verbose "review_gate_report_write: SKIP (#{exception.message})"
181
181
  end
182
182
 
183
183
  # Human-readable review gate report for merge-readiness evidence.
@@ -208,8 +208,8 @@ module Carson
208
208
  )
209
209
  puts_verbose "review_sweep_report_markdown: #{markdown_path}"
210
210
  puts_verbose "review_sweep_report_json: #{json_path}"
211
- rescue StandardError => e
212
- puts_verbose "review_sweep_report_write: SKIP (#{e.message})"
211
+ rescue StandardError => exception
212
+ puts_verbose "review_sweep_report_write: SKIP (#{exception.message})"
213
213
  end
214
214
 
215
215
  # Human-readable scheduled sweep report.
@@ -1,3 +1,4 @@
1
+ # Implements the review gate (merge readiness) and sweep (late activity scan) workflows.
1
2
  require_relative "review/query_text"
2
3
  require_relative "review/data_access"
3
4
  require_relative "review/gate_support"
@@ -6,6 +7,7 @@ require_relative "review/utility"
6
7
 
7
8
  module Carson
8
9
  class Runtime
10
+ # PR review gate and sweep workflow.
9
11
  module Review
10
12
  include QueryText
11
13
  include DataAccess
@@ -123,11 +125,11 @@ module Carson
123
125
  end
124
126
  block_reasons.each { |reason| puts_line "BLOCK: #{reason}" }
125
127
  EXIT_BLOCK
126
- rescue JSON::ParserError => e
127
- puts_line "ERROR: invalid gh JSON response (#{e.message})."
128
+ rescue JSON::ParserError => exception
129
+ puts_line "ERROR: invalid gh JSON response (#{exception.message})."
128
130
  EXIT_ERROR
129
- rescue StandardError => e
130
- puts_line "ERROR: #{e.message}"
131
+ rescue StandardError => exception
132
+ puts_line "ERROR: #{exception.message}"
131
133
  EXIT_ERROR
132
134
  end
133
135
 
@@ -176,11 +178,11 @@ module Carson
176
178
  end
177
179
  puts_line "BLOCK: actionable late review activity detected."
178
180
  EXIT_BLOCK
179
- rescue JSON::ParserError => e
180
- puts_line "ERROR: invalid gh JSON response (#{e.message})."
181
+ rescue JSON::ParserError => exception
182
+ puts_line "ERROR: invalid gh JSON response (#{exception.message})."
181
183
  EXIT_ERROR
182
- rescue StandardError => e
183
- puts_line "ERROR: #{e.message}"
184
+ rescue StandardError => exception
185
+ puts_line "ERROR: #{exception.message}"
184
186
  EXIT_ERROR
185
187
  end
186
188
  end
@@ -1,8 +1,10 @@
1
+ # Handles first-time setup, onboard, offboard, refresh, and config persistence.
1
2
  require "set"
2
3
  require "uri"
3
4
 
4
5
  module Carson
5
6
  class Runtime
7
+ # First-time setup, onboard, offboard, and refresh operations.
6
8
  module Setup
7
9
  WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
8
10
 
@@ -173,8 +175,8 @@ module Carson
173
175
  options.each_with_index do |option, index|
174
176
  puts_line " #{index + 1}) #{option.fetch( :label )}"
175
177
  end
176
- out.print "#{BADGE} Choice [#{default + 1}]: "
177
- out.flush
178
+ output.print "#{BADGE} Choice [#{default + 1}]: "
179
+ output.flush
178
180
  raw = self.in.gets
179
181
  return options[ default ].fetch( :value ) if raw.nil?
180
182
 
@@ -190,8 +192,8 @@ module Carson
190
192
  end
191
193
 
192
194
  def prompt_custom_value( label: )
193
- out.print "#{BADGE} #{label}: "
194
- out.flush
195
+ output.print "#{BADGE} #{label}: "
196
+ output.flush
195
197
  raw = self.in.gets
196
198
  return nil if raw.nil?
197
199
 
@@ -383,8 +385,8 @@ module Carson
383
385
  # Reusable Y/n prompt following existing prompt_choice conventions.
384
386
  def prompt_yes_no( default: true )
385
387
  hint = default ? "Y/n" : "y/N"
386
- out.print "#{BADGE} [#{hint}]: "
387
- out.flush
388
+ output.print "#{BADGE} [#{hint}]: "
389
+ output.flush
388
390
  raw = self.in.gets
389
391
  return default if raw.nil?
390
392
 
@@ -9,7 +9,7 @@ module Carson
9
9
  data = gather_status
10
10
 
11
11
  if json_output
12
- out.puts JSON.pretty_generate( data )
12
+ output.puts JSON.pretty_generate( data )
13
13
  else
14
14
  print_status( data: data )
15
15
  end
@@ -35,14 +35,14 @@ module Carson
35
35
  next
36
36
  end
37
37
  begin
38
- rt = build_scoped_runtime( repo_path: repo_path )
39
- data = rt.send( :gather_status )
38
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
39
+ data = scoped_runtime.send( :gather_status )
40
40
  results << { name: repo_name, status: "ok" }.merge( data )
41
- rescue StandardError => e
42
- results << { name: repo_name, status: "error", error: e.message }
41
+ rescue StandardError => exception
42
+ results << { name: repo_name, status: "error", error: exception.message }
43
43
  end
44
44
  end
45
- out.puts JSON.pretty_generate( { command: "status", repos: results } )
45
+ output.puts JSON.pretty_generate( { command: "status", repos: results } )
46
46
  return EXIT_OK
47
47
  end
48
48
 
@@ -58,8 +58,8 @@ module Carson
58
58
  end
59
59
 
60
60
  begin
61
- rt = build_scoped_runtime( repo_path: repo_path )
62
- data = rt.send( :gather_status )
61
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
62
+ data = scoped_runtime.send( :gather_status )
63
63
  branch = data.fetch( :branch )
64
64
  dirty = branch.fetch( :dirty ) ? " (dirty)" : ""
65
65
  worktrees = data.fetch( :worktrees )
@@ -72,9 +72,9 @@ module Carson
72
72
 
73
73
  # Show pending operations for this repo.
74
74
  repo_pending = status_pending_for_repo( all_pending: all_pending, repo_path: repo_path )
75
- repo_pending.each { |desc| puts_line " pending: #{desc}" }
76
- rescue StandardError => e
77
- puts_line "#{repo_name}: FAIL (#{e.message})"
75
+ repo_pending.each { |description| puts_line " pending: #{description}" }
76
+ rescue StandardError => exception
77
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
78
78
  end
79
79
  end
80
80
 
@@ -159,14 +159,14 @@ module Carson
159
159
  def gather_worktree_info
160
160
  entries = worktree_list
161
161
 
162
- # Filter out the main worktree (the repository root itself).
162
+ # Filter output the main worktree (the repository root itself).
163
163
  # Use realpath for comparison — git returns canonical paths that may differ from repo_root.
164
164
  canonical_root = realpath_safe( repo_root )
165
- entries.reject { it.fetch( :path ) == canonical_root }.map do |wt|
165
+ entries.reject { it.fetch( :path ) == canonical_root }.map do |worktree|
166
166
  {
167
- path: wt.fetch( :path ),
168
- name: File.basename( wt.fetch( :path ) ),
169
- branch: wt.fetch( :branch, nil )
167
+ path: worktree.fetch( :path ),
168
+ name: File.basename( worktree.fetch( :path ) ),
169
+ branch: worktree.fetch( :branch, nil )
170
170
  }
171
171
  end
172
172
  end
@@ -222,7 +222,7 @@ module Carson
222
222
  stdout, _, success, = git_run( "branch", "-vv" )
223
223
  return { count: 0 } unless success
224
224
 
225
- gone_branches = stdout.lines.select { |l| l.include?( ": gone]" ) }
225
+ gone_branches = stdout.lines.select { |line| line.include?( ": gone]" ) }
226
226
  { count: gone_branches.size }
227
227
  end
228
228
 
@@ -252,9 +252,9 @@ module Carson
252
252
  if worktrees.any?
253
253
  puts_line ""
254
254
  puts_line "Worktrees:"
255
- worktrees.each do |wt|
256
- branch_label = wt.fetch( :branch ) || "(detached)"
257
- puts_line " #{wt.fetch( :name )} #{branch_label}"
255
+ worktrees.each do |worktree|
256
+ branch_label = worktree.fetch( :branch ) || "(detached)"
257
+ puts_line " #{worktree.fetch( :name )} #{branch_label}"
258
258
  end
259
259
  end
260
260
 
@@ -23,11 +23,11 @@ module Carson
23
23
  DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
24
24
 
25
25
  # Runtime wiring for repository context, tool paths, and output streams.
26
- def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin, verbose: false )
26
+ def initialize( repo_root:, tool_root:, output:, error:, in_stream: $stdin, verbose: false )
27
27
  @repo_root = repo_root
28
28
  @tool_root = tool_root
29
- @out = out
30
- @err = err
29
+ @output = output
30
+ @error = error
31
31
  @in = in_stream
32
32
  @verbose = verbose
33
33
  @config = Config.load( repo_root: repo_root )
@@ -40,7 +40,7 @@ module Carson
40
40
 
41
41
  private
42
42
 
43
- attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
43
+ attr_reader :repo_root, :tool_root, :output, :error, :in, :config, :git_adapter, :github_adapter
44
44
 
45
45
  # Returns true when full diagnostic output is enabled via --verbose.
46
46
  def verbose?
@@ -55,12 +55,12 @@ module Carson
55
55
  # Runs a block with all output captured (suppressed from the user).
56
56
  # Returns the block's return value; output is silently discarded.
57
57
  def with_captured_output
58
- saved_out, saved_err = @out, @err
59
- @out = StringIO.new
60
- @err = StringIO.new
58
+ saved_output, saved_error = @output, @error
59
+ @output = StringIO.new
60
+ @error = StringIO.new
61
61
  yield
62
62
  ensure
63
- @out, @err = saved_out, saved_err
63
+ @output, @error = saved_output, saved_error
64
64
  end
65
65
 
66
66
  # Returns true when the repository has at least one commit (HEAD exists).
@@ -95,9 +95,9 @@ module Carson
95
95
  # Prefixes non-empty lines with the Carson badge (⧓).
96
96
  def puts_line( message )
97
97
  if message.to_s.strip.empty?
98
- out.puts ""
98
+ output.puts ""
99
99
  else
100
- out.puts "#{BADGE} #{message}"
100
+ output.puts "#{BADGE} #{message}"
101
101
  end
102
102
  end
103
103
 
@@ -160,6 +160,20 @@ module Carson
160
160
  text.empty? ? default : text
161
161
  end
162
162
 
163
+ # Temporarily sets an environment variable for the duration of the block.
164
+ # Restores the previous value (or deletes the key) when the block completes.
165
+ def with_env_var( key, value )
166
+ previous = ENV.key?( key ) ? ENV.fetch( key ) : nil
167
+ ENV[ key ] = value
168
+ yield
169
+ ensure
170
+ if previous.nil?
171
+ ENV.delete( key )
172
+ else
173
+ ENV[ key ] = previous
174
+ end
175
+ end
176
+
163
177
  # Chooses best available error text from gh stderr/stdout.
164
178
  def gh_error_text( stdout_text:, stderr_text:, fallback: )
165
179
  combined = [ stderr_text.to_s.strip, stdout_text.to_s.strip ].reject( &:empty? ).join( " | " )
@@ -182,8 +196,8 @@ module Carson
182
196
  # Runs git command, streams outputs, and raises on non-zero exit.
183
197
  def git_system!( *args )
184
198
  stdout_text, stderr_text, success, = git_run( *args )
185
- out.print stdout_text unless stdout_text.empty?
186
- err.print stderr_text unless stderr_text.empty?
199
+ output.print stdout_text unless stdout_text.empty?
200
+ error.print stderr_text unless stderr_text.empty?
187
201
  raise "git #{args.join( ' ' )} failed" unless success
188
202
  end
189
203
 
@@ -191,7 +205,7 @@ module Carson
191
205
  def git_capture!( *args )
192
206
  stdout_text, stderr_text, success, = git_run( *args )
193
207
  unless success
194
- err.print stderr_text unless stderr_text.empty?
208
+ error.print stderr_text unless stderr_text.empty?
195
209
  raise "git #{args.join( ' ' )} failed"
196
210
  end
197
211
  stdout_text
@@ -234,9 +248,9 @@ module Carson
234
248
  def save_batch_pending( data )
235
249
  path = batch_pending_path
236
250
  FileUtils.mkdir_p( File.dirname( path ) )
237
- tmp = "#{path}.tmp"
238
- File.write( tmp, JSON.pretty_generate( data ) )
239
- File.rename( tmp, path )
251
+ temporary_path = "#{path}.tmp"
252
+ File.write( temporary_path, JSON.pretty_generate( data ) )
253
+ File.rename( temporary_path, path )
240
254
  end
241
255
 
242
256
  # Adds or updates an entry in the pending log, incrementing attempts.
@@ -293,10 +307,10 @@ module Carson
293
307
  reasons = []
294
308
 
295
309
  # Active worktrees beyond the main working tree.
296
- rt = build_scoped_runtime( repo_path: repo_path )
297
- worktrees = rt.send( :worktree_list )
298
- main_root = rt.send( :realpath_safe, repo_path )
299
- active = worktrees.reject { |wt| wt.fetch( :path ) == main_root }
310
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
311
+ worktrees = scoped_runtime.send( :worktree_list )
312
+ main_root = scoped_runtime.send( :realpath_safe, repo_path )
313
+ active = worktrees.reject { |worktree| worktree.fetch( :path ) == main_root }
300
314
  if active.any?
301
315
  reasons << "#{active.count} active worktree#{active.count == 1 ? '' : 's'}"
302
316
  end
@@ -308,15 +322,15 @@ module Carson
308
322
  end
309
323
 
310
324
  { safe: reasons.empty?, reasons: reasons }
311
- rescue StandardError => e
312
- { safe: false, reasons: [ e.message ] }
325
+ rescue StandardError => exception
326
+ { safe: false, reasons: [ exception.message ] }
313
327
  end
314
328
 
315
329
  # Creates a scoped Runtime for a governed repo with captured output.
316
330
  def build_scoped_runtime( repo_path: )
317
- buf = verbose? ? out : StringIO.new
318
- err_buf = verbose? ? err : StringIO.new
319
- Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? )
331
+ buffer = verbose? ? output : StringIO.new
332
+ error_buffer = verbose? ? error : StringIO.new
333
+ Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? )
320
334
  end
321
335
  end
322
336
  end
@@ -1,3 +1,4 @@
1
+ # Reads the VERSION file and exposes it as Carson::VERSION.
1
2
  module Carson
2
3
  version_path = File.expand_path( "../../VERSION", __dir__ )
3
4
  VERSION = File.file?( version_path ) ? File.read( version_path ).strip : "0.0.0"
data/lib/carson.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # Loads all Carson modules and defines the top-level namespace.
1
2
  require_relative "carson/version"
2
3
 
3
4
  module Carson
@@ -2,12 +2,14 @@
2
2
 
3
3
  This repository is governed by [Carson](https://github.com/wanghailei/carson), an autonomous governance runtime. Carson lives on the maintainer's workstation, not inside this repository.
4
4
 
5
- ## What Carson Does Not Do
6
-
7
- Carson has no `commit`, `push`, or `pr` commands. Use `git` and `gh` for those. Carson audits and governs; you execute.
8
-
9
5
  ## Commands
10
6
 
7
+ **Delivery:**
8
+ ```bash
9
+ carson deliver # push branch, create PR
10
+ carson deliver --merge # push, create PR, merge if CI green and review clear
11
+ ```
12
+
11
13
  **Before committing:**
12
14
  ```bash
13
15
  carson audit # full governance check — run before every commit
@@ -24,6 +26,7 @@ carson review gate # block until actionable review findings are resolved
24
26
  ```bash
25
27
  carson sync # fast-forward local main from remote
26
28
  carson prune # remove stale branches (safer than git branch -d on squash repos)
29
+ carson housekeep # sync + prune + sweep stale worktrees
27
30
  ```
28
31
 
29
32
  ## Exit Codes