threadkeeper 0.17.6__tar.gz → 0.17.7__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.6 → threadkeeper-0.17.7}/PKG-INFO +15 -6
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/README.md +14 -5
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/pyproject.toml +1 -1
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_agent_status.py +39 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_daemon_liveness.py +4 -2
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_evolve_daemon.py +162 -17
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_watchdog.py +36 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_tool_annotations.py +5 -2
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/agent_metadata.py +1 -1
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/agent_status.py +4 -4
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/db.py +23 -2
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/evolve_daemon.py +348 -49
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/server.py +1 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/spawn_budget.py +7 -5
- threadkeeper-0.17.7/threadkeeper/tools/evolve_research.py +22 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/spawn.py +16 -3
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper.egg-info/PKG-INFO +15 -6
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper.egg-info/SOURCES.txt +1 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/LICENSE +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/setup.cfg +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_adapters.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_backup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_concepts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_curator.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_forget.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_helpers.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_host_env.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_host_main.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_identity.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_lessons.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_nudges.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_panel.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_permissions.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_pickup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_process_health.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_retention.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_setup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_skills.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_threads.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/config.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/host.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/link_health.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.17.6 → threadkeeper-0.17.7}/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.7
|
|
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
|
|
@@ -354,7 +354,10 @@ repair partial work, and continue rather than restart blindly.
|
|
|
354
354
|
`THREADKEEPER_SPAWN_TIMEOUT_RETRY_LIMIT` (default 3; 0 disables) bounds the
|
|
355
355
|
retry chain, with `THREADKEEPER_SPAWN_TIMEOUT_RETRY_DELAY_S` available for a
|
|
356
356
|
non-zero delay. Timed-out children are surfaced as `tasks_timed_out` in
|
|
357
|
-
`mp_dashboard` and `timed_out` in `agent_status`.
|
|
357
|
+
`mp_dashboard` and `timed_out` in `agent_status`. `agent_status` and
|
|
358
|
+
`tk-agent-status` are observation-only and never terminate or respawn a
|
|
359
|
+
child; timeout enforcement and the continuation retry live solely in the
|
|
360
|
+
spawn-budget daemon.
|
|
358
361
|
|
|
359
362
|
`tk-agent-status` exposes autonomous learning loop status as structured JSON
|
|
360
363
|
or compact text for external monitors:
|
|
@@ -868,10 +871,16 @@ runs as **two alternating phases**, never co-granting web research and
|
|
|
868
871
|
shell/`bypassPermissions` to the same child:
|
|
869
872
|
|
|
870
873
|
- **research phase** — a read-only child with `WebSearch`/`WebFetch` and
|
|
871
|
-
read-only repo reads but **no shell, no `
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
874
|
+
read-only repo reads but **no shell, no generic `Write`, no
|
|
875
|
+
`bypassPermissions`, and no GitHub access**. Before dispatch, the parent
|
|
876
|
+
registers one pass ID, owner child, and digest target. The child can submit
|
|
877
|
+
only that pass through `evolve_research_handoff(...)`; it cannot choose a
|
|
878
|
+
path. The handoff is one-shot, capped at 12,000 characters / 400 lines,
|
|
879
|
+
atomically persisted with a SHA-256, and records rejected, failed, expired,
|
|
880
|
+
and tampered outcomes in Evolve telemetry. The later audit reads only a
|
|
881
|
+
fresh accepted handoff whose final file still matches that hash. With no
|
|
882
|
+
`Bash`/`gh`/network-write tool it has no exfiltration channel, so the
|
|
883
|
+
untrusted pages it reads cannot act.
|
|
875
884
|
- **audit phase** — the privileged child (`bypassPermissions` + `Bash`/`Edit`/
|
|
876
885
|
`Write`) that audits the repo, opens the `docs/ROADMAP.md` PR, and creates or
|
|
877
886
|
updates GitHub issues. It holds **no web tools**; it consumes the research
|
|
@@ -310,7 +310,10 @@ repair partial work, and continue rather than restart blindly.
|
|
|
310
310
|
`THREADKEEPER_SPAWN_TIMEOUT_RETRY_LIMIT` (default 3; 0 disables) bounds the
|
|
311
311
|
retry chain, with `THREADKEEPER_SPAWN_TIMEOUT_RETRY_DELAY_S` available for a
|
|
312
312
|
non-zero delay. Timed-out children are surfaced as `tasks_timed_out` in
|
|
313
|
-
`mp_dashboard` and `timed_out` in `agent_status`.
|
|
313
|
+
`mp_dashboard` and `timed_out` in `agent_status`. `agent_status` and
|
|
314
|
+
`tk-agent-status` are observation-only and never terminate or respawn a
|
|
315
|
+
child; timeout enforcement and the continuation retry live solely in the
|
|
316
|
+
spawn-budget daemon.
|
|
314
317
|
|
|
315
318
|
`tk-agent-status` exposes autonomous learning loop status as structured JSON
|
|
316
319
|
or compact text for external monitors:
|
|
@@ -824,10 +827,16 @@ runs as **two alternating phases**, never co-granting web research and
|
|
|
824
827
|
shell/`bypassPermissions` to the same child:
|
|
825
828
|
|
|
826
829
|
- **research phase** — a read-only child with `WebSearch`/`WebFetch` and
|
|
827
|
-
read-only repo reads but **no shell, no `
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
830
|
+
read-only repo reads but **no shell, no generic `Write`, no
|
|
831
|
+
`bypassPermissions`, and no GitHub access**. Before dispatch, the parent
|
|
832
|
+
registers one pass ID, owner child, and digest target. The child can submit
|
|
833
|
+
only that pass through `evolve_research_handoff(...)`; it cannot choose a
|
|
834
|
+
path. The handoff is one-shot, capped at 12,000 characters / 400 lines,
|
|
835
|
+
atomically persisted with a SHA-256, and records rejected, failed, expired,
|
|
836
|
+
and tampered outcomes in Evolve telemetry. The later audit reads only a
|
|
837
|
+
fresh accepted handoff whose final file still matches that hash. With no
|
|
838
|
+
`Bash`/`gh`/network-write tool it has no exfiltration channel, so the
|
|
839
|
+
untrusted pages it reads cannot act.
|
|
831
840
|
- **audit phase** — the privileged child (`bypassPermissions` + `Bash`/`Edit`/
|
|
832
841
|
`Write`) that audits the repo, opens the `docs/ROADMAP.md` PR, and creates or
|
|
833
842
|
updates GitHub issues. It holds **no web tools**; it consumes the research
|
|
@@ -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.7"
|
|
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" }]
|
|
@@ -784,3 +784,42 @@ def test_recent_failures_notify_flag_off_when_toggle_disabled(
|
|
|
784
784
|
fails = {f["task_id"]: f for f in snap["recent_failures"]}
|
|
785
785
|
assert "deadchild2" in fails # still listed in the menu
|
|
786
786
|
assert fails["deadchild2"]["notify"] is False # but no banner posted
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_agent_status_tool_default_read_does_not_enforce_lifecycle(
|
|
790
|
+
fresh_mp, monkeypatch
|
|
791
|
+
):
|
|
792
|
+
pkg = fresh_mp
|
|
793
|
+
_insert_task(pkg, "tk_overdue", "Build a compact menu-bar status app.")
|
|
794
|
+
conn = pkg["db"].get_db()
|
|
795
|
+
conn.execute(
|
|
796
|
+
"UPDATE tasks SET started_at=? WHERE id=?",
|
|
797
|
+
(int(time.time()) - 10000, "tk_overdue"),
|
|
798
|
+
)
|
|
799
|
+
conn.commit()
|
|
800
|
+
|
|
801
|
+
import threadkeeper.spawn_budget as sb
|
|
802
|
+
|
|
803
|
+
reap_calls: list = []
|
|
804
|
+
respawn_calls: list = []
|
|
805
|
+
monkeypatch.setattr(
|
|
806
|
+
sb, "_reap_timed_out",
|
|
807
|
+
lambda conn, row, now: reap_calls.append(row["id"]) or True,
|
|
808
|
+
)
|
|
809
|
+
monkeypatch.setattr(
|
|
810
|
+
sb, "_respawn_timed_out",
|
|
811
|
+
lambda conn, row, age: respawn_calls.append(row["id"]),
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
result = _txt(_tool(pkg, "agent_status")())
|
|
815
|
+
|
|
816
|
+
assert reap_calls == []
|
|
817
|
+
assert respawn_calls == []
|
|
818
|
+
row = conn.execute(
|
|
819
|
+
"SELECT ended_at, return_code FROM tasks WHERE id='tk_overdue'"
|
|
820
|
+
).fetchone()
|
|
821
|
+
assert row["ended_at"] is None
|
|
822
|
+
assert row["return_code"] is None
|
|
823
|
+
count = conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0]
|
|
824
|
+
assert count == 1
|
|
825
|
+
assert "tk_overdue" in result or "agents=1" in result
|
|
@@ -14,7 +14,7 @@ def test_daemon_health_is_additive_without_schema_version_bump(mp_with_cid):
|
|
|
14
14
|
db = pkg["db"]
|
|
15
15
|
conn = db.get_db()
|
|
16
16
|
conn.execute("DROP TABLE daemon_health")
|
|
17
|
-
conn.execute("PRAGMA user_version =
|
|
17
|
+
conn.execute(f"PRAGMA user_version = {db.CURRENT_SCHEMA_VERSION}")
|
|
18
18
|
conn.commit()
|
|
19
19
|
conn.close()
|
|
20
20
|
|
|
@@ -23,7 +23,9 @@ def test_daemon_health_is_additive_without_schema_version_bump(mp_with_cid):
|
|
|
23
23
|
|
|
24
24
|
conn = db.get_db()
|
|
25
25
|
try:
|
|
26
|
-
assert conn.execute("PRAGMA user_version").fetchone()[0] ==
|
|
26
|
+
assert conn.execute("PRAGMA user_version").fetchone()[0] == (
|
|
27
|
+
db.CURRENT_SCHEMA_VERSION
|
|
28
|
+
)
|
|
27
29
|
assert conn.execute(
|
|
28
30
|
"SELECT 1 FROM sqlite_master "
|
|
29
31
|
"WHERE type='table' AND name='daemon_health'"
|
|
@@ -103,23 +103,49 @@ def _add_evolve(conn, suggestion, rationale=None, applied=0, status="pending"):
|
|
|
103
103
|
|
|
104
104
|
def _seed_research(pkg, conn, text="- idea: adopt Y\n sources: https://ex.com\n"):
|
|
105
105
|
"""Make run_evolve_pass take the AUDIT branch on its next call: record a
|
|
106
|
-
prior research spawn so _last_spawn_phase()=='research', and
|
|
107
|
-
|
|
106
|
+
prior research spawn so _last_spawn_phase()=='research', and create a
|
|
107
|
+
fresh, accepted handoff that the audit phase may fence into its prompt."""
|
|
108
108
|
now = int(time.time())
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
pass_id, f = pkg["ed"]._authorize_research_handoff(
|
|
110
|
+
conn, now, _FAKE_CID,
|
|
111
|
+
)
|
|
112
|
+
persisted = text.rstrip() + "\n"
|
|
113
|
+
f.write_text(persisted, encoding="utf-8")
|
|
114
|
+
digest = pkg["ed"].hashlib.sha256(persisted.encode("utf-8")).hexdigest()
|
|
115
|
+
conn.execute(
|
|
116
|
+
"UPDATE evolve_research_handoffs SET status='accepted', "
|
|
117
|
+
"content_sha256=?, content_chars=?, completed_at=? WHERE pass_id=?",
|
|
118
|
+
(digest, len(persisted), now, pass_id),
|
|
119
|
+
)
|
|
113
120
|
conn.execute(
|
|
114
121
|
"INSERT INTO events (session_id, kind, target, summary, created_at) "
|
|
115
122
|
"VALUES (?, 'evolve_review_pass', ?, ?, ?)",
|
|
116
|
-
("s_prev", str(now), "spawned research
|
|
123
|
+
("s_prev", str(now), "spawned research pass=seeded ok task=tk pid=1",
|
|
117
124
|
now),
|
|
118
125
|
)
|
|
119
126
|
conn.commit()
|
|
120
127
|
return f, text
|
|
121
128
|
|
|
122
129
|
|
|
130
|
+
def _authorize_researcher(pkg, conn, now=None):
|
|
131
|
+
"""Create the parent/task state a real researcher handoff requires."""
|
|
132
|
+
now = int(time.time()) if now is None else int(now)
|
|
133
|
+
pass_id, target = pkg["ed"]._authorize_research_handoff(
|
|
134
|
+
conn, now, _FAKE_CID,
|
|
135
|
+
)
|
|
136
|
+
conn.execute(
|
|
137
|
+
"INSERT INTO tasks "
|
|
138
|
+
"(id, pid, parent_cid, spawned_cid, cwd, prompt, started_at, role, write_origin) "
|
|
139
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
140
|
+
(
|
|
141
|
+
f"tk_research_{pass_id[-6:]}", 0, "parent", _FAKE_CID,
|
|
142
|
+
str(target.parent), "research", now, "evolve_researcher", "evolve",
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
conn.commit()
|
|
146
|
+
return pass_id, target
|
|
147
|
+
|
|
148
|
+
|
|
123
149
|
def test_audit_prompt_uses_paginated_issue_dedup(tmp_path, monkeypatch):
|
|
124
150
|
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
125
151
|
prompt = pkg["ed"].EVOLVE_AUDIT_PROMPT
|
|
@@ -475,7 +501,7 @@ def test_run_evolve_pass_force_spawns_research_first(
|
|
|
475
501
|
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
476
502
|
calls = {}
|
|
477
503
|
import threadkeeper.tools.spawn as spawn_mod
|
|
478
|
-
monkeypatch.setattr(spawn_mod, "
|
|
504
|
+
monkeypatch.setattr(spawn_mod, "_spawn_impl",
|
|
479
505
|
lambda **kw: calls.update(kw) or "ok task=tk_ev pid=1")
|
|
480
506
|
|
|
481
507
|
out = pkg["ed"].run_evolve_pass(force=True)
|
|
@@ -513,7 +539,7 @@ def test_run_evolve_pass_below_min(tmp_path, monkeypatch):
|
|
|
513
539
|
_add_evolve(conn, "only one")
|
|
514
540
|
calls = {}
|
|
515
541
|
import threadkeeper.tools.spawn as spawn_mod
|
|
516
|
-
monkeypatch.setattr(spawn_mod, "
|
|
542
|
+
monkeypatch.setattr(spawn_mod, "_spawn_impl",
|
|
517
543
|
lambda **kw: calls.update(kw) or "ok task=tk_ev pid=1")
|
|
518
544
|
|
|
519
545
|
out = pkg["ed"].run_evolve_pass(force=True)
|
|
@@ -539,7 +565,7 @@ def test_run_evolve_pass_skips_legacy_backlog_until_interval(
|
|
|
539
565
|
calls = {}
|
|
540
566
|
import threadkeeper.tools.spawn as spawn_mod
|
|
541
567
|
monkeypatch.setattr(
|
|
542
|
-
spawn_mod, "
|
|
568
|
+
spawn_mod, "_spawn_impl",
|
|
543
569
|
lambda **kw: calls.update(kw) or "ok task=tk_ev pid=1",
|
|
544
570
|
)
|
|
545
571
|
|
|
@@ -558,7 +584,7 @@ def test_run_evolve_pass_research_phase_is_read_only(tmp_path, monkeypatch):
|
|
|
558
584
|
_add_evolve(conn, "suggestion alpha")
|
|
559
585
|
calls = {}
|
|
560
586
|
import threadkeeper.tools.spawn as spawn_mod
|
|
561
|
-
monkeypatch.setattr(spawn_mod, "
|
|
587
|
+
monkeypatch.setattr(spawn_mod, "_spawn_impl",
|
|
562
588
|
lambda **kw: calls.update(kw) or "ok task=tk_ev pid=1")
|
|
563
589
|
out = pkg["ed"].run_evolve_pass(force=True)
|
|
564
590
|
assert out.startswith("spawned research")
|
|
@@ -567,11 +593,128 @@ def test_run_evolve_pass_research_phase_is_read_only(tmp_path, monkeypatch):
|
|
|
567
593
|
# web research yes; bypass/shell/GitHub-write no
|
|
568
594
|
assert "WebSearch" in calls["extra_allowed_tools"]
|
|
569
595
|
assert "WebFetch" in calls["extra_allowed_tools"]
|
|
596
|
+
assert "Write" not in calls["extra_allowed_tools"]
|
|
597
|
+
assert "evolve_research_handoff" in calls["extra_allowed_tools"]
|
|
598
|
+
assert calls["allowed_tools_override"] == (
|
|
599
|
+
"mcp__thread-keeper__broadcast",
|
|
600
|
+
)
|
|
570
601
|
assert calls["permission_mode"] != "bypassPermissions"
|
|
571
602
|
assert "Bash" not in calls["extra_allowed_tools"]
|
|
572
603
|
assert "Edit" not in calls["extra_allowed_tools"]
|
|
573
604
|
assert "evolve_decide" not in calls["extra_allowed_tools"]
|
|
574
605
|
assert pkg["ed"]._last_evolve_ts(conn) > 0
|
|
606
|
+
handoff = conn.execute(
|
|
607
|
+
"SELECT owner_cid, status, target_path FROM evolve_research_handoffs"
|
|
608
|
+
).fetchone()
|
|
609
|
+
assert handoff["owner_cid"] == calls["child_cid_override"]
|
|
610
|
+
assert handoff["status"] == "pending"
|
|
611
|
+
assert Path(handoff["target_path"]).name.startswith("RESEARCH-")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def test_research_handoff_accepts_only_registered_owner_and_target(
|
|
615
|
+
tmp_path, monkeypatch,
|
|
616
|
+
):
|
|
617
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
618
|
+
conn = pkg["db"].get_db()
|
|
619
|
+
pass_id, target = _authorize_researcher(pkg, conn)
|
|
620
|
+
sentinel = tmp_path / "source.py"
|
|
621
|
+
sentinel.write_text("unchanged\n", encoding="utf-8")
|
|
622
|
+
monkeypatch.setenv("THREADKEEPER_SPAWNED_CHILD", "1")
|
|
623
|
+
monkeypatch.setenv("THREADKEEPER_WRITE_ORIGIN", "evolve")
|
|
624
|
+
|
|
625
|
+
out = _tool(pkg, "evolve_research_handoff")(
|
|
626
|
+
pass_id=pass_id,
|
|
627
|
+
content="- idea: scoped handoff\n sources: https://example.test\n",
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
persisted = "- idea: scoped handoff\n sources: https://example.test\n"
|
|
631
|
+
assert out.startswith("ok pass_id=")
|
|
632
|
+
assert target.read_text(encoding="utf-8") == persisted
|
|
633
|
+
assert sentinel.read_text(encoding="utf-8") == "unchanged\n"
|
|
634
|
+
row = conn.execute(
|
|
635
|
+
"SELECT status, content_chars, content_sha256 FROM evolve_research_handoffs "
|
|
636
|
+
"WHERE pass_id=?", (pass_id,),
|
|
637
|
+
).fetchone()
|
|
638
|
+
assert row["status"] == "accepted"
|
|
639
|
+
assert row["content_chars"] == len(persisted)
|
|
640
|
+
assert row["content_sha256"] == pkg["ed"].hashlib.sha256(
|
|
641
|
+
persisted.encode("utf-8")
|
|
642
|
+
).hexdigest()
|
|
643
|
+
assert pkg["ed"]._latest_research(int(time.time())) == (target, persisted)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def test_research_handoff_rejects_wrong_owner_and_malformed_content(
|
|
647
|
+
tmp_path, monkeypatch,
|
|
648
|
+
):
|
|
649
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
650
|
+
conn = pkg["db"].get_db()
|
|
651
|
+
monkeypatch.setenv("THREADKEEPER_SPAWNED_CHILD", "1")
|
|
652
|
+
monkeypatch.setenv("THREADKEEPER_WRITE_ORIGIN", "evolve")
|
|
653
|
+
|
|
654
|
+
pass_id, target = _authorize_researcher(pkg, conn)
|
|
655
|
+
monkeypatch.setattr(pkg["ed"].identity, "_detect_self_cid", lambda: "other")
|
|
656
|
+
assert _tool(pkg, "evolve_research_handoff")(
|
|
657
|
+
pass_id=pass_id, content="- harmless\n",
|
|
658
|
+
) == "ERR research_handoff_not_authorized"
|
|
659
|
+
assert not target.exists()
|
|
660
|
+
assert conn.execute(
|
|
661
|
+
"SELECT status FROM evolve_research_handoffs WHERE pass_id=?", (pass_id,)
|
|
662
|
+
).fetchone()["status"] == "pending"
|
|
663
|
+
|
|
664
|
+
monkeypatch.setattr(pkg["ed"].identity, "_detect_self_cid", lambda: _FAKE_CID)
|
|
665
|
+
malformed_id, malformed_target = _authorize_researcher(pkg, conn)
|
|
666
|
+
out = _tool(pkg, "evolve_research_handoff")(
|
|
667
|
+
pass_id=malformed_id, content="bad\x00digest",
|
|
668
|
+
)
|
|
669
|
+
assert out == "ERR digest_malformed reason=malformed_nul"
|
|
670
|
+
malformed = conn.execute(
|
|
671
|
+
"SELECT status, failure FROM evolve_research_handoffs WHERE pass_id=?",
|
|
672
|
+
(malformed_id,),
|
|
673
|
+
).fetchone()
|
|
674
|
+
assert malformed["status"] == "failed"
|
|
675
|
+
assert malformed["failure"] == "malformed_nul"
|
|
676
|
+
assert not malformed_target.exists()
|
|
677
|
+
telemetry = conn.execute(
|
|
678
|
+
"SELECT summary FROM events WHERE kind=? AND summary LIKE ? "
|
|
679
|
+
"ORDER BY id DESC LIMIT 1",
|
|
680
|
+
(pkg["ed"].EVOLVE_RESEARCH_HANDOFF_KIND, f"pass_id={malformed_id}%"),
|
|
681
|
+
).fetchone()["summary"]
|
|
682
|
+
assert "status=failed reason=malformed_nul" in telemetry
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def test_research_handoff_rejects_oversize_and_audit_rejects_tampering(
|
|
686
|
+
tmp_path, monkeypatch,
|
|
687
|
+
):
|
|
688
|
+
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
689
|
+
conn = pkg["db"].get_db()
|
|
690
|
+
monkeypatch.setenv("THREADKEEPER_SPAWNED_CHILD", "1")
|
|
691
|
+
monkeypatch.setenv("THREADKEEPER_WRITE_ORIGIN", "evolve")
|
|
692
|
+
|
|
693
|
+
oversize_id, _ = _authorize_researcher(pkg, conn)
|
|
694
|
+
out = _tool(pkg, "evolve_research_handoff")(
|
|
695
|
+
pass_id=oversize_id,
|
|
696
|
+
content="x" * (pkg["ed"].EVOLVE_RESEARCH_MAX_CHARS + 1),
|
|
697
|
+
)
|
|
698
|
+
assert out == (
|
|
699
|
+
f"ERR digest_too_large max_chars={pkg['ed'].EVOLVE_RESEARCH_MAX_CHARS}"
|
|
700
|
+
)
|
|
701
|
+
assert conn.execute(
|
|
702
|
+
"SELECT status, failure FROM evolve_research_handoffs WHERE pass_id=?",
|
|
703
|
+
(oversize_id,),
|
|
704
|
+
).fetchone()["failure"] == "too_large"
|
|
705
|
+
|
|
706
|
+
pass_id, target = _authorize_researcher(pkg, conn)
|
|
707
|
+
assert _tool(pkg, "evolve_research_handoff")(
|
|
708
|
+
pass_id=pass_id, content="- trusted before replacement\n",
|
|
709
|
+
).startswith("ok pass_id=")
|
|
710
|
+
target.write_text("- replacement after handoff\n", encoding="utf-8")
|
|
711
|
+
assert pkg["ed"]._latest_research(int(time.time())) == (None, "")
|
|
712
|
+
tampered = conn.execute(
|
|
713
|
+
"SELECT status, failure FROM evolve_research_handoffs WHERE pass_id=?",
|
|
714
|
+
(pass_id,),
|
|
715
|
+
).fetchone()
|
|
716
|
+
assert tampered["status"] == "tampered"
|
|
717
|
+
assert tampered["failure"] == "sha256_mismatch"
|
|
575
718
|
|
|
576
719
|
|
|
577
720
|
def test_run_evolve_pass_audit_phase_no_web_consumes_fenced_research(
|
|
@@ -708,10 +851,12 @@ def test_web_research_and_privileged_write_never_cogranted(tmp_path, monkeypatch
|
|
|
708
851
|
_add_evolve(conn, "suggestion alpha")
|
|
709
852
|
captured = []
|
|
710
853
|
import threadkeeper.tools.spawn as spawn_mod
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
854
|
+
def capture(**kw):
|
|
855
|
+
captured.append(dict(kw))
|
|
856
|
+
return "ok task=tk_ev pid=1"
|
|
857
|
+
|
|
858
|
+
monkeypatch.setattr(spawn_mod, "spawn", capture)
|
|
859
|
+
monkeypatch.setattr(spawn_mod, "_spawn_impl", capture)
|
|
715
860
|
|
|
716
861
|
# Phase 1 records a real "spawned research" pass, so phase 2 takes the audit
|
|
717
862
|
# branch on the next forced call.
|
|
@@ -748,7 +893,7 @@ def test_run_evolve_pass_runs_reviewer_in_repo_root(tmp_path, monkeypatch):
|
|
|
748
893
|
monkeypatch.setattr(pkg["ed"], "_ensure_repo_ready", lambda: (repo, ""))
|
|
749
894
|
calls = {}
|
|
750
895
|
import threadkeeper.tools.spawn as spawn_mod
|
|
751
|
-
monkeypatch.setattr(spawn_mod, "
|
|
896
|
+
monkeypatch.setattr(spawn_mod, "_spawn_impl",
|
|
752
897
|
lambda **kw: calls.update(kw) or "ok task=tk_ev pid=1")
|
|
753
898
|
|
|
754
899
|
out = pkg["ed"].run_evolve_pass(force=True)
|
|
@@ -837,7 +982,7 @@ def test_run_evolve_pass_single_flight_lock_race(tmp_path, monkeypatch):
|
|
|
837
982
|
errors.append(e)
|
|
838
983
|
release_spawn.set()
|
|
839
984
|
|
|
840
|
-
monkeypatch.setattr(spawn_mod, "
|
|
985
|
+
monkeypatch.setattr(spawn_mod, "_spawn_impl", fake_spawn)
|
|
841
986
|
|
|
842
987
|
t = threading.Thread(target=run_pass)
|
|
843
988
|
t.start()
|
|
@@ -402,3 +402,39 @@ def test_agent_status_reports_timed_out(mp_with_cid, monkeypatch):
|
|
|
402
402
|
snap = agent_status_snapshot(refresh=False)
|
|
403
403
|
assert snap["timed_out_count"] == 1
|
|
404
404
|
assert "timed_out=1" in format_agent_status(snap)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_refresh_all_running_enforce_false_skips_kill_and_respawn(
|
|
408
|
+
mp_with_cid, monkeypatch
|
|
409
|
+
):
|
|
410
|
+
monkeypatch.setenv("THREADKEEPER_SPAWN_MAX_RUNTIME_S", "60")
|
|
411
|
+
pkg = mp_with_cid(_FAKE_CID)
|
|
412
|
+
|
|
413
|
+
import threadkeeper.spawn_budget as sb
|
|
414
|
+
|
|
415
|
+
reap_calls: list = []
|
|
416
|
+
respawn_calls: list = []
|
|
417
|
+
monkeypatch.setattr(
|
|
418
|
+
sb, "_reap_timed_out",
|
|
419
|
+
lambda conn, row, now: reap_calls.append(row["id"]) or True,
|
|
420
|
+
)
|
|
421
|
+
monkeypatch.setattr(
|
|
422
|
+
sb, "_respawn_timed_out",
|
|
423
|
+
lambda conn, row, age: respawn_calls.append(row["id"]),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
old = int(time.time()) - 200
|
|
427
|
+
conn = _insert_running(pkg, "tk_enforce", os.getpid(), old)
|
|
428
|
+
|
|
429
|
+
sb._refresh_all_running(conn, enforce=False)
|
|
430
|
+
|
|
431
|
+
assert reap_calls == []
|
|
432
|
+
assert respawn_calls == []
|
|
433
|
+
row = conn.execute(
|
|
434
|
+
"SELECT ended_at FROM tasks WHERE id='tk_enforce'"
|
|
435
|
+
).fetchone()
|
|
436
|
+
assert row["ended_at"] is None
|
|
437
|
+
|
|
438
|
+
sb._refresh_all_running(conn)
|
|
439
|
+
|
|
440
|
+
assert len(reap_calls) == 1
|
|
@@ -60,7 +60,8 @@ WRITE_TOOLS = {
|
|
|
60
60
|
"evolve_apply_roadmap_issue", "evolve_decide",
|
|
61
61
|
"evolve_format", "evolve_issue_create", "evolve_mark_applied",
|
|
62
62
|
"evolve_mark_curator_report_applied",
|
|
63
|
-
"evolve_mark_roadmap_issue_applied", "
|
|
63
|
+
"evolve_mark_roadmap_issue_applied", "evolve_research_handoff",
|
|
64
|
+
"export_distillates", "extract_recent",
|
|
64
65
|
"idle_thread", "inbox", "ingest", "lesson_append", "lesson_restore",
|
|
65
66
|
"link", "live_status", "lesson_get", "lesson_list", "mark_skill_materialized",
|
|
66
67
|
"memory_guard_reclaim", "note",
|
|
@@ -155,7 +156,9 @@ def test_status_tools_emit_validating_structured_content(fresh_mp, name):
|
|
|
155
156
|
assert tool.output_schema is not None, f"{name} must advertise outputSchema"
|
|
156
157
|
|
|
157
158
|
fn = pkg["mcp"]._tool_manager._tools[name].fn
|
|
158
|
-
|
|
159
|
+
# agent_status is now side-effect-free under its default, so the
|
|
160
|
+
# annotation test exercises the default path.
|
|
161
|
+
kwargs: dict = {}
|
|
159
162
|
result = fn(**kwargs)
|
|
160
163
|
|
|
161
164
|
# legacy human-readable text block preserved
|
|
@@ -90,7 +90,7 @@ AGENT_ROLES: Final[tuple[dict[str, str], ...]] = (
|
|
|
90
90
|
"evolve reviewer before a roadmap decision."
|
|
91
91
|
),
|
|
92
92
|
"reads": "Repository code, documentation, and research sources",
|
|
93
|
-
"writes": "
|
|
93
|
+
"writes": "One parent-authorized, pass-scoped research digest",
|
|
94
94
|
"impact": "read_only",
|
|
95
95
|
"interval_key": "THREADKEEPER_EVOLVE_REVIEW_INTERVAL_S",
|
|
96
96
|
},
|
|
@@ -330,15 +330,15 @@ def _status_for(pid: int | None, ended_at: int | None) -> str:
|
|
|
330
330
|
|
|
331
331
|
|
|
332
332
|
def _refresh_rss(conn) -> None:
|
|
333
|
-
"""Refresh task liveness/RSS
|
|
333
|
+
"""Refresh task liveness/RSS in observation-only mode.
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
Status reads measure only and never kill or respawn; lifecycle
|
|
336
|
+
enforcement stays daemon-owned.
|
|
337
337
|
"""
|
|
338
338
|
try:
|
|
339
339
|
from .spawn_budget import _refresh_all_running
|
|
340
340
|
|
|
341
|
-
_refresh_all_running(conn)
|
|
341
|
+
_refresh_all_running(conn, enforce=False)
|
|
342
342
|
except Exception:
|
|
343
343
|
# A status widget should degrade to the last cached RSS instead of
|
|
344
344
|
# failing when ps is briefly unavailable or the DB is locked.
|
|
@@ -35,7 +35,7 @@ __all__ = [
|
|
|
35
35
|
"SCHEMA",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
-
CURRENT_SCHEMA_VERSION =
|
|
38
|
+
CURRENT_SCHEMA_VERSION = 5
|
|
39
39
|
|
|
40
40
|
# sqlite-vec extension state. We probe once at first get_db() call and
|
|
41
41
|
# cache the verdict. _VEC_AVAILABLE = True means vec0 virtual tables work
|
|
@@ -556,6 +556,25 @@ CREATE TABLE IF NOT EXISTS evolve_issues (
|
|
|
556
556
|
created_at INTEGER NOT NULL
|
|
557
557
|
);
|
|
558
558
|
|
|
559
|
+
-- Parent-authorized handoffs from an unprivileged Evolve web-research child.
|
|
560
|
+
-- The child never chooses a path: it can only submit content for the exact
|
|
561
|
+
-- pass row created before it was spawned. The audit phase accepts only a
|
|
562
|
+
-- completed row whose final on-disk SHA-256 still matches this record.
|
|
563
|
+
CREATE TABLE IF NOT EXISTS evolve_research_handoffs (
|
|
564
|
+
pass_id TEXT PRIMARY KEY,
|
|
565
|
+
target_path TEXT NOT NULL UNIQUE,
|
|
566
|
+
owner_cid TEXT NOT NULL,
|
|
567
|
+
authorized_at INTEGER NOT NULL,
|
|
568
|
+
expires_at INTEGER NOT NULL,
|
|
569
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
570
|
+
'pending', 'writing', 'accepted', 'failed', 'tampered', 'expired'
|
|
571
|
+
)),
|
|
572
|
+
content_sha256 TEXT,
|
|
573
|
+
content_chars INTEGER,
|
|
574
|
+
completed_at INTEGER,
|
|
575
|
+
failure TEXT
|
|
576
|
+
);
|
|
577
|
+
|
|
559
578
|
CREATE INDEX IF NOT EXISTS idx_notes_thread ON notes(thread_id);
|
|
560
579
|
CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at DESC);
|
|
561
580
|
CREATE INDEX IF NOT EXISTS idx_threads_state ON threads(state);
|
|
@@ -582,6 +601,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_evolve_issues_fingerprint
|
|
|
582
601
|
ON evolve_issues(fingerprint);
|
|
583
602
|
CREATE INDEX IF NOT EXISTS idx_evolve_issues_hash
|
|
584
603
|
ON evolve_issues(content_hash);
|
|
604
|
+
CREATE INDEX IF NOT EXISTS idx_evolve_research_handoffs_ready
|
|
605
|
+
ON evolve_research_handoffs(status, completed_at DESC);
|
|
585
606
|
CREATE INDEX IF NOT EXISTS idx_evolve_issues_number
|
|
586
607
|
ON evolve_issues(issue_number);
|
|
587
608
|
CREATE INDEX IF NOT EXISTS idx_probes_category ON probes(category);
|
|
@@ -925,7 +946,7 @@ def _rebuild_dialog_fts_if_needed(conn: sqlite3.Connection) -> None:
|
|
|
925
946
|
|
|
926
947
|
|
|
927
948
|
def _run_schema_migrations(conn: sqlite3.Connection, from_version: int) -> None:
|
|
928
|
-
if from_version not in (0, 1, 2, 3):
|
|
949
|
+
if from_version not in (0, 1, 2, 3, 4):
|
|
929
950
|
raise RuntimeError(
|
|
930
951
|
f"unsupported SQLite schema version {from_version}; "
|
|
931
952
|
f"expected 0..{CURRENT_SCHEMA_VERSION}"
|