claude-dev-env 1.82.0 → 1.83.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/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- 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/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -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_terminology_sweep.py +14 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_constants_config.py +159 -1
- package/hooks/blocking/code_rules_docstrings.py +312 -9
- package/hooks/blocking/code_rules_enforcer.py +29 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_naming_collection.py +141 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- 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_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_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_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- 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 -1
- package/hooks/hooks_constants/blocking_check_limits.py +43 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- 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/package.json +1 -1
- package/rules/CLAUDE.md +4 -1
- 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 +5 -0
- 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 +1 -1
- package/skills/autoconverge/SKILL.md +10 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +69 -0
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +54 -18
- package/skills/autoconverge/workflow/converge.mjs +97 -33
- package/skills/everything-search/SKILL.md +5 -0
package/CLAUDE.md
CHANGED
|
@@ -115,30 +115,33 @@ Before any coding task, call the `initial_instructions` tool to load the Serena
|
|
|
115
115
|
|
|
116
116
|
### Tool hierarchy for code navigation
|
|
117
117
|
1. **Serena** — symbol-level navigation (declarations, references, implementations, rename)
|
|
118
|
-
2. **
|
|
118
|
+
2. **es.exe** — file-system search by name/path/extension/size/date (Everything CLI)
|
|
119
119
|
3. **Grep/Glob** — content and pattern matching
|
|
120
120
|
|
|
121
|
-
## Everything Search (
|
|
121
|
+
## Everything Search (es.exe CLI)
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
The `everything_search` MCP tool is available in every session.
|
|
123
|
+
Search files by name, path, extension, size, or date with the Everything command-line tool `es.exe`. It reads the same live index the desktop app keeps, so results return instantly.
|
|
125
124
|
|
|
126
|
-
###
|
|
127
|
-
|
|
125
|
+
### Invocation and scope tokens
|
|
126
|
+
Run `es.exe` with a scoped query — a project path, an `ext:`/`dm:`/`size:` filter, or a name pattern. The `es_exe_path_rewriter` hook resolves a `{project-name}` placeholder or a bare registry key from `~/.claude/project-paths.json` into its quoted absolute path before the command runs (it allows and rewrites, never blocks).
|
|
127
|
+
|
|
128
|
+
### Hard limits
|
|
129
|
+
- Scope every search. A bare whole-drive scan or a network-share sweep is out of bounds.
|
|
128
130
|
|
|
129
131
|
### Fallback order
|
|
130
|
-
1. **
|
|
131
|
-
2. **
|
|
132
|
-
3. **
|
|
132
|
+
1. **es.exe** — file-system search by name/path/extension/size/date
|
|
133
|
+
2. **Debug** — try to find out why es.exe isn't working, and then prompt user for next-steps if you can't self-heal.
|
|
134
|
+
3. **Grep** — file-content search (Grep owns content)
|
|
135
|
+
4. **Glob** — relative-path pattern matching within the current project
|
|
133
136
|
|
|
134
137
|
### Search syntax quick reference
|
|
135
|
-
- `ext:py` — find by extension (multiple: `ext:ts
|
|
138
|
+
- `ext:py` — find by extension (multiple: `ext:ts | ext:js`)
|
|
136
139
|
- `path:src\components` — match against full path
|
|
137
|
-
- `count:10` — limit number of results to 10
|
|
138
140
|
- `*.config.*` — wildcards
|
|
139
141
|
- `size:>10mb` — size filter
|
|
140
142
|
- `dm:today` / `dm:thisweek` — date modified filter
|
|
141
|
-
- `
|
|
142
|
-
- `parent:node_modules package.json` — match parent folder
|
|
143
|
+
- `-n 50` — limit results; `-sort dm` — sort by date modified
|
|
143
144
|
- `foo bar` — AND, `foo | bar` — OR, `!foo` — NOT
|
|
144
145
|
- `"exact phrase"` — literal match
|
|
146
|
+
|
|
147
|
+
Full operator reference: `skills/everything-search/SKILL.md`.
|
|
@@ -15,6 +15,7 @@ Python scripts invoked at runtime by the PR-loop skills. Each script is a standa
|
|
|
15
15
|
| `terminology_sweep.py` | Flags a prose term that near-misses an identifier introduced on added code lines of a unified diff (shared leading word, divergent tail) |
|
|
16
16
|
| `reviews_disabled.py` | Shared helper for the `CLAUDE_REVIEWS_DISABLED` opt-out gate; parses the env-var token to find which reviewer types are suppressed |
|
|
17
17
|
| `copilot_quota.py` | Copilot premium-request quota pre-check: resolves a configured GitHub account, reads its remaining `premium_interactions` quota via `gh api copilot_internal/user`, and exits 0 (run Copilot) or non-zero (skip: out of quota, API down, or no account configured) |
|
|
18
|
+
| `reviewer_availability.py` | Unified reviewer-availability entry point for Copilot and Bugbot: reuses `copilot_quota.py` and `reviews_disabled.py` and exits 0 when the named `--reviewer` may be spawned, non-zero when it is opted out or (for Copilot) out of quota |
|
|
18
19
|
| `fix_hookspath.py` | Repairs a malformed `core.hooksPath` global git config entry |
|
|
19
20
|
| `_claude_permissions_common.py` | Internal helpers shared by the grant/revoke scripts: atomic settings.json writes, list mutation, path helpers |
|
|
20
21
|
|
|
@@ -7,6 +7,7 @@ Runnable helpers used by **bugteam**, **qbug**, **pr-converge**, and related ski
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
| --- | --- |
|
|
9
9
|
| `preflight.py` | Local checks before a PR-loop run (pytest discovery, optional pre-commit, hooksPath sanity). |
|
|
10
|
+
| `reviewer_availability.py` | Unified availability check for Copilot and Bugbot; reuses `copilot_quota.py` and `reviews_disabled.py` and exits 0 when the named `--reviewer` may be spawned. |
|
|
10
11
|
| `code_rules_gate.py` | CODE_RULES gate over PR-scoped diffs (`--base`, staged-only, path filters). |
|
|
11
12
|
| `fix_hookspath.py` | Repair `core.hooksPath` when it does not point at the packaged git-hooks tree. |
|
|
12
13
|
| `grant_project_claude_permissions.py` / `revoke_project_claude_permissions.py` | Claude Code permission JSON helpers used during publish-style flows. |
|
|
@@ -17,6 +17,7 @@ Named constants for every script in `_shared/pr-loop/scripts/`. Each module owns
|
|
|
17
17
|
| `reviews_disabled_constants.py` | `reviews_disabled.py` — `CLAUDE_REVIEWS_DISABLED` token taxonomy |
|
|
18
18
|
| `copilot_quota_constants.py` | `copilot_quota.py` — `COPILOT_QUOTA_ACCOUNT` env-var name, `copilot_internal/user` API path, the `premium_interactions` gating field names, the four skip/run exit codes, and the default `.env` path |
|
|
19
19
|
| `terminology_sweep_constants.py` | `terminology_sweep.py` — identifier and prose-token regexes, diff-parsing prefixes, code-file extensions, and the finding-message template |
|
|
20
|
+
| `reviewer_availability_constants.py` | `reviewer_availability.py` — the available/down exit codes for the unified reviewer-availability pre-check |
|
|
20
21
|
| `__init__.py` | Empty package marker |
|
|
21
22
|
|
|
22
23
|
## Convention
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Configuration constants for the unified reviewer-availability pre-check.
|
|
2
|
+
|
|
3
|
+
The down code is 3 so a deliberate reviewer-down report stays distinguishable
|
|
4
|
+
from a generic interpreter crash (exit 1) and an argparse usage error
|
|
5
|
+
(exit 2). A caller that gates on the down code treats any other non-zero exit
|
|
6
|
+
as a broken check, not a down reviewer, and fails open.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
EXIT_CODE_REVIEWER_AVAILABLE: int = 0
|
|
12
|
+
EXIT_CODE_REVIEWER_DOWN: int = 3
|
|
@@ -24,8 +24,6 @@ HYPHENATED_PROSE_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
|
24
24
|
r"\b[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)+\b"
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
PROSE_WORD_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z][A-Za-z0-9]*")
|
|
28
|
-
|
|
29
27
|
STRING_LITERAL_CONTENT_PATTERN: re.Pattern[str] = re.compile(
|
|
30
28
|
r"\"([^\"]*)\"|'([^']*)'|`([^`]*)`"
|
|
31
29
|
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Unified reviewer-availability pre-check for GitHub Copilot and Cursor Bugbot.
|
|
2
|
+
|
|
3
|
+
Run this before either reviewer step spawns. It answers one question: is the
|
|
4
|
+
named reviewer available to run right now. It reuses two existing checks
|
|
5
|
+
instead of re-implementing their rules. The ``CLAUDE_REVIEWS_DISABLED``
|
|
6
|
+
opt-out gate lives in ``reviews_disabled.py``. The Copilot premium-interaction
|
|
7
|
+
quota pre-check lives in ``copilot_quota.py``.
|
|
8
|
+
|
|
9
|
+
Copilot counts as down in two cases: it is opted out via
|
|
10
|
+
``CLAUDE_REVIEWS_DISABLED``, or its quota pre-check returns anything other
|
|
11
|
+
than quota available (out of quota, the quota API or account down, or no
|
|
12
|
+
account configured).
|
|
13
|
+
|
|
14
|
+
Bugbot carries no quota or availability API. It counts as down only when it
|
|
15
|
+
is opted out via ``CLAUDE_REVIEWS_DISABLED``. A genuine runtime outage shows
|
|
16
|
+
up later as a poll timeout, not here.
|
|
17
|
+
|
|
18
|
+
The exit code tells the caller what to do. Exit 0 means the reviewer is
|
|
19
|
+
available and may be spawned. The documented down code (3) means skip it. Any
|
|
20
|
+
other non-zero exit is a broken check — an interpreter crash or a usage
|
|
21
|
+
error — not a down report, so a gating caller fails open on it. Every path
|
|
22
|
+
prints one line: available lines go to stdout, down lines go to stderr.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import sys
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from copilot_quota import (
|
|
33
|
+
COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH,
|
|
34
|
+
EXIT_CODE_QUOTA_AVAILABLE,
|
|
35
|
+
evaluate_copilot_quota,
|
|
36
|
+
)
|
|
37
|
+
from pr_loop_shared_constants.reviewer_availability_constants import (
|
|
38
|
+
EXIT_CODE_REVIEWER_AVAILABLE,
|
|
39
|
+
EXIT_CODE_REVIEWER_DOWN,
|
|
40
|
+
)
|
|
41
|
+
from pr_loop_shared_constants.reviews_disabled_constants import (
|
|
42
|
+
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
|
|
43
|
+
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
44
|
+
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
|
|
45
|
+
)
|
|
46
|
+
from reviews_disabled import is_bugbot_disabled_via_env, is_copilot_disabled_via_env
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ReviewerAvailability:
|
|
51
|
+
"""One reviewer-availability outcome: the exit code and the single log line.
|
|
52
|
+
|
|
53
|
+
The exit code tells the caller what to do, where 0 means the reviewer may
|
|
54
|
+
be spawned and the documented down code (3) means skip it. The CLI prints
|
|
55
|
+
the message on one line, to stdout when available and to stderr when down.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
exit_code: int
|
|
59
|
+
message: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _evaluate_copilot_availability(env_file_path: Path) -> ReviewerAvailability:
|
|
63
|
+
"""Decide whether GitHub Copilot is available to spawn.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
env_file_path: The ``.env`` file the quota pre-check consults for the
|
|
67
|
+
configured account when neither the flag nor the environment
|
|
68
|
+
variable names one.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A ReviewerAvailability that is down when Copilot is opted out via
|
|
72
|
+
``CLAUDE_REVIEWS_DISABLED`` or the quota pre-check reports anything
|
|
73
|
+
other than quota available, and available otherwise.
|
|
74
|
+
"""
|
|
75
|
+
if is_copilot_disabled_via_env():
|
|
76
|
+
return ReviewerAvailability(
|
|
77
|
+
EXIT_CODE_REVIEWER_DOWN,
|
|
78
|
+
f"reviewer-availability: copilot is disabled via "
|
|
79
|
+
f"{CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME} — skipping.",
|
|
80
|
+
)
|
|
81
|
+
quota_decision = evaluate_copilot_quota(cli_account=None, env_file_path=env_file_path)
|
|
82
|
+
if quota_decision.exit_code != EXIT_CODE_QUOTA_AVAILABLE:
|
|
83
|
+
return ReviewerAvailability(EXIT_CODE_REVIEWER_DOWN, quota_decision.message)
|
|
84
|
+
return ReviewerAvailability(EXIT_CODE_REVIEWER_AVAILABLE, quota_decision.message)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _evaluate_bugbot_availability() -> ReviewerAvailability:
|
|
88
|
+
"""Decide whether Cursor Bugbot is available to spawn.
|
|
89
|
+
|
|
90
|
+
Bugbot carries no quota or availability API, so this checks only the
|
|
91
|
+
deterministic ``CLAUDE_REVIEWS_DISABLED`` opt-out.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A ReviewerAvailability that is down when Bugbot is opted out via
|
|
95
|
+
``CLAUDE_REVIEWS_DISABLED``, and available otherwise.
|
|
96
|
+
"""
|
|
97
|
+
if is_bugbot_disabled_via_env():
|
|
98
|
+
return ReviewerAvailability(
|
|
99
|
+
EXIT_CODE_REVIEWER_DOWN,
|
|
100
|
+
f"reviewer-availability: bugbot is disabled via "
|
|
101
|
+
f"{CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME} — skipping.",
|
|
102
|
+
)
|
|
103
|
+
return ReviewerAvailability(
|
|
104
|
+
EXIT_CODE_REVIEWER_AVAILABLE,
|
|
105
|
+
"reviewer-availability: bugbot is available.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def evaluate_reviewer_availability(
|
|
110
|
+
reviewer_token: str, env_file_path: Path
|
|
111
|
+
) -> ReviewerAvailability:
|
|
112
|
+
"""Decide whether the named reviewer is available to spawn.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
reviewer_token: Either the Copilot or the Bugbot reviewer token.
|
|
116
|
+
env_file_path: The ``.env`` file the Copilot quota pre-check consults
|
|
117
|
+
for the configured account.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The ReviewerAvailability for the named reviewer.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: When reviewer_token names neither the Copilot nor the
|
|
124
|
+
Bugbot reviewer token.
|
|
125
|
+
"""
|
|
126
|
+
if reviewer_token == CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN:
|
|
127
|
+
return _evaluate_copilot_availability(env_file_path)
|
|
128
|
+
if reviewer_token == CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN:
|
|
129
|
+
return _evaluate_bugbot_availability()
|
|
130
|
+
raise ValueError(f"unknown reviewer token: {reviewer_token}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _parse_arguments(all_argv: list[str]) -> argparse.Namespace:
|
|
134
|
+
"""Parse command-line arguments for the reviewer-availability check.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
all_argv: Argument list excluding the program name, typically
|
|
138
|
+
``sys.argv[1:]``.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Namespace exposing a ``reviewer`` attribute constrained to the
|
|
142
|
+
Copilot and Bugbot reviewer tokens.
|
|
143
|
+
"""
|
|
144
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--reviewer",
|
|
147
|
+
required=True,
|
|
148
|
+
choices=[
|
|
149
|
+
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
|
|
150
|
+
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
|
|
151
|
+
],
|
|
152
|
+
help="Reviewer to check availability for",
|
|
153
|
+
)
|
|
154
|
+
return parser.parse_args(all_argv)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(all_arguments: list[str]) -> int:
|
|
158
|
+
"""Run the reviewer-availability check end-to-end and print its decision.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
all_arguments: Argument list excluding the program name.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
0 when the named reviewer is available to spawn, the documented
|
|
165
|
+
reviewer-down exit code when it is down.
|
|
166
|
+
"""
|
|
167
|
+
arguments = _parse_arguments(all_arguments)
|
|
168
|
+
availability = evaluate_reviewer_availability(
|
|
169
|
+
reviewer_token=arguments.reviewer,
|
|
170
|
+
env_file_path=COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH,
|
|
171
|
+
)
|
|
172
|
+
message_stream = (
|
|
173
|
+
sys.stdout
|
|
174
|
+
if availability.exit_code == EXIT_CODE_REVIEWER_AVAILABLE
|
|
175
|
+
else sys.stderr
|
|
176
|
+
)
|
|
177
|
+
print(availability.message, file=message_stream)
|
|
178
|
+
return availability.exit_code
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -7,11 +7,18 @@ finds the other.
|
|
|
7
7
|
|
|
8
8
|
This sweep reads a unified diff and collects every multi-word identifier added
|
|
9
9
|
on code lines. Then it scans each added prose line — a Markdown line, or a
|
|
10
|
-
comment, docstring, or string inside a code file. It flags a
|
|
11
|
-
|
|
10
|
+
comment, docstring, or string inside a code file. It flags a hyphenated term
|
|
11
|
+
that shares an identifier's leading word but diverges in the tail. Each
|
|
12
12
|
near-miss prints as one ``file:line`` finding, and the run exits non-zero when
|
|
13
13
|
any finding remains, so a commit gate can block on it.
|
|
14
14
|
|
|
15
|
+
Only hyphenated terms are candidates. A hyphen marks a deliberate compound —
|
|
16
|
+
the author bound the words into one term — so a divergent tail there is a real
|
|
17
|
+
naming drift. A bare spaced word run is ordinary prose: windowing every
|
|
18
|
+
sentence that starts with an identifier's first word ("each attempt", "file
|
|
19
|
+
exists") floods a vocabulary-dense change with false findings and gives the
|
|
20
|
+
gate nothing actionable.
|
|
21
|
+
|
|
15
22
|
A shared leading token alone is too weak a signal: ordinary English compounds
|
|
16
23
|
such as ``read-only`` and ``data-driven`` collide with unrelated identifiers
|
|
17
24
|
(``read_config``, ``data_source``) and would block a commit falsely. To keep the
|
|
@@ -56,7 +63,6 @@ from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
|
|
|
56
63
|
JSDOC_CONTINUATION_MARKER,
|
|
57
64
|
MARKDOWN_FILE_EXTENSION,
|
|
58
65
|
MINIMUM_IDENTIFIER_TOKEN_COUNT,
|
|
59
|
-
PROSE_WORD_PATTERN,
|
|
60
66
|
PYTHON_COMMENT_MARKER,
|
|
61
67
|
SNAKE_CASE_IDENTIFIER_PATTERN,
|
|
62
68
|
STRING_LITERAL_CONTENT_PATTERN,
|
|
@@ -234,35 +240,6 @@ def _hyphenated_candidates(fragment: str) -> list[tuple[str, IdentifierTuple]]:
|
|
|
234
240
|
return all_candidates
|
|
235
241
|
|
|
236
242
|
|
|
237
|
-
def _spaced_candidates(
|
|
238
|
-
fragment: str,
|
|
239
|
-
identifiers_by_first_token: dict[str, list[IdentifierTuple]],
|
|
240
|
-
) -> list[tuple[str, IdentifierTuple]]:
|
|
241
|
-
"""Return spaced word windows whose first word matches an identifier prefix.
|
|
242
|
-
|
|
243
|
-
Only a window anchored at a known identifier prefix and sized to one of that
|
|
244
|
-
prefix's identifiers is returned, which bounds the windows to the shapes the
|
|
245
|
-
near-miss check can act on.
|
|
246
|
-
|
|
247
|
-
Args:
|
|
248
|
-
fragment: The prose fragment to scan.
|
|
249
|
-
identifiers_by_first_token: Identifier tuples grouped by leading token.
|
|
250
|
-
|
|
251
|
-
Returns:
|
|
252
|
-
``(display, token_tuple)`` pairs for each candidate window.
|
|
253
|
-
"""
|
|
254
|
-
all_words = [
|
|
255
|
-
each_word.lower() for each_word in PROSE_WORD_PATTERN.findall(fragment)
|
|
256
|
-
]
|
|
257
|
-
all_candidates: list[tuple[str, IdentifierTuple]] = []
|
|
258
|
-
for each_index, each_word in enumerate(all_words):
|
|
259
|
-
for each_identifier in identifiers_by_first_token.get(each_word, []):
|
|
260
|
-
window = tuple(all_words[each_index : each_index + len(each_identifier)])
|
|
261
|
-
if len(window) == len(each_identifier):
|
|
262
|
-
all_candidates.append((" ".join(window), window))
|
|
263
|
-
return all_candidates
|
|
264
|
-
|
|
265
|
-
|
|
266
243
|
def _tokens_are_plural_variants(first_word: str, second_word: str) -> bool:
|
|
267
244
|
"""Return whether two words are singular/plural forms of one word.
|
|
268
245
|
|
|
@@ -348,7 +325,6 @@ def _findings_for_line(
|
|
|
348
325
|
reported_tuples: set[IdentifierTuple] = set()
|
|
349
326
|
for each_fragment in _prose_fragments(file_path, line_text):
|
|
350
327
|
all_candidates = _hyphenated_candidates(each_fragment)
|
|
351
|
-
all_candidates += _spaced_candidates(each_fragment, identifiers_by_first_token)
|
|
352
328
|
for each_display, each_tuple in all_candidates:
|
|
353
329
|
matched_identifier = _near_miss_identifier(
|
|
354
330
|
each_tuple, all_identifier_tuples, identifiers_by_first_token
|
|
@@ -24,6 +24,8 @@ pytest suite for the scripts and constants in `_shared/pr-loop/scripts/`. Each t
|
|
|
24
24
|
| `test_reviews_disabled.py` | `reviews_disabled.py` opt-out gate parsing |
|
|
25
25
|
| `test_copilot_quota.py` | `copilot_quota.py` end-to-end: account resolution, premium-quota classification, exit codes, and skip logging |
|
|
26
26
|
| `test_copilot_quota_constants.py` | `pr_loop_shared_constants/copilot_quota_constants.py` |
|
|
27
|
+
| `test_reviewer_availability.py` | `reviewer_availability.py` end-to-end: Copilot and Bugbot availability, opt-out via `CLAUDE_REVIEWS_DISABLED`, and every Copilot quota outcome |
|
|
28
|
+
| `test_reviewer_availability_constants.py` | `pr_loop_shared_constants/reviewer_availability_constants.py` |
|
|
27
29
|
| `test_revoke_project_claude_permissions.py` | `revoke_project_claude_permissions.py` end-to-end |
|
|
28
30
|
| `test_agent_config_carveout.py` | Agent-config deny-rule carve-out logic |
|
|
29
31
|
| `conftest.py` | Shared pytest fixtures |
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Direct unit tests for the unified reviewer_availability pre-check.
|
|
2
|
+
|
|
3
|
+
Every case drives the production ``main`` path through
|
|
4
|
+
``evaluate_reviewer_availability``. Copilot cases stub only the ``gh``
|
|
5
|
+
subprocess boundary inside ``copilot_quota`` (``_run_gh``), reusing the same
|
|
6
|
+
captured fixture ``test_copilot_quota.py`` drives. Opt-out cases set or clear
|
|
7
|
+
``CLAUDE_REVIEWS_DISABLED`` directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import importlib.util
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_module(module_name: str) -> ModuleType:
|
|
20
|
+
scripts_directory = Path(__file__).parent.parent
|
|
21
|
+
if str(scripts_directory) not in sys.path:
|
|
22
|
+
sys.path.insert(0, str(scripts_directory))
|
|
23
|
+
module_path = scripts_directory / f"{module_name}.py"
|
|
24
|
+
specification = importlib.util.spec_from_file_location(module_name, module_path)
|
|
25
|
+
assert specification is not None
|
|
26
|
+
assert specification.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(specification)
|
|
28
|
+
sys.modules[specification.name] = module
|
|
29
|
+
specification.loader.exec_module(module)
|
|
30
|
+
return module
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
copilot_quota = _load_module("copilot_quota")
|
|
34
|
+
reviewer_availability = _load_module("reviewer_availability")
|
|
35
|
+
|
|
36
|
+
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "copilot_internal_user_jonecho.json"
|
|
37
|
+
AVAILABLE_USER_JSON = FIXTURE_PATH.read_text(encoding="utf-8")
|
|
38
|
+
FAKE_TOKEN_RESULT = (0, "ghp_faketoken_value\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _exhausted_user_json() -> str:
|
|
42
|
+
user = json.loads(AVAILABLE_USER_JSON)
|
|
43
|
+
premium = user["quota_snapshots"]["premium_interactions"]
|
|
44
|
+
premium["remaining"] = 0
|
|
45
|
+
premium["quota_remaining"] = 0.0
|
|
46
|
+
premium["percent_remaining"] = 0.0
|
|
47
|
+
return json.dumps(user)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _gh_stub(token_result: tuple[int, str], api_result: tuple[int, str]):
|
|
51
|
+
def _fake_run_gh(
|
|
52
|
+
command_arguments: list[str],
|
|
53
|
+
extra_environment: dict[str, str] | None = None,
|
|
54
|
+
) -> tuple[int, str]:
|
|
55
|
+
if command_arguments and command_arguments[0] == "auth":
|
|
56
|
+
return token_result
|
|
57
|
+
if command_arguments and command_arguments[0] == "api":
|
|
58
|
+
return api_result
|
|
59
|
+
raise AssertionError(f"unexpected gh command {command_arguments}")
|
|
60
|
+
|
|
61
|
+
return _fake_run_gh
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.fixture(autouse=True)
|
|
65
|
+
def _isolate_reviewer_environment(
|
|
66
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
67
|
+
) -> None:
|
|
68
|
+
monkeypatch.delenv("COPILOT_QUOTA_ACCOUNT", raising=False)
|
|
69
|
+
monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
|
|
70
|
+
monkeypatch.setattr(
|
|
71
|
+
reviewer_availability, "COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH", tmp_path / ".env"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_main_reports_copilot_available_when_quota_confirmed_available(
|
|
76
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
77
|
+
) -> None:
|
|
78
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
79
|
+
monkeypatch.setattr(
|
|
80
|
+
copilot_quota, "_run_gh", _gh_stub(FAKE_TOKEN_RESULT, (0, AVAILABLE_USER_JSON))
|
|
81
|
+
)
|
|
82
|
+
exit_code = reviewer_availability.main(["--reviewer", "copilot"])
|
|
83
|
+
captured = capsys.readouterr()
|
|
84
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_AVAILABLE
|
|
85
|
+
assert "running Copilot" in captured.out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_main_reports_copilot_down_when_opted_out_via_env(
|
|
89
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
90
|
+
) -> None:
|
|
91
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
|
|
92
|
+
|
|
93
|
+
def _fail_if_called(
|
|
94
|
+
command_arguments: list[str],
|
|
95
|
+
extra_environment: dict[str, str] | None = None,
|
|
96
|
+
) -> tuple[int, str]:
|
|
97
|
+
raise AssertionError("gh must not run once copilot is opted out")
|
|
98
|
+
|
|
99
|
+
monkeypatch.setattr(copilot_quota, "_run_gh", _fail_if_called)
|
|
100
|
+
exit_code = reviewer_availability.main(["--reviewer", "copilot"])
|
|
101
|
+
captured = capsys.readouterr()
|
|
102
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
|
|
103
|
+
assert "CLAUDE_REVIEWS_DISABLED" in captured.err
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_main_reports_copilot_down_when_out_of_quota(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
108
|
+
) -> None:
|
|
109
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
copilot_quota,
|
|
112
|
+
"_run_gh",
|
|
113
|
+
_gh_stub(FAKE_TOKEN_RESULT, (0, _exhausted_user_json())),
|
|
114
|
+
)
|
|
115
|
+
exit_code = reviewer_availability.main(["--reviewer", "copilot"])
|
|
116
|
+
captured = capsys.readouterr()
|
|
117
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
|
|
118
|
+
assert "scenario A" in captured.err
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_main_reports_copilot_down_when_quota_api_is_down(
|
|
122
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
123
|
+
) -> None:
|
|
124
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
125
|
+
monkeypatch.setattr(
|
|
126
|
+
copilot_quota, "_run_gh", _gh_stub(FAKE_TOKEN_RESULT, (0, "not json at all"))
|
|
127
|
+
)
|
|
128
|
+
exit_code = reviewer_availability.main(["--reviewer", "copilot"])
|
|
129
|
+
captured = capsys.readouterr()
|
|
130
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
|
|
131
|
+
assert "scenario B" in captured.err
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_main_reports_copilot_down_when_no_account_configured(
|
|
135
|
+
capsys: pytest.CaptureFixture[str],
|
|
136
|
+
) -> None:
|
|
137
|
+
exit_code = reviewer_availability.main(["--reviewer", "copilot"])
|
|
138
|
+
captured = capsys.readouterr()
|
|
139
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
|
|
140
|
+
assert "scenario C" in captured.err
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_main_reports_bugbot_available_when_not_opted_out(
|
|
144
|
+
capsys: pytest.CaptureFixture[str],
|
|
145
|
+
) -> None:
|
|
146
|
+
exit_code = reviewer_availability.main(["--reviewer", "bugbot"])
|
|
147
|
+
captured = capsys.readouterr()
|
|
148
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_AVAILABLE
|
|
149
|
+
assert "bugbot" in captured.out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_main_reports_bugbot_down_when_opted_out_via_env(
|
|
153
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
154
|
+
) -> None:
|
|
155
|
+
monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
|
|
156
|
+
exit_code = reviewer_availability.main(["--reviewer", "bugbot"])
|
|
157
|
+
captured = capsys.readouterr()
|
|
158
|
+
assert exit_code == reviewer_availability.EXIT_CODE_REVIEWER_DOWN
|
|
159
|
+
assert "CLAUDE_REVIEWS_DISABLED" in captured.err
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Tests for reviewer_availability_constants.py extracted constant set."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_constants_module() -> ModuleType:
|
|
9
|
+
module_path = (
|
|
10
|
+
Path(__file__).parent.parent
|
|
11
|
+
/ "pr_loop_shared_constants"
|
|
12
|
+
/ "reviewer_availability_constants.py"
|
|
13
|
+
)
|
|
14
|
+
specification = importlib.util.spec_from_file_location(
|
|
15
|
+
"pr_loop_shared_constants.reviewer_availability_constants", module_path
|
|
16
|
+
)
|
|
17
|
+
assert specification is not None
|
|
18
|
+
assert specification.loader is not None
|
|
19
|
+
module = importlib.util.module_from_spec(specification)
|
|
20
|
+
specification.loader.exec_module(module)
|
|
21
|
+
return module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
constants_module = _load_constants_module()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_available_exit_code_matches_the_posix_success_convention() -> None:
|
|
28
|
+
assert constants_module.EXIT_CODE_REVIEWER_AVAILABLE == 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_down_exit_code_is_distinct_from_the_available_exit_code() -> None:
|
|
32
|
+
assert (
|
|
33
|
+
constants_module.EXIT_CODE_REVIEWER_DOWN
|
|
34
|
+
!= constants_module.EXIT_CODE_REVIEWER_AVAILABLE
|
|
35
|
+
)
|
|
36
|
+
assert constants_module.EXIT_CODE_REVIEWER_DOWN != 0
|
|
@@ -191,7 +191,7 @@ def test_does_not_flag_y_to_ies_plural_variant() -> None:
|
|
|
191
191
|
assert sweep_diff(diff) == []
|
|
192
192
|
|
|
193
193
|
|
|
194
|
-
def
|
|
194
|
+
def test_does_not_flag_spaced_prose_sharing_identifier_prefix() -> None:
|
|
195
195
|
diff = (
|
|
196
196
|
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
197
197
|
"--- a/api/quota.py\n"
|
|
@@ -204,9 +204,19 @@ def test_still_flags_non_plural_divergent_tail() -> None:
|
|
|
204
204
|
"@@ -0,0 +1,1 @@\n"
|
|
205
205
|
"+The premium request budget gates the run.\n"
|
|
206
206
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
assert sweep_diff(diff) == []
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_does_not_flag_ordinary_sentence_sharing_loop_variable_prefix() -> None:
|
|
211
|
+
diff = (
|
|
212
|
+
"diff --git a/api/scan.py b/api/scan.py\n"
|
|
213
|
+
"--- a/api/scan.py\n"
|
|
214
|
+
"+++ b/api/scan.py\n"
|
|
215
|
+
"@@ -0,0 +1,2 @@\n"
|
|
216
|
+
"+each_node = walk(tree)\n"
|
|
217
|
+
'+DESCRIPTION = "Each attempt polls the review endpoint once."\n'
|
|
218
|
+
)
|
|
219
|
+
assert sweep_diff(diff) == []
|
|
210
220
|
|
|
211
221
|
|
|
212
222
|
def test_flags_near_miss_inside_code_comment() -> None:
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -86,9 +86,11 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
86
86
|
| `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
|
|
87
87
|
| `pytest_testpaths_orphan_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | New `test_*.py` files created under a directory absent from a package's explicit pytest `testpaths` allowlist |
|
|
88
88
|
| `question_to_user_enforcer.py` | Stop | User-directed questions not routed through `AskUserQuestion` |
|
|
89
|
+
| `reviewer_spawn_gate.py` | PreToolUse (Bash) | A sentinel-marked autoconverge reviewer-spawn command (Copilot review request, Bugbot rerun comment) run while `reviewer_availability.py` reports that reviewer down or out of quota |
|
|
89
90
|
| `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file locally via `Show-Asset.ps1` |
|
|
90
91
|
| `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
|
|
91
92
|
| `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
|
|
93
|
+
| `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
|
|
92
94
|
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
|
|
93
95
|
| `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
|
|
94
96
|
| `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
|