threadkeeper 0.19.0__tar.gz → 0.20.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. {threadkeeper-0.19.0/threadkeeper.egg-info → threadkeeper-0.20.0}/PKG-INFO +463 -89
  2. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/README.md +458 -87
  3. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/pyproject.toml +13 -5
  4. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_agent_status.py +36 -17
  5. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_config_settings.py +3 -0
  6. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_curator.py +674 -22
  7. threadkeeper-0.20.0/tests/test_dashboard_app.py +143 -0
  8. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_db_schema.py +85 -0
  9. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_egress_policy.py +10 -3
  10. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_evolve_applier.py +632 -13
  11. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_evolve_daemon.py +163 -0
  12. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_host_env.py +38 -1
  13. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_identity.py +2 -0
  14. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_ingest_denylist.py +1 -1
  15. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_lessons.py +596 -22
  16. threadkeeper-0.20.0/tests/test_mcp_protocol_conformance.py +235 -0
  17. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_mcp_resources_prompts.py +160 -0
  18. threadkeeper-0.20.0/tests/test_mcp_server_instructions.py +139 -0
  19. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_mcp_skills_extension.py +5 -1
  20. threadkeeper-0.20.0/tests/test_mcp_tasks.py +158 -0
  21. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_memory_eval.py +84 -4
  22. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_menubar_app.py +1 -1
  23. threadkeeper-0.20.0/tests/test_model_router.py +244 -0
  24. threadkeeper-0.20.0/tests/test_model_router_digest.py +107 -0
  25. threadkeeper-0.20.0/tests/test_model_router_tools.py +194 -0
  26. threadkeeper-0.20.0/tests/test_model_routes.py +183 -0
  27. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_nudges.py +4 -7
  28. threadkeeper-0.20.0/tests/test_project_scope.py +113 -0
  29. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_release_workflows.py +18 -4
  30. threadkeeper-0.20.0/tests/test_session_read_purity.py +104 -0
  31. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_setup.py +1 -1
  32. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_shadow_review.py +6 -1
  33. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skill_hint.py +4 -5
  34. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skill_tier.py +137 -0
  35. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skill_updater.py +69 -0
  36. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skill_use_parser.py +33 -0
  37. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skills.py +160 -37
  38. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_config.py +193 -0
  39. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_hint.py +9 -16
  40. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_watchdog.py +32 -0
  41. threadkeeper-0.20.0/tests/test_storage.py +256 -0
  42. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_tool_annotations.py +56 -3
  43. threadkeeper-0.20.0/tests/test_tool_profiles.py +170 -0
  44. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_tools_smoke.py +1 -0
  45. threadkeeper-0.20.0/tests/test_tracing.py +237 -0
  46. threadkeeper-0.20.0/tests/test_workflow_retention.py +255 -0
  47. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/_mcp.py +115 -3
  48. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/_setup.py +7 -55
  49. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/_spawn_wrap.py +23 -4
  50. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/base.py +6 -0
  51. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/codex.py +1 -0
  52. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/agent_metadata.py +15 -0
  53. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/agent_status.py +136 -32
  54. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/README.md +5 -0
  55. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +49 -13
  56. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/backup.py +37 -0
  57. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/brief.py +50 -41
  58. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/candidate_reviewer.py +9 -6
  59. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/config.py +73 -9
  60. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/curator.py +1017 -326
  61. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/curator_snapshots.py +137 -3
  62. threadkeeper-0.20.0/threadkeeper/dashboard_app.py +411 -0
  63. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/db.py +387 -38
  64. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/dialectic_miner.py +10 -5
  65. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/dialectic_validator.py +4 -2
  66. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/elicitation.py +3 -0
  67. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/embeddings.py +12 -4
  68. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/evolve_applier.py +593 -82
  69. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/evolve_daemon.py +60 -27
  70. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/forget.py +13 -0
  71. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/github_budget.py +9 -1
  72. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/host.py +42 -17
  73. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/identity.py +13 -3
  74. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/ingest.py +42 -5
  75. threadkeeper-0.20.0/threadkeeper/instructions.py +80 -0
  76. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/lessons.py +130 -6
  77. threadkeeper-0.20.0/threadkeeper/mcp_tasks.py +327 -0
  78. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/migrate_embeddings.py +3 -2
  79. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/model_catalog.py +9 -0
  80. threadkeeper-0.20.0/threadkeeper/model_router.py +373 -0
  81. threadkeeper-0.20.0/threadkeeper/model_router_digest.py +234 -0
  82. threadkeeper-0.20.0/threadkeeper/model_routes.py +318 -0
  83. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/notify.py +1 -0
  84. threadkeeper-0.20.0/threadkeeper/project_scope.py +204 -0
  85. threadkeeper-0.20.0/threadkeeper/protocol.py +186 -0
  86. threadkeeper-0.20.0/threadkeeper/read_context.py +24 -0
  87. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/retention.py +5 -1
  88. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/retrieval.py +34 -8
  89. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/server.py +11 -1
  90. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/shadow_review.py +4 -1
  91. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/skill_audit.py +4 -1
  92. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/skill_updater.py +41 -0
  93. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/spawn_budget.py +32 -23
  94. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/spawn_config.py +166 -45
  95. threadkeeper-0.20.0/threadkeeper/storage.py +740 -0
  96. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/identity.py +6 -0
  97. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/migrate.py +13 -13
  98. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/protocol.py +3 -1
  99. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/reset_node.py +5 -2
  100. threadkeeper-0.20.0/threadkeeper/tool_profiles.py +364 -0
  101. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tool_schemas.py +1 -0
  102. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/agent_status.py +15 -2
  103. threadkeeper-0.20.0/threadkeeper/tools/capabilities.py +15 -0
  104. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/concepts.py +25 -10
  105. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/core_memory.py +43 -16
  106. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/curator.py +41 -10
  107. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/dashboard.py +7 -0
  108. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/db_maintenance.py +19 -1
  109. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/dialectic.py +39 -13
  110. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/dialog.py +24 -7
  111. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/distill.py +26 -10
  112. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/evolve_applier.py +53 -21
  113. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/extract.py +44 -25
  114. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/lessons.py +577 -256
  115. threadkeeper-0.20.0/threadkeeper/tools/model_router.py +152 -0
  116. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/pickup.py +4 -3
  117. threadkeeper-0.20.0/threadkeeper/tools/project_scope.py +33 -0
  118. threadkeeper-0.20.0/threadkeeper/tools/resources.py +460 -0
  119. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/session.py +8 -3
  120. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/shadow_review.py +28 -11
  121. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/skills.py +139 -42
  122. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/spawn.py +83 -9
  123. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/style.py +14 -2
  124. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/sync.py +2 -2
  125. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/threads.py +60 -22
  126. threadkeeper-0.20.0/threadkeeper/tracing.py +572 -0
  127. threadkeeper-0.20.0/threadkeeper/workflow_artifacts.py +575 -0
  128. {threadkeeper-0.19.0 → threadkeeper-0.20.0/threadkeeper.egg-info}/PKG-INFO +463 -89
  129. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper.egg-info/SOURCES.txt +29 -0
  130. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper.egg-info/entry_points.txt +1 -0
  131. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper.egg-info/requires.txt +5 -1
  132. threadkeeper-0.19.0/tests/test_session_read_purity.py +0 -51
  133. threadkeeper-0.19.0/threadkeeper/tools/resources.py +0 -100
  134. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/LICENSE +0 -0
  135. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/setup.cfg +0 -0
  136. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_adapters.py +0 -0
  137. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_antigravity_ingest.py +0 -0
  138. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_auto_update.py +0 -0
  139. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_backup.py +0 -0
  140. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_brief_footprint.py +0 -0
  141. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_brief_sections.py +0 -0
  142. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_candidate_reviewer.py +0 -0
  143. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_ci_parallel.py +0 -0
  144. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_concepts.py +0 -0
  145. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_config_watcher.py +0 -0
  146. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_core_memory.py +0 -0
  147. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_daemon_host_config.py +0 -0
  148. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_daemon_host_integration.py +0 -0
  149. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_daemon_liveness.py +0 -0
  150. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_daemon_state.py +0 -0
  151. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dashboard.py +0 -0
  152. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_db_compact.py +0 -0
  153. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_db_contention.py +0 -0
  154. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_db_leak_guard.py +0 -0
  155. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_db_migration_fts.py +0 -0
  156. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_db_runtime.py +0 -0
  157. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_delegated_search.py +0 -0
  158. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic.py +0 -0
  159. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic_feed_tools.py +0 -0
  160. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic_miner.py +0 -0
  161. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic_observation_resolve.py +0 -0
  162. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic_recompute.py +0 -0
  163. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic_tier.py +0 -0
  164. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialectic_validator.py +0 -0
  165. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_dialog_fts_external.py +0 -0
  166. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_documentation_inventory.py +0 -0
  167. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_embed_routing.py +0 -0
  168. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_embedding_generation.py +0 -0
  169. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_embedding_model_revision.py +0 -0
  170. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_embedding_storage_dedup.py +0 -0
  171. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_error_paths.py +0 -0
  172. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_eval_harness.py +0 -0
  173. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_evolve_apply_2.py +0 -0
  174. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_evolve_apply_3.py +0 -0
  175. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_evolve_apply_43.py +0 -0
  176. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_evolve_apply_45.py +0 -0
  177. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_extract_daemon.py +0 -0
  178. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_extract_dedup.py +0 -0
  179. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_forget.py +0 -0
  180. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_github_budget.py +0 -0
  181. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_github_safety.py +0 -0
  182. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_helpers.py +0 -0
  183. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_hook_child_guard.py +0 -0
  184. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_host_embed.py +0 -0
  185. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_host_main.py +0 -0
  186. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_host_wedged.py +0 -0
  187. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_hybrid_retrieval.py +0 -0
  188. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_i18n_multilang.py +0 -0
  189. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_ingest_cursor.py +0 -0
  190. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_ingest_secret_redaction.py +0 -0
  191. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_ingest_status.py +0 -0
  192. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_ingest_transactions.py +0 -0
  193. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_lesson_violations.py +0 -0
  194. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_mcp_sdk_compat.py +0 -0
  195. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_memory_authority_lifecycle.py +0 -0
  196. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_memory_compatibility.py +0 -0
  197. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_memory_guard.py +0 -0
  198. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_memory_guard_host.py +0 -0
  199. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_missed_spawns.py +0 -0
  200. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_model_catalog.py +0 -0
  201. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_notify_daemon.py +0 -0
  202. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_onnx_embeddings.py +0 -0
  203. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_panel.py +0 -0
  204. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_permissions.py +0 -0
  205. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_pickup.py +0 -0
  206. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_probe_daemon.py +0 -0
  207. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_process_health.py +0 -0
  208. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_retention.py +0 -0
  209. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_review_prompts.py +0 -0
  210. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_search_fts_punctuation.py +0 -0
  211. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
  212. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skill_passive_tier.py +0 -0
  213. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_skill_watcher.py +0 -0
  214. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_background_cwd.py +0 -0
  215. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_budget.py +0 -0
  216. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_child_env.py +0 -0
  217. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_codex_stdin.py +0 -0
  218. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_permission_gate.py +0 -0
  219. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_reap.py +0 -0
  220. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_result.py +0 -0
  221. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_slim.py +0 -0
  222. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_worktree_isolation.py +0 -0
  223. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_spawn_wrap.py +0 -0
  224. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_sync_capture.py +0 -0
  225. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_sync_identity.py +0 -0
  226. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_sync_migrate.py +0 -0
  227. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_sync_protocol.py +0 -0
  228. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_sync_reset_node.py +0 -0
  229. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_sync_transport.py +0 -0
  230. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_task_retention.py +0 -0
  231. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_task_spool.py +0 -0
  232. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_thin_session.py +0 -0
  233. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_thread_janitor.py +0 -0
  234. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_threads.py +0 -0
  235. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_validate_threads.py +0 -0
  236. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_vec_search.py +0 -0
  237. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/tests/test_verify_ingest.py +0 -0
  238. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/__init__.py +0 -0
  239. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/__init__.py +0 -0
  240. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  241. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/antigravity.py +0 -0
  242. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/claude_code.py +0 -0
  243. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  244. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/copilot.py +0 -0
  245. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/adapters/vscode.py +0 -0
  246. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  247. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  248. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  249. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/authority.py +0 -0
  250. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/auto_update.py +0 -0
  251. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/config_io.py +0 -0
  252. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/config_watcher.py +0 -0
  253. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/daemon_liveness.py +0 -0
  254. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/daemon_state.py +0 -0
  255. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/daemon_supervisor.py +0 -0
  256. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/egress.py +0 -0
  257. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/eval/__init__.py +0 -0
  258. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/eval/__main__.py +0 -0
  259. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/eval/harness.py +0 -0
  260. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/extract_daemon.py +0 -0
  261. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/github_safety.py +0 -0
  262. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/harvest.py +0 -0
  263. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/helpers.py +0 -0
  264. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/host_embed.py +0 -0
  265. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/i18n.py +0 -0
  266. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/lesson_violations.py +0 -0
  267. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/link_health.py +0 -0
  268. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/mcp_skills.py +0 -0
  269. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/memory_compat.py +0 -0
  270. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/memory_guard.py +0 -0
  271. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/menubar_app.py +0 -0
  272. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/nudges.py +0 -0
  273. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/permissions.py +0 -0
  274. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/probe_daemon.py +0 -0
  275. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/process_health.py +0 -0
  276. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/review_prompts.py +0 -0
  277. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/search_proxy.py +0 -0
  278. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/skill_watcher.py +0 -0
  279. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/spawn_result.py +0 -0
  280. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/__init__.py +0 -0
  281. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/capture.py +0 -0
  282. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/daemon.py +0 -0
  283. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/sync/server.py +0 -0
  284. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/task_spool.py +0 -0
  285. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/thread_janitor.py +0 -0
  286. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/__init__.py +0 -0
  287. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/authority.py +0 -0
  288. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  289. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/config_watch.py +0 -0
  290. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/consolidate.py +0 -0
  291. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/correlation.py +0 -0
  292. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  293. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/evolve_research.py +0 -0
  294. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/forget.py +0 -0
  295. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/graph.py +0 -0
  296. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/invariants.py +0 -0
  297. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/memory_guard.py +0 -0
  298. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/missed_spawns.py +0 -0
  299. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/panel.py +0 -0
  300. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/peers.py +0 -0
  301. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/probes.py +0 -0
  302. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/process_health.py +0 -0
  303. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/prompts.py +0 -0
  304. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/tools/validate.py +0 -0
  305. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/trash.py +0 -0
  306. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/verify_ingest.py +0 -0
  307. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper/wikilinks.py +0 -0
  308. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  309. {threadkeeper-0.19.0 → threadkeeper-0.20.0}/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.19.0
3
+ Version: 0.20.0
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,>=2.2.0
25
+ Requires-Dist: mcp<3,>=2.3.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
@@ -35,6 +35,9 @@ Provides-Extra: semantic-st
35
35
  Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic-st"
36
36
  Requires-Dist: numpy>=1.24.0; extra == "semantic-st"
37
37
  Requires-Dist: sqlite-vec>=0.1.9; extra == "semantic-st"
38
+ Provides-Extra: encrypted
39
+ Requires-Dist: keyring>=25.0; extra == "encrypted"
40
+ Requires-Dist: sqlcipher3-wheels>=0.5.7; extra == "encrypted"
38
41
  Provides-Extra: dev
39
42
  Requires-Dist: pytest>=9.1.1; extra == "dev"
40
43
  Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
@@ -150,6 +153,12 @@ managed instructions block and call `brief()` / `context()` before answering, or
150
153
  `memory://brief` resource the host attaches automatically (see
151
154
  [MCP primitives](#mcp-primitives-tools-resources-prompts)).
152
155
 
156
+ The MCP initialization response also carries concise, static lifecycle guidance:
157
+ load the brief/context, open/note/close substantive threads, prefer read-only
158
+ resources for host-attached context, and use dry-run plus confirmation before
159
+ destructive maintenance. Hooks and the managed files remain reinforcement for
160
+ clients that do not surface server instructions.
161
+
153
162
  ### Alternative installs
154
163
 
155
164
  If you don't have `pipx` and don't want to install it:
@@ -219,12 +228,12 @@ the `CLIAdapter` contract. See [CONTRIBUTING.md](CONTRIBUTING.md).
219
228
 
220
229
  ### Python MCP SDK surface
221
230
 
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,
231
+ thread-keeper requires MCP Python SDK 2.3 or later (`mcp>=2.3.0,<3`). Its
232
+ stable Skills and durable Tasks extensions use the MCP 2026-07-28 extension surface, while tools,
224
233
  resources, prompts, annotations, structured content, elicitation, and stdio
225
234
  continue to use the same `MCPServer` instance.
226
235
 
227
- ### MCP primitives (tools, resources, prompts, elicitation)
236
+ ### MCP primitives (tools, resources, prompts, Apps, elicitation)
228
237
 
229
238
  MCP has three server primitives. thread-keeper uses all three, mapped to the
230
239
  read/act split, plus MCP elicitation for host-native confirmations:
@@ -232,8 +241,9 @@ read/act split, plus MCP elicitation for host-native confirmations:
232
241
  | Primitive | Control | What thread-keeper exposes | When to use |
233
242
  |---|---|---|---|
234
243
  | **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, `wikilink_health`, … | the agent decides to call them |
235
- | **Resources** | application-controlled, read-only | `memory://brief`, `memory://context`, `memory://dashboard`, `memory://agent-status` | the **host** attaches/pulls them automatically |
244
+ | **Resources** | application-controlled, read-only | `memory://brief`, `memory://context`, `memory://dashboard`, `memory://agent-status` | the **host** attaches/pulls them automatically; capable hosts may subscribe |
236
245
  | **Prompts** | user-controlled templates | `review_recent_threads`, `run_library_curation`, `audit_threadkeeper` | the user runs them (Claude Code: `/mcp__thread-keeper__<name>`) |
246
+ | **MCP App** | host-rendered, display-only | `open_dashboard` → `ui://thread-keeper/dashboard` | a host that advertises MCP Apps opens the sandboxed dashboard |
237
247
 
238
248
  **Resources** back the genuinely read-only memory views with the same render
239
249
  functions as the matching tools, so the content is identical — `memory://brief`
@@ -247,6 +257,24 @@ so an automatic host pull is **side-effect-free**.
247
257
  **Prompts** turn the curation / audit / review flows into discoverable,
248
258
  parameterized commands; each just drives the existing tools.
249
259
 
260
+ **Portable dashboard App.** `open_dashboard` is a read-only progressive
261
+ enhancement over the existing status data. In an MCP Apps-capable host it opens
262
+ `ui://thread-keeper/dashboard` in a sandboxed iframe and shows autonomous
263
+ loops, running children, budget state, task history, recent failures, and
264
+ recent materializations. It never sends prompts, transcripts, task-log text,
265
+ credentials, child identifiers, or local paths to the iframe. Loading and the
266
+ App's Refresh button only open the existing database in SQLite read-only mode;
267
+ they never bootstrap a session, reconcile tasks, start daemons, refresh process
268
+ RSS, or write telemetry.
269
+
270
+ This is portable across macOS, Windows, and Linux because it uses the existing
271
+ stdio server registration in the table above — no native app installation is
272
+ needed. If the host does not advertise the `io.modelcontextprotocol/ui`
273
+ extension with `text/html;profile=mcp-app`, `open_dashboard` returns a short
274
+ text fallback; use `agent_status(json_output=True)` or `mp_dashboard()` as the
275
+ tested fallback. The macOS menu-bar app remains an optional native frontend
276
+ over the same status data, not a requirement for this dashboard.
277
+
250
278
  **Elicitation** is a client feature, not a server primitive. When a host
251
279
  advertises form-mode elicitation, high-stakes mutations can pause for a
252
280
  structured user choice instead of relying on an ignorable text nudge. The first
@@ -256,7 +284,8 @@ the previous immediate tool behavior.
256
284
 
257
285
  Everything here is **additive and capability-gated**: a host that advertises the
258
286
  `resources` / `prompts` capabilities sees those primitives; one that advertises
259
- `elicitation.form` gets structured confirmations for covered high-stakes writes.
287
+ MCP Apps gets the dashboard iframe; and one that advertises `elicitation.form`
288
+ gets structured confirmations for covered high-stakes writes.
260
289
  Hosts without a capability fall back to the SessionStart hook plus the `brief()`
261
290
  / `context()` tools and the existing write behavior — same content, no
262
291
  regression. Static URIs only for now (resource *templates* with `{param}` are
@@ -285,6 +314,23 @@ activate it, approve its tools, record use telemetry, or approve supporting
285
314
  files. Activation and any user approval remain responsibilities of the host's
286
315
  skill-loading path.
287
316
 
317
+ ### MCP Tasks for headless spawn
318
+
319
+ Clients using MCP 2026-07-28 that advertise the
320
+ `io.modelcontextprotocol/tasks` extension can call `spawn(..., visible=False)`
321
+ as a durable asynchronous operation. The response is an opaque public task
322
+ handle; poll it with `tasks/get` and request cooperative cancellation with
323
+ `tasks/cancel`. Handles are bound to the authenticated caller (or the local OS
324
+ user for stdio), survive a host reconnect, and never expose the short internal
325
+ spawn ID. The server suggests a five-second polling interval.
326
+
327
+ Only headless `spawn` is task-eligible: visible Terminal.app work has no stable
328
+ process-group handle. `tournament`, `tasks`, `task_logs`, and the legacy custom
329
+ spawn lifecycle stay unchanged. Clients that do not advertise the extension
330
+ receive the existing synchronous `spawn` response, including on older protocol
331
+ versions. `spawn` does not pause for approval, so it deliberately does not
332
+ implement `tasks/update` or task enumeration.
333
+
288
334
  ### Memory egress (cross-provider privacy)
289
335
 
290
336
  thread-keeper is "one user model … shared across CLIs," and that sharing is by
@@ -322,6 +368,32 @@ override wins over `.env`):
322
368
  THREADKEEPER_MEMORY_EGRESS=same-vendor
323
369
  ```
324
370
 
371
+ ### Project memory boundaries
372
+
373
+ Memory is also isolated by repository. thread-keeper derives a path-free
374
+ `prj_…` namespace from the repository remote (or, when no remote exists, its
375
+ Git history), so ordinary moves and Git worktrees retain one identity without
376
+ storing a local path. New project memory is scoped to that namespace; portable
377
+ memory must be explicitly written as `global` or `shared`.
378
+
379
+ `brief()`, `search()`, `dialog_search()`, resources, and learning-loop inputs
380
+ read the current project plus explicit global/shared rows. Set
381
+ `cross_project=True` on `search` or `dialog_search` only for deliberate
382
+ cross-project recall. Those results show `scope=`/`project=` provenance and
383
+ write a `cross_project_recall` audit event. The `memory_scope_status()` tool
384
+ shows the current namespace and the migration accounting.
385
+
386
+ Existing databases keep their memory recallable: legacy rows without reliable
387
+ repository provenance become `global`, as all memory effectively was before
388
+ namespaces, while tasks whose working directory still resolves are scoped to
389
+ their project; new memory is scoped from then on. The migration report records
390
+ the counts. A database that was already at schema v5 before namespaces existed
391
+ gets the namespace columns and this one-time backfill on its next start. Rows
392
+ explicitly marked `unresolved` stay excluded from automatic recall. This
393
+ boundary controls *which local memory is recalled*; it is separate from
394
+ `THREADKEEPER_MEMORY_EGRESS`, which controls whether personal memory may be
395
+ sent to another model provider.
396
+
325
397
  ---
326
398
 
327
399
  ## Core systems
@@ -666,9 +738,23 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
666
738
  though an existing same-slug long lesson may be corrected without increasing
667
739
  its body size; near-duplicate slugs are blocked, and semantic body matches are
668
740
  routed to the incumbent lesson or surfaced for curation instead of minting a
669
- sibling lesson. A clear new directive or debunk also flags older permissive
670
- lessons on the same concrete practice for patch, cross-link, or supersession
671
- review.
741
+ sibling lesson. The same slug and semantic gates apply to every writer,
742
+ foreground sessions included: a foreground near-duplicate is refused with the
743
+ neighbour's slug, and the author either extends it with `lesson_patch` or, after
744
+ reading it, passes `new_despite=<slug>` (comma-separated when the gates name
745
+ several neighbours) to keep a deliberately separate rule. Loop writers are
746
+ recognized by their write origin, so a loop child cannot opt out by passing a
747
+ foreground-looking `source`. A same-slug rewrite is never folded into a
748
+ different lesson, and the Curator verifier writes the umbrella of a verified
749
+ consolidation without the gates.
750
+ A new lesson that debunks a near-duplicate is refused too: correct the existing
751
+ lesson instead of adding a conflicting one. Refusals are recorded as
752
+ `lesson_refused` events so the gate's hit rate stays measurable. Replacing a
753
+ lesson by its exact title keeps the creator's `origin` (and, for a
754
+ human-authored lesson, its `source` too), and each new Curator
755
+ pass first backfills legacy or mislabeled origin markers (loop sources such as
756
+ `shadow` and `curator` can never count as human-authored), keeping the
757
+ original file as `lessons.md.pre-origin-backfill`.
672
758
  Before a genuinely new fallback lesson is written,
673
759
  `lesson_neighbors(title, body, summary, k=3)` shows the nearest existing lesson
674
760
  slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
@@ -764,22 +850,60 @@ for semantic review. System and installed-plugin sources are resolved from
764
850
  their read-only caches rather than misreported as missing mirrors; telemetry
765
851
  rows with no real `SKILL.md` remain explicit orphans. The same inventory also
766
852
  flags a dense lesson subtopic when at least
767
- `THREADKEEPER_CURATOR_PROMOTION_MIN_LESSONS` lessons (default 3) share a pair
768
- of meaningful title terms. A non-protected candidate must become one validated,
769
- checklist-style canonical skill before its source lessons are retired; protected
770
- clusters are left for human review. A read-only research child reads every
771
- complete skill and relevant support file, performs current web research against
772
- official docs and comparable public skills, then writes a bounded
773
- `RESEARCH-<pass>-batch-NNN-of-MMM.json` handoff through a destination-scoped
774
- tool. A separate web-free evaluator receives that handoff as fenced, untrusted
775
- data and writes numbered per-skill verdicts to
853
+ `THREADKEEPER_CURATOR_PROMOTION_MIN_LESSONS` lessons (default 3) share two
854
+ concrete title terms and a non-generic body mechanism. Generic joins and
855
+ domain-wide/high-document-frequency title words cannot form a candidate. The
856
+ inventory and `curator_review_status()` report emitted/rejected counts and
857
+ rejection reasons, so the rule can be tuned from production data. Every
858
+ candidate, protected members included, becomes a PROMOTE proposal: the
859
+ verifier creates or patches one validated, checklist-style canonical skill
860
+ before it retires the source lessons. The evaluator reads every complete skill and relevant support
861
+ file, but has no web tools. External research is off by default. With explicit
862
+ `THREADKEEPER_CURATOR_WEB_RESEARCH=1`, a separate Claude-only researcher gets
863
+ only fixed public compatibility queries, `WebSearch`, `WebFetch`, and one
864
+ parent-authorized handoff writer; it cannot read local memory or the
865
+ repository. The evaluator receives that handoff as fenced, untrusted data and
866
+ writes numbered per-skill verdicts to
776
867
  `~/.threadkeeper/curator/REPORT-<isodate>.md` for a one-batch pass or
777
868
  `REPORT-<isodate>-batch-NNN-of-MMM.md` for a multi-batch pass: KEEP / REPAIR /
778
- UPDATE / MERGE / SPLIT / DEPRECATE / DELETE / CROSS_LINK / HUMAN_REVIEW.
779
- Similar names and cosine scores are only candidates; merge/delete decisions
780
- compare intent, workflow, inputs, outcomes, and unique details. Pinned and
781
- foreground-authored entries are marked `[PROTECTED]`, and delete-class tools
782
- enforce the same boundary server-side. The pass is
869
+ UPDATE / MERGE / SPLIT / DEPRECATE / DELETE / CROSS_LINK / DEFER. DEFER means
870
+ the evidence is insufficient for now and a later pass re-examines the entry;
871
+ no verdict waits for a person. Similar names and cosine scores are only
872
+ candidates; merge/delete decisions compare intent, workflow, inputs, outcomes,
873
+ and unique details. Pinned entries are marked `[PINNED]` (keep only), and
874
+ foreground-authored or legacy entries `[PROTECTED]`; the mutating tools enforce
875
+ both boundaries server-side.
876
+
877
+ Destructive decisions are settled by a second agent, not by a person. The
878
+ evaluator applies only in-place edits that remove nothing (lesson and skill
879
+ patches, repairs, cross-links, concept curation) to entries that are neither
880
+ `[PROTECTED]` nor `[PINNED]`. Every removal, consolidation, merge, split and
881
+ promotion, and every change to a `[PROTECTED]` entry, goes into a
882
+ `VERIFY_PROPOSALS` block at the end of its report. When that block holds at
883
+ least one numbered proposal, the batch gets a third child: an independent
884
+ verifier that re-reads every lesson and skill a proposal touches, tries to
885
+ refute it (a merge that would lose a distinct rule, a removal whose "covered
886
+ by" claim is only partly true, a promotion that is not one procedure), and
887
+ applies only the confirmed or corrected proposals inside the pass snapshot. It
888
+ patches missing residue into a covering skill before removing a lesson,
889
+ records `keep_both` merge verdicts for the pairs it refutes, and writes a
890
+ provenanced `VERIFY-<pass>[-batch-NNN-of-MMM].md` report ending with
891
+ `CURATOR_VERIFY_COMPLETE`. A pass is endorsed only after every batch's
892
+ verification is complete or has used its three attempts. Server-side,
893
+ `lesson_remove` and `skill_manage(action='delete')` refuse the evaluator
894
+ outright, and `lesson_patch` / same-slug `lesson_append` refuse it on protected
895
+ lessons; only the verifier phase, inside a pass carrying both pass-id and
896
+ snapshot-dir context, may change protected entries, and no curator phase may
897
+ touch a pinned one. The verifier is also scoped: the parent records every name
898
+ its batch's proposals mention under the pass snapshot
899
+ (`verify_scope/batch-NNN.json`), and the server refuses a verifier change to
900
+ any lesson or skill outside that list. The batch inventory and the proposals
901
+ reach it as fenced, untrusted data. The Evolve report applier never removes
902
+ anything: it may patch, and the server refuses its `lesson_remove` and skill
903
+ deletes. The parent records the mode each evaluator actually ran in, so a batch
904
+ whose research failed (and was evaluated without mutation tools) never reaches
905
+ the verifier. Report destinations are authorized by exact pass and name, so a
906
+ long pass cannot age them out. The pass is
783
907
  single-flight across processes — a non-blocking `fcntl.flock` pidfile
784
908
  (`<db dir>/curator.lock`) plus a running-children check serialize it, so
785
909
  multiple MCP server instances can't run overlapping (now destructive) passes
@@ -841,11 +965,13 @@ SHA-256 as provenance. Research and evaluation are two phases of each batch
841
965
  inside the durable pass: the web-free evaluator for a batch starts only after
842
966
  that batch's handoff validates. A researcher that fails or leaves a missing,
843
967
  malformed, swapped, or mismatched handoff is retried (three attempts); after
844
- that the batch gets a non-mutating evaluator that records `HUMAN_REVIEW` for
845
- decisions that depend on current external facts. A destructive pass takes its
968
+ that the batch gets a non-mutating evaluator that marks `DEFER` for decisions
969
+ that depend on current external facts, so a later pass re-examines them. A destructive pass takes its
846
970
  recovery snapshot right before its first mutating evaluator.
847
971
  `THREADKEEPER_CURATOR_WEB_RESEARCH=0` skips the research phase entirely (and
848
972
  its token cost); the evaluator then judges currency from local evidence only.
973
+ A batch that holds no skills never gets a researcher: the fixed research topics
974
+ only concern skills, so its evaluator starts at once.
849
975
  Each report path is then
850
976
  authorized in the existing parent-authored `curator_pass` event;
851
977
  `curator_report_write` records its SHA-256 in `curator_report_provenance`.
@@ -853,34 +979,39 @@ This makes the report directory an untrusted transport: a stray or forged
853
979
  `REPORT-*.md` file cannot acquire the provenance needed by the applier.
854
980
 
855
981
  The web-enabled researcher never receives lesson, skill, or concept mutation
856
- tools. The evaluator never receives web tools. Curator applies its own PATCH /
857
- PRUNE / CONSOLIDATE directly by default (the evaluator writes the REPORT first,
858
- then mutates — `lesson_remove` is in its toolset so it can actually prune and
859
- consolidate duplicate lessons). Set
982
+ tools. The evaluator never receives web tools. By default the evaluator applies
983
+ its own in-place edits directly after writing the REPORT, and the verifier
984
+ applies the confirmed destructive proposals; neither the evaluator's toolset
985
+ nor the server lets the evaluator remove anything. Set
860
986
  `THREADKEEPER_CURATOR_DESTRUCTIVE=0` for advisory REPORT-only. Pinned and
861
- untracked skills remain protected. Foreground-authored skills are protected by
862
- default; set `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS=1` to grant the
863
- Curator explicit snapshot-scoped authority to repair, merge, and delete those
864
- skills too. The opt-in never overrides pins and is accepted only inside a real
865
- Curator pass carrying both pass-id and snapshot-dir context. Lessons are
987
+ untracked skills remain protected. Foreground-authored skills are `[PROTECTED]`
988
+ by default, so only the verifier changes them;
989
+ `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS=1` (which does not limit the
990
+ verifier) additionally lets the
991
+ evaluator make in-place repairs to them. Lessons are
866
992
  stamped with an explicit `origin=<THREADKEEPER_WRITE_ORIGIN>` marker when
867
993
  appended; missing, legacy, or unknown lesson provenance is protected by
868
994
  default. `lesson_remove` and `skill_manage(action='delete')` refuse protected
869
995
  foreground/unknown-origin entries unless `force=True` is called from a
870
- foreground writer; curator/spawned children cannot elevate themselves with
871
- `force`. Before a destructive child is spawned, thread-keeper writes
996
+ foreground writer or the caller is the Curator verifier phase; curator/spawned
997
+ children cannot elevate themselves with `force`. Before a destructive child is spawned, thread-keeper writes
872
998
  a recoverable snapshot under
873
999
  `<reports_dir>/snapshots/<pass-id>/` (default
874
1000
  `~/.threadkeeper/curator/snapshots/<pass-id>/`). The snapshot contains
875
1001
  `lessons.md`, copied in-scope skill dirs, a `manifest.json`, and per-action
876
1002
  tombstones for curator prunes/deletes. Retention is bounded by
877
- `THREADKEEPER_CURATOR_SNAPSHOT_RETENTION` (default 10, current pass always kept).
1003
+ `THREADKEEPER_CURATOR_SNAPSHOT_RETENTION` (default 10) for legacy/direct
1004
+ snapshots. Daemon-owned snapshots instead follow the coordinated workflow
1005
+ retention policy, so their pass, report, manifest, and recovery archive cannot
1006
+ age out on different schedules; active work and an endorsed report that has not
1007
+ been applied always keep recovery material.
878
1008
  Use `curator_restore(pass_id, lesson_slug="...")` or
879
1009
  `curator_restore(pass_id, skill_name="...")` to restore an item from a snapshot.
880
1010
  As a prevention layer before recovery is needed, a destructive Curator pass
881
1011
  has one server-side shared admission budget for `lesson_remove` and
882
1012
  `skill_manage(action='delete')`, including across bounded child batches.
883
- `THREADKEEPER_CURATOR_MAX_DESTRUCTIVE_PER_PASS` defaults to 10; set it to 0 to
1013
+ `THREADKEEPER_CURATOR_MAX_DESTRUCTIVE_PER_PASS` defaults to 30 (only verified
1014
+ proposals delete anything); set it to 0 to
884
1015
  disable those autonomous deletes. The pass ID makes the count durable and
885
1016
  cross-process, while foreground/human deletes are unaffected. `mp_dashboard`
886
1017
  shows admitted and refused operations with `status=HIT` when the Curator reaches
@@ -914,9 +1045,21 @@ The shared lesson file has its own write serialization: `lesson_append`,
914
1045
  `lessons.md.lock` around file creation/read/mutate/write, so foreground calls
915
1046
  and learning-loop children cannot last-writer-win over each other's sections.
916
1047
 
1048
+ `lesson_list(k=20, query="")` lists the newest lessons, or with `query` (the
1049
+ managed instructions pass the opening request) the most relevant ones with a
1050
+ relevance score: semantic when embeddings are available, otherwise the share
1051
+ of query words (any script) a lesson contains. Lesson vectors are stored in the
1052
+ `lesson_vectors` table keyed by body hash and embedding fingerprint, so each
1053
+ body is encoded once per install. Missing vectors are encoded in chunks of 32
1054
+ within an 8-second budget per call; a call that runs out falls back to the
1055
+ lexical ranking and the next call resumes. Each line shows the lesson's summary. Without a query a session
1056
+ would only ever see the newest few lessons, whatever the task.
1057
+
917
1058
  Lesson access is tracked the same way skill access is: `lesson_list` increments
918
1059
  `lesson_usage.view_count` for displayed rows and `lesson_get` increments
919
- `lesson_usage.use_count` for the returned lesson. Curator dry runs include a
1060
+ `lesson_usage.use_count` for the returned lesson. Only foreground reads count:
1061
+ the curator and other loops read every lesson they audit, and counting those
1062
+ reads would make every lesson look recently useful. Curator dry runs include a
920
1063
  ranked `STALE LESSONS (dry-run decay ranking)` section computed as
921
1064
  `access_frequency × exp(-days_since_access / tau)`, filtered to unprotected
922
1065
  lessons with no recent access and low pull-count. That decay list is advisory
@@ -1011,15 +1154,27 @@ with that name instead of minting overlapping roadmap PRs.
1011
1154
 
1012
1155
  The Evolve applier is the downstream implementer. `evolve_apply_roadmap_issue()`
1013
1156
  picks one open GitHub issue at a time (`roadmap` label first, then FIFO), but
1014
- the automatic pass first scans already-open same-repo applier PRs for GitHub
1015
- merge conflicts. A conflicted `roadmap/…` or `evolve/…` PR is repaired before
1016
- any new issue/report/evolve work is started; if the PR sweep itself cannot read
1017
- GitHub state, the pass fails closed instead of taking fresh work blind. The
1018
- conflict-repair child checks out the existing PR branch, merges the current
1019
- base branch, resolves conflicts, runs the full suite, and pushes back to the
1020
- same branch. It then waits for GitHub checks on the pushed PR head and runs
1021
- `gh pr merge --squash --delete-branch`, so GitHub lands the repaired PR into
1022
- `main` through branch protection rather than a raw local `git push origin main`.
1157
+ the automatic pass first sweeps the open PRs authored by the account
1158
+ thread-keeper's `gh` acts as — yours: applier, reviewer, and hand-made PRs
1159
+ alike. A PR from any other account (a collaborator, a bot, a fork) is never
1160
+ updated, repaired, or merged. If the PR sweep itself cannot read GitHub state
1161
+ or the `gh` account, the pass fails closed instead of taking fresh work blind.
1162
+ The sweep squash-merges the oldest non-draft PR GitHub reports `CLEAN` (up to
1163
+ date, required checks green) through the protected merge API, pinned to the
1164
+ head SHA it saw green, and then asks GitHub to update the oldest `BEHIND` one —
1165
+ the "Update branch" button — so CI reruns against the new base. Open
1166
+ work in progress as a draft: drafts are never merged. Strict branch
1167
+ protection lets only one PR merge per base commit, so PRs land one at a time,
1168
+ oldest first. Between full passes the daemon repeats only this cheap landing
1169
+ sweep every 15 minutes, so a green PR lands minutes after its checks finish;
1170
+ it skips while any reviewer/applier git writer runs. A conflicted applier PR
1171
+ (`roadmap/…` or `evolve/…` head branch in this repository) is repaired before
1172
+ any new issue/report/evolve work is started: the conflict-repair child checks
1173
+ out the existing PR branch, merges the current base branch, resolves
1174
+ conflicts, runs the full suite, and pushes back to the same branch, then
1175
+ stops; the landing sweep merges it once it is green. A hand-made PR with
1176
+ conflicts, and red or otherwise blocked PRs, wait for a human. Set
1177
+ `THREADKEEPER_EVOLVE_AUTOLAND=0` to leave every merge to a human.
1023
1178
  The roadmap issue child skips issues carrying denylisted human-gate labels,
1024
1179
  skips issues with an active Evolve claim comment, posts its own claim comment
1025
1180
  before spawning, and advances to the next issue when an issue-local dispatch
@@ -1054,9 +1209,13 @@ task’s tree. This prevents unrelated unfinished tests from blocking every PR
1054
1209
  repair. Ignored files (including `.venv`) and explicit operator checkouts stay
1055
1210
  in place; live writers prevent recovery, and backup failures block dispatch.
1056
1211
  Each managed-checkout child fetches the
1057
- configured branch only to retrieve the configured immutable commit, then
1058
- prepares or resumes its deterministic local/remote feature branch from
1059
- `THREADKEEPER_EVOLVE_REPO_COMMIT`, never from the branch's moving tip. Retries
1212
+ configured branch, prepares or resumes its deterministic local/remote feature
1213
+ branch from the freshly fetched `origin/<THREADKEEPER_EVOLVE_REPO_BRANCH>` tip,
1214
+ rebases onto it, and re-syncs the managed venv to that branch's declared
1215
+ dependencies. `THREADKEEPER_EVOLVE_REPO_COMMIT` bounds only what the parent
1216
+ provisions and parks the checkout at; it is never a branch base, because a base
1217
+ that never moves cut every applier PR from one stale commit and each PR was
1218
+ born conflicted with `main`. Retries
1060
1219
  therefore validate prior branch work instead of discovering a branch-name
1061
1220
  collision after changing the base checkout. A shared git-writer running-task
1062
1221
  check prevents the privileged reviewer audit and code/PR applier from
@@ -1066,8 +1225,9 @@ If a killed child leaves an unresolved merge or plain tracked WIP in the default
1066
1225
  auto-managed checkout, the next code-producing pass archives the diff before
1067
1226
  recovering it. Merge recovery remains limited to `roadmap/…`/`evolve/…`
1068
1227
  branches whose exact PR is confirmed open or merged. For an open PR, the parent
1069
- archives the interrupted merge, aborts it, refreshes the disposable checkout,
1070
- and lets the normal conflict-repair sweep retry that same PR. A merged PR's
1228
+ archives the interrupted merge (including edits the child made after staging
1229
+ a resolution), resets it, refreshes the disposable checkout, and lets the
1230
+ normal conflict-repair sweep retry that same PR. A merged PR's
1071
1231
  leftover merge is discarded as stale. Plain abandoned WIP is recoverable on
1072
1232
  those applier branches when PR state is readable, and also on the configured
1073
1233
  base branch: the disposable base can contain orphaned edits when an older child
@@ -1160,6 +1320,11 @@ daily deterministic `docs/roadmap-audit-YYYY-MM-DD` branch so repeated audit
1160
1320
  passes update or skip the existing roadmap PR rather than opening a second one.
1161
1321
  Automatic apply passes respect the configured interval so multiple foreground
1162
1322
  MCP server startups do not repeatedly spawn workers for the same open issue.
1323
+ That interval is consumed only by a real child launch or a completed empty
1324
+ check. A busy applier, checkout/GitHub/claim/race error, or refused spawn keeps
1325
+ the prior cursor and retries on a bounded short cadence; repeated identical
1326
+ blockers are recorded once until the blocker changes. `evolve_apply_status()`
1327
+ shows the last attempt, last successful dispatch, and any pending retry time.
1163
1328
  Manual tools such as `evolve_apply_conflicted_pr()` and
1164
1329
  `evolve_apply_roadmap_issue()` dispatch immediately. If no conflicted applier PR
1165
1330
  or roadmap issue is startable, the pass falls back to Curator reports and then
@@ -1337,8 +1502,45 @@ Adding a new language is a two-file PR — see [CONTRIBUTING.md](CONTRIBUTING.md
1337
1502
 
1338
1503
  The most-used env knobs (full list in `threadkeeper/config.py`):
1339
1504
 
1505
+ ### MCP tool profiles
1506
+
1507
+ The server can advertise a role-sized MCP tool surface instead of its full
1508
+ catalog. Set `THREADKEEPER_TOOL_PROFILE` before server startup, then restart
1509
+ the host after changing it:
1510
+
1511
+ | Profile | Intended surface |
1512
+ | --- | --- |
1513
+ | `core` | daily session memory: `brief`, `context`, threads, notes, search, and peer coordination |
1514
+ | `coordination` | `core` plus child spawning, task status, pickup, and delegated search |
1515
+ | `memory-admin` | `core` plus durable memory, concepts, lessons, graph links, and explicit erasure |
1516
+ | `learning-loops` | `core` plus extraction, validation, skills, Curator, probes, and dialectic workflows |
1517
+ | `full` | every registered tool; the compatibility default when no profile is configured |
1518
+
1519
+ New `thread-keeper-setup` registrations use `core`. Existing server entries
1520
+ without the setting retain `full` until you opt in. Add exact, comma-separated
1521
+ tool names with `THREADKEEPER_TOOL_INCLUDE`, or remove names with
1522
+ `THREADKEEPER_TOOL_EXCLUDE`; unknown names and contradictory overrides stop the
1523
+ server rather than exposing an unintended surface.
1524
+
1525
+ Spawned children default to `core`; archival, Curator, candidate-review,
1526
+ shadow-review, probe-runner, and dialectic-validator roles use
1527
+ `learning-loops`, while Evolve roles request `full`. The server receives the
1528
+ parent’s resolved allowlist and intersects it with the child profile, so a
1529
+ child cannot regain a tool its parent did not expose.
1530
+ `THREADKEEPER_CHILD_TOOL_PROFILE` explicitly selects a different child profile,
1531
+ still under that parent constraint.
1532
+
1533
+ Every non-full profile includes read-only `capability_search(query)`. It returns
1534
+ at most six matching capability *families* with a summary and the profile that
1535
+ would expose them. It does not list hidden concrete tools or enable any action;
1536
+ use it as the fallback on hosts that lack native deferred tool discovery.
1537
+
1340
1538
  | Knob | Default | Purpose |
1341
1539
  |---|---|---|
1540
+ | `THREADKEEPER_TOOL_PROFILE` | `full` | MCP registry profile: `core`, `coordination`, `memory-admin`, `learning-loops`, or `full`; read at server start |
1541
+ | `THREADKEEPER_TOOL_INCLUDE` | "" | exact comma-separated tool names to add to the selected profile; unknown names fail closed |
1542
+ | `THREADKEEPER_TOOL_EXCLUDE` | "" | exact comma-separated tool names to remove from the selected profile; unknown or overlapping names fail closed |
1543
+ | `THREADKEEPER_CHILD_TOOL_PROFILE` | role default | spawned-child profile override; always intersected with the parent’s resolved tool surface |
1342
1544
  | `THREADKEEPER_DB` | `~/.threadkeeper/db.sqlite` | SQLite file |
1343
1545
  | `THREADKEEPER_TASK_LOG_DIR` | `~/.threadkeeper/tasks` | owner-only task spool for spawn logs, stdin prompts, command scripts, and small runtime logs |
1344
1546
  | `THREADKEEPER_RETENTION_INTERVAL_S` | 0 (off) | SQLite retention/compaction daemon tick; 0 disables the daemon |
@@ -1349,7 +1551,13 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1349
1551
  | `THREADKEEPER_PROBE_RESULT_RETENTION_DAYS` | 0 | prune old `probe_results` and refresh reliability aggregates; 0 keeps forever |
1350
1552
  | `THREADKEEPER_RETENTION_WAL_CHECKPOINT` | false | run `PRAGMA wal_checkpoint(TRUNCATE)` during retention passes |
1351
1553
  | `THREADKEEPER_RETENTION_VACUUM_AFTER_ROWS` | 0 | run `VACUUM` after a pass deletes at least this many rows; 0 disables VACUUM |
1554
+ | `THREADKEEPER_WORKFLOW_RETENTION_DAYS` | 30 | age protection for terminal Curator passes and consumed Evolve research handoffs; `0` disables the age bound |
1555
+ | `THREADKEEPER_WORKFLOW_RETENTION_COUNT` | 100 | newest terminal Curator passes and research handoffs protected by count; `0` disables the count bound |
1352
1556
  | `THREADKEEPER_MEMORY_EGRESS` | `all` | cross-provider scope for personal-class memory (verbatim quotes + dialectic user-model) in `brief()`. `all` = current behavior, egress to whichever vendor backs the consuming CLI. `same-vendor` = personal renders only for Claude/Anthropic, omitted for OpenAI/Google/Microsoft CLIs. `work-only` = personal never rendered, any vendor. See [Memory egress](#memory-egress-cross-provider-privacy) |
1557
+ | `THREADKEEPER_OTEL_ENABLED` | false | enable privacy-safe OTLP tracing only when an endpoint is also configured |
1558
+ | `THREADKEEPER_OTEL_ENDPOINT` | "" | OTLP/HTTP collector trace endpoint, normally `https://collector.example/v1/traces` |
1559
+ | `THREADKEEPER_OTEL_EXPORT_TIMEOUT_S` | 2 | collector request timeout; export happens outside the local workflow |
1560
+ | `THREADKEEPER_OTEL_EXPORT_QUEUE_SIZE` | 256 | bounded per-process span queue (1–2048); oldest queued spans are dropped under pressure |
1353
1561
  | `THREADKEEPER_AUTO_REVIEW` | "" (off) | auto-review on `close_thread` |
1354
1562
  | `THREADKEEPER_AUTO_UPDATE_INTERVAL_S` | 86400 | MCP self-update check interval; 0 disables |
1355
1563
  | `THREADKEEPER_AUTO_UPDATE_RESTART` | "1" | exit MCP process after an update passes setup/import smoke checks so the host restarts on new code |
@@ -1381,11 +1589,11 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1381
1589
  | `THREADKEEPER_CANDIDATE_REVIEW_BATCH_SIZE` | 40 | most pending candidates (oldest first) one reviewer child receives; the rest stay pending for the next pass |
1382
1590
  | `THREADKEEPER_LEARNING_LOOP_SKILL_CREATE_LIMIT` | 2 | max new skills one autonomous learning-loop child (`candidate_review`, `shadow_review`, or `background_review`) may create in its session; foreground creation is unaffected |
1383
1591
  | `THREADKEEPER_CURATOR_INTERVAL_S` | 259200 | deep curator audit every three days; set `0` to disable |
1384
- | `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS` | 0 | allow snapshot-scoped Curator repair/merge/delete of foreground skills; pinned/untracked skills remain protected |
1592
+ | `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS` | 0 | let the Curator evaluator make in-place repairs to foreground skills; the verifier changes foreground skills its proposals name regardless, and pinned skills stay protected |
1385
1593
  | `THREADKEEPER_CURATOR_MIN_LESSONS` | 3 | min lessons before curator engages |
1386
- | `THREADKEEPER_CURATOR_WEB_RESEARCH` | `1` (on) | run a read-only web research child per batch before the web-free evaluator; `0` skips research (the evaluator uses local evidence only) |
1594
+ | `THREADKEEPER_CURATOR_WEB_RESEARCH` | `0` (off) | explicit consent for the isolated Curator web researcher; it receives only fixed public compatibility queries, and decisions that need facts the handoff lacks are marked `DEFER` for a later pass |
1387
1595
  | `THREADKEEPER_CURATOR_DESTRUCTIVE` | `1` (on) | curator child writes its REPORT then applies its own PATCH/PRUNE/CONSOLIDATE directly (incl. `lesson_remove` for prune/consolidate); set `0` for advisory REPORT-only. `[PROTECTED]` entries are refused server-side |
1388
- | `THREADKEEPER_CURATOR_MAX_DESTRUCTIVE_PER_PASS` | 10 | shared server-side ceiling for one destructive Curator pass across `lesson_remove` and `skill_manage(action='delete')`; `0` disables autonomous deletes and foreground deletes bypass it |
1596
+ | `THREADKEEPER_CURATOR_MAX_DESTRUCTIVE_PER_PASS` | 30 | shared server-side ceiling for one destructive Curator pass across `lesson_remove` and `skill_manage(action='delete')`; `0` disables autonomous deletes and foreground deletes bypass it |
1389
1597
  | `THREADKEEPER_CURATOR_SNAPSHOT_RETENTION` | 10 | number of destructive curator pre-mutation snapshots to retain under `<reports_dir>/snapshots`; current pass is always retained |
1390
1598
  | `THREADKEEPER_CURATOR_TRASH_TTL_DAYS` | 30 | days to retain recovery artifacts under `<db dir>/curator/trash` for `lesson_remove` and `skill_manage(action='delete')`; expired artifacts are swept on new trash writes |
1391
1599
  | `THREADKEEPER_PROBE_INTERVAL_S` | 0 (off) | probe daemon tick (s); 1800 = 30 min recommended so finished probe answers are graded promptly |
@@ -1427,12 +1635,17 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1427
1635
  | `THREADKEEPER_SKILL_NUDGE_INTERVAL` | 10 | events between `skill_hint` nudges |
1428
1636
  | `THREADKEEPER_DIALECTIC_MINE_INTERVAL_S` | 0 (off) | dialectic_miner daemon tick (s); 0 disables mechanical observation capture |
1429
1637
  | `THREADKEEPER_DIALECTIC_VALIDATE_INTERVAL_S` | 0 (off) | dialectic_validator daemon tick (s); 0 disables LLM-driven claim synthesis |
1638
+ | `THREADKEEPER_MODEL_ROUTER_INTERVAL_S` | 604800 (weekly) | model-router pass interval (s); 0 disables it and `auto` follows the active CLI |
1639
+ | `THREADKEEPER_MODEL_ROUTER_FALLBACK_CLI` | codex | CLI for the router's fallback run |
1640
+ | `THREADKEEPER_MODEL_ROUTER_FALLBACK_MODEL` | gpt-6.1-sol | model for the router's fallback run |
1641
+ | `THREADKEEPER_MODEL_ROUTER_FALLBACK_EFFORT` | xhigh | effort for the router's fallback run |
1430
1642
  | `THREADKEEPER_DIALECTIC_VALIDATE_MIN` | 5 | min buffered observations before validator engages |
1431
1643
  | `THREADKEEPER_DIALECTIC_VALIDATE_FLUSH_AGE_S` | 259200 | age-flush: an undersized observation buffer is still validated once its oldest eligible row is this old (0 = threshold only) |
1432
1644
  | `THREADKEEPER_DIALECTIC_VALIDATE_BATCH_SIZE` | 50 | max observations sent to one validator child; prevents oversized prompts and drains large queues incrementally |
1433
1645
  | `THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S` | 0 (off) | evolve-reviewer daemon tick (s); audits thread-keeper for safety/leaks/optimization/new ideas, updates roadmap/issues, and includes legacy evolve suggestions as input. Runs as two alternating phases — read-only web research, then a privileged web-free audit that consumes the fenced research digest (#79) — so a full cycle spans two ticks |
1434
1646
  | `THREADKEEPER_EVOLVE_REVIEW_BACKLOG_MAX` | 25 | max open, not-yet-applied `roadmap`-label issues before the issue-creating audit is skipped and records `backlog_saturated`; skip-labelled and untrusted-author roadmap issues still count; `0` disables the cap |
1435
1647
  | `THREADKEEPER_EVOLVE_APPLY_INTERVAL_S` | 0 (off) | evolve-applier daemon tick (s); implements one open GitHub issue at a time, then falls back to Curator reports and promoted legacy evolve suggestions. Empty checks are throttled between intervals; actionable work and manual apply tools still dispatch |
1648
+ | `THREADKEEPER_EVOLVE_AUTOLAND` | true | land open non-draft PRs authored by the account `gh` acts as: squash-merge one once GitHub reports it up to date with required checks green, and update one that fell behind the base. Other accounts' PRs are never touched. `0`/`false` leaves every merge to a human |
1436
1649
  | `THREADKEEPER_EVOLVE_REPO_ROOT` | (auto) | absolute path to the thread-keeper git checkout the evolve reviewer/applier branch, test, and open PRs against. When empty, the repo is resolved automatically: the package's parent dir for an editable `install.sh`, else a managed checkout under the DB dir that is auto-cloned on first use. Set this to pin an explicit checkout |
1437
1650
  | `THREADKEEPER_EVOLVE_AUTO_CLONE` | true | auto-provision a managed checkout that runs remote `pip install -e` and tests; set `0`/`false` on shared or multi-user hosts unless that remote-code-execution boundary is explicitly accepted |
1438
1651
  | `THREADKEEPER_EVOLVE_REPO_URL` | upstream repo | HTTPS `github.com` source for the managed clone; restart-only, and other hosts/schemes are refused |
@@ -1492,7 +1705,7 @@ THREADKEEPER_SPAWN__DEFAULT=claude
1492
1705
  # supported CLI keys: claude, codex, antigravity (agy executable), copilot
1493
1706
  THREADKEEPER_SPAWN__LOOP__SHADOW_OBSERVER=claude # heaviest reasoning → keep on Claude
1494
1707
  THREADKEEPER_SPAWN__LOOP__CURATOR=codex # weekly audit → Codex is fine
1495
- THREADKEEPER_SPAWN__LOOP__CANDIDATE_REVIEWER=auto # "auto" = follow active CLI
1708
+ THREADKEEPER_SPAWN__LOOP__CANDIDATE_REVIEWER=auto # "auto" = model router decides (active CLI until it has)
1496
1709
  # model pin per CLI or per role: THREADKEEPER_SPAWN__MODEL__<KEY>=<model>
1497
1710
  THREADKEEPER_SPAWN__MODEL__CLAUDE=opus
1498
1711
  THREADKEEPER_SPAWN__MODEL__CODEX=gpt-5.5
@@ -1505,7 +1718,8 @@ THREADKEEPER_SPAWN__EFFORT__CURATOR=xhigh
1505
1718
  ```
1506
1719
 
1507
1720
  Resolution per role: `SPAWN__LOOP__<role>` → `SPAWN__DEFAULT` → active CLI →
1508
- `claude`; `"auto"` (or unset) defers to the active CLI. Real environment
1721
+ `claude`; unset defers to the active CLI, and `"auto"` does too until the model
1722
+ router (below) has decided for that role. Real environment
1509
1723
  variables override the `.env`. Force host detection with
1510
1724
  `THREADKEEPER_ACTIVE_CLI=claude` (or `codex`, `antigravity`/`agy`,
1511
1725
  `copilot`). `agy` is normalized to `antigravity`. See `.env.example`
@@ -1513,6 +1727,28 @@ for the full knob list. `spawn_status()` includes warnings when a configured
1513
1727
  spawn CLI is unsupported or a model key does not match a supported CLI/startup
1514
1728
  role, while keeping the same fallback resolution.
1515
1729
 
1730
+ ### Model router (`auto` roles)
1731
+
1732
+ Set a role's CLI to `auto` to let the weekly **model router** choose its CLI,
1733
+ model and effort:
1734
+
1735
+ ```bash
1736
+ THREADKEEPER_SPAWN__LOOP__SHADOW_OBSERVER=auto
1737
+ ```
1738
+
1739
+ The router reviews the last 30 days of that role's spawns (success rate,
1740
+ failures such as provider capacity errors, run time, tokens, recent outcomes)
1741
+ plus the local model catalog, and records one decision per `auto` role with a
1742
+ reason. Until it has decided, `auto` keeps its old meaning and follows the
1743
+ active CLI. While a role is routed, its `SPAWN__MODEL__<ROLE>` and
1744
+ `SPAWN__EFFORT__<ROLE>` pins are ignored and `spawn_status()` warns about them.
1745
+
1746
+ The router runs on `claude` / `opus` / `max` (override with the usual
1747
+ `SPAWN__LOOP|MODEL|EFFORT__MODEL_ROUTER` keys). If that run fails, the same pass
1748
+ reruns on the fallback below; a pass that fails twice retries after 6 hours.
1749
+ `model_router_status()` shows current decisions and `model_router_run()` runs a
1750
+ pass now. Set `THREADKEEPER_MODEL_ROUTER_INTERVAL_S=0` to disable the router.
1751
+
1516
1752
  Adapters without headless support (Claude Desktop, VS Code) can't be
1517
1753
  spawn targets — `spawn_status()` reports them as "no adapter" and any
1518
1754
  override pointing at them falls back to the next priority level.
@@ -1562,6 +1798,38 @@ Three tools keep the memory tidy. `consolidate()` and `forget()` default to
1562
1798
 
1563
1799
  ## Telemetry
1564
1800
 
1801
+ ### Privacy-safe OTLP traces
1802
+
1803
+ OTLP export is **off by default**. To opt in, set both
1804
+ `THREADKEEPER_OTEL_ENABLED=1` and `THREADKEEPER_OTEL_ENDPOINT` to an
1805
+ OTLP/HTTP `/v1/traces` endpoint. ThreadKeeper sends OTLP JSON asynchronously;
1806
+ collector failure, timeout, or a full queue never delays a local MCP call,
1807
+ spawn, watchdog action, or SQLite write.
1808
+
1809
+ One spawned workflow is linked as `MCP spawn → workflow → agent → child MCP
1810
+ operation`. The W3C trace context is saved with the task and passed only in the
1811
+ private child environment (and its slim MCP config), never in a prompt or CLI
1812
+ argument. A watchdog continuation uses the prior agent span as its parent, so
1813
+ retries remain in the same trace.
1814
+
1815
+ The exporter has a fixed allowlist: operation/tool identifier, latency and
1816
+ queue wait, provider/model identifiers, input/output/total tokens, USD cost,
1817
+ retry count, and terminal outcome. Operation names are fixed; tool/provider
1818
+ identifiers are limited to 64 safe characters, model identifiers to 96, and a
1819
+ process exports at most 64 distinct model values (later values become
1820
+ `unknown`).
1821
+ Everything else is excluded by construction: prompts, MCP arguments/results,
1822
+ memory bodies, user quotes, credentials, paths, task logs, and SQLite event
1823
+ summaries are never span attributes.
1824
+
1825
+ The local SQLite event ledger remains the source of truth. It retains only the
1826
+ opaque trace linkage beside a task row, so its existing task-retention policy
1827
+ also removes that linkage. Configure collector retention at the collector; no
1828
+ trace payload is persisted locally. If traces do not arrive, first verify the
1829
+ endpoint includes `/v1/traces`; then inspect collector connectivity. A full
1830
+ queue intentionally drops its oldest pending spans rather than growing memory
1831
+ or blocking the workflow.
1832
+
1565
1833
  - **`mp_dashboard(window_days=7)`** — one-call rollup of the whole
1566
1834
  system, read-only. Three sections: **stores** (threads by state,
1567
1835
  notes/dialog/distill/concepts counts, skills + claims by tier,
@@ -1634,7 +1902,9 @@ that keeps a write transaction open for a minute or more.
1634
1902
 
1635
1903
  Schema migration uses SQLite `PRAGMA user_version`: a current database skips
1636
1904
  legacy `ALTER TABLE` work, while an old or fresh v0 database migrates once
1637
- under a writer transaction and records the current version. Duplicate-column
1905
+ under a writer transaction and records the current version. Every startup
1906
+ also replays the idempotent baseline `CREATE ... IF NOT EXISTS` schema, so a
1907
+ table added after the last version bump still reaches existing databases. Duplicate-column
1638
1908
  migrations are the only expected no-op; other DDL errors are logged and raised.
1639
1909
 
1640
1910
  On POSIX systems, startup and `get_db()` harden the default local store
@@ -1642,9 +1912,10 @@ best-effort: `~/.threadkeeper` is `0700`, while `db.sqlite`, SQLite
1642
1912
  `-wal`/`-shm` sidecars, `~/.threadkeeper/.env`, curator `REPORT-*.md`
1643
1913
  files, and headless spawn logs are owner-only (`0600`).
1644
1914
 
1645
- Use `tk-backup` for disaster recovery. It uses SQLite `VACUUM INTO`, so
1646
- committed frames still living in the live `-wal` sidecar are included in a
1647
- compacted snapshot without quiescing background writers:
1915
+ Use `tk-backup` for disaster recovery. In the default SQLite profile it uses
1916
+ `VACUUM INTO`; the encrypted profile uses SQLCipher's online backup API into a
1917
+ new encrypted destination. Both paths include committed frames still living in
1918
+ the live `-wal` sidecar without quiescing background writers:
1648
1919
 
1649
1920
  ```bash
1650
1921
  tk-backup create ~/threadkeeper-backup.sqlite
@@ -1667,6 +1938,73 @@ first and copy `db.sqlite`, `db.sqlite-wal`, and `db.sqlite-shm` together. To
1667
1938
  wipe memory, also stop thread-keeper first, then remove the main DB and both
1668
1939
  sidecars.
1669
1940
 
1941
+ ### Opt-in encrypted storage (experimental)
1942
+
1943
+ The default `sqlite` profile is unchanged. For sensitive memory at rest, the
1944
+ optional `sqlcipher` profile encrypts the main database, WAL/rollback-journal
1945
+ pages, and profile-aware snapshots with SQLCipher. SQLCipher temporary
1946
+ structures are kept in memory with cipher memory security enabled. Install it
1947
+ explicitly and initialize a new store before starting any MCP server:
1948
+
1949
+ ```bash
1950
+ pip install 'threadkeeper[encrypted]'
1951
+ tk-storage init --db ~/.threadkeeper/secure.sqlite
1952
+ export THREADKEEPER_DB=~/.threadkeeper/secure.sqlite
1953
+ export THREADKEEPER_STORAGE_PROFILE=sqlcipher
1954
+ ```
1955
+
1956
+ The profile name may live in normal configuration; the encryption key never
1957
+ does. Thread-keeper generates a random key and retrieves it from the operating
1958
+ system secret store through `keyring`. It never accepts a key in an environment
1959
+ variable, config file, command argument, event, log, or telemetry. If the
1960
+ secret store, SQLCipher backend, or key is unavailable, opening the encrypted
1961
+ profile fails closed. A wrong key cannot initialize or replace an existing
1962
+ database.
1963
+
1964
+ Use the normal backup command after enabling the profile; it recognizes the
1965
+ profile and creates an encrypted destination with its own OS-secret-store key:
1966
+
1967
+ ```bash
1968
+ tk-backup create ~/threadkeeper-secure-backup.sqlite
1969
+ tk-backup restore ~/threadkeeper-secure-backup.sqlite --yes
1970
+ tk-storage integrity-check --db ~/.threadkeeper/secure.sqlite
1971
+ tk-storage rekey --db ~/.threadkeeper/secure.sqlite
1972
+ ```
1973
+
1974
+ Stop all MCP servers, the daemon host, and CLI sessions before a restore or
1975
+ rekey. If a machine loses power during rekey, normal opens intentionally stop;
1976
+ run `tk-storage recover-rekey --db …` to have the OS secret store validate and
1977
+ finish the pending rotation without printing either key. Back up the operating
1978
+ system account and its secret store according to your platform's recovery
1979
+ procedure. Thread-keeper deliberately has no key-export command: exporting a
1980
+ raw database key into a shell history or ordinary file defeats this profile.
1981
+
1982
+ Move an existing store through a separately validated output path. The source
1983
+ is never replaced, so it remains available until the new store has been
1984
+ verified:
1985
+
1986
+ ```bash
1987
+ tk-storage migrate ~/.threadkeeper/db.sqlite ~/.threadkeeper/secure.sqlite \
1988
+ --from-profile sqlite --to-profile sqlcipher
1989
+ tk-storage migrate ~/.threadkeeper/secure.sqlite ~/.threadkeeper/plain.sqlite \
1990
+ --from-profile sqlcipher --to-profile sqlite
1991
+ ```
1992
+
1993
+ Only switch `THREADKEEPER_DB` and `THREADKEEPER_STORAGE_PROFILE` after the
1994
+ destination passes `tk-storage integrity-check` (for SQLCipher) or
1995
+ `sqlite3 <path> 'PRAGMA integrity_check'` (for ordinary SQLite). The migration
1996
+ checks integrity plus table/index/trigger coverage before the destination is
1997
+ atomically published.
1998
+
1999
+ This is an at-rest database profile, not full-device encryption. It does not
2000
+ encrypt task logs, curator reports/snapshots, exported briefs, source CLI
2001
+ transcripts, skills, `node.id`, or sync traffic. Put the whole state directory
2002
+ on full-disk encryption, use the existing owner-only permissions, set retention
2003
+ for logs/reports, and use TLS/private networking for sync. The profile is
2004
+ experimental: automated SQLCipher coverage currently runs on Linux; validate
2005
+ the OS keyring integration on macOS or Windows before relying on it for a
2006
+ recovery-sensitive deployment.
2007
+
1670
2008
  ### Selective Erasure
1671
2009
 
1672
2010
  For one regretted or sensitive conversation, use targeted erasure instead of
@@ -1702,6 +2040,19 @@ the high-volume tables (`dialog_messages`, `dialog_fts`, `dialog_vec`,
1702
2040
  `signals`, `events`, `tasks`, `probe_results`) so growth is visible before it
1703
2041
  becomes a problem.
1704
2042
 
2043
+ Terminal Curator passes own their frozen batch text, `REPORT-*`, `AUDIT-*`, and
2044
+ destructive snapshot; Evolve handoffs own their `RESEARCH-*` digest. The
2045
+ retention daemon keeps active/retryable work, recovery material, and endorsed
2046
+ but unapplied reports. After both enabled workflow bounds expire, it first
2047
+ compacts batch/research bodies to a SHA-256 plus a 512-character summary and
2048
+ removes only owned files. A later pass removes the compacted rows. File removal
2049
+ is path-contained to the configured Curator or Evolve directory, rejects
2050
+ symlinks, and is journaled so an interrupted delete retries safely. Use
2051
+ `workflow_retention_preview()` for the dry-run counts/bytes and oldest terminal
2052
+ pass; `mp_dashboard()` shows its latest outcome. A selective `forget()` that
2053
+ matches a frozen workflow body compacts it immediately, including active work;
2054
+ the normal report/lesson review guidance still applies to generalized content.
2055
+
1705
2056
  `db_compact()` is the opt-in disk-reclaim tool: `VACUUM` + a mandatory
1706
2057
  `dialog_fts` rebuild (schema v2 keys the FTS index on `dialog_messages`
1707
2058
  rowids, which `VACUUM` is permitted to renumber — the rebuild is what
@@ -1868,9 +2219,9 @@ reusable verdict logic lives in `threadkeeper/verify_ingest.py`.
1868
2219
  ## Memory-quality evaluation
1869
2220
 
1870
2221
  The ingest verifier above answers *"did we capture the data?"*. The
1871
- memory-quality harness answers the harder question — *"when we retrieve it,
1872
- do we recall the right fact, and do we **refuse** to answer about things that
1873
- never happened?"* It's modeled on
2222
+ memory-quality harness answers two separate questions: *"did we retrieve the
2223
+ right evidence?"* and *"does an agent produce the right final answer from
2224
+ that evidence?"* It's modeled on
1874
2225
  [LongMemEval](https://arxiv.org/pdf/2410.10813) (ICLR 2025) plus mem0's 2026
1875
2226
  [tokens-per-retrieval](https://mem0.ai/blog/ai-memory-benchmarks-in-2026)
1876
2227
  cost axis, and runs the **real** `search()` / `dialog_search()` / `brief()`
@@ -1881,24 +2232,34 @@ python scripts/memory_eval/run.py # bundled demo corpus, lexical
1881
2232
  python scripts/memory_eval/run.py --json # machine-readable report
1882
2233
  python scripts/memory_eval/run.py --db snap.sqlite --ground-truth my_labels.json
1883
2234
  python scripts/memory_eval/run.py --semantic # use embeddings if installed
1884
- python scripts/memory_eval/run.py --judge llm # LLM-graded (needs ANTHROPIC_API_KEY)
2235
+ python scripts/memory_eval/run.py --matrix --strict # FTS/hybrid migration gate
2236
+ age -d holdout.json.age | python scripts/memory_eval/run.py \
2237
+ --ground-truth /dev/stdin --private-holdout # no per-case output
1885
2238
  ```
1886
2239
 
1887
- It reports four headline groups over a fixed ground-truth set:
2240
+ It reports these headline groups over a fixed ground-truth set:
1888
2241
 
1889
- - **accuracy** — fraction of questions whose retrieval recalled the gold
1890
- fact, broken out per the five LongMemEval axes (information extraction,
1891
- multi-session reasoning, temporal reasoning, knowledge updates, abstention).
2242
+ - **evidence recall** and **final-answer correctness** — separate scores, both
2243
+ per axis. The final answer comes from a deterministic replay responder that
2244
+ may emit only facts found in the returned context; it gives CI a stable
2245
+ boundary between retrieval and reasoning failures.
1892
2246
  - **abstention rate** — of the *never-happened* questions, the fraction the
1893
- system correctly refused. This is the highest-payoff axis: it directly
1894
- measures whether the auto-injected `brief()` context fabricates or surfaces
1895
- stale facts.
2247
+ system correctly refused. Retention/curation removals and egress-blocked
2248
+ personal memory are also required to end in a refusal.
1896
2249
  - **tokens-per-retrieval** — mean / median / max tokens of what each query
1897
- returned, so recall is never read apart from cost (a wider window that
1898
- recalls more also costs more).
2250
+ returned, plus equivalent final-answer token totals.
1899
2251
  - **retrieval latency** — mean / p50 / p95 / max wall-clock milliseconds. With
1900
2252
  `--semantic`, the backend is reported as `hybrid`, because dense candidates
1901
2253
  augment rather than replace FTS.
2254
+ - **outcome classification** — retrieval failure, reasoning failure,
2255
+ stale-memory use, removed-memory use, and privacy-policy violation.
2256
+
2257
+ `--matrix` runs every public-fixture case through the FTS and hybrid backends,
2258
+ three fixture states (`baseline`, `retained`, `curated`), and every personal
2259
+ memory egress policy (`all`, `same-vendor`, `work-only`). A hybrid row is marked
2260
+ unavailable when the optional semantic extra is not installed. `--strict`
2261
+ requires the versioned fixture thresholds and zero safety violations, making
2262
+ the same command suitable for retrieval/model migration gates.
1902
2263
 
1903
2264
  For DB concurrency, run the reproducible local gate:
1904
2265
 
@@ -1910,18 +2271,20 @@ The JSON result includes expected/actual writes, throughput, p50/p95/p99/max
1910
2271
  write latency, worker errors, elapsed time, and `PRAGMA quick_check`; a non-zero
1911
2272
  exit means a lost write, worker failure, or integrity failure.
1912
2273
 
1913
- With no `--db` the harness builds the bundled fixture
1914
- (`scripts/memory_eval/ground_truth.json` — a fictional "billing service" told
1915
- across three sessions) into a throwaway DB; it's a **golden baseline** where a
1916
- faithful retrieval scores 100%, so a regression in the retrieval tools drops
1917
- the number. `--db` runs **read-only**: the snapshot is copied to a temp file
1918
- and the original is never opened for writing. The default judge is **lexical**
1919
- (deterministic, offline, no API key, no embeddings) so the command is
1920
- reproducible and CI-safe; `--judge llm` grades answer *reasoning* (not just
1921
- retrieval recall) with an Anthropic model when a key is set — the intended
1922
- optimization target for lesson-decay tuning (#27) and bi-temporal claims (#28)
1923
- work. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for how the
1924
- axes map onto thread-keeper's retrieval surface.
2274
+ With no `--db` the harness builds the bundled, versioned public synthetic
2275
+ fixture (`scripts/memory_eval/ground_truth.json` — a fictional "billing
2276
+ service") into a throwaway DB. Bump its `version` whenever cases, labels, or
2277
+ thresholds change; CI uses the checked-in version and its stable thresholds.
2278
+ `--db` runs **read-only**: the snapshot is copied to a temp file and the
2279
+ original is never opened for writing.
2280
+
2281
+ For a private holdout, keep the corpus encrypted or outside the repository and
2282
+ pass it with `--private-holdout`. The runner does not decrypt data itself; it
2283
+ accepts a locally decrypted file or stdin and deletes its temporary DB on exit.
2284
+ Private mode emits aggregate metrics only — no case rows, failure details,
2285
+ retrieved context, or replayed answer is written to stdout, artifacts, or the
2286
+ repository. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the corpus
2287
+ contract and migration-gate interpretation.
1925
2288
 
1926
2289
  ## Evaluating learning-loop decision quality
1927
2290
 
@@ -2023,7 +2386,9 @@ threadkeeper/
2023
2386
  so `tools/list` carries MCP 2025-06-18 `ToolAnnotations` for every
2024
2387
  registered tool: `readOnlyHint=True` for pure reads (`brief`, `context`, `search`,
2025
2388
  `dialog_search`, the status tools, …) and `readOnlyHint=False`
2026
- for mutations. `lesson_list` / `lesson_get` are classified as non-destructive
2389
+ for mutations. Advertised reads use query-only database connections and never
2390
+ bootstrap a session, start daemon work, refresh process data, or mutate
2391
+ lifecycle state. `lesson_list` / `lesson_get` are classified as non-destructive
2027
2392
  writes because they bump lesson access counters. Delete/overwrite/kill tools
2028
2393
  carry `destructiveHint=True` (`compost` is read-only — it only surfaces idle
2029
2394
  threads). A confirmation/elicitation host reads this to decide which calls
@@ -2038,6 +2403,15 @@ capability before prompting, uses only a flat primitive schema, and leaves
2038
2403
  unsupported clients on the existing text/tool fallback path. The first protected
2039
2404
  write is `dialectic_supersede`.
2040
2405
 
2406
+ **Protocol compatibility (#344).** The stdio server supports the
2407
+ `2025-11-25` initialize-handshake era and the `2026-07-28` request-envelope
2408
+ era. Legacy hosts retain the existing tools, resources, and prompts. Modern
2409
+ calls derive identity and capabilities from every request, static discovery
2410
+ catalogs advertise public cache freshness, and replaying a completed write with
2411
+ the same caller-bound JSON-RPC id returns the original result instead of
2412
+ committing a second mutation. Sealed continuation state expires after five
2413
+ minutes; an interrupted write with no terminal result is rejected on replay.
2414
+
2041
2415
  Detailed map in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
2042
2416
  Open work in [docs/ROADMAP.md](docs/ROADMAP.md) and the
2043
2417
  [Issues tab](https://github.com/po4erk91/thread-keeper/issues).