Dev10x 0.77.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.77.0/src/Dev10x.egg-info → dev10x-0.78.0}/PKG-INFO +1 -1
  2. {dev10x-0.77.0 → dev10x-0.78.0}/pyproject.toml +12 -2
  3. {dev10x-0.77.0 → dev10x-0.78.0/src/Dev10x.egg-info}/PKG-INFO +1 -1
  4. {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/SOURCES.txt +10 -0
  5. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/permission.py +30 -6
  6. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/skill.py +23 -33
  7. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/friction_level.py +43 -0
  8. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/install_version.py +51 -0
  9. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/session_rules.py +3 -17
  10. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/edit_validator.py +35 -4
  11. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session.py +2 -0
  12. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session_dispatch.py +39 -0
  13. dev10x-0.78.0/src/dev10x/mcp/_app.py +76 -0
  14. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/git_tools.py +60 -4
  15. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/github_tools.py +26 -1
  16. dev10x-0.78.0/src/dev10x/mcp/knowledge_prompts.py +85 -0
  17. dev10x-0.78.0/src/dev10x/mcp/knowledge_resources.py +132 -0
  18. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/misc_tools.py +23 -1
  19. dev10x-0.78.0/src/dev10x/mcp/resource_watcher.py +413 -0
  20. dev10x-0.78.0/src/dev10x/mcp/roots_manager.py +277 -0
  21. dev10x-0.78.0/src/dev10x/mcp/roots_tools.py +41 -0
  22. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/server_cli.py +9 -0
  23. dev10x-0.78.0/src/dev10x/skills/harvest/__init__.py +7 -0
  24. dev10x-0.78.0/src/dev10x/skills/harvest/closed_prs.py +150 -0
  25. dev10x-0.78.0/src/dev10x/skills/harvest/review_threads.py +195 -0
  26. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/pr_notify.py +10 -29
  27. dev10x-0.78.0/src/dev10x/skills/notifications/slack_notify.py +312 -0
  28. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/subprocess_utils.py +46 -3
  29. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/__init__.py +7 -0
  30. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/skill_redirect.py +18 -19
  31. dev10x-0.78.0/src/dev10x/validators/spec_drift.py +185 -0
  32. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_subprocess_utils.py +87 -0
  33. dev10x-0.77.0/src/dev10x/mcp/_app.py +0 -11
  34. {dev10x-0.77.0 → dev10x-0.78.0}/LICENSE +0 -0
  35. {dev10x-0.77.0 → dev10x-0.78.0}/README.md +0 -0
  36. {dev10x-0.77.0 → dev10x-0.78.0}/setup.cfg +0 -0
  37. {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/dependency_links.txt +0 -0
  38. {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/entry_points.txt +0 -0
  39. {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/requires.txt +0 -0
  40. {dev10x-0.77.0 → dev10x-0.78.0}/src/Dev10x.egg-info/top_level.txt +0 -0
  41. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/__init__.py +0 -0
  42. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/__init__.py +0 -0
  43. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/analyze.py +0 -0
  44. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/log_reader.py +0 -0
  45. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/audit/permissions_model.py +0 -0
  46. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/cli.py +0 -0
  47. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/__init__.py +0 -0
  48. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/config.py +0 -0
  49. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/github_app.py +0 -0
  50. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/github_app_api.py +0 -0
  51. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/hook.py +0 -0
  52. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/init.py +0 -0
  53. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/platform.py +0 -0
  54. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/playbook.py +0 -0
  55. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/commands/validate.py +0 -0
  56. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/config/__init__.py +0 -0
  57. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/config/loader.py +0 -0
  58. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/db/__init__.py +0 -0
  59. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/__init__.py +0 -0
  60. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/audit_writer.py +0 -0
  61. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/claude_paths.py +0 -0
  62. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/__init__.py +0 -0
  63. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/allow_rule.py +0 -0
  64. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/branch_name.py +0 -0
  65. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/repository_ref.py +0 -0
  66. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/result.py +0 -0
  67. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/skill_name.py +0 -0
  68. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/common/ticket_id.py +0 -0
  69. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/config_loader.py +0 -0
  70. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/dev10x_paths.py +0 -0
  71. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/__init__.py +0 -0
  72. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/config_document.py +0 -0
  73. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/plan.py +0 -0
  74. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/session_state.py +0 -0
  75. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/settings_document.py +0 -0
  76. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/documents/task.py +0 -0
  77. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/events/__init__.py +0 -0
  78. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/events/hook_input.py +0 -0
  79. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/file_locks.py +0 -0
  80. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/git_context.py +0 -0
  81. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/hook_telemetry.py +0 -0
  82. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/profile_tier.py +0 -0
  83. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/__init__.py +0 -0
  84. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/policy_rule.py +0 -0
  85. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/rule_engine.py +0 -0
  86. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/sql.py +0 -0
  87. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/rules/validation_rule.py +0 -0
  88. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/sensitivity.py +0 -0
  89. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/domain/session_document.py +0 -0
  90. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/git/__init__.py +0 -0
  91. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/github/__init__.py +0 -0
  92. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/github/app_auth.py +0 -0
  93. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/github/slack.py +0 -0
  94. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/__init__.py +0 -0
  95. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/audit_emit.py +0 -0
  96. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/permission_diagnostics.py +0 -0
  97. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session_place.py +0 -0
  98. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/session_policy.py +0 -0
  99. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/skill.py +0 -0
  100. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/hooks/task_plan_sync.py +0 -0
  101. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/__init__.py +0 -0
  102. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/audit_tools.py +0 -0
  103. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/daemon.py +0 -0
  104. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/plan_tools.py +0 -0
  105. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/server_db.py +0 -0
  106. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/session_store.py +0 -0
  107. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/transport.py +0 -0
  108. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/mcp/wiring.py +0 -0
  109. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/monitor/__init__.py +0 -0
  110. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/permission/__init__.py +0 -0
  111. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/plan/__init__.py +0 -0
  112. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/plan/service.py +0 -0
  113. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/platform/__init__.py +0 -0
  114. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/platform/registry.py +0 -0
  115. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/release/__init__.py +0 -0
  116. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/runner/__init__.py +0 -0
  117. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/__init__.py +0 -0
  118. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/_matcher.py +0 -0
  119. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/index_parser.py +0 -0
  120. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/norms_renderer.py +0 -0
  121. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/scope/safeguards_renderer.py +0 -0
  122. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/session/__init__.py +0 -0
  123. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/session/queries.py +0 -0
  124. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skill_index/__init__.py +0 -0
  125. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skill_index/builder.py +0 -0
  126. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/__init__.py +0 -0
  127. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/__init__.py +0 -0
  128. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/analyze_actions.py +0 -0
  129. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/analyze_permissions.py +0 -0
  130. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/cli_friction.py +0 -0
  131. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/extract_session.py +0 -0
  132. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/instruction_budget.py +0 -0
  133. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/audit/privacy.py +0 -0
  134. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/common/__init__.py +0 -0
  135. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/common/jtbd.py +0 -0
  136. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/database/__init__.py +0 -0
  137. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/__init__.py +0 -0
  138. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/registry.py +0 -0
  139. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/__init__.py +0 -0
  140. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/forbidden_token_priming.py +0 -0
  141. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/mcp_horizontal_duplicates.py +0 -0
  142. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/mcp_vs_script_drift.py +0 -0
  143. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategies/missing_linear_mcp_allow.py +0 -0
  144. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/doctor/strategy.py +0 -0
  145. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/evidence/__init__.py +0 -0
  146. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/git_fixup/__init__.py +0 -0
  147. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/git_fixup/find_fixup_target.py +0 -0
  148. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/__init__.py +0 -0
  149. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/monitor/ci_check_status.py +0 -0
  150. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/notifications/__init__.py +0 -0
  151. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/notifications/slack_review_request.py +0 -0
  152. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/__init__.py +0 -0
  153. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/batch_detection.py +0 -0
  154. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/orchestration/subagent_protocol.py +0 -0
  155. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/__init__.py +0 -0
  156. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/backup.py +0 -0
  157. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/baseline-permissions.yaml +0 -0
  158. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/clean_project_files.py +0 -0
  159. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/cluster_paths.py +0 -0
  160. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/config.py +0 -0
  161. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/doctor.py +0 -0
  162. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/enumerate_mcp.py +0 -0
  163. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/file_lock.py +0 -0
  164. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/local_skill_approval.py +0 -0
  165. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/merge_worktree_permissions.py +0 -0
  166. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/update_paths.py +0 -0
  167. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission/uv_run_project.py +0 -0
  168. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/__init__.py +0 -0
  169. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/fixtures.py +0 -0
  170. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/matrix.py +0 -0
  171. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/permission_investigator/report.py +0 -0
  172. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/__init__.py +0 -0
  173. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/compare.py +0 -0
  174. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/discovery.py +0 -0
  175. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/playbook/report.py +0 -0
  176. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/release/__init__.py +0 -0
  177. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/release/classifier.py +0 -0
  178. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/skills/release/collect_prs.py +0 -0
  179. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/spec/__init__.py +0 -0
  180. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/spec/drift_detector.py +0 -0
  181. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/utilities/__init__.py +0 -0
  182. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/_quote_strip.py +0 -0
  183. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/base.py +0 -0
  184. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/bash_aggregation.py +0 -0
  185. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/command-skill-map.yaml +0 -0
  186. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/command_substitution.py +0 -0
  187. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/commit_jtbd.py +0 -0
  188. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/execution_safety.py +0 -0
  189. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/mcp_prefix.py +0 -0
  190. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/pipeline_allow.py +0 -0
  191. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/pr_base.py +0 -0
  192. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/prefix_friction.py +0 -0
  193. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/redundant_fetch.py +0 -0
  194. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/registry.py +0 -0
  195. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/safe_expansion.py +0 -0
  196. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/safe_subshell.py +0 -0
  197. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/sensitivity_target.py +0 -0
  198. {dev10x-0.77.0 → dev10x-0.78.0}/src/dev10x/validators/sql_safety.py +0 -0
  199. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cli.py +0 -0
  200. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cli_lazy.py +0 -0
  201. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cwd_discipline_sweep.py +0 -0
  202. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_cwd_enforcement.py +0 -0
  203. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_integration.py +0 -0
  204. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_no_module_scope_gitcontext.py +0 -0
  205. {dev10x-0.77.0 → dev10x-0.78.0}/tests/test_script_loadability.py +0 -0
  206. {dev10x-0.77.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.77.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.77.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.77.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
@@ -82,8 +82,13 @@ src/dev10x/mcp/audit_tools.py
82
82
  src/dev10x/mcp/daemon.py
83
83
  src/dev10x/mcp/git_tools.py
84
84
  src/dev10x/mcp/github_tools.py
85
+ src/dev10x/mcp/knowledge_prompts.py
86
+ src/dev10x/mcp/knowledge_resources.py
85
87
  src/dev10x/mcp/misc_tools.py
86
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
87
92
  src/dev10x/mcp/server_cli.py
88
93
  src/dev10x/mcp/server_db.py
89
94
  src/dev10x/mcp/session_store.py
@@ -128,10 +133,14 @@ src/dev10x/skills/doctor/strategies/missing_linear_mcp_allow.py
128
133
  src/dev10x/skills/evidence/__init__.py
129
134
  src/dev10x/skills/git_fixup/__init__.py
130
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
131
139
  src/dev10x/skills/monitor/__init__.py
132
140
  src/dev10x/skills/monitor/ci_check_status.py
133
141
  src/dev10x/skills/monitor/pr_notify.py
134
142
  src/dev10x/skills/notifications/__init__.py
143
+ src/dev10x/skills/notifications/slack_notify.py
135
144
  src/dev10x/skills/notifications/slack_review_request.py
136
145
  src/dev10x/skills/orchestration/__init__.py
137
146
  src/dev10x/skills/orchestration/batch_detection.py
@@ -181,6 +190,7 @@ src/dev10x/validators/safe_expansion.py
181
190
  src/dev10x/validators/safe_subshell.py
182
191
  src/dev10x/validators/sensitivity_target.py
183
192
  src/dev10x/validators/skill_redirect.py
193
+ src/dev10x/validators/spec_drift.py
184
194
  src/dev10x/validators/sql_safety.py
185
195
  tests/test_cli.py
186
196
  tests/test_cli_lazy.py
@@ -310,12 +310,31 @@ def init() -> None:
310
310
  default=False,
311
311
  help=(
312
312
  "Do not remove project rules that are exact duplicates of global rules. "
313
- "Use this when global→project rule inheritance cannot be verified "
314
- "(finding #47: local settings.local.json may not inherit 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`."
315
328
  ),
316
329
  )
317
330
  def clean(
318
- *, dry_run: bool, verbose: bool, summary: bool, restore: bool, skip_global_dedup: bool
331
+ *,
332
+ dry_run: bool,
333
+ verbose: bool,
334
+ summary: bool,
335
+ restore: bool,
336
+ skip_global_dedup: bool,
337
+ aggressive: bool,
319
338
  ) -> None:
320
339
  """Clean redundant permissions from project settings files."""
321
340
  from dev10x.skills.permission import clean_project_files as mod
@@ -323,6 +342,11 @@ def clean(
323
342
  if restore:
324
343
  sys.exit(mod._restore(config_path=mod.find_config()))
325
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
+
326
350
  config_path = mod.find_config()
327
351
  click.echo(f"Config: {config_path}")
328
352
  config = mod.load_config(config_path)
@@ -407,9 +431,9 @@ def clean(
407
431
  if total_global_dedup > 0 and not skip_global_dedup:
408
432
  click.echo(
409
433
  f"\n⚠ WARNING: {total_global_dedup} rules removed as exact duplicates of global"
410
- " rules. Global→project rule inheritance is NOT guaranteed when a project has its"
411
- " own settings.local.json (finding #47). Re-run with --skip-global-dedup to"
412
- " preserve project-local copies if you see new permission prompts."
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`."
413
437
  )
414
438
 
415
439
  if total_secrets > 0:
@@ -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")
@@ -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
  ]
@@ -69,26 +69,12 @@ class DecisionGuidanceRule(PolicyRule[str]):
69
69
  raise UnknownFrictionLevelError(f"Unknown friction level: {self.friction_level!r}")
70
70
 
71
71
  summary = PlanSummary.from_dict(data=self.plan)
72
- decisions = summary.pending_decisions
73
- if not decisions:
74
- has_remaining = bool(summary.pending_tasks)
75
- if has_remaining:
72
+ if not summary.pending_decisions:
73
+ if summary.pending_tasks:
76
74
  return "Session resumed with tasks remaining. Auto-advance through the task list."
77
75
  return ""
78
76
 
79
- if self.friction_level is FrictionLevel.ADAPTIVE:
80
- return (
81
- "Session resumed with pending decisions. Friction level is adaptive — "
82
- "auto-select recommended options for all queued decisions and continue "
83
- "advancing through the task list without calling AskUserQuestion."
84
- )
85
- if self.friction_level in (FrictionLevel.STRICT, FrictionLevel.GUIDED):
86
- return (
87
- "Session resumed with pending decisions. "
88
- "Re-ask each pending decision using AskUserQuestion — "
89
- "invoke Dev10x:ask before advancing."
90
- )
91
- raise UnknownFrictionLevelError(f"Unhandled friction level: {self.friction_level!r}")
77
+ return self.friction_level.pending_decisions_guidance()
92
78
 
93
79
 
94
80
  __all__ = [
@@ -1,7 +1,14 @@
1
- """Edit/Write tool validator — blocks sensitive file writes.
1
+ """Edit/Write tool validator — blocks sensitive file writes and spec drift.
2
2
 
3
- Loads rules from command-skill-map.yaml where matcher="Edit|Write",
4
- delegates evaluation to RuleEngine. First block wins.
3
+ Two validation passes:
4
+
5
+ 1. YAML-rule pass: loads rules from command-skill-map.yaml where
6
+ matcher="Edit|Write", delegates evaluation to RuleEngine.
7
+ First block wins.
8
+
9
+ 2. Python-validator pass: runs active Edit|Write validators from the
10
+ shared ValidatorRegistry (e.g. SpecDriftValidator / DX015).
11
+ Only validators whose ``should_run()`` returns True participate.
5
12
  """
6
13
 
7
14
  from __future__ import annotations
@@ -10,7 +17,7 @@ import sys
10
17
  from pathlib import Path
11
18
  from typing import TYPE_CHECKING, Any
12
19
 
13
- from dev10x.domain.events.hook_input import HookResult
20
+ from dev10x.domain.events.hook_input import HookInput, HookResult
14
21
 
15
22
  if TYPE_CHECKING:
16
23
  from dev10x.domain.rules.rule_engine import RuleEngine
@@ -26,6 +33,28 @@ def _build_engine(*, yaml_path: Path) -> RuleEngine:
26
33
  return RuleEngine.from_config(config=config)
27
34
 
28
35
 
36
+ def _run_python_validators(*, data: dict[str, Any], debug: bool = False) -> None:
37
+ """Run Python validators that handle Edit|Write tool calls.
38
+
39
+ Mirrors the dispatch pattern in ``commands/hook.py:_validate_bash_body``
40
+ but filters to validators whose ``should_run`` returns True for the
41
+ given Edit|Write input. YAML-rule blocks have already been handled
42
+ before this function is called.
43
+ """
44
+ from dev10x.validators import get_chain
45
+
46
+ inp = HookInput.from_dict(data=data)
47
+ for result in get_chain().run(inp=inp):
48
+ if debug:
49
+ import sys as _sys
50
+
51
+ print(
52
+ f"[DEBUG] Python validator blocked: {result}",
53
+ file=_sys.stderr,
54
+ )
55
+ result.emit()
56
+
57
+
29
58
  def validate_edit_write(
30
59
  *,
31
60
  data: dict[str, Any],
@@ -55,4 +84,6 @@ def validate_edit_write(
55
84
  )
56
85
  HookResult(message=match.message).emit()
57
86
 
87
+ _run_python_validators(data=data, debug=debug)
88
+
58
89
  sys.exit(0)
@@ -26,6 +26,7 @@ from dev10x.domain.session_document import (
26
26
  from dev10x.hooks.session_dispatch import (
27
27
  build_autonomy_reassurance_context, # noqa: F401 — re-exported via __all__
28
28
  build_guidance_context, # noqa: F401 — re-exported via __all__
29
+ build_hook_version_drift_context, # noqa: F401 — re-exported via __all__
29
30
  build_install_check_context, # noqa: F401 — re-exported via __all__
30
31
  build_reload_context, # noqa: F401 — re-exported via __all__
31
32
  context_compact, # noqa: F401 — re-exported via __all__
@@ -73,6 +74,7 @@ def _format_decision_guidance(*, plan, friction_level):
73
74
  __all__ = [
74
75
  "build_autonomy_reassurance_context",
75
76
  "build_guidance_context",
77
+ "build_hook_version_drift_context",
76
78
  "build_install_check_context",
77
79
  "build_reload_context",
78
80
  "context_compact",
@@ -139,6 +139,44 @@ def build_install_check_context() -> str:
139
139
  return ""
140
140
 
141
141
 
142
+ def build_hook_version_drift_context() -> str:
143
+ """Warn when the running-hook version lags the latest installed version.
144
+
145
+ Claude Code loads hooks once at session start from ``$CLAUDE_PLUGIN_ROOT``.
146
+ An on-disk ``claude plugin update`` installs a newer version but does NOT
147
+ swap the running hooks — the session continues executing the pre-upgrade
148
+ hooks until it restarts. This means shipped friction fixes, new validators,
149
+ and catalog improvements are dormant in long-running sessions.
150
+
151
+ This check is **distinct** from :func:`build_install_check_context`, which
152
+ compares the installed version against the last-applied upgrade-cleanup
153
+ version. That check detects settings staleness; this one detects
154
+ running-hook staleness — they can diverge when settings were refreshed
155
+ but the session was not restarted.
156
+
157
+ Returns an empty string when no drift is detected or when either version
158
+ cannot be determined (``--plugin-dir`` dev installs, new users, etc.).
159
+ """
160
+ from dev10x.domain.install_version import (
161
+ read_latest_installed_version,
162
+ read_running_hook_version,
163
+ )
164
+
165
+ running = read_running_hook_version()
166
+ if running is None:
167
+ return ""
168
+ latest = read_latest_installed_version()
169
+ if latest is None:
170
+ return ""
171
+ if running == latest:
172
+ return ""
173
+ return (
174
+ f"Dev10x hooks running v{running} but v{latest} is installed on disk.\n"
175
+ "Restart this session (or run `/Dev10x:upgrade-cleanup`) to activate "
176
+ "shipped friction fixes, validators, and catalog improvements."
177
+ )
178
+
179
+
142
180
  def session_install_check() -> None:
143
181
  """Emit install-state guidance as additionalContext (SessionStart hook)."""
144
182
  content = build_install_check_context()
@@ -233,6 +271,7 @@ def session_goodbye(data: dict | None = None) -> None:
233
271
 
234
272
 
235
273
  __all__ = [
274
+ "build_hook_version_drift_context",
236
275
  "build_install_check_context",
237
276
  "build_reload_context",
238
277
  "build_autonomy_reassurance_context",
@@ -0,0 +1,76 @@
1
+ """Shared FastMCP application instance for the Dev10x CLI server.
2
+
3
+ Split out of server_cli.py (GH-243/A6) so per-domain tool modules
4
+ register against one server without a circular import.
5
+
6
+ GH-341: A lifespan context manager starts the knowledge-resource file
7
+ watcher and wires it to the active MCP session so that
8
+ ``notifications/resources/list_changed`` and
9
+ ``notifications/resources/updated`` are emitted to clients whenever the
10
+ underlying files change.
11
+
12
+ GH-344: The same lifespan also creates a :class:`ClientRootsManager`
13
+ that fetches and caches the client-declared directory roots via
14
+ ``roots/list`` on session initialisation and refreshes the cache on
15
+ ``notifications/roots/list_changed``. The roots are used to scope
16
+ CWD/worktree operations, complementing the GH-979 effective-CWD
17
+ discipline.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import logging
24
+ from collections.abc import AsyncIterator
25
+ from contextlib import asynccontextmanager
26
+
27
+ from mcp.server.fastmcp import FastMCP
28
+
29
+ from dev10x.mcp.resource_watcher import KnowledgeResourceWatcher, wire_watcher_to_server
30
+ from dev10x.mcp.roots_manager import ClientRootsManager, wire_roots_to_server
31
+ from dev10x.subprocess_utils import get_plugin_root
32
+
33
+ log = logging.getLogger(__name__)
34
+
35
+
36
+ @asynccontextmanager
37
+ async def _server_lifespan(app: FastMCP) -> AsyncIterator[None]:
38
+ """Lifespan that wires the knowledge-resource watcher and roots manager.
39
+
40
+ On server start:
41
+
42
+ 1. Creates a :class:`~dev10x.mcp.resource_watcher.KnowledgeResourceWatcher`
43
+ rooted at the plugin install directory and registers its
44
+ ``InitializedNotification`` handler (GH-341).
45
+ 2. Creates a :class:`~dev10x.mcp.roots_manager.ClientRootsManager` and
46
+ registers its ``InitializedNotification`` handler (chained after the
47
+ watcher's) plus a ``RootsListChangedNotification`` handler (GH-344).
48
+ 3. Starts the resource-watcher poll loop as a background asyncio task.
49
+
50
+ On server shutdown (lifespan exit) the background task is cancelled.
51
+ """
52
+ plugin_root = get_plugin_root()
53
+ watcher = KnowledgeResourceWatcher(plugin_root=plugin_root)
54
+
55
+ # GH-341: resource watcher registers the first InitializedNotification handler.
56
+ wire_watcher_to_server(server=app._mcp_server, watcher=watcher)
57
+
58
+ # GH-344: roots manager chains its handler after the watcher's.
59
+ roots_manager = ClientRootsManager()
60
+ wire_roots_to_server(server=app._mcp_server, manager=roots_manager)
61
+
62
+ task = asyncio.create_task(watcher.run(), name="dev10x-resource-watcher")
63
+ log.debug("Resource watcher task started for root: %s", plugin_root)
64
+
65
+ try:
66
+ yield
67
+ finally:
68
+ task.cancel()
69
+ try:
70
+ await task
71
+ except asyncio.CancelledError:
72
+ pass
73
+ log.debug("Resource watcher task stopped")
74
+
75
+
76
+ server = FastMCP(name="Dev10x-cli", lifespan=_server_lifespan)