carson 4.1.1 → 4.2.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/API.md +1 -1
- data/RELEASE.md +25 -0
- data/VERSION +1 -1
- data/carson.gemspec +0 -1
- data/lib/carson/ledger.rb +0 -122
- data/lib/carson/runtime/abandon.rb +6 -5
- data/lib/carson/runtime/local/hooks.rb +1 -1
- data/lib/carson/runtime/local/worktree.rb +81 -7
- data/lib/carson/runtime/recover.rb +2 -2
- data/lib/carson/warehouse/bureau.rb +117 -0
- data/lib/carson/warehouse/seal.rb +55 -0
- data/lib/carson/warehouse/workbench.rb +516 -0
- data/lib/carson/warehouse.rb +28 -180
- data/lib/carson/worktree.rb +10 -0
- data/lib/carson.rb +1 -1
- data/lib/{carson/cli.rb → cli.rb} +147 -2
- metadata +11 -28
- /data/config/{.github/hooks → hooks}/command-guard +0 -0
- /data/config/{.github/hooks → hooks}/pre-commit +0 -0
- /data/config/{.github/hooks → hooks}/pre-merge-commit +0 -0
- /data/config/{.github/hooks → hooks}/pre-push +0 -0
- /data/config/{.github/hooks → hooks}/prepare-commit-msg +0 -0
data/lib/carson/warehouse.rb
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# A governed repository. In the FedEx metaphor, the warehouse is where
|
|
2
|
-
# parcels
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
require "digest"
|
|
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.
|
|
6
5
|
require "fileutils"
|
|
7
|
-
require "json"
|
|
8
6
|
require "open3"
|
|
9
7
|
|
|
8
|
+
require_relative "warehouse/workbench"
|
|
9
|
+
require_relative "warehouse/seal"
|
|
10
|
+
require_relative "warehouse/bureau"
|
|
11
|
+
|
|
10
12
|
module Carson
|
|
11
|
-
# A governed repository — the warehouse where parcels are
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# A governed repository — the warehouse where parcels are built on
|
|
14
|
+
# workbenches (worktrees) with labels (branches). An intelligent
|
|
15
|
+
# warehouse that manages itself: packing parcels, checking compliance,
|
|
16
|
+
# managing workbenches, and sweeping up.
|
|
15
17
|
class Warehouse
|
|
18
|
+
include Workbench
|
|
19
|
+
include Seal
|
|
20
|
+
include Bureau
|
|
21
|
+
|
|
16
22
|
attr_reader :path
|
|
17
23
|
|
|
18
24
|
def initialize( path:, main_label: "main", bureau_address: "github", compliance_checker: nil )
|
|
@@ -24,12 +30,12 @@ module Carson
|
|
|
24
30
|
|
|
25
31
|
# --- What the warehouse knows ---
|
|
26
32
|
|
|
27
|
-
# The label on the current
|
|
33
|
+
# The label on the current workbench (branch name).
|
|
28
34
|
def current_label
|
|
29
35
|
git( "rev-parse", "--abbrev-ref", "HEAD" ).first.strip
|
|
30
36
|
end
|
|
31
37
|
|
|
32
|
-
# The tip of the parcel on the current
|
|
38
|
+
# The tip of the parcel on the current workbench (commit SHA).
|
|
33
39
|
def current_head
|
|
34
40
|
git( "rev-parse", "HEAD" ).first.strip
|
|
35
41
|
end
|
|
@@ -44,7 +50,7 @@ module Carson
|
|
|
44
50
|
@bureau_address
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
# Is the warehouse floor clean? No uncommitted changes on the current
|
|
53
|
+
# Is the warehouse floor clean? No uncommitted changes on the current workbench.
|
|
48
54
|
def clean?
|
|
49
55
|
output, _, status = git( "status", "--porcelain" )
|
|
50
56
|
status.success? && output.strip.empty?
|
|
@@ -78,7 +84,6 @@ module Carson
|
|
|
78
84
|
# Ensure the warehouse complies with company standards (template sync).
|
|
79
85
|
# Delegates to the injected compliance checker. If no checker is set,
|
|
80
86
|
# the warehouse assumes compliance — no templates to enforce.
|
|
81
|
-
# Returns a hash: { compliant: true/false, committed: true/false, error: nil/string }
|
|
82
87
|
def submit_compliance!
|
|
83
88
|
return { compliant: true, committed: false } unless @compliance_checker
|
|
84
89
|
|
|
@@ -87,15 +92,13 @@ module Carson
|
|
|
87
92
|
|
|
88
93
|
# Update the warehouse's production standard — rebase onto latest registry state.
|
|
89
94
|
# Called after the bureau refuses a parcel for being behind standard.
|
|
90
|
-
# Returns true on success, false on failure.
|
|
91
95
|
def rebase_on_latest_standard!( registry: "#{bureau_address}/#{main_label}" )
|
|
92
96
|
_, _, status = git( "rebase", registry )
|
|
93
97
|
status.success?
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
# Pack a parcel — stage all changes and commit.
|
|
97
|
-
# Refuses if the
|
|
98
|
-
# Returns true on success, false on failure.
|
|
101
|
+
# Refuses if the workbench is sealed (parcel already in flight).
|
|
99
102
|
def pack!( message: )
|
|
100
103
|
if sealed?
|
|
101
104
|
raise "Branch is locked — PR ##{sealed_tracking_number} in flight. " \
|
|
@@ -106,38 +109,8 @@ module Carson
|
|
|
106
109
|
status.success?
|
|
107
110
|
end
|
|
108
111
|
|
|
109
|
-
# --- Shelf seal ---
|
|
110
|
-
|
|
111
|
-
# Seal the shelf — no more packing until the delivery outcome is confirmed.
|
|
112
|
-
# The courier seals the shelf after shipping and filing the waybill.
|
|
113
|
-
# The marker lives outside the worktree (~/.carson/seals/) so it does
|
|
114
|
-
# not pollute git status or block delivery with a dirty-tree guard.
|
|
115
|
-
def seal_shelf!( tracking_number: )
|
|
116
|
-
marker = delivering_marker_path
|
|
117
|
-
FileUtils.mkdir_p( File.dirname( marker ) )
|
|
118
|
-
File.write( marker, "#{tracking_number}\n#{@path}" )
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Unseal the shelf — the courier brought back the parcel.
|
|
122
|
-
# Called when the delivery outcome is held or rejected.
|
|
123
|
-
def unseal_shelf!
|
|
124
|
-
File.delete( delivering_marker_path ) if File.exist?( delivering_marker_path )
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Is this shelf sealed for a delivery in flight?
|
|
128
|
-
def sealed?
|
|
129
|
-
File.exist?( delivering_marker_path )
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# The tracking number of the in-flight delivery (nil if not sealed).
|
|
133
|
-
def sealed_tracking_number
|
|
134
|
-
return nil unless sealed?
|
|
135
|
-
File.read( delivering_marker_path ).lines.first.strip
|
|
136
|
-
end
|
|
137
|
-
|
|
138
112
|
# Receive the latest standard from the registry after a parcel is accepted.
|
|
139
113
|
# Fast-forwards local main without switching branches.
|
|
140
|
-
# Returns true on success, false on failure.
|
|
141
114
|
#
|
|
142
115
|
# Two paths depending on the main worktree's checkout state:
|
|
143
116
|
# - Main checked out → merge --ff-only (updates ref + working tree).
|
|
@@ -146,24 +119,20 @@ module Carson
|
|
|
146
119
|
def receive_latest_standard!( remote: bureau_address )
|
|
147
120
|
root = main_worktree_root
|
|
148
121
|
|
|
149
|
-
# Fetch remote tracking refs — always safe, even when main is checked out.
|
|
150
122
|
_, _, fetch_status = Open3.capture3( "git", "-C", root, "fetch", remote )
|
|
151
123
|
return false unless fetch_status.success?
|
|
152
124
|
|
|
153
|
-
# Determine how to advance local main.
|
|
154
125
|
head_ref, _, head_status = Open3.capture3(
|
|
155
126
|
"git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"
|
|
156
127
|
)
|
|
157
128
|
return false unless head_status.success?
|
|
158
129
|
|
|
159
130
|
if head_ref.strip == @main_label
|
|
160
|
-
# Main is checked out in the main worktree — fast-forward via merge.
|
|
161
131
|
_, _, merge_status = Open3.capture3(
|
|
162
132
|
"git", "-C", root, "merge", "--ff-only", "#{remote}/#{@main_label}"
|
|
163
133
|
)
|
|
164
134
|
merge_status.success?
|
|
165
135
|
else
|
|
166
|
-
# Main is not checked out — safe to update the ref via fetch refspec.
|
|
167
136
|
_, _, refspec_status = Open3.capture3(
|
|
168
137
|
"git", "-C", root, "fetch", remote, "#{@main_label}:#{@main_label}"
|
|
169
138
|
)
|
|
@@ -171,72 +140,8 @@ module Carson
|
|
|
171
140
|
end
|
|
172
141
|
end
|
|
173
142
|
|
|
174
|
-
# --- Bureau interaction ---
|
|
175
|
-
# The warehouse owns the connection to the bureau (GitHub).
|
|
176
|
-
# It queries, files, and registers on behalf of the courier.
|
|
177
|
-
|
|
178
|
-
# Check the parcel's status at the bureau using the waybill.
|
|
179
|
-
# Calls gh pr view + gh pr checks. Records findings onto the waybill.
|
|
180
|
-
def check_parcel_at_bureau_with( waybill )
|
|
181
|
-
state = fetch_pr_state_for( waybill.tracking_number )
|
|
182
|
-
ci, ci_diagnostic = fetch_ci_state_for( waybill.tracking_number )
|
|
183
|
-
waybill.record( state: state, ci: ci, ci_diagnostic: ci_diagnostic )
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# File a waybill at the bureau for this parcel.
|
|
187
|
-
# Calls gh pr create. Returns a Waybill with tracking number, or nil on failure.
|
|
188
|
-
def file_waybill_for!( parcel, title: nil, body_file: nil )
|
|
189
|
-
filing_title = title || Waybill.default_title_for( parcel.label )
|
|
190
|
-
arguments = [ "pr", "create", "--title", filing_title, "--head", parcel.label ]
|
|
191
|
-
|
|
192
|
-
if body_file && File.exist?( body_file )
|
|
193
|
-
arguments.push( "--body-file", body_file )
|
|
194
|
-
else
|
|
195
|
-
arguments.push( "--body", "" )
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
stdout, _, status = gh( *arguments )
|
|
199
|
-
tracking_number = nil
|
|
200
|
-
url = nil
|
|
201
|
-
|
|
202
|
-
if status.success?
|
|
203
|
-
url = stdout.to_s.strip
|
|
204
|
-
tracking_number = url.split( "/" ).last.to_i
|
|
205
|
-
tracking_number = nil if tracking_number == 0
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# If create failed or returned no number, try to find an existing PR.
|
|
209
|
-
unless tracking_number
|
|
210
|
-
tracking_number, url = find_existing_waybill_for( parcel.label )
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
return nil unless tracking_number
|
|
214
|
-
|
|
215
|
-
Waybill.new( label: parcel.label, tracking_number: tracking_number, url: url )
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Register the parcel at the bureau using the waybill.
|
|
219
|
-
# Calls gh pr merge. Stamps the waybill on success.
|
|
220
|
-
def register_parcel_at_bureau_with!( waybill, method: )
|
|
221
|
-
_, _, status = gh( "pr", "merge", waybill.tracking_number.to_s, "--#{method}" )
|
|
222
|
-
if status.success?
|
|
223
|
-
waybill.stamp( :accepted )
|
|
224
|
-
else
|
|
225
|
-
# Re-check the state — the merge may have revealed a new blocker.
|
|
226
|
-
check_parcel_at_bureau_with( waybill )
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
143
|
# --- Inventory ---
|
|
231
144
|
|
|
232
|
-
# All shelves (worktree paths).
|
|
233
|
-
def shelves
|
|
234
|
-
output, = git( "worktree", "list", "--porcelain" )
|
|
235
|
-
output.lines
|
|
236
|
-
.select { it.start_with?( "worktree " ) }
|
|
237
|
-
.map { it.sub( "worktree ", "" ).strip }
|
|
238
|
-
end
|
|
239
|
-
|
|
240
145
|
# All labels (branch names).
|
|
241
146
|
def labels
|
|
242
147
|
output, = git( "branch", "--format", "%(refname:short)" )
|
|
@@ -249,88 +154,31 @@ module Carson
|
|
|
249
154
|
merged_output.lines.map { it.strip }.include?( name )
|
|
250
155
|
end
|
|
251
156
|
|
|
252
|
-
#
|
|
253
|
-
|
|
157
|
+
# All worktree paths (transitional — use workbenches for Worktree instances).
|
|
158
|
+
def shelves
|
|
159
|
+
output, = git( "worktree", "list", "--porcelain" )
|
|
160
|
+
output.lines
|
|
161
|
+
.select { it.start_with?( "worktree " ) }
|
|
162
|
+
.map { it.sub( "worktree ", "" ).strip }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The main warehouse location — resolves correctly even from a workbench.
|
|
254
166
|
def main_worktree_root
|
|
255
167
|
git_common_dir, = git( "rev-parse", "--path-format=absolute", "--git-common-dir" )
|
|
256
168
|
common = git_common_dir.strip
|
|
257
|
-
# If it ends with /.git, the parent is the main worktree root.
|
|
258
169
|
common.end_with?( "/.git" ) ? File.dirname( common ) : common
|
|
259
170
|
end
|
|
260
171
|
|
|
261
172
|
private
|
|
262
173
|
|
|
263
|
-
# Path to the delivery marker file — signals the shelf is sealed.
|
|
264
|
-
# Lives outside the worktree at ~/.carson/seals/<sha256-of-path>
|
|
265
|
-
# so it does not pollute git status.
|
|
266
|
-
def delivering_marker_path
|
|
267
|
-
seals_dir = File.join( Dir.home, ".carson", "seals" )
|
|
268
|
-
key = Digest::SHA256.hexdigest( @path )
|
|
269
|
-
File.join( seals_dir, key )
|
|
270
|
-
end
|
|
271
|
-
|
|
272
174
|
# All git commands go through this single gateway.
|
|
273
|
-
# Returns [stdout, stderr, status].
|
|
274
175
|
def git( *arguments )
|
|
275
176
|
Open3.capture3( "git", "-C", path, *arguments )
|
|
276
177
|
end
|
|
277
178
|
|
|
278
179
|
# All gh commands go through this single gateway.
|
|
279
|
-
# Returns [stdout, stderr, status].
|
|
280
180
|
def gh( *arguments )
|
|
281
181
|
Open3.capture3( "gh", *arguments, chdir: path )
|
|
282
182
|
end
|
|
283
|
-
|
|
284
|
-
# Fetch PR state from the bureau for a tracking number.
|
|
285
|
-
# Returns the parsed state hash, or nil on failure.
|
|
286
|
-
def fetch_pr_state_for( tracking_number )
|
|
287
|
-
stdout, _, status = gh(
|
|
288
|
-
"pr", "view", tracking_number.to_s,
|
|
289
|
-
"--json", "number,state,isDraft,url,mergeStateStatus,mergeable,mergedAt"
|
|
290
|
-
)
|
|
291
|
-
return nil unless status.success?
|
|
292
|
-
|
|
293
|
-
JSON.parse( stdout )
|
|
294
|
-
rescue JSON::ParserError
|
|
295
|
-
nil
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# Fetch CI state from the bureau for a tracking number.
|
|
299
|
-
# Returns [ci_symbol, diagnostic_or_nil].
|
|
300
|
-
# Captures the first line of stderr as diagnostic when the command fails.
|
|
301
|
-
def fetch_ci_state_for( tracking_number )
|
|
302
|
-
stdout, stderr, status = gh(
|
|
303
|
-
"pr", "checks", tracking_number.to_s,
|
|
304
|
-
"--json", "name,bucket"
|
|
305
|
-
)
|
|
306
|
-
unless status.success?
|
|
307
|
-
return [ :error, stderr.to_s.strip.lines.first&.strip ]
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
checks = JSON.parse( stdout ) rescue []
|
|
311
|
-
return [ :none, nil ] if checks.empty?
|
|
312
|
-
|
|
313
|
-
buckets = checks.map { it[ "bucket" ].to_s.downcase }
|
|
314
|
-
return [ :fail, nil ] if buckets.include?( "fail" )
|
|
315
|
-
return [ :pending, nil ] if buckets.include?( "pending" )
|
|
316
|
-
|
|
317
|
-
[ :pass, nil ]
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# Try to find an existing PR for this label at the bureau.
|
|
321
|
-
# Returns [tracking_number, url] or [nil, nil].
|
|
322
|
-
def find_existing_waybill_for( label )
|
|
323
|
-
stdout, _, status = gh(
|
|
324
|
-
"pr", "view", label,
|
|
325
|
-
"--json", "number,url,state"
|
|
326
|
-
)
|
|
327
|
-
if status.success?
|
|
328
|
-
data = JSON.parse( stdout ) rescue nil
|
|
329
|
-
if data && data[ "number" ] && data[ "state" ] == "OPEN"
|
|
330
|
-
return [ data[ "number" ], data[ "url" ].to_s ]
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
[ nil, nil ]
|
|
334
|
-
end
|
|
335
183
|
end
|
|
336
184
|
end
|
data/lib/carson/worktree.rb
CHANGED
|
@@ -420,6 +420,16 @@ module Carson
|
|
|
420
420
|
false
|
|
421
421
|
end
|
|
422
422
|
|
|
423
|
+
# Is the workbench surface clean? (no uncommitted changes)
|
|
424
|
+
def clean?
|
|
425
|
+
return false unless exists?
|
|
426
|
+
|
|
427
|
+
stdout, = Open3.capture3( "git", "status", "--porcelain", chdir: path )
|
|
428
|
+
stdout.to_s.strip.empty?
|
|
429
|
+
rescue StandardError
|
|
430
|
+
false
|
|
431
|
+
end
|
|
432
|
+
|
|
423
433
|
# rubocop:disable Layout/AccessModifierIndentation -- tab-width calculation produces unfixable mixed tabs+spaces
|
|
424
434
|
private
|
|
425
435
|
# rubocop:enable Layout/AccessModifierIndentation
|
data/lib/carson.rb
CHANGED
|
@@ -5,7 +5,7 @@ require "optparse"
|
|
|
5
5
|
module Carson
|
|
6
6
|
class CLI
|
|
7
7
|
PORTFOLIO_COMMANDS = %w[onboard offboard list refresh version].freeze
|
|
8
|
-
REPO_COMMANDS = %w[deliver receive sync status audit prune housekeep worktree abandon recover review template setup].freeze
|
|
8
|
+
REPO_COMMANDS = %w[deliver receive sync status audit prune housekeep worktree abandon recover review template setup checkin checkout].freeze
|
|
9
9
|
ALL_COMMANDS = ( PORTFOLIO_COMMANDS + REPO_COMMANDS ).freeze
|
|
10
10
|
|
|
11
11
|
def self.start( arguments:, repo_root:, tool_root:, output:, error: )
|
|
@@ -229,6 +229,10 @@ module Carson
|
|
|
229
229
|
parse_housekeep_command( arguments: arguments, error: error )
|
|
230
230
|
when "worktree"
|
|
231
231
|
parse_worktree_subcommand( arguments: arguments, error: error )
|
|
232
|
+
when "checkin"
|
|
233
|
+
parse_checkin_command( arguments: arguments, error: error )
|
|
234
|
+
when "checkout"
|
|
235
|
+
parse_checkout_command( arguments: arguments, error: error )
|
|
232
236
|
when "abandon"
|
|
233
237
|
parse_abandon_command( arguments: arguments, error: error )
|
|
234
238
|
when "recover"
|
|
@@ -523,6 +527,71 @@ module Carson
|
|
|
523
527
|
{ command: :invalid }
|
|
524
528
|
end
|
|
525
529
|
|
|
530
|
+
# --- checkin ---
|
|
531
|
+
|
|
532
|
+
def self.parse_checkin_command( arguments:, error: )
|
|
533
|
+
options = { json: false }
|
|
534
|
+
checkin_parser = OptionParser.new do |parser|
|
|
535
|
+
parser.banner = "Usage: carson checkin <name> [--json]"
|
|
536
|
+
parser.separator ""
|
|
537
|
+
parser.separator "Prepare a fresh workbench from the latest standard."
|
|
538
|
+
parser.separator "The agent names the workbench — it becomes the branch."
|
|
539
|
+
parser.separator ""
|
|
540
|
+
parser.separator "Options:"
|
|
541
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
542
|
+
parser.separator ""
|
|
543
|
+
parser.separator "Examples:"
|
|
544
|
+
parser.separator " carson checkin feature-auth"
|
|
545
|
+
parser.separator " carson checkin oo/refactor-courier"
|
|
546
|
+
end
|
|
547
|
+
checkin_parser.parse!( arguments )
|
|
548
|
+
name = arguments.shift.to_s.strip
|
|
549
|
+
if name.empty?
|
|
550
|
+
error.puts "#{BADGE} Missing name. Use: carson checkin <name>"
|
|
551
|
+
error.puts checkin_parser
|
|
552
|
+
return { command: :invalid }
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
{ command: "checkin", workbench_name: name, json: options.fetch( :json ) }
|
|
556
|
+
rescue OptionParser::ParseError => exception
|
|
557
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
558
|
+
error.puts checkin_parser
|
|
559
|
+
{ command: :invalid }
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# --- checkout ---
|
|
563
|
+
|
|
564
|
+
def self.parse_checkout_command( arguments:, error: )
|
|
565
|
+
options = { json: false, force: false }
|
|
566
|
+
checkout_parser = OptionParser.new do |parser|
|
|
567
|
+
parser.banner = "Usage: carson checkout <name> [--json] [--force]"
|
|
568
|
+
parser.separator ""
|
|
569
|
+
parser.separator "Release a workbench when safe."
|
|
570
|
+
parser.separator "Removes the directory and local branch."
|
|
571
|
+
parser.separator ""
|
|
572
|
+
parser.separator "Options:"
|
|
573
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
574
|
+
parser.on( "--force", "Skip safety checks" ) { options[ :force ] = true }
|
|
575
|
+
parser.separator ""
|
|
576
|
+
parser.separator "Examples:"
|
|
577
|
+
parser.separator " carson checkout feature-auth"
|
|
578
|
+
parser.separator " carson checkout oo/refactor-courier --force"
|
|
579
|
+
end
|
|
580
|
+
checkout_parser.parse!( arguments )
|
|
581
|
+
name = arguments.shift.to_s.strip
|
|
582
|
+
if name.empty?
|
|
583
|
+
error.puts "#{BADGE} Missing name. Use: carson checkout <name>"
|
|
584
|
+
error.puts checkout_parser
|
|
585
|
+
return { command: :invalid }
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
{ command: "checkout", workbench_name: name, force: options.fetch( :force ), json: options.fetch( :json ) }
|
|
589
|
+
rescue OptionParser::ParseError => exception
|
|
590
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
591
|
+
error.puts checkout_parser
|
|
592
|
+
{ command: :invalid }
|
|
593
|
+
end
|
|
594
|
+
|
|
526
595
|
# --- review ---
|
|
527
596
|
|
|
528
597
|
def self.parse_review_subcommand( arguments:, error: )
|
|
@@ -898,7 +967,7 @@ module Carson
|
|
|
898
967
|
# referenced by Claude Code's PreToolUse hook. It must exist regardless of
|
|
899
968
|
# whether `carson refresh` has been run in any governed repo.
|
|
900
969
|
def self.ensure_global_artefacts!( tool_root: )
|
|
901
|
-
source = File.join( tool_root, "config", "
|
|
970
|
+
source = File.join( tool_root, "config", "hooks", "command-guard" )
|
|
902
971
|
return unless File.file?( source )
|
|
903
972
|
|
|
904
973
|
hooks_base = File.expand_path( "~/.carson/hooks" )
|
|
@@ -937,6 +1006,10 @@ module Carson
|
|
|
937
1006
|
runtime.worktree_list!( json_output: parsed.fetch( :json, false ) )
|
|
938
1007
|
when "worktree:remove"
|
|
939
1008
|
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ), json_output: parsed.fetch( :json, false ) )
|
|
1009
|
+
when "checkin"
|
|
1010
|
+
dispatch_checkin( parsed: parsed, runtime: runtime )
|
|
1011
|
+
when "checkout"
|
|
1012
|
+
dispatch_checkout( parsed: parsed, runtime: runtime )
|
|
940
1013
|
when "onboard"
|
|
941
1014
|
runtime.onboard!
|
|
942
1015
|
when "refresh:all"
|
|
@@ -978,5 +1051,77 @@ module Carson
|
|
|
978
1051
|
Runtime::EXIT_ERROR
|
|
979
1052
|
end
|
|
980
1053
|
end
|
|
1054
|
+
|
|
1055
|
+
# --- Direct Warehouse dispatch for checkin/checkout ---
|
|
1056
|
+
# No Runtime — CLI builds the Warehouse and calls domain methods directly.
|
|
1057
|
+
# This is the target architecture; other commands migrate here as Runtime dissolves.
|
|
1058
|
+
|
|
1059
|
+
def self.dispatch_checkin( parsed:, runtime: )
|
|
1060
|
+
warehouse = build_warehouse( runtime: runtime )
|
|
1061
|
+
result = warehouse.checkin!( name: parsed.fetch( :workbench_name ) )
|
|
1062
|
+
report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
def self.dispatch_checkout( parsed:, runtime: )
|
|
1066
|
+
warehouse = build_warehouse( runtime: runtime )
|
|
1067
|
+
workbench = warehouse.workbench_named( parsed.fetch( :workbench_name ) )
|
|
1068
|
+
|
|
1069
|
+
unless workbench
|
|
1070
|
+
name = parsed.fetch( :workbench_name )
|
|
1071
|
+
result = { command: "checkout", status: "error",
|
|
1072
|
+
name: name,
|
|
1073
|
+
error: "#{name} is not a registered workbench",
|
|
1074
|
+
recovery: "carson worktree list" }
|
|
1075
|
+
return report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
result = warehouse.checkout!( workbench, force: parsed.fetch( :force, false ) )
|
|
1079
|
+
report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
# Build a Warehouse rooted at the main worktree.
|
|
1083
|
+
# Uses runtime's resolved root and config for remote/branch names.
|
|
1084
|
+
def self.build_warehouse( runtime: )
|
|
1085
|
+
Warehouse.new(
|
|
1086
|
+
path: runtime.send( :main_worktree_root ),
|
|
1087
|
+
main_label: runtime.config.main_branch,
|
|
1088
|
+
bureau_address: runtime.config.git_remote
|
|
1089
|
+
)
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
# Render a workbench result as JSON or human text.
|
|
1093
|
+
# Returns the appropriate exit code.
|
|
1094
|
+
def self.report_workbench( result:, json:, output: )
|
|
1095
|
+
status = result[ :status ]
|
|
1096
|
+
exit_code = case status
|
|
1097
|
+
when "ok" then Runtime::EXIT_OK
|
|
1098
|
+
when "block" then Runtime::EXIT_BLOCK
|
|
1099
|
+
else Runtime::EXIT_ERROR
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
if json
|
|
1103
|
+
output.puts JSON.pretty_generate( result )
|
|
1104
|
+
else
|
|
1105
|
+
case status
|
|
1106
|
+
when "ok"
|
|
1107
|
+
case result[ :command ]
|
|
1108
|
+
when "checkin"
|
|
1109
|
+
output.puts "#{BADGE} Workbench ready: #{result[ :name ]}"
|
|
1110
|
+
output.puts " path: #{result[ :path ]}"
|
|
1111
|
+
output.puts " branch: #{result[ :branch ]}"
|
|
1112
|
+
when "checkout"
|
|
1113
|
+
output.puts "#{BADGE} Workbench released: #{result[ :name ]}"
|
|
1114
|
+
end
|
|
1115
|
+
when "error"
|
|
1116
|
+
output.puts "#{BADGE} #{result[ :error ]}"
|
|
1117
|
+
output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
|
|
1118
|
+
when "block"
|
|
1119
|
+
output.puts "#{BADGE} #{result[ :error ]}"
|
|
1120
|
+
output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
exit_code
|
|
1125
|
+
end
|
|
981
1126
|
end
|
|
982
1127
|
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.
|
|
4
|
+
version: 4.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -10,27 +10,7 @@ authors:
|
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
12
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
|
-
dependencies:
|
|
14
|
-
- !ruby/object:Gem::Dependency
|
|
15
|
-
name: sqlite3
|
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
|
17
|
-
requirements:
|
|
18
|
-
- - ">="
|
|
19
|
-
- !ruby/object:Gem::Version
|
|
20
|
-
version: '1.3'
|
|
21
|
-
- - "<"
|
|
22
|
-
- !ruby/object:Gem::Version
|
|
23
|
-
version: '3'
|
|
24
|
-
type: :runtime
|
|
25
|
-
prerelease: false
|
|
26
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
27
|
-
requirements:
|
|
28
|
-
- - ">="
|
|
29
|
-
- !ruby/object:Gem::Version
|
|
30
|
-
version: '1.3'
|
|
31
|
-
- - "<"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '3'
|
|
13
|
+
dependencies: []
|
|
34
14
|
description: 'Carson is an autonomous git strategist and repositories governor that
|
|
35
15
|
lives outside the repositories it governs — no Carson-owned artefacts in your repo.
|
|
36
16
|
As strategist, Carson knows when to branch, how to isolate concurrent work, and
|
|
@@ -53,11 +33,11 @@ files:
|
|
|
53
33
|
- RELEASE.md
|
|
54
34
|
- VERSION
|
|
55
35
|
- carson.gemspec
|
|
56
|
-
- config
|
|
57
|
-
- config
|
|
58
|
-
- config
|
|
59
|
-
- config
|
|
60
|
-
- config
|
|
36
|
+
- config/hooks/command-guard
|
|
37
|
+
- config/hooks/pre-commit
|
|
38
|
+
- config/hooks/pre-merge-commit
|
|
39
|
+
- config/hooks/pre-push
|
|
40
|
+
- config/hooks/prepare-commit-msg
|
|
61
41
|
- exe/carson
|
|
62
42
|
- icon.svg
|
|
63
43
|
- lib/carson.rb
|
|
@@ -68,7 +48,6 @@ files:
|
|
|
68
48
|
- lib/carson/adapters/github.rb
|
|
69
49
|
- lib/carson/adapters/prompt.rb
|
|
70
50
|
- lib/carson/branch.rb
|
|
71
|
-
- lib/carson/cli.rb
|
|
72
51
|
- lib/carson/config.rb
|
|
73
52
|
- lib/carson/courier.rb
|
|
74
53
|
- lib/carson/delivery.rb
|
|
@@ -103,8 +82,12 @@ files:
|
|
|
103
82
|
- lib/carson/runtime/status.rb
|
|
104
83
|
- lib/carson/version.rb
|
|
105
84
|
- lib/carson/warehouse.rb
|
|
85
|
+
- lib/carson/warehouse/bureau.rb
|
|
86
|
+
- lib/carson/warehouse/seal.rb
|
|
87
|
+
- lib/carson/warehouse/workbench.rb
|
|
106
88
|
- lib/carson/waybill.rb
|
|
107
89
|
- lib/carson/worktree.rb
|
|
90
|
+
- lib/cli.rb
|
|
108
91
|
homepage: https://github.com/wanghailei/carson
|
|
109
92
|
licenses:
|
|
110
93
|
- PolyForm-Shield-1.0.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|