carson 4.1.2 → 4.2.1
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 +29 -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/warehouse.rb +8 -3
- data/lib/carson.rb +1 -1
- data/lib/{carson/cli.rb → cli.rb} +151 -4
- 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: 251588ff2b1c952538965ea349b498e424c9646f6c57965dd516e7729e002035
|
|
4
|
+
data.tar.gz: 2fee9945faa9fbace57ef59dfd77cf5fb9c38992f4e573e796daeef8b1972017
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3981e6facd927beb517b18151eb0d4804863e6adab4d4d39039e6e9313e644f57caae09e0b292e1e33d49b383e1f3f7975decfe3d0672dc372f3582b757bc513
|
|
7
|
+
data.tar.gz: 55175ac0b094290e602d79854e65208fb5f1baa23753ed05c786c48ddda562b2015de76fb7244dc90c12a2495b2d84269e40c3f9521077d646776eeab843c760
|
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,35 @@ Release-note scope rule:
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 4.2.1
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`label_absorbed?` detects rebase-merged branches** — previously only detected merge commits, missing branches integrated via rebase merge.
|
|
15
|
+
|
|
16
|
+
### UX
|
|
17
|
+
|
|
18
|
+
- **`carson --help` promotes the agent workflow** — `checkin` and `deliver` now appear in a dedicated "Agent workflow" section at the top of repository commands. `recover` and `worktree` removed from help — agents reach these through Carson's block messages, not by browsing.
|
|
19
|
+
|
|
20
|
+
## 4.2.0
|
|
21
|
+
|
|
22
|
+
### New
|
|
23
|
+
|
|
24
|
+
- **`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.
|
|
25
|
+
- **`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.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **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.
|
|
30
|
+
- **`tear_down_workbench!` renamed to `remove_workbench!`** — simpler verb, matches the domain.
|
|
31
|
+
- **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.
|
|
32
|
+
|
|
33
|
+
### UX
|
|
34
|
+
|
|
35
|
+
- `carson checkin` output: `⧓ Workbench ready: <name>` with path and branch.
|
|
36
|
+
- `carson checkout` output: `⧓ Workbench released: <name>`.
|
|
37
|
+
- Agent lifecycle simplified: `checkin → work → deliver → checkin` (repeat). No explicit checkout between tasks.
|
|
38
|
+
|
|
10
39
|
## 4.1.2
|
|
11
40
|
|
|
12
41
|
### Fixed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.1
|
|
1
|
+
4.2.1
|
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/warehouse.rb
CHANGED
|
@@ -148,10 +148,15 @@ module Carson
|
|
|
148
148
|
output.lines.map { it.strip }.reject { it.empty? }
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
# Has this label been
|
|
151
|
+
# Has this label's content been absorbed into main?
|
|
152
|
+
# Content-aware: compares tree content, not SHA ancestry.
|
|
153
|
+
# Catches rebase-merged and squash-merged branches that
|
|
154
|
+
# `git branch --merged` misses (replayed SHAs differ).
|
|
152
155
|
def label_absorbed?( name )
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
_, _, status = Open3.capture3(
|
|
157
|
+
"git", "diff", "--quiet", @main_label, name,
|
|
158
|
+
chdir: path )
|
|
159
|
+
status.success?
|
|
155
160
|
end
|
|
156
161
|
|
|
157
162
|
# All worktree paths (transitional — use workbenches for Worktree instances).
|
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: )
|
|
@@ -140,12 +140,14 @@ module Carson
|
|
|
140
140
|
parser.separator " refresh Re-install hooks and configuration (all governed repos)"
|
|
141
141
|
parser.separator " version Show Carson version"
|
|
142
142
|
parser.separator ""
|
|
143
|
+
parser.separator "Agent workflow:"
|
|
144
|
+
parser.separator " checkin Prepare a fresh workbench from the latest standard"
|
|
145
|
+
parser.separator " deliver Ship committed work — push, PR, merge, cleanup"
|
|
146
|
+
parser.separator ""
|
|
143
147
|
parser.separator "Repository commands (from CWD or with explicit repo):"
|
|
148
|
+
parser.separator " checkout Release a workbench when done"
|
|
144
149
|
parser.separator " status Show repository delivery state"
|
|
145
150
|
parser.separator " audit Run pre-commit health checks"
|
|
146
|
-
parser.separator " deliver Start autonomous branch delivery"
|
|
147
|
-
parser.separator " recover Merge the repair PR for one baseline-red governance check"
|
|
148
|
-
parser.separator " worktree Manage isolated coding worktrees"
|
|
149
151
|
parser.separator ""
|
|
150
152
|
parser.separator "Run `carson <command> --help` for details on a specific command."
|
|
151
153
|
end
|
|
@@ -229,6 +231,10 @@ module Carson
|
|
|
229
231
|
parse_housekeep_command( arguments: arguments, error: error )
|
|
230
232
|
when "worktree"
|
|
231
233
|
parse_worktree_subcommand( arguments: arguments, error: error )
|
|
234
|
+
when "checkin"
|
|
235
|
+
parse_checkin_command( arguments: arguments, error: error )
|
|
236
|
+
when "checkout"
|
|
237
|
+
parse_checkout_command( arguments: arguments, error: error )
|
|
232
238
|
when "abandon"
|
|
233
239
|
parse_abandon_command( arguments: arguments, error: error )
|
|
234
240
|
when "recover"
|
|
@@ -523,6 +529,71 @@ module Carson
|
|
|
523
529
|
{ command: :invalid }
|
|
524
530
|
end
|
|
525
531
|
|
|
532
|
+
# --- checkin ---
|
|
533
|
+
|
|
534
|
+
def self.parse_checkin_command( arguments:, error: )
|
|
535
|
+
options = { json: false }
|
|
536
|
+
checkin_parser = OptionParser.new do |parser|
|
|
537
|
+
parser.banner = "Usage: carson checkin <name> [--json]"
|
|
538
|
+
parser.separator ""
|
|
539
|
+
parser.separator "Prepare a fresh workbench from the latest standard."
|
|
540
|
+
parser.separator "The agent names the workbench — it becomes the branch."
|
|
541
|
+
parser.separator ""
|
|
542
|
+
parser.separator "Options:"
|
|
543
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
544
|
+
parser.separator ""
|
|
545
|
+
parser.separator "Examples:"
|
|
546
|
+
parser.separator " carson checkin feature-auth"
|
|
547
|
+
parser.separator " carson checkin oo/refactor-courier"
|
|
548
|
+
end
|
|
549
|
+
checkin_parser.parse!( arguments )
|
|
550
|
+
name = arguments.shift.to_s.strip
|
|
551
|
+
if name.empty?
|
|
552
|
+
error.puts "#{BADGE} Missing name. Use: carson checkin <name>"
|
|
553
|
+
error.puts checkin_parser
|
|
554
|
+
return { command: :invalid }
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
{ command: "checkin", workbench_name: name, json: options.fetch( :json ) }
|
|
558
|
+
rescue OptionParser::ParseError => exception
|
|
559
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
560
|
+
error.puts checkin_parser
|
|
561
|
+
{ command: :invalid }
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# --- checkout ---
|
|
565
|
+
|
|
566
|
+
def self.parse_checkout_command( arguments:, error: )
|
|
567
|
+
options = { json: false, force: false }
|
|
568
|
+
checkout_parser = OptionParser.new do |parser|
|
|
569
|
+
parser.banner = "Usage: carson checkout <name> [--json] [--force]"
|
|
570
|
+
parser.separator ""
|
|
571
|
+
parser.separator "Release a workbench when safe."
|
|
572
|
+
parser.separator "Removes the directory and local branch."
|
|
573
|
+
parser.separator ""
|
|
574
|
+
parser.separator "Options:"
|
|
575
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
576
|
+
parser.on( "--force", "Skip safety checks" ) { options[ :force ] = true }
|
|
577
|
+
parser.separator ""
|
|
578
|
+
parser.separator "Examples:"
|
|
579
|
+
parser.separator " carson checkout feature-auth"
|
|
580
|
+
parser.separator " carson checkout oo/refactor-courier --force"
|
|
581
|
+
end
|
|
582
|
+
checkout_parser.parse!( arguments )
|
|
583
|
+
name = arguments.shift.to_s.strip
|
|
584
|
+
if name.empty?
|
|
585
|
+
error.puts "#{BADGE} Missing name. Use: carson checkout <name>"
|
|
586
|
+
error.puts checkout_parser
|
|
587
|
+
return { command: :invalid }
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
{ command: "checkout", workbench_name: name, force: options.fetch( :force ), json: options.fetch( :json ) }
|
|
591
|
+
rescue OptionParser::ParseError => exception
|
|
592
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
593
|
+
error.puts checkout_parser
|
|
594
|
+
{ command: :invalid }
|
|
595
|
+
end
|
|
596
|
+
|
|
526
597
|
# --- review ---
|
|
527
598
|
|
|
528
599
|
def self.parse_review_subcommand( arguments:, error: )
|
|
@@ -937,6 +1008,10 @@ module Carson
|
|
|
937
1008
|
runtime.worktree_list!( json_output: parsed.fetch( :json, false ) )
|
|
938
1009
|
when "worktree:remove"
|
|
939
1010
|
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ), json_output: parsed.fetch( :json, false ) )
|
|
1011
|
+
when "checkin"
|
|
1012
|
+
dispatch_checkin( parsed: parsed, runtime: runtime )
|
|
1013
|
+
when "checkout"
|
|
1014
|
+
dispatch_checkout( parsed: parsed, runtime: runtime )
|
|
940
1015
|
when "onboard"
|
|
941
1016
|
runtime.onboard!
|
|
942
1017
|
when "refresh:all"
|
|
@@ -978,5 +1053,77 @@ module Carson
|
|
|
978
1053
|
Runtime::EXIT_ERROR
|
|
979
1054
|
end
|
|
980
1055
|
end
|
|
1056
|
+
|
|
1057
|
+
# --- Direct Warehouse dispatch for checkin/checkout ---
|
|
1058
|
+
# No Runtime — CLI builds the Warehouse and calls domain methods directly.
|
|
1059
|
+
# This is the target architecture; other commands migrate here as Runtime dissolves.
|
|
1060
|
+
|
|
1061
|
+
def self.dispatch_checkin( parsed:, runtime: )
|
|
1062
|
+
warehouse = build_warehouse( runtime: runtime )
|
|
1063
|
+
result = warehouse.checkin!( name: parsed.fetch( :workbench_name ) )
|
|
1064
|
+
report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
def self.dispatch_checkout( parsed:, runtime: )
|
|
1068
|
+
warehouse = build_warehouse( runtime: runtime )
|
|
1069
|
+
workbench = warehouse.workbench_named( parsed.fetch( :workbench_name ) )
|
|
1070
|
+
|
|
1071
|
+
unless workbench
|
|
1072
|
+
name = parsed.fetch( :workbench_name )
|
|
1073
|
+
result = { command: "checkout", status: "error",
|
|
1074
|
+
name: name,
|
|
1075
|
+
error: "#{name} is not a registered workbench",
|
|
1076
|
+
recovery: "carson worktree list" }
|
|
1077
|
+
return report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
result = warehouse.checkout!( workbench, force: parsed.fetch( :force, false ) )
|
|
1081
|
+
report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# Build a Warehouse rooted at the main worktree.
|
|
1085
|
+
# Uses runtime's resolved root and config for remote/branch names.
|
|
1086
|
+
def self.build_warehouse( runtime: )
|
|
1087
|
+
Warehouse.new(
|
|
1088
|
+
path: runtime.send( :main_worktree_root ),
|
|
1089
|
+
main_label: runtime.config.main_branch,
|
|
1090
|
+
bureau_address: runtime.config.git_remote
|
|
1091
|
+
)
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
# Render a workbench result as JSON or human text.
|
|
1095
|
+
# Returns the appropriate exit code.
|
|
1096
|
+
def self.report_workbench( result:, json:, output: )
|
|
1097
|
+
status = result[ :status ]
|
|
1098
|
+
exit_code = case status
|
|
1099
|
+
when "ok" then Runtime::EXIT_OK
|
|
1100
|
+
when "block" then Runtime::EXIT_BLOCK
|
|
1101
|
+
else Runtime::EXIT_ERROR
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
if json
|
|
1105
|
+
output.puts JSON.pretty_generate( result )
|
|
1106
|
+
else
|
|
1107
|
+
case status
|
|
1108
|
+
when "ok"
|
|
1109
|
+
case result[ :command ]
|
|
1110
|
+
when "checkin"
|
|
1111
|
+
output.puts "#{BADGE} Workbench ready: #{result[ :name ]}"
|
|
1112
|
+
output.puts " path: #{result[ :path ]}"
|
|
1113
|
+
output.puts " branch: #{result[ :branch ]}"
|
|
1114
|
+
when "checkout"
|
|
1115
|
+
output.puts "#{BADGE} Workbench released: #{result[ :name ]}"
|
|
1116
|
+
end
|
|
1117
|
+
when "error"
|
|
1118
|
+
output.puts "#{BADGE} #{result[ :error ]}"
|
|
1119
|
+
output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
|
|
1120
|
+
when "block"
|
|
1121
|
+
output.puts "#{BADGE} #{result[ :error ]}"
|
|
1122
|
+
output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
|
|
1123
|
+
end
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
exit_code
|
|
1127
|
+
end
|
|
981
1128
|
end
|
|
982
1129
|
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.1
|
|
4
|
+
version: 4.2.1
|
|
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
|