threadkeeper 0.18.2__tar.gz → 0.18.4__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.18.2/threadkeeper.egg-info → threadkeeper-0.18.4}/PKG-INFO +30 -8
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/README.md +28 -6
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/pyproject.toml +5 -7
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_error_paths.py +1 -1
- threadkeeper-0.18.4/tests/test_evolve_apply_43.py +97 -0
- threadkeeper-0.18.4/tests/test_evolve_apply_45.py +107 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_mcp_resources_prompts.py +3 -4
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_mcp_sdk_compat.py +4 -5
- threadkeeper-0.18.4/tests/test_mcp_skills_extension.py +183 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_release_workflows.py +7 -11
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_slim.py +32 -12
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/_mcp.py +47 -24
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/brief.py +106 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/elicitation.py +1 -2
- threadkeeper-0.18.4/threadkeeper/mcp_skills.py +327 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/dialectic.py +3 -1
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/spawn.py +7 -5
- {threadkeeper-0.18.2 → threadkeeper-0.18.4/threadkeeper.egg-info}/PKG-INFO +30 -8
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper.egg-info/SOURCES.txt +4 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper.egg-info/requires.txt +1 -1
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/LICENSE +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/setup.cfg +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_adapters.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_antigravity_ingest.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_backup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_ci_parallel.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_concepts.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_curator.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_db_leak_guard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_documentation_inventory.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_forget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_helpers.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_hook_child_guard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_host_env.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_host_main.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_identity.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_lesson_violations.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_lessons.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_nudges.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_panel.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_permissions.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_pickup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_process_health.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_retention.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_setup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_skills.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_background_cwd.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_child_env.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_result.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_threads.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_tool_annotations.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/config.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/db.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/host.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/lesson_violations.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/link_health.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/server.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/spawn_result.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/evolve_research.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.18.2 → threadkeeper-0.18.4}/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.18.
|
|
3
|
+
Version: 0.18.4
|
|
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
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
22
22
|
Requires-Python: >=3.11
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: mcp<3,>=
|
|
25
|
+
Requires-Dist: mcp<3,>=2.2.0
|
|
26
26
|
Requires-Dist: pydantic>=2.13.4
|
|
27
27
|
Requires-Dist: pydantic-settings>=2.14.2
|
|
28
28
|
Requires-Dist: pyyaml>=6.0.3
|
|
@@ -217,13 +217,12 @@ Cline, … — so a single registration there reaches all of them at once.
|
|
|
217
217
|
Adding a new CLI = one file under `threadkeeper/adapters/` implementing
|
|
218
218
|
the `CLIAdapter` contract. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
219
219
|
|
|
220
|
-
### Python MCP SDK
|
|
220
|
+
### Python MCP SDK surface
|
|
221
221
|
|
|
222
|
-
thread-keeper
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
the same public contract across both supported SDK majors.
|
|
222
|
+
thread-keeper requires MCP Python SDK 2.2 or later (`mcp>=2.2.0,<3`). Its
|
|
223
|
+
stable Skills extension uses the MCP 2026-07-28 extension surface, while tools,
|
|
224
|
+
resources, prompts, annotations, structured content, elicitation, and stdio
|
|
225
|
+
continue to use the same `MCPServer` instance.
|
|
227
226
|
|
|
228
227
|
### MCP primitives (tools, resources, prompts, elicitation)
|
|
229
228
|
|
|
@@ -263,6 +262,29 @@ Hosts without a capability fall back to the SessionStart hook plus the `brief()`
|
|
|
263
262
|
regression. Static URIs only for now (resource *templates* with `{param}` are
|
|
264
263
|
still unevenly supported across hosts).
|
|
265
264
|
|
|
265
|
+
### MCP Skills extension
|
|
266
|
+
|
|
267
|
+
On MCP 2026-07-28 hosts, thread-keeper also advertises the stable
|
|
268
|
+
`io.modelcontextprotocol/skills` extension. `skills/list` pages the canonical
|
|
269
|
+
library deterministically, and `skills/get` returns the full manifest for one
|
|
270
|
+
skill. Each resource URI is origin-qualified, for example
|
|
271
|
+
`skill://thread-keeper/release-check/SKILL.md`, so hosts retain the server
|
|
272
|
+
identity alongside a possibly colliding skill name.
|
|
273
|
+
|
|
274
|
+
The manifest covers `SKILL.md` and every permitted file below `references/`,
|
|
275
|
+
`templates/`, `scripts/`, or `assets/`, with its byte length and a
|
|
276
|
+
`sha256:<hex>` digest. Files are individually available through ordinary
|
|
277
|
+
`resources/read`; traversal, undeclared files, and reads over 1 MiB are
|
|
278
|
+
rejected. Hosts should compare both size and digest before using a retrieved
|
|
279
|
+
file. The canonical directory is published once; the existing per-CLI
|
|
280
|
+
filesystem mirrors remain the fallback for clients that do not implement the
|
|
281
|
+
extension.
|
|
282
|
+
|
|
283
|
+
Discovery and `resources/read` are delivery only. Reading `SKILL.md` does not
|
|
284
|
+
activate it, approve its tools, record use telemetry, or approve supporting
|
|
285
|
+
files. Activation and any user approval remain responsibilities of the host's
|
|
286
|
+
skill-loading path.
|
|
287
|
+
|
|
266
288
|
### Memory egress (cross-provider privacy)
|
|
267
289
|
|
|
268
290
|
thread-keeper is "one user model … shared across CLIs," and that sharing is by
|
|
@@ -173,13 +173,12 @@ Cline, … — so a single registration there reaches all of them at once.
|
|
|
173
173
|
Adding a new CLI = one file under `threadkeeper/adapters/` implementing
|
|
174
174
|
the `CLIAdapter` contract. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
175
175
|
|
|
176
|
-
### Python MCP SDK
|
|
176
|
+
### Python MCP SDK surface
|
|
177
177
|
|
|
178
|
-
thread-keeper
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
the same public contract across both supported SDK majors.
|
|
178
|
+
thread-keeper requires MCP Python SDK 2.2 or later (`mcp>=2.2.0,<3`). Its
|
|
179
|
+
stable Skills extension uses the MCP 2026-07-28 extension surface, while tools,
|
|
180
|
+
resources, prompts, annotations, structured content, elicitation, and stdio
|
|
181
|
+
continue to use the same `MCPServer` instance.
|
|
183
182
|
|
|
184
183
|
### MCP primitives (tools, resources, prompts, elicitation)
|
|
185
184
|
|
|
@@ -219,6 +218,29 @@ Hosts without a capability fall back to the SessionStart hook plus the `brief()`
|
|
|
219
218
|
regression. Static URIs only for now (resource *templates* with `{param}` are
|
|
220
219
|
still unevenly supported across hosts).
|
|
221
220
|
|
|
221
|
+
### MCP Skills extension
|
|
222
|
+
|
|
223
|
+
On MCP 2026-07-28 hosts, thread-keeper also advertises the stable
|
|
224
|
+
`io.modelcontextprotocol/skills` extension. `skills/list` pages the canonical
|
|
225
|
+
library deterministically, and `skills/get` returns the full manifest for one
|
|
226
|
+
skill. Each resource URI is origin-qualified, for example
|
|
227
|
+
`skill://thread-keeper/release-check/SKILL.md`, so hosts retain the server
|
|
228
|
+
identity alongside a possibly colliding skill name.
|
|
229
|
+
|
|
230
|
+
The manifest covers `SKILL.md` and every permitted file below `references/`,
|
|
231
|
+
`templates/`, `scripts/`, or `assets/`, with its byte length and a
|
|
232
|
+
`sha256:<hex>` digest. Files are individually available through ordinary
|
|
233
|
+
`resources/read`; traversal, undeclared files, and reads over 1 MiB are
|
|
234
|
+
rejected. Hosts should compare both size and digest before using a retrieved
|
|
235
|
+
file. The canonical directory is published once; the existing per-CLI
|
|
236
|
+
filesystem mirrors remain the fallback for clients that do not implement the
|
|
237
|
+
extension.
|
|
238
|
+
|
|
239
|
+
Discovery and `resources/read` are delivery only. Reading `SKILL.md` does not
|
|
240
|
+
activate it, approve its tools, record use telemetry, or approve supporting
|
|
241
|
+
files. Activation and any user approval remain responsibilities of the host's
|
|
242
|
+
skill-loading path.
|
|
243
|
+
|
|
222
244
|
### Memory egress (cross-provider privacy)
|
|
223
245
|
|
|
224
246
|
thread-keeper is "one user model … shared across CLIs," and that sharing is by
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "threadkeeper"
|
|
7
|
-
version = "0.18.
|
|
7
|
+
version = "0.18.4"
|
|
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" }]
|
|
@@ -26,12 +26,10 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
# future unreviewed major from changing the server at install time.
|
|
34
|
-
"mcp>=1.10.0,<3",
|
|
29
|
+
# MCP 2.2 provides the 2026-07-28 extension surface used for the stable
|
|
30
|
+
# Skills distribution protocol. <3 keeps a future unreviewed major from
|
|
31
|
+
# changing the server at install time.
|
|
32
|
+
"mcp>=2.2.0,<3",
|
|
35
33
|
"pydantic>=2.13.4",
|
|
36
34
|
"pydantic-settings>=2.14.2",
|
|
37
35
|
"pyyaml>=6.0.3",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
on bad input rather than raising Python exceptions.
|
|
3
3
|
|
|
4
4
|
A future error-boundary refactor (a `@safe_tool` decorator, schema validation,
|
|
5
|
-
or a top-level
|
|
5
|
+
or a top-level MCPServer wrapper) must keep these tests green.
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Golden coverage for surfacing the surgical lesson_patch action in brief().
|
|
2
|
+
|
|
3
|
+
The format should teach a fresh agent to make a narrow lesson correction with
|
|
4
|
+
the atomic operation rather than overwrite an otherwise-correct lesson body.
|
|
5
|
+
The seeded fixture also pins nearby long-lived brief sections.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
_FAKE_CID = "eeee3333-4444-5555-6666-777788889999"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _bootstrap(tmp_path, monkeypatch):
|
|
18
|
+
env = {
|
|
19
|
+
"THREADKEEPER_DB": str(tmp_path / "db.sqlite"),
|
|
20
|
+
"THREADKEEPER_LESSONS": str(tmp_path / "lessons.md"),
|
|
21
|
+
"CLAUDE_PROJECTS_DIR": str(tmp_path / "fake_claude_projects"),
|
|
22
|
+
"THREADKEEPER_INGEST_INTERVAL_S": "0",
|
|
23
|
+
"THREADKEEPER_INGEST_CAP": "0",
|
|
24
|
+
"THREADKEEPER_SKILL_WATCH_INTERVAL_S": "0",
|
|
25
|
+
"THREADKEEPER_SPAWN_BUDGET_POLL_S": "0",
|
|
26
|
+
"THREADKEEPER_SEARCH_PROXY_POLL_S": "0",
|
|
27
|
+
"THREADKEEPER_MEMORY_GUARD_POLL_S": "0",
|
|
28
|
+
"THREADKEEPER_SHADOW_REVIEW_INTERVAL_S": "0",
|
|
29
|
+
"THREADKEEPER_CURATOR_INTERVAL_S": "0",
|
|
30
|
+
"THREADKEEPER_EXTRACT_INTERVAL_S": "0",
|
|
31
|
+
"THREADKEEPER_CANDIDATE_REVIEW_INTERVAL_S": "0",
|
|
32
|
+
"THREADKEEPER_PROBE_INTERVAL_S": "0",
|
|
33
|
+
"THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S": "0",
|
|
34
|
+
"THREADKEEPER_DISABLE_BG_DAEMONS": "1",
|
|
35
|
+
"THREADKEEPER_TASK_LOG_DIR": str(tmp_path / "tasks"),
|
|
36
|
+
"THREADKEEPER_CLIENT": "pytest",
|
|
37
|
+
"THREADKEEPER_FORCE_CID": _FAKE_CID,
|
|
38
|
+
"THREADKEEPER_NO_EMBEDDINGS": "1",
|
|
39
|
+
}
|
|
40
|
+
for key, value in env.items():
|
|
41
|
+
monkeypatch.setenv(key, value)
|
|
42
|
+
Path(env["CLAUDE_PROJECTS_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
43
|
+
for name in [m for m in list(sys.modules) if m.startswith("threadkeeper")]:
|
|
44
|
+
del sys.modules[name]
|
|
45
|
+
import threadkeeper.server # noqa: F401
|
|
46
|
+
from threadkeeper import _mcp, db
|
|
47
|
+
|
|
48
|
+
return {"mcp": _mcp.mcp, "db": db}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _tool(pkg, name):
|
|
52
|
+
return pkg["mcp"]._tool_manager._tools[name].fn
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _add_evolve(conn, suggestion):
|
|
56
|
+
conn.execute(
|
|
57
|
+
"INSERT INTO evolve (suggestion, applied, status, created_at) "
|
|
58
|
+
"VALUES (?,?,?,?)",
|
|
59
|
+
(suggestion, 0, "promoted", int(time.time())),
|
|
60
|
+
)
|
|
61
|
+
conn.commit()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_brief_surfaces_lesson_patch_and_preserves_existing_sections(
|
|
65
|
+
tmp_path, monkeypatch,
|
|
66
|
+
):
|
|
67
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
68
|
+
conn = pkg["db"].get_db()
|
|
69
|
+
lesson_append = _tool(pkg, "lesson_append")
|
|
70
|
+
open_thread = _tool(pkg, "open_thread")
|
|
71
|
+
|
|
72
|
+
lesson_append(
|
|
73
|
+
title="repair stale cross-links",
|
|
74
|
+
body="Use the current cross-link target.",
|
|
75
|
+
summary="Keep links current.",
|
|
76
|
+
source="foreground",
|
|
77
|
+
)
|
|
78
|
+
open_thread(question="a seeded open thread that must survive")
|
|
79
|
+
_add_evolve(conn, "a promoted suggestion that must surface")
|
|
80
|
+
|
|
81
|
+
from threadkeeper.brief import render_brief
|
|
82
|
+
|
|
83
|
+
text = render_brief(conn)
|
|
84
|
+
|
|
85
|
+
# New: a precise, atomic lesson-edit action is available at session start.
|
|
86
|
+
assert "lesson_patch" in text
|
|
87
|
+
assert "lesson_patch(slug, old_string, new_string)" in text
|
|
88
|
+
assert "unique lesson-body substring" in text
|
|
89
|
+
|
|
90
|
+
# Existing: the added section leaves the surrounding brief intact.
|
|
91
|
+
assert "open" in text
|
|
92
|
+
assert "a seeded open thread that must survive" in text
|
|
93
|
+
assert "evolve_pending" in text
|
|
94
|
+
assert "★" in text
|
|
95
|
+
assert "a promoted suggestion that must surface" in text
|
|
96
|
+
assert "user-facing" in text
|
|
97
|
+
assert "Do NOT cite internal IDs" in text
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Golden coverage for lesson recommendation reconciliation in brief()."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_FAKE_CID = "bbbb3333-4444-5555-6666-777788889999"
|
|
10
|
+
_OPEN_Q = "seeded open thread remains visible"
|
|
11
|
+
_OLD_LESSON = "aws-sso-preflight-uses-sts"
|
|
12
|
+
_NEW_LESSON = "aws-sso-token-freshness-sts-is-cached"
|
|
13
|
+
_COMMAND = "aws sts get-caller-identity"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _bootstrap(tmp_path, monkeypatch):
|
|
17
|
+
env = {
|
|
18
|
+
"THREADKEEPER_DB": str(tmp_path / "db.sqlite"),
|
|
19
|
+
"CLAUDE_PROJECTS_DIR": str(tmp_path / "fake_claude_projects"),
|
|
20
|
+
"THREADKEEPER_LESSONS": str(tmp_path / "lessons.md"),
|
|
21
|
+
"THREADKEEPER_DISABLE_BG_DAEMONS": "1",
|
|
22
|
+
"THREADKEEPER_INGEST_INTERVAL_S": "0",
|
|
23
|
+
"THREADKEEPER_INGEST_CAP": "0",
|
|
24
|
+
"THREADKEEPER_SKILL_WATCH_INTERVAL_S": "0",
|
|
25
|
+
"THREADKEEPER_SPAWN_BUDGET_POLL_S": "0",
|
|
26
|
+
"THREADKEEPER_SEARCH_PROXY_POLL_S": "0",
|
|
27
|
+
"THREADKEEPER_MEMORY_GUARD_POLL_S": "0",
|
|
28
|
+
"THREADKEEPER_SHADOW_REVIEW_INTERVAL_S": "0",
|
|
29
|
+
"THREADKEEPER_CURATOR_INTERVAL_S": "0",
|
|
30
|
+
"THREADKEEPER_EXTRACT_INTERVAL_S": "0",
|
|
31
|
+
"THREADKEEPER_CANDIDATE_REVIEW_INTERVAL_S": "0",
|
|
32
|
+
"THREADKEEPER_PROBE_INTERVAL_S": "0",
|
|
33
|
+
"THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S": "0",
|
|
34
|
+
"THREADKEEPER_THREAD_JANITOR_INTERVAL_S": "0",
|
|
35
|
+
"THREADKEEPER_BRIEF_LEAN": "0",
|
|
36
|
+
"THREADKEEPER_TASK_LOG_DIR": str(tmp_path / "tasks"),
|
|
37
|
+
"THREADKEEPER_CLIENT": "pytest",
|
|
38
|
+
"THREADKEEPER_FORCE_CID": _FAKE_CID,
|
|
39
|
+
"THREADKEEPER_NO_EMBEDDINGS": "1",
|
|
40
|
+
}
|
|
41
|
+
for key, value in env.items():
|
|
42
|
+
monkeypatch.setenv(key, value)
|
|
43
|
+
Path(env["CLAUDE_PROJECTS_DIR"]).mkdir(parents=True, exist_ok=True)
|
|
44
|
+
for name in [module for module in list(sys.modules) if module.startswith("threadkeeper")]:
|
|
45
|
+
del sys.modules[name]
|
|
46
|
+
import threadkeeper.server # noqa: F401
|
|
47
|
+
from threadkeeper import _mcp, db, identity, lessons
|
|
48
|
+
return {"mcp": _mcp.mcp, "db": db, "identity": identity, "lessons": lessons}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _tool(pkg, name):
|
|
52
|
+
return pkg["mcp"]._tool_manager._tools[name].fn
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _add_evolve(conn, suggestion):
|
|
56
|
+
conn.execute(
|
|
57
|
+
"INSERT INTO evolve (suggestion, applied, status, created_at) "
|
|
58
|
+
"VALUES (?,?,?,?)",
|
|
59
|
+
(suggestion, 0, "promoted", int(time.time())),
|
|
60
|
+
)
|
|
61
|
+
conn.commit()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_brief_surfaces_lesson_reconciliation_and_keeps_existing_sections(
|
|
65
|
+
tmp_path, monkeypatch,
|
|
66
|
+
):
|
|
67
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
68
|
+
conn = pkg["db"].get_db()
|
|
69
|
+
|
|
70
|
+
pkg["lessons"].append_lesson(
|
|
71
|
+
_OLD_LESSON,
|
|
72
|
+
body=(
|
|
73
|
+
f"Run `{_COMMAND}` to gate an AWS SSO freshness check before "
|
|
74
|
+
"using fixture-backed cloud tests."
|
|
75
|
+
),
|
|
76
|
+
source="shadow",
|
|
77
|
+
)
|
|
78
|
+
pkg["lessons"].append_lesson(
|
|
79
|
+
_NEW_LESSON,
|
|
80
|
+
body=(
|
|
81
|
+
f"Never gate SSO freshness on `{_COMMAND}`: it is cached and can "
|
|
82
|
+
"return success after the token expires."
|
|
83
|
+
),
|
|
84
|
+
source="shadow",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
_tool(pkg, "open_thread")(question=_OPEN_Q)
|
|
88
|
+
_add_evolve(conn, "a promoted suggestion that must remain visible")
|
|
89
|
+
|
|
90
|
+
from threadkeeper.brief import render_brief
|
|
91
|
+
text = render_brief(conn)
|
|
92
|
+
|
|
93
|
+
# New behavior: a newer debunk flags the older recommendation by command
|
|
94
|
+
# and lesson slug, with a concrete reconciliation action.
|
|
95
|
+
assert "lesson_reconciliation" in text
|
|
96
|
+
assert f"`{_COMMAND}`" in text
|
|
97
|
+
assert f"debunked_by={_NEW_LESSON}" in text
|
|
98
|
+
assert f"still_recommended_by={_OLD_LESSON}" in text
|
|
99
|
+
assert "patch its recommendation or cross-link the correcting lesson" in text
|
|
100
|
+
|
|
101
|
+
# Regression: the established brief sections still survive this addition.
|
|
102
|
+
assert "open" in text
|
|
103
|
+
assert _OPEN_Q in text
|
|
104
|
+
assert "evolve_pending" in text
|
|
105
|
+
assert "★ \"a promoted suggestion that must remain visible\"" in text
|
|
106
|
+
assert "user-facing: paraphrase plain" in text
|
|
107
|
+
assert "Do NOT cite internal IDs" in text
|
|
@@ -158,10 +158,9 @@ def test_audit_threadkeeper_prompt_renders(fresh_mp):
|
|
|
158
158
|
# ──────────────────────────────────────────────────────────────────────
|
|
159
159
|
|
|
160
160
|
def test_server_advertises_resources_and_prompts_capabilities(fresh_mp):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
caps = lowlevel.get_capabilities(NotificationOptions(), {})
|
|
161
|
+
caps = fresh_mp["mcp"]._lowlevel_server.get_capabilities(
|
|
162
|
+
NotificationOptions(), {}, protocol_version="2026-07-28",
|
|
163
|
+
)
|
|
165
164
|
assert caps.resources is not None
|
|
166
165
|
assert caps.prompts is not None
|
|
167
166
|
assert caps.tools is not None # tools unaffected
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
"""MCP SDK
|
|
1
|
+
"""MCP SDK 2.x stdio-server smoke coverage.
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
is available. CI runs it once per supported SDK-major/Python combination.
|
|
3
|
+
The stable Skills extension requires MCP SDK 2.2 or later. This test starts
|
|
4
|
+
the real ``python -m threadkeeper.server`` process and verifies that the public
|
|
5
|
+
tools registry remains available over stdio on the supported SDK surface.
|
|
7
6
|
"""
|
|
8
7
|
from __future__ import annotations
|
|
9
8
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Protocol-level coverage for the stable MCP Skills extension (#336)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import hashlib
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from mcp.server.mcpserver.exceptions import ResourceNotFoundError
|
|
10
|
+
from mcp.shared.exceptions import MCPError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_STABLE_PROTOCOL = "2026-07-28"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _tool(pkg, name):
|
|
17
|
+
return pkg["mcp"]._tool_manager._tools[name].fn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _write_skill(pkg, name: str, *, extra_frontmatter: str = ""):
|
|
21
|
+
skill_dir = pkg["config"].CLAUDE_SKILLS_DIR / name
|
|
22
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
(skill_dir / "SKILL.md").write_text(
|
|
24
|
+
"---\n"
|
|
25
|
+
f"name: {name}\n"
|
|
26
|
+
f"description: {name} description\n"
|
|
27
|
+
f"{extra_frontmatter}"
|
|
28
|
+
"---\n\n"
|
|
29
|
+
f"# {name}\n",
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
return skill_dir
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _call_extension(pkg, method: str, params):
|
|
36
|
+
entry = pkg["mcp"]._lowlevel_server._request_handlers[method]
|
|
37
|
+
return asyncio.run(
|
|
38
|
+
entry.handler(SimpleNamespace(protocol_version=_STABLE_PROTOCOL), params)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_discovery_advertises_stable_skills_extension_without_changing_core_contracts(fresh_mp):
|
|
43
|
+
from mcp.server.lowlevel.server import NotificationOptions
|
|
44
|
+
from threadkeeper.mcp_skills import SKILLS_EXTENSION_ID
|
|
45
|
+
|
|
46
|
+
server = fresh_mp["mcp"]._lowlevel_server
|
|
47
|
+
caps = server.get_capabilities(
|
|
48
|
+
NotificationOptions(), {}, protocol_version=_STABLE_PROTOCOL,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert caps.extensions == {SKILLS_EXTENSION_ID: {}}
|
|
52
|
+
assert caps.resources is not None
|
|
53
|
+
assert caps.prompts is not None
|
|
54
|
+
assert caps.tools is not None
|
|
55
|
+
assert {"skills/list", "skills/get"} <= set(server._request_handlers)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_list_paginates_in_uri_order_and_keeps_the_server_origin_in_each_uri(fresh_mp):
|
|
59
|
+
from threadkeeper.mcp_skills import ListSkillsParams, SKILL_LIST_PAGE_SIZE
|
|
60
|
+
|
|
61
|
+
for index in range(SKILL_LIST_PAGE_SIZE + 1):
|
|
62
|
+
_write_skill(fresh_mp, f"page-{index:03d}")
|
|
63
|
+
|
|
64
|
+
first = _call_extension(fresh_mp, "skills/list", ListSkillsParams())
|
|
65
|
+
second = _call_extension(
|
|
66
|
+
fresh_mp, "skills/list", ListSkillsParams(cursor=first["nextCursor"]),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
first_uris = [entry["uri"] for entry in first["skills"]]
|
|
70
|
+
second_uris = [entry["uri"] for entry in second["skills"]]
|
|
71
|
+
assert first["resultType"] == second["resultType"] == "complete"
|
|
72
|
+
assert len(first_uris) == SKILL_LIST_PAGE_SIZE
|
|
73
|
+
assert first_uris == sorted(first_uris)
|
|
74
|
+
assert first_uris + second_uris == sorted(first_uris + second_uris)
|
|
75
|
+
assert all(uri.startswith("skill://thread-keeper/") for uri in first_uris + second_uris)
|
|
76
|
+
# The authority is an origin namespace, so a same-named skill from another
|
|
77
|
+
# server cannot collapse into this server's entry.
|
|
78
|
+
assert "skill://another-server/page-000/SKILL.md" not in first_uris
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_get_returns_complete_manifest_and_read_bytes_match_each_digest(fresh_mp):
|
|
82
|
+
from threadkeeper.mcp_skills import GetSkillParams, skill_catalog
|
|
83
|
+
|
|
84
|
+
skill_dir = _write_skill(
|
|
85
|
+
fresh_mp,
|
|
86
|
+
"manifest-skill",
|
|
87
|
+
extra_frontmatter="license: MIT\nmetadata:\n owner: thread-keeper\n",
|
|
88
|
+
)
|
|
89
|
+
reference = skill_dir / "references" / "guide.txt"
|
|
90
|
+
reference.parent.mkdir()
|
|
91
|
+
reference.write_bytes(b"reference bytes\n")
|
|
92
|
+
|
|
93
|
+
entry = next(item for item in skill_catalog() if item.name == "manifest-skill")
|
|
94
|
+
result = _call_extension(
|
|
95
|
+
fresh_mp, "skills/get", GetSkillParams(uri=entry.uri),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
skill = result["skill"]
|
|
99
|
+
assert result["resultType"] == "complete"
|
|
100
|
+
assert skill["frontmatter"] == {
|
|
101
|
+
"name": "manifest-skill",
|
|
102
|
+
"description": "manifest-skill description",
|
|
103
|
+
"license": "MIT",
|
|
104
|
+
"metadata": {"owner": "thread-keeper"},
|
|
105
|
+
}
|
|
106
|
+
assert {item["uri"] for item in skill["resources"]} == {
|
|
107
|
+
item.uri for item in entry.files
|
|
108
|
+
}
|
|
109
|
+
listed_resources = {
|
|
110
|
+
str(resource.uri): resource
|
|
111
|
+
for resource in asyncio.run(fresh_mp["mcp"].list_resources())
|
|
112
|
+
}
|
|
113
|
+
listed_uris = set(listed_resources)
|
|
114
|
+
assert {item["uri"] for item in skill["resources"]} <= listed_uris
|
|
115
|
+
for file_entry in skill["resources"]:
|
|
116
|
+
assert listed_resources[file_entry["uri"]].size == file_entry["size"]
|
|
117
|
+
contents = asyncio.run(fresh_mp["mcp"].read_resource(file_entry["uri"]))
|
|
118
|
+
data = contents[0].content
|
|
119
|
+
assert isinstance(data, bytes)
|
|
120
|
+
assert len(data) == file_entry["size"]
|
|
121
|
+
assert f"sha256:{hashlib.sha256(data).hexdigest()}" == file_entry["digest"]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_get_rejects_unknown_or_non_skill_markdown_uri(fresh_mp):
|
|
125
|
+
from threadkeeper.mcp_skills import GetSkillParams
|
|
126
|
+
|
|
127
|
+
_write_skill(fresh_mp, "known-skill")
|
|
128
|
+
for uri in (
|
|
129
|
+
"skill://thread-keeper/known-skill/references/guide.md",
|
|
130
|
+
"skill://thread-keeper/missing/SKILL.md",
|
|
131
|
+
):
|
|
132
|
+
with pytest.raises(MCPError) as raised:
|
|
133
|
+
_call_extension(fresh_mp, "skills/get", GetSkillParams(uri=uri))
|
|
134
|
+
assert raised.value.error.code == -32602
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_resource_reads_reject_traversal_undeclared_and_oversized_files(fresh_mp):
|
|
138
|
+
from threadkeeper.mcp_skills import skill_catalog
|
|
139
|
+
|
|
140
|
+
skill_dir = _write_skill(fresh_mp, "safe-skill")
|
|
141
|
+
(skill_dir / "private.txt").write_text("not declared", encoding="utf-8")
|
|
142
|
+
bad_uris = (
|
|
143
|
+
"skill://thread-keeper/safe-skill/../SKILL.md",
|
|
144
|
+
"skill://thread-keeper/safe-skill/references/%2e%2e/SKILL.md",
|
|
145
|
+
"skill://thread-keeper/safe-skill/private.txt",
|
|
146
|
+
)
|
|
147
|
+
for uri in bad_uris:
|
|
148
|
+
with pytest.raises(ResourceNotFoundError):
|
|
149
|
+
asyncio.run(fresh_mp["mcp"].read_resource(uri))
|
|
150
|
+
|
|
151
|
+
large_dir = _write_skill(fresh_mp, "large-skill") / "references"
|
|
152
|
+
large_dir.mkdir()
|
|
153
|
+
(large_dir / "large.bin").write_bytes(b"x" * (1_048_576 + 1))
|
|
154
|
+
assert "large-skill" not in {entry.name for entry in skill_catalog()}
|
|
155
|
+
with pytest.raises(ResourceNotFoundError):
|
|
156
|
+
asyncio.run(
|
|
157
|
+
fresh_mp["mcp"].read_resource(
|
|
158
|
+
"skill://thread-keeper/large-skill/SKILL.md"
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_skill_delivery_is_side_effect_free_and_filesystem_mirrors_remain_fallback(fresh_mp):
|
|
164
|
+
from threadkeeper.tools.skills import _mirror_targets
|
|
165
|
+
|
|
166
|
+
create = _tool(fresh_mp, "skill_manage")
|
|
167
|
+
assert create(
|
|
168
|
+
action="create",
|
|
169
|
+
name="fallback-skill",
|
|
170
|
+
description="Proves filesystem mirrors remain available.",
|
|
171
|
+
content="# Fallback\n",
|
|
172
|
+
).startswith("ok path=")
|
|
173
|
+
|
|
174
|
+
targets = _mirror_targets("fallback-skill")
|
|
175
|
+
assert targets
|
|
176
|
+
assert all((target / "SKILL.md").exists() for target in targets)
|
|
177
|
+
|
|
178
|
+
conn = fresh_mp["db"].get_db()
|
|
179
|
+
before = conn.execute("SELECT COUNT(*) AS count FROM events").fetchone()["count"]
|
|
180
|
+
uri = "skill://thread-keeper/fallback-skill/SKILL.md"
|
|
181
|
+
assert asyncio.run(fresh_mp["mcp"].read_resource(uri))[0].content.startswith(b"---")
|
|
182
|
+
after = conn.execute("SELECT COUNT(*) AS count FROM events").fetchone()["count"]
|
|
183
|
+
assert after == before
|
|
@@ -98,29 +98,25 @@ def test_glama_dockerfile_pin_matches_the_project_release():
|
|
|
98
98
|
assert f"threadkeeper=={version}" in DOCKERFILE.read_text()
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
def
|
|
101
|
+
def test_mcp_requirement_uses_the_2x_skills_extension_surface():
|
|
102
102
|
deps = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"]["dependencies"]
|
|
103
103
|
mcp_req = next(d for d in deps if d.split(">")[0].split("<")[0].strip() == "mcp")
|
|
104
|
-
assert mcp_req == "mcp>=
|
|
104
|
+
assert mcp_req == "mcp>=2.2.0,<3"
|
|
105
105
|
|
|
106
|
-
adapter = (ROOT / "threadkeeper" / "_mcp.py").read_text()
|
|
107
|
-
assert "mcp.server.mcpserver" in adapter
|
|
108
|
-
assert "mcp.server.fastmcp" in adapter
|
|
109
106
|
for importer in (
|
|
107
|
+
ROOT / "threadkeeper" / "_mcp.py",
|
|
110
108
|
ROOT / "threadkeeper" / "elicitation.py",
|
|
111
109
|
ROOT / "threadkeeper" / "tools" / "dialectic.py",
|
|
112
110
|
):
|
|
113
111
|
text = importer.read_text()
|
|
114
112
|
assert "mcp.server.fastmcp" not in text
|
|
115
|
-
|
|
113
|
+
assert "mcp.server.mcpserver" in (ROOT / "threadkeeper" / "_mcp.py").read_text()
|
|
116
114
|
|
|
117
115
|
|
|
118
|
-
def
|
|
116
|
+
def test_ci_matrix_covers_the_supported_mcp_sdk_surface():
|
|
119
117
|
matrix = _workflow("test.yml")["jobs"]["pytest-shard"]["strategy"]["matrix"]
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
assert majors == {">=1.10.0,<2", ">=2.0.0,<3"}
|
|
118
|
+
assert matrix["mcp"] == [">=2.2.0,<3"]
|
|
119
|
+
assert "include" not in matrix
|
|
124
120
|
assert '"mcp${{ matrix.mcp }}"' in _workflow_text("test.yml")
|
|
125
121
|
|
|
126
122
|
def test_ci_security_scanning_covers_code_and_resolved_dependencies():
|