threadkeeper 0.17.4__tar.gz → 0.17.6__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 (256) hide show
  1. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/PKG-INFO +21 -3
  2. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/README.md +20 -2
  3. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/pyproject.toml +1 -1
  4. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_adapters.py +1 -0
  5. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_candidate_reviewer.py +2 -0
  6. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_curator.py +127 -4
  7. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_lessons.py +135 -0
  8. threadkeeper-0.17.6/tests/test_setup.py +31 -0
  9. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_shadow_review.py +2 -0
  10. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_use_parser.py +2 -1
  11. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skills.py +36 -0
  12. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_slim.py +3 -0
  13. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_tool_annotations.py +6 -4
  14. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/_setup.py +8 -1
  15. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/codex.py +8 -0
  16. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/candidate_reviewer.py +10 -1
  17. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/curator.py +177 -7
  18. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/db.py +28 -0
  19. threadkeeper-0.17.6/threadkeeper/link_health.py +81 -0
  20. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/shadow_review.py +8 -1
  21. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/curator.py +40 -0
  22. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/lessons.py +210 -3
  23. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/skills.py +16 -3
  24. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/spawn.py +7 -1
  25. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/PKG-INFO +21 -3
  26. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/SOURCES.txt +2 -0
  27. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/LICENSE +0 -0
  28. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/setup.cfg +0 -0
  29. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_agent_status.py +0 -0
  30. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_auto_update.py +0 -0
  31. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_backup.py +0 -0
  32. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_brief_footprint.py +0 -0
  33. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_brief_sections.py +0 -0
  34. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_concepts.py +0 -0
  35. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_config_settings.py +0 -0
  36. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_config_watcher.py +0 -0
  37. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_core_memory.py +0 -0
  38. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_host_config.py +0 -0
  39. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_host_integration.py +0 -0
  40. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_liveness.py +0 -0
  41. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_state.py +0 -0
  42. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dashboard.py +0 -0
  43. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_compact.py +0 -0
  44. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_contention.py +0 -0
  45. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_migration_fts.py +0 -0
  46. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_runtime.py +0 -0
  47. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_schema.py +0 -0
  48. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_delegated_search.py +0 -0
  49. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic.py +0 -0
  50. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_feed_tools.py +0 -0
  51. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_miner.py +0 -0
  52. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_observation_resolve.py +0 -0
  53. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_recompute.py +0 -0
  54. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_tier.py +0 -0
  55. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_validator.py +0 -0
  56. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialog_fts_external.py +0 -0
  57. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_egress_policy.py +0 -0
  58. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embed_routing.py +0 -0
  59. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embedding_generation.py +0 -0
  60. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embedding_model_revision.py +0 -0
  61. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embedding_storage_dedup.py +0 -0
  62. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_error_paths.py +0 -0
  63. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_eval_harness.py +0 -0
  64. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_applier.py +0 -0
  65. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_apply_2.py +0 -0
  66. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_apply_3.py +0 -0
  67. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_daemon.py +0 -0
  68. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_extract_daemon.py +0 -0
  69. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_extract_dedup.py +0 -0
  70. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_forget.py +0 -0
  71. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_github_budget.py +0 -0
  72. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_github_safety.py +0 -0
  73. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_helpers.py +0 -0
  74. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_embed.py +0 -0
  75. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_env.py +0 -0
  76. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_main.py +0 -0
  77. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_wedged.py +0 -0
  78. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_hybrid_retrieval.py +0 -0
  79. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_i18n_multilang.py +0 -0
  80. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_identity.py +0 -0
  81. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_cursor.py +0 -0
  82. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_denylist.py +0 -0
  83. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_secret_redaction.py +0 -0
  84. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_status.py +0 -0
  85. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_transactions.py +0 -0
  86. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_mcp_resources_prompts.py +0 -0
  87. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_memory_eval.py +0 -0
  88. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_memory_guard.py +0 -0
  89. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_memory_guard_host.py +0 -0
  90. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_menubar_app.py +0 -0
  91. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_missed_spawns.py +0 -0
  92. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_model_catalog.py +0 -0
  93. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_notify_daemon.py +0 -0
  94. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_nudges.py +0 -0
  95. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_onnx_embeddings.py +0 -0
  96. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_panel.py +0 -0
  97. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_permissions.py +0 -0
  98. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_pickup.py +0 -0
  99. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_probe_daemon.py +0 -0
  100. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_process_health.py +0 -0
  101. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_release_workflows.py +0 -0
  102. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_retention.py +0 -0
  103. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_review_prompts.py +0 -0
  104. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_search_fts_punctuation.py +0 -0
  105. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_session_read_purity.py +0 -0
  106. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_single_flight_prompt_prefixes.py +0 -0
  107. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_hint.py +0 -0
  108. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_passive_tier.py +0 -0
  109. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_tier.py +0 -0
  110. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_updater.py +0 -0
  111. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_watcher.py +0 -0
  112. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_budget.py +0 -0
  113. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_codex_stdin.py +0 -0
  114. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_config.py +0 -0
  115. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_hint.py +0 -0
  116. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_permission_gate.py +0 -0
  117. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_reap.py +0 -0
  118. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_watchdog.py +0 -0
  119. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_worktree_isolation.py +0 -0
  120. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_wrap.py +0 -0
  121. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_capture.py +0 -0
  122. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_identity.py +0 -0
  123. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_migrate.py +0 -0
  124. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_protocol.py +0 -0
  125. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_reset_node.py +0 -0
  126. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_transport.py +0 -0
  127. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_task_retention.py +0 -0
  128. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_task_spool.py +0 -0
  129. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_thin_session.py +0 -0
  130. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_thread_janitor.py +0 -0
  131. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_threads.py +0 -0
  132. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_tools_smoke.py +0 -0
  133. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_validate_threads.py +0 -0
  134. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_vec_search.py +0 -0
  135. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_verify_ingest.py +0 -0
  136. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/__init__.py +0 -0
  137. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/_mcp.py +0 -0
  138. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/_spawn_wrap.py +0 -0
  139. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/__init__.py +0 -0
  140. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/_hook_helpers.py +0 -0
  141. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/antigravity.py +0 -0
  142. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/base.py +0 -0
  143. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/claude_code.py +0 -0
  144. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/claude_desktop.py +0 -0
  145. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/copilot.py +0 -0
  146. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/vscode.py +0 -0
  147. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/agent_metadata.py +0 -0
  148. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/agent_status.py +0 -0
  149. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  150. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  151. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  152. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  153. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  154. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/auto_update.py +0 -0
  155. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/backup.py +0 -0
  156. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/brief.py +0 -0
  157. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/config.py +0 -0
  158. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/config_io.py +0 -0
  159. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/config_watcher.py +0 -0
  160. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/curator_snapshots.py +0 -0
  161. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/daemon_liveness.py +0 -0
  162. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/daemon_state.py +0 -0
  163. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/daemon_supervisor.py +0 -0
  164. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/dialectic_miner.py +0 -0
  165. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/dialectic_validator.py +0 -0
  166. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/egress.py +0 -0
  167. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/elicitation.py +0 -0
  168. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/embeddings.py +0 -0
  169. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/eval/__init__.py +0 -0
  170. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/eval/__main__.py +0 -0
  171. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/eval/harness.py +0 -0
  172. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/evolve_applier.py +0 -0
  173. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/evolve_daemon.py +0 -0
  174. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/extract_daemon.py +0 -0
  175. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/forget.py +0 -0
  176. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/github_budget.py +0 -0
  177. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/github_safety.py +0 -0
  178. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/harvest.py +0 -0
  179. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/helpers.py +0 -0
  180. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/host.py +0 -0
  181. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/host_embed.py +0 -0
  182. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/i18n.py +0 -0
  183. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/identity.py +0 -0
  184. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/ingest.py +0 -0
  185. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/lessons.py +0 -0
  186. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/memory_guard.py +0 -0
  187. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/menubar_app.py +0 -0
  188. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/migrate_embeddings.py +0 -0
  189. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/model_catalog.py +0 -0
  190. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/notify.py +0 -0
  191. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/nudges.py +0 -0
  192. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/permissions.py +0 -0
  193. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/probe_daemon.py +0 -0
  194. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/process_health.py +0 -0
  195. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/retention.py +0 -0
  196. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/retrieval.py +0 -0
  197. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/review_prompts.py +0 -0
  198. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/search_proxy.py +0 -0
  199. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/server.py +0 -0
  200. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/skill_audit.py +0 -0
  201. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/skill_updater.py +0 -0
  202. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/skill_watcher.py +0 -0
  203. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/spawn_budget.py +0 -0
  204. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/spawn_config.py +0 -0
  205. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/__init__.py +0 -0
  206. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/capture.py +0 -0
  207. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/daemon.py +0 -0
  208. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/identity.py +0 -0
  209. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/migrate.py +0 -0
  210. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/protocol.py +0 -0
  211. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/reset_node.py +0 -0
  212. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/server.py +0 -0
  213. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/task_spool.py +0 -0
  214. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/thread_janitor.py +0 -0
  215. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tool_schemas.py +0 -0
  216. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/__init__.py +0 -0
  217. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/agent_status.py +0 -0
  218. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/candidate_reviewer.py +0 -0
  219. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/concepts.py +0 -0
  220. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/config_watch.py +0 -0
  221. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/consolidate.py +0 -0
  222. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/core_memory.py +0 -0
  223. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/correlation.py +0 -0
  224. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dashboard.py +0 -0
  225. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/db_maintenance.py +0 -0
  226. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dialectic.py +0 -0
  227. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dialectic_feed.py +0 -0
  228. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dialog.py +0 -0
  229. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/distill.py +0 -0
  230. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/evolve_applier.py +0 -0
  231. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/extract.py +0 -0
  232. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/forget.py +0 -0
  233. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/graph.py +0 -0
  234. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/invariants.py +0 -0
  235. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/memory_guard.py +0 -0
  236. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/missed_spawns.py +0 -0
  237. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/panel.py +0 -0
  238. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/peers.py +0 -0
  239. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/pickup.py +0 -0
  240. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/probes.py +0 -0
  241. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/process_health.py +0 -0
  242. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/prompts.py +0 -0
  243. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/resources.py +0 -0
  244. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/session.py +0 -0
  245. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/shadow_review.py +0 -0
  246. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/style.py +0 -0
  247. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/sync.py +0 -0
  248. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/threads.py +0 -0
  249. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/validate.py +0 -0
  250. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/trash.py +0 -0
  251. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/verify_ingest.py +0 -0
  252. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/wikilinks.py +0 -0
  253. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/dependency_links.txt +0 -0
  254. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/entry_points.txt +0 -0
  255. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/requires.txt +0 -0
  256. {threadkeeper-0.17.4 → threadkeeper-0.17.6}/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.17.4
3
+ Version: 0.17.6
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
@@ -137,6 +137,9 @@ config, copies hooks to
137
137
  each CLI's per-user instructions file (`CLAUDE.md` / `AGENTS.md` /
138
138
  `copilot-instructions.md` — Claude Desktop and VS Code
139
139
  have no global instructions file, so that step is skipped for them).
140
+ The generated MCP entry pins imports to this configured installation, so an
141
+ agent launched inside another thread-keeper checkout cannot load that checkout's
142
+ unmerged code against the live memory database.
140
143
 
141
144
  Restart your CLI of choice. Hook-capable clients inject a brief on the first
142
145
  message; hookless clients such as Codex and Antigravity CLI either follow the
@@ -217,7 +220,7 @@ read/act split, plus MCP elicitation for host-native confirmations:
217
220
 
218
221
  | Primitive | Control | What thread-keeper exposes | When to use |
219
222
  |---|---|---|---|
220
- | **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, … | the agent decides to call them |
223
+ | **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, `wikilink_health`, … | the agent decides to call them |
221
224
  | **Resources** | application-controlled, read-only | `memory://brief`, `memory://context`, `memory://dashboard`, `memory://agent-status` | the **host** attaches/pulls them automatically |
222
225
  | **Prompts** | user-controlled templates | `review_recent_threads`, `run_library_curation`, `audit_threadkeeper` | the user runs them (Claude Code: `/mcp__thread-keeper__<name>`) |
223
226
 
@@ -611,7 +614,14 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
611
614
  though an existing same-slug long lesson may be corrected without increasing
612
615
  its body size; near-duplicate slugs are blocked, and semantic body matches are
613
616
  routed to the incumbent lesson or surfaced for curation instead of minting a
614
- sibling lesson.
617
+ sibling lesson. A clear new directive or debunk also flags older permissive
618
+ lessons on the same concrete practice for patch, cross-link, or supersession
619
+ review.
620
+ Before a genuinely new fallback lesson is written,
621
+ `lesson_neighbors(title, body, summary, k=3)` shows the nearest existing lesson
622
+ slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
623
+ that preflight to patch/consolidate an incumbent or add a `[[slug]]` cross-link
624
+ to a related, distinct lesson while its body is still editable.
615
625
 
616
626
  #### 3. Extract daemon
617
627
 
@@ -716,6 +726,14 @@ non-forced direct `curator_review()` calls return `not_due` inside the
716
726
  configured interval and record that status without spawning. A manual
717
727
  `curator_review(force=True)` bypasses the interval but still respects the lock.
718
728
 
729
+ When a Curator reviews a lesson pair and deliberately keeps both, it records a
730
+ structured `keep_both` merge verdict with the two slugs and a short reason.
731
+ Later inventories show those prior verdicts and each lesson's current
732
+ bidirectional `[[wikilink]]` adjacency (`links=[...]`), including for the
733
+ relevant side of a multi-batch review. This preserves intentional
734
+ general/specific and prevention/recovery layering without making a child
735
+ re-read both lesson bodies to rediscover it.
736
+
719
737
  For automation-created skills, the audit keeps foreground consultation separate
720
738
  from maintenance: a background-review skill with `fg_uses=0` after 14 days is
721
739
  still a false-positive prune candidate even if automatic review or sync loops
@@ -93,6 +93,9 @@ config, copies hooks to
93
93
  each CLI's per-user instructions file (`CLAUDE.md` / `AGENTS.md` /
94
94
  `copilot-instructions.md` — Claude Desktop and VS Code
95
95
  have no global instructions file, so that step is skipped for them).
96
+ The generated MCP entry pins imports to this configured installation, so an
97
+ agent launched inside another thread-keeper checkout cannot load that checkout's
98
+ unmerged code against the live memory database.
96
99
 
97
100
  Restart your CLI of choice. Hook-capable clients inject a brief on the first
98
101
  message; hookless clients such as Codex and Antigravity CLI either follow the
@@ -173,7 +176,7 @@ read/act split, plus MCP elicitation for host-native confirmations:
173
176
 
174
177
  | Primitive | Control | What thread-keeper exposes | When to use |
175
178
  |---|---|---|---|
176
- | **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, … | the agent decides to call them |
179
+ | **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, `wikilink_health`, … | the agent decides to call them |
177
180
  | **Resources** | application-controlled, read-only | `memory://brief`, `memory://context`, `memory://dashboard`, `memory://agent-status` | the **host** attaches/pulls them automatically |
178
181
  | **Prompts** | user-controlled templates | `review_recent_threads`, `run_library_curation`, `audit_threadkeeper` | the user runs them (Claude Code: `/mcp__thread-keeper__<name>`) |
179
182
 
@@ -567,7 +570,14 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
567
570
  though an existing same-slug long lesson may be corrected without increasing
568
571
  its body size; near-duplicate slugs are blocked, and semantic body matches are
569
572
  routed to the incumbent lesson or surfaced for curation instead of minting a
570
- sibling lesson.
573
+ sibling lesson. A clear new directive or debunk also flags older permissive
574
+ lessons on the same concrete practice for patch, cross-link, or supersession
575
+ review.
576
+ Before a genuinely new fallback lesson is written,
577
+ `lesson_neighbors(title, body, summary, k=3)` shows the nearest existing lesson
578
+ slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
579
+ that preflight to patch/consolidate an incumbent or add a `[[slug]]` cross-link
580
+ to a related, distinct lesson while its body is still editable.
571
581
 
572
582
  #### 3. Extract daemon
573
583
 
@@ -672,6 +682,14 @@ non-forced direct `curator_review()` calls return `not_due` inside the
672
682
  configured interval and record that status without spawning. A manual
673
683
  `curator_review(force=True)` bypasses the interval but still respects the lock.
674
684
 
685
+ When a Curator reviews a lesson pair and deliberately keeps both, it records a
686
+ structured `keep_both` merge verdict with the two slugs and a short reason.
687
+ Later inventories show those prior verdicts and each lesson's current
688
+ bidirectional `[[wikilink]]` adjacency (`links=[...]`), including for the
689
+ relevant side of a multi-batch review. This preserves intentional
690
+ general/specific and prevention/recovery layering without making a child
691
+ re-read both lesson bodies to rediscover it.
692
+
675
693
  For automation-created skills, the audit keeps foreground consultation separate
676
694
  from maintenance: a background-review skill with `fg_uses=0` after 14 days is
677
695
  still a false-positive prune candidate even if automatic review or sync loops
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.17.4"
7
+ version = "0.17.6"
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" }]
@@ -383,6 +383,7 @@ def test_codex_spawn_argv_skips_git_repo_check(tmp_path, monkeypatch):
383
383
  assert argv[:3] == ["/usr/local/bin/codex", "exec", "--skip-git-repo-check"]
384
384
  assert argv[-1] == "-"
385
385
  assert "-m" in argv and "gpt-5.5" in argv
386
+ assert 'mcp_servers.thread-keeper.env.PYTHONSAFEPATH="1"' in argv
386
387
  # Default (non-bypass) path still sandboxes.
387
388
  assert "--sandbox" in argv and "workspace-write" in argv
388
389
 
@@ -203,6 +203,8 @@ def test_run_spawns_when_threshold_met(tmp_path, monkeypatch):
203
203
  assert "skill_manage" in allowed
204
204
  assert "accept_candidate" in allowed
205
205
  assert "reject_candidate" in allowed
206
+ assert "lesson_neighbors(title=<prospective>" in kw["prompt"]
207
+ assert "lesson_neighbors" in allowed
206
208
  assert "Bash" not in allowed
207
209
  # De-privileged (issue #76): no bare Read/Write/Edit — only the
208
210
  # path-scoped skill/lesson/candidate MCP tools.
@@ -152,15 +152,15 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
152
152
  conn.execute(
153
153
  "INSERT INTO skill_usage "
154
154
  "(name, created_at, created_by_origin, last_used_at, "
155
- " use_count, pinned, state) "
156
- "VALUES (?, ?, 'foreground', ?, 5, 1, 'active')",
155
+ " use_count, foreground_use_count, view_count, pinned, state) "
156
+ "VALUES (?, ?, 'foreground', ?, 5, 4, 3, 1, 'active')",
157
157
  ("pinned-skill", now - 86400, now - 3600),
158
158
  )
159
159
  conn.execute(
160
160
  "INSERT INTO skill_usage "
161
161
  "(name, created_at, created_by_origin, last_used_at, "
162
- " use_count, state) "
163
- "VALUES (?, ?, 'background_review', ?, 2, 'active')",
162
+ " use_count, foreground_use_count, view_count, state) "
163
+ "VALUES (?, ?, 'background_review', ?, 2, 1, 6, 'active')",
164
164
  ("auto-created-skill", now - 172800, now - 7200),
165
165
  )
166
166
  conn.commit()
@@ -177,6 +177,8 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
177
177
  # background_review skill (not pinned) is NOT protected
178
178
  assert "SKILL auto-created-skill [PROTECTED]" not in dump
179
179
  assert "SKILL auto-created-skill" in dump
180
+ assert "fg_uses=4 uses=5 views=3 maintenance_patches=0" in dump
181
+ assert "fg_uses=1 uses=2 views=6 maintenance_patches=0" in dump
180
182
  assert "STALE LESSONS (dry-run decay ranking)" in dump
181
183
 
182
184
 
@@ -279,6 +281,64 @@ def test_collect_inventory_preview_truncates_large_store(tmp_path, monkeypatch):
279
281
  assert "live curator pass reviews complete bounded batches" in dump
280
282
 
281
283
 
284
+ def test_curator_reuses_merge_verdicts_and_wikilink_adjacency(
285
+ tmp_path, monkeypatch,
286
+ ):
287
+ """A later pass receives durable keep-both context without body reads."""
288
+ pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
289
+ pkg["lessons"].append_lesson(
290
+ title="general-prevention",
291
+ body="The broad guard points to [[specific-recovery]].",
292
+ source="shadow",
293
+ )
294
+ pkg["lessons"].append_lesson(
295
+ title="specific-recovery",
296
+ body="The repair path points to [[general-prevention]].",
297
+ source="shadow",
298
+ )
299
+
300
+ import threadkeeper.tools.spawn as spawn_mod
301
+ captured: list[dict] = []
302
+
303
+ def fake_spawn(**kwargs):
304
+ captured.append(kwargs)
305
+ return f"spawn task_id=curator-{len(captured)} pid=0"
306
+
307
+ monkeypatch.setattr(spawn_mod, "spawn", fake_spawn)
308
+ assert "curator-1" in pkg["curator"].run_curator_pass(force=True)
309
+
310
+ from threadkeeper._mcp import mcp
311
+ verdict = mcp._tool_manager._tools["curator_merge_verdict"].fn
312
+ out = verdict(
313
+ "specific-recovery",
314
+ "general-prevention",
315
+ "prevention/recovery",
316
+ )
317
+ assert out == (
318
+ "ok merge_verdict=general-prevention,specific-recovery "
319
+ "decision=keep_both"
320
+ )
321
+
322
+ assert "curator-2" in pkg["curator"].run_curator_pass(force=True)
323
+ prompt = captured[-1]["prompt"]
324
+ assert "general-prevention" in prompt
325
+ assert "specific-recovery" in prompt
326
+ assert "links=[specific-recovery]" in prompt
327
+ assert "links=[general-prevention]" in prompt
328
+ assert "## PRIOR MERGE VERDICTS (n=1)" in prompt
329
+ assert "decision=keep_both reason=prevention/recovery" in prompt
330
+
331
+ conn = pkg["db"].get_db()
332
+ row = conn.execute(
333
+ "SELECT left_slug, right_slug, decision, reason "
334
+ "FROM curator_merge_verdicts"
335
+ ).fetchone()
336
+ assert tuple(row) == (
337
+ "general-prevention", "specific-recovery", "keep_both",
338
+ "prevention/recovery",
339
+ )
340
+
341
+
282
342
  # ──────────────────────────────────────────────────────────────────────
283
343
  # run_curator_pass — dispatch logic
284
344
  # ──────────────────────────────────────────────────────────────────────
@@ -362,6 +422,7 @@ def test_run_curator_pass_spawns_when_threshold_met(tmp_path, monkeypatch):
362
422
  assert "evolve_format" in allowed
363
423
  assert "Read" in allowed
364
424
  assert "curator_report_write" in allowed
425
+ assert "curator_merge_verdict" in allowed
365
426
  assert "Write" not in allowed
366
427
  assert "WebSearch" in allowed
367
428
  assert "WebFetch" in allowed
@@ -914,6 +975,68 @@ def test_skill_validate_tool_returns_post_change_contract(tmp_path, monkeypatch)
914
975
  }
915
976
 
916
977
 
978
+ def test_wikilink_health_reports_dangling_lesson_and_skill_links(
979
+ tmp_path, monkeypatch,
980
+ ):
981
+ pkg = _bootstrap(tmp_path, monkeypatch)
982
+ pkg["lessons"].append_lesson(
983
+ title="valid lesson",
984
+ body="Available target.",
985
+ source="shadow",
986
+ )
987
+ pkg["lessons"].append_lesson(
988
+ title="lesson source",
989
+ body="See [[valid-lesson]] and [[missing-lesson]].",
990
+ source="shadow",
991
+ )
992
+ _write_audit_skill(
993
+ pkg["skills_dir"], "skill-source",
994
+ "See [[valid-lesson]] and [[missing-skill]].",
995
+ )
996
+ conn = pkg["db"].get_db()
997
+
998
+ from threadkeeper.link_health import scan_wikilink_health
999
+
1000
+ result = scan_wikilink_health(conn)
1001
+
1002
+ assert result["summary"] == {
1003
+ "lessons_scanned": 2,
1004
+ "skills_scanned": 1,
1005
+ "references_scanned": 4,
1006
+ "dangling_references": 2,
1007
+ }
1008
+ assert result["dangling_references"] == [
1009
+ {
1010
+ "source_kind": "lesson",
1011
+ "source": "lesson-source",
1012
+ "target": "missing-lesson",
1013
+ },
1014
+ {
1015
+ "source_kind": "skill",
1016
+ "source": "skill-source",
1017
+ "target": "missing-skill",
1018
+ },
1019
+ ]
1020
+
1021
+
1022
+ def test_wikilink_health_tool_exposes_checker_result(tmp_path, monkeypatch):
1023
+ pkg = _bootstrap(tmp_path, monkeypatch)
1024
+ pkg["lessons"].append_lesson(
1025
+ title="lesson source", body="See [[missing-target]].", source="shadow",
1026
+ )
1027
+ from threadkeeper._mcp import mcp
1028
+
1029
+ result = mcp._tool_manager._tools["wikilink_health"].fn()
1030
+ payload = json.loads(result)
1031
+
1032
+ assert payload["summary"]["dangling_references"] == 1
1033
+ assert payload["dangling_references"] == [{
1034
+ "source_kind": "lesson",
1035
+ "source": "lesson-source",
1036
+ "target": "missing-target",
1037
+ }]
1038
+
1039
+
917
1040
  def test_curator_report_write_is_path_scoped_and_replaceable(
918
1041
  tmp_path, monkeypatch,
919
1042
  ):
@@ -205,6 +205,73 @@ def test_mcp_lesson_append_validates_inputs(tmp_path, monkeypatch):
205
205
  assert out.startswith("ok slug=ok-one")
206
206
 
207
207
 
208
+ def test_lesson_neighbors_previews_ranked_slugs_without_writing(
209
+ tmp_path, monkeypatch,
210
+ ):
211
+ pkg = _bootstrap(tmp_path, monkeypatch)
212
+ la = _tool(pkg, "lesson_append")
213
+ ln = _tool(pkg, "lesson_neighbors")
214
+ la(title="reset ios network", body="Reset iOS simulator network settings.")
215
+ la(title="serialize device boot", body="Serialize device boot locks.")
216
+ la(title="unrelated release notes", body="Document release notes.")
217
+
218
+ import threadkeeper.embeddings as embeddings
219
+
220
+ monkeypatch.setattr(
221
+ embeddings,
222
+ "encode_many",
223
+ lambda texts: [[1.0, 0.0], [0.96, 0.0], [0.83, 0.0], [0.40, 0.0]],
224
+ )
225
+
226
+ out = ln(
227
+ title="recover simulator connectivity",
228
+ summary="Restore device networking before a test run.",
229
+ body="Reset simulator network settings before retrying device tests.",
230
+ k=2,
231
+ )
232
+
233
+ assert out.splitlines() == [
234
+ "lesson_neighbors total=2 mode=semantic prospective_slug=recover-simulator-connectivity",
235
+ " 1. reset-ios-network score=0.96",
236
+ " 2. serialize-device-boot score=0.83",
237
+ ]
238
+ assert pkg["lessons"].count_lessons() == 3
239
+
240
+
241
+ def test_lesson_neighbors_leaves_existing_lesson_write_semantics_unchanged(
242
+ tmp_path, monkeypatch,
243
+ ):
244
+ pkg = _bootstrap(tmp_path, monkeypatch)
245
+ ln = _tool(pkg, "lesson_neighbors")
246
+ la = _tool(pkg, "lesson_append")
247
+
248
+ assert ln(title="new lesson", body="new body") == (
249
+ "lesson_neighbors total=0 mode=semantic"
250
+ )
251
+ assert la(title="new lesson", body="new body") == (
252
+ f"ok slug=new-lesson path={pkg['path']}"
253
+ )
254
+
255
+
256
+ def test_lesson_neighbors_falls_back_to_lexical_ranking(tmp_path, monkeypatch):
257
+ pkg = _bootstrap(tmp_path, monkeypatch)
258
+ la = _tool(pkg, "lesson_append")
259
+ ln = _tool(pkg, "lesson_neighbors")
260
+ la(
261
+ title="reset simulator network",
262
+ body="Reset simulator network settings before device tests.",
263
+ )
264
+ import threadkeeper.embeddings as embeddings
265
+
266
+ monkeypatch.setattr(embeddings, "encode_many", lambda texts: None)
267
+ out = ln(
268
+ title="repair simulator connectivity",
269
+ body="Reset simulator network before retrying device tests.",
270
+ )
271
+ assert "mode=lexical" in out
272
+ assert "reset-simulator-network" in out
273
+
274
+
208
275
  def test_mcp_lesson_patch_replaces_one_unique_substring(tmp_path, monkeypatch):
209
276
  pkg = _bootstrap(tmp_path, monkeypatch)
210
277
  la = _tool(pkg, "lesson_append")
@@ -356,6 +423,74 @@ def test_shadow_lesson_append_semantic_duplicate_patches_incumbent(
356
423
  assert "serialize AVD boot lock acquisition" in body
357
424
 
358
425
 
426
+ def test_new_directive_flags_older_permissive_lesson_for_reconciliation(
427
+ tmp_path, monkeypatch,
428
+ ):
429
+ pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
430
+ la = _tool(pkg, "lesson_append")
431
+
432
+ first = la(
433
+ title="module globals for test configuration",
434
+ body=(
435
+ "You can use module globals to pass test configuration between "
436
+ "runner steps when setup is simple."
437
+ ),
438
+ source="shadow",
439
+ )
440
+ assert first.startswith("ok")
441
+
442
+ out = la(
443
+ title="inject test configuration explicitly",
444
+ body=(
445
+ "Never use module globals to pass test configuration between "
446
+ "runner steps; inject dependencies explicitly instead."
447
+ ),
448
+ source="shadow",
449
+ )
450
+
451
+ assert out.startswith("ok slug=inject-test-configuration-explicitly")
452
+ assert "reconciliation=module-globals-for-test-configuration" in out
453
+ assert "review=patch_or_cross_link" in out
454
+ assert pkg["lessons"].count_lessons() == 2
455
+ row = pkg["db"].get_db().execute(
456
+ "SELECT target, summary FROM events WHERE kind='lesson_reconciliation'"
457
+ ).fetchone()
458
+ assert row["target"] == "module-globals-for-test-configuration"
459
+ assert "new=inject-test-configuration-explicitly" in row["summary"]
460
+ assert "reason=absolute_directive" in row["summary"]
461
+
462
+
463
+ def test_new_debunk_flags_older_permissive_lesson_for_reconciliation(
464
+ tmp_path, monkeypatch,
465
+ ):
466
+ pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
467
+ la = _tool(pkg, "lesson_append")
468
+
469
+ la(
470
+ title="shared runner environment state",
471
+ body=(
472
+ "It is safe to keep shared runner environment state throughout "
473
+ "a test workflow."
474
+ ),
475
+ source="shadow",
476
+ )
477
+
478
+ out = la(
479
+ title="shared runner environment state is unreliable",
480
+ body=(
481
+ "Keeping shared runner environment state is unreliable; pass the "
482
+ "environment to each command instead."
483
+ ),
484
+ source="shadow",
485
+ )
486
+
487
+ assert "reconciliation=shared-runner-environment-state" in out
488
+ row = pkg["db"].get_db().execute(
489
+ "SELECT summary FROM events WHERE kind='lesson_reconciliation'"
490
+ ).fetchone()
491
+ assert "reason=debunk" in row["summary"]
492
+
493
+
359
494
  def test_shadow_lesson_append_semantic_borderline_surfaces_duplicate(
360
495
  tmp_path, monkeypatch,
361
496
  ):
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def test_install_mcp_servers_pins_imports_to_configured_package(
5
+ tmp_path, monkeypatch,
6
+ ):
7
+ import threadkeeper._setup as setup_mod
8
+ import threadkeeper.adapters as adapters_mod
9
+
10
+ captured = {}
11
+
12
+ class FakeAdapter:
13
+ name = "fake"
14
+
15
+ def register_mcp_server(self, **kwargs):
16
+ captured.update(kwargs)
17
+ return "ok"
18
+
19
+ monkeypatch.setattr(setup_mod, "REPO_ROOT", tmp_path)
20
+ monkeypatch.setattr(
21
+ adapters_mod, "installed_adapters", lambda: [FakeAdapter()],
22
+ )
23
+
24
+ assert setup_mod.install_mcp_servers(dry_run=False) == [
25
+ "mcp_server[fake]: ok"
26
+ ]
27
+ assert captured["env"] == {
28
+ "PYTHONPATH": str(tmp_path),
29
+ "PYTHONSAFEPATH": "1",
30
+ }
31
+
@@ -373,8 +373,10 @@ def test_run_shadow_pass_spawns_when_threshold_met(tmp_path, monkeypatch):
373
373
  assert kw["write_origin"] == "shadow_review"
374
374
  assert "SHADOW LEARNING OBSERVER" in kw["prompt"]
375
375
  assert "lesson_list(k=80)" in kw["prompt"]
376
+ assert "lesson_neighbors(title=<prospective>" in kw["prompt"]
376
377
  assert "hard cap 450" in kw["prompt"]
377
378
  assert "mcp__thread-keeper__lesson_get" in kw["extra_allowed_tools"]
379
+ assert "mcp__thread-keeper__lesson_neighbors" in kw["extra_allowed_tools"]
378
380
  assert "mcp__thread-keeper__lesson_patch" in kw["extra_allowed_tools"]
379
381
  assert long_msg.strip()[:40] in kw["prompt"]
380
382
  # De-privileged (issue #76): no bare Read/Write — path-scoped skill/
@@ -182,11 +182,12 @@ def test_ingest_file_bumps_skill_usage(tmp_path, monkeypatch):
182
182
  conn.commit()
183
183
  assert added == 1
184
184
  row = conn.execute(
185
- "SELECT use_count, last_used_at, created_by_origin "
185
+ "SELECT use_count, foreground_use_count, last_used_at, created_by_origin "
186
186
  "FROM skill_usage WHERE name='swift-ios'"
187
187
  ).fetchone()
188
188
  assert row is not None
189
189
  assert row["use_count"] == 1
190
+ assert row["foreground_use_count"] == 1
190
191
  assert row["last_used_at"] is not None
191
192
  assert row["created_by_origin"] == "foreground"
192
193
 
@@ -466,6 +466,42 @@ def test_skill_record_bumps_counters(skills_pkg):
466
466
  assert r["last_viewed_at"] is not None
467
467
 
468
468
 
469
+ def test_skill_list_records_views_for_returned_skills(skills_pkg):
470
+ _seed_skill(skills_pkg, name="listed-active")
471
+ _seed_skill(skills_pkg, name="listed-archived")
472
+ conn = skills_pkg["db"].get_db()
473
+ conn.execute(
474
+ "UPDATE skill_usage SET state='archived' WHERE name='listed-archived'"
475
+ )
476
+ conn.commit()
477
+
478
+ listed = _tool(skills_pkg, "skill_list")
479
+ rendered = listed()
480
+ assert "listed-active" in rendered
481
+ assert "views=1" in rendered
482
+ rows = {
483
+ row["name"]: row
484
+ for row in conn.execute(
485
+ "SELECT name, view_count, last_viewed_at FROM skill_usage "
486
+ "WHERE name IN ('listed-active', 'listed-archived')"
487
+ ).fetchall()
488
+ }
489
+ assert rows["listed-active"]["view_count"] == 1
490
+ assert rows["listed-active"]["last_viewed_at"] is not None
491
+ assert rows["listed-archived"]["view_count"] == 0
492
+
493
+ assert "listed-archived" in listed(include_archived=True)
494
+ rows = {
495
+ row["name"]: row
496
+ for row in conn.execute(
497
+ "SELECT name, view_count FROM skill_usage "
498
+ "WHERE name IN ('listed-active', 'listed-archived')"
499
+ ).fetchall()
500
+ }
501
+ assert rows["listed-active"]["view_count"] == 2
502
+ assert rows["listed-archived"]["view_count"] == 1
503
+
504
+
469
505
  def test_skill_list_omits_archived_by_default(skills_pkg):
470
506
  _seed_skill(skills_pkg, name="alive")
471
507
  _seed_skill(skills_pkg, name="ghost")
@@ -59,6 +59,7 @@ def test_build_slim_mcp_config_from_claude_json(tmp_path, monkeypatch):
59
59
  assert mp["command"] == "/path/to/python"
60
60
  assert mp["args"] == ["-m", "threadkeeper.server"]
61
61
  assert mp["env"]["PYTHONPATH"] == "/path/to/repo"
62
+ assert mp["env"]["PYTHONSAFEPATH"] == "1"
62
63
  assert mp["env"]["THREADKEEPER_FORCE_CID"] == _FAKE_CID
63
64
  assert mp["env"]["THREADKEEPER_SPAWNED_CHILD"] == "1"
64
65
  assert mp["env"]["THREADKEEPER_NO_EMBEDDINGS"] == "1"
@@ -92,6 +93,7 @@ def test_build_slim_mcp_config_synthesizes_when_no_claude_json(tmp_path, monkeyp
92
93
  assert mp["command"] == sys.executable
93
94
  assert "threadkeeper.server" in mp["args"]
94
95
  assert "PYTHONPATH" in mp["env"]
96
+ assert mp["env"]["PYTHONSAFEPATH"] == "1"
95
97
  assert mp["env"]["THREADKEEPER_SPAWNED_CHILD"] == "1"
96
98
  assert mp["env"]["THREADKEEPER_NO_EMBEDDINGS"] == "1"
97
99
 
@@ -142,6 +144,7 @@ def test_slim_config_is_owner_only_and_minimizes_env(tmp_path, monkeypatch):
142
144
  env = json.loads(slim_path.read_text())["mcpServers"]["thread-keeper"]["env"]
143
145
  # Needed keys survive: package discovery + thread-keeper knobs + overrides.
144
146
  assert env["PYTHONPATH"] == "/path/to/repo"
147
+ assert env["PYTHONSAFEPATH"] == "1"
145
148
  assert env["THREADKEEPER_TZ"] == "Europe/Kyiv"
146
149
  assert env["THREADKEEPER_FORCE_CID"] == _FAKE_CID
147
150
  assert env["THREADKEEPER_NO_EMBEDDINGS"] == "1"
@@ -33,9 +33,10 @@ READ_TOOLS = {
33
33
  "memory_guard_status", "mp_dashboard", "mp_health", "neighbors", "peers",
34
34
  "pending_distillates", "pickup_candidates", "reliability_for",
35
35
  "review_candidates", "run_probe", "search", "shadow_review_status",
36
- "skill_list", "skill_validate", "spawn_budget_status", "spawn_status",
36
+ "skill_validate", "spawn_budget_status", "spawn_status",
37
37
  "sync_peers", "sync_status", "task_logs",
38
- "task_thread", "tasks", "weak_spots", "whoami",
38
+ "task_thread", "tasks", "weak_spots", "whoami", "wikilink_health",
39
+ "lesson_neighbors",
39
40
  }
40
41
 
41
42
  # --- delete/overwrite/kill tools: destructiveHint=True -----------------------
@@ -50,7 +51,8 @@ DELETE_CLASS_TOOLS = {
50
51
  WRITE_TOOLS = {
51
52
  "accept_candidate", "ask", "auto_review_trigger", "broadcast",
52
53
  "candidate_review_run", "claim_pickup", "close_thread", "config_reload",
53
- "convene_panel", "core_set", "curator_report_write", "curator_review",
54
+ "convene_panel", "core_set", "curator_merge_verdict",
55
+ "curator_report_write", "curator_review",
54
56
  "db_compact", "db_deduplicate_embeddings", "dialectic_claim",
55
57
  "dialectic_evidence", "dialectic_mine_run", "dialectic_observation_resolve",
56
58
  "dialectic_supersede", "dialectic_validate_run", "distill", "evolve_apply",
@@ -65,7 +67,7 @@ WRITE_TOOLS = {
65
67
  "open_dialog_window", "open_thread", "presence", "record_attempt",
66
68
  "register_concept", "register_probe", "reject_candidate", "release_pickup",
67
69
  "respond", "review_thread", "search_via_parent", "session_end",
68
- "shadow_review_run", "skill_record", "spawn", "spawn_budget_set",
70
+ "shadow_review_run", "skill_list", "skill_record", "spawn", "spawn_budget_set",
69
71
  "style_set", "sync_now", "tag_signal", "tournament", "validate_threads",
70
72
  "verbatim_user", "vote_distill", "wait", "whisper",
71
73
  }
@@ -128,7 +128,14 @@ def install_mcp_servers(dry_run: bool) -> list[str]:
128
128
  if venv_python.exists():
129
129
  python_bin = str(venv_python)
130
130
  args = ["-m", "threadkeeper.server"]
131
- env = {"PYTHONPATH": str(REPO_ROOT)}
131
+ env = {
132
+ "PYTHONPATH": str(REPO_ROOT),
133
+ # MCP clients often launch the server from the agent's current
134
+ # checkout. Without safe-path mode, ``python -m`` puts that checkout
135
+ # ahead of the configured install and can import unmerged Evolve code
136
+ # against the live DB.
137
+ "PYTHONSAFEPATH": "1",
138
+ }
132
139
 
133
140
  lines: list[str] = []
134
141
  adapters = installed_adapters()
@@ -337,6 +337,14 @@ class CodexAdapter(CLIAdapter):
337
337
  if {"websearch", "webfetch"} & requested_tools:
338
338
  argv.append("--search")
339
339
  argv += ["exec", "--skip-git-repo-check"]
340
+ # Codex launches configured stdio MCP servers from the agent cwd. A
341
+ # repository checkout can therefore shadow the installed package used
342
+ # by thread-keeper's MCP entry. Scope safe-path mode to the MCP server
343
+ # only; shell/test Python processes in the child keep normal semantics.
344
+ argv += [
345
+ "-c",
346
+ 'mcp_servers.thread-keeper.env.PYTHONSAFEPATH="1"',
347
+ ]
340
348
  if model:
341
349
  argv += ["-m", model]
342
350
  if effort: