claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,65 @@
1
+ """Paginated file intake flattens --slurp pages without losing paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
12
+ if str(_SCRIPTS_DIR) not in sys.path:
13
+ sys.path.insert(0, str(_SCRIPTS_DIR))
14
+
15
+ from split_pr_paginate import fetch_all_pr_changed_files, parse_paginated_files_payload
16
+
17
+
18
+ def test_fetch_all_pr_changed_files_builds_paginated_gh_api_command(
19
+ monkeypatch: pytest.MonkeyPatch,
20
+ ) -> None:
21
+ all_captured: list[list[str]] = []
22
+
23
+ class _Completed:
24
+ returncode = 0
25
+ stdout = (
26
+ '[[{"filename": "x.py", "additions": 1, "deletions": 0, "sha": "s"}]]'
27
+ )
28
+ stderr = ""
29
+
30
+ def _fake_run(all_command: list[str], **_kwargs: object) -> _Completed:
31
+ all_captured.append(list(all_command))
32
+ return _Completed()
33
+
34
+ monkeypatch.setattr("split_pr_paginate.subprocess.run", _fake_run)
35
+ all_files = fetch_all_pr_changed_files("jl-cmd", "claude-dev-env", 1)
36
+ assert all_files[0]["path"] == "x.py"
37
+ assert "--paginate" in all_captured[0]
38
+ assert "--slurp" in all_captured[0]
39
+
40
+
41
+ def test_slurp_pages_are_flattened_in_order() -> None:
42
+ raw = json.dumps(
43
+ [
44
+ [{"filename": "a.py", "additions": 1, "deletions": 0, "sha": "1"}],
45
+ [{"filename": "b.py", "additions": 2, "deletions": 1, "sha": "2"}],
46
+ ]
47
+ )
48
+ all_files = parse_paginated_files_payload(raw)
49
+ assert [each["path"] for each in all_files] == ["a.py", "b.py"]
50
+ assert all_files[1]["additions"] == 2
51
+
52
+
53
+ def test_single_page_array_still_parses() -> None:
54
+ raw = json.dumps([{"filename": "only.py", "additions": 3, "deletions": 0}])
55
+ all_files = parse_paginated_files_payload(raw)
56
+ assert len(all_files) == 1
57
+ assert all_files[0]["path"] == "only.py"
58
+
59
+
60
+ def test_rest_filename_preferred_over_internal_path_key() -> None:
61
+ raw = json.dumps(
62
+ [{"filename": "from-api.py", "path": "stale.py", "additions": 1, "deletions": 0}]
63
+ )
64
+ all_files = parse_paginated_files_payload(raw)
65
+ assert all_files[0]["path"] == "from-api.py"
@@ -0,0 +1,73 @@
1
+ """Split-plan schema validation rejects unassigned paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIR))
13
+
14
+ from split_pr_script_types import build_split_plan, validate_split_plan
15
+
16
+
17
+ def test_build_and_validate_full_assignment() -> None:
18
+ plan = build_split_plan(
19
+ source_commit="abc123",
20
+ all_changed_paths=["a.py", "b.md"],
21
+ all_slices=[
22
+ {
23
+ "id": "01-config",
24
+ "title": "feat: feat: config",
25
+ "layer": "config",
26
+ "all_paths": ["a.py"],
27
+ },
28
+ {
29
+ "id": "02-docs",
30
+ "title": "docs only",
31
+ "layer": "docs",
32
+ "all_paths": ["b.md"],
33
+ },
34
+ ],
35
+ )
36
+ validate_split_plan(plan)
37
+ assert plan["schema_version"] == 1
38
+ assert plan["all_slices"][0]["title"] == "feat: config"
39
+ assert plan["all_slices"][1]["title"] == "chore: docs only"
40
+
41
+
42
+ def test_validate_rejects_unassigned_path() -> None:
43
+ plan = build_split_plan(
44
+ source_commit="abc123",
45
+ all_changed_paths=["a.py", "orphan.py"],
46
+ all_slices=[
47
+ {
48
+ "id": "01",
49
+ "title": "feat: only a",
50
+ "layer": "backend",
51
+ "all_paths": ["a.py"],
52
+ }
53
+ ],
54
+ )
55
+ with pytest.raises(ValueError, match="missing"):
56
+ validate_split_plan(plan)
57
+
58
+
59
+ def test_validate_rejects_empty_slice_id() -> None:
60
+ plan = build_split_plan(
61
+ source_commit="abc123",
62
+ all_changed_paths=["a.py"],
63
+ all_slices=[
64
+ {
65
+ "id": " ",
66
+ "title": "feat: bare",
67
+ "layer": "backend",
68
+ "all_paths": ["a.py"],
69
+ }
70
+ ],
71
+ )
72
+ with pytest.raises(ValueError, match="slice id"):
73
+ validate_split_plan(plan)
@@ -0,0 +1,28 @@
1
+ """Title normalization carries exactly one conventional prefix."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
9
+ if str(_SCRIPTS_DIR) not in sys.path:
10
+ sys.path.insert(0, str(_SCRIPTS_DIR))
11
+
12
+ from split_pr_title import normalize_split_title
13
+
14
+
15
+ def test_single_prefix_unchanged() -> None:
16
+ assert normalize_split_title("feat: add gate") == "feat: add gate"
17
+
18
+
19
+ def test_stacked_prefixes_collapse_to_one() -> None:
20
+ assert normalize_split_title("feat: feat: add gate") == "feat: add gate"
21
+
22
+
23
+ def test_bare_title_gets_default_chore_prefix() -> None:
24
+ assert normalize_split_title("add gate") == "chore: add gate"
25
+
26
+
27
+ def test_scoped_prefix_normalized() -> None:
28
+ assert normalize_split_title("fix(hooks): repair gate") == "fix: repair gate"
@@ -0,0 +1,46 @@
1
+ """Dependency graph verifier CLI and rebuild checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
12
+ if str(_SCRIPTS_DIR) not in sys.path:
13
+ sys.path.insert(0, str(_SCRIPTS_DIR))
14
+
15
+ from split_pr_dependency_graph import build_dependency_graph
16
+ from verify_dependency_graph import main, verify_dependency_graph_document
17
+
18
+
19
+ def test_verify_accepts_rebuilt_graph() -> None:
20
+ all_slices = [
21
+ {"id": "01-config", "layer": "config"},
22
+ {"id": "02-docs", "layer": "docs"},
23
+ ]
24
+ graph = build_dependency_graph(all_slices)
25
+ graph["all_slices"] = all_slices
26
+ verify_dependency_graph_document(graph)
27
+
28
+
29
+ def test_verify_rejects_order_mismatch() -> None:
30
+ all_slices = [
31
+ {"id": "01-config", "layer": "config"},
32
+ {"id": "02-docs", "layer": "docs"},
33
+ ]
34
+ graph = build_dependency_graph(all_slices)
35
+ graph["all_slices"] = all_slices
36
+ graph["topological_order"] = ["02-docs", "01-config"]
37
+ with pytest.raises(ValueError, match="mismatch"):
38
+ verify_dependency_graph_document(graph)
39
+
40
+
41
+ def test_cli_ok(tmp_path: Path) -> None:
42
+ all_slices = [{"id": "a", "layer": "backend"}]
43
+ graph = build_dependency_graph(all_slices)
44
+ path = tmp_path / "g.json"
45
+ path.write_text(json.dumps(graph), encoding="utf-8")
46
+ assert main(["--graph-json", str(path)]) == 0
@@ -0,0 +1,56 @@
1
+ """verify_plan accepts full coverage and rejects gaps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
12
+ if str(_SCRIPTS_DIR) not in sys.path:
13
+ sys.path.insert(0, str(_SCRIPTS_DIR))
14
+
15
+ from verify_plan import main, verify_split_plan_coverage
16
+
17
+
18
+ def _full_plan() -> dict[str, object]:
19
+ return {
20
+ "schema_version": 1,
21
+ "source_commit": "abc",
22
+ "all_changed_paths": ["src/a.py", "docs/b.md"],
23
+ "all_slices": [
24
+ {
25
+ "id": "01-backend",
26
+ "title": "feat: a",
27
+ "layer": "backend",
28
+ "all_paths": ["src/a.py"],
29
+ },
30
+ {
31
+ "id": "02-docs",
32
+ "title": "docs: b",
33
+ "layer": "docs",
34
+ "all_paths": ["docs/b.md"],
35
+ },
36
+ ],
37
+ }
38
+
39
+
40
+ def test_verify_accepts_full_unique_coverage() -> None:
41
+ verify_split_plan_coverage(_full_plan())
42
+
43
+
44
+ def test_verify_rejects_missing_path() -> None:
45
+ plan = _full_plan()
46
+ all_slices = plan["all_slices"]
47
+ assert isinstance(all_slices, list)
48
+ plan["all_slices"] = [all_slices[0]]
49
+ with pytest.raises(ValueError, match="missing"):
50
+ verify_split_plan_coverage(plan)
51
+
52
+
53
+ def test_cli_ok_on_temp_plan(tmp_path: Path) -> None:
54
+ plan_path = tmp_path / "plan.json"
55
+ plan_path.write_text(json.dumps(_full_plan()), encoding="utf-8")
56
+ assert main(["--plan-json", str(plan_path)]) == 0
@@ -0,0 +1,50 @@
1
+ """Contract: source_commit and title normalization are required."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIR))
13
+
14
+ from verify_plan import verify_split_plan_coverage
15
+
16
+
17
+ def test_missing_source_commit_fails() -> None:
18
+ plan = {
19
+ "schema_version": 1,
20
+ "source_commit": "",
21
+ "all_changed_paths": ["a.py"],
22
+ "all_slices": [
23
+ {
24
+ "id": "01",
25
+ "title": "feat: a",
26
+ "layer": "backend",
27
+ "all_paths": ["a.py"],
28
+ }
29
+ ],
30
+ }
31
+ with pytest.raises(ValueError, match="source_commit"):
32
+ verify_split_plan_coverage(plan)
33
+
34
+
35
+ def test_stacked_title_fails_after_build_would_have_normalized() -> None:
36
+ plan = {
37
+ "schema_version": 1,
38
+ "source_commit": "abc",
39
+ "all_changed_paths": ["a.py"],
40
+ "all_slices": [
41
+ {
42
+ "id": "01",
43
+ "title": "feat: feat: a",
44
+ "layer": "backend",
45
+ "all_paths": ["a.py"],
46
+ }
47
+ ],
48
+ }
49
+ with pytest.raises(ValueError, match="title"):
50
+ verify_split_plan_coverage(plan)
@@ -0,0 +1,45 @@
1
+ """Path normalization fails closed on unsafe paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIR))
13
+
14
+ from verify_plan import normalize_repo_path, verify_split_plan_coverage
15
+
16
+
17
+ def test_normalize_strips_dot_slash() -> None:
18
+ assert normalize_repo_path("./src/a.py") == "src/a.py"
19
+ assert normalize_repo_path("src\\a.py") == "src/a.py"
20
+
21
+
22
+ def test_normalize_rejects_parent_and_absolute() -> None:
23
+ with pytest.raises(ValueError):
24
+ normalize_repo_path("../secret")
25
+ with pytest.raises(ValueError):
26
+ normalize_repo_path("/etc/passwd")
27
+ with pytest.raises(ValueError):
28
+ normalize_repo_path("C:/Windows/system32")
29
+
30
+
31
+ def test_verify_normalizes_mixed_separators() -> None:
32
+ plan = {
33
+ "schema_version": 1,
34
+ "source_commit": "abc",
35
+ "all_changed_paths": ["src\\a.py"],
36
+ "all_slices": [
37
+ {
38
+ "id": "01",
39
+ "title": "feat: a",
40
+ "layer": "backend",
41
+ "all_paths": ["./src/a.py"],
42
+ }
43
+ ],
44
+ }
45
+ verify_split_plan_coverage(plan)
@@ -0,0 +1,111 @@
1
+ """CLI and checks for a split-plan dependency graph document.
2
+
3
+ ::
4
+
5
+ python verify_dependency_graph.py --graph-json graph.json
6
+ # exit 0 when shape is valid and order matches a rebuild when all_slices set
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ if str(Path(__file__).resolve().parent) not in sys.path:
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+
19
+ from config.dependency_constants import (
20
+ EDGE_KEY_FROM,
21
+ EDGE_KEY_TO,
22
+ GRAPH_KEY_EDGES,
23
+ GRAPH_KEY_NODES,
24
+ GRAPH_KEY_ORDER,
25
+ )
26
+ from config.plan_constants import (
27
+ EXIT_CODE_SUCCESS,
28
+ PLAN_KEY_ALL_SLICES,
29
+ UTF8_ENCODING,
30
+ )
31
+ from config.split_pr_constants import EXIT_CODE_FAILURE
32
+ from split_pr_dependency_graph import build_dependency_graph
33
+ from split_pr_script_types import JsonObject
34
+
35
+
36
+ def verify_dependency_graph_document(all_graph: JsonObject) -> None:
37
+ """Validate graph shape; when ``all_slices`` is set, match rebuild order.
38
+
39
+ Args:
40
+ all_graph: Graph document with nodes, edges, topological_order, and
41
+ optional ``all_slices``. Always checks: non-empty unique nodes,
42
+ order lists each node once, edges reference known nodes. When
43
+ ``all_slices`` is a non-empty list, rebuilds and requires
44
+ topological_order to match.
45
+
46
+ Raises:
47
+ ValueError: When shape checks fail or topological_order mismatches
48
+ a rebuild from ``all_slices``.
49
+ """
50
+ all_nodes = all_graph.get(GRAPH_KEY_NODES)
51
+ all_edges = all_graph.get(GRAPH_KEY_EDGES)
52
+ all_order = all_graph.get(GRAPH_KEY_ORDER)
53
+ if not isinstance(all_nodes, list) or not all_nodes:
54
+ raise ValueError("nodes must be a non-empty list")
55
+ if not isinstance(all_edges, list):
56
+ raise ValueError("edges must be a list")
57
+ if not isinstance(all_order, list):
58
+ raise ValueError("topological_order must be a list")
59
+ all_node_ids = [str(each) for each in all_nodes]
60
+ if len(all_node_ids) != len(set(all_node_ids)):
61
+ raise ValueError("nodes contains duplicates")
62
+ all_order_ids = [str(each) for each in all_order]
63
+ if sorted(all_order_ids) != sorted(all_node_ids):
64
+ raise ValueError("topological_order must list each node exactly once")
65
+ all_node_set = set(all_node_ids)
66
+ for each_edge in all_edges:
67
+ if not isinstance(each_edge, dict):
68
+ raise ValueError("each edge must be an object")
69
+ edge_from = str(each_edge.get(EDGE_KEY_FROM, ""))
70
+ edge_to = str(each_edge.get(EDGE_KEY_TO, ""))
71
+ if edge_from not in all_node_set or edge_to not in all_node_set:
72
+ raise ValueError(f"edge uses unknown node: {each_edge!r}")
73
+ all_slices = all_graph.get(PLAN_KEY_ALL_SLICES)
74
+ if isinstance(all_slices, list) and all_slices:
75
+ rebuilt = build_dependency_graph(all_slices)
76
+ if rebuilt[GRAPH_KEY_ORDER] != all_order_ids:
77
+ raise ValueError(
78
+ f"topological_order mismatch expected {rebuilt[GRAPH_KEY_ORDER]!r} "
79
+ f"got {all_order_ids!r}"
80
+ )
81
+
82
+
83
+ def main(all_argv: list[str]) -> int:
84
+ """CLI entry for graph verification.
85
+
86
+ Args:
87
+ all_argv: Args without program name.
88
+
89
+ Returns:
90
+ 0 on success, 1 on failure.
91
+
92
+ Raises:
93
+ Does not raise; writes a JSON error payload on failure.
94
+ """
95
+ parser = argparse.ArgumentParser(description=__doc__)
96
+ parser.add_argument("--graph-json", type=Path, required=True)
97
+ arguments = parser.parse_args(all_argv)
98
+ try:
99
+ payload = json.loads(arguments.graph_json.read_text(encoding=UTF8_ENCODING))
100
+ if not isinstance(payload, dict):
101
+ raise ValueError("graph root must be an object")
102
+ verify_dependency_graph_document(payload)
103
+ except (OSError, ValueError, json.JSONDecodeError) as error:
104
+ print(json.dumps({"ok": False, "error": str(error)}))
105
+ return EXIT_CODE_FAILURE
106
+ print(json.dumps({"ok": True}))
107
+ return EXIT_CODE_SUCCESS
108
+
109
+
110
+ if __name__ == "__main__":
111
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,139 @@
1
+ """Verify complete unique path coverage for a split-plan document.
2
+
3
+ ::
4
+
5
+ python verify_plan.py --plan-json plan.json
6
+ # exit 0 when every changed path is assigned once
7
+
8
+ Fails closed on unassigned paths, duplicate assignments, empty source
9
+ commit, and non-normalized titles.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ if str(Path(__file__).resolve().parent) not in sys.path:
20
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
21
+
22
+ from config.plan_constants import (
23
+ EXIT_CODE_SUCCESS,
24
+ PATH_SEPARATOR,
25
+ PLAN_KEY_ALL_CHANGED_PATHS,
26
+ PLAN_KEY_ALL_SLICES,
27
+ PLAN_KEY_SOURCE_COMMIT,
28
+ SLICE_KEY_ALL_PATHS,
29
+ UTF8_ENCODING,
30
+ VERIFY_PAYLOAD_KEY_OK,
31
+ )
32
+ from config.split_pr_constants import EXIT_CODE_FAILURE, PAYLOAD_KEY_ERROR
33
+ from split_pr_script_types import JsonObject, validate_split_plan
34
+
35
+
36
+ def _is_drive_path(path_text: str) -> bool:
37
+ return len(path_text) >= 2 and path_text[1] == ":"
38
+
39
+
40
+ def normalize_repo_path(raw_path: str) -> str:
41
+ """Normalize a changed-path string for coverage comparison.
42
+
43
+ Args:
44
+ raw_path: Path as stored in the plan or intake list.
45
+
46
+ Returns:
47
+ Forward-slash path without a leading ``./``.
48
+
49
+ Raises:
50
+ ValueError: When the path is empty, absolute, or has ``..`` segments.
51
+ """
52
+ text = raw_path.strip().replace("\\", PATH_SEPARATOR)
53
+ if not text or text.startswith(PATH_SEPARATOR) or _is_drive_path(text):
54
+ raise ValueError(f"unsafe or empty path: {raw_path!r}")
55
+ all_parts = [
56
+ each for each in text.split(PATH_SEPARATOR) if each not in ("", ".")
57
+ ]
58
+ if any(each == ".." for each in all_parts):
59
+ raise ValueError(f"ambiguous parent path segment: {raw_path!r}")
60
+ if not all_parts:
61
+ raise ValueError(f"empty path after normalization: {raw_path!r}")
62
+ return PATH_SEPARATOR.join(all_parts)
63
+
64
+
65
+ def _normalize_plan_paths(all_plan: JsonObject) -> JsonObject:
66
+ all_changed = all_plan.get(PLAN_KEY_ALL_CHANGED_PATHS, [])
67
+ if not isinstance(all_changed, list):
68
+ raise ValueError("all_changed_paths must be a list")
69
+ all_slices = all_plan.get(PLAN_KEY_ALL_SLICES, [])
70
+ if not isinstance(all_slices, list):
71
+ raise ValueError("all_slices must be a list")
72
+ all_normalized_changed = [normalize_repo_path(str(each)) for each in all_changed]
73
+ all_normalized_slices: list[JsonObject] = []
74
+ for each_slice in all_slices:
75
+ if not isinstance(each_slice, dict):
76
+ raise ValueError("slice must be an object")
77
+ all_paths = each_slice.get(SLICE_KEY_ALL_PATHS, [])
78
+ if not isinstance(all_paths, list):
79
+ raise ValueError("slice all_paths must be a list")
80
+ all_normalized_slices.append(
81
+ {
82
+ **each_slice,
83
+ SLICE_KEY_ALL_PATHS: [
84
+ normalize_repo_path(str(each_path)) for each_path in all_paths
85
+ ],
86
+ }
87
+ )
88
+ return {
89
+ **all_plan,
90
+ PLAN_KEY_ALL_CHANGED_PATHS: all_normalized_changed,
91
+ PLAN_KEY_ALL_SLICES: all_normalized_slices,
92
+ }
93
+
94
+
95
+ def verify_split_plan_coverage(all_plan: JsonObject) -> None:
96
+ """Normalize paths then run the schema assignment validator.
97
+
98
+ Args:
99
+ all_plan: Split-plan document.
100
+
101
+ Raises:
102
+ ValueError: When coverage or safety checks fail.
103
+ """
104
+ source_commit = all_plan.get(PLAN_KEY_SOURCE_COMMIT)
105
+ if not isinstance(source_commit, str) or not source_commit.strip():
106
+ raise ValueError("source_commit is required")
107
+ normalized = _normalize_plan_paths(all_plan)
108
+ validate_split_plan(normalized)
109
+
110
+
111
+ def main(all_argv: list[str]) -> int:
112
+ """CLI: verify a plan JSON file.
113
+
114
+ Args:
115
+ all_argv: Args without program name.
116
+
117
+ Returns:
118
+ Exit code 0 on success, 1 on failure.
119
+
120
+ Raises:
121
+ Does not raise; converts failures into a JSON error payload.
122
+ """
123
+ parser = argparse.ArgumentParser(description=__doc__)
124
+ parser.add_argument("--plan-json", type=Path, required=True)
125
+ arguments = parser.parse_args(all_argv)
126
+ try:
127
+ payload = json.loads(arguments.plan_json.read_text(encoding=UTF8_ENCODING))
128
+ if not isinstance(payload, dict):
129
+ raise ValueError("plan root must be an object")
130
+ verify_split_plan_coverage(payload)
131
+ except (OSError, ValueError, json.JSONDecodeError) as error:
132
+ print(json.dumps({VERIFY_PAYLOAD_KEY_OK: False, PAYLOAD_KEY_ERROR: str(error)}))
133
+ return EXIT_CODE_FAILURE
134
+ print(json.dumps({VERIFY_PAYLOAD_KEY_OK: True}))
135
+ return EXIT_CODE_SUCCESS
136
+
137
+
138
+ if __name__ == "__main__":
139
+ raise SystemExit(main(sys.argv[1:]))
@@ -28,8 +28,8 @@
28
28
  Lead every response with the direct answer or result. Open with the substance — the
29
29
  conclusion, the fix, the decision — before any supporting context or explanation. Expand
30
30
  depth in proportion to the complexity of the ask: a direct question earns a sentence; a
31
- multi-file investigation earns structured explanation. Depth scales with the ask; a fixed
32
- reply-length cap is wrong.
31
+ multi-file investigation or requested full audit earns structured explanation long enough
32
+ to cover the substance. Depth scales with the ask; a fixed reply-length cap is wrong.
33
33
 
34
34
  Reference specific code locations using the pattern `file_path:line_number` so the user can
35
35
  navigate directly to the source.
@@ -381,6 +381,15 @@
381
381
  work around tool limitations — are removed when the task is done.
382
382
  </cleanup>
383
383
 
384
+ <communication_contract>
385
+ Visible output is concise by default. The first progress update is one sentence.
386
+ Later updates mark only important discoveries or direction changes. The final
387
+ response begins with the outcome. When thinking is disabled, speak one brief
388
+ natural-language sentence before a tool call that needs context, state when no
389
+ fitting tool exists, and never emit internal-system XML in visible output.
390
+ Full contract: rules/opus5-communication-contract.md (opus5-communication-contract-v1).
391
+ </communication_contract>
392
+
384
393
  <help_and_feedback>
385
394
  When the user asks for help with this tool or wants to report a problem:
386
395
  - For usage help, direct them to: /help