carson 3.30.3 → 4.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 414294f12e0a6e1560b7c60df2ee77676734a63f279e40675bd4d10b55660851
4
- data.tar.gz: 2dafbe3def3939e75263fb47de9e474d073f083adbb8fb22ec997ff839727768
3
+ metadata.gz: b65ac7c43c1275299430d71f23f1da3a9896b112aa1f57e334222d06282a3a62
4
+ data.tar.gz: ff4efcd085b8cb0deb4385bfdb40d98407d463547b6e065fbfc455f516cc4dbb
5
5
  SHA512:
6
- metadata.gz: fbc81621f5aa779bf26ef20316083c1e207266f99bbefaba4fa78eea4c475745c67f30410b66e4a29257ed88b54400068a8cfb6d40e1aabeda16615b9baad3bf
7
- data.tar.gz: 973068e6d3e716f44e7a86fed848eeab6c5f26b95839e414061ed9f745ef5ed79a000ec60d753294cd27844bd6878beb14c3739ac24e4cdac04f2510592212d5
6
+ metadata.gz: b49a0b8760ad230996f6330b378fd7f9a202977c148683c4ae3eec066f2b820f6210e59a0e251ef275471d90db0d8154dee82cc5f97c3e46378cfad1fce074c2
7
+ data.tar.gz: 64cc150e7bb257ebbd6d48a603ab7eaa3e3b9794f08fcc65625bb8264628cc99e14f5603fe526d7818253ac57169de995cdc71c413c75cc6a3e2b56dbbb41f2c
data/RELEASE.md CHANGED
@@ -10,11 +10,42 @@ Release-note scope rule:
10
10
  ### Breaking
11
11
 
12
12
  - CLI grammar is now two-tier: portfolio commands (`list`, `onboard`, `offboard`, `refresh`, `version`) and repo-scoped commands (`carson <repo> <command>` or `carson <command>` from CWD)
13
+
14
+ ## 4.0.1
15
+
16
+ ### What changed
17
+
18
+ - **Workbench seal** — `carson deliver` seals the workbench after shipping. `pack!` refuses while sealed ("Shelf is sealed — parcel in flight"). `carson audit` blocks `git commit` on sealed workbenches. Delivered/held/rejected unseals; filed stays sealed.
19
+ - **Delivery progress** — `carson deliver` announces what it's doing: "Carson is delivering committed changes on branch X to Y…" and reports each poll check with count.
20
+ - **Seal guard in `carson audit`** — moved from bash hook template to Ruby. The pre-commit hook calls `carson audit` which checks for `.carson-delivering` marker.
21
+ - **`error_at_registry` is transient** — CI assessment errors no longer cause immediate rejection. The courier keeps polling.
22
+
23
+ ### Known limitation
24
+
25
+ - The workbench seal blocks `git commit` but does not block file edits (Write/Edit tools). That requires Claude Code PreToolUse hooks — a separate enforcement layer.
13
26
  - `govern` renamed to `receive` — single-repo only, no portfolio iteration
14
27
  - `repos` renamed to `list`
15
28
  - `--all` removed from all repo commands; use `carson list --json` to script batch operations
16
29
  - `refresh` is now portfolio-only (always refreshes all governed repos)
17
30
 
31
+ ## 4.0.0
32
+
33
+ ### Breaking
34
+
35
+ - **`carson deliver` waits for CI and merges automatically** — `carson deliver` now polls GitHub up to 6 times (default 30s interval) instead of checking once and returning immediately. PRs that pass CI are merged without re-running `carson deliver`. Agents that relied on instant return should expect it to block for up to 3 minutes.
36
+ - **Hold reasons renamed** — JSON output field `hold_reason` values changed: `inspector_pending` → `pending_at_registry`, `inspector_failed` → `failed_at_registry`, `inspector_error` → `error_at_registry`. Agents parsing these values must update.
37
+ - **Hold messages include recovery commands** — Human output for held deliveries now shows actionable next steps (e.g. `→ git rebase origin/main` then `→ carson deliver`) instead of prose descriptions.
38
+
39
+ ### New
40
+
41
+ - **`deliver.poll_interval_at_registry` config** — Controls how long `carson deliver` waits between CI checks (default 30 seconds). Override via config or `CARSON_POLL_INTERVAL_AT_REGISTRY` environment variable.
42
+ - **`filed` outcome** — When `carson deliver` exhausts all checks without a definitive answer from GitHub, it reports "filed" with `→ carson status` as the next step, instead of the old "deferred" with `→ carson deliver`.
43
+
44
+ ### Migration
45
+
46
+ - If you parse `hold_reason` from JSON output, update: `inspector_pending` → `pending_at_registry`, `inspector_failed` → `failed_at_registry`, `inspector_error` → `error_at_registry`.
47
+ - `carson deliver` now takes up to ~3 minutes (6 checks × 30s). Adjust timeouts in CI or automation scripts if needed.
48
+
18
49
  ## 3.30.3
19
50
 
20
51
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.30.3
1
+ 4.0.1
data/lib/carson/config.rb CHANGED
@@ -32,7 +32,8 @@ module Carson
32
32
  :workflow_style,
33
33
  :govern_repos, :govern_merge_method,
34
34
  :govern_agent_provider, :govern_state_path,
35
- :govern_check_wait
35
+ :govern_check_wait,
36
+ :poll_interval_at_registry
36
37
 
37
38
  def self.load( repo_root: )
38
39
  base_data = default_data
@@ -81,7 +82,10 @@ module Carson
81
82
  "audit" => {
82
83
  "advisory_check_names" => [ "Scheduled review sweep", "Carson governance", "Tag, release, publish" ]
83
84
  },
84
- "govern" => {
85
+ "deliver" => {
86
+ "poll_interval_at_registry" => 30
87
+ },
88
+ "govern" => {
85
89
  "repos" => [],
86
90
  "merge" => {
87
91
  "method" => "squash"
@@ -167,6 +171,8 @@ module Carson
167
171
  audit = fetch_hash_section( data: copy, key: "audit" )
168
172
  advisory_names = env_string_array( key: "CARSON_AUDIT_ADVISORY_CHECK_NAMES" )
169
173
  audit[ "advisory_check_names" ] = advisory_names unless advisory_names.empty?
174
+ deliver = fetch_hash_section( data: copy, key: "deliver" )
175
+ deliver[ "poll_interval_at_registry" ] = env_integer( key: "CARSON_POLL_INTERVAL_AT_REGISTRY", fallback: deliver.fetch( "poll_interval_at_registry" ) )
170
176
  govern = fetch_hash_section( data: copy, key: "govern" )
171
177
  govern_repos = env_string_array( key: "CARSON_GOVERN_REPOS" )
172
178
  govern[ "repos" ] = govern_repos unless govern_repos.empty?
@@ -238,6 +244,9 @@ module Carson
238
244
  audit_hash = fetch_hash( hash: data, key: "audit" )
239
245
  @audit_advisory_check_names = fetch_optional_string_array( hash: audit_hash, key: "advisory_check_names" )
240
246
 
247
+ deliver_hash = fetch_hash( hash: data, key: "deliver" )
248
+ @poll_interval_at_registry = fetch_non_negative_integer( hash: deliver_hash, key: "poll_interval_at_registry" )
249
+
241
250
  govern_hash = fetch_hash( hash: data, key: "govern" )
242
251
  @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
243
252
  govern_merge_hash = fetch_hash( hash: govern_hash, key: "merge" )
@@ -0,0 +1,291 @@
1
+ # Carson Co.
2
+ module Carson
3
+ # The delivery person — picks up parcels and delivers them to the registry.
4
+ #
5
+ # The courier is a Carson employee assigned to a warehouse. They pick up
6
+ # a parcel, ship it to the bureau, file a waybill, and wait at the
7
+ # registry while the bureaucrats check it.
8
+ #
9
+ # The courier is a thin orchestrator: it creates a Waybill and sends it
10
+ # messages. The domain logic lives in the objects, not the courier.
11
+ #
12
+ # == The bureau
13
+ #
14
+ # The bureau is a registry (GitHub) where bureaucrats work. They check
15
+ # parcels (CI, review, mergeability) and either accept them into the
16
+ # registry or hold them with a reason.
17
+ #
18
+ # == Shelf seal
19
+ #
20
+ # Once the parcel ships and the waybill is filed, the warehouse seals
21
+ # the shelf. No more packing until the delivery outcome is confirmed.
22
+ # Delivered → shelf done (housekeep removes it).
23
+ # Held/rejected → courier unseals (agent can fix and re-deliver).
24
+ # Filed (checks exhausted) → shelf stays sealed (parcel still in flight).
25
+ #
26
+ # == Situations the courier encounters
27
+ #
28
+ # Each numbered situation is handled by a specific guard or branch in the
29
+ # delivery flow. The number appears in the code comment where it's handled.
30
+ #
31
+ # 01. Parcel on main — cannot deliver from the destination.
32
+ # 02. Parcel behind standard — not based on client's latest standard.
33
+ # 03. Shipping fails — warehouse couldn't push to the bureau.
34
+ # 04. Waybill filing fails — bureau rejected the paperwork.
35
+ # 05. Pending at registry — bureaucrats still checking (CI running).
36
+ # 06. Failed at registry — bureaucrats rejected (CI failed).
37
+ # 07. Review pending — review still in progress.
38
+ # 08. Review changes requested — reviewer wants corrections.
39
+ # 09. Merge conflict — parcel conflicts with registry contents.
40
+ # 10. Behind standard (post-filing) — standard changed since shipping.
41
+ # 11. Policy block — bureau regulation prevents acceptance.
42
+ # 12. Draft waybill — form not finalised.
43
+ # 13. Mergeability pending — bureau still processing eligibility.
44
+ # 14. Acceptance succeeds — parcel enters the registry. Delivered.
45
+ # 15. Acceptance fails — classify why, report.
46
+ # 16. Bureau unreachable — cannot contact the bureau.
47
+ # 17. Parcel already delivered — already in registry.
48
+ # 18. Waybill closed — cancelled by someone externally.
49
+ #
50
+ # == Design: wait and poll at the registry
51
+ #
52
+ # The courier waits at the registry while the bureaucrats check the parcel.
53
+ # It polls up to MAX_CHECKS_AT_REGISTRY times, pausing between each check.
54
+ # If the bureaucrats give a definitive answer (accepted or rejected), the
55
+ # courier acts immediately. If the checks are exhausted without a definitive
56
+ # answer, the courier reports "filed" — the parcel is still at the registry
57
+ # and the shelf stays sealed.
58
+ #
59
+ # == Future: destination modes
60
+ #
61
+ # Currently remote-centred (ship → waybill → registry → acceptance).
62
+ # A future local-centred mode merges locally; remote is a synced backup.
63
+ # The destination mode should be injectable, not baked in.
64
+ class Courier
65
+ # Exit codes — shared contract between Carson employees and the CLI.
66
+ OK = 0
67
+ ERROR = 1
68
+ BLOCKED = 2
69
+
70
+ BADGE = "\u29D3".freeze
71
+
72
+ # The courier checks the registry up to 6 times before leaving.
73
+ MAX_CHECKS_AT_REGISTRY = 6
74
+
75
+ def initialize( warehouse, ledger: nil, merge_method: "rebase", poll_interval_at_registry: 30, output: $stdout )
76
+ @warehouse = warehouse
77
+ @ledger = ledger
78
+ @merge_method = merge_method
79
+ @poll_interval_at_registry = poll_interval_at_registry
80
+ @output = output
81
+ end
82
+
83
+ # Deliver a parcel to the registry.
84
+ # Ships it, files a waybill, seals the shelf, waits at the registry.
85
+ def deliver( parcel, title: nil, body_file: nil, commit_message: nil )
86
+ result = {
87
+ command: "deliver",
88
+ label: parcel.label,
89
+ remote_main: "#{@warehouse.bureau_address}/#{@warehouse.main_label}"
90
+ }
91
+
92
+ # 01. Parcel on main — cannot deliver from the destination.
93
+ if parcel.on_main?( @warehouse.main_label )
94
+ return blocked( result,
95
+ "cannot deliver from #{@warehouse.main_label}",
96
+ recovery: "carson worktree create <name>" )
97
+ end
98
+
99
+ # Dirty tree guard — the warehouse knows if its floor is clean.
100
+ if commit_message && @warehouse.clean?
101
+ return blocked( result,
102
+ "working tree is already clean",
103
+ recovery: "carson deliver" )
104
+ end
105
+ if !commit_message && !@warehouse.clean?
106
+ return blocked( result,
107
+ "working tree is dirty",
108
+ recovery: "carson deliver --commit \"describe this delivery\"" )
109
+ end
110
+
111
+ # Submit compliance — ensure templates are in sync before delivery.
112
+ compliance = @warehouse.submit_compliance!
113
+ unless compliance[ :compliant ]
114
+ return error( result, compliance[ :error ] || "compliance check failed" )
115
+ end
116
+
117
+ # Pack the parcel if the sender provided a commit message.
118
+ # Skip if compliance already committed everything (tree is now clean).
119
+ if commit_message && !@warehouse.clean?
120
+ unless @warehouse.pack!( message: commit_message )
121
+ return error( result, "packing failed — nothing to commit?" )
122
+ end
123
+ end
124
+ # Refresh parcel head — compliance or pack may have created commits.
125
+ parcel = Parcel.new( label: parcel.label, head: @warehouse.current_head, shelf: parcel.shelf )
126
+
127
+ # 02. Parcel behind standard — not based on client's latest standard.
128
+ @warehouse.fetch_latest( registry: @warehouse.main_label )
129
+ unless @warehouse.based_on_latest_standard?( parcel )
130
+ remote_main = "#{@warehouse.bureau_address}/#{@warehouse.main_label}"
131
+ return blocked( result,
132
+ "branch is behind #{remote_main}",
133
+ recovery: "git rebase #{remote_main}, then carson deliver" )
134
+ end
135
+
136
+ # Announce the delivery.
137
+ say "Carson is delivering committed changes on branch #{parcel.label} to #{result[ :remote_main ]}..."
138
+
139
+ # The courier picks up the parcel — start tracking.
140
+ record( parcel, status: "preparing", summary: "delivery accepted" )
141
+
142
+ # 03. Shipping fails — warehouse couldn't push to the bureau.
143
+ unless @warehouse.ship( parcel )
144
+ return error( result, "push failed" )
145
+ end
146
+
147
+ # File a waybill with the bureau.
148
+ waybill = Waybill.new(
149
+ label: parcel.label,
150
+ warehouse_path: @warehouse.path
151
+ )
152
+ waybill.file!( title: title, body_file: body_file )
153
+
154
+ # 04. Waybill filing fails — bureau rejected the paperwork.
155
+ unless waybill.filed?
156
+ return error( result, "PR creation failed", recovery: "carson deliver" )
157
+ end
158
+
159
+ result[ :tracking_number ] = waybill.tracking_number
160
+ result[ :url ] = waybill.url
161
+
162
+ # Seal the shelf — no more packing until the outcome is confirmed.
163
+ @warehouse.seal_shelf!( tracking_number: waybill.tracking_number )
164
+
165
+ # Wait at the registry while the bureaucrats check the parcel.
166
+ wait_and_poll_at_registry( waybill, result )
167
+
168
+ # Unseal based on outcome:
169
+ # delivered/held/rejected → unseal (shelf done or parcel returned)
170
+ # filed → stay sealed (parcel still in flight)
171
+ outcome = result[ :outcome ]
172
+ @warehouse.unseal_shelf! if outcome == "delivered" || outcome == "held" || outcome == "rejected"
173
+
174
+ # Update the ledger with the final outcome.
175
+ record( parcel, status: outcome || "filed", summary: result[ :hold_reason ] )
176
+
177
+ result[ :exit ] ||= OK
178
+ result
179
+ end
180
+
181
+ private
182
+
183
+ # Wait at the registry, polling the bureaucrats up to MAX_CHECKS_AT_REGISTRY
184
+ # times. The courier stays until a definitive answer comes back or the
185
+ # checks are exhausted.
186
+ def wait_and_poll_at_registry( waybill, result )
187
+ MAX_CHECKS_AT_REGISTRY.times do |check|
188
+ waybill.refresh!
189
+
190
+ # 14/17. Already accepted — parcel is in the registry.
191
+ if waybill.accepted?
192
+ result[ :outcome ] = "delivered"
193
+ result[ :synced ] = @warehouse.receive_latest_standard!
194
+ return
195
+ end
196
+
197
+ # 18. Waybill closed — cancelled externally.
198
+ if waybill.rejected?
199
+ result[ :outcome ] = "rejected"
200
+ result[ :exit ] = BLOCKED
201
+ return
202
+ end
203
+
204
+ # Cleared or mergeability pending — try to accept.
205
+ if waybill.cleared? || waybill.mergeability_pending?
206
+ waybill.accept!( method: @merge_method )
207
+
208
+ if waybill.accepted?
209
+ result[ :outcome ] = "delivered"
210
+ result[ :synced ] = @warehouse.receive_latest_standard!
211
+ return
212
+ end
213
+ end
214
+
215
+ # 05-12. Definitively blocked — courier takes parcel back.
216
+ if definitively_blocked?( waybill )
217
+ result[ :outcome ] = "held"
218
+ result[ :exit ] = BLOCKED
219
+ result[ :hold_reason ] = waybill.hold_reason
220
+ return
221
+ end
222
+
223
+ # Report progress — the courier tells what the bureaucrats said.
224
+ say "#{waybill.hold_summary} (#{check + 1}/#{MAX_CHECKS_AT_REGISTRY})..."
225
+
226
+ # Still waiting — pause before the next check.
227
+ pause_between_polls unless check == MAX_CHECKS_AT_REGISTRY - 1
228
+ end
229
+
230
+ # Exhausted all checks — bureau hasn't given a definitive answer.
231
+ result[ :outcome ] = "filed"
232
+ result[ :hold_reason ] = waybill.hold_reason
233
+ end
234
+
235
+ # Is the waybill blocked by something that won't resolve by waiting?
236
+ # CI failure, merge conflict, policy block — the courier should take
237
+ # the parcel back immediately.
238
+ def definitively_blocked?( waybill )
239
+ return false unless waybill.held?
240
+ reason = waybill.hold_reason
241
+ [ "failed_at_registry", "merge_conflict",
242
+ "behind_registry", "policy_block", "draft" ].include?( reason )
243
+ end
244
+
245
+ # The courier speaks — reports progress to whoever is listening.
246
+ def say( message )
247
+ @output&.puts "#{BADGE} #{message}"
248
+ end
249
+
250
+ # Pause between poll checks. Overridable for test isolation.
251
+ def pause_between_polls
252
+ sleep @poll_interval_at_registry
253
+ end
254
+
255
+ # Record a delivery state change in the ledger.
256
+ # No-op when no ledger is injected (e.g. tests).
257
+ def record( parcel, status:, summary: nil )
258
+ return unless @ledger
259
+
260
+ # The ledger needs a repository-like object with .path pointing
261
+ # to the main warehouse root (not a side shelf).
262
+ repo = Struct.new( :path ).new( @warehouse.main_worktree_root )
263
+ @ledger.upsert_delivery(
264
+ repository: repo,
265
+ branch_name: parcel.label,
266
+ head: parcel.head,
267
+ worktree_path: @warehouse.path,
268
+ pr_number: nil,
269
+ pr_url: nil,
270
+ status: status,
271
+ summary: summary,
272
+ cause: nil
273
+ )
274
+ end
275
+
276
+ # Build a blocked result — the courier cannot proceed.
277
+ def blocked( result, message, recovery: nil )
278
+ result[ :exit ] = BLOCKED
279
+ result[ :error ] = message
280
+ result[ :recovery ] = recovery
281
+ result
282
+ end
283
+
284
+ def error( result, message, recovery: nil )
285
+ result[ :exit ] = ERROR
286
+ result[ :error ] = message
287
+ result[ :recovery ] = recovery
288
+ result
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,28 @@
1
+ # The committed changes on a branch — the thing being delivered.
2
+ #
3
+ # In the FedEx metaphor, a parcel sits on a shelf (worktree) identified
4
+ # by a label (branch name). Agents put committed changes — the parcel —
5
+ # on a branch, then call Carson to deliver it.
6
+ #
7
+ # The parcel does not deliver itself. The courier does that.
8
+ # The parcel does not pack itself. The warehouse does that.
9
+ module Carson
10
+ # The committed changes being delivered. Knows its label (branch),
11
+ # head (commit SHA), and shelf (worktree). The protagonist of every
12
+ # delivery — without a parcel, there is nothing to deliver.
13
+ class Parcel
14
+ attr_reader :label, :head, :shelf
15
+
16
+ def initialize( label:, head:, shelf: nil )
17
+ @label = label
18
+ @head = head
19
+ @shelf = shelf
20
+ end
21
+
22
+ # Is this parcel sitting on the main shelf?
23
+ # The main shelf is the destination — you cannot deliver FROM the destination.
24
+ def on_main?( main_label )
25
+ label == main_label
26
+ end
27
+ end
28
+ end
@@ -7,6 +7,12 @@ module Carson
7
7
  class Runtime
8
8
  module Audit
9
9
  def audit!( json_output: false )
10
+ # Sealed workbench guard — hard block before anything else.
11
+ # The warehouse seals the workbench when a parcel ships.
12
+ # No commits allowed until the delivery outcome is confirmed.
13
+ sealed_result = audit_sealed_workbench( json_output: json_output )
14
+ return sealed_result unless sealed_result.nil?
15
+
10
16
  fingerprint_status = block_if_outsider_fingerprints!
11
17
  return fingerprint_status unless fingerprint_status.nil?
12
18
  unless head_exists?
@@ -177,6 +183,30 @@ module Carson
177
183
  end
178
184
  end
179
185
 
186
+ # Check if the workbench is sealed (parcel in flight).
187
+ # Returns EXIT_BLOCK if sealed, nil otherwise.
188
+ def audit_sealed_workbench( json_output: )
189
+ marker_path = File.join( work_dir, ".carson-delivering" )
190
+ return nil unless File.exist?( marker_path )
191
+
192
+ tracking_number = File.read( marker_path ).strip rescue "unknown"
193
+ if json_output
194
+ require "json"
195
+ output.puts JSON.pretty_generate( {
196
+ command: "audit",
197
+ status: "block",
198
+ reason: "workbench_sealed",
199
+ tracking_number: tracking_number,
200
+ recovery: "carson worktree create <name>",
201
+ exit_code: EXIT_BLOCK
202
+ } )
203
+ else
204
+ puts_line "Workbench is sealed — parcel in flight (PR ##{tracking_number})."
205
+ puts_line " \u2192 carson worktree create <name>"
206
+ end
207
+ EXIT_BLOCK
208
+ end
209
+
180
210
  def audit_working_tree_report
181
211
  dirty_reason = dirty_worktree_reason
182
212
  return { dirty: false, context: nil, status: "ok" } if dirty_reason.nil?
@@ -6,118 +6,61 @@ module Carson
6
6
  DELIVER_MERGE_ATTEMPT_CAP = 3
7
7
 
8
8
  # Entry point for `carson deliver`.
9
- # Pushes the current branch, ensures a PR exists, records delivery state,
10
- # waits for merge readiness, and integrates when the path is clear.
11
- # When --commit is supplied, Carson creates one all-dirty agent-authored commit first.
9
+ # Delegates to the OO domain model: Warehouse Courier Waybill.
10
+ # The Courier orchestrates the delivery; Carson renders the result.
12
11
  def deliver!( title: nil, body_file: nil, commit_message: nil, json_output: false )
13
- branch_name = current_branch
14
- main_branch = config.main_branch
15
- remote_name = config.git_remote
16
- result = {
17
- command: "deliver",
18
- branch: branch_name,
19
- git_remote: remote_name,
20
- watch_window_seconds: config.govern_check_wait.to_i,
21
- waited_seconds: 0,
22
- merge_attempted: false
23
- }
12
+ warehouse = Warehouse.new(
13
+ path: work_dir,
14
+ main_label: config.main_branch,
15
+ bureau_address: config.git_remote,
16
+ compliance_checker: method( :deliver_compliance_checker )
17
+ )
18
+ parcel = Parcel.new(
19
+ label: current_branch,
20
+ head: current_head
21
+ )
22
+ courier = Courier.new( warehouse,
23
+ ledger: ledger,
24
+ merge_method: config.govern_merge_method,
25
+ poll_interval_at_registry: config.poll_interval_at_registry,
26
+ output: output
27
+ )
28
+
29
+ result = courier.deliver( parcel,
30
+ title: title,
31
+ body_file: body_file,
32
+ commit_message: commit_message
33
+ )
24
34
 
25
- if branch_name == main_branch
26
- result[ :error ] = "cannot deliver from #{main_branch}"
27
- result[ :recovery ] = "carson worktree create <name>"
28
- return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
29
- end
35
+ deliver_oo_finish( result: result, json_output: json_output )
36
+ end
30
37
 
31
- initial_dirty = working_tree_dirty?
32
- if initial_dirty && commit_message.to_s.strip.empty?
33
- result[ :error ] = "working tree is dirty"
34
- result[ :recovery ] = "carson deliver --commit \"describe this delivery\""
35
- return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
36
- end
38
+ private
37
39
 
38
- if !initial_dirty && !commit_message.to_s.strip.empty?
39
- result[ :commit ] = blocked_commit_payload(
40
- message: commit_message,
41
- summary: "blocked — working tree is already clean"
42
- )
43
- result[ :error ] = "working tree is already clean"
44
- result[ :recovery ] = "carson deliver"
45
- return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
46
- end
40
+ # --- OO bridge methods ---
47
41
 
42
+ # Compliance checker for the Warehouse. Wraps the existing template_apply!
43
+ # machinery and returns the hash contract submit_compliance! expects.
44
+ def deliver_compliance_checker( _warehouse )
48
45
  sync_exit, sync_diagnostics = deliver_template_sync
49
- if sync_exit == EXIT_ERROR
50
- result[ :error ] = sync_diagnostics.to_s.strip.empty? ? "template sync failed" : sync_diagnostics.strip
51
- return deliver_finish( result: result, exit_code: sync_exit, json_output: json_output )
52
- end
53
- template_sync_committed = sync_exit == EXIT_BLOCK
54
-
55
- unless commit_message.to_s.strip.empty?
56
- commit_exit = prepare_delivery_commit!(
57
- commit_message: commit_message,
58
- template_sync_committed: template_sync_committed,
59
- result: result
60
- )
61
- return deliver_finish( result: result, exit_code: commit_exit, json_output: json_output ) unless commit_exit == EXIT_OK
62
- end
63
-
64
- freshness = assess_branch_freshness(
65
- head_ref: current_head,
66
- remote: remote_name,
67
- main: main_branch
68
- )
69
- result[ :freshness ] = freshness_payload( freshness: freshness )
70
- unless freshness.fetch( :ready )
71
- result[ :summary ] = freshness.fetch( :summary )
72
- result[ :error ] = freshness.fetch( :summary )
73
- result[ :recovery ] = freshness_recovery( freshness: freshness )
74
- result[ :main_branch ] = main_branch
75
- return deliver_finish( result: result, exit_code: EXIT_BLOCK, json_output: json_output )
46
+ case sync_exit
47
+ when EXIT_OK
48
+ { compliant: true, committed: false }
49
+ when EXIT_BLOCK
50
+ { compliant: true, committed: true }
51
+ else
52
+ { compliant: false, committed: false, error: sync_diagnostics.to_s.strip.empty? ? "template sync failed" : sync_diagnostics.strip }
76
53
  end
54
+ end
77
55
 
78
- push_exit = push_branch!( branch: branch_name, remote: remote_name, result: result )
79
- return deliver_finish( result: result, exit_code: push_exit, json_output: json_output ) unless push_exit == EXIT_OK
80
-
81
- pr_number, pr_url = find_or_create_pr!(
82
- branch: branch_name,
83
- title: title,
84
- body_file: body_file,
85
- result: result
86
- )
87
- return deliver_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output ) if pr_number.nil?
88
-
89
- branch = branch_record( name: branch_name )
90
- delivery = ledger.upsert_delivery(
91
- repository: repository_record,
92
- branch_name: branch.name,
93
- head: branch.head || current_head,
94
- worktree_path: branch.worktree || repo_root,
95
- pr_number: pr_number,
96
- pr_url: pr_url,
97
- status: "preparing",
98
- summary: "delivery accepted",
99
- cause: nil
100
- )
101
- delivery = settle_delivery!(
102
- delivery: delivery,
103
- branch_name: branch.name,
104
- remote: remote_name,
105
- main: main_branch,
106
- result: result
107
- )
108
-
109
- result[ :pr_number ] = pr_number
110
- result[ :pr_url ] = pr_url
111
- result[ :ci ] = "pass" if delivery.integrated?
112
- result[ :delivery ] = delivery_payload( delivery: delivery )
113
- result[ :main_branch ] = main_branch
114
- result[ :summary ] = delivery.summary
115
- result[ :next_step ] = deliver_next_step( delivery: delivery, result: result )
116
-
117
- deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
56
+ # Render the OO result JSON or human via Carson.report.
57
+ def deliver_oo_finish( result:, json_output: )
58
+ format = json_output ? :json : :human
59
+ Carson.report( result, format: format, output: output )
60
+ result[ :exit ] || Courier::OK
118
61
  end
119
62
 
120
- private
63
+ # --- Legacy deliver methods (used by receive!, status!, etc.) ---
121
64
 
122
65
  def prepare_delivery_commit!( commit_message:, template_sync_committed:, result: )
123
66
  if working_tree_dirty?
@@ -0,0 +1,186 @@
1
+ # A governed repository. In the FedEx metaphor, the warehouse is where
2
+ # parcels (committed changes) are stored on shelves (worktrees) with
3
+ # labels (branches). Git commands are hidden inside — callers never
4
+ # see git terms.
5
+ require "open3"
6
+
7
+ module Carson
8
+ # A governed repository — the warehouse where parcels are stored on
9
+ # shelves (worktrees) with labels (branches). Wraps git operations
10
+ # with story-language methods. An intelligent warehouse that manages
11
+ # itself: packing parcels, checking compliance, and sweeping up.
12
+ class Warehouse
13
+ attr_reader :path
14
+
15
+ def initialize( path:, main_label: "main", bureau_address: "github", compliance_checker: nil )
16
+ @path = path
17
+ @main_label = main_label
18
+ @bureau_address = bureau_address
19
+ @compliance_checker = compliance_checker
20
+ end
21
+
22
+ # --- What the warehouse knows ---
23
+
24
+ # The label on the current shelf (branch name).
25
+ def current_label
26
+ git( "rev-parse", "--abbrev-ref", "HEAD" ).first.strip
27
+ end
28
+
29
+ # The tip of the parcel on the current shelf (commit SHA).
30
+ def current_head
31
+ git( "rev-parse", "HEAD" ).first.strip
32
+ end
33
+
34
+ # The destination label (from config).
35
+ def main_label
36
+ @main_label
37
+ end
38
+
39
+ # The bureau's address (remote name).
40
+ def bureau_address
41
+ @bureau_address
42
+ end
43
+
44
+ # Is the warehouse floor clean? No uncommitted changes on the current shelf.
45
+ def clean?
46
+ output, _, status = git( "status", "--porcelain" )
47
+ status.success? && output.strip.empty?
48
+ end
49
+
50
+ # --- Warehouse operations ---
51
+
52
+ # Ship a parcel to the bureau.
53
+ # The warehouse sends the parcel's label to the remote.
54
+ def ship( parcel, remote: bureau_address )
55
+ _, _, status = git( "push", "-u", remote, parcel.label )
56
+ status.success?
57
+ end
58
+
59
+ # Get latest registry state from the bureau (git fetch).
60
+ # Returns true on success, false on failure.
61
+ def fetch_latest( remote: bureau_address, registry: nil )
62
+ arguments = [ "fetch", remote ]
63
+ arguments << registry if registry
64
+ _, _, status = git( *arguments )
65
+ status.success?
66
+ end
67
+
68
+ # Is the parcel based on the client's latest production standard?
69
+ # Checks whether the registry tip is an ancestor of the parcel's head.
70
+ def based_on_latest_standard?( parcel, registry: "#{bureau_address}/#{main_label}" )
71
+ _, _, status = git( "merge-base", "--is-ancestor", registry, parcel.head )
72
+ status.success?
73
+ end
74
+
75
+ # Ensure the warehouse complies with company standards (template sync).
76
+ # Delegates to the injected compliance checker. If no checker is set,
77
+ # the warehouse assumes compliance — no templates to enforce.
78
+ # Returns a hash: { compliant: true/false, committed: true/false, error: nil/string }
79
+ def submit_compliance!
80
+ return { compliant: true, committed: false } unless @compliance_checker
81
+
82
+ @compliance_checker.call( self )
83
+ end
84
+
85
+ # Update the warehouse's production standard — rebase onto latest registry state.
86
+ # Called after the bureau refuses a parcel for being behind standard.
87
+ # Returns true on success, false on failure.
88
+ def rebase_on_latest_standard!( registry: "#{bureau_address}/#{main_label}" )
89
+ _, _, status = git( "rebase", registry )
90
+ status.success?
91
+ end
92
+
93
+ # Pack a parcel — stage all changes and commit.
94
+ # Refuses if the shelf is sealed (parcel already in flight).
95
+ # Returns true on success, false on failure.
96
+ def pack!( message: )
97
+ if sealed?
98
+ raise "Shelf is sealed — parcel in flight (PR ##{sealed_tracking_number}). " \
99
+ "Create a new worktree to continue working."
100
+ end
101
+ git( "add", "-A" )
102
+ _, _, status = git( "commit", "-m", message )
103
+ status.success?
104
+ end
105
+
106
+ # --- Shelf seal ---
107
+
108
+ # Seal the shelf — no more packing until the delivery outcome is confirmed.
109
+ # The courier seals the shelf after shipping and filing the waybill.
110
+ def seal_shelf!( tracking_number: )
111
+ File.write( delivering_marker_path, tracking_number.to_s )
112
+ end
113
+
114
+ # Unseal the shelf — the courier brought back the parcel.
115
+ # Called when the delivery outcome is held or rejected.
116
+ def unseal_shelf!
117
+ File.delete( delivering_marker_path ) if File.exist?( delivering_marker_path )
118
+ end
119
+
120
+ # Is this shelf sealed for a delivery in flight?
121
+ def sealed?
122
+ File.exist?( delivering_marker_path )
123
+ end
124
+
125
+ # The tracking number of the in-flight delivery (nil if not sealed).
126
+ def sealed_tracking_number
127
+ return nil unless sealed?
128
+ File.read( delivering_marker_path ).strip
129
+ end
130
+
131
+ # Receive the latest standard from the registry after a parcel is accepted.
132
+ # Fast-forwards local main without switching branches.
133
+ # Returns true on success, false on failure.
134
+ def receive_latest_standard!( remote: bureau_address )
135
+ _, _, status = Open3.capture3(
136
+ "git", "-C", main_worktree_root,
137
+ "fetch", remote, "#{main_label}:#{main_label}"
138
+ )
139
+ status.success?
140
+ end
141
+
142
+ # --- Inventory ---
143
+
144
+ # All shelves (worktree paths).
145
+ def shelves
146
+ output, = git( "worktree", "list", "--porcelain" )
147
+ output.lines
148
+ .select { it.start_with?( "worktree " ) }
149
+ .map { it.sub( "worktree ", "" ).strip }
150
+ end
151
+
152
+ # All labels (branch names).
153
+ def labels
154
+ output, = git( "branch", "--format", "%(refname:short)" )
155
+ output.lines.map { it.strip }.reject { it.empty? }
156
+ end
157
+
158
+ # Has this label been merged into main?
159
+ def label_absorbed?( name )
160
+ merged_output, = git( "branch", "--merged", main_label, "--format", "%(refname:short)" )
161
+ merged_output.lines.map { it.strip }.include?( name )
162
+ end
163
+
164
+ # The main warehouse location — resolves correctly even from a side shelf.
165
+ # Used by sync! and ledger recording to always reference the canonical path.
166
+ def main_worktree_root
167
+ git_common_dir, = git( "rev-parse", "--path-format=absolute", "--git-common-dir" )
168
+ common = git_common_dir.strip
169
+ # If it ends with /.git, the parent is the main worktree root.
170
+ common.end_with?( "/.git" ) ? File.dirname( common ) : common
171
+ end
172
+
173
+ private
174
+
175
+ # Path to the delivery marker file — signals the shelf is sealed.
176
+ def delivering_marker_path
177
+ File.join( path, ".carson-delivering" )
178
+ end
179
+
180
+ # All git commands go through this single gateway.
181
+ # Returns [stdout, stderr, status].
182
+ def git( *arguments )
183
+ Open3.capture3( "git", "-C", path, *arguments )
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,243 @@
1
+ # The shipping document filed with the bureau (GitHub PR).
2
+ #
3
+ # The courier files a waybill with the bureau when delivering a parcel.
4
+ # The waybill has a tracking number (PR number), knows the bureau's
5
+ # response (cleared/held/accepted/rejected), and can ask the bureau
6
+ # to accept the parcel into the registry.
7
+ #
8
+ # The bureau is a registry where bureaucrats work. They check parcels
9
+ # (CI, review, mergeability) and either accept them into the registry
10
+ # or hold them with a reason.
11
+ #
12
+ # The waybill uses gh CLI internally — that's a tool, not the domain.
13
+ require "json"
14
+ require "open3"
15
+
16
+ module Carson
17
+ # The shipping document filed with the bureau (GitHub PR). Has a
18
+ # tracking number, knows the bureaucrats' response (cleared/held/
19
+ # accepted/rejected), and can ask the bureau to accept the parcel
20
+ # into the registry. Uses gh CLI internally — that's a tool, not
21
+ # the domain.
22
+ class Waybill
23
+ attr_reader :tracking_number, :url, :label
24
+
25
+ def initialize( label:, warehouse_path:, tracking_number: nil, url: nil, review_gate: nil )
26
+ @label = label
27
+ @warehouse_path = warehouse_path
28
+ @tracking_number = tracking_number
29
+ @url = url
30
+ @review_gate = review_gate
31
+ @state = nil
32
+ @ci = nil
33
+ end
34
+
35
+ # --- Filing ---
36
+
37
+ # Has the waybill been filed with the bureau?
38
+ def filed?
39
+ !tracking_number.nil?
40
+ end
41
+
42
+ # File the waybill with the bureau. Creates a PR on GitHub.
43
+ def file!( title: nil, body_file: nil )
44
+ filing_title = title || default_title
45
+ arguments = [ "pr", "create", "--title", filing_title, "--head", label ]
46
+
47
+ if body_file && File.exist?( body_file )
48
+ arguments.push( "--body-file", body_file )
49
+ else
50
+ arguments.push( "--body", "" )
51
+ end
52
+
53
+ stdout, stderr, success, = gh( *arguments )
54
+ if success
55
+ @url = stdout.to_s.strip
56
+ @tracking_number = @url.split( "/" ).last.to_i
57
+ @tracking_number = nil if @tracking_number == 0
58
+ end
59
+
60
+ # If create failed or returned no number, try to find existing.
61
+ find_existing! unless filed?
62
+ self
63
+ end
64
+
65
+ # Generate a human-readable title from the label.
66
+ def default_title
67
+ label.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) do |character|
68
+ character.upcase
69
+ end
70
+ end
71
+
72
+ # --- Bureau's response ---
73
+
74
+ # Check with the bureau for the latest on this waybill.
75
+ def refresh!
76
+ @state = fetch_state
77
+ @ci = fetch_ci
78
+ self
79
+ end
80
+
81
+ # Has the bureau accepted the parcel into the registry?
82
+ def accepted?
83
+ @state&.dig( "state" ) == "MERGED"
84
+ end
85
+
86
+ # Has the bureau rejected the waybill (closed without merge)?
87
+ def rejected?
88
+ @state&.dig( "state" ) == "CLOSED"
89
+ end
90
+
91
+ # Is the waybill still a draft?
92
+ def draft?
93
+ @state&.dig( "isDraft" ) || false
94
+ end
95
+
96
+ # Has the bureau cleared the parcel for delivery?
97
+ # All bureaucrats pass, no merge blocks, merge state is clean.
98
+ def cleared?
99
+ return false unless filed?
100
+ return false if draft?
101
+ return false unless @ci == :pass
102
+ return false if merge_conflicting? || merge_behind? || merge_policy_blocked?
103
+ merge_status = @state&.dig( "mergeStateStatus" ).to_s.upcase
104
+ mergeable = @state&.dig( "mergeable" ).to_s.upcase
105
+ merge_status == "CLEAN" || mergeable == "MERGEABLE"
106
+ end
107
+
108
+ # Is something blocking this waybill?
109
+ def held?
110
+ return false if cleared? || accepted? || rejected?
111
+ filed?
112
+ end
113
+
114
+ # Why is the waybill being held?
115
+ def hold_reason
116
+ return "draft" if draft?
117
+ return "pending_at_registry" if @ci == :pending
118
+ return "failed_at_registry" if @ci == :fail
119
+ return "error_at_registry" if @ci == :error
120
+ return "merge_conflict" if merge_conflicting?
121
+ return "behind_registry" if merge_behind?
122
+ return "policy_block" if merge_policy_blocked?
123
+ "mergeability_pending"
124
+ end
125
+
126
+ # Human-readable explanation of why the waybill is held.
127
+ def hold_summary
128
+ case hold_reason
129
+ when "draft" then "waybill is still a draft"
130
+ when "pending_at_registry" then "waiting for bureaucrats to check"
131
+ when "failed_at_registry" then "bureaucrats rejected the parcel"
132
+ when "error_at_registry" then "unable to reach the bureaucrats"
133
+ when "merge_conflict" then "parcel has conflicts with registry"
134
+ when "behind_registry" then "parcel is behind the registry"
135
+ when "policy_block" then "blocked by bureau policy"
136
+ else "waiting for bureau assessment"
137
+ end
138
+ end
139
+
140
+ # Is the hold specifically because mergeability is still pending?
141
+ def mergeability_pending?
142
+ hold_reason == "mergeability_pending"
143
+ end
144
+
145
+ # --- Acceptance ---
146
+
147
+ # Ask the bureau to accept the parcel into the registry.
148
+ # Updates own state after the attempt.
149
+ def accept!( method: )
150
+ gh( "pr", "merge", tracking_number.to_s, "--#{method}" )
151
+ refresh!
152
+ self
153
+ end
154
+
155
+ # --- Observation data for delivery records ---
156
+
157
+ # Returns a hash of the bureau's current state for tracking records.
158
+ def to_observation
159
+ return {} unless @state.is_a?( Hash )
160
+
161
+ {
162
+ pull_request_state: @state[ "state" ],
163
+ pull_request_draft: @state[ "isDraft" ],
164
+ pull_request_merged_at: @state[ "mergedAt" ]
165
+ }
166
+ end
167
+
168
+ # --- Test support ---
169
+
170
+ # Stub the bureau's response for testing without gh CLI.
171
+ def stub_bureau_response( state: nil, ci: nil )
172
+ @state = state if state
173
+ @ci = ci if ci
174
+ end
175
+
176
+ private
177
+
178
+ def merge_conflicting?
179
+ status = @state&.dig( "mergeStateStatus" ).to_s.upcase
180
+ mergeable = @state&.dig( "mergeable" ).to_s.upcase
181
+ mergeable == "CONFLICTING" || status == "DIRTY" || status == "CONFLICTING"
182
+ end
183
+
184
+ def merge_behind?
185
+ @state&.dig( "mergeStateStatus" ).to_s.upcase == "BEHIND"
186
+ end
187
+
188
+ def merge_policy_blocked?
189
+ @state&.dig( "mergeStateStatus" ).to_s.upcase == "BLOCKED"
190
+ end
191
+
192
+ def fetch_state
193
+ stdout, _, success, = gh(
194
+ "pr", "view", tracking_number.to_s,
195
+ "--json", "number,state,isDraft,url,mergeStateStatus,mergeable,mergedAt"
196
+ )
197
+ return nil unless success
198
+
199
+ JSON.parse( stdout )
200
+ rescue JSON::ParserError
201
+ nil
202
+ end
203
+
204
+ def fetch_ci
205
+ stdout, _, success, = gh(
206
+ "pr", "checks", tracking_number.to_s,
207
+ "--json", "name,bucket"
208
+ )
209
+ return :error unless success
210
+
211
+ checks = JSON.parse( stdout ) rescue []
212
+ return :none if checks.empty?
213
+
214
+ buckets = checks.map do |entry|
215
+ entry[ "bucket" ].to_s.downcase
216
+ end
217
+ return :fail if buckets.include?( "fail" )
218
+ return :pending if buckets.include?( "pending" )
219
+
220
+ :pass
221
+ end
222
+
223
+ def find_existing!
224
+ stdout, _, success, = gh(
225
+ "pr", "view", label,
226
+ "--json", "number,url,state"
227
+ )
228
+ if success
229
+ data = JSON.parse( stdout ) rescue nil
230
+ if data && data[ "number" ] && data[ "state" ] == "OPEN"
231
+ @tracking_number = data[ "number" ]
232
+ @url = data[ "url" ].to_s
233
+ end
234
+ end
235
+ end
236
+
237
+ # All gh commands go through this single gateway.
238
+ def gh( *arguments )
239
+ stdout, stderr, status = Open3.capture3( "gh", *arguments, chdir: @warehouse_path )
240
+ [ stdout, stderr, status.success?, status.exitstatus ]
241
+ end
242
+ end
243
+ end
data/lib/carson.rb CHANGED
@@ -3,6 +3,78 @@ require_relative "carson/version"
3
3
 
4
4
  module Carson
5
5
  BADGE = "\u29D3".freeze # ⧓ BLACK BOWTIE (U+29D3)
6
+
7
+ # The company renders results for whoever is listening.
8
+ # JSON is the primary format (agents consume it). Human-readable is secondary.
9
+ # Domain objects return result hashes — Carson decides how to present them.
10
+ def self.report( result, format: :json, output: $stdout )
11
+ case format
12
+ when :json
13
+ require "json"
14
+ output.puts JSON.pretty_generate( result )
15
+ when :human
16
+ report_human( result, output: output )
17
+ end
18
+ end
19
+
20
+ # Human-readable delivery report — technical language for agents and humans.
21
+ # Story language is internal (source code). Output speaks the client's language.
22
+ def self.report_human( result, output: $stdout )
23
+ if result[ :error ]
24
+ output.puts "#{BADGE} #{result[ :error ]}"
25
+ output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
26
+ return
27
+ end
28
+
29
+ output.puts "#{BADGE} Delivery: #{result[ :label ]}" if result[ :label ]
30
+ output.puts "#{BADGE} PR ##{result[ :tracking_number ]} #{result[ :url ]}" if result[ :tracking_number ]
31
+
32
+ remote_main = result[ :remote_main ] || "origin/main"
33
+
34
+ case result[ :outcome ]
35
+ when "delivered"
36
+ output.puts "#{BADGE} Merged."
37
+ output.puts "#{BADGE} Local main synced." if result[ :synced ]
38
+ when "held"
39
+ diagnosis, *recovery_steps = translate_hold( result[ :hold_reason ], remote_main: remote_main )
40
+ output.puts "#{BADGE} #{diagnosis}"
41
+ recovery_steps.each do |step|
42
+ output.puts " \u2192 #{step}"
43
+ end
44
+ when "rejected"
45
+ output.puts "#{BADGE} PR closed externally."
46
+ when "filed"
47
+ output.puts "#{BADGE} Bureau hasn't responded yet. Run carson status to check back."
48
+ end
49
+ end
50
+
51
+ # Translate internal hold reasons to agent-actionable output.
52
+ # Returns [ diagnosis, *recovery_steps ]. The diagnosis says what
53
+ # happened. Each recovery step is a command the agent can execute.
54
+ def self.translate_hold( reason, remote_main: "origin/main" )
55
+ case reason
56
+ when "draft"
57
+ [ "PR is still a draft." ]
58
+ when "pending_at_registry"
59
+ [ "Waiting for CI checks.", "carson status" ]
60
+ when "failed_at_registry"
61
+ [ "CI checks failed.", "carson deliver" ]
62
+ when "error_at_registry"
63
+ [ "Unable to assess CI checks.", "carson status" ]
64
+ when "merge_conflict"
65
+ [ "Merge conflict with #{remote_main}.", "git rebase #{remote_main}", "carson deliver" ]
66
+ when "behind_registry"
67
+ [ "Branch is behind #{remote_main}.", "git rebase #{remote_main}", "carson deliver" ]
68
+ when "policy_block"
69
+ [ "Blocked by branch protection rules." ]
70
+ when "mergeability_pending"
71
+ [ "GitHub is calculating mergeability.", "carson status" ]
72
+ else
73
+ [ "Waiting for merge readiness.", "carson status" ]
74
+ end
75
+ end
76
+
77
+ private_class_method :report_human
6
78
  end
7
79
 
8
80
  require_relative "carson/repository"
@@ -10,6 +82,10 @@ require_relative "carson/branch"
10
82
  require_relative "carson/delivery"
11
83
  require_relative "carson/revision"
12
84
  require_relative "carson/ledger"
85
+ require_relative "carson/parcel"
86
+ require_relative "carson/warehouse"
87
+ require_relative "carson/waybill"
88
+ require_relative "carson/courier"
13
89
  require_relative "carson/worktree"
14
90
  require_relative "carson/config"
15
91
  require_relative "carson/adapters/git"
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: 3.30.3
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -70,8 +70,10 @@ files:
70
70
  - lib/carson/branch.rb
71
71
  - lib/carson/cli.rb
72
72
  - lib/carson/config.rb
73
+ - lib/carson/courier.rb
73
74
  - lib/carson/delivery.rb
74
75
  - lib/carson/ledger.rb
76
+ - lib/carson/parcel.rb
75
77
  - lib/carson/repository.rb
76
78
  - lib/carson/revision.rb
77
79
  - lib/carson/runtime.rb
@@ -100,6 +102,8 @@ files:
100
102
  - lib/carson/runtime/setup.rb
101
103
  - lib/carson/runtime/status.rb
102
104
  - lib/carson/version.rb
105
+ - lib/carson/warehouse.rb
106
+ - lib/carson/waybill.rb
103
107
  - lib/carson/worktree.rb
104
108
  homepage: https://github.com/wanghailei/carson
105
109
  licenses: