threadkeeper 0.17.6__tar.gz → 0.18.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 (268) hide show
  1. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/PKG-INFO +184 -78
  2. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/README.md +182 -76
  3. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/pyproject.toml +5 -6
  4. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_adapters.py +64 -0
  5. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_agent_status.py +96 -0
  6. threadkeeper-0.18.0/tests/test_antigravity_ingest.py +217 -0
  7. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_auto_update.py +27 -0
  8. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_candidate_reviewer.py +45 -0
  9. threadkeeper-0.18.0/tests/test_ci_parallel.py +48 -0
  10. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_config_settings.py +4 -1
  11. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_curator.py +709 -12
  12. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_daemon_liveness.py +4 -2
  13. threadkeeper-0.18.0/tests/test_db_leak_guard.py +136 -0
  14. threadkeeper-0.18.0/tests/test_documentation_inventory.py +60 -0
  15. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_evolve_applier.py +292 -28
  16. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_evolve_daemon.py +255 -23
  17. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_i18n_multilang.py +23 -1
  18. threadkeeper-0.18.0/tests/test_lesson_violations.py +91 -0
  19. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_mcp_resources_prompts.py +4 -1
  20. threadkeeper-0.18.0/tests/test_mcp_sdk_compat.py +144 -0
  21. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_menubar_app.py +1 -1
  22. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_notify_daemon.py +68 -2
  23. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_panel.py +20 -0
  24. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_pickup.py +18 -0
  25. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_probe_daemon.py +16 -0
  26. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_release_workflows.py +20 -14
  27. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_single_flight_prompt_prefixes.py +10 -0
  28. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skill_watcher.py +41 -0
  29. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skills.py +25 -0
  30. threadkeeper-0.18.0/tests/test_spawn_background_cwd.py +138 -0
  31. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_budget.py +89 -0
  32. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_codex_stdin.py +49 -4
  33. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_permission_gate.py +61 -7
  34. threadkeeper-0.18.0/tests/test_spawn_result.py +17 -0
  35. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_watchdog.py +77 -0
  36. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_thread_janitor.py +20 -0
  37. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_tool_annotations.py +44 -18
  38. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_verify_ingest.py +13 -0
  39. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/_mcp.py +20 -4
  40. threadkeeper-0.18.0/threadkeeper/adapters/antigravity.py +387 -0
  41. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/base.py +28 -0
  42. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/codex.py +50 -1
  43. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/agent_metadata.py +13 -1
  44. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/agent_status.py +29 -9
  45. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/auto_update.py +22 -2
  46. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/candidate_reviewer.py +38 -11
  47. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/config.py +43 -0
  48. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/curator.py +1379 -276
  49. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/db.py +221 -21
  50. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/elicitation.py +2 -1
  51. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/evolve_applier.py +181 -31
  52. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/evolve_daemon.py +370 -57
  53. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/github_safety.py +10 -4
  54. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/host.py +31 -0
  55. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/i18n.py +51 -18
  56. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/ingest.py +25 -7
  57. threadkeeper-0.18.0/threadkeeper/lesson_violations.py +99 -0
  58. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/notify.py +35 -5
  59. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/probe_daemon.py +8 -2
  60. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/server.py +2 -1
  61. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/shadow_review.py +12 -5
  62. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/skill_watcher.py +65 -2
  63. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/spawn_budget.py +49 -25
  64. threadkeeper-0.18.0/threadkeeper/spawn_result.py +47 -0
  65. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/thread_janitor.py +28 -9
  66. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tool_schemas.py +1 -1
  67. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/curator.py +125 -5
  68. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/dashboard.py +31 -0
  69. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/dialectic.py +1 -3
  70. threadkeeper-0.18.0/threadkeeper/tools/evolve_research.py +22 -0
  71. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/lessons.py +40 -0
  72. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/panel.py +6 -3
  73. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/pickup.py +26 -8
  74. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/process_health.py +5 -0
  75. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/resources.py +1 -1
  76. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/skills.py +6 -1
  77. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/spawn.py +355 -164
  78. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/verify_ingest.py +11 -7
  79. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper.egg-info/PKG-INFO +184 -78
  80. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper.egg-info/SOURCES.txt +11 -0
  81. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper.egg-info/requires.txt +1 -1
  82. threadkeeper-0.17.6/threadkeeper/adapters/antigravity.py +0 -193
  83. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/LICENSE +0 -0
  84. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/setup.cfg +0 -0
  85. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_backup.py +0 -0
  86. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_brief_footprint.py +0 -0
  87. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_brief_sections.py +0 -0
  88. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_concepts.py +0 -0
  89. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_config_watcher.py +0 -0
  90. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_core_memory.py +0 -0
  91. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_daemon_host_config.py +0 -0
  92. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_daemon_host_integration.py +0 -0
  93. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_daemon_state.py +0 -0
  94. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dashboard.py +0 -0
  95. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_db_compact.py +0 -0
  96. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_db_contention.py +0 -0
  97. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_db_migration_fts.py +0 -0
  98. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_db_runtime.py +0 -0
  99. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_db_schema.py +0 -0
  100. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_delegated_search.py +0 -0
  101. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic.py +0 -0
  102. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic_feed_tools.py +0 -0
  103. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic_miner.py +0 -0
  104. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic_observation_resolve.py +0 -0
  105. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic_recompute.py +0 -0
  106. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic_tier.py +0 -0
  107. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialectic_validator.py +0 -0
  108. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_dialog_fts_external.py +0 -0
  109. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_egress_policy.py +0 -0
  110. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_embed_routing.py +0 -0
  111. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_embedding_generation.py +0 -0
  112. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_embedding_model_revision.py +0 -0
  113. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_embedding_storage_dedup.py +0 -0
  114. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_error_paths.py +0 -0
  115. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_eval_harness.py +0 -0
  116. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_evolve_apply_2.py +0 -0
  117. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_evolve_apply_3.py +0 -0
  118. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_extract_daemon.py +0 -0
  119. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_extract_dedup.py +0 -0
  120. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_forget.py +0 -0
  121. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_github_budget.py +0 -0
  122. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_github_safety.py +0 -0
  123. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_helpers.py +0 -0
  124. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_host_embed.py +0 -0
  125. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_host_env.py +0 -0
  126. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_host_main.py +0 -0
  127. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_host_wedged.py +0 -0
  128. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_hybrid_retrieval.py +0 -0
  129. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_identity.py +0 -0
  130. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_ingest_cursor.py +0 -0
  131. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_ingest_denylist.py +0 -0
  132. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_ingest_secret_redaction.py +0 -0
  133. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_ingest_status.py +0 -0
  134. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_ingest_transactions.py +0 -0
  135. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_lessons.py +0 -0
  136. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_memory_eval.py +0 -0
  137. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_memory_guard.py +0 -0
  138. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_memory_guard_host.py +0 -0
  139. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_missed_spawns.py +0 -0
  140. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_model_catalog.py +0 -0
  141. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_nudges.py +0 -0
  142. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_onnx_embeddings.py +0 -0
  143. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_permissions.py +0 -0
  144. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_process_health.py +0 -0
  145. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_retention.py +0 -0
  146. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_review_prompts.py +0 -0
  147. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_search_fts_punctuation.py +0 -0
  148. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_session_read_purity.py +0 -0
  149. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_setup.py +0 -0
  150. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_shadow_review.py +0 -0
  151. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skill_hint.py +0 -0
  152. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skill_passive_tier.py +0 -0
  153. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skill_tier.py +0 -0
  154. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skill_updater.py +0 -0
  155. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_skill_use_parser.py +0 -0
  156. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_config.py +0 -0
  157. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_hint.py +0 -0
  158. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_reap.py +0 -0
  159. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_slim.py +0 -0
  160. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_worktree_isolation.py +0 -0
  161. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_spawn_wrap.py +0 -0
  162. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_sync_capture.py +0 -0
  163. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_sync_identity.py +0 -0
  164. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_sync_migrate.py +0 -0
  165. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_sync_protocol.py +0 -0
  166. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_sync_reset_node.py +0 -0
  167. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_sync_transport.py +0 -0
  168. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_task_retention.py +0 -0
  169. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_task_spool.py +0 -0
  170. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_thin_session.py +0 -0
  171. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_threads.py +0 -0
  172. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_tools_smoke.py +0 -0
  173. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_validate_threads.py +0 -0
  174. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/tests/test_vec_search.py +0 -0
  175. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/__init__.py +0 -0
  176. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/_setup.py +0 -0
  177. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/_spawn_wrap.py +0 -0
  178. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/__init__.py +0 -0
  179. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  180. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/claude_code.py +0 -0
  181. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  182. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/copilot.py +0 -0
  183. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/adapters/vscode.py +0 -0
  184. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  185. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  186. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  187. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  188. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  189. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/backup.py +0 -0
  190. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/brief.py +0 -0
  191. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/config_io.py +0 -0
  192. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/config_watcher.py +0 -0
  193. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/curator_snapshots.py +0 -0
  194. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/daemon_liveness.py +0 -0
  195. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/daemon_state.py +0 -0
  196. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/daemon_supervisor.py +0 -0
  197. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/dialectic_miner.py +0 -0
  198. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/dialectic_validator.py +0 -0
  199. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/egress.py +0 -0
  200. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/embeddings.py +0 -0
  201. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/eval/__init__.py +0 -0
  202. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/eval/__main__.py +0 -0
  203. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/eval/harness.py +0 -0
  204. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/extract_daemon.py +0 -0
  205. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/forget.py +0 -0
  206. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/github_budget.py +0 -0
  207. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/harvest.py +0 -0
  208. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/helpers.py +0 -0
  209. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/host_embed.py +0 -0
  210. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/identity.py +0 -0
  211. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/lessons.py +0 -0
  212. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/link_health.py +0 -0
  213. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/memory_guard.py +0 -0
  214. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/menubar_app.py +0 -0
  215. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/migrate_embeddings.py +0 -0
  216. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/model_catalog.py +0 -0
  217. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/nudges.py +0 -0
  218. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/permissions.py +0 -0
  219. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/process_health.py +0 -0
  220. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/retention.py +0 -0
  221. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/retrieval.py +0 -0
  222. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/review_prompts.py +0 -0
  223. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/search_proxy.py +0 -0
  224. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/skill_audit.py +0 -0
  225. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/skill_updater.py +0 -0
  226. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/spawn_config.py +0 -0
  227. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/__init__.py +0 -0
  228. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/capture.py +0 -0
  229. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/daemon.py +0 -0
  230. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/identity.py +0 -0
  231. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/migrate.py +0 -0
  232. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/protocol.py +0 -0
  233. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/reset_node.py +0 -0
  234. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/sync/server.py +0 -0
  235. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/task_spool.py +0 -0
  236. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/__init__.py +0 -0
  237. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/agent_status.py +0 -0
  238. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  239. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/concepts.py +0 -0
  240. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/config_watch.py +0 -0
  241. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/consolidate.py +0 -0
  242. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/core_memory.py +0 -0
  243. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/correlation.py +0 -0
  244. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/db_maintenance.py +0 -0
  245. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  246. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/dialog.py +0 -0
  247. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/distill.py +0 -0
  248. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/evolve_applier.py +0 -0
  249. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/extract.py +0 -0
  250. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/forget.py +0 -0
  251. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/graph.py +0 -0
  252. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/invariants.py +0 -0
  253. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/memory_guard.py +0 -0
  254. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/missed_spawns.py +0 -0
  255. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/peers.py +0 -0
  256. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/probes.py +0 -0
  257. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/prompts.py +0 -0
  258. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/session.py +0 -0
  259. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/shadow_review.py +0 -0
  260. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/style.py +0 -0
  261. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/sync.py +0 -0
  262. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/threads.py +0 -0
  263. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/tools/validate.py +0 -0
  264. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/trash.py +0 -0
  265. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper/wikilinks.py +0 -0
  266. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  267. {threadkeeper-0.17.6 → threadkeeper-0.18.0}/threadkeeper.egg-info/entry_points.txt +0 -0
  268. {threadkeeper-0.17.6 → threadkeeper-0.18.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.17.6
3
+ Version: 0.18.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<2,>=1.10.0
25
+ Requires-Dist: mcp<3,>=1.10.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
@@ -108,6 +108,8 @@ Integrity API provenance from the expected GitHub Trusted Publisher. Dirty or
108
108
  diverged git checkouts are skipped rather than overwritten. Restarts are gated
109
109
  on install/setup success plus a subprocess import smoke check, so a broken or
110
110
  unverified update is recorded but the current server keeps running.
111
+ Interpreters without pip (uv-created or pipx venvs) install through
112
+ `uv pip install --python <interpreter>` when `uv` is available.
111
113
  Upstream PyPI publishing is intentionally gated: green merge-to-main builds are
112
114
  auto-tagged, but every upload pauses for a human approval on the protected
113
115
  `pypi` GitHub Environment (a maintainer-signed annotated `v*` tag remains the
@@ -193,17 +195,19 @@ thread-keeper-setup --dry-run
193
195
  | Claude Code | `~/.claude.json` `mcpServers` | `~/.claude/CLAUDE.md` | `~/.claude/settings.json` `hooks` | `~/.claude/projects/**/*.jsonl` |
194
196
  | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` `mcpServers` (macOS); `%APPDATA%\Claude\…` (Win); `~/.config/Claude/…` (Linux) | none (GUI-only) | not supported by the app | none — chats live in Electron IndexedDB |
195
197
  | Codex (CLI + desktop) | `~/.codex/config.toml` `[mcp_servers]` (shared between CLI and `Codex.app`) | `~/.codex/AGENTS.md` | not supported | `~/.codex/sessions/**/rollout-*.jsonl` |
196
- | Antigravity CLI (`agy`) | `~/.gemini/config/mcp_config.json` `mcpServers` | `~/.gemini/config/AGENTS.md` | not wired yet | not yet parsed — sqlite/protobuf under `~/.gemini/antigravity-cli/conversations/*.db` |
198
+ | Antigravity CLI (`agy`) | `~/.gemini/config/mcp_config.json` `mcpServers` | `~/.gemini/config/AGENTS.md` | not wired yet | `~/.gemini/antigravity-cli/conversations/*.db` (sqlite/protobuf, read-only: user prompts and final answers) |
197
199
  | Copilot | `~/.copilot/mcp-config.json` `mcpServers` | `~/.copilot/copilot-instructions.md` | `~/.copilot/hooks.json` | `~/.copilot/session-store.db` (sqlite) |
198
200
  | VS Code | `~/Library/Application Support/Code/User/mcp.json` `servers` (macOS); `%APPDATA%\Code\User\mcp.json` (Win); `~/.config/Code/User/mcp.json` (Linux) | none (per-workspace only) | not supported | none — extensions own their history |
199
201
 
200
202
  Every CLI that produces parseable transcripts feeds the same
201
203
  `dialog_messages` table with a `source` tag, so `dialog_search()` finds
202
- matches regardless of where the conversation happened. Claude Desktop,
203
- Antigravity CLI, and the VS Code adapter are the exceptions — MCP registration
204
- only; their chats don't reach the table for now (Electron IndexedDB on the
205
- Claude Desktop side; sqlite/protobuf on the Antigravity side; per-extension
206
- stores on the VS Code side).
204
+ matches regardless of where the conversation happened. Claude Desktop and
205
+ the VS Code adapter are the exceptions — MCP registration only; their chats
206
+ don't reach the table for now (Electron IndexedDB on the Claude Desktop side;
207
+ per-extension stores on the VS Code side). Antigravity keeps one SQLite file
208
+ per conversation with protobuf step payloads; thread-keeper opens it read-only
209
+ (never creating WAL sidecars next to it) and keeps only user prompts and final
210
+ model answers, skipping tool calls and thinking.
207
211
 
208
212
  VS Code's user-level `mcp.json` is the central host that **every
209
213
  MCP-aware VS Code extension** consumes — GitHub Copilot Chat, the
@@ -213,6 +217,14 @@ Cline, … — so a single registration there reaches all of them at once.
213
217
  Adding a new CLI = one file under `threadkeeper/adapters/` implementing
214
218
  the `CLIAdapter` contract. See [CONTRIBUTING.md](CONTRIBUTING.md).
215
219
 
220
+ ### Python MCP SDK compatibility
221
+
222
+ thread-keeper supports MCP Python SDK 1.x and 2.x (`mcp>=1.10.0,<3`). Fresh
223
+ installs resolve 2.x. A small internal adapter uses the SDK 2.x `MCPServer`
224
+ name and falls back to its SDK 1.x `FastMCP` predecessor, so tools, resources,
225
+ prompts, annotations, structured content, elicitation, and stdio behavior keep
226
+ the same public contract across both supported SDK majors.
227
+
216
228
  ### MCP primitives (tools, resources, prompts, elicitation)
217
229
 
218
230
  MCP has three server primitives. thread-keeper uses all three, mapped to the
@@ -309,9 +321,10 @@ task row so dead children stop counting against the cap. Admission control
309
321
  refuses a new spawn that would exceed `THREADKEEPER_SPAWN_BUDGET_MB`
310
322
  (3 GB default). Slim children that need semantic search delegate to the parent
311
323
  via `search_via_parent` — no per-child copy of the embedding model. Admission
312
- uses a SQLite `BEGIN IMMEDIATE` reservation: `spawn()` re-checks the budget and
313
- inserts the child task row with its RSS estimate before `Popen`, so two
314
- concurrent spawns cannot both squeeze through the cap.
324
+ uses a short SQLite `BEGIN IMMEDIATE` reservation: `spawn()` re-checks the
325
+ budget and commits the child task row with its RSS estimate before `Popen`, so
326
+ two concurrent spawns cannot both squeeze through the cap, and no write lock is
327
+ held while the child launches.
315
328
 
316
329
  When `cwd` is inside a Git checkout, `spawn()` also requires the source
317
330
  checkout's tracked files to be clean, then starts the child from a unique
@@ -319,6 +332,15 @@ branch/worktree under `THREADKEEPER_TASK_LOG_DIR/worktrees/`. Parallel children
319
332
  therefore never share a mutable checkout or Git index. Non-Git directories keep
320
333
  their existing behavior; a dirty Git checkout is refused before a child starts.
321
334
 
335
+ Learning-loop children that name no `cwd` (Curator, shadow review, candidate
336
+ and dialectic review, probes, panels, the archivist) start in the owner-only
337
+ `<db dir>/workspace` directory instead of the spawning process's working
338
+ directory. The daemon host keeps the directory of whichever session started it,
339
+ so these children used to run inside an unrelated user project, and a dirty Git
340
+ checkout there refused every loop spawn. Foreground spawns keep the caller's
341
+ directory, and callers that pass `cwd` (the Evolve reviewer and applier) are
342
+ unchanged.
343
+
322
344
  The spawn wrapper also records each completed child's `duration_s`,
323
345
  `tokens_in`, `tokens_out`, `tokens_total`, and `cost_usd` when the underlying
324
346
  CLI emits a recognizable usage trailer. Optional daily ceilings
@@ -354,7 +376,10 @@ repair partial work, and continue rather than restart blindly.
354
376
  `THREADKEEPER_SPAWN_TIMEOUT_RETRY_LIMIT` (default 3; 0 disables) bounds the
355
377
  retry chain, with `THREADKEEPER_SPAWN_TIMEOUT_RETRY_DELAY_S` available for a
356
378
  non-zero delay. Timed-out children are surfaced as `tasks_timed_out` in
357
- `mp_dashboard` and `timed_out` in `agent_status`.
379
+ `mp_dashboard` and `timed_out` in `agent_status`. `agent_status` and
380
+ `tk-agent-status` are observation-only and never terminate or respawn a
381
+ child; timeout enforcement and the continuation retry live solely in the
382
+ spawn-budget daemon.
358
383
 
359
384
  `tk-agent-status` exposes autonomous learning loop status as structured JSON
360
385
  or compact text for external monitors:
@@ -532,7 +557,7 @@ shows agents focused on their primary task rarely do).
532
557
  | 2 | shadow_review daemon | every 15 min (env knob) | recent `dialog_messages` window | SKILL.md, lessons.md |
533
558
  | 3 | extract daemon | every 10 min (env knob) | recent `dialog_messages` window | `extract_candidates` pending queue |
534
559
  | 4 | candidate-reviewer daemon | every 1 h (env knob) | pending candidates queue | SKILL.md (create/patch) / notes / verbatim / reject |
535
- | 5 | Curator daemon | every 7 days (env knob) | every existing lesson + recently-touched skill | `REPORT-<date>.md`; Evolve applier applies it after roadmap issues |
560
+ | 5 | Curator daemon | every 7 days (env knob) | every existing lesson + recently-touched skill | pass-scoped research handoffs and `REPORT-<date>.md`; Evolve applier applies reports after roadmap issues |
536
561
  | 6 | evolve_reviewer daemon | configurable (env knob; 0=off) | code/docs/issues; web research in a separate read-only phase (#79) | roadmap updates + GitHub issues |
537
562
  | 7 | evolve_applier daemon | configurable (env knob; 0=off) | open GitHub issues, Curator reports, legacy promoted evolve suggestions | PRs + applied markers |
538
563
  | 8 | dialectic_miner daemon | configurable (env knob; 0=off) | recent `dialog_messages` — user replies + preceding-assistant context | `dialectic_observations` buffer |
@@ -623,6 +648,15 @@ slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
623
648
  that preflight to patch/consolidate an incumbent or add a `[[slug]]` cross-link
624
649
  to a related, distinct lesson while its body is still editable.
625
650
 
651
+ When the dialog shows a rule being broken again although a lesson already
652
+ covers it, the reviewers call `lesson_violation(slug, evidence)` instead of
653
+ writing a duplicate. One conversation counts once per lesson per day; at
654
+ `THREADKEEPER_LESSON_VIOLATION_THRESHOLD` (3) violations inside
655
+ `THREADKEEPER_LESSON_VIOLATION_WINDOW_DAYS` (90) the lesson is
656
+ memory-insufficient: `mp_dashboard` lists it and the Curator inventory marks it
657
+ `[MEMORY-INSUFFICIENT]` and recommends escalating the rule to a PreToolUse-style
658
+ hook or other hard guard.
659
+
626
660
  #### 3. Extract daemon
627
661
 
628
662
  Every `THREADKEEPER_EXTRACT_INTERVAL_S` seconds (default off, 600 =
@@ -706,10 +740,12 @@ flags a dense lesson subtopic when at least
706
740
  `THREADKEEPER_CURATOR_PROMOTION_MIN_LESSONS` lessons (default 3) share a pair
707
741
  of meaningful title terms. A non-protected candidate must become one validated,
708
742
  checklist-style canonical skill before its source lessons are retired; protected
709
- clusters are left for human review. The child reads every
743
+ clusters are left for human review. A read-only research child reads every
710
744
  complete skill and relevant support file, performs current web research against
711
- official docs and comparable
712
- public skills, then writes numbered per-skill verdicts to
745
+ official docs and comparable public skills, then writes a bounded
746
+ `RESEARCH-<pass>-batch-NNN-of-MMM.json` handoff through a destination-scoped
747
+ tool. A separate web-free evaluator receives that handoff as fenced, untrusted
748
+ data and writes numbered per-skill verdicts to
713
749
  `~/.threadkeeper/curator/REPORT-<isodate>.md` for a one-batch pass or
714
750
  `REPORT-<isodate>-batch-NNN-of-MMM.md` for a multi-batch pass: KEEP / REPAIR /
715
751
  UPDATE / MERGE / SPLIT / DEPRECATE / DELETE / CROSS_LINK / HUMAN_REVIEW.
@@ -741,25 +777,59 @@ have increased its patch counter. Patches are maintenance activity, not proof
741
777
  that a foreground user or agent consulted the skill.
742
778
 
743
779
  Before spawning, the scheduler hashes lessons, concepts, skill bodies, support
744
- trees, validators, and mirror state. Repeated manual calls over identical bytes
745
- return `unchanged_inventory`; the scheduled three-day pass still runs because
746
- CLI behavior, official guidance, and external alternatives can change without
747
- local file changes. `curator_review_status()` shows the inventory hash plus the
748
- latest report, deterministic audit manifest, recovery snapshot, last endorsed
749
- `inventory_sha256`, and the current inventory hash. Spawned pass events record
750
- `entries`, `batches`, `batch_entries`, and `max_batch_chars`, making partial or
751
- large reviews visible in the normal `curator_pass` trail.
752
-
753
- Each report path is explicitly authorized in a parent-authored `curator_pass`
754
- event before its child is launched. `curator_report_write` only accepts that
755
- exact path from the spawned Curator carrying the matching pass ID, then records
756
- the persisted report's SHA-256 in `curator_report_provenance`. This makes the
757
- report directory an untrusted transport: a stray or forged `REPORT-*.md` file
758
- cannot acquire the provenance needed by the applier.
759
-
760
- Curator applies its own PATCH / PRUNE / CONSOLIDATE directly by default (it
761
- writes the REPORT first, then mutates — `lesson_remove` is in its toolset so it
762
- can actually prune and consolidate duplicate lessons). Set
780
+ trees, validators, and mirror state. It then persists a pass manifest with the
781
+ expected reports and the rendered text of every batch before launching any
782
+ child. A dispatch is not completion: every batch must exit successfully and
783
+ have a final, matching provenance record before the inventory is endorsed.
784
+ Repeated manual calls over identical bytes return `unchanged_inventory` only
785
+ after that endorsement; scheduled passes still run because CLI behavior,
786
+ official guidance, and external alternatives can change without local file
787
+ changes. Every required inventory source (lessons, skill telemetry, skill
788
+ files, and concepts) must read successfully before a pass is created; a
789
+ successfully empty source remains valid, while a failed one records
790
+ `inventory_error source=<source> error=<type>` without authorizing a report,
791
+ creating a recovery snapshot, or launching a child.
792
+
793
+ A pass keeps reviewing the batches it froze even when the live inventory
794
+ changes, and resuming it never re-reads the inventory. Failed and timed-out
795
+ batches are retried without redispatching completed work, up to three launched
796
+ attempts per batch; a spawn memory or spend-budget refusal leaves the batch
797
+ waiting without using an attempt (a spend-cap refusal also alerts). A pass
798
+ whose batch exhausts its attempts, or that outlives two Curator intervals
799
+ (at least six hours), is abandoned so the next due tick starts fresh.
800
+ `THREADKEEPER_CURATOR_MAX_CONCURRENT_BATCHES` (default `1`) bounds one pass's
801
+ live children while normal spawn admission still enforces the shared RSS
802
+ budget. While a pass is active, the daemon polls it every
803
+ `THREADKEEPER_CURATOR_BATCH_POLL_S` seconds (default `60`) instead of waiting
804
+ for the next full Curator interval. `curator_review_status()` and the Curator
805
+ row in `agent_status` expose expected, running, failed, complete, and
806
+ unapplied batch counts alongside the inventory hash, reports, audit manifest,
807
+ and recovery snapshot.
808
+
809
+ Each research handoff path is explicitly authorized for one pass, inventory
810
+ fingerprint, manifest digest, and batch before its web-enabled child is
811
+ launched. `curator_research_write` accepts only that destination from a spawned
812
+ `curator_researcher` child carrying the matching pass ID, and records the final
813
+ SHA-256 as provenance. Research and evaluation are two phases of each batch
814
+ inside the durable pass: the web-free evaluator for a batch starts only after
815
+ that batch's handoff validates. A researcher that fails or leaves a missing,
816
+ malformed, swapped, or mismatched handoff is retried (three attempts); after
817
+ that the batch gets a non-mutating evaluator that records `HUMAN_REVIEW` for
818
+ decisions that depend on current external facts. A destructive pass takes its
819
+ recovery snapshot right before its first mutating evaluator.
820
+ `THREADKEEPER_CURATOR_WEB_RESEARCH=0` skips the research phase entirely (and
821
+ its token cost); the evaluator then judges currency from local evidence only.
822
+ Each report path is then
823
+ authorized in the existing parent-authored `curator_pass` event;
824
+ `curator_report_write` records its SHA-256 in `curator_report_provenance`.
825
+ This makes the report directory an untrusted transport: a stray or forged
826
+ `REPORT-*.md` file cannot acquire the provenance needed by the applier.
827
+
828
+ The web-enabled researcher never receives lesson, skill, or concept mutation
829
+ tools. The evaluator never receives web tools. Curator applies its own PATCH /
830
+ PRUNE / CONSOLIDATE directly by default (the evaluator writes the REPORT first,
831
+ then mutates — `lesson_remove` is in its toolset so it can actually prune and
832
+ consolidate duplicate lessons). Set
763
833
  `THREADKEEPER_CURATOR_DESTRUCTIVE=0` for advisory REPORT-only. Pinned and
764
834
  untracked skills remain protected. Foreground-authored skills are protected by
765
835
  default; set `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS=1` to grant the
@@ -800,11 +870,13 @@ or `skill_manage(action='restore', name=...)`. Trash retention is bounded by
800
870
  `THREADKEEPER_CURATOR_TRASH_TTL_DAYS` (30 days by default) and swept on new
801
871
  trash writes. Advisory mode does not write snapshots. The existing Evolve
802
872
  applier is
803
- also the Curator apply worker: after the roadmap issue queue is empty, it looks
804
- for the latest complete Curator report (`CURATOR_PASS_COMPLETE`) whose path and
805
- current SHA-256 match an unapplied `curator_report_provenance` event, then
806
- spawns an `evolve_applier` child to apply only safe, still-current memory
807
- maintenance through `lesson_append` / `lesson_patch` / `lesson_remove` / `skill_manage` /
873
+ also the Curator apply worker: after the roadmap issue queue is empty, it
874
+ enumerates every complete, unapplied report in the oldest endorsed Curator pass
875
+ (`CURATOR_PASS_COMPLETE`) whose path and current SHA-256 match an unapplied
876
+ `curator_report_provenance` event, then spawns an `evolve_applier` child to
877
+ apply only safe, still-current memory
878
+ maintenance through `lesson_append` / `lesson_patch` / `lesson_remove` /
879
+ `skill_manage` /
808
880
  `concept_manage`. It never touches `[PROTECTED]`,
809
881
  foreground/user, pinned, or validated entries. Only after the child finishes
810
882
  does it call `evolve_mark_curator_report_applied(...)` with the verified hash;
@@ -868,10 +940,16 @@ runs as **two alternating phases**, never co-granting web research and
868
940
  shell/`bypassPermissions` to the same child:
869
941
 
870
942
  - **research phase** — a read-only child with `WebSearch`/`WebFetch` and
871
- read-only repo reads but **no shell, no `bypassPermissions`, and no GitHub
872
- access**. It distills external findings into a digest file under
873
- `~/.threadkeeper/evolve-research/`. With no `Bash`/`gh`/network-write tool it
874
- has no exfiltration channel, so the untrusted pages it reads cannot act.
943
+ read-only repo reads but **no shell, no generic `Write`, no
944
+ `bypassPermissions`, and no GitHub access**. Before dispatch, the parent
945
+ registers one pass ID, owner child, and digest target. The child can submit
946
+ only that pass through `evolve_research_handoff(...)`; it cannot choose a
947
+ path. The handoff is one-shot, capped at 12,000 characters / 400 lines,
948
+ atomically persisted with a SHA-256, and records rejected, failed, expired,
949
+ and tampered outcomes in Evolve telemetry. The later audit reads only a
950
+ fresh accepted handoff whose final file still matches that hash. With no
951
+ `Bash`/`gh`/network-write tool it has no exfiltration channel, so the
952
+ untrusted pages it reads cannot act.
875
953
  - **audit phase** — the privileged child (`bypassPermissions` + `Bash`/`Edit`/
876
954
  `Write`) that audits the repo, opens the `docs/ROADMAP.md` PR, and creates or
877
955
  updates GitHub issues. It holds **no web tools**; it consumes the research
@@ -880,8 +958,18 @@ shell/`bypassPermissions` to the same child:
880
958
 
881
959
  A full research → audit cycle therefore spans two due passes.
882
960
 
883
- Before a privileged audit can create more issues, the parent counts open,
884
- not-yet-applied roadmap work with a paginated GitHub REST read. At
961
+ Privileged reviewer and applier launches use private server-owned launchers.
962
+ The public `spawn()` tool refuses `permission_mode="bypassPermissions"` even if
963
+ a caller supplies Evolve-looking role or provenance metadata; only the explicit
964
+ `THREADKEEPER_ALLOW_BYPASS_PERMISSIONS_SPAWN=1` operator override opens that
965
+ mode for public calls.
966
+
967
+ Before a privileged audit can create more issues, the parent counts only open,
968
+ not-yet-applied issues carrying the deliberate `roadmap` label with a paginated
969
+ GitHub REST read. Issues without that label do not consume the reviewer cap.
970
+ Roadmap issues with an applier skip label or an untrusted author still count:
971
+ those gates stop autonomous pickup, not the reviewer's view of intended roadmap
972
+ work. At
885
973
  `THREADKEEPER_EVOLVE_REVIEW_BACKLOG_MAX` (default 25), it withholds that audit
886
974
  and records `backlog_saturated open=<n> cap=<max>` on the
887
975
  `evolve_review_pass` event; set the knob to `0` to opt out. The read-only
@@ -1005,19 +1093,24 @@ which bypasses the gate as explicit promotion. This removes the untrusted input
1005
1093
  at the boundary and complements the in-prompt data-fencing of #22/#76. The
1006
1094
  public claim comment also carries only an opaque per-host token (a 6-char hash
1007
1095
  of the hostname), never the raw hostname/PID/git-rev; the full host identity is
1008
- recorded in the local event log for multi-host triage.
1009
-
1010
- **Privilege + public-body guard (#22).** Stored evolve suggestions and external
1096
+ recorded in the local event log for multi-host triage. A visible claim marker is
1097
+ also not a bearer token: the claim lock accepts it only when the comment's
1098
+ GitHub `author_association` is in that trusted association set, or when its
1099
+ author login is listed in `THREADKEEPER_EVOLVE_CLAIM_AUTOMATION_ACTORS`. Missing
1100
+ author metadata and untrusted marker comments remain visible on GitHub but do
1101
+ not block roadmap work or affect claim-race resolution.
1102
+
1103
+ **Privilege + public-body guard.** Stored evolve suggestions and external
1011
1104
  GitHub issue bodies are wrapped in explicit data fences before a privileged
1012
- child sees them. The exposed `spawn()` tool refuses
1013
- `permission_mode="bypassPermissions"` unless the request comes from the evolve
1014
- daemon role/write-origin pairs (`evolve_reviewer`/`evolve`,
1015
- `evolve_applier`/`evolve_apply`) or the operator explicitly opts in with
1016
- `THREADKEEPER_ALLOW_BYPASS_PERMISSIONS_SPAWN=1`. Privileged evolve children also
1017
- get a PATH-prepended `gh` wrapper that scrubs `gh issue create`, `gh issue
1018
- comment`, and `gh pr create` bodies before the real GitHub CLI sees them:
1019
- home-directory paths and common token shapes are redacted, and a body is
1020
- refused if a known unsafe pattern remains.
1105
+ child sees them. The public `spawn()` tool refuses
1106
+ `permission_mode="bypassPermissions"` regardless of caller-supplied role or
1107
+ provenance metadata; privileged Evolve launches use private server-owned
1108
+ capabilities, and public calls need the explicit
1109
+ `THREADKEEPER_ALLOW_BYPASS_PERMISSIONS_SPAWN=1` operator override. Privileged
1110
+ Evolve children also get a PATH-prepended `gh` wrapper that scrubs `gh issue
1111
+ create`, `gh issue comment`, and `gh pr create` bodies before the real GitHub
1112
+ CLI sees them: home-directory paths and common token shapes are redacted, and a
1113
+ body is refused if a known unsafe pattern remains.
1021
1114
 
1022
1115
  Fallback/manual paths remain:
1023
1116
 
@@ -1081,7 +1174,9 @@ Three detection sources per tick:
1081
1174
 
1082
1175
  1. **Admission failures / terminal timeouts** — a `<loop>_pass` event whose
1083
1176
  summary is a spawn/budget failure (e.g. `token_budget_exceeded`,
1084
- `claude_cli_not_found`), plus `spawn_timeout_retry_failed`.
1177
+ `claude_cli_not_found`), plus `spawn_timeout_retry_failed`. A budget
1178
+ refusal names the local cap that refused the child — the spawn memory
1179
+ budget or the daily token/cost budget — not the CLI subscription.
1085
1180
  2. **Dead children** — a `tasks` row that ended with a non-zero, non-timeout
1086
1181
  return code. This is the important one: `spawn()` returns `ok task=…` at
1087
1182
  *launch*, so a `*_pass` summary is a false success when a child later dies
@@ -1131,7 +1226,11 @@ category off silences the banner without hiding the history. Each materializatio
1131
1226
  uses its own skill or lesson toggle; generic task reports are history-only.
1132
1227
  Foreground writes appear too, without waiting for a background child to finish.
1133
1228
  A pathless skill mark only silences its reminder and has no materialization
1134
- banner. Enabling a toggle never replays backlog.
1229
+ banner. Enabling a toggle never replays backlog. Child-log excerpts and spawn
1230
+ error reasons in both lists are bounded sanitized summaries: credential-shaped
1231
+ values and private home paths are redacted before they reach the MCP status
1232
+ response, menu, or notification; raw logs remain available only through
1233
+ `task_logs`.
1135
1234
 
1136
1235
  **Settings in the app.** The menu-bar app's **Settings ▸ Notifications** tab
1137
1236
  edits these same `THREADKEEPER_NOTIFY_*` keys visually — switches for the
@@ -1188,9 +1287,9 @@ claim earned trust. Thresholds:
1188
1287
 
1189
1288
  All multilingual regex and prompt fragments live in
1190
1289
  `threadkeeper/i18n.py` — the rest of the codebase stays English-only.
1191
- Currently ships ten locales: **English, Mandarin Chinese, Hindi,
1192
- Spanish, Portuguese, French, German, Arabic, Russian, Japanese**
1193
- (~82 % of the world's speakers).
1290
+ Currently ships twelve locales: **English, Mandarin Chinese, Hindi,
1291
+ Spanish, Portuguese (Brazilian and European), French, German, Arabic,
1292
+ Russian, Japanese, Korean, Bengali**.
1194
1293
 
1195
1294
  Adding a new language is a two-file PR — see [CONTRIBUTING.md](CONTRIBUTING.md).
1196
1295
 
@@ -1239,10 +1338,14 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1239
1338
  | `THREADKEEPER_CANDIDATE_REVIEW_INTERVAL_S` | 0 (off) | candidate-reviewer daemon tick (s), restart-throttled by the last `candidate_review_pass`; 3600 = 1h recommended |
1240
1339
  | `THREADKEEPER_CANDIDATE_REVIEW_MIN` | 3 | min pending candidates before reviewer engages |
1241
1340
  | `THREADKEEPER_CANDIDATE_REVIEW_FLUSH_AGE_S` | 259200 | age-flush: an undersized pending queue is still reviewed once its oldest candidate is this old (0 = threshold only) |
1341
+ | `THREADKEEPER_LESSON_VIOLATION_THRESHOLD` | 3 | violations of one lesson inside the window that make it memory-insufficient (recommend a hook) |
1342
+ | `THREADKEEPER_LESSON_VIOLATION_WINDOW_DAYS` | 90 | window for counting `lesson_violation` records |
1343
+ | `THREADKEEPER_CANDIDATE_REVIEW_BATCH_SIZE` | 40 | most pending candidates (oldest first) one reviewer child receives; the rest stay pending for the next pass |
1242
1344
  | `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 |
1243
1345
  | `THREADKEEPER_CURATOR_INTERVAL_S` | 259200 | deep curator audit every three days; set `0` to disable |
1244
1346
  | `THREADKEEPER_CURATOR_MANAGE_FOREGROUND_SKILLS` | 0 | allow snapshot-scoped Curator repair/merge/delete of foreground skills; pinned/untracked skills remain protected |
1245
1347
  | `THREADKEEPER_CURATOR_MIN_LESSONS` | 3 | min lessons before curator engages |
1348
+ | `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) |
1246
1349
  | `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 |
1247
1350
  | `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 |
1248
1351
  | `THREADKEEPER_CURATOR_SNAPSHOT_RETENTION` | 10 | number of destructive curator pre-mutation snapshots to retain under `<reports_dir>/snapshots`; current pass is always retained |
@@ -1250,7 +1353,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1250
1353
  | `THREADKEEPER_PROBE_INTERVAL_S` | 0 (off) | probe daemon tick (s); 1800 = 30 min recommended so finished probe answers are graded promptly |
1251
1354
  | `THREADKEEPER_PROBE_COOLDOWN_S` | 604800 | per-category probe cooldown; 86400 = 1d recommended for active reliability tracking |
1252
1355
  | `THREADKEEPER_SPAWN_BUDGET_MB` | 3072 | combined child RSS cap (MB); 0 disables |
1253
- | `THREADKEEPER_ALLOW_BYPASS_PERMISSIONS_SPAWN` | "" (off) | explicit override that lets ordinary `spawn()` calls request `permission_mode="bypassPermissions"`; default off means only evolve daemon role/write-origin pairs can use the dangerous mode |
1356
+ | `THREADKEEPER_ALLOW_BYPASS_PERMISSIONS_SPAWN` | "" (off) | explicit operator override that lets public `spawn()` calls request `permission_mode="bypassPermissions"`; otherwise only private server-owned Evolve launchers can use it |
1254
1357
  | `THREADKEEPER_SPAWN_TOKEN_BUDGET` | 0 | recorded 24h spawned-child token ceiling; 0 disables |
1255
1358
  | `THREADKEEPER_SPAWN_COST_BUDGET_USD` | 0 | recorded 24h spawned-child dollar ceiling; 0 disables |
1256
1359
  | `THREADKEEPER_SPAWN_MAX_RUNTIME_S` | 3600 | wall-clock lifetime cap (s) for a spawned child; over-cap live children are SIGTERM→SIGKILL'd and closed with `return_code` 124; 0 disables |
@@ -1287,7 +1390,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1287
1390
  | `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) |
1288
1391
  | `THREADKEEPER_DIALECTIC_VALIDATE_BATCH_SIZE` | 50 | max observations sent to one validator child; prevents oversized prompts and drains large queues incrementally |
1289
1392
  | `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 |
1290
- | `THREADKEEPER_EVOLVE_REVIEW_BACKLOG_MAX` | 25 | max open, not-yet-applied roadmap issues before the issue-creating audit is skipped and records `backlog_saturated`; `0` disables the cap |
1393
+ | `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 |
1291
1394
  | `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 |
1292
1395
  | `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 |
1293
1396
  | `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 |
@@ -1298,6 +1401,7 @@ The most-used env knobs (full list in `threadkeeper/config.py`):
1298
1401
  | `THREADKEEPER_EVOLVE_REPO_PROVISION_LOCK_TIMEOUT_S` | 5 | maximum seconds to wait for another clone/venv provisioning operation before returning `ERR evolve_repo_provisioning_in_progress retry_later=1`; `0` is immediate |
1299
1402
  | `THREADKEEPER_EVOLVE_APPLY_SKIP_LABELS` | `blocked,needs-design,wontfix,question,discussion,help wanted` | comma-separated labels that exclude GitHub issues from autonomous Evolve applier pickup. Exact-number apply returns `skipped: label X`; set to `off` to clear |
1300
1403
  | `THREADKEEPER_EVOLVE_TRUSTED_AUTHOR_ASSOCIATIONS` | `OWNER,MEMBER,COLLABORATOR` | comma-separated GitHub author associations eligible for **autonomous** issue pickup on this public repo; issues from other authors are skipped unless promoted (trust label or exact-number invocation) |
1404
+ | `THREADKEEPER_EVOLVE_CLAIM_AUTOMATION_ACTORS` | (empty) | comma-separated GitHub logins allowed to own visible Evolve claim comments without a trusted repository association; all other marker comments are advisory text only |
1301
1405
  | `THREADKEEPER_EVOLVE_TRUST_LABELS` | (empty) | comma-separated labels that promote an untrusted-author issue into the autonomous queue; on a public repo only collaborators can apply labels, so a trust label is a maintainer endorsement |
1302
1406
  | `THREADKEEPER_ROADMAP_ISSUE_MAX_ATTEMPTS` | 3 | poison-issue dead-letter cap: after this many implementer spawns for a roadmap issue with no resulting PR, the issue gets a `blocked` label + one summary comment and is excluded from the auto-drain until a human intervenes. A manual `evolve_apply_roadmap_issue(issue_number=N)` bypasses the cap, but the default skip-label gate still refuses the `blocked` label until it is removed or reconfigured |
1303
1407
  | `THREADKEEPER_ROADMAP_ISSUE_BACKOFF_BASE_S` | 172800 (2d) | base failure-backoff window for a roadmap issue; doubles per attempt (`base * 2^(attempts-1)`, capped at 30d). Defers re-selection of a repeatedly-aborting issue beyond the fixed 24h claim TTL |
@@ -1482,8 +1586,10 @@ hardening, WAL/schema migration, and vec-table setup once per process.
1482
1586
  `PRAGMA query_only=ON`, so retrieval cannot accidentally migrate, heartbeat,
1483
1587
  or write. `run_write()` opens a fresh connection, acquires `BEGIN IMMEDIATE`,
1484
1588
  runs a DB-only callback, and closes it; only `SQLITE_BUSY`/`SQLITE_LOCKED` are
1485
- retried with bounded jitter. `get_db()` remains a compatibility API for older
1486
- low-level call sites.
1589
+ retried with bounded jitter, including a lock hit while the connection is being
1590
+ set up. `get_db()` remains a compatibility API for older low-level call sites;
1591
+ its connections are tracked, and the daemon host logs the call site of any
1592
+ that keeps a write transaction open for a minute or more.
1487
1593
 
1488
1594
  Schema migration uses SQLite `PRAGMA user_version`: a current database skips
1489
1595
  legacy `ALTER TABLE` work, while an old or fresh v0 database migrates once
@@ -1818,8 +1924,8 @@ pip install -e '.[semantic,dev]'
1818
1924
  python -m pytest
1819
1925
  ```
1820
1926
 
1821
- 869 tests passing on Python 3.11 / 3.12 / 3.13 (1 skipped). CI runs
1822
- the suite and a resolved-dependency `pip-audit` gate on every push and PR.
1927
+ The suite runs on Python 3.11 / 3.12 / 3.13. CI runs it, together with a
1928
+ resolved-dependency `pip-audit` gate, on every push and PR.
1823
1929
  CodeQL scans the Python source on those changes and weekly; see
1824
1930
  [SECURITY.md](SECURITY.md#continuous-security-scanning) for the exception
1825
1931
  process and reporting policy.
@@ -1831,7 +1937,7 @@ process and reporting policy.
1831
1937
  ```
1832
1938
  threadkeeper/
1833
1939
  ├── server.py # MCP entry: python -m threadkeeper.server
1834
- ├── _mcp.py # FastMCP singleton + read_tool()/write_tool() annotation wrappers
1940
+ ├── _mcp.py # MCPServer singleton + read_tool()/write_tool() annotation wrappers
1835
1941
  ├── tool_schemas.py # typed outputSchema models for the structured status tools
1836
1942
  ├── _setup.py # `thread-keeper-setup` installer
1837
1943
  ├── config.py # env-driven defaults
@@ -1842,7 +1948,7 @@ threadkeeper/
1842
1948
  ├── eval/ # offline learning-loop decision-quality harness (python -m threadkeeper.eval)
1843
1949
  ├── brief.py # render_brief / render_context
1844
1950
  ├── shadow_review.py # autonomous learning observer
1845
- ├── i18n.py # 10 locales of regex + prompt bundles
1951
+ ├── i18n.py # 12 locales of regex + prompt bundles
1846
1952
  ├── adapters/ # one file per supported CLI
1847
1953
  │ ├── claude_code.py
1848
1954
  │ ├── claude_desktop.py
@@ -1850,7 +1956,7 @@ threadkeeper/
1850
1956
  │ ├── antigravity.py
1851
1957
  │ ├── copilot.py
1852
1958
  │ └── vscode.py
1853
- └── tools/ # @read_tool()/@write_tool() entries — 120 of them
1959
+ └── tools/ # registered @read_tool()/@write_tool() entries
1854
1960
  ├── threads.py
1855
1961
  ├── peers.py
1856
1962
  ├── spawn.py
@@ -1862,14 +1968,14 @@ threadkeeper/
1862
1968
 
1863
1969
  **Tool annotation contract (#67).** Every tool registers through
1864
1970
  `@read_tool()` or `@write_tool(destructive=…, idempotent=…)` (in `_mcp.py`),
1865
- so `tools/list` carries MCP 2025-06-18 `ToolAnnotations` for all 113 tools:
1866
- `readOnlyHint=True` for pure reads (`brief`, `context`, `search`,
1971
+ so `tools/list` carries MCP 2025-06-18 `ToolAnnotations` for every
1972
+ registered tool: `readOnlyHint=True` for pure reads (`brief`, `context`, `search`,
1867
1973
  `dialog_search`, the status tools, …) and `readOnlyHint=False`
1868
1974
  for mutations. `lesson_list` / `lesson_get` are classified as non-destructive
1869
- writes because they bump lesson access counters. The ten delete/overwrite/kill
1870
- tools carry `destructiveHint=True` (`compost` is read-only — it only surfaces
1871
- idle threads). A confirmation/elicitation host reads this to decide which calls
1872
- warrant a prompt. The five status tools (`context`, `spawn_budget_status`,
1975
+ writes because they bump lesson access counters. Delete/overwrite/kill tools
1976
+ carry `destructiveHint=True` (`compost` is read-only — it only surfaces idle
1977
+ threads). A confirmation/elicitation host reads this to decide which calls
1978
+ warrant a prompt. The status tools (`context`, `spawn_budget_status`,
1873
1979
  `spawn_status`, `mp_health`, `agent_status`) additionally advertise an
1874
1980
  `outputSchema` and return `structuredContent` alongside the legacy text
1875
1981
  block. The contract is enforced by `tests/test_tool_annotations.py`.