carson 3.21.1 → 3.22.1

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/cli.rb CHANGED
@@ -4,6 +4,8 @@ require "optparse"
4
4
  module Carson
5
5
  class CLI
6
6
  def self.start( arguments:, repo_root:, tool_root:, output:, error: )
7
+ ensure_global_artefacts!( tool_root: tool_root )
8
+
7
9
  parsed = parse_args( arguments: arguments, output: output, error: error )
8
10
  command = parsed.fetch( :command )
9
11
  return Runtime::EXIT_OK if command == :help
@@ -22,7 +24,7 @@ module Carson
22
24
  target_repo_root = parsed.fetch( :repo_root, nil )
23
25
  target_repo_root = repo_root if target_repo_root.to_s.strip.empty?
24
26
  unless Dir.exist?( target_repo_root )
25
- error.puts "#{BADGE} ERROR: repository path does not exist: #{target_repo_root}"
27
+ error.puts "#{BADGE} Repository path not found: #{target_repo_root}"
26
28
  return Runtime::EXIT_ERROR
27
29
  end
28
30
 
@@ -30,10 +32,10 @@ module Carson
30
32
  runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, output: output, error: error, verbose: verbose )
31
33
  dispatch( parsed: parsed, runtime: runtime )
32
34
  rescue ConfigError => exception
33
- error.puts "#{BADGE} CONFIG ERROR: #{exception.message}"
35
+ error.puts "#{BADGE} Configuration problem: #{exception.message}"
34
36
  Runtime::EXIT_ERROR
35
37
  rescue StandardError => exception
36
- error.puts "#{BADGE} ERROR: #{exception.message}"
38
+ error.puts "#{BADGE} #{exception.message}"
37
39
  Runtime::EXIT_ERROR
38
40
  end
39
41
 
@@ -147,7 +149,7 @@ module Carson
147
149
  parser.on( "--main-branch NAME", "Main branch name" ) { |value| options[ "git.main_branch" ] = value }
148
150
  parser.on( "--workflow STYLE", "Workflow style (branch or trunk)" ) { |value| options[ "workflow.style" ] = value }
149
151
  parser.on( "--merge METHOD", "Merge method (squash, rebase, or merge)" ) { |value| options[ "govern.merge.method" ] = value }
150
- parser.on( "--canonical PATH", "Canonical template directory path" ) { |value| options[ "template.canonical" ] = value }
152
+ parser.on( "--canonical PATH", "Canonical lint policy directory path" ) { |value| options[ "lint.canonical" ] = value }
151
153
  parser.separator ""
152
154
  parser.separator "Examples:"
153
155
  parser.separator " carson setup Auto-detect and write config"
@@ -163,6 +165,7 @@ module Carson
163
165
  { command: "setup", cli_choices: options }
164
166
  rescue OptionParser::ParseError => exception
165
167
  error.puts "#{BADGE} #{exception.message}"
168
+ error.puts setup_parser
166
169
  { command: :invalid }
167
170
  end
168
171
 
@@ -193,6 +196,7 @@ module Carson
193
196
  }
194
197
  rescue OptionParser::ParseError => exception
195
198
  error.puts "#{BADGE} #{exception.message}"
199
+ error.puts onboard_parser
196
200
  { command: :invalid }
197
201
  end
198
202
 
@@ -220,6 +224,7 @@ module Carson
220
224
  }
221
225
  rescue OptionParser::ParseError => exception
222
226
  error.puts "#{BADGE} #{exception.message}"
227
+ error.puts offboard_parser
223
228
  { command: :invalid }
224
229
  end
225
230
 
@@ -263,6 +268,7 @@ module Carson
263
268
  }
264
269
  rescue OptionParser::ParseError => exception
265
270
  error.puts "#{BADGE} #{exception.message}"
271
+ error.puts refresh_parser
266
272
  { command: :invalid }
267
273
  end
268
274
 
@@ -290,6 +296,7 @@ module Carson
290
296
  { command: "prune", json: options[ :json ] }
291
297
  rescue OptionParser::ParseError => exception
292
298
  error.puts "#{BADGE} #{exception.message}"
299
+ error.puts prune_parser
293
300
  { command: :invalid }
294
301
  end
295
302
 
@@ -346,6 +353,7 @@ module Carson
346
353
  end
347
354
  rescue OptionParser::ParseError => exception
348
355
  error.puts "#{BADGE} #{exception.message}"
356
+ error.puts worktree_parser
349
357
  { command: :invalid }
350
358
  end
351
359
 
@@ -376,6 +384,7 @@ module Carson
376
384
  { command: "review:#{action}" }
377
385
  rescue OptionParser::ParseError => exception
378
386
  error.puts "#{BADGE} #{exception.message}"
387
+ error.puts review_parser
379
388
  { command: :invalid }
380
389
  end
381
390
 
@@ -434,6 +443,7 @@ module Carson
434
443
  { command: "template:apply", push_prep: options.fetch( :push_prep ) }
435
444
  rescue OptionParser::ParseError => exception
436
445
  error.puts "#{BADGE} #{exception.message}"
446
+ error.puts( apply_parser || template_parser )
437
447
  { command: :invalid }
438
448
  end
439
449
 
@@ -466,6 +476,7 @@ module Carson
466
476
  { command: "audit", json: options[ :json ] }
467
477
  rescue OptionParser::ParseError => exception
468
478
  error.puts "#{BADGE} #{exception.message}"
479
+ error.puts audit_parser
469
480
  { command: :invalid }
470
481
  end
471
482
 
@@ -497,6 +508,7 @@ module Carson
497
508
  { command: "sync", json: options[ :json ] }
498
509
  rescue OptionParser::ParseError => exception
499
510
  error.puts "#{BADGE} #{exception.message}"
511
+ error.puts sync_parser
500
512
  { command: :invalid }
501
513
  end
502
514
 
@@ -528,6 +540,7 @@ module Carson
528
540
  { command: "status", json: options[ :json ] }
529
541
  rescue OptionParser::ParseError => exception
530
542
  error.puts "#{BADGE} #{exception.message}"
543
+ error.puts status_parser
531
544
  { command: :invalid }
532
545
  end
533
546
 
@@ -566,6 +579,7 @@ module Carson
566
579
  }
567
580
  rescue OptionParser::ParseError => exception
568
581
  error.puts "#{BADGE} #{exception.message}"
582
+ error.puts deliver_parser
569
583
  { command: :invalid }
570
584
  end
571
585
 
@@ -594,27 +608,30 @@ module Carson
594
608
  { command: "repos", json: options[ :json ] }
595
609
  rescue OptionParser::ParseError => exception
596
610
  error.puts "#{BADGE} #{exception.message}"
611
+ error.puts repos_parser
597
612
  { command: :invalid }
598
613
  end
599
614
 
600
615
  # --- housekeep ---
601
616
 
602
617
  def self.parse_housekeep_command( arguments:, error: )
603
- options = { all: false, json: false }
618
+ options = { all: false, json: false, dry_run: false }
604
619
  housekeep_parser = OptionParser.new do |parser|
605
- parser.banner = "Usage: carson housekeep [REPO] [--all] [--json]"
620
+ parser.banner = "Usage: carson housekeep [REPO] [--all] [--dry-run] [--json]"
606
621
  parser.separator ""
607
622
  parser.separator "Run housekeeping: sync main, reap dead worktrees, and prune stale branches."
608
623
  parser.separator "Defaults to the current repository."
609
624
  parser.separator ""
610
625
  parser.separator "Options:"
611
626
  parser.on( "--all", "Housekeep all governed repositories" ) { options[ :all ] = true }
627
+ parser.on( "--dry-run", "Show what would be reaped/deleted without making changes" ) { options[ :dry_run ] = true }
612
628
  parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
613
629
  parser.separator ""
614
630
  parser.separator "Examples:"
615
- parser.separator " carson housekeep Housekeep the current repository"
616
- parser.separator " carson housekeep nexus Housekeep a named governed repo"
617
- parser.separator " carson housekeep --all Housekeep all governed repos"
631
+ parser.separator " carson housekeep Housekeep the current repository"
632
+ parser.separator " carson housekeep --dry-run Preview what housekeep would do"
633
+ parser.separator " carson housekeep nexus Housekeep a named governed repo"
634
+ parser.separator " carson housekeep --all Housekeep all governed repos"
618
635
  end
619
636
  housekeep_parser.parse!( arguments )
620
637
 
@@ -623,7 +640,7 @@ module Carson
623
640
  return { command: :invalid }
624
641
  end
625
642
 
626
- return { command: "housekeep:all", json: options[ :json ] } if options[ :all ]
643
+ return { command: "housekeep:all", json: options[ :json ], dry_run: options[ :dry_run ] } if options[ :all ]
627
644
 
628
645
  if arguments.length > 1
629
646
  error.puts "#{BADGE} Too many arguments for housekeep. Use: carson housekeep [repo]"
@@ -631,11 +648,12 @@ module Carson
631
648
  end
632
649
 
633
650
  target = arguments.shift
634
- return { command: "housekeep:target", target: target, json: options[ :json ] } if target
651
+ return { command: "housekeep:target", target: target, json: options[ :json ], dry_run: options[ :dry_run ] } if target
635
652
 
636
- { command: "housekeep", json: options[ :json ] }
653
+ { command: "housekeep", json: options[ :json ], dry_run: options[ :dry_run ] }
637
654
  rescue OptionParser::ParseError => exception
638
655
  error.puts "#{BADGE} #{exception.message}"
656
+ error.puts housekeep_parser
639
657
  { command: :invalid }
640
658
  end
641
659
 
@@ -658,7 +676,7 @@ module Carson
658
676
  parser.on( "--dry-run", "Run all checks but do not merge or dispatch" ) { options[ :dry_run ] = true }
659
677
  parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
660
678
  parser.on( "--loop SECONDS", Integer, "Run continuously, sleeping SECONDS between cycles" ) do |seconds|
661
- error.puts( "#{BADGE} Error: --loop must be a positive integer" ) || ( return { command: :invalid } ) if seconds < 1
679
+ error.puts( "#{BADGE} --loop expects a positive integer" ) || ( return { command: :invalid } ) if seconds < 1
662
680
  options[ :loop_seconds ] = seconds
663
681
  end
664
682
  parser.separator ""
@@ -685,6 +703,27 @@ module Carson
685
703
  { command: :invalid }
686
704
  end
687
705
 
706
+ # --- global artefacts ---
707
+
708
+ # Ensures global (non-repo) artefacts are installed at CLI startup.
709
+ # The command-guard lives at a stable path (~/.carson/hooks/command-guard)
710
+ # referenced by Claude Code's PreToolUse hook. It must exist regardless of
711
+ # whether `carson refresh` has been run in any governed repo.
712
+ def self.ensure_global_artefacts!( tool_root: )
713
+ source = File.join( tool_root, "hooks", "command-guard" )
714
+ return unless File.file?( source )
715
+
716
+ hooks_base = File.expand_path( "~/.carson/hooks" )
717
+ target = File.join( hooks_base, "command-guard" )
718
+ return if File.file?( target ) && FileUtils.identical?( source, target )
719
+
720
+ FileUtils.mkdir_p( hooks_base )
721
+ FileUtils.cp( source, target )
722
+ FileUtils.chmod( 0o755, target )
723
+ rescue StandardError
724
+ # Best-effort — do not block any command if this fails.
725
+ end
726
+
688
727
  # --- dispatch ---
689
728
 
690
729
  def self.dispatch( parsed:, runtime: )
@@ -734,11 +773,11 @@ module Carson
734
773
  when "repos"
735
774
  runtime.repos!( json_output: parsed.fetch( :json, false ) )
736
775
  when "housekeep"
737
- runtime.housekeep!( json_output: parsed.fetch( :json, false ) )
776
+ runtime.housekeep!( json_output: parsed.fetch( :json, false ), dry_run: parsed.fetch( :dry_run, false ) )
738
777
  when "housekeep:target"
739
- runtime.housekeep_target!( target: parsed.fetch( :target ), json_output: parsed.fetch( :json, false ) )
778
+ runtime.housekeep_target!( target: parsed.fetch( :target ), json_output: parsed.fetch( :json, false ), dry_run: parsed.fetch( :dry_run, false ) )
740
779
  when "housekeep:all"
741
- runtime.housekeep_all!( json_output: parsed.fetch( :json, false ) )
780
+ runtime.housekeep_all!( json_output: parsed.fetch( :json, false ), dry_run: parsed.fetch( :dry_run, false ) )
742
781
  when "govern"
743
782
  runtime.govern!(
744
783
  dry_run: parsed.fetch( :dry_run, false ),
data/lib/carson/config.rb CHANGED
@@ -7,9 +7,24 @@ module Carson
7
7
 
8
8
  # Config is built-in only for outsider mode; host repositories do not carry Carson config files.
9
9
  class Config
10
+ CANONICAL_GITHUB_DIRECTORIES = %w[actions workflows ISSUE_TEMPLATE DISCUSSION_TEMPLATE linters].freeze
11
+ CANONICAL_GITHUB_ROOT_FILES = [
12
+ ".mega-linter.yml",
13
+ "AGENTS.md",
14
+ "CLAUDE.md",
15
+ "CODEOWNERS",
16
+ "FUNDING.yml",
17
+ "carson.md",
18
+ "copilot-instructions.md",
19
+ "dependabot.yml",
20
+ "funding.yml",
21
+ "labeler.yml",
22
+ "pull_request_template.md"
23
+ ].freeze
24
+
10
25
  attr_accessor :git_remote
11
26
  attr_reader :main_branch, :protected_branches, :hooks_path, :managed_hooks,
12
- :template_managed_files, :template_canonical,
27
+ :template_managed_files, :lint_canonical, :template_canonical,
13
28
  :review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
14
29
  :review_sweep_states, :review_disposition, :review_risk_keywords,
15
30
  :review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
@@ -38,7 +53,10 @@ module Carson
38
53
  "managed" => [ "pre-commit", "prepare-commit-msg", "pre-merge-commit", "pre-push" ]
39
54
  },
40
55
  "template" => {
41
- "managed_files" => [ ".github/carson.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md" ],
56
+ "managed_files" => [],
57
+ "canonical" => nil
58
+ },
59
+ "lint" => {
42
60
  "canonical" => nil
43
61
  },
44
62
  "workflow" => {
@@ -66,7 +84,9 @@ module Carson
66
84
  "govern" => {
67
85
  "repos" => [],
68
86
  "auto_merge" => true,
69
- "merge_method" => "squash",
87
+ "merge" => {
88
+ "method" => "squash"
89
+ },
70
90
  "agent" => {
71
91
  "provider" => "auto",
72
92
  "codex" => {},
@@ -154,7 +174,10 @@ module Carson
154
174
  govern_auto_merge = ENV.fetch( "CARSON_GOVERN_AUTO_MERGE", "" ).to_s.strip
155
175
  govern[ "auto_merge" ] = ( govern_auto_merge == "true" ) unless govern_auto_merge.empty?
156
176
  govern_method = ENV.fetch( "CARSON_GOVERN_MERGE_METHOD", "" ).to_s.strip
157
- govern[ "merge_method" ] = govern_method unless govern_method.empty?
177
+ unless govern_method.empty?
178
+ govern[ "merge" ] ||= {}
179
+ govern[ "merge" ][ "method" ] = govern_method
180
+ end
158
181
  agent = fetch_hash_section( data: govern, key: "agent" )
159
182
  govern_provider = ENV.fetch( "CARSON_GOVERN_AGENT_PROVIDER", "" ).to_s.strip
160
183
  agent[ "provider" ] = govern_provider unless govern_provider.empty?
@@ -189,8 +212,11 @@ module Carson
189
212
  @hooks_path = fetch_string( hash: fetch_hash( hash: data, key: "hooks" ), key: "path" )
190
213
  @managed_hooks = fetch_string_array( hash: fetch_hash( hash: data, key: "hooks" ), key: "managed" )
191
214
 
192
- @template_managed_files = fetch_string_array( hash: fetch_hash( hash: data, key: "template" ), key: "managed_files" )
193
- @template_canonical = fetch_optional_path( hash: fetch_hash( hash: data, key: "template" ), key: "canonical" )
215
+ template_hash = fetch_hash( hash: data, key: "template" )
216
+ @template_managed_files = fetch_optional_string_array( hash: template_hash, key: "managed_files" )
217
+ @lint_canonical = fetch_optional_path( hash: fetch_hash( hash: data, key: "lint" ), key: "canonical" )
218
+ @lint_canonical ||= fetch_optional_path( hash: template_hash, key: "canonical" )
219
+ @template_canonical = @lint_canonical
194
220
  resolve_canonical_files!
195
221
 
196
222
  workflow_hash = fetch_hash( hash: data, key: "workflow" )
@@ -215,7 +241,8 @@ module Carson
215
241
  govern_hash = fetch_hash( hash: data, key: "govern" )
216
242
  @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
217
243
  @govern_auto_merge = fetch_optional_boolean( hash: govern_hash, key: "auto_merge", default: true, key_path: "govern.auto_merge" )
218
- @govern_merge_method = fetch_string( hash: govern_hash, key: "merge_method" ).downcase
244
+ govern_merge_hash = fetch_hash( hash: govern_hash, key: "merge" )
245
+ @govern_merge_method = fetch_string( hash: govern_merge_hash, key: "method" ).downcase
219
246
  govern_agent_hash = fetch_hash( hash: govern_hash, key: "agent" )
220
247
  @govern_agent_provider = fetch_string( hash: govern_agent_hash, key: "provider" ).downcase
221
248
  dispatch_path = govern_hash.fetch( "dispatch_state_path" ).to_s
@@ -317,18 +344,32 @@ module Carson
317
344
  safe_expand_path( text )
318
345
  end
319
346
 
320
- # Discovers files in the canonical directory and appends them to managed_files.
321
- # Canonical files mirror the .github/ structure and are synced alongside Carson's own governance files.
347
+ # Discovers files in the canonical lint-policy directory and appends them to managed_files.
348
+ # Explicit GitHub paths stay under .github/; flat policy files default to .github/linters/.
322
349
  def resolve_canonical_files!
323
- return if @template_canonical.nil? || @template_canonical.empty?
324
- return unless Dir.exist?( @template_canonical )
350
+ return if @lint_canonical.nil? || @lint_canonical.empty?
351
+ return unless Dir.exist?( @lint_canonical )
325
352
 
326
- Dir.glob( File.join( @template_canonical, "**", "*" ) ).sort.each do |absolute_path|
353
+ Dir.glob( File.join( @lint_canonical, "**", "*" ), File::FNM_DOTMATCH ).sort.each do |absolute_path|
354
+ basename = File.basename( absolute_path )
355
+ next if basename == "." || basename == ".."
327
356
  next unless File.file?( absolute_path )
328
- relative = absolute_path.delete_prefix( "#{@template_canonical}/" )
329
- managed_path = ".github/#{relative}"
357
+ relative = absolute_path.delete_prefix( "#{@lint_canonical}/" )
358
+ managed_path = canonical_managed_path( relative_path: relative )
330
359
  @template_managed_files << managed_path unless @template_managed_files.include?( managed_path )
331
360
  end
332
361
  end
362
+
363
+ def canonical_managed_path( relative_path: )
364
+ raw_relative = relative_path.to_s
365
+ return ".github/#{raw_relative.delete_prefix( '.github/' )}" if raw_relative.start_with?( ".github/" )
366
+
367
+ clean_relative = raw_relative
368
+ first_segment = clean_relative.split( "/", 2 ).first
369
+ return ".github/#{clean_relative}" if CANONICAL_GITHUB_DIRECTORIES.include?( first_segment )
370
+ return ".github/#{clean_relative}" if !clean_relative.include?( "/" ) && CANONICAL_GITHUB_ROOT_FILES.include?( clean_relative )
371
+
372
+ ".github/linters/#{clean_relative}"
373
+ end
333
374
  end
334
375
  end
@@ -26,6 +26,12 @@ module Carson
26
26
  puts_verbose ""
27
27
  puts_verbose "[Working Tree]"
28
28
  puts_verbose git_capture!( "status", "--short", "--branch" ).strip
29
+ working_tree = audit_working_tree_report
30
+ if working_tree.fetch( :status ) == "block"
31
+ puts_verbose "ACTION: #{working_tree.fetch( :error )}; #{working_tree.fetch( :recovery )}."
32
+ audit_state = "block"
33
+ audit_concise_problems << "Working tree: #{working_tree.fetch( :error )} — #{working_tree.fetch( :recovery )}."
34
+ end
29
35
  puts_verbose ""
30
36
  puts_verbose "[Hooks]"
31
37
  hooks_ok = hooks_health_report
@@ -110,17 +116,17 @@ module Carson
110
116
  end
111
117
  audit_concise_problems << "Baseline (#{default_branch_baseline.fetch( :default_branch, config.main_branch )}): #{parts.join( ', ' )}."
112
118
  end
113
- if config.template_canonical.nil? || config.template_canonical.to_s.empty?
119
+ if config.lint_canonical.nil? || config.lint_canonical.to_s.empty?
114
120
  puts_verbose ""
115
- puts_verbose "[Canonical Templates]"
116
- puts_verbose "HINT: canonical templates not configured — run carson setup to enable."
121
+ puts_verbose "[Canonical Lint Policy]"
122
+ puts_verbose "HINT: lint.canonical not configured — run carson setup to enable."
117
123
  end
118
- write_and_print_pr_monitor_report(
119
- report: monitor_report.merge(
120
- default_branch_baseline: default_branch_baseline,
121
- audit_status: audit_state
122
- )
124
+ write_and_print_pr_monitor_report(
125
+ report: monitor_report.merge(
126
+ default_branch_baseline: default_branch_baseline,
127
+ audit_status: audit_state
123
128
  )
129
+ )
124
130
  exit_code = audit_state == "block" ? EXIT_BLOCK : EXIT_OK
125
131
 
126
132
  if json_output
@@ -128,6 +134,7 @@ module Carson
128
134
  command: "audit",
129
135
  status: audit_state,
130
136
  branch: current_branch,
137
+ working_tree: working_tree,
131
138
  hooks: { status: hooks_status },
132
139
  main_sync: main_sync,
133
140
  pr: monitor_report[ :pr ],
@@ -175,7 +182,7 @@ module Carson
175
182
  repos.each do |repo_path|
176
183
  repo_name = File.basename( repo_path )
177
184
  unless Dir.exist?( repo_path )
178
- puts_line "#{repo_name}: FAIL (path not found)"
185
+ puts_line "#{repo_name}: not found"
179
186
  record_batch_skip( command: "audit", repo_path: repo_path, reason: "path not found" )
180
187
  failed += 1
181
188
  next
@@ -190,15 +197,15 @@ module Carson
190
197
  clear_batch_success( command: "audit", repo_path: repo_path )
191
198
  passed += 1
192
199
  when EXIT_BLOCK
193
- puts_line "#{repo_name}: BLOCK" unless verbose?
200
+ puts_line "#{repo_name}: needs attention" unless verbose?
194
201
  blocked += 1
195
202
  else
196
- puts_line "#{repo_name}: FAIL" unless verbose?
203
+ puts_line "#{repo_name}: could not complete" unless verbose?
197
204
  record_batch_skip( command: "audit", repo_path: repo_path, reason: "audit failed" )
198
205
  failed += 1
199
206
  end
200
207
  rescue StandardError => exception
201
- puts_line "#{repo_name}: FAIL (#{exception.message})"
208
+ puts_line "#{repo_name}: could not complete (#{exception.message})"
202
209
  record_batch_skip( command: "audit", repo_path: repo_path, reason: exception.message )
203
210
  failed += 1
204
211
  end
@@ -207,11 +214,30 @@ module Carson
207
214
  puts_line ""
208
215
  puts_line "Audit all complete: #{passed} ok, #{blocked} blocked, #{failed} failed."
209
216
  blocked.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK
210
- end
217
+ end
211
218
 
212
- private
213
- def pr_and_check_report
214
- report = {
219
+ # rubocop:disable Layout/AccessModifierIndentation -- tab-width calculation produces unfixable mixed tabs+spaces
220
+ private
221
+ # rubocop:enable Layout/AccessModifierIndentation
222
+ def audit_working_tree_report
223
+ dirty_reason = dirty_worktree_reason
224
+ return { dirty: false, context: nil, status: "ok" } if dirty_reason.nil?
225
+
226
+ if dirty_reason == "main_worktree"
227
+ {
228
+ dirty: true,
229
+ context: dirty_reason,
230
+ status: "block",
231
+ error: "main working tree has uncommitted changes",
232
+ recovery: "create a worktree with carson worktree create <name>"
233
+ }
234
+ else
235
+ { dirty: true, context: dirty_reason, status: "ok" }
236
+ end
237
+ end
238
+
239
+ def pr_and_check_report
240
+ report = {
215
241
  generated_at: Time.now.utc.iso8601,
216
242
  branch: current_branch,
217
243
  status: "ok",
@@ -581,7 +607,6 @@ module Carson
581
607
  lines.join( "\n" )
582
608
  end
583
609
 
584
- # True when there are no staged/unstaged/untracked file changes.
585
610
  end
586
611
 
587
612
  include Audit