claude-dev-env 1.93.1 → 1.95.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/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +35 -27
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/audit-contract.md +1 -1
- package/_shared/pr-loop/gh-payloads.md +3 -3
- package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
- package/_shared/pr-loop/scripts/README.md +2 -2
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
- package/_shared/pr-loop/state-schema.md +3 -22
- package/bin/CLAUDE.md +11 -2
- package/bin/ever-shipped-skills.mjs +70 -0
- package/bin/install.mjs +138 -13
- package/bin/install.prune.test.mjs +457 -0
- package/bin/install.test.mjs +10 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_shared.py +82 -0
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/pii_commit_command.py +398 -0
- package/hooks/blocking/pii_payload_scan.py +133 -0
- package/hooks/blocking/pii_prevention_blocker.py +151 -449
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +9 -2
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/hooks/pyproject.toml +75 -4
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +10 -1
- package/scripts/Get-SessionAccount.ps1 +210 -0
- package/scripts/tests/CLAUDE.md +15 -1
- package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
- package/skills/CLAUDE.md +6 -27
- package/skills/anthropic-plan/SKILL.md +1 -13
- package/skills/autoconverge/CLAUDE.md +5 -2
- package/skills/autoconverge/SKILL.md +112 -387
- package/skills/autoconverge/reference/CLAUDE.md +4 -1
- package/skills/autoconverge/reference/closing-report.md +9 -2
- package/skills/autoconverge/reference/convergence.md +26 -22
- package/skills/autoconverge/reference/headless-safety.md +44 -0
- package/skills/autoconverge/reference/multi-pr.md +88 -0
- package/skills/autoconverge/reference/self-closing-loop.md +84 -0
- package/skills/autoconverge/reference/stop-conditions.md +21 -13
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
- package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +286 -96
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/CONSTRAINTS.md +3 -3
- package/skills/bugteam/PROMPTS.md +8 -19
- package/skills/bugteam/SKILL.md +13 -14
- package/skills/bugteam/reference/CLAUDE.md +1 -1
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
- package/skills/bugteam/reference/github-pr-reviews.md +56 -38
- package/skills/bugteam/reference/team-setup.md +11 -11
- package/skills/bugteam/scripts/CLAUDE.md +6 -6
- package/skills/bugteam/scripts/README.md +20 -8
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
- package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
- package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/copilot-finding-triage/SKILL.md +80 -28
- package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
- package/skills/copilot-finding-triage/templates/notification.md +8 -3
- package/skills/everything-search/SKILL.md +1 -2
- package/skills/grokify/SKILL.md +58 -0
- package/skills/grokify/templates/handoff-template.md +92 -0
- package/skills/orchestrator/SKILL.md +35 -25
- package/skills/orchestrator-refresh/SKILL.md +15 -11
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +132 -233
- package/skills/pr-converge/reference/convergence-gates.md +102 -66
- package/skills/pr-converge/reference/examples.md +37 -25
- package/skills/pr-converge/reference/fix-protocol.md +7 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
- package/skills/pr-converge/reference/per-tick.md +64 -41
- package/skills/pr-converge/reference/state-schema.md +27 -2
- package/skills/pr-converge/scripts/CLAUDE.md +8 -0
- package/skills/pr-converge/scripts/README.md +4 -3
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
- package/skills/pr-converge/scripts/check_convergence.py +189 -645
- package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
- package/skills/pr-converge/scripts/conftest.py +8 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
- package/skills/pr-fix-protocol/SKILL.md +20 -20
- package/skills/privacy-hygiene/SKILL.md +4 -3
- package/skills/reviewer-gates/SKILL.md +26 -8
- package/skills/session-log/SKILL.md +11 -12
- package/skills/session-tidy/SKILL.md +40 -30
- package/skills/skill-builder/CLAUDE.md +13 -10
- package/skills/skill-builder/SKILL.md +31 -31
- package/skills/skill-builder/references/CLAUDE.md +10 -6
- package/skills/skill-builder/references/delegation-map.md +13 -4
- package/skills/skill-builder/references/description-field.md +113 -0
- package/skills/skill-builder/references/self-audit-checklist.md +21 -4
- package/skills/skill-builder/references/skill-modularity.md +113 -0
- package/skills/skill-builder/references/skill-types.md +4 -0
- package/skills/skill-builder/templates/CLAUDE.md +4 -4
- package/skills/skill-builder/templates/gap-analysis.md +25 -1
- package/skills/skill-builder/workflows/CLAUDE.md +6 -4
- package/skills/skill-builder/workflows/improve-skill.md +17 -16
- package/skills/skill-builder/workflows/new-skill.md +34 -10
- package/skills/skill-builder/workflows/polish-skill.md +59 -43
- package/skills/team-advisor/SKILL.md +15 -11
- package/skills/usage-pause/SKILL.md +10 -14
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
- package/skills/bdd-protocol/CLAUDE.md +0 -26
- package/skills/bdd-protocol/SKILL.md +0 -30
- package/skills/bdd-protocol/references/CLAUDE.md +0 -10
- package/skills/bdd-protocol/references/anti-patterns.md +0 -26
- package/skills/bdd-protocol/references/example-mapping.md +0 -23
- package/skills/bg-agent/CLAUDE.md +0 -17
- package/skills/bg-agent/SKILL.md +0 -69
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
- package/skills/caveman/CLAUDE.md +0 -15
- package/skills/caveman/SKILL.md +0 -38
- package/skills/code/CLAUDE.md +0 -17
- package/skills/code/SKILL.md +0 -178
- package/skills/deep-research/CLAUDE.md +0 -17
- package/skills/deep-research/SKILL.md +0 -80
- package/skills/findbugs/CLAUDE.md +0 -20
- package/skills/findbugs/SKILL.md +0 -241
- package/skills/fixbugs/CLAUDE.md +0 -19
- package/skills/fixbugs/SKILL.md +0 -142
- package/skills/gh-paginate/CLAUDE.md +0 -18
- package/skills/gh-paginate/SKILL.md +0 -84
- package/skills/gotcha/CLAUDE.md +0 -33
- package/skills/gotcha/SKILL.md +0 -73
- package/skills/implement/CLAUDE.md +0 -27
- package/skills/implement/SKILL.md +0 -66
- package/skills/implement/scripts/CLAUDE.md +0 -22
- package/skills/implement/scripts/append_note.py +0 -133
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
- package/skills/implement/scripts/test_append_note.py +0 -191
- package/skills/log-audit/CLAUDE.md +0 -20
- package/skills/log-audit/SKILL.md +0 -66
- package/skills/log-audit/reference/CLAUDE.md +0 -9
- package/skills/log-audit/reference/charter.md +0 -52
- package/skills/log-audit/scripts/CLAUDE.md +0 -27
- package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
- package/skills/log-audit/scripts/collect_log_window.py +0 -199
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
- package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
- package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
- package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
- package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
- package/skills/logifix/CLAUDE.md +0 -36
- package/skills/logifix/SKILL.md +0 -69
- package/skills/logifix/scripts/CLAUDE.md +0 -16
- package/skills/logifix/scripts/logifix.ps1 +0 -205
- package/skills/monitor-open-prs/CLAUDE.md +0 -34
- package/skills/monitor-open-prs/SKILL.md +0 -88
- package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
- package/skills/monitor-open-prs/test_skill_contract.py +0 -27
- package/skills/post-audit-findings/SKILL.md +0 -80
- package/skills/pr-consistency-audit/CLAUDE.md +0 -34
- package/skills/pr-consistency-audit/SKILL.md +0 -116
- package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
- package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
- package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
- package/skills/pr-review-responder/CLAUDE.md +0 -35
- package/skills/pr-review-responder/EXAMPLES.md +0 -590
- package/skills/pr-review-responder/PRINCIPLES.md +0 -533
- package/skills/pr-review-responder/README.md +0 -168
- package/skills/pr-review-responder/SKILL.md +0 -203
- package/skills/pr-review-responder/TESTING.md +0 -350
- package/skills/pr-scope-resolve/SKILL.md +0 -45
- package/skills/pre-compact/CLAUDE.md +0 -24
- package/skills/pre-compact/SKILL.md +0 -134
- package/skills/qbug/CLAUDE.md +0 -40
- package/skills/qbug/SKILL.md +0 -387
- package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
- package/skills/refine/CLAUDE.md +0 -44
- package/skills/refine/SKILL.md +0 -262
- package/skills/refine/templates/CLAUDE.md +0 -17
- package/skills/refine/templates/implementation-notes-template.html +0 -56
- package/skills/refine/templates/plan-template.md +0 -60
- package/skills/research-mode/CLAUDE.md +0 -35
- package/skills/research-mode/SKILL.md +0 -53
- package/skills/structure-prompt/CLAUDE.md +0 -42
- package/skills/structure-prompt/SKILL.md +0 -44
- package/skills/structure-prompt/reference/CLAUDE.md +0 -28
- package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
- package/skills/structure-prompt/reference/block-classification.md +0 -27
- package/skills/structure-prompt/reference/canonical-case.md +0 -48
- package/skills/structure-prompt/reference/citation-depth.md +0 -70
- package/skills/structure-prompt/reference/cleanup.md +0 -33
- package/skills/structure-prompt/reference/constraints.md +0 -33
- package/skills/structure-prompt/reference/directives.md +0 -37
- package/skills/structure-prompt/reference/examples.md +0 -72
- package/skills/structure-prompt/reference/instantiation.md +0 -51
- package/skills/structure-prompt/reference/output-contract.md +0 -72
- package/skills/structure-prompt/reference/per-category.md +0 -23
- package/skills/structure-prompt/reference/persona.md +0 -38
- package/skills/structure-prompt/reference/research.md +0 -33
- package/skills/structure-prompt/reference/structure.md +0 -28
- package/skills/verified-build/CLAUDE.md +0 -33
- package/skills/verified-build/SKILL.md +0 -33
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Contract-pin and flag tests for check_convergence.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
check_all(all gates passing) -> exit 0, "All pre-conditions met ..."
|
|
6
|
+
check_all(one gate failing) -> exit 1, "One or more ... do not mark ready."
|
|
7
|
+
_get_pr_head_sha raises SystemExit -> exit 2 propagates
|
|
8
|
+
parse_arguments([... --bugteam-post-blocked]) -> bugteam_post_blocked True
|
|
9
|
+
|
|
10
|
+
The pin tests fix the stdout the four consumers parse (converge.mjs, pr-converge,
|
|
11
|
+
bugteam, qbug) byte-for-byte, plus the exit codes and CLI surface. The flag tests
|
|
12
|
+
drive the ``--bugteam-post-blocked`` bypass.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.util
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from types import ModuleType
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
import _pr_converge_path_setup # noqa: F401
|
|
25
|
+
from pr_converge_skill_constants.constants import EXIT_CODE_GH_ERROR
|
|
26
|
+
|
|
27
|
+
_SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
|
|
28
|
+
_PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
|
|
29
|
+
|
|
30
|
+
CURRENT_HEAD_SHA = "abcdef1234567890abcdef1234567890abcdef12"
|
|
31
|
+
|
|
32
|
+
_ALL_LEAF_GATE_NAMES = [
|
|
33
|
+
"_check_bugbot",
|
|
34
|
+
"_check_bugbot_not_dirty",
|
|
35
|
+
"_check_bugteam_clean",
|
|
36
|
+
"_check_bot_review",
|
|
37
|
+
"_count_unresolved_bot_threads",
|
|
38
|
+
"_get_mergeable",
|
|
39
|
+
"_check_no_pending_reviews",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
EXPECTED_ALL_PASS_STDOUT = (
|
|
43
|
+
"HEAD: abcdef1\n\n"
|
|
44
|
+
"1. bugbot_clean_at == current_head: PASS — clean\n"
|
|
45
|
+
"2. bugbot review body clean: PASS — clean\n"
|
|
46
|
+
"3. bugteam_clean_at == current_head: PASS — clean\n"
|
|
47
|
+
"4. copilot_clean_at == current_head: PASS — clean\n"
|
|
48
|
+
"5. zero unresolved bot threads: PASS — clean\n"
|
|
49
|
+
"6. PR is mergeable: PASS — clean\n"
|
|
50
|
+
"7. no pending requested reviews: PASS — clean\n\n"
|
|
51
|
+
"All pre-conditions met — PR is ready to mark ready.\n"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
EXPECTED_BUGBOT_DOWN_STDOUT = (
|
|
55
|
+
"HEAD: abcdef1\n\n"
|
|
56
|
+
"1. bugbot_clean_at == current_head: PASS — bypassed (bugbot_down)\n"
|
|
57
|
+
"2. bugteam_clean_at == current_head: PASS — clean\n"
|
|
58
|
+
"3. copilot_clean_at == current_head: PASS — clean\n"
|
|
59
|
+
"4. zero unresolved bot threads: PASS — clean\n"
|
|
60
|
+
"5. PR is mergeable: PASS — clean\n"
|
|
61
|
+
"6. no pending requested reviews: PASS — clean\n\n"
|
|
62
|
+
"All pre-conditions met — PR is ready to mark ready.\n"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
EXPECTED_COPILOT_DOWN_STDOUT = (
|
|
66
|
+
"HEAD: abcdef1\n\n"
|
|
67
|
+
"1. bugbot_clean_at == current_head: PASS — clean\n"
|
|
68
|
+
"2. bugbot review body clean: PASS — clean\n"
|
|
69
|
+
"3. bugteam_clean_at == current_head: PASS — clean\n"
|
|
70
|
+
"4. copilot_clean_at == current_head: PASS — bypassed (copilot_down)\n"
|
|
71
|
+
"5. zero unresolved bot threads: PASS — clean\n"
|
|
72
|
+
"6. PR is mergeable: PASS — clean\n"
|
|
73
|
+
"7. no pending requested reviews: PASS — bypassed (copilot_down)\n\n"
|
|
74
|
+
"All pre-conditions met — PR is ready to mark ready.\n"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
EXPECTED_FAIL_SUMMARY = "One or more pre-conditions not met — do not mark ready.\n"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _load_check_convergence() -> ModuleType:
|
|
81
|
+
if str(_PR_CONVERGE_DIRECTORY) not in sys.path:
|
|
82
|
+
sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
|
|
83
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
84
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
85
|
+
module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
|
|
86
|
+
spec = importlib.util.spec_from_file_location(
|
|
87
|
+
"check_convergence_contract_under_test", module_path
|
|
88
|
+
)
|
|
89
|
+
assert spec is not None
|
|
90
|
+
assert spec.loader is not None
|
|
91
|
+
module = importlib.util.module_from_spec(spec)
|
|
92
|
+
spec.loader.exec_module(module)
|
|
93
|
+
return module
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
check_convergence = _load_check_convergence()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _clean_head(**_call_keywords: object) -> str:
|
|
100
|
+
return CURRENT_HEAD_SHA
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _clean_gate(**_call_keywords: object) -> tuple[bool, str]:
|
|
104
|
+
return True, "clean"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
|
|
108
|
+
raise AssertionError(
|
|
109
|
+
"_check_bugteam_clean must not run when is_bugteam_post_blocked=True"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
114
|
+
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
|
|
115
|
+
for each_gate_name in _ALL_LEAF_GATE_NAMES:
|
|
116
|
+
monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_all_pass_stdout_and_exit_code_match_the_pinned_contract(
|
|
120
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
121
|
+
) -> None:
|
|
122
|
+
_patch_gates_clean(monkeypatch)
|
|
123
|
+
exit_code = check_convergence.check_all(
|
|
124
|
+
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
125
|
+
)
|
|
126
|
+
assert capsys.readouterr().out == EXPECTED_ALL_PASS_STDOUT
|
|
127
|
+
assert exit_code == 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_bugbot_down_bypass_line_matches_the_pinned_contract(
|
|
131
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
132
|
+
) -> None:
|
|
133
|
+
_patch_gates_clean(monkeypatch)
|
|
134
|
+
exit_code = check_convergence.check_all(
|
|
135
|
+
owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
|
|
136
|
+
)
|
|
137
|
+
assert capsys.readouterr().out == EXPECTED_BUGBOT_DOWN_STDOUT
|
|
138
|
+
assert exit_code == 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_copilot_down_bypass_lines_match_the_pinned_contract(
|
|
142
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
143
|
+
) -> None:
|
|
144
|
+
_patch_gates_clean(monkeypatch)
|
|
145
|
+
exit_code = check_convergence.check_all(
|
|
146
|
+
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
|
|
147
|
+
)
|
|
148
|
+
assert capsys.readouterr().out == EXPECTED_COPILOT_DOWN_STDOUT
|
|
149
|
+
assert exit_code == 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_a_failing_gate_yields_exit_one_and_the_fail_summary(
|
|
153
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
154
|
+
) -> None:
|
|
155
|
+
_patch_gates_clean(monkeypatch)
|
|
156
|
+
|
|
157
|
+
def _blocked_mergeable(**_call_keywords: object) -> tuple[bool, str]:
|
|
158
|
+
return False, "blocked"
|
|
159
|
+
|
|
160
|
+
monkeypatch.setattr(check_convergence, "_get_mergeable", _blocked_mergeable)
|
|
161
|
+
exit_code = check_convergence.check_all(
|
|
162
|
+
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
163
|
+
)
|
|
164
|
+
captured_stdout = capsys.readouterr().out
|
|
165
|
+
assert "6. PR is mergeable: FAIL — blocked\n" in captured_stdout
|
|
166
|
+
assert captured_stdout.endswith(EXPECTED_FAIL_SUMMARY)
|
|
167
|
+
assert exit_code == 1
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_gh_error_exit_code_is_two_and_propagates_from_head_fetch(
|
|
171
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
172
|
+
) -> None:
|
|
173
|
+
def _raise_gh_error(**_call_keywords: object) -> str:
|
|
174
|
+
raise SystemExit(EXIT_CODE_GH_ERROR)
|
|
175
|
+
|
|
176
|
+
monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _raise_gh_error)
|
|
177
|
+
with pytest.raises(SystemExit) as raised:
|
|
178
|
+
check_convergence.check_all(
|
|
179
|
+
owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
|
|
180
|
+
)
|
|
181
|
+
assert EXIT_CODE_GH_ERROR == 2
|
|
182
|
+
assert raised.value.code == 2
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_existing_cli_flags_stay_on_the_argument_surface() -> None:
|
|
186
|
+
arguments = check_convergence.parse_arguments(
|
|
187
|
+
[
|
|
188
|
+
"--owner",
|
|
189
|
+
"o",
|
|
190
|
+
"--repo",
|
|
191
|
+
"r",
|
|
192
|
+
"--pr-number",
|
|
193
|
+
"1",
|
|
194
|
+
"--bugbot-down",
|
|
195
|
+
"--copilot-down",
|
|
196
|
+
]
|
|
197
|
+
)
|
|
198
|
+
assert arguments.owner == "o"
|
|
199
|
+
assert arguments.repo == "r"
|
|
200
|
+
assert getattr(arguments, "pr_number") == 1
|
|
201
|
+
assert arguments.bugbot_down is True
|
|
202
|
+
assert arguments.copilot_down is True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_skip_bugteam_gate_when_bugteam_post_blocked_is_true(
|
|
206
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
207
|
+
) -> None:
|
|
208
|
+
_patch_gates_clean(monkeypatch)
|
|
209
|
+
monkeypatch.setattr(
|
|
210
|
+
check_convergence, "_check_bugteam_clean", _refuse_bugteam_clean
|
|
211
|
+
)
|
|
212
|
+
exit_code = check_convergence.check_all(
|
|
213
|
+
owner="o",
|
|
214
|
+
repo="r",
|
|
215
|
+
number=1,
|
|
216
|
+
is_bugbot_down=False,
|
|
217
|
+
is_copilot_down=False,
|
|
218
|
+
is_bugteam_post_blocked=True,
|
|
219
|
+
)
|
|
220
|
+
assert "bypassed (bugteam_post_blocked)" in capsys.readouterr().out
|
|
221
|
+
assert exit_code == 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_run_bugteam_gate_when_bugteam_post_blocked_is_false(
|
|
225
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
226
|
+
) -> None:
|
|
227
|
+
invoked_gate_names: list[str] = []
|
|
228
|
+
|
|
229
|
+
def _recording_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
|
|
230
|
+
invoked_gate_names.append("_check_bugteam_clean")
|
|
231
|
+
return True, "clean"
|
|
232
|
+
|
|
233
|
+
_patch_gates_clean(monkeypatch)
|
|
234
|
+
monkeypatch.setattr(
|
|
235
|
+
check_convergence, "_check_bugteam_clean", _recording_bugteam_clean
|
|
236
|
+
)
|
|
237
|
+
exit_code = check_convergence.check_all(
|
|
238
|
+
owner="o",
|
|
239
|
+
repo="r",
|
|
240
|
+
number=1,
|
|
241
|
+
is_bugbot_down=False,
|
|
242
|
+
is_copilot_down=False,
|
|
243
|
+
is_bugteam_post_blocked=False,
|
|
244
|
+
)
|
|
245
|
+
assert "_check_bugteam_clean" in invoked_gate_names
|
|
246
|
+
assert "bypassed (bugteam_post_blocked)" not in capsys.readouterr().out
|
|
247
|
+
assert exit_code == 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_bugteam_post_blocked_flag_round_trips_through_parse_arguments() -> None:
|
|
251
|
+
arguments = check_convergence.parse_arguments(
|
|
252
|
+
["--owner", "o", "--repo", "r", "--pr-number", "1", "--bugteam-post-blocked"]
|
|
253
|
+
)
|
|
254
|
+
assert arguments.bugteam_post_blocked is True
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_bugteam_post_blocked_defaults_to_false_when_flag_absent() -> None:
|
|
258
|
+
arguments = check_convergence.parse_arguments(
|
|
259
|
+
["--owner", "o", "--repo", "r", "--pr-number", "1"]
|
|
260
|
+
)
|
|
261
|
+
assert arguments.bugteam_post_blocked is False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_resolve_bugteam_post_blocked_true_when_flag_set(
|
|
265
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
266
|
+
) -> None:
|
|
267
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
268
|
+
assert check_convergence._resolve_bugteam_post_blocked(True) is True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_resolve_bugteam_post_blocked_true_when_env_disables_bugteam(
|
|
272
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
273
|
+
) -> None:
|
|
274
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
|
|
275
|
+
assert check_convergence._resolve_bugteam_post_blocked(False) is True
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_resolve_bugteam_post_blocked_false_when_flag_unset_and_env_empty(
|
|
279
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
280
|
+
) -> None:
|
|
281
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
282
|
+
assert check_convergence._resolve_bugteam_post_blocked(False) is False
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_resolve_bugteam_post_blocked_false_when_env_disables_only_copilot(
|
|
286
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
287
|
+
) -> None:
|
|
288
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
|
|
289
|
+
assert check_convergence._resolve_bugteam_post_blocked(False) is False
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_env_bugteam_token_does_not_disable_copilot_or_bugbot_opt_out(
|
|
293
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
294
|
+
) -> None:
|
|
295
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
|
|
296
|
+
assert check_convergence._resolve_copilot_down(False) is False
|
|
297
|
+
assert check_convergence._resolve_bugteam_post_blocked(False) is True
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
|
|
301
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
302
|
+
) -> None:
|
|
303
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
|
|
304
|
+
captured_bugteam_post_blocked: list[bool] = []
|
|
305
|
+
|
|
306
|
+
def stub_check_all(
|
|
307
|
+
*,
|
|
308
|
+
owner: str,
|
|
309
|
+
repo: str,
|
|
310
|
+
number: int,
|
|
311
|
+
is_bugbot_down: bool,
|
|
312
|
+
is_copilot_down: bool,
|
|
313
|
+
is_bugteam_post_blocked: bool = False,
|
|
314
|
+
) -> int:
|
|
315
|
+
captured_bugteam_post_blocked.append(is_bugteam_post_blocked)
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
monkeypatch.setattr(check_convergence, "check_all", stub_check_all)
|
|
319
|
+
exit_code = check_convergence.main(["--owner", "o", "--repo", "r", "--pr-number", "1"])
|
|
320
|
+
assert exit_code == 0
|
|
321
|
+
assert captured_bugteam_post_blocked == [True]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Behavioral tests for the review and Bugbot convergence gate leaves.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
_flatten_paginated_reviews(two pages) -> newest-first flat list
|
|
6
|
+
_bugbot_run_conclusion_detail(success) -> (True, "check run ...")
|
|
7
|
+
_check_bugbot(named complete run) -> (True, "check run ...")
|
|
8
|
+
|
|
9
|
+
Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
import check_convergence_gates as gates
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
|
|
22
|
+
pages = [
|
|
23
|
+
[{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
|
|
24
|
+
[{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
|
|
25
|
+
]
|
|
26
|
+
flattened = gates._flatten_paginated_reviews(json.dumps(pages))
|
|
27
|
+
assert flattened is not None
|
|
28
|
+
assert [each_review["id"] for each_review in flattened] == [2, 1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
|
|
32
|
+
assert (
|
|
33
|
+
gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
|
|
38
|
+
complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
|
|
39
|
+
passed, detail = gates._bugbot_run_conclusion_detail(
|
|
40
|
+
{"id": 55, "conclusion": complete_conclusion, "html_url": ""}
|
|
41
|
+
)
|
|
42
|
+
assert passed is True
|
|
43
|
+
assert "check run 55" in detail
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_check_bugbot_reports_success_when_the_named_run_is_complete(
|
|
47
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
48
|
+
) -> None:
|
|
49
|
+
complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
|
|
50
|
+
run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
|
|
51
|
+
payload = {
|
|
52
|
+
"check_runs": [
|
|
53
|
+
{
|
|
54
|
+
"name": run_name,
|
|
55
|
+
"id": 9,
|
|
56
|
+
"conclusion": complete_conclusion,
|
|
57
|
+
"html_url": "",
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
|
|
63
|
+
return 0, json.dumps(payload)
|
|
64
|
+
|
|
65
|
+
monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
|
|
66
|
+
passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
|
|
67
|
+
assert passed is True
|
|
68
|
+
assert "check run 9" in detail
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Behavioral tests for the review-thread and pending-reviewer gate leaves.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
_is_unresolved_bot_thread(cursor, unresolved) -> True
|
|
6
|
+
_count_unresolved_bot_threads(one cursor thread) -> (False, "1 unresolved ...")
|
|
7
|
+
_format_unresolved_detail(over limit) -> "... and N more"
|
|
8
|
+
|
|
9
|
+
Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
import check_convergence_thread_gates as thread_gates
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_is_unresolved_bot_thread_true_for_cursor_authored_open_thread() -> None:
|
|
22
|
+
thread = {
|
|
23
|
+
"isResolved": False,
|
|
24
|
+
"isOutdated": False,
|
|
25
|
+
"comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
|
|
26
|
+
}
|
|
27
|
+
logins = (thread_gates.CURSOR_LOGIN_FILTER_SUBSTRING,)
|
|
28
|
+
assert thread_gates._is_unresolved_bot_thread(thread, logins) is True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_is_unresolved_bot_thread_false_for_resolved_thread() -> None:
|
|
32
|
+
thread = {
|
|
33
|
+
"isResolved": True,
|
|
34
|
+
"isOutdated": False,
|
|
35
|
+
"comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
|
|
36
|
+
}
|
|
37
|
+
logins = (thread_gates.CURSOR_LOGIN_FILTER_SUBSTRING,)
|
|
38
|
+
assert thread_gates._is_unresolved_bot_thread(thread, logins) is False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_count_unresolved_bot_threads_counts_a_single_cursor_thread(
|
|
42
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
43
|
+
) -> None:
|
|
44
|
+
thread = {
|
|
45
|
+
"isResolved": False,
|
|
46
|
+
"isOutdated": False,
|
|
47
|
+
"path": "src/app.py",
|
|
48
|
+
"comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
|
|
49
|
+
}
|
|
50
|
+
review_threads = {
|
|
51
|
+
"nodes": [thread],
|
|
52
|
+
"pageInfo": {"hasNextPage": False, "endCursor": None},
|
|
53
|
+
}
|
|
54
|
+
graphql_payload = {
|
|
55
|
+
"data": {"repository": {"pullRequest": {"reviewThreads": review_threads}}}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def _stub_gh_graphql(query: str, variables: dict[str, object]) -> tuple[int, str]:
|
|
59
|
+
return 0, json.dumps(graphql_payload)
|
|
60
|
+
|
|
61
|
+
monkeypatch.setattr(thread_gates, "_gh_graphql", _stub_gh_graphql)
|
|
62
|
+
passed, detail = thread_gates._count_unresolved_bot_threads(
|
|
63
|
+
owner="o", repo="r", number=1
|
|
64
|
+
)
|
|
65
|
+
assert passed is False
|
|
66
|
+
assert detail.startswith("1 unresolved")
|
|
67
|
+
assert "src/app.py" in detail
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_format_unresolved_detail_bounds_the_listed_paths() -> None:
|
|
71
|
+
over_limit = thread_gates.UNRESOLVED_THREAD_DETAIL_MAX + 2
|
|
72
|
+
all_threads = [{"path": f"file{each_index}.py"} for each_index in range(over_limit)]
|
|
73
|
+
detail = thread_gates._format_unresolved_detail(all_threads)
|
|
74
|
+
assert detail.startswith(f"{over_limit} unresolved")
|
|
75
|
+
assert "more" in detail
|
|
@@ -9,56 +9,56 @@ description: >-
|
|
|
9
9
|
|
|
10
10
|
# PR Fix Protocol
|
|
11
11
|
|
|
12
|
-
**Core principle:** a finding is addressed only when the fix is pushed, the thread carries a reply, and the thread is resolved — as one unit of work. Anything
|
|
12
|
+
**Core principle:** a finding is addressed only when the fix is pushed, the thread carries a reply, and the thread is resolved — as one unit of work. Anything short of all three leaves a convergence gate stalled.
|
|
13
13
|
|
|
14
14
|
## How callers invoke this
|
|
15
15
|
|
|
16
|
-
- **Skill-capable contexts** (a lead session
|
|
17
|
-
- **Fallback** (a subagent or teammate without the `Skill` tool): the caller's spawn prompt says
|
|
16
|
+
- **Skill-capable contexts** (a lead session that holds the `Skill` tool): `Skill({skill: "pr-fix-protocol", args: "--skill <caller> --pr <URL> --worktree <path> [findings payload or 'sweep']"})`.
|
|
17
|
+
- **Fallback** (a subagent or teammate without the `Skill` tool): the caller's spawn prompt says to read `~/.claude/skills/pr-fix-protocol/SKILL.md` and apply it with the parameters below.
|
|
18
18
|
|
|
19
|
-
The caller passes: its identity, the PR scope, the PR worktree path, this round's findings (or `sweep` for a thread sweep with no fresh findings), and its post-push obligations
|
|
19
|
+
The caller passes: its identity, the PR scope, the PR worktree path, this round's findings (or `sweep` for a thread sweep with no fresh findings), and its post-push obligations — which clean-SHA state fields to reset and which reviewers to re-trigger.
|
|
20
20
|
|
|
21
21
|
## Executor choice
|
|
22
22
|
|
|
23
|
-
- **Single-PR loops** (no shared `state.json`): the lead spawns `Agent(subagent_type: "clean-coder")` to write the fix.
|
|
24
|
-
- **Multi-PR orchestration** (shared `state.json`): a per-PR clean-coder teammate owns edits, replies, and state writes; the orchestrator
|
|
23
|
+
- **Single-PR loops** (no shared `state.json`): the lead spawns `Agent(subagent_type: "clean-coder")` to write the fix. Stop when `Agent` is unavailable. A spawned clean-coder starts in its own working directory, so its prompt names the PR worktree path and directs it to edit, stage, and commit there.
|
|
24
|
+
- **Multi-PR orchestration** (shared `state.json`): a per-PR clean-coder teammate owns edits, replies, and state writes; the orchestrator holds back from inline edits. The teammate obligations — reply before writing state, which state fields to set, idle handoff — live in the calling skill's multi-PR reference.
|
|
25
25
|
|
|
26
|
-
Run every git command in the PR worktree
|
|
26
|
+
Run every git command in the PR worktree. `git add`, `git commit`, and `git push` act on the repo of the current working directory, so a cross-repo PR's fix lands in the PR's repo only when the working directory is its worktree.
|
|
27
27
|
|
|
28
28
|
## The fix sequence
|
|
29
29
|
|
|
30
|
-
Follow the shared 13-step sequence in [`_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md) exactly: read each file:line, capture the pre-fix SHA and contents,
|
|
30
|
+
Follow the shared 13-step sequence in [`_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md) exactly: read each file:line, capture the pre-fix SHA and contents, write a failing test first where the finding has behavior, apply the fix narrowly, `py_compile`, run the post-fix self-audit, stage by explicit path, make one commit, fast-forward push, then reply and resolve atomically per thread. Its Constraints section — narrow scope, keep the commit hooks, preserve helpers — binds every executor.
|
|
31
31
|
|
|
32
|
-
**Reply transport:** the GitHub MCP `add_reply_to_pull_request_comment` is primary. `python "$HOME/.claude/skills/pr-converge/scripts/post_fix_reply.py" --owner <O> --repo <R> --pr-number <N> --in-reply-to <COMMENT_ID> --body "Fixed in <SHA> — <what changed>"
|
|
32
|
+
**Reply transport:** the GitHub MCP `add_reply_to_pull_request_comment` is primary. For a script-only context or a multi-PR teammate, the fallback is `python "$HOME/.claude/skills/pr-converge/scripts/post_fix_reply.py" --owner <O> --repo <R> --pr-number <N> --in-reply-to <COMMENT_ID> --body "Fixed in <SHA> — <what changed>"`. Both carry the body shape from [`_shared/pr-loop/audit-reply-template.md`](../../_shared/pr-loop/audit-reply-template.md). For a body-only finding with no inline thread, post a top-level review reply that cites the new HEAD SHA.
|
|
33
33
|
|
|
34
|
-
**Thread resolution:** `pull_request_review_write(method="resolve_thread", threadId=<PRRT node id>)` right after each reply. Harvest thread node ids (`PRRT_…`) from `get_review_comments` at fetch time.
|
|
34
|
+
**Thread resolution:** call `pull_request_review_write(method="resolve_thread", threadId=<PRRT node id>)` right after each reply. Harvest the thread node ids (`PRRT_…`) from `get_review_comments` at fetch time.
|
|
35
35
|
|
|
36
36
|
## The unresolved-thread sweep (hard gate)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
A caller advances a phase, records a clean SHA, or marks a PR ready only when the PR carries zero unresolved review threads.
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
pull_request_read(method="get_review_comments") → filter threads where is_resolved == false
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
- The filter is purely `is_resolved == false`. Author, anchor commit, and `is_outdated`
|
|
45
|
-
- `is_outdated` is informational, not a skip flag
|
|
46
|
-
- Per unresolved thread: verify the concern against current HEAD. The concern holds → fix (this protocol) → reply → resolve. The concern does not hold on current HEAD → reply
|
|
47
|
-
- Code changed during the sweep → push and hand control back to the caller's re-entry phase. Only resolutions
|
|
44
|
+
- The filter is purely `is_resolved == false`. Author, anchor commit, and `is_outdated` play no part in the count.
|
|
45
|
+
- `is_outdated` is informational, not a skip flag. GitHub marks a thread outdated when its anchor line changes, yet the concern can still apply to current HEAD — the fix may have moved rather than landed. Verify each outdated thread against current HEAD like any other.
|
|
46
|
+
- Per unresolved thread: verify the concern against current HEAD. The concern holds → fix (this protocol) → reply → resolve. The concern does not hold on current HEAD → reply with a note that explains why → resolve.
|
|
47
|
+
- Code changed during the sweep → push and hand control back to the caller's re-entry phase. Only resolutions and no code → re-run the sweep and keep the caller's state.
|
|
48
48
|
|
|
49
49
|
## Post-push obligations
|
|
50
50
|
|
|
51
51
|
Every push through this protocol:
|
|
52
52
|
|
|
53
|
-
1. Re-resolve `current_head`
|
|
53
|
+
1. Re-resolve `current_head` — `git rev-parse HEAD` locally, and the PR `get` call for API-visible state.
|
|
54
54
|
2. Reset the caller-named clean-SHA fields to null (`bugbot_clean_at`, `code_review_clean_at`, `copilot_clean_at` — whichever the caller tracks). A new HEAD invalidates every prior clean.
|
|
55
|
-
3. Re-trigger reviewers per the caller's parameter
|
|
55
|
+
3. Re-trigger reviewers per the caller's parameter — for Cursor Bugbot the `reviewer-gates` Bugbot flow, for Copilot the caller's request step. Audit-family callers (`bugteam`, `qbug`) skip re-triggering, since their next loop iteration is the reviewer.
|
|
56
56
|
|
|
57
57
|
## Gotchas
|
|
58
58
|
|
|
59
|
-
- **Reply
|
|
60
|
-
- **An unchanged HEAD after step 11 means no commit landed
|
|
61
|
-
- **A resolved thread with no reply reads as dismissed.** Reviewers
|
|
59
|
+
- **Reply, then resolve, atomic per thread.** Batching every reply ahead of any resolve, or yielding to the orchestrator between the two, leaves threads half-addressed when a run dies mid-loop.
|
|
60
|
+
- **An unchanged HEAD after step 11 means no commit landed.** Exit `stuck — could not address findings` rather than report the findings as fixed.
|
|
61
|
+
- **A resolved thread with no reply reads as dismissed.** Reviewers and the humans reading the PR need the one-paragraph explanation the template carries.
|
|
62
62
|
|
|
63
63
|
## Folder map
|
|
64
64
|
|
|
@@ -22,8 +22,8 @@ Find and remove personal data and high-confidence secrets before they land in gi
|
|
|
22
22
|
|
|
23
23
|
| Category | Blocked examples | Allowed residual |
|
|
24
24
|
|---|---|---|
|
|
25
|
-
| Email | `
|
|
26
|
-
| Home path | `C:/Users/
|
|
25
|
+
| Email | `user@example.com` | `user@example.com`, `user@example.org`, `user@example.net` |
|
|
26
|
+
| Home path | `C:/Users/example/...`, `/Users/example/...`, `/home/example/...` | `C:/Users/example/...`, `C:/Users/<you>/...`, `/Users/alice/...` |
|
|
27
27
|
| LAN address | Unlisted `10.x` / `172.16–31.x` / `192.168.x` | Public addresses; your NAS host from `CLAUDE_NAS_HOST` or `~/.claude/local-identity.json`; entries in `ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES` |
|
|
28
28
|
| Secret | `ghp_…`, `github_pat_…`, `AKIA…`, PEM private-key headers | Public keys, redacted `***`, env var names without values |
|
|
29
29
|
|
|
@@ -55,7 +55,7 @@ git diff --name-only origin/main...HEAD
|
|
|
55
55
|
Run from the repo root (PowerShell-friendly example):
|
|
56
56
|
|
|
57
57
|
```
|
|
58
|
-
rg -n --hidden -g '!node_modules' -g '!.git' -e '@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' -e 'Users[/\\][^/\\]+' -e '/home/[^/]+' -e '\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+)\b' -e '\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b' -e '\bgithub_pat_[A-Za-z0-9_]{20,}\b' -e '\bAKIA[0-9A-Z]{16}\b' -e '-----BEGIN [A-Z ]*PRIVATE KEY-----'
|
|
58
|
+
rg -n --hidden -g '!node_modules' -g '!.git' -e '@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' -e 'Users[/\\][^/\\]+' -e '[/]home/[^/]+' -e '\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+)\b' -e '\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b' -e '\bgithub_pat_[A-Za-z0-9_]{20,}\b' -e '\bAKIA[0-9A-Z]{16}\b' -e '-----BEGIN [A-Z ]*PRIVATE KEY-----'
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
Review each hit. Ignore:
|
|
@@ -104,6 +104,7 @@ Hooks register via `hooks/hooks.json` into `~/.claude/settings.json`. Once insta
|
|
|
104
104
|
## Open knobs
|
|
105
105
|
|
|
106
106
|
- **NAS / LAN allowlist:** Unlisted private IPs are blocked. The scanner resolves your NAS host from `CLAUDE_NAS_HOST`, then `~/.claude/local-identity.json` (`nas.host`), and allowlists it when it is a private address, so the committed tree holds no real host. `ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES` in `hooks_constants` holds the static allowlist for any host every machine must share.
|
|
107
|
+
- **Commit-scan exempt repositories:** named owner/repo slugs skip the staged-commit PII scan only. Set `CLAUDE_PII_EXEMPT_REPOS` (comma-separated `owner/repo` values) or list them under `pii_exempt_repositories` in `~/.claude/local-identity.json`. Matching uses the repository's `remote.origin.url` and accepts only the exact host `github.com` (https, ssh scheme, or scp-style). A repository with no readable origin is never exempt (fail-closed to scanning). Write / Edit / MultiEdit and durable post bodies still scan in every repository.
|
|
107
108
|
- **Public maintainer identity:** when a real email or name is intentional product surface, keep it and note that in the PR body so reviewers do not treat it as a leak.
|
|
108
109
|
|
|
109
110
|
## What this skill does not do
|
|
@@ -61,26 +61,44 @@ Run the pre-check once per run, not per tick or per round. Every later tick read
|
|
|
61
61
|
|
|
62
62
|
## Gate 3: Bugbot trigger, acknowledge, and CI-detect
|
|
63
63
|
|
|
64
|
-
Cursor Bugbot signals through CI check runs
|
|
64
|
+
Cursor Bugbot signals through CI check runs.
|
|
65
|
+
Mode flags and exit codes for `check_bugbot_ci.py` live in the script docstring
|
|
66
|
+
— run
|
|
67
|
+
`python "$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py" --help`
|
|
68
|
+
for every mode. Caller action sequence against `<current_head>`:
|
|
65
69
|
|
|
66
70
|
1. **Silent-pass pre-check (always first).**
|
|
67
71
|
`python "$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py" --check-clean --owner <O> --repo <R> --sha <current_head>`
|
|
68
|
-
-
|
|
69
|
-
-
|
|
72
|
+
- Silent pass confirmed → record `bugbot_clean_at = <current_head>` and stop; do not trigger.
|
|
73
|
+
- Silent pass not confirmed → continue.
|
|
70
74
|
2. **Already-queued check.**
|
|
71
75
|
`... check_bugbot_ci.py --check-active --owner <O> --repo <R> --sha <current_head>`
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
3. **Trigger.** Post exactly `bugbot run` as an issue comment (`add_issue_comment`) — no `@cursor[bot]` mention, no other text. `bugbot run` is
|
|
76
|
+
- Already queued → skip posting; wait for completion (callers schedule their own wakeup).
|
|
77
|
+
- Not active → continue.
|
|
78
|
+
3. **Trigger.** Post exactly `bugbot run` as an issue comment (`add_issue_comment`) — no `@cursor[bot]` mention, no other text. `bugbot run` is the only re-trigger Cursor Bugbot recognizes; every other phrasing silently no-ops. Wait 8 seconds with an in-turn Monitor delay, never a foreground `sleep` (blocked in headless runs).
|
|
75
79
|
4. **Acknowledge check.**
|
|
76
80
|
`... check_bugbot_ci.py --owner <O> --repo <R> --sha <current_head>`
|
|
77
|
-
-
|
|
78
|
-
-
|
|
81
|
+
- Check run present → record `bugbot_acknowledged_at = <now, ISO 8601>`; the caller polls on its own cadence.
|
|
82
|
+
- Check run absent → Bugbot is down: set `bugbot_down = true` and route past the Bugbot phase; the run continues on its remaining signals.
|
|
79
83
|
|
|
80
84
|
The silent-pass pre-check runs first so a bot that already finished cleanly is never re-prompted — Bugbot refuses to re-run on an evaluated commit, and without the pre-check the acknowledge step would falsely mark `bugbot_down = true`.
|
|
81
85
|
|
|
82
86
|
**Flag reset rule:** `bugbot_down` resets to false whenever `<current_head>` changes — a new HEAD invalidates the old down-detection. `copilot_down` never resets mid-run; it holds until the run ends.
|
|
83
87
|
|
|
88
|
+
## Convergence-check bypass flags
|
|
89
|
+
|
|
90
|
+
When a reviewer is down, `check_convergence.py` must skip that reviewer's gate so the run still marks ready on its remaining signals. Each condition has one CLI flag and one matching `CLAUDE_REVIEWS_DISABLED` token:
|
|
91
|
+
|
|
92
|
+
| Condition | `check_convergence.py` flag | `CLAUDE_REVIEWS_DISABLED` token |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Bugbot down or off | `--bugbot-down` | `bugbot` |
|
|
95
|
+
| Copilot down or opted out | `--copilot-down` | `copilot` |
|
|
96
|
+
| Bugteam CLEAN post blocked | `--bugteam-post-blocked` | `bugteam` |
|
|
97
|
+
|
|
98
|
+
The caller passes the flag on its own `check_convergence.py` run. The flag alone does not reach the independent mark-ready blocker hook, which re-runs `check_convergence.py` with no flags on `gh pr ready`. Each flag also honors its token: `check_convergence.py` reads the same bypass from `CLAUDE_REVIEWS_DISABLED`. So export the matching token in the same shell before `gh pr ready`, and the hook's no-flag re-check inherits the bypass through the env. Combine tokens with commas when more than one reviewer is down (`CLAUDE_REVIEWS_DISABLED="copilot,bugteam"`).
|
|
99
|
+
|
|
100
|
+
The `bugteam` token here means the bugteam CLEAN-review gate is skipped for this convergence check. It does not disable the bugbot or copilot gates: each token is scoped to its own reviewer.
|
|
101
|
+
|
|
84
102
|
## Gotchas
|
|
85
103
|
|
|
86
104
|
- **`bugbot run` is load-bearing text.** Alternative phrasings (`@cursor run`, `cursor review`, `bugbot please`) return no error and do nothing.
|