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 +4 -4
- data/RELEASE.md +13 -0
- data/VERSION +1 -1
- data/config/hooks/pre-push +9 -55
- data/lib/carson/config.rb +3 -4
- data/lib/carson/courier.rb +14 -17
- data/lib/carson/runtime/deliver.rb +1 -1
- data/lib/cli.rb +34 -32
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aaef0edd290f56dd797540049069bdb07d2d5e86cef8ca30c6c40d735d37740e
|
|
4
|
+
data.tar.gz: 6b5d0df422508ef217f275fdaed1ecbc0a6b3b1e800acab0c1520c38a3cd58cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
1
|
+
4.3.1
|
data/config/hooks/pre-push
CHANGED
|
@@ -1,60 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Carson pre-push hook
|
|
2
|
+
# Carson pre-push hook.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
:
|
|
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
|
-
"
|
|
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
|
-
|
|
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 ) }
|
data/lib/carson/courier.rb
CHANGED
|
@@ -64,11 +64,10 @@ module Carson
|
|
|
64
64
|
# == Workstyle
|
|
65
65
|
#
|
|
66
66
|
# The courier's gesture depends on the workstyle:
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
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,
|
|
82
|
+
def initialize( warehouse, bureau: false, ledger: nil, merge_method: "rebase", poll_interval_at_bureau: 30, output: $stdout )
|
|
84
83
|
@warehouse = warehouse
|
|
85
|
-
@
|
|
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
|
-
#
|
|
94
|
-
#
|
|
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 )
|
|
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:
|
|
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
|
|
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
|
-
#
|
|
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",
|
|
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[ :
|
|
284
|
+
result[ :sync_error ] = stderr.strip
|
|
288
285
|
end
|
|
289
286
|
|
|
290
287
|
result
|
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 "
|
|
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
|
|
145
|
-
parser.separator " deliver
|
|
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 "
|
|
148
|
-
parser.separator "
|
|
149
|
-
parser.separator "
|
|
150
|
-
parser.separator "
|
|
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
|
|
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] [--
|
|
810
|
+
parser.banner = "Usage: carson deliver [--json] [--commit MESSAGE]"
|
|
817
811
|
parser.separator ""
|
|
818
|
-
parser.separator "
|
|
819
|
-
parser.separator "Use --commit to
|
|
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 (
|
|
824
|
-
parser.on( "--body-file PATH", "
|
|
825
|
-
parser.on( "--commit MESSAGE", "Commit all
|
|
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
|
|
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.
|
|
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
|
|
1123
|
-
courier = Courier.new( warehouse,
|
|
1124
|
-
|
|
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(
|
|
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}
|
|
1150
|
-
elsif result[ :
|
|
1151
|
-
output.puts "#{BADGE}
|
|
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"
|