claude-dev-env 8.6.1 → 8.8.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/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -2
- package/_shared/advisor/reference/identity.md +1 -1
- package/_shared/advisor/reference/spawn-walk-log.md +1 -1
- package/_shared/advisor/reference/warm-up.md +3 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -5
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +85 -6
- package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/docs/references/advisor-tool.md +3 -2
- package/docs/second-claude-account.md +50 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +10 -8
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_pii_scanner.py +8 -0
- package/hooks/blocking/test_state_description_blocker.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/long-horizon-autonomy.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/claude_account_choice.py +246 -0
- package/scripts/claude_account_profile.py +371 -0
- package/scripts/claude_chain_usage.py +49 -5
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +166 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +21 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapters.py +30 -0
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_claude_account_choice.py +217 -0
- package/scripts/test_claude_account_profile.py +220 -0
- package/scripts/test_claude_chain_usage.py +40 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Report authored prose that defines its subject against a rejected reading.
|
|
2
|
+
|
|
3
|
+
A contrast leaves the reader holding two readings where one would do. The
|
|
4
|
+
detector lives in ``scripts/contrast_framing.py`` and reads its pattern list
|
|
5
|
+
from ``scripts/dev_env_scripts_constants/contrast_framing_constants.py``, the
|
|
6
|
+
same list the durable post linter reads.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from contrast_framing import describe_contrast_framing, find_contrast_framing
|
|
14
|
+
|
|
15
|
+
from .config import constants
|
|
16
|
+
from .model import Diagnostic, Document, Location, Severity
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def accepts_authored_markdown(document: Document) -> bool:
|
|
20
|
+
"""Return whether the document is Markdown an author wrote.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
document: Candidate document.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True for Markdown outside the generated documents.
|
|
27
|
+
"""
|
|
28
|
+
if document.path.suffix.lower() not in constants.ALL_MARKDOWN_SUFFIXES:
|
|
29
|
+
return False
|
|
30
|
+
return document.path.name.lower() not in constants.ALL_GENERATED_DOCUMENT_NAMES
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def contrast_framing_diagnostics(
|
|
34
|
+
document: Document, repository_root: Path
|
|
35
|
+
) -> tuple[Diagnostic, ...]:
|
|
36
|
+
"""Report each contrast-framing occurrence in one document.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
document: Current document text and path.
|
|
40
|
+
repository_root: Request repository root, unused by this detector.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Contrast-framing diagnostics in document order.
|
|
44
|
+
"""
|
|
45
|
+
del repository_root
|
|
46
|
+
all_lines = document.text.splitlines()
|
|
47
|
+
return tuple(
|
|
48
|
+
Diagnostic(
|
|
49
|
+
constants.CONTRAST_FRAMING_RULE_ID,
|
|
50
|
+
Severity.ERROR,
|
|
51
|
+
describe_contrast_framing(
|
|
52
|
+
all_lines[each_line_number - 1], each_form_name
|
|
53
|
+
),
|
|
54
|
+
Location(document.path, each_line_number, each_column),
|
|
55
|
+
)
|
|
56
|
+
for each_line_number, each_column, each_form_name in find_contrast_framing(
|
|
57
|
+
document.text
|
|
58
|
+
)
|
|
59
|
+
)
|
|
@@ -10,6 +10,7 @@ from shared_tree_paths import resolve_shared_scripts_directory
|
|
|
10
10
|
|
|
11
11
|
from . import (
|
|
12
12
|
adapter_configuration,
|
|
13
|
+
adapter_contrast_framing,
|
|
13
14
|
adapter_detectors,
|
|
14
15
|
adapter_pairing,
|
|
15
16
|
adapter_retired_hook_prose,
|
|
@@ -438,6 +439,35 @@ def accepts_instruction_markdown(document: Document) -> bool:
|
|
|
438
439
|
return adapter_retired_hook_prose.accepts_instruction_markdown(document)
|
|
439
440
|
|
|
440
441
|
|
|
442
|
+
def accepts_authored_markdown(document: Document) -> bool:
|
|
443
|
+
"""Return whether the document is Markdown an author wrote.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
document: Candidate document.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
True for Markdown outside the generated documents.
|
|
450
|
+
"""
|
|
451
|
+
return adapter_contrast_framing.accepts_authored_markdown(document)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def contrast_framing_diagnostics(
|
|
455
|
+
document: Document, repository_root: Path
|
|
456
|
+
) -> tuple[Diagnostic, ...]:
|
|
457
|
+
"""Report prose that defines its subject against a rejected reading.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
document: Current Markdown text and path.
|
|
461
|
+
repository_root: Request repository root.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Contrast-framing diagnostics.
|
|
465
|
+
"""
|
|
466
|
+
return adapter_contrast_framing.contrast_framing_diagnostics(
|
|
467
|
+
document, repository_root
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
441
471
|
def retired_hook_prose_diagnostics(
|
|
442
472
|
document: Document, repository_root: Path
|
|
443
473
|
) -> tuple[Diagnostic, ...]:
|
|
@@ -104,6 +104,11 @@ def _text_document_rules() -> tuple[model.Rule, ...]:
|
|
|
104
104
|
adapters.accepts_workflow,
|
|
105
105
|
adapters.workflow_substitution_diagnostics,
|
|
106
106
|
),
|
|
107
|
+
build_document_rule(
|
|
108
|
+
"contrast-framing",
|
|
109
|
+
adapters.accepts_authored_markdown,
|
|
110
|
+
adapters.contrast_framing_diagnostics,
|
|
111
|
+
),
|
|
107
112
|
build_document_rule(
|
|
108
113
|
"retired-hook-prose",
|
|
109
114
|
adapters.accepts_instruction_markdown,
|
|
@@ -21,6 +21,16 @@ ALL_CHECK_IDS = (
|
|
|
21
21
|
CHECK_ID_TRACKED_PERSONAL_DATA,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
SEVERITY_BREAKING = "breaking"
|
|
25
|
+
SEVERITY_SMELL = "smell"
|
|
26
|
+
SEVERITY_BY_CHECK_ID: dict[str, str] = {
|
|
27
|
+
CHECK_ID_CLAUDE_MD_ORPHANS: SEVERITY_BREAKING,
|
|
28
|
+
CHECK_ID_ENV_VAR_DOCUMENTATION: SEVERITY_BREAKING,
|
|
29
|
+
CHECK_ID_PACKAGE_INVENTORY: SEVERITY_SMELL,
|
|
30
|
+
CHECK_ID_PYTEST_TESTPATHS: SEVERITY_BREAKING,
|
|
31
|
+
CHECK_ID_TRACKED_PERSONAL_DATA: SEVERITY_BREAKING,
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
SUCCESS_EXIT_CODE = 0
|
|
25
35
|
FINDINGS_EXIT_CODE = 1
|
|
26
36
|
FAILED_CHECK_EXIT_CODE = INCOMPLETE_EXIT_CODE
|
|
@@ -28,6 +38,7 @@ USAGE_EXIT_CODE = INVALID_INPUT_EXIT_CODE
|
|
|
28
38
|
|
|
29
39
|
REPOSITORY_ROOT_FLAG = "--repository-root"
|
|
30
40
|
FINDING_LINE_TEMPLATE = "{check_id}: {relative_path}: {message}"
|
|
41
|
+
ADVISORY_FINDING_LINE_TEMPLATE = "advisory: {check_id}: {relative_path}: {message}"
|
|
31
42
|
RULE_FAILED_LINE_TEMPLATE = "error: rule failed: {check_id}"
|
|
32
43
|
REPORT_LINE_SEPARATOR = "\n"
|
|
33
44
|
EMPTY_REPORT_TEXT = ""
|
|
@@ -57,6 +68,7 @@ PACKAGE_INVENTORY_CONSTANTS_MODULE_NAME = (
|
|
|
57
68
|
)
|
|
58
69
|
PYTEST_TESTPATHS_MODULE_NAME = "blocking.pytest_testpaths_orphan_blocker"
|
|
59
70
|
PII_SCANNER_MODULE_NAME = "blocking.pii_scanner"
|
|
71
|
+
FOLLOWUP_LEDGER_MODULE_NAME = "followup_ledger"
|
|
60
72
|
REPOSITORY_EXEMPTION_MODULE_NAME = (
|
|
61
73
|
"blocking.pii_prevention_blocker_parts.repository_exemption"
|
|
62
74
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Repository policy document and exception-entry schema."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from typing import NamedTuple
|
|
4
5
|
|
|
5
6
|
CONFIG_RELATIVE_PATH = "config/repository-policy.json"
|
|
6
7
|
DIGEST_PATTERN = re.compile(r"[0-9a-f]{64}")
|
|
@@ -11,9 +12,34 @@ FIRST_PRINTABLE_CODEPOINT = 32
|
|
|
11
12
|
VERSION_FIELD = "version"
|
|
12
13
|
SUPPORTED_DOCUMENT_VERSION = 1
|
|
13
14
|
EMAIL_EXEMPTIONS_FIELD = "email_exemptions"
|
|
15
|
+
PRIVATE_IP_EXEMPTIONS_FIELD = "private_ip_exemptions"
|
|
14
16
|
PATH_EXEMPTIONS_FIELD = "path_exemptions"
|
|
15
|
-
ALL_EXEMPTION_FIELDS = frozenset({EMAIL_EXEMPTIONS_FIELD, PATH_EXEMPTIONS_FIELD})
|
|
16
|
-
ALL_DOCUMENT_FIELDS = ALL_EXEMPTION_FIELDS | {VERSION_FIELD}
|
|
17
17
|
EMAIL_EXEMPTION_SUBJECT = "Email exemption"
|
|
18
|
+
PRIVATE_IP_EXEMPTION_SUBJECT = "Private IP exemption"
|
|
18
19
|
PATH_EXEMPTION_SUBJECT = "Path exemption"
|
|
19
20
|
EMAIL_CATEGORY = "email"
|
|
21
|
+
PRIVATE_IP_CATEGORY = "private-ip"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MatchExemptionFamily(NamedTuple):
|
|
25
|
+
"""One scanner category cleared by a committed path and match digest."""
|
|
26
|
+
|
|
27
|
+
field_name: str
|
|
28
|
+
category: str
|
|
29
|
+
subject: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
EMAIL_EXEMPTION_FAMILY = MatchExemptionFamily(
|
|
33
|
+
EMAIL_EXEMPTIONS_FIELD, EMAIL_CATEGORY, EMAIL_EXEMPTION_SUBJECT
|
|
34
|
+
)
|
|
35
|
+
PRIVATE_IP_EXEMPTION_FAMILY = MatchExemptionFamily(
|
|
36
|
+
PRIVATE_IP_EXEMPTIONS_FIELD, PRIVATE_IP_CATEGORY, PRIVATE_IP_EXEMPTION_SUBJECT
|
|
37
|
+
)
|
|
38
|
+
ALL_MATCH_EXEMPTION_FAMILIES = (
|
|
39
|
+
EMAIL_EXEMPTION_FAMILY,
|
|
40
|
+
PRIVATE_IP_EXEMPTION_FAMILY,
|
|
41
|
+
)
|
|
42
|
+
ALL_EXEMPTION_FIELDS = frozenset(
|
|
43
|
+
each_family.field_name for each_family in ALL_MATCH_EXEMPTION_FAMILIES
|
|
44
|
+
) | {PATH_EXEMPTIONS_FIELD}
|
|
45
|
+
ALL_DOCUMENT_FIELDS = ALL_EXEMPTION_FIELDS | {VERSION_FIELD}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Record advisory committed-tree findings in the follow-up ledger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
|
|
8
|
+
from repository_checks.config.constants import (
|
|
9
|
+
ALL_FAIL_CLOSED_EXCEPTION_TYPES,
|
|
10
|
+
FOLLOWUP_LEDGER_MODULE_NAME,
|
|
11
|
+
SEVERITY_SMELL,
|
|
12
|
+
)
|
|
13
|
+
from repository_checks.hook_modules import load_hooks_module
|
|
14
|
+
from repository_checks.models import RepositoryCheckReport, RepositoryFinding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def record_smell_findings(repository_root: Path, report: RepositoryCheckReport) -> None:
|
|
18
|
+
"""Append every advisory finding to the repository's follow-up ledger.
|
|
19
|
+
|
|
20
|
+
The ledger keeps the gate's decision unchanged, so a ledger that cannot be
|
|
21
|
+
loaded or written leaves the report's exit code as it stands.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
repository_root: The repository whose ledger receives the findings.
|
|
25
|
+
report: The findings this run produced.
|
|
26
|
+
"""
|
|
27
|
+
if not report.all_smell_findings:
|
|
28
|
+
return
|
|
29
|
+
try:
|
|
30
|
+
ledger = load_hooks_module(FOLLOWUP_LEDGER_MODULE_NAME)
|
|
31
|
+
origin_commit = ledger.head_commit(repository_root)
|
|
32
|
+
for each_finding in report.all_smell_findings:
|
|
33
|
+
_append_finding(ledger, repository_root, each_finding, origin_commit)
|
|
34
|
+
except ALL_FAIL_CLOSED_EXCEPTION_TYPES:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _append_finding(
|
|
39
|
+
ledger: ModuleType,
|
|
40
|
+
repository_root: Path,
|
|
41
|
+
finding: RepositoryFinding,
|
|
42
|
+
origin_commit: str,
|
|
43
|
+
) -> None:
|
|
44
|
+
ledger.record_followup_finding(
|
|
45
|
+
repository_root,
|
|
46
|
+
ledger.FollowupFinding(
|
|
47
|
+
finding.check_id,
|
|
48
|
+
finding.relative_path,
|
|
49
|
+
finding.message,
|
|
50
|
+
finding.check_id,
|
|
51
|
+
SEVERITY_SMELL,
|
|
52
|
+
origin_commit,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Load the scanner matches a repository owns through its policy document."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from repository_checks.config.policy_document import (
|
|
8
|
+
ALL_MATCH_EXEMPTION_FAMILIES,
|
|
9
|
+
MatchExemptionFamily,
|
|
10
|
+
)
|
|
11
|
+
from repository_checks.policy_document import (
|
|
12
|
+
PolicyConfigurationRunFatal,
|
|
13
|
+
entry_path_and_digest,
|
|
14
|
+
read_policy_entries,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_match_exemptions(
|
|
19
|
+
repository_root: Path, family: MatchExemptionFamily
|
|
20
|
+
) -> frozenset[tuple[str, str, str]]:
|
|
21
|
+
"""Read one family of matches the repository owns.
|
|
22
|
+
|
|
23
|
+
::
|
|
24
|
+
|
|
25
|
+
the email family over a document owning one address
|
|
26
|
+
ok: {("contacts.py", "email", "<digest of the address>")}
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
repository_root: Repository containing the optional policy configuration.
|
|
30
|
+
family: Document field, scanner category, and rejection subject to read.
|
|
31
|
+
Returns:
|
|
32
|
+
Exact path, scanner category, and digest identities.
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: Configuration contains invalid JSON or schema fields.
|
|
35
|
+
OSError: Configuration cannot be read.
|
|
36
|
+
"""
|
|
37
|
+
exemptions: set[tuple[str, str, str]] = set()
|
|
38
|
+
for each_entry in read_policy_entries(repository_root, family.field_name):
|
|
39
|
+
relative_path, digest = entry_path_and_digest(each_entry, family.subject)
|
|
40
|
+
identity = (relative_path, family.category, digest)
|
|
41
|
+
if identity in exemptions:
|
|
42
|
+
raise PolicyConfigurationRunFatal(
|
|
43
|
+
f"Duplicate repository {family.subject.lower()}"
|
|
44
|
+
)
|
|
45
|
+
exemptions.add(identity)
|
|
46
|
+
return frozenset(exemptions)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_all_match_exemptions(
|
|
50
|
+
repository_root: Path,
|
|
51
|
+
) -> frozenset[tuple[str, str, str]]:
|
|
52
|
+
"""Read every family of matches the repository owns.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
repository_root: Repository containing the optional policy configuration.
|
|
56
|
+
Returns:
|
|
57
|
+
Exact path, scanner category, and digest identities across all families.
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: Configuration contains invalid JSON or schema fields.
|
|
60
|
+
OSError: Configuration cannot be read.
|
|
61
|
+
"""
|
|
62
|
+
return frozenset().union(
|
|
63
|
+
*(
|
|
64
|
+
load_match_exemptions(repository_root, each_family)
|
|
65
|
+
for each_family in ALL_MATCH_EXEMPTION_FAMILIES
|
|
66
|
+
)
|
|
67
|
+
)
|
|
@@ -7,10 +7,28 @@ from dataclasses import dataclass
|
|
|
7
7
|
from repository_checks.config.constants import (
|
|
8
8
|
FAILED_CHECK_EXIT_CODE,
|
|
9
9
|
FINDINGS_EXIT_CODE,
|
|
10
|
+
SEVERITY_BREAKING,
|
|
11
|
+
SEVERITY_BY_CHECK_ID,
|
|
12
|
+
SEVERITY_SMELL,
|
|
10
13
|
SUCCESS_EXIT_CODE,
|
|
11
14
|
)
|
|
12
15
|
|
|
13
16
|
|
|
17
|
+
def severity_for_check_id(check_id: str) -> str:
|
|
18
|
+
"""Return the severity a check identifier declares.
|
|
19
|
+
|
|
20
|
+
A check identifier absent from the table reads as breaking, so a check
|
|
21
|
+
added without a severity row fails the tree rather than passing quietly.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
check_id: Stable identifier of the check that raised a finding.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Either SEVERITY_BREAKING or SEVERITY_SMELL.
|
|
28
|
+
"""
|
|
29
|
+
return SEVERITY_BY_CHECK_ID.get(check_id, SEVERITY_BREAKING)
|
|
30
|
+
|
|
31
|
+
|
|
14
32
|
@dataclass(frozen=True)
|
|
15
33
|
class RepositoryFinding:
|
|
16
34
|
"""One committed-tree finding with a stable check id and relative path."""
|
|
@@ -19,19 +37,45 @@ class RepositoryFinding:
|
|
|
19
37
|
relative_path: str
|
|
20
38
|
message: str
|
|
21
39
|
|
|
40
|
+
@property
|
|
41
|
+
def severity(self) -> str:
|
|
42
|
+
"""Return the severity this finding's check identifier declares."""
|
|
43
|
+
return severity_for_check_id(self.check_id)
|
|
44
|
+
|
|
22
45
|
|
|
23
46
|
@dataclass(frozen=True)
|
|
24
47
|
class RepositoryCheckReport:
|
|
25
|
-
"""Sorted findings and the check ids that failed while they ran.
|
|
48
|
+
"""Sorted findings and the check ids that failed while they ran.
|
|
49
|
+
|
|
50
|
+
Splits its findings into the breaking ones and the advisory ones, and
|
|
51
|
+
derives the exit code from the breaking ones alone.
|
|
52
|
+
"""
|
|
26
53
|
|
|
27
54
|
all_findings: tuple[RepositoryFinding, ...]
|
|
28
55
|
all_failed_check_ids: tuple[str, ...]
|
|
29
56
|
|
|
57
|
+
@property
|
|
58
|
+
def all_breaking_findings(self) -> tuple[RepositoryFinding, ...]:
|
|
59
|
+
"""Return the findings that fail the tree."""
|
|
60
|
+
return self._findings_of_severity(SEVERITY_BREAKING)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def all_smell_findings(self) -> tuple[RepositoryFinding, ...]:
|
|
64
|
+
"""Return the findings that report and leave the tree passing."""
|
|
65
|
+
return self._findings_of_severity(SEVERITY_SMELL)
|
|
66
|
+
|
|
30
67
|
@property
|
|
31
68
|
def exit_code(self) -> int:
|
|
32
69
|
"""Return the process status for this report."""
|
|
33
70
|
if self.all_failed_check_ids:
|
|
34
71
|
return FAILED_CHECK_EXIT_CODE
|
|
35
|
-
if self.
|
|
72
|
+
if self.all_breaking_findings:
|
|
36
73
|
return FINDINGS_EXIT_CODE
|
|
37
74
|
return SUCCESS_EXIT_CODE
|
|
75
|
+
|
|
76
|
+
def _findings_of_severity(self, severity: str) -> tuple[RepositoryFinding, ...]:
|
|
77
|
+
return tuple(
|
|
78
|
+
each_finding
|
|
79
|
+
for each_finding in self.all_findings
|
|
80
|
+
if each_finding.severity == severity
|
|
81
|
+
)
|
|
@@ -3,12 +3,21 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from repository_checks.config import constants as repository_constants
|
|
6
|
-
from repository_checks.models import RepositoryCheckReport
|
|
6
|
+
from repository_checks.models import RepositoryCheckReport, RepositoryFinding
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def render_report(report: RepositoryCheckReport) -> str:
|
|
10
10
|
"""Render findings and failed checks as newline-terminated text.
|
|
11
11
|
|
|
12
|
+
An advisory finding carries an ``advisory:`` prefix, so a reader of a
|
|
13
|
+
passing run can tell the reported line from one that failed the tree.
|
|
14
|
+
|
|
15
|
+
::
|
|
16
|
+
|
|
17
|
+
claude-md-orphans: notes/CLAUDE.md: references missing file x.py
|
|
18
|
+
advisory: package-inventory: pipeline/seam.py: production file is
|
|
19
|
+
absent from package inventory
|
|
20
|
+
|
|
12
21
|
Args:
|
|
13
22
|
report: Findings and failed check identifiers.
|
|
14
23
|
|
|
@@ -16,12 +25,7 @@ def render_report(report: RepositoryCheckReport) -> str:
|
|
|
16
25
|
The report body, or an empty string when the tree is clean.
|
|
17
26
|
"""
|
|
18
27
|
all_lines = [
|
|
19
|
-
|
|
20
|
-
check_id=each_finding.check_id,
|
|
21
|
-
relative_path=each_finding.relative_path,
|
|
22
|
-
message=each_finding.message,
|
|
23
|
-
)
|
|
24
|
-
for each_finding in report.all_findings
|
|
28
|
+
_finding_line(each_finding) for each_finding in report.all_findings
|
|
25
29
|
]
|
|
26
30
|
all_lines.extend(_failed_check_lines(report))
|
|
27
31
|
if not all_lines:
|
|
@@ -32,6 +36,18 @@ def render_report(report: RepositoryCheckReport) -> str:
|
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
|
|
39
|
+
def _finding_line(finding: RepositoryFinding) -> str:
|
|
40
|
+
if finding.severity == repository_constants.SEVERITY_SMELL:
|
|
41
|
+
line_template = repository_constants.ADVISORY_FINDING_LINE_TEMPLATE
|
|
42
|
+
else:
|
|
43
|
+
line_template = repository_constants.FINDING_LINE_TEMPLATE
|
|
44
|
+
return line_template.format(
|
|
45
|
+
check_id=finding.check_id,
|
|
46
|
+
relative_path=finding.relative_path,
|
|
47
|
+
message=finding.message,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
35
51
|
def _failed_check_lines(report: RepositoryCheckReport) -> list[str]:
|
|
36
52
|
return [
|
|
37
53
|
repository_constants.RULE_FAILED_LINE_TEMPLATE.format(check_id=each_check_id)
|
|
@@ -10,8 +10,8 @@ from types import ModuleType
|
|
|
10
10
|
from policy_lint.config import constants as policy_constants
|
|
11
11
|
|
|
12
12
|
from repository_checks.config import constants as repository_constants
|
|
13
|
-
from repository_checks.email_exemptions import load_email_exemptions
|
|
14
13
|
from repository_checks.hook_modules import load_hooks_module
|
|
14
|
+
from repository_checks.match_exemptions import load_all_match_exemptions
|
|
15
15
|
from repository_checks.models import RepositoryFinding
|
|
16
16
|
from repository_checks.path_exemptions import load_path_exemptions
|
|
17
17
|
|
|
@@ -31,7 +31,7 @@ def collect_tracked_secret_findings(
|
|
|
31
31
|
scanner = load_hooks_module(repository_constants.PII_SCANNER_MODULE_NAME)
|
|
32
32
|
exemption = load_hooks_module(repository_constants.REPOSITORY_EXEMPTION_MODULE_NAME)
|
|
33
33
|
all_allowlisted_literals = exemption.repository_allowlisted_values(repository_root)
|
|
34
|
-
all_exact_exemptions =
|
|
34
|
+
all_exact_exemptions = load_all_match_exemptions(repository_root)
|
|
35
35
|
all_exact_exemptions |= repository_constants.ALL_TRACKED_SECRET_EXACT_EXEMPTIONS
|
|
36
36
|
all_path_exemptions = load_path_exemptions(repository_root)
|
|
37
37
|
all_findings: list[RepositoryFinding] = []
|
|
@@ -16,6 +16,7 @@ from repository_checks.config.constants import (
|
|
|
16
16
|
REPOSITORY_ROOT_FLAG,
|
|
17
17
|
USAGE_EXIT_CODE,
|
|
18
18
|
)
|
|
19
|
+
from repository_checks.followups import record_smell_findings
|
|
19
20
|
from repository_checks.reporting import render_report
|
|
20
21
|
from repository_checks.runner import run_repository_checks
|
|
21
22
|
|
|
@@ -90,8 +91,8 @@ def main(
|
|
|
90
91
|
stderr: Error stream.
|
|
91
92
|
|
|
92
93
|
Returns:
|
|
93
|
-
Zero when the tree
|
|
94
|
-
and three when a check fails closed.
|
|
94
|
+
Zero when the tree carries no breaking finding, one for a breaking
|
|
95
|
+
finding, two for usage errors, and three when a check fails closed.
|
|
95
96
|
"""
|
|
96
97
|
try:
|
|
97
98
|
namespace = _build_parser().parse_args(list(all_arguments))
|
|
@@ -102,6 +103,7 @@ def main(
|
|
|
102
103
|
if namespace.repository_root is not None:
|
|
103
104
|
selected_root = Path(namespace.repository_root).resolve()
|
|
104
105
|
report = run_repository_checks(selected_root)
|
|
106
|
+
record_smell_findings(selected_root, report)
|
|
105
107
|
stdout.write(render_report(report))
|
|
106
108
|
return report.exit_code
|
|
107
109
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report whether every review finding on a pull request head is answered.
|
|
3
|
+
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
python3 review_closure.py jl-cmd/claude-dev-env 1442
|
|
7
|
+
CLOSED jl-cmd/claude-dev-env#1442 ab845eb :: every review finding on this
|
|
8
|
+
commit is answered
|
|
9
|
+
|
|
10
|
+
A finding waits until the agent driving the pull request replies to it or
|
|
11
|
+
pushes the fix. Exit status is 0 for CLOSED, 1 for OPEN, and 2 when the pull
|
|
12
|
+
request state could not be read.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
from dev_env_scripts_constants.review_closure_constants import (
|
|
23
|
+
CLOSED_EXIT_CODE,
|
|
24
|
+
COMMAND_DESCRIPTION,
|
|
25
|
+
DRIVER_LOGIN_ARGUMENT_HELP,
|
|
26
|
+
ERROR_EXIT_CODE,
|
|
27
|
+
FINDING_LINE_TEMPLATE,
|
|
28
|
+
NUMBER_ARGUMENT_HELP,
|
|
29
|
+
OPEN_EXIT_CODE,
|
|
30
|
+
SLUG_ARGUMENT_HELP,
|
|
31
|
+
)
|
|
32
|
+
from pr_verification.github_parsing import GitHubError
|
|
33
|
+
from review_closure_github import (
|
|
34
|
+
github_token,
|
|
35
|
+
read_approvals_conclusion,
|
|
36
|
+
read_pull_request,
|
|
37
|
+
read_review_threads,
|
|
38
|
+
)
|
|
39
|
+
from review_closure_model import (
|
|
40
|
+
OpenFinding,
|
|
41
|
+
all_open_findings,
|
|
42
|
+
driver_logins,
|
|
43
|
+
head_sha,
|
|
44
|
+
verdict_line,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ClosureReport:
|
|
50
|
+
"""What one run of this command found on a pull request."""
|
|
51
|
+
|
|
52
|
+
verdict: str
|
|
53
|
+
all_findings: tuple[OpenFinding, ...]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
57
|
+
"""Build the command line this script accepts.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
A parser taking a repository slug, a pull request number, and any
|
|
61
|
+
number of extra driver logins.
|
|
62
|
+
"""
|
|
63
|
+
parser = argparse.ArgumentParser(description=COMMAND_DESCRIPTION)
|
|
64
|
+
parser.add_argument("slug", help=SLUG_ARGUMENT_HELP)
|
|
65
|
+
parser.add_argument("number", type=int, help=NUMBER_ARGUMENT_HELP)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--driver-login",
|
|
68
|
+
action="append",
|
|
69
|
+
default=[],
|
|
70
|
+
help=DRIVER_LOGIN_ARGUMENT_HELP,
|
|
71
|
+
)
|
|
72
|
+
return parser
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def closure_report(
|
|
76
|
+
slug: str, number: int, all_extra_logins: Sequence[str], token: str
|
|
77
|
+
) -> ClosureReport:
|
|
78
|
+
"""Read one pull request and judge the findings on its head.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
slug: The repository as ``owner/name``.
|
|
82
|
+
number: The pull request number.
|
|
83
|
+
all_extra_logins: Logins that count as the driving agent beside the
|
|
84
|
+
account that opened the pull request.
|
|
85
|
+
token: The GitHub token the reads authenticate with.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The verdict line and each finding that waits.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
GitHubError: The pull request state could not be read.
|
|
92
|
+
"""
|
|
93
|
+
all_pull_request_fields = read_pull_request(slug, number, token)
|
|
94
|
+
all_findings = all_open_findings(
|
|
95
|
+
read_review_threads(slug, number, token),
|
|
96
|
+
driver_logins(all_pull_request_fields, all_extra_logins),
|
|
97
|
+
read_approvals_conclusion(slug, head_sha(all_pull_request_fields), token),
|
|
98
|
+
)
|
|
99
|
+
return ClosureReport(
|
|
100
|
+
verdict=verdict_line(slug, all_pull_request_fields, all_findings),
|
|
101
|
+
all_findings=all_findings,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main(all_arguments: Sequence[str]) -> int:
|
|
106
|
+
"""Print the review closure verdict for one pull request.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
all_arguments: The command line, without the program name.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Zero when every finding is answered, one when a finding waits, and
|
|
113
|
+
two when the pull request state could not be read.
|
|
114
|
+
"""
|
|
115
|
+
parsed = build_parser().parse_args(all_arguments)
|
|
116
|
+
try:
|
|
117
|
+
report = closure_report(
|
|
118
|
+
parsed.slug, parsed.number, parsed.driver_login, github_token()
|
|
119
|
+
)
|
|
120
|
+
except GitHubError as failure:
|
|
121
|
+
print(failure, file=sys.stderr)
|
|
122
|
+
return ERROR_EXIT_CODE
|
|
123
|
+
print(report.verdict)
|
|
124
|
+
for each_finding in report.all_findings:
|
|
125
|
+
print(
|
|
126
|
+
FINDING_LINE_TEMPLATE.format(
|
|
127
|
+
subject=each_finding.subject, reason=each_finding.reason
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return OPEN_EXIT_CODE if report.all_findings else CLOSED_EXIT_CODE
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
raise SystemExit(main(sys.argv[1:]))
|