claude-dev-env 2.8.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 (323) hide show
  1. package/CLAUDE.md +8 -2
  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 +62 -562
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +58 -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/CODE_RULES.md +4 -2
  44. package/docs/references/CLAUDE.md +3 -2
  45. package/docs/references/advisor-tool.md +44 -6
  46. package/docs/references/prose-style-enforcement.md +25 -0
  47. package/docs/references/team-advisor-skill.md +14 -8
  48. package/hooks/blocking/CLAUDE.md +6 -6
  49. package/hooks/blocking/_path_setup.py +9 -5
  50. package/hooks/blocking/code_rules_docstrings.py +124 -30
  51. package/hooks/blocking/code_rules_enforcer.py +161 -16
  52. package/hooks/blocking/code_rules_shared.py +40 -23
  53. package/hooks/blocking/config/CLAUDE.md +3 -5
  54. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  55. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  56. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  57. package/hooks/blocking/hedging_language_blocker.py +103 -20
  58. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  59. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  60. package/hooks/blocking/plain_language_blocker.py +139 -20
  61. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  62. package/hooks/blocking/state_description_blocker.py +7 -1
  63. package/hooks/blocking/tdd_enforcer.py +8 -0
  64. package/hooks/blocking/test__path_setup.py +28 -0
  65. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  66. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  67. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  69. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  70. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  71. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  72. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  73. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  74. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  75. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  76. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  77. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  78. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  79. package/hooks/blocking/test_state_description_blocker.py +45 -2
  80. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  81. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  82. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  83. package/hooks/hooks.json +15 -0
  84. package/hooks/hooks_constants/CLAUDE.md +14 -3
  85. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  86. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  87. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  88. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  89. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  90. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  91. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  92. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  93. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  94. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  95. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  96. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  97. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  98. package/hooks/hooks_constants/session_start_injector.py +163 -0
  99. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  100. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  101. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  102. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  103. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  104. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  105. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  106. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  107. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  108. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  109. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  110. package/hooks/observability/CLAUDE.md +2 -0
  111. package/hooks/observability/prose_matcher_advisory.py +237 -0
  112. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  113. package/hooks/session/CLAUDE.md +9 -1
  114. package/hooks/session/_path_setup.py +13 -0
  115. package/hooks/session/issue_tracker_session_starter.py +135 -0
  116. package/hooks/session/orchestrator_auto_starter.py +100 -0
  117. package/hooks/session/test__path_setup.py +28 -0
  118. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  119. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  120. package/hooks/session/test_working_style_prompt.py +47 -0
  121. package/hooks/session/untracked_repo_detector.py +1 -24
  122. package/hooks/session/working_style_prompt.py +36 -0
  123. package/hooks/validators/_path_setup.py +19 -0
  124. package/hooks/validators/run_all_validators.py +8 -13
  125. package/installable-surfaces.manifest.json +21 -0
  126. package/output-styles/CLAUDE.md +15 -0
  127. package/package.json +5 -2
  128. package/rules/CLAUDE.md +1 -0
  129. package/rules/code-standards.md +33 -7
  130. package/rules/durable-post-artifacts.md +2 -2
  131. package/rules/eli11-replies.md +7 -2
  132. package/rules/hedging-claims.md +4 -2
  133. package/rules/long-horizon-autonomy.md +3 -1
  134. package/rules/opus5-communication-contract.md +45 -0
  135. package/rules/plain-language.md +2 -2
  136. package/rules/research-mode.md +1 -1
  137. package/scripts/CLAUDE.md +13 -2
  138. package/scripts/Sync-RepoMain.ps1 +215 -0
  139. package/scripts/active_capability_references.py +218 -0
  140. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  141. package/scripts/claude_chain_runner.py +394 -6
  142. package/scripts/claude_chain_usage.py +1 -1
  143. package/scripts/codex_compat_materializer.py +105 -85
  144. package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
  145. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  146. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  147. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  148. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
  149. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  150. package/scripts/grok_patch_artifacts.py +123 -0
  151. package/scripts/grok_run_ledger.py +318 -0
  152. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  153. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  154. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  155. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  156. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  157. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  158. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  159. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  160. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  161. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  162. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  163. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  164. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  165. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  166. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  167. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  168. package/scripts/spawn_grok_batch.py +556 -9
  169. package/scripts/test_active_capability_references.py +108 -0
  170. package/scripts/test_claude_chain_runner.py +414 -82
  171. package/scripts/test_claude_chain_usage.py +12 -12
  172. package/scripts/test_grok_patch_artifacts.py +82 -0
  173. package/scripts/test_grok_run_ledger.py +116 -0
  174. package/scripts/test_resolve_worker_spawn.py +2 -2
  175. package/scripts/test_spawn_grok_batch.py +295 -0
  176. package/scripts/test_verify_installable_package.py +208 -0
  177. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  178. package/scripts/verify_installable_package.py +612 -0
  179. package/settings.json +10 -0
  180. package/skills/CLAUDE.md +6 -2
  181. package/skills/_shared/CLAUDE.md +37 -4
  182. package/skills/_shared/advisor/CLAUDE.md +9 -0
  183. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  184. package/skills/_shared/advisor/scripts/README.md +9 -0
  185. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  186. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  187. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  188. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  189. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  190. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  191. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  192. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  193. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
  195. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  196. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  199. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  200. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  201. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  202. package/skills/_shared/pr-loop/state-schema.md +5 -0
  203. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  204. package/skills/autoconverge/reference/convergence.md +2 -1
  205. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  206. package/skills/beat-sheet/SKILL.md +54 -0
  207. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  208. package/skills/bugteam/CONSTRAINTS.md +4 -4
  209. package/skills/bugteam/EXAMPLES.md +1 -1
  210. package/skills/bugteam/reference/README.md +1 -1
  211. package/skills/e-code-review/SKILL.md +29 -3
  212. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  213. package/skills/e-code-review/reference/medium.md +15 -4
  214. package/skills/e-code-review/reference/runner-selection.md +40 -0
  215. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  216. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  217. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  218. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  219. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  220. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  221. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  222. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  223. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  224. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  225. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  226. package/skills/e-code-review/scripts/grok_code_review.py +230 -0
  227. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  228. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  229. package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
  230. package/skills/grok-spawn/SKILL.md +5 -0
  231. package/skills/orchestrator/SKILL.md +5 -0
  232. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  233. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  234. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  235. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  236. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  237. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  238. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  239. package/skills/pr-converge/scripts/conftest.py +35 -0
  240. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  241. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  242. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  243. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  244. package/skills/rebase/SKILL.md +15 -3
  245. package/skills/reviewer-gates/SKILL.md +2 -2
  246. package/skills/show/SKILL.md +51 -0
  247. package/skills/show/references/accessibility.md +7 -0
  248. package/skills/show/references/art.md +3 -0
  249. package/skills/show/references/charts.md +3 -0
  250. package/skills/show/references/core-design.md +14 -0
  251. package/skills/show/references/erds.md +3 -0
  252. package/skills/show/references/flowcharts.md +3 -0
  253. package/skills/show/references/host-and-html.md +3 -0
  254. package/skills/show/references/illustrative-diagrams.md +10 -0
  255. package/skills/show/references/interaction.md +3 -0
  256. package/skills/show/references/mockups.md +3 -0
  257. package/skills/show/references/quality-gates.md +7 -0
  258. package/skills/show/references/structural-diagrams.md +3 -0
  259. package/skills/show/references/subject-inventory.md +21 -0
  260. package/skills/show/references/svg-contract.md +22 -0
  261. package/skills/show/routing.yaml +30 -0
  262. package/skills/show/samples/pr1262-v2.svg +222 -0
  263. package/skills/show/scripts/README.md +6 -0
  264. package/skills/show/scripts/validate-artifact.py +91 -0
  265. package/skills/show/scripts/validate-package.py +18 -0
  266. package/skills/show/templates/html-widget.html +4 -0
  267. package/skills/show/templates/svg-base.svg +19 -0
  268. package/skills/show/tests/fixtures/css-var.svg +6 -0
  269. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  270. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  271. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  272. package/skills/show/tests/fixtures/invalid.svg +1 -0
  273. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  274. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  275. package/skills/show/tests/fixtures/valid.html +1 -0
  276. package/skills/show/tests/test_validate-artifact.py +74 -0
  277. package/skills/show/tests/test_validators.py +59 -0
  278. package/skills/show/workflows/create-visual.md +13 -0
  279. package/skills/show/workflows/review-visual.md +20 -0
  280. package/skills/split-pr/SKILL.md +85 -0
  281. package/skills/split-pr/reference/path-layers.md +16 -0
  282. package/skills/split-pr/reference/proposal-format.md +15 -0
  283. package/skills/split-pr/reference/split-further-loop.md +10 -0
  284. package/skills/split-pr/reference/splitting-principles.md +26 -0
  285. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  286. package/skills/split-pr/scripts/categorize_files.py +106 -0
  287. package/skills/split-pr/scripts/config/__init__.py +1 -0
  288. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  289. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  290. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  291. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  292. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  293. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  294. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  295. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  296. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  297. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  298. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  299. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  300. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  301. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  302. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  303. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  304. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  305. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  306. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  307. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  308. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  309. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  310. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  311. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  312. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  313. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  314. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  315. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  316. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  317. package/skills/split-pr/scripts/verify_plan.py +139 -0
  318. package/skills/task-build/reference/tool-routing.md +3 -0
  319. package/skills/team-advisor/SKILL.md +23 -44
  320. package/system-prompts/software-engineer.xml +17 -5
  321. package/commands/initialize.md +0 -90
  322. package/commands/stubcheck.md +0 -88
  323. package/skills/test_markdown_link_integrity.py +0 -107
@@ -0,0 +1,184 @@
1
+ """Materialize verified split slices as local Git commits with restore safety.
2
+
3
+ ::
4
+
5
+ outcome = materialize_slice_commit(
6
+ repository_path=repo,
7
+ all_slice_paths=["a.py"],
8
+ commit_message="chore: config slice",
9
+ expected_base_sha=head_before,
10
+ run=run_process,
11
+ )
12
+ # on failure, repository returns to expected_base_sha with clean tree
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+
19
+ from config.git_operations_constants import (
20
+ ALL_GIT_CLEAN_FORCE_DIR,
21
+ ALL_GIT_RESTORE_STAGED_WORKTREE,
22
+ ALL_GIT_REV_PARSE_HEAD,
23
+ ALL_GIT_STATUS_PORCELAIN,
24
+ ERROR_ADD_FAILED,
25
+ ERROR_BASE_MISMATCH_TEMPLATE,
26
+ ERROR_CHECKOUT_FAILED,
27
+ ERROR_CLEAN_FAILED,
28
+ ERROR_COMMIT_FAILED,
29
+ ERROR_DIRTY_TREE,
30
+ ERROR_EMPTY_SLICE_PATHS,
31
+ ERROR_RESTORE_FAILED,
32
+ ERROR_REV_PARSE_FAILED,
33
+ ERROR_STATUS_FAILED,
34
+ GIT_ADD,
35
+ GIT_CHECKOUT,
36
+ GIT_COMMAND,
37
+ GIT_COMMIT,
38
+ GIT_COMMIT_MESSAGE_FLAG,
39
+ GIT_FORCE_FLAG,
40
+ GIT_PATHSPEC_SEPARATOR,
41
+ RECORD_KEY_ALL_PATHS,
42
+ RECORD_KEY_BASE_SHA,
43
+ RECORD_KEY_COMMIT_SHA,
44
+ RECORD_KEY_EXIT_CODE,
45
+ )
46
+ from config.plan_constants import EXIT_CODE_SUCCESS
47
+ from split_pr_process_runner import CapturedProcessOutcome, ProcessRunner, run_process
48
+ from split_pr_script_types import JsonObject
49
+
50
+
51
+ def _git(
52
+ run: ProcessRunner,
53
+ repository_path: Path,
54
+ all_git_args: list[str],
55
+ ) -> CapturedProcessOutcome:
56
+ return run([GIT_COMMAND, *all_git_args], str(repository_path))
57
+
58
+
59
+ def read_head_sha(repository_path: Path, run: ProcessRunner = run_process) -> str:
60
+ """Return the current HEAD commit SHA.
61
+
62
+ Args:
63
+ repository_path: Git repository root.
64
+ run: Process runner (injectable for tests).
65
+
66
+ Returns:
67
+ Stripped HEAD sha text.
68
+
69
+ Raises:
70
+ RuntimeError: When rev-parse fails.
71
+ """
72
+ outcome = _git(run, repository_path, list(ALL_GIT_REV_PARSE_HEAD))
73
+ if not outcome.is_success:
74
+ raise RuntimeError(outcome.stderr_text or ERROR_REV_PARSE_FAILED)
75
+ return outcome.stdout_text.strip()
76
+
77
+
78
+ def assert_clean_worktree(
79
+ repository_path: Path,
80
+ run: ProcessRunner = run_process,
81
+ ) -> None:
82
+ """Refuse materialization when the worktree is dirty.
83
+
84
+ Args:
85
+ repository_path: Git repository root.
86
+ run: Process runner.
87
+
88
+ Raises:
89
+ RuntimeError: When porcelain status is non-empty or the command fails.
90
+ """
91
+ outcome = _git(run, repository_path, list(ALL_GIT_STATUS_PORCELAIN))
92
+ if not outcome.is_success:
93
+ raise RuntimeError(outcome.stderr_text or ERROR_STATUS_FAILED)
94
+ if outcome.stdout_text.strip():
95
+ raise RuntimeError(ERROR_DIRTY_TREE)
96
+
97
+
98
+ def restore_repository_state(
99
+ repository_path: Path,
100
+ target_sha: str,
101
+ run: ProcessRunner = run_process,
102
+ ) -> None:
103
+ """Checkout target_sha and discard uncommitted changes.
104
+
105
+ Args:
106
+ repository_path: Git repository root.
107
+ target_sha: Commit to restore.
108
+ run: Process runner.
109
+
110
+ Raises:
111
+ RuntimeError: When checkout or clean fails.
112
+ """
113
+ checkout = _git(
114
+ run, repository_path, [GIT_CHECKOUT, GIT_FORCE_FLAG, target_sha]
115
+ )
116
+ if not checkout.is_success:
117
+ raise RuntimeError(checkout.stderr_text or ERROR_CHECKOUT_FAILED)
118
+ restore = _git(run, repository_path, list(ALL_GIT_RESTORE_STAGED_WORKTREE))
119
+ if not restore.is_success:
120
+ raise RuntimeError(restore.stderr_text or ERROR_RESTORE_FAILED)
121
+ clean = _git(run, repository_path, list(ALL_GIT_CLEAN_FORCE_DIR))
122
+ if not clean.is_success:
123
+ raise RuntimeError(clean.stderr_text or ERROR_CLEAN_FAILED)
124
+
125
+
126
+ def materialize_slice_commit(
127
+ repository_path: Path,
128
+ all_slice_paths: list[str],
129
+ commit_message: str,
130
+ expected_base_sha: str,
131
+ run: ProcessRunner = run_process,
132
+ ) -> JsonObject:
133
+ """Stage only slice paths and create one commit from a verified base.
134
+
135
+ Args:
136
+ repository_path: Git repository root.
137
+ all_slice_paths: Paths relative to the repository for this slice only.
138
+ commit_message: Conventional commit message (already normalized).
139
+ expected_base_sha: HEAD must equal this before staging.
140
+ run: Process runner.
141
+
142
+ Returns:
143
+ Map with keys commit_sha, base_sha, all_paths.
144
+
145
+ Raises:
146
+ RuntimeError: On dirty tree, base mismatch, add/commit failure, or
147
+ when restore after failure also fails.
148
+ """
149
+ assert_clean_worktree(repository_path, run)
150
+ head_sha = read_head_sha(repository_path, run)
151
+ if head_sha != expected_base_sha:
152
+ raise RuntimeError(
153
+ ERROR_BASE_MISMATCH_TEMPLATE.format(
154
+ head_sha=head_sha,
155
+ expected_base_sha=expected_base_sha,
156
+ )
157
+ )
158
+ if not all_slice_paths:
159
+ raise RuntimeError(ERROR_EMPTY_SLICE_PATHS)
160
+ try:
161
+ add_outcome = _git(
162
+ run,
163
+ repository_path,
164
+ [GIT_ADD, GIT_PATHSPEC_SEPARATOR, *all_slice_paths],
165
+ )
166
+ if not add_outcome.is_success:
167
+ raise RuntimeError(add_outcome.stderr_text or ERROR_ADD_FAILED)
168
+ commit_outcome = _git(
169
+ run,
170
+ repository_path,
171
+ [GIT_COMMIT, GIT_COMMIT_MESSAGE_FLAG, commit_message],
172
+ )
173
+ if not commit_outcome.is_success:
174
+ raise RuntimeError(commit_outcome.stderr_text or ERROR_COMMIT_FAILED)
175
+ new_sha = read_head_sha(repository_path, run)
176
+ return {
177
+ RECORD_KEY_COMMIT_SHA: new_sha,
178
+ RECORD_KEY_BASE_SHA: expected_base_sha,
179
+ RECORD_KEY_ALL_PATHS: list(all_slice_paths),
180
+ RECORD_KEY_EXIT_CODE: EXIT_CODE_SUCCESS,
181
+ }
182
+ except RuntimeError:
183
+ restore_repository_state(repository_path, expected_base_sha, run)
184
+ raise
@@ -0,0 +1,58 @@
1
+ """Deterministic layer order for split-plan slices.
2
+
3
+ ::
4
+
5
+ sort_slices_by_layer_order([{"layer": "docs"}, {"layer": "config"}])
6
+ # config first, then docs
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from config.plan_constants import (
12
+ ALL_LAYER_RANK_BY_NAME,
13
+ DEFAULT_SLICE_LAYER,
14
+ SLICE_KEY_LAYER,
15
+ UNKNOWN_LAYER_RANK,
16
+ )
17
+
18
+ JsonObject = dict[str, object]
19
+
20
+
21
+ def layer_rank(layer_name: str) -> int:
22
+ """Return the stable rank for a layer name (unknown layers sort last).
23
+
24
+ Args:
25
+ layer_name: Layer token such as ``config`` or ``tests``.
26
+
27
+ Returns:
28
+ Integer rank; lower values sort earlier.
29
+ """
30
+ return ALL_LAYER_RANK_BY_NAME.get(layer_name, UNKNOWN_LAYER_RANK)
31
+
32
+
33
+ def sort_slices_by_layer_order(all_slices: list[JsonObject]) -> list[JsonObject]:
34
+ """Return a new list of slices sorted by layer rank then original index.
35
+
36
+ Args:
37
+ all_slices: Slice maps that may carry a ``layer`` key.
38
+
39
+ Returns:
40
+ Fresh list sorted stably by layer rank.
41
+ """
42
+ all_indexed_slices = list(enumerate(all_slices))
43
+
44
+ def _rank_for_sort(
45
+ all_indexed_slice: tuple[int, JsonObject],
46
+ ) -> tuple[int, int]:
47
+ each_index, each_slice = all_indexed_slice
48
+ layer_field = each_slice.get(SLICE_KEY_LAYER, DEFAULT_SLICE_LAYER)
49
+ if layer_field is None:
50
+ layer_name = DEFAULT_SLICE_LAYER
51
+ else:
52
+ layer_name = str(layer_field)
53
+ return (layer_rank(layer_name), each_index)
54
+
55
+ return [
56
+ each_slice
57
+ for _each_index, each_slice in sorted(all_indexed_slices, key=_rank_for_sort)
58
+ ]
@@ -0,0 +1,119 @@
1
+ """Paginated PR changed-file intake at an exact source commit identity.
2
+
3
+ Uses ``gh api --paginate --slurp`` so every page of the pulls files list is
4
+ collected before filtering (cross-page sorts stay correct).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import subprocess
11
+
12
+ from config.plan_constants import (
13
+ EXIT_CODE_SUCCESS,
14
+ FILE_KEY_ADDITIONS,
15
+ FILE_KEY_DELETIONS,
16
+ FILE_KEY_PATH,
17
+ FILE_KEY_SHA,
18
+ GH_API,
19
+ GH_COMMAND,
20
+ GH_PAGINATE_FLAG,
21
+ GH_PULLS_FILES_PATH_TEMPLATE,
22
+ GH_REST_FILE_FILENAME,
23
+ GH_SLURP_FLAG,
24
+ UTF8_ENCODING,
25
+ )
26
+
27
+ JsonObject = dict[str, object]
28
+
29
+
30
+ def fetch_all_pr_changed_files(
31
+ owner: str,
32
+ repo: str,
33
+ pr_number: int,
34
+ ) -> list[JsonObject]:
35
+ """Fetch every changed file across all pages for a pull request.
36
+
37
+ Args:
38
+ owner: Repository owner.
39
+ repo: Repository name.
40
+ pr_number: Pull request number.
41
+
42
+ Returns:
43
+ List of path/additions/deletions/sha maps.
44
+
45
+ Raises:
46
+ RuntimeError: When the gh API call fails.
47
+ json.JSONDecodeError: When the response is not JSON.
48
+ """
49
+ api_path = GH_PULLS_FILES_PATH_TEMPLATE.format(
50
+ owner=owner,
51
+ repo=repo,
52
+ pr_number=pr_number,
53
+ )
54
+ all_command = [
55
+ GH_COMMAND,
56
+ GH_API,
57
+ api_path,
58
+ GH_PAGINATE_FLAG,
59
+ GH_SLURP_FLAG,
60
+ ]
61
+ completed = subprocess.run(
62
+ all_command,
63
+ capture_output=True,
64
+ text=True,
65
+ check=False,
66
+ encoding=UTF8_ENCODING,
67
+ )
68
+ if completed.returncode != EXIT_CODE_SUCCESS:
69
+ raise RuntimeError(completed.stderr.strip() or "gh api pulls files failed")
70
+ return parse_paginated_files_payload(completed.stdout)
71
+
72
+
73
+ def parse_paginated_files_payload(raw_json: str) -> list[JsonObject]:
74
+ """Flatten a --slurp JSON array-of-pages into file records.
75
+
76
+ Args:
77
+ raw_json: stdout from ``gh api --paginate --slurp``.
78
+
79
+ Returns:
80
+ Deduplicated file records in first-seen order.
81
+
82
+ Raises:
83
+ ValueError: When the JSON root is not an array of pages or files.
84
+ json.JSONDecodeError: When raw_json is not valid JSON.
85
+ """
86
+ loaded = json.loads(raw_json)
87
+ all_pages: list[object]
88
+ if isinstance(loaded, list) and loaded and isinstance(loaded[0], list):
89
+ all_pages = loaded
90
+ elif isinstance(loaded, list):
91
+ all_pages = [loaded]
92
+ else:
93
+ raise ValueError("paginated payload must be a JSON array")
94
+ all_file_records: list[JsonObject] = []
95
+ all_seen_paths: set[str] = set()
96
+ for each_page in all_pages:
97
+ if not isinstance(each_page, list):
98
+ continue
99
+ for each_file in each_page:
100
+ if not isinstance(each_file, dict):
101
+ continue
102
+ path = each_file.get(GH_REST_FILE_FILENAME)
103
+ if path is None:
104
+ path = each_file.get(FILE_KEY_PATH)
105
+ if not path:
106
+ continue
107
+ path_text = str(path)
108
+ if path_text in all_seen_paths:
109
+ continue
110
+ all_seen_paths.add(path_text)
111
+ all_file_records.append(
112
+ {
113
+ FILE_KEY_PATH: path_text,
114
+ FILE_KEY_ADDITIONS: int(each_file.get(FILE_KEY_ADDITIONS, 0) or 0),
115
+ FILE_KEY_DELETIONS: int(each_file.get(FILE_KEY_DELETIONS, 0) or 0),
116
+ FILE_KEY_SHA: str(each_file.get(FILE_KEY_SHA, "") or ""),
117
+ }
118
+ )
119
+ return all_file_records
@@ -0,0 +1,52 @@
1
+ """Run subprocess commands with captured text output for Git materialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+
9
+ from config.plan_constants import EXIT_CODE_SUCCESS, UTF8_ENCODING
10
+
11
+ ProcessRunner = Callable[[list[str], str], "CapturedProcessOutcome"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CapturedProcessOutcome:
16
+ """Captured outcome of one subprocess invocation."""
17
+
18
+ exit_code: int
19
+ stdout_text: str
20
+ stderr_text: str
21
+
22
+ @property
23
+ def is_success(self) -> bool:
24
+ return self.exit_code == EXIT_CODE_SUCCESS
25
+
26
+
27
+ def run_process(
28
+ all_command: list[str],
29
+ working_directory: str,
30
+ ) -> CapturedProcessOutcome:
31
+ """Run a command and return exit code plus captured stdout/stderr text.
32
+
33
+ Args:
34
+ all_command: argv list (no shell).
35
+ working_directory: Directory the child process runs in.
36
+
37
+ Returns:
38
+ CapturedProcessOutcome with exit_code and text streams.
39
+ """
40
+ completed = subprocess.run(
41
+ all_command,
42
+ cwd=working_directory,
43
+ capture_output=True,
44
+ text=True,
45
+ check=False,
46
+ encoding=UTF8_ENCODING,
47
+ )
48
+ return CapturedProcessOutcome(
49
+ exit_code=completed.returncode,
50
+ stdout_text=completed.stdout or "",
51
+ stderr_text=completed.stderr or "",
52
+ )
@@ -0,0 +1,126 @@
1
+ """Canonical split-plan records and validation.
2
+
3
+ ::
4
+
5
+ plan = build_split_plan(
6
+ source_commit="abc123",
7
+ all_changed_paths=["a.py", "b.py"],
8
+ all_slices=[{"id": "01-config", "title": "feat: cfg", "layer": "config", "all_paths": ["a.py"]}],
9
+ )
10
+ validate_split_plan(plan) # raises ValueError when a path is unassigned
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from config.plan_constants import (
16
+ DEFAULT_SLICE_LAYER,
17
+ PLAN_KEY_ALL_CHANGED_PATHS,
18
+ PLAN_KEY_ALL_SLICES,
19
+ PLAN_KEY_SCHEMA_VERSION,
20
+ PLAN_KEY_SOURCE_COMMIT,
21
+ PLAN_SCHEMA_VERSION,
22
+ SLICE_KEY_ALL_PATHS,
23
+ SLICE_KEY_ID,
24
+ SLICE_KEY_LAYER,
25
+ SLICE_KEY_TITLE,
26
+ TITLE_PREFIX_SEPARATOR,
27
+ )
28
+ from split_pr_layer_order import sort_slices_by_layer_order
29
+ from split_pr_title import normalize_split_title
30
+
31
+ JsonObject = dict[str, object]
32
+
33
+
34
+ def build_split_plan(
35
+ source_commit: str,
36
+ all_changed_paths: list[str],
37
+ all_slices: list[JsonObject],
38
+ ) -> JsonObject:
39
+ """Build a schema-versioned split plan with normalized titles and layer order.
40
+
41
+ Args:
42
+ source_commit: Exact commit SHA the file list was taken from.
43
+ all_changed_paths: Full path set for the source commit.
44
+ all_slices: Slice maps with id, title, layer, and path lists.
45
+
46
+ Returns:
47
+ Plan document ready for validate_split_plan.
48
+ """
49
+ all_normalized_slices: list[JsonObject] = []
50
+ for each_slice in all_slices:
51
+ title = normalize_split_title(str(each_slice.get(SLICE_KEY_TITLE, "")))
52
+ all_paths = each_slice.get(SLICE_KEY_ALL_PATHS, [])
53
+ if not isinstance(all_paths, list):
54
+ all_paths = []
55
+ all_normalized_slices.append(
56
+ {
57
+ SLICE_KEY_ID: str(each_slice.get(SLICE_KEY_ID, "")),
58
+ SLICE_KEY_TITLE: title,
59
+ SLICE_KEY_LAYER: str(
60
+ each_slice.get(SLICE_KEY_LAYER, DEFAULT_SLICE_LAYER)
61
+ ),
62
+ SLICE_KEY_ALL_PATHS: [str(each_path) for each_path in all_paths],
63
+ }
64
+ )
65
+ all_ordered = sort_slices_by_layer_order(all_normalized_slices)
66
+ return {
67
+ PLAN_KEY_SCHEMA_VERSION: PLAN_SCHEMA_VERSION,
68
+ PLAN_KEY_SOURCE_COMMIT: source_commit,
69
+ PLAN_KEY_ALL_CHANGED_PATHS: list(all_changed_paths),
70
+ PLAN_KEY_ALL_SLICES: all_ordered,
71
+ }
72
+
73
+
74
+ def validate_split_plan(all_plan: JsonObject) -> None:
75
+ """Require every changed path is assigned to exactly one slice.
76
+
77
+ Args:
78
+ all_plan: Plan document from build_split_plan.
79
+
80
+ Raises:
81
+ ValueError: When schema, assignment, or title contracts fail.
82
+ """
83
+ if all_plan.get(PLAN_KEY_SCHEMA_VERSION) != PLAN_SCHEMA_VERSION:
84
+ raise ValueError(
85
+ f"schema_version must be {PLAN_SCHEMA_VERSION}, "
86
+ f"got {all_plan.get(PLAN_KEY_SCHEMA_VERSION)!r}"
87
+ )
88
+ source_commit = all_plan.get(PLAN_KEY_SOURCE_COMMIT)
89
+ if not isinstance(source_commit, str) or not source_commit.strip():
90
+ raise ValueError("source_commit must be a non-empty string")
91
+ all_changed = all_plan.get(PLAN_KEY_ALL_CHANGED_PATHS)
92
+ if not isinstance(all_changed, list):
93
+ raise ValueError("all_changed_paths must be a list")
94
+ all_slices = all_plan.get(PLAN_KEY_ALL_SLICES)
95
+ if not isinstance(all_slices, list) or not all_slices:
96
+ raise ValueError("all_slices must be a non-empty list")
97
+ all_assigned: list[str] = []
98
+ for each_slice in all_slices:
99
+ if not isinstance(each_slice, dict):
100
+ raise ValueError("each slice must be an object")
101
+ slice_id = each_slice.get(SLICE_KEY_ID)
102
+ if not isinstance(slice_id, str) or not slice_id.strip():
103
+ raise ValueError(f"slice id must be a non-empty string: {slice_id!r}")
104
+ title = each_slice.get(SLICE_KEY_TITLE)
105
+ if not isinstance(title, str) or TITLE_PREFIX_SEPARATOR not in title:
106
+ raise ValueError(f"slice title missing conventional prefix: {title!r}")
107
+ normalized = normalize_split_title(title)
108
+ if normalized != title:
109
+ raise ValueError(
110
+ f"slice title is not normalized: {title!r} != {normalized!r}"
111
+ )
112
+ all_paths = each_slice.get(SLICE_KEY_ALL_PATHS)
113
+ if not isinstance(all_paths, list):
114
+ raise ValueError("slice all_paths must be a list")
115
+ for each_path in all_paths:
116
+ all_assigned.append(str(each_path))
117
+ all_changed_set = {str(each_path) for each_path in all_changed}
118
+ all_assigned_set = set(all_assigned)
119
+ if all_assigned_set != all_changed_set:
120
+ missing = sorted(all_changed_set - all_assigned_set)
121
+ extra = sorted(all_assigned_set - all_changed_set)
122
+ raise ValueError(
123
+ f"path assignment mismatch missing={missing!r} extra={extra!r}"
124
+ )
125
+ if len(all_assigned) != len(all_assigned_set):
126
+ raise ValueError("a path is assigned to more than one slice")
@@ -0,0 +1,41 @@
1
+ """Normalize split-PR titles to exactly one conventional commit prefix.
2
+
3
+ ::
4
+
5
+ normalize_split_title("feat: foo") -> "feat: foo"
6
+ normalize_split_title("feat: feat: foo") -> "feat: foo"
7
+ normalize_split_title("bare title") -> "chore: bare title"
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from config.plan_constants import (
15
+ CONVENTIONAL_PREFIX_PATTERN,
16
+ DEFAULT_EMPTY_TITLE_REMAINDER,
17
+ DEFAULT_TITLE_PREFIX,
18
+ TITLE_PREFIX_SEPARATOR,
19
+ )
20
+
21
+ def normalize_split_title(raw_title: str) -> str:
22
+ """Return a title with exactly one conventional-commit prefix.
23
+
24
+ Args:
25
+ raw_title: Candidate title that may lack a prefix or stack several.
26
+
27
+ Returns:
28
+ Title of the form ``<prefix>: <remainder>`` with one prefix only.
29
+ """
30
+ prefix_pattern = re.compile(CONVENTIONAL_PREFIX_PATTERN, re.IGNORECASE)
31
+ remainder = raw_title.strip()
32
+ chosen_prefix = DEFAULT_TITLE_PREFIX
33
+ while True:
34
+ match = prefix_pattern.match(remainder)
35
+ if match is None:
36
+ break
37
+ chosen_prefix = match.group("prefix").lower()
38
+ remainder = remainder[match.end() :].lstrip()
39
+ if not remainder:
40
+ remainder = DEFAULT_EMPTY_TITLE_REMAINDER
41
+ return f"{chosen_prefix}{TITLE_PREFIX_SEPARATOR}{remainder}"