carson 2.8.0 → 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: 0c3f3bf2f0ea58a2d460c36fb57b6313d5dab97c08b76ec94ae1c1e9b685b24f
4
- data.tar.gz: 48562197450233540c28a6fa912c38eaa636910ed3bab57e32e95d7b0b66c10a
3
+ metadata.gz: 45443353a34297748fec8177c152d898dabb8cf7f1641d7870b7aff59d9a2f6c
4
+ data.tar.gz: 627bdc81dcf78ad41998840796a3427512c39b6b2e80e2b961895bdf4423a456
5
5
  SHA512:
6
- metadata.gz: 63a4d9fed728d78b2fcaf0d0d6ffb158a93e464af24997d661b24d77a2764e4a84014f45d4cfe2e204c7bac337c796f8e69e3722fe30a135f98650176271dba3
7
- data.tar.gz: aba034301bccbf786198a1a44bd767d3f239370807a68841a89b6ffc40340719ef37dea2ddae9a0d54eedadf1b42c2446d02614721ab946586b809de0438a2d4
6
+ metadata.gz: 96a9e858b5d934d5e5570443e25b79e28255cfe54279623be8d44474e55d592a7c9cd3161da8611d84fd24141cd10a98b6922fb364609630d843f651858bf78c
7
+ data.tar.gz: ee85efae91e54e07d872c560ad7305553b3d45246f9ec502b6fef5b7d1e6e843c8a03787629adb55190541cf5101be76a3f3ee625ec31540876b718905013ba4
data/RELEASE.md CHANGED
@@ -5,6 +5,79 @@ 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
+
37
+ ## 2.8.1 — Onboard UX and Install Cleanup
38
+
39
+ ### What changed
40
+
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.
42
+ - **Graceful handling of fresh repos.** Onboard no longer fails with a fatal error on repositories with no commits yet.
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.
45
+
46
+ ### What users must do now
47
+
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.
50
+
51
+ ### Breaking or removed behaviour
52
+
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`.
54
+
55
+ ### Upgrade steps
56
+
57
+ ```bash
58
+ cd ~/Dev/carson
59
+ git pull
60
+ bash install.sh
61
+ carson version
62
+ ```
63
+
64
+ ### Engineering Appendix
65
+
66
+ #### Modified components
67
+
68
+ - `lib/carson/runtime.rb` — added `concise?` flag and `with_captured_output` helper for suppressing sub-command detail during onboard.
69
+ - `lib/carson/runtime/local.rb` — rewrote `onboard!` to use concise orchestration (`onboard_apply!`), added `onboard_report_remote!` and `onboard_run_audit!` helpers, added `unless concise?` guards to `prepare!` and `template_apply!`.
70
+ - `install.sh` — capture `gem install` stderr and filter out RubyGems PATH warning.
71
+ - `script/install_global_carson.sh` — same PATH warning suppression.
72
+ - `test/runtime_govern_test.rb` — updated onboard output assertions.
73
+
74
+ #### Public interface and config changes
75
+
76
+ - No new CLI commands or config keys.
77
+ - Exit status contract unchanged.
78
+
79
+ ---
80
+
8
81
  ## 2.8.0 — Interactive Setup and Remote Detection
9
82
 
10
83
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.8.0
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,26 +160,34 @@ 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 )}"
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}"
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
172
+
161
173
  inspect!
162
174
  end
163
175
 
164
176
  # One-command onboarding for new repositories: detect remote, install hooks,
165
- # apply templates, and produce a first audit report.
177
+ # apply templates, and run initial audit.
166
178
  def onboard!
167
179
  fingerprint_status = block_if_outsider_fingerprints!
168
180
  return fingerprint_status unless fingerprint_status.nil?
169
181
 
170
- print_header "Onboard"
171
182
  unless inside_git_work_tree?
172
183
  puts_line "ERROR: #{repo_root} is not a git repository."
173
184
  return EXIT_ERROR
174
185
  end
175
186
 
187
+ repo_name = File.basename( repo_root )
188
+ puts_line ""
189
+ puts_line "Onboarding #{repo_name}..."
190
+
176
191
  unless global_config_exists?
177
192
  if self.in.respond_to?( :tty? ) && self.in.tty?
178
193
  setup_status = setup!
@@ -182,21 +197,7 @@ module Carson
182
197
  end
183
198
  end
184
199
 
185
- report_detected_remote!
186
- hook_status = prepare!
187
- return hook_status unless hook_status == EXIT_OK
188
-
189
- template_status = template_apply!
190
- return template_status unless template_status == EXIT_OK
191
-
192
- audit_status = audit!
193
- if audit_status == EXIT_OK
194
- puts_line "OK: Carson onboard completed for #{repo_root}."
195
- elsif audit_status == EXIT_BLOCK
196
- puts_line "BLOCK: Carson onboard completed with policy blocks; resolve and rerun carson audit."
197
- end
198
- print_onboarding_guidance
199
- audit_status
200
+ onboard_apply!
200
201
  end
201
202
 
202
203
  # Re-applies hooks, templates, and audit after upgrading Carson.
@@ -204,29 +205,52 @@ module Carson
204
205
  fingerprint_status = block_if_outsider_fingerprints!
205
206
  return fingerprint_status unless fingerprint_status.nil?
206
207
 
207
- print_header "Refresh"
208
208
  unless inside_git_work_tree?
209
209
  puts_line "ERROR: #{repo_root} is not a git repository."
210
210
  return EXIT_ERROR
211
211
  end
212
- 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! }
213
233
  return hook_status unless hook_status == EXIT_OK
234
+ puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
214
235
 
215
- template_status = template_apply!
236
+ template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
237
+ template_status = with_captured_output { template_apply! }
216
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
217
244
 
218
245
  audit_status = audit!
219
- if audit_status == EXIT_OK
220
- puts_line "OK: Carson refresh completed for #{repo_root}."
221
- elsif audit_status == EXIT_BLOCK
222
- puts_line "BLOCK: Carson refresh completed with policy blocks; resolve and rerun carson audit."
223
- end
246
+ puts_line "Refresh complete."
224
247
  audit_status
225
248
  end
226
249
 
227
250
  # Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
228
251
  def offboard!
229
- print_header "Offboard"
252
+ puts_verbose ""
253
+ puts_verbose "[Offboard]"
230
254
  unless inside_git_work_tree?
231
255
  puts_line "ERROR: #{repo_root} is not a git repository."
232
256
  return EXIT_ERROR
@@ -240,16 +264,20 @@ module Carson
240
264
  absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" )
241
265
  if File.exist?( absolute )
242
266
  FileUtils.rm_rf( absolute )
243
- puts_line "removed_path: #{relative}"
267
+ puts_verbose "removed_path: #{relative}"
244
268
  removed_count += 1
245
269
  else
246
- puts_line "skip_missing_path: #{relative}"
270
+ puts_verbose "skip_missing_path: #{relative}"
247
271
  missing_count += 1
248
272
  end
249
273
  end
250
274
  remove_empty_offboard_directories!
251
- puts_line "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
252
- 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
253
281
  EXIT_OK
254
282
  end
255
283
 
@@ -258,9 +286,13 @@ module Carson
258
286
  fingerprint_status = block_if_outsider_fingerprints!
259
287
  return fingerprint_status unless fingerprint_status.nil?
260
288
 
261
- print_header "Inspect"
289
+ puts_verbose ""
290
+ puts_verbose "[Inspect]"
262
291
  ok = hooks_health_report( strict: true )
263
- 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
264
296
  ok ? EXIT_OK : EXIT_BLOCK
265
297
  end
266
298
 
@@ -269,14 +301,24 @@ module Carson
269
301
  fingerprint_status = block_if_outsider_fingerprints!
270
302
  return fingerprint_status unless fingerprint_status.nil?
271
303
 
272
- print_header "Template Sync Check"
304
+ puts_verbose ""
305
+ puts_verbose "[Template Sync Check]"
273
306
  results = template_results
274
307
  drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
275
308
  error_count = results.count { |entry| entry.fetch( :status ) == "error" }
276
309
  results.each do |entry|
277
- 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
278
321
  end
279
- puts_line "template_summary: total=#{results.count} drift=#{drift_count} error=#{error_count}"
280
322
  return EXIT_ERROR if error_count.positive?
281
323
 
282
324
  drift_count.positive? ? EXIT_BLOCK : EXIT_OK
@@ -287,29 +329,37 @@ module Carson
287
329
  fingerprint_status = block_if_outsider_fingerprints!
288
330
  return fingerprint_status unless fingerprint_status.nil?
289
331
 
290
- print_header "Template Sync Apply"
332
+ puts_verbose ""
333
+ puts_verbose "[Template Sync Apply]"
291
334
  results = template_results
292
335
  applied = 0
293
336
  results.each do |entry|
294
337
  if entry.fetch( :status ) == "error"
295
- puts_line "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
338
+ puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
296
339
  next
297
340
  end
298
341
 
299
342
  file_path = File.join( repo_root, entry.fetch( :file ) )
300
343
  if entry.fetch( :status ) == "ok"
301
- puts_line "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
344
+ puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
302
345
  next
303
346
  end
304
347
 
305
348
  FileUtils.mkdir_p( File.dirname( file_path ) )
306
349
  File.write( file_path, entry.fetch( :applied_content ) )
307
- puts_line "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
350
+ puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
308
351
  applied += 1
309
352
  end
310
353
 
311
354
  error_count = results.count { |entry| entry.fetch( :status ) == "error" }
312
- puts_line "template_apply_summary: updated=#{applied} error=#{error_count}"
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
313
363
  error_count.positive? ? EXIT_ERROR : EXIT_OK
314
364
  end
315
365
 
@@ -370,9 +420,9 @@ module Carson
370
420
  def print_hooks_path_status( configured:, expected: )
371
421
  configured_abs = configured.nil? ? nil : File.expand_path( configured )
372
422
  hooks_path_ok = configured_abs == expected
373
- puts_line "hooks_path: #{configured || '(unset)'}"
374
- puts_line "hooks_path_expected: #{expected}"
375
- 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" )
376
426
  hooks_path_ok
377
427
  end
378
428
 
@@ -381,7 +431,7 @@ module Carson
381
431
  exists = File.file?( path )
382
432
  symlink = File.symlink?( path )
383
433
  executable = exists && !symlink && File.executable?( path )
384
- 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}"
385
435
  end
386
436
  end
387
437
 
@@ -406,13 +456,13 @@ module Carson
406
456
  if strict && !hooks_path_ok
407
457
  configured_text = configured.to_s.strip
408
458
  if configured_text.empty?
409
- puts_line "ACTION: hooks path is unset (expected=#{expected})."
459
+ puts_verbose "ACTION: hooks path is unset (expected=#{expected})."
410
460
  else
411
- puts_line "ACTION: hooks path mismatch (configured=#{configured_text}, expected=#{expected})."
461
+ puts_verbose "ACTION: hooks path mismatch (configured=#{configured_text}, expected=#{expected})."
412
462
  end
413
463
  end
414
464
  message = strict ? "ACTION: run carson prepare to align hooks with Carson #{Carson::VERSION}." : "ACTION: run carson prepare to enforce local main protections."
415
- puts_line message
465
+ puts_verbose message
416
466
  end
417
467
 
418
468
  # Returns ahead/behind counts for local main versus configured remote main.
@@ -611,17 +661,17 @@ module Carson
611
661
  def disable_carson_hooks_path!
612
662
  configured = configured_hooks_path
613
663
  if configured.nil?
614
- puts_line "hooks_path: (unset)"
664
+ puts_verbose "hooks_path: (unset)"
615
665
  return EXIT_OK
616
666
  end
617
- puts_line "hooks_path: #{configured}"
667
+ puts_verbose "hooks_path: #{configured}"
618
668
  configured_abs = File.expand_path( configured, repo_root )
619
669
  unless carson_managed_hooks_path?( configured_abs: configured_abs )
620
- puts_line "hooks_path_kept: #{configured} (not Carson-managed)"
670
+ puts_verbose "hooks_path_kept: #{configured} (not Carson-managed)"
621
671
  return EXIT_OK
622
672
  end
623
673
  git_system!( "config", "--unset", "core.hooksPath" )
624
- puts_line "hooks_path_unset: core.hooksPath"
674
+ puts_verbose "hooks_path_unset: core.hooksPath"
625
675
  EXIT_OK
626
676
  rescue StandardError => e
627
677
  puts_line "ERROR: unable to update core.hooksPath (#{e.message})"
@@ -667,25 +717,77 @@ module Carson
667
717
  next unless Dir.empty?( absolute )
668
718
 
669
719
  Dir.rmdir( absolute )
670
- puts_line "removed_empty_dir: #{relative}"
720
+ puts_verbose "removed_empty_dir: #{relative}"
671
721
  end
672
722
  end
673
723
 
674
724
  # Verifies configured remote exists and logs status without mutating remotes.
675
725
  def report_detected_remote!
676
726
  if git_remote_exists?( remote_name: config.git_remote )
677
- puts_line "remote_ok: #{config.git_remote}"
727
+ puts_verbose "remote_ok: #{config.git_remote}"
678
728
  else
679
729
  puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
680
730
  end
681
731
  end
682
732
 
683
- def print_onboarding_guidance
684
- puts_line ""
685
- puts_line "Carson is ready. Current workflow: #{config.workflow_style}"
733
+ # Concise onboard orchestration: hooks, templates, remote, audit, guidance.
734
+ def onboard_apply!
735
+ hook_status = with_captured_output { prepare! }
736
+ return hook_status unless hook_status == EXIT_OK
737
+ puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
738
+
739
+ template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
740
+ template_status = with_captured_output { template_apply! }
741
+ return template_status unless template_status == EXIT_OK
742
+ if template_drift_count.positive?
743
+ puts_line "Templates synced (#{template_drift_count} file#{plural_suffix( count: template_drift_count )} updated)."
744
+ else
745
+ puts_line "Templates in sync."
746
+ end
747
+
748
+ onboard_report_remote!
749
+ audit_status = onboard_run_audit!
750
+
686
751
  puts_line ""
687
- puts_line "Reconfigure anytime with: carson setup"
688
- puts_line "Run carson refresh after changing config."
752
+ puts_line "Carson is ready. Workflow: #{config.workflow_style}"
753
+ puts_line "Reconfigure anytime: carson setup"
754
+ audit_status
755
+ end
756
+
757
+ # Friendly remote status for onboard output.
758
+ def onboard_report_remote!
759
+ if git_remote_exists?( remote_name: config.git_remote )
760
+ puts_line "Remote: #{config.git_remote} (connected)."
761
+ else
762
+ puts_line "Remote not configured yet — carson setup will walk you through it."
763
+ end
764
+ end
765
+
766
+ # Runs audit with captured output; reports summary instead of full detail.
767
+ def onboard_run_audit!
768
+ audit_error = nil
769
+ audit_status = with_captured_output { audit! }
770
+ rescue StandardError => e
771
+ audit_error = e
772
+ audit_status = EXIT_OK
773
+ ensure
774
+ return onboard_print_audit_result( status: audit_status, error: audit_error )
775
+ end
776
+
777
+ def onboard_print_audit_result( status:, error: )
778
+ if error
779
+ if error.message.to_s.match?( /HEAD|rev-parse/ )
780
+ puts_line "No commits yet — run carson audit after your first commit."
781
+ else
782
+ puts_line "Audit skipped — run carson audit for details."
783
+ end
784
+ return EXIT_OK
785
+ end
786
+
787
+ if status == EXIT_BLOCK
788
+ puts_line "Some checks need attention — run carson audit for details."
789
+ end
790
+ status
689
791
  end
690
792
 
691
793
  # Uses `git remote get-url` as existence check to avoid parsing remote lists.
@@ -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
@@ -4,6 +4,7 @@
4
4
  require "fileutils"
5
5
  require "json"
6
6
  require "open3"
7
+ require "stringio"
7
8
  require "time"
8
9
 
9
10
  module Carson
@@ -22,12 +23,13 @@ module Carson
22
23
  DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
23
24
 
24
25
  # Runtime wiring for repository context, tool paths, and output streams.
25
- 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 )
26
27
  @repo_root = repo_root
27
28
  @tool_root = tool_root
28
29
  @out = out
29
30
  @err = err
30
31
  @in = in_stream
32
+ @verbose = verbose
31
33
  @config = Config.load( repo_root: repo_root )
32
34
  @git_adapter = Adapters::Git.new( repo_root: repo_root )
33
35
  @github_adapter = Adapters::GitHub.new( repo_root: repo_root )
@@ -37,6 +39,27 @@ module Carson
37
39
 
38
40
  attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
39
41
 
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?
50
+ end
51
+
52
+ # Runs a block with all output captured (suppressed from the user).
53
+ # Returns the block's return value; output is silently discarded.
54
+ def with_captured_output
55
+ saved_out, saved_err = @out, @err
56
+ @out = StringIO.new
57
+ @err = StringIO.new
58
+ yield
59
+ ensure
60
+ @out, @err = saved_out, saved_err
61
+ end
62
+
40
63
  # Current local branch name.
41
64
  def current_branch
42
65
  git_capture!( "rev-parse", "--abbrev-ref", "HEAD" ).strip
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.0
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang