threadkeeper 0.18.3__tar.gz → 0.18.4__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 (273) hide show
  1. {threadkeeper-0.18.3/threadkeeper.egg-info → threadkeeper-0.18.4}/PKG-INFO +30 -8
  2. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/README.md +28 -6
  3. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/pyproject.toml +5 -7
  4. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_error_paths.py +1 -1
  5. threadkeeper-0.18.4/tests/test_evolve_apply_43.py +97 -0
  6. threadkeeper-0.18.4/tests/test_evolve_apply_45.py +107 -0
  7. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_mcp_resources_prompts.py +3 -4
  8. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_mcp_sdk_compat.py +4 -5
  9. threadkeeper-0.18.4/tests/test_mcp_skills_extension.py +183 -0
  10. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_release_workflows.py +7 -11
  11. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/_mcp.py +47 -24
  12. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/brief.py +106 -0
  13. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/elicitation.py +1 -2
  14. threadkeeper-0.18.4/threadkeeper/mcp_skills.py +327 -0
  15. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/dialectic.py +3 -1
  16. {threadkeeper-0.18.3 → threadkeeper-0.18.4/threadkeeper.egg-info}/PKG-INFO +30 -8
  17. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper.egg-info/SOURCES.txt +4 -0
  18. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper.egg-info/requires.txt +1 -1
  19. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/LICENSE +0 -0
  20. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/setup.cfg +0 -0
  21. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_adapters.py +0 -0
  22. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_agent_status.py +0 -0
  23. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_antigravity_ingest.py +0 -0
  24. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_auto_update.py +0 -0
  25. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_backup.py +0 -0
  26. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_brief_footprint.py +0 -0
  27. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_brief_sections.py +0 -0
  28. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_candidate_reviewer.py +0 -0
  29. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_ci_parallel.py +0 -0
  30. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_concepts.py +0 -0
  31. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_config_settings.py +0 -0
  32. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_config_watcher.py +0 -0
  33. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_core_memory.py +0 -0
  34. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_curator.py +0 -0
  35. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_daemon_host_config.py +0 -0
  36. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_daemon_host_integration.py +0 -0
  37. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_daemon_liveness.py +0 -0
  38. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_daemon_state.py +0 -0
  39. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dashboard.py +0 -0
  40. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_db_compact.py +0 -0
  41. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_db_contention.py +0 -0
  42. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_db_leak_guard.py +0 -0
  43. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_db_migration_fts.py +0 -0
  44. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_db_runtime.py +0 -0
  45. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_db_schema.py +0 -0
  46. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_delegated_search.py +0 -0
  47. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic.py +0 -0
  48. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic_feed_tools.py +0 -0
  49. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic_miner.py +0 -0
  50. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic_observation_resolve.py +0 -0
  51. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic_recompute.py +0 -0
  52. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic_tier.py +0 -0
  53. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialectic_validator.py +0 -0
  54. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_dialog_fts_external.py +0 -0
  55. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_documentation_inventory.py +0 -0
  56. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_egress_policy.py +0 -0
  57. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_embed_routing.py +0 -0
  58. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_embedding_generation.py +0 -0
  59. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_embedding_model_revision.py +0 -0
  60. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_embedding_storage_dedup.py +0 -0
  61. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_eval_harness.py +0 -0
  62. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_evolve_applier.py +0 -0
  63. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_evolve_apply_2.py +0 -0
  64. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_evolve_apply_3.py +0 -0
  65. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_evolve_daemon.py +0 -0
  66. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_extract_daemon.py +0 -0
  67. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_extract_dedup.py +0 -0
  68. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_forget.py +0 -0
  69. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_github_budget.py +0 -0
  70. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_github_safety.py +0 -0
  71. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_helpers.py +0 -0
  72. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_hook_child_guard.py +0 -0
  73. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_host_embed.py +0 -0
  74. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_host_env.py +0 -0
  75. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_host_main.py +0 -0
  76. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_host_wedged.py +0 -0
  77. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_hybrid_retrieval.py +0 -0
  78. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_i18n_multilang.py +0 -0
  79. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_identity.py +0 -0
  80. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_ingest_cursor.py +0 -0
  81. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_ingest_denylist.py +0 -0
  82. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_ingest_secret_redaction.py +0 -0
  83. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_ingest_status.py +0 -0
  84. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_ingest_transactions.py +0 -0
  85. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_lesson_violations.py +0 -0
  86. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_lessons.py +0 -0
  87. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_memory_eval.py +0 -0
  88. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_memory_guard.py +0 -0
  89. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_memory_guard_host.py +0 -0
  90. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_menubar_app.py +0 -0
  91. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_missed_spawns.py +0 -0
  92. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_model_catalog.py +0 -0
  93. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_notify_daemon.py +0 -0
  94. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_nudges.py +0 -0
  95. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_onnx_embeddings.py +0 -0
  96. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_panel.py +0 -0
  97. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_permissions.py +0 -0
  98. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_pickup.py +0 -0
  99. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_probe_daemon.py +0 -0
  100. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_process_health.py +0 -0
  101. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_retention.py +0 -0
  102. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_review_prompts.py +0 -0
  103. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_search_fts_punctuation.py +0 -0
  104. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_session_read_purity.py +0 -0
  105. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_setup.py +0 -0
  106. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_shadow_review.py +0 -0
  107. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_single_flight_prompt_prefixes.py +0 -0
  108. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skill_hint.py +0 -0
  109. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skill_passive_tier.py +0 -0
  110. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skill_tier.py +0 -0
  111. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skill_updater.py +0 -0
  112. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skill_use_parser.py +0 -0
  113. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skill_watcher.py +0 -0
  114. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_skills.py +0 -0
  115. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_background_cwd.py +0 -0
  116. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_budget.py +0 -0
  117. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_child_env.py +0 -0
  118. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_codex_stdin.py +0 -0
  119. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_config.py +0 -0
  120. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_hint.py +0 -0
  121. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_permission_gate.py +0 -0
  122. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_reap.py +0 -0
  123. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_result.py +0 -0
  124. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_slim.py +0 -0
  125. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_watchdog.py +0 -0
  126. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_worktree_isolation.py +0 -0
  127. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_spawn_wrap.py +0 -0
  128. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_sync_capture.py +0 -0
  129. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_sync_identity.py +0 -0
  130. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_sync_migrate.py +0 -0
  131. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_sync_protocol.py +0 -0
  132. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_sync_reset_node.py +0 -0
  133. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_sync_transport.py +0 -0
  134. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_task_retention.py +0 -0
  135. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_task_spool.py +0 -0
  136. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_thin_session.py +0 -0
  137. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_thread_janitor.py +0 -0
  138. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_threads.py +0 -0
  139. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_tool_annotations.py +0 -0
  140. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_tools_smoke.py +0 -0
  141. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_validate_threads.py +0 -0
  142. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_vec_search.py +0 -0
  143. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/tests/test_verify_ingest.py +0 -0
  144. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/__init__.py +0 -0
  145. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/_setup.py +0 -0
  146. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/_spawn_wrap.py +0 -0
  147. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/__init__.py +0 -0
  148. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/_hook_helpers.py +0 -0
  149. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/antigravity.py +0 -0
  150. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/base.py +0 -0
  151. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/claude_code.py +0 -0
  152. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/claude_desktop.py +0 -0
  153. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/codex.py +0 -0
  154. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/copilot.py +0 -0
  155. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/adapters/vscode.py +0 -0
  156. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/agent_metadata.py +0 -0
  157. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/agent_status.py +0 -0
  158. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  159. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  160. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  161. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  162. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  163. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/auto_update.py +0 -0
  164. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/backup.py +0 -0
  165. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/candidate_reviewer.py +0 -0
  166. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/config.py +0 -0
  167. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/config_io.py +0 -0
  168. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/config_watcher.py +0 -0
  169. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/curator.py +0 -0
  170. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/curator_snapshots.py +0 -0
  171. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/daemon_liveness.py +0 -0
  172. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/daemon_state.py +0 -0
  173. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/daemon_supervisor.py +0 -0
  174. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/db.py +0 -0
  175. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/dialectic_miner.py +0 -0
  176. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/dialectic_validator.py +0 -0
  177. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/egress.py +0 -0
  178. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/embeddings.py +0 -0
  179. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/eval/__init__.py +0 -0
  180. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/eval/__main__.py +0 -0
  181. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/eval/harness.py +0 -0
  182. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/evolve_applier.py +0 -0
  183. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/evolve_daemon.py +0 -0
  184. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/extract_daemon.py +0 -0
  185. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/forget.py +0 -0
  186. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/github_budget.py +0 -0
  187. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/github_safety.py +0 -0
  188. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/harvest.py +0 -0
  189. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/helpers.py +0 -0
  190. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/host.py +0 -0
  191. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/host_embed.py +0 -0
  192. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/i18n.py +0 -0
  193. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/identity.py +0 -0
  194. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/ingest.py +0 -0
  195. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/lesson_violations.py +0 -0
  196. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/lessons.py +0 -0
  197. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/link_health.py +0 -0
  198. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/memory_guard.py +0 -0
  199. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/menubar_app.py +0 -0
  200. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/migrate_embeddings.py +0 -0
  201. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/model_catalog.py +0 -0
  202. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/notify.py +0 -0
  203. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/nudges.py +0 -0
  204. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/permissions.py +0 -0
  205. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/probe_daemon.py +0 -0
  206. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/process_health.py +0 -0
  207. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/retention.py +0 -0
  208. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/retrieval.py +0 -0
  209. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/review_prompts.py +0 -0
  210. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/search_proxy.py +0 -0
  211. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/server.py +0 -0
  212. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/shadow_review.py +0 -0
  213. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/skill_audit.py +0 -0
  214. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/skill_updater.py +0 -0
  215. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/skill_watcher.py +0 -0
  216. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/spawn_budget.py +0 -0
  217. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/spawn_config.py +0 -0
  218. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/spawn_result.py +0 -0
  219. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/__init__.py +0 -0
  220. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/capture.py +0 -0
  221. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/daemon.py +0 -0
  222. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/identity.py +0 -0
  223. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/migrate.py +0 -0
  224. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/protocol.py +0 -0
  225. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/reset_node.py +0 -0
  226. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/sync/server.py +0 -0
  227. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/task_spool.py +0 -0
  228. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/thread_janitor.py +0 -0
  229. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tool_schemas.py +0 -0
  230. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/__init__.py +0 -0
  231. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/agent_status.py +0 -0
  232. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/candidate_reviewer.py +0 -0
  233. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/concepts.py +0 -0
  234. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/config_watch.py +0 -0
  235. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/consolidate.py +0 -0
  236. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/core_memory.py +0 -0
  237. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/correlation.py +0 -0
  238. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/curator.py +0 -0
  239. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/dashboard.py +0 -0
  240. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/db_maintenance.py +0 -0
  241. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/dialectic_feed.py +0 -0
  242. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/dialog.py +0 -0
  243. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/distill.py +0 -0
  244. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/evolve_applier.py +0 -0
  245. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/evolve_research.py +0 -0
  246. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/extract.py +0 -0
  247. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/forget.py +0 -0
  248. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/graph.py +0 -0
  249. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/invariants.py +0 -0
  250. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/lessons.py +0 -0
  251. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/memory_guard.py +0 -0
  252. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/missed_spawns.py +0 -0
  253. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/panel.py +0 -0
  254. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/peers.py +0 -0
  255. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/pickup.py +0 -0
  256. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/probes.py +0 -0
  257. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/process_health.py +0 -0
  258. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/prompts.py +0 -0
  259. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/resources.py +0 -0
  260. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/session.py +0 -0
  261. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/shadow_review.py +0 -0
  262. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/skills.py +0 -0
  263. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/spawn.py +0 -0
  264. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/style.py +0 -0
  265. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/sync.py +0 -0
  266. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/threads.py +0 -0
  267. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/tools/validate.py +0 -0
  268. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/trash.py +0 -0
  269. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/verify_ingest.py +0 -0
  270. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper/wikilinks.py +0 -0
  271. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper.egg-info/dependency_links.txt +0 -0
  272. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper.egg-info/entry_points.txt +0 -0
  273. {threadkeeper-0.18.3 → threadkeeper-0.18.4}/threadkeeper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.18.3
3
+ Version: 0.18.4
4
4
  Summary: Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Copilot, and VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server.
5
5
  Author: thread-keeper contributors
6
6
  License: MIT
@@ -22,7 +22,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
22
  Requires-Python: >=3.11
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
- Requires-Dist: mcp<3,>=1.10.0
25
+ Requires-Dist: mcp<3,>=2.2.0
26
26
  Requires-Dist: pydantic>=2.13.4
27
27
  Requires-Dist: pydantic-settings>=2.14.2
28
28
  Requires-Dist: pyyaml>=6.0.3
@@ -217,13 +217,12 @@ Cline, … — so a single registration there reaches all of them at once.
217
217
  Adding a new CLI = one file under `threadkeeper/adapters/` implementing
218
218
  the `CLIAdapter` contract. See [CONTRIBUTING.md](CONTRIBUTING.md).
219
219
 
220
- ### Python MCP SDK compatibility
220
+ ### Python MCP SDK surface
221
221
 
222
- thread-keeper supports MCP Python SDK 1.x and 2.x (`mcp>=1.10.0,<3`). Fresh
223
- installs resolve 2.x. A small internal adapter uses the SDK 2.x `MCPServer`
224
- name and falls back to its SDK 1.x `FastMCP` predecessor, so tools, resources,
225
- prompts, annotations, structured content, elicitation, and stdio behavior keep
226
- the same public contract across both supported SDK majors.
222
+ thread-keeper requires MCP Python SDK 2.2 or later (`mcp>=2.2.0,<3`). Its
223
+ stable Skills extension uses the MCP 2026-07-28 extension surface, while tools,
224
+ resources, prompts, annotations, structured content, elicitation, and stdio
225
+ continue to use the same `MCPServer` instance.
227
226
 
228
227
  ### MCP primitives (tools, resources, prompts, elicitation)
229
228
 
@@ -263,6 +262,29 @@ Hosts without a capability fall back to the SessionStart hook plus the `brief()`
263
262
  regression. Static URIs only for now (resource *templates* with `{param}` are
264
263
  still unevenly supported across hosts).
265
264
 
265
+ ### MCP Skills extension
266
+
267
+ On MCP 2026-07-28 hosts, thread-keeper also advertises the stable
268
+ `io.modelcontextprotocol/skills` extension. `skills/list` pages the canonical
269
+ library deterministically, and `skills/get` returns the full manifest for one
270
+ skill. Each resource URI is origin-qualified, for example
271
+ `skill://thread-keeper/release-check/SKILL.md`, so hosts retain the server
272
+ identity alongside a possibly colliding skill name.
273
+
274
+ The manifest covers `SKILL.md` and every permitted file below `references/`,
275
+ `templates/`, `scripts/`, or `assets/`, with its byte length and a
276
+ `sha256:<hex>` digest. Files are individually available through ordinary
277
+ `resources/read`; traversal, undeclared files, and reads over 1 MiB are
278
+ rejected. Hosts should compare both size and digest before using a retrieved
279
+ file. The canonical directory is published once; the existing per-CLI
280
+ filesystem mirrors remain the fallback for clients that do not implement the
281
+ extension.
282
+
283
+ Discovery and `resources/read` are delivery only. Reading `SKILL.md` does not
284
+ activate it, approve its tools, record use telemetry, or approve supporting
285
+ files. Activation and any user approval remain responsibilities of the host's
286
+ skill-loading path.
287
+
266
288
  ### Memory egress (cross-provider privacy)
267
289
 
268
290
  thread-keeper is "one user model … shared across CLIs," and that sharing is by
@@ -173,13 +173,12 @@ Cline, … — so a single registration there reaches all of them at once.
173
173
  Adding a new CLI = one file under `threadkeeper/adapters/` implementing
174
174
  the `CLIAdapter` contract. See [CONTRIBUTING.md](CONTRIBUTING.md).
175
175
 
176
- ### Python MCP SDK compatibility
176
+ ### Python MCP SDK surface
177
177
 
178
- thread-keeper supports MCP Python SDK 1.x and 2.x (`mcp>=1.10.0,<3`). Fresh
179
- installs resolve 2.x. A small internal adapter uses the SDK 2.x `MCPServer`
180
- name and falls back to its SDK 1.x `FastMCP` predecessor, so tools, resources,
181
- prompts, annotations, structured content, elicitation, and stdio behavior keep
182
- the same public contract across both supported SDK majors.
178
+ thread-keeper requires MCP Python SDK 2.2 or later (`mcp>=2.2.0,<3`). Its
179
+ stable Skills extension uses the MCP 2026-07-28 extension surface, while tools,
180
+ resources, prompts, annotations, structured content, elicitation, and stdio
181
+ continue to use the same `MCPServer` instance.
183
182
 
184
183
  ### MCP primitives (tools, resources, prompts, elicitation)
185
184
 
@@ -219,6 +218,29 @@ Hosts without a capability fall back to the SessionStart hook plus the `brief()`
219
218
  regression. Static URIs only for now (resource *templates* with `{param}` are
220
219
  still unevenly supported across hosts).
221
220
 
221
+ ### MCP Skills extension
222
+
223
+ On MCP 2026-07-28 hosts, thread-keeper also advertises the stable
224
+ `io.modelcontextprotocol/skills` extension. `skills/list` pages the canonical
225
+ library deterministically, and `skills/get` returns the full manifest for one
226
+ skill. Each resource URI is origin-qualified, for example
227
+ `skill://thread-keeper/release-check/SKILL.md`, so hosts retain the server
228
+ identity alongside a possibly colliding skill name.
229
+
230
+ The manifest covers `SKILL.md` and every permitted file below `references/`,
231
+ `templates/`, `scripts/`, or `assets/`, with its byte length and a
232
+ `sha256:<hex>` digest. Files are individually available through ordinary
233
+ `resources/read`; traversal, undeclared files, and reads over 1 MiB are
234
+ rejected. Hosts should compare both size and digest before using a retrieved
235
+ file. The canonical directory is published once; the existing per-CLI
236
+ filesystem mirrors remain the fallback for clients that do not implement the
237
+ extension.
238
+
239
+ Discovery and `resources/read` are delivery only. Reading `SKILL.md` does not
240
+ activate it, approve its tools, record use telemetry, or approve supporting
241
+ files. Activation and any user approval remain responsibilities of the host's
242
+ skill-loading path.
243
+
222
244
  ### Memory egress (cross-provider privacy)
223
245
 
224
246
  thread-keeper is "one user model … shared across CLIs," and that sharing is by
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.18.3"
7
+ version = "0.18.4"
8
8
  description = "Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Copilot, and VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server."
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "thread-keeper contributors" }]
@@ -26,12 +26,10 @@ classifiers = [
26
26
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
27
  ]
28
28
  dependencies = [
29
- # >=1.10.0 for the MCP 2025-06-18 tool vocabulary: ToolAnnotations
30
- # (readOnly/destructive/idempotent hints) + structured outputSchema (#67).
31
- # Supports the maintained SDK 1.x and 2.x lines. The narrow adapter in
32
- # _mcp.py maps the 1.x FastMCP name to the 2.x MCPServer name; <3 keeps a
33
- # future unreviewed major from changing the server at install time.
34
- "mcp>=1.10.0,<3",
29
+ # MCP 2.2 provides the 2026-07-28 extension surface used for the stable
30
+ # Skills distribution protocol. <3 keeps a future unreviewed major from
31
+ # changing the server at install time.
32
+ "mcp>=2.2.0,<3",
35
33
  "pydantic>=2.13.4",
36
34
  "pydantic-settings>=2.14.2",
37
35
  "pyyaml>=6.0.3",
@@ -2,7 +2,7 @@
2
2
  on bad input rather than raising Python exceptions.
3
3
 
4
4
  A future error-boundary refactor (a `@safe_tool` decorator, schema validation,
5
- or a top-level FastMCP wrapper) must keep these tests green.
5
+ or a top-level MCPServer wrapper) must keep these tests green.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
@@ -0,0 +1,97 @@
1
+ """Golden coverage for surfacing the surgical lesson_patch action in brief().
2
+
3
+ The format should teach a fresh agent to make a narrow lesson correction with
4
+ the atomic operation rather than overwrite an otherwise-correct lesson body.
5
+ The seeded fixture also pins nearby long-lived brief sections.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+
13
+
14
+ _FAKE_CID = "eeee3333-4444-5555-6666-777788889999"
15
+
16
+
17
+ def _bootstrap(tmp_path, monkeypatch):
18
+ env = {
19
+ "THREADKEEPER_DB": str(tmp_path / "db.sqlite"),
20
+ "THREADKEEPER_LESSONS": str(tmp_path / "lessons.md"),
21
+ "CLAUDE_PROJECTS_DIR": str(tmp_path / "fake_claude_projects"),
22
+ "THREADKEEPER_INGEST_INTERVAL_S": "0",
23
+ "THREADKEEPER_INGEST_CAP": "0",
24
+ "THREADKEEPER_SKILL_WATCH_INTERVAL_S": "0",
25
+ "THREADKEEPER_SPAWN_BUDGET_POLL_S": "0",
26
+ "THREADKEEPER_SEARCH_PROXY_POLL_S": "0",
27
+ "THREADKEEPER_MEMORY_GUARD_POLL_S": "0",
28
+ "THREADKEEPER_SHADOW_REVIEW_INTERVAL_S": "0",
29
+ "THREADKEEPER_CURATOR_INTERVAL_S": "0",
30
+ "THREADKEEPER_EXTRACT_INTERVAL_S": "0",
31
+ "THREADKEEPER_CANDIDATE_REVIEW_INTERVAL_S": "0",
32
+ "THREADKEEPER_PROBE_INTERVAL_S": "0",
33
+ "THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S": "0",
34
+ "THREADKEEPER_DISABLE_BG_DAEMONS": "1",
35
+ "THREADKEEPER_TASK_LOG_DIR": str(tmp_path / "tasks"),
36
+ "THREADKEEPER_CLIENT": "pytest",
37
+ "THREADKEEPER_FORCE_CID": _FAKE_CID,
38
+ "THREADKEEPER_NO_EMBEDDINGS": "1",
39
+ }
40
+ for key, value in env.items():
41
+ monkeypatch.setenv(key, value)
42
+ Path(env["CLAUDE_PROJECTS_DIR"]).mkdir(parents=True, exist_ok=True)
43
+ for name in [m for m in list(sys.modules) if m.startswith("threadkeeper")]:
44
+ del sys.modules[name]
45
+ import threadkeeper.server # noqa: F401
46
+ from threadkeeper import _mcp, db
47
+
48
+ return {"mcp": _mcp.mcp, "db": db}
49
+
50
+
51
+ def _tool(pkg, name):
52
+ return pkg["mcp"]._tool_manager._tools[name].fn
53
+
54
+
55
+ def _add_evolve(conn, suggestion):
56
+ conn.execute(
57
+ "INSERT INTO evolve (suggestion, applied, status, created_at) "
58
+ "VALUES (?,?,?,?)",
59
+ (suggestion, 0, "promoted", int(time.time())),
60
+ )
61
+ conn.commit()
62
+
63
+
64
+ def test_brief_surfaces_lesson_patch_and_preserves_existing_sections(
65
+ tmp_path, monkeypatch,
66
+ ):
67
+ pkg = _bootstrap(tmp_path, monkeypatch)
68
+ conn = pkg["db"].get_db()
69
+ lesson_append = _tool(pkg, "lesson_append")
70
+ open_thread = _tool(pkg, "open_thread")
71
+
72
+ lesson_append(
73
+ title="repair stale cross-links",
74
+ body="Use the current cross-link target.",
75
+ summary="Keep links current.",
76
+ source="foreground",
77
+ )
78
+ open_thread(question="a seeded open thread that must survive")
79
+ _add_evolve(conn, "a promoted suggestion that must surface")
80
+
81
+ from threadkeeper.brief import render_brief
82
+
83
+ text = render_brief(conn)
84
+
85
+ # New: a precise, atomic lesson-edit action is available at session start.
86
+ assert "lesson_patch" in text
87
+ assert "lesson_patch(slug, old_string, new_string)" in text
88
+ assert "unique lesson-body substring" in text
89
+
90
+ # Existing: the added section leaves the surrounding brief intact.
91
+ assert "open" in text
92
+ assert "a seeded open thread that must survive" in text
93
+ assert "evolve_pending" in text
94
+ assert "★" in text
95
+ assert "a promoted suggestion that must surface" in text
96
+ assert "user-facing" in text
97
+ assert "Do NOT cite internal IDs" in text
@@ -0,0 +1,107 @@
1
+ """Golden coverage for lesson recommendation reconciliation in brief()."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ import time
6
+ from pathlib import Path
7
+
8
+
9
+ _FAKE_CID = "bbbb3333-4444-5555-6666-777788889999"
10
+ _OPEN_Q = "seeded open thread remains visible"
11
+ _OLD_LESSON = "aws-sso-preflight-uses-sts"
12
+ _NEW_LESSON = "aws-sso-token-freshness-sts-is-cached"
13
+ _COMMAND = "aws sts get-caller-identity"
14
+
15
+
16
+ def _bootstrap(tmp_path, monkeypatch):
17
+ env = {
18
+ "THREADKEEPER_DB": str(tmp_path / "db.sqlite"),
19
+ "CLAUDE_PROJECTS_DIR": str(tmp_path / "fake_claude_projects"),
20
+ "THREADKEEPER_LESSONS": str(tmp_path / "lessons.md"),
21
+ "THREADKEEPER_DISABLE_BG_DAEMONS": "1",
22
+ "THREADKEEPER_INGEST_INTERVAL_S": "0",
23
+ "THREADKEEPER_INGEST_CAP": "0",
24
+ "THREADKEEPER_SKILL_WATCH_INTERVAL_S": "0",
25
+ "THREADKEEPER_SPAWN_BUDGET_POLL_S": "0",
26
+ "THREADKEEPER_SEARCH_PROXY_POLL_S": "0",
27
+ "THREADKEEPER_MEMORY_GUARD_POLL_S": "0",
28
+ "THREADKEEPER_SHADOW_REVIEW_INTERVAL_S": "0",
29
+ "THREADKEEPER_CURATOR_INTERVAL_S": "0",
30
+ "THREADKEEPER_EXTRACT_INTERVAL_S": "0",
31
+ "THREADKEEPER_CANDIDATE_REVIEW_INTERVAL_S": "0",
32
+ "THREADKEEPER_PROBE_INTERVAL_S": "0",
33
+ "THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S": "0",
34
+ "THREADKEEPER_THREAD_JANITOR_INTERVAL_S": "0",
35
+ "THREADKEEPER_BRIEF_LEAN": "0",
36
+ "THREADKEEPER_TASK_LOG_DIR": str(tmp_path / "tasks"),
37
+ "THREADKEEPER_CLIENT": "pytest",
38
+ "THREADKEEPER_FORCE_CID": _FAKE_CID,
39
+ "THREADKEEPER_NO_EMBEDDINGS": "1",
40
+ }
41
+ for key, value in env.items():
42
+ monkeypatch.setenv(key, value)
43
+ Path(env["CLAUDE_PROJECTS_DIR"]).mkdir(parents=True, exist_ok=True)
44
+ for name in [module for module in list(sys.modules) if module.startswith("threadkeeper")]:
45
+ del sys.modules[name]
46
+ import threadkeeper.server # noqa: F401
47
+ from threadkeeper import _mcp, db, identity, lessons
48
+ return {"mcp": _mcp.mcp, "db": db, "identity": identity, "lessons": lessons}
49
+
50
+
51
+ def _tool(pkg, name):
52
+ return pkg["mcp"]._tool_manager._tools[name].fn
53
+
54
+
55
+ def _add_evolve(conn, suggestion):
56
+ conn.execute(
57
+ "INSERT INTO evolve (suggestion, applied, status, created_at) "
58
+ "VALUES (?,?,?,?)",
59
+ (suggestion, 0, "promoted", int(time.time())),
60
+ )
61
+ conn.commit()
62
+
63
+
64
+ def test_brief_surfaces_lesson_reconciliation_and_keeps_existing_sections(
65
+ tmp_path, monkeypatch,
66
+ ):
67
+ pkg = _bootstrap(tmp_path, monkeypatch)
68
+ conn = pkg["db"].get_db()
69
+
70
+ pkg["lessons"].append_lesson(
71
+ _OLD_LESSON,
72
+ body=(
73
+ f"Run `{_COMMAND}` to gate an AWS SSO freshness check before "
74
+ "using fixture-backed cloud tests."
75
+ ),
76
+ source="shadow",
77
+ )
78
+ pkg["lessons"].append_lesson(
79
+ _NEW_LESSON,
80
+ body=(
81
+ f"Never gate SSO freshness on `{_COMMAND}`: it is cached and can "
82
+ "return success after the token expires."
83
+ ),
84
+ source="shadow",
85
+ )
86
+
87
+ _tool(pkg, "open_thread")(question=_OPEN_Q)
88
+ _add_evolve(conn, "a promoted suggestion that must remain visible")
89
+
90
+ from threadkeeper.brief import render_brief
91
+ text = render_brief(conn)
92
+
93
+ # New behavior: a newer debunk flags the older recommendation by command
94
+ # and lesson slug, with a concrete reconciliation action.
95
+ assert "lesson_reconciliation" in text
96
+ assert f"`{_COMMAND}`" in text
97
+ assert f"debunked_by={_NEW_LESSON}" in text
98
+ assert f"still_recommended_by={_OLD_LESSON}" in text
99
+ assert "patch its recommendation or cross-link the correcting lesson" in text
100
+
101
+ # Regression: the established brief sections still survive this addition.
102
+ assert "open" in text
103
+ assert _OPEN_Q in text
104
+ assert "evolve_pending" in text
105
+ assert "★ \"a promoted suggestion that must remain visible\"" in text
106
+ assert "user-facing: paraphrase plain" in text
107
+ assert "Do NOT cite internal IDs" in text
@@ -158,10 +158,9 @@ def test_audit_threadkeeper_prompt_renders(fresh_mp):
158
158
  # ──────────────────────────────────────────────────────────────────────
159
159
 
160
160
  def test_server_advertises_resources_and_prompts_capabilities(fresh_mp):
161
- mcp = fresh_mp["mcp"]
162
- # MCP SDK 2.x renamed the high-level wrapper's low-level server handle.
163
- lowlevel = getattr(mcp, "_lowlevel_server", None) or mcp._mcp_server
164
- caps = lowlevel.get_capabilities(NotificationOptions(), {})
161
+ caps = fresh_mp["mcp"]._lowlevel_server.get_capabilities(
162
+ NotificationOptions(), {}, protocol_version="2026-07-28",
163
+ )
165
164
  assert caps.resources is not None
166
165
  assert caps.prompts is not None
167
166
  assert caps.tools is not None # tools unaffected
@@ -1,9 +1,8 @@
1
- """MCP SDK compatibility and stdio-server smoke coverage.
1
+ """MCP SDK 2.x stdio-server smoke coverage.
2
2
 
3
- The server keeps one narrow import adapter for MCP SDK 1.x and 2.x. This test
4
- starts the real ``python -m threadkeeper.server`` process and uses the legacy
5
- stdio handshake shared by both majors, then verifies the public tools registry
6
- is available. CI runs it once per supported SDK-major/Python combination.
3
+ The stable Skills extension requires MCP SDK 2.2 or later. This test starts
4
+ the real ``python -m threadkeeper.server`` process and verifies that the public
5
+ tools registry remains available over stdio on the supported SDK surface.
7
6
  """
8
7
  from __future__ import annotations
9
8
 
@@ -0,0 +1,183 @@
1
+ """Protocol-level coverage for the stable MCP Skills extension (#336)."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import hashlib
6
+ from types import SimpleNamespace
7
+
8
+ import pytest
9
+ from mcp.server.mcpserver.exceptions import ResourceNotFoundError
10
+ from mcp.shared.exceptions import MCPError
11
+
12
+
13
+ _STABLE_PROTOCOL = "2026-07-28"
14
+
15
+
16
+ def _tool(pkg, name):
17
+ return pkg["mcp"]._tool_manager._tools[name].fn
18
+
19
+
20
+ def _write_skill(pkg, name: str, *, extra_frontmatter: str = ""):
21
+ skill_dir = pkg["config"].CLAUDE_SKILLS_DIR / name
22
+ skill_dir.mkdir(parents=True, exist_ok=True)
23
+ (skill_dir / "SKILL.md").write_text(
24
+ "---\n"
25
+ f"name: {name}\n"
26
+ f"description: {name} description\n"
27
+ f"{extra_frontmatter}"
28
+ "---\n\n"
29
+ f"# {name}\n",
30
+ encoding="utf-8",
31
+ )
32
+ return skill_dir
33
+
34
+
35
+ def _call_extension(pkg, method: str, params):
36
+ entry = pkg["mcp"]._lowlevel_server._request_handlers[method]
37
+ return asyncio.run(
38
+ entry.handler(SimpleNamespace(protocol_version=_STABLE_PROTOCOL), params)
39
+ )
40
+
41
+
42
+ def test_discovery_advertises_stable_skills_extension_without_changing_core_contracts(fresh_mp):
43
+ from mcp.server.lowlevel.server import NotificationOptions
44
+ from threadkeeper.mcp_skills import SKILLS_EXTENSION_ID
45
+
46
+ server = fresh_mp["mcp"]._lowlevel_server
47
+ caps = server.get_capabilities(
48
+ NotificationOptions(), {}, protocol_version=_STABLE_PROTOCOL,
49
+ )
50
+
51
+ assert caps.extensions == {SKILLS_EXTENSION_ID: {}}
52
+ assert caps.resources is not None
53
+ assert caps.prompts is not None
54
+ assert caps.tools is not None
55
+ assert {"skills/list", "skills/get"} <= set(server._request_handlers)
56
+
57
+
58
+ def test_list_paginates_in_uri_order_and_keeps_the_server_origin_in_each_uri(fresh_mp):
59
+ from threadkeeper.mcp_skills import ListSkillsParams, SKILL_LIST_PAGE_SIZE
60
+
61
+ for index in range(SKILL_LIST_PAGE_SIZE + 1):
62
+ _write_skill(fresh_mp, f"page-{index:03d}")
63
+
64
+ first = _call_extension(fresh_mp, "skills/list", ListSkillsParams())
65
+ second = _call_extension(
66
+ fresh_mp, "skills/list", ListSkillsParams(cursor=first["nextCursor"]),
67
+ )
68
+
69
+ first_uris = [entry["uri"] for entry in first["skills"]]
70
+ second_uris = [entry["uri"] for entry in second["skills"]]
71
+ assert first["resultType"] == second["resultType"] == "complete"
72
+ assert len(first_uris) == SKILL_LIST_PAGE_SIZE
73
+ assert first_uris == sorted(first_uris)
74
+ assert first_uris + second_uris == sorted(first_uris + second_uris)
75
+ assert all(uri.startswith("skill://thread-keeper/") for uri in first_uris + second_uris)
76
+ # The authority is an origin namespace, so a same-named skill from another
77
+ # server cannot collapse into this server's entry.
78
+ assert "skill://another-server/page-000/SKILL.md" not in first_uris
79
+
80
+
81
+ def test_get_returns_complete_manifest_and_read_bytes_match_each_digest(fresh_mp):
82
+ from threadkeeper.mcp_skills import GetSkillParams, skill_catalog
83
+
84
+ skill_dir = _write_skill(
85
+ fresh_mp,
86
+ "manifest-skill",
87
+ extra_frontmatter="license: MIT\nmetadata:\n owner: thread-keeper\n",
88
+ )
89
+ reference = skill_dir / "references" / "guide.txt"
90
+ reference.parent.mkdir()
91
+ reference.write_bytes(b"reference bytes\n")
92
+
93
+ entry = next(item for item in skill_catalog() if item.name == "manifest-skill")
94
+ result = _call_extension(
95
+ fresh_mp, "skills/get", GetSkillParams(uri=entry.uri),
96
+ )
97
+
98
+ skill = result["skill"]
99
+ assert result["resultType"] == "complete"
100
+ assert skill["frontmatter"] == {
101
+ "name": "manifest-skill",
102
+ "description": "manifest-skill description",
103
+ "license": "MIT",
104
+ "metadata": {"owner": "thread-keeper"},
105
+ }
106
+ assert {item["uri"] for item in skill["resources"]} == {
107
+ item.uri for item in entry.files
108
+ }
109
+ listed_resources = {
110
+ str(resource.uri): resource
111
+ for resource in asyncio.run(fresh_mp["mcp"].list_resources())
112
+ }
113
+ listed_uris = set(listed_resources)
114
+ assert {item["uri"] for item in skill["resources"]} <= listed_uris
115
+ for file_entry in skill["resources"]:
116
+ assert listed_resources[file_entry["uri"]].size == file_entry["size"]
117
+ contents = asyncio.run(fresh_mp["mcp"].read_resource(file_entry["uri"]))
118
+ data = contents[0].content
119
+ assert isinstance(data, bytes)
120
+ assert len(data) == file_entry["size"]
121
+ assert f"sha256:{hashlib.sha256(data).hexdigest()}" == file_entry["digest"]
122
+
123
+
124
+ def test_get_rejects_unknown_or_non_skill_markdown_uri(fresh_mp):
125
+ from threadkeeper.mcp_skills import GetSkillParams
126
+
127
+ _write_skill(fresh_mp, "known-skill")
128
+ for uri in (
129
+ "skill://thread-keeper/known-skill/references/guide.md",
130
+ "skill://thread-keeper/missing/SKILL.md",
131
+ ):
132
+ with pytest.raises(MCPError) as raised:
133
+ _call_extension(fresh_mp, "skills/get", GetSkillParams(uri=uri))
134
+ assert raised.value.error.code == -32602
135
+
136
+
137
+ def test_resource_reads_reject_traversal_undeclared_and_oversized_files(fresh_mp):
138
+ from threadkeeper.mcp_skills import skill_catalog
139
+
140
+ skill_dir = _write_skill(fresh_mp, "safe-skill")
141
+ (skill_dir / "private.txt").write_text("not declared", encoding="utf-8")
142
+ bad_uris = (
143
+ "skill://thread-keeper/safe-skill/../SKILL.md",
144
+ "skill://thread-keeper/safe-skill/references/%2e%2e/SKILL.md",
145
+ "skill://thread-keeper/safe-skill/private.txt",
146
+ )
147
+ for uri in bad_uris:
148
+ with pytest.raises(ResourceNotFoundError):
149
+ asyncio.run(fresh_mp["mcp"].read_resource(uri))
150
+
151
+ large_dir = _write_skill(fresh_mp, "large-skill") / "references"
152
+ large_dir.mkdir()
153
+ (large_dir / "large.bin").write_bytes(b"x" * (1_048_576 + 1))
154
+ assert "large-skill" not in {entry.name for entry in skill_catalog()}
155
+ with pytest.raises(ResourceNotFoundError):
156
+ asyncio.run(
157
+ fresh_mp["mcp"].read_resource(
158
+ "skill://thread-keeper/large-skill/SKILL.md"
159
+ )
160
+ )
161
+
162
+
163
+ def test_skill_delivery_is_side_effect_free_and_filesystem_mirrors_remain_fallback(fresh_mp):
164
+ from threadkeeper.tools.skills import _mirror_targets
165
+
166
+ create = _tool(fresh_mp, "skill_manage")
167
+ assert create(
168
+ action="create",
169
+ name="fallback-skill",
170
+ description="Proves filesystem mirrors remain available.",
171
+ content="# Fallback\n",
172
+ ).startswith("ok path=")
173
+
174
+ targets = _mirror_targets("fallback-skill")
175
+ assert targets
176
+ assert all((target / "SKILL.md").exists() for target in targets)
177
+
178
+ conn = fresh_mp["db"].get_db()
179
+ before = conn.execute("SELECT COUNT(*) AS count FROM events").fetchone()["count"]
180
+ uri = "skill://thread-keeper/fallback-skill/SKILL.md"
181
+ assert asyncio.run(fresh_mp["mcp"].read_resource(uri))[0].content.startswith(b"---")
182
+ after = conn.execute("SELECT COUNT(*) AS count FROM events").fetchone()["count"]
183
+ assert after == before
@@ -98,29 +98,25 @@ def test_glama_dockerfile_pin_matches_the_project_release():
98
98
  assert f"threadkeeper=={version}" in DOCKERFILE.read_text()
99
99
 
100
100
 
101
- def test_mcp_requirement_supports_both_sdk_majors_through_one_adapter():
101
+ def test_mcp_requirement_uses_the_2x_skills_extension_surface():
102
102
  deps = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["dependencies"]
103
103
  mcp_req = next(d for d in deps if d.split(">")[0].split("<")[0].strip() == "mcp")
104
- assert mcp_req == "mcp>=1.10.0,<3"
104
+ assert mcp_req == "mcp>=2.2.0,<3"
105
105
 
106
- adapter = (ROOT / "threadkeeper" / "_mcp.py").read_text()
107
- assert "mcp.server.mcpserver" in adapter
108
- assert "mcp.server.fastmcp" in adapter
109
106
  for importer in (
107
+ ROOT / "threadkeeper" / "_mcp.py",
110
108
  ROOT / "threadkeeper" / "elicitation.py",
111
109
  ROOT / "threadkeeper" / "tools" / "dialectic.py",
112
110
  ):
113
111
  text = importer.read_text()
114
112
  assert "mcp.server.fastmcp" not in text
115
- assert "mcp.server.mcpserver" not in text
113
+ assert "mcp.server.mcpserver" in (ROOT / "threadkeeper" / "_mcp.py").read_text()
116
114
 
117
115
 
118
- def test_ci_matrix_covers_both_supported_mcp_sdk_majors():
116
+ def test_ci_matrix_covers_the_supported_mcp_sdk_surface():
119
117
  matrix = _workflow("test.yml")["jobs"]["pytest-shard"]["strategy"]["matrix"]
120
- majors = set(matrix["mcp"]) | {
121
- extra["mcp"] for extra in matrix.get("include", [])
122
- }
123
- assert majors == {">=1.10.0,<2", ">=2.0.0,<3"}
118
+ assert matrix["mcp"] == [">=2.2.0,<3"]
119
+ assert "include" not in matrix
124
120
  assert '"mcp${{ matrix.mcp }}"' in _workflow_text("test.yml")
125
121
 
126
122
  def test_ci_security_scanning_covers_code_and_resolved_dependencies():