claude-dev-env 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CLAUDE.md +53 -49
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +2 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +0 -1
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/test_agent_frontmatter.py +67 -18
  31. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  32. package/bin/CLAUDE.md +68 -5
  33. package/bin/ever-shipped-skills.mjs +1 -0
  34. package/bin/install-constants.mjs +88 -0
  35. package/bin/install.mjs +1138 -114
  36. package/bin/install.prune.test.mjs +869 -19
  37. package/bin/install.test.mjs +906 -2
  38. package/commands/implement.md +1 -1
  39. package/commands/right-size.md +1 -1
  40. package/docs/CLAUDE.md +1 -0
  41. package/docs/host-pool-health-monitor.md +102 -0
  42. package/docs/references/CLAUDE.md +4 -2
  43. package/docs/references/advisor-tool.md +13 -0
  44. package/docs/references/code-review-enforcement.md +10 -0
  45. package/docs/references/team-advisor-skill.md +14 -0
  46. package/hooks/blocking/CLAUDE.md +1 -0
  47. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  48. package/hooks/blocking/code_review_push_gate.py +9 -4
  49. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  50. package/hooks/blocking/config/__init__.py +5 -5
  51. package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
  52. package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
  53. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  54. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  55. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  56. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  57. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  58. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  59. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  60. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  61. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  62. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  63. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  64. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  65. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  66. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  67. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  68. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  69. package/hooks/git-hooks/test_pre_push.py +25 -0
  70. package/hooks/hooks.json +10 -0
  71. package/hooks/hooks_constants/CLAUDE.md +2 -1
  72. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  73. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  74. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  75. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  76. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  77. package/hooks/pyproject.toml +1 -0
  78. package/hooks/validators/CLAUDE.md +1 -0
  79. package/hooks/validators/config/__init__.py +0 -0
  80. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  81. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  82. package/hooks/validators/conftest.py +4 -0
  83. package/hooks/validators/ruff_integration.py +49 -5
  84. package/hooks/validators/run_all_validators.py +206 -9
  85. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  86. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  87. package/hooks/validators/test_ruff_integration.py +92 -1
  88. package/hooks/validators/test_run_all_validators.py +115 -68
  89. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  90. package/package.json +10 -2
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +45 -44
  93. package/rules/state-what-is.md +25 -0
  94. package/rules/verified-commit-gate-skip.md +1 -1
  95. package/scripts/CLAUDE.md +1 -0
  96. package/scripts/Capture-PoolHealth.ps1 +410 -0
  97. package/scripts/_code_review_test_support.py +404 -0
  98. package/scripts/claude_chain_runner.py +141 -1
  99. package/scripts/conftest.py +16 -1
  100. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  101. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  102. package/scripts/resolve_worker_spawn.py +626 -626
  103. package/scripts/spawn_grok_batch.py +672 -672
  104. package/scripts/test_claude_chain_runner.py +131 -0
  105. package/scripts/test_invoke_code_review_chain.py +70 -0
  106. package/scripts/test_invoke_code_review_cli.py +192 -0
  107. package/scripts/test_invoke_code_review_contract.py +256 -0
  108. package/scripts/test_invoke_code_review_git.py +123 -0
  109. package/scripts/test_invoke_code_review_mode.py +99 -0
  110. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  111. package/skills/CLAUDE.md +2 -0
  112. package/skills/auditing-claude-config/SKILL.md +114 -114
  113. package/skills/autoconverge/SKILL.md +427 -427
  114. package/skills/autoconverge/reference/convergence.md +24 -3
  115. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  116. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  117. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  118. package/skills/autoconverge/workflow/converge.mjs +167 -0
  119. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  120. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  121. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  122. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  123. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  124. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  125. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  126. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  127. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  128. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  129. package/skills/closeout/SKILL.md +33 -50
  130. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  131. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  132. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  133. package/skills/condensing-instructions/SKILL.md +81 -0
  134. package/skills/copilot-review/SKILL.md +119 -119
  135. package/skills/e-code-review/SKILL.md +52 -0
  136. package/skills/e-code-review/reference/fix.md +54 -0
  137. package/skills/e-code-review/reference/loop.md +43 -0
  138. package/skills/e-code-review/reference/low.md +57 -0
  139. package/skills/e-code-review/reference/medium.md +153 -0
  140. package/skills/e-code-review/reference/xhigh.md +182 -0
  141. package/skills/e-simplify/SKILL.md +97 -0
  142. package/skills/issue-tracker/SKILL.md +92 -0
  143. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  144. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  145. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  146. package/skills/orchestrator/SKILL.md +162 -21
  147. package/skills/orchestrator/scripts/status_gate.py +625 -0
  148. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  149. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  150. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  151. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  152. package/skills/orchestrator-refresh/SKILL.md +110 -35
  153. package/skills/plan-to-pr/SKILL.md +155 -0
  154. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  155. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  156. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  157. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  158. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  159. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  160. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  161. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  162. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  163. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  164. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  165. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  166. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  167. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  168. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  169. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  170. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  171. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  172. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  173. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  174. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  175. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  176. package/skills/pr-converge/SKILL.md +472 -469
  177. package/skills/pr-converge/reference/examples.md +3 -3
  178. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  179. package/skills/pr-converge/reference/ground-rules.md +7 -4
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  181. package/skills/pr-converge/reference/per-tick.md +5 -5
  182. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  183. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  184. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  185. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  186. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  187. package/skills/pr-fix-protocol/SKILL.md +1 -1
  188. package/skills/privacy-hygiene/SKILL.md +68 -68
  189. package/skills/prototype/workflows/promotion.md +1 -1
  190. package/skills/release-notes-html/SKILL.md +164 -0
  191. package/skills/task-build/CLAUDE.md +8 -7
  192. package/skills/task-build/SKILL.md +16 -8
  193. package/skills/task-build/reference/tool-routing.md +19 -0
  194. package/scripts/test_invoke_code_review.py +0 -966
  195. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,256 @@
1
+ """Mutation-style tests for the Plan-to-PR packet validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ VALIDATOR_PATH = Path(__file__).with_name("validate_packet.py")
12
+
13
+
14
+ def write_packet(packet_directory: Path) -> None:
15
+ """Create a complete packet and its grounded source file."""
16
+ packet_directory.mkdir(parents=True)
17
+ repository_root = packet_directory.parents[2]
18
+ source_file = repository_root / "src" / "auth.py"
19
+ source_file.parent.mkdir(parents=True, exist_ok=True)
20
+ source_file.write_text(
21
+ "def authenticate_user():\n return True\n", encoding="utf-8"
22
+ )
23
+ task_details = "## Tasks\n- task-1: Login validation; src/auth.py; python -m pytest tests/test_auth.py; python -m pytest tests/test_auth.py; python -m compileall src/auth.py\n"
24
+ for filename, sections in {
25
+ "context.md": (
26
+ "Request",
27
+ "Repository Facts",
28
+ "Constraints",
29
+ "Source References",
30
+ ),
31
+ "plan.md": ("Implementation", "Decisions", "Dependencies", "Risks"),
32
+ "tasks.md": (),
33
+ "handoff.md": ("Approval", "Task Order"),
34
+ }.items():
35
+ headings = "\n".join(
36
+ f"## {section}\nGrounded packet detail." for section in sections
37
+ )
38
+ if filename == "tasks.md":
39
+ headings = task_details.rstrip()
40
+ if filename == "handoff.md":
41
+ headings += "\nPacket: add-login\nApproval: approved\nTask Order: task-1\nTask task-1: Login validation\nAllowed files: src/auth.py\nAcceptance command: python -m pytest tests/test_auth.py\nTest command: python -m pytest tests/test_auth.py\nVerification command: python -m compileall src/auth.py"
42
+ (packet_directory / filename).write_text(headings + "\n", encoding="utf-8")
43
+ payload = {
44
+ "schema_version": 1,
45
+ "slug": packet_directory.name,
46
+ "status": "approved",
47
+ "request": "Add login validation.",
48
+ "allowed_files": ["src/auth.py"],
49
+ "sources": [
50
+ {
51
+ "path": "src/auth.py",
52
+ "locator": "section authenticate_user",
53
+ "fact": "The helper returns the login decision.",
54
+ }
55
+ ],
56
+ "decisions": ["Reuse the existing helper."],
57
+ "open_questions": [],
58
+ "tasks": [
59
+ {
60
+ "id": "task-1",
61
+ "deliverable": "Login validation",
62
+ "allowed_files": ["src/auth.py"],
63
+ "acceptance_command": "python -m pytest tests/test_auth.py",
64
+ "test_command": "python -m pytest tests/test_auth.py",
65
+ "verification_command": "python -m compileall src/auth.py",
66
+ "commit": 1,
67
+ }
68
+ ],
69
+ "validation": {
70
+ "schema_valid": True,
71
+ "boundary_valid": True,
72
+ "markdown_matches": True,
73
+ "validated_by": "native-plan-to-pr",
74
+ },
75
+ }
76
+ (packet_directory / "packet.json").write_text(json.dumps(payload), encoding="utf-8")
77
+
78
+
79
+ def run_validator(packet_directory: Path) -> subprocess.CompletedProcess[str]:
80
+ """Run the validator as a real CLI process."""
81
+ return subprocess.run(
82
+ [sys.executable, str(VALIDATOR_PATH), str(packet_directory)],
83
+ capture_output=True,
84
+ text=True,
85
+ check=False,
86
+ )
87
+
88
+
89
+ def test_valid_packet_passes(tmp_path: Path) -> None:
90
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
91
+ write_packet(packet_directory)
92
+ validator_run = run_validator(packet_directory)
93
+ assert validator_run.returncode == 0
94
+ assert validator_run.stdout.strip() == "packet validation passed"
95
+
96
+
97
+ def test_missing_packet_file_reports_field(tmp_path: Path) -> None:
98
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
99
+ write_packet(packet_directory)
100
+ (packet_directory / "plan.md").unlink()
101
+ validator_run = run_validator(packet_directory)
102
+ assert validator_run.returncode != 0
103
+ assert "missing required file: plan.md" in validator_run.stderr
104
+
105
+
106
+ def test_placeholder_packet_fails(tmp_path: Path) -> None:
107
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
108
+ write_packet(packet_directory)
109
+ (packet_directory / "context.md").write_text(
110
+ "## Request\nTODO: fill this in", encoding="utf-8"
111
+ )
112
+ validator_run = run_validator(packet_directory)
113
+ assert validator_run.returncode != 0
114
+ assert "context.md: contains unresolved" in validator_run.stderr
115
+
116
+
117
+ def test_task_scope_outside_allowed_files_fails(tmp_path: Path) -> None:
118
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
119
+ write_packet(packet_directory)
120
+ packet_file = packet_directory / "packet.json"
121
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
122
+ packet_payload["tasks"][0]["allowed_files"] = ["src/other.py"]
123
+ packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
124
+ validator_run = run_validator(packet_directory)
125
+ assert validator_run.returncode != 0
126
+ assert "outside packet scope" in validator_run.stderr
127
+
128
+
129
+ def test_unknown_schema_field_fails(tmp_path: Path) -> None:
130
+ """Schema-shaped packets reject fields outside the packet contract."""
131
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
132
+ write_packet(packet_directory)
133
+ packet_file = packet_directory / "packet.json"
134
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
135
+ packet_payload["unexpected"] = True
136
+ packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
137
+ validator_run = run_validator(packet_directory)
138
+ assert validator_run.returncode != 0
139
+ assert "unknown field: unexpected" in validator_run.stderr
140
+
141
+
142
+ def test_cyclic_dependencies_fail(tmp_path: Path) -> None:
143
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
144
+ write_packet(packet_directory)
145
+ packet_file = packet_directory / "packet.json"
146
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
147
+ packet_payload["tasks"].append(
148
+ {
149
+ "id": "task-2",
150
+ "deliverable": "Second change",
151
+ "allowed_files": ["src/auth.py"],
152
+ "acceptance_command": "python -m pytest tests/test_auth.py",
153
+ "test_command": "python -m pytest tests/test_auth.py",
154
+ "verification_command": "python -m compileall src/auth.py",
155
+ "commit": 1,
156
+ "dependencies": ["task-1"],
157
+ }
158
+ )
159
+ packet_payload["tasks"][0]["dependencies"] = ["task-2"]
160
+ packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
161
+ validator_run = run_validator(packet_directory)
162
+ assert validator_run.returncode != 0
163
+ assert "dependency cycle detected" in validator_run.stderr
164
+
165
+
166
+ def test_duplicate_dependencies_fail(tmp_path: Path) -> None:
167
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
168
+ write_packet(packet_directory)
169
+ packet_file = packet_directory / "packet.json"
170
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
171
+ packet_payload["tasks"][0]["dependencies"] = ["task-1", "task-1"]
172
+ packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
173
+ validator_run = run_validator(packet_directory)
174
+ assert validator_run.returncode != 0
175
+ assert "dependencies: entries must be unique" in validator_run.stderr
176
+
177
+
178
+ def test_incomplete_handoff_fails(tmp_path: Path) -> None:
179
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
180
+ write_packet(packet_directory)
181
+ (packet_directory / "handoff.md").write_text(
182
+ "## Approval\nApproval: approved\n## Task Order\nTask Order: task-1\n",
183
+ encoding="utf-8",
184
+ )
185
+ validator_run = run_validator(packet_directory)
186
+ assert validator_run.returncode != 0
187
+ assert "handoff.md: task task-1 missing deliverable" in validator_run.stderr
188
+
189
+
190
+ def test_handoff_rejects_wrong_slug_approval_order_and_suffixed_commands(
191
+ tmp_path: Path,
192
+ ) -> None:
193
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
194
+ write_packet(packet_directory)
195
+ handoff_file = packet_directory / "handoff.md"
196
+ handoff_text = handoff_file.read_text(encoding="utf-8")
197
+ handoff_file.write_text(
198
+ handoff_text.replace("Packet: add-login", "Packet: add-login-extra")
199
+ .replace("Approval: approved", "Approval: pending")
200
+ .replace("Task Order: task-1", "Task Order: task-2, task-1")
201
+ .replace(
202
+ "Acceptance command: python -m pytest tests/test_auth.py",
203
+ "Acceptance command: python -m pytest tests/test_auth.py --quiet",
204
+ ),
205
+ encoding="utf-8",
206
+ )
207
+ validator_run = run_validator(packet_directory)
208
+ assert validator_run.returncode != 0
209
+ assert "missing packet slug" in validator_run.stderr
210
+ assert "missing approval state" in validator_run.stderr
211
+ assert "task order does not match" in validator_run.stderr
212
+ assert "missing acceptance command" in validator_run.stderr
213
+
214
+
215
+ def test_task_markdown_rejects_suffixed_commands(tmp_path: Path) -> None:
216
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
217
+ write_packet(packet_directory)
218
+ tasks_file = packet_directory / "tasks.md"
219
+ task_text = tasks_file.read_text(encoding="utf-8")
220
+ tasks_file.write_text(
221
+ task_text.replace(
222
+ "python -m pytest tests/test_auth.py;",
223
+ "python -m pytest tests/test_auth.py --quiet;",
224
+ ),
225
+ encoding="utf-8",
226
+ )
227
+ validator_run = run_validator(packet_directory)
228
+ assert validator_run.returncode != 0
229
+ assert "tasks.md: task task-1 missing acceptance_command" in validator_run.stderr
230
+
231
+
232
+ def test_handoff_rejects_undeclared_task(tmp_path: Path) -> None:
233
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
234
+ write_packet(packet_directory)
235
+ handoff_file = packet_directory / "handoff.md"
236
+ handoff_file.write_text(
237
+ handoff_file.read_text(encoding="utf-8") + "\nTask task-2: Undeclared task\n",
238
+ encoding="utf-8",
239
+ )
240
+ validator_run = run_validator(packet_directory)
241
+ assert validator_run.returncode != 0
242
+ assert "handoff.md: declared tasks do not match packet.json" in validator_run.stderr
243
+
244
+
245
+ def test_source_locator_and_path_boundaries_fail(tmp_path: Path) -> None:
246
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
247
+ write_packet(packet_directory)
248
+ packet_file = packet_directory / "packet.json"
249
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
250
+ packet_payload["sources"][0]["locator"] = "authenticate_user"
251
+ packet_payload["sources"][0]["path"] = "src/auth..py"
252
+ packet_file.write_text(json.dumps(packet_payload), encoding="utf-8")
253
+ validator_run = run_validator(packet_directory)
254
+ assert validator_run.returncode != 0
255
+ assert "locator: must be line, lines, or section locator" in validator_run.stderr
256
+ assert "path: must be a repository-relative path" in validator_run.stderr
@@ -0,0 +1,135 @@
1
+ """Exercise the protocol validator through its command-line interface."""
2
+
3
+ import hashlib
4
+ import importlib.util
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ SCRIPT_PATH = Path(__file__).with_name("validate_protocol.py")
11
+ SCHEMA_PATH = SCRIPT_PATH.parent.parent / "reference" / "run-record.schema.json"
12
+ sys.path.insert(0, str(SCRIPT_PATH.parent))
13
+ VALIDATE_PROTOCOL_SPEC = importlib.util.spec_from_file_location("validate_protocol", SCRIPT_PATH)
14
+ assert VALIDATE_PROTOCOL_SPEC is not None
15
+ assert VALIDATE_PROTOCOL_SPEC.loader is not None
16
+ VALIDATE_PROTOCOL_MODULE = importlib.util.module_from_spec(VALIDATE_PROTOCOL_SPEC)
17
+ VALIDATE_PROTOCOL_SPEC.loader.exec_module(VALIDATE_PROTOCOL_MODULE)
18
+ ProtocolValidationError = VALIDATE_PROTOCOL_MODULE.ProtocolValidationError
19
+ validate_record = VALIDATE_PROTOCOL_MODULE.validate_record
20
+
21
+
22
+ def valid_record() -> dict[str, object]:
23
+ verification = {"acceptance_output": "passed: pytest", "verifier_output": "passed: clean", "verified_commit_gate": "passed: exact surface", "surface_hash": "a" * 64}
24
+ return {
25
+ "task_identity": "task-3", "deliverable": "validator", "allowed_files": ["validator.py"],
26
+ "acceptance_check": "pytest", "baseline": "clean", "worker_route": "implementation worker; effort=low", "commit": "a1b2c3d",
27
+ "review_record": {"resolved_model": "Luna", "effort": "low", "command": "/e-code-review low", "findings": [], "repair_status": "clean", "surface_hash": "a" * 64, "findings_only": True, "has_repair_flag": False},
28
+ "repair_record": {"resolved_model": "Luna", "effort": "low", "confirmed_findings": [], "repair_status": "not-required", "surface_hash": "a" * 64},
29
+ "reverification_record": verification, "verification_record": verification.copy(),
30
+ }
31
+
32
+
33
+ def run_validator(record_path: Path) -> subprocess.CompletedProcess[str]:
34
+ return subprocess.run([sys.executable, str(SCRIPT_PATH), str(record_path)], capture_output=True, text=True, check=False)
35
+
36
+
37
+ def mutate_record(record: dict[str, object], field_name: str, mutated_field: object) -> None:
38
+ if field_name in ("findings_only", "review_extra"):
39
+ review_record = record["review_record"]
40
+ assert isinstance(review_record, dict)
41
+ review_record[field_name] = mutated_field
42
+ elif mutated_field is None:
43
+ del record[field_name]
44
+ else:
45
+ record[field_name] = mutated_field
46
+
47
+
48
+ def test_cli_accepts_valid_record(tmp_path: Path) -> None:
49
+ record_path = tmp_path / "record.json"
50
+ record_path.write_text(json.dumps(valid_record()), encoding="utf-8")
51
+ completed_run = run_validator(record_path)
52
+ assert completed_run.returncode == 0
53
+ assert completed_run.stdout.strip() == "protocol validation passed"
54
+ assert completed_run.stderr == ""
55
+
56
+
57
+ def test_cli_rejects_invalid_utf8_record(tmp_path: Path) -> None:
58
+ record_path = tmp_path / "invalid-utf8.json"
59
+ record_path.write_bytes(b"{\xff")
60
+
61
+ completed_run = run_validator(record_path)
62
+
63
+ assert completed_run.returncode == 2
64
+ assert completed_run.stdout == ""
65
+ assert completed_run.stderr == "protocol validation failed: invalid UTF-8\n"
66
+
67
+
68
+ def test_cli_rejects_invalid_mutations(tmp_path: Path) -> None:
69
+ mutations: tuple[tuple[str, str, object], ...] = (("bad-commit", "commit", "not-a-commit"), ("bad-review", "findings_only", False), ("missing-evidence", "verification_record", None), ("bad-type", "allowed_files", "file.py"), ("empty-allowed-files", "allowed_files", []), ("extra-review-field", "review_extra", "unexpected"))
70
+ for each_name, each_field, mutated_field in mutations:
71
+ record = valid_record()
72
+ mutate_record(record, each_field, mutated_field)
73
+ record_path = tmp_path / f"{each_name}.json"
74
+ record_path.write_text(json.dumps(record), encoding="utf-8")
75
+ completed_run = run_validator(record_path)
76
+ assert completed_run.returncode == 2
77
+ assert completed_run.stdout == ""
78
+ assert completed_run.stderr.startswith("protocol validation failed:")
79
+
80
+
81
+ def test_validate_record_rejects_absolute_allowed_file_paths() -> None:
82
+ for each_absolute_path in ("/absolute/file.py", r"C:\absolute\file.py", "C:/absolute/file.py"):
83
+ record = valid_record()
84
+ record["allowed_files"] = [each_absolute_path]
85
+
86
+ try:
87
+ validate_record(record, SCHEMA_PATH)
88
+ except ProtocolValidationError as error:
89
+ assert str(error) == "allowed_files contains an absolute path"
90
+ else:
91
+ raise AssertionError("absolute allowed file paths must be rejected")
92
+
93
+
94
+ def test_validate_record_enforces_modified_schema(tmp_path: Path) -> None:
95
+ schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
96
+ assert isinstance(schema, dict)
97
+ required_fields = schema["required"]
98
+ assert isinstance(required_fields, list)
99
+ required_fields.append("schema_added_field")
100
+ modified_schema_path = tmp_path / "modified-schema.json"
101
+ modified_schema_path.write_text(json.dumps(schema), encoding="utf-8")
102
+
103
+ try:
104
+ validate_record(valid_record(), modified_schema_path)
105
+ except ProtocolValidationError as error:
106
+ assert "schema_added_field" in str(error)
107
+ else:
108
+ raise AssertionError("modified schema must reject a record missing its new required field")
109
+
110
+
111
+ def test_validate_record_accepts_worktree_with_matching_committed_surface_hash(tmp_path: Path) -> None:
112
+ worktree_path = tmp_path / "worktree"
113
+ worktree_path.mkdir()
114
+ subprocess.run(["git", "init"], cwd=worktree_path, check=True, capture_output=True)
115
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=worktree_path, check=True)
116
+ subprocess.run(["git", "config", "user.name", "Protocol Test"], cwd=worktree_path, check=True)
117
+ tracked_file = worktree_path / "validator.py"
118
+ tracked_file.write_text("baseline\n", encoding="utf-8")
119
+ subprocess.run(["git", "add", "validator.py"], cwd=worktree_path, check=True)
120
+ subprocess.run(["git", "commit", "-m", "baseline"], cwd=worktree_path, check=True, capture_output=True)
121
+ tracked_file.write_text("validated\n", encoding="utf-8")
122
+ subprocess.run(["git", "add", "validator.py"], cwd=worktree_path, check=True)
123
+ subprocess.run(["git", "commit", "-m", "validated"], cwd=worktree_path, check=True, capture_output=True)
124
+ commit_hash = subprocess.run(["git", "rev-parse", "HEAD"], cwd=worktree_path, check=True, capture_output=True, text=True).stdout.strip()
125
+ committed_surface = subprocess.run(["git", "diff", f"{commit_hash}^", commit_hash, "--", "validator.py"], cwd=worktree_path, check=True, capture_output=True).stdout
126
+ surface_hash = hashlib.sha256(committed_surface).hexdigest()
127
+ record = valid_record()
128
+ record["worktree"] = str(worktree_path)
129
+ record["commit"] = commit_hash
130
+ for each_field_name in ("review_record", "repair_record", "reverification_record", "verification_record"):
131
+ nested_record = record[each_field_name]
132
+ assert isinstance(nested_record, dict)
133
+ nested_record["surface_hash"] = surface_hash
134
+
135
+ validate_record(record, SCHEMA_PATH)
@@ -0,0 +1,158 @@
1
+ """Regression tests for set-level task validation."""
2
+
3
+ import json
4
+ import importlib.util
5
+ import subprocess
6
+ import sys
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ SCRIPT_DIRECTORY = Path(__file__).parent
13
+ sys.path.insert(0, str(SCRIPT_DIRECTORY))
14
+ VALIDATE_PROTOCOL_SPEC = importlib.util.spec_from_file_location("validate_protocol", SCRIPT_DIRECTORY / "validate_protocol.py")
15
+ assert VALIDATE_PROTOCOL_SPEC is not None
16
+ assert VALIDATE_PROTOCOL_SPEC.loader is not None
17
+ VALIDATE_PROTOCOL_MODULE = importlib.util.module_from_spec(VALIDATE_PROTOCOL_SPEC)
18
+ sys.modules["validate_protocol"] = VALIDATE_PROTOCOL_MODULE
19
+ VALIDATE_PROTOCOL_SPEC.loader.exec_module(VALIDATE_PROTOCOL_MODULE)
20
+ SCHEMA_PATH = SCRIPT_DIRECTORY.parent / "reference" / "run-record.schema.json"
21
+ ProtocolValidationError = VALIDATE_PROTOCOL_MODULE.ProtocolValidationError
22
+ TEST_VALIDATE_PROTOCOL_SPEC = importlib.util.spec_from_file_location("test_validate_protocol", SCRIPT_DIRECTORY / "test_validate_protocol.py")
23
+ assert TEST_VALIDATE_PROTOCOL_SPEC is not None
24
+ assert TEST_VALIDATE_PROTOCOL_SPEC.loader is not None
25
+ TEST_VALIDATE_PROTOCOL_MODULE = importlib.util.module_from_spec(TEST_VALIDATE_PROTOCOL_SPEC)
26
+ TEST_VALIDATE_PROTOCOL_SPEC.loader.exec_module(TEST_VALIDATE_PROTOCOL_MODULE)
27
+ valid_record = TEST_VALIDATE_PROTOCOL_MODULE.valid_record
28
+ VALIDATE_RUN_SPEC = importlib.util.spec_from_file_location("validate_run", SCRIPT_DIRECTORY / "validate_run.py")
29
+ assert VALIDATE_RUN_SPEC is not None
30
+ assert VALIDATE_RUN_SPEC.loader is not None
31
+ VALIDATE_RUN_MODULE = importlib.util.module_from_spec(VALIDATE_RUN_SPEC)
32
+ VALIDATE_RUN_SPEC.loader.exec_module(VALIDATE_RUN_MODULE)
33
+ main = VALIDATE_RUN_MODULE.main
34
+ validate_run = VALIDATE_RUN_MODULE.validate_run
35
+
36
+
37
+ def test_validate_run_accepts_one_record_per_commit() -> None:
38
+ task_record = valid_record()
39
+ validate_run([task_record], {"a1b2c3d"}, SCHEMA_PATH, {})
40
+
41
+
42
+ def test_validate_run_accepts_matching_explicit_mapping() -> None:
43
+ task_record = valid_record()
44
+ validate_run(
45
+ [task_record],
46
+ {"a1b2c3d"},
47
+ SCHEMA_PATH,
48
+ {"task-3": "a1b2c3d"},
49
+ )
50
+
51
+
52
+ @pytest.mark.parametrize(
53
+ "explicit_mapping",
54
+ [
55
+ {"wrong-task": "a1b2c3d"},
56
+ {"task-3": "deadbeef"},
57
+ ],
58
+ )
59
+ def test_validate_run_rejects_mismatched_explicit_mapping(
60
+ explicit_mapping: dict[str, str],
61
+ ) -> None:
62
+ with pytest.raises(ProtocolValidationError, match="explicit task mappings"):
63
+ validate_run([valid_record()], {"a1b2c3d"}, SCHEMA_PATH, explicit_mapping)
64
+
65
+
66
+ def test_validate_run_rejects_duplicate_commits_in_explicit_mapping() -> None:
67
+ first_record = valid_record()
68
+ second_record = valid_record()
69
+ second_record["task_identity"] = "task-4"
70
+ second_record["commit"] = "deadbeef"
71
+ with pytest.raises(ProtocolValidationError, match="unique commits"):
72
+ validate_run(
73
+ [first_record, second_record],
74
+ {"a1b2c3d"},
75
+ SCHEMA_PATH,
76
+ {"task-1": "a1b2c3d", "task-4": "a1b2c3d"},
77
+ )
78
+
79
+
80
+ @pytest.mark.parametrize(
81
+ "mutation",
82
+ [
83
+ lambda record: record.update(task_identity="task-3"),
84
+ lambda record: record.update(commit="outside"),
85
+ ],
86
+ )
87
+ def test_validate_run_rejects_invalid_commit_or_identity(
88
+ mutation: Callable[[dict[str, object]], None],
89
+ ) -> None:
90
+ first_record = valid_record()
91
+ second_record = valid_record()
92
+ second_record["task_identity"] = "task-4"
93
+ mutation(second_record)
94
+ with pytest.raises(ProtocolValidationError):
95
+ validate_run(
96
+ [first_record, second_record], {"a1b2c3d", "deadbeef"}, SCHEMA_PATH, {}
97
+ )
98
+
99
+
100
+ def test_validate_run_rejects_count_mismatch_without_mapping() -> None:
101
+ with pytest.raises(ProtocolValidationError):
102
+ validate_run([valid_record()], {"a1b2c3d", "deadbeef"}, SCHEMA_PATH, {})
103
+
104
+
105
+ def create_committed_worktree(worktree_path: Path) -> str:
106
+ worktree_path.mkdir()
107
+ initialize_worktree(worktree_path)
108
+ tracked_file = worktree_path / "validator.py"
109
+ tracked_file.write_text("validated\n", encoding="utf-8")
110
+ commit_file(worktree_path)
111
+ return read_head(worktree_path)
112
+
113
+
114
+ def initialize_worktree(worktree_path: Path) -> None:
115
+ subprocess.run(["git", "init"], cwd=worktree_path, check=True, capture_output=True)
116
+ subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=worktree_path, check=True)
117
+ subprocess.run(["git", "config", "user.name", "Protocol Test"], cwd=worktree_path, check=True)
118
+
119
+
120
+ def commit_file(worktree_path: Path) -> None:
121
+ subprocess.run(["git", "add", "validator.py"], cwd=worktree_path, check=True)
122
+ subprocess.run(["git", "commit", "-m", "validated"], cwd=worktree_path, check=True, capture_output=True)
123
+
124
+
125
+ def read_head(worktree_path: Path) -> str:
126
+ return subprocess.run(["git", "rev-parse", "HEAD"], cwd=worktree_path, check=True, capture_output=True, text=True).stdout.strip()
127
+
128
+
129
+ def test_main_rejects_surface_mismatch_when_worktree_is_selected(
130
+ tmp_path: Path,
131
+ ) -> None:
132
+ worktree_path = tmp_path / "worktree"
133
+ commit_hash = create_committed_worktree(worktree_path)
134
+ task_record = valid_record()
135
+ task_record["commit"] = commit_hash
136
+ task_record["allowed_files"] = ["validator.py"]
137
+ for each_field_name in (
138
+ "review_record",
139
+ "repair_record",
140
+ "reverification_record",
141
+ "verification_record",
142
+ ):
143
+ nested_record = task_record[each_field_name]
144
+ assert isinstance(nested_record, dict)
145
+ nested_record["surface_hash"] = "0" * 64
146
+ record_path = tmp_path / "records.json"
147
+ record_path.write_text(json.dumps([task_record]), encoding="utf-8")
148
+ exit_code = main(
149
+ [
150
+ "validate_run.py",
151
+ str(record_path),
152
+ "--commits",
153
+ commit_hash,
154
+ "--worktree",
155
+ str(worktree_path),
156
+ ]
157
+ )
158
+ assert exit_code == 2