claude-dev-env 1.95.0 → 2.0.1

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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,168 @@
1
+ """Behavior tests for the Write/Edit payload PII evaluation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ _HOOK_DIR = Path(__file__).parent
13
+ _HOOKS_DIR = _HOOK_DIR.parent
14
+
15
+ try:
16
+ from pii_payload_scan import evaluate_post_body_texts, evaluate_write_edit_payload
17
+ except ImportError:
18
+ for each_bootstrap_directory in (str(_HOOK_DIR), str(_HOOKS_DIR)):
19
+ if each_bootstrap_directory not in sys.path:
20
+ sys.path.insert(0, each_bootstrap_directory)
21
+ from pii_payload_scan import evaluate_post_body_texts, evaluate_write_edit_payload
22
+
23
+ _ALLOW_SLUG = "AllowOwner/allow-repo"
24
+ _OTHER_SLUG = "OtherOwner/other-repo"
25
+
26
+
27
+ def test_post_body_with_pii_is_blocked_by_default() -> None:
28
+ deny_reason = evaluate_post_body_texts([_assembled_fixture_email()])
29
+ assert deny_reason is not None
30
+ assert "post body" in deny_reason
31
+
32
+
33
+ def test_post_body_allowlisted_values_plumb_through_to_the_scan() -> None:
34
+ allowed_value = _assembled_fixture_email()
35
+ deny_reason = evaluate_post_body_texts(
36
+ [allowed_value], all_allowlisted_values=frozenset({allowed_value})
37
+ )
38
+ assert deny_reason is None
39
+
40
+
41
+ def test_post_body_unlisted_value_still_blocked_beside_allowlist() -> None:
42
+ deny_reason = evaluate_post_body_texts(
43
+ [_assembled_unlisted_email()],
44
+ all_allowlisted_values=frozenset({_assembled_fixture_email()}),
45
+ )
46
+ assert deny_reason is not None
47
+ assert "post body" in deny_reason
48
+
49
+
50
+ def _assembled_fixture_email() -> str:
51
+ return "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
52
+
53
+
54
+ def _assembled_unlisted_email() -> str:
55
+ return "other.person" + "@" + "different-co" + ".example" + ".io"
56
+
57
+
58
+ def _write_allowlist_identity(identity_path: Path, slug: str, value: str) -> None:
59
+ identity_path.write_text(
60
+ json.dumps({"pii_allowlisted_values": {slug: [value]}}),
61
+ encoding="utf-8",
62
+ )
63
+
64
+
65
+ def _init_repo_with_github_origin(repository_root: Path, origin_slug: str) -> None:
66
+ repository_root.mkdir(parents=True, exist_ok=True)
67
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
68
+ origin_url = "https://github.com/" + origin_slug + ".git"
69
+ subprocess.run(
70
+ ["git", "remote", "add", "origin", origin_url],
71
+ cwd=repository_root,
72
+ check=True,
73
+ )
74
+
75
+
76
+ def _write_deny_reason(target_path: Path, value: str) -> str | None:
77
+ content = "owner email " + value + "\n"
78
+ return evaluate_write_edit_payload(
79
+ "Write", {"file_path": str(target_path), "content": content}
80
+ )
81
+
82
+
83
+ def test_write_payload_with_a_fixture_email_is_flagged(tmp_path: Path) -> None:
84
+ deny_reason = _write_deny_reason(tmp_path / "notes.md", _assembled_fixture_email())
85
+ assert deny_reason is not None
86
+ assert "email" in deny_reason
87
+
88
+
89
+ def test_allowlisted_value_under_the_allowlisted_repo_is_allowed(
90
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
91
+ ) -> None:
92
+ allowed_value = _assembled_fixture_email()
93
+ identity_path = tmp_path / "local-identity.json"
94
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
95
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
96
+ repository_root = tmp_path / "repo"
97
+ _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
98
+ assert _write_deny_reason(repository_root / "notes.md", allowed_value) is None
99
+
100
+
101
+ def test_allowlisted_value_in_a_different_repo_is_blocked(
102
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
103
+ ) -> None:
104
+ allowed_value = _assembled_fixture_email()
105
+ identity_path = tmp_path / "local-identity.json"
106
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
107
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
108
+ repository_root = tmp_path / "repo"
109
+ _init_repo_with_github_origin(repository_root, _OTHER_SLUG)
110
+ deny_reason = _write_deny_reason(repository_root / "notes.md", allowed_value)
111
+ assert deny_reason is not None
112
+ assert "email" in deny_reason
113
+
114
+
115
+ def test_unlisted_value_in_the_allowlisted_repo_is_blocked(
116
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
117
+ ) -> None:
118
+ identity_path = tmp_path / "local-identity.json"
119
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, _assembled_fixture_email())
120
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
121
+ repository_root = tmp_path / "repo"
122
+ _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
123
+ deny_reason = _write_deny_reason(
124
+ repository_root / "notes.md", _assembled_unlisted_email()
125
+ )
126
+ assert deny_reason is not None
127
+ assert "email" in deny_reason
128
+
129
+
130
+ def test_clean_write_payload_skips_repository_resolution(
131
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
132
+ ) -> None:
133
+ identity_path = tmp_path / "local-identity.json"
134
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, _assembled_fixture_email())
135
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
136
+ repository_root = tmp_path / "repo"
137
+ _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
138
+
139
+ def _fail_if_called(_working_directory: str | None) -> None:
140
+ raise AssertionError(
141
+ "resolve_repository_root should not run for a payload with no PII"
142
+ )
143
+
144
+ monkeypatch.setattr(
145
+ sys.modules["pii_payload_scan"], "resolve_repository_root", _fail_if_called
146
+ )
147
+ deny_reason = evaluate_write_edit_payload(
148
+ "Write",
149
+ {
150
+ "file_path": str(repository_root / "notes.md"),
151
+ "content": "nothing sensitive here\n",
152
+ },
153
+ )
154
+ assert deny_reason is None
155
+
156
+
157
+ def test_allowlisted_value_under_missing_nested_parent_is_allowed(
158
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
159
+ ) -> None:
160
+ allowed_value = _assembled_fixture_email()
161
+ identity_path = tmp_path / "local-identity.json"
162
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
163
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
164
+ repository_root = tmp_path / "repo"
165
+ _init_repo_with_github_origin(repository_root, _ALLOW_SLUG)
166
+ nested_target = repository_root / "new_dir" / "deeper" / "notes.md"
167
+ assert not nested_target.parent.exists()
168
+ assert _write_deny_reason(nested_target, allowed_value) is None
@@ -0,0 +1,108 @@
1
+ """End-to-end tests for the absent-but-tracked restore exemption.
2
+
3
+ Drives the real ``tdd_enforcer.py`` entry hook over a Write payload in a temp
4
+ git repository, proving a remove-then-Write rewrite of committed code is allowed
5
+ even when the paired test's freshness window has lapsed, while new untracked
6
+ code and real edits to a present file stay gated.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ SCRIPT_PATH = Path(__file__).parent / "tdd_enforcer.py"
16
+ STALE_MTIME_OFFSET_SECONDS = 700
17
+ DISABLE_EPHEMERAL_ENVIRONMENT = {"CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT": "1"}
18
+
19
+
20
+ def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
21
+ return subprocess.run(
22
+ [sys.executable, str(SCRIPT_PATH)],
23
+ input=json.dumps(payload),
24
+ text=True,
25
+ capture_output=True,
26
+ check=False,
27
+ env={**os.environ, **DISABLE_EPHEMERAL_ENVIRONMENT},
28
+ )
29
+
30
+
31
+ def _write_payload(file_path: Path, content: str) -> dict:
32
+ return {"tool_name": "Write", "tool_input": {"file_path": str(file_path), "content": content}}
33
+
34
+
35
+ def _decision_from(completed: subprocess.CompletedProcess[str]) -> str | None:
36
+ if not completed.stdout:
37
+ return None
38
+ hook_output = json.loads(completed.stdout).get("hookSpecificOutput", {})
39
+ return hook_output.get("permissionDecision")
40
+
41
+
42
+ def _run_git(repository_root: Path, *arguments: str) -> None:
43
+ subprocess.run(
44
+ ["git", *arguments],
45
+ cwd=str(repository_root),
46
+ check=True,
47
+ capture_output=True,
48
+ env={**os.environ, "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_SYSTEM": os.devnull},
49
+ )
50
+
51
+
52
+ def _init_git_repository(repository_root: Path) -> None:
53
+ _run_git(repository_root, "init", "-q")
54
+ _run_git(repository_root, "config", "user.email", "test@example.com")
55
+ _run_git(repository_root, "config", "user.name", "Test")
56
+ _run_git(repository_root, "config", "commit.gpgsign", "false")
57
+
58
+
59
+ def _commit_tracked_service(repository_root: Path) -> tuple[Path, Path]:
60
+ production_file = repository_root / "service.py"
61
+ production_file.write_text("def serve(): return 1\n")
62
+ sibling_test = repository_root / "test_service.py"
63
+ sibling_test.write_text("def test_serve(): pass\n")
64
+ _run_git(repository_root, "add", "-A")
65
+ _run_git(repository_root, "commit", "-q", "-m", "init")
66
+ return production_file, sibling_test
67
+
68
+
69
+ def test_should_allow_restore_write_of_absent_but_tracked_file_despite_stale_window(
70
+ tmp_path: Path,
71
+ ) -> None:
72
+ repository_root = tmp_path / "repo"
73
+ repository_root.mkdir()
74
+ _init_git_repository(repository_root)
75
+ production_file, sibling_test = _commit_tracked_service(repository_root)
76
+ stale_timestamp = os.stat(sibling_test).st_mtime - STALE_MTIME_OFFSET_SECONDS
77
+ os.utime(sibling_test, (stale_timestamp, stale_timestamp))
78
+ production_file.unlink()
79
+
80
+ completed = _run_hook(_write_payload(production_file, "def serve(): return 2\n"))
81
+
82
+ assert _decision_from(completed) == "allow"
83
+
84
+
85
+ def test_should_still_deny_write_of_absent_untracked_file_without_fresh_test(
86
+ tmp_path: Path,
87
+ ) -> None:
88
+ repository_root = tmp_path / "repo"
89
+ repository_root.mkdir()
90
+ _init_git_repository(repository_root)
91
+ untracked_file = repository_root / "brand_new.py"
92
+
93
+ completed = _run_hook(_write_payload(untracked_file, "def fresh(): return 1\n"))
94
+
95
+ assert _decision_from(completed) == "deny"
96
+
97
+
98
+ def test_should_still_deny_write_of_present_file_with_stale_test(tmp_path: Path) -> None:
99
+ repository_root = tmp_path / "repo"
100
+ repository_root.mkdir()
101
+ _init_git_repository(repository_root)
102
+ production_file, sibling_test = _commit_tracked_service(repository_root)
103
+ stale_timestamp = os.stat(sibling_test).st_mtime - STALE_MTIME_OFFSET_SECONDS
104
+ os.utime(sibling_test, (stale_timestamp, stale_timestamp))
105
+
106
+ completed = _run_hook(_write_payload(production_file, "def serve(): return 2\n"))
107
+
108
+ assert _decision_from(completed) == "deny"
@@ -69,6 +69,36 @@ def _write_sidecar(agent_transcript_file: pathlib.Path, agent_type: str) -> None
69
69
  )
70
70
 
71
71
 
72
+ def _write_verdict_transcript(agent_transcript_file: pathlib.Path, verdict_body: str) -> None:
73
+ verdict_text = f"ok\n```verdict\n{verdict_body}\n```\n"
74
+ agent_transcript_file.write_text(
75
+ json.dumps(
76
+ {
77
+ "type": "assistant",
78
+ "message": {"content": [{"type": "text", "text": verdict_text}]},
79
+ }
80
+ )
81
+ + "\n",
82
+ encoding="utf-8",
83
+ )
84
+
85
+
86
+ def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
87
+ repo_root = tmp_path / "repo"
88
+ repo_root.mkdir()
89
+ return repo_root
90
+
91
+
92
+ def _mint_payload(
93
+ agent_transcript_file: pathlib.Path, repo_root: pathlib.Path, agent_id: str
94
+ ) -> dict[str, str]:
95
+ return {
96
+ "agent_transcript_path": str(agent_transcript_file),
97
+ "cwd": str(repo_root),
98
+ "agent_id": agent_id,
99
+ }
100
+
101
+
72
102
  def test_resolves_subagent_type_from_sidecar(tmp_path: pathlib.Path) -> None:
73
103
  agent_transcript = tmp_path / "agent-7.jsonl"
74
104
  agent_transcript.write_text("", encoding="utf-8")
@@ -141,7 +171,7 @@ def test_verifier_type_without_a_verdict_mints_nothing(tmp_path: pathlib.Path) -
141
171
  assert mint_for_payload(payload) is None
142
172
 
143
173
 
144
- def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
174
+ def _init_repo_with_upstream(repo_root: pathlib.Path) -> None:
145
175
  subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
146
176
  subprocess.run(
147
177
  ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"], check=True
@@ -151,37 +181,20 @@ def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
151
181
  subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
152
182
  subprocess.run(["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True)
153
183
  subprocess.run(["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"], check=True)
184
+
185
+
186
+ def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
187
+ _init_repo_with_upstream(repo_root)
154
188
  (repo_root / "module.py").write_text("answer = 2\n", encoding="utf-8")
155
189
 
156
190
 
157
191
  def test_clean_verifier_verdict_mints_a_verdict_file(tmp_path: pathlib.Path) -> None:
158
- repo_root = tmp_path / "repo"
159
- repo_root.mkdir()
192
+ repo_root = _make_repo(tmp_path)
160
193
  _init_repo_with_upstream_and_edit(repo_root)
161
194
  agent_transcript = tmp_path / "agent-7.jsonl"
162
- agent_transcript.write_text(
163
- json.dumps(
164
- {
165
- "type": "assistant",
166
- "message": {
167
- "content": [
168
- {
169
- "type": "text",
170
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
171
- }
172
- ]
173
- },
174
- }
175
- )
176
- + "\n",
177
- encoding="utf-8",
178
- )
195
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
179
196
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
180
- payload = {
181
- "agent_transcript_path": str(agent_transcript),
182
- "cwd": str(repo_root),
183
- "agent_id": "a02b9583eedc74093",
184
- }
197
+ payload = _mint_payload(agent_transcript, repo_root, "a02b9583eedc74093")
185
198
  verdict_path = mint_for_payload(payload)
186
199
  try:
187
200
  assert verdict_path is not None
@@ -199,7 +212,8 @@ def _deny_rules() -> list[str]:
199
212
 
200
213
 
201
214
  def test_settings_deny_verdict_directory_write() -> None:
202
- assert "Write($HOME/.claude/verification/**)" in _deny_rules()
215
+ assert "Edit($HOME/.claude/verification/**)" in _deny_rules()
216
+ assert "Write($HOME/.claude/verification/**)" not in _deny_rules()
203
217
 
204
218
 
205
219
  def test_settings_deny_verdict_directory_edit() -> None:
@@ -209,87 +223,28 @@ def test_settings_deny_verdict_directory_edit() -> None:
209
223
  def test_minter_refuses_when_attested_hash_equals_empty_surface_hash(
210
224
  tmp_path: pathlib.Path,
211
225
  ) -> None:
212
- repo_root = tmp_path / "repo"
213
- repo_root.mkdir()
226
+ repo_root = _make_repo(tmp_path)
214
227
  _init_repo_with_upstream_and_edit(repo_root)
215
228
  attested_empty = empty_surface_hash()
216
229
  verdict_fence = json.dumps(
217
230
  {"all_pass": True, "findings": [], "manifest_sha256": attested_empty}
218
231
  )
219
232
  agent_transcript = tmp_path / "agent-7.jsonl"
220
- agent_transcript.write_text(
221
- json.dumps(
222
- {
223
- "type": "assistant",
224
- "message": {
225
- "content": [
226
- {
227
- "type": "text",
228
- "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
229
- }
230
- ]
231
- },
232
- }
233
- )
234
- + "\n",
235
- encoding="utf-8",
236
- )
233
+ _write_verdict_transcript(agent_transcript, verdict_fence)
237
234
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
238
- payload = {
239
- "agent_transcript_path": str(agent_transcript),
240
- "cwd": str(repo_root),
241
- "agent_id": "empty-surface-1",
242
- }
235
+ payload = _mint_payload(agent_transcript, repo_root, "empty-surface-1")
243
236
  assert mint_for_payload(payload) is None
244
237
 
245
238
 
246
239
  def test_minter_refuses_when_recomputed_surface_is_empty(
247
240
  tmp_path: pathlib.Path,
248
241
  ) -> None:
249
- repo_root = tmp_path / "repo"
250
- repo_root.mkdir()
251
- subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
252
- subprocess.run(
253
- ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"],
254
- check=True,
255
- )
256
- subprocess.run(
257
- ["git", "-C", str(repo_root), "config", "user.name", "verifier"],
258
- check=True,
259
- )
260
- (repo_root / "module.py").write_text("answer = 1\n", encoding="utf-8")
261
- subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
262
- subprocess.run(
263
- ["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True
264
- )
265
- subprocess.run(
266
- ["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"],
267
- check=True,
268
- )
242
+ repo_root = _make_repo(tmp_path)
243
+ _init_repo_with_upstream(repo_root)
269
244
  agent_transcript = tmp_path / "agent-7.jsonl"
270
- agent_transcript.write_text(
271
- json.dumps(
272
- {
273
- "type": "assistant",
274
- "message": {
275
- "content": [
276
- {
277
- "type": "text",
278
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
279
- }
280
- ]
281
- },
282
- }
283
- )
284
- + "\n",
285
- encoding="utf-8",
286
- )
245
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
287
246
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
288
- payload = {
289
- "agent_transcript_path": str(agent_transcript),
290
- "cwd": str(repo_root),
291
- "agent_id": "empty-recompute-1",
292
- }
247
+ payload = _mint_payload(agent_transcript, repo_root, "empty-recompute-1")
293
248
  assert mint_for_payload(payload) is None
294
249
 
295
250
 
@@ -297,23 +252,7 @@ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
297
252
  tmp_path: pathlib.Path,
298
253
  ) -> None:
299
254
  agent_transcript = tmp_path / "agent-7.jsonl"
300
- agent_transcript.write_text(
301
- json.dumps(
302
- {
303
- "type": "assistant",
304
- "message": {
305
- "content": [
306
- {
307
- "type": "text",
308
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
309
- }
310
- ]
311
- },
312
- }
313
- )
314
- + "\n",
315
- encoding="utf-8",
316
- )
255
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
317
256
  payload = {"agent_transcript_path": str(agent_transcript)}
318
257
  assert resolved_subagent_type(payload) is None
319
258
 
@@ -321,67 +260,25 @@ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
321
260
  def test_does_not_mint_when_sidecar_absent_but_transcript_has_verdict(
322
261
  tmp_path: pathlib.Path,
323
262
  ) -> None:
324
- repo_root = tmp_path / "repo"
325
- repo_root.mkdir()
263
+ repo_root = _make_repo(tmp_path)
326
264
  _init_repo_with_upstream_and_edit(repo_root)
327
265
  agent_transcript = tmp_path / "agent-7.jsonl"
328
- agent_transcript.write_text(
329
- json.dumps(
330
- {
331
- "type": "assistant",
332
- "message": {
333
- "content": [
334
- {
335
- "type": "text",
336
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
337
- }
338
- ]
339
- },
340
- }
341
- )
342
- + "\n",
343
- encoding="utf-8",
344
- )
345
- payload = {
346
- "agent_transcript_path": str(agent_transcript),
347
- "cwd": str(repo_root),
348
- "agent_id": "no-sidecar-1",
349
- }
266
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
267
+ payload = _mint_payload(agent_transcript, repo_root, "no-sidecar-1")
350
268
  assert mint_for_payload(payload) is None
351
269
 
352
270
 
353
271
  def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
354
- repo_root = tmp_path / "repo"
355
- repo_root.mkdir()
272
+ repo_root = _make_repo(tmp_path)
356
273
  _init_repo_with_upstream_and_edit(repo_root)
357
274
  attested_hash = "c" * 64
358
- agent_transcript = tmp_path / "agent-7.jsonl"
359
275
  verdict_fence = json.dumps(
360
276
  {"all_pass": True, "findings": [], "manifest_sha256": attested_hash}
361
277
  )
362
- agent_transcript.write_text(
363
- json.dumps(
364
- {
365
- "type": "assistant",
366
- "message": {
367
- "content": [
368
- {
369
- "type": "text",
370
- "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
371
- }
372
- ]
373
- },
374
- }
375
- )
376
- + "\n",
377
- encoding="utf-8",
378
- )
278
+ agent_transcript = tmp_path / "agent-7.jsonl"
279
+ _write_verdict_transcript(agent_transcript, verdict_fence)
379
280
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
380
- payload = {
381
- "agent_transcript_path": str(agent_transcript),
382
- "cwd": str(repo_root),
383
- "agent_id": "attest-1",
384
- }
281
+ payload = _mint_payload(agent_transcript, repo_root, "attest-1")
385
282
  verdict_path = mint_for_payload(payload)
386
283
  try:
387
284
  assert verdict_path is not None
@@ -0,0 +1,10 @@
1
+ """Put the blocking and hooks directories on sys.path for the blocking tests."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _blocking_directory = str(Path(__file__).resolve().parents[1])
7
+ _hooks_directory = str(Path(__file__).resolve().parents[2])
8
+ for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
9
+ if each_bootstrap_directory not in sys.path:
10
+ sys.path.insert(0, each_bootstrap_directory)