claude-dev-env 8.6.1 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
  3. package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
  4. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
  5. package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
  6. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
  7. package/bin/install.test.mjs +25 -0
  8. package/docs/high-trust-agent-delivery.md +132 -0
  9. package/hooks/advisory/refactor_guard.py +2 -2
  10. package/hooks/advisory/test_refactor_guard.py +21 -0
  11. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  12. package/hooks/blocking/code_rules_shared.py +4 -7
  13. package/hooks/blocking/conftest.py +22 -0
  14. package/hooks/blocking/state_description_blocker.py +10 -8
  15. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  16. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
  17. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  18. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  19. package/hooks/blocking/test_code_rules_shared.py +18 -0
  20. package/hooks/blocking/test_conftest.py +35 -0
  21. package/hooks/blocking/test_state_description_blocker.py +16 -0
  22. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  23. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  24. package/hooks/git-hooks/test_config.py +10 -3
  25. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  26. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  27. package/hooks/git-hooks/verification_notice_state.py +3 -1
  28. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  29. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  30. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  31. package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
  32. package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
  33. package/hooks/validators/exempt_paths.py +3 -18
  34. package/hooks/validators/test_exempt_paths.py +20 -0
  35. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  36. package/package.json +1 -1
  37. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  38. package/rules/archiving-agent-config.md +10 -0
  39. package/rules/code-standards.md +1 -0
  40. package/rules/correction-lens.md +134 -0
  41. package/rules/flag-non-breaking-findings.md +11 -0
  42. package/rules/no-contrast-framing.md +68 -0
  43. package/rules/review-closure-is-a-check.md +44 -0
  44. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  45. package/scripts/agent_merge_check.py +381 -0
  46. package/scripts/check.ps1 +12 -1
  47. package/scripts/ci/install-playtest.mjs +291 -0
  48. package/scripts/ci/install-playtest.test.mjs +84 -0
  49. package/scripts/ci/installer-lifecycle.mjs +11 -30
  50. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  51. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  52. package/scripts/ci/scratch-home-install.mjs +73 -0
  53. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  54. package/scripts/codex_compat_materializer.py +33 -16
  55. package/scripts/contrast_framing.py +101 -0
  56. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  57. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  58. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  59. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  60. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  61. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  62. package/scripts/durable_post_lint.py +30 -4
  63. package/scripts/followup_cli.py +6 -10
  64. package/scripts/merge_gate_checks.py +236 -0
  65. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  66. package/scripts/policy_lint/adapters.py +30 -0
  67. package/scripts/policy_lint/config/constants.py +2 -0
  68. package/scripts/policy_lint/registry.py +5 -0
  69. package/scripts/repository_checks/config/constants.py +12 -0
  70. package/scripts/repository_checks/config/policy_document.py +28 -2
  71. package/scripts/repository_checks/followups.py +54 -0
  72. package/scripts/repository_checks/match_exemptions.py +67 -0
  73. package/scripts/repository_checks/models.py +46 -2
  74. package/scripts/repository_checks/reporting.py +23 -7
  75. package/scripts/repository_checks/tracked_secrets.py +2 -2
  76. package/scripts/repository_policy.py +4 -2
  77. package/scripts/review_closure.py +134 -0
  78. package/scripts/review_closure_github.py +345 -0
  79. package/scripts/review_closure_model.py +360 -0
  80. package/scripts/test_agent_merge_check.py +317 -0
  81. package/scripts/test_durable_post_lint.py +46 -0
  82. package/scripts/test_followup_cli.py +40 -0
  83. package/scripts/test_merge_gate_checks.py +92 -0
  84. package/scripts/tests/test_code_review_constants.py +78 -0
  85. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  86. package/scripts/tests/test_contrast_framing.py +117 -0
  87. package/scripts/tests/test_followups.py +88 -0
  88. package/scripts/tests/test_match_exemptions.py +291 -0
  89. package/scripts/tests/test_models.py +67 -2
  90. package/scripts/tests/test_package_inventory.py +6 -4
  91. package/scripts/tests/test_pester_suites_run.py +53 -0
  92. package/scripts/tests/test_policy_document.py +39 -1
  93. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  94. package/scripts/tests/test_reporting.py +18 -0
  95. package/scripts/tests/test_repository_policy.py +43 -1
  96. package/scripts/tests/test_review_closure.py +126 -0
  97. package/scripts/tests/test_review_closure_github.py +240 -0
  98. package/scripts/tests/test_review_closure_model.py +299 -0
  99. package/scripts/tests/test_tracked_secrets.py +42 -0
  100. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  101. package/scripts/repository_checks/email_exemptions.py +0 -39
  102. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -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.all_findings:
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
- repository_constants.FINDING_LINE_TEMPLATE.format(
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 = load_email_exemptions(repository_root)
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 is clean, one for findings, two for usage errors,
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:]))