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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f75abd3dbe087565ec4338de23c0b6ca0e5a3269dd217deebfd4e13653e9cc71
4
- data.tar.gz: bdfd8b3c61839f93d1424d28b94d3314a1a6f07158b961e2a4f04cbcedf5f6de
3
+ metadata.gz: a8eb2ac9b4d1ac6375fc581b2be86768ad5f441685b36dca3358e31aa43715b8
4
+ data.tar.gz: e2c7e9e3b3a4b35e26c8fbb7d59c46c10db8cf67b01000b0893d528a84055ce1
5
5
  SHA512:
6
- metadata.gz: f90470bef8509ee72e7dfffc57463f7c6fcb10918ee3e4b9a5af72698302edfa7c4259971d23ff50225e63d2a5900fa05f8308b296c56316f88ae903ed5f276d
7
- data.tar.gz: a115dd2f198afd4f014165588e476538137611aea3186599d539fb65ac7eccfc13f3fcd920e2786597ba0dec4ce7a1a2339d81af59158cd4337032c5c2e59990
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.3.5
1
+ 4.5.0
@@ -8,7 +8,8 @@ module Carson
8
8
 
9
9
  # --- Delegates to Warehouse::Workbench ---
10
10
 
11
- # Creates a new worktree under .claude/worktrees/<name>.
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 = %w[ .claude .codex ].freeze
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 .claude/worktrees/<name> first, then searches all registered
68
- # workbenches by directory name.
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
- scoped_candidate = realpath_safe( File.join( main_worktree_root, ".claude", "worktrees", name ) )
82
- found = workbench_at( path: scoped_candidate )
83
- return found if found
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: .claude/worktrees/<name>.
87
- root = main_worktree_root
88
- candidate = realpath_safe( File.join( root, ".claude", "worktrees", name ) )
89
- found = workbench_at( path: candidate )
90
- return found if found
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, branches from the local standard,
109
- # ensures .claude/ is excluded from git status.
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, ".claude", "worktrees" )
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 .claude/ is excluded from git status.
123
- ensure_claude_dir_excluded!
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
- if workbench.holds_cwd?
236
- return { status: :block, result_status: "block",
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, ".claude", "worktrees", name )
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
- # Ensure .claude/ is in .git/info/exclude.
402
- def ensure_claude_dir_excluded!
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
- return if existing.lines.any? { |line| line.strip == ".claude/" }
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 ".claude/" }
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
@@ -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 = %w[ .claude .codex ].freeze
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
- if entry&.holds_cwd?
274
- safe_root = runtime.main_worktree_root
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
- my_pid = Process.pid
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 == my_pid
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
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: 4.3.5
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang