carson 2.29.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 +22 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +39 -2
- data/lib/carson/runtime/audit.rb +0 -1
- data/lib/carson/runtime/local/onboard.rb +42 -0
- data/lib/carson/runtime/local/prune.rb +17 -1
- 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,28 @@ 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
|
+
|
|
19
|
+
## 2.30.0 — Prune Rebase Merge Support + Audit Noise Reduction
|
|
20
|
+
|
|
21
|
+
### What changed
|
|
22
|
+
|
|
23
|
+
- **`carson prune` now handles rebase and cherry-pick merges.** When a stale branch fails SHA-based merged PR evidence (commit hashes change after rebase), prune falls back to content-level comparison against main. If the branch content is already absorbed into main, it is safely deleted.
|
|
24
|
+
- **Canonical templates hint removed from concise audit output.** The "canonical templates not configured" message no longer appears on every commit. Verbose mode retains it for debugging.
|
|
25
|
+
|
|
26
|
+
### Migration
|
|
27
|
+
|
|
28
|
+
- No action required. Prune behaviour is strictly more capable; existing workflows are unaffected.
|
|
29
|
+
|
|
8
30
|
## 2.29.0 — Audit Concise Output UX
|
|
9
31
|
|
|
10
32
|
### 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"
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -102,7 +102,6 @@ module Carson
|
|
|
102
102
|
puts_verbose ""
|
|
103
103
|
puts_verbose "[Canonical Templates]"
|
|
104
104
|
puts_verbose "HINT: canonical templates not configured — run carson setup to enable."
|
|
105
|
-
audit_concise_problems << "Hint: canonical templates not configured — run carson setup to enable."
|
|
106
105
|
end
|
|
107
106
|
write_and_print_pr_monitor_report(
|
|
108
107
|
report: monitor_report.merge(
|
|
@@ -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 ""
|
|
@@ -305,6 +305,8 @@ module Carson
|
|
|
305
305
|
end
|
|
306
306
|
|
|
307
307
|
# Guarded force-delete policy for stale branches.
|
|
308
|
+
# Checks merged PR evidence first (exact SHA match), then falls back to
|
|
309
|
+
# absorbed-into-main detection (covers rebase merges where commit hashes change).
|
|
308
310
|
def force_delete_evidence_for_stale_branch( branch:, delete_error_text: )
|
|
309
311
|
return [ nil, "safe delete failure is not merge-related" ] unless non_merged_delete_error?( error_text: delete_error_text )
|
|
310
312
|
return [ nil, "gh CLI not available; cannot verify merged PR evidence" ] unless gh_available?
|
|
@@ -318,7 +320,21 @@ module Carson
|
|
|
318
320
|
branch_tip_sha = tip_sha_text.to_s.strip
|
|
319
321
|
return [ nil, "unable to read local branch tip sha" ] if branch_tip_sha.empty?
|
|
320
322
|
|
|
321
|
-
merged_pr_for_branch( branch: branch, branch_tip_sha: branch_tip_sha )
|
|
323
|
+
merged_pr, error = merged_pr_for_branch( branch: branch, branch_tip_sha: branch_tip_sha )
|
|
324
|
+
return [ merged_pr, error ] unless merged_pr.nil?
|
|
325
|
+
|
|
326
|
+
# Fallback: branch content is already on main (rebase/cherry-pick merges rewrite SHAs).
|
|
327
|
+
if branch_absorbed_into_main?( branch: branch )
|
|
328
|
+
absorbed_evidence = {
|
|
329
|
+
number: nil,
|
|
330
|
+
url: "absorbed into #{config.main_branch}",
|
|
331
|
+
merged_at: Time.now.utc.iso8601,
|
|
332
|
+
head_sha: branch_tip_sha
|
|
333
|
+
}
|
|
334
|
+
return [ absorbed_evidence, nil ]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
[ nil, error ]
|
|
322
338
|
end
|
|
323
339
|
|
|
324
340
|
# Finds merged PR evidence for the exact local branch tip.
|
|
@@ -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
|