claude-dev-env 1.58.0 → 1.60.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.
- package/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -55,16 +55,25 @@ directory, so the working directory must be the PR worktree before any local
|
|
|
55
55
|
work begins. Re-resolve it every tick — a rebase or a fresh HEAD can move the
|
|
56
56
|
branch tip.
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`
|
|
61
|
-
forms and dropping any trailing `.git
|
|
58
|
+
Classify the working directory against the PR's repo. The preflight script
|
|
59
|
+
reads the current working tree's origin, parses its `<owner>/<repo>` (accepting
|
|
60
|
+
the `https://github.com/<owner>/<repo>`, `git@github.com:<owner>/<repo>`, and
|
|
61
|
+
`ssh://git@github.com/<owner>/<repo>` forms and dropping any trailing `.git`),
|
|
62
|
+
and prints a `PREFLIGHT_OUTCOME=<same_repo|different_repo|re_rooted>` line plus a
|
|
63
|
+
human-readable summary:
|
|
62
64
|
|
|
63
65
|
```bash
|
|
64
|
-
|
|
66
|
+
python "$HOME/.claude/skills/_shared/pr-loop/scripts/preflight_worktree.py" --owner <owner> --repo <repo> --mode classify
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
A `same_repo` outcome exits 0 only when the worktree machinery is healthy; a
|
|
70
|
+
`same_repo` outcome whose `git worktree list` probe failed exits non-zero in
|
|
71
|
+
every mode. A `different_repo` outcome exits 0 in classify mode. A `re_rooted`
|
|
72
|
+
outcome (no git work tree, or no readable origin) exits non-zero. Route on the
|
|
73
|
+
`PREFLIGHT_OUTCOME` value:
|
|
74
|
+
|
|
75
|
+
- **`PREFLIGHT_OUTCOME=same_repo`** (the working directory is a checkout of the
|
|
76
|
+
PR's repo): the `EnterWorktree`
|
|
68
77
|
pre-flight checkout is the PR worktree, and the working directory already
|
|
69
78
|
points here, so no `cd` is needed. Bring the branch to the PR head with the
|
|
70
79
|
same deterministic `checkout -B` the cross-repo case uses, after confirming
|
|
@@ -74,9 +83,13 @@ git remote get-url origin
|
|
|
74
83
|
git fetch origin
|
|
75
84
|
git checkout -B <branch> origin/<branch>
|
|
76
85
|
```
|
|
86
|
+
When the script prints an `ABORT:` line whose recovery names `git worktree
|
|
87
|
+
prune`, the working tree's worktree machinery is broken and the preflight
|
|
88
|
+
exits non-zero: stop the tick, run that `git worktree prune` in the named
|
|
89
|
+
directory, and re-run rather than continuing the checkout.
|
|
77
90
|
|
|
78
|
-
-
|
|
79
|
-
example, the PR lives in `llm-settings` while the session runs from
|
|
91
|
+
- **`PREFLIGHT_OUTCOME=different_repo`** (the session is rooted in another repo
|
|
92
|
+
— for example, the PR lives in `llm-settings` while the session runs from
|
|
80
93
|
`claude-code-config`): route the working directory into a checkout of the
|
|
81
94
|
PR's repo. This is routine and automatic — never pause, and never raise it as
|
|
82
95
|
a fork (see [ground-rules.md](ground-rules.md)). `EnterWorktree` is scoped to
|
|
@@ -128,6 +141,13 @@ git remote get-url origin
|
|
|
128
141
|
a Windows-safe recursive remove (per
|
|
129
142
|
`$HOME/.claude/rules/windows-filesystem-safe.md`).
|
|
130
143
|
|
|
144
|
+
- **`PREFLIGHT_OUTCOME=re_rooted`** (the working directory is not a git work
|
|
145
|
+
tree, or its origin remote is unreadable — a resumed or background session can
|
|
146
|
+
re-root to the home directory): the tick cannot locate the PR's repo from
|
|
147
|
+
here, so neither cwd reuse nor a temp-clone route is safe. Report the printed
|
|
148
|
+
`ABORT` line as a hard blocker and stop the tick. Recover by starting the
|
|
149
|
+
session from a checkout of the PR's repo and re-running.
|
|
150
|
+
|
|
131
151
|
## Step 2: Branch on `phase`
|
|
132
152
|
|
|
133
153
|
### `phase == BUGBOT`
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
4
|
python scripts/check_convergence.py --owner <O> --repo <R> --pr-number <N>
|
|
5
|
-
[--bugbot-down]
|
|
5
|
+
[--bugbot-down] [--copilot-down]
|
|
6
6
|
|
|
7
7
|
The bugbot check-run gate is bypassed when either ``--bugbot-down`` is
|
|
8
8
|
passed OR the ``CLAUDE_REVIEWS_DISABLED`` environment variable lists the
|
|
9
9
|
``bugbot`` token, so a Bugbot opt-out closes the gate without the flag.
|
|
10
10
|
|
|
11
|
+
The Copilot review gate and the pending-requested-reviews gate are bypassed
|
|
12
|
+
when either ``--copilot-down`` is passed OR the ``CLAUDE_REVIEWS_DISABLED``
|
|
13
|
+
environment variable lists the ``copilot`` token, so a Copilot outage or
|
|
14
|
+
quota exhaustion closes the broader convergence gate on the remaining
|
|
15
|
+
signals without the flag.
|
|
16
|
+
|
|
11
17
|
Exit codes:
|
|
12
18
|
0 — all pre-conditions met
|
|
13
19
|
1 — one or more conditions not met (FAIL lines printed to stdout)
|
|
@@ -58,7 +64,10 @@ _shared_pr_loop_scripts_dir = (
|
|
|
58
64
|
if str(_shared_pr_loop_scripts_dir) not in sys.path:
|
|
59
65
|
sys.path.insert(0, str(_shared_pr_loop_scripts_dir))
|
|
60
66
|
|
|
61
|
-
from reviews_disabled import
|
|
67
|
+
from reviews_disabled import (
|
|
68
|
+
is_bugbot_disabled_via_env,
|
|
69
|
+
is_copilot_disabled_via_env,
|
|
70
|
+
)
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
def _is_bugteam_review(review_body: str) -> bool:
|
|
@@ -489,14 +498,141 @@ def _check_no_pending_reviews(
|
|
|
489
498
|
return True, "no pending reviewers"
|
|
490
499
|
|
|
491
500
|
|
|
492
|
-
def
|
|
501
|
+
def _bugbot_conditions(
|
|
502
|
+
*, owner: str, repo: str, number: int, head_sha: str, is_bugbot_down: bool
|
|
503
|
+
) -> list[tuple[str, tuple[bool, str]]]:
|
|
504
|
+
"""Build the Bugbot gate conditions, bypassed when Bugbot is down.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
owner: GitHub repository owner login.
|
|
508
|
+
repo: GitHub repository name.
|
|
509
|
+
number: Pull request number to inspect.
|
|
510
|
+
head_sha: Current PR HEAD SHA the gates evaluate against.
|
|
511
|
+
is_bugbot_down: When True, emit a single bypassed check-run
|
|
512
|
+
condition and skip the review-body content gate entirely.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
The bugbot check-run condition, plus the review-body content
|
|
516
|
+
condition when the check-run gate is present and passing.
|
|
517
|
+
"""
|
|
518
|
+
if is_bugbot_down:
|
|
519
|
+
return [("bugbot_clean_at == current_head", (True, "bypassed (bugbot_down)"))]
|
|
520
|
+
conditions: list[tuple[str, tuple[bool, str]]] = [
|
|
521
|
+
(
|
|
522
|
+
"bugbot_clean_at == current_head",
|
|
523
|
+
_check_bugbot(owner=owner, repo=repo, sha=head_sha),
|
|
524
|
+
)
|
|
525
|
+
]
|
|
526
|
+
if conditions[-1][1][0]:
|
|
527
|
+
conditions.append(
|
|
528
|
+
(
|
|
529
|
+
"bugbot review body clean",
|
|
530
|
+
_check_bugbot_not_dirty(
|
|
531
|
+
owner=owner, repo=repo, number=number, head_sha=head_sha
|
|
532
|
+
),
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
return conditions
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _copilot_review_condition(
|
|
539
|
+
*, owner: str, repo: str, number: int, head_sha: str, is_copilot_down: bool
|
|
540
|
+
) -> tuple[str, tuple[bool, str]]:
|
|
541
|
+
"""Build the Copilot review gate condition, bypassed when Copilot is down.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
owner: GitHub repository owner login.
|
|
545
|
+
repo: GitHub repository name.
|
|
546
|
+
number: Pull request number to inspect.
|
|
547
|
+
head_sha: Current PR HEAD SHA the gate evaluates against.
|
|
548
|
+
is_copilot_down: When True, return a bypassed condition rather than
|
|
549
|
+
demanding a Copilot review that an outage or quota exhaustion
|
|
550
|
+
keeps from landing.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
The Copilot review gate condition for the current HEAD.
|
|
554
|
+
"""
|
|
555
|
+
if is_copilot_down:
|
|
556
|
+
return ("copilot_clean_at == current_head", (True, "bypassed (copilot_down)"))
|
|
557
|
+
return (
|
|
558
|
+
"copilot_clean_at == current_head",
|
|
559
|
+
_check_bot_review(
|
|
560
|
+
owner=owner,
|
|
561
|
+
repo=repo,
|
|
562
|
+
number=number,
|
|
563
|
+
head_sha=head_sha,
|
|
564
|
+
login_substring=COPILOT_LOGIN_FILTER_SUBSTRING,
|
|
565
|
+
clean_states=ALL_COPILOT_CLEAN_REVIEW_STATES,
|
|
566
|
+
dirty_states=ALL_COPILOT_DIRTY_REVIEW_STATES,
|
|
567
|
+
label="copilot",
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _pending_reviews_condition(
|
|
573
|
+
*, owner: str, repo: str, number: int, is_copilot_down: bool
|
|
574
|
+
) -> tuple[str, tuple[bool, str]]:
|
|
575
|
+
"""Build the pending-requested-reviews condition, bypassed when Copilot is down.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
owner: GitHub repository owner login.
|
|
579
|
+
repo: GitHub repository name.
|
|
580
|
+
number: Pull request number to inspect.
|
|
581
|
+
is_copilot_down: When True, return a bypassed condition so a Copilot
|
|
582
|
+
review request that will never land does not strand the gate.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
The pending-requested-reviews condition, which the gate checks for a
|
|
586
|
+
still-pending Copilot reviewer.
|
|
587
|
+
"""
|
|
588
|
+
if is_copilot_down:
|
|
589
|
+
return ("no pending requested reviews", (True, "bypassed (copilot_down)"))
|
|
590
|
+
return (
|
|
591
|
+
"no pending requested reviews",
|
|
592
|
+
_check_no_pending_reviews(owner=owner, repo=repo, number=number),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _print_conditions(all_conditions: list[tuple[str, tuple[bool, str]]]) -> int:
|
|
597
|
+
"""Print one PASS/FAIL line per condition and return the aggregate exit code.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
all_conditions: Ordered (label, (passed, detail)) gate results.
|
|
601
|
+
|
|
602
|
+
Returns:
|
|
603
|
+
``0`` when every condition passed, ``1`` when at least one failed.
|
|
604
|
+
"""
|
|
605
|
+
is_all_passed = True
|
|
606
|
+
for each_index, (each_label, (each_passed, each_detail)) in enumerate(
|
|
607
|
+
all_conditions, start=1
|
|
608
|
+
):
|
|
609
|
+
status = "PASS" if each_passed else "FAIL"
|
|
610
|
+
print(f"{each_index}. {each_label}: {status} — {each_detail}")
|
|
611
|
+
if not each_passed:
|
|
612
|
+
is_all_passed = False
|
|
613
|
+
print()
|
|
614
|
+
if is_all_passed:
|
|
615
|
+
print("All pre-conditions met — PR is ready to mark ready.")
|
|
616
|
+
else:
|
|
617
|
+
print("One or more pre-conditions not met — do not mark ready.")
|
|
618
|
+
return 0 if is_all_passed else 1
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def check_all(
|
|
622
|
+
*,
|
|
623
|
+
owner: str,
|
|
624
|
+
repo: str,
|
|
625
|
+
number: int,
|
|
626
|
+
is_bugbot_down: bool,
|
|
627
|
+
is_copilot_down: bool,
|
|
628
|
+
) -> int:
|
|
493
629
|
"""Run every convergence gate and print one PASS/FAIL line per condition.
|
|
494
630
|
|
|
495
631
|
Args:
|
|
496
632
|
owner: GitHub repository owner login.
|
|
497
633
|
repo: GitHub repository name.
|
|
498
634
|
number: Pull request number to inspect.
|
|
499
|
-
|
|
635
|
+
is_bugbot_down: When True, bypass both the Cursor Bugbot check-run
|
|
500
636
|
presence gate and the bugbot review-body content gate. The
|
|
501
637
|
check-run gate appears in the condition list with a
|
|
502
638
|
``bypassed (bugbot_down)`` note; the review-body gate is
|
|
@@ -504,6 +640,13 @@ def check_all(*, owner: str, repo: str, number: int, bugbot_down: bool) -> int:
|
|
|
504
640
|
declared Cursor Bugbot unreachable on the current HEAD so the
|
|
505
641
|
broader convergence gate can still close on the remaining
|
|
506
642
|
signals.
|
|
643
|
+
is_copilot_down: When True, bypass both the Copilot review gate and
|
|
644
|
+
the pending-requested-reviews gate, each shown with a
|
|
645
|
+
``bypassed (copilot_down)`` note. Callers pass True when Copilot
|
|
646
|
+
is down or out of quota on the current HEAD so the broader
|
|
647
|
+
convergence gate can still close on the remaining signals; the
|
|
648
|
+
bypassed pending gate keeps a Copilot review request that will
|
|
649
|
+
never land from stranding the gate.
|
|
507
650
|
|
|
508
651
|
Returns:
|
|
509
652
|
``0`` when every gate reports PASS, ``1`` when at least one gate
|
|
@@ -522,29 +665,15 @@ def check_all(*, owner: str, repo: str, number: int, bugbot_down: bool) -> int:
|
|
|
522
665
|
print(f"HEAD: {head_sha[:7]}\n")
|
|
523
666
|
|
|
524
667
|
conditions: list[tuple[str, tuple[bool, str]]] = []
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
668
|
+
conditions.extend(
|
|
669
|
+
_bugbot_conditions(
|
|
670
|
+
owner=owner,
|
|
671
|
+
repo=repo,
|
|
672
|
+
number=number,
|
|
673
|
+
head_sha=head_sha,
|
|
674
|
+
is_bugbot_down=is_bugbot_down,
|
|
532
675
|
)
|
|
533
|
-
|
|
534
|
-
conditions.append(
|
|
535
|
-
(
|
|
536
|
-
"bugbot_clean_at == current_head",
|
|
537
|
-
_check_bugbot(owner=owner, repo=repo, sha=head_sha),
|
|
538
|
-
)
|
|
539
|
-
)
|
|
540
|
-
if conditions[-1][1][0]:
|
|
541
|
-
conditions.append(
|
|
542
|
-
(
|
|
543
|
-
"bugbot review body clean",
|
|
544
|
-
_check_bugbot_not_dirty(owner=owner, repo=repo, number=number, head_sha=head_sha),
|
|
545
|
-
)
|
|
546
|
-
)
|
|
547
|
-
|
|
676
|
+
)
|
|
548
677
|
conditions.append(
|
|
549
678
|
(
|
|
550
679
|
"bugteam_clean_at == current_head",
|
|
@@ -553,54 +682,30 @@ def check_all(*, owner: str, repo: str, number: int, bugbot_down: bool) -> int:
|
|
|
553
682
|
),
|
|
554
683
|
)
|
|
555
684
|
)
|
|
556
|
-
|
|
557
685
|
conditions.append(
|
|
558
|
-
(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
head_sha=head_sha,
|
|
565
|
-
login_substring=COPILOT_LOGIN_FILTER_SUBSTRING,
|
|
566
|
-
clean_states=ALL_COPILOT_CLEAN_REVIEW_STATES,
|
|
567
|
-
dirty_states=ALL_COPILOT_DIRTY_REVIEW_STATES,
|
|
568
|
-
label="copilot",
|
|
569
|
-
),
|
|
686
|
+
_copilot_review_condition(
|
|
687
|
+
owner=owner,
|
|
688
|
+
repo=repo,
|
|
689
|
+
number=number,
|
|
690
|
+
head_sha=head_sha,
|
|
691
|
+
is_copilot_down=is_copilot_down,
|
|
570
692
|
)
|
|
571
693
|
)
|
|
572
|
-
|
|
573
694
|
conditions.append(
|
|
574
695
|
(
|
|
575
696
|
"zero unresolved bot threads",
|
|
576
697
|
_count_unresolved_bot_threads(owner=owner, repo=repo, number=number),
|
|
577
698
|
)
|
|
578
699
|
)
|
|
579
|
-
|
|
580
700
|
conditions.append(
|
|
581
701
|
("PR is mergeable", _get_mergeable(owner=owner, repo=repo, number=number))
|
|
582
702
|
)
|
|
583
|
-
|
|
584
703
|
conditions.append(
|
|
585
|
-
(
|
|
586
|
-
|
|
587
|
-
_check_no_pending_reviews(owner=owner, repo=repo, number=number),
|
|
704
|
+
_pending_reviews_condition(
|
|
705
|
+
owner=owner, repo=repo, number=number, is_copilot_down=is_copilot_down
|
|
588
706
|
)
|
|
589
707
|
)
|
|
590
|
-
|
|
591
|
-
is_all_passed = True
|
|
592
|
-
for each_index, (each_label, (each_passed, each_detail)) in enumerate(conditions, start=1):
|
|
593
|
-
status = "PASS" if each_passed else "FAIL"
|
|
594
|
-
print(f"{each_index}. {each_label}: {status} — {each_detail}")
|
|
595
|
-
if not each_passed:
|
|
596
|
-
is_all_passed = False
|
|
597
|
-
|
|
598
|
-
print()
|
|
599
|
-
if is_all_passed:
|
|
600
|
-
print("All pre-conditions met — PR is ready to mark ready.")
|
|
601
|
-
else:
|
|
602
|
-
print("One or more pre-conditions not met — do not mark ready.")
|
|
603
|
-
return 0 if is_all_passed else 1
|
|
708
|
+
return _print_conditions(conditions)
|
|
604
709
|
|
|
605
710
|
|
|
606
711
|
def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
@@ -611,10 +716,10 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
611
716
|
``sys.argv[1:]``.
|
|
612
717
|
|
|
613
718
|
Returns:
|
|
614
|
-
Namespace exposing ``owner``, ``repo``, ``pr_number``,
|
|
615
|
-
``bugbot_down`` attributes. ``bugbot_down``
|
|
616
|
-
|
|
617
|
-
|
|
719
|
+
Namespace exposing ``owner``, ``repo``, ``pr_number``,
|
|
720
|
+
``bugbot_down``, and ``copilot_down`` attributes. ``bugbot_down``
|
|
721
|
+
and ``copilot_down`` default to False so the base hook contract
|
|
722
|
+
(``--owner X --repo Y --pr-number N``) picks up the full gate set.
|
|
618
723
|
"""
|
|
619
724
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
620
725
|
parser.add_argument("--owner", required=True, help="GitHub repository owner")
|
|
@@ -630,6 +735,14 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
|
630
735
|
"declared Cursor Bugbot unreachable on the current HEAD."
|
|
631
736
|
),
|
|
632
737
|
)
|
|
738
|
+
parser.add_argument(
|
|
739
|
+
"--copilot-down",
|
|
740
|
+
action="store_true",
|
|
741
|
+
help=(
|
|
742
|
+
"Bypass the Copilot review gate and the pending-requested-reviews "
|
|
743
|
+
"gate when Copilot is down or out of quota on the current HEAD."
|
|
744
|
+
),
|
|
745
|
+
)
|
|
633
746
|
return parser.parse_args(all_argv)
|
|
634
747
|
|
|
635
748
|
|
|
@@ -647,6 +760,23 @@ def _resolve_bugbot_down(bugbot_down_flag: bool) -> bool:
|
|
|
647
760
|
return bugbot_down_flag or is_bugbot_disabled_via_env()
|
|
648
761
|
|
|
649
762
|
|
|
763
|
+
def _resolve_copilot_down(is_copilot_down_flag: bool) -> bool:
|
|
764
|
+
"""Combine the explicit flag with the CLAUDE_REVIEWS_DISABLED env opt-out.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
is_copilot_down_flag: Value of the ``--copilot-down`` CLI flag.
|
|
768
|
+
|
|
769
|
+
Returns:
|
|
770
|
+
True when the flag is set OR ``CLAUDE_REVIEWS_DISABLED`` lists the
|
|
771
|
+
``copilot`` token, so a Copilot outage signalled through the env var
|
|
772
|
+
bypasses the Copilot gates even when the caller omits the flag. The
|
|
773
|
+
mark-ready blocker hook re-runs this script without the flag, so the
|
|
774
|
+
env token is the only channel a genuine Copilot outage has to pass
|
|
775
|
+
that independent gate.
|
|
776
|
+
"""
|
|
777
|
+
return is_copilot_down_flag or is_copilot_disabled_via_env()
|
|
778
|
+
|
|
779
|
+
|
|
650
780
|
def main(all_arguments: list[str]) -> int:
|
|
651
781
|
"""Run the script end-to-end against parsed CLI arguments.
|
|
652
782
|
|
|
@@ -661,7 +791,8 @@ def main(all_arguments: list[str]) -> int:
|
|
|
661
791
|
owner=arguments.owner,
|
|
662
792
|
repo=arguments.repo,
|
|
663
793
|
number=getattr(arguments, "pr_number"),
|
|
664
|
-
|
|
794
|
+
is_bugbot_down=_resolve_bugbot_down(arguments.bugbot_down),
|
|
795
|
+
is_copilot_down=_resolve_copilot_down(arguments.copilot_down),
|
|
665
796
|
)
|
|
666
797
|
|
|
667
798
|
|
|
@@ -245,6 +245,34 @@ def should_resolve_bugbot_down_false_when_env_disables_only_bugteam(
|
|
|
245
245
|
assert check_convergence._resolve_bugbot_down(False) is False
|
|
246
246
|
|
|
247
247
|
|
|
248
|
+
def should_resolve_copilot_down_true_when_flag_set(
|
|
249
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
250
|
+
) -> None:
|
|
251
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
252
|
+
assert check_convergence._resolve_copilot_down(True) is True
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def should_resolve_copilot_down_true_when_env_disables_copilot(
|
|
256
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
257
|
+
) -> None:
|
|
258
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
|
|
259
|
+
assert check_convergence._resolve_copilot_down(False) is True
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def should_resolve_copilot_down_false_when_flag_unset_and_env_empty(
|
|
263
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
264
|
+
) -> None:
|
|
265
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
266
|
+
assert check_convergence._resolve_copilot_down(False) is False
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def should_resolve_copilot_down_false_when_env_disables_only_bugbot(
|
|
270
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
271
|
+
) -> None:
|
|
272
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
273
|
+
assert check_convergence._resolve_copilot_down(False) is False
|
|
274
|
+
|
|
275
|
+
|
|
248
276
|
def should_bypass_bugbot_gates_when_bugbot_down_is_true(
|
|
249
277
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
250
278
|
) -> None:
|
|
@@ -324,7 +352,7 @@ def should_bypass_bugbot_gates_when_bugbot_down_is_true(
|
|
|
324
352
|
)
|
|
325
353
|
|
|
326
354
|
exit_code = check_convergence.check_all(
|
|
327
|
-
owner="o", repo="r", number=1,
|
|
355
|
+
owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
|
|
328
356
|
)
|
|
329
357
|
captured_stdout = capsys.readouterr().out
|
|
330
358
|
|
|
@@ -334,6 +362,122 @@ def should_bypass_bugbot_gates_when_bugbot_down_is_true(
|
|
|
334
362
|
assert exit_code == 0
|
|
335
363
|
|
|
336
364
|
|
|
365
|
+
def should_bypass_copilot_gates_when_copilot_down_is_true(
|
|
366
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
367
|
+
) -> None:
|
|
368
|
+
all_invocation_names: list[str] = []
|
|
369
|
+
|
|
370
|
+
def stub_get_pr_head_sha(*, owner: str, repo: str, number: int) -> str:
|
|
371
|
+
return CURRENT_HEAD_SHA
|
|
372
|
+
|
|
373
|
+
def stub_check_bugbot(*, owner: str, repo: str, sha: str) -> tuple[bool, str]:
|
|
374
|
+
return True, "stub passing"
|
|
375
|
+
|
|
376
|
+
def stub_check_bugbot_not_dirty(
|
|
377
|
+
*, owner: str, repo: str, number: int, head_sha: str
|
|
378
|
+
) -> tuple[bool, str]:
|
|
379
|
+
return True, "stub passing"
|
|
380
|
+
|
|
381
|
+
def stub_check_bugteam_clean(
|
|
382
|
+
*, owner: str, repo: str, number: int, head_sha: str
|
|
383
|
+
) -> tuple[bool, str]:
|
|
384
|
+
return True, "stub passing"
|
|
385
|
+
|
|
386
|
+
def stub_check_bot_review_should_not_be_called(
|
|
387
|
+
*,
|
|
388
|
+
owner: str,
|
|
389
|
+
repo: str,
|
|
390
|
+
number: int,
|
|
391
|
+
head_sha: str,
|
|
392
|
+
login_substring: str,
|
|
393
|
+
clean_states: tuple[str, ...],
|
|
394
|
+
dirty_states: tuple[str, ...],
|
|
395
|
+
label: str,
|
|
396
|
+
) -> tuple[bool, str]:
|
|
397
|
+
all_invocation_names.append("_check_bot_review")
|
|
398
|
+
raise AssertionError(
|
|
399
|
+
"_check_bot_review must not be invoked when copilot_down=True"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def stub_count_unresolved_bot_threads(
|
|
403
|
+
*, owner: str, repo: str, number: int
|
|
404
|
+
) -> tuple[bool, str]:
|
|
405
|
+
return True, "stub passing"
|
|
406
|
+
|
|
407
|
+
def stub_get_mergeable(
|
|
408
|
+
*, owner: str, repo: str, number: int
|
|
409
|
+
) -> tuple[bool, str]:
|
|
410
|
+
return True, "stub passing"
|
|
411
|
+
|
|
412
|
+
def stub_check_no_pending_reviews_should_not_be_called(
|
|
413
|
+
*, owner: str, repo: str, number: int
|
|
414
|
+
) -> tuple[bool, str]:
|
|
415
|
+
all_invocation_names.append("_check_no_pending_reviews")
|
|
416
|
+
raise AssertionError(
|
|
417
|
+
"_check_no_pending_reviews must not be invoked when copilot_down=True"
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", stub_get_pr_head_sha)
|
|
421
|
+
monkeypatch.setattr(check_convergence, "_check_bugbot", stub_check_bugbot)
|
|
422
|
+
monkeypatch.setattr(
|
|
423
|
+
check_convergence, "_check_bugbot_not_dirty", stub_check_bugbot_not_dirty
|
|
424
|
+
)
|
|
425
|
+
monkeypatch.setattr(check_convergence, "_check_bugteam_clean", stub_check_bugteam_clean)
|
|
426
|
+
monkeypatch.setattr(
|
|
427
|
+
check_convergence,
|
|
428
|
+
"_check_bot_review",
|
|
429
|
+
stub_check_bot_review_should_not_be_called,
|
|
430
|
+
)
|
|
431
|
+
monkeypatch.setattr(
|
|
432
|
+
check_convergence, "_count_unresolved_bot_threads", stub_count_unresolved_bot_threads
|
|
433
|
+
)
|
|
434
|
+
monkeypatch.setattr(check_convergence, "_get_mergeable", stub_get_mergeable)
|
|
435
|
+
monkeypatch.setattr(
|
|
436
|
+
check_convergence,
|
|
437
|
+
"_check_no_pending_reviews",
|
|
438
|
+
stub_check_no_pending_reviews_should_not_be_called,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
exit_code = check_convergence.check_all(
|
|
442
|
+
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
|
|
443
|
+
)
|
|
444
|
+
captured_stdout = capsys.readouterr().out
|
|
445
|
+
|
|
446
|
+
assert "_check_bot_review" not in all_invocation_names
|
|
447
|
+
assert "_check_no_pending_reviews" not in all_invocation_names
|
|
448
|
+
assert "bypassed (copilot_down)" in captured_stdout
|
|
449
|
+
assert exit_code == 0
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def should_accept_copilot_down_flag_in_parsed_arguments() -> None:
|
|
453
|
+
arguments = check_convergence.parse_arguments(
|
|
454
|
+
["--owner", "o", "--repo", "r", "--pr-number", "1", "--copilot-down"]
|
|
455
|
+
)
|
|
456
|
+
assert arguments.copilot_down is True
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def should_return_zero_from_print_conditions_when_every_condition_passes(
|
|
460
|
+
capsys: pytest.CaptureFixture[str],
|
|
461
|
+
) -> None:
|
|
462
|
+
exit_code = check_convergence._print_conditions(
|
|
463
|
+
[("gate one", (True, "ok")), ("gate two", (True, "ok"))]
|
|
464
|
+
)
|
|
465
|
+
captured_stdout = capsys.readouterr().out
|
|
466
|
+
assert exit_code == 0
|
|
467
|
+
assert "All pre-conditions met" in captured_stdout
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def should_return_one_from_print_conditions_when_any_condition_fails(
|
|
471
|
+
capsys: pytest.CaptureFixture[str],
|
|
472
|
+
) -> None:
|
|
473
|
+
exit_code = check_convergence._print_conditions(
|
|
474
|
+
[("gate one", (True, "ok")), ("gate two", (False, "nope"))]
|
|
475
|
+
)
|
|
476
|
+
captured_stdout = capsys.readouterr().out
|
|
477
|
+
assert exit_code == 1
|
|
478
|
+
assert "One or more pre-conditions not met" in captured_stdout
|
|
479
|
+
|
|
480
|
+
|
|
337
481
|
def should_propagate_systemexit_from_get_pr_head_sha(
|
|
338
482
|
monkeypatch: pytest.MonkeyPatch,
|
|
339
483
|
) -> None:
|
|
@@ -347,6 +491,33 @@ def should_propagate_systemexit_from_get_pr_head_sha(
|
|
|
347
491
|
)
|
|
348
492
|
|
|
349
493
|
with pytest.raises(SystemExit) as exc_info:
|
|
350
|
-
check_convergence.check_all(
|
|
494
|
+
check_convergence.check_all(
|
|
495
|
+
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
496
|
+
)
|
|
351
497
|
|
|
352
498
|
assert exc_info.value.code == check_convergence.EXIT_CODE_GH_ERROR
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def should_derive_copilot_down_from_env_when_main_omits_the_flag(
|
|
502
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
503
|
+
) -> None:
|
|
504
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
|
|
505
|
+
captured_copilot_down: list[bool] = []
|
|
506
|
+
|
|
507
|
+
def stub_check_all(
|
|
508
|
+
*,
|
|
509
|
+
owner: str,
|
|
510
|
+
repo: str,
|
|
511
|
+
number: int,
|
|
512
|
+
is_bugbot_down: bool,
|
|
513
|
+
is_copilot_down: bool,
|
|
514
|
+
) -> int:
|
|
515
|
+
captured_copilot_down.append(is_copilot_down)
|
|
516
|
+
return 0
|
|
517
|
+
|
|
518
|
+
monkeypatch.setattr(check_convergence, "check_all", stub_check_all)
|
|
519
|
+
exit_code = check_convergence.main(
|
|
520
|
+
["--owner", "o", "--repo", "r", "--pr-number", "1"]
|
|
521
|
+
)
|
|
522
|
+
assert exit_code == 0
|
|
523
|
+
assert captured_copilot_down == [True]
|
package/skills/rebase/SKILL.md
CHANGED
|
@@ -60,11 +60,10 @@ When in doubt, ask. Both work; the choice affects history shape, not correctness
|
|
|
60
60
|
| Tool | Use when | Example |
|
|
61
61
|
|---|---|---|
|
|
62
62
|
| `mcp__serena__find_symbol` / `find_referencing_symbols` | Symbol-aware language server is available — definition vs. reference distinction matters, and you want call-site context | `find_referencing_symbols(symbol_name)` returns every caller with file/line and surrounding code |
|
|
63
|
-
| `mcp__zoekt__search_symbols` / `search` | Cross-repo or large codebase indexed in zoekt; faster than grep on big trees | `search(query)` returns ranked matches with snippets |
|
|
64
63
|
| `Grep` tool (ripgrep) | Local single-repo plain-text scan; no symbol awareness needed | `Grep(pattern, type="py")` — much faster than shell `grep` and respects `.gitignore` |
|
|
65
64
|
| `grep -rn` | Last resort; only when the above are unavailable | — |
|
|
66
65
|
|
|
67
|
-
The Grep tool is the default for plain-text scans (faster than shell grep, respects gitignore). Reach for serena when you need to distinguish "this name is defined here" from "this name is referenced here," which catches false positives from comments, docstrings, and string literals.
|
|
66
|
+
The Grep tool is the default for plain-text scans (faster than shell grep, respects gitignore). Reach for serena when you need to distinguish "this name is defined here" from "this name is referenced here," which catches false positives from comments, docstrings, and string literals.
|
|
68
67
|
|
|
69
68
|
This is the bug that hides best. Don't skip it.
|
|
70
69
|
|
|
@@ -107,7 +106,6 @@ When in doubt, ask. Both work; the choice affects history shape, not correctness
|
|
|
107
106
|
11. **Reference scan for removals/renames.** For every symbol the rebase deleted or renamed (per the commit messages from step 4), scan the post-rebase tree using the same tool-preference order as step 4:
|
|
108
107
|
|
|
109
108
|
- **Preferred:** `mcp__serena__find_referencing_symbols` (symbol-aware; ignores false matches in comments and string literals).
|
|
110
|
-
- **Fallback:** `mcp__zoekt__search` for cross-repo or large trees.
|
|
111
109
|
- **Then:** the `Grep` tool (e.g., `Grep(pattern="<symbol>", type="py")`) for fast in-repo scans.
|
|
112
110
|
- **Last resort:** `grep -rn "<symbol>" .` (let ripgrep defaults and `.gitignore` handle scoping)
|
|
113
111
|
|
|
@@ -156,7 +154,7 @@ After rebase, BEFORE push:
|
|
|
156
154
|
python -c "import …" ──► must succeed
|
|
157
155
|
pytest --collect-only ──► must succeed
|
|
158
156
|
targeted pytest run ──► must pass
|
|
159
|
-
symbol scan (serena →
|
|
157
|
+
symbol scan (serena → Grep) ──► no stale references
|
|
160
158
|
|
|
161
159
|
Push:
|
|
162
160
|
not main/master/release ──► force-with-lease, ask first
|