carson 4.3.5 → 4.5.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/MANUAL.md +2 -0
- data/README.md +4 -0
- data/RELEASE.md +26 -0
- data/VERSION +1 -1
- data/lib/carson/runtime/local/worktree.rb +2 -1
- data/lib/carson/warehouse/workbench.rb +48 -27
- data/lib/carson/worktree.rb +8 -15
- 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: a8eb2ac9b4d1ac6375fc581b2be86768ad5f441685b36dca3358e31aa43715b8
|
|
4
|
+
data.tar.gz: e2c7e9e3b3a4b35e26c8fbb7d59c46c10db8cf67b01000b0893d528a84055ce1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58f5388783aa5efbd5a74393c75b6552278bab58d06bc00bcea1a4467353aac70b7aa54685517d02062d61d0befa16898867f82b5ca9c7fc2a0908baa441cf94
|
|
7
|
+
data.tar.gz: d8e760dba0ca676a9a5c6e88d729102fa86f8c9ea205507fbe46821280c42593352bc985d486d6c82b377311cb5564820f06d7d15ddb63e15ee17ebab750780e
|
data/MANUAL.md
CHANGED
|
@@ -153,6 +153,8 @@ carson worktree create my-feature
|
|
|
153
153
|
cd /path/to/.claude/worktrees/my-feature
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
The worktree lives under the calling harness's agent directory — `.claude/worktrees/` from Claude Code, `.pi/worktrees/` from Pi (detected via `PI_CODING_AGENT`). Set `CARSON_AGENT_DIR` to override. Commands that take a worktree name resolve it across all agent directories, so worktrees created by another harness remain reachable.
|
|
157
|
+
|
|
156
158
|
On the governed main working tree, Carson blocks raw `git add` / `git commit` and blocks raw `git worktree add/remove`, raw `git pull --rebase`, and raw `gh pr create/merge`. Use `carson worktree create`, `carson sync`, and `carson deliver` instead.
|
|
157
159
|
|
|
158
160
|
**2. Work** — make changes, test them, and either commit normally or let Carson create the delivery commit.
|
data/README.md
CHANGED
|
@@ -51,6 +51,10 @@ carson onboard your/repo/path
|
|
|
51
51
|
carson worktree create your-worktree
|
|
52
52
|
cd your/repo/path/.claude/worktrees/your-worktree
|
|
53
53
|
|
|
54
|
+
# Carson places the worktree under the calling harness's agent directory:
|
|
55
|
+
# .claude/worktrees/ from Claude Code, .pi/worktrees/ from Pi
|
|
56
|
+
# (detected via PI_CODING_AGENT). Set CARSON_AGENT_DIR to override.
|
|
57
|
+
|
|
54
58
|
# work and test, then commit and hand the branch to Carson
|
|
55
59
|
git add -A
|
|
56
60
|
git commit -m "fix: describe this delivery"
|
data/RELEASE.md
CHANGED
|
@@ -7,6 +7,32 @@ Release-note scope rule:
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 4.5.0
|
|
11
|
+
|
|
12
|
+
### New
|
|
13
|
+
|
|
14
|
+
- **Harness-aware worktree directories.** `carson checkin` / `carson worktree create` place the worktree under the calling harness's agent directory — `.pi/worktrees/` from a Pi session (detected via `PI_CODING_AGENT`), `.claude/worktrees/` otherwise, exactly as before. Set `CARSON_AGENT_DIR` to override. Bare-name resolution, `.git/info/exclude` bookkeeping, and diagnostics follow the chosen directory, and commands that take a worktree name resolve across all agent directories, so worktrees created by another harness remain reachable.
|
|
15
|
+
- **Sweep and reap cover `.pi/worktrees/`.** Both agent-directory lists gained `.pi`.
|
|
16
|
+
|
|
17
|
+
### Why
|
|
18
|
+
|
|
19
|
+
The worktree directory was hardcoded to `.claude/worktrees/`, so Pi sessions produced Claude-flavoured paths. Each harness's worktrees now live under their own directory.
|
|
20
|
+
|
|
21
|
+
### Migration
|
|
22
|
+
|
|
23
|
+
None. Existing `.claude/worktrees/` worktrees resolve by name as before.
|
|
24
|
+
|
|
25
|
+
## 4.4.0
|
|
26
|
+
|
|
27
|
+
### New
|
|
28
|
+
|
|
29
|
+
- **Auto-chdir on checkout from inside worktree.** `carson checkout` no longer blocks when the current working directory is inside the worktree being removed. Carson `Dir.chdir`s to the main worktree root and completes the removal automatically. Both the warehouse (`assess_removal`) and legacy (`remove_check`) paths are fixed.
|
|
30
|
+
- **Parent shell excluded from held-by-other-process check.** `held_by_other_process?` now excludes `Process.ppid` (the user's shell that launched Carson) in addition to `Process.pid`. Prevents false blocks when the user runs `carson checkout` from inside the worktree.
|
|
31
|
+
|
|
32
|
+
### Why
|
|
33
|
+
|
|
34
|
+
Running `carson checkout` from inside the target worktree is the most natural invocation — agents and users are already there. Blocking and printing a recovery command added friction for no safety benefit, since Carson can move its own process out.
|
|
35
|
+
|
|
10
36
|
## 4.3.5
|
|
11
37
|
|
|
12
38
|
### Fixed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.5.0
|
|
@@ -8,7 +8,8 @@ module Carson
|
|
|
8
8
|
|
|
9
9
|
# --- Delegates to Warehouse::Workbench ---
|
|
10
10
|
|
|
11
|
-
# Creates a new
|
|
11
|
+
# Creates a new workbench under the calling harness's agent
|
|
12
|
+
# directory (<agent_dir>/worktrees/<name>, e.g. .pi from a Pi session).
|
|
12
13
|
def worktree_create!( name:, json_output: false )
|
|
13
14
|
result = worktree_warehouse.build_workbench!( name: name )
|
|
14
15
|
finish_worktree( result: result, json_output: json_output )
|
|
@@ -10,7 +10,9 @@ module Carson
|
|
|
10
10
|
module Workbench
|
|
11
11
|
|
|
12
12
|
# Agent directory names whose workbenches the warehouse may sweep.
|
|
13
|
-
AGENT_DIRS
|
|
13
|
+
# Carson::Worktree::AGENT_DIRS (legacy) carries the same list — keep
|
|
14
|
+
# both in lockstep until reap moves here.
|
|
15
|
+
AGENT_DIRS = %w[ .claude .codex .pi ].freeze
|
|
14
16
|
|
|
15
17
|
# --- Inventory ---
|
|
16
18
|
|
|
@@ -64,8 +66,9 @@ module Carson
|
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
# Resolve a bare name and find the workbench.
|
|
67
|
-
# Tries
|
|
68
|
-
#
|
|
69
|
+
# Tries the calling harness's agent directory first (scoped, then
|
|
70
|
+
# flat), then the other agent directories, then searches all
|
|
71
|
+
# registered workbenches by directory name.
|
|
69
72
|
def workbench_named( name )
|
|
70
73
|
if Pathname.new( name ).absolute?
|
|
71
74
|
return workbench_at( path: name )
|
|
@@ -76,18 +79,20 @@ module Carson
|
|
|
76
79
|
found = workbench_at( path: relative_candidate )
|
|
77
80
|
return found if found
|
|
78
81
|
|
|
79
|
-
# Try scoped path (e.g. "claude/foo" → .claude/worktrees/claude/foo)
|
|
82
|
+
# Try scoped path (e.g. "claude/foo" → .claude/worktrees/claude/foo),
|
|
83
|
+
# the calling harness's directory first.
|
|
80
84
|
if name.include?( "/" )
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
ordered_agent_dirs.each do |dir|
|
|
86
|
+
found = workbench_at( path: realpath_safe( File.join( main_worktree_root, dir, "worktrees", name ) ) )
|
|
87
|
+
return found if found
|
|
88
|
+
end
|
|
84
89
|
end
|
|
85
90
|
|
|
86
|
-
# Try flat layout:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
# Try flat layout: <agent_dir>/worktrees/<name>, calling harness first.
|
|
92
|
+
ordered_agent_dirs.each do |dir|
|
|
93
|
+
found = workbench_at( path: realpath_safe( File.join( main_worktree_root, dir, "worktrees", name ) ) )
|
|
94
|
+
return found if found
|
|
95
|
+
end
|
|
91
96
|
|
|
92
97
|
# Search all registered workbenches by dirname.
|
|
93
98
|
matches = workbenches.select { |wb| File.basename( wb.path ) == name }
|
|
@@ -105,11 +110,12 @@ module Carson
|
|
|
105
110
|
# --- Lifecycle ---
|
|
106
111
|
|
|
107
112
|
# Build a new workbench from local main.
|
|
108
|
-
# Creates the directory
|
|
109
|
-
#
|
|
113
|
+
# Creates the directory under the calling harness's agent directory,
|
|
114
|
+
# branches from the local standard, and excludes the agent directory
|
|
115
|
+
# from git status.
|
|
110
116
|
def build_workbench!( name: )
|
|
111
117
|
root = main_worktree_root
|
|
112
|
-
worktrees_dir = File.join( root,
|
|
118
|
+
worktrees_dir = File.join( root, agent_dir, "worktrees" )
|
|
113
119
|
workbench_path = File.join( worktrees_dir, name )
|
|
114
120
|
|
|
115
121
|
if Dir.exist?( workbench_path )
|
|
@@ -119,8 +125,8 @@ module Carson
|
|
|
119
125
|
recovery: "carson worktree remove #{name}, then retry" }
|
|
120
126
|
end
|
|
121
127
|
|
|
122
|
-
# Ensure
|
|
123
|
-
|
|
128
|
+
# Ensure the agent directory is excluded from git status.
|
|
129
|
+
ensure_agent_dir_excluded!
|
|
124
130
|
|
|
125
131
|
# Create the worktree with a new branch.
|
|
126
132
|
FileUtils.mkdir_p( File.dirname( workbench_path ) )
|
|
@@ -232,11 +238,8 @@ module Carson
|
|
|
232
238
|
return { status: :ok, missing: true }
|
|
233
239
|
end
|
|
234
240
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
error: "current working directory is inside this worktree",
|
|
238
|
-
recovery: "cd #{main_worktree_root} && carson checkout #{File.basename( workbench.path )}" }
|
|
239
|
-
end
|
|
241
|
+
# Auto-chdir out so removal can proceed.
|
|
242
|
+
Dir.chdir( main_worktree_root ) if workbench.holds_cwd?
|
|
240
243
|
|
|
241
244
|
if workbench.held_by_other_process?
|
|
242
245
|
return { status: :block, result_status: "block",
|
|
@@ -383,7 +386,7 @@ module Carson
|
|
|
383
386
|
wt_list, = git( "worktree", "list", "--porcelain" )
|
|
384
387
|
branch_list, = git( "branch", "--list", name )
|
|
385
388
|
git_version, = Open3.capture3( "git", "--version" )
|
|
386
|
-
workbench_path = File.join( root,
|
|
389
|
+
workbench_path = File.join( root, agent_dir, "worktrees", name )
|
|
387
390
|
entry = workbench_at( path: workbench_path )
|
|
388
391
|
{
|
|
389
392
|
git_stdout: git_stdout.to_s.strip,
|
|
@@ -398,8 +401,25 @@ module Carson
|
|
|
398
401
|
}
|
|
399
402
|
end
|
|
400
403
|
|
|
401
|
-
#
|
|
402
|
-
|
|
404
|
+
# The calling harness's workbench directory name.
|
|
405
|
+
# CARSON_AGENT_DIR wins over detection; absent detection, .claude is the
|
|
406
|
+
# historical default so Claude Code and Codex sessions behave as before.
|
|
407
|
+
# Pi sessions export PI_CODING_AGENT.
|
|
408
|
+
def agent_dir
|
|
409
|
+
override = ENV.fetch( "CARSON_AGENT_DIR", "" ).to_s.strip
|
|
410
|
+
return override unless override.empty?
|
|
411
|
+
return ".pi" if ENV.key?( "PI_CODING_AGENT" )
|
|
412
|
+
|
|
413
|
+
".claude"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Agent directories in resolution order: the calling harness's first.
|
|
417
|
+
def ordered_agent_dirs
|
|
418
|
+
( [ agent_dir ] + AGENT_DIRS ).uniq
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Ensure the calling harness's agent directory is in .git/info/exclude.
|
|
422
|
+
def ensure_agent_dir_excluded!
|
|
403
423
|
git_dir = File.join( main_worktree_root, ".git" )
|
|
404
424
|
return unless File.directory?( git_dir )
|
|
405
425
|
|
|
@@ -408,9 +428,10 @@ module Carson
|
|
|
408
428
|
|
|
409
429
|
FileUtils.mkdir_p( info_dir )
|
|
410
430
|
existing = File.exist?( exclude_path ) ? File.read( exclude_path ) : ""
|
|
411
|
-
|
|
431
|
+
entry = "#{agent_dir}/"
|
|
432
|
+
return if existing.lines.any? { |line| line.strip == entry }
|
|
412
433
|
|
|
413
|
-
File.open( exclude_path, "a" ) { |file| file.puts
|
|
434
|
+
File.open( exclude_path, "a" ) { |file| file.puts entry }
|
|
414
435
|
rescue StandardError
|
|
415
436
|
# Best-effort — do not block workbench creation.
|
|
416
437
|
end
|
data/lib/carson/worktree.rb
CHANGED
|
@@ -12,7 +12,9 @@ require "pathname"
|
|
|
12
12
|
module Carson
|
|
13
13
|
class Worktree
|
|
14
14
|
# Agent directory names whose worktrees Carson may sweep.
|
|
15
|
-
AGENT_DIRS
|
|
15
|
+
# Warehouse::Workbench::AGENT_DIRS carries the same list — keep both in
|
|
16
|
+
# lockstep until reap/sweep move to the warehouse.
|
|
17
|
+
AGENT_DIRS = %w[ .claude .codex .pi ].freeze
|
|
16
18
|
|
|
17
19
|
attr_reader :path, :branch, :prunable_reason
|
|
18
20
|
|
|
@@ -270,18 +272,8 @@ module Carson
|
|
|
270
272
|
entry = find( path: resolved_path, runtime: runtime )
|
|
271
273
|
branch = entry&.branch
|
|
272
274
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
status: :block,
|
|
277
|
-
result_status: "block",
|
|
278
|
-
exit_code: Runtime::EXIT_BLOCK,
|
|
279
|
-
resolved_path: resolved_path,
|
|
280
|
-
branch: branch,
|
|
281
|
-
error: "current working directory is inside this worktree",
|
|
282
|
-
recovery: "cd #{safe_root} && carson checkout #{File.basename( resolved_path )}"
|
|
283
|
-
}
|
|
284
|
-
end
|
|
275
|
+
# Auto-chdir out so removal can proceed.
|
|
276
|
+
Dir.chdir( runtime.main_worktree_root ) if entry&.holds_cwd?
|
|
285
277
|
|
|
286
278
|
if entry&.held_by_other_process?
|
|
287
279
|
return {
|
|
@@ -388,11 +380,12 @@ module Carson
|
|
|
388
380
|
return false if stdout.nil? || stdout.empty?
|
|
389
381
|
|
|
390
382
|
normalised = File.join( canonical, "" )
|
|
391
|
-
|
|
383
|
+
# Exclude this process and its parent (the user's shell that launched Carson).
|
|
384
|
+
excluded_pids = [ Process.pid, Process.ppid ].to_set
|
|
392
385
|
stdout.lines.drop( 1 ).any? do |line|
|
|
393
386
|
fields = line.strip.split( /\s+/ )
|
|
394
387
|
next false unless fields.length >= 9
|
|
395
|
-
next false if fields[ 1 ].to_i
|
|
388
|
+
next false if excluded_pids.include?( fields[ 1 ].to_i )
|
|
396
389
|
name = fields[ 8.. ].join( " " )
|
|
397
390
|
name == canonical || name.start_with?( normalised )
|
|
398
391
|
end
|