threadkeeper 0.17.3__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.3 → threadkeeper-0.17.5}/PKG-INFO +19 -4
  2. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/README.md +18 -3
  3. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/pyproject.toml +1 -1
  4. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_curator.py +48 -4
  5. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_lessons.py +68 -0
  6. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_use_parser.py +2 -1
  7. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skills.py +36 -0
  8. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_slim.py +7 -2
  9. threadkeeper-0.17.5/tests/test_spawn_worktree_isolation.py +112 -0
  10. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_tool_annotations.py +2 -2
  11. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/curator.py +5 -4
  12. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/skill_audit.py +15 -2
  13. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/lessons.py +99 -2
  14. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/skills.py +16 -3
  15. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/spawn.py +133 -0
  16. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/PKG-INFO +19 -4
  17. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/SOURCES.txt +1 -0
  18. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/LICENSE +0 -0
  19. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/setup.cfg +0 -0
  20. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_adapters.py +0 -0
  21. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_agent_status.py +0 -0
  22. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_auto_update.py +0 -0
  23. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_backup.py +0 -0
  24. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_brief_footprint.py +0 -0
  25. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_brief_sections.py +0 -0
  26. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_candidate_reviewer.py +0 -0
  27. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_concepts.py +0 -0
  28. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_config_settings.py +0 -0
  29. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_config_watcher.py +0 -0
  30. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_core_memory.py +0 -0
  31. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_host_config.py +0 -0
  32. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_host_integration.py +0 -0
  33. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_liveness.py +0 -0
  34. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_state.py +0 -0
  35. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dashboard.py +0 -0
  36. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_compact.py +0 -0
  37. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_contention.py +0 -0
  38. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_migration_fts.py +0 -0
  39. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_runtime.py +0 -0
  40. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_schema.py +0 -0
  41. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_delegated_search.py +0 -0
  42. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic.py +0 -0
  43. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_feed_tools.py +0 -0
  44. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_miner.py +0 -0
  45. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_observation_resolve.py +0 -0
  46. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_recompute.py +0 -0
  47. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_tier.py +0 -0
  48. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_validator.py +0 -0
  49. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialog_fts_external.py +0 -0
  50. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_egress_policy.py +0 -0
  51. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embed_routing.py +0 -0
  52. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embedding_generation.py +0 -0
  53. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embedding_model_revision.py +0 -0
  54. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embedding_storage_dedup.py +0 -0
  55. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_error_paths.py +0 -0
  56. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_eval_harness.py +0 -0
  57. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_applier.py +0 -0
  58. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_apply_2.py +0 -0
  59. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_apply_3.py +0 -0
  60. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_daemon.py +0 -0
  61. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_extract_daemon.py +0 -0
  62. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_extract_dedup.py +0 -0
  63. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_forget.py +0 -0
  64. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_github_budget.py +0 -0
  65. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_github_safety.py +0 -0
  66. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_helpers.py +0 -0
  67. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_embed.py +0 -0
  68. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_env.py +0 -0
  69. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_main.py +0 -0
  70. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_wedged.py +0 -0
  71. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_hybrid_retrieval.py +0 -0
  72. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_i18n_multilang.py +0 -0
  73. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_identity.py +0 -0
  74. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_cursor.py +0 -0
  75. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_denylist.py +0 -0
  76. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_secret_redaction.py +0 -0
  77. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_status.py +0 -0
  78. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_transactions.py +0 -0
  79. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_mcp_resources_prompts.py +0 -0
  80. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_memory_eval.py +0 -0
  81. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_memory_guard.py +0 -0
  82. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_memory_guard_host.py +0 -0
  83. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_menubar_app.py +0 -0
  84. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_missed_spawns.py +0 -0
  85. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_model_catalog.py +0 -0
  86. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_notify_daemon.py +0 -0
  87. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_nudges.py +0 -0
  88. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_onnx_embeddings.py +0 -0
  89. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_panel.py +0 -0
  90. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_permissions.py +0 -0
  91. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_pickup.py +0 -0
  92. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_probe_daemon.py +0 -0
  93. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_process_health.py +0 -0
  94. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_release_workflows.py +0 -0
  95. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_retention.py +0 -0
  96. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_review_prompts.py +0 -0
  97. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_search_fts_punctuation.py +0 -0
  98. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_session_read_purity.py +0 -0
  99. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_shadow_review.py +0 -0
  100. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_single_flight_prompt_prefixes.py +0 -0
  101. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_hint.py +0 -0
  102. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_passive_tier.py +0 -0
  103. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_tier.py +0 -0
  104. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_updater.py +0 -0
  105. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_watcher.py +0 -0
  106. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_budget.py +0 -0
  107. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_codex_stdin.py +0 -0
  108. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_config.py +0 -0
  109. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_hint.py +0 -0
  110. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_permission_gate.py +0 -0
  111. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_reap.py +0 -0
  112. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_watchdog.py +0 -0
  113. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_wrap.py +0 -0
  114. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_capture.py +0 -0
  115. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_identity.py +0 -0
  116. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_migrate.py +0 -0
  117. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_protocol.py +0 -0
  118. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_reset_node.py +0 -0
  119. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_transport.py +0 -0
  120. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_task_retention.py +0 -0
  121. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_task_spool.py +0 -0
  122. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_thin_session.py +0 -0
  123. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_thread_janitor.py +0 -0
  124. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_threads.py +0 -0
  125. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_tools_smoke.py +0 -0
  126. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_validate_threads.py +0 -0
  127. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_vec_search.py +0 -0
  128. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_verify_ingest.py +0 -0
  129. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/__init__.py +0 -0
  130. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/_mcp.py +0 -0
  131. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/_setup.py +0 -0
  132. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/_spawn_wrap.py +0 -0
  133. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/__init__.py +0 -0
  134. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/_hook_helpers.py +0 -0
  135. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/antigravity.py +0 -0
  136. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/base.py +0 -0
  137. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_code.py +0 -0
  138. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_desktop.py +0 -0
  139. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/codex.py +0 -0
  140. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/copilot.py +0 -0
  141. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/vscode.py +0 -0
  142. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/agent_metadata.py +0 -0
  143. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/agent_status.py +0 -0
  144. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  145. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  146. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  147. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  148. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  149. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/auto_update.py +0 -0
  150. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/backup.py +0 -0
  151. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/brief.py +0 -0
  152. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/candidate_reviewer.py +0 -0
  153. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/config.py +0 -0
  154. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/config_io.py +0 -0
  155. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/config_watcher.py +0 -0
  156. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/curator_snapshots.py +0 -0
  157. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/daemon_liveness.py +0 -0
  158. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/daemon_state.py +0 -0
  159. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/daemon_supervisor.py +0 -0
  160. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/db.py +0 -0
  161. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/dialectic_miner.py +0 -0
  162. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/dialectic_validator.py +0 -0
  163. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/egress.py +0 -0
  164. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/elicitation.py +0 -0
  165. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/embeddings.py +0 -0
  166. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/eval/__init__.py +0 -0
  167. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/eval/__main__.py +0 -0
  168. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/eval/harness.py +0 -0
  169. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/evolve_applier.py +0 -0
  170. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/evolve_daemon.py +0 -0
  171. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/extract_daemon.py +0 -0
  172. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/forget.py +0 -0
  173. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/github_budget.py +0 -0
  174. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/github_safety.py +0 -0
  175. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/harvest.py +0 -0
  176. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/helpers.py +0 -0
  177. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/host.py +0 -0
  178. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/host_embed.py +0 -0
  179. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/i18n.py +0 -0
  180. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/identity.py +0 -0
  181. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/ingest.py +0 -0
  182. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/lessons.py +0 -0
  183. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/memory_guard.py +0 -0
  184. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/menubar_app.py +0 -0
  185. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/migrate_embeddings.py +0 -0
  186. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/model_catalog.py +0 -0
  187. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/notify.py +0 -0
  188. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/nudges.py +0 -0
  189. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/permissions.py +0 -0
  190. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/probe_daemon.py +0 -0
  191. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/process_health.py +0 -0
  192. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/retention.py +0 -0
  193. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/retrieval.py +0 -0
  194. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/review_prompts.py +0 -0
  195. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/search_proxy.py +0 -0
  196. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/server.py +0 -0
  197. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/shadow_review.py +0 -0
  198. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/skill_updater.py +0 -0
  199. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/skill_watcher.py +0 -0
  200. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/spawn_budget.py +0 -0
  201. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/spawn_config.py +0 -0
  202. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/__init__.py +0 -0
  203. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/capture.py +0 -0
  204. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/daemon.py +0 -0
  205. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/identity.py +0 -0
  206. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/migrate.py +0 -0
  207. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/protocol.py +0 -0
  208. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/reset_node.py +0 -0
  209. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/server.py +0 -0
  210. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/task_spool.py +0 -0
  211. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/thread_janitor.py +0 -0
  212. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tool_schemas.py +0 -0
  213. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/__init__.py +0 -0
  214. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/agent_status.py +0 -0
  215. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/candidate_reviewer.py +0 -0
  216. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/concepts.py +0 -0
  217. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/config_watch.py +0 -0
  218. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/consolidate.py +0 -0
  219. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/core_memory.py +0 -0
  220. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/correlation.py +0 -0
  221. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/curator.py +0 -0
  222. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dashboard.py +0 -0
  223. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/db_maintenance.py +0 -0
  224. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic.py +0 -0
  225. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic_feed.py +0 -0
  226. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dialog.py +0 -0
  227. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/distill.py +0 -0
  228. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/evolve_applier.py +0 -0
  229. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/extract.py +0 -0
  230. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/forget.py +0 -0
  231. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/graph.py +0 -0
  232. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/invariants.py +0 -0
  233. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/memory_guard.py +0 -0
  234. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/missed_spawns.py +0 -0
  235. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/panel.py +0 -0
  236. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/peers.py +0 -0
  237. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/pickup.py +0 -0
  238. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/probes.py +0 -0
  239. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/process_health.py +0 -0
  240. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/prompts.py +0 -0
  241. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/resources.py +0 -0
  242. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/session.py +0 -0
  243. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/shadow_review.py +0 -0
  244. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/style.py +0 -0
  245. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/sync.py +0 -0
  246. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/threads.py +0 -0
  247. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/validate.py +0 -0
  248. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/trash.py +0 -0
  249. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/verify_ingest.py +0 -0
  250. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/wikilinks.py +0 -0
  251. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/dependency_links.txt +0 -0
  252. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/entry_points.txt +0 -0
  253. {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/requires.txt +0 -0
  254. {threadkeeper-0.17.3 → 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.3
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
@@ -310,6 +310,12 @@ uses a SQLite `BEGIN IMMEDIATE` reservation: `spawn()` re-checks the budget and
310
310
  inserts the child task row with its RSS estimate before `Popen`, so two
311
311
  concurrent spawns cannot both squeeze through the cap.
312
312
 
313
+ When `cwd` is inside a Git checkout, `spawn()` also requires the source
314
+ checkout's tracked files to be clean, then starts the child from a unique
315
+ branch/worktree under `THREADKEEPER_TASK_LOG_DIR/worktrees/`. Parallel children
316
+ therefore never share a mutable checkout or Git index. Non-Git directories keep
317
+ their existing behavior; a dirty Git checkout is refused before a child starts.
318
+
313
319
  The spawn wrapper also records each completed child's `duration_s`,
314
320
  `tokens_in`, `tokens_out`, `tokens_total`, and `cost_usd` when the underlying
315
321
  CLI emits a recognizable usage trailer. Optional daily ceilings
@@ -605,7 +611,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
605
611
  though an existing same-slug long lesson may be corrected without increasing
606
612
  its body size; near-duplicate slugs are blocked, and semantic body matches are
607
613
  routed to the incumbent lesson or surfaced for curation instead of minting a
608
- 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.
609
617
 
610
618
  #### 3. Extract daemon
611
619
 
@@ -710,6 +718,12 @@ non-forced direct `curator_review()` calls return `not_due` inside the
710
718
  configured interval and record that status without spawning. A manual
711
719
  `curator_review(force=True)` bypasses the interval but still respects the lock.
712
720
 
721
+ For automation-created skills, the audit keeps foreground consultation separate
722
+ from maintenance: a background-review skill with `fg_uses=0` after 14 days is
723
+ still a false-positive prune candidate even if automatic review or sync loops
724
+ have increased its patch counter. Patches are maintenance activity, not proof
725
+ that a foreground user or agent consulted the skill.
726
+
713
727
  Before spawning, the scheduler hashes lessons, concepts, skill bodies, support
714
728
  trees, validators, and mirror state. Repeated manual calls over identical bytes
715
729
  return `unchanged_inventory`; the scheduled three-day pass still runs because
@@ -1549,8 +1563,9 @@ Spawn task spool files live in `THREADKEEPER_TASK_LOG_DIR` (default
1549
1563
  the hardened `~/.threadkeeper` perimeter by default; explicit overrides are
1550
1564
  refused when the configured directory is a symlink or is not owned by the
1551
1565
  current user. `spawn()` creates captured headless `.log`, stdin prompt spool,
1552
- and visible `.command` files with no-follow owner-only opens. `consolidate()`
1553
- garbage-collects task spool files once their task row is no longer retained.
1566
+ and visible `.command` files with no-follow owner-only opens. Git-backed spawns
1567
+ also create their isolated worktrees there. `consolidate()` garbage-collects
1568
+ task spool files once their task row is no longer retained.
1554
1569
 
1555
1570
  ---
1556
1571
 
@@ -266,6 +266,12 @@ uses a SQLite `BEGIN IMMEDIATE` reservation: `spawn()` re-checks the budget and
266
266
  inserts the child task row with its RSS estimate before `Popen`, so two
267
267
  concurrent spawns cannot both squeeze through the cap.
268
268
 
269
+ When `cwd` is inside a Git checkout, `spawn()` also requires the source
270
+ checkout's tracked files to be clean, then starts the child from a unique
271
+ branch/worktree under `THREADKEEPER_TASK_LOG_DIR/worktrees/`. Parallel children
272
+ therefore never share a mutable checkout or Git index. Non-Git directories keep
273
+ their existing behavior; a dirty Git checkout is refused before a child starts.
274
+
269
275
  The spawn wrapper also records each completed child's `duration_s`,
270
276
  `tokens_in`, `tokens_out`, `tokens_total`, and `cost_usd` when the underlying
271
277
  CLI emits a recognizable usage trailer. Optional daily ceilings
@@ -561,7 +567,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
561
567
  though an existing same-slug long lesson may be corrected without increasing
562
568
  its body size; near-duplicate slugs are blocked, and semantic body matches are
563
569
  routed to the incumbent lesson or surfaced for curation instead of minting a
564
- 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.
565
573
 
566
574
  #### 3. Extract daemon
567
575
 
@@ -666,6 +674,12 @@ non-forced direct `curator_review()` calls return `not_due` inside the
666
674
  configured interval and record that status without spawning. A manual
667
675
  `curator_review(force=True)` bypasses the interval but still respects the lock.
668
676
 
677
+ For automation-created skills, the audit keeps foreground consultation separate
678
+ from maintenance: a background-review skill with `fg_uses=0` after 14 days is
679
+ still a false-positive prune candidate even if automatic review or sync loops
680
+ have increased its patch counter. Patches are maintenance activity, not proof
681
+ that a foreground user or agent consulted the skill.
682
+
669
683
  Before spawning, the scheduler hashes lessons, concepts, skill bodies, support
670
684
  trees, validators, and mirror state. Repeated manual calls over identical bytes
671
685
  return `unchanged_inventory`; the scheduled three-day pass still runs because
@@ -1505,8 +1519,9 @@ Spawn task spool files live in `THREADKEEPER_TASK_LOG_DIR` (default
1505
1519
  the hardened `~/.threadkeeper` perimeter by default; explicit overrides are
1506
1520
  refused when the configured directory is a symlink or is not owned by the
1507
1521
  current user. `spawn()` creates captured headless `.log`, stdin prompt spool,
1508
- and visible `.command` files with no-follow owner-only opens. `consolidate()`
1509
- garbage-collects task spool files once their task row is no longer retained.
1522
+ and visible `.command` files with no-follow owner-only opens. Git-backed spawns
1523
+ also create their isolated worktrees there. `consolidate()` garbage-collects
1524
+ task spool files once their task row is no longer retained.
1510
1525
 
1511
1526
  ---
1512
1527
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.17.3"
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
 
@@ -843,6 +845,48 @@ def test_skill_validator_is_exhaustive_and_semantic(tmp_path, monkeypatch):
843
845
  assert "3. SKILL second-skill" in checklist
844
846
 
845
847
 
848
+ def test_curator_prune_rubric_keeps_patched_unconsulted_skill_eligible(
849
+ tmp_path, monkeypatch,
850
+ ):
851
+ pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
852
+ pkg["lessons"].append_lesson(title="one", body="b1", source="shadow")
853
+ pkg["lessons"].append_lesson(title="two", body="b2", source="shadow")
854
+ _write_audit_skill(
855
+ pkg["skills_dir"], "patched-background-skill", "# Rule\nDo the test.",
856
+ )
857
+ now = int(time.time())
858
+ conn = pkg["db"].get_db()
859
+ conn.execute(
860
+ "INSERT INTO skill_usage "
861
+ "(name, created_at, created_by_origin, last_patched_at, patch_count, "
862
+ "foreground_use_count, state) VALUES (?, ?, ?, ?, ?, ?, ?)",
863
+ (
864
+ "patched-background-skill", now - 30 * 86400,
865
+ "background_review", now, 4, 0, "active",
866
+ ),
867
+ )
868
+ conn.commit()
869
+
870
+ import threadkeeper.tools.spawn as spawn_mod
871
+ captured: list[dict] = []
872
+
873
+ def fake_spawn(**kwargs):
874
+ captured.append(kwargs)
875
+ return "spawn task_id=patched-background-skill pid=0"
876
+
877
+ monkeypatch.setattr(spawn_mod, "spawn", fake_spawn)
878
+ pkg["curator"].run_curator_pass(force=True)
879
+
880
+ prompt = captured[0]["prompt"]
881
+ assert "origin=background_review AND fg_uses=0" in prompt
882
+ assert "they are not foreground consultation" in prompt
883
+ assert "SKILL patched-background-skill" in prompt
884
+ assert "fg_uses=0" in prompt
885
+ assert "maintenance_patches=4" in prompt
886
+ assert "created=30d_ago" in prompt
887
+ assert "patches=0" not in prompt
888
+
889
+
846
890
  def test_skill_validate_tool_returns_post_change_contract(tmp_path, monkeypatch):
847
891
  pkg = _bootstrap(tmp_path, monkeypatch)
848
892
  primary = pkg["skills_dir"]
@@ -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")
@@ -171,7 +171,7 @@ def test_visible_command_script_is_owner_only(mp_with_cid, monkeypatch):
171
171
  monkeypatch.setattr(spawn_mod.subprocess, "Popen", _FakePopen)
172
172
 
173
173
  spawn_fn = pkg["mcp"]._tool_manager._tools["spawn"].fn
174
- out = spawn_fn(prompt="do a thing", visible=True)
174
+ out = spawn_fn(prompt="do a thing", cwd=str(pkg["tmp"]), visible=True)
175
175
  assert out.startswith("ok task="), out
176
176
 
177
177
  cmd_files = list(spawn_mod.TASK_LOG_DIR.glob("*.command"))
@@ -197,7 +197,12 @@ def test_headless_log_file_is_owner_only(mp_with_cid, monkeypatch):
197
197
  monkeypatch.setattr(spawn_mod.subprocess, "Popen", _FakePopen)
198
198
 
199
199
  spawn_fn = pkg["mcp"]._tool_manager._tools["spawn"].fn
200
- out = spawn_fn(prompt="do a thing", visible=False, capture_output=True)
200
+ out = spawn_fn(
201
+ prompt="do a thing",
202
+ cwd=str(pkg["tmp"]),
203
+ visible=False,
204
+ capture_output=True,
205
+ )
201
206
  assert out.startswith("ok task="), out
202
207
 
203
208
  log_files = list(spawn_mod.TASK_LOG_DIR.glob("*.log"))
@@ -0,0 +1,112 @@
1
+ """Git worktree isolation on the shared spawn path."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+
8
+ _FAKE_CID = "aaaa0000-bbbb-1111-cccc-222233334444"
9
+
10
+
11
+ def _git(repo: Path, *args: str) -> str:
12
+ proc = subprocess.run(
13
+ ["git", "-C", str(repo), *args],
14
+ text=True,
15
+ capture_output=True,
16
+ check=True,
17
+ )
18
+ return proc.stdout.strip()
19
+
20
+
21
+ def _init_repo(tmp_path: Path) -> Path:
22
+ repo = tmp_path / "repo"
23
+ repo.mkdir()
24
+ _git(repo, "init", "-b", "main")
25
+ _git(repo, "config", "user.email", "test@example.com")
26
+ _git(repo, "config", "user.name", "Test User")
27
+ (repo / "tracked.txt").write_text("base\n", encoding="utf-8")
28
+ _git(repo, "add", "tracked.txt")
29
+ _git(repo, "commit", "-m", "base")
30
+ return repo
31
+
32
+
33
+ def _configure_spawn(monkeypatch, pkg):
34
+ import threadkeeper.identity as identity
35
+ import threadkeeper.spawn_config as spawn_config
36
+ import threadkeeper.tools.spawn as spawn_mod
37
+
38
+ monkeypatch.setattr(spawn_mod, "_claude_bin", lambda: "/bin/true")
39
+ monkeypatch.setattr(identity, "_active_cli", "claude")
40
+ monkeypatch.setattr(
41
+ spawn_config, "resolve_agent", lambda role, active_cli=None: "claude"
42
+ )
43
+ monkeypatch.setattr(spawn_config, "resolve_model", lambda cli, role="": "")
44
+ return spawn_mod
45
+
46
+
47
+ def test_git_spawns_get_distinct_branch_worktrees(mp_with_cid, monkeypatch):
48
+ pkg = mp_with_cid(_FAKE_CID)
49
+ repo = _init_repo(pkg["tmp"])
50
+ spawn_mod = _configure_spawn(monkeypatch, pkg)
51
+ child_cwds: list[Path] = []
52
+
53
+ class _FakePopen:
54
+ def __init__(self, _args, **kwargs):
55
+ child_cwds.append(Path(kwargs["cwd"]))
56
+ self.pid = 4200 + len(child_cwds)
57
+
58
+ monkeypatch.setattr(spawn_mod.subprocess, "Popen", _FakePopen)
59
+
60
+ first = spawn_mod.spawn(
61
+ prompt="first child", cwd=str(repo), visible=False, capture_output=False
62
+ )
63
+ second = spawn_mod.spawn(
64
+ prompt="second child", cwd=str(repo), visible=False, capture_output=False
65
+ )
66
+
67
+ assert first.startswith("ok task="), first
68
+ assert second.startswith("ok task="), second
69
+ assert len(child_cwds) == 2
70
+ assert child_cwds[0] != child_cwds[1]
71
+ assert all(path != repo and path.is_dir() for path in child_cwds)
72
+ roots = [
73
+ spawn_mod._git_result(["rev-parse", "--show-toplevel"], path)[0]
74
+ for path in child_cwds
75
+ ]
76
+ assert roots == [str(path) for path in child_cwds]
77
+ branches = [
78
+ spawn_mod._git_result(["branch", "--show-current"], path)[0]
79
+ for path in child_cwds
80
+ ]
81
+ assert len(set(branches)) == 2
82
+ assert all(branch.startswith("threadkeeper/spawn-tk_") for branch in branches)
83
+ assert spawn_mod._git_result(
84
+ ["status", "--porcelain", "--untracked-files=no"], repo
85
+ )[0] == ""
86
+
87
+
88
+ def test_dirty_git_checkout_is_refused_before_child_launch(
89
+ mp_with_cid, monkeypatch,
90
+ ):
91
+ pkg = mp_with_cid(_FAKE_CID)
92
+ repo = _init_repo(pkg["tmp"])
93
+ spawn_mod = _configure_spawn(monkeypatch, pkg)
94
+ (repo / "tracked.txt").write_text("uncommitted\n", encoding="utf-8")
95
+
96
+ def _unexpected_launch(*_args, **_kwargs):
97
+ raise AssertionError("dirty checkout must be rejected before Popen")
98
+
99
+ monkeypatch.setattr(spawn_mod.subprocess, "Popen", _unexpected_launch)
100
+
101
+ out = spawn_mod.spawn(
102
+ prompt="must not launch", cwd=str(repo), visible=False, capture_output=False
103
+ )
104
+
105
+ assert out == "ERR spawn_dirty_worktree mode=git"
106
+ worktrees, err = spawn_mod._git_result(
107
+ ["worktree", "list", "--porcelain"], repo
108
+ )
109
+ assert not err
110
+ assert worktrees.count("worktree ") == 1
111
+ conn = pkg["db"].get_db()
112
+ assert conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 0
@@ -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
  }
@@ -248,10 +248,11 @@ promotion and linking plan in REPORT.md for a foreground human instead.
248
248
  superseded by a newer entry, or a **FALSE POSITIVE** (auto-created
249
249
  by the background-review loop but never validated by actual use).
250
250
  Specifically flag as PRUNE:
251
- • origin=background_review AND use_count=0 AND patches=0 AND
252
- created >14 days ago → strong false-positive signal: nobody ever
253
- consulted it, and the agent that created it never came back to
254
- refine it.
251
+ • origin=background_review AND fg_uses=0 AND created >14 days ago
252
+ → strong false-positive signal: no foreground user or agent ever
253
+ consulted it. `maintenance_patches` count automated or other maintenance
254
+ writes; they are not foreground consultation and never reset this
255
+ eligibility signal.
255
256
  • SKILL_OUTCOME signals (in the events table) marking the skill
256
257
  as 'wrong' more often than 'helped' → user-judgment override.
257
258
  Format:
@@ -18,6 +18,7 @@ import os
18
18
  from pathlib import Path
19
19
  import re
20
20
  import sqlite3
21
+ import time
21
22
  from typing import Iterable
22
23
 
23
24
  import yaml
@@ -245,7 +246,8 @@ def _telemetry_rows(conn: sqlite3.Connection, include_archived: bool) -> dict[st
245
246
  rows = conn.execute(
246
247
  "SELECT name, created_at, created_by_origin, last_used_at, "
247
248
  "last_viewed_at, last_patched_at, use_count, view_count, "
248
- "patch_count, pinned, state, tier FROM skill_usage" + where
249
+ "patch_count, foreground_use_count, pinned, state, tier "
250
+ "FROM skill_usage" + where
249
251
  + " ORDER BY name"
250
252
  ).fetchall()
251
253
  except sqlite3.OperationalError:
@@ -327,7 +329,7 @@ def _record_for(
327
329
  for key in (
328
330
  "created_at", "last_used_at", "last_viewed_at",
329
331
  "last_patched_at", "use_count", "view_count", "patch_count",
330
- "pinned",
332
+ "foreground_use_count", "pinned",
331
333
  )
332
334
  },
333
335
  "source_path": str(source) if source else None,
@@ -533,9 +535,20 @@ def format_skill_checklist(manifest: dict) -> str:
533
535
  for index, record in enumerate(manifest["skills"], start=1):
534
536
  flags = ",".join(record["findings"]) or "clean"
535
537
  protected = " [PROTECTED]" if record["protected"] else ""
538
+ telemetry = record["telemetry"]
539
+ created_at = int(telemetry.get("created_at") or 0)
540
+ created_age = (
541
+ max(0, int(time.time()) - created_at) // 86400
542
+ if created_at else "?"
543
+ )
536
544
  lines.append(
537
545
  f"{index}. SKILL {record['name']}{protected} "
538
546
  f"state={record['state']} origin={record['origin']} "
547
+ f"fg_uses={telemetry.get('foreground_use_count') or 0} "
548
+ f"uses={telemetry.get('use_count') or 0} "
549
+ f"views={telemetry.get('view_count') or 0} "
550
+ f"maintenance_patches={telemetry.get('patch_count') or 0} "
551
+ f"created={created_age}d_ago "
539
552
  f"source_kind={record['source_kind']} "
540
553
  f"source={record['source_path'] or '-'} findings={flags}"
541
554
  )