carson 4.3.0 → 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: 5d25d50d20c3cc3244b3ab246ca7f5b661e097146993fee1cc5b8dd66f978938
4
- data.tar.gz: 1d2f6a26fc79dc62688c409557f2b74328f593f2e582af17c4a6fa020bb1b75b
3
+ metadata.gz: aaef0edd290f56dd797540049069bdb07d2d5e86cef8ca30c6c40d735d37740e
4
+ data.tar.gz: 6b5d0df422508ef217f275fdaed1ecbc0a6b3b1e800acab0c1520c38a3cd58cd
5
5
  SHA512:
6
- metadata.gz: 234de9b2d4bad297af4b7bf24bd396629690bde5925fa743d9438316dcbb146f88e444b0aed61a72c022de32e34ba09aeedfb078afc4711973437eb5a38e6279
7
- data.tar.gz: 2d3438c23a8146367289413e28d756eee5cb303c58c5820273f471482a83f351d0fc379a3db01a6edbbcdd5f13afbce508b8f999344d0e295dfb6fd74c816f88
6
+ metadata.gz: eede4e41d8d530bb528d0515352f6879b2c4b8f64dacec0c767128f4f8151dcb22b620d0d946ebac395e5edb199f0296acfd7120c4aa211c3e70fc0069910fe1
7
+ data.tar.gz: ce5aa317bba3e47807e918a1432ae2ff9b7254f8f921e09420062e84cefd4bdee3eb3de20073e6d8c1167c6ccde487ac267842f4fc17ec2b6dce4ca6eedf70f8
data/RELEASE.md CHANGED
@@ -7,6 +7,19 @@ 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
+
10
23
  ## 4.3.0
11
24
 
12
25
  ### New
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.3.0
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
@@ -34,7 +34,7 @@ module Carson
34
34
  :govern_agent_provider, :govern_state_path,
35
35
  :govern_check_wait,
36
36
  :poll_interval_at_bureau,
37
- :workstyle
37
+ :bureau
38
38
 
39
39
  def self.load( repo_root: )
40
40
  base_data = default_data
@@ -86,7 +86,7 @@ module Carson
86
86
  "deliver" => {
87
87
  "poll_interval_at_bureau" => 30
88
88
  },
89
- "workstyle" => "local",
89
+ "bureau" => false,
90
90
  "govern" => {
91
91
  "repos" => [],
92
92
  "merge" => {
@@ -249,8 +249,7 @@ module Carson
249
249
  deliver_hash = fetch_hash( hash: data, key: "deliver" )
250
250
  @poll_interval_at_bureau = fetch_non_negative_integer( hash: deliver_hash, key: "poll_interval_at_bureau" )
251
251
 
252
- workstyle_raw = data.fetch( "workstyle", "local" ).to_s.downcase
253
- @workstyle = [ "local", "remote" ].include?( workstyle_raw ) ? workstyle_raw.to_sym : :local
252
+ @bureau = !!data.fetch( "bureau", false )
254
253
 
255
254
  govern_hash = fetch_hash( hash: data, key: "govern" )
256
255
  @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
@@ -64,11 +64,10 @@ module Carson
64
64
  # == Workstyle
65
65
  #
66
66
  # The courier's gesture depends on the workstyle:
67
- # - :local push main to backup vault (simple, no PR, no waiting)
68
- # - :remote ship waybill → bureau acceptance (complex Bureau trip)
69
- #
70
- # The courier doesn't know whether it's doing "backup" or "primary" —
71
- # it just delivers to wherever the workstyle dictates.
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.
72
71
  class Courier
73
72
  # Exit codes — shared contract between Carson employees and the CLI.
74
73
  OK = 0
@@ -80,9 +79,9 @@ module Carson
80
79
  # The courier checks the bureau up to 6 times before leaving.
81
80
  MAX_CHECKS_AT_BUREAU = 6
82
81
 
83
- def initialize( warehouse, workstyle: :local, 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 )
84
83
  @warehouse = warehouse
85
- @workstyle = workstyle
84
+ @bureau = bureau
86
85
  @ledger = ledger
87
86
  @merge_method = merge_method
88
87
  @poll_interval_at_bureau = poll_interval_at_bureau
@@ -90,10 +89,10 @@ module Carson
90
89
  end
91
90
 
92
91
  # Deliver a parcel.
93
- # Local gesture: push main to backup vault.
94
- # Remote gesture: ship to Bureau, file waybill, poll, register.
92
+ # Default: sync the vault to the remote.
93
+ # Bureau enhancement: also file waybill, poll, register.
95
94
  def deliver( parcel, title: nil, body_file: nil, commit_message: nil )
96
- return deliver_locally( parcel ) if @workstyle == :local
95
+ return deliver_locally( parcel ) unless @bureau
97
96
 
98
97
  result = {
99
98
  command: "deliver",
@@ -255,9 +254,9 @@ module Carson
255
254
  result[ :diagnostic ] = waybill.ci_diagnostic
256
255
  end
257
256
 
258
- # Local gesture: push main to the backup vault.
257
+ # Local gesture: sync the vault to the remote.
259
258
  # The parcel is already in the vault (accepted by the Warehouse).
260
- # The courier's job is to push the vault state to the remote backup.
259
+ # The courier's job is to push the vault state to the remote.
261
260
  def deliver_locally( parcel )
262
261
  result = {
263
262
  command: "deliver",
@@ -269,11 +268,9 @@ module Carson
269
268
  main = @warehouse.main_label
270
269
  root = @warehouse.main_worktree_root
271
270
 
272
- # --no-verify bypasses the pre-push hook that blocks direct pushes
273
- # to main in governed repos. This IS Carson's delivery — the vault
274
- # accepted the parcel, now the courier backs it up.
271
+ # The pre-push hook understands local workstyle — no bypass needed.
275
272
  _, stderr, status = Open3.capture3(
276
- "git", "-C", root, "push", "--no-verify", remote, main
273
+ "git", "-C", root, "push", remote, main
277
274
  )
278
275
 
279
276
  if status.success?
@@ -284,7 +281,7 @@ module Carson
284
281
  result[ :exit ] = OK
285
282
  result[ :outcome ] = "delivered"
286
283
  result[ :synced ] = false
287
- result[ :backup_error ] = stderr.strip
284
+ result[ :sync_error ] = stderr.strip
288
285
  end
289
286
 
290
287
  result
@@ -20,7 +20,7 @@ module Carson
20
20
  head: current_head
21
21
  )
22
22
  courier = Courier.new( warehouse,
23
- workstyle: :remote,
23
+ bureau: true,
24
24
  ledger: ledger,
25
25
  merge_method: config.govern_merge_method,
26
26
  poll_interval_at_bureau: config.poll_interval_at_bureau,
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,15 +1017,15 @@ module Carson
1023
1017
  when "template:apply"
1024
1018
  runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
1025
1019
  when "deliver"
1026
- if runtime.config.workstyle == :local
1027
- dispatch_deliver_locally( parsed: parsed, runtime: runtime )
1028
- else
1020
+ if runtime.config.bureau
1029
1021
  runtime.deliver!(
1030
1022
  title: parsed.fetch( :title, nil ),
1031
1023
  body_file: parsed.fetch( :body_file, nil ),
1032
1024
  commit_message: parsed.fetch( :commit_message, nil ),
1033
1025
  json_output: parsed.fetch( :json, false )
1034
1026
  )
1027
+ else
1028
+ dispatch_deliver_locally( parsed: parsed, runtime: runtime )
1035
1029
  end
1036
1030
  when "recover"
1037
1031
  runtime.recover!(
@@ -1102,6 +1096,14 @@ module Carson
1102
1096
  json = parsed.fetch( :json, false )
1103
1097
  output = runtime.output
1104
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
+
1105
1107
  # Step 1: Prepare.
1106
1108
  prep = warehouse.prepare!( parcel, message: message )
1107
1109
  unless prep[ :status ] == "ok"
@@ -1119,12 +1121,12 @@ module Carson
1119
1121
  return report_deliver( result: accept, json: json, output: output )
1120
1122
  end
1121
1123
 
1122
- # Step 3: Courier delivers backup.
1123
- courier = Courier.new( warehouse, workstyle: :local, output: output )
1124
- backup = courier.deliver( parcel )
1124
+ # Step 3: Courier syncs to remote.
1125
+ courier = Courier.new( warehouse, output: output )
1126
+ sync = courier.deliver( parcel )
1125
1127
 
1126
1128
  # Combine results.
1127
- result = accept.merge( backup.slice( :outcome, :synced, :backup_error ) )
1129
+ result = accept.merge( sync.slice( :outcome, :synced, :sync_error ) )
1128
1130
  result[ :command ] = "deliver"
1129
1131
  result[ :outcome ] ||= "delivered"
1130
1132
  report_deliver( result: result, json: json, output: output )
@@ -1146,9 +1148,9 @@ module Carson
1146
1148
  when "ok"
1147
1149
  output.puts "#{BADGE} #{result[ :branch ]} merged into main."
1148
1150
  if result[ :synced ]
1149
- output.puts "#{BADGE} Pushed to #{result.fetch( :remote_main, "remote" )}."
1150
- elsif result[ :backup_error ]
1151
- output.puts "#{BADGE} Backup failed."
1151
+ output.puts "#{BADGE} Synced to remote."
1152
+ elsif result[ :sync_error ]
1153
+ output.puts "#{BADGE} Sync failed."
1152
1154
  output.puts " \u2192 git push"
1153
1155
  end
1154
1156
  when "block", "error"
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.3.0
4
+ version: 4.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang