threadkeeper 0.17.4__tar.gz → 0.17.5__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 (254) hide show
  1. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/PKG-INFO +4 -2
  2. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/README.md +3 -1
  3. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/pyproject.toml +1 -1
  4. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_curator.py +6 -4
  5. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_lessons.py +68 -0
  6. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_use_parser.py +2 -1
  7. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skills.py +36 -0
  8. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_tool_annotations.py +2 -2
  9. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/lessons.py +99 -2
  10. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/skills.py +16 -3
  11. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/PKG-INFO +4 -2
  12. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/LICENSE +0 -0
  13. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/setup.cfg +0 -0
  14. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_adapters.py +0 -0
  15. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_agent_status.py +0 -0
  16. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_auto_update.py +0 -0
  17. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_backup.py +0 -0
  18. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_brief_footprint.py +0 -0
  19. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_brief_sections.py +0 -0
  20. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_candidate_reviewer.py +0 -0
  21. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_concepts.py +0 -0
  22. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_config_settings.py +0 -0
  23. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_config_watcher.py +0 -0
  24. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_core_memory.py +0 -0
  25. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_host_config.py +0 -0
  26. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_host_integration.py +0 -0
  27. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_liveness.py +0 -0
  28. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_state.py +0 -0
  29. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dashboard.py +0 -0
  30. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_compact.py +0 -0
  31. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_contention.py +0 -0
  32. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_migration_fts.py +0 -0
  33. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_runtime.py +0 -0
  34. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_schema.py +0 -0
  35. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_delegated_search.py +0 -0
  36. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic.py +0 -0
  37. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_feed_tools.py +0 -0
  38. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_miner.py +0 -0
  39. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_observation_resolve.py +0 -0
  40. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_recompute.py +0 -0
  41. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_tier.py +0 -0
  42. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_validator.py +0 -0
  43. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialog_fts_external.py +0 -0
  44. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_egress_policy.py +0 -0
  45. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embed_routing.py +0 -0
  46. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embedding_generation.py +0 -0
  47. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embedding_model_revision.py +0 -0
  48. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embedding_storage_dedup.py +0 -0
  49. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_error_paths.py +0 -0
  50. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_eval_harness.py +0 -0
  51. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_applier.py +0 -0
  52. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_apply_2.py +0 -0
  53. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_apply_3.py +0 -0
  54. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_daemon.py +0 -0
  55. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_extract_daemon.py +0 -0
  56. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_extract_dedup.py +0 -0
  57. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_forget.py +0 -0
  58. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_github_budget.py +0 -0
  59. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_github_safety.py +0 -0
  60. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_helpers.py +0 -0
  61. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_embed.py +0 -0
  62. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_env.py +0 -0
  63. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_main.py +0 -0
  64. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_wedged.py +0 -0
  65. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_hybrid_retrieval.py +0 -0
  66. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_i18n_multilang.py +0 -0
  67. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_identity.py +0 -0
  68. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_cursor.py +0 -0
  69. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_denylist.py +0 -0
  70. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_secret_redaction.py +0 -0
  71. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_status.py +0 -0
  72. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_transactions.py +0 -0
  73. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_mcp_resources_prompts.py +0 -0
  74. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_memory_eval.py +0 -0
  75. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_memory_guard.py +0 -0
  76. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_memory_guard_host.py +0 -0
  77. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_menubar_app.py +0 -0
  78. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_missed_spawns.py +0 -0
  79. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_model_catalog.py +0 -0
  80. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_notify_daemon.py +0 -0
  81. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_nudges.py +0 -0
  82. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_onnx_embeddings.py +0 -0
  83. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_panel.py +0 -0
  84. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_permissions.py +0 -0
  85. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_pickup.py +0 -0
  86. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_probe_daemon.py +0 -0
  87. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_process_health.py +0 -0
  88. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_release_workflows.py +0 -0
  89. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_retention.py +0 -0
  90. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_review_prompts.py +0 -0
  91. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_search_fts_punctuation.py +0 -0
  92. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_session_read_purity.py +0 -0
  93. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_shadow_review.py +0 -0
  94. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_single_flight_prompt_prefixes.py +0 -0
  95. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_hint.py +0 -0
  96. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_passive_tier.py +0 -0
  97. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_tier.py +0 -0
  98. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_updater.py +0 -0
  99. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_watcher.py +0 -0
  100. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_budget.py +0 -0
  101. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_codex_stdin.py +0 -0
  102. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_config.py +0 -0
  103. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_hint.py +0 -0
  104. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_permission_gate.py +0 -0
  105. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_reap.py +0 -0
  106. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_slim.py +0 -0
  107. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_watchdog.py +0 -0
  108. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_worktree_isolation.py +0 -0
  109. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_wrap.py +0 -0
  110. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_capture.py +0 -0
  111. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_identity.py +0 -0
  112. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_migrate.py +0 -0
  113. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_protocol.py +0 -0
  114. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_reset_node.py +0 -0
  115. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_transport.py +0 -0
  116. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_task_retention.py +0 -0
  117. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_task_spool.py +0 -0
  118. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_thin_session.py +0 -0
  119. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_thread_janitor.py +0 -0
  120. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_threads.py +0 -0
  121. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_tools_smoke.py +0 -0
  122. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_validate_threads.py +0 -0
  123. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_vec_search.py +0 -0
  124. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_verify_ingest.py +0 -0
  125. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/__init__.py +0 -0
  126. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/_mcp.py +0 -0
  127. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/_setup.py +0 -0
  128. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/_spawn_wrap.py +0 -0
  129. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/__init__.py +0 -0
  130. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/_hook_helpers.py +0 -0
  131. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/antigravity.py +0 -0
  132. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/base.py +0 -0
  133. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_code.py +0 -0
  134. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_desktop.py +0 -0
  135. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/codex.py +0 -0
  136. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/copilot.py +0 -0
  137. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/vscode.py +0 -0
  138. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/agent_metadata.py +0 -0
  139. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/agent_status.py +0 -0
  140. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  141. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  142. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  143. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  144. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  145. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/auto_update.py +0 -0
  146. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/backup.py +0 -0
  147. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/brief.py +0 -0
  148. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/candidate_reviewer.py +0 -0
  149. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/config.py +0 -0
  150. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/config_io.py +0 -0
  151. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/config_watcher.py +0 -0
  152. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/curator.py +0 -0
  153. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/curator_snapshots.py +0 -0
  154. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/daemon_liveness.py +0 -0
  155. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/daemon_state.py +0 -0
  156. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/daemon_supervisor.py +0 -0
  157. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/db.py +0 -0
  158. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/dialectic_miner.py +0 -0
  159. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/dialectic_validator.py +0 -0
  160. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/egress.py +0 -0
  161. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/elicitation.py +0 -0
  162. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/embeddings.py +0 -0
  163. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/eval/__init__.py +0 -0
  164. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/eval/__main__.py +0 -0
  165. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/eval/harness.py +0 -0
  166. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/evolve_applier.py +0 -0
  167. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/evolve_daemon.py +0 -0
  168. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/extract_daemon.py +0 -0
  169. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/forget.py +0 -0
  170. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/github_budget.py +0 -0
  171. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/github_safety.py +0 -0
  172. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/harvest.py +0 -0
  173. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/helpers.py +0 -0
  174. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/host.py +0 -0
  175. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/host_embed.py +0 -0
  176. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/i18n.py +0 -0
  177. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/identity.py +0 -0
  178. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/ingest.py +0 -0
  179. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/lessons.py +0 -0
  180. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/memory_guard.py +0 -0
  181. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/menubar_app.py +0 -0
  182. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/migrate_embeddings.py +0 -0
  183. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/model_catalog.py +0 -0
  184. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/notify.py +0 -0
  185. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/nudges.py +0 -0
  186. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/permissions.py +0 -0
  187. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/probe_daemon.py +0 -0
  188. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/process_health.py +0 -0
  189. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/retention.py +0 -0
  190. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/retrieval.py +0 -0
  191. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/review_prompts.py +0 -0
  192. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/search_proxy.py +0 -0
  193. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/server.py +0 -0
  194. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/shadow_review.py +0 -0
  195. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/skill_audit.py +0 -0
  196. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/skill_updater.py +0 -0
  197. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/skill_watcher.py +0 -0
  198. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/spawn_budget.py +0 -0
  199. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/spawn_config.py +0 -0
  200. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/__init__.py +0 -0
  201. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/capture.py +0 -0
  202. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/daemon.py +0 -0
  203. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/identity.py +0 -0
  204. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/migrate.py +0 -0
  205. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/protocol.py +0 -0
  206. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/reset_node.py +0 -0
  207. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/server.py +0 -0
  208. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/task_spool.py +0 -0
  209. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/thread_janitor.py +0 -0
  210. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tool_schemas.py +0 -0
  211. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/__init__.py +0 -0
  212. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/agent_status.py +0 -0
  213. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/candidate_reviewer.py +0 -0
  214. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/concepts.py +0 -0
  215. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/config_watch.py +0 -0
  216. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/consolidate.py +0 -0
  217. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/core_memory.py +0 -0
  218. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/correlation.py +0 -0
  219. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/curator.py +0 -0
  220. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dashboard.py +0 -0
  221. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/db_maintenance.py +0 -0
  222. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic.py +0 -0
  223. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic_feed.py +0 -0
  224. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dialog.py +0 -0
  225. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/distill.py +0 -0
  226. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/evolve_applier.py +0 -0
  227. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/extract.py +0 -0
  228. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/forget.py +0 -0
  229. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/graph.py +0 -0
  230. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/invariants.py +0 -0
  231. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/memory_guard.py +0 -0
  232. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/missed_spawns.py +0 -0
  233. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/panel.py +0 -0
  234. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/peers.py +0 -0
  235. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/pickup.py +0 -0
  236. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/probes.py +0 -0
  237. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/process_health.py +0 -0
  238. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/prompts.py +0 -0
  239. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/resources.py +0 -0
  240. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/session.py +0 -0
  241. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/shadow_review.py +0 -0
  242. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/spawn.py +0 -0
  243. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/style.py +0 -0
  244. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/sync.py +0 -0
  245. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/threads.py +0 -0
  246. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/validate.py +0 -0
  247. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/trash.py +0 -0
  248. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/verify_ingest.py +0 -0
  249. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/wikilinks.py +0 -0
  250. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/SOURCES.txt +0 -0
  251. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/dependency_links.txt +0 -0
  252. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/entry_points.txt +0 -0
  253. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/requires.txt +0 -0
  254. {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.17.4
3
+ Version: 0.17.5
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
@@ -611,7 +611,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
611
611
  though an existing same-slug long lesson may be corrected without increasing
612
612
  its body size; near-duplicate slugs are blocked, and semantic body matches are
613
613
  routed to the incumbent lesson or surfaced for curation instead of minting a
614
- sibling lesson.
614
+ sibling lesson. A clear new directive or debunk also flags older permissive
615
+ lessons on the same concrete practice for patch, cross-link, or supersession
616
+ review.
615
617
 
616
618
  #### 3. Extract daemon
617
619
 
@@ -567,7 +567,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
567
567
  though an existing same-slug long lesson may be corrected without increasing
568
568
  its body size; near-duplicate slugs are blocked, and semantic body matches are
569
569
  routed to the incumbent lesson or surfaced for curation instead of minting a
570
- sibling lesson.
570
+ sibling lesson. A clear new directive or debunk also flags older permissive
571
+ lessons on the same concrete practice for patch, cross-link, or supersession
572
+ review.
571
573
 
572
574
  #### 3. Extract daemon
573
575
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.17.4"
7
+ version = "0.17.5"
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" }]
@@ -152,15 +152,15 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
152
152
  conn.execute(
153
153
  "INSERT INTO skill_usage "
154
154
  "(name, created_at, created_by_origin, last_used_at, "
155
- " use_count, pinned, state) "
156
- "VALUES (?, ?, 'foreground', ?, 5, 1, 'active')",
155
+ " use_count, foreground_use_count, view_count, pinned, state) "
156
+ "VALUES (?, ?, 'foreground', ?, 5, 4, 3, 1, 'active')",
157
157
  ("pinned-skill", now - 86400, now - 3600),
158
158
  )
159
159
  conn.execute(
160
160
  "INSERT INTO skill_usage "
161
161
  "(name, created_at, created_by_origin, last_used_at, "
162
- " use_count, state) "
163
- "VALUES (?, ?, 'background_review', ?, 2, 'active')",
162
+ " use_count, foreground_use_count, view_count, state) "
163
+ "VALUES (?, ?, 'background_review', ?, 2, 1, 6, 'active')",
164
164
  ("auto-created-skill", now - 172800, now - 7200),
165
165
  )
166
166
  conn.commit()
@@ -177,6 +177,8 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
177
177
  # background_review skill (not pinned) is NOT protected
178
178
  assert "SKILL auto-created-skill [PROTECTED]" not in dump
179
179
  assert "SKILL auto-created-skill" in dump
180
+ assert "fg_uses=4 uses=5 views=3 maintenance_patches=0" in dump
181
+ assert "fg_uses=1 uses=2 views=6 maintenance_patches=0" in dump
180
182
  assert "STALE LESSONS (dry-run decay ranking)" in dump
181
183
 
182
184
 
@@ -356,6 +356,74 @@ def test_shadow_lesson_append_semantic_duplicate_patches_incumbent(
356
356
  assert "serialize AVD boot lock acquisition" in body
357
357
 
358
358
 
359
+ def test_new_directive_flags_older_permissive_lesson_for_reconciliation(
360
+ tmp_path, monkeypatch,
361
+ ):
362
+ pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
363
+ la = _tool(pkg, "lesson_append")
364
+
365
+ first = la(
366
+ title="module globals for test configuration",
367
+ body=(
368
+ "You can use module globals to pass test configuration between "
369
+ "runner steps when setup is simple."
370
+ ),
371
+ source="shadow",
372
+ )
373
+ assert first.startswith("ok")
374
+
375
+ out = la(
376
+ title="inject test configuration explicitly",
377
+ body=(
378
+ "Never use module globals to pass test configuration between "
379
+ "runner steps; inject dependencies explicitly instead."
380
+ ),
381
+ source="shadow",
382
+ )
383
+
384
+ assert out.startswith("ok slug=inject-test-configuration-explicitly")
385
+ assert "reconciliation=module-globals-for-test-configuration" in out
386
+ assert "review=patch_or_cross_link" in out
387
+ assert pkg["lessons"].count_lessons() == 2
388
+ row = pkg["db"].get_db().execute(
389
+ "SELECT target, summary FROM events WHERE kind='lesson_reconciliation'"
390
+ ).fetchone()
391
+ assert row["target"] == "module-globals-for-test-configuration"
392
+ assert "new=inject-test-configuration-explicitly" in row["summary"]
393
+ assert "reason=absolute_directive" in row["summary"]
394
+
395
+
396
+ def test_new_debunk_flags_older_permissive_lesson_for_reconciliation(
397
+ tmp_path, monkeypatch,
398
+ ):
399
+ pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
400
+ la = _tool(pkg, "lesson_append")
401
+
402
+ la(
403
+ title="shared runner environment state",
404
+ body=(
405
+ "It is safe to keep shared runner environment state throughout "
406
+ "a test workflow."
407
+ ),
408
+ source="shadow",
409
+ )
410
+
411
+ out = la(
412
+ title="shared runner environment state is unreliable",
413
+ body=(
414
+ "Keeping shared runner environment state is unreliable; pass the "
415
+ "environment to each command instead."
416
+ ),
417
+ source="shadow",
418
+ )
419
+
420
+ assert "reconciliation=shared-runner-environment-state" in out
421
+ row = pkg["db"].get_db().execute(
422
+ "SELECT summary FROM events WHERE kind='lesson_reconciliation'"
423
+ ).fetchone()
424
+ assert "reason=debunk" in row["summary"]
425
+
426
+
359
427
  def test_shadow_lesson_append_semantic_borderline_surfaces_duplicate(
360
428
  tmp_path, monkeypatch,
361
429
  ):
@@ -182,11 +182,12 @@ def test_ingest_file_bumps_skill_usage(tmp_path, monkeypatch):
182
182
  conn.commit()
183
183
  assert added == 1
184
184
  row = conn.execute(
185
- "SELECT use_count, last_used_at, created_by_origin "
185
+ "SELECT use_count, foreground_use_count, last_used_at, created_by_origin "
186
186
  "FROM skill_usage WHERE name='swift-ios'"
187
187
  ).fetchone()
188
188
  assert row is not None
189
189
  assert row["use_count"] == 1
190
+ assert row["foreground_use_count"] == 1
190
191
  assert row["last_used_at"] is not None
191
192
  assert row["created_by_origin"] == "foreground"
192
193
 
@@ -466,6 +466,42 @@ def test_skill_record_bumps_counters(skills_pkg):
466
466
  assert r["last_viewed_at"] is not None
467
467
 
468
468
 
469
+ def test_skill_list_records_views_for_returned_skills(skills_pkg):
470
+ _seed_skill(skills_pkg, name="listed-active")
471
+ _seed_skill(skills_pkg, name="listed-archived")
472
+ conn = skills_pkg["db"].get_db()
473
+ conn.execute(
474
+ "UPDATE skill_usage SET state='archived' WHERE name='listed-archived'"
475
+ )
476
+ conn.commit()
477
+
478
+ listed = _tool(skills_pkg, "skill_list")
479
+ rendered = listed()
480
+ assert "listed-active" in rendered
481
+ assert "views=1" in rendered
482
+ rows = {
483
+ row["name"]: row
484
+ for row in conn.execute(
485
+ "SELECT name, view_count, last_viewed_at FROM skill_usage "
486
+ "WHERE name IN ('listed-active', 'listed-archived')"
487
+ ).fetchall()
488
+ }
489
+ assert rows["listed-active"]["view_count"] == 1
490
+ assert rows["listed-active"]["last_viewed_at"] is not None
491
+ assert rows["listed-archived"]["view_count"] == 0
492
+
493
+ assert "listed-archived" in listed(include_archived=True)
494
+ rows = {
495
+ row["name"]: row
496
+ for row in conn.execute(
497
+ "SELECT name, view_count FROM skill_usage "
498
+ "WHERE name IN ('listed-active', 'listed-archived')"
499
+ ).fetchall()
500
+ }
501
+ assert rows["listed-active"]["view_count"] == 2
502
+ assert rows["listed-archived"]["view_count"] == 1
503
+
504
+
469
505
  def test_skill_list_omits_archived_by_default(skills_pkg):
470
506
  _seed_skill(skills_pkg, name="alive")
471
507
  _seed_skill(skills_pkg, name="ghost")
@@ -33,7 +33,7 @@ READ_TOOLS = {
33
33
  "memory_guard_status", "mp_dashboard", "mp_health", "neighbors", "peers",
34
34
  "pending_distillates", "pickup_candidates", "reliability_for",
35
35
  "review_candidates", "run_probe", "search", "shadow_review_status",
36
- "skill_list", "skill_validate", "spawn_budget_status", "spawn_status",
36
+ "skill_validate", "spawn_budget_status", "spawn_status",
37
37
  "sync_peers", "sync_status", "task_logs",
38
38
  "task_thread", "tasks", "weak_spots", "whoami",
39
39
  }
@@ -65,7 +65,7 @@ WRITE_TOOLS = {
65
65
  "open_dialog_window", "open_thread", "presence", "record_attempt",
66
66
  "register_concept", "register_probe", "reject_candidate", "release_pickup",
67
67
  "respond", "review_thread", "search_via_parent", "session_end",
68
- "shadow_review_run", "skill_record", "spawn", "spawn_budget_set",
68
+ "shadow_review_run", "skill_list", "skill_record", "spawn", "spawn_budget_set",
69
69
  "style_set", "sync_now", "tag_signal", "tournament", "validate_threads",
70
70
  "verbatim_user", "vote_distill", "wait", "whisper",
71
71
  }
@@ -81,6 +81,24 @@ _LESSON_SLUG_STOPWORDS = {
81
81
  "a", "an", "and", "as", "before", "for", "in", "is", "not", "of",
82
82
  "on", "or", "the", "to", "via", "with",
83
83
  }
84
+ _RECONCILIATION_ABSOLUTE_RE = re.compile(
85
+ r"\b(?:always|never|must(?:\s+not)?|do\s+not|don't|cannot|can't)\b",
86
+ re.IGNORECASE,
87
+ )
88
+ _RECONCILIATION_DEBUNK_RE = re.compile(
89
+ r"\b(?:avoid|unsafe|unreliable|broken|wrong|deprecated|no\s+longer)\b",
90
+ re.IGNORECASE,
91
+ )
92
+ _RECONCILIATION_PERMISSIVE_RE = re.compile(
93
+ r"\b(?:can|may|allowed|permit(?:ted)?|safe(?:ly)?|okay|ok|fine|use)\b",
94
+ re.IGNORECASE,
95
+ )
96
+ _RECONCILIATION_TOKEN_STOPWORDS = _LESSON_SLUG_STOPWORDS | {
97
+ "all", "always", "any", "avoid", "can", "cannot", "dont", "do",
98
+ "fine", "guidance", "lesson", "may", "must", "never", "not", "ok",
99
+ "okay", "old", "only", "practice", "safe", "safely", "should", "the",
100
+ "to", "unsafe", "use", "using", "with", "wrong",
101
+ }
84
102
 
85
103
 
86
104
  def _row_to_dict(row) -> dict | None:
@@ -185,6 +203,55 @@ def _normalized_lesson_text(text: str) -> str:
185
203
  return " ".join((text or "").lower().split())
186
204
 
187
205
 
206
+ def _reconciliation_reason(text: str) -> str | None:
207
+ """Classify an incoming lesson that can supersede prior guidance."""
208
+ if _RECONCILIATION_ABSOLUTE_RE.search(text):
209
+ return "absolute_directive"
210
+ if _RECONCILIATION_DEBUNK_RE.search(text):
211
+ return "debunk"
212
+ return None
213
+
214
+
215
+ def _reconciliation_tokens(text: str) -> set[str]:
216
+ return {
217
+ token for token in _LESSON_TOKEN_RE.findall(text.lower())
218
+ if len(token) > 2 and token not in _RECONCILIATION_TOKEN_STOPWORDS
219
+ }
220
+
221
+
222
+ def _reconciliation_candidates(
223
+ title: str,
224
+ summary: str,
225
+ body: str,
226
+ ) -> list[tuple[str, str]]:
227
+ """Find older permissive lessons a clear new directive may replace.
228
+
229
+ This is deliberately lexical rather than semantic. A high semantic score
230
+ already routes ordinary duplicates through the dedup path; reconciliation
231
+ needs a transparent, conservative signal before that path can merge two
232
+ pieces of guidance that actually disagree.
233
+ """
234
+ incoming = _semantic_text(title, summary, body)
235
+ reason = _reconciliation_reason(incoming)
236
+ if not reason:
237
+ return []
238
+ candidate_slug = _slugify(title)
239
+ candidate_tokens = _reconciliation_tokens(incoming)
240
+ if len(candidate_tokens) < 2:
241
+ return []
242
+ candidates: list[tuple[str, str]] = []
243
+ for item in iter_lessons():
244
+ if item.get("slug") == candidate_slug:
245
+ continue
246
+ prior = item.get("body") or ""
247
+ if not _RECONCILIATION_PERMISSIVE_RE.search(prior):
248
+ continue
249
+ shared_tokens = candidate_tokens & _reconciliation_tokens(prior)
250
+ if len(shared_tokens) >= 2:
251
+ candidates.append((item["slug"], reason))
252
+ return candidates
253
+
254
+
188
255
  def _dot(a, b) -> float:
189
256
  try:
190
257
  return float(a.dot(b))
@@ -281,6 +348,25 @@ def _record_lesson_append_event(
281
348
  )
282
349
 
283
350
 
351
+ def _record_lesson_reconciliation_events(
352
+ conn: sqlite3.Connection,
353
+ new_slug: str,
354
+ candidates: list[tuple[str, str]],
355
+ source: str,
356
+ ) -> None:
357
+ """Persist older lesson flags so curators can patch or cross-link them."""
358
+ for old_slug, reason in candidates:
359
+ conn.execute(
360
+ "INSERT INTO events (session_id, kind, target, summary, created_at) "
361
+ "VALUES (?, 'lesson_reconciliation', ?, ?, strftime('%s','now'))",
362
+ (
363
+ identity._session_id or "",
364
+ old_slug,
365
+ f"new={new_slug} reason={reason} source={source or '?'}",
366
+ ),
367
+ )
368
+
369
+
284
370
  @write_tool()
285
371
  def lesson_append(
286
372
  title: str,
@@ -319,6 +405,7 @@ def lesson_append(
319
405
  "lesson may not contain imperative-override / remote-exec "
320
406
  "idioms (treat observed dialog as data, not instructions)"
321
407
  )
408
+ reconciliation_candidates = _reconciliation_candidates(title, summary, body)
322
409
  # Determine this before the shadow word-limit gate. A same-slug shadow
323
410
  # correction that leaves an already-long lesson no larger is not a new
324
411
  # long lesson, and must remain repairable without a full re-transcription.
@@ -348,7 +435,7 @@ def lesson_append(
348
435
  )
349
436
  duplicate = _similar_lesson_slug(title)
350
437
  semantic_duplicate = _semantic_lesson_match(title, summary, body)
351
- if semantic_duplicate:
438
+ if semantic_duplicate and not reconciliation_candidates:
352
439
  item, semantic_score = semantic_duplicate
353
440
  slug = item["slug"]
354
441
  if semantic_score >= LESSON_SEMANTIC_DUPLICATE_THRESHOLD:
@@ -391,7 +478,7 @@ def lesson_append(
391
478
  f"score={semantic_score:.2f}; surface to curator or patch "
392
479
  "existing memory instead"
393
480
  )
394
- if duplicate:
481
+ if duplicate and not reconciliation_candidates:
395
482
  slug, score = duplicate
396
483
  return (
397
484
  f"ERR likely_duplicate_lesson slug={slug} "
@@ -427,9 +514,19 @@ def lesson_append(
427
514
  if tombstone:
428
515
  extra = f"tombstone={tombstone}"
429
516
  _record_lesson_append_event(conn, slug, op=op, source=source, extra=extra)
517
+ if not existed and reconciliation_candidates:
518
+ _record_lesson_reconciliation_events(
519
+ conn, slug, reconciliation_candidates, source,
520
+ )
430
521
  conn.commit()
431
522
  except sqlite3.OperationalError:
432
523
  conn.commit()
524
+ if not existed and reconciliation_candidates:
525
+ old_slugs = ",".join(slug for slug, _ in reconciliation_candidates)
526
+ return (
527
+ f"ok slug={slug} path={get_path()} reconciliation={old_slugs}; "
528
+ "review=patch_or_cross_link"
529
+ )
433
530
  return f"ok slug={slug} path={get_path()}"
434
531
 
435
532
 
@@ -1053,12 +1053,17 @@ def _action_restore(name: str) -> str:
1053
1053
  # skill_list
1054
1054
  # ──────────────────────────────────────────────────────────────────────────
1055
1055
 
1056
- @read_tool()
1056
+ @write_tool()
1057
1057
  def skill_list(include_archived: bool = False) -> str:
1058
1058
  """List skills with telemetry. Format:
1059
1059
  <name> tier=<hypothesis|observed|validated> origin=<...>
1060
1060
  state=<active|stale|archived> uses=N fg_uses=N
1061
1061
  views=N patches=N wrong=N pinned=0/1 last_active=<age>
1062
+
1063
+ Each returned row is a visibility event: its ``view_count`` and
1064
+ ``last_viewed_at`` are updated before the list is returned. The curator
1065
+ inventory itself is deliberately excluded, so an automated audit cannot
1066
+ make every skill look recently consulted.
1062
1067
  """
1063
1068
  conn = get_db()
1064
1069
  _ensure_session(conn)
@@ -1074,6 +1079,13 @@ def skill_list(include_archived: bool = False) -> str:
1074
1079
  if not rows:
1075
1080
  return "no_skills_tracked"
1076
1081
  now = int(time.time())
1082
+ conn.executemany(
1083
+ "UPDATE skill_usage SET last_viewed_at=?, view_count=view_count+1, "
1084
+ "state=CASE WHEN state='stale' THEN 'active' ELSE state END "
1085
+ "WHERE name=?",
1086
+ ((now, r["name"]) for r in rows),
1087
+ )
1088
+ conn.commit()
1077
1089
  out: list[str] = []
1078
1090
  for r in rows:
1079
1091
  last = max(
@@ -1104,8 +1116,9 @@ def skill_list(include_archived: bool = False) -> str:
1104
1116
  )
1105
1117
  out.append(
1106
1118
  f"{r['name']} tier={tier} origin={r['created_by_origin']} "
1107
- f"state={r['state']} uses={r['use_count']} "
1108
- f"fg_uses={fg_uses} views={r['view_count']} "
1119
+ f"state={'active' if r['state'] == 'stale' else r['state']} "
1120
+ f"uses={r['use_count']} "
1121
+ f"fg_uses={fg_uses} views={(r['view_count'] or 0) + 1} "
1109
1122
  f"patches={r['patch_count']} wrong={wrong_n} "
1110
1123
  f"pinned={r['pinned']} last_active={age}_ago"
1111
1124
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.17.4
3
+ Version: 0.17.5
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
@@ -611,7 +611,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
611
611
  though an existing same-slug long lesson may be corrected without increasing
612
612
  its body size; near-duplicate slugs are blocked, and semantic body matches are
613
613
  routed to the incumbent lesson or surfaced for curation instead of minting a
614
- sibling lesson.
614
+ sibling lesson. A clear new directive or debunk also flags older permissive
615
+ lessons on the same concrete practice for patch, cross-link, or supersession
616
+ review.
615
617
 
616
618
  #### 3. Extract daemon
617
619
 
File without changes
File without changes