claude-dev-env 2.36.0 → 4.0.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 (66) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/team-advisor/SKILL.md +11 -11
  3. package/_shared/advisor/advisor-protocol.md +51 -51
  4. package/_shared/advisor/reference/advisor-block.md +9 -9
  5. package/_shared/advisor/reference/astra-rung.md +39 -0
  6. package/_shared/advisor/reference/identity.md +4 -4
  7. package/_shared/advisor/reference/lifecycle.md +6 -6
  8. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  9. package/_shared/advisor/reference/third-party-bind.md +11 -11
  10. package/_shared/advisor/reference/warm-up.md +13 -13
  11. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  12. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  13. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  17. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  18. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  19. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  20. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  21. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  22. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  23. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  24. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  25. package/docs/references/team-advisor-skill.md +3 -3
  26. package/hooks/validators/README.md +35 -1
  27. package/package.json +1 -1
  28. package/scripts/check.ps1 +18 -6
  29. package/scripts/policy_lint/adapter_detectors.py +42 -2
  30. package/scripts/policy_lint/adapters.py +24 -0
  31. package/scripts/policy_lint/config/constants.py +11 -0
  32. package/scripts/policy_lint/engine.py +1 -1
  33. package/scripts/policy_lint/model.py +1 -0
  34. package/scripts/policy_lint/registry.py +10 -1
  35. package/scripts/repository_checks/__init__.py +1 -0
  36. package/scripts/repository_checks/claude_md.py +108 -0
  37. package/scripts/repository_checks/config/__init__.py +1 -0
  38. package/scripts/repository_checks/config/constants.py +150 -0
  39. package/scripts/repository_checks/env_var_documentation.py +243 -0
  40. package/scripts/repository_checks/hook_modules.py +34 -0
  41. package/scripts/repository_checks/models.py +37 -0
  42. package/scripts/repository_checks/package_inventory.py +118 -0
  43. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  44. package/scripts/repository_checks/reporting.py +39 -0
  45. package/scripts/repository_checks/runner.py +93 -0
  46. package/scripts/repository_checks/tracked_secrets.py +147 -0
  47. package/scripts/repository_checks/tracked_tree.py +28 -0
  48. package/scripts/repository_policy.py +120 -0
  49. package/scripts/tests/repository_policy_test_support.py +89 -0
  50. package/scripts/tests/test_cde_lint.py +25 -1
  51. package/scripts/tests/test_claude_md.py +68 -0
  52. package/scripts/tests/test_env_var_documentation.py +133 -0
  53. package/scripts/tests/test_hook_modules.py +17 -0
  54. package/scripts/tests/test_models.py +26 -0
  55. package/scripts/tests/test_package_inventory.py +96 -0
  56. package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
  57. package/scripts/tests/test_pytest_testpaths.py +66 -0
  58. package/scripts/tests/test_reporting.py +54 -0
  59. package/scripts/tests/test_repository_policy.py +71 -0
  60. package/scripts/tests/test_runner.py +59 -0
  61. package/scripts/tests/test_tracked_secrets.py +175 -0
  62. package/scripts/tests/test_tracked_tree.py +28 -0
  63. package/_shared/advisor/reference/sol-rung.md +0 -39
  64. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  65. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  66. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
@@ -105,10 +105,19 @@ def test_sendmessage_reply_wait_is_positive_bound() -> None:
105
105
  assert ADVISOR_SENDMESSAGE_REPLY_WAIT_SECONDS == 120
106
106
 
107
107
 
108
- def test_resolve_codex_model_id_maps_sol() -> None:
109
- assert resolve_codex_model_id(f" {ADVISOR_MODEL_TIER.lower()} ") == (
110
- ADVISOR_CODEX_MODEL_ID
111
- )
108
+ @pytest.mark.parametrize("tier_name", ["Astra", "astra", "ASTRA", " Astra "])
109
+ def test_resolve_codex_model_id_maps_astra(tier_name: str) -> None:
110
+ assert resolve_codex_model_id(tier_name) == "gpt-6-astra"
111
+
112
+
113
+ def test_resolve_codex_model_id_rejects_old_sol_tier() -> None:
114
+ with pytest.raises(ValueError, match="not a known model tier"):
115
+ resolve_codex_model_id("Sol")
116
+
117
+
118
+ def test_resolve_cli_model_id_rejects_astra_as_a_claude_alias() -> None:
119
+ with pytest.raises(ValueError, match="not a known model tier"):
120
+ resolve_cli_model_id("Astra")
112
121
 
113
122
 
114
123
  def test_resolve_codex_model_id_rejects_claude_tier() -> None:
@@ -132,6 +141,7 @@ def test_cli_model_alias_map_keys_match_known_tiers() -> None:
132
141
  def test_canonical_tier_name_strips_and_normalizes() -> None:
133
142
  assert canonical_tier_name(" opus ") == "Opus"
134
143
  assert canonical_tier_name("thirdparty") == "ThirdParty"
144
+ assert canonical_tier_name(" astra ") == "Astra"
135
145
  assert canonical_tier_name("") is None
136
146
  assert canonical_tier_name("Titan") is None
137
147
 
@@ -109,6 +109,20 @@ ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND: tuple[str, ...] = (
109
109
 
110
110
  GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS: int = 30
111
111
 
112
+ GIT_NO_MATCH_EXIT_CODE: int = 1
113
+
114
+ GIT_OPERATION_FAILURE_TEMPLATE: str = "{operation} failed: {details}"
115
+
116
+ GIT_DIFF_OPERATION: str = "git diff --cached"
117
+
118
+ GIT_GREP_OPERATION_TEMPLATE: str = "git grep for {identifier}"
119
+
120
+ GIT_FAILURE_WITHOUT_DETAILS: str = "no diagnostic text"
121
+
122
+ GIT_TEXT_ENCODING: str = "utf-8"
123
+
124
+ GIT_DECODE_ERRORS: str = "replace"
125
+
112
126
  TERMINOLOGY_SWEEP_GATE_HEADER: str = (
113
127
  "terminology_sweep: {finding_count} cross-surface terminology near-miss(es) "
114
128
  "on staged prose:"
@@ -1,7 +1,7 @@
1
1
  """Flag a prose term that names a code identifier a hair differently.
2
2
 
3
3
  A change adds the field ``premium_request_interactions`` while the same
4
- branch's docs call it the ``premium-request-budget`` field: one thing, two
4
+ branch's docs call it the ``premium-request-budget`` field. One thing, two
5
5
  names, and a reader who searches one never finds the other.
6
6
 
7
7
  ::
@@ -14,38 +14,29 @@ names, and a reader who searches one never finds the other.
14
14
 
15
15
  The sweep reads a unified diff, collects the multi-word identifiers added on
16
16
  code lines, and scans each added prose line for a hyphen or space variant that
17
- renames only the final word of an identifier: it agrees on every earlier word
18
- and differs on the last. A term that diverges in any earlier word, or shares
19
- only one leading word, is ordinary prose. It also spares English compound
20
- tails, singular/plural forms, stopword windows, and words that are themselves
21
- tokens of another introduced identifier.
17
+ renames only the final word of an identifier. The variant agrees on every
18
+ earlier word and differs on the last. A term that diverges in any earlier
19
+ word, or shares only one leading word, is ordinary prose. It also spares
20
+ English compound tails, singular/plural forms, stopword windows, and words
21
+ that are themselves tokens of another introduced identifier.
22
22
  """
23
23
 
24
24
  import argparse
25
25
  import os
26
26
  import subprocess
27
27
  import sys
28
+ from collections.abc import Sequence
28
29
  from pathlib import Path
29
30
 
30
- parent_directory = str(Path(__file__).resolve().parent)
31
- if parent_directory not in sys.path:
32
- sys.path.insert(0, parent_directory)
33
-
34
- from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
31
+ from pr_loop_shared_constants.terminology_sweep_constants import (
35
32
  ALL_COMMON_ENGLISH_COMPOUND_TAIL_WORDS,
33
+ ALL_DIFF_FILE_PATH_STRIP_PREFIXES,
34
+ ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND,
36
35
  ALL_GIT_GREP_BASE_TREE_COMMAND_PREFIX,
37
36
  ALL_PROSE_STOPWORD_TOKENS,
38
37
  ALL_STRING_ESCAPE_SEQUENCES,
39
- ALL_TEST_FILE_NAME_INFIX_MARKERS,
40
- GIT_BASE_TREE_REVISION,
41
- IDENTIFIER_TOKEN_SEPARATOR,
42
- PROSE_WINDOW_WORD_SEPARATOR,
43
- TEST_DIRECTORY_PATH_SEGMENT,
44
- TEST_FILE_PREFIX,
45
- TEST_FILE_SUFFIX,
46
- ALL_DIFF_FILE_PATH_STRIP_PREFIXES,
47
- ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND,
48
38
  ALL_SWEEP_CODE_FILE_EXTENSIONS,
39
+ ALL_TEST_FILE_NAME_INFIX_MARKERS,
49
40
  CAMEL_CASE_IDENTIFIER_PATTERN,
50
41
  CAMEL_CASE_WORD_PATTERN,
51
42
  DIFF_ADDED_LINE_PREFIX,
@@ -55,19 +46,32 @@ from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
55
46
  DIFF_NEW_FILE_HEADER_PREFIX,
56
47
  DIFF_OLD_FILE_HEADER_PREFIX,
57
48
  DIFF_REMOVED_LINE_PREFIX,
49
+ GIT_BASE_TREE_REVISION,
50
+ GIT_DECODE_ERRORS,
51
+ GIT_DIFF_OPERATION,
58
52
  GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
53
+ GIT_FAILURE_WITHOUT_DETAILS,
54
+ GIT_GREP_OPERATION_TEMPLATE,
55
+ GIT_NO_MATCH_EXIT_CODE,
56
+ GIT_OPERATION_FAILURE_TEMPLATE,
57
+ GIT_TEXT_ENCODING,
59
58
  HYPHENATED_PROSE_TOKEN_PATTERN,
59
+ IDENTIFIER_TOKEN_SEPARATOR,
60
60
  INLINE_CODE_SPAN_PATTERN,
61
61
  JAVASCRIPT_LINE_COMMENT_MARKER,
62
62
  JSDOC_CONTINUATION_MARKER,
63
63
  MARKDOWN_FILE_EXTENSION,
64
64
  MINIMUM_IDENTIFIER_TOKEN_COUNT,
65
+ PROSE_WINDOW_WORD_SEPARATOR,
65
66
  PROSE_WORD_PATTERN,
66
67
  PYTHON_COMMENT_MARKER,
67
68
  SNAKE_CASE_IDENTIFIER_PATTERN,
68
69
  STRING_LITERAL_CONTENT_PATTERN,
69
70
  TERMINOLOGY_FINDING_TEMPLATE,
70
71
  TERMINOLOGY_SWEEP_DESCRIPTION,
72
+ TEST_DIRECTORY_PATH_SEGMENT,
73
+ TEST_FILE_PREFIX,
74
+ TEST_FILE_SUFFIX,
71
75
  )
72
76
 
73
77
  IdentifierTuple = tuple[str, ...]
@@ -173,8 +177,8 @@ def _prose_fragments(file_path: str, line_text: str) -> list[str]:
173
177
  A Markdown line is prose with its inline-code spans removed, since a
174
178
  backticked span names code verbatim. A code line contributes its comment
175
179
  tail, its JSDoc continuation text, and the contents of its string
176
- literals. A test module contributes its comment tail and JSDoc text only
177
- — its string literals hold fixture data, not prose.
180
+ literals. A test module contributes its comment tail and JSDoc text only.
181
+ Its string literals hold fixture data, not prose.
178
182
 
179
183
  Args:
180
184
  file_path: The path the added line belongs to.
@@ -201,9 +205,9 @@ def _prose_fragments(file_path: str, line_text: str) -> list[str]:
201
205
  def _is_test_file(file_path: str) -> bool:
202
206
  """Return whether a diff path names a test code module.
203
207
 
204
- A test module's string literals hold fixture data — embedded diffs,
205
- generated source, file trees — not documentation prose, so the sweep
206
- reads only its comments and JSDoc lines. A ``quota.test.mjs``, a
208
+ A test module's string literals hold fixture data: embedded diffs,
209
+ generated source, and file trees. That is not documentation prose, so the
210
+ sweep reads only its comments and JSDoc lines. A ``quota.test.mjs``, a
207
211
  ``layout.spec.ts``, and a ``fixtures.py`` under a tests directory are all
208
212
  test modules, so every one of these paths counts::
209
213
 
@@ -260,8 +264,8 @@ def _string_literal_fragments(line_text: str) -> list[str]:
260
264
  Escape sequences inside a literal (``\\n``, ``\\t``) are replaced with
261
265
  spaces before the words are read, so an escape letter never glues onto a
262
266
  neighbouring word as a phantom prose term. A literal left with no
263
- whitespace after that replacement is an identifier-shaped value — a UID,
264
- a key, a path — not prose, and contributes nothing.
267
+ whitespace after that replacement is an identifier-shaped value: a UID,
268
+ a key, or a path. It is not prose, and contributes nothing.
265
269
  """
266
270
  all_fragments: list[str] = []
267
271
  for each_match in STRING_LITERAL_CONTENT_PATTERN.finditer(line_text):
@@ -413,9 +417,7 @@ def _word_is_identifier_vocabulary(
413
417
  return True
414
418
  if prose_word.endswith("es") and prose_word[:-2] in all_identifier_tokens:
415
419
  return True
416
- if prose_word.endswith("ies") and prose_word[:-3] + "y" in all_identifier_tokens:
417
- return True
418
- return False
420
+ return prose_word.endswith("ies") and prose_word[:-3] + "y" in all_identifier_tokens
419
421
 
420
422
 
421
423
  def _near_miss_identifier(
@@ -525,22 +527,8 @@ def _findings_for_line(
525
527
  return all_findings
526
528
 
527
529
 
528
- def sweep_diff(
529
- diff_text: str,
530
- all_preexisting_identifier_tuples: frozenset[IdentifierTuple] = frozenset(),
531
- ) -> list[str]:
532
- """Return every terminology near-miss finding for a unified diff.
533
-
534
- Args:
535
- diff_text: The unified-diff text to sweep.
536
- all_preexisting_identifier_tuples: Token tuples of added-line identifiers
537
- the base tree already names. A pre-existing identifier is not one
538
- the diff introduces, so no prose is flagged against it. Its
539
- tokens still count as code vocabulary.
540
-
541
- Returns:
542
- One finding string per near-miss term on an added prose line.
543
- """
530
+ def _find_terminology_near_misses(
531
+ diff_text: str, all_preexisting_identifier_tuples: frozenset[IdentifierTuple]) -> list[str]:
544
532
  all_added_lines = _parse_added_lines(diff_text)
545
533
  all_identifier_tuples = _collect_introduced_identifiers(all_added_lines)
546
534
  all_identifier_tokens = frozenset(
@@ -571,6 +559,27 @@ def sweep_diff(
571
559
  return all_findings
572
560
 
573
561
 
562
+ def sweep_diff(
563
+ diff_text: str,
564
+ all_preexisting_identifier_tuples: frozenset[IdentifierTuple] = frozenset(),
565
+ ) -> list[str]:
566
+ """Return every terminology near-miss finding for a unified diff.
567
+
568
+ Args:
569
+ diff_text: The unified-diff text to sweep.
570
+ all_preexisting_identifier_tuples: Token tuples of added-line identifiers
571
+ the base tree already names. A pre-existing identifier is not one
572
+ the diff introduces, so no prose is flagged against it. Its
573
+ tokens still count as code vocabulary.
574
+
575
+ Returns:
576
+ One finding string per near-miss term on an added prose line.
577
+ """
578
+ return _find_terminology_near_misses(
579
+ diff_text, all_preexisting_identifier_tuples
580
+ )
581
+
582
+
574
583
  def repository_environment() -> dict[str, str]:
575
584
  """Return the process environment with every GIT_ variable removed.
576
585
 
@@ -619,8 +628,8 @@ def _base_tree_names(
619
628
 
620
629
  Each name is looked up as a whole word in the base revision's tree. A
621
630
  repository with no commits yet, or a lookup that git cannot run, reports
622
- the name as absent, so the sweep then treats it as newly introduced —
623
- the behaviour the sweep has with no base-tree check at all.
631
+ the name as absent, so the sweep then treats it as newly introduced.
632
+ That is the behaviour the sweep has with no base-tree check at all.
624
633
 
625
634
  Args:
626
635
  repository_root: The repository root the lookups run in.
@@ -652,11 +661,90 @@ def _base_tree_names(
652
661
  return frozenset(all_present_names)
653
662
 
654
663
 
664
+ def _run_strict_git(
665
+ repository_root: Path, all_arguments: Sequence[str]
666
+ ) -> subprocess.CompletedProcess[str]:
667
+ try:
668
+ return subprocess.run(
669
+ list(all_arguments),
670
+ cwd=str(repository_root),
671
+ capture_output=True,
672
+ text=True,
673
+ encoding=GIT_TEXT_ENCODING,
674
+ errors=GIT_DECODE_ERRORS,
675
+ timeout=GIT_DIFF_SUBPROCESS_TIMEOUT_SECONDS,
676
+ check=False,
677
+ env=repository_environment(),
678
+ )
679
+ except (subprocess.TimeoutExpired, OSError) as e:
680
+ raise RuntimeError(str(e)) from e
681
+
682
+
683
+ def _raise_for_git_failure(
684
+ git_process: subprocess.CompletedProcess[str], operation: str
685
+ ) -> None:
686
+ if git_process.returncode == 0:
687
+ return
688
+ details = git_process.stderr.strip() or git_process.stdout.strip()
689
+ raise RuntimeError(
690
+ GIT_OPERATION_FAILURE_TEMPLATE.format(
691
+ operation=operation,
692
+ details=details or GIT_FAILURE_WITHOUT_DETAILS,
693
+ )
694
+ )
695
+
696
+
697
+ def _strict_base_tree_names(
698
+ repository_root: Path, all_names: frozenset[str]
699
+ ) -> frozenset[str]:
700
+ all_present_names: set[str] = set()
701
+ for each_name in sorted(all_names):
702
+ all_arguments = (
703
+ *ALL_GIT_GREP_BASE_TREE_COMMAND_PREFIX,
704
+ each_name,
705
+ GIT_BASE_TREE_REVISION,
706
+ )
707
+ grep_process = _run_strict_git(repository_root, all_arguments)
708
+ if grep_process.returncode == 0:
709
+ all_present_names.add(each_name)
710
+ continue
711
+ if grep_process.returncode == GIT_NO_MATCH_EXIT_CODE:
712
+ continue
713
+ operation = GIT_GREP_OPERATION_TEMPLATE.format(identifier=each_name)
714
+ _raise_for_git_failure(grep_process, operation)
715
+ return frozenset(all_present_names)
716
+
717
+
718
+ def strict_staged_terminology_findings(repository_root: Path) -> list[str]:
719
+ """Return near-miss findings for the staged diff.
720
+
721
+ Args:
722
+ repository_root: The repository root the staged diff is read from.
723
+
724
+ Returns:
725
+ One finding string per near-miss term on a staged prose line.
726
+
727
+ Raises:
728
+ RuntimeError: Git could not read the staged diff or look up an
729
+ identifier in HEAD.
730
+ """
731
+ diff_process = _run_strict_git(
732
+ repository_root, ALL_GIT_DIFF_CACHED_UNIFIED_ZERO_COMMAND
733
+ )
734
+ _raise_for_git_failure(diff_process, GIT_DIFF_OPERATION)
735
+ all_added_names = _identifier_names_on_added_code_lines(diff_process.stdout)
736
+ preexisting_tuples = frozenset(
737
+ _identifier_token_tuple(each_name)
738
+ for each_name in _strict_base_tree_names(repository_root, all_added_names)
739
+ )
740
+ return _find_terminology_near_misses(diff_process.stdout, preexisting_tuples)
741
+
742
+
655
743
  def staged_terminology_findings(repository_root: Path) -> list[str]:
656
744
  """Return terminology near-miss findings for a repository's staged diff.
657
745
 
658
746
  An identifier the base tree already names is not one the staged diff
659
- introduces, so no prose is flagged against it — only genuinely new
747
+ introduces, so no prose is flagged against it. Only genuinely new
660
748
  identifiers are swept.
661
749
 
662
750
  Args:
@@ -684,7 +772,7 @@ def staged_terminology_findings(repository_root: Path) -> list[str]:
684
772
  _identifier_token_tuple(each_name)
685
773
  for each_name in _base_tree_names(repository_root, all_added_names)
686
774
  )
687
- return sweep_diff(diff_process.stdout, preexisting_tuples)
775
+ return _find_terminology_near_misses(diff_process.stdout, preexisting_tuples)
688
776
 
689
777
 
690
778
  def _read_diff_text(diff_file_path: str | None) -> str:
@@ -2,6 +2,7 @@
2
2
 
3
3
  import importlib.util
4
4
  import subprocess
5
+ import sys
5
6
  from pathlib import Path
6
7
  from types import ModuleType
7
8
 
@@ -10,17 +11,26 @@ import pytest
10
11
 
11
12
  def _load_sweep_module() -> ModuleType:
12
13
  module_path = Path(__file__).parent.parent / "terminology_sweep.py"
14
+ module_directory = str(module_path.parent)
15
+ should_remove_module_directory = module_directory not in sys.path
16
+ if should_remove_module_directory:
17
+ sys.path.insert(0, module_directory)
13
18
  spec = importlib.util.spec_from_file_location("terminology_sweep", module_path)
14
19
  assert spec is not None
15
20
  assert spec.loader is not None
16
21
  module = importlib.util.module_from_spec(spec)
17
- spec.loader.exec_module(module)
22
+ try:
23
+ spec.loader.exec_module(module)
24
+ finally:
25
+ if should_remove_module_directory:
26
+ sys.path.remove(module_directory)
18
27
  return module
19
28
 
20
29
 
21
30
  sweep_module = _load_sweep_module()
22
31
  sweep_diff = sweep_module.sweep_diff
23
32
  staged_terminology_findings = sweep_module.staged_terminology_findings
33
+ strict_staged_terminology_findings = sweep_module.strict_staged_terminology_findings
24
34
  main = sweep_module.main
25
35
  parse_added_lines = sweep_module._parse_added_lines
26
36
 
@@ -282,6 +292,8 @@ def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
282
292
  env=_hermetic_git_environment(),
283
293
  )
284
294
  findings = staged_terminology_findings(tmp_path)
295
+ with pytest.raises(RuntimeError, match="git grep"):
296
+ strict_staged_terminology_findings(tmp_path)
285
297
  assert any("premium-request" in each_finding for each_finding in findings)
286
298
 
287
299
 
@@ -1,6 +1,6 @@
1
- # Team-Advisor Skill
1
+ # Team-advisor skill
2
2
 
3
- `/team-advisor` binds one standing warm advisor for this session at the strongest reachable tier. This skill is the selected custom reproduction path for advisor behavior: it forwards explicit packets to a warm Agent/SendMessage advisor or the read-only `codex_sol_advisor.py` path across many decision points.
3
+ `/team-advisor` binds one standing warm advisor for this session at the strongest reachable tier. It sends explicit packets to a warm Agent/SendMessage advisor or the read-only `codex_astra_advisor.py` helper.
4
4
 
5
5
  ## Refs
6
6
 
@@ -17,4 +17,4 @@ Follow the call rules in `advisor-tool.md` (orientation first, then consult befo
17
17
 
18
18
  ## Selected path
19
19
 
20
- `/team-advisor` is the repository's advisor implementation. It provides explicit first-consult packets, delta consults, a standing warm reviewer, an in-session Sol spawn on Codex, and a read-only Sol CLI option.
20
+ `/team-advisor` is the repository's advisor implementation. It provides explicit first-consult packets, delta consults, a standing warm reviewer, an in-session Astra spawn on Codex, and a read-only Astra CLI option.
@@ -2,7 +2,41 @@
2
2
 
3
3
  AST-based Python style checks for code quality enforcement.
4
4
 
5
- The checks live in `python_style_checks.py`; the shared source-line splitting, function-discovery, and statement-classification helpers they build on live in `python_style_helpers.py`. `python_style_import_bootstrap.py` recognizes the repo's sys.path bootstrap-guard idiom so the imports-at-top check does not penalize a file for following it. `hook_timing_harness.py` measures real wall-clock time for the Write/Edit hooks `hooks.json` registers, reporting p50 and p95 over a configurable run count. `fast_save_validators.py` runs the twelve non-Mypy, non-Ruff checks in-process for the Write/Edit save-path gate, with no per-check subprocess.
5
+ The checks live in `python_style_checks.py`. Shared source-line splitting, function discovery, and statement classification live in `python_style_helpers.py`. `python_style_import_bootstrap.py` recognizes the repo's sys.path bootstrap-guard idiom so the imports-at-top check does not penalize a file for following it. `fast_save_validators.py` runs the twelve non-Mypy, non-Ruff checks in-process for the Write/Edit save-path gate, with no per-check subprocess.
6
+
7
+ ## Production modules
8
+
9
+ | File | Role |
10
+ |---|---|
11
+ | `abbreviation_checks.py` | Abbreviation and single-letter name checks |
12
+ | `code_quality_checks.py` | Function, nesting, and file length checks |
13
+ | `comment_checks.py` | Comment detection for the opt-in diff-aware linter |
14
+ | `exempt_paths.py` | Shared config, test, and hook-infrastructure path exemptions |
15
+ | `fast_save_validators.py` | In-process Write/Edit save-path validator roster |
16
+ | `file_structure_checks.py` | File structure checks for pre-PR validation |
17
+ | `git_checks.py` | Git and GitHub checks for pre-push review |
18
+ | `health_check.py` | Validator availability, dependency, and version checks |
19
+ | `hook_timing_harness.py` | Hook runtime measurement and threshold report |
20
+ | `magic_value_checks.py` | Hardcoded magic-number checks |
21
+ | `mypy_integration.py` | Mypy static type checking integration |
22
+ | `output_formatter.py` | Colored, diff, progress, and JSON validator output |
23
+ | `pr_reference_checks.py` | PR and commit reference checks in comments |
24
+ | `project_roots.py` | Project-root resolution from a path |
25
+ | `pyproject_config_discovery.py` | Walk-up discovery of tool pyproject tables |
26
+ | `python_antipattern_checks.py` | Mutable defaults, bare except, and print checks |
27
+ | `python_style_checks.py` | Import placement, decorator spacing, blank lines, and view naming |
28
+ | `python_style_helpers.py` | Source-line splitting and function-discovery helpers |
29
+ | `python_style_import_bootstrap.py` | sys.path bootstrap-guard recognizer |
30
+ | `react_checks.py` | React class-component and error-boundary checks |
31
+ | `ruff_integration.py` | Ruff lint integration |
32
+ | `run_all_validators.py` | Pre-push validator orchestration and report |
33
+ | `security_checks.py` | Hardcoded-secret, SQL injection, and XSS checks |
34
+ | `system_temporary_roots.py` | System temp-root membership for staged copies |
35
+ | `todo_checks.py` | TODO/FIXME tracking with issue-reference requirement |
36
+ | `type_safety_checks.py` | Missing type hints and Any usage checks |
37
+ | `useless_test_checks.py` | Useless-test detection |
38
+ | `validator_base.py` | Shared validator dataclasses, source read, and parse |
39
+ | `validator_defaults.py` | Shared constants for the validators package |
6
40
 
7
41
  ## Checks Implemented
8
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.36.0",
3
+ "version": "4.0.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/check.ps1 CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env pwsh
2
2
  <#
3
3
  .SYNOPSIS
4
- One-shot quality gate — runs ruff, hooks mypy, pr-loop mypy,
5
- process-tree mypy, and the blocking pytest suite from a single entry point.
4
+ One-shot quality gate. Runs ruff, hooks mypy, pr-loop mypy,
5
+ process-tree mypy, repository-policy, and the blocking pytest suite from a
6
+ single entry point.
6
7
 
7
8
  .DESCRIPTION
8
9
  Resolves paths relative to $PSScriptRoot so the script works from any CWD
@@ -11,9 +12,10 @@
11
12
  propagates this file). Tools: ruff over hooks/, mypy over hooks blocking
12
13
  and validators, mypy-pr-loop over _shared/pr-loop/scripts production
13
14
  modules, mypy-process-tree over the shared process-tree kill helper and
14
- its constants, and optional pytest over the blocking enforcer suite. Each tool
15
- runs sequentially; the first non-zero exit code is preserved as the
16
- script's exit code so CI/pre-commit can short-circuit on the first failure.
15
+ its constants, repository-policy over the committed tree, and optional
16
+ pytest over the blocking enforcer suite. Each tool runs in sequence. The
17
+ first non-zero exit code is preserved as the script's exit code so CI and
18
+ pre-commit can stop on the first failure.
17
19
 
18
20
  .PARAMETER SkipTests
19
21
  Skip the pytest run. Useful during local iteration when you want only the
@@ -25,19 +27,23 @@
25
27
  .PARAMETER SkipRuff
26
28
  Skip the ruff run.
27
29
 
30
+ .PARAMETER SkipRepositoryPolicy
31
+ Skip the committed-tree repository_policy.py run.
32
+
28
33
  .PARAMETER CommentPolicyBase
29
34
  Optional git reference used as the comment-policy baseline.
30
35
 
31
36
  .OUTPUTS
32
37
  Per-tool status lines on stdout. Final summary line:
33
38
  CHECK: OK
34
- CHECK: FAILED tools=ruff,mypy,mypy-pr-loop,mypy-process-tree,pytest
39
+ CHECK: FAILED tools=ruff,repository-policy,mypy,mypy-pr-loop,mypy-process-tree,pytest
35
40
  #>
36
41
  [CmdletBinding()]
37
42
  param(
38
43
  [switch]$SkipTests,
39
44
  [switch]$SkipMypy,
40
45
  [switch]$SkipRuff,
46
+ [switch]$SkipRepositoryPolicy,
41
47
  [string]$CommentPolicyBase
42
48
  )
43
49
 
@@ -121,6 +127,12 @@ Invoke-Tool -Label 'comment-policy' -Action {
121
127
  }
122
128
  }
123
129
 
130
+ if (-not $SkipRepositoryPolicy) {
131
+ Invoke-Tool -Label 'repository-policy' -Action {
132
+ python (Join-Path $PSScriptRoot 'repository_policy.py') --repository-root $repositoryRoot
133
+ }
134
+ }
135
+
124
136
  if (-not $SkipMypy) {
125
137
  Invoke-Tool -Label 'mypy' -Action {
126
138
  Push-Location $hooksRoot
@@ -3,11 +3,51 @@ from __future__ import annotations
3
3
  from collections.abc import Iterable
4
4
  from contextlib import redirect_stderr, redirect_stdout
5
5
  from io import StringIO
6
- from pathlib import Path
6
+ from pathlib import Path, PurePosixPath
7
7
 
8
8
  from . import adapter_support
9
9
  from .config import constants
10
- from .model import Diagnostic, Document
10
+ from .model import Diagnostic, Document, DocumentSet, Location, SelectionKind, Severity
11
+
12
+
13
+ def _terminology_diagnostic(finding: str) -> Diagnostic:
14
+ finding_match = constants.TERMINOLOGY_FINDING_PATTERN.fullmatch(finding)
15
+ if finding_match is None:
16
+ raise ValueError(finding)
17
+ return Diagnostic(
18
+ constants.TERMINOLOGY_SWEEP_RULE_ID,
19
+ Severity.ERROR,
20
+ finding_match.group(constants.TERMINOLOGY_MESSAGE_GROUP),
21
+ Location(
22
+ PurePosixPath(finding_match.group(constants.TERMINOLOGY_PATH_GROUP)),
23
+ int(finding_match.group(constants.TERMINOLOGY_LINE_NUMBER_GROUP)),
24
+ ),
25
+ )
26
+
27
+
28
+ def terminology_diagnostics(
29
+ document_set: DocumentSet,
30
+ load_module: adapter_support.HookModuleLoader,
31
+ ) -> tuple[Diagnostic, ...]:
32
+ """Report staged prose that near-misses a newly introduced identifier.
33
+
34
+ Args:
35
+ document_set: Candidate staged documents and repository root.
36
+ load_module: Shared script module loader.
37
+
38
+ Returns:
39
+ Diagnostics that name the prose file and line.
40
+ """
41
+ if document_set.selection is not SelectionKind.STAGED:
42
+ return ()
43
+ terminology_module = load_module(constants.TERMINOLOGY_SWEEP_MODULE_NAME)
44
+ helper_stdout = StringIO()
45
+ helper_stderr = StringIO()
46
+ with redirect_stdout(helper_stdout), redirect_stderr(helper_stderr):
47
+ all_findings = terminology_module.strict_staged_terminology_findings(
48
+ document_set.repository_root
49
+ )
50
+ return tuple(_terminology_diagnostic(each_finding) for each_finding in all_findings)
11
51
 
12
52
 
13
53
  def _stable_validator_message(document: Document, raw_message: str) -> str:
@@ -23,6 +23,16 @@ def _hooks_module(module_name: str) -> ModuleType:
23
23
  return importlib.import_module(module_name)
24
24
 
25
25
 
26
+ def _pr_loop_script_module(module_name: str) -> ModuleType:
27
+ package_directory = Path(__file__).resolve().parent.parent.parent
28
+ scripts_directory = str(
29
+ package_directory.joinpath(*constants.ALL_PR_LOOP_SCRIPTS_PATH_SEGMENTS)
30
+ )
31
+ if scripts_directory not in sys.path:
32
+ sys.path.insert(0, scripts_directory)
33
+ return importlib.import_module(module_name)
34
+
35
+
26
36
  def validator_diagnostics(
27
37
  document: Document, repository_root: Path
28
38
  ) -> tuple[Diagnostic, ...]:
@@ -222,6 +232,20 @@ def test_pairing_diagnostics(document_set: DocumentSet) -> tuple[Diagnostic, ...
222
232
  return adapter_pairing.test_pairing_diagnostics(document_set, _hooks_module)
223
233
 
224
234
 
235
+ def terminology_diagnostics(document_set: DocumentSet) -> tuple[Diagnostic, ...]:
236
+ """Report staged prose that near-misses a newly introduced identifier.
237
+
238
+ Args:
239
+ document_set: Candidate staged documents and repository root.
240
+
241
+ Returns:
242
+ Diagnostics that name the prose file and line.
243
+ """
244
+ return adapter_detectors.terminology_diagnostics(
245
+ document_set, _pr_loop_script_module
246
+ )
247
+
248
+
225
249
  def hook_format_diagnostics(
226
250
  document: Document, repository_root: Path
227
251
  ) -> tuple[Diagnostic, ...]:
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  INCOMPLETE_EXIT_CODE = 3
2
4
  INVALID_INPUT_EXIT_CODE = 2
3
5
  GIT_EXECUTABLE = "git"
@@ -64,3 +66,12 @@ FAST_SAVE_VALIDATORS_STEM = "fast_save_validators"
64
66
  ALL_OVERLAPPING_VALIDATOR_NAMES = frozenset(
65
67
  {"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
66
68
  )
69
+ ALL_PR_LOOP_SCRIPTS_PATH_SEGMENTS = ("_shared", "pr-loop", "scripts")
70
+ TERMINOLOGY_SWEEP_MODULE_NAME = "terminology_sweep"
71
+ TERMINOLOGY_SWEEP_RULE_ID = "terminology-sweep"
72
+ TERMINOLOGY_FINDING_PATTERN = re.compile(
73
+ r"^(?P<path>.+):(?P<line_number>\d+): (?P<message>.+)$"
74
+ )
75
+ TERMINOLOGY_PATH_GROUP = "path"
76
+ TERMINOLOGY_LINE_NUMBER_GROUP = "line_number"
77
+ TERMINOLOGY_MESSAGE_GROUP = "message"
@@ -180,7 +180,7 @@ def _run_changeset_rule(
180
180
  all_diagnostics: list[Diagnostic],
181
181
  all_skipped_rules: list[str],
182
182
  ) -> None:
183
- if document_set.selection not in {SelectionKind.STAGED, SelectionKind.BASE}:
183
+ if document_set.selection not in rule.selections:
184
184
  all_skipped_rules.append(rule.rule_id)
185
185
  return
186
186
  all_diagnostics.extend(rule.check(document_set))
@@ -284,6 +284,7 @@ class DocumentRule:
284
284
  class ChangeSetRule:
285
285
  rule_id: str
286
286
  rule_sets: frozenset[str]
287
+ selections: frozenset[SelectionKind]
287
288
  check: DocumentSetChecker
288
289
 
289
290