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.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/syncing-submodules/SKILL.md +101 -0
- package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
- package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
- package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
- package/.agents/skills/team-advisor/SKILL.md +11 -11
- package/_shared/advisor/advisor-protocol.md +51 -51
- package/_shared/advisor/reference/advisor-block.md +9 -9
- package/_shared/advisor/reference/astra-rung.md +39 -0
- package/_shared/advisor/reference/identity.md +4 -4
- package/_shared/advisor/reference/lifecycle.md +6 -6
- package/_shared/advisor/reference/spawn-walk-log.md +9 -9
- package/_shared/advisor/reference/third-party-bind.md +11 -11
- package/_shared/advisor/reference/warm-up.md +13 -13
- package/_shared/advisor/scripts/codex_astra_advisor.py +308 -0
- package/_shared/advisor/scripts/codex_astra_preflight.py +115 -0
- package/_shared/advisor/scripts/codex_astra_reply.py +159 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +3 -3
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +38 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +17 -18
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +271 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +24 -0
- package/_shared/advisor/scripts/tests/test_codex_astra_reply.py +42 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +55 -30
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +14 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +14 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +138 -50
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -1
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/ever-shipped-skills.test.mjs +30 -0
- package/bin/git_hooks_installer.mjs +0 -1
- package/bin/git_hooks_installer.test.mjs +16 -1
- package/bin/install.mjs +5 -2
- package/bin/install.prune.test.mjs +36 -24
- package/bin/install.test.mjs +7 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/validators/README.md +35 -1
- package/package.json +1 -1
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/adapter_detectors.py +42 -2
- package/scripts/policy_lint/adapters.py +24 -0
- package/scripts/policy_lint/config/constants.py +11 -0
- package/scripts/policy_lint/engine.py +1 -1
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +10 -1
- package/scripts/repository_checks/__init__.py +1 -0
- package/scripts/repository_checks/claude_md.py +108 -0
- package/scripts/repository_checks/config/__init__.py +1 -0
- package/scripts/repository_checks/config/constants.py +150 -0
- package/scripts/repository_checks/env_var_documentation.py +243 -0
- package/scripts/repository_checks/hook_modules.py +34 -0
- package/scripts/repository_checks/models.py +37 -0
- package/scripts/repository_checks/package_inventory.py +118 -0
- package/scripts/repository_checks/pytest_testpaths.py +84 -0
- package/scripts/repository_checks/reporting.py +39 -0
- package/scripts/repository_checks/runner.py +93 -0
- package/scripts/repository_checks/tracked_secrets.py +147 -0
- package/scripts/repository_checks/tracked_tree.py +28 -0
- package/scripts/repository_policy.py +120 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +25 -1
- package/scripts/tests/test_claude_md.py +68 -0
- package/scripts/tests/test_env_var_documentation.py +133 -0
- package/scripts/tests/test_hook_modules.py +17 -0
- package/scripts/tests/test_models.py +26 -0
- package/scripts/tests/test_package_inventory.py +96 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +117 -0
- package/scripts/tests/test_pytest_testpaths.py +66 -0
- package/scripts/tests/test_reporting.py +54 -0
- package/scripts/tests/test_repository_policy.py +71 -0
- package/scripts/tests/test_runner.py +59 -0
- package/scripts/tests/test_tracked_secrets.py +175 -0
- package/scripts/tests/test_tracked_tree.py +28 -0
- package/_shared/advisor/reference/sol-rung.md +0 -39
- package/_shared/advisor/scripts/codex_sol_advisor.py +0 -581
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +0 -38
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +0 -740
- package/hooks/git-hooks/post_commit.py +0 -241
- 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
|
|
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
|
|
18
|
-
and differs on the last. A term that diverges in any earlier
|
|
19
|
-
only one leading word, is ordinary prose. It also spares
|
|
20
|
-
tails, singular/plural forms, stopword windows, and words
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
205
|
-
generated source, file trees
|
|
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
|
|
264
|
-
a key, a path
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
+
});
|
|
@@ -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 {
|
|
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 (
|
|
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 {
|
package/bin/install.test.mjs
CHANGED
|
@@ -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-
|
|
1
|
+
# Team-advisor skill
|
|
2
2
|
|
|
3
|
-
`/team-advisor` binds one standing warm advisor for this session at the strongest reachable tier.
|
|
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
|
|
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
|
|
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
|
|