carson 3.23.2 → 3.24.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: b22f115d99fffc1ed689ceb214523b8a33da08fc84a955fcc766d778be21e40c
4
- data.tar.gz: 6a270c175cb8affabba0d6d747e08701a50c61124a4c5486525113ff350cedbf
3
+ metadata.gz: 92803422cb09be4d2f4b31cf6c0b0f98796373c2a6a403af55c0e09361d0e951
4
+ data.tar.gz: 36f06f845aca0878e2d18de009e661c662baf08675ce3b3331e08f4f2907cc58
5
5
  SHA512:
6
- metadata.gz: 00ac0ad1897f1d04520f4653aec941e6d9c0dfeb7d13656a4ecb1eff6a3cffe24af0b839447f1324ac11c6eb6c40e00b01e1cba6c3fa86a5d21871576b4bb2d2
7
- data.tar.gz: fad5645c4a8befb085b4617db5542117bf593f1475cc5a65ccbf29b207e4c5ea9fd938bb503d2fa35e66b652ecc01fae269d68c5c60f2e895055cd8c3b0190b3
6
+ metadata.gz: 356bdf1b684829c5d506d00d303db0473f1cacdfd70d7831c21db201d465dd51fd3c03aa7a63f1cebc5658f6fe98321d6cc2bbc99c6f049753325f8898bb7479
7
+ data.tar.gz: ecad319106eb238264d3155740c36960521a70448183946c1ed1646d8b1b5291345d7eef8c0463a68e5cc55b0efe5d7687574d1ac2d869eeaaac08b73bafed81
data/API.md CHANGED
@@ -25,7 +25,7 @@ carson <command> [subcommand] [arguments]
25
25
  | Command | Purpose |
26
26
  |---|---|
27
27
  | `carson audit` | Evaluate governance status and generate report output. |
28
- | `carson deliver` | Start autonomous branch delivery for the current checkout: push, create or refresh PR, record delivery state, and return immediately. |
28
+ | `carson deliver [--commit MESSAGE]` | Start autonomous branch delivery for the current checkout. Plain `deliver` transports existing commits only; `--commit` creates one all-dirty delivery commit first, then pushes, creates or refreshes the PR, records delivery state, and returns immediately. |
29
29
  | `carson sync` | Fast-forward local `main` from configured remote when tree is clean. |
30
30
  | `carson prune` | Remove stale local branches whose upstream refs no longer exist. |
31
31
  | `carson template check` | Detect drift between managed templates and host `.github/*` files. |
data/MANUAL.md CHANGED
@@ -152,12 +152,14 @@ carson worktree create my-feature
152
152
  cd /path/to/.claude/worktrees/my-feature
153
153
  ```
154
154
 
155
- **2. Work** — make changes, commit, iterate.
155
+ **2. Work** — make changes, test them, and either commit normally or let Carson create the delivery commit.
156
156
 
157
- **3. Hand the branch to Carson** — `deliver` is the asynchronous branch handoff. In remote authority Carson pushes the branch, creates or refreshes the PR, records delivery state, and returns immediately. Managed template drift is corrected and committed automatically before push (3.23.0+).
157
+ **3. Hand the branch to Carson** — `deliver` is the asynchronous branch handoff. In remote authority Carson pushes the branch, creates or refreshes the PR, records delivery state, and returns immediately. Plain `carson deliver` transports existing commits and blocks if the worktree is dirty. `carson deliver --commit "..."` creates one all-dirty agent-authored commit first, then continues the same delivery flow. Managed template drift is still corrected in a separate Carson-managed commit before push.
158
158
 
159
159
  ```bash
160
160
  carson deliver
161
+ # or, if the worktree is still dirty:
162
+ carson deliver --commit "fix: describe this delivery"
161
163
  # Output: PR #N, Delivery: queued|gated
162
164
  # Next: carson status
163
165
  ```
data/README.md CHANGED
@@ -60,15 +60,15 @@ carson onboard your/repo/path
60
60
  carson worktree create your-worktree
61
61
  cd your/repo/path/.claude/worktrees/your-worktree
62
62
 
63
- # work, test, commit
64
- carson deliver
63
+ # work and test, then either commit yourself or let Carson create the delivery commit
64
+ carson deliver --commit "fix: describe this delivery"
65
65
  carson status
66
66
 
67
67
  # keep govern running to advance queued deliveries
68
68
  carson govern --loop 300
69
69
  ```
70
70
 
71
- By default, repositories onboard as `remote`. `carson deliver` is the branch handoff: it pushes the branch, creates or refreshes the PR, records delivery state, and returns immediately. `carson status` shows the active branch deliveries, and `carson govern` advances queued work across the governed portfolio.
71
+ By default, repositories onboard as `remote`. `carson deliver` is the branch handoff: it pushes the branch, creates or refreshes the PR, records delivery state, and returns immediately. Use plain `carson deliver` when the branch is already committed. Use `carson deliver --commit "..."` when the worktree is dirty and Carson should create one all-dirty delivery commit first. `carson status` shows the active branch deliveries, and `carson govern` advances queued work across the governed portfolio.
72
72
 
73
73
  ## Portfolio Layer
74
74
 
data/RELEASE.md CHANGED
@@ -5,6 +5,23 @@ 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
+ ## 3.24.0
9
+
10
+ ### What changed
11
+
12
+ - **Govern now finds worktree-created deliveries** — `repository_record` stored the worktree CWD as `repo_path` in the ledger, but `govern` looked up deliveries by the main tree path from config. The SQL query never matched, so `carson govern` always reported "no active deliveries" for worktree-created PRs. Now `repository_record` uses `main_worktree_root` so the ledger key is always the canonical main tree path. Also fixed the govern fallback path, `reconcile_delivery!`, `housekeep_repo!`, and `review_evidence` for the same mismatch.
13
+ - **Status shows canonical repository name** — `carson status` from a worktree displayed the worktree folder name (e.g. `feature-branch`) as `Repository:` instead of the actual repository name. Now correctly shows the canonical name.
14
+
15
+ ### No migration required
16
+
17
+ ## 3.23.3
18
+
19
+ ### What changed
20
+
21
+ - **Fix: review gate failed with "accepts at most 1 arg(s), received 3"** — `current_pull_request_for_branch` placed `--json` after the `--` end-of-options separator, causing `gh pr view` to treat it as a positional argument instead of a flag. Flags now precede `--`.
22
+
23
+ ### No migration required
24
+
8
25
  ## 3.23.2
9
26
 
10
27
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.23.2
1
+ 3.24.0
data/lib/carson/cli.rb CHANGED
@@ -550,22 +550,29 @@ module Carson
550
550
  return { command: :invalid }
551
551
  end
552
552
 
553
- options = { json: false, title: nil, body_file: nil }
553
+ options = { json: false, title: nil, body_file: nil, commit_message: nil }
554
554
  deliver_parser = OptionParser.new do |parser|
555
- parser.banner = "Usage: carson deliver [--json] [--title TITLE] [--body-file PATH]"
555
+ parser.banner = "Usage: carson deliver [--json] [--title TITLE] [--body-file PATH] [--commit MESSAGE]"
556
556
  parser.separator ""
557
557
  parser.separator "Push the current branch, create or refresh the pull request, and hand the branch to Carson."
558
- parser.separator "Carson records delivery state and continues from there."
558
+ parser.separator "Use --commit to create one all-dirty delivery commit before Carson pushes and opens the PR."
559
559
  parser.separator ""
560
560
  parser.separator "Options:"
561
561
  parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
562
562
  parser.on( "--title TITLE", "PR title (defaults to branch name)" ) { |value| options[ :title ] = value }
563
563
  parser.on( "--body-file PATH", "File containing PR body text" ) { |value| options[ :body_file ] = value }
564
+ parser.on( "--commit MESSAGE", "Commit all dirty user changes before delivery" ) { |value| options[ :commit_message ] = value }
564
565
  parser.separator ""
565
566
  parser.separator "Examples:"
566
- parser.separator " carson deliver Push, open a PR, and register delivery state"
567
+ parser.separator " carson deliver Deliver existing commits"
568
+ parser.separator " carson deliver --commit \"fix: harden flow\" Commit dirty changes, then deliver"
567
569
  end
568
570
  deliver_parser.parse!( arguments )
571
+ if options.fetch( :commit_message, nil ).to_s.strip.empty? && !options.fetch( :commit_message, nil ).nil?
572
+ error.puts "#{BADGE} --commit requires a non-empty message"
573
+ error.puts deliver_parser
574
+ return { command: :invalid }
575
+ end
569
576
  unless arguments.empty?
570
577
  error.puts "#{BADGE} Unexpected arguments for deliver: #{arguments.join( ' ' )}"
571
578
  error.puts deliver_parser
@@ -575,7 +582,8 @@ module Carson
575
582
  command: "deliver",
576
583
  json: options.fetch( :json ),
577
584
  title: options[ :title ],
578
- body_file: options[ :body_file ]
585
+ body_file: options[ :body_file ],
586
+ commit_message: options[ :commit_message ]
579
587
  }
580
588
  rescue OptionParser::ParseError => exception
581
589
  error.puts "#{BADGE} #{exception.message}"
@@ -763,6 +771,7 @@ module Carson
763
771
  runtime.deliver!(
764
772
  title: parsed.fetch( :title, nil ),
765
773
  body_file: parsed.fetch( :body_file, nil ),
774
+ commit_message: parsed.fetch( :commit_message, nil ),
766
775
  json_output: parsed.fetch( :json, false )
767
776
  )
768
777
  when "review:gate"
@@ -5,7 +5,8 @@ module Carson
5
5
  module Deliver
6
6
  # Entry point for `carson deliver`.
7
7
  # Pushes the current branch, ensures a PR exists, records delivery state, and returns.
8
- def deliver!( title: nil, body_file: nil, json_output: false )
8
+ # When --commit is supplied, Carson creates one all-dirty agent-authored commit first.
9
+ def deliver!( title: nil, body_file: nil, commit_message: nil, json_output: false )
9
10
  branch_name = current_branch
10
11
  main_branch = config.main_branch
11
12
  remote_name = config.git_remote
@@ -17,11 +18,38 @@ module Carson
17
18
  return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
18
19
  end
19
20
 
21
+ initial_dirty = working_tree_dirty?
22
+ if initial_dirty && commit_message.to_s.strip.empty?
23
+ result[ :error ] = "working tree is dirty"
24
+ result[ :recovery ] = "carson deliver --commit \"describe this delivery\""
25
+ return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
26
+ end
27
+
28
+ if !initial_dirty && !commit_message.to_s.strip.empty?
29
+ result[ :commit ] = blocked_commit_payload(
30
+ message: commit_message,
31
+ summary: "blocked — working tree is already clean"
32
+ )
33
+ result[ :error ] = "working tree is already clean"
34
+ result[ :recovery ] = "carson deliver"
35
+ return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
36
+ end
37
+
20
38
  sync_exit, sync_diagnostics = deliver_template_sync
21
39
  if sync_exit == EXIT_ERROR
22
40
  result[ :error ] = sync_diagnostics.to_s.strip.empty? ? "template sync failed" : sync_diagnostics.strip
23
41
  return deliver_finish( result: result, exit_code: sync_exit, json_output: json_output )
24
42
  end
43
+ template_sync_committed = sync_exit == EXIT_BLOCK
44
+
45
+ unless commit_message.to_s.strip.empty?
46
+ commit_exit = prepare_delivery_commit!(
47
+ commit_message: commit_message,
48
+ template_sync_committed: template_sync_committed,
49
+ result: result
50
+ )
51
+ return deliver_finish( result: result, exit_code: commit_exit, json_output: json_output ) unless commit_exit == EXIT_OK
52
+ end
25
53
 
26
54
  push_exit = push_branch!( branch: branch_name, remote: remote_name, result: result )
27
55
  return deliver_finish( result: result, exit_code: push_exit, json_output: json_output ) unless push_exit == EXIT_OK
@@ -61,6 +89,93 @@ module Carson
61
89
 
62
90
  private
63
91
 
92
+ def prepare_delivery_commit!( commit_message:, template_sync_committed:, result: )
93
+ if working_tree_dirty?
94
+ return create_delivery_commit!( commit_message: commit_message, result: result )
95
+ end
96
+
97
+ if template_sync_committed
98
+ result[ :commit ] = skipped_commit_payload(
99
+ message: commit_message,
100
+ summary: "skipped — template sync committed all pending changes"
101
+ )
102
+ return EXIT_OK
103
+ end
104
+
105
+ # The caller blocks the ordinary clean-tree case before template sync.
106
+ # Keep this branch as a post-sync safety net so future sequencing changes
107
+ # do not silently turn a clean tree into a successful no-op commit request.
108
+ result[ :commit ] = blocked_commit_payload(
109
+ message: commit_message,
110
+ summary: "blocked — working tree is already clean"
111
+ )
112
+ result[ :error ] = "working tree is already clean"
113
+ result[ :recovery ] = "carson deliver"
114
+ EXIT_BLOCK
115
+ end
116
+
117
+ def create_delivery_commit!( commit_message:, result: )
118
+ _, add_stderr, add_success, = git_run( "add", "-A" )
119
+ unless add_success
120
+ error_text = add_stderr.to_s.strip
121
+ error_text = "git add failed" if error_text.empty?
122
+ result[ :commit ] = blocked_commit_payload(
123
+ message: commit_message,
124
+ summary: "blocked — #{error_text}"
125
+ )
126
+ result[ :error ] = error_text
127
+ result[ :recovery ] = "git status"
128
+ return EXIT_ERROR
129
+ end
130
+
131
+ commit_stdout, commit_stderr, commit_success, = git_run( "commit", "-m", commit_message )
132
+ unless commit_success
133
+ error_text = [ commit_stderr.to_s.strip, commit_stdout.to_s.strip ].reject( &:empty? ).join( " | " )
134
+ error_text = "git commit failed" if error_text.empty?
135
+ result[ :commit ] = blocked_commit_payload(
136
+ message: commit_message,
137
+ summary: "blocked — #{error_text}"
138
+ )
139
+ result[ :error ] = error_text
140
+ result[ :recovery ] = "git status"
141
+ return EXIT_ERROR
142
+ end
143
+
144
+ result[ :commit ] = created_commit_payload(
145
+ message: commit_message,
146
+ head: current_head,
147
+ summary: "created agent-authored commit"
148
+ )
149
+ EXIT_OK
150
+ end
151
+
152
+ def created_commit_payload( message:, head:, summary: )
153
+ {
154
+ status: "created",
155
+ message: message,
156
+ head: head,
157
+ summary: summary
158
+ }
159
+ end
160
+
161
+ def skipped_commit_payload( message:, summary: )
162
+ {
163
+ status: "skipped",
164
+ message: message,
165
+ head: nil,
166
+ summary: summary
167
+ }
168
+ end
169
+
170
+ def blocked_commit_payload( message:, summary: )
171
+ {
172
+ status: "blocked",
173
+ message: message,
174
+ head: nil,
175
+ summary: summary
176
+ }
177
+ end
178
+
64
179
  def deliver_template_sync
65
180
  saved_output, saved_error = @output, @error
66
181
  captured_out = StringIO.new
@@ -135,6 +250,9 @@ module Carson
135
250
  return
136
251
  end
137
252
 
253
+ if result[ :commit ]
254
+ puts_line "Commit: #{result.dig( :commit, :summary )}"
255
+ end
138
256
  puts_line "PR: ##{result[ :pr_number ]} #{result[ :pr_url ]}" if result[ :pr_number ]
139
257
  if result[ :delivery ]
140
258
  puts_line "Delivery: #{result.dig( :delivery, :status )}"
@@ -18,7 +18,7 @@ module Carson
18
18
  def govern_cycle!( dry_run:, json_output: )
19
19
  print_header "Carson Govern"
20
20
  repositories = governed_repo_paths
21
- repositories = [ repo_root ] if repositories.empty?
21
+ repositories = [ repository_record.path ] if repositories.empty?
22
22
  puts_line "governing #{repositories.length} repo#{plural_suffix( count: repositories.length )}"
23
23
 
24
24
  report = {
@@ -64,7 +64,7 @@ module Carson
64
64
  end
65
65
 
66
66
  def govern_repo!( repo_path:, dry_run: )
67
- scoped_runtime = repo_path == repo_root ? self : build_scoped_runtime( repo_path: repo_path )
67
+ scoped_runtime = repo_runtime_for( repo_path: repo_path )
68
68
  repository = Repository.new( path: repo_path, authority: scoped_runtime.config.govern_authority, runtime: scoped_runtime )
69
69
  deliveries = scoped_runtime.ledger.active_deliveries( repo_path: repo_path )
70
70
 
@@ -108,7 +108,7 @@ module Carson
108
108
  end
109
109
 
110
110
  def reconcile_delivery!( delivery: )
111
- branch = Repository.new( path: repo_root, authority: config.govern_authority, runtime: self ).branch( delivery.branch ).reload
111
+ branch = repository_record.branch( delivery.branch ).reload
112
112
  if branch.head && branch.head != delivery.head
113
113
  return ledger.update_delivery(
114
114
  delivery: delivery,
@@ -288,7 +288,7 @@ module Carson
288
288
  end
289
289
 
290
290
  def housekeep_repo!( repo_path: )
291
- scoped_runtime = repo_path == repo_root ? self : build_scoped_runtime( repo_path: repo_path )
291
+ scoped_runtime = repo_runtime_for( repo_path: repo_path )
292
292
  sync_status = scoped_runtime.sync!
293
293
  scoped_runtime.prune! if sync_status == EXIT_OK
294
294
  end
@@ -373,7 +373,7 @@ module Carson
373
373
  end
374
374
 
375
375
  def review_evidence( delivery:, repo_path: )
376
- repo_runtime = repo_path == repo_root ? self : build_scoped_runtime( repo_path: repo_path )
376
+ repo_runtime = repo_runtime_for( repo_path: repo_path )
377
377
  owner, repo = repo_runtime.send( :repository_coordinates )
378
378
  details = repo_runtime.send( :pull_request_details, owner: owner, repo: repo, pr_number: delivery.pull_request_number )
379
379
  pr_author = details.dig( :author, :login ).to_s
@@ -399,6 +399,10 @@ module Carson
399
399
  { summary: revision.summary.to_s, dispatched_at: revision.started_at.to_s }
400
400
  end
401
401
 
402
+ def repo_runtime_for( repo_path: )
403
+ realpath_safe( repo_path ) == realpath_safe( repo_root ) ? self : build_scoped_runtime( repo_path: repo_path )
404
+ end
405
+
402
406
  def thread_body( details:, url: )
403
407
  Array( details[ :review_threads ] ).each do |thread|
404
408
  thread[ :comments ].each do |comment|
@@ -121,7 +121,7 @@ module Carson
121
121
 
122
122
  # Pull request selected by current branch; nil is returned when no PR exists.
123
123
  def current_pull_request_for_branch( branch_name: )
124
- stdout_text, stderr_text, success, = gh_run( "pr", "view", "--", branch_name, "--json", "number,title,url,state" )
124
+ stdout_text, stderr_text, success, = gh_run( "pr", "view", "--json", "number,title,url,state", "--", branch_name )
125
125
  unless success
126
126
  error_text = gh_error_text( stdout_text: stdout_text, stderr_text: stderr_text, fallback: "unable to read PR for branch #{branch_name}" )
127
127
  return nil if error_text.downcase.include?( "no pull requests found" )
@@ -54,7 +54,7 @@ module Carson
54
54
  def gather_status
55
55
  repository = repository_record
56
56
  branch = branch_record
57
- deliveries = ledger.active_deliveries( repo_path: repo_root )
57
+ deliveries = ledger.active_deliveries( repo_path: repository.path )
58
58
 
59
59
  {
60
60
  version: Carson::VERSION,
@@ -87,8 +87,11 @@ module Carson
87
87
  end
88
88
 
89
89
  # Passive repository record for the current runtime context.
90
+ # Uses main_worktree_root so the repo_path stored in the ledger is always the
91
+ # canonical main tree path, regardless of which worktree the command runs from.
92
+ # This ensures govern (which looks up by main tree path) finds worktree deliveries.
90
93
  def repository_record
91
- Repository.new( path: repo_root, authority: config.govern_authority, runtime: self )
94
+ Repository.new( path: main_worktree_root, authority: config.govern_authority, runtime: self )
92
95
  end
93
96
 
94
97
  # Passive branch record for the current checkout.
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.23.2
4
+ version: 3.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang