carson 4.0.3 → 4.1.0
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 +19 -0
- data/VERSION +1 -1
- data/config/.github/hooks/command-guard +18 -0
- data/lib/carson/adapters/prompt.rb +1 -1
- data/lib/carson/cli.rb +4 -15
- data/lib/carson/config.rb +4 -4
- data/lib/carson/courier.rb +48 -43
- data/lib/carson/runtime/audit.rb +1 -1
- data/lib/carson/runtime/deliver.rb +2 -2
- data/lib/carson/runtime/housekeep.rb +1 -1
- data/lib/carson/runtime/receive.rb +10 -0
- data/lib/carson/warehouse.rb +118 -3
- data/lib/carson/waybill.rb +55 -135
- data/lib/carson.rb +25 -31
- 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: 4f0dff052fa48cda5c1c0819ff109a9e93c70579c3e450d8af3e82bb55140b54
|
|
4
|
+
data.tar.gz: b2543300a607dccd88aaa3286e3c37b34cb4e847a30b3fb202ee76cc223125fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0b1853fe814ad941a983d4d0676eb1912ec818b3387b3d74ab61241cb413bea296a797b6bb2f7fbbca8b30f978fc6df203a27a1879727bd753c3a615efdf8ec
|
|
7
|
+
data.tar.gz: 3a51ccae965f6f0008123396c0347a3758c8ad28fac60ceabf8a058bb44538afd2988155b9ec895fe0419f0ddf82279c5f86517f2626dec5dfe96b2375877153
|
data/RELEASE.md
CHANGED
|
@@ -7,9 +7,28 @@ Release-note scope rule:
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 4.1.0
|
|
11
|
+
|
|
10
12
|
### Breaking
|
|
11
13
|
|
|
12
14
|
- 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)
|
|
15
|
+
- Config key `poll_interval_at_registry` renamed to `poll_interval_at_bureau`. Env var `CARSON_POLL_INTERVAL_AT_REGISTRY` renamed to `CARSON_POLL_INTERVAL_AT_BUREAU`.
|
|
16
|
+
- Hold reason codes renamed: `*_at_registry` → `*_at_bureau`, `behind_registry` → `behind_bureau`. Affects JSON output consumers matching on these strings.
|
|
17
|
+
- `Carson.translate_hold` removed. Recovery steps now come from `Carson.recovery_steps_for_hold` (private).
|
|
18
|
+
- `Carson.report` format `:human` renamed to `:text`.
|
|
19
|
+
|
|
20
|
+
### What changed
|
|
21
|
+
|
|
22
|
+
- **Waybill is a data object** — `Waybill` no longer calls `gh`. It records findings written onto it by the Warehouse and answers questions about its state. `fetch_ci`, `refresh!`, `accept!`, `file!` removed. Added `record()`, `stamp()`, `ci_diagnostic`.
|
|
23
|
+
- **Warehouse owns bureau interaction** — `check_parcel_at_bureau_with()`, `file_waybill_for!()`, `register_parcel_at_bureau_with!()` added. The Warehouse queries GitHub and writes findings onto the Waybill. Diagnostic context (stderr from `gh pr checks`) is captured and surfaced.
|
|
24
|
+
- **CI diagnostic surfaces in output** (#468) — When CI checks cannot be assessed, the first line of `gh` stderr appears in polling messages and the delivery report. Agents can see WHY checks failed, not just that they failed.
|
|
25
|
+
- **No story language in CLI output** (#458) — `hold_summary` returns client language directly. Polling messages, delivery reports, seal messages, and housekeep output all use technical terms (CI checks, PR, branch, merge). Story language (bureau, bureaucrat, parcel, waybill) is confined to source code.
|
|
26
|
+
- **Consistent naming: bureau, not registry** — GitHub interactions use "bureau" consistently. "Registry" was a subset concept that leaked into method and constant names, creating confusion with "bureau."
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **Repeated opaque CI error polling** (#468) — `fetch_ci` discarded `gh pr checks` stderr. Six retries showed "unable to reach the bureaucrats" with no cause. Now captures the first line of stderr and shows it: "Unable to assess CI checks. — HTTP 404: Not Found (1/6)..."
|
|
31
|
+
- **Story language in polling output** (#458) — Courier polling used `waybill.hold_summary` (story language) instead of client language. Now uses client-language summaries directly.
|
|
13
32
|
|
|
14
33
|
## 4.0.3
|
|
15
34
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0
|
|
1
|
+
4.1.0
|
|
@@ -96,6 +96,9 @@ on_main_branch=false
|
|
|
96
96
|
delivery_pattern='(^|&&|\|\||;|\|)\s*gh\s+pr\s+(create|merge)\b'
|
|
97
97
|
worktree_pattern='(^|&&|\|\||;|\|)\s*git\s+worktree\s+(add|remove)\b'
|
|
98
98
|
pull_rebase_pattern='(^|&&|\|\||;|\|)\s*git\s+pull([^;&|]|\\\|)*--rebase\b'
|
|
99
|
+
fetch_pattern='(^|&&|\|\||;|\|)\s*git\s+fetch\b'
|
|
100
|
+
rebase_pattern='(^|&&|\|\||;|\|)\s*git\s+rebase\b'
|
|
101
|
+
rebase_continue_pattern='(^|&&|\|\||;|\|)\s*git\s+rebase\s+--(continue|abort|skip)\b'
|
|
99
102
|
main_mutation_pattern='(^|&&|\|\||;|\|)\s*git\s+(add|commit)\b'
|
|
100
103
|
|
|
101
104
|
if grep -qE "$delivery_pattern" <<<"$command_text"; then
|
|
@@ -116,6 +119,21 @@ if grep -qE "$pull_rebase_pattern" <<<"$command_text"; then
|
|
|
116
119
|
"Use \`carson sync\` instead — it owns main-branch alignment in governed repos."
|
|
117
120
|
fi
|
|
118
121
|
|
|
122
|
+
if grep -qE "$fetch_pattern" <<<"$command_text"; then
|
|
123
|
+
block_command \
|
|
124
|
+
"This repo is Carson-governed — do not use raw \`git fetch\`." \
|
|
125
|
+
"Carson commands handle fetching internally. Use \`carson deliver\` or \`carson sync\`."
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
if grep -qE "$rebase_pattern" <<<"$command_text"; then
|
|
129
|
+
# Allow conflict resolution: --continue, --abort, --skip.
|
|
130
|
+
if ! grep -qE "$rebase_continue_pattern" <<<"$command_text"; then
|
|
131
|
+
block_command \
|
|
132
|
+
"This repo is Carson-governed — do not use raw \`git rebase\`." \
|
|
133
|
+
"\`carson deliver\` rebases automatically when the branch is behind."
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
119
137
|
if [ "$on_main_worktree" = true ] && [ "$on_main_branch" = true ] && grep -qE "$main_mutation_pattern" <<<"$command_text"; then
|
|
120
138
|
block_command \
|
|
121
139
|
"Main working tree is read-only in this Carson-governed repo." \
|
|
@@ -9,7 +9,7 @@ module Carson
|
|
|
9
9
|
|
|
10
10
|
def build_prompt( work_order: )
|
|
11
11
|
parts = []
|
|
12
|
-
parts << "You are an automated coding agent
|
|
12
|
+
parts << "You are an automated coding agent working on a pull request."
|
|
13
13
|
parts << "Repository: #{sanitize( File.basename( work_order.repo ) )}"
|
|
14
14
|
parts << "<pr_branch>#{sanitize( work_order.branch )}</pr_branch>"
|
|
15
15
|
parts << "PR: ##{work_order.pr_number}"
|
data/lib/carson/cli.rb
CHANGED
|
@@ -142,18 +142,10 @@ module Carson
|
|
|
142
142
|
parser.separator ""
|
|
143
143
|
parser.separator "Repository commands (from CWD or with explicit repo):"
|
|
144
144
|
parser.separator " status Show repository delivery state"
|
|
145
|
-
parser.separator " setup Initialise Carson configuration"
|
|
146
145
|
parser.separator " audit Run pre-commit health checks"
|
|
147
|
-
parser.separator " abandon Close and clean up abandoned delivery work"
|
|
148
|
-
parser.separator " sync Sync local main with remote"
|
|
149
146
|
parser.separator " deliver Start autonomous branch delivery"
|
|
150
147
|
parser.separator " recover Merge the repair PR for one baseline-red governance check"
|
|
151
|
-
parser.separator " prune Remove stale local branches"
|
|
152
148
|
parser.separator " worktree Manage isolated coding worktrees"
|
|
153
|
-
parser.separator " housekeep Sync, reap worktrees, and prune branches"
|
|
154
|
-
parser.separator " review Manage PR review workflow"
|
|
155
|
-
parser.separator " template Manage canonical template files"
|
|
156
|
-
parser.separator " receive Triage and advance deliveries for one repo"
|
|
157
149
|
parser.separator ""
|
|
158
150
|
parser.separator "Run `carson <command> --help` for details on a specific command."
|
|
159
151
|
end
|
|
@@ -478,16 +470,14 @@ module Carson
|
|
|
478
470
|
def self.parse_worktree_subcommand( arguments:, error: )
|
|
479
471
|
options = { json: false, force: false }
|
|
480
472
|
worktree_parser = OptionParser.new do |parser|
|
|
481
|
-
parser.banner = "Usage: carson worktree <create|list
|
|
473
|
+
parser.banner = "Usage: carson worktree <create|list> <name> [options]"
|
|
482
474
|
parser.separator ""
|
|
483
475
|
parser.separator "Manage isolated worktrees for coding agents."
|
|
484
|
-
parser.separator "Create auto-syncs main before branching.
|
|
485
|
-
parser.separator "unpushed commits and CWD-inside-worktree by default."
|
|
476
|
+
parser.separator "Create auto-syncs main before branching."
|
|
486
477
|
parser.separator ""
|
|
487
478
|
parser.separator "Subcommands:"
|
|
488
|
-
parser.separator " create <name>
|
|
489
|
-
parser.separator " list
|
|
490
|
-
parser.separator " remove <name> [--force] Remove a worktree (--force skips safety checks)"
|
|
479
|
+
parser.separator " create <name> Create a new worktree with a fresh branch"
|
|
480
|
+
parser.separator " list List registered worktrees with cleanup status"
|
|
491
481
|
parser.separator ""
|
|
492
482
|
parser.separator "Options:"
|
|
493
483
|
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
@@ -496,7 +486,6 @@ module Carson
|
|
|
496
486
|
parser.separator "Examples:"
|
|
497
487
|
parser.separator " carson worktree create feature-x Create an isolated worktree"
|
|
498
488
|
parser.separator " carson worktree list Show registered worktrees"
|
|
499
|
-
parser.separator " carson worktree remove feature-x Remove after work is pushed"
|
|
500
489
|
end
|
|
501
490
|
worktree_parser.parse!( arguments )
|
|
502
491
|
|
data/lib/carson/config.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Carson
|
|
|
33
33
|
:govern_repos, :govern_merge_method,
|
|
34
34
|
:govern_agent_provider, :govern_state_path,
|
|
35
35
|
:govern_check_wait,
|
|
36
|
-
:
|
|
36
|
+
:poll_interval_at_bureau
|
|
37
37
|
|
|
38
38
|
def self.load( repo_root: )
|
|
39
39
|
base_data = default_data
|
|
@@ -83,7 +83,7 @@ module Carson
|
|
|
83
83
|
"advisory_check_names" => [ "Scheduled review sweep", "Carson governance", "Tag, release, publish" ]
|
|
84
84
|
},
|
|
85
85
|
"deliver" => {
|
|
86
|
-
"
|
|
86
|
+
"poll_interval_at_bureau" => 30
|
|
87
87
|
},
|
|
88
88
|
"govern" => {
|
|
89
89
|
"repos" => [],
|
|
@@ -172,7 +172,7 @@ module Carson
|
|
|
172
172
|
advisory_names = env_string_array( key: "CARSON_AUDIT_ADVISORY_CHECK_NAMES" )
|
|
173
173
|
audit[ "advisory_check_names" ] = advisory_names unless advisory_names.empty?
|
|
174
174
|
deliver = fetch_hash_section( data: copy, key: "deliver" )
|
|
175
|
-
deliver[ "
|
|
175
|
+
deliver[ "poll_interval_at_bureau" ] = env_integer( key: "CARSON_POLL_INTERVAL_AT_BUREAU", fallback: deliver.fetch( "poll_interval_at_bureau" ) )
|
|
176
176
|
govern = fetch_hash_section( data: copy, key: "govern" )
|
|
177
177
|
govern_repos = env_string_array( key: "CARSON_GOVERN_REPOS" )
|
|
178
178
|
govern[ "repos" ] = govern_repos unless govern_repos.empty?
|
|
@@ -245,7 +245,7 @@ module Carson
|
|
|
245
245
|
@audit_advisory_check_names = fetch_optional_string_array( hash: audit_hash, key: "advisory_check_names" )
|
|
246
246
|
|
|
247
247
|
deliver_hash = fetch_hash( hash: data, key: "deliver" )
|
|
248
|
-
@
|
|
248
|
+
@poll_interval_at_bureau = fetch_non_negative_integer( hash: deliver_hash, key: "poll_interval_at_bureau" )
|
|
249
249
|
|
|
250
250
|
govern_hash = fetch_hash( hash: data, key: "govern" )
|
|
251
251
|
@govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
|
data/lib/carson/courier.rb
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
# Carson Co.
|
|
2
2
|
module Carson
|
|
3
|
-
# The delivery person — picks up parcels and delivers them to the
|
|
3
|
+
# The delivery person — picks up parcels and delivers them to the bureau.
|
|
4
4
|
#
|
|
5
5
|
# The courier is a Carson employee assigned to a warehouse. They pick up
|
|
6
|
-
# a parcel,
|
|
7
|
-
#
|
|
6
|
+
# a parcel, ask the warehouse to ship it, file a waybill, and wait at
|
|
7
|
+
# the bureau while the bureaucrats check it.
|
|
8
8
|
#
|
|
9
|
-
# The courier is a thin orchestrator: it
|
|
10
|
-
#
|
|
9
|
+
# The courier is a thin orchestrator: it asks the warehouse to interact
|
|
10
|
+
# with the bureau, reads the waybill for status, and reports results.
|
|
11
|
+
# The domain logic lives in the objects, not the courier.
|
|
11
12
|
#
|
|
12
13
|
# == The bureau
|
|
13
14
|
#
|
|
14
|
-
# The bureau
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# The bureau (GitHub) is where bureaucrats work. They check parcels
|
|
16
|
+
# (CI, review, mergeability) and either accept them into the registry
|
|
17
|
+
# or hold them with a reason. The warehouse owns the connection to
|
|
18
|
+
# the bureau — the courier asks the warehouse to check, file, and
|
|
19
|
+
# register.
|
|
17
20
|
#
|
|
18
21
|
# == Shelf seal
|
|
19
22
|
#
|
|
@@ -32,8 +35,8 @@ module Carson
|
|
|
32
35
|
# 02. Parcel behind standard — not based on client's latest standard.
|
|
33
36
|
# 03. Shipping fails — warehouse couldn't push to the bureau.
|
|
34
37
|
# 04. Waybill filing fails — bureau rejected the paperwork.
|
|
35
|
-
# 05. Pending at
|
|
36
|
-
# 06. Failed at
|
|
38
|
+
# 05. Pending at bureau — bureaucrats still checking (CI running).
|
|
39
|
+
# 06. Failed at bureau — bureaucrats rejected (CI failed).
|
|
37
40
|
# 07. Review pending — review still in progress.
|
|
38
41
|
# 08. Review changes requested — reviewer wants corrections.
|
|
39
42
|
# 09. Merge conflict — parcel conflicts with registry contents.
|
|
@@ -47,18 +50,18 @@ module Carson
|
|
|
47
50
|
# 17. Parcel already delivered — already in registry.
|
|
48
51
|
# 18. Waybill closed — cancelled by someone externally.
|
|
49
52
|
#
|
|
50
|
-
# == Design: wait and poll at the
|
|
53
|
+
# == Design: wait and poll at the bureau
|
|
51
54
|
#
|
|
52
|
-
# The courier waits at the
|
|
53
|
-
# It polls up to
|
|
55
|
+
# The courier waits at the bureau while the bureaucrats check the parcel.
|
|
56
|
+
# It polls up to MAX_CHECKS_AT_BUREAU times, pausing between each check.
|
|
54
57
|
# If the bureaucrats give a definitive answer (accepted or rejected), the
|
|
55
58
|
# courier acts immediately. If the checks are exhausted without a definitive
|
|
56
|
-
# answer, the courier reports "filed" — the parcel is still at the
|
|
59
|
+
# answer, the courier reports "filed" — the parcel is still at the bureau
|
|
57
60
|
# and the shelf stays sealed.
|
|
58
61
|
#
|
|
59
62
|
# == Future: destination modes
|
|
60
63
|
#
|
|
61
|
-
# Currently remote-centred (ship → waybill →
|
|
64
|
+
# Currently remote-centred (ship → waybill → bureau → acceptance).
|
|
62
65
|
# A future local-centred mode merges locally; remote is a synced backup.
|
|
63
66
|
# The destination mode should be injectable, not baked in.
|
|
64
67
|
class Courier
|
|
@@ -69,19 +72,19 @@ module Carson
|
|
|
69
72
|
|
|
70
73
|
BADGE = "\u29D3".freeze
|
|
71
74
|
|
|
72
|
-
# The courier checks the
|
|
73
|
-
|
|
75
|
+
# The courier checks the bureau up to 6 times before leaving.
|
|
76
|
+
MAX_CHECKS_AT_BUREAU = 6
|
|
74
77
|
|
|
75
|
-
def initialize( warehouse, ledger: nil, merge_method: "rebase",
|
|
78
|
+
def initialize( warehouse, ledger: nil, merge_method: "rebase", poll_interval_at_bureau: 30, output: $stdout )
|
|
76
79
|
@warehouse = warehouse
|
|
77
80
|
@ledger = ledger
|
|
78
81
|
@merge_method = merge_method
|
|
79
|
-
@
|
|
82
|
+
@poll_interval_at_bureau = poll_interval_at_bureau
|
|
80
83
|
@output = output
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
# Deliver a parcel to the registry.
|
|
84
|
-
# Ships it, files a waybill, seals the shelf, waits at the
|
|
87
|
+
# Ships it, files a waybill, seals the shelf, waits at the bureau.
|
|
85
88
|
def deliver( parcel, title: nil, body_file: nil, commit_message: nil )
|
|
86
89
|
result = {
|
|
87
90
|
command: "deliver",
|
|
@@ -153,15 +156,11 @@ module Carson
|
|
|
153
156
|
return error( result, "push failed" )
|
|
154
157
|
end
|
|
155
158
|
|
|
156
|
-
# File a waybill with the bureau.
|
|
157
|
-
waybill =
|
|
158
|
-
label: parcel.label,
|
|
159
|
-
warehouse_path: @warehouse.path
|
|
160
|
-
)
|
|
161
|
-
waybill.file!( title: title, body_file: body_file )
|
|
159
|
+
# File a waybill with the bureau — the warehouse handles the gh call.
|
|
160
|
+
waybill = @warehouse.file_waybill_for!( parcel, title: title, body_file: body_file )
|
|
162
161
|
|
|
163
162
|
# 04. Waybill filing fails — bureau rejected the paperwork.
|
|
164
|
-
unless waybill
|
|
163
|
+
unless waybill
|
|
165
164
|
return error( result, "PR creation failed", recovery: "carson deliver" )
|
|
166
165
|
end
|
|
167
166
|
|
|
@@ -171,8 +170,8 @@ module Carson
|
|
|
171
170
|
# Seal the shelf — no more packing until the outcome is confirmed.
|
|
172
171
|
@warehouse.seal_shelf!( tracking_number: waybill.tracking_number )
|
|
173
172
|
|
|
174
|
-
# Wait at the
|
|
175
|
-
|
|
173
|
+
# Wait at the bureau while the bureaucrats check the parcel.
|
|
174
|
+
wait_and_poll_at_bureau( waybill, result )
|
|
176
175
|
|
|
177
176
|
# Unseal based on outcome:
|
|
178
177
|
# delivered/held/rejected → unseal (shelf done or parcel returned)
|
|
@@ -180,7 +179,7 @@ module Carson
|
|
|
180
179
|
outcome = result[ :outcome ]
|
|
181
180
|
@warehouse.unseal_shelf! if outcome == "delivered" || outcome == "held" || outcome == "rejected"
|
|
182
181
|
|
|
183
|
-
# Update the ledger with the final outcome.
|
|
182
|
+
# Update the ledger with the final outcome and PR identity.
|
|
184
183
|
record( parcel, status: outcome || "filed", summary: result[ :hold_reason ], waybill: waybill )
|
|
185
184
|
|
|
186
185
|
result[ :exit ] ||= OK
|
|
@@ -189,12 +188,12 @@ module Carson
|
|
|
189
188
|
|
|
190
189
|
private
|
|
191
190
|
|
|
192
|
-
# Wait at the
|
|
191
|
+
# Wait at the bureau, polling the bureaucrats up to MAX_CHECKS_AT_BUREAU
|
|
193
192
|
# times. The courier stays until a definitive answer comes back or the
|
|
194
193
|
# checks are exhausted.
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
waybill
|
|
194
|
+
def wait_and_poll_at_bureau( waybill, result )
|
|
195
|
+
MAX_CHECKS_AT_BUREAU.times do |check|
|
|
196
|
+
@warehouse.check_parcel_at_bureau_with( waybill )
|
|
198
197
|
|
|
199
198
|
# 14/17. Already accepted — parcel is in the registry.
|
|
200
199
|
if waybill.accepted?
|
|
@@ -210,9 +209,9 @@ module Carson
|
|
|
210
209
|
return
|
|
211
210
|
end
|
|
212
211
|
|
|
213
|
-
# Cleared or mergeability pending —
|
|
212
|
+
# Cleared or mergeability pending — ask the warehouse to register.
|
|
214
213
|
if waybill.cleared? || waybill.mergeability_pending?
|
|
215
|
-
|
|
214
|
+
@warehouse.register_parcel_at_bureau_with!( waybill, method: @merge_method )
|
|
216
215
|
|
|
217
216
|
if waybill.accepted?
|
|
218
217
|
result[ :outcome ] = "delivered"
|
|
@@ -226,19 +225,25 @@ module Carson
|
|
|
226
225
|
result[ :outcome ] = "held"
|
|
227
226
|
result[ :exit ] = BLOCKED
|
|
228
227
|
result[ :hold_reason ] = waybill.hold_reason
|
|
228
|
+
result[ :hold_summary ] = waybill.hold_summary( remote_main: result[ :remote_main ] )
|
|
229
|
+
result[ :diagnostic ] = waybill.ci_diagnostic
|
|
229
230
|
return
|
|
230
231
|
end
|
|
231
232
|
|
|
232
|
-
# Report progress —
|
|
233
|
-
|
|
233
|
+
# Report progress — client-language summary from the waybill.
|
|
234
|
+
summary = waybill.hold_summary( remote_main: result[ :remote_main ] )
|
|
235
|
+
detail = waybill.ci_diagnostic ? " \u2014 #{waybill.ci_diagnostic}" : ""
|
|
236
|
+
say "#{summary}#{detail} (#{check + 1}/#{MAX_CHECKS_AT_BUREAU})..."
|
|
234
237
|
|
|
235
238
|
# Still waiting — pause before the next check.
|
|
236
|
-
pause_between_polls unless check ==
|
|
239
|
+
pause_between_polls unless check == MAX_CHECKS_AT_BUREAU - 1
|
|
237
240
|
end
|
|
238
241
|
|
|
239
242
|
# Exhausted all checks — bureau hasn't given a definitive answer.
|
|
240
243
|
result[ :outcome ] = "filed"
|
|
241
244
|
result[ :hold_reason ] = waybill.hold_reason
|
|
245
|
+
result[ :hold_summary ] = waybill.hold_summary( remote_main: result[ :remote_main ] )
|
|
246
|
+
result[ :diagnostic ] = waybill.ci_diagnostic
|
|
242
247
|
end
|
|
243
248
|
|
|
244
249
|
# Is the waybill blocked by something that won't resolve by waiting?
|
|
@@ -247,8 +252,8 @@ module Carson
|
|
|
247
252
|
def definitively_blocked?( waybill )
|
|
248
253
|
return false unless waybill.held?
|
|
249
254
|
reason = waybill.hold_reason
|
|
250
|
-
[ "
|
|
251
|
-
"
|
|
255
|
+
[ "failed_at_bureau", "merge_conflict",
|
|
256
|
+
"behind_bureau", "policy_block", "draft" ].include?( reason )
|
|
252
257
|
end
|
|
253
258
|
|
|
254
259
|
# The courier speaks — reports progress to whoever is listening.
|
|
@@ -258,11 +263,11 @@ module Carson
|
|
|
258
263
|
|
|
259
264
|
# Pause between poll checks. Overridable for test isolation.
|
|
260
265
|
def pause_between_polls
|
|
261
|
-
sleep @
|
|
266
|
+
sleep @poll_interval_at_bureau
|
|
262
267
|
end
|
|
263
268
|
|
|
264
269
|
# Record a delivery state change in the ledger.
|
|
265
|
-
#
|
|
270
|
+
# When a waybill is provided, its PR identity is persisted.
|
|
266
271
|
def record( parcel, status:, summary: nil, waybill: nil )
|
|
267
272
|
return unless @ledger
|
|
268
273
|
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -202,7 +202,7 @@ module Carson
|
|
|
202
202
|
exit_code: EXIT_BLOCK
|
|
203
203
|
} )
|
|
204
204
|
else
|
|
205
|
-
puts_line "
|
|
205
|
+
puts_line "Branch is locked — PR ##{tracking_number} in flight."
|
|
206
206
|
puts_line " \u2192 carson worktree create <name>"
|
|
207
207
|
end
|
|
208
208
|
EXIT_BLOCK
|
|
@@ -22,7 +22,7 @@ module Carson
|
|
|
22
22
|
courier = Courier.new( warehouse,
|
|
23
23
|
ledger: ledger,
|
|
24
24
|
merge_method: config.govern_merge_method,
|
|
25
|
-
|
|
25
|
+
poll_interval_at_bureau: config.poll_interval_at_bureau,
|
|
26
26
|
output: output
|
|
27
27
|
)
|
|
28
28
|
|
|
@@ -55,7 +55,7 @@ module Carson
|
|
|
55
55
|
|
|
56
56
|
# Render the OO result — JSON or human via Carson.report.
|
|
57
57
|
def deliver_oo_finish( result:, json_output: )
|
|
58
|
-
format = json_output ? :json : :
|
|
58
|
+
format = json_output ? :json : :text
|
|
59
59
|
Carson.report( result, format: format, output: output )
|
|
60
60
|
result[ :exit ] || Courier::OK
|
|
61
61
|
end
|
|
@@ -262,7 +262,7 @@ module Carson
|
|
|
262
262
|
|
|
263
263
|
next unless current_head == delivery.head
|
|
264
264
|
|
|
265
|
-
reason = "
|
|
265
|
+
reason = "merged — delivery recorded"
|
|
266
266
|
reaped = reap_one_worktree!( worktree: worktree, reason: reason )
|
|
267
267
|
next unless reaped
|
|
268
268
|
|
|
@@ -127,6 +127,16 @@ module Carson
|
|
|
127
127
|
)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# No PR number — courier failed to record it. Cannot reconcile against GitHub.
|
|
131
|
+
unless delivery.pull_request_number
|
|
132
|
+
return ledger.update_delivery(
|
|
133
|
+
delivery: delivery,
|
|
134
|
+
status: "failed",
|
|
135
|
+
cause: "policy",
|
|
136
|
+
summary: "PR number missing from delivery record — run carson deliver to refile"
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
130
140
|
pr_state = pull_request_state( number: delivery.pull_request_number )
|
|
131
141
|
if pr_state && pr_state[ "state" ] == "MERGED"
|
|
132
142
|
return ledger.update_delivery(
|
data/lib/carson/warehouse.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# A governed repository. In the FedEx metaphor, the warehouse is where
|
|
2
2
|
# parcels (committed changes) are stored on shelves (worktrees) with
|
|
3
|
-
# labels (branches). Git commands are hidden inside — callers
|
|
4
|
-
# see git terms.
|
|
3
|
+
# labels (branches). Git and gh commands are hidden inside — callers
|
|
4
|
+
# never see git or GitHub terms.
|
|
5
5
|
require "digest"
|
|
6
6
|
require "fileutils"
|
|
7
|
+
require "json"
|
|
7
8
|
require "open3"
|
|
8
9
|
|
|
9
10
|
module Carson
|
|
@@ -97,7 +98,7 @@ module Carson
|
|
|
97
98
|
# Returns true on success, false on failure.
|
|
98
99
|
def pack!( message: )
|
|
99
100
|
if sealed?
|
|
100
|
-
raise "
|
|
101
|
+
raise "Branch is locked — PR ##{sealed_tracking_number} in flight. " \
|
|
101
102
|
"Create a new worktree to continue working."
|
|
102
103
|
end
|
|
103
104
|
git( "add", "-A" )
|
|
@@ -170,6 +171,62 @@ module Carson
|
|
|
170
171
|
end
|
|
171
172
|
end
|
|
172
173
|
|
|
174
|
+
# --- Bureau interaction ---
|
|
175
|
+
# The warehouse owns the connection to the bureau (GitHub).
|
|
176
|
+
# It queries, files, and registers on behalf of the courier.
|
|
177
|
+
|
|
178
|
+
# Check the parcel's status at the bureau using the waybill.
|
|
179
|
+
# Calls gh pr view + gh pr checks. Records findings onto the waybill.
|
|
180
|
+
def check_parcel_at_bureau_with( waybill )
|
|
181
|
+
state = fetch_pr_state_for( waybill.tracking_number )
|
|
182
|
+
ci, ci_diagnostic = fetch_ci_state_for( waybill.tracking_number )
|
|
183
|
+
waybill.record( state: state, ci: ci, ci_diagnostic: ci_diagnostic )
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# File a waybill at the bureau for this parcel.
|
|
187
|
+
# Calls gh pr create. Returns a Waybill with tracking number, or nil on failure.
|
|
188
|
+
def file_waybill_for!( parcel, title: nil, body_file: nil )
|
|
189
|
+
filing_title = title || Waybill.default_title_for( parcel.label )
|
|
190
|
+
arguments = [ "pr", "create", "--title", filing_title, "--head", parcel.label ]
|
|
191
|
+
|
|
192
|
+
if body_file && File.exist?( body_file )
|
|
193
|
+
arguments.push( "--body-file", body_file )
|
|
194
|
+
else
|
|
195
|
+
arguments.push( "--body", "" )
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
stdout, _, status = gh( *arguments )
|
|
199
|
+
tracking_number = nil
|
|
200
|
+
url = nil
|
|
201
|
+
|
|
202
|
+
if status.success?
|
|
203
|
+
url = stdout.to_s.strip
|
|
204
|
+
tracking_number = url.split( "/" ).last.to_i
|
|
205
|
+
tracking_number = nil if tracking_number == 0
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# If create failed or returned no number, try to find an existing PR.
|
|
209
|
+
unless tracking_number
|
|
210
|
+
tracking_number, url = find_existing_waybill_for( parcel.label )
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
return nil unless tracking_number
|
|
214
|
+
|
|
215
|
+
Waybill.new( label: parcel.label, tracking_number: tracking_number, url: url )
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Register the parcel at the bureau using the waybill.
|
|
219
|
+
# Calls gh pr merge. Stamps the waybill on success.
|
|
220
|
+
def register_parcel_at_bureau_with!( waybill, method: )
|
|
221
|
+
_, _, status = gh( "pr", "merge", waybill.tracking_number.to_s, "--#{method}" )
|
|
222
|
+
if status.success?
|
|
223
|
+
waybill.stamp( :accepted )
|
|
224
|
+
else
|
|
225
|
+
# Re-check the state — the merge may have revealed a new blocker.
|
|
226
|
+
check_parcel_at_bureau_with( waybill )
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
173
230
|
# --- Inventory ---
|
|
174
231
|
|
|
175
232
|
# All shelves (worktree paths).
|
|
@@ -217,5 +274,63 @@ module Carson
|
|
|
217
274
|
def git( *arguments )
|
|
218
275
|
Open3.capture3( "git", "-C", path, *arguments )
|
|
219
276
|
end
|
|
277
|
+
|
|
278
|
+
# All gh commands go through this single gateway.
|
|
279
|
+
# Returns [stdout, stderr, status].
|
|
280
|
+
def gh( *arguments )
|
|
281
|
+
Open3.capture3( "gh", *arguments, chdir: path )
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Fetch PR state from the bureau for a tracking number.
|
|
285
|
+
# Returns the parsed state hash, or nil on failure.
|
|
286
|
+
def fetch_pr_state_for( tracking_number )
|
|
287
|
+
stdout, _, status = gh(
|
|
288
|
+
"pr", "view", tracking_number.to_s,
|
|
289
|
+
"--json", "number,state,isDraft,url,mergeStateStatus,mergeable,mergedAt"
|
|
290
|
+
)
|
|
291
|
+
return nil unless status.success?
|
|
292
|
+
|
|
293
|
+
JSON.parse( stdout )
|
|
294
|
+
rescue JSON::ParserError
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Fetch CI state from the bureau for a tracking number.
|
|
299
|
+
# Returns [ci_symbol, diagnostic_or_nil].
|
|
300
|
+
# Captures the first line of stderr as diagnostic when the command fails.
|
|
301
|
+
def fetch_ci_state_for( tracking_number )
|
|
302
|
+
stdout, stderr, status = gh(
|
|
303
|
+
"pr", "checks", tracking_number.to_s,
|
|
304
|
+
"--json", "name,bucket"
|
|
305
|
+
)
|
|
306
|
+
unless status.success?
|
|
307
|
+
return [ :error, stderr.to_s.strip.lines.first&.strip ]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
checks = JSON.parse( stdout ) rescue []
|
|
311
|
+
return [ :none, nil ] if checks.empty?
|
|
312
|
+
|
|
313
|
+
buckets = checks.map { it[ "bucket" ].to_s.downcase }
|
|
314
|
+
return [ :fail, nil ] if buckets.include?( "fail" )
|
|
315
|
+
return [ :pending, nil ] if buckets.include?( "pending" )
|
|
316
|
+
|
|
317
|
+
[ :pass, nil ]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Try to find an existing PR for this label at the bureau.
|
|
321
|
+
# Returns [tracking_number, url] or [nil, nil].
|
|
322
|
+
def find_existing_waybill_for( label )
|
|
323
|
+
stdout, _, status = gh(
|
|
324
|
+
"pr", "view", label,
|
|
325
|
+
"--json", "number,url,state"
|
|
326
|
+
)
|
|
327
|
+
if status.success?
|
|
328
|
+
data = JSON.parse( stdout ) rescue nil
|
|
329
|
+
if data && data[ "number" ] && data[ "state" ] == "OPEN"
|
|
330
|
+
return [ data[ "number" ], data[ "url" ].to_s ]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
[ nil, nil ]
|
|
334
|
+
end
|
|
220
335
|
end
|
|
221
336
|
end
|
data/lib/carson/waybill.rb
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
# The shipping document filed with the bureau (GitHub PR).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# to accept the parcel into the registry.
|
|
3
|
+
# A waybill is a data object — it records findings and answers questions.
|
|
4
|
+
# It does not fetch, file, or accept anything. The warehouse handles all
|
|
5
|
+
# bureau interaction and writes findings onto the waybill.
|
|
7
6
|
#
|
|
8
|
-
# The
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# The waybill uses gh CLI internally — that's a tool, not the domain.
|
|
13
|
-
require "json"
|
|
14
|
-
require "open3"
|
|
7
|
+
# The waybill has a tracking number (PR number), a label (branch name),
|
|
8
|
+
# and records the bureau's response (cleared/held/accepted/rejected).
|
|
9
|
+
# ci_diagnostic preserves the first line of stderr when CI checks fail.
|
|
15
10
|
|
|
16
11
|
module Carson
|
|
17
12
|
# The shipping document filed with the bureau (GitHub PR). Has a
|
|
18
|
-
# tracking number,
|
|
19
|
-
# accepted/rejected)
|
|
20
|
-
#
|
|
21
|
-
# the domain.
|
|
13
|
+
# tracking number, records the bureaucrats' response (cleared/held/
|
|
14
|
+
# accepted/rejected). A data object — state is written onto it by
|
|
15
|
+
# the warehouse, never fetched by the waybill itself.
|
|
22
16
|
class Waybill
|
|
23
|
-
attr_reader :tracking_number, :url, :label
|
|
17
|
+
attr_reader :tracking_number, :url, :label, :ci_diagnostic
|
|
24
18
|
|
|
25
|
-
def initialize( label:,
|
|
19
|
+
def initialize( label:, tracking_number: nil, url: nil )
|
|
26
20
|
@label = label
|
|
27
|
-
@warehouse_path = warehouse_path
|
|
28
21
|
@tracking_number = tracking_number
|
|
29
22
|
@url = url
|
|
30
|
-
@review_gate = review_gate
|
|
31
23
|
@state = nil
|
|
32
24
|
@ci = nil
|
|
25
|
+
@ci_diagnostic = nil
|
|
26
|
+
@verdict = nil
|
|
33
27
|
end
|
|
34
28
|
|
|
35
29
|
# --- Filing ---
|
|
@@ -39,53 +33,46 @@ module Carson
|
|
|
39
33
|
!tracking_number.nil?
|
|
40
34
|
end
|
|
41
35
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if body_file && File.exist?( body_file )
|
|
48
|
-
arguments.push( "--body-file", body_file )
|
|
49
|
-
else
|
|
50
|
-
arguments.push( "--body", "" )
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
stdout, stderr, success, = gh( *arguments )
|
|
54
|
-
if success
|
|
55
|
-
@url = stdout.to_s.strip
|
|
56
|
-
@tracking_number = @url.split( "/" ).last.to_i
|
|
57
|
-
@tracking_number = nil if @tracking_number == 0
|
|
36
|
+
# Generate a title from the label. Class method so the warehouse
|
|
37
|
+
# can compute the title before creating the waybill.
|
|
38
|
+
def self.default_title_for( label )
|
|
39
|
+
label.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) do |character|
|
|
40
|
+
character.upcase
|
|
58
41
|
end
|
|
59
|
-
|
|
60
|
-
# If create failed or returned no number, try to find existing.
|
|
61
|
-
find_existing! unless filed?
|
|
62
|
-
self
|
|
63
42
|
end
|
|
64
43
|
|
|
65
|
-
#
|
|
44
|
+
# Instance convenience — delegates to the class method.
|
|
66
45
|
def default_title
|
|
67
|
-
|
|
68
|
-
character.upcase
|
|
69
|
-
end
|
|
46
|
+
self.class.default_title_for( label )
|
|
70
47
|
end
|
|
71
48
|
|
|
72
|
-
# ---
|
|
49
|
+
# --- Recorded state ---
|
|
50
|
+
|
|
51
|
+
# Record findings from a bureau check onto the waybill.
|
|
52
|
+
# Called by the warehouse after querying the bureau.
|
|
53
|
+
def record( state:, ci:, ci_diagnostic: nil )
|
|
54
|
+
@state = state
|
|
55
|
+
@ci = ci
|
|
56
|
+
@ci_diagnostic = ci_diagnostic
|
|
57
|
+
end
|
|
73
58
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@
|
|
78
|
-
self
|
|
59
|
+
# Stamp the waybill with a verdict.
|
|
60
|
+
# Called by the warehouse after registering the parcel at the bureau.
|
|
61
|
+
def stamp( verdict )
|
|
62
|
+
@verdict = verdict
|
|
79
63
|
end
|
|
80
64
|
|
|
65
|
+
# --- Bureau's response queries ---
|
|
66
|
+
|
|
81
67
|
# Has the bureau accepted the parcel into the registry?
|
|
68
|
+
# True when stamped :accepted OR when the recorded state shows MERGED.
|
|
82
69
|
def accepted?
|
|
83
|
-
@state&.dig( "state" ) == "MERGED"
|
|
70
|
+
@verdict == :accepted || @state&.dig( "state" ) == "MERGED"
|
|
84
71
|
end
|
|
85
72
|
|
|
86
73
|
# Has the bureau rejected the waybill (closed without merge)?
|
|
87
74
|
def rejected?
|
|
88
|
-
@state&.dig( "state" ) == "CLOSED"
|
|
75
|
+
@verdict == :rejected || @state&.dig( "state" ) == "CLOSED"
|
|
89
76
|
end
|
|
90
77
|
|
|
91
78
|
# Is the waybill still a draft?
|
|
@@ -111,29 +98,31 @@ module Carson
|
|
|
111
98
|
filed?
|
|
112
99
|
end
|
|
113
100
|
|
|
114
|
-
# Why is the waybill being held?
|
|
101
|
+
# Why is the waybill being held? Code string for recovery step lookup.
|
|
115
102
|
def hold_reason
|
|
116
103
|
return "draft" if draft?
|
|
117
|
-
return "
|
|
118
|
-
return "
|
|
119
|
-
return "
|
|
104
|
+
return "pending_at_bureau" if @ci == :pending
|
|
105
|
+
return "failed_at_bureau" if @ci == :fail
|
|
106
|
+
return "error_at_bureau" if @ci == :error
|
|
120
107
|
return "merge_conflict" if merge_conflicting?
|
|
121
|
-
return "
|
|
108
|
+
return "behind_bureau" if merge_behind?
|
|
122
109
|
return "policy_block" if merge_policy_blocked?
|
|
123
110
|
"mergeability_pending"
|
|
124
111
|
end
|
|
125
112
|
|
|
126
|
-
#
|
|
127
|
-
|
|
113
|
+
# Client-language summary of why the waybill is held.
|
|
114
|
+
# Agents read this directly — no translation layer needed.
|
|
115
|
+
def hold_summary( remote_main: "github/main" )
|
|
128
116
|
case hold_reason
|
|
129
|
-
when "draft" then "
|
|
130
|
-
when "
|
|
131
|
-
when "
|
|
132
|
-
when "
|
|
133
|
-
when "merge_conflict" then "
|
|
134
|
-
when "
|
|
135
|
-
when "policy_block" then "
|
|
136
|
-
|
|
117
|
+
when "draft" then "PR is still a draft."
|
|
118
|
+
when "pending_at_bureau" then "Waiting for CI checks."
|
|
119
|
+
when "failed_at_bureau" then "CI checks failed."
|
|
120
|
+
when "error_at_bureau" then "Unable to assess CI checks."
|
|
121
|
+
when "merge_conflict" then "Merge conflict with #{remote_main}."
|
|
122
|
+
when "behind_bureau" then "Branch is behind #{remote_main}."
|
|
123
|
+
when "policy_block" then "Blocked by branch protection rules."
|
|
124
|
+
when "mergeability_pending" then "GitHub is calculating mergeability."
|
|
125
|
+
else "Waiting for merge readiness."
|
|
137
126
|
end
|
|
138
127
|
end
|
|
139
128
|
|
|
@@ -142,16 +131,6 @@ module Carson
|
|
|
142
131
|
hold_reason == "mergeability_pending"
|
|
143
132
|
end
|
|
144
133
|
|
|
145
|
-
# --- Acceptance ---
|
|
146
|
-
|
|
147
|
-
# Ask the bureau to accept the parcel into the registry.
|
|
148
|
-
# Updates own state after the attempt.
|
|
149
|
-
def accept!( method: )
|
|
150
|
-
gh( "pr", "merge", tracking_number.to_s, "--#{method}" )
|
|
151
|
-
refresh!
|
|
152
|
-
self
|
|
153
|
-
end
|
|
154
|
-
|
|
155
134
|
# --- Observation data for delivery records ---
|
|
156
135
|
|
|
157
136
|
# Returns a hash of the bureau's current state for tracking records.
|
|
@@ -165,14 +144,6 @@ module Carson
|
|
|
165
144
|
}
|
|
166
145
|
end
|
|
167
146
|
|
|
168
|
-
# --- Test support ---
|
|
169
|
-
|
|
170
|
-
# Stub the bureau's response for testing without gh CLI.
|
|
171
|
-
def stub_bureau_response( state: nil, ci: nil )
|
|
172
|
-
@state = state if state
|
|
173
|
-
@ci = ci if ci
|
|
174
|
-
end
|
|
175
|
-
|
|
176
147
|
private
|
|
177
148
|
|
|
178
149
|
def merge_conflicting?
|
|
@@ -188,56 +159,5 @@ module Carson
|
|
|
188
159
|
def merge_policy_blocked?
|
|
189
160
|
@state&.dig( "mergeStateStatus" ).to_s.upcase == "BLOCKED"
|
|
190
161
|
end
|
|
191
|
-
|
|
192
|
-
def fetch_state
|
|
193
|
-
stdout, _, success, = gh(
|
|
194
|
-
"pr", "view", tracking_number.to_s,
|
|
195
|
-
"--json", "number,state,isDraft,url,mergeStateStatus,mergeable,mergedAt"
|
|
196
|
-
)
|
|
197
|
-
return nil unless success
|
|
198
|
-
|
|
199
|
-
JSON.parse( stdout )
|
|
200
|
-
rescue JSON::ParserError
|
|
201
|
-
nil
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def fetch_ci
|
|
205
|
-
stdout, _, success, = gh(
|
|
206
|
-
"pr", "checks", tracking_number.to_s,
|
|
207
|
-
"--json", "name,bucket"
|
|
208
|
-
)
|
|
209
|
-
return :error unless success
|
|
210
|
-
|
|
211
|
-
checks = JSON.parse( stdout ) rescue []
|
|
212
|
-
return :none if checks.empty?
|
|
213
|
-
|
|
214
|
-
buckets = checks.map do |entry|
|
|
215
|
-
entry[ "bucket" ].to_s.downcase
|
|
216
|
-
end
|
|
217
|
-
return :fail if buckets.include?( "fail" )
|
|
218
|
-
return :pending if buckets.include?( "pending" )
|
|
219
|
-
|
|
220
|
-
:pass
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def find_existing!
|
|
224
|
-
stdout, _, success, = gh(
|
|
225
|
-
"pr", "view", label,
|
|
226
|
-
"--json", "number,url,state"
|
|
227
|
-
)
|
|
228
|
-
if success
|
|
229
|
-
data = JSON.parse( stdout ) rescue nil
|
|
230
|
-
if data && data[ "number" ] && data[ "state" ] == "OPEN"
|
|
231
|
-
@tracking_number = data[ "number" ]
|
|
232
|
-
@url = data[ "url" ].to_s
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# All gh commands go through this single gateway.
|
|
238
|
-
def gh( *arguments )
|
|
239
|
-
stdout, stderr, status = Open3.capture3( "gh", *arguments, chdir: @warehouse_path )
|
|
240
|
-
[ stdout, stderr, status.success?, status.exitstatus ]
|
|
241
|
-
end
|
|
242
162
|
end
|
|
243
163
|
end
|
data/lib/carson.rb
CHANGED
|
@@ -5,21 +5,21 @@ module Carson
|
|
|
5
5
|
BADGE = "\u29D3".freeze # ⧓ BLACK BOWTIE (U+29D3)
|
|
6
6
|
|
|
7
7
|
# The company renders results for whoever is listening.
|
|
8
|
-
# JSON is the primary format (agents consume it).
|
|
8
|
+
# JSON is the primary format (agents consume it). Text is secondary.
|
|
9
9
|
# Domain objects return result hashes — Carson decides how to present them.
|
|
10
10
|
def self.report( result, format: :json, output: $stdout )
|
|
11
11
|
case format
|
|
12
12
|
when :json
|
|
13
13
|
require "json"
|
|
14
14
|
output.puts JSON.pretty_generate( result )
|
|
15
|
-
when :
|
|
16
|
-
|
|
15
|
+
when :text
|
|
16
|
+
report_text( result, output: output )
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# Text delivery report — client language for agents.
|
|
21
21
|
# Story language is internal (source code). Output speaks the client's language.
|
|
22
|
-
def self.
|
|
22
|
+
def self.report_text( result, output: $stdout )
|
|
23
23
|
if result[ :error ]
|
|
24
24
|
output.puts "#{BADGE} #{result[ :error ]}"
|
|
25
25
|
output.puts " \u2192 #{result[ :recovery ]}" if result[ :recovery ]
|
|
@@ -40,45 +40,39 @@ module Carson
|
|
|
40
40
|
output.puts "#{BADGE} Local main not synced \u2014 run carson sync."
|
|
41
41
|
end
|
|
42
42
|
when "held"
|
|
43
|
-
|
|
44
|
-
output.puts "#{BADGE} #{
|
|
45
|
-
|
|
43
|
+
summary = result[ :hold_summary ] || "Waiting for merge readiness."
|
|
44
|
+
output.puts "#{BADGE} #{summary}"
|
|
45
|
+
recovery_steps_for_hold( result[ :hold_reason ], remote_main: remote_main ).each do |step|
|
|
46
46
|
output.puts " \u2192 #{step}"
|
|
47
47
|
end
|
|
48
48
|
when "rejected"
|
|
49
49
|
output.puts "#{BADGE} PR closed externally."
|
|
50
50
|
when "filed"
|
|
51
|
-
|
|
51
|
+
summary = result[ :hold_summary ] || "Waiting for merge readiness."
|
|
52
|
+
diagnostic = result[ :diagnostic ] ? " (#{result[ :diagnostic ]})" : ""
|
|
53
|
+
output.puts "#{BADGE} #{summary}#{diagnostic}"
|
|
54
|
+
output.puts " \u2192 carson status"
|
|
52
55
|
end
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
def self.translate_hold( reason, remote_main: "origin/main" )
|
|
58
|
+
# Recovery commands for a held delivery.
|
|
59
|
+
# The report knows what commands to suggest for each situation.
|
|
60
|
+
def self.recovery_steps_for_hold( reason, remote_main: "origin/main" )
|
|
59
61
|
case reason
|
|
60
|
-
when "
|
|
61
|
-
[ "
|
|
62
|
-
when "
|
|
63
|
-
[ "
|
|
64
|
-
when "failed_at_registry"
|
|
65
|
-
[ "CI checks failed.", "carson deliver" ]
|
|
66
|
-
when "error_at_registry"
|
|
67
|
-
[ "Unable to assess CI checks.", "carson status" ]
|
|
62
|
+
when "pending_at_bureau", "mergeability_pending", "error_at_bureau"
|
|
63
|
+
[ "carson status" ]
|
|
64
|
+
when "failed_at_bureau"
|
|
65
|
+
[ "carson deliver" ]
|
|
68
66
|
when "merge_conflict"
|
|
69
|
-
[ "
|
|
70
|
-
when "
|
|
71
|
-
[ "
|
|
72
|
-
when "policy_block"
|
|
73
|
-
[ "Blocked by branch protection rules." ]
|
|
74
|
-
when "mergeability_pending"
|
|
75
|
-
[ "GitHub is calculating mergeability.", "carson status" ]
|
|
67
|
+
[ "git rebase #{remote_main}", "carson deliver" ]
|
|
68
|
+
when "behind_bureau"
|
|
69
|
+
[ "carson deliver" ]
|
|
76
70
|
else
|
|
77
|
-
[
|
|
71
|
+
[]
|
|
78
72
|
end
|
|
79
73
|
end
|
|
80
74
|
|
|
81
|
-
private_class_method :
|
|
75
|
+
private_class_method :report_text, :recovery_steps_for_hold
|
|
82
76
|
end
|
|
83
77
|
|
|
84
78
|
require_relative "carson/repository"
|
|
@@ -87,8 +81,8 @@ require_relative "carson/delivery"
|
|
|
87
81
|
require_relative "carson/revision"
|
|
88
82
|
require_relative "carson/ledger"
|
|
89
83
|
require_relative "carson/parcel"
|
|
90
|
-
require_relative "carson/warehouse"
|
|
91
84
|
require_relative "carson/waybill"
|
|
85
|
+
require_relative "carson/warehouse"
|
|
92
86
|
require_relative "carson/courier"
|
|
93
87
|
require_relative "carson/worktree"
|
|
94
88
|
require_relative "carson/config"
|