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
package/bin/install.mjs CHANGED
@@ -3,13 +3,18 @@
3
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, copyFileSync, unlinkSync, rmSync, rmdirSync, renameSync, realpathSync, lstatSync } from 'node:fs';
4
4
  import { join, dirname, resolve, relative, basename, isAbsolute, extname } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
- import { execSync, execFileSync } from 'node:child_process';
6
+ import { execSync, execFileSync, spawnSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { createRequire } from 'node:module';
9
9
  import { installAllGitHooks } from './git_hooks_installer.mjs';
10
10
  import { installMypyIniForClaudeHooks } from './install_mypy_ini.mjs';
11
11
  import { expandHomeDirectoryTokensInSettings } from './expand_home_directory_tokens.mjs';
12
12
  import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
13
+ import {
14
+ managedDenyEntriesFromPackageSettings,
15
+ mergeManagedPermissionsIntoSettings,
16
+ pruneManagedPermissionsFromSettings,
17
+ } from './merge_managed_permissions.mjs';
13
18
  import {
14
19
  SKIPPED_SOURCE_ENTRY_NAMES,
15
20
  SKIPPED_SOURCE_FILE_EXTENSIONS,
@@ -17,17 +22,72 @@ import {
17
22
  MANAGED_SKILLS_DIRECTORY_NAME,
18
23
  MANAGED_HOOKS_DIRECTORY_NAME,
19
24
  SETTINGS_FILE_NAME,
20
- MYPY_INI_FILE_NAME,
21
25
  } from './install-constants.mjs';
26
+ import {
27
+ resolveInstallRoot,
28
+ parseExplicitTargetFromArgv,
29
+ } from './resolve-install-root.mjs';
30
+ import {
31
+ parseInstallTargetSelectionFromArgv,
32
+ resolveInstallTargets,
33
+ buildTargetManifestRecord,
34
+ stripTargetSelectionFlagsFromArgv,
35
+ resolveProfilesRootDirectory,
36
+ MAIN_DEFAULT_TARGET_IDENTITY,
37
+ TARGET_IDENTITY_FLAG,
38
+ } from './select-install-targets.mjs';
39
+ import {
40
+ buildInstallPlan,
41
+ buildUninstallPlan,
42
+ InstallPlanPreflightError,
43
+ } from './install-plan.mjs';
44
+ import {
45
+ FAULT_PHASES,
46
+ InstallTransactionFaultError,
47
+ TRANSACTION_JOURNAL_DIRECTORY_NAME,
48
+ capturePriorInstallSnapshot,
49
+ resolveFaultPhaseFromEnvironment,
50
+ runWithInstallTransaction,
51
+ } from './install-transaction.mjs';
22
52
 
23
- const CLAUDE_HOME = join(homedir(), '.claude');
24
53
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
25
- const MANIFEST_FILE = join(CLAUDE_HOME, '.claude-dev-env-manifest.json');
26
- const MYPY_INI_INSTALL_PATH = join(homedir(), MYPY_INI_FILE_NAME);
54
+ const INSTALL_ROOT_RESOLUTION = resolveInstallRoot({
55
+ environment: process.env,
56
+ homeDirectory: homedir(),
57
+ explicitTarget: (() => {
58
+ try {
59
+ return parseExplicitTargetFromArgv(process.argv.slice(2));
60
+ } catch {
61
+ return null;
62
+ }
63
+ })(),
64
+ });
65
+ const CLAUDE_HOME = INSTALL_ROOT_RESOLUTION.managedRoot;
66
+ const MANIFEST_FILE = INSTALL_ROOT_RESOLUTION.manifestFilePath;
67
+ const MYPY_INI_INSTALL_PATH = INSTALL_ROOT_RESOLUTION.mypyIniInstallPath;
27
68
  const PACKAGE_NAME = 'claude-dev-env';
28
69
  const PACKAGE_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version;
29
70
  const packageRequire = createRequire(import.meta.url);
30
71
 
72
+ /**
73
+ * Target identity for the current process hop. Multi-profile parent hops re-exec
74
+ * each child with --target-identity; a bare main install stays "main".
75
+ */
76
+ const INSTALL_TARGET_IDENTITY = (() => {
77
+ try {
78
+ const selection = parseInstallTargetSelectionFromArgv(process.argv.slice(2));
79
+ if (selection.targetIdentity) {
80
+ return selection.targetIdentity;
81
+ }
82
+ if (selection.mode === 'profiles' && selection.allProfileIds.length === 1) {
83
+ return selection.allProfileIds[0];
84
+ }
85
+ return MAIN_DEFAULT_TARGET_IDENTITY;
86
+ } catch {
87
+ return MAIN_DEFAULT_TARGET_IDENTITY;
88
+ }
89
+ })();
90
+
31
91
  export const CONTENT_DIRECTORIES = ['rules', 'docs', 'commands', 'agents', 'system-prompts', 'scripts', '_shared', 'audit-rubrics'];
32
92
 
33
93
  /**
@@ -48,8 +108,7 @@ const NEVER_PRUNED_SKILL_DIRECTORIES = new Set(['_shared']);
48
108
  const PRUNED_SKILLS_BACKUP_DIRECTORY_NAME = '.claude-dev-env-pruned';
49
109
  const RETIRED_SKILL_REASON_LABEL = 'retired';
50
110
  const STALE_FILE_REASON_LABEL = 'stale';
51
- const MANIFEST_FILES_KEY = 'files';
52
- const MANIFEST_SKILLS_KEY = 'skills';
111
+ const MANIFEST_MANAGED_PERMISSIONS_KEY = 'managedPermissions';
53
112
 
54
113
  export const CORE_INCLUDE_DIRECTORIES = [
55
114
  'rules', 'docs', 'commands', 'agents', 'audit-rubrics', '_shared', 'scripts',
@@ -62,6 +121,7 @@ export const CORE_SKILLS = [
62
121
  'privacy-hygiene',
63
122
  'issue-tracker',
64
123
  'recall', 'remember', 'task-build',
124
+ 'show',
65
125
  ];
66
126
 
67
127
  export function collectPackageSourceConflicts(packageDirectory) {
@@ -110,24 +170,6 @@ export function collectPackageSourceConflicts(packageDirectory) {
110
170
  return allConflicts;
111
171
  }
112
172
 
113
- function abortWhenPackageSourceHasConflicts(packageDirectory) {
114
- const conflicts = collectPackageSourceConflicts(packageDirectory);
115
- if (conflicts.length === 0) return;
116
- console.error(
117
- `\nERROR: ${PACKAGE_NAME} source has unmerged conflicts under ${packageDirectory}:\n`,
118
- );
119
- for (const conflict of conflicts) {
120
- console.error(` ${conflict.statusCode} ${conflict.path}`);
121
- }
122
- console.error(
123
- '\nResolve the conflicts in the package source before running the installer.',
124
- );
125
- console.error(
126
- 'Installing from a conflicted source can copy stale or broken files into ~/.claude/.\n',
127
- );
128
- process.exit(1);
129
- }
130
-
131
173
  function resolveDependencyPackageRoot(dependencyPackageName) {
132
174
  const dependencyPackageJsonPath = packageRequire.resolve(
133
175
  `${dependencyPackageName}/package.json`
@@ -1357,6 +1399,37 @@ export function pruneRetiredHookEntriesFromSettings(settingsPath, retiredHookRel
1357
1399
  return removedCount;
1358
1400
  }
1359
1401
 
1402
+ /**
1403
+ * Load ~/.claude/settings.json for an in-place merge, or `{}` when absent/empty.
1404
+ *
1405
+ * Malformed JSON or a non-object root ends the process so install never writes
1406
+ * onto a shape the harness cannot read.
1407
+ *
1408
+ * @param {string} settingsPath Absolute path to settings.json.
1409
+ * @returns {object}
1410
+ */
1411
+ function loadClaudeSettingsObjectOrExit(settingsPath) {
1412
+ if (!existsSync(settingsPath)) {
1413
+ return {};
1414
+ }
1415
+ const raw = readFileSync(settingsPath, 'utf8').trim();
1416
+ if (!raw) {
1417
+ return {};
1418
+ }
1419
+ let settings;
1420
+ try {
1421
+ settings = JSON.parse(raw);
1422
+ } catch {
1423
+ console.error(' ERROR: settings.json is malformed JSON. Fix it and rerun.');
1424
+ process.exit(1);
1425
+ }
1426
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
1427
+ console.error(' ERROR: settings.json holds a value other than a JSON object. Fix it and rerun.');
1428
+ process.exit(1);
1429
+ }
1430
+ return settings;
1431
+ }
1432
+
1360
1433
  /**
1361
1434
  * Merge one package source root's hook groups into ~/.claude/settings.json.
1362
1435
  *
@@ -1373,74 +1446,68 @@ function mergeHooks(hooksSourceRoot, pythonCommand) {
1373
1446
  if (!existsSync(hooksJsonPath)) return 0;
1374
1447
  const hooksConfig = JSON.parse(readFileSync(hooksJsonPath, 'utf8'));
1375
1448
  const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
1376
- let settings = {};
1377
- if (existsSync(settingsPath)) {
1378
- const raw = readFileSync(settingsPath, 'utf8').trim();
1379
- if (raw) {
1380
- try { settings = JSON.parse(raw); }
1381
- catch { console.error(' ERROR: settings.json is malformed JSON. Fix it and rerun.'); process.exit(1); }
1382
- if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
1383
- console.error(' ERROR: settings.json holds a value other than a JSON object. Fix it and rerun.');
1384
- process.exit(1);
1385
- }
1386
- }
1387
- }
1449
+ const settings = loadClaudeSettingsObjectOrExit(settingsPath);
1388
1450
  const groupCount = mergeHooksIntoSettings(settings, hooksConfig, CLAUDE_HOME, pythonCommand);
1389
1451
  writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
1390
1452
  return groupCount;
1391
1453
  }
1392
1454
 
1393
- function writeManifest(installedFiles, skillNames) {
1394
- const manifest = {
1395
- package: PACKAGE_NAME,
1396
- version: PACKAGE_VERSION,
1397
- installedAt: new Date().toISOString(),
1398
- [MANIFEST_FILES_KEY]: installedFiles,
1399
- };
1400
- if (skillNames) {
1401
- manifest[MANIFEST_SKILLS_KEY] = skillNames;
1455
+ function writeManifest(installedFiles, skillNames, managedPermissions = null) {
1456
+ const manifest = buildTargetManifestRecord({
1457
+ packageName: PACKAGE_NAME,
1458
+ packageVersion: PACKAGE_VERSION,
1459
+ targetIdentity: INSTALL_TARGET_IDENTITY,
1460
+ managedRoot: CLAUDE_HOME,
1461
+ files: installedFiles,
1462
+ skills: skillNames,
1463
+ });
1464
+ if (managedPermissions && Array.isArray(managedPermissions.deny) && managedPermissions.deny.length > 0) {
1465
+ manifest[MANIFEST_MANAGED_PERMISSIONS_KEY] = {
1466
+ deny: [...managedPermissions.deny],
1467
+ };
1402
1468
  }
1403
1469
  writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2) + '\n');
1404
1470
  }
1405
1471
 
1406
1472
  /**
1407
- * Read the file list and the skill list the previous install recorded, in one
1408
- * parse of the manifest.
1473
+ * Load package-owned deny entries from the package settings.json source.
1409
1474
  *
1410
- * A full install's manifest holds thousands of path strings, so the record is
1411
- * read and parsed once and both lists are handed back together.
1412
- *
1413
- * Either list is null when the manifest is missing, unreadable, or holds no array
1414
- * at that key, so a caller treats every such case as "no prior record": the skills
1415
- * list leans on the ever-shipped set to find retired skills, and the files list
1416
- * holds the stale-file prune for that run rather than guessing at what a prior
1417
- * install wrote. A `--update` run purges the manifest before reinstalling, so this
1418
- * read happens at the top of `install()` while the record is still on disk.
1419
- *
1420
- * @returns {{files: string[]|null, skills: string[]|null}} The recorded lists, each null when absent.
1475
+ * @returns {string[]}
1421
1476
  */
1422
- function readPriorManifestArrays() {
1423
- const missingRecord = { files: null, skills: null };
1424
- if (!existsSync(MANIFEST_FILE)) return missingRecord;
1477
+ function loadPackageManagedDenyEntries() {
1478
+ const packageSettingsPath = join(PACKAGE_ROOT, SETTINGS_FILE_NAME);
1479
+ if (!existsSync(packageSettingsPath)) {
1480
+ return [];
1481
+ }
1425
1482
  try {
1426
- const priorManifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf8'));
1427
- return {
1428
- files: arrayOrNull(priorManifest[MANIFEST_FILES_KEY]),
1429
- skills: arrayOrNull(priorManifest[MANIFEST_SKILLS_KEY]),
1430
- };
1431
- } catch {
1432
- return missingRecord;
1483
+ const packageSettings = JSON.parse(readFileSync(packageSettingsPath, 'utf8'));
1484
+ return managedDenyEntriesFromPackageSettings(packageSettings);
1485
+ } catch (parseError) {
1486
+ console.warn(
1487
+ ` Warning: could not read package ${SETTINGS_FILE_NAME} for managed permissions (${parseError.message})`,
1488
+ );
1489
+ return [];
1433
1490
  }
1434
1491
  }
1435
1492
 
1436
1493
  /**
1437
- * Return a recorded manifest value when it is an array, and null otherwise.
1494
+ * Merge package-owned permission defaults into ~/.claude/settings.json.
1438
1495
  *
1439
- * @param {unknown} recordedEntries The value read at a manifest key.
1440
- * @returns {string[]|null} The array, or null when the key holds anything else.
1496
+ * @returns {{addedCount: number, alreadyPresentCount: number, managedDenyEntries: string[]}}
1441
1497
  */
1442
- function arrayOrNull(recordedEntries) {
1443
- return Array.isArray(recordedEntries) ? recordedEntries : null;
1498
+ function mergeManagedPermissions() {
1499
+ const managedDenyEntries = loadPackageManagedDenyEntries();
1500
+ if (managedDenyEntries.length === 0) {
1501
+ return { addedCount: 0, alreadyPresentCount: 0, managedDenyEntries: [] };
1502
+ }
1503
+ const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
1504
+ const settingsExisted = existsSync(settingsPath);
1505
+ const settings = loadClaudeSettingsObjectOrExit(settingsPath);
1506
+ const mergeOutcome = mergeManagedPermissionsIntoSettings(settings, managedDenyEntries);
1507
+ if (mergeOutcome.addedCount > 0 || !settingsExisted) {
1508
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
1509
+ }
1510
+ return mergeOutcome;
1444
1511
  }
1445
1512
 
1446
1513
  /**
@@ -1660,25 +1727,104 @@ function runFullInstallPrunes(
1660
1727
  * @returns {void}
1661
1728
  */
1662
1729
  function install(selectedGroups, options = {}) {
1663
- const { files: priorManifestFiles, skills: priorManifestSkills } = readPriorManifestArrays();
1664
- const isUpdateRefresh = Boolean(options.isUpdateRefresh);
1665
- if (isUpdateRefresh && !selectedGroups && existsSync(MANIFEST_FILE)) {
1730
+ let plan;
1731
+ try {
1732
+ plan = buildInstallPlan({
1733
+ packageRoot: PACKAGE_ROOT,
1734
+ managedRoot: CLAUDE_HOME,
1735
+ manifestFilePath: MANIFEST_FILE,
1736
+ targetIdentity: INSTALL_TARGET_IDENTITY,
1737
+ selectedGroups,
1738
+ isUpdateRefresh: Boolean(options.isUpdateRefresh),
1739
+ installGroups: INSTALL_GROUPS,
1740
+ collectSourceConflicts: collectPackageSourceConflicts,
1741
+ detectPythonCommand: detectPython,
1742
+ packageName: PACKAGE_NAME,
1743
+ });
1744
+ } catch (preflightError) {
1745
+ if (preflightError instanceof InstallPlanPreflightError) {
1746
+ console.error(preflightError.message);
1747
+ process.exit(1);
1748
+ }
1749
+ throw preflightError;
1750
+ }
1751
+ executeInstallPlan(plan);
1752
+ }
1753
+
1754
+ /**
1755
+ * Apply one preflighted installation plan inside a snapshot/restore transaction.
1756
+ *
1757
+ * Captures the prior managed installation before any purge or write, then
1758
+ * restores settings, manifest, managed files, and core.hooksPath on failure.
1759
+ *
1760
+ * @param {ReturnType<typeof buildInstallPlan>} plan
1761
+ * @returns {void}
1762
+ */
1763
+ function executeInstallPlan(plan) {
1764
+ const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
1765
+ const snapshot = capturePriorInstallSnapshot({
1766
+ managedRoot: CLAUDE_HOME,
1767
+ manifestFilePath: MANIFEST_FILE,
1768
+ settingsPath: plan.settingsPath,
1769
+ priorManifestFiles: plan.priorManifest.files,
1770
+ journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
1771
+ });
1772
+
1773
+ try {
1774
+ runWithInstallTransaction({
1775
+ snapshot,
1776
+ faultPhase,
1777
+ runMutations: ({ throwIfFault, syncWrittenPaths }) => {
1778
+ executeInstallPlanMutations(plan, { throwIfFault, syncWrittenPaths });
1779
+ },
1780
+ });
1781
+ } catch (mutationError) {
1782
+ if (mutationError instanceof InstallTransactionFaultError) {
1783
+ console.error(
1784
+ `ERROR: install aborted at ${mutationError.phase}; prior installation restored.`,
1785
+ );
1786
+ process.exit(1);
1787
+ }
1788
+ const message = mutationError instanceof Error
1789
+ ? mutationError.message
1790
+ : String(mutationError);
1791
+ console.error(`ERROR: install failed (${message}); prior installation restored.`);
1792
+ process.exit(1);
1793
+ }
1794
+ }
1795
+
1796
+ /**
1797
+ * Mutation body for one preflighted plan. Invoked inside the transaction wrapper.
1798
+ *
1799
+ * @param {ReturnType<typeof buildInstallPlan>} plan
1800
+ * @param {{
1801
+ * throwIfFault: (phase: string) => void,
1802
+ * syncWrittenPaths: (allPaths: string[]) => void,
1803
+ * }} transactionHelpers
1804
+ * @returns {void}
1805
+ */
1806
+ function executeInstallPlanMutations(plan, transactionHelpers) {
1807
+ const { throwIfFault, syncWrittenPaths } = transactionHelpers;
1808
+ const selectedGroups = plan.selectedGroups;
1809
+ const priorManifestFiles = plan.priorManifest.files;
1810
+ const priorManifestSkills = plan.priorManifest.skills;
1811
+ const isUpdateRefresh = plan.isUpdateRefresh;
1812
+ const pythonCommand = plan.pythonCommand;
1813
+
1814
+ if (plan.shouldPurgeBeforeReinstall) {
1666
1815
  console.log(
1667
1816
  `${PACKAGE_NAME}: --update — removing prior managed files under ${CLAUDE_HOME}, then reinstalling from the package.\n`,
1668
1817
  );
1669
- purgeManagedInstallation({ requireManifest: false });
1818
+ purgeManagedInstallation({
1819
+ requireManifest: false,
1820
+ throwIfFault,
1821
+ });
1670
1822
  } else if (isUpdateRefresh) {
1671
1823
  const installScope = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'full';
1672
1824
  console.log(`${PACKAGE_NAME}: --update — re-running ${installScope} install into ${CLAUDE_HOME}\n`);
1673
1825
  }
1674
1826
  const groupLabel = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'all';
1675
1827
  console.log(`\nInstalling ${PACKAGE_NAME} (${groupLabel})...\n`);
1676
- abortWhenPackageSourceHasConflicts(PACKAGE_ROOT);
1677
- const pythonCommand = detectPython();
1678
- if (!pythonCommand) {
1679
- console.error('ERROR: No usable Python 3 found. Install Python 3.8+ from python.org and ensure py, python3, or python is on PATH. On Windows the Microsoft Store python.exe alias is rejected because it cannot run hooks.');
1680
- process.exit(1);
1681
- }
1682
1828
  console.log(` Python: ${pythonCommand}`);
1683
1829
  mkdirSync(CLAUDE_HOME, { recursive: true });
1684
1830
 
@@ -1779,6 +1925,9 @@ function install(selectedGroups, options = {}) {
1779
1925
  }
1780
1926
  summary.skills = { created: skillsCreated, updated: skillsUpdated, pruned: 0, paths: skillPaths };
1781
1927
  allInstalledFiles.push(...skillPaths);
1928
+ syncWrittenPaths(allInstalledFiles);
1929
+ throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
1930
+ throwIfFault(FAULT_PHASES.BEFORE_DURABLE_PROMOTION);
1782
1931
  const shouldInstallAnyHooks = shouldInstallAllHooks || (allowedHookFiles && allowedHookFiles.size > 0);
1783
1932
  if (shouldInstallAnyHooks) {
1784
1933
  let totalHooksCreated = 0;
@@ -1811,6 +1960,8 @@ function install(selectedGroups, options = {}) {
1811
1960
  console.log(` Hook files: ${totalHooksCreated} new, ${totalHooksUpdated} updated`);
1812
1961
  summary.hookGroups = totalHookGroups;
1813
1962
  console.log(` Hook groups: ${totalHookGroups} merged into settings.json`);
1963
+ syncWrittenPaths(allInstalledFiles);
1964
+ throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
1814
1965
 
1815
1966
  console.warn(
1816
1967
  ' Warning: git hook installation sets core.hooksPath globally — '
@@ -1831,6 +1982,9 @@ function install(selectedGroups, options = {}) {
1831
1982
  console.warn(` Git hooks: ${gitHookInstallationResult.hooksPathConfigurationResult.reason}`);
1832
1983
  }
1833
1984
  console.log(` Git hook shims: ${gitHookInstallationResult.createdShimPaths.length} files (pre-commit, pre-push, post-commit)`);
1985
+ syncWrittenPaths(allInstalledFiles);
1986
+ throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
1987
+ throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
1834
1988
 
1835
1989
  const mypyIniInstallResult = installMypyIniForClaudeHooks({
1836
1990
  homeDirectory: homedir(),
@@ -1847,7 +2001,23 @@ function install(selectedGroups, options = {}) {
1847
2001
  console.warn(` To enable mypy for Claude hooks, add this line under [mypy]:`);
1848
2002
  console.warn(` ${mypyIniInstallResult.expectedLine}`);
1849
2003
  }
2004
+ } else {
2005
+ syncWrittenPaths(allInstalledFiles);
2006
+ throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
2007
+ throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
2008
+ throwIfFault(FAULT_PHASES.AFTER_LINK_PUBLICATION);
2009
+ }
2010
+
2011
+ const permissionMerge = mergeManagedPermissions();
2012
+ summary.managedPermissions = permissionMerge;
2013
+ if (permissionMerge.managedDenyEntries.length > 0) {
2014
+ console.log(
2015
+ ` Permissions: ${permissionMerge.addedCount} managed deny(s) added, `
2016
+ + `${permissionMerge.alreadyPresentCount} already present `
2017
+ + `(${permissionMerge.managedDenyEntries.length} package-owned)`,
2018
+ );
1850
2019
  }
2020
+
1851
2021
  const claudeHubSource = join(PACKAGE_ROOT, 'CLAUDE.md');
1852
2022
  if (existsSync(claudeHubSource)) {
1853
2023
  const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
@@ -1893,7 +2063,16 @@ function install(selectedGroups, options = {}) {
1893
2063
  const manifestFiles = didPruneFinish
1894
2064
  ? manifestFilesWithFailedPrunes(allInstalledFiles, failedPrunePaths)
1895
2065
  : unionOnComparisonKey(priorManifestFiles || [], allInstalledFiles);
1896
- writeManifest(manifestFiles, manifestSkillNames);
2066
+ const managedPermissionDenyEntries = summary.managedPermissions.managedDenyEntries;
2067
+ writeManifest(
2068
+ manifestFiles,
2069
+ manifestSkillNames,
2070
+ managedPermissionDenyEntries.length > 0
2071
+ ? { deny: managedPermissionDenyEntries }
2072
+ : null,
2073
+ );
2074
+ syncWrittenPaths([...allInstalledFiles, MANIFEST_FILE, plan.settingsPath]);
2075
+ throwIfFault(FAULT_PHASES.AFTER_MANIFEST_WRITE);
1897
2076
  console.log(`\nInstalled ${PACKAGE_NAME}:`);
1898
2077
  for (const directory of CONTENT_DIRECTORIES) {
1899
2078
  if (summary[directory]) {
@@ -1985,66 +2164,92 @@ function removeRecordedFile(filePath) {
1985
2164
  }
1986
2165
 
1987
2166
  /**
1988
- * Remove every file the manifest records, then drop the directories the removals
1989
- * emptied.
1990
- *
1991
- * A record is removed when it names a path the installer writes: anything under
1992
- * ~/.claude, plus the `~/.mypy.ini` the install writes in the home directory.
1993
- * Every other record is skipped with a warning and counted, so one malformed
1994
- * entry costs that entry alone: the purge removes every legitimate record, clears
1995
- * the manifest, and leaves the user with a whole uninstall rather than a
1996
- * half-removed install.
2167
+ * Build the uninstall plan for this managed root.
1997
2168
  *
1998
- * Directory cleanup runs after the file loop so a directory holding two recorded
1999
- * files is judged once both are gone. Each walk stops at the managed root the
2000
- * purged file sits under, which keeps ~/.claude itself and every unmanaged
2001
- * sibling directory in place.
2169
+ * @param {boolean} requireManifest
2170
+ * @returns {ReturnType<typeof buildUninstallPlan>}
2171
+ */
2172
+ function resolveUninstallPlan(requireManifest) {
2173
+ return buildUninstallPlan({
2174
+ managedRoot: CLAUDE_HOME,
2175
+ manifestFilePath: MANIFEST_FILE,
2176
+ requireManifest,
2177
+ isRemovableRecord: isRemovableManifestRecord,
2178
+ });
2179
+ }
2180
+
2181
+ /**
2182
+ * Apply one preflighted uninstall plan: remove removable files, prune managed
2183
+ * hooks from settings, unset core.hooksPath when it points here, then drop the
2184
+ * ownership manifest. Fault phases mirror the install transaction names.
2002
2185
  *
2003
- * @param {{requireManifest: boolean}} options `requireManifest` exits when no manifest exists.
2004
- * @returns {number|void} 0 when no manifest exists and none is required.
2186
+ * @param {ReturnType<typeof buildUninstallPlan>} plan
2187
+ * @param {{ throwIfFault?: (phase: string) => void }} [helpers]
2188
+ * @returns {number} Count of files this call removed.
2005
2189
  */
2006
- function purgeManagedInstallation({ requireManifest }) {
2007
- if (!existsSync(MANIFEST_FILE)) {
2008
- if (requireManifest) {
2009
- console.error('No installation manifest found. Nothing to uninstall.');
2010
- process.exit(1);
2011
- }
2012
- return 0;
2013
- }
2014
- const manifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf8'));
2190
+ function executeUninstallPlan(plan, helpers = {}) {
2191
+ const throwIfFault = helpers.throwIfFault || (() => {});
2015
2192
  let removed = 0;
2016
- let skippedUnmanagedCount = 0;
2017
2193
  const managedRootByEmptiedDirectory = new Map();
2018
- for (const filePath of manifest.files) {
2019
- if (!isRemovableManifestRecord(filePath)) {
2020
- console.warn(` Warning: skipping ${filePath} — the manifest record names no path this installer writes`);
2021
- skippedUnmanagedCount++;
2022
- continue;
2194
+ for (const filePath of plan.removableFiles) {
2195
+ if (removeRecordedFile(filePath)) {
2196
+ removed += 1;
2023
2197
  }
2024
- if (removeRecordedFile(filePath)) removed++;
2025
2198
  const managedRoot = owningManagedRoot(filePath);
2026
- if (managedRoot) managedRootByEmptiedDirectory.set(dirname(resolve(filePath)), managedRoot);
2199
+ if (managedRoot) {
2200
+ managedRootByEmptiedDirectory.set(dirname(resolve(filePath)), managedRoot);
2201
+ }
2027
2202
  }
2028
2203
  for (const [emptiedDirectory, managedRoot] of managedRootByEmptiedDirectory) {
2029
2204
  removeEmptiedParentDirectories(emptiedDirectory, managedRoot);
2030
2205
  }
2031
- if (skippedUnmanagedCount > 0) {
2032
- console.warn(` ${skippedUnmanagedCount} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME} and outside ${MYPY_INI_INSTALL_PATH}`);
2206
+ for (const eachSkippedPath of plan.skippedFiles) {
2207
+ console.warn(
2208
+ ` Warning: skipping ${eachSkippedPath} — the manifest record names no path this installer writes`,
2209
+ );
2033
2210
  }
2034
- const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
2035
- if (existsSync(settingsPath)) {
2036
- const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
2211
+ if (plan.skippedFiles.length > 0) {
2212
+ console.warn(
2213
+ ` ${plan.skippedFiles.length} manifest record(s) skipped — each names a path outside ${CLAUDE_HOME} and outside ${MYPY_INI_INSTALL_PATH}`,
2214
+ );
2215
+ }
2216
+ throwIfFault(FAULT_PHASES.AFTER_FILE_STAGING);
2217
+
2218
+ if (existsSync(plan.settingsPath)) {
2219
+ const settings = JSON.parse(readFileSync(plan.settingsPath, 'utf8'));
2220
+ let settingsChanged = false;
2037
2221
  if (settings.hooks) {
2038
2222
  const managedHookRelativePaths = managedHookScriptRelativePathsFromSourceRoots(
2039
- managedPackageSourceRoots()
2223
+ managedPackageSourceRoots(),
2040
2224
  );
2041
2225
  pruneManagedHooksFromSettings(settings, managedHookRelativePaths);
2042
- writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
2226
+ settingsChanged = true;
2043
2227
  console.log(' Hook entries removed from settings.json');
2044
2228
  }
2229
+ const managedDenyFromPlan = plan.managedPermissionDenyEntries.length > 0
2230
+ ? plan.managedPermissionDenyEntries
2231
+ : loadPackageManagedDenyEntries();
2232
+ if (managedDenyFromPlan.length > 0) {
2233
+ const pruneOutcome = pruneManagedPermissionsFromSettings(settings, managedDenyFromPlan);
2234
+ if (pruneOutcome.removedCount > 0) {
2235
+ settingsChanged = true;
2236
+ console.log(
2237
+ ` Permission entries removed from settings.json: ${pruneOutcome.removedCount} managed deny(s)`,
2238
+ );
2239
+ }
2240
+ }
2241
+ if (settingsChanged) {
2242
+ writeFileSync(plan.settingsPath, JSON.stringify(settings, null, 4) + '\n');
2243
+ }
2045
2244
  }
2245
+ throwIfFault(FAULT_PHASES.AFTER_SETTINGS_WRITE);
2246
+
2046
2247
  unsetGlobalGitHooksPathIfOurs();
2047
- unlinkSync(MANIFEST_FILE);
2248
+ throwIfFault(FAULT_PHASES.AFTER_GIT_CONFIG);
2249
+
2250
+ unlinkSync(plan.manifestFilePath);
2251
+ throwIfFault(FAULT_PHASES.AFTER_MANIFEST_WRITE);
2252
+
2048
2253
  for (const directory of MANAGED_TOP_LEVEL_DIRECTORY_NAMES) {
2049
2254
  const dirPath = join(CLAUDE_HOME, directory);
2050
2255
  try {
@@ -2054,11 +2259,85 @@ function purgeManagedInstallation({ requireManifest }) {
2054
2259
  } catch { /* leave non-empty dirs */ }
2055
2260
  }
2056
2261
  console.log(`\nRemoved ${removed} files.\n`);
2262
+ return removed;
2057
2263
  }
2058
2264
 
2265
+ /**
2266
+ * Remove every file the ownership record lists for this managed root.
2267
+ *
2268
+ * Preflights the uninstall plan (settings JSON must be an object when present)
2269
+ * before any removal. When called from `--update` inside an install transaction,
2270
+ * pass that transaction's `throwIfFault` so a fault restores the outer snapshot
2271
+ * without nesting a second journal.
2272
+ *
2273
+ * @param {{
2274
+ * requireManifest: boolean,
2275
+ * throwIfFault?: (phase: string) => void,
2276
+ * }} options
2277
+ * @returns {number|void} 0 when no manifest exists and none is required.
2278
+ */
2279
+ function purgeManagedInstallation({ requireManifest, throwIfFault }) {
2280
+ const plan = resolveUninstallPlan(requireManifest);
2281
+ if (plan.isNoOp) {
2282
+ return 0;
2283
+ }
2284
+ return executeUninstallPlan(plan, { throwIfFault });
2285
+ }
2286
+
2287
+ /**
2288
+ * Uninstall the selected managed root inside a snapshot/restore transaction.
2289
+ *
2290
+ * Captures files, settings, manifest, and core.hooksPath before removal so any
2291
+ * later fault restores a retryable installation from the ownership record.
2292
+ *
2293
+ * @returns {void}
2294
+ */
2059
2295
  function uninstall() {
2060
2296
  console.log(`\nUninstalling ${PACKAGE_NAME}...\n`);
2061
- purgeManagedInstallation({ requireManifest: true });
2297
+ let plan;
2298
+ try {
2299
+ plan = resolveUninstallPlan(true);
2300
+ } catch (preflightError) {
2301
+ if (preflightError instanceof InstallPlanPreflightError) {
2302
+ console.error(preflightError.message);
2303
+ process.exit(1);
2304
+ }
2305
+ throw preflightError;
2306
+ }
2307
+ if (plan.isNoOp) {
2308
+ return;
2309
+ }
2310
+
2311
+ const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
2312
+ const snapshot = capturePriorInstallSnapshot({
2313
+ managedRoot: CLAUDE_HOME,
2314
+ manifestFilePath: MANIFEST_FILE,
2315
+ settingsPath: plan.settingsPath,
2316
+ priorManifestFiles: plan.removableFiles,
2317
+ journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
2318
+ });
2319
+
2320
+ try {
2321
+ runWithInstallTransaction({
2322
+ snapshot,
2323
+ faultPhase,
2324
+ runMutations: ({ throwIfFault }) => {
2325
+ executeUninstallPlan(plan, { throwIfFault });
2326
+ },
2327
+ });
2328
+ } catch (mutationError) {
2329
+ if (mutationError instanceof InstallTransactionFaultError) {
2330
+ console.error(
2331
+ `ERROR: uninstall aborted at ${mutationError.phase}; prior installation restored.`,
2332
+ );
2333
+ process.exit(1);
2334
+ }
2335
+ const message = mutationError instanceof Error
2336
+ ? mutationError.message
2337
+ : String(mutationError);
2338
+ console.error(`ERROR: uninstall failed (${message}); prior installation restored.`);
2339
+ process.exit(1);
2340
+ }
2062
2341
  }
2063
2342
 
2064
2343
  /**
@@ -2078,10 +2357,13 @@ function printHelp() {
2078
2357
  ${PACKAGE_NAME} - Claude Code development standards installer
2079
2358
 
2080
2359
  Usage:
2081
- npx ${PACKAGE_NAME} Install everything
2360
+ npx ${PACKAGE_NAME} Install everything into the main default root
2082
2361
  npx ${PACKAGE_NAME} --update Full install: remove prior manifest-tracked files first, then reinstall
2083
2362
  npx ${PACKAGE_NAME} --only X Install specific groups
2084
- npx ${PACKAGE_NAME} --uninstall Remove installed files
2363
+ npx ${PACKAGE_NAME} --target DIR Install into DIR instead of ~/.claude (overrides CLAUDE_CONFIG_DIR)
2364
+ npx ${PACKAGE_NAME} --profile ID Install into one named profile root (under the profiles root)
2365
+ npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
2366
+ npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
2085
2367
  npx ${PACKAGE_NAME} --help Show this help
2086
2368
 
2087
2369
  Groups:
@@ -2090,8 +2372,16 @@ ${groupLines}
2090
2372
  Examples:
2091
2373
  npx ${PACKAGE_NAME} --only core
2092
2374
  npx ${PACKAGE_NAME} --only core,journal
2375
+ npx ${PACKAGE_NAME} --profile editor --only core
2376
+ npx ${PACKAGE_NAME} --profiles editor,mel --only core
2377
+
2378
+ Install location: ~/.claude/ by default; CLAUDE_CONFIG_DIR or --target selects another managed root.
2379
+ Named profiles resolve under LLM_SETTINGS_PROFILES_ROOT or ~/.claude-profiles/<directoryName>.
2093
2380
 
2094
- Install location: ~/.claude/
2381
+ Root precedence: --target > CLAUDE_CONFIG_DIR > ~/.claude
2382
+ Profile selection (--profile/--profiles) is mutually exclusive with --target.
2383
+ Target selection finishes before any mutation. Each target writes its own manifest
2384
+ with package, version, targetIdentity, managedRoot, files, and skills.
2095
2385
 
2096
2386
  If ~/.claude/CLAUDE.md already exists and differs from the package copy, the installer
2097
2387
  writes the previous contents to ~/.claude/backups/CLAUDE.md.<timestamp>.bak first.
@@ -2130,30 +2420,186 @@ function realPathOrSelf(filesystemPath) {
2130
2420
  }
2131
2421
  }
2132
2422
 
2423
+ /**
2424
+ * Load profile id → directoryName from the A1 launcher contract when present.
2425
+ * Falls back to identity mapping when the file is missing, unreadable, or empty.
2426
+ *
2427
+ * @returns {Record<string, string>}
2428
+ */
2429
+ function loadDirectoryNameByProfileId() {
2430
+ const fallbackDirectoryNameByProfileId = {
2431
+ main: 'main',
2432
+ editor: 'editor',
2433
+ mel: 'mel',
2434
+ ev: 'ev',
2435
+ master: 'master',
2436
+ kimi: 'kimi',
2437
+ };
2438
+ const manifestPath = join(
2439
+ PACKAGE_ROOT,
2440
+ 'scripts',
2441
+ 'profile-isolation-launchers',
2442
+ 'config',
2443
+ 'profiles.manifest.json',
2444
+ );
2445
+ if (!existsSync(manifestPath)) {
2446
+ return fallbackDirectoryNameByProfileId;
2447
+ }
2448
+ try {
2449
+ const document = JSON.parse(readFileSync(manifestPath, 'utf8'));
2450
+ /** @type {Record<string, string>} */
2451
+ const directoryNameByProfileId = {};
2452
+ const profiles = document && typeof document === 'object' ? document.profiles : null;
2453
+ if (profiles && typeof profiles === 'object') {
2454
+ for (const [eachProfileId, eachProfile] of Object.entries(profiles)) {
2455
+ if (eachProfile && typeof eachProfile === 'object' && typeof eachProfile.directoryName === 'string') {
2456
+ directoryNameByProfileId[eachProfileId] = eachProfile.directoryName;
2457
+ }
2458
+ }
2459
+ }
2460
+ if (Object.keys(directoryNameByProfileId).length === 0) {
2461
+ return fallbackDirectoryNameByProfileId;
2462
+ }
2463
+ return directoryNameByProfileId;
2464
+ } catch {
2465
+ return fallbackDirectoryNameByProfileId;
2466
+ }
2467
+ }
2468
+
2469
+ /**
2470
+ * Run one install hop for a resolved target. Multi-target parents re-exec this
2471
+ * module with --target and --target-identity so CLAUDE_HOME is correct at load.
2472
+ *
2473
+ * Abort-first: the first non-zero child status ends the loop.
2474
+ *
2475
+ * @param {import('./select-install-targets.mjs').ResolvedInstallTarget[]} allTargets
2476
+ * @param {string[]} childArgv
2477
+ * @returns {void}
2478
+ */
2479
+ function runInstallForAllTargets(allTargets, childArgv) {
2480
+ if (allTargets.length === 1) {
2481
+ const onlyTarget = allTargets[0];
2482
+ if (onlyTarget.managedRoot !== CLAUDE_HOME || onlyTarget.targetIdentity !== INSTALL_TARGET_IDENTITY) {
2483
+ const childStatus = spawnInstallChild(onlyTarget, childArgv);
2484
+ process.exit(childStatus);
2485
+ }
2486
+ return;
2487
+ }
2488
+ for (const eachTarget of allTargets) {
2489
+ console.log(
2490
+ `\n${PACKAGE_NAME}: target ${eachTarget.targetIdentity} → ${eachTarget.managedRoot}`,
2491
+ );
2492
+ const childStatus = spawnInstallChild(eachTarget, childArgv);
2493
+ if (childStatus !== 0) {
2494
+ console.error(
2495
+ `ERROR: install failed for target ${eachTarget.targetIdentity} (exit ${childStatus})`,
2496
+ );
2497
+ process.exit(childStatus === null ? 1 : childStatus);
2498
+ }
2499
+ }
2500
+ process.exit(0);
2501
+ }
2502
+
2503
+ /**
2504
+ * @param {import('./select-install-targets.mjs').ResolvedInstallTarget} target
2505
+ * @param {string[]} childArgv
2506
+ * @returns {number | null}
2507
+ */
2508
+ function spawnInstallChild(target, childArgv) {
2509
+ const result = spawnSync(
2510
+ process.execPath,
2511
+ [
2512
+ fileURLToPath(import.meta.url),
2513
+ '--target',
2514
+ target.managedRoot,
2515
+ TARGET_IDENTITY_FLAG,
2516
+ target.targetIdentity,
2517
+ ...childArgv,
2518
+ ],
2519
+ {
2520
+ stdio: 'inherit',
2521
+ env: process.env,
2522
+ },
2523
+ );
2524
+ if (result.error) {
2525
+ console.error(`ERROR: failed to spawn install child: ${result.error.message}`);
2526
+ return 1;
2527
+ }
2528
+ return result.status === null ? 1 : result.status;
2529
+ }
2530
+
2133
2531
  if (invokedAsEntryPoint(import.meta.url, process.argv[1])) {
2134
2532
  const rawArgs = process.argv.slice(2);
2135
- const args = rawArgs.filter((flag) => flag !== '--update');
2136
- const isUpdateRefresh = rawArgs.includes('--update');
2137
- if (args.includes('--help') || args.includes('-h')) {
2533
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
2138
2534
  printHelp();
2139
- } else if (args.includes('--uninstall')) {
2140
- uninstall();
2141
2535
  } else {
2142
- const onlyIndex = args.indexOf('--only');
2143
- let selectedGroups = null;
2144
- if (onlyIndex !== -1) {
2145
- const onlyValue = args[onlyIndex + 1];
2146
- if (!onlyValue || onlyValue.startsWith('--')) {
2147
- console.error(`ERROR: --only requires a comma-separated list of groups.\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
2148
- process.exit(1);
2536
+ let targetSelection;
2537
+ try {
2538
+ targetSelection = parseInstallTargetSelectionFromArgv(rawArgs);
2539
+ } catch (selectionError) {
2540
+ const message = selectionError instanceof Error
2541
+ ? selectionError.message
2542
+ : String(selectionError);
2543
+ console.error(`ERROR: ${message}`);
2544
+ process.exit(1);
2545
+ }
2546
+
2547
+ const homeDirectory = homedir();
2548
+ let allTargets;
2549
+ try {
2550
+ allTargets = resolveInstallTargets(targetSelection, {
2551
+ homeDirectory,
2552
+ environment: process.env,
2553
+ profilesRoot: resolveProfilesRootDirectory({
2554
+ homeDirectory,
2555
+ environment: process.env,
2556
+ }),
2557
+ directoryNameByProfileId: loadDirectoryNameByProfileId(),
2558
+ });
2559
+ } catch (resolveError) {
2560
+ const message = resolveError instanceof Error
2561
+ ? resolveError.message
2562
+ : String(resolveError);
2563
+ console.error(`ERROR: ${message}`);
2564
+ process.exit(1);
2565
+ }
2566
+
2567
+ const childArgv = stripTargetSelectionFlagsFromArgv(rawArgs);
2568
+ const args = childArgv.filter((flag) => flag !== '--update');
2569
+ const isUpdateRefresh = childArgv.includes('--update');
2570
+
2571
+ const needsChildHop = allTargets.length > 1
2572
+ || allTargets[0].managedRoot !== CLAUDE_HOME
2573
+ || (
2574
+ targetSelection.mode === 'profiles'
2575
+ && allTargets[0].targetIdentity !== INSTALL_TARGET_IDENTITY
2576
+ );
2577
+
2578
+ if (needsChildHop && !args.includes('--uninstall')) {
2579
+ runInstallForAllTargets(allTargets, childArgv);
2580
+ } else if (args.includes('--uninstall')) {
2581
+ if (needsChildHop) {
2582
+ runInstallForAllTargets(allTargets, childArgv);
2583
+ } else {
2584
+ uninstall();
2149
2585
  }
2150
- selectedGroups = onlyValue.split(',').map(name => name.trim());
2151
- const invalidGroups = selectedGroups.filter(name => !INSTALL_GROUPS[name]);
2152
- if (invalidGroups.length > 0) {
2153
- console.error(`ERROR: Unknown group(s): ${invalidGroups.join(', ')}\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
2154
- process.exit(1);
2586
+ } else {
2587
+ const onlyIndex = args.indexOf('--only');
2588
+ let selectedGroups = null;
2589
+ if (onlyIndex !== -1) {
2590
+ const onlyValue = args[onlyIndex + 1];
2591
+ if (!onlyValue || onlyValue.startsWith('--')) {
2592
+ console.error(`ERROR: --only requires a comma-separated list of groups.\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
2593
+ process.exit(1);
2594
+ }
2595
+ selectedGroups = onlyValue.split(',').map(name => name.trim());
2596
+ const invalidGroups = selectedGroups.filter(name => !INSTALL_GROUPS[name]);
2597
+ if (invalidGroups.length > 0) {
2598
+ console.error(`ERROR: Unknown group(s): ${invalidGroups.join(', ')}\nAvailable groups: ${Object.keys(INSTALL_GROUPS).join(', ')}`);
2599
+ process.exit(1);
2600
+ }
2155
2601
  }
2602
+ install(selectedGroups, { isUpdateRefresh });
2156
2603
  }
2157
- install(selectedGroups, { isUpdateRefresh });
2158
2604
  }
2159
2605
  }