carson 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76c0a5589cad84c4aaba1fee42bd667a21a55cea4840092b2984ae9eaeb84f6c
4
- data.tar.gz: 656462b041f6764fd38780b500945ca0389b8235d741805367613051a27bd9da
3
+ metadata.gz: 45443353a34297748fec8177c152d898dabb8cf7f1641d7870b7aff59d9a2f6c
4
+ data.tar.gz: 627bdc81dcf78ad41998840796a3427512c39b6b2e80e2b961895bdf4423a456
5
5
  SHA512:
6
- metadata.gz: 14cc66d4582c9ddfb9c5e87ec8e550d1f97cbcf6701a499e730dea4c06b5598ccdc6e8f61057a921ddba2d6ac9799fed4b3191a1a6dc1e440c4bc7249eec06bd
7
- data.tar.gz: ce5e6f40342c08378830563ae0188cf61964d1f4650cc0be47a8cf52cb72f02b4f6413fa2b8d15a3dc70e3714628897446ab852ab7b276ac68a98cff0e5d1d54
6
+ metadata.gz: 96a9e858b5d934d5e5570443e25b79e28255cfe54279623be8d44474e55d592a7c9cd3161da8611d84fd24141cd10a98b6922fb364609630d843f651858bf78c
7
+ data.tar.gz: ee85efae91e54e07d872c560ad7305553b3d45246f9ec502b6fef5b7d1e6e843c8a03787629adb55190541cf5101be76a3f3ee625ec31540876b718905013ba4
data/RELEASE.md CHANGED
@@ -5,6 +5,35 @@ Release-note scope rule:
5
5
  - `RELEASE.md` records only version deltas, breaking changes, and migration actions.
6
6
  - Operational usage guides live in `MANUAL.md` and `API.md`.
7
7
 
8
+ ## 2.9.0 — Concise UX for All Commands
9
+
10
+ ### What changed
11
+
12
+ - **Concise output by default.** Every Carson command now prints clean, minimal output — what happened, what needs attention, what to do next. Diagnostic key-value lines are suppressed unless `--verbose` is passed.
13
+ - **`--verbose` flag.** Global flag available on all commands. Restores full diagnostic output (same as pre-2.9.0 behaviour). The pre-commit hook runs `carson audit` (no flags) so it automatically gets clean output.
14
+ - **Audit concise output.** A healthy audit prints one line (`Audit: ok`). Problems print only actionable summaries (e.g. `Hooks: mismatch — run carson prepare.`).
15
+ - **Refresh concise output.** Prints ~5 lines: hooks installed, templates in sync, audit result, done.
16
+ - **All other commands.** `prepare`, `inspect`, `offboard`, `template check/apply`, `prune`, `review gate/sweep`, `govern`, `lint setup`, `setup`, and `housekeep` all follow the same concise/verbose pattern.
17
+
18
+ ### What users must do now
19
+
20
+ 1. Upgrade Carson to `2.9.0`.
21
+ 2. Use `--verbose` when you need full diagnostics (debugging, CI troubleshooting).
22
+
23
+ ### Breaking or removed behaviour
24
+
25
+ - Default output is now concise. Scripts that parse Carson's key-value diagnostic lines must add `--verbose`.
26
+ - Removed `@concise` internal flag (replaced by `--verbose` opt-in pattern).
27
+
28
+ ### Upgrade steps
29
+
30
+ ```bash
31
+ cd ~/Dev/carson
32
+ git pull
33
+ bash install.sh
34
+ carson version
35
+ ```
36
+
8
37
  ## 2.8.1 — Onboard UX and Install Cleanup
9
38
 
10
39
  ### What changed
@@ -12,14 +41,16 @@ Release-note scope rule:
12
41
  - **Concise onboard output.** `carson onboard` now prints a clean 8-line summary instead of verbose internal state (hook paths, template statuses, config lines). Tells users what happened, what needs attention, and what to do next.
13
42
  - **Graceful handling of fresh repos.** Onboard no longer fails with a fatal error on repositories with no commits yet.
14
43
  - **Suppressed RubyGems PATH warning.** The misleading `WARNING: You don't have ... in your PATH, gem executables will not run` message from `gem install --user-install` is now suppressed during installation. Carson symlinks the executable to `~/.carson/bin`, making the gem bin directory irrelevant.
44
+ - **Default workflow style changed from `trunk` to `branch`.** All governed repositories now enforce PR-only merges by default. Direct commits, merge commits, and pushes to protected branches (`main`/`master`) are blocked by hooks unless explicitly opted out.
15
45
 
16
46
  ### What users must do now
17
47
 
18
48
  1. Upgrade Carson to `2.8.1`.
49
+ 2. If you rely on direct commits to main, re-run `carson setup` and choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk` in your environment.
19
50
 
20
51
  ### Breaking or removed behaviour
21
52
 
22
- - None.
53
+ - Default `workflow.style` changed from `trunk` to `branch`. Repositories that previously relied on the implicit `trunk` default will now block direct commits to protected branches. Escape hatches: run `carson setup` to choose `trunk`, or set `CARSON_WORKFLOW_STYLE=trunk`.
23
54
 
24
55
  ### Upgrade steps
25
56
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.8.1
1
+ 2.9.0
data/lib/carson/cli.rb CHANGED
@@ -19,7 +19,8 @@ module Carson
19
19
  return Runtime::EXIT_ERROR
20
20
  end
21
21
 
22
- runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, out: out, err: err )
22
+ verbose = parsed.fetch( :verbose, false )
23
+ runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
23
24
  dispatch( parsed: parsed, runtime: runtime )
24
25
  rescue ConfigError => e
25
26
  err.puts "#{BADGE} CONFIG ERROR: #{e.message}"
@@ -30,12 +31,14 @@ module Carson
30
31
  end
31
32
 
32
33
  def self.parse_args( argv:, out:, err: )
34
+ verbose = argv.delete( "--verbose" ) ? true : false
33
35
  parser = build_parser
34
36
  preset = parse_preset_command( argv: argv, out: out, parser: parser )
35
- return preset unless preset.nil?
37
+ return preset.merge( verbose: verbose ) unless preset.nil?
36
38
 
37
39
  command = argv.shift
38
- parse_command( command: command, argv: argv, parser: parser, err: err )
40
+ result = parse_command( command: command, argv: argv, parser: parser, err: err )
41
+ result.merge( verbose: verbose )
39
42
  rescue OptionParser::ParseError => e
40
43
  err.puts "#{BADGE} #{e.message}"
41
44
  err.puts parser
@@ -8,42 +8,62 @@ module Carson
8
8
  fingerprint_status = block_if_outsider_fingerprints!
9
9
  return fingerprint_status unless fingerprint_status.nil?
10
10
  audit_state = "ok"
11
- print_header "Repository"
12
- puts_line "root: #{repo_root}"
13
- puts_line "current_branch: #{current_branch}"
14
- print_header "Working Tree"
15
- puts_line git_capture!( "status", "--short", "--branch" ).strip
16
- print_header "Hooks"
11
+ audit_concise_problems = []
12
+ puts_verbose ""
13
+ puts_verbose "[Repository]"
14
+ puts_verbose "root: #{repo_root}"
15
+ puts_verbose "current_branch: #{current_branch}"
16
+ puts_verbose ""
17
+ puts_verbose "[Working Tree]"
18
+ puts_verbose git_capture!( "status", "--short", "--branch" ).strip
19
+ puts_verbose ""
20
+ puts_verbose "[Hooks]"
17
21
  hooks_ok = hooks_health_report
18
- audit_state = "block" unless hooks_ok
19
- print_header "Local Lint Quality"
22
+ unless hooks_ok
23
+ audit_state = "block"
24
+ audit_concise_problems << "Hooks: mismatch — run carson prepare."
25
+ end
26
+ puts_verbose ""
27
+ puts_verbose "[Local Lint Quality]"
20
28
  local_lint_quality = local_lint_quality_report
21
- audit_state = "block" if local_lint_quality.fetch( :status ) == "block"
22
- print_header "Main Sync Status"
29
+ if local_lint_quality.fetch( :status ) == "block"
30
+ audit_state = "block"
31
+ blocking_langs = local_lint_quality.fetch( :languages ).select { |l| l.fetch( :status ) == "block" }
32
+ blocking_langs.each do |lang|
33
+ exit_code = lang.fetch( :exit_code, 1 )
34
+ audit_concise_problems << "Lint: #{lang.fetch( :language )} failed (exit #{exit_code})."
35
+ end
36
+ end
37
+ puts_verbose ""
38
+ puts_verbose "[Main Sync Status]"
23
39
  ahead_count, behind_count, main_error = main_sync_counts
24
40
  if main_error
25
- puts_line "main_vs_remote_main: unknown"
26
- puts_line "WARN: unable to calculate main sync status (#{main_error})."
41
+ puts_verbose "main_vs_remote_main: unknown"
42
+ puts_verbose "WARN: unable to calculate main sync status (#{main_error})."
27
43
  audit_state = "attention" if audit_state == "ok"
28
44
  elsif ahead_count.positive?
29
- puts_line "main_vs_remote_main_ahead: #{ahead_count}"
30
- puts_line "main_vs_remote_main_behind: #{behind_count}"
31
- puts_line "ACTION: local #{config.main_branch} is ahead of #{config.git_remote}/#{config.main_branch} by #{ahead_count} commit#{plural_suffix( count: ahead_count )}; reset local drift before commit/push workflows."
45
+ puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
46
+ puts_verbose "main_vs_remote_main_behind: #{behind_count}"
47
+ puts_verbose "ACTION: local #{config.main_branch} is ahead of #{config.git_remote}/#{config.main_branch} by #{ahead_count} commit#{plural_suffix( count: ahead_count )}; reset local drift before commit/push workflows."
32
48
  audit_state = "block"
49
+ audit_concise_problems << "Main sync: ahead by #{ahead_count} — reset local drift."
33
50
  elsif behind_count.positive?
34
- puts_line "main_vs_remote_main_ahead: #{ahead_count}"
35
- puts_line "main_vs_remote_main_behind: #{behind_count}"
36
- puts_line "ACTION: local #{config.main_branch} is behind #{config.git_remote}/#{config.main_branch} by #{behind_count} commit#{plural_suffix( count: behind_count )}; run carson sync."
51
+ puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
52
+ puts_verbose "main_vs_remote_main_behind: #{behind_count}"
53
+ puts_verbose "ACTION: local #{config.main_branch} is behind #{config.git_remote}/#{config.main_branch} by #{behind_count} commit#{plural_suffix( count: behind_count )}; run carson sync."
37
54
  audit_state = "attention" if audit_state == "ok"
55
+ audit_concise_problems << "Main sync: behind by #{behind_count} — run carson sync."
38
56
  else
39
- puts_line "main_vs_remote_main_ahead: 0"
40
- puts_line "main_vs_remote_main_behind: 0"
41
- puts_line "ACTION: local #{config.main_branch} is in sync with #{config.git_remote}/#{config.main_branch}."
57
+ puts_verbose "main_vs_remote_main_ahead: 0"
58
+ puts_verbose "main_vs_remote_main_behind: 0"
59
+ puts_verbose "ACTION: local #{config.main_branch} is in sync with #{config.git_remote}/#{config.main_branch}."
42
60
  end
43
- print_header "PR and Required Checks (gh)"
61
+ puts_verbose ""
62
+ puts_verbose "[PR and Required Checks (gh)]"
44
63
  monitor_report = pr_and_check_report
45
64
  audit_state = "attention" if audit_state == "ok" && monitor_report.fetch( :status ) != "ok"
46
- print_header "Default Branch CI Baseline (gh)"
65
+ puts_verbose ""
66
+ puts_verbose "[Default Branch CI Baseline (gh)]"
47
67
  default_branch_baseline = default_branch_ci_baseline_report
48
68
  audit_state = "block" if default_branch_baseline.fetch( :status ) == "block"
49
69
  audit_state = "attention" if audit_state == "ok" && default_branch_baseline.fetch( :status ) != "ok"
@@ -56,9 +76,14 @@ module Carson
56
76
  audit_status: audit_state
57
77
  )
58
78
  )
59
- print_header "Audit Result"
60
- puts_line "status: #{audit_state}"
61
- puts_line( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
79
+ puts_verbose ""
80
+ puts_verbose "[Audit Result]"
81
+ puts_verbose "status: #{audit_state}"
82
+ puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
83
+ unless verbose?
84
+ audit_concise_problems.each { |problem| puts_line problem }
85
+ puts_line "Audit: #{audit_state}"
86
+ end
62
87
  audit_state == "block" ? EXIT_BLOCK : EXIT_OK
63
88
  end
64
89
 
@@ -83,7 +108,7 @@ module Carson
83
108
  unless gh_available?
84
109
  report[ :status ] = "skipped"
85
110
  report[ :skip_reason ] = "gh CLI not available in PATH"
86
- puts_line "SKIP: #{report.fetch( :skip_reason )}"
111
+ puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
87
112
  return report
88
113
  end
89
114
  pr_stdout, pr_stderr, pr_success, = gh_run( "pr", "view", current_branch, "--json", "number,title,url,state,reviewDecision" )
@@ -91,7 +116,7 @@ module Carson
91
116
  error_text = gh_error_text( stdout_text: pr_stdout, stderr_text: pr_stderr, fallback: "unable to read PR for branch #{current_branch}" )
92
117
  report[ :status ] = "skipped"
93
118
  report[ :skip_reason ] = error_text
94
- puts_line "SKIP: #{error_text}"
119
+ puts_verbose "SKIP: #{error_text}"
95
120
  return report
96
121
  end
97
122
  pr_data = JSON.parse( pr_stdout )
@@ -102,16 +127,16 @@ module Carson
102
127
  state: pr_data[ "state" ].to_s,
103
128
  review_decision: blank_to( value: pr_data[ "reviewDecision" ], default: "NONE" )
104
129
  }
105
- puts_line "pr: ##{report.dig( :pr, :number )} #{report.dig( :pr, :title )}"
106
- puts_line "url: #{report.dig( :pr, :url )}"
107
- puts_line "review_decision: #{report.dig( :pr, :review_decision )}"
130
+ puts_verbose "pr: ##{report.dig( :pr, :number )} #{report.dig( :pr, :title )}"
131
+ puts_verbose "url: #{report.dig( :pr, :url )}"
132
+ puts_verbose "review_decision: #{report.dig( :pr, :review_decision )}"
108
133
  checks_stdout, checks_stderr, checks_success, checks_exit = gh_run( "pr", "checks", report.dig( :pr, :number ).to_s, "--required", "--json", "name,state,bucket,workflow,link" )
109
134
  if checks_stdout.to_s.strip.empty?
110
135
  error_text = gh_error_text( stdout_text: checks_stdout, stderr_text: checks_stderr, fallback: "required checks unavailable" )
111
136
  report[ :checks ][ :status ] = "skipped"
112
137
  report[ :checks ][ :skip_reason ] = error_text
113
138
  report[ :status ] = "attention"
114
- puts_line "checks: SKIP (#{error_text})"
139
+ puts_verbose "checks: SKIP (#{error_text})"
115
140
  return report
116
141
  end
117
142
  checks_data = JSON.parse( checks_stdout )
@@ -123,17 +148,17 @@ module Carson
123
148
  report[ :checks ][ :pending_count ] = pending.count
124
149
  report[ :checks ][ :failing ] = normalise_check_entries( entries: failing )
125
150
  report[ :checks ][ :pending ] = normalise_check_entries( entries: pending )
126
- puts_line "required_checks_total: #{report.dig( :checks, :required_total )}"
127
- puts_line "required_checks_failing: #{report.dig( :checks, :failing_count )}"
128
- puts_line "required_checks_pending: #{report.dig( :checks, :pending_count )}"
129
- report.dig( :checks, :failing ).each { |entry| puts_line "check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
130
- report.dig( :checks, :pending ).each { |entry| puts_line "check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
151
+ puts_verbose "required_checks_total: #{report.dig( :checks, :required_total )}"
152
+ puts_verbose "required_checks_failing: #{report.dig( :checks, :failing_count )}"
153
+ puts_verbose "required_checks_pending: #{report.dig( :checks, :pending_count )}"
154
+ report.dig( :checks, :failing ).each { |entry| puts_verbose "check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
155
+ report.dig( :checks, :pending ).each { |entry| puts_verbose "check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
131
156
  report[ :status ] = "attention" if report.dig( :checks, :failing_count ).positive? || report.dig( :checks, :pending_count ).positive?
132
157
  report
133
158
  rescue JSON::ParserError => e
134
159
  report[ :status ] = "skipped"
135
160
  report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
136
- puts_line "SKIP: #{report.fetch( :skip_reason )}"
161
+ puts_verbose "SKIP: #{report.fetch( :skip_reason )}"
137
162
  report
138
163
  end
139
164
 
@@ -148,8 +173,8 @@ module Carson
148
173
  blocking_languages: 0,
149
174
  languages: []
150
175
  }
151
- puts_line "lint_target_source: #{target_source}"
152
- puts_line "lint_target_files_total: #{target_files.count}"
176
+ puts_verbose "lint_target_source: #{target_source}"
177
+ puts_verbose "lint_target_files_total: #{target_files.count}"
153
178
  config.lint_languages.each do |language, entry|
154
179
  language_report = lint_language_report(
155
180
  language: language,
@@ -162,7 +187,7 @@ module Carson
162
187
  report[ :status ] = "block"
163
188
  report[ :blocking_languages ] += 1
164
189
  end
165
- puts_line "lint_blocking_languages: #{report.fetch( :blocking_languages )}"
190
+ puts_verbose "lint_blocking_languages: #{report.fetch( :blocking_languages )}"
166
191
  report
167
192
  rescue StandardError => e
168
193
  report ||= {
@@ -191,7 +216,7 @@ module Carson
191
216
  if github_pull_request_event?
192
217
  files = lint_target_files_for_pull_request
193
218
  return [ files, "github_pull_request" ] unless files.nil?
194
- puts_line "WARN: unable to resolve pull request changed files; falling back to full repository files."
219
+ puts_verbose "WARN: unable to resolve pull request changed files; falling back to full repository files."
195
220
  end
196
221
 
197
222
  if github_actions_environment?
@@ -265,16 +290,16 @@ module Carson
265
290
  config_files: entry.fetch( :config_files ),
266
291
  exit_code: 0
267
292
  }
268
- puts_line "lint_language: #{language} enabled=#{report.fetch( :enabled )} files=#{report.fetch( :file_count )}"
293
+ puts_verbose "lint_language: #{language} enabled=#{report.fetch( :enabled )} files=#{report.fetch( :file_count )}"
269
294
  if language == "ruby" && outsider_mode?
270
295
  local_rubocop_path = File.join( repo_root, ".rubocop.yml" )
271
296
  if File.file?( local_rubocop_path )
272
297
  report[ :status ] = "block"
273
298
  report[ :reason ] = "repo-local RuboCop config is forbidden: #{relative_path( local_rubocop_path )}; remove it and use ~/.carson/lint/rubocop.yml."
274
299
  report[ :exit_code ] = EXIT_BLOCK
275
- puts_line "lint_#{language}_status: block"
276
- puts_line "lint_#{language}_reason: #{report.fetch( :reason )}"
277
- puts_line "ACTION: remove .rubocop.yml from this repository and run carson lint setup --source <path-or-git-url>."
300
+ puts_verbose "lint_#{language}_status: block"
301
+ puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
302
+ puts_verbose "ACTION: remove .rubocop.yml from this repository and run carson lint setup --source <path-or-git-url>."
278
303
  return report
279
304
  end
280
305
  end
@@ -286,9 +311,9 @@ module Carson
286
311
  report[ :status ] = "block"
287
312
  report[ :reason ] = "missing config files: #{missing_config_files.join( ', ' )}"
288
313
  report[ :exit_code ] = EXIT_BLOCK
289
- puts_line "lint_#{language}_status: block"
290
- puts_line "lint_#{language}_reason: #{report.fetch( :reason )}"
291
- puts_line "ACTION: run carson lint setup --source <path-or-git-url> to prepare ~/.carson/lint policy files."
314
+ puts_verbose "lint_#{language}_status: block"
315
+ puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
316
+ puts_verbose "ACTION: run carson lint setup --source <path-or-git-url> to prepare ~/.carson/lint policy files."
292
317
  return report
293
318
  end
294
319
 
@@ -298,16 +323,16 @@ module Carson
298
323
  report[ :status ] = "block"
299
324
  report[ :reason ] = "missing lint command"
300
325
  report[ :exit_code ] = EXIT_BLOCK
301
- puts_line "lint_#{language}_status: block"
302
- puts_line "lint_#{language}_reason: #{report.fetch( :reason )}"
326
+ puts_verbose "lint_#{language}_status: block"
327
+ puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
303
328
  return report
304
329
  end
305
330
  unless command_available_for_lint?( command_name: command_name )
306
331
  report[ :status ] = "block"
307
332
  report[ :reason ] = "command not available: #{command_name}"
308
333
  report[ :exit_code ] = EXIT_BLOCK
309
- puts_line "lint_#{language}_status: block"
310
- puts_line "lint_#{language}_reason: #{report.fetch( :reason )}"
334
+ puts_verbose "lint_#{language}_status: block"
335
+ puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
311
336
  return report
312
337
  end
313
338
 
@@ -322,9 +347,9 @@ module Carson
322
347
  fallback: "lint command failed for #{language}"
323
348
  )
324
349
  end
325
- puts_line "lint_#{language}_status: #{report.fetch( :status )}"
326
- puts_line "lint_#{language}_exit: #{report.fetch( :exit_code )}"
327
- puts_line "lint_#{language}_reason: #{report.fetch( :reason )}" unless report.fetch( :reason ).nil?
350
+ puts_verbose "lint_#{language}_status: #{report.fetch( :status )}"
351
+ puts_verbose "lint_#{language}_exit: #{report.fetch( :exit_code )}"
352
+ puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}" unless report.fetch( :reason ).nil?
328
353
  report
329
354
  end
330
355
 
@@ -405,7 +430,7 @@ module Carson
405
430
  unless gh_available?
406
431
  report[ :status ] = "skipped"
407
432
  report[ :skip_reason ] = "gh CLI not available in PATH"
408
- puts_line "baseline: SKIP (#{report.fetch( :skip_reason )})"
433
+ puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
409
434
  return report
410
435
  end
411
436
  owner, repo = repository_coordinates
@@ -455,32 +480,32 @@ module Carson
455
480
  report[ :status ] = "block" if report.fetch( :pending_count ).positive?
456
481
  report[ :status ] = "block" if report.fetch( :no_check_evidence )
457
482
  report[ :status ] = "attention" if report.fetch( :status ) == "ok" && ( report.fetch( :advisory_failing_count ).positive? || report.fetch( :advisory_pending_count ).positive? )
458
- puts_line "default_branch_repository: #{report.fetch( :repository )}"
459
- puts_line "default_branch_name: #{report.fetch( :default_branch )}"
460
- puts_line "default_branch_head_sha: #{report.fetch( :head_sha )}"
461
- puts_line "default_branch_workflows_total: #{report.fetch( :workflows_total )}"
462
- puts_line "default_branch_check_runs_total: #{report.fetch( :check_runs_total )}"
463
- puts_line "default_branch_failing: #{report.fetch( :failing_count )}"
464
- puts_line "default_branch_pending: #{report.fetch( :pending_count )}"
465
- puts_line "default_branch_advisory_failing: #{report.fetch( :advisory_failing_count )}"
466
- puts_line "default_branch_advisory_pending: #{report.fetch( :advisory_pending_count )}"
467
- report.fetch( :failing ).each { |entry| puts_line "default_branch_check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
468
- report.fetch( :pending ).each { |entry| puts_line "default_branch_check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
469
- report.fetch( :advisory_failing ).each { |entry| puts_line "default_branch_check_advisory_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
470
- report.fetch( :advisory_pending ).each { |entry| puts_line "default_branch_check_advisory_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
483
+ puts_verbose "default_branch_repository: #{report.fetch( :repository )}"
484
+ puts_verbose "default_branch_name: #{report.fetch( :default_branch )}"
485
+ puts_verbose "default_branch_head_sha: #{report.fetch( :head_sha )}"
486
+ puts_verbose "default_branch_workflows_total: #{report.fetch( :workflows_total )}"
487
+ puts_verbose "default_branch_check_runs_total: #{report.fetch( :check_runs_total )}"
488
+ puts_verbose "default_branch_failing: #{report.fetch( :failing_count )}"
489
+ puts_verbose "default_branch_pending: #{report.fetch( :pending_count )}"
490
+ puts_verbose "default_branch_advisory_failing: #{report.fetch( :advisory_failing_count )}"
491
+ puts_verbose "default_branch_advisory_pending: #{report.fetch( :advisory_pending_count )}"
492
+ report.fetch( :failing ).each { |entry| puts_verbose "default_branch_check_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
493
+ report.fetch( :pending ).each { |entry| puts_verbose "default_branch_check_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} #{entry.fetch( :link )}".strip }
494
+ report.fetch( :advisory_failing ).each { |entry| puts_verbose "default_branch_check_advisory_fail: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
495
+ report.fetch( :advisory_pending ).each { |entry| puts_verbose "default_branch_check_advisory_pending: #{entry.fetch( :workflow )} / #{entry.fetch( :name )} (advisory) #{entry.fetch( :link )}".strip }
471
496
  if report.fetch( :no_check_evidence )
472
- puts_line "ACTION: default branch has workflow files but no check-runs; align workflow triggers and branch protection check names."
497
+ puts_verbose "ACTION: default branch has workflow files but no check-runs; align workflow triggers and branch protection check names."
473
498
  end
474
499
  report
475
500
  rescue JSON::ParserError => e
476
501
  report[ :status ] = "skipped"
477
502
  report[ :skip_reason ] = "invalid gh JSON response (#{e.message})"
478
- puts_line "baseline: SKIP (#{report.fetch( :skip_reason )})"
503
+ puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
479
504
  report
480
505
  rescue StandardError => e
481
506
  report[ :status ] = "skipped"
482
507
  report[ :skip_reason ] = e.message
483
- puts_line "baseline: SKIP (#{report.fetch( :skip_reason )})"
508
+ puts_verbose "baseline: SKIP (#{report.fetch( :skip_reason )})"
484
509
  report
485
510
  end
486
511
 
@@ -577,10 +602,10 @@ module Carson
577
602
  # Writes monitor report artefacts and prints their locations.
578
603
  def write_and_print_pr_monitor_report( report: )
579
604
  markdown_path, json_path = write_pr_monitor_report( report: report )
580
- puts_line "report_markdown: #{markdown_path}"
581
- puts_line "report_json: #{json_path}"
605
+ puts_verbose "report_markdown: #{markdown_path}"
606
+ puts_verbose "report_json: #{json_path}"
582
607
  rescue StandardError => e
583
- puts_line "report_write: SKIP (#{e.message})"
608
+ puts_verbose "report_write: SKIP (#{e.message})"
584
609
  end
585
610
 
586
611
  # Persists report in both machine-readable JSON and human-readable Markdown.
@@ -703,28 +728,29 @@ module Carson
703
728
  return { status: "ok", split_required: false } if files.empty?
704
729
 
705
730
  scope = scope_integrity_status( files: files, branch: current_branch )
706
- print_header "Scope Integrity Guard"
707
- puts_line "scope_file_source: #{files_source}"
708
- puts_line "scope_file_count: #{files.count}"
709
- puts_line "branch: #{scope.fetch( :branch )}"
710
- puts_line "scope_basis: changed_paths_only"
711
- puts_line "detected_groups: #{scope.fetch( :detected_groups ).sort.join( ', ' )}"
712
- puts_line "core_groups: #{scope.fetch( :core_groups ).empty? ? 'none' : scope.fetch( :core_groups ).sort.join( ', ' )}"
713
- puts_line "non_doc_groups: #{scope.fetch( :non_doc_groups ).empty? ? 'none' : scope.fetch( :non_doc_groups ).sort.join( ', ' )}"
714
- puts_line "docs_only_changes: #{scope.fetch( :docs_only )}"
715
- puts_line "unmatched_paths_count: #{scope.fetch( :unmatched_paths ).count}"
716
- scope.fetch( :unmatched_paths ).each { |path| puts_line "unmatched_path: #{path}" }
717
- puts_line "violating_files_count: #{scope.fetch( :violating_files ).count}"
718
- scope.fetch( :violating_files ).each { |path| puts_line "violating_file: #{path} (group=#{scope.fetch( :grouped_paths ).fetch( path )})" }
719
- puts_line "checklist_single_business_intent: pass"
720
- puts_line "checklist_single_scope_group: #{scope.fetch( :split_required ) ? 'advisory' : 'pass'}"
721
- puts_line "checklist_cross_boundary_changes_justified: #{( scope.fetch( :split_required ) || scope.fetch( :misc_present ) ) ? 'advisory' : 'pass'}"
731
+ puts_verbose ""
732
+ puts_verbose "[Scope Integrity Guard]"
733
+ puts_verbose "scope_file_source: #{files_source}"
734
+ puts_verbose "scope_file_count: #{files.count}"
735
+ puts_verbose "branch: #{scope.fetch( :branch )}"
736
+ puts_verbose "scope_basis: changed_paths_only"
737
+ puts_verbose "detected_groups: #{scope.fetch( :detected_groups ).sort.join( ', ' )}"
738
+ puts_verbose "core_groups: #{scope.fetch( :core_groups ).empty? ? 'none' : scope.fetch( :core_groups ).sort.join( ', ' )}"
739
+ puts_verbose "non_doc_groups: #{scope.fetch( :non_doc_groups ).empty? ? 'none' : scope.fetch( :non_doc_groups ).sort.join( ', ' )}"
740
+ puts_verbose "docs_only_changes: #{scope.fetch( :docs_only )}"
741
+ puts_verbose "unmatched_paths_count: #{scope.fetch( :unmatched_paths ).count}"
742
+ scope.fetch( :unmatched_paths ).each { |path| puts_verbose "unmatched_path: #{path}" }
743
+ puts_verbose "violating_files_count: #{scope.fetch( :violating_files ).count}"
744
+ scope.fetch( :violating_files ).each { |path| puts_verbose "violating_file: #{path} (group=#{scope.fetch( :grouped_paths ).fetch( path )})" }
745
+ puts_verbose "checklist_single_business_intent: pass"
746
+ puts_verbose "checklist_single_scope_group: #{scope.fetch( :split_required ) ? 'advisory' : 'pass'}"
747
+ puts_verbose "checklist_cross_boundary_changes_justified: #{( scope.fetch( :split_required ) || scope.fetch( :misc_present ) ) ? 'advisory' : 'pass'}"
722
748
  if scope.fetch( :split_required )
723
- puts_line "ACTION: multiple module groups detected (informational only)."
749
+ puts_verbose "ACTION: multiple module groups detected (informational only)."
724
750
  elsif scope.fetch( :misc_present )
725
- puts_line "ACTION: unmatched paths detected; classify via scope.path_groups for stricter module checks."
751
+ puts_verbose "ACTION: unmatched paths detected; classify via scope.path_groups for stricter module checks."
726
752
  else
727
- puts_line "ACTION: scope integrity is within commit policy."
753
+ puts_verbose "ACTION: scope integrity is within commit policy."
728
754
  end
729
755
  { status: scope.fetch( :status ), split_required: scope.fetch( :split_required ) }
730
756
  end
@@ -83,7 +83,8 @@ module Carson
83
83
 
84
84
  # Standalone housekeep: sync + prune.
85
85
  def housekeep!
86
- print_header "Housekeep"
86
+ puts_verbose ""
87
+ puts_verbose "[Housekeep]"
87
88
  sync_status = sync!
88
89
  if sync_status != EXIT_OK
89
90
  puts_line "housekeep: sync returned #{sync_status}; skipping prune."
@@ -571,8 +572,8 @@ module Carson
571
572
  md_path = File.join( report_dir, GOVERN_REPORT_MD )
572
573
  File.write( json_path, JSON.pretty_generate( report ) )
573
574
  File.write( md_path, render_govern_markdown( report: report ) )
574
- puts_line "report_json: #{json_path}"
575
- puts_line "report_markdown: #{md_path}"
575
+ puts_verbose "report_json: #{json_path}"
576
+ puts_verbose "report_markdown: #{md_path}"
576
577
  end
577
578
 
578
579
  def render_govern_markdown( report: )
@@ -632,7 +633,11 @@ module Carson
632
633
  end
633
634
 
634
635
  repos_count = Array( report[ :repos ] ).length
635
- puts_line "govern_summary: repos=#{repos_count} prs=#{total_prs} ready=#{ready_count} blocked=#{blocked_count}"
636
+ if verbose?
637
+ puts_line "govern_summary: repos=#{repos_count} prs=#{total_prs} ready=#{ready_count} blocked=#{blocked_count}"
638
+ else
639
+ puts_line "Govern: #{repos_count} repo#{plural_suffix( count: repos_count )}, #{total_prs} PR#{plural_suffix( count: total_prs )} (#{ready_count} ready, #{blocked_count} blocked)"
640
+ end
636
641
  end
637
642
  end
638
643
 
@@ -7,7 +7,8 @@ module Carson
7
7
  module Lint
8
8
  # Prepares canonical lint policy files under ~/.carson/lint from an explicit source.
9
9
  def lint_setup!( source:, ref: "main", force: false )
10
- print_header "Lint Setup"
10
+ puts_verbose ""
11
+ puts_verbose "[Lint Setup]"
11
12
  source_text = source.to_s.strip
12
13
  if source_text.empty?
13
14
  puts_line "ERROR: lint setup requires --source <path-or-git-url>."
@@ -29,12 +30,12 @@ module Carson
29
30
  target_coding_dir: target_coding_dir,
30
31
  force: force
31
32
  )
32
- puts_line "lint_setup_source: #{source_text}"
33
- puts_line "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
34
- puts_line "lint_setup_target: #{target_coding_dir}"
35
- puts_line "lint_setup_created: #{copy_result.fetch( :created )}"
36
- puts_line "lint_setup_updated: #{copy_result.fetch( :updated )}"
37
- puts_line "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
33
+ puts_verbose "lint_setup_source: #{source_text}"
34
+ puts_verbose "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
35
+ puts_verbose "lint_setup_target: #{target_coding_dir}"
36
+ puts_verbose "lint_setup_created: #{copy_result.fetch( :created )}"
37
+ puts_verbose "lint_setup_updated: #{copy_result.fetch( :updated )}"
38
+ puts_verbose "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
38
39
 
39
40
  missing_policy = missing_lint_policy_files
40
41
  if missing_policy.empty?
@@ -43,7 +44,7 @@ module Carson
43
44
  end
44
45
 
45
46
  missing_policy.each do |entry|
46
- puts_line "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
47
+ puts_verbose "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
47
48
  end
48
49
  puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
49
50
  EXIT_ERROR
@@ -43,12 +43,19 @@ module Carson
43
43
  return prune_no_stale_branches if stale_branches.empty?
44
44
 
45
45
  counters = prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch )
46
- puts_line "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
46
+ puts_verbose "prune_summary: deleted=#{counters.fetch( :deleted )} skipped=#{counters.fetch( :skipped )}"
47
+ unless verbose?
48
+ puts_line "Pruned #{counters.fetch( :deleted )} stale branch#{plural_suffix( count: counters.fetch( :deleted ) )}."
49
+ end
47
50
  EXIT_OK
48
51
  end
49
52
 
50
53
  def prune_no_stale_branches
51
- puts_line "OK: no stale local branches tracking deleted #{config.git_remote} branches."
54
+ if verbose?
55
+ puts_line "OK: no stale local branches tracking deleted #{config.git_remote} branches."
56
+ else
57
+ puts_line "No stale branches."
58
+ end
52
59
  EXIT_OK
53
60
  end
54
61
 
@@ -72,7 +79,7 @@ module Carson
72
79
 
73
80
  def prune_skip_stale_branch( type:, branch:, upstream: )
74
81
  status = type == :protected ? "skip_protected_branch" : "skip_current_branch"
75
- puts_line "#{status}: #{branch} (upstream=#{upstream})"
82
+ puts_verbose "#{status}: #{branch} (upstream=#{upstream})"
76
83
  :skipped
77
84
  end
78
85
 
@@ -89,8 +96,8 @@ module Carson
89
96
  end
90
97
 
91
98
  def prune_safe_delete_success( branch:, upstream:, stdout_text: )
92
- out.print stdout_text unless stdout_text.empty?
93
- puts_line "deleted_local_branch: #{branch} (upstream=#{upstream})"
99
+ out.print stdout_text if verbose? && !stdout_text.empty?
100
+ puts_verbose "deleted_local_branch: #{branch} (upstream=#{upstream})"
94
101
  :deleted
95
102
  end
96
103
 
@@ -108,20 +115,20 @@ module Carson
108
115
  end
109
116
 
110
117
  def prune_force_delete_success( branch:, upstream:, merged_pr:, force_stdout: )
111
- out.print force_stdout unless force_stdout.empty?
112
- puts_line "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
118
+ out.print force_stdout if verbose? && !force_stdout.empty?
119
+ puts_verbose "deleted_local_branch_force: #{branch} (upstream=#{upstream}) merged_pr=#{merged_pr.fetch( :url )}"
113
120
  :deleted
114
121
  end
115
122
 
116
123
  def prune_force_delete_failed( branch:, upstream:, force_stderr: )
117
124
  force_error_text = normalise_branch_delete_error( error_text: force_stderr )
118
- puts_line "fail_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error_text}"
125
+ puts_verbose "fail_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error_text}"
119
126
  :skipped
120
127
  end
121
128
 
122
129
  def prune_force_delete_skipped( branch:, upstream:, delete_error_text:, force_error: )
123
- puts_line "skip_delete_branch: #{branch} (upstream=#{upstream}) reason=#{delete_error_text}"
124
- puts_line "skip_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error}" unless force_error.to_s.strip.empty?
130
+ puts_verbose "skip_delete_branch: #{branch} (upstream=#{upstream}) reason=#{delete_error_text}"
131
+ puts_verbose "skip_force_delete_branch: #{branch} (upstream=#{upstream}) reason=#{force_error}" unless force_error.to_s.strip.empty?
125
132
  :skipped
126
133
  end
127
134
 
@@ -153,12 +160,15 @@ module Carson
153
160
  target_path = File.join( hooks_dir, hook_name )
154
161
  FileUtils.cp( source_path, target_path )
155
162
  FileUtils.chmod( 0o755, target_path )
156
- puts_line "hook_written: #{relative_path( target_path )}" unless concise?
163
+ puts_verbose "hook_written: #{relative_path( target_path )}"
157
164
  end
158
165
  git_system!( "config", "core.hooksPath", hooks_dir )
159
166
  File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
160
- puts_line "configured_hooks_path: #{hooks_dir}" unless concise?
161
- return EXIT_OK if concise?
167
+ puts_verbose "configured_hooks_path: #{hooks_dir}"
168
+ unless verbose?
169
+ puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
170
+ return EXIT_OK
171
+ end
162
172
 
163
173
  inspect!
164
174
  end
@@ -188,8 +198,6 @@ module Carson
188
198
  end
189
199
 
190
200
  onboard_apply!
191
- ensure
192
- @concise = false
193
201
  end
194
202
 
195
203
  # Re-applies hooks, templates, and audit after upgrading Carson.
@@ -197,29 +205,52 @@ module Carson
197
205
  fingerprint_status = block_if_outsider_fingerprints!
198
206
  return fingerprint_status unless fingerprint_status.nil?
199
207
 
200
- print_header "Refresh"
201
208
  unless inside_git_work_tree?
202
209
  puts_line "ERROR: #{repo_root} is not a git repository."
203
210
  return EXIT_ERROR
204
211
  end
205
- hook_status = prepare!
212
+
213
+ if verbose?
214
+ puts_verbose ""
215
+ puts_verbose "[Refresh]"
216
+ hook_status = prepare!
217
+ return hook_status unless hook_status == EXIT_OK
218
+
219
+ template_status = template_apply!
220
+ return template_status unless template_status == EXIT_OK
221
+
222
+ audit_status = audit!
223
+ if audit_status == EXIT_OK
224
+ puts_line "OK: Carson refresh completed for #{repo_root}."
225
+ elsif audit_status == EXIT_BLOCK
226
+ puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
227
+ end
228
+ return audit_status
229
+ end
230
+
231
+ puts_line "Refresh"
232
+ hook_status = with_captured_output { prepare! }
206
233
  return hook_status unless hook_status == EXIT_OK
234
+ puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
207
235
 
208
- template_status = template_apply!
236
+ template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
237
+ template_status = with_captured_output { template_apply! }
209
238
  return template_status unless template_status == EXIT_OK
239
+ if template_drift_count.positive?
240
+ puts_line "Templates applied (#{template_drift_count} updated)."
241
+ else
242
+ puts_line "Templates in sync."
243
+ end
210
244
 
211
245
  audit_status = audit!
212
- if audit_status == EXIT_OK
213
- puts_line "OK: Carson refresh completed for #{repo_root}."
214
- elsif audit_status == EXIT_BLOCK
215
- puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
216
- end
246
+ puts_line "Refresh complete."
217
247
  audit_status
218
248
  end
219
249
 
220
250
  # Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
221
251
  def offboard!
222
- print_header "Offboard"
252
+ puts_verbose ""
253
+ puts_verbose "[Offboard]"
223
254
  unless inside_git_work_tree?
224
255
  puts_line "ERROR: #{repo_root} is not a git repository."
225
256
  return EXIT_ERROR
@@ -233,16 +264,20 @@ module Carson
233
264
  absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" )
234
265
  if File.exist?( absolute )
235
266
  FileUtils.rm_rf( absolute )
236
- puts_line "removed_path: #{relative}"
267
+ puts_verbose "removed_path: #{relative}"
237
268
  removed_count += 1
238
269
  else
239
- puts_line "skip_missing_path: #{relative}"
270
+ puts_verbose "skip_missing_path: #{relative}"
240
271
  missing_count += 1
241
272
  end
242
273
  end
243
274
  remove_empty_offboard_directories!
244
- puts_line "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
245
- puts_line "OK: Carson offboard completed for #{repo_root}."
275
+ puts_verbose "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
276
+ if verbose?
277
+ puts_line "OK: Carson offboard completed for #{repo_root}."
278
+ else
279
+ puts_line "Removed #{removed_count} file#{plural_suffix( count: removed_count )}. Offboard complete."
280
+ end
246
281
  EXIT_OK
247
282
  end
248
283
 
@@ -251,9 +286,13 @@ module Carson
251
286
  fingerprint_status = block_if_outsider_fingerprints!
252
287
  return fingerprint_status unless fingerprint_status.nil?
253
288
 
254
- print_header "Inspect"
289
+ puts_verbose ""
290
+ puts_verbose "[Inspect]"
255
291
  ok = hooks_health_report( strict: true )
256
- puts_line( ok ? "status: ok" : "status: block" )
292
+ puts_verbose( ok ? "status: ok" : "status: block" )
293
+ unless verbose?
294
+ puts_line( ok ? "Hooks: ok" : "Hooks: block" )
295
+ end
257
296
  ok ? EXIT_OK : EXIT_BLOCK
258
297
  end
259
298
 
@@ -262,14 +301,24 @@ module Carson
262
301
  fingerprint_status = block_if_outsider_fingerprints!
263
302
  return fingerprint_status unless fingerprint_status.nil?
264
303
 
265
- print_header "Template Sync Check"
304
+ puts_verbose ""
305
+ puts_verbose "[Template Sync Check]"
266
306
  results = template_results
267
307
  drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
268
308
  error_count = results.count { |entry| entry.fetch( :status ) == "error" }
269
309
  results.each do |entry|
270
- puts_line "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
310
+ puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
311
+ end
312
+ puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
313
+ unless verbose?
314
+ if drift_count.positive?
315
+ drift_files = results.select { |entry| entry.fetch( :status ) == "drift" }.map { |entry| entry.fetch( :file ) }
316
+ puts_line "Templates: #{drift_count} of #{results.count} drifted"
317
+ drift_files.each { |file| puts_line " #{file}" }
318
+ else
319
+ puts_line "Templates: #{results.count} files in sync"
320
+ end
271
321
  end
272
- puts_line "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
273
322
  return EXIT_ERROR if error_count.positive?
274
323
 
275
324
  drift_count.positive? ? EXIT_BLOCK : EXIT_OK
@@ -280,29 +329,37 @@ module Carson
280
329
  fingerprint_status = block_if_outsider_fingerprints!
281
330
  return fingerprint_status unless fingerprint_status.nil?
282
331
 
283
- print_header "Template Sync Apply" unless concise?
332
+ puts_verbose ""
333
+ puts_verbose "[Template Sync Apply]"
284
334
  results = template_results
285
335
  applied = 0
286
336
  results.each do |entry|
287
337
  if entry.fetch( :status ) == "error"
288
- puts_line "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}" unless concise?
338
+ puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
289
339
  next
290
340
  end
291
341
 
292
342
  file_path = File.join( repo_root, entry.fetch( :file ) )
293
343
  if entry.fetch( :status ) == "ok"
294
- puts_line "template_file: #{entry.fetch( :file )} status=ok reason=in_sync" unless concise?
344
+ puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
295
345
  next
296
346
  end
297
347
 
298
348
  FileUtils.mkdir_p( File.dirname( file_path ) )
299
349
  File.write( file_path, entry.fetch( :applied_content ) )
300
- puts_line "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}" unless concise?
350
+ puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
301
351
  applied += 1
302
352
  end
303
353
 
304
354
  error_count = results.count { |entry| entry.fetch( :status ) == "error" }
305
- puts_line "template_apply_summary: updated=#{applied} error=#{error_count}" unless concise?
355
+ puts_verbose "template_apply_summary: updated=#{applied} error=#{error_count}"
356
+ unless verbose?
357
+ if applied.positive?
358
+ puts_line "Templates applied (#{applied} updated)."
359
+ else
360
+ puts_line "Templates in sync."
361
+ end
362
+ end
306
363
  error_count.positive? ? EXIT_ERROR : EXIT_OK
307
364
  end
308
365
 
@@ -363,9 +420,9 @@ module Carson
363
420
  def print_hooks_path_status( configured:, expected: )
364
421
  configured_abs = configured.nil? ? nil : File.expand_path( configured )
365
422
  hooks_path_ok = configured_abs == expected
366
- puts_line "hooks_path: #{configured || '(unset)'}"
367
- puts_line "hooks_path_expected: #{expected}"
368
- puts_line( hooks_path_ok ? "hooks_path_status: ok" : "hooks_path_status: attention" )
423
+ puts_verbose "hooks_path: #{configured || '(unset)'}"
424
+ puts_verbose "hooks_path_expected: #{expected}"
425
+ puts_verbose( hooks_path_ok ? "hooks_path_status: ok" : "hooks_path_status: attention" )
369
426
  hooks_path_ok
370
427
  end
371
428
 
@@ -374,7 +431,7 @@ module Carson
374
431
  exists = File.file?( path )
375
432
  symlink = File.symlink?( path )
376
433
  executable = exists && !symlink && File.executable?( path )
377
- puts_line "hook_file: #{relative_path( path )} exists=#{exists} symlink=#{symlink} executable=#{executable}"
434
+ puts_verbose "hook_file: #{relative_path( path )} exists=#{exists} symlink=#{symlink} executable=#{executable}"
378
435
  end
379
436
  end
380
437
 
@@ -399,13 +456,13 @@ module Carson
399
456
  if strict && !hooks_path_ok
400
457
  configured_text = configured.to_s.strip
401
458
  if configured_text.empty?
402
- puts_line "ACTION: hooks path is unset (expected=#{expected})."
459
+ puts_verbose "ACTION: hooks path is unset (expected=#{expected})."
403
460
  else
404
- puts_line "ACTION: hooks path mismatch (configured=#{configured_text}, expected=#{expected})."
461
+ puts_verbose "ACTION: hooks path mismatch (configured=#{configured_text}, expected=#{expected})."
405
462
  end
406
463
  end
407
464
  message = strict ? "ACTION: run carson prepare to align hooks with Carson #{Carson::VERSION}." : "ACTION: run carson prepare to enforce local main protections."
408
- puts_line message
465
+ puts_verbose message
409
466
  end
410
467
 
411
468
  # Returns ahead/behind counts for local main versus configured remote main.
@@ -604,17 +661,17 @@ module Carson
604
661
  def disable_carson_hooks_path!
605
662
  configured = configured_hooks_path
606
663
  if configured.nil?
607
- puts_line "hooks_path: (unset)"
664
+ puts_verbose "hooks_path: (unset)"
608
665
  return EXIT_OK
609
666
  end
610
- puts_line "hooks_path: #{configured}"
667
+ puts_verbose "hooks_path: #{configured}"
611
668
  configured_abs = File.expand_path( configured, repo_root )
612
669
  unless carson_managed_hooks_path?( configured_abs: configured_abs )
613
- puts_line "hooks_path_kept: #{configured} (not Carson-managed)"
670
+ puts_verbose "hooks_path_kept: #{configured} (not Carson-managed)"
614
671
  return EXIT_OK
615
672
  end
616
673
  git_system!( "config", "--unset", "core.hooksPath" )
617
- puts_line "hooks_path_unset: core.hooksPath"
674
+ puts_verbose "hooks_path_unset: core.hooksPath"
618
675
  EXIT_OK
619
676
  rescue StandardError => e
620
677
  puts_line "ERROR: unable to update core.hooksPath (#{e.message})"
@@ -660,14 +717,14 @@ module Carson
660
717
  next unless Dir.empty?( absolute )
661
718
 
662
719
  Dir.rmdir( absolute )
663
- puts_line "removed_empty_dir: #{relative}"
720
+ puts_verbose "removed_empty_dir: #{relative}"
664
721
  end
665
722
  end
666
723
 
667
724
  # Verifies configured remote exists and logs status without mutating remotes.
668
725
  def report_detected_remote!
669
726
  if git_remote_exists?( remote_name: config.git_remote )
670
- puts_line "remote_ok: #{config.git_remote}"
727
+ puts_verbose "remote_ok: #{config.git_remote}"
671
728
  else
672
729
  puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
673
730
  end
@@ -675,14 +732,12 @@ module Carson
675
732
 
676
733
  # Concise onboard orchestration: hooks, templates, remote, audit, guidance.
677
734
  def onboard_apply!
678
- @concise = true
679
-
680
- hook_status = prepare!
735
+ hook_status = with_captured_output { prepare! }
681
736
  return hook_status unless hook_status == EXIT_OK
682
737
  puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
683
738
 
684
739
  template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
685
- template_status = template_apply!
740
+ template_status = with_captured_output { template_apply! }
686
741
  return template_status unless template_status == EXIT_OK
687
742
  if template_drift_count.positive?
688
743
  puts_line "Templates synced (#{template_drift_count} file#{plural_suffix( count: template_drift_count )} updated)."
@@ -8,10 +8,10 @@ module Carson
8
8
  return unless config.review_wait_seconds.positive?
9
9
  quick = review_gate_snapshot( owner: owner, repo: repo, pr_number: pr_number )
10
10
  if quick[ :unresolved_threads ].empty? && quick[ :unacknowledged_actionable ].empty?
11
- puts_line "warmup_skip: all threads resolved"
11
+ puts_verbose "warmup_skip: all threads resolved"
12
12
  return quick
13
13
  end
14
- puts_line "warmup_wait_seconds: #{config.review_wait_seconds}"
14
+ puts_verbose "warmup_wait_seconds: #{config.review_wait_seconds}"
15
15
  sleep config.review_wait_seconds
16
16
  nil
17
17
  end
@@ -19,7 +19,7 @@ module Carson
19
19
  # Poll delay between consecutive snapshot reads during convergence checks.
20
20
  def wait_for_review_poll
21
21
  return unless config.review_poll_seconds.positive?
22
- puts_line "poll_wait_seconds: #{config.review_poll_seconds}"
22
+ puts_verbose "poll_wait_seconds: #{config.review_poll_seconds}"
23
23
  sleep config.review_poll_seconds
24
24
  end
25
25
 
@@ -173,10 +173,10 @@ module Carson
173
173
  json_name: REVIEW_GATE_REPORT_JSON,
174
174
  renderer: method( :render_review_gate_markdown )
175
175
  )
176
- puts_line "review_gate_report_markdown: #{markdown_path}"
177
- puts_line "review_gate_report_json: #{json_path}"
176
+ puts_verbose "review_gate_report_markdown: #{markdown_path}"
177
+ puts_verbose "review_gate_report_json: #{json_path}"
178
178
  rescue StandardError => e
179
- puts_line "review_gate_report_write: SKIP (#{e.message})"
179
+ puts_verbose "review_gate_report_write: SKIP (#{e.message})"
180
180
  end
181
181
 
182
182
  # Human-readable review gate report for merge-readiness evidence.
@@ -202,10 +202,10 @@ module Carson
202
202
  json_name: REVIEW_SWEEP_REPORT_JSON,
203
203
  renderer: method( :render_review_sweep_markdown )
204
204
  )
205
- puts_line "review_sweep_report_markdown: #{markdown_path}"
206
- puts_line "review_sweep_report_json: #{json_path}"
205
+ puts_verbose "review_sweep_report_markdown: #{markdown_path}"
206
+ puts_verbose "review_sweep_report_json: #{json_path}"
207
207
  rescue StandardError => e
208
- puts_line "review_sweep_report_write: SKIP (#{e.message})"
208
+ puts_verbose "review_sweep_report_write: SKIP (#{e.message})"
209
209
  end
210
210
 
211
211
  # Human-readable scheduled sweep report.
@@ -16,7 +16,11 @@ module Carson
16
16
  def review_gate!
17
17
  fingerprint_status = block_if_outsider_fingerprints!
18
18
  return fingerprint_status unless fingerprint_status.nil?
19
- print_header "Review Gate"
19
+ puts_verbose ""
20
+ puts_verbose "[Review Gate]"
21
+ unless verbose?
22
+ puts_line "Review Gate"
23
+ end
20
24
  unless gh_available?
21
25
  puts_line "ERROR: gh CLI not available in PATH."
22
26
  return EXIT_ERROR
@@ -67,13 +71,13 @@ module Carson
67
71
  snapshot = review_gate_snapshot( owner: owner, repo: repo, pr_number: pr_summary.fetch( :number ) )
68
72
  last_snapshot = snapshot
69
73
  signature = review_gate_signature( snapshot: snapshot )
70
- puts_line "poll_attempt: #{poll_attempts}/#{config.review_max_polls}"
71
- puts_line "latest_activity: #{snapshot.fetch( :latest_activity ) || 'unknown'}"
72
- puts_line "unresolved_threads: #{snapshot.fetch( :unresolved_threads ).count}"
73
- puts_line "unacknowledged_actionable: #{snapshot.fetch( :unacknowledged_actionable ).count}"
74
+ puts_verbose "poll_attempt: #{poll_attempts}/#{config.review_max_polls}"
75
+ puts_verbose "latest_activity: #{snapshot.fetch( :latest_activity ) || 'unknown'}"
76
+ puts_verbose "unresolved_threads: #{snapshot.fetch( :unresolved_threads ).count}"
77
+ puts_verbose "unacknowledged_actionable: #{snapshot.fetch( :unacknowledged_actionable ).count}"
74
78
  if !last_signature.nil? && signature == last_signature
75
79
  converged = true
76
- puts_line "convergence: stable"
80
+ puts_verbose "convergence: stable"
77
81
  break
78
82
  end
79
83
  last_signature = signature
@@ -110,6 +114,9 @@ module Carson
110
114
  unacknowledged_actionable: last_snapshot.fetch( :unacknowledged_actionable )
111
115
  }
112
116
  write_review_gate_report( report: report )
117
+ unless verbose?
118
+ puts_line "Polling... (converged after #{poll_attempts} attempt#{plural_suffix( count: poll_attempts )})"
119
+ end
113
120
  if block_reasons.empty?
114
121
  puts_line "OK: review gate passed."
115
122
  return EXIT_OK
@@ -128,7 +135,8 @@ module Carson
128
135
  def review_sweep!
129
136
  fingerprint_status = block_if_outsider_fingerprints!
130
137
  return fingerprint_status unless fingerprint_status.nil?
131
- print_header "Review Sweep"
138
+ puts_verbose ""
139
+ puts_verbose "[Review Sweep]"
132
140
  unless gh_available?
133
141
  puts_line "ERROR: gh CLI not available in PATH."
134
142
  return EXIT_ERROR
@@ -137,8 +145,8 @@ module Carson
137
145
  owner, repo = repository_coordinates
138
146
  cutoff_time = Time.now.utc - ( config.review_sweep_window_days * 86_400 )
139
147
  pull_requests = recent_pull_requests_for_sweep( owner: owner, repo: repo, cutoff_time: cutoff_time )
140
- puts_line "window_days: #{config.review_sweep_window_days}"
141
- puts_line "candidate_prs: #{pull_requests.count}"
148
+ puts_verbose "window_days: #{config.review_sweep_window_days}"
149
+ puts_verbose "candidate_prs: #{pull_requests.count}"
142
150
  findings = []
143
151
 
144
152
  pull_requests.each do |entry|
@@ -4,7 +4,8 @@ module Carson
4
4
  WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
5
5
 
6
6
  def setup!
7
- print_header "Setup"
7
+ puts_verbose ""
8
+ puts_verbose "[Setup]"
8
9
 
9
10
  unless inside_git_work_tree?
10
11
  puts_line "WARN: not a git repository. Skipping remote and branch detection."
@@ -43,19 +44,19 @@ module Carson
43
44
  choices = {}
44
45
  if detected && detected != config.git_remote
45
46
  choices[ "git.remote" ] = detected
46
- puts_line "detected_remote: #{detected}"
47
+ puts_verbose "detected_remote: #{detected}"
47
48
  elsif detected
48
- puts_line "detected_remote: #{detected}"
49
+ puts_verbose "detected_remote: #{detected}"
49
50
  else
50
- puts_line "detected_remote: none"
51
+ puts_verbose "detected_remote: none"
51
52
  end
52
53
 
53
54
  branch = detect_main_branch
54
55
  if branch && branch != config.main_branch
55
56
  choices[ "git.main_branch" ] = branch
56
- puts_line "detected_main_branch: #{branch}"
57
+ puts_verbose "detected_main_branch: #{branch}"
57
58
  elsif branch
58
- puts_line "detected_main_branch: #{branch}"
59
+ puts_verbose "detected_main_branch: #{branch}"
59
60
  end
60
61
 
61
62
  write_setup_config( choices: choices )
@@ -103,8 +104,8 @@ module Carson
103
104
  puts_line ""
104
105
  puts_line "Workflow style"
105
106
  options = [
106
- { label: "trunkcommit directly to main (default)", value: "trunk" },
107
- { label: "branchenforce PR-only merges", value: "branch" }
107
+ { label: "branchenforce PR-only merges (default)", value: "branch" },
108
+ { label: "trunkcommit directly to main", value: "trunk" }
108
109
  ]
109
110
  prompt_choice( options: options, default: 0 )
110
111
  end
@@ -23,13 +23,13 @@ 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 )
26
+ def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin, verbose: false )
27
27
  @repo_root = repo_root
28
28
  @tool_root = tool_root
29
29
  @out = out
30
30
  @err = err
31
31
  @in = in_stream
32
- @concise = false
32
+ @verbose = verbose
33
33
  @config = Config.load( repo_root: repo_root )
34
34
  @git_adapter = Adapters::Git.new( repo_root: repo_root )
35
35
  @github_adapter = Adapters::GitHub.new( repo_root: repo_root )
@@ -39,9 +39,14 @@ module Carson
39
39
 
40
40
  attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
41
41
 
42
- # Returns true when output should be minimal (used by onboard/refresh).
43
- def concise?
44
- @concise
42
+ # Returns true when full diagnostic output is enabled via --verbose.
43
+ def verbose?
44
+ @verbose
45
+ end
46
+
47
+ # Prints a line only when verbose mode is active.
48
+ def puts_verbose( message )
49
+ puts_line( message ) if verbose?
45
50
  end
46
51
 
47
52
  # Runs a block with all output captured (suppressed from the user).
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carson
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.1
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang