claude-dev-env 2.9.0 → 2.11.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 (318) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/advisor/CLAUDE.md +3 -2
  3. package/_shared/advisor/advisor-protocol.md +74 -108
  4. package/_shared/advisor/reference/advisor-block.md +37 -0
  5. package/_shared/advisor/reference/cli-chain.md +45 -0
  6. package/_shared/advisor/reference/consult-format.md +41 -0
  7. package/_shared/advisor/reference/lifecycle.md +21 -0
  8. package/_shared/advisor/reference/sol-rung.md +31 -0
  9. package/_shared/advisor/reference/spawn-walk-log.md +31 -0
  10. package/_shared/advisor/reference/third-party-bind.md +30 -0
  11. package/_shared/advisor/reference/warm-up.md +33 -0
  12. package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
  13. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
  17. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
  18. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
  19. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
  20. package/_shared/advisor/scripts/tier_model_ids.py +24 -0
  21. package/_shared/pr-loop/CLAUDE.md +1 -1
  22. package/_shared/pr-loop/audit-contract.md +17 -6
  23. package/_shared/pr-loop/audit-reply-template.md +4 -4
  24. package/_shared/pr-loop/code-rules-gate.md +3 -5
  25. package/_shared/pr-loop/fix-protocol.md +2 -3
  26. package/_shared/pr-loop/gh-payloads.md +1 -1
  27. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  28. package/_shared/pr-loop/scripts/README.md +1 -1
  29. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  32. package/_shared/pr-loop/scripts/preflight.py +9 -4
  33. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  34. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  35. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  36. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  37. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  38. package/_shared/pr-loop/state-schema.md +5 -14
  39. package/agents/CLAUDE.md +2 -2
  40. package/agents/clean-coder.md +58 -548
  41. package/agents/code-quality-agent.md +10 -2
  42. package/agents/code-verifier.md +1 -1
  43. package/agents/test_agent_frontmatter.py +32 -40
  44. package/audit-rubrics/CLAUDE.md +2 -1
  45. package/audit-rubrics/audit-categories.json +704 -0
  46. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  47. package/bin/CLAUDE.md +16 -5
  48. package/bin/ever-shipped-skills.mjs +2 -0
  49. package/bin/install-plan.mjs +402 -0
  50. package/bin/install-transaction.mjs +455 -0
  51. package/bin/install.mjs +593 -147
  52. package/bin/install.plan.test.mjs +194 -0
  53. package/bin/install.profile-root.test.mjs +154 -0
  54. package/bin/install.profiles.test.mjs +253 -0
  55. package/bin/install.settings-defaults.test.mjs +200 -0
  56. package/bin/install.transaction.test.mjs +400 -0
  57. package/bin/install.uninstall-transaction.test.mjs +418 -0
  58. package/bin/merge_managed_permissions.mjs +130 -0
  59. package/bin/resolve-install-root.mjs +181 -0
  60. package/bin/select-install-targets.mjs +401 -0
  61. package/commands/CLAUDE.md +0 -2
  62. package/docs/references/CLAUDE.md +3 -1
  63. package/docs/references/advisor-tool.md +25 -7
  64. package/docs/references/prose-style-enforcement.md +25 -0
  65. package/docs/references/team-advisor-skill.md +3 -3
  66. package/docs/references/weak-executor-advisor.md +91 -0
  67. package/hooks/blocking/CLAUDE.md +6 -6
  68. package/hooks/blocking/_path_setup.py +9 -5
  69. package/hooks/blocking/code_rules_docstrings.py +124 -30
  70. package/hooks/blocking/code_rules_enforcer.py +161 -16
  71. package/hooks/blocking/code_rules_shared.py +40 -23
  72. package/hooks/blocking/config/CLAUDE.md +3 -5
  73. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  74. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  75. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  76. package/hooks/blocking/hedging_language_blocker.py +103 -20
  77. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  78. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  79. package/hooks/blocking/plain_language_blocker.py +139 -20
  80. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  81. package/hooks/blocking/state_description_blocker.py +7 -1
  82. package/hooks/blocking/tdd_enforcer.py +8 -0
  83. package/hooks/blocking/test__path_setup.py +28 -0
  84. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  86. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  87. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  88. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  89. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  90. package/hooks/blocking/test_fable_spawn_gate.py +18 -11
  91. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  92. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  93. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  94. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  95. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  96. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  97. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  98. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  99. package/hooks/blocking/test_state_description_blocker.py +45 -2
  100. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  101. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  102. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  103. package/hooks/hooks.json +15 -0
  104. package/hooks/hooks_constants/CLAUDE.md +14 -3
  105. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  106. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  107. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  108. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  109. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  110. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  111. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  112. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  113. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  114. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  115. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  116. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  117. package/hooks/hooks_constants/session_start_injector.py +163 -0
  118. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  119. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  120. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  121. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  122. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  123. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  124. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  125. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  126. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  127. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  128. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  129. package/hooks/observability/CLAUDE.md +2 -0
  130. package/hooks/observability/prose_matcher_advisory.py +237 -0
  131. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  132. package/hooks/session/CLAUDE.md +9 -1
  133. package/hooks/session/_path_setup.py +13 -0
  134. package/hooks/session/issue_tracker_session_starter.py +135 -0
  135. package/hooks/session/orchestrator_auto_starter.py +100 -0
  136. package/hooks/session/test__path_setup.py +28 -0
  137. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  138. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  139. package/hooks/session/test_working_style_prompt.py +47 -0
  140. package/hooks/session/untracked_repo_detector.py +1 -24
  141. package/hooks/session/working_style_prompt.py +36 -0
  142. package/hooks/validators/_path_setup.py +19 -0
  143. package/hooks/validators/run_all_validators.py +8 -13
  144. package/installable-surfaces.manifest.json +21 -0
  145. package/output-styles/CLAUDE.md +1 -3
  146. package/package.json +4 -2
  147. package/rules/CLAUDE.md +1 -0
  148. package/rules/durable-post-artifacts.md +2 -2
  149. package/rules/eli11-replies.md +6 -1
  150. package/rules/hedging-claims.md +4 -2
  151. package/rules/long-horizon-autonomy.md +3 -1
  152. package/rules/opus5-communication-contract.md +45 -0
  153. package/rules/plain-language.md +2 -2
  154. package/rules/research-mode.md +1 -1
  155. package/scripts/CLAUDE.md +11 -0
  156. package/scripts/Sync-RepoMain.ps1 +215 -0
  157. package/scripts/active_capability_references.py +218 -0
  158. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  159. package/scripts/claude_chain_runner.py +394 -6
  160. package/scripts/claude_chain_usage.py +1 -1
  161. package/scripts/codex_compat_materializer.py +105 -85
  162. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  163. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  164. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  165. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  166. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  167. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  168. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  169. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  170. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  171. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  172. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  173. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  174. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  175. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  176. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  177. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  178. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  179. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  180. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  181. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  182. package/scripts/spawn_grok_batch.py +3 -0
  183. package/scripts/test_active_capability_references.py +108 -0
  184. package/scripts/test_claude_chain_runner.py +414 -82
  185. package/scripts/test_claude_chain_usage.py +12 -12
  186. package/scripts/test_resolve_worker_spawn.py +2 -2
  187. package/scripts/test_verify_installable_package.py +208 -0
  188. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  189. package/scripts/verify_installable_package.py +612 -0
  190. package/settings.json +10 -0
  191. package/skills/CLAUDE.md +2 -0
  192. package/skills/_shared/advisor/CLAUDE.md +1 -1
  193. package/skills/_shared/advisor/scripts/README.md +2 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  195. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  196. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  199. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  200. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  201. package/skills/autoconverge/reference/convergence.md +2 -1
  202. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  203. package/skills/beat-sheet/SKILL.md +54 -0
  204. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  205. package/skills/bugteam/CONSTRAINTS.md +4 -4
  206. package/skills/bugteam/EXAMPLES.md +1 -1
  207. package/skills/bugteam/reference/README.md +1 -1
  208. package/skills/e-code-review/SKILL.md +26 -5
  209. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  210. package/skills/e-code-review/reference/medium.md +15 -4
  211. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  212. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  213. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  214. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  215. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  216. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  217. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  218. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  219. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  220. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  221. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  222. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  223. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  224. package/skills/grokify/SKILL.md +1 -1
  225. package/skills/grokify/templates/handoff-template.md +2 -2
  226. package/skills/orchestrator/SKILL.md +5 -4
  227. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  228. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  229. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  230. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  231. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  232. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  233. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  234. package/skills/pr-converge/scripts/conftest.py +35 -0
  235. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  237. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  238. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  239. package/skills/rebase/SKILL.md +15 -3
  240. package/skills/reviewer-gates/SKILL.md +2 -2
  241. package/skills/show/SKILL.md +51 -0
  242. package/skills/show/references/accessibility.md +7 -0
  243. package/skills/show/references/art.md +3 -0
  244. package/skills/show/references/charts.md +3 -0
  245. package/skills/show/references/core-design.md +14 -0
  246. package/skills/show/references/erds.md +3 -0
  247. package/skills/show/references/flowcharts.md +3 -0
  248. package/skills/show/references/host-and-html.md +3 -0
  249. package/skills/show/references/illustrative-diagrams.md +10 -0
  250. package/skills/show/references/interaction.md +3 -0
  251. package/skills/show/references/mockups.md +3 -0
  252. package/skills/show/references/quality-gates.md +7 -0
  253. package/skills/show/references/structural-diagrams.md +3 -0
  254. package/skills/show/references/subject-inventory.md +21 -0
  255. package/skills/show/references/svg-contract.md +22 -0
  256. package/skills/show/routing.yaml +30 -0
  257. package/skills/show/samples/pr1262-v2.svg +222 -0
  258. package/skills/show/scripts/README.md +6 -0
  259. package/skills/show/scripts/validate-artifact.py +91 -0
  260. package/skills/show/scripts/validate-package.py +18 -0
  261. package/skills/show/templates/html-widget.html +4 -0
  262. package/skills/show/templates/svg-base.svg +19 -0
  263. package/skills/show/tests/fixtures/css-var.svg +6 -0
  264. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  265. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  266. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  267. package/skills/show/tests/fixtures/invalid.svg +1 -0
  268. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  269. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  270. package/skills/show/tests/fixtures/valid.html +1 -0
  271. package/skills/show/tests/test_validate-artifact.py +74 -0
  272. package/skills/show/tests/test_validators.py +59 -0
  273. package/skills/show/workflows/create-visual.md +13 -0
  274. package/skills/show/workflows/review-visual.md +20 -0
  275. package/skills/split-pr/SKILL.md +85 -0
  276. package/skills/split-pr/reference/path-layers.md +16 -0
  277. package/skills/split-pr/reference/proposal-format.md +15 -0
  278. package/skills/split-pr/reference/split-further-loop.md +10 -0
  279. package/skills/split-pr/reference/splitting-principles.md +26 -0
  280. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  281. package/skills/split-pr/scripts/categorize_files.py +106 -0
  282. package/skills/split-pr/scripts/config/__init__.py +1 -0
  283. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  284. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  285. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  286. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  287. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  288. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  289. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  290. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  291. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  292. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  293. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  294. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  295. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  296. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  297. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  298. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  299. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  300. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  301. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  302. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  303. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  304. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  305. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  306. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  307. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  308. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  309. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  310. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  311. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  312. package/skills/split-pr/scripts/verify_plan.py +139 -0
  313. package/skills/team-advisor/SKILL.md +7 -4
  314. package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
  315. package/system-prompts/software-engineer.xml +11 -2
  316. package/commands/initialize.md +0 -90
  317. package/commands/stubcheck.md +0 -88
  318. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Pure CLI vs Desktop embedded-version compatibility classifier.
3
+ *
4
+ * The classifier takes probe results only. Callers own process spawn, timeouts,
5
+ * and binary discovery. Fail closed on unreadable or missing inputs.
6
+ */
7
+
8
+ export const COMPATIBILITY_POLICY_VERSION = 1;
9
+
10
+ /** @typedef {'pass' | 'warn' | 'block'} CompatibilityAction */
11
+ /** @typedef {
12
+ * | 'equal'
13
+ * | 'patch-drift'
14
+ * | 'minor-drift'
15
+ * | 'major-drift'
16
+ * | 'missing-binary'
17
+ * | 'unreadable'
18
+ * | 'process-error'
19
+ * | 'non-semver'
20
+ * } CompatibilityClass */
21
+
22
+ /**
23
+ * Action table for every compatibility class. Missing / unreadable / process
24
+ * errors and major/minor drift block; patch drift warns; equality passes.
25
+ */
26
+ export const COMPATIBILITY_ACTION_BY_CLASS = Object.freeze({
27
+ equal: 'pass',
28
+ 'patch-drift': 'warn',
29
+ 'minor-drift': 'block',
30
+ 'major-drift': 'block',
31
+ 'missing-binary': 'block',
32
+ unreadable: 'block',
33
+ 'process-error': 'block',
34
+ 'non-semver': 'block',
35
+ });
36
+
37
+ /**
38
+ * @typedef {{
39
+ * path: string | null,
40
+ * versionText: string | null,
41
+ * errorCode?: 'missing' | 'unreadable' | 'process-error' | null,
42
+ * errorMessage?: string | null,
43
+ * }} VersionProbeResult
44
+ */
45
+
46
+ /**
47
+ * @typedef {{
48
+ * cliPath: string | null,
49
+ * desktopPath: string | null,
50
+ * cliVersion: string | null,
51
+ * desktopVersion: string | null,
52
+ * policyVersion: number,
53
+ * class: CompatibilityClass,
54
+ * action: CompatibilityAction,
55
+ * message: string,
56
+ * }} CompatibilityResult
57
+ */
58
+
59
+ /**
60
+ * Classify CLI and Desktop embedded version compatibility from probe results.
61
+ *
62
+ * ::
63
+ *
64
+ * classifyVersionCompatibility({
65
+ * cli: { path: 'cli', versionText: '2.1.220' },
66
+ * desktop: { path: 'desktop', versionText: '2.1.220' },
67
+ * })
68
+ * // class: equal, action: pass
69
+ *
70
+ * classifyVersionCompatibility({
71
+ * cli: { path: 'cli', versionText: '2.1.220' },
72
+ * desktop: { path: 'desktop', versionText: '2.1.219' },
73
+ * })
74
+ * // class: patch-drift, action: warn
75
+ *
76
+ * @param {{
77
+ * cli: VersionProbeResult,
78
+ * desktop: VersionProbeResult,
79
+ * }} parameters
80
+ * @returns {CompatibilityResult}
81
+ */
82
+ export function classifyVersionCompatibility(parameters) {
83
+ const cliProbe = normalizeProbe(parameters.cli);
84
+ const desktopProbe = normalizeProbe(parameters.desktop);
85
+
86
+ const base = {
87
+ cliPath: cliProbe.path,
88
+ desktopPath: desktopProbe.path,
89
+ cliVersion: extractVersionLabel(cliProbe.versionText),
90
+ desktopVersion: extractVersionLabel(desktopProbe.versionText),
91
+ policyVersion: COMPATIBILITY_POLICY_VERSION,
92
+ };
93
+
94
+ const probeFailure = firstProbeFailure(cliProbe, desktopProbe);
95
+ if (probeFailure) {
96
+ return finalizeResult({
97
+ ...base,
98
+ class: probeFailure.className,
99
+ message: probeFailure.message,
100
+ });
101
+ }
102
+
103
+ const cliVersion = parseSemver(cliProbe.versionText);
104
+ const desktopVersion = parseSemver(desktopProbe.versionText);
105
+ if (!cliVersion || !desktopVersion) {
106
+ return finalizeResult({
107
+ ...base,
108
+ class: 'non-semver',
109
+ message: formatNonSemverMessage(cliProbe.versionText, desktopProbe.versionText),
110
+ });
111
+ }
112
+
113
+ if (
114
+ cliVersion.major === desktopVersion.major
115
+ && cliVersion.minor === desktopVersion.minor
116
+ && cliVersion.patch === desktopVersion.patch
117
+ ) {
118
+ return finalizeResult({
119
+ ...base,
120
+ class: 'equal',
121
+ message: `CLI and Desktop versions match (${formatSemver(cliVersion)})`,
122
+ });
123
+ }
124
+
125
+ if (cliVersion.major !== desktopVersion.major) {
126
+ return finalizeResult({
127
+ ...base,
128
+ class: 'major-drift',
129
+ message: formatDriftMessage('major', cliVersion, desktopVersion),
130
+ });
131
+ }
132
+
133
+ if (cliVersion.minor !== desktopVersion.minor) {
134
+ return finalizeResult({
135
+ ...base,
136
+ class: 'minor-drift',
137
+ message: formatDriftMessage('minor', cliVersion, desktopVersion),
138
+ });
139
+ }
140
+
141
+ return finalizeResult({
142
+ ...base,
143
+ class: 'patch-drift',
144
+ message: formatDriftMessage('patch', cliVersion, desktopVersion),
145
+ });
146
+ }
147
+
148
+ /**
149
+ * True when the classified action blocks launch before profile-state mutation.
150
+ *
151
+ * @param {CompatibilityResult} result
152
+ * @returns {boolean}
153
+ */
154
+ export function shouldBlockLaunch(result) {
155
+ return result.action === 'block';
156
+ }
157
+
158
+ /**
159
+ * Parse a version string into major.minor.patch when the leading triple is semver-shaped.
160
+ *
161
+ * @param {string | null | undefined} versionText
162
+ * @returns {{major: number, minor: number, patch: number} | null}
163
+ */
164
+ export function parseSemver(versionText) {
165
+ if (typeof versionText !== 'string') {
166
+ return null;
167
+ }
168
+ const trimmed = versionText.trim();
169
+ if (!trimmed) {
170
+ return null;
171
+ }
172
+ const match = trimmed.match(/(\d+)\.(\d+)\.(\d+)/);
173
+ if (!match) {
174
+ return null;
175
+ }
176
+ return {
177
+ major: Number(match[1]),
178
+ minor: Number(match[2]),
179
+ patch: Number(match[3]),
180
+ };
181
+ }
182
+
183
+ /**
184
+ * @param {VersionProbeResult | null | undefined} probe
185
+ * @returns {VersionProbeResult}
186
+ */
187
+ function normalizeProbe(probe) {
188
+ if (!probe || typeof probe !== 'object') {
189
+ return {
190
+ path: null,
191
+ versionText: null,
192
+ errorCode: 'missing',
193
+ errorMessage: 'probe missing',
194
+ };
195
+ }
196
+ return {
197
+ path: typeof probe.path === 'string' ? probe.path : null,
198
+ versionText: typeof probe.versionText === 'string' ? probe.versionText : null,
199
+ errorCode: probe.errorCode ?? null,
200
+ errorMessage: probe.errorMessage ?? null,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * @param {VersionProbeResult} cliProbe
206
+ * @param {VersionProbeResult} desktopProbe
207
+ * @returns {{className: CompatibilityClass, message: string} | null}
208
+ */
209
+ function firstProbeFailure(cliProbe, desktopProbe) {
210
+ for (const [eachLabel, eachProbe] of [
211
+ ['CLI', cliProbe],
212
+ ['Desktop', desktopProbe],
213
+ ]) {
214
+ if (eachProbe.errorCode === 'missing' || !eachProbe.path) {
215
+ return {
216
+ className: 'missing-binary',
217
+ message: `${eachLabel} binary path is missing`,
218
+ };
219
+ }
220
+ if (eachProbe.errorCode === 'process-error') {
221
+ return {
222
+ className: 'process-error',
223
+ message: `${eachLabel} version probe failed: ${eachProbe.errorMessage || 'process error'}`,
224
+ };
225
+ }
226
+ if (eachProbe.errorCode === 'unreadable' || eachProbe.versionText === null) {
227
+ return {
228
+ className: 'unreadable',
229
+ message: `${eachLabel} version output is unreadable`,
230
+ };
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * @param {string | null} versionText
238
+ * @returns {string | null}
239
+ */
240
+ function extractVersionLabel(versionText) {
241
+ if (typeof versionText !== 'string') {
242
+ return null;
243
+ }
244
+ const trimmed = versionText.trim();
245
+ return trimmed || null;
246
+ }
247
+
248
+ /**
249
+ * @param {{
250
+ * cliPath: string | null,
251
+ * desktopPath: string | null,
252
+ * cliVersion: string | null,
253
+ * desktopVersion: string | null,
254
+ * policyVersion: number,
255
+ * class: CompatibilityClass,
256
+ * message: string,
257
+ * }} partial
258
+ * @returns {CompatibilityResult}
259
+ */
260
+ function finalizeResult(partial) {
261
+ const action = COMPATIBILITY_ACTION_BY_CLASS[partial.class];
262
+ return {
263
+ cliPath: partial.cliPath,
264
+ desktopPath: partial.desktopPath,
265
+ cliVersion: partial.cliVersion,
266
+ desktopVersion: partial.desktopVersion,
267
+ policyVersion: partial.policyVersion,
268
+ class: partial.class,
269
+ action,
270
+ message: partial.message,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * @param {'major' | 'minor' | 'patch'} axis
276
+ * @param {{major: number, minor: number, patch: number}} cliVersion
277
+ * @param {{major: number, minor: number, patch: number}} desktopVersion
278
+ * @returns {string}
279
+ */
280
+ function formatDriftMessage(axis, cliVersion, desktopVersion) {
281
+ return `${axis} drift: CLI ${formatSemver(cliVersion)} vs Desktop ${formatSemver(desktopVersion)}`;
282
+ }
283
+
284
+ /**
285
+ * @param {{major: number, minor: number, patch: number}} version
286
+ * @returns {string}
287
+ */
288
+ function formatSemver(version) {
289
+ return `${version.major}.${version.minor}.${version.patch}`;
290
+ }
291
+
292
+ /**
293
+ * @param {string | null} cliText
294
+ * @param {string | null} desktopText
295
+ * @returns {string}
296
+ */
297
+ function formatNonSemverMessage(cliText, desktopText) {
298
+ return `non-semver version text: CLI=${JSON.stringify(cliText)} Desktop=${JSON.stringify(desktopText)}`;
299
+ }
@@ -0,0 +1,127 @@
1
+ # Read-only inventory of managed Claude profile shortcuts.
2
+ # Does not mutate Desktop, Start Menu, or taskbar state.
3
+
4
+ [CmdletBinding()]
5
+ param(
6
+ [string]$ManifestPath = (Join-Path $PSScriptRoot 'shortcut-manifest.json'),
7
+ [string]$DesktopPath = [Environment]::GetFolderPath('Desktop'),
8
+ [string]$StartMenuPath = [Environment]::GetFolderPath('StartMenu'),
9
+ [string]$OutputPath
10
+ )
11
+
12
+ Set-StrictMode -Version Latest
13
+ $ErrorActionPreference = 'Stop'
14
+
15
+ if (-not (Test-Path -LiteralPath $ManifestPath)) {
16
+ throw "shortcut manifest missing: $ManifestPath"
17
+ }
18
+
19
+ $manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
20
+ if ($manifest.schemaVersion -ne 1) {
21
+ throw "unsupported shortcut manifest schemaVersion: $($manifest.schemaVersion)"
22
+ }
23
+ if ($null -eq $manifest.allManagedShortcuts) {
24
+ throw 'shortcut manifest allManagedShortcuts is required'
25
+ }
26
+
27
+ $allSeenIds = @{}
28
+ $allSeenVisibleNames = @{}
29
+ $allSeenGrouping = @{}
30
+ foreach ($eachShortcut in $manifest.allManagedShortcuts) {
31
+ foreach ($eachFieldName in @('id', 'visibleName', 'source', 'profileId', 'locationKind', 'groupingIdentity')) {
32
+ $fieldValue = $eachShortcut.$eachFieldName
33
+ if ([string]::IsNullOrWhiteSpace([string]$fieldValue)) {
34
+ throw "shortcut missing required field $eachFieldName"
35
+ }
36
+ }
37
+ if ($allSeenIds.ContainsKey($eachShortcut.id)) {
38
+ throw "duplicate shortcut id: $($eachShortcut.id)"
39
+ }
40
+ if ($allSeenVisibleNames.ContainsKey($eachShortcut.visibleName)) {
41
+ throw "duplicate shortcut visibleName: $($eachShortcut.visibleName)"
42
+ }
43
+ if ($allSeenGrouping.ContainsKey($eachShortcut.groupingIdentity)) {
44
+ throw "duplicate shortcut groupingIdentity: $($eachShortcut.groupingIdentity)"
45
+ }
46
+ $allSeenIds[$eachShortcut.id] = $true
47
+ $allSeenVisibleNames[$eachShortcut.visibleName] = $true
48
+ $allSeenGrouping[$eachShortcut.groupingIdentity] = $true
49
+ }
50
+
51
+ function Get-ShortcutMetadata {
52
+ param(
53
+ [Parameter(Mandatory = $true)]
54
+ [string]$ShortcutPath,
55
+ [Parameter(Mandatory = $true)]
56
+ $Shell
57
+ )
58
+ if (-not (Test-Path -LiteralPath $ShortcutPath)) {
59
+ return $null
60
+ }
61
+ $link = $Shell.CreateShortcut($ShortcutPath)
62
+ return [pscustomobject]@{
63
+ path = $ShortcutPath
64
+ targetPath = $link.TargetPath
65
+ arguments = $link.Arguments
66
+ workingDirectory = $link.WorkingDirectory
67
+ iconLocation = $link.IconLocation
68
+ exists = $true
69
+ }
70
+ }
71
+
72
+ $shell = New-Object -ComObject WScript.Shell
73
+ $allRows = [System.Collections.Generic.List[object]]::new()
74
+ try {
75
+ foreach ($eachShortcut in $manifest.allManagedShortcuts) {
76
+ if ($eachShortcut.locationKind -eq 'desktop') {
77
+ $baseDirectory = $DesktopPath
78
+ }
79
+ elseif ($eachShortcut.locationKind -eq 'start-menu') {
80
+ $baseDirectory = $StartMenuPath
81
+ }
82
+ else {
83
+ throw "unsupported locationKind for $($eachShortcut.id): $($eachShortcut.locationKind)"
84
+ }
85
+ $shortcutPath = Join-Path $baseDirectory ($eachShortcut.visibleName + '.lnk')
86
+ $metadata = Get-ShortcutMetadata -ShortcutPath $shortcutPath -Shell $shell
87
+ $allRows.Add([pscustomobject]@{
88
+ id = $eachShortcut.id
89
+ visibleName = $eachShortcut.visibleName
90
+ source = $eachShortcut.source
91
+ profileId = $eachShortcut.profileId
92
+ locationKind = $eachShortcut.locationKind
93
+ targetKind = $eachShortcut.targetKind
94
+ launcherName = $eachShortcut.launcherName
95
+ groupingIdentity = $eachShortcut.groupingIdentity
96
+ expectedPath = $shortcutPath
97
+ exists = [bool]$metadata
98
+ targetPath = if ($metadata) { $metadata.targetPath } else { $null }
99
+ arguments = if ($metadata) { $metadata.arguments } else { $null }
100
+ workingDirectory = if ($metadata) { $metadata.workingDirectory } else { $null }
101
+ iconLocation = if ($metadata) { $metadata.iconLocation } else { $null }
102
+ liveMutationAuthorized = [bool]$manifest.policy.liveMutationAuthorized
103
+ })
104
+ }
105
+ }
106
+ finally {
107
+ [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell)
108
+ }
109
+
110
+ $result = [pscustomobject]@{
111
+ mode = 'read-only-inventory'
112
+ policy = $manifest.policy
113
+ scannedAt = (Get-Date).ToString('o')
114
+ desktopPath = $DesktopPath
115
+ startMenuPath = $StartMenuPath
116
+ shortcuts = $allRows
117
+ }
118
+
119
+ $json = $result | ConvertTo-Json -Depth 6
120
+ if ($OutputPath) {
121
+ $directory = Split-Path -Parent $OutputPath
122
+ if ($directory -and -not (Test-Path -LiteralPath $directory)) {
123
+ New-Item -ItemType Directory -Path $directory | Out-Null
124
+ }
125
+ [IO.File]::WriteAllText($OutputPath, $json, [Text.UTF8Encoding]::new($false))
126
+ }
127
+ Write-Output $json
@@ -0,0 +1,51 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "policy": {
4
+ "unsuffixedNames": "Native Desktop wins: Claude Profile A and Claude Profile B",
5
+ "browserNames": "Chrome variants use explicit Web or Chrome source labels",
6
+ "authority": "The owner confirms or revises this policy before live mutation.",
7
+ "liveMutationAuthorized": false
8
+ },
9
+ "allManagedShortcuts": [
10
+ {
11
+ "id": "desktop-profile-a-native",
12
+ "visibleName": "Claude Profile A",
13
+ "source": "native-desktop",
14
+ "profileId": "profile-a",
15
+ "locationKind": "desktop",
16
+ "targetKind": "native-launcher",
17
+ "launcherName": "claude-profile-a",
18
+ "groupingIdentity": "native-profile-a"
19
+ },
20
+ {
21
+ "id": "desktop-profile-b-native",
22
+ "visibleName": "Claude Profile B",
23
+ "source": "native-desktop",
24
+ "profileId": "profile-b",
25
+ "locationKind": "desktop",
26
+ "targetKind": "native-launcher",
27
+ "launcherName": "claude-profile-b",
28
+ "groupingIdentity": "native-profile-b"
29
+ },
30
+ {
31
+ "id": "start-profile-a-chrome",
32
+ "visibleName": "Claude Profile A (Chrome)",
33
+ "source": "chrome-start-menu",
34
+ "profileId": "profile-a",
35
+ "locationKind": "start-menu",
36
+ "targetKind": "chrome-profile",
37
+ "launcherName": "claude-profile-a",
38
+ "groupingIdentity": "chrome-profile-a"
39
+ },
40
+ {
41
+ "id": "start-profile-b-chrome",
42
+ "visibleName": "Claude Profile B (Chrome)",
43
+ "source": "chrome-start-menu",
44
+ "profileId": "profile-b",
45
+ "locationKind": "start-menu",
46
+ "targetKind": "chrome-profile",
47
+ "launcherName": "claude-profile-b",
48
+ "groupingIdentity": "chrome-profile-b"
49
+ }
50
+ ]
51
+ }
@@ -0,0 +1,77 @@
1
+ # Read-only reconciliation preview for managed Claude profile shortcuts.
2
+ # Live mutation is refused while policy.liveMutationAuthorized is false.
3
+
4
+ [CmdletBinding()]
5
+ param(
6
+ [string]$ManifestPath = (Join-Path $PSScriptRoot 'shortcut-manifest.json'),
7
+ [string]$DesktopPath = [Environment]::GetFolderPath('Desktop'),
8
+ [string]$StartMenuPath = [Environment]::GetFolderPath('StartMenu'),
9
+ [string]$OutputPath,
10
+ [switch]$Apply
11
+ )
12
+
13
+ Set-StrictMode -Version Latest
14
+ $ErrorActionPreference = 'Stop'
15
+
16
+ $inventoryScript = Join-Path $PSScriptRoot 'shortcut-inventory.ps1'
17
+ if (-not (Test-Path -LiteralPath $inventoryScript)) {
18
+ throw "inventory script missing: $inventoryScript"
19
+ }
20
+
21
+ $manifest = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json
22
+ $inventoryJson = & $inventoryScript -ManifestPath $ManifestPath -DesktopPath $DesktopPath -StartMenuPath $StartMenuPath
23
+ $inventory = $inventoryJson | ConvertFrom-Json
24
+
25
+ $allActions = [System.Collections.Generic.List[object]]::new()
26
+ foreach ($eachRow in $inventory.shortcuts) {
27
+ $actualPresent = [bool]$eachRow.exists
28
+ $hasSourceIdentity = -not [string]::IsNullOrWhiteSpace([string]$eachRow.source) -and
29
+ -not [string]::IsNullOrWhiteSpace([string]$eachRow.profileId) -and
30
+ -not [string]::IsNullOrWhiteSpace([string]$eachRow.groupingIdentity)
31
+ $status = if (-not $actualPresent) {
32
+ 'missing'
33
+ }
34
+ elseif ($hasSourceIdentity) {
35
+ 'present-unverified-target'
36
+ }
37
+ else {
38
+ 'source-ambiguous'
39
+ }
40
+ $allActions.Add([pscustomobject]@{
41
+ id = $eachRow.id
42
+ visibleName = $eachRow.visibleName
43
+ source = $eachRow.source
44
+ profileId = $eachRow.profileId
45
+ groupingIdentity = $eachRow.groupingIdentity
46
+ desiredPresent = $true
47
+ actualPresent = $actualPresent
48
+ status = $status
49
+ expectedPath = $eachRow.expectedPath
50
+ mutation = 'none'
51
+ })
52
+ }
53
+
54
+ if ($Apply) {
55
+ if (-not [bool]$manifest.policy.liveMutationAuthorized) {
56
+ throw 'live shortcut mutation refused: policy.liveMutationAuthorized is false; owner confirmation required'
57
+ }
58
+ throw 'live shortcut mutation adapter is residual after owner confirmation (N1 apply path)'
59
+ }
60
+
61
+ $preview = [pscustomobject]@{
62
+ mode = 'preview-only'
63
+ policy = $manifest.policy
64
+ actionCount = $allActions.Count
65
+ actions = $allActions
66
+ liveMutationAuthorized = [bool]$manifest.policy.liveMutationAuthorized
67
+ }
68
+
69
+ $json = $preview | ConvertTo-Json -Depth 6
70
+ if ($OutputPath) {
71
+ $directory = Split-Path -Parent $OutputPath
72
+ if ($directory -and -not (Test-Path -LiteralPath $directory)) {
73
+ New-Item -ItemType Directory -Path $directory | Out-Null
74
+ }
75
+ [IO.File]::WriteAllText($OutputPath, $json, [Text.UTF8Encoding]::new($false))
76
+ }
77
+ Write-Output $json
@@ -238,6 +238,9 @@ def invoke_advisor_launcher(
238
238
 
239
239
  Returns:
240
240
  ``(session_id_or_none, advisor_body_text, returncode)``.
241
+
242
+ Raises:
243
+ AdvisorFailureError: When the launcher is missing or times out.
241
244
  """
242
245
  all_command_arguments = [
243
246
  launcher,
@@ -0,0 +1,108 @@
1
+ """Behavioral tests for active capability reference resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _SCRIPTS = Path(__file__).resolve().parent
9
+ if str(_SCRIPTS) not in sys.path:
10
+ sys.path.insert(0, str(_SCRIPTS))
11
+
12
+ from active_capability_references import (
13
+ build_capability_inventory,
14
+ classify_capability_reference,
15
+ extract_active_capability_names,
16
+ strip_inert_fenced_blocks,
17
+ unresolved_active_capabilities,
18
+ )
19
+ from dev_env_scripts_constants.active_capability_constants import (
20
+ ALL_BANNED_ACTIVE_CAPABILITY_NAMES,
21
+ )
22
+
23
+ PACKAGE_ROOT = Path(__file__).resolve().parent.parent
24
+
25
+
26
+ def test_inventory_includes_shipped_skill_and_agent_names() -> None:
27
+ inventory = build_capability_inventory(PACKAGE_ROOT)
28
+ assert "e-code-review" in inventory.all_skill_names or len(inventory.all_skill_names) > 0
29
+ assert inventory.all_agent_names
30
+ assert inventory.all_command_names
31
+
32
+
33
+ def test_strip_inert_fenced_blocks_drops_historical_examples() -> None:
34
+ markdown = (
35
+ "Use /commit for commits.\n"
36
+ "```historical\n"
37
+ "Use /qbug for bugs.\n"
38
+ "```\n"
39
+ "Still active.\n"
40
+ )
41
+ stripped = strip_inert_fenced_blocks(markdown)
42
+ assert "/qbug" not in stripped
43
+ assert "/commit" in stripped
44
+
45
+
46
+ def test_extract_active_capability_names_finds_slash_and_backticks() -> None:
47
+ markdown = "Run /privacy-hygiene then open `issue-tracker`.\n"
48
+ all_hits = extract_active_capability_names(markdown)
49
+ all_names = {each_name for _line, each_name in all_hits}
50
+ assert "privacy-hygiene" in all_names
51
+ assert "issue-tracker" in all_names
52
+
53
+
54
+ def test_banned_names_always_fail_classification() -> None:
55
+ for each_banned_name in sorted(ALL_BANNED_ACTIVE_CAPABILITY_NAMES):
56
+ reason = classify_capability_reference(each_banned_name)
57
+ assert reason is not None
58
+ assert each_banned_name in reason
59
+
60
+
61
+ def test_known_and_unknown_non_banned_names_pass() -> None:
62
+ inventory = build_capability_inventory(PACKAGE_ROOT)
63
+ known_name = next(iter(inventory.all_skill_names), "e-code-review")
64
+ assert classify_capability_reference(known_name) is None
65
+ assert classify_capability_reference("not-a-real-capability") is None
66
+
67
+
68
+ def test_unresolved_reports_file_and_line_for_fixture(tmp_path: Path) -> None:
69
+ package_root = tmp_path / "pkg"
70
+ skills = package_root / "skills" / "real-skill"
71
+ skills.mkdir(parents=True)
72
+ (skills / "SKILL.md").write_text(
73
+ "Active instruction: call /qbug now.\n",
74
+ encoding="utf-8",
75
+ )
76
+ (package_root / "agents").mkdir()
77
+ (package_root / "agents" / "real-agent.md").write_text("# agent\n", encoding="utf-8")
78
+ (package_root / "commands").mkdir()
79
+ (package_root / "commands" / "real-cmd.md").write_text("# cmd\n", encoding="utf-8")
80
+ all_unresolved = unresolved_active_capabilities(
81
+ package_root,
82
+ all_relative_markdown_paths=["skills/real-skill/SKILL.md"],
83
+ )
84
+ assert all_unresolved
85
+ first = all_unresolved[0]
86
+ assert first.capability_name == "qbug"
87
+ assert first.line_number == 1
88
+ assert first.file_path == "skills/real-skill/SKILL.md"
89
+ assert "banned" in first.reason
90
+
91
+
92
+ def test_inert_historical_qbug_does_not_fail(tmp_path: Path) -> None:
93
+ package_root = tmp_path / "pkg"
94
+ skill_dir = package_root / "skills" / "ok-skill"
95
+ skill_dir.mkdir(parents=True)
96
+ (skill_dir / "SKILL.md").write_text(
97
+ "```historical\nLegacy /qbug workflow.\n```\nUse /ok-skill.\n",
98
+ encoding="utf-8",
99
+ )
100
+ (package_root / "agents").mkdir()
101
+ (package_root / "commands").mkdir()
102
+ all_unresolved = unresolved_active_capabilities(
103
+ package_root,
104
+ all_relative_markdown_paths=["skills/ok-skill/SKILL.md"],
105
+ )
106
+ assert all(
107
+ each.capability_name != "qbug" for each in all_unresolved
108
+ )