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 +4 -4
- data/RELEASE.md +11 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +39 -2
- data/lib/carson/runtime/local/onboard.rb +42 -0
- data/lib/carson/runtime/local/worktree.rb +103 -0
- data/lib/carson/runtime/local.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96820bedb9187d90787072b441c35129457a7d45202899ea520a9bb1e3b50876
|
|
4
|
+
data.tar.gz: f8963204bde45cee5e7e662dcc5f5bb947e84372f0d24a03d6ef494c3653adbf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
data/lib/carson/runtime/local.rb
CHANGED
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.
|
|
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
|