carson 3.0.0 → 3.1.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 +18 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +17 -3
- data/lib/carson/runtime/local/worktree.rb +77 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 046c14a614687e668eae6d9f3df0eadbe02fcb103a1c93f25657791a0d72962c
|
|
4
|
+
data.tar.gz: 26ebca4203a0a9b3df1e38795580188bcf6951049304ae0eb4899deda35106f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38cb7ebf2e65293d5ea26bfb527314d86f7889949db033649fc49822b65feb7b7ea0e8fa5f35b6d959024ff4d280691432efde50dc15c37bf223665cd7d908b7
|
|
7
|
+
data.tar.gz: d05902ee1637919dd0a9781fa2de46e4370d2806df83c02c730584095f18811baea6168f3614d81900229dd5ec3c141ffb2ce6a40a8c338f233aa1a88a2a9b81
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.1.0 — Worktree Lifecycle
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson worktree create <name>`** — creates a worktree under `.claude/worktrees/<name>` with a new branch based on main. One command, one result: path and branch name reported.
|
|
13
|
+
- **`carson worktree done <name>`** — marks a worktree as completed without deleting it. Verifies all changes are committed and pushed. Blocks with actionable guidance if uncommitted changes or unpushed commits exist. The worktree directory persists for batch cleanup later.
|
|
14
|
+
- **Deferred deletion model.** The full worktree lifecycle is now: create → work → done → batch cleanup. No worktree is deleted during an active session. Cleanup happens later via `carson worktree remove` or `carson housekeep`.
|
|
15
|
+
|
|
16
|
+
### UX
|
|
17
|
+
|
|
18
|
+
- `carson worktree create` reports path and branch — ready to `cd` into immediately.
|
|
19
|
+
- `carson worktree done` gives recovery commands when changes are uncommitted or unpushed.
|
|
20
|
+
- Error messages across worktree subcommands now show `create|done|remove` in usage hints.
|
|
21
|
+
|
|
22
|
+
### Migration
|
|
23
|
+
|
|
24
|
+
- No breaking changes. `carson worktree remove` continues to work as before.
|
|
25
|
+
|
|
8
26
|
## 3.0.0 — Agent-Oriented Carson
|
|
9
27
|
|
|
10
28
|
### Theme
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.1.0
|
data/lib/carson/cli.rb
CHANGED
|
@@ -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 [status [--json]|setup
|
|
56
|
+
opts.banner = "Usage: carson [status [--json]|setup|audit|sync|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -170,12 +170,22 @@ module Carson
|
|
|
170
170
|
def self.parse_worktree_subcommand( argv:, parser:, err: )
|
|
171
171
|
action = argv.shift
|
|
172
172
|
if action.to_s.strip.empty?
|
|
173
|
-
err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree remove <name
|
|
173
|
+
err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree create|done|remove <name>"
|
|
174
174
|
err.puts parser
|
|
175
175
|
return { command: :invalid }
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
case action
|
|
179
|
+
when "create"
|
|
180
|
+
name = argv.shift
|
|
181
|
+
if name.to_s.strip.empty?
|
|
182
|
+
err.puts "#{BADGE} Missing name for worktree create. Use: carson worktree create <name>"
|
|
183
|
+
return { command: :invalid }
|
|
184
|
+
end
|
|
185
|
+
{ command: "worktree:create", worktree_name: name }
|
|
186
|
+
when "done"
|
|
187
|
+
name = argv.shift
|
|
188
|
+
{ command: "worktree:done", worktree_name: name }
|
|
179
189
|
when "remove"
|
|
180
190
|
force = argv.delete( "--force" ) ? true : false
|
|
181
191
|
worktree_path = argv.shift
|
|
@@ -185,7 +195,7 @@ module Carson
|
|
|
185
195
|
end
|
|
186
196
|
{ command: "worktree:remove", worktree_path: worktree_path, force: force }
|
|
187
197
|
else
|
|
188
|
-
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree remove <name
|
|
198
|
+
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree create|done|remove <name>"
|
|
189
199
|
{ command: :invalid }
|
|
190
200
|
end
|
|
191
201
|
end
|
|
@@ -289,6 +299,10 @@ module Carson
|
|
|
289
299
|
runtime.prune!
|
|
290
300
|
when "prune:all"
|
|
291
301
|
runtime.prune_all!
|
|
302
|
+
when "worktree:create"
|
|
303
|
+
runtime.worktree_create!( name: parsed.fetch( :worktree_name ) )
|
|
304
|
+
when "worktree:done"
|
|
305
|
+
runtime.worktree_done!( name: parsed.fetch( :worktree_name, nil ) )
|
|
292
306
|
when "worktree:remove"
|
|
293
307
|
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ) )
|
|
294
308
|
when "onboard"
|
|
@@ -2,7 +2,83 @@ module Carson
|
|
|
2
2
|
class Runtime
|
|
3
3
|
module Local
|
|
4
4
|
# Safe worktree lifecycle management for coding agents.
|
|
5
|
-
#
|
|
5
|
+
# Three operations: create, done (mark completed), remove (batch cleanup).
|
|
6
|
+
# The deferred deletion model: worktrees persist after use, cleaned up later.
|
|
7
|
+
|
|
8
|
+
# Creates a new worktree under .claude/worktrees/<name> with a fresh branch.
|
|
9
|
+
def worktree_create!( name: )
|
|
10
|
+
worktrees_dir = File.join( repo_root, ".claude", "worktrees" )
|
|
11
|
+
wt_path = File.join( worktrees_dir, name )
|
|
12
|
+
|
|
13
|
+
if Dir.exist?( wt_path )
|
|
14
|
+
puts_line "ERROR: worktree already exists: #{name}"
|
|
15
|
+
puts_line " Path: #{wt_path}"
|
|
16
|
+
return EXIT_ERROR
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Determine the base branch (main branch from config).
|
|
20
|
+
base = config.main_branch
|
|
21
|
+
|
|
22
|
+
# Create the worktree with a new branch based on the main branch.
|
|
23
|
+
FileUtils.mkdir_p( worktrees_dir )
|
|
24
|
+
_, wt_stderr, wt_success, = git_run( "worktree", "add", wt_path, "-b", name, base )
|
|
25
|
+
unless wt_success
|
|
26
|
+
error_text = wt_stderr.to_s.strip
|
|
27
|
+
error_text = "unable to create worktree" if error_text.empty?
|
|
28
|
+
puts_line "ERROR: #{error_text}"
|
|
29
|
+
return EXIT_ERROR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts_line "Worktree created: #{name}"
|
|
33
|
+
puts_line " Path: #{wt_path}"
|
|
34
|
+
puts_line " Branch: #{name}"
|
|
35
|
+
EXIT_OK
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Marks a worktree as completed without deleting it.
|
|
39
|
+
# Verifies all changes are committed. Deferred deletion — cleanup happens later.
|
|
40
|
+
def worktree_done!( name: nil )
|
|
41
|
+
if name.to_s.strip.empty?
|
|
42
|
+
# Try to detect current worktree from CWD.
|
|
43
|
+
puts_line "ERROR: missing worktree name. Use: carson worktree done <name>"
|
|
44
|
+
return EXIT_ERROR
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
resolved_path = resolve_worktree_path( worktree_path: name )
|
|
48
|
+
|
|
49
|
+
unless worktree_registered?( path: resolved_path )
|
|
50
|
+
puts_line "ERROR: #{name} is not a registered worktree."
|
|
51
|
+
return EXIT_ERROR
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check for uncommitted changes in the worktree.
|
|
55
|
+
wt_status, _, status_success, = Open3.capture3( "git", "status", "--porcelain", chdir: resolved_path )
|
|
56
|
+
if status_success && !wt_status.strip.empty?
|
|
57
|
+
puts_line "Worktree has uncommitted changes: #{name}"
|
|
58
|
+
puts_line " Commit your changes first, then run `carson worktree done #{name}` again."
|
|
59
|
+
return EXIT_BLOCK
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check for unpushed commits.
|
|
63
|
+
branch = worktree_branch( path: resolved_path )
|
|
64
|
+
if branch
|
|
65
|
+
remote = config.git_remote
|
|
66
|
+
remote_ref = "#{remote}/#{branch}"
|
|
67
|
+
ahead, _, ahead_ok, = Open3.capture3( "git", "rev-list", "--count", "#{remote_ref}..#{branch}", chdir: resolved_path )
|
|
68
|
+
if ahead_ok && ahead.strip.to_i > 0
|
|
69
|
+
puts_line "Worktree has unpushed commits: #{name}"
|
|
70
|
+
puts_line " Push with `git -C #{resolved_path} push #{remote} #{branch}` first."
|
|
71
|
+
return EXIT_BLOCK
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts_line "Worktree done: #{name}"
|
|
76
|
+
puts_line " Branch: #{branch || '(detached)'}"
|
|
77
|
+
puts_line " Cleanup later with `carson worktree remove #{name}` or `carson housekeep`."
|
|
78
|
+
EXIT_OK
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Removes a worktree: directory, git registration, and branch.
|
|
6
82
|
# Never forces removal — if the worktree has uncommitted changes, refuses unless
|
|
7
83
|
# the user explicitly passes force: true via CLI --force flag.
|
|
8
84
|
def worktree_remove!( worktree_path:, force: false )
|