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.
- package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- 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/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_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/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/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/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/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_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_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
|
@@ -27,7 +27,8 @@ publish_plan_max_positional_arguments = 3
|
|
|
27
27
|
publish_plan_failure_injector_position = 2
|
|
28
28
|
frontmatter_unsupported_fields = ("tools", "model", "color", "disable-model-invocation")
|
|
29
29
|
instruction_alias_filenames = frozenset({"AGENTS.md", "CLAUDE.md"})
|
|
30
|
-
|
|
30
|
+
codex_instruction_rule_relative_paths = ("rules/failure-blast-radius.md", "rules/correction-lens.md")
|
|
31
|
+
codex_instruction_source_separator = ", "
|
|
31
32
|
codex_instruction_target_path = "AGENTS.md"
|
|
32
33
|
codex_instruction_section_heading = "## Excerpt for repository-instruction sessions"
|
|
33
34
|
codex_hook_manifest_source_path = "hooks/hooks.json"
|
|
@@ -502,41 +503,57 @@ def convert_agent(agent: ClaudeAgent) -> str:
|
|
|
502
503
|
return content
|
|
503
504
|
|
|
504
505
|
|
|
505
|
-
def
|
|
506
|
-
"""Extract
|
|
506
|
+
def render_codex_instruction_excerpt(rule_content: str, rule_relative_path: str) -> str:
|
|
507
|
+
"""Extract one rule's repository-instruction contract.
|
|
508
|
+
|
|
509
|
+
A rule reaches a Codex session through the fenced block under its
|
|
510
|
+
"Excerpt for repository-instruction sessions" heading, and this returns
|
|
511
|
+
that block's own text.
|
|
507
512
|
|
|
508
513
|
Args:
|
|
509
|
-
rule_content: Canonical
|
|
514
|
+
rule_content: Canonical rule text.
|
|
515
|
+
rule_relative_path: Repository-relative path of that rule, named in errors.
|
|
510
516
|
|
|
511
517
|
Returns:
|
|
512
518
|
The fenced repository-instruction excerpt with a trailing newline.
|
|
513
519
|
|
|
514
520
|
Raises:
|
|
515
|
-
MaterializerError: If the
|
|
521
|
+
MaterializerError: If the rule lacks a complete excerpt.
|
|
516
522
|
"""
|
|
517
523
|
heading_start = rule_content.find(codex_instruction_section_heading)
|
|
518
524
|
if heading_start < 0:
|
|
519
|
-
raise MaterializerError("
|
|
525
|
+
raise MaterializerError(f"{rule_relative_path} requires a Codex excerpt")
|
|
520
526
|
fence_start = rule_content.find("```", heading_start)
|
|
521
527
|
if fence_start < 0:
|
|
522
|
-
raise MaterializerError("
|
|
528
|
+
raise MaterializerError(f"{rule_relative_path} requires a Codex excerpt")
|
|
523
529
|
content_start = rule_content.find(line_separator, fence_start)
|
|
524
530
|
fence_end = rule_content.find(line_separator + "```", content_start + 1)
|
|
525
531
|
if content_start < 0 or fence_end < 0:
|
|
526
|
-
raise MaterializerError("
|
|
532
|
+
raise MaterializerError(f"{rule_relative_path} requires a complete Codex excerpt")
|
|
527
533
|
return rule_content[content_start + len(line_separator) : fence_end].rstrip() + line_separator
|
|
528
534
|
|
|
529
535
|
|
|
530
536
|
def _build_codex_instruction_projection(config: MaterializerConfig) -> PlannedFile | None:
|
|
531
|
-
"""Build the managed AGENTS.md projection
|
|
532
|
-
|
|
533
|
-
|
|
537
|
+
"""Build the managed AGENTS.md projection from every projected rule present."""
|
|
538
|
+
all_present_rules: list[tuple[str, str]] = []
|
|
539
|
+
for each_relative_path in codex_instruction_rule_relative_paths:
|
|
540
|
+
candidate_path = config.source_root / each_relative_path
|
|
541
|
+
if not candidate_path.exists() and not _is_reparse_point(candidate_path):
|
|
542
|
+
continue
|
|
543
|
+
source_path = _validated_source_file(config, each_relative_path, each_relative_path)
|
|
544
|
+
rule_content = source_path.read_text(encoding="utf-8")
|
|
545
|
+
all_present_rules.append(
|
|
546
|
+
(each_relative_path, render_codex_instruction_excerpt(rule_content, each_relative_path))
|
|
547
|
+
)
|
|
548
|
+
if not all_present_rules:
|
|
534
549
|
return None
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
550
|
+
projected_content = line_separator.join(
|
|
551
|
+
each_excerpt for _, each_excerpt in all_present_rules
|
|
552
|
+
)
|
|
538
553
|
return PlannedFile(
|
|
539
|
-
|
|
554
|
+
codex_instruction_source_separator.join(
|
|
555
|
+
each_path for each_path, _ in all_present_rules
|
|
556
|
+
),
|
|
540
557
|
codex_instruction_target_path,
|
|
541
558
|
projected_content,
|
|
542
559
|
hash_content(projected_content),
|
|
@@ -987,7 +1004,7 @@ def discover_agents(config: MaterializerConfig) -> list[ClaudeAgent]:
|
|
|
987
1004
|
if _is_reparse_point(each_path):
|
|
988
1005
|
raise MaterializerError(f"source reparse point is not allowed: {each_path}")
|
|
989
1006
|
relative_source = each_path.relative_to(config.source_root).as_posix()
|
|
990
|
-
if relative_source
|
|
1007
|
+
if relative_source in codex_instruction_rule_relative_paths:
|
|
991
1008
|
_validate_containment(config.source_root, each_path)
|
|
992
1009
|
continue
|
|
993
1010
|
_validate_containment(config.source_root, each_path)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Find contrast framing in authored prose.
|
|
2
|
+
|
|
3
|
+
A contrast defines its subject against a rejected alternative, so the reader
|
|
4
|
+
carries two readings where one would do.
|
|
5
|
+
|
|
6
|
+
::
|
|
7
|
+
|
|
8
|
+
all_hits = find_contrast_framing("A diff regression, not shared code.")
|
|
9
|
+
ok: all_hits == [(1, 18, "trailing-comma-not")]
|
|
10
|
+
flag: find_contrast_framing("The function runs more than 30 lines.") == []
|
|
11
|
+
|
|
12
|
+
Code spans, fenced blocks, paths, and link targets are blanked before the
|
|
13
|
+
patterns run, so an example quoted in backticks stays quiet. A blockquote line
|
|
14
|
+
is skipped, since it carries someone else's words. A comma-``not`` match
|
|
15
|
+
reports only when no bare ``not`` already sits earlier in the line, so a
|
|
16
|
+
second one in a list stays quiet, and a list that opens with ``not`` keeps
|
|
17
|
+
even its first comma quiet. The pattern list lives in the constants module
|
|
18
|
+
beside this one, and the durable post linter reads the same list.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
from banned_prose_words import prose_lines
|
|
26
|
+
from dev_env_scripts_constants.contrast_framing_constants import (
|
|
27
|
+
ALL_CONTRAST_FRAMING_FORMS,
|
|
28
|
+
CONTRAST_FRAMING_MESSAGE_TEMPLATE,
|
|
29
|
+
CONTRAST_FRAMING_QUOTED_LINE_LIMIT,
|
|
30
|
+
ContrastFramingForm,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_BARE_NOT = re.compile(r"\bnot\b", re.IGNORECASE)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _reports(form: ContrastFramingForm, prose_line: str, match_start: int) -> bool:
|
|
37
|
+
if not form.repeats_as_a_list:
|
|
38
|
+
return True
|
|
39
|
+
return _BARE_NOT.search(prose_line[:match_start]) is None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _line_hits(line_number: int, prose_line: str) -> list[tuple[int, int, str]]:
|
|
43
|
+
if prose_line.lstrip().startswith(">"):
|
|
44
|
+
return []
|
|
45
|
+
return [
|
|
46
|
+
(line_number, each_match.start() + 1, each_form.name)
|
|
47
|
+
for each_form in ALL_CONTRAST_FRAMING_FORMS
|
|
48
|
+
for each_match in each_form.pattern.finditer(prose_line)
|
|
49
|
+
if _reports(each_form, prose_line, each_match.start())
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_contrast_framing(document_text: str) -> list[tuple[int, int, str]]:
|
|
54
|
+
"""Return each contrast-framing occurrence in authored prose.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
document_text: Full document source.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Ordered ``(line number, column, form name)`` triples, both 1-based.
|
|
61
|
+
"""
|
|
62
|
+
all_hits: list[tuple[int, int, str]] = []
|
|
63
|
+
for each_line_number, each_prose_line in enumerate(prose_lines(document_text), 1):
|
|
64
|
+
all_hits.extend(_line_hits(each_line_number, each_prose_line))
|
|
65
|
+
return sorted(all_hits)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def form_named(form_name: str) -> ContrastFramingForm:
|
|
69
|
+
"""Return the contrast form carrying one name.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
form_name: Name reported by ``find_contrast_framing``.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
The matching form.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
KeyError: If no form carries that name.
|
|
79
|
+
"""
|
|
80
|
+
for each_form in ALL_CONTRAST_FRAMING_FORMS:
|
|
81
|
+
if each_form.name == form_name:
|
|
82
|
+
return each_form
|
|
83
|
+
raise KeyError(form_name)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def describe_contrast_framing(source_line: str, form_name: str) -> str:
|
|
87
|
+
"""Return the reader-facing message for one occurrence.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
source_line: The document line the occurrence sits on.
|
|
91
|
+
form_name: Name of the form that matched.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The message naming the form, the line it sits on, and the fix.
|
|
95
|
+
"""
|
|
96
|
+
matched_form = form_named(form_name)
|
|
97
|
+
return CONTRAST_FRAMING_MESSAGE_TEMPLATE.format(
|
|
98
|
+
name=form_name,
|
|
99
|
+
text=source_line.strip()[:CONTRAST_FRAMING_QUOTED_LINE_LIMIT],
|
|
100
|
+
guidance=matched_form.guidance,
|
|
101
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Named constants for agent_merge_check, the agent merge check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
GITHUB_API_ROOT = "https://api.github.com"
|
|
6
|
+
GITHUB_GRAPHQL_ENDPOINT = "https://api.github.com/graphql"
|
|
7
|
+
PULL_REQUEST_ENDPOINT_TEMPLATE = "{api_root}/repos/{slug}/pulls/{number}"
|
|
8
|
+
REVIEW_THREADS_ENDPOINT_TEMPLATE = (
|
|
9
|
+
"{api_root}/repos/{slug}/pulls/{number}/ccr/review_threads"
|
|
10
|
+
)
|
|
11
|
+
ACCEPT_HEADER = "Accept"
|
|
12
|
+
GITHUB_ACCEPT_TYPE = "application/vnd.github+json"
|
|
13
|
+
AUTHORIZATION_HEADER = "Authorization"
|
|
14
|
+
BEARER_PREFIX = "Bearer "
|
|
15
|
+
CONTENT_TYPE_HEADER = "Content-Type"
|
|
16
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
17
|
+
UTF8_ENCODING = "utf-8"
|
|
18
|
+
REQUEST_TIMEOUT_SECONDS = 30
|
|
19
|
+
|
|
20
|
+
ALL_TOKEN_ENVIRONMENT_VARIABLES = ("GH_TOKEN", "GITHUB_TOKEN")
|
|
21
|
+
|
|
22
|
+
DRAFT_KEY = "draft"
|
|
23
|
+
MERGEABLE_STATE_KEY = "mergeable_state"
|
|
24
|
+
NUMBER_KEY = "number"
|
|
25
|
+
TITLE_KEY = "title"
|
|
26
|
+
HEAD_KEY = "head"
|
|
27
|
+
SHA_KEY = "sha"
|
|
28
|
+
|
|
29
|
+
MERGEABLE_STATE_CLEAN = "clean"
|
|
30
|
+
MERGEABLE_STATE_BEHIND = "behind"
|
|
31
|
+
MERGEABLE_STATE_DIRTY = "dirty"
|
|
32
|
+
MERGEABLE_STATE_BLOCKED = "blocked"
|
|
33
|
+
MERGEABLE_STATE_UNSTABLE = "unstable"
|
|
34
|
+
MERGEABLE_STATE_UNKNOWN = "unknown"
|
|
35
|
+
SETTLE_ATTEMPT_COUNT = 5
|
|
36
|
+
SETTLE_WAIT_SECONDS = 3
|
|
37
|
+
|
|
38
|
+
DRAFT_HOLD_REASON = (
|
|
39
|
+
"The pull request is a draft, so it carries no verdict to merge on. "
|
|
40
|
+
"Mark it ready once its checks pass."
|
|
41
|
+
)
|
|
42
|
+
BEHIND_HOLD_REASON = (
|
|
43
|
+
"The head is behind the base branch and the branch rule requires an "
|
|
44
|
+
"up-to-date head. Merge the base branch into this one and push."
|
|
45
|
+
)
|
|
46
|
+
DIRTY_HOLD_REASON = (
|
|
47
|
+
"The head conflicts with the base branch. Merge the base branch into "
|
|
48
|
+
"this one, resolve the conflict, and push."
|
|
49
|
+
)
|
|
50
|
+
BLOCKED_HOLD_REASON = (
|
|
51
|
+
"A required status check is not passing on this head. Read the failing "
|
|
52
|
+
"check, fix it, and push."
|
|
53
|
+
)
|
|
54
|
+
UNSTABLE_HOLD_REASON = (
|
|
55
|
+
"A check on this head is failing or still running. Wait for it, and fix "
|
|
56
|
+
"it when it comes back red."
|
|
57
|
+
)
|
|
58
|
+
ALL_HOLD_REASONS_BY_STATE = {
|
|
59
|
+
MERGEABLE_STATE_BEHIND: BEHIND_HOLD_REASON,
|
|
60
|
+
MERGEABLE_STATE_DIRTY: DIRTY_HOLD_REASON,
|
|
61
|
+
MERGEABLE_STATE_BLOCKED: BLOCKED_HOLD_REASON,
|
|
62
|
+
MERGEABLE_STATE_UNSTABLE: UNSTABLE_HOLD_REASON,
|
|
63
|
+
}
|
|
64
|
+
UNKNOWN_STATE_HOLD_TEMPLATE = (
|
|
65
|
+
"GitHub reports the merge state as {state}, which this check does not "
|
|
66
|
+
"treat as ready. Read the pull request page."
|
|
67
|
+
)
|
|
68
|
+
UNRESOLVED_THREADS_HOLD_TEMPLATE = (
|
|
69
|
+
"{count} review thread(s) are open. Answer each one and resolve it."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
MERGE_VERDICT_LABEL = "MERGE"
|
|
73
|
+
HOLD_VERDICT_LABEL = "HOLD"
|
|
74
|
+
VERDICT_LINE_TEMPLATE = "{label} {slug}#{number} {sha} :: {detail}"
|
|
75
|
+
SHORT_SHA_LENGTH = 7
|
|
76
|
+
READY_DETAIL = "green, no open review thread, ready for the agent to merge"
|
|
77
|
+
|
|
78
|
+
MERGE_EXIT_CODE = 0
|
|
79
|
+
HOLD_EXIT_CODE = 1
|
|
80
|
+
ERROR_EXIT_CODE = 2
|
|
81
|
+
|
|
82
|
+
NO_SIGN_IN_MESSAGE = "No GitHub token in the environment. Set GH_TOKEN or GITHUB_TOKEN."
|
|
83
|
+
SLUG_SEPARATOR = "/"
|
|
84
|
+
SLUG_ARGUMENT_HELP = "Repository as owner/name, such as jl-cmd/claude-dev-env."
|
|
85
|
+
NUMBER_ARGUMENT_HELP = "Pull request number."
|
|
86
|
+
COMMAND_DESCRIPTION = (
|
|
87
|
+
"Report whether a pull request is ready for the agent that drives it to merge it."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
REVIEW_THREAD_PAGE_SIZE = 100
|
|
91
|
+
UNRESOLVED_THREAD_QUERY = """
|
|
92
|
+
query($owner: String!, $name: String!, $number: Int!, $pageSize: Int!) {
|
|
93
|
+
repository(owner: $owner, name: $name) {
|
|
94
|
+
pullRequest(number: $number) {
|
|
95
|
+
reviewThreads(first: $pageSize) {
|
|
96
|
+
nodes { isResolved isOutdated }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
"""
|
|
102
|
+
QUERY_KEY = "query"
|
|
103
|
+
VARIABLES_KEY = "variables"
|
|
104
|
+
OWNER_VARIABLE = "owner"
|
|
105
|
+
NAME_VARIABLE = "name"
|
|
106
|
+
NUMBER_VARIABLE = "number"
|
|
107
|
+
PAGE_SIZE_VARIABLE = "pageSize"
|
|
108
|
+
DATA_KEY = "data"
|
|
109
|
+
REPOSITORY_KEY = "repository"
|
|
110
|
+
PULL_REQUEST_KEY = "pullRequest"
|
|
111
|
+
REVIEW_THREADS_KEY = "reviewThreads"
|
|
112
|
+
NODES_KEY = "nodes"
|
|
113
|
+
ALL_THREAD_NODE_KEYS = (
|
|
114
|
+
DATA_KEY,
|
|
115
|
+
REPOSITORY_KEY,
|
|
116
|
+
PULL_REQUEST_KEY,
|
|
117
|
+
REVIEW_THREADS_KEY,
|
|
118
|
+
NODES_KEY,
|
|
119
|
+
)
|
|
120
|
+
ALL_RESOLVED_KEYS = ("isResolved", "is_resolved", "resolved")
|
|
121
|
+
ALL_OUTDATED_KEYS = ("isOutdated", "is_outdated", "outdated")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Constants for the contrast-framing detector.
|
|
2
|
+
|
|
3
|
+
Each form names one shape a sentence takes when it defines its subject
|
|
4
|
+
against a rejected reading::
|
|
5
|
+
|
|
6
|
+
"a diff regression, not shared code" -> trailing-comma-not
|
|
7
|
+
"park it rather than fix it" -> substitution-rather-than
|
|
8
|
+
"precision matters more than coverage" -> comparative-ranking
|
|
9
|
+
ok: "the function runs more than 30 lines"
|
|
10
|
+
|
|
11
|
+
The rule file ``rules/no-contrast-framing.md`` carries one row for each name
|
|
12
|
+
here, and a test holds the two in step.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ContrastFramingForm:
|
|
23
|
+
"""One banned contrast form and the pattern that finds it."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
pattern: re.Pattern[str]
|
|
27
|
+
guidance: str
|
|
28
|
+
repeats_as_a_list: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ALL_CONTRAST_FRAMING_FORMS: tuple[ContrastFramingForm, ...] = (
|
|
32
|
+
ContrastFramingForm(
|
|
33
|
+
"trailing-comma-not",
|
|
34
|
+
re.compile(r",\s+not\s+[\w\"'`(]", re.IGNORECASE),
|
|
35
|
+
"Drop the clause after the comma and state what is true.",
|
|
36
|
+
repeats_as_a_list=True,
|
|
37
|
+
),
|
|
38
|
+
ContrastFramingForm(
|
|
39
|
+
"corrective-it-is-not",
|
|
40
|
+
re.compile(
|
|
41
|
+
r"\b(?:it|that|this)(?:'s|\s+is|\s+was)\s+not\b[^.!?]*?,\s*"
|
|
42
|
+
r"(?:it|that|this)(?:'s|\s+is|\s+was)\b",
|
|
43
|
+
re.IGNORECASE,
|
|
44
|
+
),
|
|
45
|
+
"State what it is and leave the rejected reading out.",
|
|
46
|
+
),
|
|
47
|
+
ContrastFramingForm(
|
|
48
|
+
"substitution-rather-than",
|
|
49
|
+
re.compile(r"\brather than\b", re.IGNORECASE),
|
|
50
|
+
"Name the thing you chose and stop.",
|
|
51
|
+
),
|
|
52
|
+
ContrastFramingForm(
|
|
53
|
+
"additive-not-just",
|
|
54
|
+
re.compile(r"\bnot (?:just|only|merely|simply)\b", re.IGNORECASE),
|
|
55
|
+
"Say the point the sentence is building toward.",
|
|
56
|
+
),
|
|
57
|
+
ContrastFramingForm(
|
|
58
|
+
"comparative-ranking",
|
|
59
|
+
re.compile(
|
|
60
|
+
r"\b(?:matters?|counts?|helps?|weighs?)\s+more\s+than\b"
|
|
61
|
+
r"|\b(?:more|less)\s+(?:important|useful|valuable)\s+than\b"
|
|
62
|
+
r"|\bmore\s+than\s+(?:trying|attempting|hoping)\b",
|
|
63
|
+
re.IGNORECASE,
|
|
64
|
+
),
|
|
65
|
+
"State the thing you want. Leave the ranking out.",
|
|
66
|
+
),
|
|
67
|
+
ContrastFramingForm(
|
|
68
|
+
"substitution-as-opposed-to",
|
|
69
|
+
re.compile(r"\bas opposed to\b", re.IGNORECASE),
|
|
70
|
+
"Name the thing you chose and stop.",
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
CONTRAST_FRAMING_MESSAGE_TEMPLATE: str = (
|
|
75
|
+
'Contrast framing ({name}): "{text}". {guidance}'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
CONTRAST_FRAMING_QUOTED_LINE_LIMIT: int = 120
|
|
79
|
+
|
|
80
|
+
CONTRAST_FRAMING_FINDING_CODE: str = "contrast-framing"
|
|
81
|
+
|
|
82
|
+
CONTRAST_FRAMING_RULE_DOCUMENT: str = "rules/no-contrast-framing.md"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Names and values ``merge_gate_checks.py`` reads a merge policy with.
|
|
2
|
+
|
|
3
|
+
The fenced block marker and the ruleset field names describe a document and a
|
|
4
|
+
GitHub payload that both live outside this package, so they sit here rather
|
|
5
|
+
than inside the module that reads them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
REQUIRED_CHECK_BLOCK_INFO_STRING: str = "required-status-checks"
|
|
9
|
+
FENCE_MARKER: str = "```"
|
|
10
|
+
RULESET_API_TEMPLATE: str = (
|
|
11
|
+
"https://api.github.com/repos/{repository}/rules/branches/{branch}"
|
|
12
|
+
)
|
|
13
|
+
RULESET_API_ACCEPT_HEADER: str = "application/vnd.github+json"
|
|
14
|
+
RULESET_REQUEST_TIMEOUT_SECONDS: int = 30
|
|
15
|
+
REQUIRED_STATUS_CHECKS_RULE_TYPE: str = "required_status_checks"
|
|
16
|
+
RULE_TYPE_KEY: str = "type"
|
|
17
|
+
RULE_PARAMETERS_KEY: str = "parameters"
|
|
18
|
+
RULE_CONTEXT_KEY: str = "context"
|
|
19
|
+
ALL_GITHUB_TOKEN_VARIABLES: tuple[str, ...] = ("GITHUB_TOKEN", "GH_TOKEN")
|
|
20
|
+
MERGE_GATE_CLEAN_EXIT_CODE: int = 0
|
|
21
|
+
MERGE_GATE_MISSING_CHECK_EXIT_CODE: int = 1
|
|
22
|
+
JSON_INDENT_WIDTH: int = 2
|
|
23
|
+
UTF8_ENCODING: str = "utf-8"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Constants for the review_closure script.
|
|
2
|
+
|
|
3
|
+
Per the project's configuration conventions, script-level scalar constants
|
|
4
|
+
live in dev_env_scripts_constants alongside timing.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
GITHUB_API_ROOT: str = "https://api.github.com"
|
|
8
|
+
"""Root of the GitHub REST API."""
|
|
9
|
+
|
|
10
|
+
GITHUB_GRAPHQL_ENDPOINT: str = "https://api.github.com/graphql"
|
|
11
|
+
"""Endpoint that answers the review-thread query."""
|
|
12
|
+
|
|
13
|
+
PULL_REQUEST_ENDPOINT_TEMPLATE: str = "{api_root}/repos/{slug}/pulls/{number}"
|
|
14
|
+
"""REST route carrying one pull request."""
|
|
15
|
+
|
|
16
|
+
REVIEW_THREADS_ENDPOINT_TEMPLATE: str = (
|
|
17
|
+
"{api_root}/repos/{slug}/pulls/{number}/ccr/review_threads"
|
|
18
|
+
)
|
|
19
|
+
"""REST route a Claude Code session has for review threads."""
|
|
20
|
+
|
|
21
|
+
CHECK_RUNS_ENDPOINT_TEMPLATE: str = (
|
|
22
|
+
"{api_root}/repos/{slug}/commits/{sha}/check-runs?per_page={page_size}"
|
|
23
|
+
)
|
|
24
|
+
"""REST route carrying the check runs reported on one commit."""
|
|
25
|
+
|
|
26
|
+
REVIEW_THREAD_PAGE_SIZE: int = 100
|
|
27
|
+
"""How many review threads and comments one query asks for."""
|
|
28
|
+
|
|
29
|
+
CHECK_RUN_PAGE_SIZE: int = 100
|
|
30
|
+
"""How many check runs one request asks for."""
|
|
31
|
+
|
|
32
|
+
REVIEW_THREAD_QUERY: str = """
|
|
33
|
+
query($owner: String!, $name: String!, $number: Int!, $pageSize: Int!) {
|
|
34
|
+
repository(owner: $owner, name: $name) {
|
|
35
|
+
pullRequest(number: $number) {
|
|
36
|
+
reviewThreads(first: $pageSize) {
|
|
37
|
+
nodes {
|
|
38
|
+
path
|
|
39
|
+
isResolved
|
|
40
|
+
isOutdated
|
|
41
|
+
comments(first: $pageSize) {
|
|
42
|
+
nodes {
|
|
43
|
+
body
|
|
44
|
+
author { login }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
"""
|
|
53
|
+
"""GraphQL query for the review threads on one pull request."""
|
|
54
|
+
|
|
55
|
+
ALL_THREAD_NODE_KEYS: tuple[str, ...] = (
|
|
56
|
+
"data",
|
|
57
|
+
"repository",
|
|
58
|
+
"pullRequest",
|
|
59
|
+
"reviewThreads",
|
|
60
|
+
"nodes",
|
|
61
|
+
)
|
|
62
|
+
"""Path from the GraphQL answer down to the review-thread list."""
|
|
63
|
+
|
|
64
|
+
ALL_RESOLVED_KEYS: tuple[str, ...] = ("isResolved", "resolved", "is_resolved")
|
|
65
|
+
"""Field names either route uses for a resolved review thread."""
|
|
66
|
+
|
|
67
|
+
ALL_OUTDATED_KEYS: tuple[str, ...] = ("isOutdated", "outdated", "is_outdated")
|
|
68
|
+
"""Field names either route uses for a thread whose code has been replaced."""
|
|
69
|
+
|
|
70
|
+
ALL_COMMENT_LIST_KEYS: tuple[str, ...] = ("comments", "all_comments")
|
|
71
|
+
"""Field names either route uses for a thread's comment list."""
|
|
72
|
+
|
|
73
|
+
COMMENT_NODES_KEY: str = "nodes"
|
|
74
|
+
"""GraphQL wraps a comment list in this field."""
|
|
75
|
+
|
|
76
|
+
COMMENT_BODY_KEY: str = "body"
|
|
77
|
+
"""Field carrying a comment's text."""
|
|
78
|
+
|
|
79
|
+
ALL_COMMENT_AUTHOR_KEYS: tuple[str, ...] = ("author", "user")
|
|
80
|
+
"""Field names either route uses for a comment's author."""
|
|
81
|
+
|
|
82
|
+
AUTHOR_LOGIN_KEY: str = "login"
|
|
83
|
+
"""Field carrying an author's GitHub login."""
|
|
84
|
+
|
|
85
|
+
THREAD_PATH_KEY: str = "path"
|
|
86
|
+
"""Field carrying the file a review thread points at."""
|
|
87
|
+
|
|
88
|
+
UNNAMED_THREAD_SUBJECT: str = "a review thread"
|
|
89
|
+
"""Subject text for a thread that names no file."""
|
|
90
|
+
|
|
91
|
+
RED_CIRCLE_MARKER: str = "\U0001f534"
|
|
92
|
+
"""The red circle a review marks a blocking finding with."""
|
|
93
|
+
|
|
94
|
+
APPROVALS_CHECK_NAME: str = "Claude Approvals"
|
|
95
|
+
"""Name of the check run whose rows carry blocking findings."""
|
|
96
|
+
|
|
97
|
+
ALL_BLOCKING_CONCLUSIONS: tuple[str, ...] = ("failure", "action_required")
|
|
98
|
+
"""Check-run conclusions that mean a blocking row is open on this head."""
|
|
99
|
+
|
|
100
|
+
APPROVALS_OPEN_REASON: str = (
|
|
101
|
+
"the Claude Approvals check reports a blocking row on this commit"
|
|
102
|
+
)
|
|
103
|
+
"""Why a failing approvals check leaves the pull request open."""
|
|
104
|
+
|
|
105
|
+
RED_CIRCLE_OPEN_REASON: str = (
|
|
106
|
+
"a blocking finding here carries no reply from the driving agent"
|
|
107
|
+
)
|
|
108
|
+
"""Why a red-circle thread stays open until the agent answers it."""
|
|
109
|
+
|
|
110
|
+
UNANSWERED_OPEN_REASON: str = "unresolved, with no reply from the driving agent"
|
|
111
|
+
"""Why an ordinary thread stays open."""
|
|
112
|
+
|
|
113
|
+
CHECK_RUNS_KEY: str = "check_runs"
|
|
114
|
+
"""Field carrying the check-run list in the REST answer."""
|
|
115
|
+
|
|
116
|
+
CHECK_RUN_NAME_KEY: str = "name"
|
|
117
|
+
"""Field carrying a check run's name."""
|
|
118
|
+
|
|
119
|
+
CHECK_RUN_CONCLUSION_KEY: str = "conclusion"
|
|
120
|
+
"""Field carrying a check run's conclusion."""
|
|
121
|
+
|
|
122
|
+
HEAD_KEY: str = "head"
|
|
123
|
+
"""Field carrying a pull request's head."""
|
|
124
|
+
|
|
125
|
+
SHA_KEY: str = "sha"
|
|
126
|
+
"""Field carrying a commit identifier."""
|
|
127
|
+
|
|
128
|
+
NUMBER_KEY: str = "number"
|
|
129
|
+
"""Field carrying a pull request number."""
|
|
130
|
+
|
|
131
|
+
USER_KEY: str = "user"
|
|
132
|
+
"""Field carrying the account that opened a pull request."""
|
|
133
|
+
|
|
134
|
+
SHORT_SHA_LENGTH: int = 7
|
|
135
|
+
"""How much of a commit identifier the verdict line prints."""
|
|
136
|
+
|
|
137
|
+
SLUG_SEPARATOR: str = "/"
|
|
138
|
+
"""Separator between owner and name in a repository slug."""
|
|
139
|
+
|
|
140
|
+
QUERY_KEY: str = "query"
|
|
141
|
+
"""GraphQL payload field carrying the query text."""
|
|
142
|
+
|
|
143
|
+
VARIABLES_KEY: str = "variables"
|
|
144
|
+
"""GraphQL payload field carrying the query variables."""
|
|
145
|
+
|
|
146
|
+
OWNER_VARIABLE: str = "owner"
|
|
147
|
+
"""GraphQL variable naming the repository owner."""
|
|
148
|
+
|
|
149
|
+
NAME_VARIABLE: str = "name"
|
|
150
|
+
"""GraphQL variable naming the repository."""
|
|
151
|
+
|
|
152
|
+
NUMBER_VARIABLE: str = "number"
|
|
153
|
+
"""GraphQL variable naming the pull request number."""
|
|
154
|
+
|
|
155
|
+
PAGE_SIZE_VARIABLE: str = "pageSize"
|
|
156
|
+
"""GraphQL variable naming the page size."""
|
|
157
|
+
|
|
158
|
+
ALL_TOKEN_ENVIRONMENT_VARIABLES: tuple[str, ...] = (
|
|
159
|
+
"GITHUB_TOKEN",
|
|
160
|
+
"GH_TOKEN",
|
|
161
|
+
)
|
|
162
|
+
"""Environment variables that carry a GitHub token, in the order read."""
|
|
163
|
+
|
|
164
|
+
NO_SIGN_IN_MESSAGE: str = (
|
|
165
|
+
"No GitHub token found. Set GITHUB_TOKEN or GH_TOKEN and run this again."
|
|
166
|
+
)
|
|
167
|
+
"""What to print when no token is available."""
|
|
168
|
+
|
|
169
|
+
COMMAND_DESCRIPTION: str = (
|
|
170
|
+
"Report whether every review finding on a pull request head has been "
|
|
171
|
+
"answered by the agent driving it."
|
|
172
|
+
)
|
|
173
|
+
"""Help text for the command."""
|
|
174
|
+
|
|
175
|
+
SLUG_ARGUMENT_HELP: str = "Repository as owner/name."
|
|
176
|
+
"""Help text for the repository argument."""
|
|
177
|
+
|
|
178
|
+
NUMBER_ARGUMENT_HELP: str = "Pull request number."
|
|
179
|
+
"""Help text for the pull request argument."""
|
|
180
|
+
|
|
181
|
+
DRIVER_LOGIN_ARGUMENT_HELP: str = (
|
|
182
|
+
"A login whose comments count as the driving agent's reply. Repeatable. "
|
|
183
|
+
"The account that opened the pull request always counts."
|
|
184
|
+
)
|
|
185
|
+
"""Help text for the driver login argument."""
|
|
186
|
+
|
|
187
|
+
CLOSED_VERDICT_LABEL: str = "CLOSED"
|
|
188
|
+
"""Verdict label for a pull request with every finding answered."""
|
|
189
|
+
|
|
190
|
+
OPEN_VERDICT_LABEL: str = "OPEN"
|
|
191
|
+
"""Verdict label for a pull request with a finding still waiting."""
|
|
192
|
+
|
|
193
|
+
CLOSED_DETAIL: str = "every review finding on this commit is answered"
|
|
194
|
+
"""Detail text on a closed verdict."""
|
|
195
|
+
|
|
196
|
+
OPEN_DETAIL_TEMPLATE: str = "{count} review finding(s) wait on the driving agent"
|
|
197
|
+
"""Detail text on an open verdict."""
|
|
198
|
+
|
|
199
|
+
VERDICT_LINE_TEMPLATE: str = "{label} {slug}#{number} {sha} :: {detail}"
|
|
200
|
+
"""Shape of the one verdict line this command prints."""
|
|
201
|
+
|
|
202
|
+
FINDING_LINE_TEMPLATE: str = " - {subject}: {reason}"
|
|
203
|
+
"""Shape of each open-finding line printed under the verdict."""
|
|
204
|
+
|
|
205
|
+
CLOSED_EXIT_CODE: int = 0
|
|
206
|
+
"""Exit status when every finding is answered."""
|
|
207
|
+
|
|
208
|
+
OPEN_EXIT_CODE: int = 1
|
|
209
|
+
"""Exit status when a finding waits on the driving agent."""
|
|
210
|
+
|
|
211
|
+
ERROR_EXIT_CODE: int = 2
|
|
212
|
+
"""Exit status when the pull request state could not be read."""
|
|
213
|
+
|
|
214
|
+
REQUEST_TIMEOUT_SECONDS: int = 30
|
|
215
|
+
"""How long one GitHub request waits for an answer."""
|
|
216
|
+
|
|
217
|
+
REQUEST_FAILED_TEMPLATE: str = "{url} answered {status}"
|
|
218
|
+
"""What to report when GitHub answers with something other than success."""
|
|
219
|
+
|
|
220
|
+
GET_METHOD: str = "GET"
|
|
221
|
+
"""HTTP method for a read."""
|
|
222
|
+
|
|
223
|
+
POST_METHOD: str = "POST"
|
|
224
|
+
"""HTTP method the GraphQL query uses."""
|
|
225
|
+
|
|
226
|
+
COMMENT_IDS_KEY: str = "comment_ids"
|
|
227
|
+
"""Field the session route uses for a thread's comment identifiers."""
|
|
228
|
+
|
|
229
|
+
COMMENT_IDENTIFIER_KEY: str = "id"
|
|
230
|
+
"""Field carrying a review comment's identifier."""
|
|
231
|
+
|
|
232
|
+
REVIEW_COMMENTS_ENDPOINT_TEMPLATE: str = (
|
|
233
|
+
"{api_root}/repos/{slug}/pulls/{number}/comments?per_page={page_size}&page={page}"
|
|
234
|
+
)
|
|
235
|
+
"""REST route carrying one page of a pull request's review comments."""
|
|
236
|
+
|
|
237
|
+
REVIEW_COMMENT_PAGE_SIZE: int = 100
|
|
238
|
+
"""How many review comments one page carries."""
|
|
239
|
+
|
|
240
|
+
MAX_REVIEW_COMMENT_PAGES: int = 20
|
|
241
|
+
"""How many comment pages one run reads before it stops."""
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Behavioral tests for the host-aware code-review invoker constants module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dev_env_scripts_constants import code_review_constants as review_constants
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_default_effort_is_a_known_token() -> None:
|
|
9
|
+
assert (
|
|
10
|
+
review_constants.DEFAULT_CODE_REVIEW_EFFORT
|
|
11
|
+
in review_constants.ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
|
|
12
|
+
)
|