claude-dev-env 2.36.0 → 4.1.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 (84) hide show
  1. package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
  2. package/.agents/skills/syncing-submodules/SKILL.md +101 -0
  3. package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
  4. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
  5. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
  6. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
  8. package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
  10. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
  11. package/.agents/skills/team-advisor/SKILL.md +11 -11
  12. package/_shared/advisor/advisor-protocol.md +51 -51
  13. package/_shared/advisor/reference/advisor-block.md +9 -9
  14. package/_shared/advisor/reference/astra-rung.md +39 -0
  15. package/_shared/advisor/reference/identity.md +4 -4
  16. package/_shared/advisor/reference/lifecycle.md +6 -6
  17. package/_shared/advisor/reference/spawn-walk-log.md +9 -9
  18. package/_shared/advisor/reference/third-party-bind.md +11 -11
  19. package/_shared/advisor/reference/warm-up.md +13 -13
  20. package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
  21. package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
  22. package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
  23. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
  24. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
  25. package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
  26. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
  27. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
  28. package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
  29. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
  30. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
  31. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
  33. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
  34. package/bin/ever-shipped-skills.mjs +1 -0
  35. package/bin/ever-shipped-skills.test.mjs +30 -0
  36. package/bin/git_hooks_installer.mjs +0 -1
  37. package/bin/git_hooks_installer.test.mjs +16 -1
  38. package/bin/install.mjs +5 -2
  39. package/bin/install.prune.test.mjs +36 -24
  40. package/bin/install.test.mjs +7 -0
  41. package/docs/references/team-advisor-skill.md +3 -3
  42. package/hooks/validators/README.md +35 -1
  43. package/package.json +1 -1
  44. package/scripts/check.ps1 +18 -6
  45. package/scripts/policy_lint/adapter_detectors.py +42 -2
  46. package/scripts/policy_lint/adapters.py +24 -0
  47. package/scripts/policy_lint/config/constants.py +11 -0
  48. package/scripts/policy_lint/engine.py +1 -1
  49. package/scripts/policy_lint/model.py +1 -0
  50. package/scripts/policy_lint/registry.py +10 -1
  51. package/scripts/repository_checks/__init__.py +1 -0
  52. package/scripts/repository_checks/claude_md.py +108 -0
  53. package/scripts/repository_checks/config/__init__.py +1 -0
  54. package/scripts/repository_checks/config/constants.py +150 -0
  55. package/scripts/repository_checks/env_var_documentation.py +243 -0
  56. package/scripts/repository_checks/hook_modules.py +34 -0
  57. package/scripts/repository_checks/models.py +37 -0
  58. package/scripts/repository_checks/package_inventory.py +118 -0
  59. package/scripts/repository_checks/pytest_testpaths.py +84 -0
  60. package/scripts/repository_checks/reporting.py +39 -0
  61. package/scripts/repository_checks/runner.py +93 -0
  62. package/scripts/repository_checks/tracked_secrets.py +147 -0
  63. package/scripts/repository_checks/tracked_tree.py +28 -0
  64. package/scripts/repository_policy.py +120 -0
  65. package/scripts/tests/repository_policy_test_support.py +89 -0
  66. package/scripts/tests/test_cde_lint.py +25 -1
  67. package/scripts/tests/test_claude_md.py +68 -0
  68. package/scripts/tests/test_env_var_documentation.py +133 -0
  69. package/scripts/tests/test_hook_modules.py +17 -0
  70. package/scripts/tests/test_models.py +26 -0
  71. package/scripts/tests/test_package_inventory.py +96 -0
  72. package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
  73. package/scripts/tests/test_pytest_testpaths.py +66 -0
  74. package/scripts/tests/test_reporting.py +54 -0
  75. package/scripts/tests/test_repository_policy.py +71 -0
  76. package/scripts/tests/test_runner.py +59 -0
  77. package/scripts/tests/test_tracked_secrets.py +175 -0
  78. package/scripts/tests/test_tracked_tree.py +28 -0
  79. package/_shared/advisor/reference/sol-rung.md +0 -39
  80. package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
  81. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
  82. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
  83. package/hooks/git-hooks/post_commit.py +0 -241
  84. package/hooks/git-hooks/test_post_commit.py +0 -298
@@ -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
 
@@ -119,6 +119,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
119
119
  'source-command-sr-loop',
120
120
  'split-pr',
121
121
  'structure-prompt',
122
+ 'syncing-submodules',
122
123
  'task-build',
123
124
  'team-advisor',
124
125
  'update',
@@ -0,0 +1,30 @@
1
+ import { strict as assert } from 'node:assert';
2
+ import { test } from 'node:test';
3
+ import { existsSync, readdirSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
7
+ import {
8
+ MANAGED_SKILLS_DIRECTORY_NAME,
9
+ PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
10
+ } from './install-constants.mjs';
11
+
12
+ const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
13
+ const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
14
+
15
+ test('EVER_SHIPPED_SKILL_NAMES includes every current shipped skill', () => {
16
+ const sourceSkillsDirectory = join(
17
+ PACKAGE_DIRECTORY,
18
+ PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
19
+ MANAGED_SKILLS_DIRECTORY_NAME,
20
+ );
21
+ const allCurrentSkillNames = readdirSync(sourceSkillsDirectory, { withFileTypes: true })
22
+ .filter(eachEntry => eachEntry.isDirectory())
23
+ .filter(eachEntry => existsSync(join(sourceSkillsDirectory, eachEntry.name, 'SKILL.md')))
24
+ .map(eachEntry => eachEntry.name);
25
+ const allUncoveredSkillNames = allCurrentSkillNames.filter(
26
+ eachSkillName => !EVER_SHIPPED_SKILL_NAMES.has(eachSkillName),
27
+ );
28
+
29
+ assert.deepEqual(allUncoveredSkillNames, [], 'every current skill must enter the fallback set');
30
+ });
@@ -6,7 +6,6 @@ import { join } from 'node:path';
6
6
  export const KNOWN_GIT_HOOK_NAMES = Object.freeze([
7
7
  'pre-commit',
8
8
  'pre-push',
9
- 'post-commit',
10
9
  ]);
11
10
 
12
11
  export const SHIM_DOCSTRING = (
@@ -58,6 +58,22 @@ test('writeAllGitHookShims creates one shim per known hook name', () => {
58
58
  });
59
59
 
60
60
 
61
+ test('known git hooks exclude post-commit', () => {
62
+ assert.equal(KNOWN_GIT_HOOK_NAMES.includes('post-commit'), false);
63
+ });
64
+
65
+
66
+ test('writeAllGitHookShims does not create a post-commit shim', () => {
67
+ const { temporaryRoot, gitHooksDirectory } = makeTemporaryGitHooksDirectory();
68
+ try {
69
+ writeAllGitHookShims({ gitHooksDirectory });
70
+ assert.equal(existsSync(join(gitHooksDirectory, 'post-commit')), false);
71
+ } finally {
72
+ rmSync(temporaryRoot, { recursive: true, force: true });
73
+ }
74
+ });
75
+
76
+
61
77
  test('configureGlobalGitHooksPath sets the path when nothing is currently configured', () => {
62
78
  const commandsRun = [];
63
79
  const gitConfigReaderReturningEmpty = () => '';
@@ -205,4 +221,3 @@ test('writeGitHookShim rejects hooks directory that is a symlink (loopP5c-5)', (
205
221
  }
206
222
  });
207
223
 
208
-
package/bin/install.mjs CHANGED
@@ -6,7 +6,10 @@ import { homedir } from 'node:os';
6
6
  import { execSync, execFileSync, spawnSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createRequire } from 'node:module';
9
- import { installAllGitHooks } from './git_hooks_installer.mjs';
9
+ import {
10
+ installAllGitHooks,
11
+ KNOWN_GIT_HOOK_NAMES,
12
+ } from './git_hooks_installer.mjs';
10
13
  import { installMypyIniForClaudeHooks } from './install_mypy_ini.mjs';
11
14
  import { expandHomeDirectoryTokensInSettings } from './expand_home_directory_tokens.mjs';
12
15
  import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
@@ -2368,7 +2371,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2368
2371
  } else {
2369
2372
  console.warn(` Git hooks: ${gitHookInstallationResult.hooksPathConfigurationResult.reason}`);
2370
2373
  }
2371
- console.log(` Git hook shims: ${gitHookInstallationResult.createdShimPaths.length} files (pre-commit, pre-push, post-commit)`);
2374
+ console.log(` Git hook shims: ${gitHookInstallationResult.createdShimPaths.length} files (${KNOWN_GIT_HOOK_NAMES.join(', ')})`);
2372
2375
  syncWrittenPaths([...allInstalledFiles, ...publishedPointerPaths]);
2373
2376
  throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
2374
2377
  throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
@@ -13,15 +13,8 @@ import {
13
13
  import { tmpdir } from 'node:os';
14
14
  import { join, dirname, basename } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
- import {
17
- MANAGED_SKILLS_DIRECTORY_NAME,
18
- PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
19
- } from './install-constants.mjs';
20
- import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
21
-
22
16
  const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
23
17
  const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
24
- const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
25
18
  const INSTALLER_PROCESS_TIMEOUT_MS = 60_000;
26
19
  const EXCLUDED_PACKAGE_COPY_DIRECTORY = 'node_modules';
27
20
 
@@ -50,6 +43,11 @@ const PRIOR_RUN_BACKUP_DIRECTORY_NAMES = [
50
43
  ];
51
44
  const SKILLS_DIRECTORY_NAME = 'skills';
52
45
  const HOOKS_DIRECTORY_NAME = 'hooks';
46
+ const GIT_HOOKS_DIRECTORY_NAME = 'git-hooks';
47
+ const PRE_COMMIT_HOOK_NAME = 'pre-commit';
48
+ const PRE_PUSH_HOOK_NAME = 'pre-push';
49
+ const POST_COMMIT_HOOK_NAME = 'post-commit';
50
+ const FOREIGN_HOOK_NAME = 'foreign-hook';
53
51
  const SETTINGS_FILE_NAME = 'settings.json';
54
52
  const MANIFEST_FILE_NAME = '.claude-dev-env-manifest.json';
55
53
  const CLAUDE_HUB_FILE_NAME = 'CLAUDE.md';
@@ -117,23 +115,6 @@ function createSandbox(homeDirectoryPrefix = 'cdev-prune-home-') {
117
115
  return { homeDirectory, claudeDirectory, skillsDirectory, manifestPath };
118
116
  }
119
117
 
120
- test('the retired-skill fallback covers every current shipped skill', () => {
121
- const sourceSkillsDirectory = join(
122
- PACKAGE_DIRECTORY,
123
- PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
124
- MANAGED_SKILLS_DIRECTORY_NAME,
125
- );
126
- const allCurrentSkillNames = readdirSync(sourceSkillsDirectory, { withFileTypes: true })
127
- .filter(eachEntry => eachEntry.isDirectory())
128
- .filter(eachEntry => existsSync(join(sourceSkillsDirectory, eachEntry.name, 'SKILL.md')))
129
- .map(eachEntry => eachEntry.name);
130
- const allUncoveredSkillNames = allCurrentSkillNames.filter(
131
- eachSkillName => !EVER_SHIPPED_SKILL_NAMES.has(eachSkillName),
132
- );
133
-
134
- assert.deepEqual(allUncoveredSkillNames, [], 'every current skill must enter the fallback set');
135
- });
136
-
137
118
  test('a core install runs the spaced-path Write dispatcher and captures its red denial', () => {
138
119
  const sandbox = createSandbox('cdev prune & (home)-');
139
120
  try {
@@ -1003,6 +984,37 @@ test('a full reinstall leaves a manifest-recorded path under no managed root in
1003
984
  }
1004
985
  });
1005
986
 
987
+ test('a reinstall removes a retired post-commit shim and preserves a foreign git hook', () => {
988
+ const sandbox = createSandbox();
989
+ try {
990
+ runInstaller(sandbox.homeDirectory, []);
991
+ const gitHooksDirectory = join(
992
+ sandbox.claudeDirectory, HOOKS_DIRECTORY_NAME, GIT_HOOKS_DIRECTORY_NAME,
993
+ );
994
+ const postCommitShimPath = join(gitHooksDirectory, POST_COMMIT_HOOK_NAME);
995
+ const foreignHookPath = join(gitHooksDirectory, FOREIGN_HOOK_NAME);
996
+ const foreignHookContentBytes = Buffer.from('foreign hook content\n', 'utf8');
997
+ writeFileWithParents(postCommitShimPath, 'generated by an earlier install\n');
998
+ writeFileWithParents(foreignHookPath, foreignHookContentBytes);
999
+ appendManifestFiles(sandbox.manifestPath, [postCommitShimPath]);
1000
+
1001
+ runInstaller(sandbox.homeDirectory, []);
1002
+
1003
+ const freshManifest = readManifest(sandbox.manifestPath);
1004
+ assert.equal(existsSync(postCommitShimPath), false);
1005
+ assert.deepEqual(readFileSync(foreignHookPath), foreignHookContentBytes);
1006
+ assert.equal(freshManifest.files.includes(postCommitShimPath), false);
1007
+ assert.equal(freshManifest.files.includes(foreignHookPath), false);
1008
+ for (const gitHookName of [PRE_COMMIT_HOOK_NAME, PRE_PUSH_HOOK_NAME]) {
1009
+ const generatedShimPath = join(gitHooksDirectory, gitHookName);
1010
+ assert.equal(existsSync(generatedShimPath), true);
1011
+ assert.equal(freshManifest.files.includes(generatedShimPath), true);
1012
+ }
1013
+ } finally {
1014
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
1015
+ }
1016
+ });
1017
+
1006
1018
  test('a full reinstall drops a retired hook script and its settings entry in one run', () => {
1007
1019
  const sandbox = createSandbox();
1008
1020
  try {
@@ -51,6 +51,13 @@ import {
51
51
  } from './expand_home_directory_tokens.mjs';
52
52
 
53
53
 
54
+ test('installer reports the authoritative Git hook names', () => {
55
+ const installerSource = readFileSync(new URL('./install.mjs', import.meta.url), 'utf8');
56
+ assert.match(installerSource, /KNOWN_GIT_HOOK_NAMES\.join\(', '\)/);
57
+ assert.doesNotMatch(installerSource, /pre-commit, pre-push, post-commit/);
58
+ });
59
+
60
+
54
61
  function createTemporaryGitRepository() {
55
62
  const repositoryRoot = mkdtempSync(join(tmpdir(), 'cdev-installer-validation-'));
56
63
  const gitOptions = { cwd: repositoryRoot, stdio: 'ignore' };
@@ -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.1.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {