threadkeeper 0.17.4__tar.gz → 0.17.6__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.17.4 → threadkeeper-0.17.6}/PKG-INFO +21 -3
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/README.md +20 -2
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/pyproject.toml +1 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_adapters.py +1 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_candidate_reviewer.py +2 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_curator.py +127 -4
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_lessons.py +135 -0
- threadkeeper-0.17.6/tests/test_setup.py +31 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_shadow_review.py +2 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_use_parser.py +2 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skills.py +36 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_slim.py +3 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_tool_annotations.py +6 -4
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/_setup.py +8 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/codex.py +8 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/candidate_reviewer.py +10 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/curator.py +177 -7
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/db.py +28 -0
- threadkeeper-0.17.6/threadkeeper/link_health.py +81 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/shadow_review.py +8 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/curator.py +40 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/lessons.py +210 -3
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/skills.py +16 -3
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/spawn.py +7 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/PKG-INFO +21 -3
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/SOURCES.txt +2 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/LICENSE +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/setup.cfg +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_backup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_concepts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_forget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_helpers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_env.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_main.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_nudges.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_panel.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_permissions.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_pickup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_process_health.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_retention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_threads.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/host.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/server.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.6}/threadkeeper.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threadkeeper
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.6
|
|
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
|
|
@@ -137,6 +137,9 @@ config, copies hooks to
|
|
|
137
137
|
each CLI's per-user instructions file (`CLAUDE.md` / `AGENTS.md` /
|
|
138
138
|
`copilot-instructions.md` — Claude Desktop and VS Code
|
|
139
139
|
have no global instructions file, so that step is skipped for them).
|
|
140
|
+
The generated MCP entry pins imports to this configured installation, so an
|
|
141
|
+
agent launched inside another thread-keeper checkout cannot load that checkout's
|
|
142
|
+
unmerged code against the live memory database.
|
|
140
143
|
|
|
141
144
|
Restart your CLI of choice. Hook-capable clients inject a brief on the first
|
|
142
145
|
message; hookless clients such as Codex and Antigravity CLI either follow the
|
|
@@ -217,7 +220,7 @@ read/act split, plus MCP elicitation for host-native confirmations:
|
|
|
217
220
|
|
|
218
221
|
| Primitive | Control | What thread-keeper exposes | When to use |
|
|
219
222
|
|---|---|---|---|
|
|
220
|
-
| **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, … | the agent decides to call them |
|
|
223
|
+
| **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, `wikilink_health`, … | the agent decides to call them |
|
|
221
224
|
| **Resources** | application-controlled, read-only | `memory://brief`, `memory://context`, `memory://dashboard`, `memory://agent-status` | the **host** attaches/pulls them automatically |
|
|
222
225
|
| **Prompts** | user-controlled templates | `review_recent_threads`, `run_library_curation`, `audit_threadkeeper` | the user runs them (Claude Code: `/mcp__thread-keeper__<name>`) |
|
|
223
226
|
|
|
@@ -611,7 +614,14 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
611
614
|
though an existing same-slug long lesson may be corrected without increasing
|
|
612
615
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
613
616
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
614
|
-
sibling lesson.
|
|
617
|
+
sibling lesson. A clear new directive or debunk also flags older permissive
|
|
618
|
+
lessons on the same concrete practice for patch, cross-link, or supersession
|
|
619
|
+
review.
|
|
620
|
+
Before a genuinely new fallback lesson is written,
|
|
621
|
+
`lesson_neighbors(title, body, summary, k=3)` shows the nearest existing lesson
|
|
622
|
+
slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
|
|
623
|
+
that preflight to patch/consolidate an incumbent or add a `[[slug]]` cross-link
|
|
624
|
+
to a related, distinct lesson while its body is still editable.
|
|
615
625
|
|
|
616
626
|
#### 3. Extract daemon
|
|
617
627
|
|
|
@@ -716,6 +726,14 @@ non-forced direct `curator_review()` calls return `not_due` inside the
|
|
|
716
726
|
configured interval and record that status without spawning. A manual
|
|
717
727
|
`curator_review(force=True)` bypasses the interval but still respects the lock.
|
|
718
728
|
|
|
729
|
+
When a Curator reviews a lesson pair and deliberately keeps both, it records a
|
|
730
|
+
structured `keep_both` merge verdict with the two slugs and a short reason.
|
|
731
|
+
Later inventories show those prior verdicts and each lesson's current
|
|
732
|
+
bidirectional `[[wikilink]]` adjacency (`links=[...]`), including for the
|
|
733
|
+
relevant side of a multi-batch review. This preserves intentional
|
|
734
|
+
general/specific and prevention/recovery layering without making a child
|
|
735
|
+
re-read both lesson bodies to rediscover it.
|
|
736
|
+
|
|
719
737
|
For automation-created skills, the audit keeps foreground consultation separate
|
|
720
738
|
from maintenance: a background-review skill with `fg_uses=0` after 14 days is
|
|
721
739
|
still a false-positive prune candidate even if automatic review or sync loops
|
|
@@ -93,6 +93,9 @@ config, copies hooks to
|
|
|
93
93
|
each CLI's per-user instructions file (`CLAUDE.md` / `AGENTS.md` /
|
|
94
94
|
`copilot-instructions.md` — Claude Desktop and VS Code
|
|
95
95
|
have no global instructions file, so that step is skipped for them).
|
|
96
|
+
The generated MCP entry pins imports to this configured installation, so an
|
|
97
|
+
agent launched inside another thread-keeper checkout cannot load that checkout's
|
|
98
|
+
unmerged code against the live memory database.
|
|
96
99
|
|
|
97
100
|
Restart your CLI of choice. Hook-capable clients inject a brief on the first
|
|
98
101
|
message; hookless clients such as Codex and Antigravity CLI either follow the
|
|
@@ -173,7 +176,7 @@ read/act split, plus MCP elicitation for host-native confirmations:
|
|
|
173
176
|
|
|
174
177
|
| Primitive | Control | What thread-keeper exposes | When to use |
|
|
175
178
|
|---|---|---|---|
|
|
176
|
-
| **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, … | the agent decides to call them |
|
|
179
|
+
| **Tools** | model-controlled (may act) | the full surface — `brief`, `note`, `spawn`, `search`, `curator_review`, `wikilink_health`, … | the agent decides to call them |
|
|
177
180
|
| **Resources** | application-controlled, read-only | `memory://brief`, `memory://context`, `memory://dashboard`, `memory://agent-status` | the **host** attaches/pulls them automatically |
|
|
178
181
|
| **Prompts** | user-controlled templates | `review_recent_threads`, `run_library_curation`, `audit_threadkeeper` | the user runs them (Claude Code: `/mcp__thread-keeper__<name>`) |
|
|
179
182
|
|
|
@@ -567,7 +570,14 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
567
570
|
though an existing same-slug long lesson may be corrected without increasing
|
|
568
571
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
569
572
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
570
|
-
sibling lesson.
|
|
573
|
+
sibling lesson. A clear new directive or debunk also flags older permissive
|
|
574
|
+
lessons on the same concrete practice for patch, cross-link, or supersession
|
|
575
|
+
review.
|
|
576
|
+
Before a genuinely new fallback lesson is written,
|
|
577
|
+
`lesson_neighbors(title, body, summary, k=3)` shows the nearest existing lesson
|
|
578
|
+
slugs (semantic, with a lexical fallback). Shadow and candidate reviewers use
|
|
579
|
+
that preflight to patch/consolidate an incumbent or add a `[[slug]]` cross-link
|
|
580
|
+
to a related, distinct lesson while its body is still editable.
|
|
571
581
|
|
|
572
582
|
#### 3. Extract daemon
|
|
573
583
|
|
|
@@ -672,6 +682,14 @@ non-forced direct `curator_review()` calls return `not_due` inside the
|
|
|
672
682
|
configured interval and record that status without spawning. A manual
|
|
673
683
|
`curator_review(force=True)` bypasses the interval but still respects the lock.
|
|
674
684
|
|
|
685
|
+
When a Curator reviews a lesson pair and deliberately keeps both, it records a
|
|
686
|
+
structured `keep_both` merge verdict with the two slugs and a short reason.
|
|
687
|
+
Later inventories show those prior verdicts and each lesson's current
|
|
688
|
+
bidirectional `[[wikilink]]` adjacency (`links=[...]`), including for the
|
|
689
|
+
relevant side of a multi-batch review. This preserves intentional
|
|
690
|
+
general/specific and prevention/recovery layering without making a child
|
|
691
|
+
re-read both lesson bodies to rediscover it.
|
|
692
|
+
|
|
675
693
|
For automation-created skills, the audit keeps foreground consultation separate
|
|
676
694
|
from maintenance: a background-review skill with `fg_uses=0` after 14 days is
|
|
677
695
|
still a false-positive prune candidate even if automatic review or sync loops
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "threadkeeper"
|
|
7
|
-
version = "0.17.
|
|
7
|
+
version = "0.17.6"
|
|
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" }]
|
|
@@ -383,6 +383,7 @@ def test_codex_spawn_argv_skips_git_repo_check(tmp_path, monkeypatch):
|
|
|
383
383
|
assert argv[:3] == ["/usr/local/bin/codex", "exec", "--skip-git-repo-check"]
|
|
384
384
|
assert argv[-1] == "-"
|
|
385
385
|
assert "-m" in argv and "gpt-5.5" in argv
|
|
386
|
+
assert 'mcp_servers.thread-keeper.env.PYTHONSAFEPATH="1"' in argv
|
|
386
387
|
# Default (non-bypass) path still sandboxes.
|
|
387
388
|
assert "--sandbox" in argv and "workspace-write" in argv
|
|
388
389
|
|
|
@@ -203,6 +203,8 @@ def test_run_spawns_when_threshold_met(tmp_path, monkeypatch):
|
|
|
203
203
|
assert "skill_manage" in allowed
|
|
204
204
|
assert "accept_candidate" in allowed
|
|
205
205
|
assert "reject_candidate" in allowed
|
|
206
|
+
assert "lesson_neighbors(title=<prospective>" in kw["prompt"]
|
|
207
|
+
assert "lesson_neighbors" in allowed
|
|
206
208
|
assert "Bash" not in allowed
|
|
207
209
|
# De-privileged (issue #76): no bare Read/Write/Edit — only the
|
|
208
210
|
# path-scoped skill/lesson/candidate MCP tools.
|
|
@@ -152,15 +152,15 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
|
|
|
152
152
|
conn.execute(
|
|
153
153
|
"INSERT INTO skill_usage "
|
|
154
154
|
"(name, created_at, created_by_origin, last_used_at, "
|
|
155
|
-
" use_count, pinned, state) "
|
|
156
|
-
"VALUES (?, ?, 'foreground', ?, 5, 1, 'active')",
|
|
155
|
+
" use_count, foreground_use_count, view_count, pinned, state) "
|
|
156
|
+
"VALUES (?, ?, 'foreground', ?, 5, 4, 3, 1, 'active')",
|
|
157
157
|
("pinned-skill", now - 86400, now - 3600),
|
|
158
158
|
)
|
|
159
159
|
conn.execute(
|
|
160
160
|
"INSERT INTO skill_usage "
|
|
161
161
|
"(name, created_at, created_by_origin, last_used_at, "
|
|
162
|
-
" use_count, state) "
|
|
163
|
-
"VALUES (?, ?, 'background_review', ?, 2, 'active')",
|
|
162
|
+
" use_count, foreground_use_count, view_count, state) "
|
|
163
|
+
"VALUES (?, ?, 'background_review', ?, 2, 1, 6, 'active')",
|
|
164
164
|
("auto-created-skill", now - 172800, now - 7200),
|
|
165
165
|
)
|
|
166
166
|
conn.commit()
|
|
@@ -177,6 +177,8 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
|
|
|
177
177
|
# background_review skill (not pinned) is NOT protected
|
|
178
178
|
assert "SKILL auto-created-skill [PROTECTED]" not in dump
|
|
179
179
|
assert "SKILL auto-created-skill" in dump
|
|
180
|
+
assert "fg_uses=4 uses=5 views=3 maintenance_patches=0" in dump
|
|
181
|
+
assert "fg_uses=1 uses=2 views=6 maintenance_patches=0" in dump
|
|
180
182
|
assert "STALE LESSONS (dry-run decay ranking)" in dump
|
|
181
183
|
|
|
182
184
|
|
|
@@ -279,6 +281,64 @@ def test_collect_inventory_preview_truncates_large_store(tmp_path, monkeypatch):
|
|
|
279
281
|
assert "live curator pass reviews complete bounded batches" in dump
|
|
280
282
|
|
|
281
283
|
|
|
284
|
+
def test_curator_reuses_merge_verdicts_and_wikilink_adjacency(
|
|
285
|
+
tmp_path, monkeypatch,
|
|
286
|
+
):
|
|
287
|
+
"""A later pass receives durable keep-both context without body reads."""
|
|
288
|
+
pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
|
|
289
|
+
pkg["lessons"].append_lesson(
|
|
290
|
+
title="general-prevention",
|
|
291
|
+
body="The broad guard points to [[specific-recovery]].",
|
|
292
|
+
source="shadow",
|
|
293
|
+
)
|
|
294
|
+
pkg["lessons"].append_lesson(
|
|
295
|
+
title="specific-recovery",
|
|
296
|
+
body="The repair path points to [[general-prevention]].",
|
|
297
|
+
source="shadow",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
import threadkeeper.tools.spawn as spawn_mod
|
|
301
|
+
captured: list[dict] = []
|
|
302
|
+
|
|
303
|
+
def fake_spawn(**kwargs):
|
|
304
|
+
captured.append(kwargs)
|
|
305
|
+
return f"spawn task_id=curator-{len(captured)} pid=0"
|
|
306
|
+
|
|
307
|
+
monkeypatch.setattr(spawn_mod, "spawn", fake_spawn)
|
|
308
|
+
assert "curator-1" in pkg["curator"].run_curator_pass(force=True)
|
|
309
|
+
|
|
310
|
+
from threadkeeper._mcp import mcp
|
|
311
|
+
verdict = mcp._tool_manager._tools["curator_merge_verdict"].fn
|
|
312
|
+
out = verdict(
|
|
313
|
+
"specific-recovery",
|
|
314
|
+
"general-prevention",
|
|
315
|
+
"prevention/recovery",
|
|
316
|
+
)
|
|
317
|
+
assert out == (
|
|
318
|
+
"ok merge_verdict=general-prevention,specific-recovery "
|
|
319
|
+
"decision=keep_both"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
assert "curator-2" in pkg["curator"].run_curator_pass(force=True)
|
|
323
|
+
prompt = captured[-1]["prompt"]
|
|
324
|
+
assert "general-prevention" in prompt
|
|
325
|
+
assert "specific-recovery" in prompt
|
|
326
|
+
assert "links=[specific-recovery]" in prompt
|
|
327
|
+
assert "links=[general-prevention]" in prompt
|
|
328
|
+
assert "## PRIOR MERGE VERDICTS (n=1)" in prompt
|
|
329
|
+
assert "decision=keep_both reason=prevention/recovery" in prompt
|
|
330
|
+
|
|
331
|
+
conn = pkg["db"].get_db()
|
|
332
|
+
row = conn.execute(
|
|
333
|
+
"SELECT left_slug, right_slug, decision, reason "
|
|
334
|
+
"FROM curator_merge_verdicts"
|
|
335
|
+
).fetchone()
|
|
336
|
+
assert tuple(row) == (
|
|
337
|
+
"general-prevention", "specific-recovery", "keep_both",
|
|
338
|
+
"prevention/recovery",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
282
342
|
# ──────────────────────────────────────────────────────────────────────
|
|
283
343
|
# run_curator_pass — dispatch logic
|
|
284
344
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -362,6 +422,7 @@ def test_run_curator_pass_spawns_when_threshold_met(tmp_path, monkeypatch):
|
|
|
362
422
|
assert "evolve_format" in allowed
|
|
363
423
|
assert "Read" in allowed
|
|
364
424
|
assert "curator_report_write" in allowed
|
|
425
|
+
assert "curator_merge_verdict" in allowed
|
|
365
426
|
assert "Write" not in allowed
|
|
366
427
|
assert "WebSearch" in allowed
|
|
367
428
|
assert "WebFetch" in allowed
|
|
@@ -914,6 +975,68 @@ def test_skill_validate_tool_returns_post_change_contract(tmp_path, monkeypatch)
|
|
|
914
975
|
}
|
|
915
976
|
|
|
916
977
|
|
|
978
|
+
def test_wikilink_health_reports_dangling_lesson_and_skill_links(
|
|
979
|
+
tmp_path, monkeypatch,
|
|
980
|
+
):
|
|
981
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
982
|
+
pkg["lessons"].append_lesson(
|
|
983
|
+
title="valid lesson",
|
|
984
|
+
body="Available target.",
|
|
985
|
+
source="shadow",
|
|
986
|
+
)
|
|
987
|
+
pkg["lessons"].append_lesson(
|
|
988
|
+
title="lesson source",
|
|
989
|
+
body="See [[valid-lesson]] and [[missing-lesson]].",
|
|
990
|
+
source="shadow",
|
|
991
|
+
)
|
|
992
|
+
_write_audit_skill(
|
|
993
|
+
pkg["skills_dir"], "skill-source",
|
|
994
|
+
"See [[valid-lesson]] and [[missing-skill]].",
|
|
995
|
+
)
|
|
996
|
+
conn = pkg["db"].get_db()
|
|
997
|
+
|
|
998
|
+
from threadkeeper.link_health import scan_wikilink_health
|
|
999
|
+
|
|
1000
|
+
result = scan_wikilink_health(conn)
|
|
1001
|
+
|
|
1002
|
+
assert result["summary"] == {
|
|
1003
|
+
"lessons_scanned": 2,
|
|
1004
|
+
"skills_scanned": 1,
|
|
1005
|
+
"references_scanned": 4,
|
|
1006
|
+
"dangling_references": 2,
|
|
1007
|
+
}
|
|
1008
|
+
assert result["dangling_references"] == [
|
|
1009
|
+
{
|
|
1010
|
+
"source_kind": "lesson",
|
|
1011
|
+
"source": "lesson-source",
|
|
1012
|
+
"target": "missing-lesson",
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
"source_kind": "skill",
|
|
1016
|
+
"source": "skill-source",
|
|
1017
|
+
"target": "missing-skill",
|
|
1018
|
+
},
|
|
1019
|
+
]
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def test_wikilink_health_tool_exposes_checker_result(tmp_path, monkeypatch):
|
|
1023
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
1024
|
+
pkg["lessons"].append_lesson(
|
|
1025
|
+
title="lesson source", body="See [[missing-target]].", source="shadow",
|
|
1026
|
+
)
|
|
1027
|
+
from threadkeeper._mcp import mcp
|
|
1028
|
+
|
|
1029
|
+
result = mcp._tool_manager._tools["wikilink_health"].fn()
|
|
1030
|
+
payload = json.loads(result)
|
|
1031
|
+
|
|
1032
|
+
assert payload["summary"]["dangling_references"] == 1
|
|
1033
|
+
assert payload["dangling_references"] == [{
|
|
1034
|
+
"source_kind": "lesson",
|
|
1035
|
+
"source": "lesson-source",
|
|
1036
|
+
"target": "missing-target",
|
|
1037
|
+
}]
|
|
1038
|
+
|
|
1039
|
+
|
|
917
1040
|
def test_curator_report_write_is_path_scoped_and_replaceable(
|
|
918
1041
|
tmp_path, monkeypatch,
|
|
919
1042
|
):
|
|
@@ -205,6 +205,73 @@ def test_mcp_lesson_append_validates_inputs(tmp_path, monkeypatch):
|
|
|
205
205
|
assert out.startswith("ok slug=ok-one")
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
def test_lesson_neighbors_previews_ranked_slugs_without_writing(
|
|
209
|
+
tmp_path, monkeypatch,
|
|
210
|
+
):
|
|
211
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
212
|
+
la = _tool(pkg, "lesson_append")
|
|
213
|
+
ln = _tool(pkg, "lesson_neighbors")
|
|
214
|
+
la(title="reset ios network", body="Reset iOS simulator network settings.")
|
|
215
|
+
la(title="serialize device boot", body="Serialize device boot locks.")
|
|
216
|
+
la(title="unrelated release notes", body="Document release notes.")
|
|
217
|
+
|
|
218
|
+
import threadkeeper.embeddings as embeddings
|
|
219
|
+
|
|
220
|
+
monkeypatch.setattr(
|
|
221
|
+
embeddings,
|
|
222
|
+
"encode_many",
|
|
223
|
+
lambda texts: [[1.0, 0.0], [0.96, 0.0], [0.83, 0.0], [0.40, 0.0]],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
out = ln(
|
|
227
|
+
title="recover simulator connectivity",
|
|
228
|
+
summary="Restore device networking before a test run.",
|
|
229
|
+
body="Reset simulator network settings before retrying device tests.",
|
|
230
|
+
k=2,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
assert out.splitlines() == [
|
|
234
|
+
"lesson_neighbors total=2 mode=semantic prospective_slug=recover-simulator-connectivity",
|
|
235
|
+
" 1. reset-ios-network score=0.96",
|
|
236
|
+
" 2. serialize-device-boot score=0.83",
|
|
237
|
+
]
|
|
238
|
+
assert pkg["lessons"].count_lessons() == 3
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_lesson_neighbors_leaves_existing_lesson_write_semantics_unchanged(
|
|
242
|
+
tmp_path, monkeypatch,
|
|
243
|
+
):
|
|
244
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
245
|
+
ln = _tool(pkg, "lesson_neighbors")
|
|
246
|
+
la = _tool(pkg, "lesson_append")
|
|
247
|
+
|
|
248
|
+
assert ln(title="new lesson", body="new body") == (
|
|
249
|
+
"lesson_neighbors total=0 mode=semantic"
|
|
250
|
+
)
|
|
251
|
+
assert la(title="new lesson", body="new body") == (
|
|
252
|
+
f"ok slug=new-lesson path={pkg['path']}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_lesson_neighbors_falls_back_to_lexical_ranking(tmp_path, monkeypatch):
|
|
257
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
258
|
+
la = _tool(pkg, "lesson_append")
|
|
259
|
+
ln = _tool(pkg, "lesson_neighbors")
|
|
260
|
+
la(
|
|
261
|
+
title="reset simulator network",
|
|
262
|
+
body="Reset simulator network settings before device tests.",
|
|
263
|
+
)
|
|
264
|
+
import threadkeeper.embeddings as embeddings
|
|
265
|
+
|
|
266
|
+
monkeypatch.setattr(embeddings, "encode_many", lambda texts: None)
|
|
267
|
+
out = ln(
|
|
268
|
+
title="repair simulator connectivity",
|
|
269
|
+
body="Reset simulator network before retrying device tests.",
|
|
270
|
+
)
|
|
271
|
+
assert "mode=lexical" in out
|
|
272
|
+
assert "reset-simulator-network" in out
|
|
273
|
+
|
|
274
|
+
|
|
208
275
|
def test_mcp_lesson_patch_replaces_one_unique_substring(tmp_path, monkeypatch):
|
|
209
276
|
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
210
277
|
la = _tool(pkg, "lesson_append")
|
|
@@ -356,6 +423,74 @@ def test_shadow_lesson_append_semantic_duplicate_patches_incumbent(
|
|
|
356
423
|
assert "serialize AVD boot lock acquisition" in body
|
|
357
424
|
|
|
358
425
|
|
|
426
|
+
def test_new_directive_flags_older_permissive_lesson_for_reconciliation(
|
|
427
|
+
tmp_path, monkeypatch,
|
|
428
|
+
):
|
|
429
|
+
pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
|
|
430
|
+
la = _tool(pkg, "lesson_append")
|
|
431
|
+
|
|
432
|
+
first = la(
|
|
433
|
+
title="module globals for test configuration",
|
|
434
|
+
body=(
|
|
435
|
+
"You can use module globals to pass test configuration between "
|
|
436
|
+
"runner steps when setup is simple."
|
|
437
|
+
),
|
|
438
|
+
source="shadow",
|
|
439
|
+
)
|
|
440
|
+
assert first.startswith("ok")
|
|
441
|
+
|
|
442
|
+
out = la(
|
|
443
|
+
title="inject test configuration explicitly",
|
|
444
|
+
body=(
|
|
445
|
+
"Never use module globals to pass test configuration between "
|
|
446
|
+
"runner steps; inject dependencies explicitly instead."
|
|
447
|
+
),
|
|
448
|
+
source="shadow",
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
assert out.startswith("ok slug=inject-test-configuration-explicitly")
|
|
452
|
+
assert "reconciliation=module-globals-for-test-configuration" in out
|
|
453
|
+
assert "review=patch_or_cross_link" in out
|
|
454
|
+
assert pkg["lessons"].count_lessons() == 2
|
|
455
|
+
row = pkg["db"].get_db().execute(
|
|
456
|
+
"SELECT target, summary FROM events WHERE kind='lesson_reconciliation'"
|
|
457
|
+
).fetchone()
|
|
458
|
+
assert row["target"] == "module-globals-for-test-configuration"
|
|
459
|
+
assert "new=inject-test-configuration-explicitly" in row["summary"]
|
|
460
|
+
assert "reason=absolute_directive" in row["summary"]
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_new_debunk_flags_older_permissive_lesson_for_reconciliation(
|
|
464
|
+
tmp_path, monkeypatch,
|
|
465
|
+
):
|
|
466
|
+
pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
|
|
467
|
+
la = _tool(pkg, "lesson_append")
|
|
468
|
+
|
|
469
|
+
la(
|
|
470
|
+
title="shared runner environment state",
|
|
471
|
+
body=(
|
|
472
|
+
"It is safe to keep shared runner environment state throughout "
|
|
473
|
+
"a test workflow."
|
|
474
|
+
),
|
|
475
|
+
source="shadow",
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
out = la(
|
|
479
|
+
title="shared runner environment state is unreliable",
|
|
480
|
+
body=(
|
|
481
|
+
"Keeping shared runner environment state is unreliable; pass the "
|
|
482
|
+
"environment to each command instead."
|
|
483
|
+
),
|
|
484
|
+
source="shadow",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
assert "reconciliation=shared-runner-environment-state" in out
|
|
488
|
+
row = pkg["db"].get_db().execute(
|
|
489
|
+
"SELECT summary FROM events WHERE kind='lesson_reconciliation'"
|
|
490
|
+
).fetchone()
|
|
491
|
+
assert "reason=debunk" in row["summary"]
|
|
492
|
+
|
|
493
|
+
|
|
359
494
|
def test_shadow_lesson_append_semantic_borderline_surfaces_duplicate(
|
|
360
495
|
tmp_path, monkeypatch,
|
|
361
496
|
):
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_install_mcp_servers_pins_imports_to_configured_package(
|
|
5
|
+
tmp_path, monkeypatch,
|
|
6
|
+
):
|
|
7
|
+
import threadkeeper._setup as setup_mod
|
|
8
|
+
import threadkeeper.adapters as adapters_mod
|
|
9
|
+
|
|
10
|
+
captured = {}
|
|
11
|
+
|
|
12
|
+
class FakeAdapter:
|
|
13
|
+
name = "fake"
|
|
14
|
+
|
|
15
|
+
def register_mcp_server(self, **kwargs):
|
|
16
|
+
captured.update(kwargs)
|
|
17
|
+
return "ok"
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr(setup_mod, "REPO_ROOT", tmp_path)
|
|
20
|
+
monkeypatch.setattr(
|
|
21
|
+
adapters_mod, "installed_adapters", lambda: [FakeAdapter()],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
assert setup_mod.install_mcp_servers(dry_run=False) == [
|
|
25
|
+
"mcp_server[fake]: ok"
|
|
26
|
+
]
|
|
27
|
+
assert captured["env"] == {
|
|
28
|
+
"PYTHONPATH": str(tmp_path),
|
|
29
|
+
"PYTHONSAFEPATH": "1",
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -373,8 +373,10 @@ def test_run_shadow_pass_spawns_when_threshold_met(tmp_path, monkeypatch):
|
|
|
373
373
|
assert kw["write_origin"] == "shadow_review"
|
|
374
374
|
assert "SHADOW LEARNING OBSERVER" in kw["prompt"]
|
|
375
375
|
assert "lesson_list(k=80)" in kw["prompt"]
|
|
376
|
+
assert "lesson_neighbors(title=<prospective>" in kw["prompt"]
|
|
376
377
|
assert "hard cap 450" in kw["prompt"]
|
|
377
378
|
assert "mcp__thread-keeper__lesson_get" in kw["extra_allowed_tools"]
|
|
379
|
+
assert "mcp__thread-keeper__lesson_neighbors" in kw["extra_allowed_tools"]
|
|
378
380
|
assert "mcp__thread-keeper__lesson_patch" in kw["extra_allowed_tools"]
|
|
379
381
|
assert long_msg.strip()[:40] in kw["prompt"]
|
|
380
382
|
# De-privileged (issue #76): no bare Read/Write — path-scoped skill/
|
|
@@ -182,11 +182,12 @@ def test_ingest_file_bumps_skill_usage(tmp_path, monkeypatch):
|
|
|
182
182
|
conn.commit()
|
|
183
183
|
assert added == 1
|
|
184
184
|
row = conn.execute(
|
|
185
|
-
"SELECT use_count, last_used_at, created_by_origin "
|
|
185
|
+
"SELECT use_count, foreground_use_count, last_used_at, created_by_origin "
|
|
186
186
|
"FROM skill_usage WHERE name='swift-ios'"
|
|
187
187
|
).fetchone()
|
|
188
188
|
assert row is not None
|
|
189
189
|
assert row["use_count"] == 1
|
|
190
|
+
assert row["foreground_use_count"] == 1
|
|
190
191
|
assert row["last_used_at"] is not None
|
|
191
192
|
assert row["created_by_origin"] == "foreground"
|
|
192
193
|
|
|
@@ -466,6 +466,42 @@ def test_skill_record_bumps_counters(skills_pkg):
|
|
|
466
466
|
assert r["last_viewed_at"] is not None
|
|
467
467
|
|
|
468
468
|
|
|
469
|
+
def test_skill_list_records_views_for_returned_skills(skills_pkg):
|
|
470
|
+
_seed_skill(skills_pkg, name="listed-active")
|
|
471
|
+
_seed_skill(skills_pkg, name="listed-archived")
|
|
472
|
+
conn = skills_pkg["db"].get_db()
|
|
473
|
+
conn.execute(
|
|
474
|
+
"UPDATE skill_usage SET state='archived' WHERE name='listed-archived'"
|
|
475
|
+
)
|
|
476
|
+
conn.commit()
|
|
477
|
+
|
|
478
|
+
listed = _tool(skills_pkg, "skill_list")
|
|
479
|
+
rendered = listed()
|
|
480
|
+
assert "listed-active" in rendered
|
|
481
|
+
assert "views=1" in rendered
|
|
482
|
+
rows = {
|
|
483
|
+
row["name"]: row
|
|
484
|
+
for row in conn.execute(
|
|
485
|
+
"SELECT name, view_count, last_viewed_at FROM skill_usage "
|
|
486
|
+
"WHERE name IN ('listed-active', 'listed-archived')"
|
|
487
|
+
).fetchall()
|
|
488
|
+
}
|
|
489
|
+
assert rows["listed-active"]["view_count"] == 1
|
|
490
|
+
assert rows["listed-active"]["last_viewed_at"] is not None
|
|
491
|
+
assert rows["listed-archived"]["view_count"] == 0
|
|
492
|
+
|
|
493
|
+
assert "listed-archived" in listed(include_archived=True)
|
|
494
|
+
rows = {
|
|
495
|
+
row["name"]: row
|
|
496
|
+
for row in conn.execute(
|
|
497
|
+
"SELECT name, view_count FROM skill_usage "
|
|
498
|
+
"WHERE name IN ('listed-active', 'listed-archived')"
|
|
499
|
+
).fetchall()
|
|
500
|
+
}
|
|
501
|
+
assert rows["listed-active"]["view_count"] == 2
|
|
502
|
+
assert rows["listed-archived"]["view_count"] == 1
|
|
503
|
+
|
|
504
|
+
|
|
469
505
|
def test_skill_list_omits_archived_by_default(skills_pkg):
|
|
470
506
|
_seed_skill(skills_pkg, name="alive")
|
|
471
507
|
_seed_skill(skills_pkg, name="ghost")
|
|
@@ -59,6 +59,7 @@ def test_build_slim_mcp_config_from_claude_json(tmp_path, monkeypatch):
|
|
|
59
59
|
assert mp["command"] == "/path/to/python"
|
|
60
60
|
assert mp["args"] == ["-m", "threadkeeper.server"]
|
|
61
61
|
assert mp["env"]["PYTHONPATH"] == "/path/to/repo"
|
|
62
|
+
assert mp["env"]["PYTHONSAFEPATH"] == "1"
|
|
62
63
|
assert mp["env"]["THREADKEEPER_FORCE_CID"] == _FAKE_CID
|
|
63
64
|
assert mp["env"]["THREADKEEPER_SPAWNED_CHILD"] == "1"
|
|
64
65
|
assert mp["env"]["THREADKEEPER_NO_EMBEDDINGS"] == "1"
|
|
@@ -92,6 +93,7 @@ def test_build_slim_mcp_config_synthesizes_when_no_claude_json(tmp_path, monkeyp
|
|
|
92
93
|
assert mp["command"] == sys.executable
|
|
93
94
|
assert "threadkeeper.server" in mp["args"]
|
|
94
95
|
assert "PYTHONPATH" in mp["env"]
|
|
96
|
+
assert mp["env"]["PYTHONSAFEPATH"] == "1"
|
|
95
97
|
assert mp["env"]["THREADKEEPER_SPAWNED_CHILD"] == "1"
|
|
96
98
|
assert mp["env"]["THREADKEEPER_NO_EMBEDDINGS"] == "1"
|
|
97
99
|
|
|
@@ -142,6 +144,7 @@ def test_slim_config_is_owner_only_and_minimizes_env(tmp_path, monkeypatch):
|
|
|
142
144
|
env = json.loads(slim_path.read_text())["mcpServers"]["thread-keeper"]["env"]
|
|
143
145
|
# Needed keys survive: package discovery + thread-keeper knobs + overrides.
|
|
144
146
|
assert env["PYTHONPATH"] == "/path/to/repo"
|
|
147
|
+
assert env["PYTHONSAFEPATH"] == "1"
|
|
145
148
|
assert env["THREADKEEPER_TZ"] == "Europe/Kyiv"
|
|
146
149
|
assert env["THREADKEEPER_FORCE_CID"] == _FAKE_CID
|
|
147
150
|
assert env["THREADKEEPER_NO_EMBEDDINGS"] == "1"
|
|
@@ -33,9 +33,10 @@ READ_TOOLS = {
|
|
|
33
33
|
"memory_guard_status", "mp_dashboard", "mp_health", "neighbors", "peers",
|
|
34
34
|
"pending_distillates", "pickup_candidates", "reliability_for",
|
|
35
35
|
"review_candidates", "run_probe", "search", "shadow_review_status",
|
|
36
|
-
"
|
|
36
|
+
"skill_validate", "spawn_budget_status", "spawn_status",
|
|
37
37
|
"sync_peers", "sync_status", "task_logs",
|
|
38
|
-
"task_thread", "tasks", "weak_spots", "whoami",
|
|
38
|
+
"task_thread", "tasks", "weak_spots", "whoami", "wikilink_health",
|
|
39
|
+
"lesson_neighbors",
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
# --- delete/overwrite/kill tools: destructiveHint=True -----------------------
|
|
@@ -50,7 +51,8 @@ DELETE_CLASS_TOOLS = {
|
|
|
50
51
|
WRITE_TOOLS = {
|
|
51
52
|
"accept_candidate", "ask", "auto_review_trigger", "broadcast",
|
|
52
53
|
"candidate_review_run", "claim_pickup", "close_thread", "config_reload",
|
|
53
|
-
"convene_panel", "core_set", "
|
|
54
|
+
"convene_panel", "core_set", "curator_merge_verdict",
|
|
55
|
+
"curator_report_write", "curator_review",
|
|
54
56
|
"db_compact", "db_deduplicate_embeddings", "dialectic_claim",
|
|
55
57
|
"dialectic_evidence", "dialectic_mine_run", "dialectic_observation_resolve",
|
|
56
58
|
"dialectic_supersede", "dialectic_validate_run", "distill", "evolve_apply",
|
|
@@ -65,7 +67,7 @@ WRITE_TOOLS = {
|
|
|
65
67
|
"open_dialog_window", "open_thread", "presence", "record_attempt",
|
|
66
68
|
"register_concept", "register_probe", "reject_candidate", "release_pickup",
|
|
67
69
|
"respond", "review_thread", "search_via_parent", "session_end",
|
|
68
|
-
"shadow_review_run", "skill_record", "spawn", "spawn_budget_set",
|
|
70
|
+
"shadow_review_run", "skill_list", "skill_record", "spawn", "spawn_budget_set",
|
|
69
71
|
"style_set", "sync_now", "tag_signal", "tournament", "validate_threads",
|
|
70
72
|
"verbatim_user", "vote_distill", "wait", "whisper",
|
|
71
73
|
}
|
|
@@ -128,7 +128,14 @@ def install_mcp_servers(dry_run: bool) -> list[str]:
|
|
|
128
128
|
if venv_python.exists():
|
|
129
129
|
python_bin = str(venv_python)
|
|
130
130
|
args = ["-m", "threadkeeper.server"]
|
|
131
|
-
env = {
|
|
131
|
+
env = {
|
|
132
|
+
"PYTHONPATH": str(REPO_ROOT),
|
|
133
|
+
# MCP clients often launch the server from the agent's current
|
|
134
|
+
# checkout. Without safe-path mode, ``python -m`` puts that checkout
|
|
135
|
+
# ahead of the configured install and can import unmerged Evolve code
|
|
136
|
+
# against the live DB.
|
|
137
|
+
"PYTHONSAFEPATH": "1",
|
|
138
|
+
}
|
|
132
139
|
|
|
133
140
|
lines: list[str] = []
|
|
134
141
|
adapters = installed_adapters()
|
|
@@ -337,6 +337,14 @@ class CodexAdapter(CLIAdapter):
|
|
|
337
337
|
if {"websearch", "webfetch"} & requested_tools:
|
|
338
338
|
argv.append("--search")
|
|
339
339
|
argv += ["exec", "--skip-git-repo-check"]
|
|
340
|
+
# Codex launches configured stdio MCP servers from the agent cwd. A
|
|
341
|
+
# repository checkout can therefore shadow the installed package used
|
|
342
|
+
# by thread-keeper's MCP entry. Scope safe-path mode to the MCP server
|
|
343
|
+
# only; shell/test Python processes in the child keep normal semantics.
|
|
344
|
+
argv += [
|
|
345
|
+
"-c",
|
|
346
|
+
'mcp_servers.thread-keeper.env.PYTHONSAFEPATH="1"',
|
|
347
|
+
]
|
|
340
348
|
if model:
|
|
341
349
|
argv += ["-m", model]
|
|
342
350
|
if effort:
|