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,9 @@
1
+ # Advisor
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/advisor/CLAUDE.md
6
+
7
+ @~/.claude/_shared/advisor/advisor-protocol.md
8
+
9
+ Scripts: `~/.claude/_shared/advisor/scripts/` (`model_tier_run_validator.py`, `tier_model_ids.py`, constants under `scripts/config/`).
@@ -0,0 +1,5 @@
1
+ # Advisor protocol
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/advisor/advisor-protocol.md
@@ -0,0 +1,9 @@
1
+ # Advisor scripts
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/advisor/scripts/model_tier_run_validator.py
6
+
7
+ @~/.claude/_shared/advisor/scripts/tier_model_ids.py
8
+
9
+ Full tree: `~/.claude/_shared/advisor/scripts/`.
@@ -0,0 +1,156 @@
1
+ # End-of-run gotcha recommendations
2
+
3
+ **Shared close-out** for every skill.
4
+
5
+ After the deliverable, recommend pasteable gotchas from issues **this run** hit.
6
+
7
+ ## When
8
+
9
+ - **With issues:** end of work, after the deliverable.
10
+ - **Clean run:** end with the deliverable only.
11
+
12
+ ## What counts
13
+
14
+ Keep only items that **bit this run**:
15
+
16
+ - Wrong **path, file, or config**
17
+ - **Command / CLI** failed or needed different flags
18
+ - **Auth, permission, or environment** blocked a step
19
+ - **Data shape** mismatch (empty set, wrong schema, bad field)
20
+ - **Tool, UI, or selector** missed the target
21
+ - **Timing** needed a retry or longer wait
22
+ - **Dependency or install** gap or wrong version
23
+ - **Output format** wrong for the next step
24
+ - A **workaround** required to finish
25
+
26
+ ## What to produce
27
+
28
+ **Order (hard):**
29
+
30
+ 1. Show findings in chat; categorize as P0-P3.
31
+ 2. Then `AskUserQuestion`.
32
+ 3. On confirm only, file via `issue-tracker`.
33
+
34
+ ### Cold-reader frame
35
+
36
+ Assume zero context.
37
+
38
+ **Exactly two setup sentences**, each on its **own rendered line**:
39
+
40
+ 1. What we were doing.
41
+ 2. Why these notes matter.
42
+
43
+ ### 1. Chat findings (always first)
44
+
45
+ #### Sentence law (HARD)
46
+
47
+ | Rule | Required |
48
+ |---|---|
49
+ | **One sentence per rendered line** | Exactly one terminal `.` `?` or `!` on that line |
50
+ | **No collapsed pairs** | A single markdown newline is **not** a line break — it joins sentences |
51
+ | **Force the break** | Blank line between every prose sentence, **or** one list item per sentence |
52
+ | **No clause glue** | No em-dash or semicolon joining two clauses |
53
+ | **One idea** | If you can split it, split it |
54
+ | **Heavy detail** | Prefer an **ASCII infographic** over a stack of sentences |
55
+
56
+ **Self-check before send:** paste the chat into a viewer that collapses soft breaks.
57
+
58
+ If two periods appear on one visual line, **split or diagram**.
59
+
60
+ #### Per gotcha (HARD shape)
61
+
62
+ | Part | Form |
63
+ |---|---|
64
+ | **Title** | Heading fragment only |
65
+ | **Broke** | One sentence alone on its rendered line |
66
+ | **Fix** | One sentence alone on its rendered line |
67
+ | **ASCII** | Required for path, flag, or before/after |
68
+
69
+ **Cap: two prose sentences per gotcha.**
70
+
71
+ ```markdown
72
+ ## Session close-out
73
+
74
+ We built phone-theme prompts from a project style catalog.
75
+
76
+ Keep these traps for the next run.
77
+
78
+ ## Gotcha recommendations
79
+
80
+ ### Wrong catalog folder
81
+
82
+ The style picker failed outside the project.
83
+
84
+ Run from the project root with data/midjourney_sref_catalog.json.
85
+
86
+ ```text
87
+ wrong folder --> fail
88
+ project root --> data/midjourney_sref_catalog.json
89
+ ```
90
+ ```
91
+
92
+ **Wrong (soft break joins sentences into one line):**
93
+
94
+ ```markdown
95
+ The style picker failed outside the project.
96
+ Run from the project root with data/midjourney_sref_catalog.json.
97
+ ```
98
+
99
+ Renders as one line with **two** sentences.
100
+
101
+ **Right (blank line forces two lines):**
102
+
103
+ ```markdown
104
+ The style picker failed outside the project.
105
+
106
+ Run from the project root with data/midjourney_sref_catalog.json.
107
+ ```
108
+
109
+ **Right (list items):**
110
+
111
+ ```markdown
112
+ - The style picker failed outside the project.
113
+ - Run from the project root with data/midjourney_sref_catalog.json.
114
+ ```
115
+
116
+ ### 2. Issue offer (`AskUserQuestion`)
117
+
118
+ Only after the chat block.
119
+
120
+ | Field | Limit |
121
+ |---|---|
122
+ | **Question** | One short sentence |
123
+ | **Header** | `File issues` |
124
+ | **label** | Plain name |
125
+ | **description** | One short sentence |
126
+
127
+ Detail and ASCII stay in chat.
128
+
129
+ **On confirm:** file via `issue-tracker`.
130
+
131
+ Cold-reader issue body.
132
+
133
+ Clean run: skip.
134
+
135
+ ## Built-in skill gotcha
136
+
137
+ **Two sentences on one rendered line.**
138
+
139
+ Markdown soft-breaks join prose.
140
+
141
+ Review the close-out before send.
142
+
143
+ Split with a blank line or a list item.
144
+
145
+ Or move the detail into an ASCII infographic.
146
+
147
+ ## Rules
148
+
149
+ - Cold-reader first.
150
+ - Chat first, then ask.
151
+ - One sentence per **rendered** line.
152
+ - Blank line or list item between prose sentences.
153
+ - Two prose sentences per gotcha max.
154
+ - ASCII for path, flag, or before/after.
155
+ - File only on confirm.
156
+ - Lived issues only.
@@ -12,7 +12,24 @@ those skills invoke during each loop tick.
12
12
  | `prompts/` | XML agent prompt templates. |
13
13
  | `scripts/` | Python scripts for loop state management, prompt building, outcome recording, path resolution, pacer selection, and preflight checks. |
14
14
 
15
- ## Key files
15
+ ## Canonical-path stubs (`~/.claude/_shared/pr-loop/`)
16
+
17
+ Open a stub, then load the `@` target:
18
+
19
+ | Stub here | Load |
20
+ |---|---|
21
+ | `audit-contract.md` | `@~/.claude/_shared/pr-loop/audit-contract.md` |
22
+ | `audit-reply-template.md` | `@~/.claude/_shared/pr-loop/audit-reply-template.md` |
23
+ | `code-rules-gate.md` | `@~/.claude/_shared/pr-loop/code-rules-gate.md` |
24
+ | `fix-protocol.md` | `@~/.claude/_shared/pr-loop/fix-protocol.md` |
25
+ | `gh-payloads.md` | `@~/.claude/_shared/pr-loop/gh-payloads.md` |
26
+ | `post-audit-thread-contract.md` | `@~/.claude/_shared/pr-loop/post-audit-thread-contract.md` |
27
+ | `precatch-rubric.md` | `@~/.claude/_shared/pr-loop/precatch-rubric.md` |
28
+ | `state-schema.md` | `@~/.claude/_shared/pr-loop/state-schema.md` |
29
+ | `worker-spawn.md` | `@~/.claude/_shared/pr-loop/worker-spawn.md` |
30
+ | `scripts/RUNTIME_SCRIPTS.md` | `@~/.claude/_shared/pr-loop/scripts/` |
31
+
32
+ ## Key files (live in this tree)
16
33
 
17
34
  | File | Role |
18
35
  |---|---|
@@ -0,0 +1,5 @@
1
+ # Audit contract
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/audit-contract.md
@@ -0,0 +1,5 @@
1
+ # Audit reply template
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/audit-reply-template.md
@@ -0,0 +1,5 @@
1
+ # Code rules gate
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/code-rules-gate.md
@@ -0,0 +1,5 @@
1
+ # Fix protocol
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/fix-protocol.md
@@ -0,0 +1,5 @@
1
+ # Gh payloads
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/gh-payloads.md
@@ -0,0 +1,5 @@
1
+ # Post-audit thread contract
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/post-audit-thread-contract.md
@@ -0,0 +1,5 @@
1
+ # Precatch rubric
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/precatch-rubric.md
@@ -2,10 +2,18 @@
2
2
 
3
3
  Python scripts that run the PR audit-fix loop at runtime. Both `bugteam` and `pr-converge` invoke these scripts during each loop tick.
4
4
 
5
- ## Key files
5
+ ## Two script homes
6
+
7
+ | Home | Path |
8
+ |---|---|
9
+ | **Skill-local** (this folder) | Converge helpers: `build_*_prompt.py`, `init_loop_state.py`, `portable_converge_driver.py`, … |
10
+ | **Runtime** (top-level shared) | Gate / preflight / review helpers — see `RUNTIME_SCRIPTS.md` → `@~/.claude/_shared/pr-loop/scripts/` |
11
+
12
+ ## Key files (skill-local)
6
13
 
7
14
  | File | Role |
8
15
  |---|---|
16
+ | `audit_category_schema.py` | Loads the A-Q schema JSON, exports category entries, validates rubric/prompt projections, renders deterministic skeletons. |
9
17
  | `build_audit_prompt.py` | Assembles the audit agent prompt from loop state and category constants. |
10
18
  | `build_fix_prompt.py` | Assembles the fix agent prompt from loop state and findings XML. |
11
19
  | `init_loop_state.py` | Initializes the per-PR `loop-state.json` file in the workspace directory. |
@@ -0,0 +1,29 @@
1
+ # PR-loop runtime scripts
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/scripts/code_rules_gate.py
6
+
7
+ @~/.claude/_shared/pr-loop/scripts/preflight.py
8
+
9
+ @~/.claude/_shared/pr-loop/scripts/post_audit_thread.py
10
+
11
+ @~/.claude/_shared/pr-loop/scripts/post_audit_review.py
12
+
13
+ @~/.claude/_shared/pr-loop/scripts/gh_util.py
14
+
15
+ @~/.claude/_shared/pr-loop/scripts/reviews_disabled.py
16
+
17
+ @~/.claude/_shared/pr-loop/scripts/reviewer_availability.py
18
+
19
+ @~/.claude/_shared/pr-loop/scripts/copilot_quota.py
20
+
21
+ @~/.claude/_shared/pr-loop/scripts/grant_project_claude_permissions.py
22
+
23
+ @~/.claude/_shared/pr-loop/scripts/revoke_project_claude_permissions.py
24
+
25
+ @~/.claude/_shared/pr-loop/scripts/verify_review.py
26
+
27
+ @~/.claude/_shared/pr-loop/scripts/README.md
28
+
29
+ Skill-local converge helpers (`build_*_prompt.py`, `init_loop_state.py`, and siblings) live in this `scripts/` folder. Runtime gate, preflight, and review helpers live under `~/.claude/_shared/pr-loop/scripts/`.
@@ -0,0 +1,355 @@
1
+ """Load and validate the A-Q audit-category schema.
2
+
3
+ One machine-readable source under audit-rubrics/audit-categories.json holds
4
+ each category id, title, slug, and sub-bucket id/axis pairs. Worked examples
5
+ stay in the rubric markdown files only.
6
+
7
+ ::
8
+
9
+ schema = load_audit_category_schema()
10
+ entries = category_id_title_entries()
11
+ # entries[0] == ("A", "API contract verification")
12
+ findings = validate_projections()
13
+ # findings == [] when rubrics and prompt skeletons match the schema
14
+
15
+ Use validate_projections before shipping a rubric or prompt change so the
16
+ three surfaces cannot drift.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import re
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ from skills_pr_loop_constants.audit_category_schema_constants import (
28
+ CATEGORY_ID_KEY,
29
+ CATEGORY_RUBRICS_DIRECTORY,
30
+ CATEGORY_SLUG_KEY,
31
+ CATEGORY_SLUG_PREFIX_TEMPLATE,
32
+ CATEGORY_SUB_BUCKETS_KEY,
33
+ CATEGORY_TITLE_KEY,
34
+ EXPECTED_CATEGORY_IDS,
35
+ HEADING_LETTER_GROUP,
36
+ HEADING_PATTERN,
37
+ HEADING_TITLE_GROUP,
38
+ JSON_INDENT,
39
+ NEWLINE,
40
+ PACKAGE_ROOT,
41
+ PROMPTS_DIRECTORY,
42
+ RUBRIC_SUB_BUCKET_ROW_TEMPLATE,
43
+ SCHEMA_CATEGORIES_KEY,
44
+ SCHEMA_RELATIVE_PATH,
45
+ SCHEMA_VERSION,
46
+ SCHEMA_VERSION_KEY,
47
+ SKELETON_COUNT_PATTERN,
48
+ SKELETON_PLACEHOLDER_FRAGMENT,
49
+ SKELETON_SEPARATOR_LINE,
50
+ SUB_BUCKET_AXIS_KEY,
51
+ UTF8_ENCODING,
52
+ )
53
+
54
+ CategoryMapping = dict[str, object]
55
+ SchemaMapping = dict[str, object]
56
+
57
+
58
+ def audit_category_schema_path() -> Path:
59
+ """Return the path to the committed A-Q schema JSON file.
60
+
61
+ Returns:
62
+ Absolute path to audit-categories.json under the package root.
63
+ """
64
+ return PACKAGE_ROOT / SCHEMA_RELATIVE_PATH
65
+
66
+
67
+ def load_audit_category_schema() -> SchemaMapping:
68
+ """Load and structurally check the A-Q schema document.
69
+
70
+ Returns:
71
+ The schema object with schema_version and categories.
72
+
73
+ Raises:
74
+ FileNotFoundError: When the schema file is missing.
75
+ ValueError: When required fields or order are wrong.
76
+ json.JSONDecodeError: When the schema file is not valid JSON.
77
+ """
78
+ schema_path = audit_category_schema_path()
79
+ loaded_document = json.loads(schema_path.read_text(encoding=UTF8_ENCODING))
80
+ if not isinstance(loaded_document, dict):
81
+ raise ValueError("schema root must be an object")
82
+ if loaded_document.get(SCHEMA_VERSION_KEY) != SCHEMA_VERSION:
83
+ raise ValueError(
84
+ f"schema_version must be {SCHEMA_VERSION}, "
85
+ f"got {loaded_document.get(SCHEMA_VERSION_KEY)!r}"
86
+ )
87
+ all_categories = loaded_document.get(SCHEMA_CATEGORIES_KEY)
88
+ if not isinstance(all_categories, list) or not all_categories:
89
+ raise ValueError("categories must be a non-empty list")
90
+ all_ids = [
91
+ each_category.get(CATEGORY_ID_KEY)
92
+ for each_category in all_categories
93
+ if isinstance(each_category, dict)
94
+ ]
95
+ if all_ids != list(EXPECTED_CATEGORY_IDS):
96
+ raise ValueError(f"categories must list A-Q in order, got {all_ids!r}")
97
+ all_checked_categories: list[CategoryMapping] = []
98
+ for each_category in all_categories:
99
+ if not isinstance(each_category, dict):
100
+ raise ValueError("each category must be an object")
101
+ all_checked_categories.append(_require_category_shape(each_category))
102
+ return {
103
+ SCHEMA_VERSION_KEY: SCHEMA_VERSION,
104
+ SCHEMA_CATEGORIES_KEY: all_checked_categories,
105
+ }
106
+
107
+
108
+ def _require_category_shape(all_category_fields: CategoryMapping) -> CategoryMapping:
109
+ category_id = all_category_fields.get(CATEGORY_ID_KEY)
110
+ if not isinstance(category_id, str) or len(category_id) != 1:
111
+ raise ValueError(f"category id must be a single letter, got {category_id!r}")
112
+ title = all_category_fields.get(CATEGORY_TITLE_KEY)
113
+ if not isinstance(title, str) or not title.strip():
114
+ raise ValueError(f"category {category_id} title must be a non-empty string")
115
+ slug = all_category_fields.get(CATEGORY_SLUG_KEY)
116
+ expected_slug_prefix = CATEGORY_SLUG_PREFIX_TEMPLATE.format(letter=category_id.lower())
117
+ if not isinstance(slug, str) or not slug.startswith(expected_slug_prefix):
118
+ raise ValueError(
119
+ f"category {category_id} slug must start with {expected_slug_prefix!r}, "
120
+ f"got {slug!r}"
121
+ )
122
+ all_sub_bucket_fields = all_category_fields.get(CATEGORY_SUB_BUCKETS_KEY)
123
+ if not isinstance(all_sub_bucket_fields, list) or not all_sub_bucket_fields:
124
+ raise ValueError(f"category {category_id} needs a non-empty sub_buckets list")
125
+ all_sub_buckets: list[CategoryMapping] = []
126
+ for each_index, each_sub_bucket in enumerate(all_sub_bucket_fields):
127
+ if not isinstance(each_sub_bucket, dict):
128
+ raise ValueError(f"category {category_id} sub_bucket {each_index} must be object")
129
+ expected_id = f"{category_id}{each_index + 1}"
130
+ sub_bucket_id = each_sub_bucket.get(CATEGORY_ID_KEY)
131
+ if sub_bucket_id != expected_id:
132
+ raise ValueError(
133
+ f"category {category_id} sub_bucket {each_index} id must be "
134
+ f"{expected_id}, got {sub_bucket_id!r}"
135
+ )
136
+ axis_name = each_sub_bucket.get(SUB_BUCKET_AXIS_KEY)
137
+ if not isinstance(axis_name, str) or not axis_name.strip():
138
+ raise ValueError(
139
+ f"category {category_id} sub_bucket {sub_bucket_id} needs axis_name"
140
+ )
141
+ all_sub_buckets.append(
142
+ {CATEGORY_ID_KEY: expected_id, SUB_BUCKET_AXIS_KEY: axis_name}
143
+ )
144
+ return {
145
+ CATEGORY_ID_KEY: category_id,
146
+ CATEGORY_TITLE_KEY: title,
147
+ CATEGORY_SLUG_KEY: slug,
148
+ CATEGORY_SUB_BUCKETS_KEY: all_sub_buckets,
149
+ }
150
+
151
+
152
+ def category_id_title_entries() -> list[tuple[str, str]]:
153
+ """Return (id, title) pairs in A-Q order from the schema.
154
+
155
+ Returns:
156
+ List of (category letter, title) tuples from the schema file.
157
+ """
158
+ all_schema = load_audit_category_schema()
159
+ all_categories = all_schema[SCHEMA_CATEGORIES_KEY]
160
+ assert isinstance(all_categories, list)
161
+ all_entries: list[tuple[str, str]] = []
162
+ for each_category in all_categories:
163
+ assert isinstance(each_category, dict)
164
+ category_id = each_category[CATEGORY_ID_KEY]
165
+ title = each_category[CATEGORY_TITLE_KEY]
166
+ assert isinstance(category_id, str)
167
+ assert isinstance(title, str)
168
+ all_entries.append((category_id, title))
169
+ return all_entries
170
+
171
+
172
+ def render_skeleton_projections(all_schema: SchemaMapping) -> dict[str, str]:
173
+ """Build deterministic skeleton text per category from the schema only.
174
+
175
+ Args:
176
+ all_schema: Loaded audit-category schema.
177
+
178
+ Returns:
179
+ Map of category letter to a stable multi-line skeleton projection.
180
+ """
181
+ projection_by_letter: dict[str, str] = {}
182
+ all_categories = all_schema[SCHEMA_CATEGORIES_KEY]
183
+ assert isinstance(all_categories, list)
184
+ for each_category in all_categories:
185
+ assert isinstance(each_category, dict)
186
+ category_id = each_category[CATEGORY_ID_KEY]
187
+ title = each_category[CATEGORY_TITLE_KEY]
188
+ slug = each_category[CATEGORY_SLUG_KEY]
189
+ all_sub_buckets = each_category[CATEGORY_SUB_BUCKETS_KEY]
190
+ assert isinstance(category_id, str)
191
+ assert isinstance(title, str)
192
+ assert isinstance(slug, str)
193
+ assert isinstance(all_sub_buckets, list)
194
+ sub_bucket_count = len(all_sub_buckets)
195
+ all_lines = [
196
+ f"Category {category_id} — {title}",
197
+ f"slug: {slug}",
198
+ f"decomposed into {sub_bucket_count} sub-buckets",
199
+ f"For each sub-bucket {category_id}1-{category_id}{sub_bucket_count}:",
200
+ ]
201
+ for each_sub_bucket in all_sub_buckets:
202
+ assert isinstance(each_sub_bucket, dict)
203
+ sub_bucket_id = each_sub_bucket[CATEGORY_ID_KEY]
204
+ axis_name = each_sub_bucket[SUB_BUCKET_AXIS_KEY]
205
+ all_lines.append(f"{sub_bucket_id}. {axis_name}")
206
+ projection_by_letter[category_id] = NEWLINE.join(all_lines) + NEWLINE
207
+ return projection_by_letter
208
+
209
+
210
+ def _validate_one_category(each_category: CategoryMapping) -> list[str]:
211
+ category_id = each_category[CATEGORY_ID_KEY]
212
+ slug = each_category[CATEGORY_SLUG_KEY]
213
+ title = each_category[CATEGORY_TITLE_KEY]
214
+ all_sub_buckets = each_category[CATEGORY_SUB_BUCKETS_KEY]
215
+ assert isinstance(category_id, str)
216
+ assert isinstance(slug, str)
217
+ assert isinstance(title, str)
218
+ assert isinstance(all_sub_buckets, list)
219
+ all_findings: list[str] = []
220
+ rubric_path = CATEGORY_RUBRICS_DIRECTORY / f"{slug}.md"
221
+ prompt_path = PROMPTS_DIRECTORY / f"{slug}.md"
222
+ if not rubric_path.is_file():
223
+ return [f"{category_id}: missing rubric {rubric_path.name}"]
224
+ if not prompt_path.is_file():
225
+ return [f"{category_id}: missing prompt {prompt_path.name}"]
226
+ rubric_text = rubric_path.read_text(encoding=UTF8_ENCODING)
227
+ all_rubric_lines = rubric_text.splitlines()
228
+ if not all_rubric_lines:
229
+ return [f"{category_id}: empty rubric"]
230
+ heading_match = HEADING_PATTERN.match(all_rubric_lines[0])
231
+ if heading_match is None:
232
+ return [f"{category_id}: rubric heading does not match pattern"]
233
+ heading_letter = heading_match.group(HEADING_LETTER_GROUP)
234
+ heading_title = heading_match.group(HEADING_TITLE_GROUP)
235
+ if heading_letter != category_id:
236
+ all_findings.append(f"{category_id}: rubric heading letter {heading_letter!r}")
237
+ if heading_title != title:
238
+ all_findings.append(
239
+ f"{category_id}: rubric title {heading_title!r} != schema {title!r}"
240
+ )
241
+ row_pattern = re.compile(
242
+ RUBRIC_SUB_BUCKET_ROW_TEMPLATE.format(category_id=re.escape(category_id)),
243
+ re.MULTILINE,
244
+ )
245
+ all_rubric_rows = row_pattern.findall(rubric_text)
246
+ all_schema_rows: list[tuple[str, str]] = []
247
+ for each_sub_bucket in all_sub_buckets:
248
+ assert isinstance(each_sub_bucket, dict)
249
+ sub_bucket_id = each_sub_bucket[CATEGORY_ID_KEY]
250
+ axis_name = each_sub_bucket[SUB_BUCKET_AXIS_KEY]
251
+ assert isinstance(sub_bucket_id, str)
252
+ assert isinstance(axis_name, str)
253
+ all_schema_rows.append((sub_bucket_id, axis_name))
254
+ all_rubric_pairs = [(row_id, axis.strip()) for row_id, axis in all_rubric_rows]
255
+ if all_rubric_pairs != all_schema_rows:
256
+ all_findings.append(
257
+ f"{category_id}: rubric sub-buckets {all_rubric_pairs!r} "
258
+ f"!= schema {all_schema_rows!r}"
259
+ )
260
+ all_findings.extend(
261
+ _validate_prompt_skeleton_count(category_id, prompt_path, len(all_sub_buckets))
262
+ )
263
+ return all_findings
264
+
265
+
266
+ def _validate_prompt_skeleton_count(
267
+ category_id: str,
268
+ prompt_path: Path,
269
+ sub_bucket_count: int,
270
+ ) -> list[str]:
271
+ prompt_text = prompt_path.read_text(encoding=UTF8_ENCODING)
272
+ all_skeleton_lines: list[str] = []
273
+ for each_line in prompt_text.splitlines():
274
+ if each_line == SKELETON_SEPARATOR_LINE:
275
+ break
276
+ all_skeleton_lines.append(each_line)
277
+ skeleton_text = NEWLINE.join(all_skeleton_lines)
278
+ if SKELETON_PLACEHOLDER_FRAGMENT in skeleton_text:
279
+ return [
280
+ f"{category_id}: prompt skeleton still uses [N] placeholder; "
281
+ f"use numeric count {sub_bucket_count}"
282
+ ]
283
+ count_match = SKELETON_COUNT_PATTERN.search(skeleton_text)
284
+ if count_match is None:
285
+ return [f"{category_id}: prompt skeleton missing sub-bucket count form"]
286
+ skeleton_count = int(count_match.group(HEADING_LETTER_GROUP))
287
+ if skeleton_count != sub_bucket_count:
288
+ return [
289
+ f"{category_id}: prompt skeleton count {skeleton_count} "
290
+ f"!= schema {sub_bucket_count}"
291
+ ]
292
+ return []
293
+
294
+
295
+ def validate_projections() -> list[str]:
296
+ """Compare schema against on-disk rubric and prompt skeleton projections.
297
+
298
+ Returns:
299
+ Finding strings; empty when every category is in parity.
300
+ """
301
+ all_schema = load_audit_category_schema()
302
+ all_categories = all_schema[SCHEMA_CATEGORIES_KEY]
303
+ assert isinstance(all_categories, list)
304
+ all_findings: list[str] = []
305
+ for each_category in all_categories:
306
+ assert isinstance(each_category, dict)
307
+ all_findings.extend(_validate_one_category(each_category))
308
+ return all_findings
309
+
310
+
311
+ def main(all_argv: list[str]) -> int:
312
+ """CLI: validate projections or print deterministic skeleton projections.
313
+
314
+ Args:
315
+ all_argv: Argument list without the program name.
316
+
317
+ Returns:
318
+ Process exit code (0 success, 1 validation findings).
319
+ """
320
+ parser = argparse.ArgumentParser(description=__doc__)
321
+ parser.add_argument(
322
+ "--validate",
323
+ action="store_true",
324
+ help="Validate rubric and prompt projections against the schema.",
325
+ )
326
+ parser.add_argument(
327
+ "--render-skeletons",
328
+ action="store_true",
329
+ help="Print deterministic skeleton projections as JSON.",
330
+ )
331
+ arguments = parser.parse_args(all_argv)
332
+ if arguments.validate:
333
+ all_findings = validate_projections()
334
+ if all_findings:
335
+ for each_finding in all_findings:
336
+ print(each_finding, file=sys.stderr)
337
+ return 1
338
+ print("OK")
339
+ return 0
340
+ if arguments.render_skeletons:
341
+ all_schema = load_audit_category_schema()
342
+ print(
343
+ json.dumps(
344
+ render_skeleton_projections(all_schema),
345
+ indent=JSON_INDENT,
346
+ ensure_ascii=False,
347
+ )
348
+ )
349
+ return 0
350
+ parser.print_help()
351
+ return 0
352
+
353
+
354
+ if __name__ == "__main__":
355
+ raise SystemExit(main(sys.argv[1:]))
@@ -8,6 +8,7 @@ Python package of named constants for the `pr-loop` shared scripts. All constant
8
8
  |---|---|
9
9
  | `__init__.py` | Package marker. |
10
10
  | `path_resolver_constants.py` | Path template strings and format constants: workspace directory naming, worktree directory name, diff patch filename pattern, outcome XML filename patterns, fix status values, audit constraint texts, audit category entries, fix execution steps, and fix constraint texts. |
11
+ | `audit_category_schema_constants.py` | Paths, regexes, and encoding constants for `audit_category_schema.py` (A-Q schema load and projection parity). |
11
12
  | `preflight_constants.py` | Constants used by `preflight_worktree.py` for exit codes and output marker strings. |
12
13
  | `handoff_constants.py` | Filename, path-segment, and template constants for the durable handoff writer. |
13
14
  | `pacer_constants.py` | Entry-skill names, pacer tokens, CLI flags, and result keys for `select_converge_pacer.py`. |