carson 4.3.2 → 4.3.3

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: cef1919871b9d457a8b1ad95b2faa127cbb1a740fa211bf48e1b18110141be5a
4
- data.tar.gz: bd4f1107862848b8a60d5ef794f43127a6b8f14146ed781bfe72d841220f4190
3
+ metadata.gz: 5c6a03fe7387ab7c3df9dc7d1bc7de0b716dbb38b68b18cf9f103f8df9a7114c
4
+ data.tar.gz: 57e656339b9a4307684fae58ed6d10f147e8c23d4cd2a45731dd85905c4f7b05
5
5
  SHA512:
6
- metadata.gz: c4047e729026abc15be0601316a3521249cdc392c60b24b6ebf61bf5454746d48d649f16f2cda705e5a01ed2c9cce91ac2b407fd8d10ad670453fd2d0742d243
7
- data.tar.gz: 9dee7343ec14dbf351fce53e0da6b406e52fe64bb71e736bb70ad23dc22c2e82013d34705d901d7b304d79b6cb40ef1cb8cc0c3412da9796ab6d853e81648156
6
+ metadata.gz: cc0ed4785b032dba61497a99ff09e29d890777d69d0dd59b1d632955a2b92a294a66571c7d04caa45b61ed0b81d56c65ce5a4550c13dfe6de32670f0103c0c54
7
+ data.tar.gz: 2419020c706bf3b1d94233c077ffdeb05475d2f443d1b8d27a1386c368d1535b797707f171270d18d2707bc98a3daf10d4c4add82d298e694375cecd27ff848e
data/RELEASE.md CHANGED
@@ -15,6 +15,16 @@ Release-note scope rule:
15
15
 
16
16
  Audit was a remote-centred pre-commit gate — it blocked commits based on delivery-time concerns (PR checks, CI baseline) that don't belong at commit time. In local-centred mode it was purely dead weight. Rather than relocate it to bureau, we verified that none of its unique checks had proven value at any gate, and dissolved the whole thing. Scars, not speculation.
17
17
 
18
+ ## 4.3.3
19
+
20
+ ### Fixed
21
+
22
+ - **`carson checkin` is 13× faster.** Removed two redundant `git fetch` calls that hit GitHub on every checkin. `checkin` now branches from local main — zero network, 0.7s instead of 9.4s. Local main is the standard; GitHub is a backup, not a gating dependency.
23
+
24
+ ### Why
25
+
26
+ Local-centred mode means local main is the source of truth. Fetching from GitHub on every checkin was a remote-centred assumption baked into the command. The two sequential network round-trips accounted for 90% of the wall time.
27
+
18
28
  ## 4.3.2
19
29
 
20
30
  ### Fixed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.3.2
1
+ 4.3.3
@@ -137,15 +137,15 @@ module Carson
137
137
 
138
138
  # 02. Parcel behind standard — not based on client's latest standard.
139
139
  # The courier rebases automatically. Only blocks on conflict.
140
- unless @warehouse.fetch_latest( registry: @warehouse.main_label )
140
+ unless @warehouse.receive_latest!
141
141
  return blocked( result,
142
142
  "cannot verify freshness — fetch failed",
143
143
  recovery: "carson sync, then carson deliver" )
144
144
  end
145
- unless @warehouse.based_on_latest_standard?( parcel )
145
+ unless @warehouse.based_on_latest?( parcel )
146
146
  remote_main = "#{@warehouse.bureau_address}/#{@warehouse.main_label}"
147
147
  say "Branch is behind #{remote_main} — rebasing..."
148
- unless @warehouse.rebase_on_latest_standard!
148
+ unless @warehouse.rebase!
149
149
  return blocked( result,
150
150
  "rebase conflict onto #{remote_main}",
151
151
  recovery: "resolve conflicts, then carson deliver" )
@@ -175,8 +175,8 @@ module Carson
175
175
  result[ :tracking_number ] = waybill.tracking_number
176
176
  result[ :url ] = waybill.url
177
177
 
178
- # Seal the shelf — no more packing until the outcome is confirmed.
179
- @warehouse.seal_shelf!( tracking_number: waybill.tracking_number )
178
+ # Seal the workbench — no more packing until the outcome is confirmed.
179
+ @warehouse.seal!( tracking: waybill.tracking_number )
180
180
 
181
181
  # Wait at the bureau while the bureaucrats check the parcel.
182
182
  wait_and_poll_at_bureau( waybill, result )
@@ -185,7 +185,7 @@ module Carson
185
185
  # delivered/held/rejected → unseal (shelf done or parcel returned)
186
186
  # filed → stay sealed (parcel still in flight)
187
187
  outcome = result[ :outcome ]
188
- @warehouse.unseal_shelf! if outcome == "delivered" || outcome == "held" || outcome == "rejected"
188
+ @warehouse.unseal! if outcome == "delivered" || outcome == "held" || outcome == "rejected"
189
189
 
190
190
  # Update the ledger with the final outcome and PR identity.
191
191
  record( parcel, status: outcome || "filed", summary: result[ :hold_reason ], waybill: waybill )
@@ -201,12 +201,12 @@ module Carson
201
201
  # checks are exhausted.
202
202
  def wait_and_poll_at_bureau( waybill, result )
203
203
  MAX_CHECKS_AT_BUREAU.times do |check|
204
- @warehouse.check_parcel_at_bureau_with( waybill )
204
+ @warehouse.check_parcel_with( waybill )
205
205
 
206
206
  # 14/17. Already accepted — parcel is in the registry.
207
207
  if waybill.accepted?
208
208
  result[ :outcome ] = "delivered"
209
- result[ :synced ] = @warehouse.receive_latest_standard!
209
+ result[ :synced ] = @warehouse.receive_latest!
210
210
  return
211
211
  end
212
212
 
@@ -219,11 +219,11 @@ module Carson
219
219
 
220
220
  # Cleared or mergeability pending — ask the warehouse to register.
221
221
  if waybill.cleared? || waybill.mergeability_pending?
222
- @warehouse.register_parcel_at_bureau_with!( waybill, method: @merge_method )
222
+ @warehouse.register_with!( waybill, method: @merge_method )
223
223
 
224
224
  if waybill.accepted?
225
225
  result[ :outcome ] = "delivered"
226
- result[ :synced ] = @warehouse.receive_latest_standard!
226
+ result[ :synced ] = @warehouse.receive_latest!
227
227
  return
228
228
  end
229
229
  end
data/lib/carson/parcel.rb CHANGED
@@ -11,12 +11,13 @@ module Carson
11
11
  # head (commit SHA), and shelf (worktree). The protagonist of every
12
12
  # delivery — without a parcel, there is nothing to deliver.
13
13
  class Parcel
14
- attr_reader :label, :head, :shelf
14
+ attr_reader :label, :head, :shelf, :origin
15
15
 
16
- def initialize( label:, head:, shelf: nil )
16
+ def initialize( label:, head:, shelf: nil, origin: nil )
17
17
  @label = label
18
18
  @head = head
19
19
  @shelf = shelf
20
+ @origin = origin
20
21
  end
21
22
 
22
23
  # Is this parcel sitting on the main shelf?
@@ -24,5 +25,13 @@ module Carson
24
25
  def on_main?( main_label )
25
26
  label == main_label
26
27
  end
28
+
29
+ # Does this parcel carry anything?
30
+ # A parcel with no commits ahead of its origin is empty —
31
+ # nothing was packed, nothing to deliver.
32
+ def empty?
33
+ return false unless origin
34
+ head == origin
35
+ end
27
36
  end
28
37
  end
@@ -87,7 +87,7 @@ module Carson
87
87
  # Unseal worktrees that were filed — receive now owns the delivery lifecycle.
88
88
  unless dry_run
89
89
  filed_worktree_paths.each do |worktree_path|
90
- Warehouse.new( path: worktree_path ).unseal_shelf!
90
+ Warehouse.new( path: worktree_path ).unseal!
91
91
  end
92
92
  end
93
93
 
@@ -1,15 +1,14 @@
1
- # The warehouse's bureau-facing concern.
2
- # The warehouse owns the connection to the bureau (GitHub).
3
- # It queries, files, and registers on behalf of the courier.
1
+ # The warehouse's bureau concern — backup and optional PR/CI.
2
+ # The bureau is just a backup. These methods exist for when
3
+ # the courier needs the warehouse to interact with it.
4
4
  require "json"
5
5
 
6
6
  module Carson
7
7
  class Warehouse
8
8
  module Bureau
9
9
 
10
- # Check the parcel's status at the bureau using the waybill.
11
- # Calls gh pr view + gh pr checks. Records findings onto the waybill.
12
- def check_parcel_at_bureau_with( waybill )
10
+ # Check a parcel's status using the waybill.
11
+ def check_parcel_with( waybill )
13
12
  state = fetch_pr_state_for( waybill.tracking_number )
14
13
  ci, ci_diagnostic = fetch_ci_state_for( waybill.tracking_number )
15
14
  waybill.record( state: state, ci: ci, ci_diagnostic: ci_diagnostic )
@@ -47,15 +46,13 @@ module Carson
47
46
  Waybill.new( label: parcel.label, tracking_number: tracking_number, url: url )
48
47
  end
49
48
 
50
- # Register the parcel at the bureau using the waybill.
51
- # Calls gh pr merge. Stamps the waybill on success.
52
- def register_parcel_at_bureau_with!( waybill, method: )
49
+ # Register a parcel using the waybill.
50
+ def register_with!( waybill, method: )
53
51
  _, _, status = gh( "pr", "merge", waybill.tracking_number.to_s, "--#{method}" )
54
52
  if status.success?
55
53
  waybill.stamp( :accepted )
56
54
  else
57
- # Re-check the state — the merge may have revealed a new blocker.
58
- check_parcel_at_bureau_with( waybill )
55
+ check_parcel_with( waybill )
59
56
  end
60
57
  end
61
58
 
@@ -1,8 +1,16 @@
1
- # The warehouse's workbench seal concern.
2
- # Once a parcel ships and the waybill is filed, the warehouse seals the
3
- # workbench. No more packing until the delivery outcome is confirmed.
4
- # The seal marker lives outside the worktree (~/.carson/seals/) so it
5
- # does not pollute git status.
1
+ # The warehouse's seal concern — bureau enhancement only.
2
+ #
3
+ # The seal is inactive in the base (local-centred) model.
4
+ # There is no "in flight" period — the parcel goes directly
5
+ # into the vault.
6
+ #
7
+ # The seal only activates with bureau enhancement, where there's
8
+ # a waiting period between shipping and bureau acceptance. During
9
+ # that period, the workbench is sealed — no more packing until
10
+ # the delivery outcome is confirmed.
11
+ #
12
+ # The seal marker lives outside the worktree (~/.carson/seals/)
13
+ # so it does not pollute git status.
6
14
  require "digest"
7
15
  require "fileutils"
8
16
 
@@ -10,17 +18,15 @@ module Carson
10
18
  class Warehouse
11
19
  module Seal
12
20
 
13
- # Seal the workbench — no more packing until delivery outcome is confirmed.
14
- # The courier seals the workbench after shipping and filing the waybill.
15
- def seal_workbench!( tracking_number: )
21
+ # Seal the workbench — no more packing until the bureau answers.
22
+ def seal!( tracking: )
16
23
  marker = delivering_marker_path
17
24
  FileUtils.mkdir_p( File.dirname( marker ) )
18
- File.write( marker, "#{tracking_number}\n#{@path}" )
25
+ File.write( marker, "#{tracking}\n#{@path}" )
19
26
  end
20
27
 
21
28
  # Unseal the workbench — the courier brought back the parcel.
22
- # Called when the delivery outcome is held or rejected.
23
- def unseal_workbench!
29
+ def unseal!
24
30
  File.delete( delivering_marker_path ) if File.exist?( delivering_marker_path )
25
31
  end
26
32
 
@@ -35,11 +41,6 @@ module Carson
35
41
  File.read( delivering_marker_path ).lines.first.strip
36
42
  end
37
43
 
38
- # --- Transitional aliases ---
39
- # Keep old names working until all callers are updated.
40
- alias seal_shelf! seal_workbench!
41
- alias unseal_shelf! unseal_workbench!
42
-
43
44
  private
44
45
 
45
46
  # Path to the delivery marker file.
@@ -1,60 +1,66 @@
1
- # The warehouse's vault concern.
2
- # The vault is local main where accepted parcels live.
3
- # In local-centred workstyle, the vault is the source of truth.
4
- # In remote-centred workstyle, the vault is the backup (receives
5
- # the standard from the bureau's registry after acceptance).
1
+ # The vault — where the production standard lives.
2
+ # The vault is local main. It is the source of truth.
3
+ # Accepted parcels live here permanently.
6
4
  require "open3"
7
5
 
8
6
  module Carson
9
7
  class Warehouse
10
- module Vault
8
+ class Vault
9
+ attr_reader :main_label
10
+
11
+ def initialize( path:, main_label: )
12
+ @path = path
13
+ @main_label = main_label
14
+ end
11
15
 
12
16
  # Accept a parcel into the vault.
13
- # Fast-forwards local main to include the parcel's branch.
14
- # Runs from the main worktree root where main is checked out.
17
+ # Fast-forwards the standard to include the parcel's branch.
15
18
  #
16
19
  # Precondition: the parcel's branch must be a fast-forward of main.
17
20
  # If not, the agent must rebase first.
18
- #
19
- # Returns a result hash:
20
- # { status: "ok", branch: ..., head: ... }
21
- # { status: "block", error: ..., recovery: ... }
22
- # { status: "error", error: ..., recovery: ... }
23
21
  def accept!( parcel )
24
- root = main_worktree_root
25
-
26
- # Verify main is checked out in the main worktree.
27
- unless main_checked_out_at?( root )
22
+ unless main_checked_out?
28
23
  return {
29
24
  status: "error",
30
25
  error: "#{@main_label} is not checked out in the main worktree.",
31
- recovery: "Check the main worktree state at #{root}."
26
+ recovery: "Check the main worktree state at #{@path}."
32
27
  }
33
28
  end
34
29
 
35
- # Fast-forward main to include the parcel's branch.
36
30
  _, stderr, status = Open3.capture3(
37
- "git", "-C", root, "merge", "--ff-only", parcel.label
31
+ "git", "-C", @path, "merge", "--ff-only", parcel.label
38
32
  )
39
33
 
40
- return vault_accepted( parcel, root ) if status.success?
34
+ return accepted( parcel ) if status.success?
41
35
 
42
- vault_blocked( parcel, stderr )
36
+ blocked( parcel, stderr )
37
+ end
38
+
39
+ # Has this label's content been absorbed into the vault?
40
+ # Content-aware — compares tree content, not SHA ancestry.
41
+ # Catches rebase-merged and squash-merged branches that
42
+ # ancestry-based checks miss (replayed SHAs differ).
43
+ def absorbed?( label )
44
+ _, _, status = Open3.capture3(
45
+ "git", "diff", "--quiet", @main_label, label,
46
+ chdir: @path
47
+ )
48
+ status.success?
43
49
  end
44
50
 
45
51
  private
46
52
 
47
- # Check whether main is the checked-out branch at a given path.
48
- def main_checked_out_at?( root )
53
+ # Is main checked out in the vault's worktree?
54
+ def main_checked_out?
49
55
  head_ref, _, status = Open3.capture3(
50
- "git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"
56
+ "git", "-C", @path, "rev-parse", "--abbrev-ref", "HEAD"
51
57
  )
52
58
  status.success? && head_ref.strip == @main_label
53
59
  end
54
60
 
55
- # Build the success result after vault acceptance.
56
- def vault_accepted( parcel, root )
57
- new_head, = Open3.capture3( "git", "-C", root, "rev-parse", "HEAD" )
61
+ # Build the success result after acceptance.
62
+ def accepted( parcel )
63
+ new_head, = Open3.capture3( "git", "-C", @path, "rev-parse", "HEAD" )
58
64
  {
59
65
  status: "ok",
60
66
  branch: parcel.label,
@@ -62,10 +68,10 @@ module Carson
62
68
  }
63
69
  end
64
70
 
65
- # Build the blocked/error result when vault acceptance fails.
71
+ # Build the blocked result when acceptance fails.
66
72
  # Distinguishes dirty-tree conflicts from diverged-history blocks
67
73
  # so the agent gets the correct recovery advice.
68
- def vault_blocked( parcel, stderr )
74
+ def blocked( parcel, stderr )
69
75
  if stderr.to_s.include?( "would be overwritten" )
70
76
  {
71
77
  status: "block",
@@ -80,7 +86,6 @@ module Carson
80
86
  }
81
87
  end
82
88
  end
83
-
84
89
  end
85
90
  end
86
91
  end
@@ -104,8 +104,8 @@ module Carson
104
104
 
105
105
  # --- Lifecycle ---
106
106
 
107
- # Build a new workbench from the latest production standard.
108
- # Creates the directory, branches from the latest standard,
107
+ # Build a new workbench from local main.
108
+ # Creates the directory, branches from the local standard,
109
109
  # ensures .claude/ is excluded from git status.
110
110
  def build_workbench!( name: )
111
111
  root = main_worktree_root
@@ -119,23 +119,12 @@ module Carson
119
119
  recovery: "carson worktree remove #{name}, then retry" }
120
120
  end
121
121
 
122
- # Determine the base branch.
123
- base = @main_label
124
-
125
- # Fetch to update remote tracking ref without mutating the main worktree.
126
- _, _, fetch_ok = git( "fetch", @bureau_address, base )
127
- if fetch_ok.success?
128
- remote_ref = "#{@bureau_address}/#{base}"
129
- _, _, ref_ok = git( "rev-parse", "--verify", remote_ref )
130
- base = remote_ref if ref_ok.success?
131
- end
132
-
133
122
  # Ensure .claude/ is excluded from git status.
134
123
  ensure_claude_dir_excluded!
135
124
 
136
125
  # Create the worktree with a new branch.
137
126
  FileUtils.mkdir_p( File.dirname( workbench_path ) )
138
- wt_stdout, wt_stderr, wt_status = git( "worktree", "add", workbench_path, "-b", name, base )
127
+ wt_stdout, wt_stderr, wt_status = git( "worktree", "add", workbench_path, "-b", name, @main_label )
139
128
  unless wt_status.success?
140
129
  error_text = wt_stderr.to_s.strip
141
130
  error_text = "unable to create worktree" if error_text.empty?
@@ -159,11 +148,10 @@ module Carson
159
148
  path: workbench_path, branch: name }
160
149
  end
161
150
 
162
- # Agent checks in — prepare a fresh workbench from the latest standard.
151
+ # Agent checks in — prepare a fresh workbench from local main.
163
152
  # Sweeps delivered workbenches first — the Warehouse cleans behind the agent.
164
153
  def checkin!( name: )
165
- receive_latest_standard!
166
- sweep_delivered_workbenches!
154
+ sweep!
167
155
  result = build_workbench!( name: name )
168
156
  result[ :command ] = "checkin"
169
157
  result
@@ -237,19 +225,20 @@ module Carson
237
225
  end
238
226
 
239
227
  # Full safety assessment before removal.
228
+ # Asks the workbench about its own state — no duplicate checks.
240
229
  # Returns { status: :ok } or { status: :block/:error, error:, recovery: }.
241
230
  def assess_removal( workbench, force: false, skip_unpushed: false )
242
231
  unless workbench.exists?
243
232
  return { status: :ok, missing: true }
244
233
  end
245
234
 
246
- if agent_at_workbench?( workbench )
235
+ if workbench.holds_cwd?
247
236
  return { status: :block, result_status: "block",
248
237
  error: "current working directory is inside this worktree",
249
- recovery: "cd #{main_worktree_root} && carson worktree remove #{File.basename( workbench.path )}" }
238
+ recovery: "cd #{main_worktree_root} && carson checkout #{File.basename( workbench.path )}" }
250
239
  end
251
240
 
252
- if workbench_held_by_process?( workbench )
241
+ if workbench.held_by_other_process?
253
242
  return { status: :block, result_status: "block",
254
243
  error: "another process has its working directory inside this worktree",
255
244
  recovery: "wait for the other session to finish, then retry" }
@@ -269,9 +258,12 @@ module Carson
269
258
  { status: :ok, missing: false }
270
259
  end
271
260
 
272
- # Sweep stale workbenches. Walk all agent-owned workbenches,
273
- # check state, tear down those safe to reap. Repair missing ones.
274
- def sweep_workbenches!
261
+ # The warehouse sweeps autonomous housekeeping.
262
+ # Walks all agent-owned workbenches. Asks each one about its state.
263
+ # If the label has been absorbed into the vault, and the workbench
264
+ # isn't sealed or occupied — safe to remove, tears it down.
265
+ # Repairs missing ones.
266
+ def sweep!
275
267
  root = main_worktree_root
276
268
 
277
269
  agent_prefixes = AGENT_DIRS.map do |dir|
@@ -284,48 +276,17 @@ module Carson
284
276
  next unless workbench.branch
285
277
  next unless agent_prefixes.any? { |prefix| workbench.path.start_with?( prefix ) }
286
278
 
287
- # Use the existing classifier if available (transitional).
288
- if respond_to?( :classify_worktree_cleanup, true )
289
- classification = classify_worktree_cleanup( worktree: workbench )
290
- next unless classification.fetch( :action ) == :reap
291
- end
292
-
293
279
  unless workbench.exists?
294
280
  repair_missing_workbench!( workbench )
295
281
  next
296
282
  end
297
283
 
298
- _, _, rm_ok = git( "worktree", "remove", workbench.path )
299
- next unless rm_ok.success?
300
-
301
- if workbench.branch
302
- git( "branch", "-D", workbench.branch )
303
- end
304
- end
305
- end
306
-
307
- private
308
-
309
- # --- Sweep ---
310
-
311
- # Sweep delivered workbenches — branches absorbed into main, not sealed,
312
- # not CWD-blocked. Called by checkin! so the Warehouse cleans behind the agent.
313
- def sweep_delivered_workbenches!
314
- root = main_worktree_root
315
-
316
- agent_prefixes = AGENT_DIRS.map do |dir|
317
- full = File.join( root, dir, "worktrees" )
318
- File.join( realpath_safe( full ), "" ) if Dir.exist?( full )
319
- end.compact
320
- return if agent_prefixes.empty?
284
+ # Only sweep workbenches whose content has been absorbed into the vault.
285
+ next unless absorbed?( workbench.branch )
321
286
 
322
- workbenches.each do |workbench|
323
- next unless workbench.branch
324
- next unless agent_prefixes.any? { |prefix| workbench.path.start_with?( prefix ) }
325
- next unless workbench.exists?
326
- next unless label_absorbed?( workbench.branch )
327
- next if agent_at_workbench?( workbench )
328
- next if workbench_held_by_process?( workbench )
287
+ # Ask the workbench about its own state — not occupied, not held.
288
+ next if workbench.holds_cwd?
289
+ next if workbench.held_by_other_process?
329
290
 
330
291
  # Do not sweep sealed workbenches — parcel still in flight.
331
292
  seal_check = Warehouse.new( path: workbench.path )
@@ -338,41 +299,7 @@ module Carson
338
299
  end
339
300
  end
340
301
 
341
- # --- Safety checks ---
342
-
343
- # Is the agent's working directory inside this workbench?
344
- def agent_at_workbench?( workbench )
345
- cwd = realpath_safe( Dir.pwd )
346
- workbench_path = realpath_safe( workbench.path )
347
- normalised = File.join( workbench_path, "" )
348
- cwd == workbench_path || cwd.start_with?( normalised )
349
- rescue StandardError
350
- false
351
- end
352
-
353
- # Is another process occupying this workbench?
354
- def workbench_held_by_process?( workbench )
355
- canonical = realpath_safe( workbench.path )
356
- return false if canonical.nil? || canonical.empty?
357
- return false unless Dir.exist?( canonical )
358
-
359
- stdout, = Open3.capture3( "lsof", "-d", "cwd" )
360
- return false if stdout.nil? || stdout.empty?
361
-
362
- normalised = File.join( canonical, "" )
363
- my_pid = Process.pid
364
- stdout.lines.drop( 1 ).any? do |line|
365
- fields = line.strip.split( /\s+/ )
366
- next false unless fields.length >= 9
367
- next false if fields[ 1 ].to_i == my_pid
368
- name = fields[ 8.. ].join( " " )
369
- name == canonical || name.start_with?( normalised )
370
- end
371
- rescue Errno::ENOENT
372
- false
373
- rescue StandardError
374
- false
375
- end
302
+ private
376
303
 
377
304
  # Would tearing down lose unpushed work?
378
305
  # Content-aware: compares tree content vs main, not SHAs.
@@ -1,7 +1,10 @@
1
- # A governed repository. In the FedEx metaphor, the warehouse is where
2
- # parcels are built on workbenches (worktrees) with labels (branches).
3
- # Git and gh commands are hidden inside callers never see git or
4
- # GitHub terms.
1
+ # A governed repository the intelligent, self-managing building
2
+ # where parcels are built on workbenches. Each warehouse belongs
3
+ # to a client. The warehouse is the local authority everything
4
+ # inside the repository is its domain.
5
+ #
6
+ # At the heart of the warehouse is the vault — where the production
7
+ # standard lives. The vault is the source of truth.
5
8
  require "fileutils"
6
9
  require "open3"
7
10
 
@@ -11,13 +14,8 @@ require_relative "warehouse/seal"
11
14
  require_relative "warehouse/bureau"
12
15
 
13
16
  module Carson
14
- # A governed repository — the warehouse where parcels are built on
15
- # workbenches (worktrees) with labels (branches). An intelligent
16
- # warehouse that manages itself: packing parcels, checking compliance,
17
- # managing workbenches, and sweeping up.
18
17
  class Warehouse
19
18
  include Workbench
20
- include Vault
21
19
  include Seal
22
20
  include Bureau
23
21
 
@@ -30,29 +28,46 @@ module Carson
30
28
  @compliance_checker = compliance_checker
31
29
  end
32
30
 
31
+ # --- The vault ---
32
+
33
+ # The vault — where the production standard lives.
34
+ def vault
35
+ @vault ||= Vault.new( path: main_worktree_root, main_label: @main_label )
36
+ end
37
+
38
+ # Accept a parcel into the vault.
39
+ def accept!( parcel )
40
+ vault.accept!( parcel )
41
+ end
42
+
43
+ # Has this label been absorbed into the vault?
44
+ def absorbed?( label )
45
+ vault.absorbed?( label )
46
+ end
47
+
33
48
  # --- What the warehouse knows ---
34
49
 
35
- # The label on the current workbench (branch name).
50
+ # The label on the current workbench.
36
51
  def current_label
37
52
  git( "rev-parse", "--abbrev-ref", "HEAD" ).first.strip
38
53
  end
39
54
 
40
- # The tip of the parcel on the current workbench (commit SHA).
55
+ # The tip of the parcel on the current workbench.
41
56
  def current_head
42
57
  git( "rev-parse", "HEAD" ).first.strip
43
58
  end
44
59
 
45
- # The destination label (from config).
60
+ # What the production standard is called.
46
61
  def main_label
47
62
  @main_label
48
63
  end
49
64
 
50
- # The bureau's address (remote name).
65
+ # The bureau's address where to send things.
51
66
  def bureau_address
52
67
  @bureau_address
53
68
  end
54
69
 
55
- # Is the warehouse floor clean? No uncommitted changes on the current workbench.
70
+ # Is the floor clean? No loose material lying around.
56
71
  def clean?
57
72
  output, _, status = git( "status", "--porcelain" )
58
73
  status.success? && output.strip.empty?
@@ -60,47 +75,36 @@ module Carson
60
75
 
61
76
  # --- Warehouse operations ---
62
77
 
63
- # Ship a parcel to the bureau.
64
- # The warehouse sends the parcel's label to the remote.
78
+ # Ship a parcel to the backup so the courier can work with it.
65
79
  def ship( parcel, remote: bureau_address )
66
80
  _, _, status = git( "push", "-u", remote, parcel.label )
67
81
  status.success?
68
82
  end
69
83
 
70
- # Get latest registry state from the bureau (git fetch).
71
- # Returns true on success, false on failure.
72
- def fetch_latest( remote: bureau_address, registry: nil )
73
- arguments = [ "fetch", remote ]
74
- arguments << registry if registry
75
- _, _, status = git( *arguments )
84
+ # Is this parcel based on the latest standard?
85
+ # The standard is vault state — is the parcel built on top of it?
86
+ def based_on_latest?( parcel )
87
+ standard = "#{bureau_address}/#{main_label}"
88
+ _, _, status = git( "merge-base", "--is-ancestor", standard, parcel.head )
76
89
  status.success?
77
90
  end
78
91
 
79
- # Is the parcel based on the client's latest production standard?
80
- # Checks whether the registry tip is an ancestor of the parcel's head.
81
- def based_on_latest_standard?( parcel, registry: "#{bureau_address}/#{main_label}" )
82
- _, _, status = git( "merge-base", "--is-ancestor", registry, parcel.head )
83
- status.success?
84
- end
85
-
86
- # Ensure the warehouse complies with company standards (template sync).
87
- # Delegates to the injected compliance checker. If no checker is set,
88
- # the warehouse assumes compliance — no templates to enforce.
92
+ # Submit compliance ensure the warehouse meets company standards.
89
93
  def submit_compliance!
90
94
  return { compliant: true, committed: false } unless @compliance_checker
91
95
 
92
96
  @compliance_checker.call( self )
93
97
  end
94
98
 
95
- # Update the warehouse's production standard — rebase onto latest registry state.
96
- # Called after the bureau refuses a parcel for being behind standard.
97
- def rebase_on_latest_standard!( registry: "#{bureau_address}/#{main_label}" )
98
- _, _, status = git( "rebase", registry )
99
+ # Rebase a workbench onto the latest standard.
100
+ # When a parcel falls behind the standard, the warehouse fixes it.
101
+ def rebase!( standard: "#{bureau_address}/#{main_label}" )
102
+ _, _, status = git( "rebase", standard )
99
103
  status.success?
100
104
  end
101
105
 
102
- # Pack a parcel — stage all changes and commit.
103
- # Refuses if the workbench is sealed (parcel already in flight).
106
+ # Pack a parcel — stage all loose material and seal it.
107
+ # Refuses if the workbench is sealed — a parcel is already in flight.
104
108
  def pack!( message: )
105
109
  if sealed?
106
110
  raise "Branch is locked — PR ##{sealed_tracking_number} in flight. " \
@@ -111,14 +115,14 @@ module Carson
111
115
  status.success?
112
116
  end
113
117
 
114
- # Receive the latest standard from the registry after a parcel is accepted.
115
- # Fast-forwards local main without switching branches.
118
+ # Receive the latest standard.
119
+ # After a parcel is accepted, the standard has changed. The warehouse
120
+ # updates its vault without disturbing the current workbench.
116
121
  #
117
- # Two paths depending on the main worktree's checkout state:
122
+ # Two paths depending on the vault's checkout state:
118
123
  # - Main checked out → merge --ff-only (updates ref + working tree).
119
- # - Main not checked out → fetch refspec (updates ref only, safe when
120
- # no worktree has the branch).
121
- def receive_latest_standard!( remote: bureau_address )
124
+ # - Main not checked out → fetch refspec (updates ref only).
125
+ def receive_latest!( remote: bureau_address )
122
126
  root = main_worktree_root
123
127
 
124
128
  _, _, fetch_status = Open3.capture3( "git", "-C", root, "fetch", remote )
@@ -145,73 +149,60 @@ module Carson
145
149
  # --- Delivery prep ---
146
150
 
147
151
  # Prepare a parcel for delivery.
148
- # Orchestrates the prep phase: pack, fetch, standard check, auto-rebase.
149
- # Returns { status: "ok" } or { status: "block"/"error", error:, recovery: }.
152
+ # Packs if the agent provided a message, checks if the parcel is based
153
+ # on the latest standard, rebases automatically if it's behind.
150
154
  def prepare!( parcel, message: nil )
151
- registry = "#{bureau_address}/#{main_label}"
155
+ standard = "#{bureau_address}/#{main_label}"
152
156
 
153
- # Pack if the agent provided a commit message.
154
157
  if message
155
158
  unless pack!( message: message )
156
159
  return { status: "error", error: "Nothing to commit.", recovery: "Stage changes first." }
157
160
  end
158
- # Update the parcel's head after packing.
159
161
  parcel = Parcel.new( label: parcel.label, head: current_head )
160
162
  end
161
163
 
162
- # Fetch the latest standard.
163
- unless fetch_latest
164
+ unless receive_latest!
164
165
  return {
165
166
  status: "block",
166
- error: "Cannot fetch latest standard.",
167
+ error: "Cannot receive latest standard.",
167
168
  recovery: "Check network and remote config, then deliver again."
168
169
  }
169
170
  end
170
171
 
171
- # Check if the parcel is based on the latest standard.
172
- unless based_on_latest_standard?( parcel, registry: registry )
173
- # Auto-rebase onto the latest standard.
174
- unless rebase_on_latest_standard!( registry: registry )
172
+ unless based_on_latest?( parcel )
173
+ unless rebase!( standard: standard )
175
174
  return {
176
175
  status: "block",
177
176
  error: "#{parcel.label} conflicts with #{@main_label}.",
178
177
  recovery: "Rebase onto #{@main_label}, resolve conflicts, deliver again."
179
178
  }
180
179
  end
181
- # Update the parcel's head after rebase.
182
180
  parcel = Parcel.new( label: parcel.label, head: current_head )
183
181
  end
184
182
 
183
+ # Stamp the parcel with its origin so it knows whether it carries anything.
184
+ origin, = git( "merge-base", main_label, parcel.label )
185
+ parcel = Parcel.new( label: parcel.label, head: parcel.head, shelf: parcel.shelf, origin: origin.strip )
186
+
187
+ if parcel.empty?
188
+ return {
189
+ status: "block",
190
+ error: "Nothing to deliver — no commits ahead of #{main_label}.",
191
+ recovery: "Commit changes, then carson deliver."
192
+ }
193
+ end
194
+
185
195
  { status: "ok", parcel: parcel }
186
196
  end
187
197
 
188
198
  # --- Inventory ---
189
199
 
190
- # All labels (branch names).
200
+ # All labels in the warehouse.
191
201
  def labels
192
202
  output, = git( "branch", "--format", "%(refname:short)" )
193
203
  output.lines.map { it.strip }.reject { it.empty? }
194
204
  end
195
205
 
196
- # Has this label's content been absorbed into main?
197
- # Content-aware: compares tree content, not SHA ancestry.
198
- # Catches rebase-merged and squash-merged branches that
199
- # `git branch --merged` misses (replayed SHAs differ).
200
- def label_absorbed?( name )
201
- _, _, status = Open3.capture3(
202
- "git", "diff", "--quiet", @main_label, name,
203
- chdir: path )
204
- status.success?
205
- end
206
-
207
- # All worktree paths (transitional — use workbenches for Worktree instances).
208
- def shelves
209
- output, = git( "worktree", "list", "--porcelain" )
210
- output.lines
211
- .select { it.start_with?( "worktree " ) }
212
- .map { it.sub( "worktree ", "" ).strip }
213
- end
214
-
215
206
  # The main warehouse location — resolves correctly even from a workbench.
216
207
  def main_worktree_root
217
208
  git_common_dir, = git( "rev-parse", "--path-format=absolute", "--git-common-dir" )
@@ -279,7 +279,7 @@ module Carson
279
279
  resolved_path: resolved_path,
280
280
  branch: branch,
281
281
  error: "current working directory is inside this worktree",
282
- recovery: "cd #{safe_root} && carson worktree remove #{File.basename( resolved_path )}"
282
+ recovery: "cd #{safe_root} && carson checkout #{File.basename( resolved_path )}"
283
283
  }
284
284
  end
285
285
 
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.2
4
+ version: 4.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang