carson 2.14.2 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce8a2ea7975761b31c6b5426d502a55dc106d375b4bd9f57c2d680674bf4e615
4
- data.tar.gz: 0762d2fe10827f145729fe397e2f483bf22b2973279cbf3cf5b518658c75654c
3
+ metadata.gz: d3142430a752f707dc361e222db4581ad007baf32da8e041fc2f4c83475fe26a
4
+ data.tar.gz: aaefa0ac48072fa8ef9427b79617dac2f82492828b596bb8f9755c2c71c093f7
5
5
  SHA512:
6
- metadata.gz: 9fd16b82c3766655488285b27bc93886b04cd1b6fe62de40c272ba61b44e6e017f2f5322ba2718246b31f7fdb82e3586903e8b2efd99df2ec0be96a4a616e32d
7
- data.tar.gz: c3520519edd34d95326d8fb33b6ff5d2cf582c57fab1626d1f71107b5049f978d101436983f4684e9cebb391d0df393d3d9e23cd7e6961869766b0dabea75952
6
+ metadata.gz: 8ccd8f2cf691b6d44bd6435d49db141cf01ee4dbf93f1c8b7c37b63ac82ce923ef72c10455118628d2431fa312a344530822bc2d5e4aaecdc89b3487ec34e26e
7
+ data.tar.gz: a24f069a42dfb80d938aef2e1cb2e397180d59b52b658d80cb47824d26151b06d62dc6904b95e004d3a9440bacb0c9f76a162cea78d406335ae9e5b874bca4e1
data/API.md CHANGED
@@ -56,6 +56,7 @@ carson <command> [subcommand] [arguments]
56
56
  |---|---|
57
57
  | `carson version` | Print installed Carson version. |
58
58
  | `carson inspect` | Verify Carson-managed hook installation and repository setup. |
59
+ | `carson check` | Report required CI check status for the current branch's open PR. Exits 0 for passing or pending; exits 2 for failing. Never exits 8. |
59
60
 
60
61
  ## Exit status contract
61
62
 
@@ -118,7 +119,7 @@ Environment overrides:
118
119
  },
119
120
  "check_wait": 30,
120
121
  "merge": {
121
- "authority": false,
122
+ "authority": true,
122
123
  "method": "squash"
123
124
  }
124
125
  }
@@ -130,7 +131,7 @@ Environment overrides:
130
131
  - `agent.provider`: `"auto"`, `"codex"`, or `"claude"`.
131
132
  - `agent.codex` / `agent.claude`: provider-specific options (reserved).
132
133
  - `check_wait`: seconds to wait for CI checks before classifying (default: `30`).
133
- - `merge.authority`: `false` (default) — Carson does not merge until explicitly enabled.
134
+ - `merge.authority`: `true` (default) — Carson may merge autonomously. Set to `false` to require explicit enablement.
134
135
  - `merge.method`: `"squash"` (default), `"merge"`, or `"rebase"`.
135
136
 
136
137
  `lint` schema:
data/RELEASE.md CHANGED
@@ -5,6 +5,46 @@ 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.15.1 — Codex Review Fixes
9
+
10
+ ### What changed
11
+
12
+ - `carson check` now correctly exits 2 for cancelled, errored, or timed-out CI checks. Previously, only `fail`-bucketed checks were treated as failing — all other non-passing states (cancelled, error) fell through to "all passing".
13
+ - Pre-push auto-commit now aborts the in-flight push and prints "Push again to include them." Previously the commit was created locally but not included in the push that triggered it.
14
+ - `--push-prep` now stages and commits untracked managed files. Previously, new managed files introduced by a gem upgrade were silently omitted.
15
+ - `API.md`: `merge.authority` default corrected from `false` to `true` to match the implementation.
16
+ - `API.md`: `carson check` added to the Info commands table.
17
+ - `docs/develop.md`: Architecture rationale updated — `govern.rb` acknowledged as a known exception to the adapter shell-out rule.
18
+ - Test coverage added for `check` bucket classification, `managed_dirty_paths` untracked handling, and CLI dispatch.
19
+
20
+ ### No migration required
21
+
22
+ No configuration or workflow changes needed.
23
+
24
+ ## 2.15.0 — JIT Auto-Commit on Pre-Push + `carson check`
25
+
26
+ ### What changed
27
+
28
+ - Pre-push hook now calls `carson template apply --push-prep` automatically. Any uncommitted changes to Carson-managed template files or `.github/linters/` are committed before the push reaches GitHub — no manual `git add` / `git commit` needed after a gem upgrade or `lint policy` run.
29
+ - `--push-prep` flag scopes the behaviour to pre-push only; interactive `carson template apply` is unchanged.
30
+ - `carson check` command added: wraps `gh pr checks --required`, exits 0 for pending or passing and 2 for failing. Useful for callers that need a clean CI status signal without `gh`'s confusing "Error: Exit code 8" for pending runs.
31
+ - CI smoke tests guarded against live audit exit codes with `|| true` so a pending or failing CI run on the default branch does not cause false test failures.
32
+
33
+ ### No migration required
34
+
35
+ Run `bash install.sh` to pick up the updated pre-push hook in all governed repos.
36
+
37
+ ## 2.14.2 — Docs Enrichment
38
+
39
+ ### What changed
40
+
41
+ - `docs/design.md` enriched with signal system, output design, prompt principles, and vocabulary guide.
42
+ - `docs/develop.md` enriched with architecture rationale, new-command walkthrough, and testing approach.
43
+
44
+ ### No migration required
45
+
46
+ Documentation only. No behavioural changes.
47
+
8
48
  ## 2.14.1 — Auto-Refresh on Install
9
49
 
10
50
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.14.2
1
+ 2.15.1
data/hooks/pre-push CHANGED
@@ -7,6 +7,7 @@ style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
7
7
 
8
8
  remote_name="${1:-unknown}"
9
9
  remote_url="${2:-unknown}"
10
+ has_commit_push=false
10
11
  while read -r local_ref local_sha remote_ref remote_sha; do
11
12
  case "$remote_ref" in
12
13
  refs/heads/main|refs/heads/master)
@@ -14,4 +15,13 @@ while read -r local_ref local_sha remote_ref remote_sha; do
14
15
  exit 1
15
16
  ;;
16
17
  esac
18
+ [[ "$local_sha" != "0000000000000000000000000000000000000000" ]] && has_commit_push=true
17
19
  done
20
+
21
+ if $has_commit_push; then
22
+ if [[ -n "${CARSON_BIN:-}" ]]; then
23
+ ruby "$CARSON_BIN" template apply --push-prep || exit 1
24
+ else
25
+ carson template apply --push-prep || exit 1
26
+ fi
27
+ fi
data/lib/carson/cli.rb CHANGED
@@ -53,7 +53,7 @@ module Carson
53
53
 
54
54
  def self.build_parser
55
55
  OptionParser.new do |opts|
56
- opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|lint policy --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
56
+ opts.banner = "Usage: carson [setup|audit|check|sync|prune|prepare|inspect|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|lint policy --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
57
57
  end
58
58
  end
59
59
 
@@ -79,7 +79,7 @@ module Carson
79
79
  when "refresh"
80
80
  parse_refresh_command( argv: argv, parser: parser, err: err )
81
81
  when "template"
82
- parse_named_subcommand( command: command, usage: "check|apply", argv: argv, parser: parser, err: err )
82
+ parse_template_subcommand( argv: argv, parser: parser, err: err )
83
83
  when "lint"
84
84
  parse_lint_subcommand( argv: argv, parser: parser, err: err )
85
85
  when "review"
@@ -143,6 +143,35 @@ module Carson
143
143
  { command: "#{command}:#{action}" }
144
144
  end
145
145
 
146
+ def self.parse_template_subcommand( argv:, parser:, err: )
147
+ action = argv.shift
148
+ if action.to_s.strip.empty?
149
+ err.puts "#{BADGE} Missing subcommand for template. Use: carson template check|apply"
150
+ err.puts parser
151
+ return { command: :invalid }
152
+ end
153
+
154
+ return { command: "template:#{action}" } unless action == "apply"
155
+
156
+ options = { push_prep: false }
157
+ apply_parser = OptionParser.new do |opts|
158
+ opts.banner = "Usage: carson template apply [--push-prep]"
159
+ opts.on( "--push-prep", "Apply templates and auto-commit any managed file changes (used by pre-push hook)" ) do
160
+ options[ :push_prep ] = true
161
+ end
162
+ end
163
+ apply_parser.parse!( argv )
164
+ unless argv.empty?
165
+ err.puts "#{BADGE} Unexpected arguments for template apply: #{argv.join( ' ' )}"
166
+ err.puts apply_parser
167
+ return { command: :invalid }
168
+ end
169
+ { command: "template:apply", push_prep: options.fetch( :push_prep ) }
170
+ rescue OptionParser::ParseError => e
171
+ err.puts "#{BADGE} #{e.message}"
172
+ { command: :invalid }
173
+ end
174
+
146
175
  def self.parse_lint_subcommand( argv:, parser:, err: )
147
176
  action = argv.shift
148
177
  unless action == "policy"
@@ -235,6 +264,8 @@ module Carson
235
264
  runtime.prepare!
236
265
  when "inspect"
237
266
  runtime.inspect!
267
+ when "check"
268
+ runtime.check!
238
269
  when "onboard"
239
270
  runtime.onboard!
240
271
  when "refresh"
@@ -246,7 +277,7 @@ module Carson
246
277
  when "template:check"
247
278
  runtime.template_check!
248
279
  when "template:apply"
249
- runtime.template_apply!
280
+ runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
250
281
  when "lint:setup"
251
282
  runtime.lint_setup!(
252
283
  source: parsed.fetch( :source ),
@@ -74,6 +74,60 @@ module Carson
74
74
  audit_state == "block" ? EXIT_BLOCK : EXIT_OK
75
75
  end
76
76
 
77
+ # Thin focused command: show required-check status for the current branch's open PR.
78
+ # Always exits 0 for pending or passing so callers never see a false "Error: Exit code 8".
79
+ def check!
80
+ unless gh_available?
81
+ puts_line "Checks: gh CLI not available."
82
+ return EXIT_ERROR
83
+ end
84
+
85
+ pr_stdout, pr_stderr, pr_success, = gh_run(
86
+ "pr", "view", current_branch,
87
+ "--json", "number,title,url"
88
+ )
89
+ unless pr_success
90
+ error_text = gh_error_text( stdout_text: pr_stdout, stderr_text: pr_stderr, fallback: "no open PR for branch #{current_branch}" )
91
+ puts_line "Checks: #{error_text}."
92
+ return EXIT_ERROR
93
+ end
94
+ pr_data = JSON.parse( pr_stdout )
95
+ pr_number = pr_data[ "number" ].to_s
96
+
97
+ checks_stdout, checks_stderr, checks_success, checks_exit = gh_run(
98
+ "pr", "checks", pr_number, "--required", "--json", "name,state,bucket,workflow,link"
99
+ )
100
+ if checks_stdout.to_s.strip.empty?
101
+ error_text = gh_error_text( stdout_text: checks_stdout, stderr_text: checks_stderr, fallback: "required checks unavailable" )
102
+ puts_line "Checks: #{error_text}."
103
+ return EXIT_ERROR
104
+ end
105
+
106
+ checks_data = JSON.parse( checks_stdout )
107
+ pending = checks_data.select { |e| e[ "bucket" ].to_s == "pending" }
108
+ failing = checks_data.select { |e| check_entry_failing?( entry: e ) }
109
+ total = checks_data.count
110
+ # gh exits 8 when required checks are still pending (not a failure).
111
+ is_pending = !checks_success && checks_exit == 8
112
+
113
+ if failing.any?
114
+ puts_line "Checks: FAIL (#{failing.count} of #{total} failing)."
115
+ normalise_check_entries( entries: failing ).each { |e| puts_line " #{e.fetch( :workflow )} / #{e.fetch( :name )} #{e.fetch( :link )}".strip }
116
+ return EXIT_BLOCK
117
+ end
118
+
119
+ if is_pending || pending.any?
120
+ puts_line "Checks: pending (#{total - pending.count} of #{total} complete)."
121
+ return EXIT_OK
122
+ end
123
+
124
+ puts_line "Checks: all passing (#{total} required)."
125
+ EXIT_OK
126
+ rescue JSON::ParserError => e
127
+ puts_line "Checks: invalid gh response (#{e.message})."
128
+ EXIT_ERROR
129
+ end
130
+
77
131
  private
78
132
  def pr_and_check_report
79
133
  report = {
@@ -127,8 +181,8 @@ module Carson
127
181
  return report
128
182
  end
129
183
  checks_data = JSON.parse( checks_stdout )
130
- failing = checks_data.select { |entry| entry[ "bucket" ].to_s == "fail" || entry[ "state" ].to_s.upcase == "FAILURE" }
131
184
  pending = checks_data.select { |entry| entry[ "bucket" ].to_s == "pending" }
185
+ failing = checks_data.select { |entry| check_entry_failing?( entry: entry ) }
132
186
  report[ :checks ][ :status ] = checks_success ? "ok" : ( checks_exit == 8 ? "pending" : "attention" )
133
187
  report[ :checks ][ :required_total ] = checks_data.count
134
188
  report[ :checks ][ :failing_count ] = failing.count
@@ -307,6 +361,12 @@ module Carson
307
361
  [ critical, advisory ]
308
362
  end
309
363
 
364
+ # Returns true when a required-check entry is in a non-passing, non-pending state.
365
+ # Cancelled, errored, timed-out, and any unknown bucket all count as failing.
366
+ def check_entry_failing?( entry: )
367
+ !%w[pass pending].include?( entry[ "bucket" ].to_s )
368
+ end
369
+
310
370
  # Failing means completed with a non-successful conclusion.
311
371
  def default_branch_check_run_failing?( entry: )
312
372
  status = entry[ "status" ].to_s.strip.downcase
@@ -367,7 +367,7 @@ module Carson
367
367
 
368
368
  # Applies managed template files as full-file writes from Carson sources.
369
369
  # Also removes superseded files that are no longer part of the managed set.
370
- def template_apply!
370
+ def template_apply!( push_prep: false )
371
371
  fingerprint_status = block_if_outsider_fingerprints!
372
372
  return fingerprint_status unless fingerprint_status.nil?
373
373
 
@@ -414,7 +414,10 @@ module Carson
414
414
  puts_line "Templates in sync."
415
415
  end
416
416
  end
417
- error_count.positive? ? EXIT_ERROR : EXIT_OK
417
+ return EXIT_ERROR if error_count.positive?
418
+
419
+ return EXIT_BLOCK if push_prep && push_prep_commit!
420
+ EXIT_OK
418
421
  end
419
422
 
420
423
  private
@@ -742,6 +745,33 @@ module Carson
742
745
  git_capture!( "status", "--porcelain" ).strip.empty?
743
746
  end
744
747
 
748
+ def push_prep_commit!
749
+ # JIT auto-commit is for feature branches only; main is protected from direct commits.
750
+ return if current_branch == config.main_branch
751
+
752
+ dirty = managed_dirty_paths
753
+ return if dirty.empty?
754
+
755
+ git_system!( "add", *dirty )
756
+ git_system!( "commit", "-m", "chore: sync Carson managed files" )
757
+ puts_line "Carson committed managed file updates. Push again to include them."
758
+ true
759
+ end
760
+
761
+ def managed_dirty_paths
762
+ template_paths = config.template_managed_files + config.template_superseded_files
763
+ linters_glob = Dir.glob( File.join( repo_root, ".github/linters/**/*" ) )
764
+ .select { |p| File.file?( p ) }
765
+ .map { |p| p.delete_prefix( "#{repo_root}/" ) }
766
+ candidates = ( template_paths + linters_glob ).uniq
767
+ return [] if candidates.empty?
768
+
769
+ stdout_text, = git_capture_soft( "status", "--porcelain", "--", *candidates )
770
+ stdout_text.to_s.lines
771
+ .map { |l| l[ 3.. ].strip }
772
+ .reject( &:empty? )
773
+ end
774
+
745
775
  def inside_git_work_tree?
746
776
  stdout_text, = git_capture_soft( "rev-parse", "--is-inside-work-tree" )
747
777
  stdout_text.to_s.strip == "true"
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.14.2
4
+ version: 2.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang