threadkeeper 0.20.0__tar.gz → 0.22.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 (314) hide show
  1. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/PKG-INFO +60 -4
  2. threadkeeper-0.20.0/threadkeeper.egg-info/PKG-INFO → threadkeeper-0.22.0/README.md +58 -49
  3. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/pyproject.toml +4 -2
  4. threadkeeper-0.22.0/tests/test_action_outcomes.py +266 -0
  5. threadkeeper-0.22.0/tests/test_brief_hook_size.py +69 -0
  6. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_brief_sections.py +43 -0
  7. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_curator.py +164 -90
  8. threadkeeper-0.22.0/tests/test_dialectic_drift_eval.py +97 -0
  9. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_applier.py +13 -1
  10. threadkeeper-0.22.0/tests/test_evolve_apply_48.py +92 -0
  11. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_forget.py +66 -0
  12. threadkeeper-0.22.0/tests/test_lesson_index.py +76 -0
  13. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_eval.py +35 -0
  14. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_retention.py +29 -0
  15. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_slim.py +104 -0
  16. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_watchdog.py +59 -10
  17. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/_setup.py +10 -0
  18. threadkeeper-0.22.0/threadkeeper/action_outcomes.py +414 -0
  19. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/authority.py +5 -2
  20. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/brief.py +53 -1
  21. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/candidate_reviewer.py +5 -0
  22. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/curator.py +295 -149
  23. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/db.py +33 -0
  24. threadkeeper-0.22.0/threadkeeper/dialectic_drift_eval.py +321 -0
  25. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_validator.py +5 -0
  26. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/forget.py +142 -11
  27. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/instructions.py +5 -2
  28. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/lessons.py +49 -0
  29. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/retention.py +8 -0
  30. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/spawn_budget.py +38 -45
  31. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/curator.py +51 -18
  32. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/spawn.py +62 -2
  33. threadkeeper-0.20.0/README.md → threadkeeper-0.22.0/threadkeeper.egg-info/PKG-INFO +105 -2
  34. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/SOURCES.txt +7 -0
  35. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/requires.txt +1 -1
  36. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/LICENSE +0 -0
  37. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/setup.cfg +0 -0
  38. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_adapters.py +0 -0
  39. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_agent_status.py +0 -0
  40. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_antigravity_ingest.py +0 -0
  41. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_auto_update.py +0 -0
  42. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_backup.py +0 -0
  43. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_brief_footprint.py +0 -0
  44. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_candidate_reviewer.py +0 -0
  45. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ci_parallel.py +0 -0
  46. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_concepts.py +0 -0
  47. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_config_settings.py +0 -0
  48. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_config_watcher.py +0 -0
  49. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_core_memory.py +0 -0
  50. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_host_config.py +0 -0
  51. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_host_integration.py +0 -0
  52. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_liveness.py +0 -0
  53. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_state.py +0 -0
  54. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dashboard.py +0 -0
  55. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dashboard_app.py +0 -0
  56. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_compact.py +0 -0
  57. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_contention.py +0 -0
  58. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_leak_guard.py +0 -0
  59. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_migration_fts.py +0 -0
  60. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_runtime.py +0 -0
  61. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_schema.py +0 -0
  62. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_delegated_search.py +0 -0
  63. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic.py +0 -0
  64. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_feed_tools.py +0 -0
  65. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_miner.py +0 -0
  66. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_observation_resolve.py +0 -0
  67. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_recompute.py +0 -0
  68. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_tier.py +0 -0
  69. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_validator.py +0 -0
  70. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialog_fts_external.py +0 -0
  71. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_documentation_inventory.py +0 -0
  72. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_egress_policy.py +0 -0
  73. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embed_routing.py +0 -0
  74. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embedding_generation.py +0 -0
  75. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embedding_model_revision.py +0 -0
  76. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embedding_storage_dedup.py +0 -0
  77. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_error_paths.py +0 -0
  78. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_eval_harness.py +0 -0
  79. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_2.py +0 -0
  80. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_3.py +0 -0
  81. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_43.py +0 -0
  82. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_45.py +0 -0
  83. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_daemon.py +0 -0
  84. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_extract_daemon.py +0 -0
  85. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_extract_dedup.py +0 -0
  86. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_github_budget.py +0 -0
  87. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_github_safety.py +0 -0
  88. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_helpers.py +0 -0
  89. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_hook_child_guard.py +0 -0
  90. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_embed.py +0 -0
  91. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_env.py +0 -0
  92. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_main.py +0 -0
  93. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_wedged.py +0 -0
  94. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_hybrid_retrieval.py +0 -0
  95. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_i18n_multilang.py +0 -0
  96. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_identity.py +0 -0
  97. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_cursor.py +0 -0
  98. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_denylist.py +0 -0
  99. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_secret_redaction.py +0 -0
  100. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_status.py +0 -0
  101. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_transactions.py +0 -0
  102. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_lesson_violations.py +0 -0
  103. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_lessons.py +0 -0
  104. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_protocol_conformance.py +0 -0
  105. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_resources_prompts.py +0 -0
  106. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_sdk_compat.py +0 -0
  107. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_server_instructions.py +0 -0
  108. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_skills_extension.py +0 -0
  109. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_tasks.py +0 -0
  110. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_authority_lifecycle.py +0 -0
  111. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_compatibility.py +0 -0
  112. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_guard.py +0 -0
  113. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_guard_host.py +0 -0
  114. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_menubar_app.py +0 -0
  115. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_missed_spawns.py +0 -0
  116. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_catalog.py +0 -0
  117. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_router.py +0 -0
  118. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_router_digest.py +0 -0
  119. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_router_tools.py +0 -0
  120. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_routes.py +0 -0
  121. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_notify_daemon.py +0 -0
  122. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_nudges.py +0 -0
  123. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_onnx_embeddings.py +0 -0
  124. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_panel.py +0 -0
  125. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_permissions.py +0 -0
  126. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_pickup.py +0 -0
  127. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_probe_daemon.py +0 -0
  128. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_process_health.py +0 -0
  129. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_project_scope.py +0 -0
  130. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_release_workflows.py +0 -0
  131. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_review_prompts.py +0 -0
  132. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_search_fts_punctuation.py +0 -0
  133. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_session_read_purity.py +0 -0
  134. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_setup.py +0 -0
  135. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_shadow_review.py +0 -0
  136. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
  137. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_hint.py +0 -0
  138. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_passive_tier.py +0 -0
  139. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_tier.py +0 -0
  140. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_updater.py +0 -0
  141. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_use_parser.py +0 -0
  142. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_watcher.py +0 -0
  143. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skills.py +0 -0
  144. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_background_cwd.py +0 -0
  145. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_budget.py +0 -0
  146. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_child_env.py +0 -0
  147. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_codex_stdin.py +0 -0
  148. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_config.py +0 -0
  149. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_hint.py +0 -0
  150. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_permission_gate.py +0 -0
  151. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_reap.py +0 -0
  152. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_result.py +0 -0
  153. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_worktree_isolation.py +0 -0
  154. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_wrap.py +0 -0
  155. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_storage.py +0 -0
  156. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_capture.py +0 -0
  157. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_identity.py +0 -0
  158. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_migrate.py +0 -0
  159. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_protocol.py +0 -0
  160. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_reset_node.py +0 -0
  161. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_transport.py +0 -0
  162. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_task_retention.py +0 -0
  163. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_task_spool.py +0 -0
  164. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_thin_session.py +0 -0
  165. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_thread_janitor.py +0 -0
  166. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_threads.py +0 -0
  167. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tool_annotations.py +0 -0
  168. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tool_profiles.py +0 -0
  169. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tools_smoke.py +0 -0
  170. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tracing.py +0 -0
  171. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_validate_threads.py +0 -0
  172. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_vec_search.py +0 -0
  173. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_verify_ingest.py +0 -0
  174. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_workflow_retention.py +0 -0
  175. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/__init__.py +0 -0
  176. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/_mcp.py +0 -0
  177. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/_spawn_wrap.py +0 -0
  178. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/__init__.py +0 -0
  179. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  180. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/antigravity.py +0 -0
  181. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/base.py +0 -0
  182. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_code.py +0 -0
  183. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  184. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/codex.py +0 -0
  185. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/copilot.py +0 -0
  186. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/vscode.py +0 -0
  187. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/agent_metadata.py +0 -0
  188. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/agent_status.py +0 -0
  189. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  190. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  191. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  192. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  193. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  194. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/auto_update.py +0 -0
  195. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/backup.py +0 -0
  196. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/config.py +0 -0
  197. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/config_io.py +0 -0
  198. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/config_watcher.py +0 -0
  199. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/curator_snapshots.py +0 -0
  200. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/daemon_liveness.py +0 -0
  201. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/daemon_state.py +0 -0
  202. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/daemon_supervisor.py +0 -0
  203. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/dashboard_app.py +0 -0
  204. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_miner.py +0 -0
  205. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/egress.py +0 -0
  206. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/elicitation.py +0 -0
  207. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/embeddings.py +0 -0
  208. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/eval/__init__.py +0 -0
  209. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/eval/__main__.py +0 -0
  210. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/eval/harness.py +0 -0
  211. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/evolve_applier.py +0 -0
  212. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/evolve_daemon.py +0 -0
  213. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/extract_daemon.py +0 -0
  214. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/github_budget.py +0 -0
  215. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/github_safety.py +0 -0
  216. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/harvest.py +0 -0
  217. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/helpers.py +0 -0
  218. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/host.py +0 -0
  219. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/host_embed.py +0 -0
  220. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/i18n.py +0 -0
  221. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/identity.py +0 -0
  222. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/ingest.py +0 -0
  223. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/lesson_violations.py +0 -0
  224. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/link_health.py +0 -0
  225. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/mcp_skills.py +0 -0
  226. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/mcp_tasks.py +0 -0
  227. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/memory_compat.py +0 -0
  228. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/memory_guard.py +0 -0
  229. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/menubar_app.py +0 -0
  230. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/migrate_embeddings.py +0 -0
  231. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_catalog.py +0 -0
  232. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_router.py +0 -0
  233. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_router_digest.py +0 -0
  234. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_routes.py +0 -0
  235. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/notify.py +0 -0
  236. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/nudges.py +0 -0
  237. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/permissions.py +0 -0
  238. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/probe_daemon.py +0 -0
  239. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/process_health.py +0 -0
  240. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/project_scope.py +0 -0
  241. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/protocol.py +0 -0
  242. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/read_context.py +0 -0
  243. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/retrieval.py +0 -0
  244. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/review_prompts.py +0 -0
  245. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/search_proxy.py +0 -0
  246. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/server.py +0 -0
  247. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/shadow_review.py +0 -0
  248. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/skill_audit.py +0 -0
  249. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/skill_updater.py +0 -0
  250. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/skill_watcher.py +0 -0
  251. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/spawn_config.py +0 -0
  252. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/spawn_result.py +0 -0
  253. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/storage.py +0 -0
  254. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/__init__.py +0 -0
  255. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/capture.py +0 -0
  256. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/daemon.py +0 -0
  257. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/identity.py +0 -0
  258. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/migrate.py +0 -0
  259. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/protocol.py +0 -0
  260. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/reset_node.py +0 -0
  261. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/server.py +0 -0
  262. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/task_spool.py +0 -0
  263. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/thread_janitor.py +0 -0
  264. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tool_profiles.py +0 -0
  265. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tool_schemas.py +0 -0
  266. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/__init__.py +0 -0
  267. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/agent_status.py +0 -0
  268. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/authority.py +0 -0
  269. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  270. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/capabilities.py +0 -0
  271. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/concepts.py +0 -0
  272. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/config_watch.py +0 -0
  273. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/consolidate.py +0 -0
  274. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/core_memory.py +0 -0
  275. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/correlation.py +0 -0
  276. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dashboard.py +0 -0
  277. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/db_maintenance.py +0 -0
  278. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic.py +0 -0
  279. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  280. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialog.py +0 -0
  281. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/distill.py +0 -0
  282. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_applier.py +0 -0
  283. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_research.py +0 -0
  284. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/extract.py +0 -0
  285. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/forget.py +0 -0
  286. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/graph.py +0 -0
  287. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/invariants.py +0 -0
  288. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/lessons.py +0 -0
  289. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/memory_guard.py +0 -0
  290. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/missed_spawns.py +0 -0
  291. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/model_router.py +0 -0
  292. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/panel.py +0 -0
  293. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/peers.py +0 -0
  294. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/pickup.py +0 -0
  295. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/probes.py +0 -0
  296. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/process_health.py +0 -0
  297. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/project_scope.py +0 -0
  298. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/prompts.py +0 -0
  299. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/resources.py +0 -0
  300. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/session.py +0 -0
  301. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/shadow_review.py +0 -0
  302. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/skills.py +0 -0
  303. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/style.py +0 -0
  304. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/sync.py +0 -0
  305. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/threads.py +0 -0
  306. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/validate.py +0 -0
  307. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tracing.py +0 -0
  308. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/trash.py +0 -0
  309. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/verify_ingest.py +0 -0
  310. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/wikilinks.py +0 -0
  311. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/workflow_artifacts.py +0 -0
  312. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  313. {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/entry_points.txt +0 -0
  314. {threadkeeper-0.20.0 → threadkeeper-0.22.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.20.0
3
+ Version: 0.22.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
@@ -39,7 +39,7 @@ Provides-Extra: encrypted
39
39
  Requires-Dist: keyring>=25.0; extra == "encrypted"
40
40
  Requires-Dist: sqlcipher3-wheels>=0.5.7; extra == "encrypted"
41
41
  Provides-Extra: dev
42
- Requires-Dist: pytest>=9.1.1; extra == "dev"
42
+ Requires-Dist: pytest<9.2,>=9.1.1; extra == "dev"
43
43
  Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
44
44
  Requires-Dist: pytest-forked>=1.6; extra == "dev"
45
45
  Requires-Dist: pytest-xdist>=3.5; extra == "dev"
@@ -972,8 +972,12 @@ recovery snapshot right before its first mutating evaluator.
972
972
  its token cost); the evaluator then judges currency from local evidence only.
973
973
  A batch that holds no skills never gets a researcher: the fixed research topics
974
974
  only concern skills, so its evaluator starts at once.
975
- Each report path is then
976
- authorized in the existing parent-authored `curator_pass` event;
975
+ Curator pass and snapshot values are explicit, per-child launch data rather
976
+ than temporary server-process environment. Ordinary foreground and daemon
977
+ spawns strip stale inherited values. The private launcher binds each task,
978
+ child identity, pass, batch, phase, role, origin, and exact
979
+ report/research destination before launch; a watchdog retry restores only that
980
+ recorded data.
977
981
  `curator_report_write` records its SHA-256 in `curator_report_provenance`.
978
982
  This makes the report directory an untrusted transport: a stray or forged
979
983
  `REPORT-*.md` file cannot acquire the provenance needed by the applier.
@@ -1055,6 +1059,18 @@ within an 8-second budget per call; a call that runs out falls back to the
1055
1059
  lexical ranking and the next call resumes. Each line shows the lesson's summary. Without a query a session
1056
1060
  would only ever see the newest few lessons, whatever the task.
1057
1061
 
1062
+ **Lesson index.** Agents rarely search lessons on their own, so every lesson
1063
+ write also refreshes `lesson-index.md` next to `lessons.md`
1064
+ (`~/.threadkeeper/lesson-index.md` by default), a sorted list of all lesson
1065
+ titles (about 35 000 characters for 550 lessons). The managed
1066
+ instructions import it (`@~/.threadkeeper/lesson-index.md`), so a CLI that
1067
+ supports imports, such as Claude Code, loads every title with its instructions;
1068
+ other CLIs are told to read the file at session start. Each title is the rule,
1069
+ and the agent opens the ones that apply with `lesson_get`. On 160 real user
1070
+ prompts an agent that saw only this index opened 65-70% of the lessons judged
1071
+ relevant, and 51-59% of its picks were relevant. Injecting the lessons whose
1072
+ embedding is closest to the prompt reached 25% recall at 12% precision.
1073
+
1058
1074
  Lesson access is tracked the same way skill access is: `lesson_list` increments
1059
1075
  `lesson_usage.view_count` for displayed rows and `lesson_get` increments
1060
1076
  `lesson_usage.use_count` for the returned lesson. Only foreground reads count:
@@ -1448,6 +1464,13 @@ claim and its evidence but closes the old valid-time interval at the new claim's
1448
1464
  slice; `dialectic_review(as_of=...)` and
1449
1465
  `dialectic_synthesis(include_history=True)` expose past validity intervals.
1450
1466
 
1467
+ **Durable-preference drift pilot.** Run
1468
+ `python -m threadkeeper.dialectic_drift_eval --json` to replay the fixed,
1469
+ anonymized durable-change / task-exception / ambiguity / poisoning corpus. It
1470
+ reports supersession precision and recall, false durable-flip and clarification
1471
+ rates, poisoning containment, cited memory IDs, and counterfactuals. The pilot
1472
+ is read-only and does not change the live supersession policy.
1473
+
1451
1474
  **Source-based evidence discount.** Each evidence row's effective weight
1452
1475
  is `base_weight × discount(WRITE_ORIGIN)`. Foreground (direct user / human
1453
1476
  signal) = 1.0. shadow_review / background_review / candidate_review /
@@ -2251,6 +2274,21 @@ It reports these headline groups over a fixed ground-truth set:
2251
2274
  - **retrieval latency** — mean / p50 / p95 / max wall-clock milliseconds. With
2252
2275
  `--semantic`, the backend is reported as `hybrid`, because dense candidates
2253
2276
  augment rather than replace FTS.
2277
+ - **forgetting lifecycle** — the bundled mutation fixture writes a fact,
2278
+ corrects it twice, supersedes its dialectic claim, consolidates duplicate
2279
+ verbatim evidence, forgets the old source, and reopens the store in a new
2280
+ process. It reports a separate score for thread notes, verbatim memory,
2281
+ dialectic claims, and lessons or skills.
2282
+
2283
+ - **operational recovery comparison** — a second fully synthetic fixture
2284
+ compares dialogue-only evidence with a bounded action/outcome record. Its
2285
+ fixed thresholds require useful recovery links, zero forbidden-string leaks,
2286
+ bounded storage relative to the dialogue baseline, and zero false causal
2287
+ links. The record shape contains only normalized action names, opaque
2288
+ objective/source references, coarse outcomes, error fingerprints, ordering,
2289
+ recovery links, authority, provenance, and expiry. It has no raw arguments,
2290
+ raw output, transcript text, paths, or secret field.
2291
+
2254
2292
  - **outcome classification** — retrieval failure, reasoning failure,
2255
2293
  stale-memory use, removed-memory use, and privacy-policy violation.
2256
2294
 
@@ -2261,6 +2299,17 @@ unavailable when the optional semantic extra is not installed. `--strict`
2261
2299
  requires the versioned fixture thresholds and zero safety violations, making
2262
2300
  the same command suitable for retrieval/model migration gates.
2263
2301
 
2302
+ Action/outcome capture is an **offline-only prototype**. The runtime has no
2303
+ live producer or export route. Evaluation records expire after 30 days, and
2304
+ the retention pass removes them; `tk-forget --selector-type objective <ref>
2305
+ --apply` removes an objective's records and invalidates any linked lesson.
2306
+ Records sourced from a forgotten dialog/session/thread are removed by the
2307
+ same targeted-erasure path. The existing candidate reviewer may see only the
2308
+ bounded recovery candidate; a lesson must keep its
2309
+ `action_outcome_candidate:<id>` provenance so invalidation hides it. Live use
2310
+ requires a later explicit opt-in plus a passing offline comparison; this
2311
+ release provides neither.
2312
+
2264
2313
  For DB concurrency, run the reproducible local gate:
2265
2314
 
2266
2315
  ```bash
@@ -2286,6 +2335,13 @@ retrieved context, or replayed answer is written to stdout, artifacts, or the
2286
2335
  repository. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the corpus
2287
2336
  contract and migration-gate interpretation.
2288
2337
 
2338
+ The lifecycle score gives current evidence `+1` and subtracts `0.5` for every
2339
+ obsolete or invalidated fact that the same retrieval path returns. A current
2340
+ fact alongside stale evidence therefore does not receive full credit. The
2341
+ post-forgetting and post-restart stages must score `1.0` in both deterministic
2342
+ FTS and hybrid variants; the JSON report names the lifecycle stage and memory
2343
+ class for any release-gate failure.
2344
+
2289
2345
  ## Evaluating learning-loop decision quality
2290
2346
 
2291
2347
  `verify_ingest` answers *"did we capture the data?"*. The decision-quality
@@ -1,50 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: threadkeeper
3
- Version: 0.20.0
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
- Author: thread-keeper contributors
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/po4erk91/thread-keeper
8
- Project-URL: Repository, https://github.com/po4erk91/thread-keeper
9
- Project-URL: Issues, https://github.com/po4erk91/thread-keeper/issues
10
- Project-URL: Documentation, https://github.com/po4erk91/thread-keeper#readme
11
- Project-URL: Changelog, https://github.com/po4erk91/thread-keeper/releases
12
- Keywords: mcp,model-context-protocol,claude,codex,antigravity,copilot,memory,agents,self-improving,skills
13
- Classifier: Development Status :: 4 - Beta
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Operating System :: MacOS
16
- Classifier: Operating System :: POSIX :: Linux
17
- Classifier: Programming Language :: Python :: 3.11
18
- Classifier: Programming Language :: Python :: 3.12
19
- Classifier: Programming Language :: Python :: 3.13
20
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
- Requires-Python: >=3.11
23
- Description-Content-Type: text/markdown
24
- License-File: LICENSE
25
- Requires-Dist: mcp<3,>=2.3.0
26
- Requires-Dist: pydantic>=2.13.4
27
- Requires-Dist: pydantic-settings>=2.14.2
28
- Requires-Dist: pyyaml>=6.0.3
29
- Provides-Extra: semantic
30
- Requires-Dist: fastembed<0.9,>=0.8; extra == "semantic"
31
- Requires-Dist: huggingface-hub>=0.20; extra == "semantic"
32
- Requires-Dist: numpy>=1.24.0; extra == "semantic"
33
- Requires-Dist: sqlite-vec>=0.1.9; extra == "semantic"
34
- Provides-Extra: semantic-st
35
- Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic-st"
36
- Requires-Dist: numpy>=1.24.0; extra == "semantic-st"
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"
41
- Provides-Extra: dev
42
- Requires-Dist: pytest>=9.1.1; extra == "dev"
43
- Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
44
- Requires-Dist: pytest-forked>=1.6; extra == "dev"
45
- Requires-Dist: pytest-xdist>=3.5; extra == "dev"
46
- Dynamic: license-file
47
-
48
1
  # thread-keeper
49
2
 
50
3
  [![tests](https://github.com/po4erk91/thread-keeper/actions/workflows/test.yml/badge.svg)](https://github.com/po4erk91/thread-keeper/actions/workflows/test.yml)
@@ -972,8 +925,12 @@ recovery snapshot right before its first mutating evaluator.
972
925
  its token cost); the evaluator then judges currency from local evidence only.
973
926
  A batch that holds no skills never gets a researcher: the fixed research topics
974
927
  only concern skills, so its evaluator starts at once.
975
- Each report path is then
976
- authorized in the existing parent-authored `curator_pass` event;
928
+ Curator pass and snapshot values are explicit, per-child launch data rather
929
+ than temporary server-process environment. Ordinary foreground and daemon
930
+ spawns strip stale inherited values. The private launcher binds each task,
931
+ child identity, pass, batch, phase, role, origin, and exact
932
+ report/research destination before launch; a watchdog retry restores only that
933
+ recorded data.
977
934
  `curator_report_write` records its SHA-256 in `curator_report_provenance`.
978
935
  This makes the report directory an untrusted transport: a stray or forged
979
936
  `REPORT-*.md` file cannot acquire the provenance needed by the applier.
@@ -1055,6 +1012,18 @@ within an 8-second budget per call; a call that runs out falls back to the
1055
1012
  lexical ranking and the next call resumes. Each line shows the lesson's summary. Without a query a session
1056
1013
  would only ever see the newest few lessons, whatever the task.
1057
1014
 
1015
+ **Lesson index.** Agents rarely search lessons on their own, so every lesson
1016
+ write also refreshes `lesson-index.md` next to `lessons.md`
1017
+ (`~/.threadkeeper/lesson-index.md` by default), a sorted list of all lesson
1018
+ titles (about 35 000 characters for 550 lessons). The managed
1019
+ instructions import it (`@~/.threadkeeper/lesson-index.md`), so a CLI that
1020
+ supports imports, such as Claude Code, loads every title with its instructions;
1021
+ other CLIs are told to read the file at session start. Each title is the rule,
1022
+ and the agent opens the ones that apply with `lesson_get`. On 160 real user
1023
+ prompts an agent that saw only this index opened 65-70% of the lessons judged
1024
+ relevant, and 51-59% of its picks were relevant. Injecting the lessons whose
1025
+ embedding is closest to the prompt reached 25% recall at 12% precision.
1026
+
1058
1027
  Lesson access is tracked the same way skill access is: `lesson_list` increments
1059
1028
  `lesson_usage.view_count` for displayed rows and `lesson_get` increments
1060
1029
  `lesson_usage.use_count` for the returned lesson. Only foreground reads count:
@@ -1448,6 +1417,13 @@ claim and its evidence but closes the old valid-time interval at the new claim's
1448
1417
  slice; `dialectic_review(as_of=...)` and
1449
1418
  `dialectic_synthesis(include_history=True)` expose past validity intervals.
1450
1419
 
1420
+ **Durable-preference drift pilot.** Run
1421
+ `python -m threadkeeper.dialectic_drift_eval --json` to replay the fixed,
1422
+ anonymized durable-change / task-exception / ambiguity / poisoning corpus. It
1423
+ reports supersession precision and recall, false durable-flip and clarification
1424
+ rates, poisoning containment, cited memory IDs, and counterfactuals. The pilot
1425
+ is read-only and does not change the live supersession policy.
1426
+
1451
1427
  **Source-based evidence discount.** Each evidence row's effective weight
1452
1428
  is `base_weight × discount(WRITE_ORIGIN)`. Foreground (direct user / human
1453
1429
  signal) = 1.0. shadow_review / background_review / candidate_review /
@@ -2251,6 +2227,21 @@ It reports these headline groups over a fixed ground-truth set:
2251
2227
  - **retrieval latency** — mean / p50 / p95 / max wall-clock milliseconds. With
2252
2228
  `--semantic`, the backend is reported as `hybrid`, because dense candidates
2253
2229
  augment rather than replace FTS.
2230
+ - **forgetting lifecycle** — the bundled mutation fixture writes a fact,
2231
+ corrects it twice, supersedes its dialectic claim, consolidates duplicate
2232
+ verbatim evidence, forgets the old source, and reopens the store in a new
2233
+ process. It reports a separate score for thread notes, verbatim memory,
2234
+ dialectic claims, and lessons or skills.
2235
+
2236
+ - **operational recovery comparison** — a second fully synthetic fixture
2237
+ compares dialogue-only evidence with a bounded action/outcome record. Its
2238
+ fixed thresholds require useful recovery links, zero forbidden-string leaks,
2239
+ bounded storage relative to the dialogue baseline, and zero false causal
2240
+ links. The record shape contains only normalized action names, opaque
2241
+ objective/source references, coarse outcomes, error fingerprints, ordering,
2242
+ recovery links, authority, provenance, and expiry. It has no raw arguments,
2243
+ raw output, transcript text, paths, or secret field.
2244
+
2254
2245
  - **outcome classification** — retrieval failure, reasoning failure,
2255
2246
  stale-memory use, removed-memory use, and privacy-policy violation.
2256
2247
 
@@ -2261,6 +2252,17 @@ unavailable when the optional semantic extra is not installed. `--strict`
2261
2252
  requires the versioned fixture thresholds and zero safety violations, making
2262
2253
  the same command suitable for retrieval/model migration gates.
2263
2254
 
2255
+ Action/outcome capture is an **offline-only prototype**. The runtime has no
2256
+ live producer or export route. Evaluation records expire after 30 days, and
2257
+ the retention pass removes them; `tk-forget --selector-type objective <ref>
2258
+ --apply` removes an objective's records and invalidates any linked lesson.
2259
+ Records sourced from a forgotten dialog/session/thread are removed by the
2260
+ same targeted-erasure path. The existing candidate reviewer may see only the
2261
+ bounded recovery candidate; a lesson must keep its
2262
+ `action_outcome_candidate:<id>` provenance so invalidation hides it. Live use
2263
+ requires a later explicit opt-in plus a passing offline comparison; this
2264
+ release provides neither.
2265
+
2264
2266
  For DB concurrency, run the reproducible local gate:
2265
2267
 
2266
2268
  ```bash
@@ -2286,6 +2288,13 @@ retrieved context, or replayed answer is written to stdout, artifacts, or the
2286
2288
  repository. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the corpus
2287
2289
  contract and migration-gate interpretation.
2288
2290
 
2291
+ The lifecycle score gives current evidence `+1` and subtracts `0.5` for every
2292
+ obsolete or invalidated fact that the same retrieval path returns. A current
2293
+ fact alongside stale evidence therefore does not receive full credit. The
2294
+ post-forgetting and post-restart stages must score `1.0` in both deterministic
2295
+ FTS and hybrid variants; the JSON report names the lifecycle stage and memory
2296
+ class for any release-gate failure.
2297
+
2289
2298
  ## Evaluating learning-loop decision quality
2290
2299
 
2291
2300
  `verify_ingest` answers *"did we capture the data?"*. The decision-quality
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.20.0"
7
+ version = "0.22.0"
8
8
  description = "Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Copilot, and VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server."
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "thread-keeper contributors" }]
@@ -70,7 +70,9 @@ encrypted = [
70
70
  # not compose with --forked (collides on the xdist worker tmp dir) and a few
71
71
  # tests still race under it. See CONTRIBUTING → Running tests.
72
72
  dev = [
73
- "pytest>=9.1.1",
73
+ # pytest 9.2.0 breaks pytest-forked 1.7.5: every forked test exits with
74
+ # status 3 before reporting. Lift the cap once pytest-forked supports it.
75
+ "pytest>=9.1.1,<9.2",
74
76
  "pytest-cov>=7.1.0",
75
77
  "pytest-forked>=1.6",
76
78
  "pytest-xdist>=3.5",
@@ -0,0 +1,266 @@
1
+ """Offline action/outcome memory stays bounded, deletable, and non-live."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import time
6
+
7
+ import pytest
8
+
9
+
10
+ def _record_pair(conn, action_outcomes, *, objective="objective-42", source_id="dialog-42"):
11
+ failed = action_outcomes.record_evaluation_event(
12
+ conn,
13
+ event_id="ao-failed",
14
+ objective_ref=objective,
15
+ action_name="github.pr.create",
16
+ outcome="failure",
17
+ error_fingerprint="http.422.validation",
18
+ sequence=1,
19
+ source_kind="dialog",
20
+ source_id=source_id,
21
+ authority="observed",
22
+ principal="fixture-runner",
23
+ channel="offline-evaluation",
24
+ created_at=100,
25
+ )
26
+ recovered = action_outcomes.record_evaluation_event(
27
+ conn,
28
+ event_id="ao-recovered",
29
+ objective_ref=objective,
30
+ action_name="github.pr.create",
31
+ outcome="success",
32
+ sequence=2,
33
+ recovered_from=failed,
34
+ source_kind="dialog",
35
+ source_id=source_id,
36
+ authority="observed",
37
+ principal="fixture-runner",
38
+ channel="offline-evaluation",
39
+ created_at=101,
40
+ )
41
+ return failed, recovered
42
+
43
+
44
+ def test_schema_and_validation_exclude_raw_payloads(fresh_mp):
45
+ from threadkeeper import action_outcomes
46
+
47
+ conn = fresh_mp["db"].get_db()
48
+ columns = {
49
+ row["name"]
50
+ for row in conn.execute("PRAGMA table_info(action_outcome_events)")
51
+ }
52
+ assert columns == {
53
+ "id", "objective_ref", "action_name", "outcome", "error_fingerprint",
54
+ "recovered_from", "sequence", "source_kind", "source_id",
55
+ "authority_class", "provenance_version", "created_at", "expires_at",
56
+ }
57
+
58
+ with pytest.raises(ValueError, match="bounded identifier"):
59
+ action_outcomes.record_evaluation_event(
60
+ conn,
61
+ event_id="ao-secret",
62
+ objective_ref="objective-1",
63
+ action_name="github.pr.create --body raw-output",
64
+ outcome="success",
65
+ sequence=1,
66
+ source_kind="dialog",
67
+ source_id="dialog-1",
68
+ authority="observed",
69
+ principal="fixture-runner",
70
+ channel="offline-evaluation",
71
+ )
72
+ with pytest.raises(RuntimeError, match="offline evaluation"):
73
+ action_outcomes.record_live_event()
74
+
75
+
76
+ def test_recovery_order_and_authorized_candidate_are_bounded(fresh_mp):
77
+ from threadkeeper import action_outcomes
78
+
79
+ conn = fresh_mp["db"].get_db()
80
+ failed, recovered = _record_pair(conn, action_outcomes)
81
+ candidate_id = action_outcomes.derive_recovery_candidate(conn, recovered)
82
+
83
+ row = conn.execute(
84
+ "SELECT content, rationale, source_uuid FROM extract_candidates WHERE id=?",
85
+ (candidate_id,),
86
+ ).fetchone()
87
+ assert row["rationale"] == "operational_recovery"
88
+ assert row["source_uuid"] == f"action-outcome:{recovered}"
89
+ assert row["content"] == (
90
+ "Operational recovery: retry github.pr.create after "
91
+ "http.422.validation."
92
+ )
93
+ assert action_outcomes.candidate_source(conn, candidate_id) == (
94
+ "action_outcome_candidate", str(candidate_id)
95
+ )
96
+ exported = action_outcomes.export_events(conn)
97
+ assert [row["id"] for row in exported] == [failed, recovered]
98
+ assert set(exported[0]) == {
99
+ "id", "objective_ref", "action_name", "outcome", "error_fingerprint",
100
+ "recovered_from", "sequence", "source_kind", "source_id",
101
+ "authority_class", "provenance_version", "created_at", "expires_at",
102
+ }
103
+
104
+ with pytest.raises(ValueError, match="earlier failure"):
105
+ action_outcomes.record_evaluation_event(
106
+ conn,
107
+ event_id="ao-out-of-order",
108
+ objective_ref="objective-42",
109
+ action_name="github.pr.create",
110
+ outcome="success",
111
+ sequence=1,
112
+ recovered_from=failed,
113
+ source_kind="dialog",
114
+ source_id="dialog-42",
115
+ authority="observed",
116
+ principal="fixture-runner",
117
+ channel="offline-evaluation",
118
+ )
119
+ with pytest.raises(ValueError, match="same objective"):
120
+ action_outcomes.record_evaluation_event(
121
+ conn,
122
+ event_id="ao-cross-objective",
123
+ objective_ref="objective-43",
124
+ action_name="github.pr.create",
125
+ outcome="success",
126
+ sequence=2,
127
+ recovered_from=failed,
128
+ source_kind="dialog",
129
+ source_id="dialog-43",
130
+ authority="observed",
131
+ principal="fixture-runner",
132
+ channel="offline-evaluation",
133
+ )
134
+
135
+
136
+ def test_expiry_and_source_or_objective_forgetting_remove_candidates_and_invalidate_lessons(
137
+ fresh_mp,
138
+ ):
139
+ from threadkeeper import action_outcomes
140
+ from threadkeeper.authority import source_is_live
141
+ from threadkeeper.tools.lessons import lesson_append
142
+
143
+ conn = fresh_mp["db"].get_db()
144
+ _failed, recovered = _record_pair(conn, action_outcomes)
145
+ candidate_id = action_outcomes.derive_recovery_candidate(conn, recovered)
146
+ conn.commit()
147
+
148
+ result = lesson_append(
149
+ title="Recover GitHub validation errors",
150
+ summary="Use the linked recovery evidence.",
151
+ body="Retry the normalized action only after the stable validation error.",
152
+ source=f"action_outcome_candidate:{candidate_id}",
153
+ )
154
+ assert result.startswith("ok slug=recover-github-validation-errors")
155
+ assert source_is_live(conn, ("lesson", "recover-github-validation-errors"))
156
+
157
+ deleted = action_outcomes.forget_objective(conn, "objective-42")
158
+ assert deleted == {"events": 2, "candidates": 1}
159
+ assert not source_is_live(conn, ("lesson", "recover-github-validation-errors"))
160
+ assert conn.execute("SELECT COUNT(*) FROM action_outcome_events").fetchone()[0] == 0
161
+ assert conn.execute("SELECT COUNT(*) FROM extract_candidates").fetchone()[0] == 0
162
+
163
+ old = action_outcomes.record_evaluation_event(
164
+ conn,
165
+ event_id="ao-expired",
166
+ objective_ref="objective-44",
167
+ action_name="github.pr.create",
168
+ outcome="failure",
169
+ error_fingerprint="http.429.rate-limit",
170
+ sequence=1,
171
+ source_kind="dialog",
172
+ source_id="dialog-44",
173
+ authority="observed",
174
+ principal="fixture-runner",
175
+ channel="offline-evaluation",
176
+ created_at=int(time.time()) - 3 * 86400,
177
+ retention_days=1,
178
+ )
179
+ assert old == "ao-expired"
180
+ assert action_outcomes.prune_expired(conn, int(time.time())) == {"events": 1, "candidates": 0}
181
+
182
+ action_outcomes.record_evaluation_event(
183
+ conn,
184
+ event_id="ao-source-forget",
185
+ objective_ref="objective-45",
186
+ action_name="github.pr.create",
187
+ outcome="failure",
188
+ error_fingerprint="http.429.rate-limit",
189
+ sequence=1,
190
+ source_kind="dialog",
191
+ source_id="dialog-45",
192
+ authority="observed",
193
+ principal="fixture-runner",
194
+ channel="offline-evaluation",
195
+ )
196
+ assert action_outcomes.forget_sources(conn, [("dialog", "dialog-45")]) == {
197
+ "events": 1,
198
+ "candidates": 0,
199
+ }
200
+
201
+ conn.execute(
202
+ "INSERT INTO dialog_messages "
203
+ "(uuid, source, role, content, created_at) VALUES "
204
+ "('dialog-46', 'pytest', 'user', 'erasable source', 100)",
205
+ )
206
+ action_outcomes.record_evaluation_event(
207
+ conn,
208
+ event_id="ao-forget-selector",
209
+ objective_ref="objective-46",
210
+ action_name="github.pr.create",
211
+ outcome="failure",
212
+ error_fingerprint="http.429.rate-limit",
213
+ sequence=1,
214
+ source_kind="dialog",
215
+ source_id="dialog-46",
216
+ authority="observed",
217
+ principal="fixture-runner",
218
+ channel="offline-evaluation",
219
+ )
220
+ conn.commit()
221
+ from threadkeeper.forget import forget_selector
222
+
223
+ assert "action_outcomes=1" in forget_selector(
224
+ "dialog-46", selector_type="uuid", conn=conn,
225
+ )
226
+ forget_selector("dialog-46", selector_type="uuid", dry_run=False, conn=conn)
227
+ assert conn.execute("SELECT COUNT(*) FROM action_outcome_events").fetchone()[0] == 0
228
+
229
+
230
+ def test_offline_comparison_sets_thresholds_without_enabling_live_ingest():
231
+ from threadkeeper import action_outcomes
232
+
233
+ fixture = {
234
+ "dialogue_only": [
235
+ "tool arguments very-sensitive-arguments tool output unbounded-output " * 20,
236
+ ],
237
+ "events": [
238
+ {
239
+ "id": "failure", "objective_ref": "objective-1",
240
+ "action_name": "github.pr.create", "outcome": "failure",
241
+ "error_fingerprint": "http.422.validation", "sequence": 1,
242
+ },
243
+ {
244
+ "id": "recovery", "objective_ref": "objective-1",
245
+ "action_name": "github.pr.create", "outcome": "success",
246
+ "sequence": 2, "recovered_from": "failure",
247
+ },
248
+ ],
249
+ "expected_recoveries": [["failure", "recovery"]],
250
+ "forbidden_substrings": ["very-sensitive-arguments", "unbounded-output"],
251
+ "thresholds": {
252
+ "min_lesson_usefulness": 1.0,
253
+ "max_privacy_leakage": 0,
254
+ "max_storage_ratio": 0.5,
255
+ "max_false_causal_links": 0,
256
+ },
257
+ }
258
+ report = action_outcomes.evaluate_offline_fixture(fixture)
259
+
260
+ assert report["thresholds_pass"] is True
261
+ assert report["bounded_action_outcomes"]["privacy_leakage"] == 0
262
+ assert report["dialogue_only"]["privacy_leakage"] == 2
263
+ assert report["bounded_action_outcomes"]["lesson_usefulness"] == 1.0
264
+ assert report["live_ingestion_allowed"] is False
265
+ assert action_outcomes.live_ingestion_allowed(True, report) is True
266
+ assert json.dumps(report)
@@ -0,0 +1,69 @@
1
+ """The SessionStart brief stays under the CLI's hook output limit."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ HOOK = Path(__file__).resolve().parents[1] / "scripts" / "hooks" / "tk-brief.sh"
10
+
11
+
12
+ def _run_hook(tmp_path: Path, context_lines: int) -> str:
13
+ fake_python = tmp_path / "python"
14
+ body = "\\n".join(f"rule {i}: " + "x" * 60 for i in range(context_lines))
15
+ fake_python.write_text(
16
+ "#!/bin/sh\ncat >/dev/null\n"
17
+ f"printf 'STATUS\\tthread-keeper: ok\\nCONTEXT\\t{body}\\n'\n",
18
+ encoding="utf-8",
19
+ )
20
+ fake_python.chmod(0o700)
21
+ env = {
22
+ "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
23
+ "HOME": str(tmp_path),
24
+ "THREADKEEPER_PYTHON": str(fake_python),
25
+ }
26
+ proc = subprocess.run(
27
+ ["bash", str(HOOK)], input="{}", capture_output=True, text=True, env=env, timeout=30,
28
+ )
29
+ assert proc.returncode == 0, proc.stderr
30
+ return json.loads(proc.stdout)["hookSpecificOutput"]["additionalContext"]
31
+
32
+
33
+ def test_oversized_brief_is_cut_at_a_line_break_under_the_hook_limit(tmp_path):
34
+ context = _run_hook(tmp_path, context_lines=400)
35
+
36
+ assert len(context) <= 9000
37
+ assert context.endswith("call brief() for the rest]")
38
+ assert "\nrule 0: " in context
39
+ kept = context.splitlines()[-2]
40
+ assert kept.startswith("rule ") and kept.endswith("x" * 60)
41
+
42
+
43
+ def test_brief_under_the_limit_is_passed_through_whole(tmp_path):
44
+ context = _run_hook(tmp_path, context_lines=20)
45
+
46
+ assert "rule 19: " in context
47
+ assert "trimmed" not in context
48
+
49
+
50
+ def test_brief_without_line_breaks_is_cut_at_the_limit(tmp_path):
51
+ fake_python = tmp_path / "python"
52
+ fake_python.write_text(
53
+ "#!/bin/sh\ncat >/dev/null\n"
54
+ "printf 'STATUS\\tok\\nCONTEXT\\t%s\\n' \"$(head -c 20000 /dev/zero | tr '\\0' x)\"\n",
55
+ encoding="utf-8",
56
+ )
57
+ fake_python.chmod(0o700)
58
+ env = {
59
+ "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
60
+ "HOME": str(tmp_path),
61
+ "THREADKEEPER_PYTHON": str(fake_python),
62
+ }
63
+ proc = subprocess.run(
64
+ ["bash", str(HOOK)], input="{}", capture_output=True, text=True, env=env, timeout=30,
65
+ )
66
+
67
+ context = json.loads(proc.stdout)["hookSpecificOutput"]["additionalContext"]
68
+ assert 8000 < len(context) <= 9000
69
+ assert context.endswith("call brief() for the rest]")
@@ -146,6 +146,49 @@ def test_consulted_skills_silent_without_events(mp_with_cid):
146
146
  assert "consulted_skills" not in txt
147
147
 
148
148
 
149
+ def test_consulted_lessons_surface_recall_telemetry_and_keep_brief_sections(
150
+ mp_with_cid,
151
+ ):
152
+ """A recalled lesson keeps its read telemetry visible without dropping
153
+ the existing thread, evolve, or user-facing brief sections."""
154
+ pkg = mp_with_cid(_FAKE_CID)
155
+ lesson_append = _tool(pkg, "lesson_append")
156
+ lesson_list = _tool(pkg, "lesson_list")
157
+ lesson_get = _tool(pkg, "lesson_get")
158
+ open_thread = _tool(pkg, "open_thread")
159
+
160
+ assert lesson_append(
161
+ title="Recall telemetry", body="Use this lesson for brief coverage.",
162
+ source="shadow",
163
+ ).startswith("ok")
164
+ lesson_list(k=1)
165
+ assert "Use this lesson for brief coverage." in lesson_get(
166
+ slug="recall-telemetry"
167
+ )
168
+ open_thread(question="keep the open section")
169
+
170
+ conn = pkg["db"].get_db()
171
+ conn.execute(
172
+ "INSERT INTO evolve (suggestion, rationale, applied, status, created_at) "
173
+ "VALUES (?, ?, 0, 'promoted', ?)",
174
+ ("seeded promoted suggestion", "brief coverage", int(time.time())),
175
+ )
176
+ conn.commit()
177
+
178
+ txt = _brief_text(pkg)
179
+
180
+ assert "consulted_lessons" in txt
181
+ assert "recall-telemetry" in txt
182
+ assert "viewed×1" in txt
183
+ assert "used×1" in txt
184
+ assert "last_recalled=" in txt
185
+ assert "open" in txt
186
+ assert "keep the open section" in txt
187
+ assert "evolve_pending" in txt
188
+ assert "★ \"seeded promoted suggestion\"" in txt
189
+ assert "⚠️ user-facing: paraphrase plain." in txt
190
+
191
+
149
192
  def test_skill_nudge_silent_after_materialization(mp_with_cid, monkeypatch):
150
193
  monkeypatch.setenv("THREADKEEPER_SKILL_NUDGE_INTERVAL", "3")
151
194
  pkg = mp_with_cid(_FAKE_CID)