claude-dev-env 2.35.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.
- package/.agents/skills/_shared/advisor/scripts/README.md +1 -1
- package/.agents/skills/autoconverge/SKILL.md +19 -15
- package/.agents/skills/autoconverge/reference/gotchas.md +3 -3
- package/.agents/skills/autoconverge/reference/multi-pr.md +6 -2
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +11 -0
- package/.agents/skills/autoconverge/workflow/converge.mjs +26 -33
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +26 -2
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +12 -28
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +16 -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/cde.mjs +175 -0
- package/bin/cde.test.mjs +222 -0
- package/bin/install-constants.mjs +4 -0
- package/bin/install.cursor-rules.test.mjs +233 -2
- package/bin/install.mjs +49 -3
- package/bin/pstack-model-preferences.codex.json +23 -0
- package/bin/select-pstack-models.test.mjs +378 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/hooks/hooks_constants/tool_names.py +20 -0
- package/hooks/validators/README.md +35 -1
- package/hooks/validators/fast_save_validators.py +63 -89
- package/hooks/validators/run_all_validators.py +16 -8
- package/hooks/validators/test_fast_save_validators_policy_lint.py +25 -0
- package/hooks/validators/test_run_all_validators_policy_lint.py +52 -0
- package/package.json +3 -2
- package/rules/pstack-models.md +119 -0
- package/scripts/cde_lint.py +258 -0
- package/scripts/check.ps1 +18 -6
- package/scripts/policy_lint/__init__.py +41 -0
- package/scripts/policy_lint/adapter_configuration.py +91 -0
- package/scripts/policy_lint/adapter_detectors.py +368 -0
- package/scripts/policy_lint/adapter_pairing.py +145 -0
- package/scripts/policy_lint/adapter_support.py +97 -0
- package/scripts/policy_lint/adapters.py +376 -0
- package/scripts/policy_lint/config/__init__.py +1 -0
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/engine.py +224 -0
- package/scripts/policy_lint/model.py +388 -0
- package/scripts/policy_lint/registry.py +171 -0
- package/scripts/policy_lint/render.py +91 -0
- package/scripts/policy_lint/selection.py +372 -0
- package/scripts/policy_lint/selection_git.py +147 -0
- 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/select_pstack_models.mjs +304 -0
- package/scripts/tests/repository_policy_test_support.py +89 -0
- package/scripts/tests/test_cde_lint.py +398 -0
- 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_engine.py +381 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +369 -0
- package/scripts/tests/test_policy_lint_selection.py +373 -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
|
@@ -19,7 +19,7 @@ from dataclasses import dataclass
|
|
|
19
19
|
from functools import lru_cache
|
|
20
20
|
from datetime import datetime
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Callable, Dict, List, Optional, Tuple
|
|
22
|
+
from typing import Callable, Collection, Dict, List, Optional, Tuple
|
|
23
23
|
|
|
24
24
|
from ._path_setup import hooks_directory_on_path # noqa: F401
|
|
25
25
|
from .config.baseline_identity_constants import (
|
|
@@ -51,7 +51,7 @@ from blocking.codex_apply_patch import (
|
|
|
51
51
|
parse_codex_apply_patch,
|
|
52
52
|
payload_patch_command,
|
|
53
53
|
)
|
|
54
|
-
from hooks_constants.
|
|
54
|
+
from hooks_constants.tool_names import (
|
|
55
55
|
APPLY_PATCH_TOOL_NAME,
|
|
56
56
|
)
|
|
57
57
|
from hooks_constants.hook_block_logger import log_hook_block
|
|
@@ -885,6 +885,8 @@ def validate_proposed_file(
|
|
|
885
885
|
file_path: str,
|
|
886
886
|
proposed_content: str,
|
|
887
887
|
config_source_path: Optional[Path] = None,
|
|
888
|
+
include_ruff: bool = True,
|
|
889
|
+
excluded_validator_names: Collection[str] = (),
|
|
888
890
|
) -> List[ValidatorResult]:
|
|
889
891
|
"""Validate *proposed_content* as if written to *file_path*.
|
|
890
892
|
|
|
@@ -901,10 +903,12 @@ def validate_proposed_file(
|
|
|
901
903
|
proposed_content: The reconstructed post-edit content of that file.
|
|
902
904
|
config_source_path: Path Ruff resolves its config from; defaults to
|
|
903
905
|
*file_path* when the caller passes nothing.
|
|
906
|
+
include_ruff: Whether to run Ruff after the in-process validators.
|
|
907
|
+
excluded_validator_names: Display names omitted from the in-process run.
|
|
904
908
|
|
|
905
909
|
Returns:
|
|
906
|
-
One ValidatorResult per
|
|
907
|
-
here; CLI/full mode is the only caller that still runs it.
|
|
910
|
+
One ValidatorResult per in-process check, plus Ruff when requested.
|
|
911
|
+
Mypy never runs here; CLI/full mode is the only caller that still runs it.
|
|
908
912
|
"""
|
|
909
913
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
910
914
|
temporary_file = _temporary_path_preserving_directory_signal(
|
|
@@ -916,10 +920,14 @@ def validate_proposed_file(
|
|
|
916
920
|
if config_source_path is not None
|
|
917
921
|
else Path(file_path)
|
|
918
922
|
)
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
+
all_results = run_fast_save_validators(
|
|
924
|
+
[temporary_file], excluded_validator_names
|
|
925
|
+
)
|
|
926
|
+
if include_ruff:
|
|
927
|
+
all_results.append(
|
|
928
|
+
run_ruff_checks([temporary_file], resolved_config_source_path)
|
|
929
|
+
)
|
|
930
|
+
return all_results
|
|
923
931
|
|
|
924
932
|
|
|
925
933
|
def _validator_summaries(results: List[ValidatorResult]) -> str:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Prove the fast-save validator seam can omit native or duplicate checks."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .fast_save_validators import run_fast_save_validators
|
|
6
|
+
|
|
7
|
+
_CLEAN_SOURCE = (
|
|
8
|
+
"def add_two_numbers(first_number: int, second_number: int) -> int:\n"
|
|
9
|
+
" return first_number + second_number\n"
|
|
10
|
+
)
|
|
11
|
+
_OVERLAPPING_VALIDATOR_NAMES = frozenset(
|
|
12
|
+
{"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_fast_save_validators_excludes_requested_outcomes(tmp_path: Path) -> None:
|
|
17
|
+
target_path = tmp_path / "probe.py"
|
|
18
|
+
target_path.write_text(_CLEAN_SOURCE, encoding="utf-8")
|
|
19
|
+
all_results = run_fast_save_validators(
|
|
20
|
+
[target_path], _OVERLAPPING_VALIDATOR_NAMES
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
all_result_names = {each_result.name for each_result in all_results}
|
|
24
|
+
assert all_result_names.isdisjoint(_OVERLAPPING_VALIDATOR_NAMES)
|
|
25
|
+
assert "Abbreviations" in all_result_names
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Prove the policy-linter validator seam keeps native tools separate."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from . import run_all_validators
|
|
8
|
+
|
|
9
|
+
_CLEAN_SOURCE = (
|
|
10
|
+
"def add_two_numbers(first_number: int, second_number: int) -> int:\n"
|
|
11
|
+
" return first_number + second_number\n"
|
|
12
|
+
)
|
|
13
|
+
_OVERLAPPING_VALIDATOR_NAMES = frozenset(
|
|
14
|
+
{"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _result_names(all_results: list[run_all_validators.ValidatorResult]) -> set[str]:
|
|
19
|
+
return {each_result.name for each_result in all_results}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_policy_lint_can_run_only_unique_in_process_validators(tmp_path: Path) -> None:
|
|
23
|
+
all_results = run_all_validators.validate_proposed_file(
|
|
24
|
+
str(tmp_path / "probe.py"),
|
|
25
|
+
_CLEAN_SOURCE,
|
|
26
|
+
include_ruff=False,
|
|
27
|
+
excluded_validator_names=_OVERLAPPING_VALIDATOR_NAMES,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
all_result_names = _result_names(all_results)
|
|
31
|
+
assert all_result_names.isdisjoint(_OVERLAPPING_VALIDATOR_NAMES)
|
|
32
|
+
assert "Ruff" not in all_result_names
|
|
33
|
+
assert "Abbreviations" in all_result_names
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_validate_proposed_file_keeps_ruff_as_an_explicit_native_step(
|
|
37
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
38
|
+
) -> None:
|
|
39
|
+
monkeypatch.setattr(
|
|
40
|
+
run_all_validators,
|
|
41
|
+
"run_ruff_checks",
|
|
42
|
+
lambda _files, _config_source_path: run_all_validators.ValidatorResult(
|
|
43
|
+
"Ruff", "37", True, "clean"
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
all_results = run_all_validators.validate_proposed_file(
|
|
47
|
+
str(tmp_path / "probe.py"),
|
|
48
|
+
_CLEAN_SOURCE,
|
|
49
|
+
excluded_validator_names=_OVERLAPPING_VALIDATOR_NAMES,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert "Ruff" in _result_names(all_results)
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "
|
|
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": {
|
|
7
7
|
"claude-dev-env": "bin/install.mjs",
|
|
8
|
-
"codex-compat": "bin/codex-compat.mjs"
|
|
8
|
+
"codex-compat": "bin/codex-compat.mjs",
|
|
9
|
+
"cde": "bin/cde.mjs"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"test": "node --test \"bin/*.test.mjs\" \".agents/skills/**/*.test.mjs\" \"tests/fresh-session/**/*.test.mjs\""
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Pstack delegation policy
|
|
2
|
+
|
|
3
|
+
Pstack uses portable role requirements. Model IDs belong to host preference
|
|
4
|
+
files. The active native subagent tool decides which model IDs the current host
|
|
5
|
+
accepts.
|
|
6
|
+
|
|
7
|
+
This policy replaces the fixed model defaults in pstack skills and setup-pstack's
|
|
8
|
+
legacy step that writes one universal model rule. Setup updates only the selected
|
|
9
|
+
host preference JSON. The installer keeps this portable policy current.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
When setup-pstack runs, read the active native subagent tool metadata. Use the
|
|
14
|
+
host name from that tool or the current host context. Do not infer the host from
|
|
15
|
+
CODEX_HOME, CLAUDE_CONFIG_DIR, or another environment variable.
|
|
16
|
+
|
|
17
|
+
Save choices in the agents home under
|
|
18
|
+
`rules/pstack-model-preferences.<host>.json`. Keep each host in its own file.
|
|
19
|
+
The file contains this shape:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"host": "current-host",
|
|
24
|
+
"modelsByRole": {
|
|
25
|
+
"feature, refactoring": ["confirmed-host-model"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Offer only model IDs the active native tool accepts. Keep the user's order.
|
|
31
|
+
Treat `inherit-parent` and `auto` as parent inheritance, not model IDs. A
|
|
32
|
+
different host starts with its own confirmed choices. It never copies the Codex
|
|
33
|
+
preference file.
|
|
34
|
+
|
|
35
|
+
## Selection before every delegation
|
|
36
|
+
|
|
37
|
+
Refresh the accepted model IDs from the active native subagent tool immediately
|
|
38
|
+
before each native delegation. Run the installed selector once for each agent.
|
|
39
|
+
Use `~/.agents/scripts/select_pstack_models.mjs` for the main profile. For a
|
|
40
|
+
named or explicit profile, use the selector under that profile's sibling agents
|
|
41
|
+
home.
|
|
42
|
+
|
|
43
|
+
Send one JSON object through standard input. Use the active host shell on
|
|
44
|
+
other operating systems:
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
$selectorInput = @'
|
|
48
|
+
{
|
|
49
|
+
"host": "current-host",
|
|
50
|
+
"inventoryHost": "current-host",
|
|
51
|
+
"role": "how critics",
|
|
52
|
+
"delegationIndex": 0,
|
|
53
|
+
"panel": {
|
|
54
|
+
"agentCount": 3,
|
|
55
|
+
"requiresDistinctModels": false
|
|
56
|
+
},
|
|
57
|
+
"availableModelIds": ["confirmed-host-model"],
|
|
58
|
+
"confirmedSuitableModelIds": [],
|
|
59
|
+
"parentModelId": "current-parent-model",
|
|
60
|
+
"parentFallback": {
|
|
61
|
+
"isAllowed": true,
|
|
62
|
+
"hasMaterialCapabilityLoss": false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
'@
|
|
66
|
+
$selection = $selectorInput | node "$HOME/.agents/scripts/select_pstack_models.mjs"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`availableModelIds` comes from the current native tool metadata.
|
|
70
|
+
`confirmedSuitableModelIds` contains available alternatives whose suitability
|
|
71
|
+
for this role the host metadata or user has confirmed. Do not rank or classify a
|
|
72
|
+
model from its name.
|
|
73
|
+
|
|
74
|
+
Use the returned `nativeSpawnArguments` in the native subagent call. An empty
|
|
75
|
+
object with `omitNativeModelArgument: true` means parent inheritance, so omit
|
|
76
|
+
the native `model` argument. Stop when `canDelegate` is false. Ask the user
|
|
77
|
+
when `requiresUserChoice` is true.
|
|
78
|
+
|
|
79
|
+
Run the selector again before the next agent call. Pass the same panel contract
|
|
80
|
+
and the new delegation index with the refreshed inventory. Report
|
|
81
|
+
`panel.selectedModelIds` and `panel.isCrossModelDiverse` as returned. A panel
|
|
82
|
+
that repeats one model is a same-model panel.
|
|
83
|
+
|
|
84
|
+
## Portable role requirements
|
|
85
|
+
|
|
86
|
+
feature, refactoring: reliable code execution
|
|
87
|
+
bug-fix: careful diagnosis and code execution
|
|
88
|
+
perf-issue: systems reasoning and performance analysis
|
|
89
|
+
hillclimb: strong iterative judgment
|
|
90
|
+
judgment and prose: strong judgment and clear prose
|
|
91
|
+
hardest tasks: highest available reasoning capability
|
|
92
|
+
how explorer: fast codebase exploration
|
|
93
|
+
how explainer: clear technical synthesis
|
|
94
|
+
how critics: independent technical criticism
|
|
95
|
+
why investigators: fast evidence gathering
|
|
96
|
+
why synthesizer: strong evidence synthesis
|
|
97
|
+
reflect tooling: reliable tool and workflow analysis
|
|
98
|
+
reflect judgment, divergent, synthesizer: strong judgment and divergent analysis
|
|
99
|
+
arena runners: independent solution development
|
|
100
|
+
arena cross-judge pool: independent comparative judgment
|
|
101
|
+
swarm workers: fast bounded task execution
|
|
102
|
+
architect runners: independent architecture design
|
|
103
|
+
interrogate reviewers: independent adversarial review
|
|
104
|
+
|
|
105
|
+
## Panel requirements
|
|
106
|
+
|
|
107
|
+
Panel size and model diversity are separate from model preferences.
|
|
108
|
+
|
|
109
|
+
- How critics use three agents by default. Cross-model diversity is optional.
|
|
110
|
+
- Arena runners use three agents by default and require distinct models.
|
|
111
|
+
- The arena cross-judge pool selects one judge. Pass `parentModelId` so the
|
|
112
|
+
selector chooses another available preference before the parent's model.
|
|
113
|
+
- Swarm uses the requested worker count. Same-model workers are valid.
|
|
114
|
+
- Architect uses two agents by default and requires distinct models.
|
|
115
|
+
- Interrogate uses three agents by default and requires distinct models.
|
|
116
|
+
|
|
117
|
+
A required-diversity panel stops when the current host lacks enough distinct
|
|
118
|
+
confirmed models. Parent inheritance is the last fallback. When inheritance
|
|
119
|
+
causes material capability loss, stop and ask the user before delegating.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the policy linter."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Callable, Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Never, TextIO
|
|
11
|
+
|
|
12
|
+
from policy_lint.config.constants import INVALID_INPUT_EXIT_CODE
|
|
13
|
+
from policy_lint.engine import lint
|
|
14
|
+
from policy_lint.model import LintReport, LintRequest, SelectionKind, TextDocument
|
|
15
|
+
from policy_lint.render import ReportFormat, render
|
|
16
|
+
from policy_lint.selection import SelectionRunFatal
|
|
17
|
+
from policy_lint.selection_git import GitSelectionError, git_bytes_for
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _LintUsageError(ValueError):
|
|
21
|
+
"""Raised when command arguments cannot form a source selection."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _LintArgumentParser(argparse.ArgumentParser):
|
|
25
|
+
def error(self, message: str) -> Never:
|
|
26
|
+
"""Raise a usage error instead of printing and exiting.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
message: Parser error text.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
_LintUsageError: Always.
|
|
33
|
+
"""
|
|
34
|
+
raise _LintUsageError(message)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _discover_repository_root(starting_directory: Path) -> Path:
|
|
38
|
+
"""Find the Git root that contains the command's current directory.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
starting_directory: Directory where the command started.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The resolved Git repository root.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
_LintUsageError: If the directory is not inside a Git repository.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
root_bytes = git_bytes_for(
|
|
51
|
+
starting_directory.resolve(), ("rev-parse", "--show-toplevel")
|
|
52
|
+
)
|
|
53
|
+
except (GitSelectionError, OSError) as error:
|
|
54
|
+
raise _LintUsageError("Current directory is not inside a Git repository") from error
|
|
55
|
+
root_text = root_bytes.decode("utf-8").strip()
|
|
56
|
+
if not root_text:
|
|
57
|
+
raise _LintUsageError("Git returned an empty repository root")
|
|
58
|
+
return Path(root_text).resolve()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resolve_shell_file_arguments(
|
|
62
|
+
all_arguments: Sequence[str], starting_directory: Path
|
|
63
|
+
) -> list[str]:
|
|
64
|
+
all_resolved_arguments: list[str] = []
|
|
65
|
+
is_reading_file_paths = False
|
|
66
|
+
for each_argument in all_arguments:
|
|
67
|
+
if each_argument in ("--files", "--text-as"):
|
|
68
|
+
is_reading_file_paths = True
|
|
69
|
+
all_resolved_arguments.append(each_argument)
|
|
70
|
+
continue
|
|
71
|
+
if each_argument.startswith("--"):
|
|
72
|
+
is_reading_file_paths = False
|
|
73
|
+
all_resolved_arguments.append(each_argument)
|
|
74
|
+
continue
|
|
75
|
+
if not is_reading_file_paths:
|
|
76
|
+
all_resolved_arguments.append(each_argument)
|
|
77
|
+
continue
|
|
78
|
+
candidate_path = Path(each_argument)
|
|
79
|
+
all_resolved_arguments.append(
|
|
80
|
+
str(
|
|
81
|
+
candidate_path
|
|
82
|
+
if candidate_path.is_absolute()
|
|
83
|
+
else starting_directory / candidate_path
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
return all_resolved_arguments
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _add_file_sources(source_group: argparse._MutuallyExclusiveGroup) -> None:
|
|
90
|
+
source_group.add_argument(
|
|
91
|
+
"--files",
|
|
92
|
+
nargs="+",
|
|
93
|
+
dest="all_file_paths",
|
|
94
|
+
metavar="PATH",
|
|
95
|
+
default=None,
|
|
96
|
+
help="Lint current worktree files.",
|
|
97
|
+
)
|
|
98
|
+
source_group.add_argument(
|
|
99
|
+
"--staged",
|
|
100
|
+
action="store_const",
|
|
101
|
+
const=SelectionKind.STAGED,
|
|
102
|
+
dest="selection_kind",
|
|
103
|
+
help="Lint staged index content.",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _add_repository_sources(
|
|
108
|
+
source_group: argparse._MutuallyExclusiveGroup,
|
|
109
|
+
) -> None:
|
|
110
|
+
source_group.add_argument(
|
|
111
|
+
"--base",
|
|
112
|
+
dest="base_revision",
|
|
113
|
+
metavar="REVISION",
|
|
114
|
+
default=None,
|
|
115
|
+
help="Lint changes from a base revision.",
|
|
116
|
+
)
|
|
117
|
+
source_group.add_argument(
|
|
118
|
+
"--repository",
|
|
119
|
+
action="store_const",
|
|
120
|
+
const=SelectionKind.REPOSITORY,
|
|
121
|
+
dest="selection_kind",
|
|
122
|
+
help="Lint tracked worktree files.",
|
|
123
|
+
)
|
|
124
|
+
source_group.add_argument(
|
|
125
|
+
"--text-as",
|
|
126
|
+
dest="text_path",
|
|
127
|
+
metavar="PATH",
|
|
128
|
+
default=None,
|
|
129
|
+
help="Lint one editor buffer from standard input.",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
134
|
+
parser = _LintArgumentParser(prog="cde lint")
|
|
135
|
+
source_group = parser.add_mutually_exclusive_group(required=True)
|
|
136
|
+
_add_file_sources(source_group)
|
|
137
|
+
_add_repository_sources(source_group)
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--format",
|
|
140
|
+
dest="report_format",
|
|
141
|
+
choices=tuple(each_format.value for each_format in ReportFormat),
|
|
142
|
+
default=ReportFormat.TEXT.value,
|
|
143
|
+
help="Report format.",
|
|
144
|
+
)
|
|
145
|
+
parser.set_defaults(selection_kind=None)
|
|
146
|
+
return parser
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _request_from_namespace(
|
|
150
|
+
namespace: argparse.Namespace,
|
|
151
|
+
stdin: TextIO,
|
|
152
|
+
repository_root: Path,
|
|
153
|
+
) -> LintRequest:
|
|
154
|
+
if namespace.all_file_paths is not None:
|
|
155
|
+
return LintRequest.files(
|
|
156
|
+
repository_root,
|
|
157
|
+
[Path(each_path) for each_path in namespace.all_file_paths],
|
|
158
|
+
)
|
|
159
|
+
if namespace.base_revision is not None:
|
|
160
|
+
return LintRequest.base(repository_root, namespace.base_revision)
|
|
161
|
+
if namespace.text_path is not None:
|
|
162
|
+
return LintRequest(
|
|
163
|
+
repository_root,
|
|
164
|
+
TextDocument(Path(namespace.text_path), stdin.read()),
|
|
165
|
+
)
|
|
166
|
+
if namespace.selection_kind is SelectionKind.STAGED:
|
|
167
|
+
return LintRequest.staged(repository_root)
|
|
168
|
+
if namespace.selection_kind is SelectionKind.REPOSITORY:
|
|
169
|
+
return LintRequest.repository(repository_root)
|
|
170
|
+
raise _LintUsageError("A source selection is required")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_lint_invocation(
|
|
174
|
+
all_arguments: Sequence[str],
|
|
175
|
+
stdin: TextIO,
|
|
176
|
+
repository_root: Path,
|
|
177
|
+
) -> tuple[LintRequest, ReportFormat]:
|
|
178
|
+
namespace = _build_parser().parse_args(list(all_arguments))
|
|
179
|
+
return (
|
|
180
|
+
_request_from_namespace(namespace, stdin, repository_root),
|
|
181
|
+
ReportFormat(namespace.report_format),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _run_linter(
|
|
186
|
+
all_arguments: Sequence[str],
|
|
187
|
+
stdin: TextIO,
|
|
188
|
+
repository_root: Path,
|
|
189
|
+
lint_runner: Callable[[LintRequest], LintReport],
|
|
190
|
+
) -> tuple[LintReport, ReportFormat]:
|
|
191
|
+
request, report_format = _parse_lint_invocation(
|
|
192
|
+
list(all_arguments),
|
|
193
|
+
stdin,
|
|
194
|
+
repository_root,
|
|
195
|
+
)
|
|
196
|
+
return lint_runner(request), report_format
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _write_invalid_input(error: Exception, stderr: TextIO) -> int:
|
|
200
|
+
stderr.write(f"{error}\n")
|
|
201
|
+
return INVALID_INPUT_EXIT_CODE
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _render_lint(
|
|
205
|
+
all_arguments: Sequence[str],
|
|
206
|
+
repository_root: Path,
|
|
207
|
+
stdin: TextIO,
|
|
208
|
+
stdout: TextIO,
|
|
209
|
+
lint_runner: Callable[[LintRequest], LintReport],
|
|
210
|
+
) -> int:
|
|
211
|
+
report, report_format = _run_linter(
|
|
212
|
+
all_arguments, stdin, repository_root, lint_runner
|
|
213
|
+
)
|
|
214
|
+
stdout.write(render(report, report_format))
|
|
215
|
+
return report.exit_code
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(
|
|
219
|
+
all_arguments: Sequence[str],
|
|
220
|
+
repository_root: Path,
|
|
221
|
+
*,
|
|
222
|
+
stdin: TextIO = sys.stdin,
|
|
223
|
+
stdout: TextIO = sys.stdout,
|
|
224
|
+
stderr: TextIO = sys.stderr,
|
|
225
|
+
lint_runner: Callable[[LintRequest], LintReport] = lint,
|
|
226
|
+
) -> int:
|
|
227
|
+
"""Run the policy-lint command.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
all_arguments: Command arguments.
|
|
231
|
+
repository_root: Repository root.
|
|
232
|
+
stdin: Input stream.
|
|
233
|
+
stdout: Output stream.
|
|
234
|
+
stderr: Error stream.
|
|
235
|
+
lint_runner: Policy-lint engine.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The process exit code.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
return _render_lint(
|
|
242
|
+
all_arguments, repository_root, stdin, stdout, lint_runner
|
|
243
|
+
)
|
|
244
|
+
except _LintUsageError as usage_error:
|
|
245
|
+
return _write_invalid_input(usage_error, stderr)
|
|
246
|
+
except SelectionRunFatal as selection_error:
|
|
247
|
+
return _write_invalid_input(selection_error, stderr)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
try:
|
|
252
|
+
shell_directory = Path.cwd()
|
|
253
|
+
shell_repository_root = _discover_repository_root(shell_directory)
|
|
254
|
+
except _LintUsageError as usage_error:
|
|
255
|
+
sys.stderr.write(f"{usage_error}\n")
|
|
256
|
+
raise SystemExit(INVALID_INPUT_EXIT_CODE) from usage_error
|
|
257
|
+
all_shell_arguments = _resolve_shell_file_arguments(sys.argv[1:], shell_directory)
|
|
258
|
+
raise SystemExit(main(all_shell_arguments, shell_repository_root))
|
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
|
|
5
|
-
process-tree mypy, and the blocking pytest suite from a
|
|
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,
|
|
15
|
-
|
|
16
|
-
script's exit code so CI
|
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from .engine import lint
|
|
2
|
+
from .model import (
|
|
3
|
+
BaseChanges,
|
|
4
|
+
ChangeSetRule,
|
|
5
|
+
ContentOrigin,
|
|
6
|
+
Diagnostic,
|
|
7
|
+
Document,
|
|
8
|
+
DocumentRule,
|
|
9
|
+
DocumentSet,
|
|
10
|
+
ExplicitFiles,
|
|
11
|
+
LintReport,
|
|
12
|
+
LintRequest,
|
|
13
|
+
Location,
|
|
14
|
+
RepositoryRule,
|
|
15
|
+
RepositoryTree,
|
|
16
|
+
SelectionKind,
|
|
17
|
+
Severity,
|
|
18
|
+
StagedChanges,
|
|
19
|
+
TextDocument,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"BaseChanges",
|
|
24
|
+
"ChangeSetRule",
|
|
25
|
+
"ContentOrigin",
|
|
26
|
+
"Diagnostic",
|
|
27
|
+
"Document",
|
|
28
|
+
"DocumentRule",
|
|
29
|
+
"DocumentSet",
|
|
30
|
+
"ExplicitFiles",
|
|
31
|
+
"LintReport",
|
|
32
|
+
"LintRequest",
|
|
33
|
+
"Location",
|
|
34
|
+
"RepositoryRule",
|
|
35
|
+
"RepositoryTree",
|
|
36
|
+
"SelectionKind",
|
|
37
|
+
"Severity",
|
|
38
|
+
"StagedChanges",
|
|
39
|
+
"TextDocument",
|
|
40
|
+
"lint",
|
|
41
|
+
]
|