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,612 @@
1
+ #!/usr/bin/env python3
2
+ """Verify the npm package delivers every installable surface users need.
3
+
4
+ Runs real ``npm pack``, reads the generated tarball member list, and checks
5
+ each directory and root file from ``installable-surfaces.manifest.json``.
6
+ Also walks ``hooks/hooks.json`` for ``command`` fields, requires each ``.py``
7
+ script path to be tracked by ``git ls-files``, and smoke-compiles those scripts.
8
+
9
+ ::
10
+
11
+ python verify_installable_package.py
12
+ -> exit 0 when the packed tarball holds every manifest surface
13
+ flag: missing package/hooks/ in the tarball fails the run
14
+
15
+ Call the public helpers from tests so the suite exercises shipped code rather
16
+ than reimplementing pack or path logic inside the test module.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import py_compile
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ import tarfile
27
+ import tempfile
28
+ from pathlib import Path
29
+ from typing import Mapping
30
+
31
+ from dev_env_scripts_constants.verify_installable_package_constants import (
32
+ CLASS_CONTRADICTORY,
33
+ CLASS_PACKAGED,
34
+ CLASS_SOURCE_ONLY,
35
+ EXIT_CODE_FAILURE,
36
+ EXIT_CODE_SUCCESS,
37
+ GIT_BINARY_NAME,
38
+ GIT_LS_FILES_SUBCOMMAND,
39
+ HOOKS_DIRECTORY_NAME,
40
+ HOOKS_DIRECTORY_PREFIX,
41
+ HOOKS_JSON_RELATIVE_PATH,
42
+ INSTALL_ENTRYPOINT_RELATIVE_PATH,
43
+ INSTALL_ENTRYPOINT_SMOKE_FAILURE_HEADER,
44
+ JSON_COMMAND_KEY,
45
+ MANIFEST_DIRECTORIES_KEY,
46
+ MANIFEST_FILENAME,
47
+ MANIFEST_ROOT_FILES_KEY,
48
+ MISSING_MANIFEST_SURFACES_HEADER,
49
+ NEWLINE_JOIN_SEPARATOR,
50
+ NODE_BINARY_NAME,
51
+ NODE_CHECK_FLAG,
52
+ NPM_BINARY_NAME,
53
+ NPM_CMD_BINARY_NAME,
54
+ NPM_PACK_DESTINATION_FLAG,
55
+ NPM_PACK_FILENAME_KEY,
56
+ NPM_PACK_JSON_FLAG,
57
+ NPM_PACK_SUBCOMMAND,
58
+ PACKAGE_JSON_EXCLUDE_PREFIX,
59
+ PACKAGE_JSON_FILENAME,
60
+ PACKAGE_JSON_FILES_KEY,
61
+ PACKAGE_PATH_FROM_REPOSITORY,
62
+ PLUGIN_ROOT_TOKEN,
63
+ PYTHON_FILE_SUFFIX,
64
+ SMOKE_COMPILE_FAILURES_HEADER,
65
+ TARBALL_PACKAGE_PREFIX,
66
+ UNTRACKED_HOOK_SCRIPTS_HEADER,
67
+ UTF8_ENCODING,
68
+ VERIFICATION_PASSED_MESSAGE,
69
+ )
70
+
71
+
72
+ def package_root_path() -> Path:
73
+ """Return the claude-dev-env package root that holds package.json."""
74
+ return Path(__file__).resolve().parent.parent
75
+
76
+
77
+ def repository_root_path() -> Path:
78
+ """Return the monorepo root two levels above the package root."""
79
+ return package_root_path().parent.parent
80
+
81
+
82
+ def resolve_npm_executable() -> str:
83
+ """Resolve the npm launcher path for the host platform.
84
+
85
+ Returns:
86
+ Absolute path to ``npm.cmd`` on Windows or ``npm`` on POSIX.
87
+
88
+ Raises:
89
+ FileNotFoundError: When neither launcher is on PATH.
90
+ """
91
+ for each_binary_name in (NPM_CMD_BINARY_NAME, NPM_BINARY_NAME):
92
+ maybe_path = shutil.which(each_binary_name)
93
+ if maybe_path is not None:
94
+ return maybe_path
95
+ raise FileNotFoundError(
96
+ f"neither {NPM_CMD_BINARY_NAME!r} nor {NPM_BINARY_NAME!r} found on PATH"
97
+ )
98
+
99
+
100
+ def load_installable_surfaces_manifest(from_package_root: Path) -> Mapping[str, object]:
101
+ """Load the committed installable-surfaces manifest.
102
+
103
+ Args:
104
+ from_package_root: Package directory that holds the manifest file.
105
+
106
+ Returns:
107
+ Parsed JSON object with directories and root_files lists.
108
+ """
109
+ manifest_path = from_package_root / MANIFEST_FILENAME
110
+ return json.loads(manifest_path.read_text(encoding=UTF8_ENCODING))
111
+
112
+
113
+ def _string_names_from_manifest_key(
114
+ all_manifest_entries: Mapping[str, object],
115
+ from_key: str,
116
+ ) -> list[str]:
117
+ """Return non-empty string entries for one manifest list key.
118
+
119
+ Args:
120
+ all_manifest_entries: Parsed installable-surfaces manifest.
121
+ from_key: JSON key whose value is a list of surface names.
122
+
123
+ Returns:
124
+ String basenames from the list, or an empty list when the key is absent
125
+ or not a list.
126
+ """
127
+ all_names = all_manifest_entries.get(from_key, [])
128
+ if not isinstance(all_names, list):
129
+ return []
130
+ return [
131
+ each_name
132
+ for each_name in all_names
133
+ if isinstance(each_name, str) and each_name
134
+ ]
135
+
136
+
137
+ def required_directory_names(all_manifest_entries: Mapping[str, object]) -> list[str]:
138
+ """Return required top-level directory names from the manifest.
139
+
140
+ Args:
141
+ all_manifest_entries: Parsed installable-surfaces manifest.
142
+
143
+ Returns:
144
+ Directory basenames the packed package must deliver.
145
+ """
146
+ return _string_names_from_manifest_key(
147
+ all_manifest_entries,
148
+ MANIFEST_DIRECTORIES_KEY,
149
+ )
150
+
151
+
152
+ def required_root_file_names(all_manifest_entries: Mapping[str, object]) -> list[str]:
153
+ """Return required package-root file names from the manifest.
154
+
155
+ Args:
156
+ all_manifest_entries: Parsed installable-surfaces manifest.
157
+
158
+ Returns:
159
+ File basenames the packed package must deliver at package root.
160
+ """
161
+ return _string_names_from_manifest_key(
162
+ all_manifest_entries,
163
+ MANIFEST_ROOT_FILES_KEY,
164
+ )
165
+
166
+
167
+ def load_package_json_file_entries(from_package_root: Path) -> set[str]:
168
+ """Load positive ``files`` entries from package.json without bang-excludes.
169
+
170
+ Args:
171
+ from_package_root: Package directory that holds package.json.
172
+
173
+ Returns:
174
+ Basename entries with trailing slashes stripped.
175
+ """
176
+ package_json_path = from_package_root / PACKAGE_JSON_FILENAME
177
+ payload = json.loads(package_json_path.read_text(encoding=UTF8_ENCODING))
178
+ all_entries = payload.get(PACKAGE_JSON_FILES_KEY, [])
179
+ if not isinstance(all_entries, list):
180
+ return set()
181
+ return {
182
+ each_entry.rstrip("/")
183
+ for each_entry in all_entries
184
+ if isinstance(each_entry, str)
185
+ and not each_entry.startswith(PACKAGE_JSON_EXCLUDE_PREFIX)
186
+ }
187
+
188
+
189
+ def classify_surface(
190
+ directory_name: str,
191
+ all_packaged_names: set[str],
192
+ from_package_root: Path,
193
+ ) -> str:
194
+ """Classify one top-level surface against on-disk state and package.json.
195
+
196
+ ::
197
+
198
+ classify_surface("hooks", {"hooks", "rules"}, package_root) -> packaged
199
+ classify_surface("scratch", {"hooks"}, package_root) -> source_only
200
+ flag: package.json lists missing_dir with no on-disk folder -> contradictory
201
+
202
+ Args:
203
+ directory_name: Top-level package directory basename.
204
+ all_packaged_names: Positive package.json files basenames.
205
+ from_package_root: Package directory used for the on-disk check.
206
+
207
+ Returns:
208
+ One of packaged, source_only, or contradictory.
209
+ """
210
+ is_on_disk = (from_package_root / directory_name).is_dir()
211
+ is_packaged = directory_name in all_packaged_names
212
+ if is_on_disk and is_packaged:
213
+ return CLASS_PACKAGED
214
+ if is_on_disk and not is_packaged:
215
+ return CLASS_SOURCE_ONLY
216
+ if not is_on_disk and is_packaged:
217
+ return CLASS_CONTRADICTORY
218
+ return CLASS_SOURCE_ONLY
219
+
220
+
221
+ def tarball_member_prefix_for_surface(surface_name: str) -> str:
222
+ """Build the npm-pack member prefix for one surface path.
223
+
224
+ Args:
225
+ surface_name: Directory or root-file path relative to the package root.
226
+
227
+ Returns:
228
+ Prefix such as ``package/hooks`` used to match tarball members.
229
+ """
230
+ return f"{TARBALL_PACKAGE_PREFIX}{surface_name}"
231
+
232
+
233
+ def surface_appears_in_tarball_members(
234
+ surface_name: str,
235
+ all_tarball_members: frozenset[str],
236
+ ) -> bool:
237
+ """Report whether a surface path appears among packed tarball members.
238
+
239
+ Args:
240
+ surface_name: Directory or root-file path relative to the package root.
241
+ all_tarball_members: Normalized member paths from the pack tarball.
242
+
243
+ Returns:
244
+ True when the exact member or a child path under the surface exists.
245
+ """
246
+ member_prefix = tarball_member_prefix_for_surface(surface_name)
247
+ if member_prefix in all_tarball_members:
248
+ return True
249
+ child_prefix = f"{member_prefix}/"
250
+ return any(each_member.startswith(child_prefix) for each_member in all_tarball_members)
251
+
252
+
253
+ def list_npm_pack_tarball_members(from_tarball_path: Path) -> frozenset[str]:
254
+ """List normalized member paths inside an npm pack tarball.
255
+
256
+ Args:
257
+ from_tarball_path: Path to a ``.tgz`` produced by ``npm pack``.
258
+
259
+ Returns:
260
+ Forward-slash member names as stored in the archive.
261
+ """
262
+ with tarfile.open(from_tarball_path, mode="r:gz") as archive:
263
+ return frozenset(
264
+ each_member.name.replace("\\", "/") for each_member in archive.getmembers()
265
+ )
266
+
267
+
268
+ def _npm_pack_filename_from_json(stdout_text: str) -> str:
269
+ payload = json.loads(stdout_text)
270
+ if isinstance(payload, list) and payload:
271
+ first_record = payload[0]
272
+ if isinstance(first_record, dict):
273
+ filename = first_record.get(NPM_PACK_FILENAME_KEY)
274
+ if isinstance(filename, str) and filename:
275
+ return filename
276
+ if isinstance(payload, dict):
277
+ filename = payload.get(NPM_PACK_FILENAME_KEY)
278
+ if isinstance(filename, str) and filename:
279
+ return filename
280
+ raise RuntimeError(f"npm pack --json missing {NPM_PACK_FILENAME_KEY}: {stdout_text!r}")
281
+
282
+
283
+ def run_npm_pack(into_directory: Path, from_package_root: Path) -> Path:
284
+ """Run ``npm pack --json`` and return the written tarball path.
285
+
286
+ Args:
287
+ into_directory: Directory that receives the generated ``.tgz``.
288
+ from_package_root: Package directory npm packs.
289
+
290
+ Returns:
291
+ Absolute path of the generated tarball.
292
+ """
293
+ completed = subprocess.run(
294
+ [
295
+ resolve_npm_executable(),
296
+ NPM_PACK_SUBCOMMAND,
297
+ NPM_PACK_JSON_FLAG,
298
+ NPM_PACK_DESTINATION_FLAG,
299
+ str(into_directory),
300
+ ],
301
+ cwd=str(from_package_root),
302
+ check=True,
303
+ capture_output=True,
304
+ text=True,
305
+ encoding=UTF8_ENCODING,
306
+ )
307
+ tarball_name = _npm_pack_filename_from_json(completed.stdout)
308
+ return into_directory / tarball_name
309
+
310
+
311
+ def missing_manifest_surfaces_in_tarball(
312
+ all_manifest_entries: Mapping[str, object],
313
+ all_tarball_members: frozenset[str],
314
+ ) -> list[str]:
315
+ """List manifest surfaces absent from the packed tarball.
316
+
317
+ Args:
318
+ all_manifest_entries: Parsed installable-surfaces manifest.
319
+ all_tarball_members: Member paths from ``npm pack``.
320
+
321
+ Returns:
322
+ Surface names (directories and root files) missing from the tarball.
323
+ """
324
+ all_required = required_directory_names(all_manifest_entries) + required_root_file_names(
325
+ all_manifest_entries
326
+ )
327
+ return [
328
+ each_surface
329
+ for each_surface in all_required
330
+ if not surface_appears_in_tarball_members(each_surface, all_tarball_members)
331
+ ]
332
+
333
+
334
+ def walk_json_command_fields(node: object) -> list[str]:
335
+ """Collect every non-empty string under a ``command`` key in JSON.
336
+
337
+ Args:
338
+ node: Arbitrary JSON-decoded object.
339
+
340
+ Returns:
341
+ Command strings in depth-first encounter order.
342
+ """
343
+ all_commands: list[str] = []
344
+ if isinstance(node, dict):
345
+ command = node.get(JSON_COMMAND_KEY)
346
+ if isinstance(command, str) and command.strip():
347
+ all_commands.append(command)
348
+ for each_child in node.values():
349
+ all_commands.extend(walk_json_command_fields(each_child))
350
+ return all_commands
351
+ if isinstance(node, list):
352
+ for each_child in node:
353
+ all_commands.extend(walk_json_command_fields(each_child))
354
+ return all_commands
355
+
356
+
357
+ def extract_python_script_token(command: str) -> str | None:
358
+ """Extract a ``.py`` path token from a hook command string.
359
+
360
+ Handles ``${CLAUDE_PLUGIN_ROOT}/hooks/...py`` shapes and absolute paths
361
+ that end with a Python script segment.
362
+
363
+ Args:
364
+ command: Full hook command line from hooks.json.
365
+
366
+ Returns:
367
+ The ``.py`` token, or None when the command has no script path.
368
+ """
369
+ if PYTHON_FILE_SUFFIX not in command:
370
+ return None
371
+ for each_part in command.replace("\\", "/").split():
372
+ if each_part.endswith(PYTHON_FILE_SUFFIX):
373
+ return each_part
374
+ return None
375
+
376
+
377
+ def package_relative_hook_script_path(script_token: str) -> str | None:
378
+ """Normalize a hook script token to a package-relative ``hooks/...`` path.
379
+
380
+ Args:
381
+ script_token: Path token extracted from a hook command.
382
+
383
+ Returns:
384
+ Package-relative path, or None when the token is not under hooks/.
385
+ """
386
+ normalized = script_token.replace("\\", "/")
387
+ if normalized.startswith(PLUGIN_ROOT_TOKEN):
388
+ normalized = normalized[len(PLUGIN_ROOT_TOKEN) :]
389
+ package_marker = f"{PACKAGE_PATH_FROM_REPOSITORY}/"
390
+ if package_marker in normalized:
391
+ normalized = normalized.split(package_marker, 1)[1]
392
+ if normalized.startswith(HOOKS_DIRECTORY_PREFIX):
393
+ return normalized
394
+ return None
395
+
396
+
397
+ def load_hook_python_script_paths(from_package_root: Path) -> list[str]:
398
+ """Load package-relative ``.py`` paths referenced by hooks.json commands.
399
+
400
+ Args:
401
+ from_package_root: Package directory that holds hooks/hooks.json.
402
+
403
+ Returns:
404
+ Deduplicated package-relative script paths in first-seen order.
405
+ """
406
+ hooks_json_path = from_package_root / HOOKS_JSON_RELATIVE_PATH
407
+ payload = json.loads(hooks_json_path.read_text(encoding=UTF8_ENCODING))
408
+ seen_paths: set[str] = set()
409
+ all_paths: list[str] = []
410
+ for each_command in walk_json_command_fields(payload):
411
+ script_token = extract_python_script_token(each_command)
412
+ if script_token is None:
413
+ continue
414
+ relative_path = package_relative_hook_script_path(script_token)
415
+ if relative_path is None or relative_path in seen_paths:
416
+ continue
417
+ seen_paths.add(relative_path)
418
+ all_paths.append(relative_path)
419
+ return all_paths
420
+
421
+
422
+ def list_git_tracked_paths_under(
423
+ under_relative_path: str,
424
+ from_repository_root: Path,
425
+ ) -> frozenset[str]:
426
+ """List git-tracked paths under a repository-relative prefix.
427
+
428
+ Args:
429
+ under_relative_path: Repository-relative directory or file prefix.
430
+ from_repository_root: Repository root for ``git ls-files``.
431
+
432
+ Returns:
433
+ Forward-slash repository-relative paths present in the index.
434
+ """
435
+ completed = subprocess.run(
436
+ [GIT_BINARY_NAME, GIT_LS_FILES_SUBCOMMAND, "--", under_relative_path],
437
+ cwd=str(from_repository_root),
438
+ check=True,
439
+ capture_output=True,
440
+ text=True,
441
+ encoding=UTF8_ENCODING,
442
+ )
443
+ return frozenset(
444
+ each_line.replace("\\", "/")
445
+ for each_line in completed.stdout.splitlines()
446
+ if each_line.strip()
447
+ )
448
+
449
+
450
+ def untracked_or_missing_hook_scripts(
451
+ all_package_relative_scripts: list[str],
452
+ from_package_root: Path,
453
+ from_repository_root: Path,
454
+ ) -> list[str]:
455
+ """Return hook scripts that are missing on disk or absent from the git index.
456
+
457
+ Args:
458
+ all_package_relative_scripts: Paths relative to the package root.
459
+ from_package_root: Package directory for on-disk existence checks.
460
+ from_repository_root: Repository root for ``git ls-files``.
461
+
462
+ Returns:
463
+ Package-relative script paths that fail the committed-file check.
464
+ """
465
+ hooks_prefix = f"{PACKAGE_PATH_FROM_REPOSITORY}/{HOOKS_DIRECTORY_NAME}"
466
+ all_tracked = list_git_tracked_paths_under(
467
+ under_relative_path=hooks_prefix,
468
+ from_repository_root=from_repository_root,
469
+ )
470
+ missing_scripts: list[str] = []
471
+ for each_script in all_package_relative_scripts:
472
+ on_disk_path = from_package_root / each_script
473
+ repository_relative = f"{PACKAGE_PATH_FROM_REPOSITORY}/{each_script}".replace(
474
+ "\\", "/"
475
+ )
476
+ if not on_disk_path.is_file():
477
+ missing_scripts.append(each_script)
478
+ continue
479
+ if repository_relative not in all_tracked:
480
+ missing_scripts.append(each_script)
481
+ return missing_scripts
482
+
483
+
484
+ def smoke_compile_python_scripts(
485
+ all_package_relative_scripts: list[str],
486
+ from_package_root: Path,
487
+ ) -> list[str]:
488
+ """Run ``py_compile`` on each package-relative script and collect failures.
489
+
490
+ Args:
491
+ all_package_relative_scripts: Paths relative to the package root.
492
+ from_package_root: Package directory that holds the scripts.
493
+
494
+ Returns:
495
+ Package-relative paths that failed to compile, with error text.
496
+ """
497
+ all_failures: list[str] = []
498
+ for each_script in all_package_relative_scripts:
499
+ script_path = from_package_root / each_script
500
+ try:
501
+ py_compile.compile(str(script_path), doraise=True)
502
+ except py_compile.PyCompileError as compile_error:
503
+ all_failures.append(f"{each_script}: {compile_error}")
504
+ return all_failures
505
+
506
+
507
+ def smoke_check_install_entrypoint(from_package_root: Path) -> str | None:
508
+ """Run ``node --check`` on the install entrypoint.
509
+
510
+ Args:
511
+ from_package_root: Package directory that holds bin/install.mjs.
512
+
513
+ Returns:
514
+ Error text when the check fails, otherwise None.
515
+ """
516
+ entrypoint_path = from_package_root / INSTALL_ENTRYPOINT_RELATIVE_PATH
517
+ completed = subprocess.run(
518
+ [NODE_BINARY_NAME, NODE_CHECK_FLAG, str(entrypoint_path)],
519
+ cwd=str(from_package_root),
520
+ capture_output=True,
521
+ text=True,
522
+ encoding=UTF8_ENCODING,
523
+ )
524
+ if completed.returncode == 0:
525
+ return None
526
+ return completed.stderr.strip() or completed.stdout.strip() or f"exit {completed.returncode}"
527
+
528
+
529
+ def verify_packed_manifest_surfaces(from_package_root: Path) -> list[str]:
530
+ """Pack the package and list manifest surfaces missing from the tarball.
531
+
532
+ Args:
533
+ from_package_root: Package directory to pack and verify.
534
+
535
+ Returns:
536
+ Missing surface names, empty when the tarball is complete.
537
+ """
538
+ all_manifest_entries = load_installable_surfaces_manifest(
539
+ from_package_root=from_package_root
540
+ )
541
+ with tempfile.TemporaryDirectory() as temporary_directory:
542
+ tarball_path = run_npm_pack(
543
+ into_directory=Path(temporary_directory),
544
+ from_package_root=from_package_root,
545
+ )
546
+ all_members = list_npm_pack_tarball_members(from_tarball_path=tarball_path)
547
+ return missing_manifest_surfaces_in_tarball(all_manifest_entries, all_members)
548
+
549
+
550
+ def run_all_installable_package_checks(
551
+ from_package_root: Path,
552
+ from_repository_root: Path,
553
+ ) -> list[str]:
554
+ """Run pack, git-index, and smoke checks; return human-readable failures.
555
+
556
+ Args:
557
+ from_package_root: Package directory under verification.
558
+ from_repository_root: Repository root for git index checks.
559
+
560
+ Returns:
561
+ Failure lines; empty when every check passes.
562
+ """
563
+ all_failures: list[str] = []
564
+ missing_surfaces = verify_packed_manifest_surfaces(from_package_root=from_package_root)
565
+ if missing_surfaces:
566
+ all_failures.append(MISSING_MANIFEST_SURFACES_HEADER)
567
+ all_failures.extend(missing_surfaces)
568
+
569
+ all_hook_scripts = load_hook_python_script_paths(from_package_root=from_package_root)
570
+ missing_hooks = untracked_or_missing_hook_scripts(
571
+ all_package_relative_scripts=all_hook_scripts,
572
+ from_package_root=from_package_root,
573
+ from_repository_root=from_repository_root,
574
+ )
575
+ if missing_hooks:
576
+ all_failures.append(UNTRACKED_HOOK_SCRIPTS_HEADER)
577
+ all_failures.extend(missing_hooks)
578
+
579
+ compile_failures = smoke_compile_python_scripts(
580
+ all_package_relative_scripts=all_hook_scripts,
581
+ from_package_root=from_package_root,
582
+ )
583
+ if compile_failures:
584
+ all_failures.append(SMOKE_COMPILE_FAILURES_HEADER)
585
+ all_failures.extend(compile_failures)
586
+
587
+ entrypoint_failure = smoke_check_install_entrypoint(from_package_root=from_package_root)
588
+ if entrypoint_failure is not None:
589
+ all_failures.append(INSTALL_ENTRYPOINT_SMOKE_FAILURE_HEADER)
590
+ all_failures.append(entrypoint_failure)
591
+ return all_failures
592
+
593
+
594
+ def main() -> int:
595
+ """CLI entry: verify pack surfaces, hook scripts, and smoke checks.
596
+
597
+ Returns:
598
+ ``0`` when every check passes, ``1`` when any check fails.
599
+ """
600
+ all_failures = run_all_installable_package_checks(
601
+ from_package_root=package_root_path(),
602
+ from_repository_root=repository_root_path(),
603
+ )
604
+ if all_failures:
605
+ print(NEWLINE_JOIN_SEPARATOR.join(all_failures))
606
+ return EXIT_CODE_FAILURE
607
+ print(VERIFICATION_PASSED_MESSAGE)
608
+ return EXIT_CODE_SUCCESS
609
+
610
+
611
+ if __name__ == "__main__":
612
+ sys.exit(main())
package/settings.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Edit($HOME/.claude/verification/**)",
5
+ "Write($HOME/.claude/code-review-stamps/**)",
6
+ "Edit($HOME/.claude/code-review-stamps/**)",
7
+ "MultiEdit($HOME/.claude/code-review-stamps/**)"
8
+ ]
9
+ }
10
+ }
package/skills/CLAUDE.md CHANGED
@@ -17,7 +17,9 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
17
17
 
18
18
  ## Shared support code
19
19
 
20
- `_shared/`support code used by more than one skill. It holds `pr-loop/`, which provides prompt templates and Python helper scripts shared across the PR-loop skills, and `advisor/`, which provides the shared warm-advisor protocol used by `team-advisor`, `orchestrator`, and `orchestrator-refresh`.
20
+ **`skills/_shared/`** — skill-local PR-loop helpers plus `@` stubs that name
21
+ canonical homes under **`@~/.claude/_shared/`** (advisor protocol, PR-loop
22
+ contracts, runtime scripts). Map: `skills/_shared/CLAUDE.md`. End-of-run gotchas: `skills/_shared/end-of-run-gotcha-recommendations.md`.
21
23
 
22
24
  ## Skill groups
23
25
 
@@ -25,7 +27,7 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
25
27
  - `anthropic-plan` — creates a source-grounded plan packet before any code changes
26
28
  - `orchestrator` — turns the session into the orchestrator: it spawns executor subagents to do the code edits and test runs; hard decisions go to a shared advisor (Claude warm `session-advisor` via SendMessage; a third-party host: max-tier Claude via CLI Claude-chain)
27
29
  - `orchestrator-refresh` — sub-skill fired by the `/orchestrator` loop to re-assert the host-matched shared-advisor discipline mid-run (Claude SendMessage; a third-party host's Claude CLI chain, no Agent-tool advisor spawn)
28
- - `team-advisor` — binds one advisor at the strongest reachable tier (Claude warm agent; a third-party host: max-tier Claude via CLI Claude-chain, fail closed when unreachable) and consults it for a second opinion before a big decision, at completion, when stuck, or when reconsidering the approach
30
+ - `team-advisor` — binds one advisor at the strongest reachable tier (Claude warm agent; a third-party host: max-tier Claude via CLI Claude-chain, fail closed when unreachable); consult cadence and weight live in `docs/references/advisor-tool.md`
29
31
  - `grokify` — builds a paste-ready Grok Build handoff with a Claude advisor charter
30
32
  - `grok-spawn` — orchestrator playbook for fleets of headless grok CLI workers (preflight, batch spec, `spawn_grok_batch.py`)
31
33
 
@@ -44,6 +46,8 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
44
46
  - `recall` — retrieves facts from memory files
45
47
  - `remember` — saves a decision, gotcha, or architectural choice to the Obsidian vault
46
48
  - `everything-search` — file-system search via the Everything `es.exe` CLI
49
+ - `beat-sheet` — reformats a reply as short single-line beats
50
+ - `show` — creates and reviews inline visual explanations: diagrams, interactive widgets, mockups, charts, and illustrations
47
51
 
48
52
  **Session and workflow management**
49
53
  - `session-log` — logs a session report to the Obsidian vault
@@ -1,11 +1,44 @@
1
- # _shared
1
+ # skills/_shared
2
2
 
3
- Support code shared across multiple skills. Each subdirectory targets a specific cross-skill concern.
3
+ **Map** for skill-install shared assets. Open a stub, then load the `@` target.
4
+
5
+ ## Two homes
6
+
7
+ | Home | Path | Holds |
8
+ |---|---|---|
9
+ | **Skills shared** | `~/.claude/skills/_shared/` | Converge helpers, end-of-run gotcha ref, and `@` stubs |
10
+ | **Top-level shared** | `~/.claude/_shared/` | Advisor protocol, PR-loop contracts, runtime scripts |
11
+
12
+
13
+ ## Reference docs (this tree)
14
+
15
+ | File | Role |
16
+ |---|---|
17
+ | **`end-of-run-gotcha-recommendations.md`** | End-of-run pasteable gotcha protocol for every skill |
4
18
 
5
19
  ## Subdirectories
6
20
 
7
21
  | Directory | Role |
8
22
  |---|---|
9
- | `pr-loop/` | Prompt templates and Python helper scripts used by both `bugteam` and `pr-converge` for their audit-fix loop. |
23
+ | **`advisor/`** | Stubs `@~/.claude/_shared/advisor/` |
24
+ | **`pr-loop/`** | Local converge helpers + stubs → `@~/.claude/_shared/pr-loop/` |
25
+
26
+ ## Canonical-path stubs
27
+
28
+ | Stub here | Load |
29
+ |---|---|
30
+ | `advisor/advisor-protocol.md` | `@~/.claude/_shared/advisor/advisor-protocol.md` |
31
+ | `advisor/CLAUDE.md` | `@~/.claude/_shared/advisor/CLAUDE.md` |
32
+ | `advisor/scripts/README.md` | `@~/.claude/_shared/advisor/scripts/` |
33
+ | `pr-loop/audit-contract.md` | `@~/.claude/_shared/pr-loop/audit-contract.md` |
34
+ | `pr-loop/audit-reply-template.md` | `@~/.claude/_shared/pr-loop/audit-reply-template.md` |
35
+ | `pr-loop/code-rules-gate.md` | `@~/.claude/_shared/pr-loop/code-rules-gate.md` |
36
+ | `pr-loop/fix-protocol.md` | `@~/.claude/_shared/pr-loop/fix-protocol.md` |
37
+ | `pr-loop/gh-payloads.md` | `@~/.claude/_shared/pr-loop/gh-payloads.md` |
38
+ | `pr-loop/post-audit-thread-contract.md` | `@~/.claude/_shared/pr-loop/post-audit-thread-contract.md` |
39
+ | `pr-loop/precatch-rubric.md` | `@~/.claude/_shared/pr-loop/precatch-rubric.md` |
40
+ | `pr-loop/state-schema.md` | `@~/.claude/_shared/pr-loop/state-schema.md` |
41
+ | `pr-loop/worker-spawn.md` | `@~/.claude/_shared/pr-loop/worker-spawn.md` |
42
+ | `pr-loop/scripts/RUNTIME_SCRIPTS.md` | `@~/.claude/_shared/pr-loop/scripts/` |
10
43
 
11
- Files here are not skills themselves and have no `SKILL.md`. They install alongside each consuming skill via the install pipeline in `packages/claude-dev-env/bin/install.mjs`.
44
+ Install via `packages/claude-dev-env/bin/install.mjs`.