carson 4.1.2 → 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 +19 -0
- data/VERSION +1 -1
- data/carson.gemspec +0 -1
- data/lib/carson/ledger.rb +0 -122
- data/lib/carson/runtime/abandon.rb +1 -1
- data/lib/carson/runtime/local/worktree.rb +1 -1
- data/lib/carson/warehouse/workbench.rb +70 -23
- data/lib/carson.rb +1 -1
- data/lib/{carson/cli.rb → cli.rb} +146 -1
- metadata +3 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57e828abcdfc77caa38be92fa08a41d8d13abd37a09e7a4bf726d07253a48749
|
|
4
|
+
data.tar.gz: 5a6141be7046db72d03971fc9c4f45f1b7c81a47d685b13c97810711ebe9db39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 409ab608b1bed1423f16aa257efcc53f608643112db23020ac1786bd77fcc94a1776df32991be7ff0c00eb5d9b31f6db454e1ea4ad6020b65aaeb4dd186e39aa
|
|
7
|
+
data.tar.gz: a482b66aa5220c17d9d1b4c2852cc10b9cfeaa0d8af803e0b7974c20994754b1eb2affbb4d3a5659804764a398f25f7c822ffe3feaaa61b4c6b7fea46cf4b88a
|
data/API.md
CHANGED
|
@@ -173,7 +173,7 @@ Environment overrides:
|
|
|
173
173
|
- `agent.codex` / `agent.claude`: provider-specific options (reserved).
|
|
174
174
|
- `check_wait`: seconds to wait for CI checks before classifying (default: `30`).
|
|
175
175
|
- `merge.method`: `"squash"` only in governed mode.
|
|
176
|
-
- `state_path`: JSON ledger path for active deliveries and revisions.
|
|
176
|
+
- `state_path`: JSON ledger path for active deliveries and revisions.
|
|
177
177
|
|
|
178
178
|
`template` schema:
|
|
179
179
|
|
data/RELEASE.md
CHANGED
|
@@ -7,6 +7,25 @@ Release-note scope rule:
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 4.2.0
|
|
11
|
+
|
|
12
|
+
### New
|
|
13
|
+
|
|
14
|
+
- **`carson checkin <name>`** — the agent's verb for getting a fresh workbench. Receives the latest production standard before building. Sweeps delivered workbenches automatically — the Warehouse cleans behind the agent, so explicit checkout is rarely needed.
|
|
15
|
+
- **`carson checkout <name>`** — the agent's verb for releasing a workbench when done. Checks the seal (parcel in flight blocks checkout), CWD, process holds, dirty state, and unpushed work. Use at end of session; the common case is handled by the next checkin.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **CLI moved outside the story world** — `lib/carson/cli.rb` → `lib/cli.rb`. `lib/carson/` is domain objects only. checkin and checkout wire CLI directly to Warehouse with no Runtime. This is the template for Runtime dissolution.
|
|
20
|
+
- **`tear_down_workbench!` renamed to `remove_workbench!`** — simpler verb, matches the domain.
|
|
21
|
+
- **Remote branch deletion removed from workbench removal** — GitHub handles remote branch cleanup on PR merge. The Warehouse now only removes the directory and local branch.
|
|
22
|
+
|
|
23
|
+
### UX
|
|
24
|
+
|
|
25
|
+
- `carson checkin` output: `⧓ Workbench ready: <name>` with path and branch.
|
|
26
|
+
- `carson checkout` output: `⧓ Workbench released: <name>`.
|
|
27
|
+
- Agent lifecycle simplified: `checkin → work → deliver → checkin` (repeat). No explicit checkout between tasks.
|
|
28
|
+
|
|
10
29
|
## 4.1.2
|
|
11
30
|
|
|
12
31
|
### Fixed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
1
|
+
4.2.0
|
data/carson.gemspec
CHANGED
|
@@ -28,7 +28,6 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.bindir = "exe"
|
|
29
29
|
spec.executables = [ "carson" ]
|
|
30
30
|
spec.require_paths = [ "lib" ]
|
|
31
|
-
spec.add_dependency "sqlite3", ">= 1.3", "< 3"
|
|
32
31
|
spec.files = Dir.glob( "{lib,exe,templates,config}/**/*", File::FNM_DOTMATCH ).select { |path| File.file?( path ) } + [
|
|
33
32
|
".github/workflows/carson_policy.yml",
|
|
34
33
|
"README.md",
|
data/lib/carson/ledger.rb
CHANGED
|
@@ -7,12 +7,10 @@ module Carson
|
|
|
7
7
|
class Ledger
|
|
8
8
|
UNSET = Object.new
|
|
9
9
|
ACTIVE_DELIVERY_STATES = Delivery::ACTIVE_STATES
|
|
10
|
-
SQLITE_HEADER = "SQLite format 3\0".b.freeze
|
|
11
10
|
|
|
12
11
|
def initialize( path: )
|
|
13
12
|
@path = File.expand_path( path )
|
|
14
13
|
FileUtils.mkdir_p( File.dirname( @path ) )
|
|
15
|
-
migrate_legacy_state_if_needed!
|
|
16
14
|
end
|
|
17
15
|
|
|
18
16
|
attr_reader :path
|
|
@@ -273,22 +271,6 @@ module Carson
|
|
|
273
271
|
)
|
|
274
272
|
end
|
|
275
273
|
|
|
276
|
-
def migrate_legacy_state_if_needed!
|
|
277
|
-
# Skip lock acquisition entirely when no legacy SQLite file exists.
|
|
278
|
-
# Read-only file checks are safe without the lock; the migration
|
|
279
|
-
# itself is idempotent so a narrow race is harmless.
|
|
280
|
-
return unless state_path_requires_migration?
|
|
281
|
-
|
|
282
|
-
with_state_lock do |lock_file|
|
|
283
|
-
lock_file.flock( File::LOCK_EX )
|
|
284
|
-
source_path = legacy_sqlite_source_path
|
|
285
|
-
next unless source_path
|
|
286
|
-
|
|
287
|
-
state = load_legacy_sqlite_state( path: source_path )
|
|
288
|
-
save_state!( state )
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
274
|
def with_state_lock
|
|
293
275
|
lock_path = "#{path}.lock"
|
|
294
276
|
FileUtils.mkdir_p( File.dirname( lock_path ) )
|
|
@@ -299,110 +281,6 @@ module Carson
|
|
|
299
281
|
end
|
|
300
282
|
end
|
|
301
283
|
|
|
302
|
-
def legacy_sqlite_source_path
|
|
303
|
-
return nil unless state_path_requires_migration?
|
|
304
|
-
return path if sqlite_database_file?( path: path )
|
|
305
|
-
|
|
306
|
-
legacy_path = legacy_state_path
|
|
307
|
-
return nil unless legacy_path
|
|
308
|
-
return legacy_path if sqlite_database_file?( path: legacy_path )
|
|
309
|
-
|
|
310
|
-
nil
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def state_path_requires_migration?
|
|
314
|
-
return true if sqlite_database_file?( path: path )
|
|
315
|
-
return false if File.exist?( path )
|
|
316
|
-
!legacy_state_path.nil?
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
def legacy_state_path
|
|
320
|
-
return nil unless path.end_with?( ".json" )
|
|
321
|
-
path.sub( /\.json\z/, ".sqlite3" )
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
def sqlite_database_file?( path: )
|
|
325
|
-
return false unless File.file?( path )
|
|
326
|
-
File.binread( path, SQLITE_HEADER.bytesize ) == SQLITE_HEADER
|
|
327
|
-
rescue StandardError
|
|
328
|
-
false
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def load_legacy_sqlite_state( path: )
|
|
332
|
-
begin
|
|
333
|
-
require "sqlite3"
|
|
334
|
-
rescue LoadError => exception
|
|
335
|
-
raise "legacy SQLite ledger found at #{path}, but sqlite3 support is unavailable: #{exception.message}"
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
database = open_legacy_sqlite_database( path: path )
|
|
339
|
-
deliveries = database.execute( "SELECT * FROM deliveries ORDER BY id ASC" )
|
|
340
|
-
revisions_by_delivery = database.execute(
|
|
341
|
-
"SELECT * FROM revisions ORDER BY delivery_id ASC, number ASC, id ASC"
|
|
342
|
-
).group_by { |row| row.fetch( "delivery_id" ) }
|
|
343
|
-
|
|
344
|
-
state = {
|
|
345
|
-
"deliveries" => {},
|
|
346
|
-
"recovery_events" => [],
|
|
347
|
-
"next_sequence" => 1
|
|
348
|
-
}
|
|
349
|
-
deliveries.each do |row|
|
|
350
|
-
key = delivery_key(
|
|
351
|
-
repo_path: row.fetch( "repo_path" ),
|
|
352
|
-
branch_name: row.fetch( "branch_name" ),
|
|
353
|
-
head: row.fetch( "head" )
|
|
354
|
-
)
|
|
355
|
-
state[ "deliveries" ][ key ] = {
|
|
356
|
-
"sequence" => row.fetch( "id" ).to_i,
|
|
357
|
-
"repo_path" => row.fetch( "repo_path" ),
|
|
358
|
-
"branch_name" => row.fetch( "branch_name" ),
|
|
359
|
-
"head" => row.fetch( "head" ),
|
|
360
|
-
"worktree_path" => row.fetch( "worktree_path" ),
|
|
361
|
-
"status" => row.fetch( "status" ),
|
|
362
|
-
"pr_number" => row.fetch( "pr_number" ),
|
|
363
|
-
"pr_url" => row.fetch( "pr_url" ),
|
|
364
|
-
"pull_request_state" => nil,
|
|
365
|
-
"pull_request_draft" => nil,
|
|
366
|
-
"pull_request_merged_at" => nil,
|
|
367
|
-
"merge_proof" => nil,
|
|
368
|
-
"cause" => row.fetch( "cause" ),
|
|
369
|
-
"summary" => row.fetch( "summary" ),
|
|
370
|
-
"created_at" => row.fetch( "created_at" ),
|
|
371
|
-
"updated_at" => row.fetch( "updated_at" ),
|
|
372
|
-
"integrated_at" => row.fetch( "integrated_at" ),
|
|
373
|
-
"superseded_at" => row.fetch( "superseded_at" ),
|
|
374
|
-
"revisions" => Array( revisions_by_delivery[ row.fetch( "id" ) ] ).map do |revision|
|
|
375
|
-
{
|
|
376
|
-
"number" => revision.fetch( "number" ).to_i,
|
|
377
|
-
"cause" => revision.fetch( "cause" ),
|
|
378
|
-
"provider" => revision.fetch( "provider" ),
|
|
379
|
-
"status" => revision.fetch( "status" ),
|
|
380
|
-
"started_at" => revision.fetch( "started_at" ),
|
|
381
|
-
"finished_at" => revision.fetch( "finished_at" ),
|
|
382
|
-
"summary" => revision.fetch( "summary" )
|
|
383
|
-
}
|
|
384
|
-
end
|
|
385
|
-
}
|
|
386
|
-
end
|
|
387
|
-
normalise_state!( state: state )
|
|
388
|
-
state
|
|
389
|
-
ensure
|
|
390
|
-
database&.close
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def open_legacy_sqlite_database( path: )
|
|
394
|
-
database = SQLite3::Database.new( "file:#{path}?immutable=1", readonly: true, uri: true )
|
|
395
|
-
database.results_as_hash = true
|
|
396
|
-
database.busy_timeout = 5_000
|
|
397
|
-
database
|
|
398
|
-
rescue SQLite3::CantOpenException
|
|
399
|
-
database&.close
|
|
400
|
-
database = SQLite3::Database.new( path, readonly: true )
|
|
401
|
-
database.results_as_hash = true
|
|
402
|
-
database.busy_timeout = 5_000
|
|
403
|
-
database
|
|
404
|
-
end
|
|
405
|
-
|
|
406
284
|
def normalise_state!( state: )
|
|
407
285
|
deliveries = state[ "deliveries" ]
|
|
408
286
|
raise "state file must contain a JSON object at #{path}" unless deliveries.is_a?( Hash )
|
|
@@ -140,7 +140,7 @@ module Carson
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
if worktree
|
|
143
|
-
check = worktree_warehouse.
|
|
143
|
+
check = worktree_warehouse.assess_removal( worktree, force: false, skip_unpushed: true )
|
|
144
144
|
return nil if check.fetch( :status ) == :ok
|
|
145
145
|
|
|
146
146
|
recovery = check[ :recovery ]
|
|
@@ -26,7 +26,7 @@ module Carson
|
|
|
26
26
|
recovery: "git worktree list" },
|
|
27
27
|
json_output: json_output )
|
|
28
28
|
end
|
|
29
|
-
result = wh.
|
|
29
|
+
result = wh.remove_workbench!( workbench, force: force, skip_unpushed: skip_unpushed )
|
|
30
30
|
finish_worktree( result: result, json_output: json_output )
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# The warehouse's workbench concern.
|
|
2
|
-
# Builds,
|
|
2
|
+
# Builds, removes, sweeps, and inventories workbenches.
|
|
3
3
|
# Workbenches are passive objects — the warehouse acts on them.
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "open3"
|
|
@@ -159,16 +159,46 @@ module Carson
|
|
|
159
159
|
path: workbench_path, branch: name }
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
#
|
|
162
|
+
# Agent checks in — prepare a fresh workbench from the latest standard.
|
|
163
|
+
# Sweeps delivered workbenches first — the Warehouse cleans behind the agent.
|
|
164
|
+
def checkin!( name: )
|
|
165
|
+
receive_latest_standard!
|
|
166
|
+
sweep_delivered_workbenches!
|
|
167
|
+
result = build_workbench!( name: name )
|
|
168
|
+
result[ :command ] = "checkin"
|
|
169
|
+
result
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Agent checks out — release the workbench when safe.
|
|
173
|
+
# A sealed workbench has a parcel in flight at the Bureau.
|
|
174
|
+
def checkout!( workbench, force: false )
|
|
175
|
+
unless force
|
|
176
|
+
seal_check = Warehouse.new( path: workbench.path )
|
|
177
|
+
if seal_check.sealed?
|
|
178
|
+
tracking = seal_check.sealed_tracking_number || "unknown"
|
|
179
|
+
return { command: "checkout", status: "block",
|
|
180
|
+
name: File.basename( workbench.path ), branch: workbench.branch,
|
|
181
|
+
error: "workbench is sealed — PR ##{tracking} is still in flight",
|
|
182
|
+
recovery: "wait for CI checks to complete, or run carson deliver to check status" }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
result = remove_workbench!( workbench, force: force )
|
|
187
|
+
result[ :command ] = "checkout"
|
|
188
|
+
result
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Remove a workbench — directory, registration, local branch.
|
|
163
192
|
# The warehouse checks safety before acting.
|
|
164
|
-
|
|
193
|
+
# Remote branch cleanup is GitHub's concern, not the warehouse's.
|
|
194
|
+
def remove_workbench!( workbench, force: false, skip_unpushed: false )
|
|
165
195
|
# If the directory is already gone, repair the stale registration.
|
|
166
196
|
unless workbench.exists?
|
|
167
197
|
return repair_missing_workbench!( workbench )
|
|
168
198
|
end
|
|
169
199
|
|
|
170
200
|
# Safety assessment.
|
|
171
|
-
assessment =
|
|
201
|
+
assessment = assess_removal( workbench, force: force, skip_unpushed: skip_unpushed )
|
|
172
202
|
unless assessment[ :status ] == :ok
|
|
173
203
|
return { command: "worktree remove", status: assessment[ :result_status ] || "error",
|
|
174
204
|
name: File.basename( workbench.path ), branch: workbench.branch,
|
|
@@ -201,22 +231,14 @@ module Carson
|
|
|
201
231
|
branch_deleted = del_ok.success?
|
|
202
232
|
end
|
|
203
233
|
|
|
204
|
-
# Step 3: delete the remote branch (best-effort).
|
|
205
|
-
remote_deleted = false
|
|
206
|
-
if branch
|
|
207
|
-
_, _, rd_ok = git( "push", @bureau_address, "--delete", branch )
|
|
208
|
-
remote_deleted = rd_ok.success?
|
|
209
|
-
end
|
|
210
|
-
|
|
211
234
|
{ command: "worktree remove", status: "ok",
|
|
212
235
|
name: File.basename( workbench.path ),
|
|
213
|
-
branch: branch, branch_deleted: branch_deleted
|
|
214
|
-
remote_deleted: remote_deleted }
|
|
236
|
+
branch: branch, branch_deleted: branch_deleted }
|
|
215
237
|
end
|
|
216
238
|
|
|
217
|
-
# Full safety assessment before
|
|
239
|
+
# Full safety assessment before removal.
|
|
218
240
|
# Returns { status: :ok } or { status: :block/:error, error:, recovery: }.
|
|
219
|
-
def
|
|
241
|
+
def assess_removal( workbench, force: false, skip_unpushed: false )
|
|
220
242
|
unless workbench.exists?
|
|
221
243
|
return { status: :ok, missing: true }
|
|
222
244
|
end
|
|
@@ -284,6 +306,38 @@ module Carson
|
|
|
284
306
|
|
|
285
307
|
private
|
|
286
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?
|
|
321
|
+
|
|
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 )
|
|
329
|
+
|
|
330
|
+
# Do not sweep sealed workbenches — parcel still in flight.
|
|
331
|
+
seal_check = Warehouse.new( path: workbench.path )
|
|
332
|
+
next if seal_check.sealed?
|
|
333
|
+
|
|
334
|
+
_, _, rm_ok = git( "worktree", "remove", workbench.path )
|
|
335
|
+
next unless rm_ok.success?
|
|
336
|
+
|
|
337
|
+
git( "branch", "-D", workbench.branch ) if workbench.branch
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
287
341
|
# --- Safety checks ---
|
|
288
342
|
|
|
289
343
|
# Is the agent's working directory inside this workbench?
|
|
@@ -370,16 +424,9 @@ module Carson
|
|
|
370
424
|
branch_deleted = del_ok.success?
|
|
371
425
|
end
|
|
372
426
|
|
|
373
|
-
remote_deleted = false
|
|
374
|
-
if branch
|
|
375
|
-
_, _, rd_ok = git( "push", @bureau_address, "--delete", branch )
|
|
376
|
-
remote_deleted = rd_ok.success?
|
|
377
|
-
end
|
|
378
|
-
|
|
379
427
|
{ command: "worktree remove", status: "ok",
|
|
380
428
|
name: File.basename( workbench.path ),
|
|
381
|
-
branch: branch, branch_deleted: branch_deleted
|
|
382
|
-
remote_deleted: remote_deleted }
|
|
429
|
+
branch: branch, branch_deleted: branch_deleted }
|
|
383
430
|
end
|
|
384
431
|
|
|
385
432
|
# --- Build helpers ---
|
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: )
|
|
@@ -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
|
|
@@ -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
|
|
@@ -108,6 +87,7 @@ files:
|
|
|
108
87
|
- lib/carson/warehouse/workbench.rb
|
|
109
88
|
- lib/carson/waybill.rb
|
|
110
89
|
- lib/carson/worktree.rb
|
|
90
|
+
- lib/cli.rb
|
|
111
91
|
homepage: https://github.com/wanghailei/carson
|
|
112
92
|
licenses:
|
|
113
93
|
- PolyForm-Shield-1.0.0
|