claude-dev-env 1.82.0 → 1.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +16 -13
- package/_shared/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -4
- package/hooks/blocking/code_rules_constants_config.py +164 -1
- package/hooks/blocking/code_rules_docstrings.py +428 -15
- package/hooks/blocking/code_rules_enforcer.py +33 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +152 -6
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +5 -0
- package/hooks/hooks_constants/CLAUDE.md +3 -4
- package/hooks/hooks_constants/blocking_check_limits.py +46 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +5 -2
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +40 -1
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +7 -2
- package/skills/autoconverge/SKILL.md +48 -61
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +167 -106
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/everything-search/SKILL.md +5 -0
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +72 -59
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +15 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Tests for the stale_comment_reference_blocker hook."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIR = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_ROOT = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIR not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIR)
|
|
13
|
+
if _HOOKS_ROOT not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_ROOT)
|
|
15
|
+
|
|
16
|
+
from stale_comment_reference_blocker import ( # noqa: E402
|
|
17
|
+
build_deny_payload,
|
|
18
|
+
evaluate,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from hooks_constants.hook_block_logger import _HOOK_BLOCKS_LOG_RELATIVE_PATH # noqa: E402
|
|
22
|
+
|
|
23
|
+
HOOK_SCRIPT_PATH = str(Path(__file__).resolve().parent / "stale_comment_reference_blocker.py")
|
|
24
|
+
|
|
25
|
+
PATCHED_SLEEP_LINE = (
|
|
26
|
+
" with patch('theme_exports.core.exporter.asyncio.sleep', new_callable=AsyncMock):\n"
|
|
27
|
+
)
|
|
28
|
+
PATCHED_POLL_LINE = " with patch('theme_exports.core.exporter.wait_for_overlay_clear', new_callable=AsyncMock):\n"
|
|
29
|
+
KEPT_COMMENT_LINE = " # Mock asyncio\n"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _run_hook(tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
|
|
33
|
+
payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
|
|
34
|
+
return subprocess.run(
|
|
35
|
+
[sys.executable, HOOK_SCRIPT_PATH],
|
|
36
|
+
input=payload,
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
check=False,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _write_module(tmp_path: Path, source_text: str) -> Path:
|
|
44
|
+
written_module_path = tmp_path / "sample_module.py"
|
|
45
|
+
written_module_path.write_text(source_text, encoding="utf-8")
|
|
46
|
+
return written_module_path
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_blocks_edit_that_orphans_the_comment_above(tmp_path: Path) -> None:
|
|
50
|
+
"""Rewriting the patched target under an unchanged comment naming it
|
|
51
|
+
is denied.
|
|
52
|
+
"""
|
|
53
|
+
written_module_path = _write_module(
|
|
54
|
+
tmp_path,
|
|
55
|
+
"def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
|
|
56
|
+
)
|
|
57
|
+
outcome = _run_hook(
|
|
58
|
+
"Edit",
|
|
59
|
+
{
|
|
60
|
+
"file_path": str(written_module_path),
|
|
61
|
+
"old_string": PATCHED_SLEEP_LINE.rstrip("\n"),
|
|
62
|
+
"new_string": PATCHED_POLL_LINE.rstrip("\n"),
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
assert outcome.returncode == 0
|
|
66
|
+
decision = json.loads(outcome.stdout)
|
|
67
|
+
assert decision["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
68
|
+
assert "asyncio" in decision["hookSpecificOutput"]["permissionDecisionReason"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_blocks_edit_that_deletes_the_line_the_comment_names(tmp_path: Path) -> None:
|
|
72
|
+
"""Deleting the patched line under an unchanged comment naming it is
|
|
73
|
+
denied the same as rewriting it would be.
|
|
74
|
+
"""
|
|
75
|
+
written_module_path = _write_module(
|
|
76
|
+
tmp_path,
|
|
77
|
+
"def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
|
|
78
|
+
)
|
|
79
|
+
outcome = _run_hook(
|
|
80
|
+
"Edit",
|
|
81
|
+
{
|
|
82
|
+
"file_path": str(written_module_path),
|
|
83
|
+
"old_string": PATCHED_SLEEP_LINE,
|
|
84
|
+
"new_string": "",
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
assert outcome.returncode == 0
|
|
88
|
+
decision = json.loads(outcome.stdout)
|
|
89
|
+
assert decision["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
90
|
+
assert "asyncio" in decision["hookSpecificOutput"]["permissionDecisionReason"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_allows_edit_that_updates_the_comment_together(tmp_path: Path) -> None:
|
|
94
|
+
"""An edit rewriting the comment and the line together passes."""
|
|
95
|
+
written_module_path = _write_module(
|
|
96
|
+
tmp_path,
|
|
97
|
+
"def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
|
|
98
|
+
)
|
|
99
|
+
outcome = _run_hook(
|
|
100
|
+
"Edit",
|
|
101
|
+
{
|
|
102
|
+
"file_path": str(written_module_path),
|
|
103
|
+
"old_string": KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
|
|
104
|
+
"new_string": " # Mock the overlay poll\n" + PATCHED_POLL_LINE,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
assert outcome.returncode == 0
|
|
108
|
+
assert outcome.stdout == ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_allows_edit_that_keeps_the_named_identifier(tmp_path: Path) -> None:
|
|
112
|
+
"""A rewrite that still carries the identifier the comment names passes."""
|
|
113
|
+
written_module_path = _write_module(
|
|
114
|
+
tmp_path,
|
|
115
|
+
"def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
|
|
116
|
+
)
|
|
117
|
+
retained_line = " with patch('theme_exports.core.exporter.asyncio.sleep'):\n"
|
|
118
|
+
outcome = _run_hook(
|
|
119
|
+
"Edit",
|
|
120
|
+
{
|
|
121
|
+
"file_path": str(written_module_path),
|
|
122
|
+
"old_string": PATCHED_SLEEP_LINE.rstrip("\n"),
|
|
123
|
+
"new_string": retained_line.rstrip("\n"),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
assert outcome.returncode == 0
|
|
127
|
+
assert outcome.stdout == ""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_allows_prose_comment_with_no_orphaned_identifier(tmp_path: Path) -> None:
|
|
131
|
+
"""A prose comment whose tokens never name anything in the edited line
|
|
132
|
+
passes.
|
|
133
|
+
"""
|
|
134
|
+
written_module_path = _write_module(
|
|
135
|
+
tmp_path,
|
|
136
|
+
"def launch_export() -> None:\n"
|
|
137
|
+
" # Overlay settle pause\n"
|
|
138
|
+
" runner.pause_for_overlay_settle()\n",
|
|
139
|
+
)
|
|
140
|
+
outcome = _run_hook(
|
|
141
|
+
"Edit",
|
|
142
|
+
{
|
|
143
|
+
"file_path": str(written_module_path),
|
|
144
|
+
"old_string": " runner.pause_for_overlay_settle()",
|
|
145
|
+
"new_string": " runner.confirm_editor_ready()",
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
assert outcome.returncode == 0
|
|
149
|
+
assert outcome.stdout == ""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_allows_non_python_file(tmp_path: Path) -> None:
|
|
153
|
+
"""A non-Python target passes untouched."""
|
|
154
|
+
plain_text_target = tmp_path / "notes.txt"
|
|
155
|
+
plain_text_target.write_text("# Mock asyncio\nasyncio row\n", encoding="utf-8")
|
|
156
|
+
outcome = _run_hook(
|
|
157
|
+
"Edit",
|
|
158
|
+
{
|
|
159
|
+
"file_path": str(plain_text_target),
|
|
160
|
+
"old_string": "asyncio row",
|
|
161
|
+
"new_string": "other row",
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
assert outcome.returncode == 0
|
|
165
|
+
assert outcome.stdout == ""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_allows_write_tool() -> None:
|
|
169
|
+
"""A non-Edit invocation passes untouched."""
|
|
170
|
+
outcome = _run_hook(
|
|
171
|
+
"Write",
|
|
172
|
+
{
|
|
173
|
+
"file_path": "src/sample_module.py",
|
|
174
|
+
"content": KEPT_COMMENT_LINE + PATCHED_POLL_LINE,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
assert outcome.returncode == 0
|
|
178
|
+
assert outcome.stdout == ""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_allows_missing_file(tmp_path: Path) -> None:
|
|
182
|
+
"""An Edit aimed at a path with no readable target passes."""
|
|
183
|
+
outcome = _run_hook(
|
|
184
|
+
"Edit",
|
|
185
|
+
{
|
|
186
|
+
"file_path": str(tmp_path / "absent_module.py"),
|
|
187
|
+
"old_string": "one",
|
|
188
|
+
"new_string": "two",
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
assert outcome.returncode == 0
|
|
192
|
+
assert outcome.stdout == ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_allows_old_string_not_found(tmp_path: Path) -> None:
|
|
196
|
+
"""An Edit whose old_string never occurs in the target passes."""
|
|
197
|
+
written_module_path = _write_module(tmp_path, "value_count = 1\n")
|
|
198
|
+
outcome = _run_hook(
|
|
199
|
+
"Edit",
|
|
200
|
+
{
|
|
201
|
+
"file_path": str(written_module_path),
|
|
202
|
+
"old_string": "never present",
|
|
203
|
+
"new_string": "still never present",
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
assert outcome.returncode == 0
|
|
207
|
+
assert outcome.stdout == ""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_deny_payload_logs_the_block(tmp_path: Path) -> None:
|
|
211
|
+
"""One record lands in the block journal for each denial built through
|
|
212
|
+
evaluate plus build_deny_payload.
|
|
213
|
+
"""
|
|
214
|
+
written_module_path = _write_module(
|
|
215
|
+
tmp_path,
|
|
216
|
+
"def launch_export() -> None:\n" + KEPT_COMMENT_LINE + PATCHED_SLEEP_LINE,
|
|
217
|
+
)
|
|
218
|
+
deny_reason = evaluate(
|
|
219
|
+
{
|
|
220
|
+
"tool_name": "Edit",
|
|
221
|
+
"tool_input": {
|
|
222
|
+
"file_path": str(written_module_path),
|
|
223
|
+
"old_string": PATCHED_SLEEP_LINE.rstrip("\n"),
|
|
224
|
+
"new_string": PATCHED_POLL_LINE.rstrip("\n"),
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
assert deny_reason is not None
|
|
229
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
230
|
+
deny_payload = build_deny_payload(deny_reason)
|
|
231
|
+
assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
232
|
+
block_log_path = tmp_path / _HOOK_BLOCKS_LOG_RELATIVE_PATH
|
|
233
|
+
all_log_records = block_log_path.read_text(encoding="utf-8").strip().splitlines()
|
|
234
|
+
assert len(all_log_records) == 1
|
|
235
|
+
parsed_log_line = json.loads(all_log_records[0])
|
|
236
|
+
assert parsed_log_line["hook"] == "stale_comment_reference_blocker.py"
|
|
@@ -633,6 +633,141 @@ def test_handles_non_string_tool_name():
|
|
|
633
633
|
assert result.stdout == ""
|
|
634
634
|
|
|
635
635
|
|
|
636
|
+
def test_detects_used_to_in_python_module_docstring() -> None:
|
|
637
|
+
"""A module docstring carrying a transitional phrase counts as
|
|
638
|
+
documentation and is denied.
|
|
639
|
+
"""
|
|
640
|
+
written_source = (
|
|
641
|
+
'"""Both paths used to pad with a flat sleep on every export."""\n'
|
|
642
|
+
"\n"
|
|
643
|
+
"RETRY_BUDGET = 5\n"
|
|
644
|
+
)
|
|
645
|
+
result = _run_hook(
|
|
646
|
+
"Write",
|
|
647
|
+
{
|
|
648
|
+
"file_path": "tests/test_exports.py",
|
|
649
|
+
"content": written_source,
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
assert result.returncode == 0
|
|
653
|
+
output = json.loads(result.stdout)
|
|
654
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
655
|
+
assert "used to" in output["hookSpecificOutput"]["permissionDecisionReason"]
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def test_detects_previously_in_python_function_docstring() -> None:
|
|
659
|
+
"""A function docstring carrying a transitional phrase is denied.
|
|
660
|
+
"""
|
|
661
|
+
written_source = (
|
|
662
|
+
"def load_configuration() -> None:\n"
|
|
663
|
+
' """Previously configured via an environment variable."""\n'
|
|
664
|
+
" raise ValueError\n"
|
|
665
|
+
)
|
|
666
|
+
result = _run_hook(
|
|
667
|
+
"Write",
|
|
668
|
+
{
|
|
669
|
+
"file_path": "src/configuration.py",
|
|
670
|
+
"content": written_source,
|
|
671
|
+
},
|
|
672
|
+
)
|
|
673
|
+
assert result.returncode == 0
|
|
674
|
+
output = json.loads(result.stdout)
|
|
675
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
676
|
+
assert "previously" in output["hookSpecificOutput"]["permissionDecisionReason"]
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def test_ignores_transitional_phrase_in_regular_python_string() -> None:
|
|
680
|
+
"""A plain string literal counts as data rather than documentation and
|
|
681
|
+
passes.
|
|
682
|
+
"""
|
|
683
|
+
result = _run_hook(
|
|
684
|
+
"Write",
|
|
685
|
+
{
|
|
686
|
+
"file_path": "src/messages.py",
|
|
687
|
+
"content": 'PADDING_NOTE = "used to pad with a flat sleep"\n',
|
|
688
|
+
},
|
|
689
|
+
)
|
|
690
|
+
assert result.returncode == 0
|
|
691
|
+
assert result.stdout == ""
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def test_ignores_quoted_mention_inside_python_docstring() -> None:
|
|
695
|
+
"""A docstring quoting a phrase as a mention in double quotes passes.
|
|
696
|
+
"""
|
|
697
|
+
written_source = (
|
|
698
|
+
'"""Blocks docstrings carrying phrases like "used to" in their prose."""\n'
|
|
699
|
+
"\n"
|
|
700
|
+
"RETRY_BUDGET = 3\n"
|
|
701
|
+
)
|
|
702
|
+
result = _run_hook(
|
|
703
|
+
"Write",
|
|
704
|
+
{
|
|
705
|
+
"file_path": "src/scanner.py",
|
|
706
|
+
"content": written_source,
|
|
707
|
+
},
|
|
708
|
+
)
|
|
709
|
+
assert result.returncode == 0
|
|
710
|
+
assert result.stdout == ""
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def test_detects_docstring_at_start_of_unparseable_edit_fragment() -> None:
|
|
714
|
+
"""An Edit fragment opening with a docstring is scanned even when
|
|
715
|
+
parsing fails.
|
|
716
|
+
"""
|
|
717
|
+
fragment = '"""The runner used to sleep between exports."""\n continue\n'
|
|
718
|
+
result = _run_hook(
|
|
719
|
+
"Edit",
|
|
720
|
+
{
|
|
721
|
+
"file_path": "src/exports.py",
|
|
722
|
+
"old_string": "placeholder",
|
|
723
|
+
"new_string": fragment,
|
|
724
|
+
},
|
|
725
|
+
)
|
|
726
|
+
assert result.returncode == 0
|
|
727
|
+
output = json.loads(result.stdout)
|
|
728
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
729
|
+
assert "used to" in output["hookSpecificOutput"]["permissionDecisionReason"]
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def test_detects_docstring_after_def_header_in_unparseable_fragment() -> None:
|
|
733
|
+
"""A triple-quoted block directly under a def header counts as a
|
|
734
|
+
docstring in a fragment.
|
|
735
|
+
"""
|
|
736
|
+
fragment = (
|
|
737
|
+
" def render_summary(self) -> None:\n"
|
|
738
|
+
' """No longer writes the summary sheet."""\n'
|
|
739
|
+
)
|
|
740
|
+
result = _run_hook(
|
|
741
|
+
"Edit",
|
|
742
|
+
{
|
|
743
|
+
"file_path": "src/reporting.py",
|
|
744
|
+
"old_string": "placeholder",
|
|
745
|
+
"new_string": fragment,
|
|
746
|
+
},
|
|
747
|
+
)
|
|
748
|
+
assert result.returncode == 0
|
|
749
|
+
output = json.loads(result.stdout)
|
|
750
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
751
|
+
assert "no longer" in output["hookSpecificOutput"]["permissionDecisionReason"]
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def test_ignores_assigned_triple_quoted_string_in_unparseable_fragment() -> None:
|
|
755
|
+
"""A triple-quoted block assigned to a name counts as data rather than
|
|
756
|
+
a docstring in a fragment.
|
|
757
|
+
"""
|
|
758
|
+
fragment = 'FIXTURE_TEXT = """used to pad with a flat sleep"""\n continue\n'
|
|
759
|
+
result = _run_hook(
|
|
760
|
+
"Edit",
|
|
761
|
+
{
|
|
762
|
+
"file_path": "src/fixtures.py",
|
|
763
|
+
"old_string": "placeholder",
|
|
764
|
+
"new_string": fragment,
|
|
765
|
+
},
|
|
766
|
+
)
|
|
767
|
+
assert result.returncode == 0
|
|
768
|
+
assert result.stdout == ""
|
|
769
|
+
|
|
770
|
+
|
|
636
771
|
def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
|
|
637
772
|
"""A deny routed through the dispatcher's native path logs one record.
|
|
638
773
|
|
package/hooks/hooks.json
CHANGED
|
@@ -60,6 +60,11 @@
|
|
|
60
60
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/conventional_pr_title_gate.py",
|
|
61
61
|
"timeout": 10
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/reviewer_spawn_gate.py",
|
|
66
|
+
"timeout": 20
|
|
67
|
+
},
|
|
63
68
|
{
|
|
64
69
|
"type": "command",
|
|
65
70
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/block_main_commit.py",
|
|
@@ -24,7 +24,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
24
24
|
| `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
|
|
25
25
|
| `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
|
|
26
26
|
| `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
|
|
27
|
-
| `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
|
|
28
27
|
| `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
|
|
29
28
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
30
29
|
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
@@ -34,10 +33,8 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
34
33
|
| `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
|
|
35
34
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
36
35
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
37
|
-
| `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
|
|
38
36
|
| `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
|
|
39
37
|
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
40
|
-
| `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
|
|
41
38
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
42
39
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
43
40
|
| `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
|
|
@@ -53,11 +50,13 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
53
50
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
54
51
|
| `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
|
|
55
52
|
| `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
|
|
53
|
+
| `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
|
|
56
54
|
| `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
|
|
57
55
|
| `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
|
|
58
56
|
| `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
|
|
59
57
|
| `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
|
|
60
|
-
| `
|
|
58
|
+
| `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
|
|
59
|
+
| `state_description_blocker_constants.py` | The set of historical/comparative phrases the state-description blocker rejects, plus the docstring-extraction and mention-span patterns for its Python docstring scan |
|
|
61
60
|
| `stuttering_check_config.py` | Config for the stuttering (repeated-phrase) check |
|
|
62
61
|
| `stuttering_import_binding_constants.py` | Import-binding patterns for the stuttering check |
|
|
63
62
|
| `subprocess_budget_completeness_constants.py` | Required argument names for the subprocess-budget completeness check |
|
|
@@ -28,7 +28,20 @@ MAX_THIN_WRAPPER_ISSUES: int = 1
|
|
|
28
28
|
MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
|
|
29
29
|
MAX_LOGGING_FSTRING_ISSUES: int = 3
|
|
30
30
|
MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
|
|
31
|
+
MAX_LOGGING_ADJACENT_LITERAL_ISSUES: int = 3
|
|
32
|
+
MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES: int = 3
|
|
31
33
|
MAX_WINDOWS_API_NONE_ISSUES: int = 3
|
|
34
|
+
MAX_NAIVE_DATETIME_ISSUES: int = 3
|
|
35
|
+
DATETIME_CLASS_ATTRIBUTE_NAME: str = "datetime"
|
|
36
|
+
NAIVE_DATETIME_TIMEZONE_KEYWORD: str = "tz"
|
|
37
|
+
NAIVE_DATETIME_UTCNOW_CONSTRUCTOR: str = "utcnow"
|
|
38
|
+
NAIVE_DATETIME_UTCFROMTIMESTAMP_CONSTRUCTOR: str = "utcfromtimestamp"
|
|
39
|
+
NAIVE_DATETIME_FROMTIMESTAMP_CONSTRUCTOR: str = "fromtimestamp"
|
|
40
|
+
FROMTIMESTAMP_POSITIONAL_TIMEZONE_ARGUMENT_COUNT: int = 2
|
|
41
|
+
ALL_ALWAYS_NAIVE_DATETIME_CONSTRUCTORS: tuple[str, ...] = (
|
|
42
|
+
NAIVE_DATETIME_UTCNOW_CONSTRUCTOR,
|
|
43
|
+
NAIVE_DATETIME_UTCFROMTIMESTAMP_CONSTRUCTOR,
|
|
44
|
+
)
|
|
32
45
|
MAX_E2E_TEST_NAMING_ISSUES: int = 3
|
|
33
46
|
MAX_IMPORT_BLOCK_SORT_ISSUES: int = 1
|
|
34
47
|
IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS: int = 15
|
|
@@ -48,6 +61,11 @@ ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
|
|
|
48
61
|
)
|
|
49
62
|
MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
|
|
50
63
|
MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES: int = 5
|
|
64
|
+
MAX_JS_SIBLING_RETURN_OBJECT_KEY_DRIFT_ISSUES: int = 5
|
|
65
|
+
MINIMUM_SIBLING_RETURN_OBJECT_KEYS: int = 2
|
|
66
|
+
SIBLING_RETURN_OBJECT_EXACT_MISSING_KEY_COUNT: int = 1
|
|
67
|
+
SIBLING_RETURN_OBJECT_SINGLE_AGREED_MISSING_KEY_COUNT: int = 1
|
|
68
|
+
MAX_JS_BARE_FLAG_RETURN_DIRECTIVE_ISSUES: int = 5
|
|
51
69
|
MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
|
|
52
70
|
DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
|
|
53
71
|
MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
|
|
@@ -59,6 +77,7 @@ MAX_STALE_TEST_NAME_TARGET_ISSUES: int = 3
|
|
|
59
77
|
STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT: int = 2
|
|
60
78
|
MAX_MODULE_DOCSTRING_CHECK_ROSTER_ISSUES: int = 5
|
|
61
79
|
MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER: int = 2
|
|
80
|
+
MINIMUM_SIBLING_OCCURRENCES_FOR_SHARED_TOKEN: int = 2
|
|
62
81
|
MAX_DOCSTRING_TUPLE_ENUMERATION_ISSUES: int = 5
|
|
63
82
|
MINIMUM_TUPLE_MEMBERS_FOR_DOCSTRING_ENUMERATION: int = 2
|
|
64
83
|
MAX_DOCSTRING_MARK_GLYPH_ENUMERATION_ISSUES: int = 5
|
|
@@ -199,6 +218,9 @@ DOCSTRING_RUNON_SENTENCE_WORD_LIMIT: int = 30
|
|
|
199
218
|
MAX_DOCSTRING_RUNON_SENTENCE_ISSUES: int = 5
|
|
200
219
|
ALL_DOCSTRING_RUNON_JOINER_MARKERS: tuple[str, ...] = ("—", " -- ", ";")
|
|
201
220
|
DOCSTRING_RUNON_SENTENCE_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"(?<=[.!?])\s+")
|
|
221
|
+
DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT: int = 6
|
|
222
|
+
DOCSTRING_NARRATIVE_LINE_JOIN_SEPARATOR: str = " "
|
|
223
|
+
MAX_DOCSTRING_PROSE_WALL_ISSUES: int = 5
|
|
202
224
|
|
|
203
225
|
ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
|
|
204
226
|
"no consumer reads",
|
|
@@ -290,6 +312,30 @@ ALL_TEST_INDICATING_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
|
290
312
|
}
|
|
291
313
|
)
|
|
292
314
|
|
|
315
|
+
MAX_DOCSTRING_DELEGATION_ENUMERATION_ISSUES: int = 5
|
|
316
|
+
MINIMUM_DELEGATION_ENUMERATION_ITEMS: int = 2
|
|
317
|
+
NEIGHBOR_SCAN_FILE_LIMIT: int = 40
|
|
318
|
+
POINTER_TO_DELEGATE_PATTERN: re.Pattern[str] = re.compile(
|
|
319
|
+
r"full doc(?:umentation)?\s+on\s+`{1,2}([A-Za-z_][A-Za-z0-9_]*)`{1,2}",
|
|
320
|
+
re.IGNORECASE,
|
|
321
|
+
)
|
|
322
|
+
TOKEN_WORD_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z][A-Za-z0-9]{2,}")
|
|
323
|
+
ALL_DELEGATION_ENUMERATION_STOP_TOKENS: frozenset[str] = frozenset(
|
|
324
|
+
{
|
|
325
|
+
"the",
|
|
326
|
+
"and",
|
|
327
|
+
"then",
|
|
328
|
+
"edit",
|
|
329
|
+
"edits",
|
|
330
|
+
"apply",
|
|
331
|
+
"applies",
|
|
332
|
+
"run",
|
|
333
|
+
"runs",
|
|
334
|
+
"step",
|
|
335
|
+
"steps",
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
|
|
293
339
|
MAX_MODULE_DOCSTRING_DATA_SCHEMA_SCOPE_ISSUES: int = 1
|
|
294
340
|
MODULE_DOCSTRING_DATA_SCHEMA_CONSTANT_SAMPLE_LIMIT: int = 6
|
|
295
341
|
ALL_USER_FACING_TEXT_SCOPE_DOCSTRING_PHRASES: tuple[str, ...] = (
|
|
@@ -185,7 +185,18 @@ LOGGING_FSTRING_PATTERN = re.compile(
|
|
|
185
185
|
LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
186
186
|
r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
|
|
187
187
|
)
|
|
188
|
+
ADJACENT_STRING_LITERAL_PATTERN: re.Pattern[str] = re.compile(
|
|
189
|
+
r'(?<!["\'])[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')'
|
|
190
|
+
r'\s*[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')(?!["\'])'
|
|
191
|
+
)
|
|
188
192
|
MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
|
|
193
|
+
ALL_LOGGING_CALL_METHOD_NAMES: frozenset[str] = frozenset(
|
|
194
|
+
{"debug", "info", "warning", "error", "critical", "exception"}
|
|
195
|
+
)
|
|
196
|
+
LOGGING_HELPER_FUNCTION_NAME_PATTERN: re.Pattern[str] = re.compile(
|
|
197
|
+
r"^log_(?:debug|info|warning|error|critical|exception)$"
|
|
198
|
+
)
|
|
199
|
+
LOGGING_RECEIVER_NAME_PATTERN: re.Pattern[str] = re.compile(r"^_?(?:logger|logging|log)$")
|
|
189
200
|
SPAWN_AGENT_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
|
|
190
201
|
r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
|
|
191
202
|
r"(?:async\s+)?function\s+spawn(?P<role>\w+?)Agent\s*\(",
|
|
@@ -233,9 +244,39 @@ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN: re.Pattern[str] = re.compile(
|
|
|
233
244
|
RETURN_CALL_OPENING_PARENTHESIS_PATTERN: re.Pattern[str] = re.compile(
|
|
234
245
|
r"\breturn\s+(?:await\s+)?(?P<callee>\w+)\s*\("
|
|
235
246
|
)
|
|
247
|
+
BARE_FLAG_CONTRACT_PATTERN: re.Pattern[str] = re.compile(
|
|
248
|
+
r"never a bare (?P<flag>\w+) flag", re.IGNORECASE
|
|
249
|
+
)
|
|
250
|
+
BARE_FLAG_RETURN_DIRECTIVE_PATTERN: re.Pattern[str] = re.compile(
|
|
251
|
+
r"\breturn\s+(?P<flag>\w+)\s*:\s*(?:true|false)\b", re.IGNORECASE
|
|
252
|
+
)
|
|
236
253
|
SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN: re.Pattern[str] = re.compile(
|
|
237
254
|
r"(?<![A-Za-z0-9_])schema\s*:"
|
|
238
255
|
)
|
|
256
|
+
RETURN_OBJECT_LITERAL_OPENING_PATTERN: re.Pattern[str] = re.compile(r"\breturn\s*\{")
|
|
257
|
+
ARROW_CONCISE_BODY_OBJECT_LITERAL_OPENING_PATTERN: re.Pattern[str] = re.compile(
|
|
258
|
+
r"=>\s*\(\s*\{"
|
|
259
|
+
)
|
|
260
|
+
JS_OBJECT_KEY_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z_$][\w$]*")
|
|
261
|
+
JS_OBJECT_METHOD_SHORTHAND_KEY_PATTERN: re.Pattern[str] = re.compile(
|
|
262
|
+
r"\A([A-Za-z_$][\w$]*)\s*\("
|
|
263
|
+
)
|
|
264
|
+
ALL_JAVASCRIPT_CONTROL_FLOW_BLOCK_KEYWORDS: frozenset[str] = frozenset(
|
|
265
|
+
{"if", "for", "await", "while", "switch", "catch", "with"}
|
|
266
|
+
)
|
|
267
|
+
ALL_JAVASCRIPT_PARENTHESIS_FREE_BLOCK_KEYWORDS: frozenset[str] = frozenset(
|
|
268
|
+
{"else", "do", "try", "finally"}
|
|
269
|
+
)
|
|
270
|
+
ALL_JAVASCRIPT_BRACKET_OPENERS: frozenset[str] = frozenset({"(", "[", "{"})
|
|
271
|
+
ALL_JAVASCRIPT_BRACKET_CLOSERS: frozenset[str] = frozenset({")", "]", "}"})
|
|
272
|
+
ALL_JAVASCRIPT_IDENTIFIER_EXTRA_CHARACTERS: frozenset[str] = frozenset({"_", "$"})
|
|
273
|
+
ALL_JAVASCRIPT_RETURN_TYPE_TERMINATORS: frozenset[str] = frozenset({"}", ">", "]"})
|
|
274
|
+
JAVASCRIPT_ARROW_TOKEN_LENGTH: int = 2
|
|
275
|
+
ALL_JAVASCRIPT_BARE_RETURN_TYPE_EXTRA_CHARACTERS: frozenset[str] = frozenset(
|
|
276
|
+
{"_", "$", ".", "|", "&", "?"}
|
|
277
|
+
)
|
|
278
|
+
JAVASCRIPT_OBJECT_SPREAD_PREFIX: str = "..."
|
|
279
|
+
JAVASCRIPT_MODULE_SCOPE_SENTINEL: int = -1
|
|
239
280
|
ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
|
|
240
281
|
"get", "items", "keys", "values", "update", "pop",
|
|
241
282
|
"setdefault", "copy", "clear",
|
|
@@ -22,8 +22,8 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
|
22
22
|
"pr_converge_skill_constants",
|
|
23
23
|
"pr_converge_scripts_constants",
|
|
24
24
|
"bugteam_scripts_constants",
|
|
25
|
-
"doc_gist_scripts_constants",
|
|
26
25
|
"implement_scripts_constants",
|
|
26
|
+
"log_audit_constants",
|
|
27
27
|
"dev_env_scripts_constants",
|
|
28
28
|
}
|
|
29
29
|
)
|
|
@@ -62,9 +62,4 @@ ALL_POST_HOSTED_HOOK_ENTRIES: tuple[PostHostedHookEntry, ...] = (
|
|
|
62
62
|
script_relative_path="workflow/auto_formatter.py",
|
|
63
63
|
is_blocking=False,
|
|
64
64
|
),
|
|
65
|
-
PostHostedHookEntry(
|
|
66
|
-
script_relative_path="workflow/doc_gist_auto_publish.py",
|
|
67
|
-
extra_argument_relative_paths=(PLUGIN_ROOT_PLACEHOLDER,),
|
|
68
|
-
is_blocking=False,
|
|
69
|
-
),
|
|
70
65
|
)
|
|
@@ -105,6 +105,10 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
105
105
|
applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
|
|
106
106
|
native_module_name=STATE_DESCRIPTION_BLOCKER_MODULE_NAME,
|
|
107
107
|
),
|
|
108
|
+
HostedHookEntry(
|
|
109
|
+
script_relative_path="blocking/stale_comment_reference_blocker.py",
|
|
110
|
+
applicable_tool_names=frozenset({EDIT_TOOL_NAME}),
|
|
111
|
+
),
|
|
108
112
|
HostedHookEntry(
|
|
109
113
|
script_relative_path="blocking/subprocess_budget_completeness.py",
|
|
110
114
|
applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Configuration constants for the reviewer_spawn_gate PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
BASH_TOOL_NAME: str = "Bash"
|
|
9
|
+
|
|
10
|
+
GATE_SENTINEL_MARKER: str = "CLAUDE_REVIEWER_GATE=autoconverge"
|
|
11
|
+
|
|
12
|
+
COPILOT_REVIEWER_TOKEN: str = "copilot"
|
|
13
|
+
BUGBOT_REVIEWER_TOKEN: str = "bugbot"
|
|
14
|
+
|
|
15
|
+
COPILOT_REVIEWER_LABEL: str = "GitHub Copilot"
|
|
16
|
+
BUGBOT_REVIEWER_LABEL: str = "Cursor Bugbot"
|
|
17
|
+
|
|
18
|
+
ALL_COPILOT_TRIGGER_MARKERS: tuple[str, ...] = (
|
|
19
|
+
"requested_reviewers",
|
|
20
|
+
"copilot-pull-request-reviewer[bot]",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
BUGBOT_TRIGGER_SCRIPT_MARKER: str = "post_fix_reply.py"
|
|
24
|
+
BUGBOT_RUN_BODY_PATTERN: re.Pattern[str] = re.compile(
|
|
25
|
+
r'--body\s+["\']bugbot run["\']', re.IGNORECASE
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
AVAILABILITY_SCRIPT_RELATIVE_PATH: Path = (
|
|
29
|
+
Path("_shared") / "pr-loop" / "scripts" / "reviewer_availability.py"
|
|
30
|
+
)
|
|
31
|
+
AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME: str = "REVIEWER_SPAWN_GATE_AVAILABILITY_SCRIPT_PATH"
|
|
32
|
+
AVAILABILITY_SCRIPT_TIMEOUT_SECONDS: int = 15
|
|
33
|
+
AVAILABILITY_REVIEWER_FLAG: str = "--reviewer"
|
|
34
|
+
AVAILABILITY_DOWN_EXIT_CODE: int = 3
|
|
35
|
+
|
|
36
|
+
DENY_REASON_TEMPLATE: str = (
|
|
37
|
+
"BLOCKED [reviewer-spawn-gate]: {reviewer_label} is down or out of quota "
|
|
38
|
+
"this run. Skip requesting {reviewer_label} on this trigger — "
|
|
39
|
+
"autoconverge requests it again once the availability check reports it "
|
|
40
|
+
"back.\n\nAvailability check output: {availability_detail}"
|
|
41
|
+
)
|