threadkeeper 0.23.0__tar.gz → 0.24.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 (336) hide show
  1. {threadkeeper-0.23.0/threadkeeper.egg-info → threadkeeper-0.24.0}/PKG-INFO +18 -4
  2. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/README.md +17 -3
  3. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/pyproject.toml +1 -1
  4. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_curator.py +103 -6
  5. threadkeeper-0.24.0/tests/test_curator_audit.py +164 -0
  6. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/curator.py +171 -27
  7. threadkeeper-0.24.0/threadkeeper/curator_audit.py +212 -0
  8. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/evolve_applier.py +18 -2
  9. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/curator.py +5 -1
  10. {threadkeeper-0.23.0 → threadkeeper-0.24.0/threadkeeper.egg-info}/PKG-INFO +18 -4
  11. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/SOURCES.txt +2 -0
  12. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/LICENSE +0 -0
  13. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/setup.cfg +0 -0
  14. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_action_outcomes.py +0 -0
  15. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_adapters.py +0 -0
  16. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_agent_status.py +0 -0
  17. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_antigravity_ingest.py +0 -0
  18. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_auto_update.py +0 -0
  19. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_backup.py +0 -0
  20. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_brief_footprint.py +0 -0
  21. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_brief_hook_size.py +0 -0
  22. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_brief_sections.py +0 -0
  23. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_candidate_reviewer.py +0 -0
  24. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_child_memory_access.py +0 -0
  25. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ci_parallel.py +0 -0
  26. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_concepts.py +0 -0
  27. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_config_settings.py +0 -0
  28. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_config_watcher.py +0 -0
  29. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_consequential_state.py +0 -0
  30. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_core_memory.py +0 -0
  31. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_host_config.py +0 -0
  32. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_host_integration.py +0 -0
  33. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_liveness.py +0 -0
  34. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_state.py +0 -0
  35. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dashboard.py +0 -0
  36. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dashboard_app.py +0 -0
  37. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_compact.py +0 -0
  38. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_contention.py +0 -0
  39. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_leak_guard.py +0 -0
  40. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_migration_fts.py +0 -0
  41. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_runtime.py +0 -0
  42. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_schema.py +0 -0
  43. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_delegated_search.py +0 -0
  44. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic.py +0 -0
  45. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_drift_eval.py +0 -0
  46. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_feed_tools.py +0 -0
  47. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_miner.py +0 -0
  48. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_observation_resolve.py +0 -0
  49. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_recompute.py +0 -0
  50. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_tier.py +0 -0
  51. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_validator.py +0 -0
  52. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialog_fts_external.py +0 -0
  53. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_documentation_inventory.py +0 -0
  54. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_egress_policy.py +0 -0
  55. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embed_routing.py +0 -0
  56. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embedding_generation.py +0 -0
  57. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embedding_model_revision.py +0 -0
  58. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embedding_storage_dedup.py +0 -0
  59. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_error_paths.py +0 -0
  60. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_eval_harness.py +0 -0
  61. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evidence_resources.py +0 -0
  62. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_applier.py +0 -0
  63. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_2.py +0 -0
  64. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_3.py +0 -0
  65. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_43.py +0 -0
  66. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_45.py +0 -0
  67. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_48.py +0 -0
  68. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_daemon.py +0 -0
  69. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_extract_daemon.py +0 -0
  70. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_extract_dedup.py +0 -0
  71. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_forget.py +0 -0
  72. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_github_budget.py +0 -0
  73. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_github_safety.py +0 -0
  74. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_helpers.py +0 -0
  75. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_hook_child_guard.py +0 -0
  76. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_embed.py +0 -0
  77. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_env.py +0 -0
  78. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_main.py +0 -0
  79. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_wedged.py +0 -0
  80. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_hybrid_retrieval.py +0 -0
  81. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_i18n_multilang.py +0 -0
  82. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_identity.py +0 -0
  83. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_cursor.py +0 -0
  84. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_denylist.py +0 -0
  85. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_secret_redaction.py +0 -0
  86. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_status.py +0 -0
  87. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_transactions.py +0 -0
  88. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lesson_freshness.py +0 -0
  89. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lesson_index.py +0 -0
  90. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lesson_violations.py +0 -0
  91. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lessons.py +0 -0
  92. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_conformance_gate.py +0 -0
  93. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_protocol_conformance.py +0 -0
  94. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_resources_prompts.py +0 -0
  95. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_sdk_compat.py +0 -0
  96. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_server_instructions.py +0 -0
  97. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_skills_extension.py +0 -0
  98. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_tasks.py +0 -0
  99. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_authority_lifecycle.py +0 -0
  100. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_compatibility.py +0 -0
  101. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_eval.py +0 -0
  102. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_guard.py +0 -0
  103. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_guard_host.py +0 -0
  104. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_menubar_app.py +0 -0
  105. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_missed_spawns.py +0 -0
  106. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_catalog.py +0 -0
  107. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_pricing.py +0 -0
  108. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_router.py +0 -0
  109. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_router_digest.py +0 -0
  110. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_router_tools.py +0 -0
  111. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_routes.py +0 -0
  112. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_notify_daemon.py +0 -0
  113. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_nudges.py +0 -0
  114. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_onnx_embeddings.py +0 -0
  115. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_panel.py +0 -0
  116. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_permissions.py +0 -0
  117. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_pickup.py +0 -0
  118. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_probe_daemon.py +0 -0
  119. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_process_health.py +0 -0
  120. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_project_scope.py +0 -0
  121. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_release_workflows.py +0 -0
  122. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_retention.py +0 -0
  123. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_review_prompts.py +0 -0
  124. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_search_fts_punctuation.py +0 -0
  125. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_semantic_bundle.py +0 -0
  126. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_session_read_purity.py +0 -0
  127. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_setup.py +0 -0
  128. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_shadow_review.py +0 -0
  129. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
  130. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_fixtures.py +0 -0
  131. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_hint.py +0 -0
  132. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_passive_tier.py +0 -0
  133. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_tier.py +0 -0
  134. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_updater.py +0 -0
  135. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_use_parser.py +0 -0
  136. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_watcher.py +0 -0
  137. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skills.py +0 -0
  138. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_background_cwd.py +0 -0
  139. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_budget.py +0 -0
  140. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_child_env.py +0 -0
  141. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_codex_stdin.py +0 -0
  142. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_config.py +0 -0
  143. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_hint.py +0 -0
  144. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_permission_gate.py +0 -0
  145. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_reap.py +0 -0
  146. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_result.py +0 -0
  147. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_slim.py +0 -0
  148. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_watchdog.py +0 -0
  149. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_worktree_isolation.py +0 -0
  150. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_wrap.py +0 -0
  151. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_storage.py +0 -0
  152. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_capture.py +0 -0
  153. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_identity.py +0 -0
  154. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_migrate.py +0 -0
  155. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_protocol.py +0 -0
  156. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_reset_node.py +0 -0
  157. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_transport.py +0 -0
  158. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_gate.py +0 -0
  159. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_retention.py +0 -0
  160. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_spool.py +0 -0
  161. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_worktree_retention.py +0 -0
  162. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_thin_session.py +0 -0
  163. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_thread_janitor.py +0 -0
  164. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_threads.py +0 -0
  165. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tool_annotations.py +0 -0
  166. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tool_profiles.py +0 -0
  167. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tools_smoke.py +0 -0
  168. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tracing.py +0 -0
  169. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_validate_threads.py +0 -0
  170. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_vec_search.py +0 -0
  171. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_verify_ingest.py +0 -0
  172. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_workflow_retention.py +0 -0
  173. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/__init__.py +0 -0
  174. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/_mcp.py +0 -0
  175. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/_setup.py +0 -0
  176. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/_spawn_wrap.py +0 -0
  177. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/action_outcomes.py +0 -0
  178. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/__init__.py +0 -0
  179. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  180. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/antigravity.py +0 -0
  181. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/base.py +0 -0
  182. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/claude_code.py +0 -0
  183. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  184. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/codex.py +0 -0
  185. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/copilot.py +0 -0
  186. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/vscode.py +0 -0
  187. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/agent_metadata.py +0 -0
  188. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/agent_status.py +0 -0
  189. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  190. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  191. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  192. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  193. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  194. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/authority.py +0 -0
  195. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/auto_update.py +0 -0
  196. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/backup.py +0 -0
  197. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/brief.py +0 -0
  198. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/candidate_reviewer.py +0 -0
  199. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/config.py +0 -0
  200. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/config_io.py +0 -0
  201. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/config_watcher.py +0 -0
  202. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/conformance_server.py +0 -0
  203. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/consequential_state.py +0 -0
  204. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/curator_snapshots.py +0 -0
  205. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/daemon_liveness.py +0 -0
  206. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/daemon_state.py +0 -0
  207. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/daemon_supervisor.py +0 -0
  208. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dashboard_app.py +0 -0
  209. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/db.py +0 -0
  210. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dialectic_drift_eval.py +0 -0
  211. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dialectic_miner.py +0 -0
  212. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dialectic_validator.py +0 -0
  213. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/egress.py +0 -0
  214. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/elicitation.py +0 -0
  215. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/embeddings.py +0 -0
  216. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/eval/__init__.py +0 -0
  217. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/eval/__main__.py +0 -0
  218. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/eval/harness.py +0 -0
  219. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/evidence.py +0 -0
  220. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/evolve_daemon.py +0 -0
  221. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/extract_daemon.py +0 -0
  222. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/forget.py +0 -0
  223. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/github_budget.py +0 -0
  224. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/github_safety.py +0 -0
  225. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/harvest.py +0 -0
  226. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/helpers.py +0 -0
  227. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/host.py +0 -0
  228. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/host_embed.py +0 -0
  229. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/i18n.py +0 -0
  230. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/identity.py +0 -0
  231. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/ingest.py +0 -0
  232. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/instructions.py +0 -0
  233. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/lesson_freshness.py +0 -0
  234. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/lesson_violations.py +0 -0
  235. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/lessons.py +0 -0
  236. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/link_health.py +0 -0
  237. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/mcp_skills.py +0 -0
  238. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/mcp_tasks.py +0 -0
  239. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/memory_access.py +0 -0
  240. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/memory_compat.py +0 -0
  241. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/memory_guard.py +0 -0
  242. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/menubar_app.py +0 -0
  243. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/migrate_embeddings.py +0 -0
  244. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_catalog.py +0 -0
  245. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_pricing.py +0 -0
  246. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_router.py +0 -0
  247. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_router_digest.py +0 -0
  248. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_routes.py +0 -0
  249. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/notify.py +0 -0
  250. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/nudges.py +0 -0
  251. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/permissions.py +0 -0
  252. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/probe_daemon.py +0 -0
  253. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/process_health.py +0 -0
  254. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/project_scope.py +0 -0
  255. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/protocol.py +0 -0
  256. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/read_context.py +0 -0
  257. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/retention.py +0 -0
  258. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/retrieval.py +0 -0
  259. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/review_prompts.py +0 -0
  260. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/search_proxy.py +0 -0
  261. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/semantic_bundle.py +0 -0
  262. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/server.py +0 -0
  263. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/shadow_review.py +0 -0
  264. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_audit.py +0 -0
  265. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_lifecycle.py +0 -0
  266. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_updater.py +0 -0
  267. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_watcher.py +0 -0
  268. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/spawn_budget.py +0 -0
  269. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/spawn_config.py +0 -0
  270. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/spawn_result.py +0 -0
  271. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/storage.py +0 -0
  272. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/__init__.py +0 -0
  273. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/capture.py +0 -0
  274. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/daemon.py +0 -0
  275. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/identity.py +0 -0
  276. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/migrate.py +0 -0
  277. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/protocol.py +0 -0
  278. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/reset_node.py +0 -0
  279. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/server.py +0 -0
  280. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/task_spool.py +0 -0
  281. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/task_worktrees.py +0 -0
  282. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/thread_janitor.py +0 -0
  283. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tool_profiles.py +0 -0
  284. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tool_schemas.py +0 -0
  285. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/__init__.py +0 -0
  286. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/agent_status.py +0 -0
  287. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/authority.py +0 -0
  288. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  289. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/capabilities.py +0 -0
  290. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/concepts.py +0 -0
  291. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/config_watch.py +0 -0
  292. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/consolidate.py +0 -0
  293. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/core_memory.py +0 -0
  294. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/correlation.py +0 -0
  295. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dashboard.py +0 -0
  296. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/db_maintenance.py +0 -0
  297. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dialectic.py +0 -0
  298. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  299. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dialog.py +0 -0
  300. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/distill.py +0 -0
  301. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/evolve_applier.py +0 -0
  302. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/evolve_research.py +0 -0
  303. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/extract.py +0 -0
  304. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/forget.py +0 -0
  305. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/graph.py +0 -0
  306. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/invariants.py +0 -0
  307. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/lessons.py +0 -0
  308. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/memory_guard.py +0 -0
  309. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/memory_proposals.py +0 -0
  310. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/missed_spawns.py +0 -0
  311. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/model_router.py +0 -0
  312. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/panel.py +0 -0
  313. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/peers.py +0 -0
  314. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/pickup.py +0 -0
  315. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/probes.py +0 -0
  316. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/process_health.py +0 -0
  317. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/project_scope.py +0 -0
  318. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/prompts.py +0 -0
  319. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/resources.py +0 -0
  320. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/session.py +0 -0
  321. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/shadow_review.py +0 -0
  322. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/skills.py +0 -0
  323. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/spawn.py +0 -0
  324. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/style.py +0 -0
  325. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/sync.py +0 -0
  326. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/threads.py +0 -0
  327. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/validate.py +0 -0
  328. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tracing.py +0 -0
  329. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/trash.py +0 -0
  330. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/verify_ingest.py +0 -0
  331. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/wikilinks.py +0 -0
  332. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/workflow_artifacts.py +0 -0
  333. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  334. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/entry_points.txt +0 -0
  335. {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/requires.txt +0 -0
  336. {threadkeeper-0.23.0 → threadkeeper-0.24.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.23.0
3
+ Version: 0.24.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
@@ -1041,8 +1041,13 @@ trees, validators, and mirror state. It persists the full pass audit manifest
1041
1041
  for parent coordination and provenance, then writes one smaller, hash-checked
1042
1042
  audit slice for every frozen batch. A worker reads only its slice: assigned
1043
1043
  skill records and their relevant consumer, link, mirror, source, duplicate, and
1044
- candidate findings. A lesson-only batch receives an explicit no-skills slice
1045
- instead of the full skill inventory. The scheduler stores the expected reports,
1044
+ candidate findings, plus integrity evidence for its assigned lessons and any
1045
+ promotion source lessons. A lesson-only batch receives its lesson evidence
1046
+ with an explicit no-skills marker. Curator audit schema 3 records exact and
1047
+ normalized lesson body hashes, content-addressed revisions, safe provenance,
1048
+ usage times, sorted link targets and unresolved links, and duplicate candidates.
1049
+ Lesson text and lesson source paths stay out of the manifest; free-form
1050
+ provenance is hashed. The scheduler stores the expected reports,
1046
1051
  slice hashes, and rendered text before launching any child. A dispatch is not
1047
1052
  completion: every batch must exit successfully and have a final, matching
1048
1053
  provenance record before the inventory is endorsed.
@@ -1056,7 +1061,16 @@ successfully empty source remains valid, while a failed one records
1056
1061
  creating a recovery snapshot, or launching a child.
1057
1062
 
1058
1063
  A pass keeps reviewing the batches it froze even when the live inventory
1059
- changes, and resuming it never re-reads the inventory. Failed and timed-out
1064
+ changes. Before evaluation and report writing, it checks the assigned lessons'
1065
+ live revisions. The prompt flags changed or removed lessons for DEFER, and the
1066
+ server adds a `CURATOR_AUDIT` header to each report with the frozen manifest
1067
+ schema version, SHA-256, reviewed revisions, and observed revision changes.
1068
+ Report writing and completion verify the saved manifest and batch slice;
1069
+ changed audit files cannot authorize a final report or endorsement. The frozen
1070
+ manifest stays unchanged, so a later pass can review the new lesson revision.
1071
+ Schema 1 and 2 skill-only manifests retain their original report contract;
1072
+ unsupported versions fail with `unsupported_curator_audit_version`.
1073
+ Resuming does not rebuild the full inventory. Failed and timed-out
1060
1074
  batches are retried without redispatching completed work, up to three launched
1061
1075
  attempts per batch; a spawn memory or spend-budget refusal leaves the batch
1062
1076
  waiting without using an attempt (a spend-cap refusal also alerts). A pass
@@ -994,8 +994,13 @@ trees, validators, and mirror state. It persists the full pass audit manifest
994
994
  for parent coordination and provenance, then writes one smaller, hash-checked
995
995
  audit slice for every frozen batch. A worker reads only its slice: assigned
996
996
  skill records and their relevant consumer, link, mirror, source, duplicate, and
997
- candidate findings. A lesson-only batch receives an explicit no-skills slice
998
- instead of the full skill inventory. The scheduler stores the expected reports,
997
+ candidate findings, plus integrity evidence for its assigned lessons and any
998
+ promotion source lessons. A lesson-only batch receives its lesson evidence
999
+ with an explicit no-skills marker. Curator audit schema 3 records exact and
1000
+ normalized lesson body hashes, content-addressed revisions, safe provenance,
1001
+ usage times, sorted link targets and unresolved links, and duplicate candidates.
1002
+ Lesson text and lesson source paths stay out of the manifest; free-form
1003
+ provenance is hashed. The scheduler stores the expected reports,
999
1004
  slice hashes, and rendered text before launching any child. A dispatch is not
1000
1005
  completion: every batch must exit successfully and have a final, matching
1001
1006
  provenance record before the inventory is endorsed.
@@ -1009,7 +1014,16 @@ successfully empty source remains valid, while a failed one records
1009
1014
  creating a recovery snapshot, or launching a child.
1010
1015
 
1011
1016
  A pass keeps reviewing the batches it froze even when the live inventory
1012
- changes, and resuming it never re-reads the inventory. Failed and timed-out
1017
+ changes. Before evaluation and report writing, it checks the assigned lessons'
1018
+ live revisions. The prompt flags changed or removed lessons for DEFER, and the
1019
+ server adds a `CURATOR_AUDIT` header to each report with the frozen manifest
1020
+ schema version, SHA-256, reviewed revisions, and observed revision changes.
1021
+ Report writing and completion verify the saved manifest and batch slice;
1022
+ changed audit files cannot authorize a final report or endorsement. The frozen
1023
+ manifest stays unchanged, so a later pass can review the new lesson revision.
1024
+ Schema 1 and 2 skill-only manifests retain their original report contract;
1025
+ unsupported versions fail with `unsupported_curator_audit_version`.
1026
+ Resuming does not rebuild the full inventory. Failed and timed-out
1013
1027
  batches are retried without redispatching completed work, up to three launched
1014
1028
  attempts per batch; a spawn memory or spend-budget refusal leaves the batch
1015
1029
  waiting without using an attempt (a spend-cap refusal also alerts). A pass
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.23.0"
7
+ version = "0.24.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" }]
@@ -136,7 +136,9 @@ def _complete_manifest_batch(pkg, *, pass_id: str | None = None) -> str:
136
136
  ).fetchone()
137
137
  report = pkg["reports_dir"] / batch["report_name"]
138
138
  report.parent.mkdir(parents=True, exist_ok=True)
139
- report.write_text("# report\n\nCURATOR_PASS_COMPLETE\n", encoding="utf-8")
139
+ header, error = pkg["curator"].curator_report_audit_header(conn, pass_id, batch["report_name"])
140
+ assert error is None
141
+ report.write_text(header + "# report\n\nCURATOR_PASS_COMPLETE\n", encoding="utf-8")
140
142
  digest = pkg["curator"].curator_report_sha256(report.read_text())
141
143
  now = int(time.time())
142
144
  conn.execute(
@@ -967,7 +969,7 @@ def test_run_curator_pass_spawns_when_threshold_met(tmp_path, monkeypatch):
967
969
  if not path.name.startswith("AUDIT-SLICE-")
968
970
  ]
969
971
  assert len(manifests) == 1
970
- assert json.loads(manifests[0].read_text())["schema_version"] == 2
972
+ assert json.loads(manifests[0].read_text())["schema_version"] == 3
971
973
  slices = list(pkg["reports_dir"].glob("AUDIT-SLICE-*.json"))
972
974
  assert len(slices) == 1
973
975
  assert f"AUDIT_MANIFEST_SLICE_PATH = {slices[0]}" in kw["prompt"]
@@ -1072,6 +1074,14 @@ def test_curator_reuses_verified_audit_slice_and_rejects_tampering(
1072
1074
  assert first.prompt == second.prompt
1073
1075
  assert slice_path.read_text() == original
1074
1076
 
1077
+ altered_batch = dict(batch_row)
1078
+ altered_batch["batch_text"] = batch_row["batch_text"].replace(
1079
+ "AUDIT_MANIFEST_SCHEMA_VERSION = 3", "AUDIT_MANIFEST_SCHEMA_VERSION = 2",
1080
+ )
1081
+ assert pkg["curator"]._evaluate_spec(
1082
+ conn, pass_row, altered_batch, "evaluate", 1,
1083
+ ) == "audit_batch_manifest_binding_mismatch"
1084
+
1075
1085
  slice_path.write_text("{}\n", encoding="utf-8")
1076
1086
  assert pkg["curator"]._evaluate_spec(
1077
1087
  conn, pass_row, batch_row, "evaluate", 1,
@@ -1119,6 +1129,91 @@ def test_worker_audit_input_scales_with_assigned_skill_records():
1119
1129
  assert worker_input_size < canonical_size * len(slices) // 2
1120
1130
 
1121
1131
 
1132
+ def test_lesson_changes_during_a_pass_are_in_prompt_and_final_report(tmp_path, monkeypatch):
1133
+ pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
1134
+ _two_shadow_lessons(pkg)
1135
+ _recording_spawn(monkeypatch)
1136
+ pkg["curator"].run_curator_pass(force=True)
1137
+ conn = pkg["db"].get_db()
1138
+ pass_row = conn.execute("SELECT * FROM curator_passes").fetchone()
1139
+ batch = conn.execute("SELECT * FROM curator_batches").fetchone()
1140
+ frozen_raw = Path(pass_row["audit_manifest_path"]).read_text()
1141
+ manifest = json.loads(frozen_raw)
1142
+ assert len(manifest["lesson_audit"]["lessons"]) == 2
1143
+ assert f"AUDIT_MANIFEST_SHA256 = {pass_row['audit_manifest_sha256']}" in batch["batch_text"]
1144
+ original = pkg["lessons_path"].read_text()
1145
+ pkg["lessons_path"].write_text(original.replace("body one", "updated body one"))
1146
+ spec = pkg["curator"]._evaluate_spec(conn, pass_row, batch, "evaluate", 1)
1147
+ assert '"lesson_revision_changes":[{"current_revision":' in spec.prompt
1148
+ assert '"slug":"lesson-one"' in spec.prompt
1149
+ assert "mark those lessons DEFER" in spec.prompt
1150
+
1151
+ from threadkeeper.tools import curator as tool_mod
1152
+ from threadkeeper._mcp import mcp
1153
+ from threadkeeper.curator_audit import REPORT_AUDIT_PREFIX
1154
+ monkeypatch.setattr(tool_mod, "WRITE_ORIGIN", "curator")
1155
+ monkeypatch.setattr(tool_mod, "SPAWNED_CHILD", True)
1156
+ monkeypatch.setenv("THREADKEEPER_CURATOR_PASS_ID", pass_row["pass_id"])
1157
+ _bind_curator_writer(pkg, pass_row["pass_id"])
1158
+ write = mcp._tool_manager._tools["curator_report_write"].fn
1159
+ assert write(content="Changed lesson: DEFER\nCURATOR_PASS_COMPLETE").startswith("ok path=")
1160
+ report = (pkg["reports_dir"] / batch["report_name"]).read_text()
1161
+ receipt = json.loads(report.splitlines()[0][len(REPORT_AUDIT_PREFIX):-4])
1162
+ assert receipt["schema_version"] == 3
1163
+ assert receipt["sha256"] == pass_row["audit_manifest_sha256"]
1164
+ assert [change["slug"] for change in receipt["lesson_revision_changes"]] == ["lesson-one"]
1165
+ assert pkg["curator"]._provenanced_report(
1166
+ conn, pass_row["pass_id"], batch["report_name"], "CURATOR_PASS_COMPLETE",
1167
+ ) is not None
1168
+ assert Path(pass_row["audit_manifest_path"]).read_text() == frozen_raw
1169
+
1170
+
1171
+ @pytest.mark.parametrize("state", ["running", "complete"])
1172
+ def test_changed_audit_file_blocks_report_write_and_completion(tmp_path, monkeypatch, state):
1173
+ pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
1174
+ _two_shadow_lessons(pkg)
1175
+ _recording_spawn(monkeypatch)
1176
+ pkg["curator"].run_curator_pass(force=True)
1177
+ conn = pkg["db"].get_db()
1178
+ pass_id = _complete_manifest_batch(pkg)
1179
+ pass_row = conn.execute("SELECT * FROM curator_passes").fetchone()
1180
+ batch = conn.execute("SELECT * FROM curator_batches").fetchone()
1181
+ conn.execute("UPDATE curator_batches SET state=?", (state,))
1182
+ conn.commit()
1183
+ Path(pass_row["audit_manifest_path"]).write_text("{}\n")
1184
+ header, error = pkg["curator"].curator_report_audit_header(conn, pass_id, batch["report_name"])
1185
+ assert header == ""
1186
+ assert error == "canonical_manifest_mismatch"
1187
+ from threadkeeper import evolve_applier
1188
+ report = pkg["reports_dir"] / batch["report_name"]
1189
+ assert not evolve_applier._curator_report_provenanced(conn, report, report.read_text())
1190
+ assert not pkg["curator"]._refresh_pass_completion(conn, pass_id, int(time.time()))
1191
+ assert conn.execute("SELECT endorsed_at FROM curator_passes").fetchone()[0] is None
1192
+
1193
+
1194
+ def test_report_provenance_requires_the_frozen_lesson_revision_receipt(tmp_path, monkeypatch):
1195
+ pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
1196
+ _two_shadow_lessons(pkg)
1197
+ _recording_spawn(monkeypatch)
1198
+ pkg["curator"].run_curator_pass(force=True)
1199
+ conn = pkg["db"].get_db()
1200
+ pass_id = _complete_manifest_batch(pkg)
1201
+ batch = conn.execute("SELECT * FROM curator_batches").fetchone()
1202
+ report = pkg["reports_dir"] / batch["report_name"]
1203
+ content = report.read_text().replace('"schema_version":3', '"schema_version":2', 1)
1204
+ report.write_text(content)
1205
+ digest = pkg["curator"].curator_report_sha256(content)
1206
+ conn.execute(
1207
+ "INSERT INTO events (session_id, kind, target, summary, created_at) VALUES (?, ?, ?, ?, ?)",
1208
+ (_FAKE_CID, pkg["curator"].CURATOR_REPORT_PROVENANCE_KIND, str(report.resolve()),
1209
+ f"pass_id={pass_id} sha256={digest}", int(time.time())),
1210
+ )
1211
+ conn.commit()
1212
+ assert pkg["curator"]._provenanced_report(
1213
+ conn, pass_id, batch["report_name"], "CURATOR_PASS_COMPLETE",
1214
+ ) is None
1215
+
1216
+
1122
1217
  def test_web_research_receives_only_public_brief_and_narrow_tools(
1123
1218
  tmp_path, monkeypatch,
1124
1219
  ):
@@ -2892,7 +2987,12 @@ def _finish_child(pkg, *, task_id, report_name, content, return_code=0):
2892
2987
  conn = pkg["db"].get_db()
2893
2988
  report = pkg["reports_dir"] / report_name
2894
2989
  report.parent.mkdir(parents=True, exist_ok=True)
2895
- report.write_text(content, encoding="utf-8")
2990
+ pass_id = conn.execute(
2991
+ "SELECT pass_id FROM curator_passes ORDER BY created_at DESC LIMIT 1"
2992
+ ).fetchone()["pass_id"]
2993
+ header, error = pkg["curator"].curator_report_audit_header(conn, pass_id, report_name)
2994
+ assert error is None
2995
+ report.write_text(header + content, encoding="utf-8")
2896
2996
  digest = pkg["curator"].curator_report_sha256(report.read_text())
2897
2997
  now = int(time.time())
2898
2998
  conn.execute(
@@ -2901,9 +3001,6 @@ def _finish_child(pkg, *, task_id, report_name, content, return_code=0):
2901
3001
  "VALUES (?, 0, '/tmp', 'curator', ?, ?, ?)",
2902
3002
  (task_id, now - 1, now, return_code),
2903
3003
  )
2904
- pass_id = conn.execute(
2905
- "SELECT pass_id FROM curator_passes ORDER BY created_at DESC LIMIT 1"
2906
- ).fetchone()["pass_id"]
2907
3004
  conn.execute(
2908
3005
  "INSERT INTO events (session_id, kind, target, summary, created_at) "
2909
3006
  "VALUES (?, ?, ?, ?, ?)",
@@ -0,0 +1,164 @@
1
+ """Lesson audit evidence is reproducible, private, and version bound."""
2
+ import copy
3
+ import hashlib
4
+ import json
5
+
6
+ import pytest
7
+
8
+
9
+ def _item(slug="alpha", body="Review release evidence before publishing.", **kwargs):
10
+ return {
11
+ "slug": slug, "body": body, "ts": 123, "source": "shadow",
12
+ "origin": "shadow_review", "usage": {}, **kwargs,
13
+ }
14
+
15
+
16
+ def _audit(items):
17
+ from threadkeeper.curator_audit import build_curator_audit
18
+ return build_curator_audit({"schema_version": 2, "skills": [{"name": "skill-target"}]}, items)
19
+
20
+
21
+ def _serialized(manifest):
22
+ from threadkeeper.skill_audit import serialized_skill_audit
23
+ return serialized_skill_audit(manifest)
24
+
25
+
26
+ def _slice(manifest, slugs=("alpha",)):
27
+ from threadkeeper.curator_audit import build_batch_audit_slice
28
+ return build_batch_audit_slice(
29
+ manifest, pass_id="pass", batch_index=1, batch_total=1,
30
+ inventory_fingerprint="f" * 64, canonical_manifest_sha256="a" * 64,
31
+ assigned_skill_names=[], assigned_lesson_slugs=slugs,
32
+ )
33
+
34
+
35
+ def test_lesson_hashes_and_manifest_serialization_are_deterministic():
36
+ from threadkeeper.lesson_freshness import lesson_revision
37
+ items = [_item("beta"), _item("alpha", usage={"last_used_at": 50, "last_viewed_at": 60})]
38
+ first = _audit(items)
39
+ second = _audit(list(reversed(items)))
40
+ assert _serialized(first) == _serialized(second)
41
+ assert first["schema_version"] == 3
42
+ records = first["lesson_audit"]["lessons"]
43
+ assert [record["slug"] for record in records] == ["alpha", "beta"]
44
+ assert records[0]["body_sha256"] == hashlib.sha256(items[1]["body"].encode()).hexdigest()
45
+ assert records[0]["revision"] == lesson_revision(items[1])
46
+ assert records[0]["timestamp"] == 123
47
+ assert records[0]["usage"]["last_used_at"] == 50
48
+ assert records[0]["usage"]["last_viewed_at"] == 60
49
+
50
+
51
+ def test_body_edit_changes_revision_and_manifest_digest_but_usage_does_not_change_revision():
52
+ before = _audit([_item()])
53
+ after = _audit([_item(body="Changed release rule.")])
54
+ digest = lambda manifest: hashlib.sha256(_serialized(manifest).encode()).hexdigest()
55
+ assert digest(before) != digest(after)
56
+ for key in ("body_sha256", "normalized_body_sha256", "revision"):
57
+ assert before["lesson_audit"]["lessons"][0][key] != after["lesson_audit"]["lessons"][0][key]
58
+ usage = _audit([_item(usage={"view_count": 10})])
59
+ assert before["lesson_audit"]["lessons"][0]["revision"] == usage["lesson_audit"]["lessons"][0]["revision"]
60
+
61
+
62
+ def test_links_include_adjacency_skill_targets_aliases_and_unresolved_targets():
63
+ audit = _audit([
64
+ _item("alpha", "[[beta]] [[beta|alias]] [[missing]] [[skill-target]] [[alpha]]"),
65
+ _item("beta", "[[zebra]] [[missing]]"),
66
+ ])["lesson_audit"]
67
+ alpha, beta = audit["lessons"]
68
+ assert alpha["outgoing_links"] == ["alpha", "beta", "missing", "skill-target"]
69
+ assert alpha["adjacent_lessons"] == ["beta"]
70
+ assert beta["adjacent_lessons"] == ["alpha"]
71
+ assert alpha["unresolved_links"] == ["missing"]
72
+ assert beta["unresolved_links"] == ["missing", "zebra"]
73
+
74
+
75
+ def test_exact_normalized_and_near_duplicate_groups_are_sorted_and_body_free():
76
+ audit = _audit([
77
+ _item("beta", "Check release evidence before publishing"),
78
+ _item("alpha", "Check release evidence before publishing"),
79
+ _item("gamma", "CHECK release evidence, before publishing!"),
80
+ _item("delta", "Check release evidence before publishing safely"),
81
+ ])["lesson_audit"]
82
+ assert audit["exact_duplicate_groups"][0]["lessons"] == ["alpha", "beta"]
83
+ assert audit["normalized_duplicate_groups"][0]["lessons"] == ["alpha", "beta", "gamma"]
84
+ assert any(pair["left"] == "alpha" and pair["right"] == "delta" for pair in audit["near_duplicate_candidates"])
85
+ assert all(set(pair) == {"left", "right", "token_jaccard", "status"} for pair in audit["near_duplicate_candidates"])
86
+
87
+
88
+ def test_protected_lesson_body_and_private_provenance_paths_never_enter_audit():
89
+ item = _item(
90
+ body="PRIVATE_BODY /Users/private/project.md C:\\private\\project [[skill-target]]",
91
+ source="/Users/private/source.md", origin="/private/origin",
92
+ usage={"pinned": 1, "source": "C:\\private\\source", "tier": "validated"},
93
+ )
94
+ manifest = _audit([item])
95
+ encoded = _serialized(manifest)
96
+ for private in ("PRIVATE_BODY", "/Users/private", "/private/origin", "C:\\private"):
97
+ assert private not in encoded
98
+ record = manifest["lesson_audit"]["lessons"][0]
99
+ assert record["protected"] is True
100
+ assert record["protection_reason"] == "pinned"
101
+ assert record["provenance"]["source_kind"] == "redacted"
102
+ assert record["provenance"]["origin"] == "unknown"
103
+ assert record["provenance"]["source_sha256"] == hashlib.sha256(item["source"].encode()).hexdigest()
104
+
105
+
106
+ def test_lesson_slices_include_only_assigned_records_and_related_evidence():
107
+ manifest = _audit([_item("alpha"), _item("beta"), _item("unrelated", "Another rule entirely")])
108
+ sliced = _slice(manifest)
109
+ assert sliced["schema_version"] == 2
110
+ assert sliced["no_skills"] is True
111
+ assert sliced["assigned_lesson_slugs"] == ["alpha"]
112
+ assert [record["slug"] for record in sliced["lesson_audit"]["lessons"]] == ["alpha"]
113
+ assert sliced["lesson_audit"]["exact_duplicate_groups"][0]["lessons"] == ["alpha", "beta"]
114
+ assert "unrelated" not in json.dumps(sliced)
115
+ with pytest.raises(ValueError, match="lesson_audit_scope_missing"):
116
+ _slice(manifest, ["missing"])
117
+
118
+
119
+ def test_report_receipt_records_changed_and_removed_lessons_and_rejects_other_revisions():
120
+ from threadkeeper.curator_audit import audit_receipt, report_audit_header, report_matches_audit
121
+ manifest = _slice(_audit([_item("alpha"), _item("beta")]), ["alpha", "beta"])
122
+ receipt = audit_receipt(manifest, [_item("alpha", "A new rule")])
123
+ assert [change["slug"] for change in receipt["lesson_revision_changes"]] == ["alpha", "beta"]
124
+ assert receipt["lesson_revision_changes"][1]["current_revision"] is None
125
+ report = report_audit_header(receipt) + "CURATOR_PASS_COMPLETE\n"
126
+ assert report_matches_audit(report, manifest)
127
+ assert not report_matches_audit("CURATOR_PASS_COMPLETE", manifest)
128
+ for key, value in (("sha256", "b" * 64), ("schema_version", 2), ("reviewed_revisions", {})):
129
+ changed = {**receipt, key: value}
130
+ assert not report_matches_audit(report_audit_header(changed), manifest)
131
+
132
+
133
+ @pytest.mark.parametrize("version", [1, 2])
134
+ def test_older_manifests_keep_their_original_slice_contract(version):
135
+ from threadkeeper.skill_audit import build_batch_skill_audit_slice
136
+ manifest = {"schema_version": version, "skills": []}
137
+ sliced = _slice(manifest, [])
138
+ assert sliced == build_batch_skill_audit_slice(
139
+ manifest, pass_id="pass", batch_index=1, batch_total=1,
140
+ inventory_fingerprint="f" * 64, canonical_manifest_sha256="a" * 64,
141
+ assigned_skill_names=[],
142
+ )
143
+
144
+
145
+ @pytest.mark.parametrize("version", [None, True, "3", 0, 4])
146
+ def test_unknown_manifest_versions_fail_clearly(version):
147
+ from threadkeeper.curator_audit import validate_manifest_version
148
+ with pytest.raises(ValueError, match="unsupported_curator_audit_version"):
149
+ validate_manifest_version({"schema_version": version})
150
+
151
+
152
+ def test_promotion_sources_are_part_of_frozen_lesson_scope():
153
+ from threadkeeper.curator_audit import batch_lesson_slugs
154
+ assert batch_lesson_slugs(
155
+ "- LESSON beta [PROTECTED]\n lessons: gamma, alpha\n- LESSON beta\n"
156
+ ) == ["alpha", "beta", "gamma"]
157
+
158
+
159
+ def test_audit_builder_does_not_change_original_skill_manifest():
160
+ from threadkeeper.curator_audit import build_curator_audit
161
+ skills = {"schema_version": 2, "skills": []}
162
+ original = copy.deepcopy(skills)
163
+ build_curator_audit(skills, [_item()])
164
+ assert skills == original