carson 3.19.0 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -3
  3. data/RELEASE.md +25 -0
  4. data/VERSION +1 -1
  5. data/exe/carson +3 -3
  6. data/hooks/command-guard +56 -0
  7. data/hooks/pre-push +37 -1
  8. data/lib/carson/adapters/agent.rb +1 -0
  9. data/lib/carson/adapters/claude.rb +2 -0
  10. data/lib/carson/adapters/codex.rb +2 -0
  11. data/lib/carson/adapters/git.rb +2 -0
  12. data/lib/carson/adapters/github.rb +2 -0
  13. data/lib/carson/adapters/prompt.rb +2 -0
  14. data/lib/carson/cli.rb +415 -414
  15. data/lib/carson/config.rb +4 -3
  16. data/lib/carson/runtime/audit.rb +84 -84
  17. data/lib/carson/runtime/deliver.rb +27 -24
  18. data/lib/carson/runtime/govern.rb +29 -29
  19. data/lib/carson/runtime/housekeep.rb +15 -15
  20. data/lib/carson/runtime/local/hooks.rb +20 -0
  21. data/lib/carson/runtime/local/onboard.rb +17 -17
  22. data/lib/carson/runtime/local/prune.rb +13 -13
  23. data/lib/carson/runtime/local/sync.rb +6 -6
  24. data/lib/carson/runtime/local/template.rb +26 -25
  25. data/lib/carson/runtime/local/worktree.rb +76 -33
  26. data/lib/carson/runtime/local.rb +1 -0
  27. data/lib/carson/runtime/repos.rb +1 -1
  28. data/lib/carson/runtime/review/data_access.rb +1 -0
  29. data/lib/carson/runtime/review/gate_support.rb +15 -14
  30. data/lib/carson/runtime/review/query_text.rb +1 -0
  31. data/lib/carson/runtime/review/sweep_support.rb +5 -4
  32. data/lib/carson/runtime/review/utility.rb +2 -1
  33. data/lib/carson/runtime/review.rb +10 -8
  34. data/lib/carson/runtime/setup.rb +12 -10
  35. data/lib/carson/runtime/status.rb +20 -20
  36. data/lib/carson/runtime.rb +39 -25
  37. data/lib/carson/version.rb +1 -0
  38. data/lib/carson.rb +1 -0
  39. data/templates/.github/carson.md +7 -4
  40. metadata +2 -1
@@ -1,3 +1,4 @@
1
+ # GraphQL query templates for pull request review data retrieval.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -1,3 +1,4 @@
1
+ # Review sweep: late-event detection, tracking issue management, and sweep reporting.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -59,7 +60,7 @@ module Carson
59
60
  )
60
61
  end
61
62
 
62
- Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) }.each do |comment|
63
+ Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) }.each do |comment|
63
64
  next if comment.fetch( :author ) == pr_author
64
65
  next if bot_username?( author: comment.fetch( :author ) )
65
66
  hits = matched_risk_keywords( text: comment.fetch( :body ) )
@@ -152,7 +153,7 @@ module Carson
152
153
  stdout_text, stderr_text, success, = gh_run( "issue", "list", "--repo", repo_slug, "--state", "all", "--limit", "100", "--json", "number,title,state,url,labels" )
153
154
  raise gh_error_text( stdout_text: stdout_text, stderr_text: stderr_text, fallback: "unable to list issues for review sweep" ) unless success
154
155
  issues = Array( JSON.parse( stdout_text ) )
155
- node = issues.find { |entry| entry[ "title" ].to_s == config.review_tracking_issue_title }
156
+ node = issues.find { it[ "title" ].to_s == config.review_tracking_issue_title }
156
157
  return nil if node.nil?
157
158
  {
158
159
  number: node[ "number" ],
@@ -207,8 +208,8 @@ module Carson
207
208
  )
208
209
  puts_verbose "review_sweep_report_markdown: #{markdown_path}"
209
210
  puts_verbose "review_sweep_report_json: #{json_path}"
210
- rescue StandardError => e
211
- puts_verbose "review_sweep_report_write: SKIP (#{e.message})"
211
+ rescue StandardError => exception
212
+ puts_verbose "review_sweep_report_write: SKIP (#{exception.message})"
212
213
  end
213
214
 
214
215
  # Human-readable scheduled sweep report.
@@ -1,3 +1,4 @@
1
+ # Review utilities: risk keyword matching, disposition parsing, URL extraction, and deduplication.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -24,7 +25,7 @@ module Carson
24
25
 
25
26
  # GitHub URL extraction for mapping disposition acknowledgements to finding URLs.
26
27
  def extract_github_urls( text: )
27
- text.to_s.scan( %r{https://github\.com/[^\s\)\]]+} ).map { |value| value.sub( /[.,;:]+$/, "" ) }.uniq
28
+ text.to_s.scan( %r{https://github\.com/[^\s\)\]]+} ).map { it.sub( /[.,;:]+$/, "" ) }.uniq
28
29
  end
29
30
 
30
31
  # Parse RFC3339 timestamps and return nil on blank/invalid values.
@@ -1,3 +1,4 @@
1
+ # Implements the review gate (merge readiness) and sweep (late activity scan) workflows.
1
2
  require_relative "review/query_text"
2
3
  require_relative "review/data_access"
3
4
  require_relative "review/gate_support"
@@ -6,6 +7,7 @@ require_relative "review/utility"
6
7
 
7
8
  module Carson
8
9
  class Runtime
10
+ # PR review gate and sweep workflow.
9
11
  module Review
10
12
  include QueryText
11
13
  include DataAccess
@@ -123,11 +125,11 @@ module Carson
123
125
  end
124
126
  block_reasons.each { |reason| puts_line "BLOCK: #{reason}" }
125
127
  EXIT_BLOCK
126
- rescue JSON::ParserError => e
127
- puts_line "ERROR: invalid gh JSON response (#{e.message})."
128
+ rescue JSON::ParserError => exception
129
+ puts_line "ERROR: invalid gh JSON response (#{exception.message})."
128
130
  EXIT_ERROR
129
- rescue StandardError => e
130
- puts_line "ERROR: #{e.message}"
131
+ rescue StandardError => exception
132
+ puts_line "ERROR: #{exception.message}"
131
133
  EXIT_ERROR
132
134
  end
133
135
 
@@ -176,11 +178,11 @@ module Carson
176
178
  end
177
179
  puts_line "BLOCK: actionable late review activity detected."
178
180
  EXIT_BLOCK
179
- rescue JSON::ParserError => e
180
- puts_line "ERROR: invalid gh JSON response (#{e.message})."
181
+ rescue JSON::ParserError => exception
182
+ puts_line "ERROR: invalid gh JSON response (#{exception.message})."
181
183
  EXIT_ERROR
182
- rescue StandardError => e
183
- puts_line "ERROR: #{e.message}"
184
+ rescue StandardError => exception
185
+ puts_line "ERROR: #{exception.message}"
184
186
  EXIT_ERROR
185
187
  end
186
188
  end
@@ -1,8 +1,10 @@
1
+ # Handles first-time setup, onboard, offboard, refresh, and config persistence.
1
2
  require "set"
2
3
  require "uri"
3
4
 
4
5
  module Carson
5
6
  class Runtime
7
+ # First-time setup, onboard, offboard, and refresh operations.
6
8
  module Setup
7
9
  WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
8
10
 
@@ -137,7 +139,7 @@ module Carson
137
139
  { label: "branch — enforce PR-only merges (default)", value: "branch" },
138
140
  { label: "trunk — commit directly to main", value: "trunk" }
139
141
  ]
140
- default_index = options.index { |o| o.fetch( :value ) == current } || 0
142
+ default_index = options.index { it.fetch( :value ) == current } || 0
141
143
  prompt_choice( options: options, default: default_index )
142
144
  end
143
145
 
@@ -151,7 +153,7 @@ module Carson
151
153
  { label: "rebase — linear history, individual commits", value: "rebase" },
152
154
  { label: "merge — merge commits", value: "merge" }
153
155
  ]
154
- default_index = options.index { |o| o.fetch( :value ) == current } || 0
156
+ default_index = options.index { it.fetch( :value ) == current } || 0
155
157
  prompt_choice( options: options, default: default_index )
156
158
  end
157
159
 
@@ -173,8 +175,8 @@ module Carson
173
175
  options.each_with_index do |option, index|
174
176
  puts_line " #{index + 1}) #{option.fetch( :label )}"
175
177
  end
176
- out.print "#{BADGE} Choice [#{default + 1}]: "
177
- out.flush
178
+ output.print "#{BADGE} Choice [#{default + 1}]: "
179
+ output.flush
178
180
  raw = self.in.gets
179
181
  return options[ default ].fetch( :value ) if raw.nil?
180
182
 
@@ -190,8 +192,8 @@ module Carson
190
192
  end
191
193
 
192
194
  def prompt_custom_value( label: )
193
- out.print "#{BADGE} #{label}: "
194
- out.flush
195
+ output.print "#{BADGE} #{label}: "
196
+ output.flush
195
197
  raw = self.in.gets
196
198
  return nil if raw.nil?
197
199
 
@@ -219,7 +221,7 @@ module Carson
219
221
  others << entry
220
222
  end
221
223
  end
222
- well_known.sort_by { |e| WELL_KNOWN_REMOTES.index( e.fetch( :name ) ) || 999 } + others.sort_by { |e| e.fetch( :name ) }
224
+ well_known.sort_by { WELL_KNOWN_REMOTES.index( it.fetch( :name ) ) || 999 } + others.sort_by { it.fetch( :name ) }
223
225
  end
224
226
 
225
227
  # Normalises a remote URL so SSH and HTTPS variants of the same host/path compare equal.
@@ -295,7 +297,7 @@ module Carson
295
297
 
296
298
  def detect_git_remote
297
299
  remotes = list_git_remotes
298
- remote_names = remotes.map { |entry| entry.fetch( :name ) }
300
+ remote_names = remotes.map { it.fetch( :name ) }
299
301
  return nil if remote_names.empty?
300
302
 
301
303
  return config.git_remote if remote_names.include?( config.git_remote )
@@ -383,8 +385,8 @@ module Carson
383
385
  # Reusable Y/n prompt following existing prompt_choice conventions.
384
386
  def prompt_yes_no( default: true )
385
387
  hint = default ? "Y/n" : "y/N"
386
- out.print "#{BADGE} [#{hint}]: "
387
- out.flush
388
+ output.print "#{BADGE} [#{hint}]: "
389
+ output.flush
388
390
  raw = self.in.gets
389
391
  return default if raw.nil?
390
392
 
@@ -9,7 +9,7 @@ module Carson
9
9
  data = gather_status
10
10
 
11
11
  if json_output
12
- out.puts JSON.pretty_generate( data )
12
+ output.puts JSON.pretty_generate( data )
13
13
  else
14
14
  print_status( data: data )
15
15
  end
@@ -35,14 +35,14 @@ module Carson
35
35
  next
36
36
  end
37
37
  begin
38
- rt = build_scoped_runtime( repo_path: repo_path )
39
- data = rt.send( :gather_status )
38
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
39
+ data = scoped_runtime.send( :gather_status )
40
40
  results << { name: repo_name, status: "ok" }.merge( data )
41
- rescue StandardError => e
42
- results << { name: repo_name, status: "error", error: e.message }
41
+ rescue StandardError => exception
42
+ results << { name: repo_name, status: "error", error: exception.message }
43
43
  end
44
44
  end
45
- out.puts JSON.pretty_generate( { command: "status", repos: results } )
45
+ output.puts JSON.pretty_generate( { command: "status", repos: results } )
46
46
  return EXIT_OK
47
47
  end
48
48
 
@@ -58,8 +58,8 @@ module Carson
58
58
  end
59
59
 
60
60
  begin
61
- rt = build_scoped_runtime( repo_path: repo_path )
62
- data = rt.send( :gather_status )
61
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
62
+ data = scoped_runtime.send( :gather_status )
63
63
  branch = data.fetch( :branch )
64
64
  dirty = branch.fetch( :dirty ) ? " (dirty)" : ""
65
65
  worktrees = data.fetch( :worktrees )
@@ -72,9 +72,9 @@ module Carson
72
72
 
73
73
  # Show pending operations for this repo.
74
74
  repo_pending = status_pending_for_repo( all_pending: all_pending, repo_path: repo_path )
75
- repo_pending.each { |desc| puts_line " pending: #{desc}" }
76
- rescue StandardError => e
77
- puts_line "#{repo_name}: FAIL (#{e.message})"
75
+ repo_pending.each { |description| puts_line " pending: #{description}" }
76
+ rescue StandardError => exception
77
+ puts_line "#{repo_name}: FAIL (#{exception.message})"
78
78
  end
79
79
  end
80
80
 
@@ -159,14 +159,14 @@ module Carson
159
159
  def gather_worktree_info
160
160
  entries = worktree_list
161
161
 
162
- # Filter out the main worktree (the repository root itself).
162
+ # Filter output the main worktree (the repository root itself).
163
163
  # Use realpath for comparison — git returns canonical paths that may differ from repo_root.
164
164
  canonical_root = realpath_safe( repo_root )
165
- entries.reject { it.fetch( :path ) == canonical_root }.map do |wt|
165
+ entries.reject { it.fetch( :path ) == canonical_root }.map do |worktree|
166
166
  {
167
- path: wt.fetch( :path ),
168
- name: File.basename( wt.fetch( :path ) ),
169
- branch: wt.fetch( :branch, nil )
167
+ path: worktree.fetch( :path ),
168
+ name: File.basename( worktree.fetch( :path ) ),
169
+ branch: worktree.fetch( :branch, nil )
170
170
  }
171
171
  end
172
172
  end
@@ -222,7 +222,7 @@ module Carson
222
222
  stdout, _, success, = git_run( "branch", "-vv" )
223
223
  return { count: 0 } unless success
224
224
 
225
- gone_branches = stdout.lines.select { |l| l.include?( ": gone]" ) }
225
+ gone_branches = stdout.lines.select { |line| line.include?( ": gone]" ) }
226
226
  { count: gone_branches.size }
227
227
  end
228
228
 
@@ -252,9 +252,9 @@ module Carson
252
252
  if worktrees.any?
253
253
  puts_line ""
254
254
  puts_line "Worktrees:"
255
- worktrees.each do |wt|
256
- branch_label = wt.fetch( :branch ) || "(detached)"
257
- puts_line " #{wt.fetch( :name )} #{branch_label}"
255
+ worktrees.each do |worktree|
256
+ branch_label = worktree.fetch( :branch ) || "(detached)"
257
+ puts_line " #{worktree.fetch( :name )} #{branch_label}"
258
258
  end
259
259
  end
260
260
 
@@ -23,11 +23,11 @@ module Carson
23
23
  DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
24
24
 
25
25
  # Runtime wiring for repository context, tool paths, and output streams.
26
- def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin, verbose: false )
26
+ def initialize( repo_root:, tool_root:, output:, error:, in_stream: $stdin, verbose: false )
27
27
  @repo_root = repo_root
28
28
  @tool_root = tool_root
29
- @out = out
30
- @err = err
29
+ @output = output
30
+ @error = error
31
31
  @in = in_stream
32
32
  @verbose = verbose
33
33
  @config = Config.load( repo_root: repo_root )
@@ -40,7 +40,7 @@ module Carson
40
40
 
41
41
  private
42
42
 
43
- attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
43
+ attr_reader :repo_root, :tool_root, :output, :error, :in, :config, :git_adapter, :github_adapter
44
44
 
45
45
  # Returns true when full diagnostic output is enabled via --verbose.
46
46
  def verbose?
@@ -55,12 +55,12 @@ module Carson
55
55
  # Runs a block with all output captured (suppressed from the user).
56
56
  # Returns the block's return value; output is silently discarded.
57
57
  def with_captured_output
58
- saved_out, saved_err = @out, @err
59
- @out = StringIO.new
60
- @err = StringIO.new
58
+ saved_output, saved_error = @output, @error
59
+ @output = StringIO.new
60
+ @error = StringIO.new
61
61
  yield
62
62
  ensure
63
- @out, @err = saved_out, saved_err
63
+ @output, @error = saved_output, saved_error
64
64
  end
65
65
 
66
66
  # Returns true when the repository has at least one commit (HEAD exists).
@@ -95,9 +95,9 @@ module Carson
95
95
  # Prefixes non-empty lines with the Carson badge (⧓).
96
96
  def puts_line( message )
97
97
  if message.to_s.strip.empty?
98
- out.puts ""
98
+ output.puts ""
99
99
  else
100
- out.puts "#{BADGE} #{message}"
100
+ output.puts "#{BADGE} #{message}"
101
101
  end
102
102
  end
103
103
 
@@ -160,6 +160,20 @@ module Carson
160
160
  text.empty? ? default : text
161
161
  end
162
162
 
163
+ # Temporarily sets an environment variable for the duration of the block.
164
+ # Restores the previous value (or deletes the key) when the block completes.
165
+ def with_env_var( key, value )
166
+ previous = ENV.key?( key ) ? ENV.fetch( key ) : nil
167
+ ENV[ key ] = value
168
+ yield
169
+ ensure
170
+ if previous.nil?
171
+ ENV.delete( key )
172
+ else
173
+ ENV[ key ] = previous
174
+ end
175
+ end
176
+
163
177
  # Chooses best available error text from gh stderr/stdout.
164
178
  def gh_error_text( stdout_text:, stderr_text:, fallback: )
165
179
  combined = [ stderr_text.to_s.strip, stdout_text.to_s.strip ].reject( &:empty? ).join( " | " )
@@ -182,8 +196,8 @@ module Carson
182
196
  # Runs git command, streams outputs, and raises on non-zero exit.
183
197
  def git_system!( *args )
184
198
  stdout_text, stderr_text, success, = git_run( *args )
185
- out.print stdout_text unless stdout_text.empty?
186
- err.print stderr_text unless stderr_text.empty?
199
+ output.print stdout_text unless stdout_text.empty?
200
+ error.print stderr_text unless stderr_text.empty?
187
201
  raise "git #{args.join( ' ' )} failed" unless success
188
202
  end
189
203
 
@@ -191,7 +205,7 @@ module Carson
191
205
  def git_capture!( *args )
192
206
  stdout_text, stderr_text, success, = git_run( *args )
193
207
  unless success
194
- err.print stderr_text unless stderr_text.empty?
208
+ error.print stderr_text unless stderr_text.empty?
195
209
  raise "git #{args.join( ' ' )} failed"
196
210
  end
197
211
  stdout_text
@@ -234,9 +248,9 @@ module Carson
234
248
  def save_batch_pending( data )
235
249
  path = batch_pending_path
236
250
  FileUtils.mkdir_p( File.dirname( path ) )
237
- tmp = "#{path}.tmp"
238
- File.write( tmp, JSON.pretty_generate( data ) )
239
- File.rename( tmp, path )
251
+ temporary_path = "#{path}.tmp"
252
+ File.write( temporary_path, JSON.pretty_generate( data ) )
253
+ File.rename( temporary_path, path )
240
254
  end
241
255
 
242
256
  # Adds or updates an entry in the pending log, incrementing attempts.
@@ -293,10 +307,10 @@ module Carson
293
307
  reasons = []
294
308
 
295
309
  # Active worktrees beyond the main working tree.
296
- rt = build_scoped_runtime( repo_path: repo_path )
297
- worktrees = rt.send( :worktree_list )
298
- main_root = rt.send( :realpath_safe, repo_path )
299
- active = worktrees.reject { |wt| wt.fetch( :path ) == main_root }
310
+ scoped_runtime = build_scoped_runtime( repo_path: repo_path )
311
+ worktrees = scoped_runtime.send( :worktree_list )
312
+ main_root = scoped_runtime.send( :realpath_safe, repo_path )
313
+ active = worktrees.reject { |worktree| worktree.fetch( :path ) == main_root }
300
314
  if active.any?
301
315
  reasons << "#{active.count} active worktree#{active.count == 1 ? '' : 's'}"
302
316
  end
@@ -308,15 +322,15 @@ module Carson
308
322
  end
309
323
 
310
324
  { safe: reasons.empty?, reasons: reasons }
311
- rescue StandardError => e
312
- { safe: false, reasons: [ e.message ] }
325
+ rescue StandardError => exception
326
+ { safe: false, reasons: [ exception.message ] }
313
327
  end
314
328
 
315
329
  # Creates a scoped Runtime for a governed repo with captured output.
316
330
  def build_scoped_runtime( repo_path: )
317
- buf = verbose? ? out : StringIO.new
318
- err_buf = verbose? ? err : StringIO.new
319
- Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? )
331
+ buffer = verbose? ? output : StringIO.new
332
+ error_buffer = verbose? ? error : StringIO.new
333
+ Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? )
320
334
  end
321
335
  end
322
336
  end
@@ -1,3 +1,4 @@
1
+ # Reads the VERSION file and exposes it as Carson::VERSION.
1
2
  module Carson
2
3
  version_path = File.expand_path( "../../VERSION", __dir__ )
3
4
  VERSION = File.file?( version_path ) ? File.read( version_path ).strip : "0.0.0"
data/lib/carson.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # Loads all Carson modules and defines the top-level namespace.
1
2
  require_relative "carson/version"
2
3
 
3
4
  module Carson
@@ -2,12 +2,14 @@
2
2
 
3
3
  This repository is governed by [Carson](https://github.com/wanghailei/carson), an autonomous governance runtime. Carson lives on the maintainer's workstation, not inside this repository.
4
4
 
5
- ## What Carson Does Not Do
6
-
7
- Carson has no `commit`, `push`, or `pr` commands. Use `git` and `gh` for those. Carson audits and governs; you execute.
8
-
9
5
  ## Commands
10
6
 
7
+ **Delivery:**
8
+ ```bash
9
+ carson deliver # push branch, create PR
10
+ carson deliver --merge # push, create PR, merge if CI green and review clear
11
+ ```
12
+
11
13
  **Before committing:**
12
14
  ```bash
13
15
  carson audit # full governance check — run before every commit
@@ -24,6 +26,7 @@ carson review gate # block until actionable review findings are resolved
24
26
  ```bash
25
27
  carson sync # fast-forward local main from remote
26
28
  carson prune # remove stale branches (safer than git branch -d on squash repos)
29
+ carson housekeep # sync + prune + sweep stale worktrees
27
30
  ```
28
31
 
29
32
  ## Exit Codes
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: 3.19.0
4
+ version: 3.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -36,6 +36,7 @@ files:
36
36
  - VERSION
37
37
  - carson.gemspec
38
38
  - exe/carson
39
+ - hooks/command-guard
39
40
  - hooks/pre-commit
40
41
  - hooks/pre-merge-commit
41
42
  - hooks/pre-push