carson 3.29.0 → 3.30.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/README.md +1 -1
- data/RELEASE.md +21 -0
- data/VERSION +1 -1
- data/carson.gemspec +0 -1
- data/lib/carson/runtime/deliver.rb +137 -114
- data/lib/carson/runtime/govern.rb +9 -8
- metadata +1 -2
- data/SKILL.md +0 -99
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5156cb5207a0ee95cad65c30d494a24ddcee97c90f2aab6861b34f9ec8e92069
|
|
4
|
+
data.tar.gz: 182a83aaa84ec1d9c0a548bdaf98e421b4eab9e4ab1248b0d67db58e42a7c5ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e42b39fad6ca26a05bd914f7653c9cd4e7bb4227c9eb03ba5288b5e54d30f87f4b8e69991236caab797964d293ec1e6b01c4ec6a8853b24f80f7a7bb1670cfe
|
|
7
|
+
data.tar.gz: fd147d7b1941670279ad3261b0cca81639d72043b3e429a1f0290aff4a9deb53000971c5bb5953d7f36b4b574a1c5b03547f9cf33fdb3bcc16bbe4f30e5b770a
|
data/README.md
CHANGED
|
@@ -64,7 +64,7 @@ cd your/repo/path
|
|
|
64
64
|
carson housekeep
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`carson deliver` runs Carson-owned branch delivery. Before any push, Carson verifies that the branch is fresh against the configured remote `main
|
|
67
|
+
`carson deliver` runs Carson-owned branch delivery. Before any push, Carson verifies that the branch is fresh against the configured remote `main` using local git. If freshness is behind or unknown, delivery stops with an explicit block and no PR side effect. After a PR exists, Carson delegates merge eligibility to GitHub's `mergeStateStatus` — if GitHub reports `CLEAN`, Carson proceeds regardless of local ancestor status. Plain `deliver` transports existing commits only; `carson deliver --commit "..."` creates one all-dirty delivery commit first, then continues the same flow. If the branch is fresh, Carson pushes it, creates or refreshes the PR, watches the delivery for a bounded settle window, merges when clear, and syncs local `main`. If the settle window expires without integration, Carson exits with an explicit `Merge deferred` or `Merge blocked` handoff instead of leaving the PR mysteriously open. Deferred and blocked exits say whether Carson attempted merge and list the next commands in order.
|
|
68
68
|
|
|
69
69
|
When one Carson-governed required check is already red on the default branch and the current PR is the repair, use `carson recover --check "..."`. Recovery is the explicit exceptional path: Carson proves the baseline failure, keeps every other gate intact, records an audit event, and never teaches operators to step outside Carson first.
|
|
70
70
|
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,27 @@ Release-note scope rule:
|
|
|
5
5
|
- `RELEASE.md` records only version deltas, breaking changes, and migration actions.
|
|
6
6
|
- Operational usage guides live in `MANUAL.md` and `API.md`.
|
|
7
7
|
|
|
8
|
+
## 3.30.0
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Post-PR merge eligibility is now delegated to GitHub** — After a PR exists, Carson uses GitHub's `mergeStateStatus` as the sole authority for merge eligibility instead of running a local `git merge-base --is-ancestor` check. If GitHub reports `CLEAN`, Carson proceeds regardless of local ancestor status. If GitHub reports `BEHIND`, the delivery is held. The pre-push freshness check (before any PR exists) is unchanged.
|
|
13
|
+
- **Govern and deliver no longer false-block in busy repos** — In repositories where govern merges PRs serially, each merge advances main and previously made every subsequent queued PR permanently stuck unless manually rebased. Carson now integrates any PR that GitHub considers merge-eligible.
|
|
14
|
+
|
|
15
|
+
### No migration required
|
|
16
|
+
|
|
17
|
+
- Existing workflows continue to work unchanged. PRs that were previously false-blocked by local freshness checks will now integrate when GitHub reports them as merge-eligible.
|
|
18
|
+
|
|
19
|
+
## 3.29.1
|
|
20
|
+
|
|
21
|
+
### What changed
|
|
22
|
+
|
|
23
|
+
- **Delivery output now shows the remote target explicitly** — `Delivery: branch → github/main` instead of the ambiguous `→ main`, making it clear the target is the remote branch, not local.
|
|
24
|
+
|
|
25
|
+
### No migration required
|
|
26
|
+
|
|
27
|
+
- Existing workflows continue to work unchanged.
|
|
28
|
+
|
|
8
29
|
## 3.29.0
|
|
9
30
|
|
|
10
31
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.30.0
|
data/carson.gemspec
CHANGED
|
@@ -16,6 +16,7 @@ module Carson
|
|
|
16
16
|
result = {
|
|
17
17
|
command: "deliver",
|
|
18
18
|
branch: branch_name,
|
|
19
|
+
git_remote: remote_name,
|
|
19
20
|
watch_window_seconds: config.govern_check_wait.to_i,
|
|
20
21
|
waited_seconds: 0,
|
|
21
22
|
merge_attempted: false
|
|
@@ -220,24 +221,8 @@ module Carson
|
|
|
220
221
|
end
|
|
221
222
|
|
|
222
223
|
# Assesses delivery readiness and records Carson's current branch state.
|
|
224
|
+
# Post-PR freshness is delegated to GitHub's mergeStateStatus via delivery_assessment.
|
|
223
225
|
def assess_delivery!( delivery:, branch_name: )
|
|
224
|
-
freshness = assess_branch_freshness(
|
|
225
|
-
head_ref: delivery.head || branch_name,
|
|
226
|
-
remote: config.git_remote,
|
|
227
|
-
main: config.main_branch
|
|
228
|
-
)
|
|
229
|
-
unless freshness.fetch( :ready )
|
|
230
|
-
return ledger.update_delivery(
|
|
231
|
-
delivery: delivery,
|
|
232
|
-
status: "gated",
|
|
233
|
-
cause: "freshness",
|
|
234
|
-
summary: freshness.fetch( :summary ),
|
|
235
|
-
pr_number: delivery.pull_request_number,
|
|
236
|
-
pr_url: delivery.pull_request_url,
|
|
237
|
-
worktree_path: delivery.worktree_path
|
|
238
|
-
)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
226
|
review = check_pr_review( number: delivery.pull_request_number, branch: branch_name, pr_url: delivery.pull_request_url )
|
|
242
227
|
ci = check_pr_ci( number: delivery.pull_request_number )
|
|
243
228
|
pr_state = pull_request_state( number: delivery.pull_request_number )
|
|
@@ -277,7 +262,14 @@ module Carson
|
|
|
277
262
|
last_evaluation = evaluation
|
|
278
263
|
successful_assessments += 1 if evaluation[ :assessment_success ]
|
|
279
264
|
result[ :ci ] = evaluation[ :ci ].to_s
|
|
280
|
-
|
|
265
|
+
if evaluation[ :cause ] == "freshness"
|
|
266
|
+
result[ :freshness ] = {
|
|
267
|
+
status: "behind",
|
|
268
|
+
reason: "freshness_behind",
|
|
269
|
+
summary: evaluation[ :summary ],
|
|
270
|
+
base_ref: "#{config.git_remote}/#{main}"
|
|
271
|
+
}
|
|
272
|
+
end
|
|
281
273
|
|
|
282
274
|
delivery = update_delivery_for_settle_evaluation( delivery: delivery, evaluation: evaluation )
|
|
283
275
|
result[ :summary ] = delivery.summary
|
|
@@ -296,7 +288,7 @@ module Carson
|
|
|
296
288
|
when :blocked
|
|
297
289
|
result[ :outcome ] = "blocked"
|
|
298
290
|
result[ :waited_seconds ] = elapsed_settle_seconds( started_at: started_at )
|
|
299
|
-
result[ :recovery ] =
|
|
291
|
+
result[ :recovery ] = "git rebase #{config.git_remote}/#{main} && carson deliver" if evaluation[ :cause ] == "freshness"
|
|
300
292
|
apply_handoff!(
|
|
301
293
|
result: result,
|
|
302
294
|
reason: evaluation.fetch( :reason ),
|
|
@@ -388,25 +380,8 @@ module Carson
|
|
|
388
380
|
delivery
|
|
389
381
|
end
|
|
390
382
|
|
|
383
|
+
# Post-PR freshness is delegated to GitHub's mergeStateStatus via settle_mergeability_assessment.
|
|
391
384
|
def evaluate_delivery_for_settle( branch_name:, head_ref:, pr_number:, pr_url:, main: )
|
|
392
|
-
freshness = assess_branch_freshness(
|
|
393
|
-
branch_name: branch_name,
|
|
394
|
-
head_ref: head_ref,
|
|
395
|
-
remote: config.git_remote,
|
|
396
|
-
main: main
|
|
397
|
-
)
|
|
398
|
-
unless freshness.fetch( :ready )
|
|
399
|
-
return {
|
|
400
|
-
phase: :blocked,
|
|
401
|
-
reason: freshness.fetch( :reason ),
|
|
402
|
-
cause: "freshness",
|
|
403
|
-
summary: freshness.fetch( :summary ),
|
|
404
|
-
assessment_success: freshness.fetch( :status ) != :unknown,
|
|
405
|
-
ci: :none,
|
|
406
|
-
freshness: freshness
|
|
407
|
-
}
|
|
408
|
-
end
|
|
409
|
-
|
|
410
385
|
review = check_pr_review( number: pr_number, branch: branch_name, pr_url: pr_url )
|
|
411
386
|
ci = settle_check_pr_ci( number: pr_number )
|
|
412
387
|
pr_state = pull_request_state( number: pr_number )
|
|
@@ -418,7 +393,7 @@ module Carson
|
|
|
418
393
|
summary: "waiting for GitHub assessment",
|
|
419
394
|
assessment_success: false,
|
|
420
395
|
ci: ci
|
|
421
|
-
}.merge(
|
|
396
|
+
}.merge( pr_state: pr_state ) if ci == :error || review.fetch( :status, :pass ) == :error || !pr_state.is_a?( Hash )
|
|
422
397
|
|
|
423
398
|
return {
|
|
424
399
|
phase: :integrated,
|
|
@@ -427,7 +402,7 @@ module Carson
|
|
|
427
402
|
summary: "integrated into #{main}",
|
|
428
403
|
assessment_success: true,
|
|
429
404
|
ci: ci
|
|
430
|
-
}.merge(
|
|
405
|
+
}.merge( pr_state: pr_state ) if pr_state[ "state" ] == "MERGED"
|
|
431
406
|
return {
|
|
432
407
|
phase: :blocked,
|
|
433
408
|
reason: "pull_request_closed",
|
|
@@ -435,7 +410,7 @@ module Carson
|
|
|
435
410
|
summary: "pull request closed without integration",
|
|
436
411
|
assessment_success: true,
|
|
437
412
|
ci: ci
|
|
438
|
-
}.merge(
|
|
413
|
+
}.merge( pr_state: pr_state ) if pr_state[ "state" ] == "CLOSED"
|
|
439
414
|
|
|
440
415
|
return {
|
|
441
416
|
phase: :blocked,
|
|
@@ -444,7 +419,7 @@ module Carson
|
|
|
444
419
|
summary: "pull request is still a draft",
|
|
445
420
|
assessment_success: true,
|
|
446
421
|
ci: ci
|
|
447
|
-
}.merge(
|
|
422
|
+
}.merge( pr_state: pr_state ) if pr_state[ "isDraft" ]
|
|
448
423
|
|
|
449
424
|
return {
|
|
450
425
|
phase: :waiting,
|
|
@@ -453,7 +428,7 @@ module Carson
|
|
|
453
428
|
summary: "waiting for CI checks",
|
|
454
429
|
assessment_success: true,
|
|
455
430
|
ci: ci
|
|
456
|
-
}.merge(
|
|
431
|
+
}.merge( pr_state: pr_state ) if ci == :pending
|
|
457
432
|
|
|
458
433
|
return {
|
|
459
434
|
phase: :blocked,
|
|
@@ -462,7 +437,7 @@ module Carson
|
|
|
462
437
|
summary: "CI checks are failing",
|
|
463
438
|
assessment_success: true,
|
|
464
439
|
ci: ci
|
|
465
|
-
}.merge(
|
|
440
|
+
}.merge( pr_state: pr_state ) if ci == :fail
|
|
466
441
|
|
|
467
442
|
return {
|
|
468
443
|
phase: :blocked,
|
|
@@ -471,7 +446,7 @@ module Carson
|
|
|
471
446
|
summary: "review changes requested",
|
|
472
447
|
assessment_success: true,
|
|
473
448
|
ci: ci
|
|
474
|
-
}.merge(
|
|
449
|
+
}.merge( pr_state: pr_state ) if review.fetch( :review, :none ) == :changes_requested
|
|
475
450
|
|
|
476
451
|
return {
|
|
477
452
|
phase: :waiting,
|
|
@@ -480,7 +455,7 @@ module Carson
|
|
|
480
455
|
summary: "waiting for review",
|
|
481
456
|
assessment_success: true,
|
|
482
457
|
ci: ci
|
|
483
|
-
}.merge(
|
|
458
|
+
}.merge( pr_state: pr_state ) if review.fetch( :review, :none ) == :review_required
|
|
484
459
|
|
|
485
460
|
return {
|
|
486
461
|
phase: :blocked,
|
|
@@ -489,49 +464,28 @@ module Carson
|
|
|
489
464
|
summary: review.fetch( :detail ).to_s,
|
|
490
465
|
assessment_success: true,
|
|
491
466
|
ci: ci
|
|
492
|
-
}.merge(
|
|
467
|
+
}.merge( pr_state: pr_state ) if review.fetch( :status, :pass ) == :fail
|
|
493
468
|
|
|
494
469
|
mergeability = settle_mergeability_assessment( pr_state: pr_state, main: main )
|
|
495
|
-
mergeability.merge( assessment_success: true, ci: ci,
|
|
470
|
+
mergeability.merge( assessment_success: true, ci: ci, pr_state: pr_state )
|
|
496
471
|
end
|
|
497
472
|
|
|
498
473
|
def settle_mergeability_assessment( pr_state:, main: )
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
phase: :blocked,
|
|
511
|
-
reason: "repository_policy_block",
|
|
512
|
-
cause: "merge",
|
|
513
|
-
summary: "merge is blocked by repository policy"
|
|
514
|
-
} if merge_state == "BLOCKED"
|
|
515
|
-
|
|
516
|
-
return {
|
|
517
|
-
phase: :blocked,
|
|
518
|
-
reason: "freshness_behind",
|
|
519
|
-
cause: "freshness",
|
|
520
|
-
summary: "branch is behind #{config.git_remote}/#{main}"
|
|
521
|
-
} if merge_state == "BEHIND"
|
|
522
|
-
|
|
523
|
-
return {
|
|
524
|
-
phase: :ready,
|
|
525
|
-
reason: "ready",
|
|
526
|
-
cause: nil,
|
|
527
|
-
summary: "ready to integrate into #{main}"
|
|
528
|
-
} if merge_state == "CLEAN" || mergeable == "MERGEABLE"
|
|
529
|
-
|
|
474
|
+
assessment = github_merge_assessment( pr_state: pr_state )
|
|
475
|
+
phase = if assessment[ :ready ]
|
|
476
|
+
:ready
|
|
477
|
+
elsif [ "mergeability_pending", "assessment_unavailable" ].include?( assessment[ :reason ] )
|
|
478
|
+
:waiting
|
|
479
|
+
else
|
|
480
|
+
:blocked
|
|
481
|
+
end
|
|
482
|
+
# Preserve the settle-loop's "assessment" cause for pending/unavailable states
|
|
483
|
+
cause = phase == :waiting ? "assessment" : assessment[ :cause ]
|
|
530
484
|
{
|
|
531
|
-
phase:
|
|
532
|
-
reason:
|
|
533
|
-
cause:
|
|
534
|
-
summary:
|
|
485
|
+
phase: phase,
|
|
486
|
+
reason: assessment[ :reason ],
|
|
487
|
+
cause: cause,
|
|
488
|
+
summary: assessment[ :summary ]
|
|
535
489
|
}
|
|
536
490
|
end
|
|
537
491
|
|
|
@@ -581,29 +535,37 @@ module Carson
|
|
|
581
535
|
end
|
|
582
536
|
end
|
|
583
537
|
|
|
538
|
+
# Pre-merge recheck using GitHub as the authority for merge eligibility.
|
|
539
|
+
# Blocks on definite GitHub-reported issues (BEHIND, CONFLICTING, BLOCKED, draft).
|
|
540
|
+
# Allows through pending/unknown states — the merge attempt itself will succeed or fail.
|
|
584
541
|
def attempt_delivery_merge!( delivery:, remote:, main:, result: )
|
|
585
|
-
freshness = assess_branch_freshness(
|
|
586
|
-
head_ref: delivery.head || delivery.branch,
|
|
587
|
-
remote: remote,
|
|
588
|
-
main: main
|
|
589
|
-
)
|
|
590
|
-
result[ :freshness ] = freshness_payload( freshness: freshness )
|
|
591
|
-
unless freshness.fetch( :ready )
|
|
592
|
-
result[ :recovery ] = freshness_recovery( freshness: freshness )
|
|
593
|
-
return {
|
|
594
|
-
phase: :blocked,
|
|
595
|
-
attempted: false,
|
|
596
|
-
reason: freshness.fetch( :reason ),
|
|
597
|
-
delivery: ledger.update_delivery(
|
|
598
|
-
delivery: delivery,
|
|
599
|
-
status: "gated",
|
|
600
|
-
cause: "freshness",
|
|
601
|
-
summary: freshness.fetch( :summary )
|
|
602
|
-
)
|
|
603
|
-
}
|
|
604
|
-
end
|
|
605
|
-
|
|
606
542
|
pr_state = pull_request_state( number: delivery.pull_request_number )
|
|
543
|
+
merge_check = github_merge_assessment( pr_state: pr_state )
|
|
544
|
+
definite_blocker = !merge_check[ :ready ] &&
|
|
545
|
+
![ "mergeability_pending", "assessment_unavailable" ].include?( merge_check[ :reason ] )
|
|
546
|
+
if definite_blocker
|
|
547
|
+
if merge_check[ :cause ] == "freshness"
|
|
548
|
+
result[ :freshness ] = {
|
|
549
|
+
status: "behind",
|
|
550
|
+
reason: "freshness_behind",
|
|
551
|
+
summary: merge_check[ :summary ],
|
|
552
|
+
base_ref: "#{remote}/#{main}"
|
|
553
|
+
}
|
|
554
|
+
end
|
|
555
|
+
result[ :recovery ] = merge_check[ :recovery ] if merge_check[ :recovery ]
|
|
556
|
+
return {
|
|
557
|
+
phase: :blocked,
|
|
558
|
+
attempted: false,
|
|
559
|
+
reason: merge_check[ :reason ],
|
|
560
|
+
delivery: ledger.update_delivery(
|
|
561
|
+
delivery: delivery,
|
|
562
|
+
status: "gated",
|
|
563
|
+
cause: merge_check[ :cause ],
|
|
564
|
+
summary: merge_check[ :summary ]
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
end
|
|
568
|
+
|
|
607
569
|
observation = pull_request_observation_attributes( pr_state: pr_state )
|
|
608
570
|
if pr_state && pr_state[ "state" ] == "MERGED"
|
|
609
571
|
return {
|
|
@@ -889,19 +851,78 @@ module Carson
|
|
|
889
851
|
[ "gated", "merge", "waiting for GitHub mergeability" ]
|
|
890
852
|
end
|
|
891
853
|
|
|
892
|
-
|
|
893
|
-
|
|
854
|
+
# Interprets GitHub's PR state into a merge readiness verdict.
|
|
855
|
+
# Pure method — no API calls. Accepts the already-fetched pr_state hash.
|
|
856
|
+
# Returns a standardised hash: { ready:, reason:, cause:, summary:, recovery: }
|
|
857
|
+
def github_merge_assessment( pr_state: )
|
|
858
|
+
unless pr_state.is_a?( Hash )
|
|
859
|
+
return {
|
|
860
|
+
ready: false,
|
|
861
|
+
reason: "assessment_unavailable",
|
|
862
|
+
cause: "merge",
|
|
863
|
+
summary: "waiting for GitHub mergeability",
|
|
864
|
+
recovery: nil
|
|
865
|
+
}
|
|
866
|
+
end
|
|
894
867
|
|
|
895
868
|
mergeable = pr_state.fetch( "mergeable", "" ).to_s.upcase
|
|
896
869
|
merge_state = pr_state.fetch( "mergeStateStatus", "" ).to_s.upcase
|
|
870
|
+
remote_main = "#{config.git_remote}/#{config.main_branch}"
|
|
897
871
|
|
|
898
|
-
return
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
872
|
+
return {
|
|
873
|
+
ready: false,
|
|
874
|
+
reason: "draft_pr",
|
|
875
|
+
cause: "policy",
|
|
876
|
+
summary: "pull request is still a draft",
|
|
877
|
+
recovery: nil
|
|
878
|
+
} if pr_state[ "isDraft" ]
|
|
903
879
|
|
|
904
|
-
|
|
880
|
+
return {
|
|
881
|
+
ready: false,
|
|
882
|
+
reason: "merge_conflict",
|
|
883
|
+
cause: "merge",
|
|
884
|
+
summary: "pull request has merge conflicts",
|
|
885
|
+
recovery: nil
|
|
886
|
+
} if mergeable == "CONFLICTING" || merge_state == "DIRTY" || merge_state == "CONFLICTING"
|
|
887
|
+
|
|
888
|
+
return {
|
|
889
|
+
ready: false,
|
|
890
|
+
reason: "repository_policy_block",
|
|
891
|
+
cause: "merge",
|
|
892
|
+
summary: "merge is blocked by repository policy",
|
|
893
|
+
recovery: nil
|
|
894
|
+
} if merge_state == "BLOCKED"
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
ready: false,
|
|
898
|
+
reason: "freshness_behind",
|
|
899
|
+
cause: "freshness",
|
|
900
|
+
summary: "branch is behind #{remote_main}",
|
|
901
|
+
recovery: "git rebase #{remote_main} && carson deliver"
|
|
902
|
+
} if merge_state == "BEHIND"
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
ready: true,
|
|
906
|
+
reason: "ready",
|
|
907
|
+
cause: nil,
|
|
908
|
+
summary: "ready to integrate into #{config.main_branch}",
|
|
909
|
+
recovery: nil
|
|
910
|
+
} if merge_state == "CLEAN" || mergeable == "MERGEABLE"
|
|
911
|
+
|
|
912
|
+
{
|
|
913
|
+
ready: false,
|
|
914
|
+
reason: "mergeability_pending",
|
|
915
|
+
cause: "merge",
|
|
916
|
+
summary: "waiting for GitHub mergeability",
|
|
917
|
+
recovery: nil
|
|
918
|
+
}
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def mergeability_assessment( pr_state: )
|
|
922
|
+
assessment = github_merge_assessment( pr_state: pr_state )
|
|
923
|
+
return nil if assessment[ :reason ] == "mergeability_pending" && pr_state.is_a?( Hash )
|
|
924
|
+
status = assessment[ :ready ] ? "queued" : "gated"
|
|
925
|
+
[ status, assessment[ :cause ], assessment[ :summary ] ]
|
|
905
926
|
end
|
|
906
927
|
|
|
907
928
|
def delivery_payload( delivery: )
|
|
@@ -948,8 +969,10 @@ module Carson
|
|
|
948
969
|
|
|
949
970
|
if result[ :delivery ]
|
|
950
971
|
branch = result[ :branch ]
|
|
972
|
+
remote = result[ :git_remote ] || "github"
|
|
951
973
|
main = result[ :main_branch ] || "main"
|
|
952
|
-
|
|
974
|
+
remote_main = "#{remote}/#{main}"
|
|
975
|
+
puts_line "Delivery: #{branch} → #{remote_main}"
|
|
953
976
|
end
|
|
954
977
|
if result[ :commit ]
|
|
955
978
|
puts_line "Committed: #{result.dig( :commit, :summary )}"
|
|
@@ -961,9 +984,9 @@ module Carson
|
|
|
961
984
|
summary = result[ :summary ]
|
|
962
985
|
if outcome == "integrated" || status == "integrated"
|
|
963
986
|
if result[ :merge_method ]
|
|
964
|
-
puts_line "Merged into #{
|
|
987
|
+
puts_line "Merged into #{remote_main} with #{result[ :merge_method ]}."
|
|
965
988
|
else
|
|
966
|
-
puts_line "Merged into #{
|
|
989
|
+
puts_line "Merged into #{remote_main}."
|
|
967
990
|
end
|
|
968
991
|
if result[ :synced ] == false
|
|
969
992
|
puts_line "Local #{main} sync failed — #{result[ :sync_error ]}."
|
|
@@ -216,19 +216,20 @@ module Carson
|
|
|
216
216
|
end
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
+
# Final pre-merge recheck using GitHub as authority for merge eligibility.
|
|
220
|
+
# Intentionally strict: blocks on ANY non-ready state (including pending/unavailable).
|
|
221
|
+
# Unlike deliver's attempt_delivery_merge! which allows speculative merges on pending states,
|
|
222
|
+
# govern runs unattended and should not speculatively attempt merges on uncertain state.
|
|
219
223
|
def integrate_delivery!( delivery:, repo_path: )
|
|
220
224
|
result = {}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
main: config.main_branch
|
|
225
|
-
)
|
|
226
|
-
unless freshness.fetch( :ready )
|
|
225
|
+
pr_state = pull_request_state( number: delivery.pull_request_number )
|
|
226
|
+
merge_check = github_merge_assessment( pr_state: pr_state )
|
|
227
|
+
unless merge_check[ :ready ]
|
|
227
228
|
return ledger.update_delivery(
|
|
228
229
|
delivery: delivery,
|
|
229
230
|
status: "gated",
|
|
230
|
-
cause:
|
|
231
|
-
summary:
|
|
231
|
+
cause: merge_check[ :cause ],
|
|
232
|
+
summary: merge_check[ :summary ]
|
|
232
233
|
)
|
|
233
234
|
end
|
|
234
235
|
|
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: 3.
|
|
4
|
+
version: 3.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -51,7 +51,6 @@ files:
|
|
|
51
51
|
- MANUAL.md
|
|
52
52
|
- README.md
|
|
53
53
|
- RELEASE.md
|
|
54
|
-
- SKILL.md
|
|
55
54
|
- VERSION
|
|
56
55
|
- carson.gemspec
|
|
57
56
|
- exe/carson
|
data/SKILL.md
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# Carson Skill
|
|
2
|
-
|
|
3
|
-
You are working in a repository governed by Carson — an autonomous git strategist and repositories governor. Carson handles git hooks, PR triage, agent dispatch, merge, and cleanup. You provide the intelligence; Carson provides the infrastructure.
|
|
4
|
-
|
|
5
|
-
## When to use Carson commands
|
|
6
|
-
|
|
7
|
-
| User intent | Command | What happens |
|
|
8
|
-
|---|---|---|
|
|
9
|
-
| "Check if my code is ready" | `carson audit` | Scope, boundary checks. Exit 0 = clean. Exit 2 = policy block. |
|
|
10
|
-
| "Is my PR mergeable?" | `carson review gate` | Polls for unresolved review threads and actionable comments. Blocks until resolved. |
|
|
11
|
-
| "What's happening across my repos?" | `carson govern --dry-run` | Classifies every open PR without taking action. Read the summary. |
|
|
12
|
-
| "Run governance continuously" | `carson govern --loop 300` | Triage-dispatch-merge cycle every 300 seconds. Ctrl-C to stop. |
|
|
13
|
-
| "Merge ready PRs and dispatch fixes" | `carson govern` | Full autonomous cycle: merge, dispatch agents, escalate. |
|
|
14
|
-
| "Set up Carson for a repo" | `carson onboard /path/to/repo` | Installs hooks, syncs templates, runs first audit. |
|
|
15
|
-
| "Refresh after upgrading Carson" | `carson refresh` | Re-applies hooks and templates for the current version. |
|
|
16
|
-
| "Update my local main" | `carson sync` | Fast-forward local main from remote. Blocks if tree is dirty. |
|
|
17
|
-
| "Clean up stale branches" | `carson prune` | Removes local branches whose upstream is gone. |
|
|
18
|
-
| "Check template drift" | `carson template check` then `carson template apply` | Detect and fix .github/* drift. |
|
|
19
|
-
| "Remove Carson from a repo" | `carson offboard /path/to/repo` | Removes hooks and managed files. |
|
|
20
|
-
| "What version?" | `carson version` | Prints installed version with ⧓ badge. |
|
|
21
|
-
|
|
22
|
-
## Exit codes
|
|
23
|
-
|
|
24
|
-
- `0` — success, all clear.
|
|
25
|
-
- `1` — runtime or configuration error. Read the error message.
|
|
26
|
-
- `2` — policy block. Something must be fixed before proceeding (unresolved review, boundary breach).
|
|
27
|
-
|
|
28
|
-
When you see exit 2, do NOT bypass it. Read the output, fix the root cause, and re-run.
|
|
29
|
-
|
|
30
|
-
## Interpreting audit output
|
|
31
|
-
|
|
32
|
-
Carson audit output is structured as labelled key-value lines prefixed with ⧓. Key sections:
|
|
33
|
-
|
|
34
|
-
- **Working Tree** — staged/unstaged status.
|
|
35
|
-
- **Main Sync Status** — whether local main matches remote. If ahead, reset drift before committing.
|
|
36
|
-
- **Scope Integrity Guard** — checks that commits stay within a single business intent and scope group.
|
|
37
|
-
- **Audit Result** — final verdict: `status: ok` (clean), `status: attention` (advisory, not blocking), `status: block` (must fix).
|
|
38
|
-
|
|
39
|
-
## Interpreting govern output
|
|
40
|
-
|
|
41
|
-
`carson govern --dry-run` classifies each PR:
|
|
42
|
-
|
|
43
|
-
- **ready** → would merge. All gates pass.
|
|
44
|
-
- **ci_failing** → would dispatch agent to fix CI.
|
|
45
|
-
- **review_blocked** → would dispatch agent to address review comments.
|
|
46
|
-
- **pending** → skip. Checks still running (within check_wait window).
|
|
47
|
-
- **needs_attention** → escalate. Needs human judgement.
|
|
48
|
-
|
|
49
|
-
The summary line: `govern_summary: repos=N prs=N ready=N blocked=N`
|
|
50
|
-
|
|
51
|
-
## Configuration
|
|
52
|
-
|
|
53
|
-
Single config file: `~/.carson/config.json`. Key settings:
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"govern": {
|
|
58
|
-
"repos": ["~/Dev/repo-a", "~/Dev/repo-b"],
|
|
59
|
-
"merge": { "method": "rebase" },
|
|
60
|
-
"agent": { "provider": "auto" }
|
|
61
|
-
},
|
|
62
|
-
"review": {
|
|
63
|
-
"bot_usernames": ["gemini-code-assist"]
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
- `govern.merge.method` — must match GitHub branch protection. Use `rebase` if linear history is required.
|
|
69
|
-
- `govern.repos` — list of repo paths for portfolio-level governance. Empty = current repo only.
|
|
70
|
-
- `govern.agent.provider` — `auto` (tries codex then claude), `codex`, or `claude`.
|
|
71
|
-
- `review.bot_usernames` — bot logins to ignore in review gate. Use GraphQL login format (no `[bot]` suffix).
|
|
72
|
-
|
|
73
|
-
Environment overrides take precedence over config file. Common ones:
|
|
74
|
-
- `CARSON_GOVERN_MERGE_METHOD`
|
|
75
|
-
- `CARSON_REVIEW_BOT_USERNAMES`
|
|
76
|
-
- `CARSON_GOVERN_CHECK_WAIT`
|
|
77
|
-
|
|
78
|
-
## Common scenarios
|
|
79
|
-
|
|
80
|
-
**Commit blocked by audit:**
|
|
81
|
-
Run `carson audit`, read the block reason, fix it, then `git add` and `git commit` again. Do not skip the hook.
|
|
82
|
-
|
|
83
|
-
**Review gate blocked:**
|
|
84
|
-
Run `carson review gate` to see which comments need disposition. Respond to each with the required prefix (default: `Disposition:`), then re-run.
|
|
85
|
-
|
|
86
|
-
**Local main drifted ahead of remote:**
|
|
87
|
-
This means a commit was made to main that couldn't be pushed (branch protection). Reset: `git checkout main && git reset --hard github/main`.
|
|
88
|
-
|
|
89
|
-
**Hooks out of date after upgrade:**
|
|
90
|
-
Run `carson refresh` to re-apply hooks and templates for the current version.
|
|
91
|
-
|
|
92
|
-
**Govern merge fails:**
|
|
93
|
-
Check that `govern.merge.method` in config matches what GitHub allows. If the repo enforces linear history, only `rebase` works.
|
|
94
|
-
|
|
95
|
-
## Boundaries
|
|
96
|
-
|
|
97
|
-
- Carson never lives inside governed repositories. No `.carson.yml`, no `bin/carson`, no `.tools/carson/`.
|
|
98
|
-
- Carson-managed files in repos are limited to `.github/*` templates.
|
|
99
|
-
- Carson's hooks live at `~/.carson/hooks/<version>/`, never in `.git/hooks/`.
|