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.
Files changed (78) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
  2. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
  5. package/docs/CODE_RULES.md +1 -1
  6. package/hooks/blocking/CLAUDE.md +3 -2
  7. package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
  8. package/hooks/blocking/code_rules_duplicate_body.py +378 -26
  9. package/hooks/blocking/code_rules_enforcer.py +36 -5
  10. package/hooks/blocking/code_rules_imports_logging.py +679 -1
  11. package/hooks/blocking/code_rules_shared.py +8 -5
  12. package/hooks/blocking/code_rules_test_assertions.py +6 -7
  13. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
  14. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
  15. package/hooks/blocking/hedging_language_blocker.py +1 -13
  16. package/hooks/blocking/intent_only_ending_blocker.py +1 -15
  17. package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
  18. package/hooks/blocking/question_to_user_enforcer.py +1 -11
  19. package/hooks/blocking/session_handoff_blocker.py +1 -15
  20. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
  21. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  22. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
  23. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
  24. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
  25. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
  26. package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
  27. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
  28. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
  29. package/hooks/blocking/test_hedging_language_blocker.py +6 -0
  30. package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
  31. package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
  32. package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
  33. package/hooks/blocking/test_session_handoff_blocker.py +6 -0
  34. package/hooks/blocking/test_verification_verdict_store.py +66 -1
  35. package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
  36. package/hooks/blocking/verification_verdict_store.py +19 -5
  37. package/hooks/blocking/verifier_verdict_minter.py +18 -15
  38. package/hooks/hooks_constants/CLAUDE.md +4 -1
  39. package/hooks/hooks_constants/blocking_check_limits.py +30 -1
  40. package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
  42. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
  43. package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
  44. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
  45. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
  46. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
  47. package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
  48. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
  49. package/hooks/hooks_constants/test_text_stripping.py +39 -0
  50. package/hooks/hooks_constants/text_stripping.py +36 -0
  51. package/hooks/validation/CLAUDE.md +1 -0
  52. package/hooks/validation/post_tool_use_dispatcher.py +2 -2
  53. package/hooks/validation/test_mypy_validator.py +1 -1
  54. package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
  55. package/hooks/workflow/auto_formatter.py +8 -5
  56. package/hooks/workflow/test_auto_formatter.py +33 -0
  57. package/package.json +1 -1
  58. package/rules/claude-md-orphan-file.md +7 -8
  59. package/rules/docstring-prose-matches-implementation.md +1 -0
  60. package/rules/package-inventory-stale-entry.md +8 -0
  61. package/rules/windows-filesystem-safe.md +2 -0
  62. package/skills/anthropic-plan/CLAUDE.md +1 -1
  63. package/skills/anthropic-plan/SKILL.md +15 -2
  64. package/skills/autoconverge/SKILL.md +6 -3
  65. package/skills/autoconverge/reference/stop-conditions.md +7 -0
  66. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
  67. package/skills/autoconverge/workflow/converge.contract.test.mjs +306 -137
  68. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
  69. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -44
  70. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
  71. package/skills/autoconverge/workflow/converge.mjs +581 -613
  72. package/skills/autoconverge/workflow/convergence_summary.py +1 -1
  73. package/skills/autoconverge/workflow/render_report.py +2 -6
  74. package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
  75. package/skills/autoconverge/workflow/test_render_report.py +1 -0
  76. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
  77. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
  78. 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 StructuredOutput convergence summary.
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 pre-built URL for the PR being reported on."""
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, URL, final sha, and round count for the PR.
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. Every diff-scoped violation
1007
- (function-length, HOME/TMP isolation, banned-noun) carries an
1008
- enclosing-unit span fragment that ``enclosing_span_range`` reconstructs
1009
- through one shared extractor registry; such a violation is blocking
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):
@@ -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 == []