carson 2.30.0 → 2.31.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: 374e4a7af2ec608b1c3c9d07ca4a5ad8b028715434c1f02fb4b4034a71b153d2
4
- data.tar.gz: a795fabcecbe2b43d03f155b0d4b730f804d0879ef4a1fe280cc4e082350c76f
3
+ metadata.gz: 96820bedb9187d90787072b441c35129457a7d45202899ea520a9bb1e3b50876
4
+ data.tar.gz: f8963204bde45cee5e7e662dcc5f5bb947e84372f0d24a03d6ef494c3653adbf
5
5
  SHA512:
6
- metadata.gz: 83b920da278fa2e457c424babaf3b06955a2317f920b5ad3681ae4da8a90fc5d5c3969d7160796b0febc49669a7cf7995c8f83ba674ce7f0865ca72eb6a48cbf
7
- data.tar.gz: 8506fe647fe4538a27dd0ef25e0ecbeb1d9c0b78c8251d45b6023e562091cd6c861ef660707684b4e3d202a69728b0779a8a623ceb79313efc7e86eb0b482b17
6
+ metadata.gz: 616dbf56c4c09e6327767ccaaa8b88595a51ae4e8275290d1b6f1349fb6b2e25cb3789e4891699b3e6b8ff878d209bf4503adcc4974506b1a72ddcee374e432d
7
+ data.tar.gz: 83b8827df9139e9a81fa587a00816333d37fc87d03a3f7f0388bbd3fb8722138978b7b99844ccbfa6713243a651c4886774f88400c226c060398f1824b64e7ba
data/RELEASE.md CHANGED
@@ -5,6 +5,17 @@ Release-note scope rule:
5
5
  - `RELEASE.md` records only version deltas, breaking changes, and migration actions.
6
6
  - Operational usage guides live in `MANUAL.md` and `API.md`.
7
7
 
8
+ ## 2.31.0 — Worktree Lifecycle + Prune --all
9
+
10
+ ### What changed
11
+
12
+ - **`carson worktree remove <name-or-path>`** — Safe worktree teardown in the correct order: removes the worktree directory and git registration, deletes the local branch, and cleans up the remote branch. Accepts either a full path or a short name (resolves under `.claude/worktrees/`). Prevents agents from being stranded in deleted directories.
13
+ - **`carson prune --all`** — Prunes stale branches across all governed repositories. Same discipline as single-repo prune, applied to the full estate.
14
+
15
+ ### Migration
16
+
17
+ - No action required. Both commands are additive.
18
+
8
19
  ## 2.30.0 — Prune Rebase Merge Support + Audit Noise Reduction
9
20
 
10
21
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.30.0
1
+ 2.31.0
data/lib/carson/cli.rb CHANGED
@@ -12,7 +12,7 @@ module Carson
12
12
  return Runtime::EXIT_OK
13
13
  end
14
14
 
15
- if command == "refresh:all"
15
+ if %w[refresh:all prune:all].include?( command )
16
16
  verbose = parsed.fetch( :verbose, false )
17
17
  runtime = Runtime.new( repo_root: repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
18
18
  return dispatch( parsed: parsed, runtime: runtime )
@@ -53,7 +53,7 @@ module Carson
53
53
 
54
54
  def self.build_parser
55
55
  OptionParser.new do |opts|
56
- opts.banner = "Usage: carson [setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]|audit|sync|prune|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
56
+ opts.banner = "Usage: carson [setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]|audit|sync|prune [--all]|worktree remove <name-or-path>|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
57
57
  end
58
58
  end
59
59
 
@@ -82,6 +82,10 @@ module Carson
82
82
  parse_refresh_command( argv: argv, parser: parser, err: err )
83
83
  when "template"
84
84
  parse_template_subcommand( argv: argv, parser: parser, err: err )
85
+ when "prune"
86
+ parse_prune_command( argv: argv, parser: parser, err: err )
87
+ when "worktree"
88
+ parse_worktree_subcommand( argv: argv, parser: parser, err: err )
85
89
  when "review"
86
90
  parse_named_subcommand( command: command, usage: "gate|sweep", argv: argv, parser: parser, err: err )
87
91
  when "govern"
@@ -154,6 +158,35 @@ module Carson
154
158
  }
155
159
  end
156
160
 
161
+ def self.parse_prune_command( argv:, parser:, err: )
162
+ all_flag = argv.delete( "--all" ) ? true : false
163
+ parser.parse!( argv )
164
+ return { command: "prune:all" } if all_flag
165
+ { command: "prune" }
166
+ end
167
+
168
+ def self.parse_worktree_subcommand( argv:, parser:, err: )
169
+ action = argv.shift
170
+ if action.to_s.strip.empty?
171
+ err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree remove <name-or-path>"
172
+ err.puts parser
173
+ return { command: :invalid }
174
+ end
175
+
176
+ case action
177
+ when "remove"
178
+ worktree_path = argv.shift
179
+ if worktree_path.to_s.strip.empty?
180
+ err.puts "#{BADGE} Missing path for worktree remove. Use: carson worktree remove <name-or-path>"
181
+ return { command: :invalid }
182
+ end
183
+ { command: "worktree:remove", worktree_path: worktree_path }
184
+ else
185
+ err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree remove <name-or-path>"
186
+ { command: :invalid }
187
+ end
188
+ end
189
+
157
190
  def self.parse_named_subcommand( command:, usage:, argv:, parser:, err: )
158
191
  action = argv.shift
159
192
  parser.parse!( argv )
@@ -240,6 +273,10 @@ module Carson
240
273
  runtime.sync!
241
274
  when "prune"
242
275
  runtime.prune!
276
+ when "prune:all"
277
+ runtime.prune_all!
278
+ when "worktree:remove"
279
+ runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ) )
243
280
  when "onboard"
244
281
  runtime.onboard!
245
282
  when "refresh"
@@ -115,6 +115,48 @@ module Carson
115
115
  failed.zero? ? EXIT_OK : EXIT_ERROR
116
116
  end
117
117
 
118
+ def prune_all!
119
+ repos = config.govern_repos
120
+ if repos.empty?
121
+ puts_line "No governed repositories configured."
122
+ puts_line " Run carson onboard in each repo to register."
123
+ return EXIT_ERROR
124
+ end
125
+
126
+ puts_line ""
127
+ puts_line "Prune all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
128
+ succeeded = 0
129
+ failed = 0
130
+
131
+ repos.each do |repo_path|
132
+ repo_name = File.basename( repo_path )
133
+ unless Dir.exist?( repo_path )
134
+ puts_line "#{repo_name}: FAIL (path not found)"
135
+ failed += 1
136
+ next
137
+ end
138
+
139
+ begin
140
+ buf = verbose? ? out : StringIO.new
141
+ err_buf = verbose? ? err : StringIO.new
142
+ rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? )
143
+ status = rt.prune!
144
+ unless verbose?
145
+ summary = buf.string.lines.last.to_s.strip
146
+ puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
147
+ end
148
+ status == EXIT_ERROR ? ( failed += 1 ) : ( succeeded += 1 )
149
+ rescue StandardError => e
150
+ puts_line "#{repo_name}: FAIL (#{e.message})"
151
+ failed += 1
152
+ end
153
+ end
154
+
155
+ puts_line ""
156
+ puts_line "Prune all complete: #{succeeded} pruned, #{failed} failed."
157
+ failed.zero? ? EXIT_OK : EXIT_ERROR
158
+ end
159
+
118
160
  # Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
119
161
  def offboard!
120
162
  puts_verbose ""
@@ -0,0 +1,103 @@
1
+ module Carson
2
+ class Runtime
3
+ module Local
4
+ # Safe worktree lifecycle management for coding agents.
5
+ # Enforces the teardown order: exit worktree → git worktree remove → branch cleanup.
6
+ def worktree_remove!( worktree_path: )
7
+ fingerprint_status = block_if_outsider_fingerprints!
8
+ return fingerprint_status unless fingerprint_status.nil?
9
+
10
+ resolved_path = resolve_worktree_path( worktree_path: worktree_path )
11
+
12
+ unless worktree_registered?( path: resolved_path )
13
+ puts_line "ERROR: #{resolved_path} is not a registered worktree."
14
+ puts_line " Registered worktrees:"
15
+ worktree_list.each { |wt| puts_line " - #{wt.fetch( :path )} [#{wt.fetch( :branch )}]" }
16
+ return EXIT_ERROR
17
+ end
18
+
19
+ branch = worktree_branch( path: resolved_path )
20
+ puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch}"
21
+
22
+ # Step 1: remove the worktree (directory + git registration).
23
+ rm_stdout, rm_stderr, rm_success, = git_run( "worktree", "remove", "--force", resolved_path )
24
+ unless rm_success
25
+ error_text = rm_stderr.to_s.strip
26
+ error_text = "unable to remove worktree" if error_text.empty?
27
+ puts_line "ERROR: #{error_text}"
28
+ return EXIT_ERROR
29
+ end
30
+ puts_verbose "worktree_removed: #{resolved_path}"
31
+
32
+ # Step 2: delete the local branch.
33
+ if branch && !config.protected_branches.include?( branch )
34
+ _, del_stderr, del_success, = git_run( "branch", "-D", branch )
35
+ if del_success
36
+ puts_verbose "branch_deleted: #{branch}"
37
+ else
38
+ puts_verbose "branch_delete_skipped: #{branch} reason=#{del_stderr.to_s.strip}"
39
+ end
40
+ end
41
+
42
+ # Step 3: delete the remote branch (best-effort).
43
+ if branch && !config.protected_branches.include?( branch )
44
+ remote_branch = branch
45
+ git_run( "push", config.git_remote, "--delete", remote_branch )
46
+ puts_verbose "remote_branch_deleted: #{config.git_remote}/#{remote_branch}"
47
+ end
48
+
49
+ unless verbose?
50
+ puts_line "Worktree removed: #{File.basename( resolved_path )}"
51
+ end
52
+ EXIT_OK
53
+ end
54
+
55
+ private
56
+
57
+ # Resolves a worktree path: if it's a bare name, look under .claude/worktrees/.
58
+ def resolve_worktree_path( worktree_path: )
59
+ return File.expand_path( worktree_path ) if worktree_path.include?( "/" )
60
+
61
+ candidate = File.join( repo_root, ".claude", "worktrees", worktree_path )
62
+ return candidate if Dir.exist?( candidate )
63
+
64
+ File.expand_path( worktree_path )
65
+ end
66
+
67
+ # Returns true if the path is a registered git worktree.
68
+ def worktree_registered?( path: )
69
+ worktree_list.any? { |wt| wt.fetch( :path ) == path }
70
+ end
71
+
72
+ # Returns the branch name checked out in a worktree, or nil.
73
+ def worktree_branch( path: )
74
+ entry = worktree_list.find { |wt| wt.fetch( :path ) == path }
75
+ entry&.fetch( :branch, nil )
76
+ end
77
+
78
+ # Parses `git worktree list --porcelain` into structured entries.
79
+ def worktree_list
80
+ output = git_capture!( "worktree", "list", "--porcelain" )
81
+ entries = []
82
+ current = {}
83
+ output.lines.each do |line|
84
+ line = line.strip
85
+ if line.empty?
86
+ entries << current unless current.empty?
87
+ current = {}
88
+ elsif line.start_with?( "worktree " )
89
+ current[ :path ] = line.sub( "worktree ", "" )
90
+ elsif line.start_with?( "branch " )
91
+ current[ :branch ] = line.sub( "branch refs/heads/", "" )
92
+ elsif line == "detached"
93
+ current[ :branch ] = nil
94
+ end
95
+ end
96
+ entries << current unless current.empty?
97
+ entries
98
+ end
99
+ end
100
+
101
+ include Local
102
+ end
103
+ end
@@ -3,6 +3,7 @@ require_relative "local/prune"
3
3
  require_relative "local/template"
4
4
  require_relative "local/hooks"
5
5
  require_relative "local/onboard"
6
+ require_relative "local/worktree"
6
7
 
7
8
  module Carson
8
9
  class Runtime
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carson
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.30.0
4
+ version: 2.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -59,6 +59,7 @@ files:
59
59
  - lib/carson/runtime/local/prune.rb
60
60
  - lib/carson/runtime/local/sync.rb
61
61
  - lib/carson/runtime/local/template.rb
62
+ - lib/carson/runtime/local/worktree.rb
62
63
  - lib/carson/runtime/review.rb
63
64
  - lib/carson/runtime/review/data_access.rb
64
65
  - lib/carson/runtime/review/gate_support.rb