carson 4.0.1 → 4.0.3
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 +20 -2
- data/VERSION +1 -1
- data/lib/carson/courier.rb +17 -8
- data/lib/carson/delivery.rb +5 -1
- data/lib/carson/runtime/audit.rb +4 -3
- data/lib/carson/runtime/deliver.rb +3 -3
- data/lib/carson/runtime/local/hooks.rb +7 -0
- data/lib/carson/runtime/local/onboard.rb +17 -1
- data/lib/carson/runtime/receive.rb +16 -0
- data/lib/carson/warehouse.rb +42 -7
- data/lib/carson/waybill.rb +1 -1
- data/lib/carson.rb +6 -2
- 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: a5678a9b3d25a0a0c6e2b37182e53607b8c41e0a199771bbae7535ec776f71c8
|
|
4
|
+
data.tar.gz: c744e380475f851bdc142dc13693f3b35a229b93a45fdbc2ecb70e33fd3ba8cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 136aeaa5ebf42de0f676875a44d8d4bf2eb758b9d70753718ba6cb27ca37756ca42af3518247e1a357c6e5a3bb8cdaa9bbdeff2d1fa9f3fd63302263b4d3c9a1
|
|
7
|
+
data.tar.gz: db5026a393f292c7dee8e1a6b3d63ec4463bcaebea179161ceb184271826d0c1a803bfc9a6a31f6cc5d7446f10be3e52739bebfbb70c548a8cb607fd1f57e4ae
|
data/RELEASE.md
CHANGED
|
@@ -11,6 +11,23 @@ Release-note scope rule:
|
|
|
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
13
|
|
|
14
|
+
## 4.0.3
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **No-checks repos can now merge** (#465) — `Waybill#cleared?` required CI to pass, but repos with no CI checks returned `:none`. These repos were permanently held. Now `:none` is treated as non-blocking.
|
|
19
|
+
- **Sync failure reported explicitly** — When local main fails to sync after a merge, `carson deliver` now prints "Local main not synced — run carson sync." instead of silently omitting the line.
|
|
20
|
+
- **Freshness fetch failure blocks delivery** — `Courier#deliver` ignored the return value of `fetch_latest`. If the fetch failed (network error), Carson reasoned from stale refs. Now blocks immediately with "cannot verify freshness — fetch failed" and Carson-first recovery.
|
|
21
|
+
- **PR data reaches the ledger** — The Courier's `record` method hardcoded `pr_number: nil` and `pr_url: nil`. Filed deliveries had no PR reference in the ledger. Now passes waybill tracking number and URL.
|
|
22
|
+
|
|
23
|
+
## 4.0.2
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **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.
|
|
28
|
+
- **`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.
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
14
31
|
## 4.0.1
|
|
15
32
|
|
|
16
33
|
### What changed
|
|
@@ -34,7 +51,7 @@ Release-note scope rule:
|
|
|
34
51
|
|
|
35
52
|
- **`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.
|
|
36
53
|
- **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.
|
|
37
|
-
- **Hold messages include recovery commands** — Human output for held deliveries now shows actionable next steps (e.g. `→
|
|
54
|
+
- **Hold messages include recovery commands** — Human output for held deliveries now shows actionable next steps (e.g. `→ carson deliver`) instead of prose descriptions.
|
|
38
55
|
|
|
39
56
|
### New
|
|
40
57
|
|
|
@@ -63,7 +80,8 @@ Release-note scope rule:
|
|
|
63
80
|
|
|
64
81
|
### What changed
|
|
65
82
|
|
|
66
|
-
- **Recovery hints re-enter through Carson, not blocked raw commands** — Delivery error recovery messages no longer suggest raw `gh pr create
|
|
83
|
+
- **Recovery hints re-enter through Carson, not blocked raw commands** — Delivery error recovery messages no longer suggest raw `gh pr create` or `gh pr merge`. All recovery paths guide the user back through Carson.
|
|
84
|
+
- **Auto-rebase on delivery** — `carson deliver` automatically rebases the branch when it's behind remote main. Only blocks if the rebase hits a conflict. No more manual `git rebase` before delivery.
|
|
67
85
|
- **Fast PR indentation guard restored** — The Ruby indentation guard in CI now correctly handles access modifier detection, fixing false positives that blocked PRs.
|
|
68
86
|
|
|
69
87
|
### No migration required
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.3
|
data/lib/carson/courier.rb
CHANGED
|
@@ -125,12 +125,21 @@ module Carson
|
|
|
125
125
|
parcel = Parcel.new( label: parcel.label, head: @warehouse.current_head, shelf: parcel.shelf )
|
|
126
126
|
|
|
127
127
|
# 02. Parcel behind standard — not based on client's latest standard.
|
|
128
|
-
|
|
128
|
+
# The courier rebases automatically. Only blocks on conflict.
|
|
129
|
+
unless @warehouse.fetch_latest( registry: @warehouse.main_label )
|
|
130
|
+
return blocked( result,
|
|
131
|
+
"cannot verify freshness — fetch failed",
|
|
132
|
+
recovery: "carson sync, then carson deliver" )
|
|
133
|
+
end
|
|
129
134
|
unless @warehouse.based_on_latest_standard?( parcel )
|
|
130
135
|
remote_main = "#{@warehouse.bureau_address}/#{@warehouse.main_label}"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
say "Branch is behind #{remote_main} — rebasing..."
|
|
137
|
+
unless @warehouse.rebase_on_latest_standard!
|
|
138
|
+
return blocked( result,
|
|
139
|
+
"rebase conflict onto #{remote_main}",
|
|
140
|
+
recovery: "resolve conflicts, then carson deliver" )
|
|
141
|
+
end
|
|
142
|
+
parcel = Parcel.new( label: parcel.label, head: @warehouse.current_head, shelf: parcel.shelf )
|
|
134
143
|
end
|
|
135
144
|
|
|
136
145
|
# Announce the delivery.
|
|
@@ -172,7 +181,7 @@ module Carson
|
|
|
172
181
|
@warehouse.unseal_shelf! if outcome == "delivered" || outcome == "held" || outcome == "rejected"
|
|
173
182
|
|
|
174
183
|
# Update the ledger with the final outcome.
|
|
175
|
-
record( parcel, status: outcome || "filed", summary: result[ :hold_reason ] )
|
|
184
|
+
record( parcel, status: outcome || "filed", summary: result[ :hold_reason ], waybill: waybill )
|
|
176
185
|
|
|
177
186
|
result[ :exit ] ||= OK
|
|
178
187
|
result
|
|
@@ -254,7 +263,7 @@ module Carson
|
|
|
254
263
|
|
|
255
264
|
# Record a delivery state change in the ledger.
|
|
256
265
|
# No-op when no ledger is injected (e.g. tests).
|
|
257
|
-
def record( parcel, status:, summary: nil )
|
|
266
|
+
def record( parcel, status:, summary: nil, waybill: nil )
|
|
258
267
|
return unless @ledger
|
|
259
268
|
|
|
260
269
|
# The ledger needs a repository-like object with .path pointing
|
|
@@ -265,8 +274,8 @@ module Carson
|
|
|
265
274
|
branch_name: parcel.label,
|
|
266
275
|
head: parcel.head,
|
|
267
276
|
worktree_path: @warehouse.path,
|
|
268
|
-
pr_number:
|
|
269
|
-
pr_url:
|
|
277
|
+
pr_number: waybill&.tracking_number,
|
|
278
|
+
pr_url: waybill&.url,
|
|
270
279
|
status: status,
|
|
271
280
|
summary: summary,
|
|
272
281
|
cause: nil
|
data/lib/carson/delivery.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Passive ledger record for one branch delivery attempt.
|
|
2
2
|
module Carson
|
|
3
3
|
class Delivery
|
|
4
|
-
ACTIVE_STATES = %w[preparing gated queued integrating escalated].freeze
|
|
4
|
+
ACTIVE_STATES = %w[preparing gated queued integrating escalated filed].freeze
|
|
5
5
|
BLOCKED_STATES = %w[gated escalated].freeze
|
|
6
6
|
READY_STATES = %w[queued].freeze
|
|
7
7
|
TERMINAL_STATES = %w[integrated failed superseded].freeze
|
|
@@ -60,6 +60,10 @@ module Carson
|
|
|
60
60
|
READY_STATES.include?( status )
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
def filed?
|
|
64
|
+
status == "filed"
|
|
65
|
+
end
|
|
66
|
+
|
|
63
67
|
def integrated?
|
|
64
68
|
status == "integrated"
|
|
65
69
|
end
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -185,11 +185,12 @@ module Carson
|
|
|
185
185
|
|
|
186
186
|
# Check if the workbench is sealed (parcel in flight).
|
|
187
187
|
# Returns EXIT_BLOCK if sealed, nil otherwise.
|
|
188
|
+
# Delegates to Warehouse so the seal path is resolved in one place.
|
|
188
189
|
def audit_sealed_workbench( json_output: )
|
|
189
|
-
|
|
190
|
-
return nil unless
|
|
190
|
+
warehouse = Warehouse.new( path: work_dir )
|
|
191
|
+
return nil unless warehouse.sealed?
|
|
191
192
|
|
|
192
|
-
tracking_number =
|
|
193
|
+
tracking_number = warehouse.sealed_tracking_number || "unknown"
|
|
193
194
|
if json_output
|
|
194
195
|
require "json"
|
|
195
196
|
output.puts JSON.pretty_generate( {
|
|
@@ -231,7 +231,7 @@ module Carson
|
|
|
231
231
|
when :blocked
|
|
232
232
|
result[ :outcome ] = "blocked"
|
|
233
233
|
result[ :waited_seconds ] = elapsed_settle_seconds( started_at: started_at )
|
|
234
|
-
result[ :recovery ] = "
|
|
234
|
+
result[ :recovery ] = "carson deliver" if evaluation[ :cause ] == "freshness"
|
|
235
235
|
apply_handoff!(
|
|
236
236
|
result: result,
|
|
237
237
|
reason: evaluation.fetch( :reason ),
|
|
@@ -752,7 +752,7 @@ module Carson
|
|
|
752
752
|
|
|
753
753
|
def freshness_recovery( freshness: )
|
|
754
754
|
remote_ref = freshness.fetch( :remote_ref )
|
|
755
|
-
return "
|
|
755
|
+
return "carson deliver" if freshness.fetch( :status ) == :behind
|
|
756
756
|
|
|
757
757
|
"carson deliver (once #{remote_ref} is reachable)"
|
|
758
758
|
end
|
|
@@ -840,7 +840,7 @@ module Carson
|
|
|
840
840
|
reason: "freshness_behind",
|
|
841
841
|
cause: "freshness",
|
|
842
842
|
summary: "branch is behind #{remote_main}",
|
|
843
|
-
recovery: "
|
|
843
|
+
recovery: "carson deliver"
|
|
844
844
|
} if merge_state == "BEHIND"
|
|
845
845
|
|
|
846
846
|
return {
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
module Carson
|
|
3
3
|
class Runtime
|
|
4
4
|
module Local
|
|
5
|
+
# Refreshes hooks only — safe to run regardless of worktree or
|
|
6
|
+
# uncommitted-changes state, because hooks write to ~/.carson/hooks/
|
|
7
|
+
# and only touch .git/config, not the working tree.
|
|
8
|
+
def refresh_hooks!
|
|
9
|
+
prepare!
|
|
10
|
+
end
|
|
11
|
+
|
|
5
12
|
private
|
|
6
13
|
|
|
7
14
|
# Installs required hook files and enforces repository hook path.
|
|
@@ -120,8 +120,14 @@ module Carson
|
|
|
120
120
|
|
|
121
121
|
safety = portfolio_repo_safety( repo_path: repo_path )
|
|
122
122
|
unless safety.fetch( :safe )
|
|
123
|
+
# Hooks write to ~/.carson/hooks/, not the repo — always safe to refresh.
|
|
124
|
+
hooks_status = refresh_hooks_single_repo( repo_path: repo_path )
|
|
123
125
|
reason = safety.fetch( :reasons ).join( ", " )
|
|
124
|
-
|
|
126
|
+
if hooks_status == EXIT_OK
|
|
127
|
+
puts_line "#{repo_name}: hooks refreshed, templates pending (#{reason})"
|
|
128
|
+
else
|
|
129
|
+
puts_line "#{repo_name}: PENDING (#{reason})"
|
|
130
|
+
end
|
|
125
131
|
record_batch_skip( command: "refresh", repo_path: repo_path, reason: reason )
|
|
126
132
|
pending += 1
|
|
127
133
|
next
|
|
@@ -304,6 +310,16 @@ module Carson
|
|
|
304
310
|
EXIT_ERROR
|
|
305
311
|
end
|
|
306
312
|
|
|
313
|
+
# Refreshes hooks only for a governed repo using a scoped Runtime.
|
|
314
|
+
# Used when the full refresh is blocked by active worktrees or uncommitted
|
|
315
|
+
# changes — hooks write to ~/.carson/hooks/ and do not touch the working tree.
|
|
316
|
+
def refresh_hooks_single_repo( repo_path: )
|
|
317
|
+
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
318
|
+
scoped_runtime.refresh_hooks!
|
|
319
|
+
rescue StandardError
|
|
320
|
+
EXIT_ERROR
|
|
321
|
+
end
|
|
322
|
+
|
|
307
323
|
def refresh_status_label( status: )
|
|
308
324
|
case status
|
|
309
325
|
when EXIT_OK then "OK"
|
|
@@ -74,7 +74,23 @@ module Carson
|
|
|
74
74
|
|
|
75
75
|
puts_line "#{repository.name}: #{deliveries.length} active deliver#{deliveries.length == 1 ? 'y' : 'ies'}" unless silent
|
|
76
76
|
|
|
77
|
+
# Collect worktree paths of filed deliveries before reconciliation.
|
|
78
|
+
# Receive takes over lifecycle management — the courier's polling window is over.
|
|
79
|
+
filed_worktree_paths = deliveries
|
|
80
|
+
.select( &:filed? )
|
|
81
|
+
.map( &:worktree_path )
|
|
82
|
+
.compact
|
|
83
|
+
.reject { |path| path.to_s.strip.empty? }
|
|
84
|
+
|
|
77
85
|
reconciled = deliveries.map { |item| scoped_runtime.send( :reconcile_delivery!, delivery: item ) }
|
|
86
|
+
|
|
87
|
+
# Unseal worktrees that were filed — receive now owns the delivery lifecycle.
|
|
88
|
+
unless dry_run
|
|
89
|
+
filed_worktree_paths.each do |worktree_path|
|
|
90
|
+
Warehouse.new( path: worktree_path ).unseal_shelf!
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
78
94
|
next_to_integrate = reconciled.find( &:ready? )&.key
|
|
79
95
|
|
|
80
96
|
reconciled.each do |delivery|
|
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
|
|
@@ -107,8 +109,12 @@ module Carson
|
|
|
107
109
|
|
|
108
110
|
# Seal the shelf — no more packing until the delivery outcome is confirmed.
|
|
109
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.
|
|
110
114
|
def seal_shelf!( tracking_number: )
|
|
111
|
-
|
|
115
|
+
marker = delivering_marker_path
|
|
116
|
+
FileUtils.mkdir_p( File.dirname( marker ) )
|
|
117
|
+
File.write( marker, "#{tracking_number}\n#{@path}" )
|
|
112
118
|
end
|
|
113
119
|
|
|
114
120
|
# Unseal the shelf — the courier brought back the parcel.
|
|
@@ -125,18 +131,43 @@ module Carson
|
|
|
125
131
|
# The tracking number of the in-flight delivery (nil if not sealed).
|
|
126
132
|
def sealed_tracking_number
|
|
127
133
|
return nil unless sealed?
|
|
128
|
-
File.read( delivering_marker_path ).strip
|
|
134
|
+
File.read( delivering_marker_path ).lines.first.strip
|
|
129
135
|
end
|
|
130
136
|
|
|
131
137
|
# Receive the latest standard from the registry after a parcel is accepted.
|
|
132
138
|
# Fast-forwards local main without switching branches.
|
|
133
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).
|
|
134
145
|
def receive_latest_standard!( remote: bureau_address )
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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"
|
|
138
155
|
)
|
|
139
|
-
|
|
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
|
|
140
171
|
end
|
|
141
172
|
|
|
142
173
|
# --- Inventory ---
|
|
@@ -173,8 +204,12 @@ module Carson
|
|
|
173
204
|
private
|
|
174
205
|
|
|
175
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.
|
|
176
209
|
def delivering_marker_path
|
|
177
|
-
File.join(
|
|
210
|
+
seals_dir = File.join( Dir.home, ".carson", "seals" )
|
|
211
|
+
key = Digest::SHA256.hexdigest( @path )
|
|
212
|
+
File.join( seals_dir, key )
|
|
178
213
|
end
|
|
179
214
|
|
|
180
215
|
# All git commands go through this single gateway.
|
data/lib/carson/waybill.rb
CHANGED
|
@@ -98,7 +98,7 @@ module Carson
|
|
|
98
98
|
def cleared?
|
|
99
99
|
return false unless filed?
|
|
100
100
|
return false if draft?
|
|
101
|
-
return false unless @ci == :pass
|
|
101
|
+
return false unless @ci == :pass || @ci == :none
|
|
102
102
|
return false if merge_conflicting? || merge_behind? || merge_policy_blocked?
|
|
103
103
|
merge_status = @state&.dig( "mergeStateStatus" ).to_s.upcase
|
|
104
104
|
mergeable = @state&.dig( "mergeable" ).to_s.upcase
|
data/lib/carson.rb
CHANGED
|
@@ -34,7 +34,11 @@ module Carson
|
|
|
34
34
|
case result[ :outcome ]
|
|
35
35
|
when "delivered"
|
|
36
36
|
output.puts "#{BADGE} Merged."
|
|
37
|
-
|
|
37
|
+
if result[ :synced ]
|
|
38
|
+
output.puts "#{BADGE} Local main synced."
|
|
39
|
+
elsif result.key?( :synced )
|
|
40
|
+
output.puts "#{BADGE} Local main not synced \u2014 run carson sync."
|
|
41
|
+
end
|
|
38
42
|
when "held"
|
|
39
43
|
diagnosis, *recovery_steps = translate_hold( result[ :hold_reason ], remote_main: remote_main )
|
|
40
44
|
output.puts "#{BADGE} #{diagnosis}"
|
|
@@ -64,7 +68,7 @@ module Carson
|
|
|
64
68
|
when "merge_conflict"
|
|
65
69
|
[ "Merge conflict with #{remote_main}.", "git rebase #{remote_main}", "carson deliver" ]
|
|
66
70
|
when "behind_registry"
|
|
67
|
-
[ "Branch is behind #{remote_main}.", "
|
|
71
|
+
[ "Branch is behind #{remote_main}.", "carson deliver" ]
|
|
68
72
|
when "policy_block"
|
|
69
73
|
[ "Blocked by branch protection rules." ]
|
|
70
74
|
when "mergeability_pending"
|