carson 4.0.0 → 4.0.2
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 +25 -5
- data/VERSION +1 -1
- data/lib/carson/courier.rb +36 -4
- data/lib/carson/runtime/audit.rb +31 -0
- data/lib/carson/runtime/deliver.rb +2 -1
- data/lib/carson/warehouse.rb +74 -4
- 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: 8bf071658d6db37302910554b54a528ac3efe7faa22a1cb88b42d13faf0b6b30
|
|
4
|
+
data.tar.gz: 0e30d7ca60c8d8f13438562b003009a64820ff7109ca1c525a127282a5c942c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d50b557c6e2a29b78d4f82dfedf2f89c4d6eacc68fb561d5684eb7c40582ba43de5ff84b6929c992fd98b5069e1ee15b17fb434f4512b3ea30f94c97a387ae6
|
|
7
|
+
data.tar.gz: 4cc5c13c062c226ffcfd8627882ecb6c3093169e61dc759d03e2b77110f93323b77bc90761107a8e2a7696b211fe42c603ad0823639c3eb062ff66753410f96b
|
data/RELEASE.md
CHANGED
|
@@ -10,6 +10,27 @@ 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.2
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Seal marker moved outside worktree** — `.carson-delivering` marker now lives at `~/.carson/seals/` instead of inside the worktree. The in-worktree marker polluted `git status`, showed as untracked in editors, and blocked `carson deliver` via the dirty-tree guard. Seal file includes the worktree path for debugging.
|
|
19
|
+
- **`receive_latest_standard!` works from worktrees** (#451) — `git fetch origin main:main` failed when `main` was checked out in the main worktree ("fatal: refusing to fetch into branch checked out"). Now uses `merge --ff-only` when main is checked out, fetch refspec otherwise.
|
|
20
|
+
- **Audit seal check delegates to Warehouse** — `carson audit` no longer hardcodes the marker path; it creates a Warehouse and asks `sealed?`. Single source of truth for seal location.
|
|
21
|
+
|
|
22
|
+
## 4.0.1
|
|
23
|
+
|
|
24
|
+
### What changed
|
|
25
|
+
|
|
26
|
+
- **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.
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
- **`error_at_registry` is transient** — CI assessment errors no longer cause immediate rejection. The courier keeps polling.
|
|
30
|
+
|
|
31
|
+
### Known limitation
|
|
32
|
+
|
|
33
|
+
- 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
34
|
- `govern` renamed to `receive` — single-repo only, no portfolio iteration
|
|
14
35
|
- `repos` renamed to `list`
|
|
15
36
|
- `--all` removed from all repo commands; use `carson list --json` to script batch operations
|
|
@@ -19,15 +40,14 @@ Release-note scope rule:
|
|
|
19
40
|
|
|
20
41
|
### Breaking
|
|
21
42
|
|
|
22
|
-
-
|
|
43
|
+
- **`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
44
|
- **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.
|
|
45
|
+
- **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
46
|
|
|
27
47
|
### New
|
|
28
48
|
|
|
29
|
-
- **`deliver.poll_interval_at_registry` config** — Controls how long
|
|
30
|
-
- **`filed` outcome** — When
|
|
49
|
+
- **`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.
|
|
50
|
+
- **`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
51
|
|
|
32
52
|
### Migration
|
|
33
53
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.2
|
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,31 @@ 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
|
+
# Delegates to Warehouse so the seal path is resolved in one place.
|
|
189
|
+
def audit_sealed_workbench( json_output: )
|
|
190
|
+
warehouse = Warehouse.new( path: work_dir )
|
|
191
|
+
return nil unless warehouse.sealed?
|
|
192
|
+
|
|
193
|
+
tracking_number = warehouse.sealed_tracking_number || "unknown"
|
|
194
|
+
if json_output
|
|
195
|
+
require "json"
|
|
196
|
+
output.puts JSON.pretty_generate( {
|
|
197
|
+
command: "audit",
|
|
198
|
+
status: "block",
|
|
199
|
+
reason: "workbench_sealed",
|
|
200
|
+
tracking_number: tracking_number,
|
|
201
|
+
recovery: "carson worktree create <name>",
|
|
202
|
+
exit_code: EXIT_BLOCK
|
|
203
|
+
} )
|
|
204
|
+
else
|
|
205
|
+
puts_line "Workbench is sealed — parcel in flight (PR ##{tracking_number})."
|
|
206
|
+
puts_line " \u2192 carson worktree create <name>"
|
|
207
|
+
end
|
|
208
|
+
EXIT_BLOCK
|
|
209
|
+
end
|
|
210
|
+
|
|
180
211
|
def audit_working_tree_report
|
|
181
212
|
dirty_reason = dirty_worktree_reason
|
|
182
213
|
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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# parcels (committed changes) are stored on shelves (worktrees) with
|
|
3
3
|
# labels (branches). Git commands are hidden inside — callers never
|
|
4
4
|
# see git terms.
|
|
5
|
+
require "digest"
|
|
6
|
+
require "fileutils"
|
|
5
7
|
require "open3"
|
|
6
8
|
|
|
7
9
|
module Carson
|
|
@@ -91,22 +93,81 @@ module Carson
|
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
# Pack a parcel — stage all changes and commit.
|
|
96
|
+
# Refuses if the shelf is sealed (parcel already in flight).
|
|
94
97
|
# Returns true on success, false on failure.
|
|
95
98
|
def pack!( message: )
|
|
99
|
+
if sealed?
|
|
100
|
+
raise "Shelf is sealed — parcel in flight (PR ##{sealed_tracking_number}). " \
|
|
101
|
+
"Create a new worktree to continue working."
|
|
102
|
+
end
|
|
96
103
|
git( "add", "-A" )
|
|
97
104
|
_, _, status = git( "commit", "-m", message )
|
|
98
105
|
status.success?
|
|
99
106
|
end
|
|
100
107
|
|
|
108
|
+
# --- Shelf seal ---
|
|
109
|
+
|
|
110
|
+
# Seal the shelf — no more packing until the delivery outcome is confirmed.
|
|
111
|
+
# The courier seals the shelf after shipping and filing the waybill.
|
|
112
|
+
# The marker lives outside the worktree (~/.carson/seals/) so it does
|
|
113
|
+
# not pollute git status or block delivery with a dirty-tree guard.
|
|
114
|
+
def seal_shelf!( tracking_number: )
|
|
115
|
+
marker = delivering_marker_path
|
|
116
|
+
FileUtils.mkdir_p( File.dirname( marker ) )
|
|
117
|
+
File.write( marker, "#{tracking_number}\n#{@path}" )
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Unseal the shelf — the courier brought back the parcel.
|
|
121
|
+
# Called when the delivery outcome is held or rejected.
|
|
122
|
+
def unseal_shelf!
|
|
123
|
+
File.delete( delivering_marker_path ) if File.exist?( delivering_marker_path )
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Is this shelf sealed for a delivery in flight?
|
|
127
|
+
def sealed?
|
|
128
|
+
File.exist?( delivering_marker_path )
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The tracking number of the in-flight delivery (nil if not sealed).
|
|
132
|
+
def sealed_tracking_number
|
|
133
|
+
return nil unless sealed?
|
|
134
|
+
File.read( delivering_marker_path ).lines.first.strip
|
|
135
|
+
end
|
|
136
|
+
|
|
101
137
|
# Receive the latest standard from the registry after a parcel is accepted.
|
|
102
138
|
# Fast-forwards local main without switching branches.
|
|
103
139
|
# Returns true on success, false on failure.
|
|
140
|
+
#
|
|
141
|
+
# Two paths depending on the main worktree's checkout state:
|
|
142
|
+
# - Main checked out → merge --ff-only (updates ref + working tree).
|
|
143
|
+
# - Main not checked out → fetch refspec (updates ref only, safe when
|
|
144
|
+
# no worktree has the branch).
|
|
104
145
|
def receive_latest_standard!( remote: bureau_address )
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
146
|
+
root = main_worktree_root
|
|
147
|
+
|
|
148
|
+
# Fetch remote tracking refs — always safe, even when main is checked out.
|
|
149
|
+
_, _, fetch_status = Open3.capture3( "git", "-C", root, "fetch", remote )
|
|
150
|
+
return false unless fetch_status.success?
|
|
151
|
+
|
|
152
|
+
# Determine how to advance local main.
|
|
153
|
+
head_ref, _, head_status = Open3.capture3(
|
|
154
|
+
"git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"
|
|
108
155
|
)
|
|
109
|
-
|
|
156
|
+
return false unless head_status.success?
|
|
157
|
+
|
|
158
|
+
if head_ref.strip == @main_label
|
|
159
|
+
# Main is checked out in the main worktree — fast-forward via merge.
|
|
160
|
+
_, _, merge_status = Open3.capture3(
|
|
161
|
+
"git", "-C", root, "merge", "--ff-only", "#{remote}/#{@main_label}"
|
|
162
|
+
)
|
|
163
|
+
merge_status.success?
|
|
164
|
+
else
|
|
165
|
+
# Main is not checked out — safe to update the ref via fetch refspec.
|
|
166
|
+
_, _, refspec_status = Open3.capture3(
|
|
167
|
+
"git", "-C", root, "fetch", remote, "#{@main_label}:#{@main_label}"
|
|
168
|
+
)
|
|
169
|
+
refspec_status.success?
|
|
170
|
+
end
|
|
110
171
|
end
|
|
111
172
|
|
|
112
173
|
# --- Inventory ---
|
|
@@ -142,6 +203,15 @@ module Carson
|
|
|
142
203
|
|
|
143
204
|
private
|
|
144
205
|
|
|
206
|
+
# Path to the delivery marker file — signals the shelf is sealed.
|
|
207
|
+
# Lives outside the worktree at ~/.carson/seals/<sha256-of-path>
|
|
208
|
+
# so it does not pollute git status.
|
|
209
|
+
def delivering_marker_path
|
|
210
|
+
seals_dir = File.join( Dir.home, ".carson", "seals" )
|
|
211
|
+
key = Digest::SHA256.hexdigest( @path )
|
|
212
|
+
File.join( seals_dir, key )
|
|
213
|
+
end
|
|
214
|
+
|
|
145
215
|
# All git commands go through this single gateway.
|
|
146
216
|
# Returns [stdout, stderr, status].
|
|
147
217
|
def git( *arguments )
|