carson 4.2.1 → 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 +36 -0
- data/VERSION +1 -1
- data/config/hooks/pre-push +9 -55
- data/lib/carson/config.rb +5 -1
- data/lib/carson/courier.rb +49 -8
- data/lib/carson/runtime/deliver.rb +1 -0
- data/lib/carson/warehouse/vault.rb +76 -0
- data/lib/carson/warehouse.rb +45 -0
- data/lib/cli.rb +109 -28
- metadata +2 -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,42 @@ 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
|
+
|
|
23
|
+
## 4.3.0
|
|
24
|
+
|
|
25
|
+
### New
|
|
26
|
+
|
|
27
|
+
- **Local-centred workstyle** — Carson now supports two workstyles. Local-centred (the new default) merges parcels directly into the local vault (main) and pushes to the remote as backup. No PR, no CI gate, no waiting. Remote-centred remains available for projects that need Bureau oversight. Set `workstyle: remote` in `.carson.yml` to use the old behaviour.
|
|
28
|
+
- **`Warehouse::Vault` concern** — the vault is local main, where accepted parcels live. `warehouse.accept!( parcel )` merges a branch into main via fast-forward. If the branch has diverged, the agent must rebase first.
|
|
29
|
+
- **`warehouse.prepare!( parcel, message: )`** — the delivery prep phase: pack, fetch latest standard, check freshness, auto-rebase if behind. No compliance gate in local workstyle — agents handle linting and testing themselves.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Courier is workstyle-aware** — `courier.deliver( parcel )` uses different gestures per workstyle. Local: push main to backup vault. Remote: the existing Bureau trip (unchanged). One verb, two gestures. The Courier doesn't know if it's doing "backup" or "primary" — it just delivers.
|
|
34
|
+
- **CLI orchestrates local delivery** — `carson deliver` in local workstyle follows the checkin/checkout pattern: CLI builds Warehouse + Courier, orchestrates `prepare!` → `accept!` → `courier.deliver`. No Runtime in the local path.
|
|
35
|
+
|
|
36
|
+
### UX
|
|
37
|
+
|
|
38
|
+
- `carson deliver` output (local workstyle): `⧓ <branch> merged into main.` / `⧓ Pushed to remote.`
|
|
39
|
+
- On failure: clear recovery instructions (`Rebase onto main and deliver again.`)
|
|
40
|
+
- On backup failure: `⧓ Backup failed.` with recovery.
|
|
41
|
+
|
|
42
|
+
### Why
|
|
43
|
+
|
|
44
|
+
The 2026-03-24 retrospective clarified that for a solo developer with agents, the remote-centred machinery (PR → CI → Bureau polling → merge) added latency, cost, and complexity without serving a need. The vault (local main) is the source of truth. GitHub is the backup vault. The Courier's job is simpler and faster.
|
|
45
|
+
|
|
10
46
|
## 4.2.1
|
|
11
47
|
|
|
12
48
|
### Fixed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.
|
|
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
|
@@ -33,7 +33,8 @@ module Carson
|
|
|
33
33
|
:govern_repos, :govern_merge_method,
|
|
34
34
|
:govern_agent_provider, :govern_state_path,
|
|
35
35
|
:govern_check_wait,
|
|
36
|
-
:poll_interval_at_bureau
|
|
36
|
+
:poll_interval_at_bureau,
|
|
37
|
+
:bureau
|
|
37
38
|
|
|
38
39
|
def self.load( repo_root: )
|
|
39
40
|
base_data = default_data
|
|
@@ -85,6 +86,7 @@ module Carson
|
|
|
85
86
|
"deliver" => {
|
|
86
87
|
"poll_interval_at_bureau" => 30
|
|
87
88
|
},
|
|
89
|
+
"bureau" => false,
|
|
88
90
|
"govern" => {
|
|
89
91
|
"repos" => [],
|
|
90
92
|
"merge" => {
|
|
@@ -247,6 +249,8 @@ module Carson
|
|
|
247
249
|
deliver_hash = fetch_hash( hash: data, key: "deliver" )
|
|
248
250
|
@poll_interval_at_bureau = fetch_non_negative_integer( hash: deliver_hash, key: "poll_interval_at_bureau" )
|
|
249
251
|
|
|
252
|
+
@bureau = !!data.fetch( "bureau", false )
|
|
253
|
+
|
|
250
254
|
govern_hash = fetch_hash( hash: data, key: "govern" )
|
|
251
255
|
@govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
|
|
252
256
|
govern_merge_hash = fetch_hash( hash: govern_hash, key: "merge" )
|
data/lib/carson/courier.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Carson Co.
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
2
4
|
module Carson
|
|
3
|
-
# The delivery
|
|
5
|
+
# The delivery worker — waits at the gate, picks up parcels, delivers them.
|
|
4
6
|
#
|
|
5
7
|
# The courier is a Carson employee assigned to a warehouse. They pick up
|
|
6
8
|
# a parcel, ask the warehouse to ship it, file a waybill, and wait at
|
|
@@ -59,11 +61,13 @@ module Carson
|
|
|
59
61
|
# answer, the courier reports "filed" — the parcel is still at the bureau
|
|
60
62
|
# and the shelf stays sealed.
|
|
61
63
|
#
|
|
62
|
-
# ==
|
|
64
|
+
# == Workstyle
|
|
63
65
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
66
|
+
# The courier's gesture depends on the workstyle:
|
|
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.
|
|
67
71
|
class Courier
|
|
68
72
|
# Exit codes — shared contract between Carson employees and the CLI.
|
|
69
73
|
OK = 0
|
|
@@ -75,17 +79,21 @@ module Carson
|
|
|
75
79
|
# The courier checks the bureau up to 6 times before leaving.
|
|
76
80
|
MAX_CHECKS_AT_BUREAU = 6
|
|
77
81
|
|
|
78
|
-
def initialize( warehouse, 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 )
|
|
79
83
|
@warehouse = warehouse
|
|
84
|
+
@bureau = bureau
|
|
80
85
|
@ledger = ledger
|
|
81
86
|
@merge_method = merge_method
|
|
82
87
|
@poll_interval_at_bureau = poll_interval_at_bureau
|
|
83
88
|
@output = output
|
|
84
89
|
end
|
|
85
90
|
|
|
86
|
-
# Deliver a parcel
|
|
87
|
-
#
|
|
91
|
+
# Deliver a parcel.
|
|
92
|
+
# Default: sync the vault to the remote.
|
|
93
|
+
# Bureau enhancement: also file waybill, poll, register.
|
|
88
94
|
def deliver( parcel, title: nil, body_file: nil, commit_message: nil )
|
|
95
|
+
return deliver_locally( parcel ) unless @bureau
|
|
96
|
+
|
|
89
97
|
result = {
|
|
90
98
|
command: "deliver",
|
|
91
99
|
label: parcel.label,
|
|
@@ -246,6 +254,39 @@ module Carson
|
|
|
246
254
|
result[ :diagnostic ] = waybill.ci_diagnostic
|
|
247
255
|
end
|
|
248
256
|
|
|
257
|
+
# Local gesture: sync the vault to the remote.
|
|
258
|
+
# The parcel is already in the vault (accepted by the Warehouse).
|
|
259
|
+
# The courier's job is to push the vault state to the remote.
|
|
260
|
+
def deliver_locally( parcel )
|
|
261
|
+
result = {
|
|
262
|
+
command: "deliver",
|
|
263
|
+
label: parcel.label,
|
|
264
|
+
remote_main: "#{@warehouse.bureau_address}/#{@warehouse.main_label}"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
remote = @warehouse.bureau_address
|
|
268
|
+
main = @warehouse.main_label
|
|
269
|
+
root = @warehouse.main_worktree_root
|
|
270
|
+
|
|
271
|
+
# The pre-push hook understands local workstyle — no bypass needed.
|
|
272
|
+
_, stderr, status = Open3.capture3(
|
|
273
|
+
"git", "-C", root, "push", remote, main
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if status.success?
|
|
277
|
+
result[ :exit ] = OK
|
|
278
|
+
result[ :outcome ] = "delivered"
|
|
279
|
+
result[ :synced ] = true
|
|
280
|
+
else
|
|
281
|
+
result[ :exit ] = OK
|
|
282
|
+
result[ :outcome ] = "delivered"
|
|
283
|
+
result[ :synced ] = false
|
|
284
|
+
result[ :sync_error ] = stderr.strip
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
result
|
|
288
|
+
end
|
|
289
|
+
|
|
249
290
|
# Is the waybill blocked by something that won't resolve by waiting?
|
|
250
291
|
# CI failure, merge conflict, policy block — the courier should take
|
|
251
292
|
# the parcel back immediately.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# The warehouse's vault concern.
|
|
2
|
+
# The vault is local main — where accepted parcels live.
|
|
3
|
+
# In local-centred workstyle, the vault is the source of truth.
|
|
4
|
+
# In remote-centred workstyle, the vault is the backup (receives
|
|
5
|
+
# the standard from the bureau's registry after acceptance).
|
|
6
|
+
require "open3"
|
|
7
|
+
|
|
8
|
+
module Carson
|
|
9
|
+
class Warehouse
|
|
10
|
+
module Vault
|
|
11
|
+
|
|
12
|
+
# Accept a parcel into the vault.
|
|
13
|
+
# Fast-forwards local main to include the parcel's branch.
|
|
14
|
+
# Runs from the main worktree root where main is checked out.
|
|
15
|
+
#
|
|
16
|
+
# Precondition: the parcel's branch must be a fast-forward of main.
|
|
17
|
+
# If not, the agent must rebase first.
|
|
18
|
+
#
|
|
19
|
+
# Returns a result hash:
|
|
20
|
+
# { status: "ok", branch: ..., head: ... }
|
|
21
|
+
# { status: "block", error: ..., recovery: ... }
|
|
22
|
+
# { status: "error", error: ..., recovery: ... }
|
|
23
|
+
def accept!( parcel )
|
|
24
|
+
root = main_worktree_root
|
|
25
|
+
|
|
26
|
+
# Verify main is checked out in the main worktree.
|
|
27
|
+
unless main_checked_out_at?( root )
|
|
28
|
+
return {
|
|
29
|
+
status: "error",
|
|
30
|
+
error: "#{@main_label} is not checked out in the main worktree.",
|
|
31
|
+
recovery: "Check the main worktree state at #{root}."
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fast-forward main to include the parcel's branch.
|
|
36
|
+
_, stderr, status = Open3.capture3(
|
|
37
|
+
"git", "-C", root, "merge", "--ff-only", parcel.label
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return vault_accepted( parcel, root ) if status.success?
|
|
41
|
+
|
|
42
|
+
vault_blocked( parcel, stderr )
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Check whether main is the checked-out branch at a given path.
|
|
48
|
+
def main_checked_out_at?( root )
|
|
49
|
+
head_ref, _, status = Open3.capture3(
|
|
50
|
+
"git", "-C", root, "rev-parse", "--abbrev-ref", "HEAD"
|
|
51
|
+
)
|
|
52
|
+
status.success? && head_ref.strip == @main_label
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build the success result after vault acceptance.
|
|
56
|
+
def vault_accepted( parcel, root )
|
|
57
|
+
new_head, = Open3.capture3( "git", "-C", root, "rev-parse", "HEAD" )
|
|
58
|
+
{
|
|
59
|
+
status: "ok",
|
|
60
|
+
branch: parcel.label,
|
|
61
|
+
head: new_head.strip
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Build the blocked/error result when vault acceptance fails.
|
|
66
|
+
def vault_blocked( parcel, stderr )
|
|
67
|
+
{
|
|
68
|
+
status: "block",
|
|
69
|
+
error: "#{parcel.label} cannot be fast-forwarded into #{@main_label}.",
|
|
70
|
+
recovery: "Rebase onto #{@main_label} and deliver again."
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/carson/warehouse.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "fileutils"
|
|
|
6
6
|
require "open3"
|
|
7
7
|
|
|
8
8
|
require_relative "warehouse/workbench"
|
|
9
|
+
require_relative "warehouse/vault"
|
|
9
10
|
require_relative "warehouse/seal"
|
|
10
11
|
require_relative "warehouse/bureau"
|
|
11
12
|
|
|
@@ -16,6 +17,7 @@ module Carson
|
|
|
16
17
|
# managing workbenches, and sweeping up.
|
|
17
18
|
class Warehouse
|
|
18
19
|
include Workbench
|
|
20
|
+
include Vault
|
|
19
21
|
include Seal
|
|
20
22
|
include Bureau
|
|
21
23
|
|
|
@@ -140,6 +142,49 @@ module Carson
|
|
|
140
142
|
end
|
|
141
143
|
end
|
|
142
144
|
|
|
145
|
+
# --- Delivery prep ---
|
|
146
|
+
|
|
147
|
+
# Prepare a parcel for delivery.
|
|
148
|
+
# Orchestrates the prep phase: pack, fetch, standard check, auto-rebase.
|
|
149
|
+
# Returns { status: "ok" } or { status: "block"/"error", error:, recovery: }.
|
|
150
|
+
def prepare!( parcel, message: nil )
|
|
151
|
+
registry = "#{bureau_address}/#{main_label}"
|
|
152
|
+
|
|
153
|
+
# Pack if the agent provided a commit message.
|
|
154
|
+
if message
|
|
155
|
+
unless pack!( message: message )
|
|
156
|
+
return { status: "error", error: "Nothing to commit.", recovery: "Stage changes first." }
|
|
157
|
+
end
|
|
158
|
+
# Update the parcel's head after packing.
|
|
159
|
+
parcel = Parcel.new( label: parcel.label, head: current_head )
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Fetch the latest standard.
|
|
163
|
+
unless fetch_latest
|
|
164
|
+
return {
|
|
165
|
+
status: "block",
|
|
166
|
+
error: "Cannot fetch latest standard.",
|
|
167
|
+
recovery: "Check network and remote config, then deliver again."
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if the parcel is based on the latest standard.
|
|
172
|
+
unless based_on_latest_standard?( parcel, registry: registry )
|
|
173
|
+
# Auto-rebase onto the latest standard.
|
|
174
|
+
unless rebase_on_latest_standard!( registry: registry )
|
|
175
|
+
return {
|
|
176
|
+
status: "block",
|
|
177
|
+
error: "#{parcel.label} conflicts with #{@main_label}.",
|
|
178
|
+
recovery: "Rebase onto #{@main_label}, resolve conflicts, deliver again."
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
# Update the parcel's head after rebase.
|
|
182
|
+
parcel = Parcel.new( label: parcel.label, head: current_head )
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
{ status: "ok", parcel: parcel }
|
|
186
|
+
end
|
|
187
|
+
|
|
143
188
|
# --- Inventory ---
|
|
144
189
|
|
|
145
190
|
# All labels (branch names).
|
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,12 +1017,16 @@ module Carson
|
|
|
1023
1017
|
when "template:apply"
|
|
1024
1018
|
runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
|
|
1025
1019
|
when "deliver"
|
|
1026
|
-
runtime.
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1020
|
+
if runtime.config.bureau
|
|
1021
|
+
runtime.deliver!(
|
|
1022
|
+
title: parsed.fetch( :title, nil ),
|
|
1023
|
+
body_file: parsed.fetch( :body_file, nil ),
|
|
1024
|
+
commit_message: parsed.fetch( :commit_message, nil ),
|
|
1025
|
+
json_output: parsed.fetch( :json, false )
|
|
1026
|
+
)
|
|
1027
|
+
else
|
|
1028
|
+
dispatch_deliver_locally( parsed: parsed, runtime: runtime )
|
|
1029
|
+
end
|
|
1032
1030
|
when "recover"
|
|
1033
1031
|
runtime.recover!(
|
|
1034
1032
|
check_name: parsed.fetch( :check_name ),
|
|
@@ -1081,6 +1079,89 @@ module Carson
|
|
|
1081
1079
|
report_workbench( result: result, json: parsed.fetch( :json, false ), output: runtime.output )
|
|
1082
1080
|
end
|
|
1083
1081
|
|
|
1082
|
+
# --- Local-centred delivery ---
|
|
1083
|
+
# CLI orchestrates: prepare → accept → courier.deliver.
|
|
1084
|
+
# No Runtime — follows the checkin/checkout pattern.
|
|
1085
|
+
|
|
1086
|
+
def self.dispatch_deliver_locally( parsed:, runtime: )
|
|
1087
|
+
# The warehouse must be at the current worktree (where the agent works),
|
|
1088
|
+
# not the main worktree root. The agent's branch and head live here.
|
|
1089
|
+
warehouse = Warehouse.new(
|
|
1090
|
+
path: runtime.send( :work_dir ),
|
|
1091
|
+
main_label: runtime.config.main_branch,
|
|
1092
|
+
bureau_address: runtime.config.git_remote
|
|
1093
|
+
)
|
|
1094
|
+
parcel = Parcel.new( label: warehouse.current_label, head: warehouse.current_head )
|
|
1095
|
+
message = parsed.fetch( :commit_message, nil )
|
|
1096
|
+
json = parsed.fetch( :json, false )
|
|
1097
|
+
output = runtime.output
|
|
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
|
+
|
|
1107
|
+
# Step 1: Prepare.
|
|
1108
|
+
prep = warehouse.prepare!( parcel, message: message )
|
|
1109
|
+
unless prep[ :status ] == "ok"
|
|
1110
|
+
prep[ :command ] = "deliver"
|
|
1111
|
+
return report_deliver( result: prep, json: json, output: output )
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Update parcel if prepare rebased or packed.
|
|
1115
|
+
parcel = prep[ :parcel ] || parcel
|
|
1116
|
+
|
|
1117
|
+
# Step 2: Accept into vault.
|
|
1118
|
+
accept = warehouse.accept!( parcel )
|
|
1119
|
+
accept[ :command ] = "deliver"
|
|
1120
|
+
unless accept[ :status ] == "ok"
|
|
1121
|
+
return report_deliver( result: accept, json: json, output: output )
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1124
|
+
# Step 3: Courier syncs to remote.
|
|
1125
|
+
courier = Courier.new( warehouse, output: output )
|
|
1126
|
+
sync = courier.deliver( parcel )
|
|
1127
|
+
|
|
1128
|
+
# Combine results.
|
|
1129
|
+
result = accept.merge( sync.slice( :outcome, :synced, :sync_error ) )
|
|
1130
|
+
result[ :command ] = "deliver"
|
|
1131
|
+
result[ :outcome ] ||= "delivered"
|
|
1132
|
+
report_deliver( result: result, json: json, output: output )
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# Render a local delivery result.
|
|
1136
|
+
def self.report_deliver( result:, json:, output: )
|
|
1137
|
+
status = result[ :status ]
|
|
1138
|
+
exit_code = case status
|
|
1139
|
+
when "ok" then Runtime::EXIT_OK
|
|
1140
|
+
when "block" then Runtime::EXIT_BLOCK
|
|
1141
|
+
else Runtime::EXIT_ERROR
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1144
|
+
if json
|
|
1145
|
+
output.puts JSON.pretty_generate( result )
|
|
1146
|
+
else
|
|
1147
|
+
case status
|
|
1148
|
+
when "ok"
|
|
1149
|
+
output.puts "#{BADGE} #{result[ :branch ]} merged into main."
|
|
1150
|
+
if result[ :synced ]
|
|
1151
|
+
output.puts "#{BADGE} Synced to remote."
|
|
1152
|
+
elsif result[ :sync_error ]
|
|
1153
|
+
output.puts "#{BADGE} Sync failed."
|
|
1154
|
+
output.puts " \u2192 git push"
|
|
1155
|
+
end
|
|
1156
|
+
when "block", "error"
|
|
1157
|
+
output.puts "#{BADGE} #{result[ :error ]}"
|
|
1158
|
+
output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
|
|
1159
|
+
end
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
exit_code
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1084
1165
|
# Build a Warehouse rooted at the main worktree.
|
|
1085
1166
|
# Uses runtime's resolved root and config for remote/branch names.
|
|
1086
1167
|
def self.build_warehouse( runtime: )
|
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.
|
|
4
|
+
version: 4.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/carson/warehouse.rb
|
|
85
85
|
- lib/carson/warehouse/bureau.rb
|
|
86
86
|
- lib/carson/warehouse/seal.rb
|
|
87
|
+
- lib/carson/warehouse/vault.rb
|
|
87
88
|
- lib/carson/warehouse/workbench.rb
|
|
88
89
|
- lib/carson/waybill.rb
|
|
89
90
|
- lib/carson/worktree.rb
|