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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d026cfc1581b568aaa9459ab21faf0d4f52d1026995b5ce62f21dca0072853a
4
- data.tar.gz: '08bdaaebab6ccaf1dac6768057f55acf3833ddbe903001b2ca3934551f280bae'
3
+ metadata.gz: 5156cb5207a0ee95cad65c30d494a24ddcee97c90f2aab6861b34f9ec8e92069
4
+ data.tar.gz: 182a83aaa84ec1d9c0a548bdaf98e421b4eab9e4ab1248b0d67db58e42a7c5ae
5
5
  SHA512:
6
- metadata.gz: 68d7fd88800a7a2756466ae88e3be1baae2df66b25d285cd54b676b6506d1ee86c69a110bf4f040c8259fc9dc8aa623120b0150b4a73c3081460c080940bfb2b
7
- data.tar.gz: 35403e45bad7632031bbc9c56a8aece6e7b3dc7eafa1c36c4f183bc7c6271beb1b39d4b35985373cb9be0baee3b8cc58491920bb2f63dcf852fb20c4198c33ed
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`. If freshness is behind or unknown, delivery stops with an explicit block and no PR side effect. 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.
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.29.0
1
+ 3.30.0
data/carson.gemspec CHANGED
@@ -37,7 +37,6 @@ Gem::Specification.new do |spec|
37
37
  "RELEASE.md",
38
38
  "VERSION",
39
39
  "LICENSE",
40
- "SKILL.md",
41
40
  "icon.svg",
42
41
  "carson.gemspec"
43
42
  ]
@@ -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
- result[ :freshness ] = freshness_payload( freshness: evaluation.fetch( :freshness ) ) if evaluation[ :freshness ]
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 ] = freshness_recovery( freshness: evaluation.fetch( :freshness ) ) if evaluation[ :cause ] == "freshness" && evaluation[ :freshness ]
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( freshness: freshness, pr_state: pr_state ) if ci == :error || review.fetch( :status, :pass ) == :error || !pr_state.is_a?( Hash )
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( freshness: freshness, pr_state: pr_state ) if pr_state[ "state" ] == "MERGED"
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( freshness: freshness, pr_state: pr_state ) if pr_state[ "state" ] == "CLOSED"
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( freshness: freshness, pr_state: pr_state ) if pr_state[ "isDraft" ]
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( freshness: freshness, pr_state: pr_state ) if ci == :pending
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( freshness: freshness, pr_state: pr_state ) if ci == :fail
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( freshness: freshness, pr_state: pr_state ) if review.fetch( :review, :none ) == :changes_requested
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( freshness: freshness, pr_state: pr_state ) if review.fetch( :review, :none ) == :review_required
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( freshness: freshness, pr_state: pr_state ) if review.fetch( :status, :pass ) == :fail
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, freshness: freshness, pr_state: pr_state )
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
- mergeable = pr_state.fetch( "mergeable", "" ).to_s.upcase
500
- merge_state = pr_state.fetch( "mergeStateStatus", "" ).to_s.upcase
501
-
502
- return {
503
- phase: :blocked,
504
- reason: "merge_conflict",
505
- cause: "merge",
506
- summary: "pull request has merge conflicts"
507
- } if mergeable == "CONFLICTING" || merge_state == "DIRTY" || merge_state == "CONFLICTING"
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: :waiting,
532
- reason: "mergeability_pending",
533
- cause: "assessment",
534
- summary: "waiting for GitHub mergeability"
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
- def mergeability_assessment( pr_state: )
893
- return nil unless pr_state.is_a?( Hash )
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 [ "gated", "policy", "pull request is still a draft" ] if pr_state[ "isDraft" ]
899
- return [ "gated", "merge", "pull request has merge conflicts" ] if mergeable == "CONFLICTING" || merge_state == "DIRTY" || merge_state == "CONFLICTING"
900
- return [ "gated", "merge", "merge is blocked by repository policy" ] if merge_state == "BLOCKED"
901
- return [ "gated", "freshness", "branch is behind #{config.git_remote}/#{config.main_branch}" ] if merge_state == "BEHIND"
902
- return [ "queued", nil, "ready to integrate into #{config.main_branch}" ] if merge_state == "CLEAN" || mergeable == "MERGEABLE"
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
- nil
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
- puts_line "Delivery: #{branch} → #{main}"
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 #{main} with #{result[ :merge_method ]}."
987
+ puts_line "Merged into #{remote_main} with #{result[ :merge_method ]}."
965
988
  else
966
- puts_line "Merged into #{main}."
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
- freshness = assess_branch_freshness(
222
- head_ref: delivery.head || delivery.branch,
223
- remote: config.git_remote,
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: "freshness",
231
- summary: freshness.fetch( :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.29.0
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/`.