claude-dev-env 1.74.0 → 1.76.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/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +36 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
- package/hooks/blocking/hedging_language_blocker.py +1 -13
- package/hooks/blocking/intent_only_ending_blocker.py +1 -15
- package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
- package/hooks/blocking/question_to_user_enforcer.py +1 -11
- package/hooks/blocking/session_handoff_blocker.py +1 -15
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
- package/hooks/blocking/test_hedging_language_blocker.py +6 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
- package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
- package/hooks/blocking/test_session_handoff_blocker.py +6 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/blocking_check_limits.py +30 -1
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
- package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
- package/hooks/hooks_constants/test_text_stripping.py +39 -0
- package/hooks/hooks_constants/text_stripping.py +36 -0
- package/hooks/validation/CLAUDE.md +1 -0
- package/hooks/validation/post_tool_use_dispatcher.py +2 -2
- package/hooks/validation/test_mypy_validator.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
- package/hooks/workflow/auto_formatter.py +8 -5
- package/hooks/workflow/test_auto_formatter.py +33 -0
- package/package.json +1 -1
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +1 -0
- package/rules/package-inventory-stale-entry.md +8 -0
- package/rules/windows-filesystem-safe.md +2 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/SKILL.md +6 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +306 -137
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -44
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
- package/skills/autoconverge/workflow/converge.mjs +581 -613
- package/skills/autoconverge/workflow/convergence_summary.py +1 -1
- package/skills/autoconverge/workflow/render_report.py +2 -6
- package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
- package/skills/autoconverge/workflow/test_render_report.py +1 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -83,7 +83,7 @@ def build_summary_prompt(
|
|
|
83
83
|
copilot_note: Outage note when the Copilot gate was bypassed, else None.
|
|
84
84
|
|
|
85
85
|
Returns:
|
|
86
|
-
The full agent prompt instructing a
|
|
86
|
+
The full agent prompt instructing a plain-JSON convergence summary.
|
|
87
87
|
"""
|
|
88
88
|
pr_url = GITHUB_PR_URL_TEMPLATE.format(owner=owner, repo=repo, number=pr_number)
|
|
89
89
|
pr_coordinates = SUMMARY_PR_COORDINATES_TEMPLATE.format(
|
|
@@ -17,7 +17,6 @@ from autoconverge_report_constants.render_report_constants import (
|
|
|
17
17
|
DEFAULT_FINDING_CATEGORY,
|
|
18
18
|
DEFAULT_FINDING_SEVERITY,
|
|
19
19
|
DEFAULT_ISSUE_ICON,
|
|
20
|
-
GITHUB_PR_URL_TEMPLATE,
|
|
21
20
|
HTML_DOCTYPE,
|
|
22
21
|
HTML_HEAD_TEMPLATE,
|
|
23
22
|
HTML_STYLE_BLOCK,
|
|
@@ -91,12 +90,11 @@ class FixRecord:
|
|
|
91
90
|
|
|
92
91
|
@dataclass(frozen=True)
|
|
93
92
|
class PrMetadata:
|
|
94
|
-
"""Owner, repo, number, and
|
|
93
|
+
"""Owner, repo, number, final sha, and round count for the PR being reported on."""
|
|
95
94
|
|
|
96
95
|
owner: str
|
|
97
96
|
repo: str
|
|
98
97
|
number: int
|
|
99
|
-
url: str
|
|
100
98
|
final_sha: str
|
|
101
99
|
round_count: int
|
|
102
100
|
|
|
@@ -888,7 +886,7 @@ def render_report_html(
|
|
|
888
886
|
|
|
889
887
|
Args:
|
|
890
888
|
run_data: Aggregated metrics from the workflow journal and transcripts.
|
|
891
|
-
pr_metadata: Owner, repo, number,
|
|
889
|
+
pr_metadata: Owner, repo, number, final sha, and round count for the PR.
|
|
892
890
|
generated_date: ISO date string derived from the journal timestamp.
|
|
893
891
|
|
|
894
892
|
Returns:
|
|
@@ -1004,13 +1002,11 @@ def main(out_stream: TextIO = sys.stdout, err_stream: TextIO = sys.stderr) -> in
|
|
|
1004
1002
|
return 1
|
|
1005
1003
|
|
|
1006
1004
|
owner, repo, pr_number = parsed_pr
|
|
1007
|
-
pr_url = GITHUB_PR_URL_TEMPLATE.format(owner=owner, repo=repo, number=pr_number)
|
|
1008
1005
|
|
|
1009
1006
|
pr_metadata = PrMetadata(
|
|
1010
1007
|
owner=owner,
|
|
1011
1008
|
repo=repo,
|
|
1012
1009
|
number=pr_number,
|
|
1013
|
-
url=pr_url,
|
|
1014
1010
|
final_sha=parsed_args.final_sha,
|
|
1015
1011
|
round_count=parsed_args.rounds,
|
|
1016
1012
|
)
|
|
@@ -47,6 +47,23 @@ def test_prompt_carries_pr_coordinates_and_round_count() -> None:
|
|
|
47
47
|
assert "gh api repos/jl-cmd/claude-code-config/pulls/581" in prompt
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def test_prompt_instructs_plain_json_object_not_structured_output() -> None:
|
|
51
|
+
"""Should instruct a plain JSON object answer and never name the StructuredOutput tool."""
|
|
52
|
+
prompt = convergence_summary.build_summary_prompt(
|
|
53
|
+
owner="jl-cmd",
|
|
54
|
+
repo="claude-code-config",
|
|
55
|
+
pr_number=581,
|
|
56
|
+
round_count=39,
|
|
57
|
+
findings=SAMPLE_FINDINGS,
|
|
58
|
+
fix_summaries=[],
|
|
59
|
+
standards_note=None,
|
|
60
|
+
copilot_note=None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert "StructuredOutput" not in prompt
|
|
64
|
+
assert "Return strictly a JSON object with keys" in prompt
|
|
65
|
+
|
|
66
|
+
|
|
50
67
|
def test_prompt_numbers_every_finding_with_severity_and_location() -> None:
|
|
51
68
|
"""Should list each aggregated finding numbered with severity, file, and line."""
|
|
52
69
|
prompt = convergence_summary.build_summary_prompt(
|
|
@@ -448,6 +448,7 @@ def test_cli_injects_summary_from_file_bypassing_transcripts(tmp_path: Path) ->
|
|
|
448
448
|
assert "An injected class the transcript never carried" in html_content
|
|
449
449
|
assert 'class="pf-grid"' in html_content
|
|
450
450
|
assert f"Raw findings ({EXPECTED_TOTAL_FINDINGS})" in html_content
|
|
451
|
+
assert "https://github.com/example-owner/example-repo/pull/211" not in html_content
|
|
451
452
|
|
|
452
453
|
|
|
453
454
|
def test_html_contains_no_hedging_words(tmp_path: Path) -> None:
|
|
@@ -23,6 +23,11 @@ from bugteam_scripts_constants.bugteam_code_rules_gate_constants import (
|
|
|
23
23
|
BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX,
|
|
24
24
|
BANNED_NOUN_SPAN_GROUP_INDEX,
|
|
25
25
|
BANNED_NOUN_VIOLATION_PATTERN,
|
|
26
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX,
|
|
27
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX,
|
|
28
|
+
INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX,
|
|
29
|
+
INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX,
|
|
30
|
+
INLINE_DUPLICATE_BODY_VIOLATION_PATTERN,
|
|
26
31
|
HUNK_HEADER_RAW_PATTERN,
|
|
27
32
|
ISOLATION_DEFINITION_LINE_GROUP_INDEX,
|
|
28
33
|
ISOLATION_SPAN_GROUP_INDEX,
|
|
@@ -958,6 +963,43 @@ def banned_noun_span_range(violation_text: str) -> range | None:
|
|
|
958
963
|
return range(definition_line, definition_line + line_span)
|
|
959
964
|
|
|
960
965
|
|
|
966
|
+
def inline_duplicate_body_span_lines(violation_text: str) -> frozenset[int] | None:
|
|
967
|
+
"""Return the union of both spans of a same-file inline-duplicate issue, or None.
|
|
968
|
+
|
|
969
|
+
The same-file inline-duplicate message names two functions that share a body —
|
|
970
|
+
the helper and the enclosing function carrying the inline copy — and the live
|
|
971
|
+
Write/Edit hook scopes the violation by the UNION of both spans, blocking when
|
|
972
|
+
an edit touches either function. So the message carries both spans: ``(inline
|
|
973
|
+
duplicate body spans: helper at line H spanning P lines, enclosing at line E
|
|
974
|
+
spanning Q lines)``. The two spans can be disjoint (an unrelated function may
|
|
975
|
+
sit between the helper and its inline copy), so this returns the union as a
|
|
976
|
+
line-number set rather than a single contiguous range — a range covering the
|
|
977
|
+
gap would wrongly block an edit confined to that intervening function, which
|
|
978
|
+
the PreToolUse path leaves unflagged.
|
|
979
|
+
|
|
980
|
+
Args:
|
|
981
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
The frozenset of every line in the helper span and the enclosing span, or
|
|
985
|
+
None when the text is not a same-file inline-duplicate violation.
|
|
986
|
+
"""
|
|
987
|
+
span_match = INLINE_DUPLICATE_BODY_VIOLATION_PATTERN.search(violation_text)
|
|
988
|
+
if span_match is None:
|
|
989
|
+
return None
|
|
990
|
+
helper_line = int(span_match.group(INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX))
|
|
991
|
+
helper_span = int(span_match.group(INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX))
|
|
992
|
+
enclosing_line = int(
|
|
993
|
+
span_match.group(INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX)
|
|
994
|
+
)
|
|
995
|
+
enclosing_span = int(
|
|
996
|
+
span_match.group(INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX)
|
|
997
|
+
)
|
|
998
|
+
helper_lines = range(helper_line, helper_line + helper_span)
|
|
999
|
+
enclosing_lines = range(enclosing_line, enclosing_line + enclosing_span)
|
|
1000
|
+
return frozenset(helper_lines) | frozenset(enclosing_lines)
|
|
1001
|
+
|
|
1002
|
+
|
|
961
1003
|
def _all_span_range_extractors() -> tuple[Callable[[str], range | None], ...]:
|
|
962
1004
|
return (
|
|
963
1005
|
function_length_span_range,
|
|
@@ -1003,10 +1045,15 @@ def split_violations_by_scope(
|
|
|
1003
1045
|
|
|
1004
1046
|
Returns:
|
|
1005
1047
|
Tuple ``(blocking, advisory)``. When *all_added_line_numbers* is
|
|
1006
|
-
None, every issue is blocking.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1048
|
+
None, every issue is blocking. A same-file inline-duplicate violation
|
|
1049
|
+
carries both the helper span and the enclosing span;
|
|
1050
|
+
``inline_duplicate_body_span_lines`` reconstructs their union as a
|
|
1051
|
+
line-number set, and the violation is blocking when an added line falls
|
|
1052
|
+
in either span — matching the live Write/Edit hook's union scoping. Every
|
|
1053
|
+
other diff-scoped violation (function-length, HOME/TMP isolation,
|
|
1054
|
+
banned-noun) carries one enclosing-unit span fragment that
|
|
1055
|
+
``enclosing_span_range`` reconstructs through one shared extractor
|
|
1056
|
+
registry; such a violation is blocking
|
|
1010
1057
|
when its declared span intersects the added lines (the unit grew or its
|
|
1011
1058
|
signature changed in this diff) and advisory otherwise (a pre-existing
|
|
1012
1059
|
untouched unit). Every other issue is blocking when its ``Line N:``
|
|
@@ -1017,6 +1064,13 @@ def split_violations_by_scope(
|
|
|
1017
1064
|
blocking: list[str] = []
|
|
1018
1065
|
advisory: list[str] = []
|
|
1019
1066
|
for each_issue in all_issues:
|
|
1067
|
+
inline_duplicate_lines = inline_duplicate_body_span_lines(each_issue)
|
|
1068
|
+
if inline_duplicate_lines is not None:
|
|
1069
|
+
if inline_duplicate_lines & all_added_line_numbers:
|
|
1070
|
+
blocking.append(each_issue)
|
|
1071
|
+
else:
|
|
1072
|
+
advisory.append(each_issue)
|
|
1073
|
+
continue
|
|
1020
1074
|
span_range = enclosing_span_range(each_issue)
|
|
1021
1075
|
if span_range is not None:
|
|
1022
1076
|
if any(each_line in all_added_line_numbers for each_line in span_range):
|
package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py
CHANGED
|
@@ -31,6 +31,15 @@ BANNED_NOUN_VIOLATION_PATTERN: re.Pattern[str] = re.compile(
|
|
|
31
31
|
BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX: int = 1
|
|
32
32
|
BANNED_NOUN_SPAN_GROUP_INDEX: int = 2
|
|
33
33
|
|
|
34
|
+
INLINE_DUPLICATE_BODY_VIOLATION_PATTERN: re.Pattern[str] = re.compile(
|
|
35
|
+
r"\(inline duplicate body spans: helper at line (\d+) spanning (\d+) lines, "
|
|
36
|
+
r"enclosing at line (\d+) spanning (\d+) lines\)"
|
|
37
|
+
)
|
|
38
|
+
INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX: int = 1
|
|
39
|
+
INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX: int = 2
|
|
40
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX: int = 3
|
|
41
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX: int = 4
|
|
42
|
+
|
|
34
43
|
ALL_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
35
44
|
{".py", ".js", ".ts", ".tsx", ".jsx"}
|
|
36
45
|
)
|
|
@@ -1022,3 +1022,45 @@ def test_main_staged_mode_passes_on_staged_deletion_of_clean_file(
|
|
|
1022
1022
|
"a staged deletion has no staged blob; the gate must skip it cleanly "
|
|
1023
1023
|
"rather than fail closed as if the file were unreadable"
|
|
1024
1024
|
)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
_INLINE_DUPLICATE_MESSAGE = (
|
|
1028
|
+
"Function '_wait_for_render' duplicates an inline block in '_navigate_then_wait'"
|
|
1029
|
+
" — this function body is also present inline (Reuse before create / DRY) "
|
|
1030
|
+
"(inline duplicate body spans: helper at line 4 spanning 10 lines, "
|
|
1031
|
+
"enclosing at line 16 spanning 11 lines)"
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def test_inline_duplicate_body_span_lines_unions_helper_and_enclosing_spans() -> None:
|
|
1036
|
+
"""The same-file inline-duplicate message carries both spans, and the gate
|
|
1037
|
+
recovers their union as a line-number set so a touch of either function blocks —
|
|
1038
|
+
mirroring the live Write/Edit hook's union scoping."""
|
|
1039
|
+
span_lines = gate_module.inline_duplicate_body_span_lines(_INLINE_DUPLICATE_MESSAGE)
|
|
1040
|
+
assert span_lines == frozenset(range(4, 14)) | frozenset(range(16, 27))
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def test_inline_duplicate_blocks_when_only_enclosing_copy_added() -> None:
|
|
1044
|
+
"""An added line in the enclosing span alone blocks, because the live hook scopes
|
|
1045
|
+
by the union of both spans and blocks the same edit — the common shape where a
|
|
1046
|
+
block is copied INTO a growing enclosing function, leaving the helper untouched."""
|
|
1047
|
+
added_line_in_enclosing_only = 18
|
|
1048
|
+
blocking, advisory = gate_module.split_violations_by_scope(
|
|
1049
|
+
[_INLINE_DUPLICATE_MESSAGE],
|
|
1050
|
+
all_added_line_numbers={added_line_in_enclosing_only},
|
|
1051
|
+
)
|
|
1052
|
+
assert blocking == [_INLINE_DUPLICATE_MESSAGE]
|
|
1053
|
+
assert advisory == []
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def test_inline_duplicate_advises_when_gap_line_added() -> None:
|
|
1057
|
+
"""An edit confined to the gap between the helper span (4-13) and the enclosing
|
|
1058
|
+
span (16-26) must not block, matching the live hook. The union set keeps the gap
|
|
1059
|
+
out of scope where a single contiguous range would wrongly block it."""
|
|
1060
|
+
gap_line_between_spans = 14
|
|
1061
|
+
blocking, advisory = gate_module.split_violations_by_scope(
|
|
1062
|
+
[_INLINE_DUPLICATE_MESSAGE],
|
|
1063
|
+
all_added_line_numbers={gap_line_between_spans},
|
|
1064
|
+
)
|
|
1065
|
+
assert advisory == [_INLINE_DUPLICATE_MESSAGE]
|
|
1066
|
+
assert blocking == []
|