carson 4.0.0 → 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 +4 -4
- data/RELEASE.md +17 -5
- data/VERSION +1 -1
- data/lib/carson/courier.rb +36 -4
- data/lib/carson/runtime/audit.rb +30 -0
- data/lib/carson/runtime/deliver.rb +2 -1
- data/lib/carson/warehouse.rb +35 -0
- 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: b65ac7c43c1275299430d71f23f1da3a9896b112aa1f57e334222d06282a3a62
|
|
4
|
+
data.tar.gz: ff4efcd085b8cb0deb4385bfdb40d98407d463547b6e065fbfc455f516cc4dbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b49a0b8760ad230996f6330b378fd7f9a202977c148683c4ae3eec066f2b820f6210e59a0e251ef275471d90db0d8154dee82cc5f97c3e46378cfad1fce074c2
|
|
7
|
+
data.tar.gz: 64cc150e7bb257ebbd6d48a603ab7eaa3e3b9794f08fcc65625bb8264628cc99e14f5603fe526d7818253ac57169de995cdc71c413c75cc6a3e2b56dbbb41f2c
|
data/RELEASE.md
CHANGED
|
@@ -10,6 +10,19 @@ 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
|
|
@@ -19,15 +32,14 @@ Release-note scope rule:
|
|
|
19
32
|
|
|
20
33
|
### Breaking
|
|
21
34
|
|
|
22
|
-
-
|
|
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.
|
|
23
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.
|
|
24
|
-
- **Hold messages include recovery commands** — Human output for held deliveries now shows actionable
|
|
25
|
-
- **Bureau model simplified** — No more "customs" or "inspector" in the domain model. The bureau is a registry where bureaucrats check parcels. Internal only — no user-facing impact beyond the hold_reason rename above.
|
|
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.
|
|
26
38
|
|
|
27
39
|
### New
|
|
28
40
|
|
|
29
|
-
- **`deliver.poll_interval_at_registry` config** — Controls how long
|
|
30
|
-
- **`filed` outcome** — When
|
|
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`.
|
|
31
43
|
|
|
32
44
|
### Migration
|
|
33
45
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.1
|
data/lib/carson/courier.rb
CHANGED
|
@@ -15,6 +15,14 @@ module Carson
|
|
|
15
15
|
# parcels (CI, review, mergeability) and either accept them into the
|
|
16
16
|
# registry or hold them with a reason.
|
|
17
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
|
+
#
|
|
18
26
|
# == Situations the courier encounters
|
|
19
27
|
#
|
|
20
28
|
# Each numbered situation is handled by a specific guard or branch in the
|
|
@@ -45,7 +53,8 @@ module Carson
|
|
|
45
53
|
# It polls up to MAX_CHECKS_AT_REGISTRY times, pausing between each check.
|
|
46
54
|
# If the bureaucrats give a definitive answer (accepted or rejected), the
|
|
47
55
|
# courier acts immediately. If the checks are exhausted without a definitive
|
|
48
|
-
# answer, the courier reports "filed" — the parcel is still at the registry
|
|
56
|
+
# answer, the courier reports "filed" — the parcel is still at the registry
|
|
57
|
+
# and the shelf stays sealed.
|
|
49
58
|
#
|
|
50
59
|
# == Future: destination modes
|
|
51
60
|
#
|
|
@@ -58,18 +67,21 @@ module Carson
|
|
|
58
67
|
ERROR = 1
|
|
59
68
|
BLOCKED = 2
|
|
60
69
|
|
|
70
|
+
BADGE = "\u29D3".freeze
|
|
71
|
+
|
|
61
72
|
# The courier checks the registry up to 6 times before leaving.
|
|
62
73
|
MAX_CHECKS_AT_REGISTRY = 6
|
|
63
74
|
|
|
64
|
-
def initialize( warehouse, ledger: nil, merge_method: "rebase", poll_interval_at_registry: 30 )
|
|
75
|
+
def initialize( warehouse, ledger: nil, merge_method: "rebase", poll_interval_at_registry: 30, output: $stdout )
|
|
65
76
|
@warehouse = warehouse
|
|
66
77
|
@ledger = ledger
|
|
67
78
|
@merge_method = merge_method
|
|
68
79
|
@poll_interval_at_registry = poll_interval_at_registry
|
|
80
|
+
@output = output
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
# Deliver a parcel to the registry.
|
|
72
|
-
# Ships it, files a waybill, waits at the registry
|
|
84
|
+
# Ships it, files a waybill, seals the shelf, waits at the registry.
|
|
73
85
|
def deliver( parcel, title: nil, body_file: nil, commit_message: nil )
|
|
74
86
|
result = {
|
|
75
87
|
command: "deliver",
|
|
@@ -121,6 +133,9 @@ module Carson
|
|
|
121
133
|
recovery: "git rebase #{remote_main}, then carson deliver" )
|
|
122
134
|
end
|
|
123
135
|
|
|
136
|
+
# Announce the delivery.
|
|
137
|
+
say "Carson is delivering committed changes on branch #{parcel.label} to #{result[ :remote_main ]}..."
|
|
138
|
+
|
|
124
139
|
# The courier picks up the parcel — start tracking.
|
|
125
140
|
record( parcel, status: "preparing", summary: "delivery accepted" )
|
|
126
141
|
|
|
@@ -144,11 +159,20 @@ module Carson
|
|
|
144
159
|
result[ :tracking_number ] = waybill.tracking_number
|
|
145
160
|
result[ :url ] = waybill.url
|
|
146
161
|
|
|
162
|
+
# Seal the shelf — no more packing until the outcome is confirmed.
|
|
163
|
+
@warehouse.seal_shelf!( tracking_number: waybill.tracking_number )
|
|
164
|
+
|
|
147
165
|
# Wait at the registry while the bureaucrats check the parcel.
|
|
148
166
|
wait_and_poll_at_registry( waybill, result )
|
|
149
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
|
+
|
|
150
174
|
# Update the ledger with the final outcome.
|
|
151
|
-
record( parcel, status:
|
|
175
|
+
record( parcel, status: outcome || "filed", summary: result[ :hold_reason ] )
|
|
152
176
|
|
|
153
177
|
result[ :exit ] ||= OK
|
|
154
178
|
result
|
|
@@ -196,6 +220,9 @@ module Carson
|
|
|
196
220
|
return
|
|
197
221
|
end
|
|
198
222
|
|
|
223
|
+
# Report progress — the courier tells what the bureaucrats said.
|
|
224
|
+
say "#{waybill.hold_summary} (#{check + 1}/#{MAX_CHECKS_AT_REGISTRY})..."
|
|
225
|
+
|
|
199
226
|
# Still waiting — pause before the next check.
|
|
200
227
|
pause_between_polls unless check == MAX_CHECKS_AT_REGISTRY - 1
|
|
201
228
|
end
|
|
@@ -215,6 +242,11 @@ module Carson
|
|
|
215
242
|
"behind_registry", "policy_block", "draft" ].include?( reason )
|
|
216
243
|
end
|
|
217
244
|
|
|
245
|
+
# The courier speaks — reports progress to whoever is listening.
|
|
246
|
+
def say( message )
|
|
247
|
+
@output&.puts "#{BADGE} #{message}"
|
|
248
|
+
end
|
|
249
|
+
|
|
218
250
|
# Pause between poll checks. Overridable for test isolation.
|
|
219
251
|
def pause_between_polls
|
|
220
252
|
sleep @poll_interval_at_registry
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -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?
|
|
@@ -22,7 +22,8 @@ module Carson
|
|
|
22
22
|
courier = Courier.new( warehouse,
|
|
23
23
|
ledger: ledger,
|
|
24
24
|
merge_method: config.govern_merge_method,
|
|
25
|
-
poll_interval_at_registry: config.poll_interval_at_registry
|
|
25
|
+
poll_interval_at_registry: config.poll_interval_at_registry,
|
|
26
|
+
output: output
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
result = courier.deliver( parcel,
|
data/lib/carson/warehouse.rb
CHANGED
|
@@ -91,13 +91,43 @@ module Carson
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Pack a parcel — stage all changes and commit.
|
|
94
|
+
# Refuses if the shelf is sealed (parcel already in flight).
|
|
94
95
|
# Returns true on success, false on failure.
|
|
95
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
|
|
96
101
|
git( "add", "-A" )
|
|
97
102
|
_, _, status = git( "commit", "-m", message )
|
|
98
103
|
status.success?
|
|
99
104
|
end
|
|
100
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
|
+
|
|
101
131
|
# Receive the latest standard from the registry after a parcel is accepted.
|
|
102
132
|
# Fast-forwards local main without switching branches.
|
|
103
133
|
# Returns true on success, false on failure.
|
|
@@ -142,6 +172,11 @@ module Carson
|
|
|
142
172
|
|
|
143
173
|
private
|
|
144
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
|
+
|
|
145
180
|
# All git commands go through this single gateway.
|
|
146
181
|
# Returns [stdout, stderr, status].
|
|
147
182
|
def git( *arguments )
|