claude-dev-env 2.3.0 → 2.4.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/CLAUDE.md +3 -2
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +3 -3
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +13 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -903
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +27 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +44 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +2 -2
|
@@ -28,6 +28,12 @@ NON_ZERO_REMOTE_SHA_ONE: str = "1" * 40
|
|
|
28
28
|
NON_ZERO_REMOTE_SHA_TWO: str = "2" * 40
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _isolate_code_review_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
32
|
+
monkeypatch.setenv(
|
|
33
|
+
"CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "no_code_review_gate.py")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
31
37
|
def test_resolve_base_reference_uses_remote_object_when_non_zero() -> None:
|
|
32
38
|
stdin_text = (
|
|
33
39
|
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
@@ -73,6 +79,7 @@ def test_main_exits_zero_when_gate_script_missing(
|
|
|
73
79
|
"CODE_RULES_GATE_PATH",
|
|
74
80
|
str(tmp_path / "does_not_exist.py"),
|
|
75
81
|
)
|
|
82
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
76
83
|
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
77
84
|
|
|
78
85
|
exit_code = pre_push.main()
|
|
@@ -94,6 +101,7 @@ def test_main_invokes_gate_with_resolved_base_reference(
|
|
|
94
101
|
encoding="utf-8",
|
|
95
102
|
)
|
|
96
103
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
104
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
97
105
|
remote_sha = "9" * 40
|
|
98
106
|
monkeypatch.setattr(
|
|
99
107
|
sys,
|
|
@@ -312,6 +320,7 @@ def test_invoke_gate_uses_resolved_path(
|
|
|
312
320
|
symlink_gate_path.symlink_to(real_gate_path)
|
|
313
321
|
resolved_path = symlink_gate_path.resolve()
|
|
314
322
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
|
|
323
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
315
324
|
monkeypatch.setattr(sys, "stdin", io.StringIO(
|
|
316
325
|
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
317
326
|
))
|
|
@@ -428,6 +437,7 @@ def test_main_allows_main_onto_main_push(
|
|
|
428
437
|
passing_gate_path = tmp_path / "gate.py"
|
|
429
438
|
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
430
439
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
440
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
431
441
|
monkeypatch.setattr(
|
|
432
442
|
sys,
|
|
433
443
|
"stdin",
|
|
@@ -439,3 +449,96 @@ def test_main_allows_main_onto_main_push(
|
|
|
439
449
|
exit_code = pre_push.main()
|
|
440
450
|
|
|
441
451
|
assert exit_code == 0
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
CODE_REVIEW_STUB_BLOCK_REASON: str = "CODE_REVIEW_STUB_BLOCK_REASON"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _write_passing_code_rules_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
458
|
+
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
459
|
+
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
460
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
461
|
+
monkeypatch.setattr(
|
|
462
|
+
sys,
|
|
463
|
+
"stdin",
|
|
464
|
+
io.StringIO(
|
|
465
|
+
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
466
|
+
),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _write_code_review_gate_stub(tmp_path: Path, returned_reason_literal: str) -> Path:
|
|
471
|
+
stub_gate_path = tmp_path / "code_review_push_gate.py"
|
|
472
|
+
stub_gate_path.write_text(
|
|
473
|
+
"def deny_reason_for_directory(target_directory):\n"
|
|
474
|
+
f" return {returned_reason_literal}\n",
|
|
475
|
+
encoding="utf-8",
|
|
476
|
+
)
|
|
477
|
+
return stub_gate_path
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_main_blocks_when_code_review_gate_returns_reason(
|
|
481
|
+
tmp_path: Path,
|
|
482
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
483
|
+
capsys: pytest.CaptureFixture[str],
|
|
484
|
+
) -> None:
|
|
485
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
486
|
+
stub_gate_path = _write_code_review_gate_stub(
|
|
487
|
+
tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
|
|
488
|
+
)
|
|
489
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
490
|
+
|
|
491
|
+
exit_code = pre_push.main()
|
|
492
|
+
|
|
493
|
+
assert exit_code == git_hooks_constants.CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
|
|
494
|
+
captured = capsys.readouterr()
|
|
495
|
+
assert CODE_REVIEW_STUB_BLOCK_REASON in captured.err
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_main_allows_when_code_review_gate_returns_none(
|
|
499
|
+
tmp_path: Path,
|
|
500
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
501
|
+
) -> None:
|
|
502
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
503
|
+
stub_gate_path = _write_code_review_gate_stub(tmp_path, "None")
|
|
504
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
505
|
+
|
|
506
|
+
exit_code = pre_push.main()
|
|
507
|
+
|
|
508
|
+
assert exit_code == 0
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_main_code_review_check_fails_open_when_gate_module_absent(
|
|
512
|
+
tmp_path: Path,
|
|
513
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
514
|
+
) -> None:
|
|
515
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
516
|
+
monkeypatch.setenv(
|
|
517
|
+
"CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "does_not_exist.py")
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
exit_code = pre_push.main()
|
|
521
|
+
|
|
522
|
+
assert exit_code == 0
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def test_main_allows_deletion_push_even_when_code_review_gate_would_block(
|
|
526
|
+
tmp_path: Path,
|
|
527
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
528
|
+
) -> None:
|
|
529
|
+
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
530
|
+
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
531
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
532
|
+
stub_gate_path = _write_code_review_gate_stub(
|
|
533
|
+
tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
|
|
534
|
+
)
|
|
535
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
536
|
+
deletion_stdin = (
|
|
537
|
+
f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} "
|
|
538
|
+
f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
539
|
+
)
|
|
540
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(deletion_stdin))
|
|
541
|
+
|
|
542
|
+
exit_code = pre_push.main()
|
|
543
|
+
|
|
544
|
+
assert exit_code == 0
|
package/hooks/hooks.json
CHANGED
|
@@ -57,6 +57,16 @@
|
|
|
57
57
|
}
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"matcher": "mcp__plugin_github_github__create_pull_request",
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_review_pr_create_gate.py",
|
|
66
|
+
"timeout": 20
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
60
70
|
{
|
|
61
71
|
"matcher": "mcp__plugin_github_github__.*",
|
|
62
72
|
"hooks": [
|
|
@@ -79,7 +89,12 @@
|
|
|
79
89
|
"type": "command",
|
|
80
90
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_converge_bugteam_enforcer.py",
|
|
81
91
|
"timeout": 10
|
|
82
|
-
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"matcher": "Agent|Task",
|
|
97
|
+
"hooks": [
|
|
83
98
|
{
|
|
84
99
|
"type": "command",
|
|
85
100
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_verifier_spawn_preflight_gate.py",
|
|
@@ -60,6 +60,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
60
60
|
| `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
|
|
61
61
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
62
62
|
| `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
|
|
63
|
+
| `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
|
|
63
64
|
| `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
|
|
64
65
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
65
66
|
| `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
|
|
@@ -75,4 +75,12 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
|
75
75
|
BashHostedHookEntry(
|
|
76
76
|
"blocking/verdict_directory_write_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
77
77
|
),
|
|
78
|
+
BashHostedHookEntry("blocking/code_review_push_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
79
|
+
BashHostedHookEntry(
|
|
80
|
+
"blocking/code_review_pr_create_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
81
|
+
),
|
|
82
|
+
BashHostedHookEntry(
|
|
83
|
+
"blocking/code_review_stamp_directory_write_blocker.py",
|
|
84
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
85
|
+
),
|
|
78
86
|
)
|
|
@@ -22,6 +22,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
|
22
22
|
"pr_converge_scripts_constants",
|
|
23
23
|
"bugteam_scripts_constants",
|
|
24
24
|
"usage_pause_constants",
|
|
25
|
+
"prototype_scripts_constants",
|
|
25
26
|
"codex_review_scripts_constants",
|
|
26
27
|
"dev_env_scripts_constants",
|
|
27
28
|
"fresh_branch_scripts_constants",
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
"""Configuration constants for the code_verifier_spawn_preflight_gate hook.
|
|
2
2
|
|
|
3
|
-
The gate denies an ``Agent`` spawn whose ``subagent_type`` is
|
|
4
|
-
when the branch carries a merge conflict against its base
|
|
5
|
-
violation on a line added in the working tree since the merge
|
|
6
|
-
the branch or uncommitted)
|
|
7
|
-
pre-flight checks before the expensive
|
|
8
|
-
deny reason to the spawning agent so that
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
The gate denies an ``Agent`` or ``Task`` spawn whose ``subagent_type`` is
|
|
4
|
+
``code-verifier`` when the branch carries a merge conflict against its base
|
|
5
|
+
ref, a CODE_RULES violation on a line added in the working tree since the merge
|
|
6
|
+
base (committed on the branch or uncommitted), or a CODE_RULES engine
|
|
7
|
+
import/load failure. It runs those pre-flight checks before the expensive
|
|
8
|
+
verification spawn and addresses its deny reason to the spawning agent so that
|
|
9
|
+
agent fixes the named issues and re-spawns. Environmental git failures stay
|
|
10
|
+
fail-open; engine import/load failure is fail-closed. Every literal the hook
|
|
11
|
+
body reads lives here; ``AGENT_TOOL_NAME`` comes from
|
|
12
|
+
``pr_converge_bugteam_enforcer_constants`` and joins ``TASK_TOOL_NAME`` in
|
|
13
|
+
``ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES`` so both spawn tool names share one source.
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
from __future__ import annotations
|
|
15
17
|
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
|
|
20
|
+
from hooks_constants.pr_converge_bugteam_enforcer_constants import AGENT_TOOL_NAME
|
|
21
|
+
|
|
18
22
|
CODE_VERIFIER_SUBAGENT_TYPE: str = "code-verifier"
|
|
23
|
+
TASK_TOOL_NAME: str = "Task"
|
|
24
|
+
ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES: frozenset[str] = frozenset(
|
|
25
|
+
{AGENT_TOOL_NAME, TASK_TOOL_NAME}
|
|
26
|
+
)
|
|
19
27
|
|
|
20
28
|
ALL_MERGE_TREE_COMMAND_FLAGS: tuple[str, ...] = (
|
|
21
29
|
"merge-tree",
|
|
@@ -39,11 +47,18 @@ ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
|
|
|
39
47
|
ALL_UNIFIED_ZERO_DIFF_FLAGS: tuple[str, ...] = ("diff", "--unified=0")
|
|
40
48
|
|
|
41
49
|
DENY_REASON_LEAD: str = (
|
|
42
|
-
"BLOCKED [code-verifier-spawn-preflight]:
|
|
43
|
-
"code-verifier is blocked because
|
|
50
|
+
"BLOCKED [code-verifier-spawn-preflight]: a code-verifier spawn "
|
|
51
|
+
"(Agent or Task tool, subagent_type code-verifier) is blocked because "
|
|
52
|
+
"the branch is not in a committable state. "
|
|
44
53
|
"Fix these, then re-spawn the code-verifier:"
|
|
45
54
|
)
|
|
46
55
|
MERGE_CONFLICT_SECTION_HEADER: str = "Merge conflicts vs {base_ref}:"
|
|
47
56
|
CODE_RULES_SECTION_HEADER: str = "CODE_RULES violations on changed lines:"
|
|
57
|
+
ENGINE_LOAD_FAILURE_SECTION: str = (
|
|
58
|
+
"CODE_RULES engine failed to load:\n"
|
|
59
|
+
" The CODE_RULES validate_content engine could not be imported or loaded "
|
|
60
|
+
"(package import/load failure). Repair the enforcer so load_validate_content "
|
|
61
|
+
"succeeds, then re-spawn code-verifier."
|
|
62
|
+
)
|
|
48
63
|
|
|
49
64
|
GATE_SCRIPTS_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts"
|
|
@@ -25,12 +25,29 @@ chained command does not bind the gate to the wrong PR::
|
|
|
25
25
|
|
|
26
26
|
GH_PR_READY_ANCHOR_PATTERN: str = r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)"
|
|
27
27
|
|
|
28
|
-
COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n"
|
|
28
|
+
COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n|(?<![>&])&(?![>&])"
|
|
29
|
+
|
|
30
|
+
BASH_LINE_CONTINUATION_PATTERN: str = r"\\\r?\n[ \t]*"
|
|
29
31
|
|
|
30
32
|
PR_URL_OWNER_REPO_NUMBER_PATTERN: str = (
|
|
31
|
-
r"https
|
|
33
|
+
r"https?://[^/]+/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
|
|
32
34
|
)
|
|
33
35
|
|
|
34
36
|
REPO_OVERRIDE_FLAG_PATTERN: str = (
|
|
35
|
-
r"(?:^|\s)(?:--repo
|
|
37
|
+
r"(?:^|\s)(?:--repo(?:=|\s+)|-R(?:=|\s+)?)"
|
|
38
|
+
r"(?:https?://[^/\s]+/|git@[^:\s]+:|(?:[\w.-]+/)*)"
|
|
39
|
+
r"(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
ALL_GH_PR_VIEW_NUMBER_COMMAND: tuple[str, ...] = (
|
|
43
|
+
"gh",
|
|
44
|
+
"pr",
|
|
45
|
+
"view",
|
|
46
|
+
"--json",
|
|
47
|
+
"number",
|
|
48
|
+
"--jq",
|
|
49
|
+
".number",
|
|
36
50
|
)
|
|
51
|
+
GH_REPO_FLAG: str = "--repo"
|
|
52
|
+
REPO_SLUG_TEMPLATE: str = "{owner}/{repo}"
|
|
53
|
+
GIT_URL_SUFFIX: str = ".git"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Segment-splitting
|
|
1
|
+
"""Segment-splitting, command-name, and deny-mode constants for the destructive command blocker."""
|
|
2
2
|
|
|
3
3
|
ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|&", "|", "&", "\n", "\r"})
|
|
4
4
|
ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
@@ -191,3 +191,5 @@ ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
|
191
191
|
"CLAUDE_JOB_DIR",
|
|
192
192
|
}
|
|
193
193
|
)
|
|
194
|
+
DESTRUCTIVE_DENY_MODE_ENV_VAR: str = "CLAUDE_DESTRUCTIVE_DENY_MODE"
|
|
195
|
+
ALL_TRUTHY_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
@@ -22,8 +22,6 @@ GH_SLURP_FLAG: str = "--slurp"
|
|
|
22
22
|
PR_COMMENTS_API_PATH_TEMPLATE: str = "repos/{{owner}}/{{repo}}/issues/{pr_number}/comments"
|
|
23
23
|
ALL_PR_DIFF_SUBCOMMANDS: tuple[str, ...] = ("pr", "diff")
|
|
24
24
|
PR_DIFF_NAME_ONLY_FLAG: str = "--name-only"
|
|
25
|
-
ALL_PR_VIEW_NUMBER_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "number")
|
|
26
|
-
PR_NUMBER_JSON_FIELD: str = "number"
|
|
27
25
|
COMMENT_BODY_JSON_FIELD: str = "body"
|
|
28
26
|
GH_COMMAND_TIMEOUT_SECONDS: int = 6
|
|
29
27
|
MAX_DIFF_SCAN_CHARS: int = 200000
|
|
@@ -83,7 +81,6 @@ __all__ = [
|
|
|
83
81
|
"ALL_HONEST_GAP_PHRASES",
|
|
84
82
|
"ALL_PROOF_HEADING_KEYWORDS",
|
|
85
83
|
"ALL_PR_DIFF_SUBCOMMANDS",
|
|
86
|
-
"ALL_PR_VIEW_NUMBER_ARGUMENTS",
|
|
87
84
|
"ALL_VISUAL_FILE_SUFFIXES",
|
|
88
85
|
"COMMENT_BODY_JSON_FIELD",
|
|
89
86
|
"DIGIT_PATTERN",
|
|
@@ -104,7 +101,6 @@ __all__ = [
|
|
|
104
101
|
"PROOF_PART_VISUAL_MESSAGE",
|
|
105
102
|
"PR_COMMENTS_API_PATH_TEMPLATE",
|
|
106
103
|
"PR_DIFF_NAME_ONLY_FLAG",
|
|
107
|
-
"PR_NUMBER_JSON_FIELD",
|
|
108
104
|
"PR_READY_GATE_MESSAGE_TEMPLATE",
|
|
109
105
|
"PR_READY_INVOCATION_PATTERN",
|
|
110
106
|
"PR_READY_UNDO_FLAG",
|
|
@@ -80,6 +80,10 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
80
80
|
script_relative_path="blocking/sensitive_file_protector.py",
|
|
81
81
|
applicable_tool_names=ALL_WRITE_AND_EDIT_TOOL_NAMES,
|
|
82
82
|
),
|
|
83
|
+
HostedHookEntry(
|
|
84
|
+
script_relative_path="blocking/code_review_stamp_directory_write_blocker.py",
|
|
85
|
+
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
86
|
+
),
|
|
83
87
|
HostedHookEntry(
|
|
84
88
|
script_relative_path="blocking/pii_prevention_blocker.py",
|
|
85
89
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Table names used when a validator resolves its config from a pyproject.toml.
|
|
2
|
+
|
|
3
|
+
A ``pyproject.toml`` nests each tool's settings under ``[tool.<name>]``, so the
|
|
4
|
+
top-level table key is ``tool`` and each tool matches its own name below it.
|
|
5
|
+
These names live here so the discovery module carries no inline table literals.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"TOOL_TABLE_KEY",
|
|
10
|
+
"MYPY_TOOL_TABLE_NAME",
|
|
11
|
+
"RUFF_TOOL_TABLE_NAME",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
TOOL_TABLE_KEY: str = "tool"
|
|
15
|
+
MYPY_TOOL_TABLE_NAME: str = "mypy"
|
|
16
|
+
RUFF_TOOL_TABLE_NAME: str = "ruff"
|
|
@@ -40,12 +40,18 @@ _EXPECTED_BASH_ORDER = (
|
|
|
40
40
|
"blocking/gh_pr_author_enforcer.py",
|
|
41
41
|
"blocking/verified_commit_gate.py",
|
|
42
42
|
"blocking/verdict_directory_write_blocker.py",
|
|
43
|
+
"blocking/code_review_push_gate.py",
|
|
44
|
+
"blocking/code_review_pr_create_gate.py",
|
|
45
|
+
"blocking/code_review_stamp_directory_write_blocker.py",
|
|
43
46
|
)
|
|
44
47
|
|
|
45
48
|
_POWERSHELL_APPLICABLE = (
|
|
46
49
|
"blocking/pii_prevention_blocker.py",
|
|
47
50
|
"blocking/verified_commit_gate.py",
|
|
48
51
|
"blocking/verdict_directory_write_blocker.py",
|
|
52
|
+
"blocking/code_review_push_gate.py",
|
|
53
|
+
"blocking/code_review_pr_create_gate.py",
|
|
54
|
+
"blocking/code_review_stamp_directory_write_blocker.py",
|
|
49
55
|
)
|
|
50
56
|
|
|
51
57
|
|
|
@@ -80,6 +86,24 @@ def test_powershell_hooks_carry_the_shared_tool_set() -> None:
|
|
|
80
86
|
assert each_entry.applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
81
87
|
|
|
82
88
|
|
|
89
|
+
def test_code_review_gate_family_registered_for_bash_and_powershell() -> None:
|
|
90
|
+
"""The three code-review shell gates run on both Bash and PowerShell."""
|
|
91
|
+
code_review_gate_paths = (
|
|
92
|
+
"blocking/code_review_push_gate.py",
|
|
93
|
+
"blocking/code_review_pr_create_gate.py",
|
|
94
|
+
"blocking/code_review_stamp_directory_write_blocker.py",
|
|
95
|
+
)
|
|
96
|
+
entry_by_path = {
|
|
97
|
+
each_entry.script_relative_path: each_entry
|
|
98
|
+
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
|
|
99
|
+
}
|
|
100
|
+
for each_gate_path in code_review_gate_paths:
|
|
101
|
+
assert each_gate_path in entry_by_path
|
|
102
|
+
assert entry_by_path[each_gate_path].applicable_tool_names == (
|
|
103
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
83
107
|
def test_every_roster_path_points_at_an_existing_hook() -> None:
|
|
84
108
|
"""Each roster path names a hook script present on disk, catching a typo."""
|
|
85
109
|
hooks_root = Path(__file__).resolve().parent.parent
|
|
@@ -63,6 +63,12 @@ def test_duplicate_rmtree_helper_blocker_runs_via_runpy() -> None:
|
|
|
63
63
|
assert entry.native_module_name is None
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def test_code_review_stamp_write_blocker_registered_for_all_file_tools() -> None:
|
|
67
|
+
entry = _entry_for("blocking/code_review_stamp_directory_write_blocker.py")
|
|
68
|
+
assert entry is not None
|
|
69
|
+
assert {"Write", "Edit", "MultiEdit"} <= entry.applicable_tool_names
|
|
70
|
+
|
|
71
|
+
|
|
66
72
|
def test_windows_rmtree_blocker_still_registered() -> None:
|
|
67
73
|
entry = _entry_for("blocking/windows_rmtree_blocker.py")
|
|
68
74
|
assert entry is not None
|
|
@@ -19,9 +19,16 @@ ALL_GH_POST_SUBCOMMANDS: dict[str, frozenset[str]] = {
|
|
|
19
19
|
"issue": frozenset({"create", "comment", "edit"}),
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
PATH_ANCHOR_CHARACTER: str = "/"
|
|
23
|
+
|
|
24
|
+
PATH_SEGMENT_START_CHARACTERS: str = "_-"
|
|
25
|
+
|
|
26
|
+
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
|
|
23
27
|
".claude-editor/jobs/",
|
|
24
28
|
".claude/worktrees/",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
ALL_BARE_VOLATILE_PATH_MARKERS: tuple[str, ...] = (
|
|
25
32
|
"appdata/local/temp",
|
|
26
33
|
"/tmp/",
|
|
27
34
|
"%temp%",
|
|
@@ -12,6 +12,7 @@ A library of check modules used by the validation hooks. Each module focuses on
|
|
|
12
12
|
| `output_formatter.py` | Formats `Violation` lists into human-readable output |
|
|
13
13
|
| `run_all_validators.py` | Entry point — runs every check module and aggregates results |
|
|
14
14
|
| `health_check.py` | Verifies that all validator dependencies (ruff, mypy) are reachable |
|
|
15
|
+
| `pyproject_config_discovery.py` | Shared walk-up primitive that resolves a tool's pyproject.toml config from an original target path, matching the `[tool.<name>]` table the tool owns |
|
|
15
16
|
| `python_style_helpers.py` | Shared source-line splitting and function-discovery helpers imported by `python_style_checks.py` |
|
|
16
17
|
|
|
17
18
|
## Check modules
|
|
@@ -4,19 +4,33 @@ import contextlib
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
6
|
import tempfile
|
|
7
|
-
import tomllib
|
|
8
7
|
from collections.abc import Iterator
|
|
9
8
|
from dataclasses import dataclass
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
|
|
11
|
+
_validators_directory = str(Path(__file__).resolve().parent)
|
|
12
12
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
13
13
|
|
|
14
|
+
try:
|
|
15
|
+
from pyproject_config_discovery import (
|
|
16
|
+
ancestor_directories,
|
|
17
|
+
find_pyproject_configuring_tool,
|
|
18
|
+
)
|
|
19
|
+
except ModuleNotFoundError:
|
|
20
|
+
if _validators_directory not in sys.path:
|
|
21
|
+
sys.path.insert(0, _validators_directory)
|
|
22
|
+
from pyproject_config_discovery import (
|
|
23
|
+
ancestor_directories,
|
|
24
|
+
find_pyproject_configuring_tool,
|
|
25
|
+
)
|
|
26
|
+
|
|
14
27
|
try:
|
|
15
28
|
from hooks_constants.mypy_integration_constants import (
|
|
16
29
|
GIT_DIRECTORY_NAME,
|
|
17
30
|
PYPROJECT_FILENAME,
|
|
18
31
|
PYTHON_SOURCE_SUFFIX,
|
|
19
32
|
)
|
|
33
|
+
from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
|
|
20
34
|
except ModuleNotFoundError:
|
|
21
35
|
if _hooks_directory not in sys.path:
|
|
22
36
|
sys.path.insert(0, _hooks_directory)
|
|
@@ -25,6 +39,7 @@ except ModuleNotFoundError:
|
|
|
25
39
|
PYPROJECT_FILENAME,
|
|
26
40
|
PYTHON_SOURCE_SUFFIX,
|
|
27
41
|
)
|
|
42
|
+
from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
|
|
28
43
|
|
|
29
44
|
|
|
30
45
|
@dataclass
|
|
@@ -48,38 +63,6 @@ def check_mypy_available() -> bool:
|
|
|
48
63
|
return False
|
|
49
64
|
|
|
50
65
|
|
|
51
|
-
def _pyproject_contains_tool_mypy(pyproject_path: Path) -> bool:
|
|
52
|
-
try:
|
|
53
|
-
with pyproject_path.open("rb") as readable_handle:
|
|
54
|
-
parsed_toml = tomllib.load(readable_handle)
|
|
55
|
-
except (OSError, tomllib.TOMLDecodeError):
|
|
56
|
-
return False
|
|
57
|
-
tool_table = parsed_toml.get("tool", {})
|
|
58
|
-
return isinstance(tool_table, dict) and "mypy" in tool_table
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _ancestor_directories(starting_file: Path) -> list[Path]:
|
|
62
|
-
"""Return *starting_file*'s directory and every parent, nearest first.
|
|
63
|
-
|
|
64
|
-
::
|
|
65
|
-
|
|
66
|
-
repo/pkg/mod.py -> [repo/pkg, repo, ... , filesystem root]
|
|
67
|
-
repo/pkg/ -> [repo/pkg, repo, ... , filesystem root]
|
|
68
|
-
|
|
69
|
-
A file resolves to its containing directory; a directory resolves to
|
|
70
|
-
itself, so both ancestor walks below start from the same first candidate.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
starting_file: The file (or directory) the walk begins from.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
The resolved starting directory followed by each of its parents.
|
|
77
|
-
"""
|
|
78
|
-
resolved_starting_file = starting_file.resolve()
|
|
79
|
-
walk_origin = resolved_starting_file.parent if resolved_starting_file.is_file() else resolved_starting_file
|
|
80
|
-
return [walk_origin, *walk_origin.parents]
|
|
81
|
-
|
|
82
|
-
|
|
83
66
|
def find_pyproject_with_mypy_config(starting_file: Path) -> Path | None:
|
|
84
67
|
"""Walk up from a starting file to locate a pyproject.toml that configures mypy.
|
|
85
68
|
|
|
@@ -97,12 +80,7 @@ def find_pyproject_with_mypy_config(starting_file: Path) -> Path | None:
|
|
|
97
80
|
table, or ``None`` when no such file exists between ``starting_file``
|
|
98
81
|
and the filesystem root.
|
|
99
82
|
"""
|
|
100
|
-
|
|
101
|
-
for each_candidate_directory in _ancestor_directories(starting_file):
|
|
102
|
-
candidate_pyproject = each_candidate_directory / pyproject_filename_for_lookup
|
|
103
|
-
if candidate_pyproject.is_file() and _pyproject_contains_tool_mypy(candidate_pyproject):
|
|
104
|
-
return candidate_pyproject
|
|
105
|
-
return None
|
|
83
|
+
return find_pyproject_configuring_tool(starting_file, MYPY_TOOL_TABLE_NAME)
|
|
106
84
|
|
|
107
85
|
|
|
108
86
|
def find_module_resolution_root(starting_file: Path) -> Path | None:
|
|
@@ -127,7 +105,7 @@ def find_module_resolution_root(starting_file: Path) -> Path | None:
|
|
|
127
105
|
"""
|
|
128
106
|
git_entry_name = GIT_DIRECTORY_NAME
|
|
129
107
|
pyproject_filename = PYPROJECT_FILENAME
|
|
130
|
-
for each_candidate_directory in
|
|
108
|
+
for each_candidate_directory in ancestor_directories(starting_file):
|
|
131
109
|
has_git_entry = (each_candidate_directory / git_entry_name).exists()
|
|
132
110
|
has_pyproject = (each_candidate_directory / pyproject_filename).is_file()
|
|
133
111
|
if has_git_entry or has_pyproject:
|
|
@@ -171,7 +149,7 @@ def mypy_working_directory(all_py_files: list[str]) -> Iterator[str]:
|
|
|
171
149
|
yield isolated_directory
|
|
172
150
|
|
|
173
151
|
|
|
174
|
-
def
|
|
152
|
+
def _native_mypy_config_argument(all_py_files: list[str]) -> list[str]:
|
|
175
153
|
"""Return the ``--config-file`` argument for the first file with a mypy config."""
|
|
176
154
|
for each_py_file in all_py_files:
|
|
177
155
|
discovered_pyproject = find_pyproject_with_mypy_config(Path(each_py_file))
|
|
@@ -180,9 +158,39 @@ def _mypy_config_argument(all_py_files: list[str]) -> list[str]:
|
|
|
180
158
|
return []
|
|
181
159
|
|
|
182
160
|
|
|
183
|
-
def
|
|
161
|
+
def _mypy_config_argument(
|
|
162
|
+
all_py_files: list[str], config_source_path: Path | None
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""Return the ``--config-file`` argument, resolved from the original path when given.
|
|
165
|
+
|
|
166
|
+
::
|
|
167
|
+
|
|
168
|
+
config_source_path resolves .../hooks/pyproject.toml
|
|
169
|
+
-> ["--config-file", ".../hooks/pyproject.toml"]
|
|
170
|
+
config_source_path given, no [tool.mypy] up-tree -> [] (native discovery)
|
|
171
|
+
config_source_path None -> native per-file discovery over all_py_files
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
all_py_files: The resolved paths mypy will check.
|
|
175
|
+
config_source_path: The original target path the staged copy stands in
|
|
176
|
+
for, or ``None`` for a native multi-file run.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The ``--config-file`` argument vector, empty when no config resolves.
|
|
180
|
+
"""
|
|
181
|
+
if config_source_path is None:
|
|
182
|
+
return _native_mypy_config_argument(all_py_files)
|
|
183
|
+
resolved_pyproject = find_pyproject_with_mypy_config(config_source_path)
|
|
184
|
+
if resolved_pyproject is not None:
|
|
185
|
+
return ["--config-file", str(resolved_pyproject)]
|
|
186
|
+
return _native_mypy_config_argument(all_py_files)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run_mypy_subprocess(
|
|
190
|
+
all_py_files: list[str], config_source_path: Path | None
|
|
191
|
+
) -> subprocess.CompletedProcess[str]:
|
|
184
192
|
"""Run mypy over *all_py_files* from each file's own project root."""
|
|
185
|
-
config_argument = _mypy_config_argument(all_py_files)
|
|
193
|
+
config_argument = _mypy_config_argument(all_py_files, config_source_path)
|
|
186
194
|
with mypy_working_directory(all_py_files) as working_directory:
|
|
187
195
|
return subprocess.run(
|
|
188
196
|
["mypy", *config_argument, "--ignore-missing-imports", "--no-error-summary"]
|
|
@@ -194,24 +202,29 @@ def _run_mypy_subprocess(all_py_files: list[str]) -> subprocess.CompletedProcess
|
|
|
194
202
|
)
|
|
195
203
|
|
|
196
204
|
|
|
197
|
-
def run_mypy_check(
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
def run_mypy_check(
|
|
206
|
+
all_files: list[Path], config_source_path: Path | None = None
|
|
207
|
+
) -> MypyResult:
|
|
208
|
+
"""Run mypy on files, resolving config from *config_source_path* when given.
|
|
209
|
+
|
|
210
|
+
A given ``config_source_path`` walks ``--config-file`` up from the original
|
|
211
|
+
target rather than the staged copy's own ancestors.
|
|
212
|
+
"""
|
|
213
|
+
if not all_files:
|
|
200
214
|
return MypyResult(passed=True, output="No files to check", error_count=0)
|
|
201
215
|
|
|
202
216
|
if not check_mypy_available():
|
|
203
217
|
return MypyResult(passed=True, output="Mypy not installed - skipping", error_count=0)
|
|
204
218
|
|
|
205
|
-
python_source_suffix = PYTHON_SOURCE_SUFFIX
|
|
206
219
|
all_py_files = [
|
|
207
220
|
str(each_file.resolve())
|
|
208
|
-
for each_file in
|
|
209
|
-
if each_file.suffix ==
|
|
221
|
+
for each_file in all_files
|
|
222
|
+
if each_file.suffix == PYTHON_SOURCE_SUFFIX
|
|
210
223
|
]
|
|
211
224
|
if not all_py_files:
|
|
212
225
|
return MypyResult(passed=True, output="No Python files", error_count=0)
|
|
213
226
|
|
|
214
|
-
completed_process = _run_mypy_subprocess(all_py_files)
|
|
227
|
+
completed_process = _run_mypy_subprocess(all_py_files, config_source_path)
|
|
215
228
|
error_count = completed_process.stdout.count(": error:")
|
|
216
229
|
|
|
217
230
|
return MypyResult(
|