carson 4.2.1 → 4.3.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: 251588ff2b1c952538965ea349b498e424c9646f6c57965dd516e7729e002035
4
- data.tar.gz: 2fee9945faa9fbace57ef59dfd77cf5fb9c38992f4e573e796daeef8b1972017
3
+ metadata.gz: aaef0edd290f56dd797540049069bdb07d2d5e86cef8ca30c6c40d735d37740e
4
+ data.tar.gz: 6b5d0df422508ef217f275fdaed1ecbc0a6b3b1e800acab0c1520c38a3cd58cd
5
5
  SHA512:
6
- metadata.gz: 3981e6facd927beb517b18151eb0d4804863e6adab4d4d39039e6e9313e644f57caae09e0b292e1e33d49b383e1f3f7975decfe3d0672dc372f3582b757bc513
7
- data.tar.gz: 55175ac0b094290e602d79854e65208fb5f1baa23753ed05c786c48ddda562b2015de76fb7244dc90c12a2495b2d84269e40c3f9521077d646776eeab843c760
6
+ metadata.gz: eede4e41d8d530bb528d0515352f6879b2c4b8f64dacec0c767128f4f8151dcb22b620d0d946ebac395e5edb199f0296acfd7120c4aa211c3e70fc0069910fe1
7
+ data.tar.gz: ce5aa317bba3e47807e918a1432ae2ff9b7254f8f921e09420062e84cefd4bdee3eb3de20073e6d8c1167c6ccde487ac267842f4fc17ec2b6dce4ca6eedf70f8
data/RELEASE.md CHANGED
@@ -7,6 +7,42 @@ Release-note scope rule:
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 4.3.1
11
+
12
+ ### Changed
13
+
14
+ - **Bureau is an enhancement, not a mode.** Config simplified from `workstyle: local/remote` to `bureau: true/false` (default: false). Local delivery is always the foundation. Bureau adds PR + CI on top.
15
+ - **Pre-push hook simplified to no-op.** Pushes to main are always legitimate — local delivery is the base. The hook no longer needs workstyle detection or push guards.
16
+ - **Parcel-on-main guard added.** `carson deliver` blocks when run from main itself — agents must work on a workbench.
17
+ - **Output: "Synced to remote"** replaces "Pushed to remote". Sync is objective.
18
+
19
+ ### Why
20
+
21
+ The key insight: remote-centred is not a different path — it's local delivery with Bureau enhancement bolted on. One path, optional layer. `workstyle` dissolved into a single `bureau` toggle. 72 insertions, 166 deletions.
22
+
23
+ ## 4.3.0
24
+
25
+ ### New
26
+
27
+ - **Local-centred workstyle** — Carson now supports two workstyles. Local-centred (the new default) merges parcels directly into the local vault (main) and pushes to the remote as backup. No PR, no CI gate, no waiting. Remote-centred remains available for projects that need Bureau oversight. Set `workstyle: remote` in `.carson.yml` to use the old behaviour.
28
+ - **`Warehouse::Vault` concern** — the vault is local main, where accepted parcels live. `warehouse.accept!( parcel )` merges a branch into main via fast-forward. If the branch has diverged, the agent must rebase first.
29
+ - **`warehouse.prepare!( parcel, message: )`** — the delivery prep phase: pack, fetch latest standard, check freshness, auto-rebase if behind. No compliance gate in local workstyle — agents handle linting and testing themselves.
30
+
31
+ ### Changed
32
+
33
+ - **Courier is workstyle-aware** — `courier.deliver( parcel )` uses different gestures per workstyle. Local: push main to backup vault. Remote: the existing Bureau trip (unchanged). One verb, two gestures. The Courier doesn't know if it's doing "backup" or "primary" — it just delivers.
34
+ - **CLI orchestrates local delivery** — `carson deliver` in local workstyle follows the checkin/checkout pattern: CLI builds Warehouse + Courier, orchestrates `prepare!` → `accept!` → `courier.deliver`. No Runtime in the local path.
35
+
36
+ ### UX
37
+
38
+ - `carson deliver` output (local workstyle): `⧓ <branch> merged into main.` / `⧓ Pushed to remote.`
39
+ - On failure: clear recovery instructions (`Rebase onto main and deliver again.`)
40
+ - On backup failure: `⧓ Backup failed.` with recovery.
41
+
42
+ ### Why
43
+
44
+ The 2026-03-24 retrospective clarified that for a solo developer with agents, the remote-centred machinery (PR → CI → Bureau polling → merge) added latency, cost, and complexity without serving a need. The vault (local main) is the source of truth. GitHub is the backup vault. The Courier's job is simpler and faster.
45
+
10
46
  ## 4.2.1
11
47
 
12
48
  ### Fixed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.2.1
1
+ 4.3.1
@@ -1,60 +1,14 @@
1
1
  #!/usr/bin/env bash
2
- # Carson pre-push hook — enforces push policy in governed repositories.
2
+ # Carson pre-push hook.
3
3
  #
4
- # Guards:
5
- # 1. Blocks direct push to main/master refs.
6
- # 2. Blocks raw git push in governed repos agents must use `carson deliver`.
7
- # Carson bypasses via --no-verify internally. No env-var signal to spoof.
4
+ # Local delivery is always the base — pushes to main are legitimate.
5
+ # When bureau enhancement is enabled, the Bureau path runs through
6
+ # Runtime.deliver!, not through raw git push. So the hook has nothing
7
+ # to enforce in either case.
8
8
  #
9
- # Bypass: git push --no-verify
9
+ # The hook remains installed for future use (e.g. bureau-specific guards).
10
+ # For now: consume stdin and exit.
10
11
  set -euo pipefail
11
12
 
12
- hooks_dir="$(cd "$(dirname "$0")" && pwd)"
13
- style="$(cat "$hooks_dir/workflow_style" 2>/dev/null || echo "branch")"
14
- [ "$style" = "trunk" ] && exit 0
15
-
16
- # --- Guard 1: block pushes to main/master refs ---
17
-
18
- remote_name="${1:-unknown}"
19
- remote_url="${2:-unknown}"
20
- has_commit_push=false
21
- while read -r local_ref local_sha remote_ref remote_sha; do
22
- case "$remote_ref" in
23
- refs/heads/main|refs/heads/master)
24
- echo "Pushes to ${remote_ref#refs/heads/} go through PRs, not direct push." >&2
25
- echo "Use \`carson deliver\` instead — it handles push, PR, and merge with safety guards." >&2
26
- echo "Bypass: git push --no-verify" >&2
27
- exit 1
28
- ;;
29
- esac
30
- [[ "$local_sha" != "0000000000000000000000000000000000000000" ]] && has_commit_push=true
31
- done
32
-
33
- # --- Guard 2: block raw git push in governed repos ---
34
- # All pushes in governed repos are blocked unconditionally.
35
- # Carson bypasses this hook via --no-verify when pushing internally.
36
- # No env-var bypass — cannot be spoofed.
37
-
38
- config_file="${HOME}/.carson/config.json"
39
- if [[ -f "$config_file" ]]; then
40
- repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
41
- if [[ -n "$repo_root" ]]; then
42
- normalised="$(cd "$repo_root" && pwd -P)"
43
- if grep -qF "\"$normalised\"" "$config_file" 2>/dev/null; then
44
- echo "This repo is Carson-governed — use \`carson deliver\` instead of raw \`git push\`." >&2
45
- echo "Use \`carson deliver\` instead — it handles push, PR, and merge with safety guards." >&2
46
- echo "Bypass: git push --no-verify" >&2
47
- exit 1
48
- fi
49
- fi
50
- fi
51
-
52
- # --- Template sync on push ---
53
-
54
- if $has_commit_push; then
55
- if [[ -n "${CARSON_BIN:-}" ]]; then
56
- ruby "$CARSON_BIN" template apply --push-prep || exit 1
57
- else
58
- carson template apply --push-prep || exit 1
59
- fi
60
- fi
13
+ cat > /dev/null
14
+ exit 0
data/lib/carson/config.rb CHANGED
@@ -33,7 +33,8 @@ module Carson
33
33
  :govern_repos, :govern_merge_method,
34
34
  :govern_agent_provider, :govern_state_path,
35
35
  :govern_check_wait,
36
- :poll_interval_at_bureau
36
+ :poll_interval_at_bureau,
37
+ :bureau
37
38
 
38
39
  def self.load( repo_root: )
39
40
  base_data = default_data
@@ -85,6 +86,7 @@ module Carson
85
86
  "deliver" => {
86
87
  "poll_interval_at_bureau" => 30
87
88
  },
89
+ "bureau" => false,
88
90
  "govern" => {
89
91
  "repos" => [],
90
92
  "merge" => {
@@ -247,6 +249,8 @@ module Carson
247
249
  deliver_hash = fetch_hash( hash: data, key: "deliver" )
248
250
  @poll_interval_at_bureau = fetch_non_negative_integer( hash: deliver_hash, key: "poll_interval_at_bureau" )
249
251
 
252
+ @bureau = !!data.fetch( "bureau", false )
253
+
250
254
  govern_hash = fetch_hash( hash: data, key: "govern" )
251
255
  @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
252
256
  govern_merge_hash = fetch_hash( hash: govern_hash, key: "merge" )
@@ -1,6 +1,8 @@
1
1
  # Carson Co.
2
+ require "open3"
3
+
2
4
  module Carson
3
- # The delivery person — picks up parcels and delivers them to the bureau.
5
+ # The delivery workerwaits at the gate, picks up parcels, delivers them.
4
6
  #
5
7
  # The courier is a Carson employee assigned to a warehouse. They pick up
6
8
  # a parcel, ask the warehouse to ship it, file a waybill, and wait at
@@ -59,11 +61,13 @@ module Carson
59
61
  # answer, the courier reports "filed" — the parcel is still at the bureau
60
62
  # and the shelf stays sealed.
61
63
  #
62
- # == Future: destination modes
64
+ # == Workstyle
63
65
  #
64
- # Currently remote-centred (ship waybill bureau → acceptance).
65
- # A future local-centred mode merges locally; remote is a synced backup.
66
- # The destination mode should be injectable, not baked in.
66
+ # The courier's gesture depends on the workstyle:
67
+ # The courier delivers parcels. The default gesture is local: sync the
68
+ # vault to the remote. When bureau enhancement is enabled, the courier
69
+ # also files a PR and waits for Bureau checks — but that path runs
70
+ # through Runtime.deliver!, not through this default gesture.
67
71
  class Courier
68
72
  # Exit codes — shared contract between Carson employees and the CLI.
69
73
  OK = 0
@@ -75,17 +79,21 @@ module Carson
75
79
  # The courier checks the bureau up to 6 times before leaving.
76
80
  MAX_CHECKS_AT_BUREAU = 6
77
81
 
78
- def initialize( warehouse, ledger: nil, merge_method: "rebase", poll_interval_at_bureau: 30, output: $stdout )
82
+ def initialize( warehouse, bureau: false, ledger: nil, merge_method: "rebase", poll_interval_at_bureau: 30, output: $stdout )
79
83
  @warehouse = warehouse
84
+ @bureau = bureau
80
85
  @ledger = ledger
81
86
  @merge_method = merge_method
82
87
  @poll_interval_at_bureau = poll_interval_at_bureau
83
88
  @output = output
84
89
  end
85
90
 
86
- # Deliver a parcel to the registry.
87
- # Ships it, files a waybill, seals the shelf, waits at the bureau.
91
+ # Deliver a parcel.
92
+ # Default: sync the vault to the remote.
93
+ # Bureau enhancement: also file waybill, poll, register.
88
94
  def deliver( parcel, title: nil, body_file: nil, commit_message: nil )
95
+ return deliver_locally( parcel ) unless @bureau
96
+
89
97
  result = {
90
98
  command: "deliver",
91
99
  label: parcel.label,
@@ -246,6 +254,39 @@ module Carson
246
254
  result[ :diagnostic ] = waybill.ci_diagnostic
247
255
  end
248
256
 
257
+ # Local gesture: sync the vault to the remote.
258
+ # The parcel is already in the vault (accepted by the Warehouse).
259
+ # The courier's job is to push the vault state to the remote.
260
+ def deliver_locally( parcel )
261
+ result = {
262
+ command: "deliver",
263
+ label: parcel.label,
264
+ remote_main: "#{@warehouse.bureau_address}/#{@warehouse.main_label}"
265
+ }
266
+
267
+ remote = @warehouse.bureau_address
268
+ main = @warehouse.main_label
269
+ root = @warehouse.main_worktree_root
270
+
271
+ # The pre-push hook understands local workstyle — no bypass needed.
272
+ _, stderr, status = Open3.capture3(
273
+ "git", "-C", root, "push", remote, main
274
+ )
275
+
276
+ if status.success?
277
+ result[ :exit ] = OK
278
+ result[ :outcome ] = "delivered"
279
+ result[ :synced ] = true
280
+ else
281
+ result[ :exit ] = OK
282
+ result[ :outcome ] = "delivered"
283
+ result[ :synced ] = false
284
+ result[ :sync_error ] = stderr.strip
285
+ end
286
+
287
+ result
288
+ end
289
+
249
290
  # Is the waybill blocked by something that won't resolve by waiting?
250
291
  # CI failure, merge conflict, policy block — the courier should take
251
292
  # the parcel back immediately.
@@ -20,6 +20,7 @@ module Carson
20
20
  head: current_head
21
21
  )
22
22
  courier = Courier.new( warehouse,
23
+ bureau: true,
23
24
  ledger: ledger,
24
25
  merge_method: config.govern_merge_method,
25
26
  poll_interval_at_bureau: config.poll_interval_at_bureau,
@@ -0,0 +1,76 @@
1
+ # The warehouse's vault concern.
2
+ # The vault is local main — where accepted parcels live.
3
+ # In local-centred workstyle, the vault is the source of truth.
4
+ # In remote-centred workstyle, the vault is the backup (receives
5
+ # the standard from the bureau's registry after acceptance).
6
+ require "open3"
7
+
8
+ module Carson
9
+ class Warehouse
10
+ module Vault
11
+
12
+ # Accept a parcel into the vault.
13
+ # Fast-forwards local main to include the parcel's branch.
14
+ # Runs from the main worktree root where main is checked out.
15
+ #
16
+ # Precondition: the parcel's branch must be a fast-forward of main.
17
+ # If not, the agent must rebase first.
18
+ #
19
+ # Returns a result hash:
20
+ # { status: "ok", branch: ..., head: ... }
21
+ # { status: "block", error: ..., recovery: ... }
22
+ # { status: "error", error: ..., recovery: ... }
23
+ def accept!( parcel )
24
+ root = main_worktree_root
25
+
26
+ # Verify main is checked out in the main worktree.
27
+ unless main_checked_out_at?( root )
28
+ return {
29
+ status: "error",
30
+ error: "#{@main_label} is not checked out in the main worktree.",
31
+ recovery: "Check the main worktree state at #{root}."
32
+ }
33
+ end
34
+
35
+ # Fast-forward main to include the parcel's branch.
36
+ _, stderr, status = Open3.capture3(
37
+ "git", "-C", root, "merge", "--ff-only", parcel.label
38
+ )
39
+
40
+ return vault_accepted( parcel, root ) if status.success?
41
+
42
+ vault_blocked( parcel, stderr )
43
+ end
44
+
45
+ private
46
+
47
+ # Check whether main is the checked-out branch at a given path.
48
+ def main_checked_out_at?( root )
49
+ head_ref, _, status = Open3.capture3(
50
+ "git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"
51
+ )
52
+ status.success? && head_ref.strip == @main_label
53
+ end
54
+
55
+ # Build the success result after vault acceptance.
56
+ def vault_accepted( parcel, root )
57
+ new_head, = Open3.capture3( "git", "-C", root, "rev-parse", "HEAD" )
58
+ {
59
+ status: "ok",
60
+ branch: parcel.label,
61
+ head: new_head.strip
62
+ }
63
+ end
64
+
65
+ # Build the blocked/error result when vault acceptance fails.
66
+ def vault_blocked( parcel, stderr )
67
+ {
68
+ status: "block",
69
+ error: "#{parcel.label} cannot be fast-forwarded into #{@main_label}.",
70
+ recovery: "Rebase onto #{@main_label} and deliver again."
71
+ }
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -6,6 +6,7 @@ require "fileutils"
6
6
  require "open3"
7
7
 
8
8
  require_relative "warehouse/workbench"
9
+ require_relative "warehouse/vault"
9
10
  require_relative "warehouse/seal"
10
11
  require_relative "warehouse/bureau"
11
12
 
@@ -16,6 +17,7 @@ module Carson
16
17
  # managing workbenches, and sweeping up.
17
18
  class Warehouse
18
19
  include Workbench
20
+ include Vault
19
21
  include Seal
20
22
  include Bureau
21
23
 
@@ -140,6 +142,49 @@ module Carson
140
142
  end
141
143
  end
142
144
 
145
+ # --- Delivery prep ---
146
+
147
+ # Prepare a parcel for delivery.
148
+ # Orchestrates the prep phase: pack, fetch, standard check, auto-rebase.
149
+ # Returns { status: "ok" } or { status: "block"/"error", error:, recovery: }.
150
+ def prepare!( parcel, message: nil )
151
+ registry = "#{bureau_address}/#{main_label}"
152
+
153
+ # Pack if the agent provided a commit message.
154
+ if message
155
+ unless pack!( message: message )
156
+ return { status: "error", error: "Nothing to commit.", recovery: "Stage changes first." }
157
+ end
158
+ # Update the parcel's head after packing.
159
+ parcel = Parcel.new( label: parcel.label, head: current_head )
160
+ end
161
+
162
+ # Fetch the latest standard.
163
+ unless fetch_latest
164
+ return {
165
+ status: "block",
166
+ error: "Cannot fetch latest standard.",
167
+ recovery: "Check network and remote config, then deliver again."
168
+ }
169
+ end
170
+
171
+ # Check if the parcel is based on the latest standard.
172
+ unless based_on_latest_standard?( parcel, registry: registry )
173
+ # Auto-rebase onto the latest standard.
174
+ unless rebase_on_latest_standard!( registry: registry )
175
+ return {
176
+ status: "block",
177
+ error: "#{parcel.label} conflicts with #{@main_label}.",
178
+ recovery: "Rebase onto #{@main_label}, resolve conflicts, deliver again."
179
+ }
180
+ end
181
+ # Update the parcel's head after rebase.
182
+ parcel = Parcel.new( label: parcel.label, head: current_head )
183
+ end
184
+
185
+ { status: "ok", parcel: parcel }
186
+ end
187
+
143
188
  # --- Inventory ---
144
189
 
145
190
  # All labels (branch names).
data/lib/cli.rb CHANGED
@@ -131,25 +131,19 @@ module Carson
131
131
  OptionParser.new do |parser|
132
132
  parser.banner = "Usage: carson <command> [options]\n carson <repo> <command> [options]"
133
133
  parser.separator ""
134
- parser.separator "Repository governance and workflow automation for coding agents."
135
- parser.separator ""
136
- parser.separator "Portfolio commands:"
137
- parser.separator " list List governed repositories"
138
- parser.separator " onboard Register a repository for governance (requires repo path)"
139
- parser.separator " offboard Remove a repository from governance (requires repo path)"
140
- parser.separator " refresh Re-install hooks and configuration (all governed repos)"
141
- parser.separator " version Show Carson version"
134
+ parser.separator "Keep agents from breaking main. Keep agents from breaking each other."
142
135
  parser.separator ""
143
136
  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"
137
+ parser.separator " checkin Get a fresh workbench"
138
+ parser.separator " deliver Accept into main and back up to remote"
146
139
  parser.separator ""
147
- parser.separator "Repository commands (from CWD or with explicit repo):"
148
- parser.separator " checkout Release a workbench when done"
149
- parser.separator " status Show repository delivery state"
150
- parser.separator " audit Run pre-commit health checks"
140
+ parser.separator "Portfolio:"
141
+ parser.separator " onboard Start governing a repository"
142
+ parser.separator " offboard Stop governing a repository"
143
+ parser.separator " list Show governed repositories"
144
+ parser.separator " version Show Carson version"
151
145
  parser.separator ""
152
- parser.separator "Run `carson <command> --help` for details on a specific command."
146
+ parser.separator "Run `carson <command> --help` for details."
153
147
  end
154
148
  end
155
149
 
@@ -813,20 +807,20 @@ module Carson
813
807
 
814
808
  options = { json: false, title: nil, body_file: nil, commit_message: nil }
815
809
  deliver_parser = OptionParser.new do |parser|
816
- parser.banner = "Usage: carson deliver [--json] [--title TITLE] [--body-file PATH] [--commit MESSAGE]"
810
+ parser.banner = "Usage: carson deliver [--json] [--commit MESSAGE]"
817
811
  parser.separator ""
818
- parser.separator "Push the current branch, create or refresh the pull request, and hand the branch to Carson."
819
- parser.separator "Use --commit to create one all-dirty delivery commit before Carson pushes and opens the PR."
812
+ parser.separator "Accept committed work into main and sync to remote."
813
+ parser.separator "Use --commit to stage and commit all changes before delivery."
820
814
  parser.separator ""
821
815
  parser.separator "Options:"
822
816
  parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
823
- parser.on( "--title TITLE", "PR title (defaults to branch name)" ) { |value| options[ :title ] = value }
824
- parser.on( "--body-file PATH", "File containing PR body text" ) { |value| options[ :body_file ] = value }
825
- parser.on( "--commit MESSAGE", "Commit all dirty user changes before delivery" ) { |value| options[ :commit_message ] = value }
817
+ parser.on( "--title TITLE", "PR title (remote workstyle only)" ) { |value| options[ :title ] = value }
818
+ parser.on( "--body-file PATH", "PR body file (remote workstyle only)" ) { |value| options[ :body_file ] = value }
819
+ parser.on( "--commit MESSAGE", "Commit all changes before delivery" ) { |value| options[ :commit_message ] = value }
826
820
  parser.separator ""
827
821
  parser.separator "Examples:"
828
822
  parser.separator " carson deliver Deliver existing commits"
829
- parser.separator " carson deliver --commit \"fix: harden flow\" Commit dirty changes, then deliver"
823
+ parser.separator " carson deliver --commit \"fix: harden flow\" Commit then deliver"
830
824
  end
831
825
  deliver_parser.parse!( arguments )
832
826
  if options.fetch( :commit_message, nil ).to_s.strip.empty? && !options.fetch( :commit_message, nil ).nil?
@@ -1023,12 +1017,16 @@ module Carson
1023
1017
  when "template:apply"
1024
1018
  runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
1025
1019
  when "deliver"
1026
- runtime.deliver!(
1027
- title: parsed.fetch( :title, nil ),
1028
- body_file: parsed.fetch( :body_file, nil ),
1029
- commit_message: parsed.fetch( :commit_message, nil ),
1030
- json_output: parsed.fetch( :json, false )
1031
- )
1020
+ if runtime.config.bureau
1021
+ runtime.deliver!(
1022
+ title: parsed.fetch( :title, nil ),
1023
+ body_file: parsed.fetch( :body_file, nil ),
1024
+ commit_message: parsed.fetch( :commit_message, nil ),
1025
+ json_output: parsed.fetch( :json, false )
1026
+ )
1027
+ else
1028
+ dispatch_deliver_locally( parsed: parsed, runtime: runtime )
1029
+ end
1032
1030
  when "recover"
1033
1031
  runtime.recover!(
1034
1032
  check_name: parsed.fetch( :check_name ),
@@ -1081,6 +1079,89 @@ module Carson
1081
1079
  report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
1082
1080
  end
1083
1081
 
1082
+ # --- Local-centred delivery ---
1083
+ # CLI orchestrates: prepare → accept → courier.deliver.
1084
+ # No Runtime — follows the checkin/checkout pattern.
1085
+
1086
+ def self.dispatch_deliver_locally( parsed:, runtime: )
1087
+ # The warehouse must be at the current worktree (where the agent works),
1088
+ # not the main worktree root. The agent's branch and head live here.
1089
+ warehouse = Warehouse.new(
1090
+ path: runtime.send( :work_dir ),
1091
+ main_label: runtime.config.main_branch,
1092
+ bureau_address: runtime.config.git_remote
1093
+ )
1094
+ parcel = Parcel.new( label: warehouse.current_label, head: warehouse.current_head )
1095
+ message = parsed.fetch( :commit_message, nil )
1096
+ json = parsed.fetch( :json, false )
1097
+ output = runtime.output
1098
+
1099
+ # Guard: cannot deliver from main itself.
1100
+ if parcel.on_main?( runtime.config.main_branch )
1101
+ result = { command: "deliver", status: "block",
1102
+ error: "Cannot deliver from #{runtime.config.main_branch}.",
1103
+ recovery: "carson checkin <name>" }
1104
+ return report_deliver( result: result, json: json, output: output )
1105
+ end
1106
+
1107
+ # Step 1: Prepare.
1108
+ prep = warehouse.prepare!( parcel, message: message )
1109
+ unless prep[ :status ] == "ok"
1110
+ prep[ :command ] = "deliver"
1111
+ return report_deliver( result: prep, json: json, output: output )
1112
+ end
1113
+
1114
+ # Update parcel if prepare rebased or packed.
1115
+ parcel = prep[ :parcel ] || parcel
1116
+
1117
+ # Step 2: Accept into vault.
1118
+ accept = warehouse.accept!( parcel )
1119
+ accept[ :command ] = "deliver"
1120
+ unless accept[ :status ] == "ok"
1121
+ return report_deliver( result: accept, json: json, output: output )
1122
+ end
1123
+
1124
+ # Step 3: Courier syncs to remote.
1125
+ courier = Courier.new( warehouse, output: output )
1126
+ sync = courier.deliver( parcel )
1127
+
1128
+ # Combine results.
1129
+ result = accept.merge( sync.slice( :outcome, :synced, :sync_error ) )
1130
+ result[ :command ] = "deliver"
1131
+ result[ :outcome ] ||= "delivered"
1132
+ report_deliver( result: result, json: json, output: output )
1133
+ end
1134
+
1135
+ # Render a local delivery result.
1136
+ def self.report_deliver( result:, json:, output: )
1137
+ status = result[ :status ]
1138
+ exit_code = case status
1139
+ when "ok" then Runtime::EXIT_OK
1140
+ when "block" then Runtime::EXIT_BLOCK
1141
+ else Runtime::EXIT_ERROR
1142
+ end
1143
+
1144
+ if json
1145
+ output.puts JSON.pretty_generate( result )
1146
+ else
1147
+ case status
1148
+ when "ok"
1149
+ output.puts "#{BADGE} #{result[ :branch ]} merged into main."
1150
+ if result[ :synced ]
1151
+ output.puts "#{BADGE} Synced to remote."
1152
+ elsif result[ :sync_error ]
1153
+ output.puts "#{BADGE} Sync failed."
1154
+ output.puts " \u2192 git push"
1155
+ end
1156
+ when "block", "error"
1157
+ output.puts "#{BADGE} #{result[ :error ]}"
1158
+ output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
1159
+ end
1160
+ end
1161
+
1162
+ exit_code
1163
+ end
1164
+
1084
1165
  # Build a Warehouse rooted at the main worktree.
1085
1166
  # Uses runtime's resolved root and config for remote/branch names.
1086
1167
  def self.build_warehouse( runtime: )
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.2.1
4
+ version: 4.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -84,6 +84,7 @@ files:
84
84
  - lib/carson/warehouse.rb
85
85
  - lib/carson/warehouse/bureau.rb
86
86
  - lib/carson/warehouse/seal.rb
87
+ - lib/carson/warehouse/vault.rb
87
88
  - lib/carson/warehouse/workbench.rb
88
89
  - lib/carson/waybill.rb
89
90
  - lib/carson/worktree.rb