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
data/lib/carson/cli.rb CHANGED
@@ -1,130 +1,131 @@
1
+ # Parses command-line arguments and dispatches to Runtime operations.
1
2
  require "optparse"
2
3
 
3
4
  module Carson
4
5
  class CLI
5
- def self.start( argv:, repo_root:, tool_root:, out:, err: )
6
- parsed = parse_args( argv: argv, out: out, err: err )
6
+ def self.start( arguments:, repo_root:, tool_root:, output:, error: )
7
+ parsed = parse_args( arguments: arguments, output: output, error: error )
7
8
  command = parsed.fetch( :command )
8
9
  return Runtime::EXIT_OK if command == :help
9
10
 
10
11
  if command == "version"
11
- out.puts "#{BADGE} #{Carson::VERSION}"
12
+ output.puts "#{BADGE} #{Carson::VERSION}"
12
13
  return Runtime::EXIT_OK
13
14
  end
14
15
 
15
16
  if %w[repos refresh:all prune:all housekeep:all housekeep:target template:check:all audit:all sync:all status:all].include?( command )
16
17
  verbose = parsed.fetch( :verbose, false )
17
- runtime = Runtime.new( repo_root: repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
18
+ runtime = Runtime.new( repo_root: repo_root, tool_root: tool_root, output: output, error: error, verbose: verbose )
18
19
  return dispatch( parsed: parsed, runtime: runtime )
19
20
  end
20
21
 
21
22
  target_repo_root = parsed.fetch( :repo_root, nil )
22
23
  target_repo_root = repo_root if target_repo_root.to_s.strip.empty?
23
24
  unless Dir.exist?( target_repo_root )
24
- err.puts "#{BADGE} ERROR: repository path does not exist: #{target_repo_root}"
25
+ error.puts "#{BADGE} ERROR: repository path does not exist: #{target_repo_root}"
25
26
  return Runtime::EXIT_ERROR
26
27
  end
27
28
 
28
29
  verbose = parsed.fetch( :verbose, false )
29
- runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
30
+ runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, output: output, error: error, verbose: verbose )
30
31
  dispatch( parsed: parsed, runtime: runtime )
31
- rescue ConfigError => e
32
- err.puts "#{BADGE} CONFIG ERROR: #{e.message}"
32
+ rescue ConfigError => exception
33
+ error.puts "#{BADGE} CONFIG ERROR: #{exception.message}"
33
34
  Runtime::EXIT_ERROR
34
- rescue StandardError => e
35
- err.puts "#{BADGE} ERROR: #{e.message}"
35
+ rescue StandardError => exception
36
+ error.puts "#{BADGE} ERROR: #{exception.message}"
36
37
  Runtime::EXIT_ERROR
37
38
  end
38
39
 
39
- def self.parse_args( argv:, out:, err: )
40
- verbose = argv.delete( "--verbose" ) ? true : false
40
+ def self.parse_args( arguments:, output:, error: )
41
+ verbose = arguments.delete( "--verbose" ) ? true : false
41
42
  parser = build_parser
42
- preset = parse_preset_command( argv: argv, out: out, parser: parser )
43
+ preset = parse_preset_command( arguments: arguments, output: output, parser: parser )
43
44
  return preset.merge( verbose: verbose ) unless preset.nil?
44
45
 
45
- command = argv.shift
46
- result = parse_command( command: command, argv: argv, err: err )
46
+ command = arguments.shift
47
+ result = parse_command( command: command, arguments: arguments, error: error )
47
48
  result.merge( verbose: verbose )
48
- rescue OptionParser::ParseError => e
49
- err.puts "#{BADGE} #{e.message}"
50
- err.puts parser
49
+ rescue OptionParser::ParseError => exception
50
+ error.puts "#{BADGE} #{exception.message}"
51
+ error.puts parser
51
52
  { command: :invalid }
52
53
  end
53
54
 
54
55
  def self.build_parser
55
- OptionParser.new do |opts|
56
- opts.banner = "Usage: carson <command> [options]"
57
- opts.separator ""
58
- opts.separator "Repository governance and workflow automation for coding agents."
59
- opts.separator ""
60
- opts.separator "Commands:"
61
- opts.separator " status Show repository state (branch, PRs, worktrees)"
62
- opts.separator " setup Initialise Carson configuration"
63
- opts.separator " audit Run pre-commit health checks"
64
- opts.separator " sync Sync local main with remote"
65
- opts.separator " deliver Push, create PR, and optionally merge"
66
- opts.separator " prune Remove stale local branches"
67
- opts.separator " worktree Manage isolated coding worktrees"
68
- opts.separator " housekeep Sync, reap worktrees, and prune branches"
69
- opts.separator " repos List governed repositories"
70
- opts.separator " onboard Register a repository for governance"
71
- opts.separator " offboard Remove a repository from governance"
72
- opts.separator " refresh Re-install hooks and configuration"
73
- opts.separator " template Manage canonical template files"
74
- opts.separator " review Manage PR review workflow"
75
- opts.separator " govern Portfolio-level PR triage loop"
76
- opts.separator " version Show Carson version"
77
- opts.separator ""
78
- opts.separator "Run `carson <command> --help` for details on a specific command."
56
+ OptionParser.new do |parser|
57
+ parser.banner = "Usage: carson <command> [options]"
58
+ parser.separator ""
59
+ parser.separator "Repository governance and workflow automation for coding agents."
60
+ parser.separator ""
61
+ parser.separator "Commands:"
62
+ parser.separator " status Show repository state (branch, PRs, worktrees)"
63
+ parser.separator " setup Initialise Carson configuration"
64
+ parser.separator " audit Run pre-commit health checks"
65
+ parser.separator " sync Sync local main with remote"
66
+ parser.separator " deliver Push, create PR, and optionally merge"
67
+ parser.separator " prune Remove stale local branches"
68
+ parser.separator " worktree Manage isolated coding worktrees"
69
+ parser.separator " housekeep Sync, reap worktrees, and prune branches"
70
+ parser.separator " repos List governed repositories"
71
+ parser.separator " onboard Register a repository for governance"
72
+ parser.separator " offboard Remove a repository from governance"
73
+ parser.separator " refresh Re-install hooks and configuration"
74
+ parser.separator " template Manage canonical template files"
75
+ parser.separator " review Manage PR review workflow"
76
+ parser.separator " govern Portfolio-level PR triage loop"
77
+ parser.separator " version Show Carson version"
78
+ parser.separator ""
79
+ parser.separator "Run `carson <command> --help` for details on a specific command."
79
80
  end
80
81
  end
81
82
 
82
- def self.parse_preset_command( argv:, out:, parser: )
83
- first = argv.first
83
+ def self.parse_preset_command( arguments:, output:, parser: )
84
+ first = arguments.first
84
85
  if [ "--help", "-h" ].include?( first )
85
- out.puts parser
86
+ output.puts parser
86
87
  return { command: :help }
87
88
  end
88
89
  return { command: "version" } if [ "--version", "-v" ].include?( first )
89
- return { command: "audit" } if argv.empty?
90
+ return { command: "audit" } if arguments.empty?
90
91
 
91
92
  nil
92
93
  end
93
94
 
94
- def self.parse_command( command:, argv:, err: )
95
+ def self.parse_command( command:, arguments:, error: )
95
96
  case command
96
97
  when "version"
97
98
  { command: "version" }
98
99
  when "setup"
99
- parse_setup_command( argv: argv, err: err )
100
+ parse_setup_command( arguments: arguments, error: error )
100
101
  when "onboard"
101
- parse_onboard_command( argv: argv, err: err )
102
+ parse_onboard_command( arguments: arguments, error: error )
102
103
  when "offboard"
103
- parse_offboard_command( argv: argv, err: err )
104
+ parse_offboard_command( arguments: arguments, error: error )
104
105
  when "refresh"
105
- parse_refresh_command( argv: argv, err: err )
106
+ parse_refresh_command( arguments: arguments, error: error )
106
107
  when "template"
107
- parse_template_subcommand( argv: argv, err: err )
108
+ parse_template_subcommand( arguments: arguments, error: error )
108
109
  when "prune"
109
- parse_prune_command( argv: argv, err: err )
110
+ parse_prune_command( arguments: arguments, error: error )
110
111
  when "worktree"
111
- parse_worktree_subcommand( argv: argv, err: err )
112
+ parse_worktree_subcommand( arguments: arguments, error: error )
112
113
  when "repos"
113
- parse_repos_command( argv: argv, err: err )
114
+ parse_repos_command( arguments: arguments, error: error )
114
115
  when "housekeep"
115
- parse_housekeep_command( argv: argv, err: err )
116
+ parse_housekeep_command( arguments: arguments, error: error )
116
117
  when "review"
117
- parse_review_subcommand( argv: argv, err: err )
118
+ parse_review_subcommand( arguments: arguments, error: error )
118
119
  when "audit"
119
- parse_audit_command( argv: argv, err: err )
120
+ parse_audit_command( arguments: arguments, error: error )
120
121
  when "sync"
121
- parse_sync_command( argv: argv, err: err )
122
+ parse_sync_command( arguments: arguments, error: error )
122
123
  when "status"
123
- parse_status_command( argv: argv, err: err )
124
+ parse_status_command( arguments: arguments, error: error )
124
125
  when "deliver"
125
- parse_deliver_command( argv: argv, err: err )
126
+ parse_deliver_command( arguments: arguments, error: error )
126
127
  when "govern"
127
- parse_govern_subcommand( argv: argv, err: err )
128
+ parse_govern_subcommand( arguments: arguments, error: error )
128
129
  else
129
130
  { command: command }
130
131
  end
@@ -132,428 +133,428 @@ module Carson
132
133
 
133
134
  # --- setup ---
134
135
 
135
- def self.parse_setup_command( argv:, err: )
136
+ def self.parse_setup_command( arguments:, error: )
136
137
  options = {}
137
- setup_parser = OptionParser.new do |opts|
138
- opts.banner = "Usage: carson setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]"
139
- opts.separator ""
140
- opts.separator "Initialise Carson configuration for the current repository."
141
- opts.separator "Detects git remote, main branch, and workflow style, then writes .carson.yml."
142
- opts.separator "Pass flags to override detected values."
143
- opts.separator ""
144
- opts.separator "Options:"
145
- opts.on( "--remote NAME", "Git remote name" ) { |v| options[ "git.remote" ] = v }
146
- opts.on( "--main-branch NAME", "Main branch name" ) { |v| options[ "git.main_branch" ] = v }
147
- opts.on( "--workflow STYLE", "Workflow style (branch or trunk)" ) { |v| options[ "workflow.style" ] = v }
148
- opts.on( "--merge METHOD", "Merge method (squash, rebase, or merge)" ) { |v| options[ "govern.merge.method" ] = v }
149
- opts.on( "--canonical PATH", "Canonical template directory path" ) { |v| options[ "template.canonical" ] = v }
150
- opts.separator ""
151
- opts.separator "Examples:"
152
- opts.separator " carson setup Auto-detect and write config"
153
- opts.separator " carson setup --remote github Use 'github' as the git remote"
154
- opts.separator " carson setup --merge squash Set squash as the merge method"
155
- end
156
- setup_parser.parse!( argv )
157
- unless argv.empty?
158
- err.puts "#{BADGE} Unexpected arguments for setup: #{argv.join( ' ' )}"
159
- err.puts setup_parser
138
+ setup_parser = OptionParser.new do |parser|
139
+ parser.banner = "Usage: carson setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]"
140
+ parser.separator ""
141
+ parser.separator "Initialise Carson configuration for the current repository."
142
+ parser.separator "Detects git remote, main branch, and workflow style, then writes .carson.yml."
143
+ parser.separator "Pass flags to override detected values."
144
+ parser.separator ""
145
+ parser.separator "Options:"
146
+ parser.on( "--remote NAME", "Git remote name" ) { |value| options[ "git.remote" ] = value }
147
+ parser.on( "--main-branch NAME", "Main branch name" ) { |value| options[ "git.main_branch" ] = value }
148
+ parser.on( "--workflow STYLE", "Workflow style (branch or trunk)" ) { |value| options[ "workflow.style" ] = value }
149
+ parser.on( "--merge METHOD", "Merge method (squash, rebase, or merge)" ) { |value| options[ "govern.merge.method" ] = value }
150
+ parser.on( "--canonical PATH", "Canonical template directory path" ) { |value| options[ "template.canonical" ] = value }
151
+ parser.separator ""
152
+ parser.separator "Examples:"
153
+ parser.separator " carson setup Auto-detect and write config"
154
+ parser.separator " carson setup --remote github Use 'github' as the git remote"
155
+ parser.separator " carson setup --merge squash Set squash as the merge method"
156
+ end
157
+ setup_parser.parse!( arguments )
158
+ unless arguments.empty?
159
+ error.puts "#{BADGE} Unexpected arguments for setup: #{arguments.join( ' ' )}"
160
+ error.puts setup_parser
160
161
  return { command: :invalid }
161
162
  end
162
163
  { command: "setup", cli_choices: options }
163
- rescue OptionParser::ParseError => e
164
- err.puts "#{BADGE} #{e.message}"
164
+ rescue OptionParser::ParseError => exception
165
+ error.puts "#{BADGE} #{exception.message}"
165
166
  { command: :invalid }
166
167
  end
167
168
 
168
169
  # --- onboard / offboard ---
169
170
 
170
- def self.parse_onboard_command( argv:, err: )
171
- onboard_parser = OptionParser.new do |opts|
172
- opts.banner = "Usage: carson onboard [REPO_PATH]"
173
- opts.separator ""
174
- opts.separator "Register a repository for Carson governance."
175
- opts.separator "Detects the remote, installs hooks, applies templates, and runs initial audit."
176
- opts.separator "Defaults to the current directory if no path is given."
177
- opts.separator ""
178
- opts.separator "Examples:"
179
- opts.separator " carson onboard Onboard the current repository"
180
- opts.separator " carson onboard ~/Dev/app Onboard a specific repository"
181
- end
182
- onboard_parser.parse!( argv )
183
- if argv.length > 1
184
- err.puts "#{BADGE} Too many arguments for onboard. Use: carson onboard [repo_path]"
185
- err.puts onboard_parser
171
+ def self.parse_onboard_command( arguments:, error: )
172
+ onboard_parser = OptionParser.new do |parser|
173
+ parser.banner = "Usage: carson onboard [REPO_PATH]"
174
+ parser.separator ""
175
+ parser.separator "Register a repository for Carson governance."
176
+ parser.separator "Detects the remote, installs hooks, applies templates, and runs initial audit."
177
+ parser.separator "Defaults to the current directory if no path is given."
178
+ parser.separator ""
179
+ parser.separator "Examples:"
180
+ parser.separator " carson onboard Onboard the current repository"
181
+ parser.separator " carson onboard ~/Dev/app Onboard a specific repository"
182
+ end
183
+ onboard_parser.parse!( arguments )
184
+ if arguments.length > 1
185
+ error.puts "#{BADGE} Too many arguments for onboard. Use: carson onboard [repo_path]"
186
+ error.puts onboard_parser
186
187
  return { command: :invalid }
187
188
  end
188
- repo_path = argv.first
189
+ repo_path = arguments.first
189
190
  {
190
191
  command: "onboard",
191
192
  repo_root: repo_path.to_s.strip.empty? ? nil : File.expand_path( repo_path )
192
193
  }
193
- rescue OptionParser::ParseError => e
194
- err.puts "#{BADGE} #{e.message}"
194
+ rescue OptionParser::ParseError => exception
195
+ error.puts "#{BADGE} #{exception.message}"
195
196
  { command: :invalid }
196
197
  end
197
198
 
198
- def self.parse_offboard_command( argv:, err: )
199
- offboard_parser = OptionParser.new do |opts|
200
- opts.banner = "Usage: carson offboard [REPO_PATH]"
201
- opts.separator ""
202
- opts.separator "Remove a repository from Carson governance."
203
- opts.separator "Unregisters the repo from Carson's portfolio and removes hooks."
204
- opts.separator "Defaults to the current directory if no path is given."
205
- opts.separator ""
206
- opts.separator "Examples:"
207
- opts.separator " carson offboard Offboard the current repository"
208
- end
209
- offboard_parser.parse!( argv )
210
- if argv.length > 1
211
- err.puts "#{BADGE} Too many arguments for offboard. Use: carson offboard [repo_path]"
212
- err.puts offboard_parser
199
+ def self.parse_offboard_command( arguments:, error: )
200
+ offboard_parser = OptionParser.new do |parser|
201
+ parser.banner = "Usage: carson offboard [REPO_PATH]"
202
+ parser.separator ""
203
+ parser.separator "Remove a repository from Carson governance."
204
+ parser.separator "Unregisters the repo from Carson's portfolio and removes hooks."
205
+ parser.separator "Defaults to the current directory if no path is given."
206
+ parser.separator ""
207
+ parser.separator "Examples:"
208
+ parser.separator " carson offboard Offboard the current repository"
209
+ end
210
+ offboard_parser.parse!( arguments )
211
+ if arguments.length > 1
212
+ error.puts "#{BADGE} Too many arguments for offboard. Use: carson offboard [repo_path]"
213
+ error.puts offboard_parser
213
214
  return { command: :invalid }
214
215
  end
215
- repo_path = argv.first
216
+ repo_path = arguments.first
216
217
  {
217
218
  command: "offboard",
218
219
  repo_root: repo_path.to_s.strip.empty? ? nil : File.expand_path( repo_path )
219
220
  }
220
- rescue OptionParser::ParseError => e
221
- err.puts "#{BADGE} #{e.message}"
221
+ rescue OptionParser::ParseError => exception
222
+ error.puts "#{BADGE} #{exception.message}"
222
223
  { command: :invalid }
223
224
  end
224
225
 
225
226
  # --- refresh ---
226
227
 
227
- def self.parse_refresh_command( argv:, err: )
228
+ def self.parse_refresh_command( arguments:, error: )
228
229
  options = { all: false }
229
- refresh_parser = OptionParser.new do |opts|
230
- opts.banner = "Usage: carson refresh [--all] [REPO_PATH]"
231
- opts.separator ""
232
- opts.separator "Re-install Carson hooks and configuration for a repository."
233
- opts.separator "Defaults to the current directory. Use --all to refresh all governed repos."
234
- opts.separator ""
235
- opts.separator "Options:"
236
- opts.on( "--all", "Refresh all governed repositories" ) { options[ :all ] = true }
237
- opts.separator ""
238
- opts.separator "Examples:"
239
- opts.separator " carson refresh Refresh the current repository"
240
- opts.separator " carson refresh --all Refresh all governed repos"
241
- end
242
- refresh_parser.parse!( argv )
243
-
244
- if options[ :all ] && !argv.empty?
245
- err.puts "#{BADGE} --all and repo_path are mutually exclusive. Use: carson refresh --all OR carson refresh [repo_path]"
246
- err.puts refresh_parser
230
+ refresh_parser = OptionParser.new do |parser|
231
+ parser.banner = "Usage: carson refresh [--all] [REPO_PATH]"
232
+ parser.separator ""
233
+ parser.separator "Re-install Carson hooks and configuration for a repository."
234
+ parser.separator "Defaults to the current directory. Use --all to refresh all governed repos."
235
+ parser.separator ""
236
+ parser.separator "Options:"
237
+ parser.on( "--all", "Refresh all governed repositories" ) { options[ :all ] = true }
238
+ parser.separator ""
239
+ parser.separator "Examples:"
240
+ parser.separator " carson refresh Refresh the current repository"
241
+ parser.separator " carson refresh --all Refresh all governed repos"
242
+ end
243
+ refresh_parser.parse!( arguments )
244
+
245
+ if options[ :all ] && !arguments.empty?
246
+ error.puts "#{BADGE} --all and repo_path are mutually exclusive. Use: carson refresh --all OR carson refresh [repo_path]"
247
+ error.puts refresh_parser
247
248
  return { command: :invalid }
248
249
  end
249
250
 
250
251
  return { command: "refresh:all" } if options[ :all ]
251
252
 
252
- if argv.length > 1
253
- err.puts "#{BADGE} Too many arguments for refresh. Use: carson refresh [repo_path]"
254
- err.puts refresh_parser
253
+ if arguments.length > 1
254
+ error.puts "#{BADGE} Too many arguments for refresh. Use: carson refresh [repo_path]"
255
+ error.puts refresh_parser
255
256
  return { command: :invalid }
256
257
  end
257
258
 
258
- repo_path = argv.first
259
+ repo_path = arguments.first
259
260
  {
260
261
  command: "refresh",
261
262
  repo_root: repo_path.to_s.strip.empty? ? nil : File.expand_path( repo_path )
262
263
  }
263
- rescue OptionParser::ParseError => e
264
- err.puts "#{BADGE} #{e.message}"
264
+ rescue OptionParser::ParseError => exception
265
+ error.puts "#{BADGE} #{exception.message}"
265
266
  { command: :invalid }
266
267
  end
267
268
 
268
269
  # --- prune ---
269
270
 
270
- def self.parse_prune_command( argv:, err: )
271
+ def self.parse_prune_command( arguments:, error: )
271
272
  options = { all: false, json: false }
272
- prune_parser = OptionParser.new do |opts|
273
- opts.banner = "Usage: carson prune [--all] [--json]"
274
- opts.separator ""
275
- opts.separator "Remove stale local branches."
276
- opts.separator "Cleans up branches gone from the remote, orphan branches with merged PRs,"
277
- opts.separator "and absorbed branches whose content is already on main."
278
- opts.separator ""
279
- opts.separator "Options:"
280
- opts.on( "--all", "Prune all governed repositories" ) { options[ :all ] = true }
281
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
282
- opts.separator ""
283
- opts.separator "Examples:"
284
- opts.separator " carson prune Clean up stale branches in this repo"
285
- opts.separator " carson prune --all Clean up across all governed repos"
286
- end
287
- prune_parser.parse!( argv )
273
+ prune_parser = OptionParser.new do |parser|
274
+ parser.banner = "Usage: carson prune [--all] [--json]"
275
+ parser.separator ""
276
+ parser.separator "Remove stale local branches."
277
+ parser.separator "Cleans up branches gone from the remote, orphan branches with merged PRs,"
278
+ parser.separator "and absorbed branches whose content is already on main."
279
+ parser.separator ""
280
+ parser.separator "Options:"
281
+ parser.on( "--all", "Prune all governed repositories" ) { options[ :all ] = true }
282
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
283
+ parser.separator ""
284
+ parser.separator "Examples:"
285
+ parser.separator " carson prune Clean up stale branches in this repo"
286
+ parser.separator " carson prune --all Clean up across all governed repos"
287
+ end
288
+ prune_parser.parse!( arguments )
288
289
  return { command: "prune:all", json: options[ :json ] } if options[ :all ]
289
290
  { command: "prune", json: options[ :json ] }
290
- rescue OptionParser::ParseError => e
291
- err.puts "#{BADGE} #{e.message}"
291
+ rescue OptionParser::ParseError => exception
292
+ error.puts "#{BADGE} #{exception.message}"
292
293
  { command: :invalid }
293
294
  end
294
295
 
295
296
  # --- worktree ---
296
297
 
297
- def self.parse_worktree_subcommand( argv:, err: )
298
+ def self.parse_worktree_subcommand( arguments:, error: )
298
299
  options = { json: false, force: false }
299
- worktree_parser = OptionParser.new do |opts|
300
- opts.banner = "Usage: carson worktree <create|remove> <name> [options]"
301
- opts.separator ""
302
- opts.separator "Manage isolated worktrees for coding agents."
303
- opts.separator "Create auto-syncs main before branching. Remove guards against"
304
- opts.separator "unpushed commits and CWD-inside-worktree by default."
305
- opts.separator ""
306
- opts.separator "Subcommands:"
307
- opts.separator " create <name> Create a new worktree with a fresh branch"
308
- opts.separator " remove <name> [--force] Remove a worktree (--force skips safety checks)"
309
- opts.separator ""
310
- opts.separator "Options:"
311
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
312
- opts.on( "--force", "Skip safety checks on remove" ) { options[ :force ] = true }
313
- opts.separator ""
314
- opts.separator "Examples:"
315
- opts.separator " carson worktree create feature-x Create an isolated worktree"
316
- opts.separator " carson worktree remove feature-x Remove after work is pushed"
317
- end
318
- worktree_parser.parse!( argv )
319
-
320
- action = argv.shift
300
+ worktree_parser = OptionParser.new do |parser|
301
+ parser.banner = "Usage: carson worktree <create|remove> <name> [options]"
302
+ parser.separator ""
303
+ parser.separator "Manage isolated worktrees for coding agents."
304
+ parser.separator "Create auto-syncs main before branching. Remove guards against"
305
+ parser.separator "unpushed commits and CWD-inside-worktree by default."
306
+ parser.separator ""
307
+ parser.separator "Subcommands:"
308
+ parser.separator " create <name> Create a new worktree with a fresh branch"
309
+ parser.separator " remove <name> [--force] Remove a worktree (--force skips safety checks)"
310
+ parser.separator ""
311
+ parser.separator "Options:"
312
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
313
+ parser.on( "--force", "Skip safety checks on remove" ) { options[ :force ] = true }
314
+ parser.separator ""
315
+ parser.separator "Examples:"
316
+ parser.separator " carson worktree create feature-x Create an isolated worktree"
317
+ parser.separator " carson worktree remove feature-x Remove after work is pushed"
318
+ end
319
+ worktree_parser.parse!( arguments )
320
+
321
+ action = arguments.shift
321
322
  if action.to_s.strip.empty?
322
- err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree create|remove <name>"
323
- err.puts worktree_parser
323
+ error.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree create|remove <name>"
324
+ error.puts worktree_parser
324
325
  return { command: :invalid }
325
326
  end
326
327
 
327
328
  case action
328
329
  when "create"
329
- name = argv.shift
330
+ name = arguments.shift
330
331
  if name.to_s.strip.empty?
331
- err.puts "#{BADGE} Missing name for worktree create. Use: carson worktree create <name>"
332
+ error.puts "#{BADGE} Missing name for worktree create. Use: carson worktree create <name>"
332
333
  return { command: :invalid }
333
334
  end
334
335
  { command: "worktree:create", worktree_name: name, json: options[ :json ] }
335
336
  when "remove"
336
- worktree_path = argv.shift
337
+ worktree_path = arguments.shift
337
338
  if worktree_path.to_s.strip.empty?
338
- err.puts "#{BADGE} Missing path for worktree remove. Use: carson worktree remove <name-or-path>"
339
+ error.puts "#{BADGE} Missing path for worktree remove. Use: carson worktree remove <name-or-path>"
339
340
  return { command: :invalid }
340
341
  end
341
342
  { command: "worktree:remove", worktree_path: worktree_path, force: options[ :force ], json: options[ :json ] }
342
343
  else
343
- err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree create|remove <name>"
344
+ error.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree create|remove <name>"
344
345
  { command: :invalid }
345
346
  end
346
- rescue OptionParser::ParseError => e
347
- err.puts "#{BADGE} #{e.message}"
347
+ rescue OptionParser::ParseError => exception
348
+ error.puts "#{BADGE} #{exception.message}"
348
349
  { command: :invalid }
349
350
  end
350
351
 
351
352
  # --- review ---
352
353
 
353
- def self.parse_review_subcommand( argv:, err: )
354
- review_parser = OptionParser.new do |opts|
355
- opts.banner = "Usage: carson review <gate|sweep>"
356
- opts.separator ""
357
- opts.separator "Manage PR review workflow."
358
- opts.separator ""
359
- opts.separator "Subcommands:"
360
- opts.separator " gate Check if review requirements are met for merge"
361
- opts.separator " sweep Scan and resolve pending review threads"
362
- opts.separator ""
363
- opts.separator "Examples:"
364
- opts.separator " carson review gate Check merge readiness"
365
- opts.separator " carson review sweep Resolve pending review threads"
366
- end
367
- review_parser.parse!( argv )
368
-
369
- action = argv.shift
354
+ def self.parse_review_subcommand( arguments:, error: )
355
+ review_parser = OptionParser.new do |parser|
356
+ parser.banner = "Usage: carson review <gate|sweep>"
357
+ parser.separator ""
358
+ parser.separator "Manage PR review workflow."
359
+ parser.separator ""
360
+ parser.separator "Subcommands:"
361
+ parser.separator " gate Check if review requirements are met for merge"
362
+ parser.separator " sweep Scan and resolve pending review threads"
363
+ parser.separator ""
364
+ parser.separator "Examples:"
365
+ parser.separator " carson review gate Check merge readiness"
366
+ parser.separator " carson review sweep Resolve pending review threads"
367
+ end
368
+ review_parser.parse!( arguments )
369
+
370
+ action = arguments.shift
370
371
  if action.to_s.strip.empty?
371
- err.puts "#{BADGE} Missing subcommand for review. Use: carson review gate|sweep"
372
- err.puts review_parser
372
+ error.puts "#{BADGE} Missing subcommand for review. Use: carson review gate|sweep"
373
+ error.puts review_parser
373
374
  return { command: :invalid }
374
375
  end
375
376
  { command: "review:#{action}" }
376
- rescue OptionParser::ParseError => e
377
- err.puts "#{BADGE} #{e.message}"
377
+ rescue OptionParser::ParseError => exception
378
+ error.puts "#{BADGE} #{exception.message}"
378
379
  { command: :invalid }
379
380
  end
380
381
 
381
382
  # --- template ---
382
383
 
383
- def self.parse_template_subcommand( argv:, err: )
384
+ def self.parse_template_subcommand( arguments:, error: )
384
385
  # Handle parent-level help or missing subcommand.
385
- if argv.empty? || [ "--help", "-h" ].include?( argv.first )
386
- template_parser = OptionParser.new do |opts|
387
- opts.banner = "Usage: carson template <check|apply> [options]"
388
- opts.separator ""
389
- opts.separator "Manage canonical template files (CI workflows, lint configs)."
390
- opts.separator ""
391
- opts.separator "Subcommands:"
392
- opts.separator " check Show template drift without making changes"
393
- opts.separator " apply [--push-prep] Sync templates into the repository"
394
- opts.separator ""
395
- opts.separator "Examples:"
396
- opts.separator " carson template check Check for template drift"
397
- opts.separator " carson template apply Apply canonical templates"
386
+ if arguments.empty? || [ "--help", "-h" ].include?( arguments.first )
387
+ template_parser = OptionParser.new do |parser|
388
+ parser.banner = "Usage: carson template <check|apply> [options]"
389
+ parser.separator ""
390
+ parser.separator "Manage canonical template files (CI workflows, lint configs)."
391
+ parser.separator ""
392
+ parser.separator "Subcommands:"
393
+ parser.separator " check Show template drift without making changes"
394
+ parser.separator " apply [--push-prep] Sync templates into the repository"
395
+ parser.separator ""
396
+ parser.separator "Examples:"
397
+ parser.separator " carson template check Check for template drift"
398
+ parser.separator " carson template apply Apply canonical templates"
398
399
  end
399
400
 
400
- if argv.empty?
401
- err.puts "#{BADGE} Missing subcommand for template. Use: carson template check|apply"
402
- err.puts template_parser
401
+ if arguments.empty?
402
+ error.puts "#{BADGE} Missing subcommand for template. Use: carson template check|apply"
403
+ error.puts template_parser
403
404
  return { command: :invalid }
404
405
  end
405
406
 
406
407
  # Let OptionParser handle --help (prints and exits).
407
- template_parser.parse!( argv )
408
+ template_parser.parse!( arguments )
408
409
  return { command: :help }
409
410
  end
410
411
 
411
- action = argv.shift
412
- return { command: "template:check:all" } if action == "check" && argv.include?( "--all" )
412
+ action = arguments.shift
413
+ return { command: "template:check:all" } if action == "check" && arguments.include?( "--all" )
413
414
  return { command: "template:#{action}" } unless action == "apply"
414
415
 
415
416
  options = { push_prep: false }
416
- apply_parser = OptionParser.new do |opts|
417
- opts.banner = "Usage: carson template apply [--push-prep]"
418
- opts.separator ""
419
- opts.separator "Sync canonical template files (CI workflows, lint configs) into the repository."
420
- opts.separator "Copies managed files from the configured canonical directory."
421
- opts.separator ""
422
- opts.separator "Options:"
423
- opts.on( "--push-prep", "Apply templates and auto-commit any managed file changes (used by pre-push hook)" ) do
417
+ apply_parser = OptionParser.new do |parser|
418
+ parser.banner = "Usage: carson template apply [--push-prep]"
419
+ parser.separator ""
420
+ parser.separator "Sync canonical template files (CI workflows, lint configs) into the repository."
421
+ parser.separator "Copies managed files from the configured canonical directory."
422
+ parser.separator ""
423
+ parser.separator "Options:"
424
+ parser.on( "--push-prep", "Apply templates and auto-commit any managed file changes (used by pre-push hook)" ) do
424
425
  options[ :push_prep ] = true
425
426
  end
426
427
  end
427
- apply_parser.parse!( argv )
428
- unless argv.empty?
429
- err.puts "#{BADGE} Unexpected arguments for template apply: #{argv.join( ' ' )}"
430
- err.puts apply_parser
428
+ apply_parser.parse!( arguments )
429
+ unless arguments.empty?
430
+ error.puts "#{BADGE} Unexpected arguments for template apply: #{arguments.join( ' ' )}"
431
+ error.puts apply_parser
431
432
  return { command: :invalid }
432
433
  end
433
434
  { command: "template:apply", push_prep: options.fetch( :push_prep ) }
434
- rescue OptionParser::ParseError => e
435
- err.puts "#{BADGE} #{e.message}"
435
+ rescue OptionParser::ParseError => exception
436
+ error.puts "#{BADGE} #{exception.message}"
436
437
  { command: :invalid }
437
438
  end
438
439
 
439
440
  # --- audit ---
440
441
 
441
- def self.parse_audit_command( argv:, err: )
442
+ def self.parse_audit_command( arguments:, error: )
442
443
  options = { json: false, all: false }
443
- audit_parser = OptionParser.new do |opts|
444
- opts.banner = "Usage: carson audit [--all] [--json]"
445
- opts.separator ""
446
- opts.separator "Run pre-commit health checks on the repository."
447
- opts.separator "Validates hooks, main-branch sync, PR status, and CI baseline."
448
- opts.separator "Exits with a non-zero status when policy violations are found."
449
- opts.separator ""
450
- opts.separator "Options:"
451
- opts.on( "--all", "Audit all governed repositories" ) { options[ :all ] = true }
452
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
453
- opts.separator ""
454
- opts.separator "Examples:"
455
- opts.separator " carson audit Check repository health (also the default command)"
456
- opts.separator " carson audit --json Structured output for agent consumption"
457
- opts.separator " carson audit --all Audit all governed repos"
458
- end
459
- audit_parser.parse!( argv )
460
- unless argv.empty?
461
- err.puts "#{BADGE} Unexpected arguments for audit: #{argv.join( ' ' )}"
444
+ audit_parser = OptionParser.new do |parser|
445
+ parser.banner = "Usage: carson audit [--all] [--json]"
446
+ parser.separator ""
447
+ parser.separator "Run pre-commit health checks on the repository."
448
+ parser.separator "Validates hooks, main-branch sync, PR status, and CI baseline."
449
+ parser.separator "Exits with a non-zero status when policy violations are found."
450
+ parser.separator ""
451
+ parser.separator "Options:"
452
+ parser.on( "--all", "Audit all governed repositories" ) { options[ :all ] = true }
453
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
454
+ parser.separator ""
455
+ parser.separator "Examples:"
456
+ parser.separator " carson audit Check repository health (also the default command)"
457
+ parser.separator " carson audit --json Structured output for agent consumption"
458
+ parser.separator " carson audit --all Audit all governed repos"
459
+ end
460
+ audit_parser.parse!( arguments )
461
+ unless arguments.empty?
462
+ error.puts "#{BADGE} Unexpected arguments for audit: #{arguments.join( ' ' )}"
462
463
  return { command: :invalid }
463
464
  end
464
465
  return { command: "audit:all" } if options[ :all ]
465
466
  { command: "audit", json: options[ :json ] }
466
- rescue OptionParser::ParseError => e
467
- err.puts "#{BADGE} #{e.message}"
467
+ rescue OptionParser::ParseError => exception
468
+ error.puts "#{BADGE} #{exception.message}"
468
469
  { command: :invalid }
469
470
  end
470
471
 
471
472
  # --- sync ---
472
473
 
473
- def self.parse_sync_command( argv:, err: )
474
+ def self.parse_sync_command( arguments:, error: )
474
475
  options = { json: false, all: false }
475
- sync_parser = OptionParser.new do |opts|
476
- opts.banner = "Usage: carson sync [--all] [--json]"
477
- opts.separator ""
478
- opts.separator "Sync the local main branch with the remote."
479
- opts.separator "Fetches and fast-forwards main without switching branches."
480
- opts.separator ""
481
- opts.separator "Options:"
482
- opts.on( "--all", "Sync all governed repositories" ) { options[ :all ] = true }
483
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
484
- opts.separator ""
485
- opts.separator "Examples:"
486
- opts.separator " carson sync Pull latest changes from remote main"
487
- opts.separator " carson sync --json Structured output for agent consumption"
488
- opts.separator " carson sync --all Sync all governed repos"
489
- end
490
- sync_parser.parse!( argv )
491
- unless argv.empty?
492
- err.puts "#{BADGE} Unexpected arguments for sync: #{argv.join( ' ' )}"
476
+ sync_parser = OptionParser.new do |parser|
477
+ parser.banner = "Usage: carson sync [--all] [--json]"
478
+ parser.separator ""
479
+ parser.separator "Sync the local main branch with the remote."
480
+ parser.separator "Fetches and fast-forwards main without switching branches."
481
+ parser.separator ""
482
+ parser.separator "Options:"
483
+ parser.on( "--all", "Sync all governed repositories" ) { options[ :all ] = true }
484
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
485
+ parser.separator ""
486
+ parser.separator "Examples:"
487
+ parser.separator " carson sync Pull latest changes from remote main"
488
+ parser.separator " carson sync --json Structured output for agent consumption"
489
+ parser.separator " carson sync --all Sync all governed repos"
490
+ end
491
+ sync_parser.parse!( arguments )
492
+ unless arguments.empty?
493
+ error.puts "#{BADGE} Unexpected arguments for sync: #{arguments.join( ' ' )}"
493
494
  return { command: :invalid }
494
495
  end
495
496
  return { command: "sync:all" } if options[ :all ]
496
497
  { command: "sync", json: options[ :json ] }
497
- rescue OptionParser::ParseError => e
498
- err.puts "#{BADGE} #{e.message}"
498
+ rescue OptionParser::ParseError => exception
499
+ error.puts "#{BADGE} #{exception.message}"
499
500
  { command: :invalid }
500
501
  end
501
502
 
502
503
  # --- status ---
503
504
 
504
- def self.parse_status_command( argv:, err: )
505
+ def self.parse_status_command( arguments:, error: )
505
506
  options = { json: false, all: false }
506
- status_parser = OptionParser.new do |opts|
507
- opts.banner = "Usage: carson status [--all] [--json]"
508
- opts.separator ""
509
- opts.separator "Show the current state of the repository."
510
- opts.separator "Reports branch, worktrees, open PRs, stale branches, and version."
511
- opts.separator ""
512
- opts.separator "Options:"
513
- opts.on( "--all", "Show status for all governed repositories" ) { options[ :all ] = true }
514
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
515
- opts.separator ""
516
- opts.separator "Examples:"
517
- opts.separator " carson status Quick overview of repository state"
518
- opts.separator " carson status --json Structured output for agent consumption"
519
- opts.separator " carson status --all Portfolio-wide status overview"
520
- end
521
- status_parser.parse!( argv )
522
- unless argv.empty?
523
- err.puts "#{BADGE} Unexpected arguments for status: #{argv.join( ' ' )}"
507
+ status_parser = OptionParser.new do |parser|
508
+ parser.banner = "Usage: carson status [--all] [--json]"
509
+ parser.separator ""
510
+ parser.separator "Show the current state of the repository."
511
+ parser.separator "Reports branch, worktrees, open PRs, stale branches, and version."
512
+ parser.separator ""
513
+ parser.separator "Options:"
514
+ parser.on( "--all", "Show status for all governed repositories" ) { options[ :all ] = true }
515
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
516
+ parser.separator ""
517
+ parser.separator "Examples:"
518
+ parser.separator " carson status Quick overview of repository state"
519
+ parser.separator " carson status --json Structured output for agent consumption"
520
+ parser.separator " carson status --all Portfolio-wide status overview"
521
+ end
522
+ status_parser.parse!( arguments )
523
+ unless arguments.empty?
524
+ error.puts "#{BADGE} Unexpected arguments for status: #{arguments.join( ' ' )}"
524
525
  return { command: :invalid }
525
526
  end
526
527
  return { command: "status:all", json: options[ :json ] } if options[ :all ]
527
528
  { command: "status", json: options[ :json ] }
528
- rescue OptionParser::ParseError => e
529
- err.puts "#{BADGE} #{e.message}"
529
+ rescue OptionParser::ParseError => exception
530
+ error.puts "#{BADGE} #{exception.message}"
530
531
  { command: :invalid }
531
532
  end
532
533
 
533
534
  # --- deliver ---
534
535
 
535
- def self.parse_deliver_command( argv:, err: )
536
+ def self.parse_deliver_command( arguments:, error: )
536
537
  options = { merge: false, json: false, title: nil, body_file: nil }
537
- deliver_parser = OptionParser.new do |opts|
538
- opts.banner = "Usage: carson deliver [--merge] [--json] [--title TITLE] [--body-file PATH]"
539
- opts.separator ""
540
- opts.separator "Push the current branch, create a pull request, and optionally merge."
541
- opts.separator "Collapses the manual push → PR → merge flow into a single command."
542
- opts.separator ""
543
- opts.separator "Options:"
544
- opts.on( "--merge", "Also merge the PR if CI passes" ) { options[ :merge ] = true }
545
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
546
- opts.on( "--title TITLE", "PR title (defaults to branch name)" ) { |v| options[ :title ] = v }
547
- opts.on( "--body-file PATH", "File containing PR body text" ) { |v| options[ :body_file ] = v }
548
- opts.separator ""
549
- opts.separator "Examples:"
550
- opts.separator " carson deliver Push and open a PR"
551
- opts.separator " carson deliver --merge Push, open a PR, and merge if CI passes"
552
- end
553
- deliver_parser.parse!( argv )
554
- unless argv.empty?
555
- err.puts "#{BADGE} Unexpected arguments for deliver: #{argv.join( ' ' )}"
556
- err.puts deliver_parser
538
+ deliver_parser = OptionParser.new do |parser|
539
+ parser.banner = "Usage: carson deliver [--merge] [--json] [--title TITLE] [--body-file PATH]"
540
+ parser.separator ""
541
+ parser.separator "Push the current branch, create a pull request, and optionally merge."
542
+ parser.separator "Collapses the manual push → PR → merge flow into a single command."
543
+ parser.separator ""
544
+ parser.separator "Options:"
545
+ parser.on( "--merge", "Also merge the PR if CI passes" ) { options[ :merge ] = true }
546
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
547
+ parser.on( "--title TITLE", "PR title (defaults to branch name)" ) { |value| options[ :title ] = value }
548
+ parser.on( "--body-file PATH", "File containing PR body text" ) { |value| options[ :body_file ] = value }
549
+ parser.separator ""
550
+ parser.separator "Examples:"
551
+ parser.separator " carson deliver Push and open a PR"
552
+ parser.separator " carson deliver --merge Push, open a PR, and merge if CI passes"
553
+ end
554
+ deliver_parser.parse!( arguments )
555
+ unless arguments.empty?
556
+ error.puts "#{BADGE} Unexpected arguments for deliver: #{arguments.join( ' ' )}"
557
+ error.puts deliver_parser
557
558
  return { command: :invalid }
558
559
  end
559
560
  {
@@ -563,113 +564,113 @@ module Carson
563
564
  title: options[ :title ],
564
565
  body_file: options[ :body_file ]
565
566
  }
566
- rescue OptionParser::ParseError => e
567
- err.puts "#{BADGE} #{e.message}"
567
+ rescue OptionParser::ParseError => exception
568
+ error.puts "#{BADGE} #{exception.message}"
568
569
  { command: :invalid }
569
570
  end
570
571
 
571
572
  # --- repos ---
572
573
 
573
- def self.parse_repos_command( argv:, err: )
574
+ def self.parse_repos_command( arguments:, error: )
574
575
  options = { json: false }
575
- repos_parser = OptionParser.new do |opts|
576
- opts.banner = "Usage: carson repos [--json]"
577
- opts.separator ""
578
- opts.separator "List all repositories governed by Carson."
579
- opts.separator "Shows the portfolio of repos registered via carson onboard."
580
- opts.separator ""
581
- opts.separator "Options:"
582
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
583
- opts.separator ""
584
- opts.separator "Examples:"
585
- opts.separator " carson repos List governed repositories"
586
- opts.separator " carson repos --json Structured output for agent consumption"
587
- end
588
- repos_parser.parse!( argv )
589
- unless argv.empty?
590
- err.puts "#{BADGE} Unexpected arguments for repos: #{argv.join( ' ' )}"
576
+ repos_parser = OptionParser.new do |parser|
577
+ parser.banner = "Usage: carson repos [--json]"
578
+ parser.separator ""
579
+ parser.separator "List all repositories governed by Carson."
580
+ parser.separator "Shows the portfolio of repos registered via carson onboard."
581
+ parser.separator ""
582
+ parser.separator "Options:"
583
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
584
+ parser.separator ""
585
+ parser.separator "Examples:"
586
+ parser.separator " carson repos List governed repositories"
587
+ parser.separator " carson repos --json Structured output for agent consumption"
588
+ end
589
+ repos_parser.parse!( arguments )
590
+ unless arguments.empty?
591
+ error.puts "#{BADGE} Unexpected arguments for repos: #{arguments.join( ' ' )}"
591
592
  return { command: :invalid }
592
593
  end
593
594
  { command: "repos", json: options[ :json ] }
594
- rescue OptionParser::ParseError => e
595
- err.puts "#{BADGE} #{e.message}"
595
+ rescue OptionParser::ParseError => exception
596
+ error.puts "#{BADGE} #{exception.message}"
596
597
  { command: :invalid }
597
598
  end
598
599
 
599
600
  # --- housekeep ---
600
601
 
601
- def self.parse_housekeep_command( argv:, err: )
602
+ def self.parse_housekeep_command( arguments:, error: )
602
603
  options = { all: false, json: false }
603
- housekeep_parser = OptionParser.new do |opts|
604
- opts.banner = "Usage: carson housekeep [REPO] [--all] [--json]"
605
- opts.separator ""
606
- opts.separator "Run housekeeping: sync main, reap dead worktrees, and prune stale branches."
607
- opts.separator "Defaults to the current repository."
608
- opts.separator ""
609
- opts.separator "Options:"
610
- opts.on( "--all", "Housekeep all governed repositories" ) { options[ :all ] = true }
611
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
612
- opts.separator ""
613
- opts.separator "Examples:"
614
- opts.separator " carson housekeep Housekeep the current repository"
615
- opts.separator " carson housekeep nexus Housekeep a named governed repo"
616
- opts.separator " carson housekeep --all Housekeep all governed repos"
617
- end
618
- housekeep_parser.parse!( argv )
619
-
620
- if options[ :all ] && !argv.empty?
621
- err.puts "#{BADGE} --all and repo target are mutually exclusive. Use: carson housekeep --all OR carson housekeep [repo]"
604
+ housekeep_parser = OptionParser.new do |parser|
605
+ parser.banner = "Usage: carson housekeep [REPO] [--all] [--json]"
606
+ parser.separator ""
607
+ parser.separator "Run housekeeping: sync main, reap dead worktrees, and prune stale branches."
608
+ parser.separator "Defaults to the current repository."
609
+ parser.separator ""
610
+ parser.separator "Options:"
611
+ parser.on( "--all", "Housekeep all governed repositories" ) { options[ :all ] = true }
612
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
613
+ parser.separator ""
614
+ parser.separator "Examples:"
615
+ parser.separator " carson housekeep Housekeep the current repository"
616
+ parser.separator " carson housekeep nexus Housekeep a named governed repo"
617
+ parser.separator " carson housekeep --all Housekeep all governed repos"
618
+ end
619
+ housekeep_parser.parse!( arguments )
620
+
621
+ if options[ :all ] && !arguments.empty?
622
+ error.puts "#{BADGE} --all and repo target are mutually exclusive. Use: carson housekeep --all OR carson housekeep [repo]"
622
623
  return { command: :invalid }
623
624
  end
624
625
 
625
626
  return { command: "housekeep:all", json: options[ :json ] } if options[ :all ]
626
627
 
627
- if argv.length > 1
628
- err.puts "#{BADGE} Too many arguments for housekeep. Use: carson housekeep [repo]"
628
+ if arguments.length > 1
629
+ error.puts "#{BADGE} Too many arguments for housekeep. Use: carson housekeep [repo]"
629
630
  return { command: :invalid }
630
631
  end
631
632
 
632
- target = argv.shift
633
+ target = arguments.shift
633
634
  return { command: "housekeep:target", target: target, json: options[ :json ] } if target
634
635
 
635
636
  { command: "housekeep", json: options[ :json ] }
636
- rescue OptionParser::ParseError => e
637
- err.puts "#{BADGE} #{e.message}"
637
+ rescue OptionParser::ParseError => exception
638
+ error.puts "#{BADGE} #{exception.message}"
638
639
  { command: :invalid }
639
640
  end
640
641
 
641
642
  # --- govern ---
642
643
 
643
- def self.parse_govern_subcommand( argv:, err: )
644
+ def self.parse_govern_subcommand( arguments:, error: )
644
645
  options = {
645
646
  dry_run: false,
646
647
  json: false,
647
648
  loop_seconds: nil
648
649
  }
649
- govern_parser = OptionParser.new do |opts|
650
- opts.banner = "Usage: carson govern [--dry-run] [--json] [--loop SECONDS]"
651
- opts.separator ""
652
- opts.separator "Portfolio-level PR triage loop."
653
- opts.separator "Scans governed repositories, classifies open PRs, and takes action"
654
- opts.separator "(merge, request review, or report). Runs once by default."
655
- opts.separator ""
656
- opts.separator "Options:"
657
- opts.on( "--dry-run", "Run all checks but do not merge or dispatch" ) { options[ :dry_run ] = true }
658
- opts.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
659
- opts.on( "--loop SECONDS", Integer, "Run continuously, sleeping SECONDS between cycles" ) do |s|
660
- err.puts( "#{BADGE} Error: --loop must be a positive integer" ) || ( return { command: :invalid } ) if s < 1
661
- options[ :loop_seconds ] = s
650
+ govern_parser = OptionParser.new do |parser|
651
+ parser.banner = "Usage: carson govern [--dry-run] [--json] [--loop SECONDS]"
652
+ parser.separator ""
653
+ parser.separator "Portfolio-level PR triage loop."
654
+ parser.separator "Scans governed repositories, classifies open PRs, and takes action"
655
+ parser.separator "(merge, request review, or report). Runs once by default."
656
+ parser.separator ""
657
+ parser.separator "Options:"
658
+ parser.on( "--dry-run", "Run all checks but do not merge or dispatch" ) { options[ :dry_run ] = true }
659
+ parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
660
+ parser.on( "--loop SECONDS", Integer, "Run continuously, sleeping SECONDS between cycles" ) do |seconds|
661
+ error.puts( "#{BADGE} Error: --loop must be a positive integer" ) || ( return { command: :invalid } ) if seconds < 1
662
+ options[ :loop_seconds ] = seconds
662
663
  end
663
- opts.separator ""
664
- opts.separator "Examples:"
665
- opts.separator " carson govern Triage all governed repos once"
666
- opts.separator " carson govern --dry-run Preview actions without applying them"
667
- opts.separator " carson govern --loop 300 Run continuously every 5 minutes"
668
- end
669
- govern_parser.parse!( argv )
670
- unless argv.empty?
671
- err.puts "#{BADGE} Unexpected arguments for govern: #{argv.join( ' ' )}"
672
- err.puts govern_parser
664
+ parser.separator ""
665
+ parser.separator "Examples:"
666
+ parser.separator " carson govern Triage all governed repos once"
667
+ parser.separator " carson govern --dry-run Preview actions without applying them"
668
+ parser.separator " carson govern --loop 300 Run continuously every 5 minutes"
669
+ end
670
+ govern_parser.parse!( arguments )
671
+ unless arguments.empty?
672
+ error.puts "#{BADGE} Unexpected arguments for govern: #{arguments.join( ' ' )}"
673
+ error.puts govern_parser
673
674
  return { command: :invalid }
674
675
  end
675
676
  {
@@ -678,9 +679,9 @@ module Carson
678
679
  json: options.fetch( :json ),
679
680
  loop_seconds: options[ :loop_seconds ]
680
681
  }
681
- rescue OptionParser::ParseError => e
682
- err.puts "#{BADGE} #{e.message}"
683
- err.puts govern_parser
682
+ rescue OptionParser::ParseError => exception
683
+ error.puts "#{BADGE} #{exception.message}"
684
+ error.puts govern_parser
684
685
  { command: :invalid }
685
686
  end
686
687