threadkeeper 0.19.1__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 (307) hide show
  1. {threadkeeper-0.19.1/threadkeeper.egg-info → threadkeeper-0.20.0}/PKG-INFO +299 -47
  2. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/README.md +294 -45
  3. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/pyproject.toml +13 -5
  4. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_curator.py +487 -17
  5. threadkeeper-0.20.0/tests/test_dashboard_app.py +143 -0
  6. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_db_schema.py +85 -0
  7. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_egress_policy.py +10 -3
  8. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_evolve_applier.py +244 -5
  9. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_host_env.py +38 -1
  10. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_lessons.py +596 -22
  11. threadkeeper-0.20.0/tests/test_mcp_server_instructions.py +139 -0
  12. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_mcp_skills_extension.py +5 -1
  13. threadkeeper-0.20.0/tests/test_mcp_tasks.py +158 -0
  14. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_menubar_app.py +1 -1
  15. threadkeeper-0.20.0/tests/test_model_router.py +244 -0
  16. threadkeeper-0.20.0/tests/test_model_router_digest.py +107 -0
  17. threadkeeper-0.20.0/tests/test_model_router_tools.py +194 -0
  18. threadkeeper-0.20.0/tests/test_model_routes.py +183 -0
  19. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_release_workflows.py +18 -4
  20. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_setup.py +1 -1
  21. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skill_tier.py +137 -0
  22. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skill_updater.py +69 -0
  23. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skill_use_parser.py +33 -0
  24. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skills.py +160 -37
  25. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_config.py +193 -0
  26. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_watchdog.py +32 -0
  27. threadkeeper-0.20.0/tests/test_storage.py +256 -0
  28. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_tool_annotations.py +4 -3
  29. threadkeeper-0.20.0/tests/test_tool_profiles.py +170 -0
  30. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/_mcp.py +32 -1
  31. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/_setup.py +7 -55
  32. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/_spawn_wrap.py +14 -1
  33. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/base.py +5 -0
  34. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/codex.py +1 -0
  35. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/agent_metadata.py +15 -0
  36. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/agent_status.py +104 -17
  37. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/README.md +5 -0
  38. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +49 -13
  39. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/backup.py +37 -0
  40. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/config.py +44 -3
  41. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/curator.py +815 -262
  42. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/curator_snapshots.py +93 -0
  43. threadkeeper-0.20.0/threadkeeper/dashboard_app.py +411 -0
  44. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/db.py +145 -26
  45. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/evolve_applier.py +170 -32
  46. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/github_budget.py +9 -1
  47. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/host.py +42 -17
  48. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/ingest.py +22 -1
  49. threadkeeper-0.20.0/threadkeeper/instructions.py +80 -0
  50. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/lessons.py +130 -6
  51. threadkeeper-0.20.0/threadkeeper/mcp_tasks.py +327 -0
  52. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/migrate_embeddings.py +3 -2
  53. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/model_catalog.py +9 -0
  54. threadkeeper-0.20.0/threadkeeper/model_router.py +373 -0
  55. threadkeeper-0.20.0/threadkeeper/model_router_digest.py +234 -0
  56. threadkeeper-0.20.0/threadkeeper/model_routes.py +318 -0
  57. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/notify.py +1 -0
  58. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/server.py +10 -1
  59. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/skill_audit.py +4 -1
  60. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/skill_updater.py +41 -0
  61. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/spawn_budget.py +31 -23
  62. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/spawn_config.py +166 -45
  63. threadkeeper-0.20.0/threadkeeper/storage.py +740 -0
  64. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/migrate.py +13 -13
  65. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/protocol.py +3 -1
  66. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/reset_node.py +5 -2
  67. threadkeeper-0.20.0/threadkeeper/tool_profiles.py +364 -0
  68. threadkeeper-0.20.0/threadkeeper/tools/capabilities.py +15 -0
  69. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/curator.py +34 -6
  70. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/dashboard.py +1 -0
  71. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/evolve_applier.py +20 -1
  72. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/lessons.py +577 -256
  73. threadkeeper-0.20.0/threadkeeper/tools/model_router.py +152 -0
  74. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/skills.py +139 -42
  75. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/spawn.py +19 -0
  76. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/workflow_artifacts.py +7 -0
  77. {threadkeeper-0.19.1 → threadkeeper-0.20.0/threadkeeper.egg-info}/PKG-INFO +299 -47
  78. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper.egg-info/SOURCES.txt +19 -0
  79. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper.egg-info/entry_points.txt +1 -0
  80. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper.egg-info/requires.txt +5 -1
  81. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/LICENSE +0 -0
  82. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/setup.cfg +0 -0
  83. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_adapters.py +0 -0
  84. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_agent_status.py +0 -0
  85. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_antigravity_ingest.py +0 -0
  86. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_auto_update.py +0 -0
  87. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_backup.py +0 -0
  88. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_brief_footprint.py +0 -0
  89. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_brief_sections.py +0 -0
  90. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_candidate_reviewer.py +0 -0
  91. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_ci_parallel.py +0 -0
  92. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_concepts.py +0 -0
  93. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_config_settings.py +0 -0
  94. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_config_watcher.py +0 -0
  95. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_core_memory.py +0 -0
  96. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_daemon_host_config.py +0 -0
  97. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_daemon_host_integration.py +0 -0
  98. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_daemon_liveness.py +0 -0
  99. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_daemon_state.py +0 -0
  100. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dashboard.py +0 -0
  101. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_db_compact.py +0 -0
  102. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_db_contention.py +0 -0
  103. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_db_leak_guard.py +0 -0
  104. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_db_migration_fts.py +0 -0
  105. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_db_runtime.py +0 -0
  106. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_delegated_search.py +0 -0
  107. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic.py +0 -0
  108. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic_feed_tools.py +0 -0
  109. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic_miner.py +0 -0
  110. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic_observation_resolve.py +0 -0
  111. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic_recompute.py +0 -0
  112. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic_tier.py +0 -0
  113. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialectic_validator.py +0 -0
  114. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_dialog_fts_external.py +0 -0
  115. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_documentation_inventory.py +0 -0
  116. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_embed_routing.py +0 -0
  117. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_embedding_generation.py +0 -0
  118. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_embedding_model_revision.py +0 -0
  119. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_embedding_storage_dedup.py +0 -0
  120. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_error_paths.py +0 -0
  121. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_eval_harness.py +0 -0
  122. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_evolve_apply_2.py +0 -0
  123. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_evolve_apply_3.py +0 -0
  124. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_evolve_apply_43.py +0 -0
  125. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_evolve_apply_45.py +0 -0
  126. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_evolve_daemon.py +0 -0
  127. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_extract_daemon.py +0 -0
  128. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_extract_dedup.py +0 -0
  129. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_forget.py +0 -0
  130. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_github_budget.py +0 -0
  131. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_github_safety.py +0 -0
  132. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_helpers.py +0 -0
  133. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_hook_child_guard.py +0 -0
  134. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_host_embed.py +0 -0
  135. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_host_main.py +0 -0
  136. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_host_wedged.py +0 -0
  137. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_hybrid_retrieval.py +0 -0
  138. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_i18n_multilang.py +0 -0
  139. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_identity.py +0 -0
  140. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_ingest_cursor.py +0 -0
  141. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_ingest_denylist.py +0 -0
  142. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_ingest_secret_redaction.py +0 -0
  143. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_ingest_status.py +0 -0
  144. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_ingest_transactions.py +0 -0
  145. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_lesson_violations.py +0 -0
  146. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_mcp_protocol_conformance.py +0 -0
  147. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_mcp_resources_prompts.py +0 -0
  148. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_mcp_sdk_compat.py +0 -0
  149. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_memory_authority_lifecycle.py +0 -0
  150. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_memory_compatibility.py +0 -0
  151. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_memory_eval.py +0 -0
  152. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_memory_guard.py +0 -0
  153. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_memory_guard_host.py +0 -0
  154. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_missed_spawns.py +0 -0
  155. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_model_catalog.py +0 -0
  156. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_notify_daemon.py +0 -0
  157. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_nudges.py +0 -0
  158. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_onnx_embeddings.py +0 -0
  159. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_panel.py +0 -0
  160. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_permissions.py +0 -0
  161. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_pickup.py +0 -0
  162. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_probe_daemon.py +0 -0
  163. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_process_health.py +0 -0
  164. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_project_scope.py +0 -0
  165. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_retention.py +0 -0
  166. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_review_prompts.py +0 -0
  167. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_search_fts_punctuation.py +0 -0
  168. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_session_read_purity.py +0 -0
  169. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_shadow_review.py +0 -0
  170. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
  171. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skill_hint.py +0 -0
  172. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skill_passive_tier.py +0 -0
  173. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_skill_watcher.py +0 -0
  174. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_background_cwd.py +0 -0
  175. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_budget.py +0 -0
  176. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_child_env.py +0 -0
  177. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_codex_stdin.py +0 -0
  178. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_hint.py +0 -0
  179. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_permission_gate.py +0 -0
  180. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_reap.py +0 -0
  181. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_result.py +0 -0
  182. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_slim.py +0 -0
  183. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_worktree_isolation.py +0 -0
  184. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_spawn_wrap.py +0 -0
  185. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_sync_capture.py +0 -0
  186. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_sync_identity.py +0 -0
  187. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_sync_migrate.py +0 -0
  188. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_sync_protocol.py +0 -0
  189. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_sync_reset_node.py +0 -0
  190. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_sync_transport.py +0 -0
  191. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_task_retention.py +0 -0
  192. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_task_spool.py +0 -0
  193. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_thin_session.py +0 -0
  194. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_thread_janitor.py +0 -0
  195. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_threads.py +0 -0
  196. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_tools_smoke.py +0 -0
  197. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_tracing.py +0 -0
  198. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_validate_threads.py +0 -0
  199. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_vec_search.py +0 -0
  200. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_verify_ingest.py +0 -0
  201. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/tests/test_workflow_retention.py +0 -0
  202. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/__init__.py +0 -0
  203. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/__init__.py +0 -0
  204. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  205. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/antigravity.py +0 -0
  206. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/claude_code.py +0 -0
  207. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  208. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/copilot.py +0 -0
  209. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/adapters/vscode.py +0 -0
  210. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  211. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  212. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  213. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/authority.py +0 -0
  214. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/auto_update.py +0 -0
  215. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/brief.py +0 -0
  216. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/candidate_reviewer.py +0 -0
  217. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/config_io.py +0 -0
  218. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/config_watcher.py +0 -0
  219. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/daemon_liveness.py +0 -0
  220. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/daemon_state.py +0 -0
  221. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/daemon_supervisor.py +0 -0
  222. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/dialectic_miner.py +0 -0
  223. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/dialectic_validator.py +0 -0
  224. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/egress.py +0 -0
  225. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/elicitation.py +0 -0
  226. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/embeddings.py +0 -0
  227. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/eval/__init__.py +0 -0
  228. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/eval/__main__.py +0 -0
  229. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/eval/harness.py +0 -0
  230. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/evolve_daemon.py +0 -0
  231. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/extract_daemon.py +0 -0
  232. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/forget.py +0 -0
  233. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/github_safety.py +0 -0
  234. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/harvest.py +0 -0
  235. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/helpers.py +0 -0
  236. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/host_embed.py +0 -0
  237. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/i18n.py +0 -0
  238. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/identity.py +0 -0
  239. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/lesson_violations.py +0 -0
  240. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/link_health.py +0 -0
  241. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/mcp_skills.py +0 -0
  242. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/memory_compat.py +0 -0
  243. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/memory_guard.py +0 -0
  244. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/menubar_app.py +0 -0
  245. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/nudges.py +0 -0
  246. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/permissions.py +0 -0
  247. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/probe_daemon.py +0 -0
  248. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/process_health.py +0 -0
  249. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/project_scope.py +0 -0
  250. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/protocol.py +0 -0
  251. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/read_context.py +0 -0
  252. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/retention.py +0 -0
  253. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/retrieval.py +0 -0
  254. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/review_prompts.py +0 -0
  255. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/search_proxy.py +0 -0
  256. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/shadow_review.py +0 -0
  257. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/skill_watcher.py +0 -0
  258. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/spawn_result.py +0 -0
  259. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/__init__.py +0 -0
  260. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/capture.py +0 -0
  261. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/daemon.py +0 -0
  262. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/identity.py +0 -0
  263. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/sync/server.py +0 -0
  264. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/task_spool.py +0 -0
  265. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/thread_janitor.py +0 -0
  266. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tool_schemas.py +0 -0
  267. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/__init__.py +0 -0
  268. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/agent_status.py +0 -0
  269. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/authority.py +0 -0
  270. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  271. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/concepts.py +0 -0
  272. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/config_watch.py +0 -0
  273. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/consolidate.py +0 -0
  274. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/core_memory.py +0 -0
  275. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/correlation.py +0 -0
  276. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/db_maintenance.py +0 -0
  277. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/dialectic.py +0 -0
  278. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  279. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/dialog.py +0 -0
  280. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/distill.py +0 -0
  281. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/evolve_research.py +0 -0
  282. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/extract.py +0 -0
  283. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/forget.py +0 -0
  284. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/graph.py +0 -0
  285. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/invariants.py +0 -0
  286. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/memory_guard.py +0 -0
  287. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/missed_spawns.py +0 -0
  288. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/panel.py +0 -0
  289. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/peers.py +0 -0
  290. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/pickup.py +0 -0
  291. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/probes.py +0 -0
  292. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/process_health.py +0 -0
  293. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/project_scope.py +0 -0
  294. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/prompts.py +0 -0
  295. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/resources.py +0 -0
  296. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/session.py +0 -0
  297. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/shadow_review.py +0 -0
  298. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/style.py +0 -0
  299. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/sync.py +0 -0
  300. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/threads.py +0 -0
  301. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tools/validate.py +0 -0
  302. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/tracing.py +0 -0
  303. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/trash.py +0 -0
  304. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/verify_ingest.py +0 -0
  305. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper/wikilinks.py +0 -0
  306. {threadkeeper-0.19.1 → threadkeeper-0.20.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  307. {threadkeeper-0.19.1 → 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.1
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:
@@ -234,6 +243,7 @@ read/act split, plus MCP elicitation for host-native confirmations:
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
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
@@ -337,9 +383,13 @@ cross-project recall. Those results show `scope=`/`project=` provenance and
337
383
  write a `cross_project_recall` audit event. The `memory_scope_status()` tool
338
384
  shows the current namespace and the migration accounting.
339
385
 
340
- Existing databases are migrated conservatively: rows without reliable
341
- repository provenance are marked `unresolved`, excluded from automatic recall,
342
- and listed in the migration report until an operator classifies them. This
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
343
393
  boundary controls *which local memory is recalled*; it is separate from
344
394
  `THREADKEEPER_MEMORY_EGRESS`, which controls whether personal memory may be
345
395
  sent to another model provider.
@@ -688,9 +738,23 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
688
738
  though an existing same-slug long lesson may be corrected without increasing
689
739
  its body size; near-duplicate slugs are blocked, and semantic body matches are
690
740
  routed to the incumbent lesson or surfaced for curation instead of minting a
691
- sibling lesson. A clear new directive or debunk also flags older permissive
692
- lessons on the same concrete practice for patch, cross-link, or supersession
693
- 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`.
694
758
  Before a genuinely new fallback lesson is written,
695
759
  `lesson_neighbors(title, body, summary, k=3)` shows the nearest existing lesson
696
760
  slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
@@ -790,10 +854,10 @@ flags a dense lesson subtopic when at least
790
854
  concrete title terms and a non-generic body mechanism. Generic joins and
791
855
  domain-wide/high-document-frequency title words cannot form a candidate. The
792
856
  inventory and `curator_review_status()` report emitted/rejected counts and
793
- rejection reasons, so the rule can be tuned from production data. A
794
- non-protected candidate must become one validated, checklist-style canonical
795
- skill before its source lessons are retired; protected clusters are left for
796
- human review. The evaluator reads every complete skill and relevant support
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
797
861
  file, but has no web tools. External research is off by default. With explicit
798
862
  `THREADKEEPER_CURATOR_WEB_RESEARCH=1`, a separate Claude-only researcher gets
799
863
  only fixed public compatibility queries, `WebSearch`, `WebFetch`, and one
@@ -802,11 +866,44 @@ repository. The evaluator receives that handoff as fenced, untrusted data and
802
866
  writes numbered per-skill verdicts to
803
867
  `~/.threadkeeper/curator/REPORT-<isodate>.md` for a one-batch pass or
804
868
  `REPORT-<isodate>-batch-NNN-of-MMM.md` for a multi-batch pass: KEEP / REPAIR /
805
- UPDATE / MERGE / SPLIT / DEPRECATE / DELETE / CROSS_LINK / HUMAN_REVIEW.
806
- Similar names and cosine scores are only candidates; merge/delete decisions
807
- compare intent, workflow, inputs, outcomes, and unique details. Pinned and
808
- foreground-authored entries are marked `[PROTECTED]`, and delete-class tools
809
- 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
810
907
  single-flight across processes — a non-blocking `fcntl.flock` pidfile
811
908
  (`<db dir>/curator.lock`) plus a running-children check serialize it, so
812
909
  multiple MCP server instances can't run overlapping (now destructive) passes
@@ -868,11 +965,13 @@ SHA-256 as provenance. Research and evaluation are two phases of each batch
868
965
  inside the durable pass: the web-free evaluator for a batch starts only after
869
966
  that batch's handoff validates. A researcher that fails or leaves a missing,
870
967
  malformed, swapped, or mismatched handoff is retried (three attempts); after
871
- that the batch gets a non-mutating evaluator that records `HUMAN_REVIEW` for
872
- 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
873
970
  recovery snapshot right before its first mutating evaluator.
874
971
  `THREADKEEPER_CURATOR_WEB_RESEARCH=0` skips the research phase entirely (and
875
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.
876
975
  Each report path is then
877
976
  authorized in the existing parent-authored `curator_pass` event;
878
977
  `curator_report_write` records its SHA-256 in `curator_report_provenance`.
@@ -880,22 +979,22 @@ This makes the report directory an untrusted transport: a stray or forged
880
979
  `REPORT-*.md` file cannot acquire the provenance needed by the applier.
881
980
 
882
981
  The web-enabled researcher never receives lesson, skill, or concept mutation
883
- tools. The evaluator never receives web tools. Curator applies its own PATCH /
884
- PRUNE / CONSOLIDATE directly by default (the evaluator writes the REPORT first,
885
- then mutates — `lesson_remove` is in its toolset so it can actually prune and
886
- 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
887
986
  `THREADKEEPER_CURATOR_DESTRUCTIVE=0` for advisory REPORT-only. Pinned and
888
- untracked skills remain protected. Foreground-authored skills are protected by
889
- default; set `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS=1` to grant the
890
- Curator explicit snapshot-scoped authority to repair, merge, and delete those
891
- skills too. The opt-in never overrides pins and is accepted only inside a real
892
- 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
893
992
  stamped with an explicit `origin=<THREADKEEPER_WRITE_ORIGIN>` marker when
894
993
  appended; missing, legacy, or unknown lesson provenance is protected by
895
994
  default. `lesson_remove` and `skill_manage(action='delete')` refuse protected
896
995
  foreground/unknown-origin entries unless `force=True` is called from a
897
- foreground writer; curator/spawned children cannot elevate themselves with
898
- `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
899
998
  a recoverable snapshot under
900
999
  `<reports_dir>/snapshots/<pass-id>/` (default
901
1000
  `~/.threadkeeper/curator/snapshots/<pass-id>/`). The snapshot contains
@@ -911,7 +1010,8 @@ Use `curator_restore(pass_id, lesson_slug="...")` or
911
1010
  As a prevention layer before recovery is needed, a destructive Curator pass
912
1011
  has one server-side shared admission budget for `lesson_remove` and
913
1012
  `skill_manage(action='delete')`, including across bounded child batches.
914
- `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
915
1015
  disable those autonomous deletes. The pass ID makes the count durable and
916
1016
  cross-process, while foreground/human deletes are unaffected. `mp_dashboard`
917
1017
  shows admitted and refused operations with `status=HIT` when the Curator reaches
@@ -945,9 +1045,21 @@ The shared lesson file has its own write serialization: `lesson_append`,
945
1045
  `lessons.md.lock` around file creation/read/mutate/write, so foreground calls
946
1046
  and learning-loop children cannot last-writer-win over each other's sections.
947
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
+
948
1058
  Lesson access is tracked the same way skill access is: `lesson_list` increments
949
1059
  `lesson_usage.view_count` for displayed rows and `lesson_get` increments
950
- `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
951
1063
  ranked `STALE LESSONS (dry-run decay ranking)` section computed as
952
1064
  `access_frequency × exp(-days_since_access / tau)`, filtered to unprotected
953
1065
  lessons with no recent access and low pull-count. That decay list is advisory
@@ -1113,8 +1225,9 @@ If a killed child leaves an unresolved merge or plain tracked WIP in the default
1113
1225
  auto-managed checkout, the next code-producing pass archives the diff before
1114
1226
  recovering it. Merge recovery remains limited to `roadmap/…`/`evolve/…`
1115
1227
  branches whose exact PR is confirmed open or merged. For an open PR, the parent
1116
- archives the interrupted merge, aborts it, refreshes the disposable checkout,
1117
- 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
1118
1231
  leftover merge is discarded as stale. Plain abandoned WIP is recoverable on
1119
1232
  those applier branches when PR state is readable, and also on the configured
1120
1233
  base branch: the disposable base can contain orphaned edits when an older child
@@ -1207,6 +1320,11 @@ daily deterministic `docs/roadmap-audit-YYYY-MM-DD` branch so repeated audit
1207
1320
  passes update or skip the existing roadmap PR rather than opening a second one.
1208
1321
  Automatic apply passes respect the configured interval so multiple foreground
1209
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.
1210
1328
  Manual tools such as `evolve_apply_conflicted_pr()` and
1211
1329
  `evolve_apply_roadmap_issue()` dispatch immediately. If no conflicted applier PR
1212
1330
  or roadmap issue is startable, the pass falls back to Curator reports and then
@@ -1384,8 +1502,45 @@ Adding a new language is a two-file PR — see [CONTRIBUTING.md](CONTRIBUTING.md
1384
1502
 
1385
1503
  The most-used env knobs (full list in `threadkeeper/config.py`):
1386
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
+
1387
1538
  | Knob | Default | Purpose |
1388
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 |
1389
1544
  | `THREADKEEPER_DB` | `~/.threadkeeper/db.sqlite` | SQLite file |
1390
1545
  | `THREADKEEPER_TASK_LOG_DIR` | `~/.threadkeeper/tasks` | owner-only task spool for spawn logs, stdin prompts, command scripts, and small runtime logs |
1391
1546
  | `THREADKEEPER_RETENTION_INTERVAL_S` | 0 (off) | SQLite retention/compaction daemon tick; 0 disables the daemon |
@@ -1434,11 +1589,11 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1434
1589
  | `THREADKEEPER_CANDIDATE_REVIEW_BATCH_SIZE` | 40 | most pending candidates (oldest first) one reviewer child receives; the rest stay pending for the next pass |
1435
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 |
1436
1591
  | `THREADKEEPER_CURATOR_INTERVAL_S` | 259200 | deep curator audit every three days; set `0` to disable |
1437
- | `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 |
1438
1593
  | `THREADKEEPER_CURATOR_MIN_LESSONS` | 3 | min lessons before curator engages |
1439
- | `THREADKEEPER_CURATOR_WEB_RESEARCH` | `0` (off) | explicit consent for the isolated Curator web researcher; it receives only fixed public compatibility queries, while foreground/private/unclassified entries use `HUMAN_REVIEW` |
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 |
1440
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 |
1441
- | `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 |
1442
1597
  | `THREADKEEPER_CURATOR_SNAPSHOT_RETENTION` | 10 | number of destructive curator pre-mutation snapshots to retain under `<reports_dir>/snapshots`; current pass is always retained |
1443
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 |
1444
1599
  | `THREADKEEPER_PROBE_INTERVAL_S` | 0 (off) | probe daemon tick (s); 1800 = 30 min recommended so finished probe answers are graded promptly |
@@ -1480,6 +1635,10 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1480
1635
  | `THREADKEEPER_SKILL_NUDGE_INTERVAL` | 10 | events between `skill_hint` nudges |
1481
1636
  | `THREADKEEPER_DIALECTIC_MINE_INTERVAL_S` | 0 (off) | dialectic_miner daemon tick (s); 0 disables mechanical observation capture |
1482
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 |
1483
1642
  | `THREADKEEPER_DIALECTIC_VALIDATE_MIN` | 5 | min buffered observations before validator engages |
1484
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) |
1485
1644
  | `THREADKEEPER_DIALECTIC_VALIDATE_BATCH_SIZE` | 50 | max observations sent to one validator child; prevents oversized prompts and drains large queues incrementally |
@@ -1546,7 +1705,7 @@ THREADKEEPER_SPAWN__DEFAULT=claude
1546
1705
  # supported CLI keys: claude, codex, antigravity (agy executable), copilot
1547
1706
  THREADKEEPER_SPAWN__LOOP__SHADOW_OBSERVER=claude # heaviest reasoning → keep on Claude
1548
1707
  THREADKEEPER_SPAWN__LOOP__CURATOR=codex # weekly audit → Codex is fine
1549
- 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)
1550
1709
  # model pin per CLI or per role: THREADKEEPER_SPAWN__MODEL__<KEY>=<model>
1551
1710
  THREADKEEPER_SPAWN__MODEL__CLAUDE=opus
1552
1711
  THREADKEEPER_SPAWN__MODEL__CODEX=gpt-5.5
@@ -1559,7 +1718,8 @@ THREADKEEPER_SPAWN__EFFORT__CURATOR=xhigh
1559
1718
  ```
1560
1719
 
1561
1720
  Resolution per role: `SPAWN__LOOP__<role>` → `SPAWN__DEFAULT` → active CLI →
1562
- `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
1563
1723
  variables override the `.env`. Force host detection with
1564
1724
  `THREADKEEPER_ACTIVE_CLI=claude` (or `codex`, `antigravity`/`agy`,
1565
1725
  `copilot`). `agy` is normalized to `antigravity`. See `.env.example`
@@ -1567,6 +1727,28 @@ for the full knob list. `spawn_status()` includes warnings when a configured
1567
1727
  spawn CLI is unsupported or a model key does not match a supported CLI/startup
1568
1728
  role, while keeping the same fallback resolution.
1569
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
+
1570
1752
  Adapters without headless support (Claude Desktop, VS Code) can't be
1571
1753
  spawn targets — `spawn_status()` reports them as "no adapter" and any
1572
1754
  override pointing at them falls back to the next priority level.
@@ -1720,7 +1902,9 @@ that keeps a write transaction open for a minute or more.
1720
1902
 
1721
1903
  Schema migration uses SQLite `PRAGMA user_version`: a current database skips
1722
1904
  legacy `ALTER TABLE` work, while an old or fresh v0 database migrates once
1723
- 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
1724
1908
  migrations are the only expected no-op; other DDL errors are logged and raised.
1725
1909
 
1726
1910
  On POSIX systems, startup and `get_db()` harden the default local store
@@ -1728,9 +1912,10 @@ best-effort: `~/.threadkeeper` is `0700`, while `db.sqlite`, SQLite
1728
1912
  `-wal`/`-shm` sidecars, `~/.threadkeeper/.env`, curator `REPORT-*.md`
1729
1913
  files, and headless spawn logs are owner-only (`0600`).
1730
1914
 
1731
- Use `tk-backup` for disaster recovery. It uses SQLite `VACUUM INTO`, so
1732
- committed frames still living in the live `-wal` sidecar are included in a
1733
- 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:
1734
1919
 
1735
1920
  ```bash
1736
1921
  tk-backup create ~/threadkeeper-backup.sqlite
@@ -1753,6 +1938,73 @@ first and copy `db.sqlite`, `db.sqlite-wal`, and `db.sqlite-shm` together. To
1753
1938
  wipe memory, also stop thread-keeper first, then remove the main DB and both
1754
1939
  sidecars.
1755
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
+
1756
2008
  ### Selective Erasure
1757
2009
 
1758
2010
  For one regretted or sensitive conversation, use targeted erasure instead of