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.
- {threadkeeper-0.23.0/threadkeeper.egg-info → threadkeeper-0.24.0}/PKG-INFO +18 -4
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/README.md +17 -3
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/pyproject.toml +1 -1
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_curator.py +103 -6
- threadkeeper-0.24.0/tests/test_curator_audit.py +164 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/curator.py +171 -27
- threadkeeper-0.24.0/threadkeeper/curator_audit.py +212 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/evolve_applier.py +18 -2
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/curator.py +5 -1
- {threadkeeper-0.23.0 → threadkeeper-0.24.0/threadkeeper.egg-info}/PKG-INFO +18 -4
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/SOURCES.txt +2 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/LICENSE +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/setup.cfg +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_action_outcomes.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_adapters.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_antigravity_ingest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_backup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_brief_hook_size.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_child_memory_access.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ci_parallel.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_concepts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_consequential_state.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dashboard_app.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_leak_guard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_drift_eval.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_documentation_inventory.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evidence_resources.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_43.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_45.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_apply_48.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_forget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_helpers.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_hook_child_guard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_env.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_main.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_identity.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lesson_freshness.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lesson_index.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lesson_violations.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_lessons.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_conformance_gate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_protocol_conformance.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_sdk_compat.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_server_instructions.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_skills_extension.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_mcp_tasks.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_authority_lifecycle.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_compatibility.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_pricing.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_router.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_router_digest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_router_tools.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_model_routes.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_nudges.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_panel.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_permissions.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_pickup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_process_health.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_project_scope.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_retention.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_semantic_bundle.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_setup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_fixtures.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_skills.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_background_cwd.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_child_env.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_result.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_storage.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_gate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_task_worktree_retention.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_threads.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tool_annotations.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tool_profiles.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_tracing.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/tests/test_workflow_retention.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/action_outcomes.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/authority.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/config.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/conformance_server.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/consequential_state.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dashboard_app.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/db.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dialectic_drift_eval.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/evidence.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/host.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/instructions.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/lesson_freshness.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/lesson_violations.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/link_health.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/mcp_skills.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/mcp_tasks.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/memory_access.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/memory_compat.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_pricing.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_router.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_router_digest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/model_routes.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/project_scope.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/protocol.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/read_context.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/semantic_bundle.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/server.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_lifecycle.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/spawn_result.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/storage.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/task_worktrees.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tool_profiles.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/authority.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/capabilities.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/evolve_research.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/memory_proposals.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/model_router.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/project_scope.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/tracing.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper/workflow_artifacts.py +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.23.0 → threadkeeper-0.24.0}/threadkeeper.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
1045
|
-
|
|
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
|
|
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
|
|
998
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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"] ==
|
|
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
|
-
|
|
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
|