Dev10x 0.76.0__tar.gz → 0.78.0__tar.gz

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 (206) hide show
  1. {dev10x-0.76.0/src/Dev10x.egg-info → dev10x-0.78.0}/PKG-INFO +1 -1
  2. {dev10x-0.76.0 → dev10x-0.78.0}/pyproject.toml +12 -2
  3. {dev10x-0.76.0 → dev10x-0.78.0/src/Dev10x.egg-info}/PKG-INFO +1 -1
  4. {dev10x-0.76.0 → dev10x-0.78.0}/src/Dev10x.egg-info/SOURCES.txt +16 -0
  5. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/permission.py +129 -1
  6. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/skill.py +23 -33
  7. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/__init__.py +4 -0
  8. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/dev10x_paths.py +15 -0
  9. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/friction_level.py +43 -0
  10. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/install_version.py +51 -0
  11. dev10x-0.78.0/src/dev10x/domain/sensitivity.py +298 -0
  12. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/session_rules.py +3 -17
  13. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/edit_validator.py +35 -4
  14. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/session.py +2 -0
  15. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/session_dispatch.py +39 -0
  16. dev10x-0.78.0/src/dev10x/mcp/_app.py +76 -0
  17. dev10x-0.78.0/src/dev10x/mcp/daemon.py +517 -0
  18. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/git_tools.py +60 -4
  19. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/github_tools.py +26 -1
  20. dev10x-0.78.0/src/dev10x/mcp/knowledge_prompts.py +85 -0
  21. dev10x-0.78.0/src/dev10x/mcp/knowledge_resources.py +132 -0
  22. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/misc_tools.py +40 -10
  23. dev10x-0.78.0/src/dev10x/mcp/resource_watcher.py +413 -0
  24. dev10x-0.78.0/src/dev10x/mcp/roots_manager.py +277 -0
  25. dev10x-0.78.0/src/dev10x/mcp/roots_tools.py +41 -0
  26. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/server_cli.py +11 -2
  27. dev10x-0.78.0/src/dev10x/mcp/session_store.py +421 -0
  28. dev10x-0.78.0/src/dev10x/mcp/wiring.py +153 -0
  29. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/registry.py +1 -0
  30. dev10x-0.78.0/src/dev10x/skills/doctor/strategies/mcp_horizontal_duplicates.py +118 -0
  31. dev10x-0.78.0/src/dev10x/skills/harvest/__init__.py +7 -0
  32. dev10x-0.78.0/src/dev10x/skills/harvest/closed_prs.py +150 -0
  33. dev10x-0.78.0/src/dev10x/skills/harvest/review_threads.py +195 -0
  34. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/pr_notify.py +10 -29
  35. dev10x-0.78.0/src/dev10x/skills/notifications/slack_notify.py +312 -0
  36. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/clean_project_files.py +16 -3
  37. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/doctor.py +137 -0
  38. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/enumerate_mcp.py +218 -0
  39. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/subprocess_utils.py +92 -5
  40. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/utilities/__init__.py +14 -1
  41. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/__init__.py +13 -0
  42. dev10x-0.78.0/src/dev10x/validators/sensitivity_target.py +122 -0
  43. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/skill_redirect.py +18 -19
  44. dev10x-0.78.0/src/dev10x/validators/spec_drift.py +185 -0
  45. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_subprocess_utils.py +181 -3
  46. dev10x-0.76.0/src/dev10x/mcp/_app.py +0 -11
  47. {dev10x-0.76.0 → dev10x-0.78.0}/LICENSE +0 -0
  48. {dev10x-0.76.0 → dev10x-0.78.0}/README.md +0 -0
  49. {dev10x-0.76.0 → dev10x-0.78.0}/setup.cfg +0 -0
  50. {dev10x-0.76.0 → dev10x-0.78.0}/src/Dev10x.egg-info/dependency_links.txt +0 -0
  51. {dev10x-0.76.0 → dev10x-0.78.0}/src/Dev10x.egg-info/entry_points.txt +0 -0
  52. {dev10x-0.76.0 → dev10x-0.78.0}/src/Dev10x.egg-info/requires.txt +0 -0
  53. {dev10x-0.76.0 → dev10x-0.78.0}/src/Dev10x.egg-info/top_level.txt +0 -0
  54. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/__init__.py +0 -0
  55. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/audit/__init__.py +0 -0
  56. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/audit/analyze.py +0 -0
  57. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/audit/log_reader.py +0 -0
  58. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/audit/permissions_model.py +0 -0
  59. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/cli.py +0 -0
  60. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/__init__.py +0 -0
  61. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/config.py +0 -0
  62. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/github_app.py +0 -0
  63. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/github_app_api.py +0 -0
  64. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/hook.py +0 -0
  65. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/init.py +0 -0
  66. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/platform.py +0 -0
  67. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/playbook.py +0 -0
  68. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/commands/validate.py +0 -0
  69. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/config/__init__.py +0 -0
  70. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/config/loader.py +0 -0
  71. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/db/__init__.py +0 -0
  72. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/audit_writer.py +0 -0
  73. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/claude_paths.py +0 -0
  74. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/__init__.py +0 -0
  75. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/allow_rule.py +0 -0
  76. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/branch_name.py +0 -0
  77. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/repository_ref.py +0 -0
  78. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/result.py +0 -0
  79. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/skill_name.py +0 -0
  80. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/common/ticket_id.py +0 -0
  81. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/config_loader.py +0 -0
  82. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/documents/__init__.py +0 -0
  83. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/documents/config_document.py +0 -0
  84. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/documents/plan.py +0 -0
  85. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/documents/session_state.py +0 -0
  86. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/documents/settings_document.py +0 -0
  87. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/documents/task.py +0 -0
  88. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/events/__init__.py +0 -0
  89. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/events/hook_input.py +0 -0
  90. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/file_locks.py +0 -0
  91. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/git_context.py +0 -0
  92. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/hook_telemetry.py +0 -0
  93. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/profile_tier.py +0 -0
  94. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/rules/__init__.py +0 -0
  95. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/rules/policy_rule.py +0 -0
  96. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/rules/rule_engine.py +0 -0
  97. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/rules/sql.py +0 -0
  98. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/rules/validation_rule.py +0 -0
  99. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/domain/session_document.py +0 -0
  100. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/git/__init__.py +0 -0
  101. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/github/__init__.py +0 -0
  102. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/github/app_auth.py +0 -0
  103. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/github/slack.py +0 -0
  104. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/__init__.py +0 -0
  105. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/audit_emit.py +0 -0
  106. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/permission_diagnostics.py +0 -0
  107. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/session_place.py +0 -0
  108. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/session_policy.py +0 -0
  109. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/skill.py +0 -0
  110. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/hooks/task_plan_sync.py +0 -0
  111. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/__init__.py +0 -0
  112. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/audit_tools.py +0 -0
  113. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/plan_tools.py +0 -0
  114. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/server_db.py +0 -0
  115. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/mcp/transport.py +0 -0
  116. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/monitor/__init__.py +0 -0
  117. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/permission/__init__.py +0 -0
  118. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/plan/__init__.py +0 -0
  119. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/plan/service.py +0 -0
  120. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/platform/__init__.py +0 -0
  121. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/platform/registry.py +0 -0
  122. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/release/__init__.py +0 -0
  123. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/runner/__init__.py +0 -0
  124. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/scope/__init__.py +0 -0
  125. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/scope/_matcher.py +0 -0
  126. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/scope/index_parser.py +0 -0
  127. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/scope/norms_renderer.py +0 -0
  128. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/scope/safeguards_renderer.py +0 -0
  129. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/session/__init__.py +0 -0
  130. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/session/queries.py +0 -0
  131. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skill_index/__init__.py +0 -0
  132. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skill_index/builder.py +0 -0
  133. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/__init__.py +0 -0
  134. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/__init__.py +0 -0
  135. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/analyze_actions.py +0 -0
  136. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/analyze_permissions.py +0 -0
  137. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/cli_friction.py +0 -0
  138. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/extract_session.py +0 -0
  139. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/instruction_budget.py +0 -0
  140. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/audit/privacy.py +0 -0
  141. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/common/__init__.py +0 -0
  142. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/common/jtbd.py +0 -0
  143. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/database/__init__.py +0 -0
  144. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/__init__.py +0 -0
  145. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/__init__.py +0 -0
  146. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/forbidden_token_priming.py +0 -0
  147. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/mcp_vs_script_drift.py +0 -0
  148. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/missing_linear_mcp_allow.py +0 -0
  149. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategy.py +0 -0
  150. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/evidence/__init__.py +0 -0
  151. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/git_fixup/__init__.py +0 -0
  152. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/git_fixup/find_fixup_target.py +0 -0
  153. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/__init__.py +0 -0
  154. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/ci_check_status.py +0 -0
  155. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/notifications/__init__.py +0 -0
  156. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/notifications/slack_review_request.py +0 -0
  157. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/__init__.py +0 -0
  158. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/batch_detection.py +0 -0
  159. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/subagent_protocol.py +0 -0
  160. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/__init__.py +0 -0
  161. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/backup.py +0 -0
  162. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/baseline-permissions.yaml +0 -0
  163. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/cluster_paths.py +0 -0
  164. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/config.py +0 -0
  165. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/file_lock.py +0 -0
  166. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/local_skill_approval.py +0 -0
  167. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/merge_worktree_permissions.py +0 -0
  168. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/update_paths.py +0 -0
  169. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission/uv_run_project.py +0 -0
  170. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/__init__.py +0 -0
  171. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/fixtures.py +0 -0
  172. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/matrix.py +0 -0
  173. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/report.py +0 -0
  174. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/__init__.py +0 -0
  175. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/compare.py +0 -0
  176. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/discovery.py +0 -0
  177. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/report.py +0 -0
  178. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/release/__init__.py +0 -0
  179. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/release/classifier.py +0 -0
  180. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/skills/release/collect_prs.py +0 -0
  181. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/spec/__init__.py +0 -0
  182. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/spec/drift_detector.py +0 -0
  183. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/_quote_strip.py +0 -0
  184. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/base.py +0 -0
  185. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/bash_aggregation.py +0 -0
  186. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/command-skill-map.yaml +0 -0
  187. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/command_substitution.py +0 -0
  188. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/commit_jtbd.py +0 -0
  189. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/execution_safety.py +0 -0
  190. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/mcp_prefix.py +0 -0
  191. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/pipeline_allow.py +0 -0
  192. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/pr_base.py +0 -0
  193. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/prefix_friction.py +0 -0
  194. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/redundant_fetch.py +0 -0
  195. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/registry.py +0 -0
  196. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/safe_expansion.py +0 -0
  197. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/safe_subshell.py +0 -0
  198. {dev10x-0.76.0 → dev10x-0.78.0}/src/dev10x/validators/sql_safety.py +0 -0
  199. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_cli.py +0 -0
  200. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_cli_lazy.py +0 -0
  201. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_cwd_discipline_sweep.py +0 -0
  202. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_cwd_enforcement.py +0 -0
  203. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_integration.py +0 -0
  204. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_no_module_scope_gitcontext.py +0 -0
  205. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_script_loadability.py +0 -0
  206. {dev10x-0.76.0 → dev10x-0.78.0}/tests/test_shell_script_flags.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Dev10x
3
- Version: 0.76.0
3
+ Version: 0.78.0
4
4
  Summary: Claude Code plugin providing reusable skills, hooks, and commands
5
5
  Author: Janusz Skonieczny
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Dev10x"
3
- version = "0.76.0"
3
+ version = "0.78.0"
4
4
  description = "Claude Code plugin providing reusable skills, hooks, and commands"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -63,7 +63,7 @@ addopts = "--cov=src/dev10x --cov-report=term-missing"
63
63
  source = ["src/dev10x"]
64
64
 
65
65
  [tool.coverage.report]
66
- fail_under = 38
66
+ fail_under = 75
67
67
  show_missing = true
68
68
  exclude_lines = ["if __name__", "pragma: no cover"]
69
69
 
@@ -74,5 +74,15 @@ src = ["src"]
74
74
  [tool.ruff.lint]
75
75
  select = ["E", "F", "I", "UP"]
76
76
 
77
+ [tool.pytest_benchmark]
78
+ # Store baselines in .benchmarks/ (gitignored per-run, cached by CI)
79
+ storage = "file"
80
+ # Fail on mean regression > 20% vs stored baseline
81
+ compare_fail = ["mean:20%"]
82
+ # Minimum rounds so outliers don't skew results
83
+ min_rounds = 5
84
+ timer = "perf_counter"
85
+ disable_gc = true
86
+
77
87
  [tool.mypy]
78
88
  mypy_path = "src"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Dev10x
3
- Version: 0.76.0
3
+ Version: 0.78.0
4
4
  Summary: Claude Code plugin providing reusable skills, hooks, and commands
5
5
  Author: Janusz Skonieczny
6
6
  License: MIT License
@@ -39,6 +39,7 @@ src/dev10x/domain/git_context.py
39
39
  src/dev10x/domain/hook_telemetry.py
40
40
  src/dev10x/domain/install_version.py
41
41
  src/dev10x/domain/profile_tier.py
42
+ src/dev10x/domain/sensitivity.py
42
43
  src/dev10x/domain/session_document.py
43
44
  src/dev10x/domain/session_rules.py
44
45
  src/dev10x/domain/common/__init__.py
@@ -78,13 +79,21 @@ src/dev10x/hooks/task_plan_sync.py
78
79
  src/dev10x/mcp/__init__.py
79
80
  src/dev10x/mcp/_app.py
80
81
  src/dev10x/mcp/audit_tools.py
82
+ src/dev10x/mcp/daemon.py
81
83
  src/dev10x/mcp/git_tools.py
82
84
  src/dev10x/mcp/github_tools.py
85
+ src/dev10x/mcp/knowledge_prompts.py
86
+ src/dev10x/mcp/knowledge_resources.py
83
87
  src/dev10x/mcp/misc_tools.py
84
88
  src/dev10x/mcp/plan_tools.py
89
+ src/dev10x/mcp/resource_watcher.py
90
+ src/dev10x/mcp/roots_manager.py
91
+ src/dev10x/mcp/roots_tools.py
85
92
  src/dev10x/mcp/server_cli.py
86
93
  src/dev10x/mcp/server_db.py
94
+ src/dev10x/mcp/session_store.py
87
95
  src/dev10x/mcp/transport.py
96
+ src/dev10x/mcp/wiring.py
88
97
  src/dev10x/monitor/__init__.py
89
98
  src/dev10x/permission/__init__.py
90
99
  src/dev10x/plan/__init__.py
@@ -118,15 +127,20 @@ src/dev10x/skills/doctor/registry.py
118
127
  src/dev10x/skills/doctor/strategy.py
119
128
  src/dev10x/skills/doctor/strategies/__init__.py
120
129
  src/dev10x/skills/doctor/strategies/forbidden_token_priming.py
130
+ src/dev10x/skills/doctor/strategies/mcp_horizontal_duplicates.py
121
131
  src/dev10x/skills/doctor/strategies/mcp_vs_script_drift.py
122
132
  src/dev10x/skills/doctor/strategies/missing_linear_mcp_allow.py
123
133
  src/dev10x/skills/evidence/__init__.py
124
134
  src/dev10x/skills/git_fixup/__init__.py
125
135
  src/dev10x/skills/git_fixup/find_fixup_target.py
136
+ src/dev10x/skills/harvest/__init__.py
137
+ src/dev10x/skills/harvest/closed_prs.py
138
+ src/dev10x/skills/harvest/review_threads.py
126
139
  src/dev10x/skills/monitor/__init__.py
127
140
  src/dev10x/skills/monitor/ci_check_status.py
128
141
  src/dev10x/skills/monitor/pr_notify.py
129
142
  src/dev10x/skills/notifications/__init__.py
143
+ src/dev10x/skills/notifications/slack_notify.py
130
144
  src/dev10x/skills/notifications/slack_review_request.py
131
145
  src/dev10x/skills/orchestration/__init__.py
132
146
  src/dev10x/skills/orchestration/batch_detection.py
@@ -174,7 +188,9 @@ src/dev10x/validators/redundant_fetch.py
174
188
  src/dev10x/validators/registry.py
175
189
  src/dev10x/validators/safe_expansion.py
176
190
  src/dev10x/validators/safe_subshell.py
191
+ src/dev10x/validators/sensitivity_target.py
177
192
  src/dev10x/validators/skill_redirect.py
193
+ src/dev10x/validators/spec_drift.py
178
194
  src/dev10x/validators/sql_safety.py
179
195
  tests/test_cli.py
180
196
  tests/test_cli_lazy.py
@@ -304,13 +304,49 @@ def init() -> None:
304
304
  help="Print one line per changed file (count) instead of full per-file details",
305
305
  )
306
306
  @click.option("--restore", is_flag=True, help="Restore settings from most recent backups")
307
- def clean(*, dry_run: bool, verbose: bool, summary: bool, restore: bool) -> None:
307
+ @click.option(
308
+ "--skip-global-dedup",
309
+ is_flag=True,
310
+ default=False,
311
+ help=(
312
+ "Do not remove project rules that are exact duplicates of global rules. "
313
+ "Retained for backward compatibility — global-dedup is now OFF by "
314
+ "default, so this flag is only meaningful alongside --aggressive."
315
+ ),
316
+ )
317
+ @click.option(
318
+ "--aggressive",
319
+ is_flag=True,
320
+ default=False,
321
+ help=(
322
+ "Also remove project rules that are exact duplicates of global rules. "
323
+ "OFF by default (finding #47): global→project rule inheritance is NOT "
324
+ "guaranteed when a project has its own settings.local.json, so stripping "
325
+ "the local copy can reintroduce permission prompts. Only enable after "
326
+ "`dev10x permission investigate` confirms inheritance holds for this "
327
+ "environment. Recover a bad run with `dev10x permission clean --restore`."
328
+ ),
329
+ )
330
+ def clean(
331
+ *,
332
+ dry_run: bool,
333
+ verbose: bool,
334
+ summary: bool,
335
+ restore: bool,
336
+ skip_global_dedup: bool,
337
+ aggressive: bool,
338
+ ) -> None:
308
339
  """Clean redundant permissions from project settings files."""
309
340
  from dev10x.skills.permission import clean_project_files as mod
310
341
 
311
342
  if restore:
312
343
  sys.exit(mod._restore(config_path=mod.find_config()))
313
344
 
345
+ # Global-dedup is opt-in (finding #47). It runs only under --aggressive,
346
+ # and --skip-global-dedup always wins so the safe behavior cannot be
347
+ # accidentally re-enabled.
348
+ skip_global_dedup = skip_global_dedup or not aggressive
349
+
314
350
  config_path = mod.find_config()
315
351
  click.echo(f"Config: {config_path}")
316
352
  config = mod.load_config(config_path)
@@ -342,6 +378,7 @@ def clean(*, dry_run: bool, verbose: bool, summary: bool, restore: bool) -> None
342
378
  total_kept = 0
343
379
  files_changed = 0
344
380
  total_secrets = 0
381
+ total_global_dedup = 0
345
382
 
346
383
  for path in sorted(settings_files):
347
384
  result, messages = mod.clean_file(
@@ -352,6 +389,7 @@ def clean(*, dry_run: bool, verbose: bool, summary: bool, restore: bool) -> None
352
389
  cache_root=cache_root,
353
390
  dry_run=dry_run,
354
391
  verbose=verbose,
392
+ skip_global_dedup=skip_global_dedup,
355
393
  )
356
394
  if result is None:
357
395
  click.echo(f"\n{path}")
@@ -376,6 +414,7 @@ def clean(*, dry_run: bool, verbose: bool, summary: bool, restore: bool) -> None
376
414
  total_removed += result.total_removed
377
415
  total_kept += len(result.kept)
378
416
  total_secrets += len(result.leaked_secrets)
417
+ total_global_dedup += len(result.exact_duplicates)
379
418
  if result.total_removed > 0:
380
419
  files_changed += 1
381
420
  else:
@@ -389,6 +428,14 @@ def clean(*, dry_run: bool, verbose: bool, summary: bool, restore: bool) -> None
389
428
  click.echo(f"{verb} {total_removed} rules across {files_changed} files.")
390
429
  click.echo(f"Kept {total_kept} rules total.")
391
430
 
431
+ if total_global_dedup > 0 and not skip_global_dedup:
432
+ click.echo(
433
+ f"\n⚠ WARNING: {total_global_dedup} rules removed as exact duplicates of global"
434
+ " rules (--aggressive). Global→project rule inheritance is NOT guaranteed when a"
435
+ " project has its own settings.local.json (finding #47). If new permission prompts"
436
+ " appear, recover with `dev10x permission clean --restore`."
437
+ )
438
+
392
439
  if total_secrets > 0:
393
440
  click.echo(
394
441
  f"\n⚠ Found {total_secrets} rules containing leaked secrets."
@@ -951,6 +998,87 @@ def doctor_enable_group(*, group_name: str, dry_run: bool) -> None:
951
998
  click.echo(f"\n{verb} {total_added} rules from group {group_name!r}.")
952
999
 
953
1000
 
1001
+ @doctor.command(name="anchor-worktree-roots")
1002
+ @click.option("--dry-run", is_flag=True, help="Show changes without modifying files")
1003
+ @click.option("--quiet", is_flag=True, help="Suppress per-file details")
1004
+ def doctor_anchor_worktree_roots(*, dry_run: bool, quiet: bool) -> None:
1005
+ """Anchor .worktrees parents in additionalDirectories and flag relative skill-script rules.
1006
+
1007
+ Worktrees accumulate per-leaf ``additionalDirectories`` entries instead of
1008
+ anchoring the project-level ``.worktrees`` parent. This command:
1009
+
1010
+ \b
1011
+ 1. Discovers every ``<project>/.worktrees`` parent beneath configured roots.
1012
+ 2. Ensures each parent is present in ``additionalDirectories`` across all
1013
+ settings files — anchoring at the parent covers all sibling and future
1014
+ worktrees without re-prompting per leaf (GH-376).
1015
+ 3. Flags bare-relative ``.claude/skills/.../scripts/`` allow rules that
1016
+ silently target different skill dirs per worktree CWD.
1017
+ """
1018
+ from dev10x.skills.permission import doctor as mod
1019
+ from dev10x.skills.permission import update_paths as paths_mod
1020
+
1021
+ config_path = paths_mod.find_config()
1022
+ config = paths_mod.load_config(config_path)
1023
+ roots = config.get("roots", [])
1024
+ if not roots:
1025
+ click.echo("No roots configured. Run `dev10x permission init` first.")
1026
+ return
1027
+
1028
+ settings_files = paths_mod.find_settings_files(
1029
+ roots=roots,
1030
+ include_user=config.get("include_user_settings", True),
1031
+ )
1032
+ if not settings_files:
1033
+ click.echo("No settings files found.")
1034
+ return
1035
+
1036
+ if dry_run and not quiet:
1037
+ click.echo("(dry run — no files will be modified)\n")
1038
+
1039
+ worktrees_parents = mod.discover_worktrees_parents(roots)
1040
+ if not quiet:
1041
+ if worktrees_parents:
1042
+ click.echo(f"Discovered {len(worktrees_parents)} .worktrees parent(s):")
1043
+ for parent in worktrees_parents:
1044
+ click.echo(f" {parent}")
1045
+ else:
1046
+ click.echo("No .worktrees parents found beneath configured roots.")
1047
+
1048
+ anchor_result = mod.anchor_worktree_roots(
1049
+ settings_files,
1050
+ roots=roots,
1051
+ dry_run=dry_run,
1052
+ )
1053
+
1054
+ workspace_count = anchor_result.workspace_anchored
1055
+ skill_script_count = sum(1 for f in anchor_result.findings if f.scope == "skill-script")
1056
+
1057
+ for finding in anchor_result.findings:
1058
+ if finding.scope == "workspace" and not quiet:
1059
+ click.echo(f"\n{finding.settings_path}")
1060
+ click.echo(f" {finding.suggestion}")
1061
+ elif finding.scope == "skill-script":
1062
+ click.echo(f"\n{finding.settings_path}")
1063
+ click.echo(f" ! RELATIVE_SKILL_SCRIPT: {finding.rule}")
1064
+ if not quiet:
1065
+ click.echo(f" {finding.suggestion}")
1066
+
1067
+ if workspace_count == 0 and skill_script_count == 0:
1068
+ click.echo("\nAll settings files already have worktrees parents anchored.")
1069
+ else:
1070
+ if workspace_count > 0:
1071
+ verb = "Would anchor" if dry_run else "Anchored"
1072
+ click.echo(
1073
+ f"\n{verb} {workspace_count} worktrees parent path(s) in additionalDirectories."
1074
+ )
1075
+ if skill_script_count > 0:
1076
+ click.echo(
1077
+ f"\nFound {skill_script_count} relative skill-script rule(s) "
1078
+ f"— rewrite to absolute plugin-cache paths or Skill() invocations."
1079
+ )
1080
+
1081
+
954
1082
  @permission.command(name="record-upgrade")
955
1083
  @click.option(
956
1084
  "--version",
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import subprocess
4
3
  import sys
5
4
  from pathlib import Path
6
5
 
@@ -23,14 +22,6 @@ def notify() -> None:
23
22
  """
24
23
 
25
24
 
26
- def _plugin_root() -> Path:
27
- """Resolve the plugin root containing the skills/ directory.
28
-
29
- `src/dev10x/commands/skill.py` -> parents[3] = plugin root.
30
- """
31
- return Path(__file__).resolve().parents[3]
32
-
33
-
34
25
  @notify.command(name="slack-review-prepare")
35
26
  @click.option("--pr", type=int, required=True, help="PR number")
36
27
  @click.option("--repo", required=True, help="GitHub repo (owner/name)")
@@ -69,37 +60,36 @@ def slack_send(
69
60
  thread_ts: str | None,
70
61
  workspace: str | None,
71
62
  ) -> None:
72
- """Send a Slack message via the plugin's slack-notify.py script.
63
+ """Send a Slack message via the importable slack_notify module (GH-442).
73
64
 
74
- Delegates to `skills/slack/slack-notify.py` while exposing a
75
- version-stable CLI surface. The underlying script handles token
76
- resolution, user-group mention expansion, and bot identity.
65
+ Delegates to `dev10x.skills.notifications.slack_notify` so the command
66
+ works when dev10x is installed via ``uvx`` — where ``skills/`` data files
67
+ are not shipped as part of the wheel and cannot be reached by filesystem
68
+ traversal from the installed package location.
77
69
  """
78
70
  if not message and not message_file:
79
71
  raise click.UsageError("Provide --message or --message-file.")
80
72
 
81
- slack_notify = _plugin_root() / "skills" / "slack" / "slack-notify.py"
82
- if not slack_notify.exists():
83
- click.echo(f"slack-notify.py not found at {slack_notify}", err=True)
84
- sys.exit(1)
73
+ from dev10x.skills.notifications import slack_notify
85
74
 
86
- cmd: list[str] = [str(slack_notify), "--channel", channel]
87
- if message_file is not None:
88
- cmd.extend(["--message-file", str(message_file)])
89
- if message is not None:
90
- cmd.extend(["--message", message])
91
- if thread_ts is not None:
92
- cmd.extend(["--thread-ts", thread_ts])
93
75
  if workspace is not None:
94
- cmd.extend(["--workspace", workspace])
95
-
96
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
97
- if result.stdout:
98
- click.echo(result.stdout.rstrip())
99
- if result.returncode != 0:
100
- if result.stderr:
101
- click.echo(result.stderr.rstrip(), err=True)
102
- sys.exit(result.returncode)
76
+ slack_notify.set_workspace(workspace)
77
+
78
+ msg: str
79
+ if message_file is not None:
80
+ msg = message_file.read_text()
81
+ else:
82
+ msg = message # type: ignore[assignment] # validated above
83
+
84
+ ts = slack_notify.send_slack_message(
85
+ channel=channel,
86
+ message=msg,
87
+ thread_ts=thread_ts,
88
+ )
89
+ if ts:
90
+ click.echo(f"✅ Slack message sent successfully! ts={ts}")
91
+ else:
92
+ sys.exit(1)
103
93
 
104
94
 
105
95
  @skill.command(name="count-instructions")
@@ -8,6 +8,7 @@ from dev10x.domain.git_context import GitContext
8
8
  from dev10x.domain.rules.rule_engine import RuleEngine, RuleMatch
9
9
  from dev10x.domain.rules.sql import SqlStatement, is_read_only_sql
10
10
  from dev10x.domain.rules.validation_rule import Compensation, Rule
11
+ from dev10x.domain.sensitivity import SensitivityClassifier, SensitivityLabel, SensitivityMatch
11
12
 
12
13
  __all__ = [
13
14
  "Compensation",
@@ -28,6 +29,9 @@ __all__ = [
28
29
  "Rule",
29
30
  "RuleEngine",
30
31
  "RuleMatch",
32
+ "SensitivityClassifier",
33
+ "SensitivityLabel",
34
+ "SensitivityMatch",
31
35
  "SqlStatement",
32
36
  "is_read_only_sql",
33
37
  ]
@@ -164,6 +164,13 @@ class Dev10xConfigDir:
164
164
  _legacy_settings_pr_merge_yaml,
165
165
  )
166
166
 
167
+ @classmethod
168
+ def plugin_maintenance_prefs_yaml(cls) -> Path:
169
+ return _with_lazy_migration(
170
+ cls._resolve("plugin-maintenance-prefs.yaml"),
171
+ _legacy_plugin_maintenance_prefs_yaml,
172
+ )
173
+
167
174
 
168
175
  def _with_lazy_migration(current: Path, legacy_provider: Callable[[], Path]) -> Path:
169
176
  migrate_path(legacy=legacy_provider(), current=current)
@@ -185,6 +192,10 @@ _legacy_github_reviewers_config_yaml = ClaudeDir.github_reviewers_config_yaml
185
192
  _legacy_settings_pr_merge_yaml = ClaudeDir.settings_pr_merge_yaml
186
193
 
187
194
 
195
+ def _legacy_plugin_maintenance_prefs_yaml() -> Path:
196
+ return ClaudeDir.memory_dev10x_dir() / "plugin-maintenance-prefs.yaml"
197
+
198
+
188
199
  def _legacy_playbooks_dir() -> Path:
189
200
  return ClaudeDir.memory_dev10x_dir() / "playbooks"
190
201
 
@@ -219,6 +230,10 @@ def _migration_pairs() -> list[tuple[Path, Path]]:
219
230
  _legacy_settings_pr_merge_yaml(),
220
231
  Dev10xConfigDir._resolve("settings-pr-merge.yaml"),
221
232
  ),
233
+ (
234
+ _legacy_plugin_maintenance_prefs_yaml(),
235
+ Dev10xConfigDir._resolve("plugin-maintenance-prefs.yaml"),
236
+ ),
222
237
  ]
223
238
 
224
239
 
@@ -54,5 +54,48 @@ class FrictionLevel(StrEnum):
54
54
  return member
55
55
  return cls.default()
56
56
 
57
+ def pending_decisions_guidance(self) -> str:
58
+ """Return the session-resume instruction for pending-decision state.
59
+
60
+ Called by ``DecisionGuidanceRule`` when the plan has one or more
61
+ unresolved decision tasks. Each member encodes its own pacing
62
+ rule, eliminating the if/elif dispatch from the rule body.
63
+
64
+ Returns:
65
+ A non-empty instruction string for every recognised member.
66
+ """
67
+ if self is FrictionLevel.ADAPTIVE:
68
+ return (
69
+ "Session resumed with pending decisions. Friction level is adaptive — "
70
+ "auto-select recommended options for all queued decisions and continue "
71
+ "advancing through the task list without calling AskUserQuestion."
72
+ )
73
+ return (
74
+ "Session resumed with pending decisions. "
75
+ "Re-ask each pending decision using AskUserQuestion — "
76
+ "invoke Dev10x:ask before advancing."
77
+ )
78
+
79
+ def fallback_guidance(self, *, fallback: str) -> str:
80
+ """Return a friction-level-specific fallback clause for block messages.
81
+
82
+ In GUIDED mode the agent is shown a concrete fallback path (skill
83
+ guardrails to apply manually, or an MCP-unavailability escape).
84
+ In STRICT and ADAPTIVE modes the block is unadorned — the fallback
85
+ text is omitted so the message stays concise.
86
+
87
+ Args:
88
+ fallback: The fallback snippet to surface in GUIDED mode.
89
+ May be an MCP-server fallback description, a manual-
90
+ guardrail list, or any other escape-hatch hint.
91
+
92
+ Returns:
93
+ The fallback clause (including a leading newline) when GUIDED,
94
+ or an empty string for other levels.
95
+ """
96
+ if self is FrictionLevel.GUIDED:
97
+ return fallback
98
+ return ""
99
+
57
100
 
58
101
  __all__ = ["FrictionLevel"]
@@ -85,6 +85,55 @@ def write_applied_version(
85
85
  return path
86
86
 
87
87
 
88
+ def read_running_hook_version(*, plugin_root: Path | None = None) -> str | None:
89
+ """Return the version that is *currently executing* inside this session.
90
+
91
+ ``$CLAUDE_PLUGIN_ROOT`` is set by Claude Code to the plugin directory
92
+ that was resolved when the session started. Hooks loaded from that
93
+ directory run for the entire session lifetime — an on-disk upgrade
94
+ does not swap them out. Reading the version from this directory gives
95
+ us the *running* version, which may lag behind the latest installed
96
+ version when the user ran ``claude plugin update`` mid-session.
97
+
98
+ Returns ``None`` when the running root is unknown (e.g., local
99
+ ``--plugin-dir`` dev runs that do not carry a versioned cache path).
100
+ """
101
+ root = plugin_root or _default_plugin_root()
102
+ return read_plugin_version(plugin_root=root)
103
+
104
+
105
+ def read_latest_installed_version(
106
+ *,
107
+ cache_dir: Path | None = None,
108
+ publisher: str = "Dev10x-Guru",
109
+ plugin_slug: str = "dev10x-claude",
110
+ ) -> str | None:
111
+ """Return the highest-version directory under the plugin cache.
112
+
113
+ Scans ``~/.claude/plugins/cache/<publisher>/<plugin_slug>/`` for
114
+ semver-named subdirectories and returns the latest. Returns ``None``
115
+ when the directory is absent or empty (e.g., ``--plugin-dir`` installs
116
+ that bypass the cache).
117
+ """
118
+ from dev10x.domain.claude_paths import ClaudeDir
119
+
120
+ root = cache_dir or (ClaudeDir.plugins_cache_dir() / publisher / plugin_slug)
121
+ if not root.is_dir():
122
+ return None
123
+ versions = sorted(
124
+ (d for d in root.iterdir() if d.is_dir()),
125
+ key=lambda p: _version_tuple(p.name),
126
+ )
127
+ return versions[-1].name if versions else None
128
+
129
+
130
+ def _version_tuple(version: str) -> tuple[int, ...]:
131
+ try:
132
+ return tuple(int(x) for x in version.split("."))
133
+ except ValueError:
134
+ return (0,)
135
+
136
+
88
137
  def record_upgrade(*, version: str | None = None) -> Result[dict[str, Any]]:
89
138
  """Record the currently-installed plugin version as applied.
90
139
 
@@ -169,7 +218,9 @@ __all__ = [
169
218
  "InstallState",
170
219
  "install_state",
171
220
  "read_applied_version",
221
+ "read_latest_installed_version",
172
222
  "read_plugin_version",
223
+ "read_running_hook_version",
173
224
  "record_upgrade",
174
225
  "write_applied_version",
175
226
  ]