claude-dev-env 1.93.0 → 1.94.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/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/code_rules_gate.py +127 -13
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -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 +1 -1
- package/bin/install.mjs +2 -7
- package/bin/install.test.mjs +10 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- 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/pii_scanner.py +26 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_pii_scanner.py +27 -2
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +20 -8
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/package.json +1 -1
- 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 +3 -25
- 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 +284 -94
- 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/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/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 +12 -8
- package/skills/orchestrator-refresh/SKILL.md +3 -3
- 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/privacy-hygiene/SKILL.md +8 -7
- 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/usage-pause/SKILL.md +10 -14
- 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-fix-protocol/SKILL.md +0 -65
- 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
|
|
@@ -22,9 +22,9 @@ 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/
|
|
27
|
-
| LAN address | Unlisted `10.x` / `172.16–31.x` / `192.168.x` | Public addresses;
|
|
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
|
+
| 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
|
|
|
30
30
|
Surfaces:
|
|
@@ -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:
|
|
@@ -71,7 +71,7 @@ Review each hit. Ignore:
|
|
|
71
71
|
|---|---|
|
|
72
72
|
| Real email | Replace with `user@example.com` or remove |
|
|
73
73
|
| Home path | Use `Path.home()`, `~`, or `C:/Users/<you>/` |
|
|
74
|
-
| LAN address | Remove, use a hostname, or
|
|
74
|
+
| LAN address | Remove, use a hostname, or — for your own NAS — set the host in `CLAUDE_NAS_HOST` or `~/.claude/local-identity.json`, both of which stay out of git |
|
|
75
75
|
| Credential material | Remove from the tree; rotate the credential; load from env/secret store |
|
|
76
76
|
| Already committed | Rewrite is not enough if already pushed — rotate secrets; scrub history only with explicit user approval |
|
|
77
77
|
|
|
@@ -87,7 +87,7 @@ Review each hit. Ignore:
|
|
|
87
87
|
- Intentional public maintainer identity published on purpose
|
|
88
88
|
- Example domains reserved for documentation (`example.com` and siblings)
|
|
89
89
|
- Placeholder home users (`example`, `user`, `alice`, `<you>`, `YOUR_USER`)
|
|
90
|
-
-
|
|
90
|
+
- The NAS host you configure locally (allowlisted at scan time, never committed)
|
|
91
91
|
- Public addresses and docs that name private ranges without a live host (still prefer hostnames)
|
|
92
92
|
|
|
93
93
|
## Enable on any machine / public repository
|
|
@@ -103,7 +103,8 @@ Hooks register via `hooks/hooks.json` into `~/.claude/settings.json`. Once insta
|
|
|
103
103
|
|
|
104
104
|
## Open knobs
|
|
105
105
|
|
|
106
|
-
- **NAS / LAN allowlist:** Unlisted private IPs are blocked. `
|
|
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.
|
|
@@ -31,12 +31,14 @@ Sessions come in many shapes — convergence loops, feature builds, research div
|
|
|
31
31
|
|
|
32
32
|
## Backend Detection (run before Step 1)
|
|
33
33
|
|
|
34
|
-
Determine which storage backend is available. First success wins.
|
|
34
|
+
Determine which storage backend is available for the write path. First success wins.
|
|
35
35
|
|
|
36
36
|
1. **Headless vault** — Bash `ob --version` to verify the obsidian-headless CLI is installed. Check `OBSIDIAN_VAULT_PATH` env var or `~/.claude/vault/` for a vault directory. When the CLI check succeeds AND at least one of those paths resolves to a vault directory, set `backend = "headless"`.
|
|
37
37
|
2. **Local vault** — fall back to `~/.claude/vault/`. Create `~/.claude/vault/sessions` via `mkdir -p` if missing. Set `backend = "local"`.
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
session-log writes with Write/Edit and publishes with Artifact, so it uses headless or local filesystem paths only. `/session-tidy` Phase 0 reuses this headless → local order and adds an Obsidian MCP branch for vault maintenance.
|
|
40
|
+
|
|
41
|
+
**Session-number detection:** Bash `ls` the project's session folder, parse filenames matching `[N]. *.html` or `[N]. *.md`. Highest N + 1. New project → start at 1.
|
|
40
42
|
|
|
41
43
|
**Output paths:**
|
|
42
44
|
- headless: `$OBSIDIAN_VAULT_PATH/sessions/[Project]/[N]. [Title].html` (falls back to `~/.claude/vault/` when the env var is unset)
|
|
@@ -139,15 +141,12 @@ Only invoke `/remember` for decisions the user confirms; `/remember` writes the
|
|
|
139
141
|
Scope: the current project's session folder only.
|
|
140
142
|
|
|
141
143
|
1. **List files** in the project's vault session folder via Bash `ls`.
|
|
142
|
-
2. **
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
- **Status coherence:** `status: completed` with `blocked: true` is contradictory. `status: in-progress` or `status: blocked` on sessions older than 7 days is stale.
|
|
146
|
-
3. **Auto-fix minor issues** via Edit:
|
|
147
|
-
- Missing frontmatter fields that can be inferred (e.g., `blocked: false` when status is `completed`; `vault_context_retrieved: false` when the field is absent, since the field defaults to false in pre-existing sessions)
|
|
144
|
+
2. **Audit** each `.html` and `.md` session file using the audit rules in `session-tidy` Phase 1 (naming, frontmatter completeness, type, status coherence, orphaned next-steps, categorization). session-tidy SKILL.md is the single home for those rules.
|
|
145
|
+
3. **Auto-fix minor HTML metadata issues** via Edit (this step's write path is HTML-oriented):
|
|
146
|
+
- Missing frontmatter fields that can be inferred (e.g., `blocked: false` when status is `completed`; `vault_context_retrieved: false` when the field is absent)
|
|
148
147
|
- `type` field set to a wrong value (correct to `session-report`)
|
|
149
148
|
|
|
150
|
-
When the Edit touches the session report created in this run, call `Artifact` again with the same `file_path` and `favicon` from step 2 to redeploy — the URL captured in step 2 stays unchanged. When the Edit touches an older session report published in a prior run, call `Artifact` on that `file_path` with no `url` argument, since this skill keeps no record of that file's earlier URL — this publishes
|
|
149
|
+
When the Edit touches the session report created in this run, call `Artifact` again with the same `file_path` and `favicon` from step 2 to redeploy — the URL captured in step 2 stays unchanged. When the Edit touches an older HTML session report published in a prior run, call `Artifact` on that `file_path` with no `url` argument, since this skill keeps no record of that file's earlier URL — this publishes that report at a new URL. Surface a `Session [N] republished at a new URL: <url>` line so the user can update any prior shares.
|
|
151
150
|
4. **Report issues that need user input:**
|
|
152
151
|
- Files with wrong naming convention (propose new name)
|
|
153
152
|
- Stale statuses (propose update to `completed` or ask)
|
|
@@ -155,7 +154,7 @@ Scope: the current project's session folder only.
|
|
|
155
154
|
|
|
156
155
|
If no issues are found, skip silently. Do not report "all clean."
|
|
157
156
|
5. **Rollup check:** if the project has 5+ sessions and no `Summary.html` or `Summary.md`, mention it:
|
|
158
|
-
> "This project has [N] sessions and no summary.
|
|
157
|
+
> "This project has [N] sessions and no summary. Run `/session-tidy` for a project rollup."
|
|
159
158
|
|
|
160
159
|
## Step 6: Finalize
|
|
161
160
|
|
|
@@ -176,7 +175,7 @@ The primary outcome comes from the session title resolved in step 1.
|
|
|
176
175
|
## Run-and-report checklist
|
|
177
176
|
|
|
178
177
|
- [ ] Backend detected and announced
|
|
179
|
-
- [ ] Session number resolved from `[N]. *.html` and `[N]. *.md` files (
|
|
178
|
+
- [ ] Session number resolved from `[N]. *.html` and `[N]. *.md` files (highest N + 1)
|
|
180
179
|
- [ ] `artifact-design` skill loaded before composing page content
|
|
181
180
|
- [ ] HTML composed for this session's character (no document wrapper tags; self-contained, responsive, theme-aware)
|
|
182
181
|
- [ ] Frontmatter HTML comment present as the first content line
|
|
@@ -185,7 +184,7 @@ The primary outcome comes from the session title resolved in step 1.
|
|
|
185
184
|
- [ ] Published via the `Artifact` tool with the fixed favicon `📓`; URL captured (or HTML emitted to chat when step 2 Write failed)
|
|
186
185
|
- [ ] Vault-context line appended via Edit (step 3); `Artifact` redeployed on the same URL
|
|
187
186
|
- [ ] Decision extraction surfaced any unrecorded items
|
|
188
|
-
- [ ] Session tidy
|
|
187
|
+
- [ ] Session tidy ran session-tidy Phase 1 audit rules on the project folder (or stayed silent when clean)
|
|
189
188
|
- [ ] `/rename` command copied to clipboard via `pwsh Set-Clipboard`
|
|
190
189
|
|
|
191
190
|
## Folder map
|