threadkeeper 0.17.3__tar.gz → 0.17.5__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.3 → threadkeeper-0.17.5}/PKG-INFO +19 -4
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/README.md +18 -3
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/pyproject.toml +1 -1
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_curator.py +48 -4
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_lessons.py +68 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_use_parser.py +2 -1
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skills.py +36 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_slim.py +7 -2
- threadkeeper-0.17.5/tests/test_spawn_worktree_isolation.py +112 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_tool_annotations.py +2 -2
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/curator.py +5 -4
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/skill_audit.py +15 -2
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/lessons.py +99 -2
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/skills.py +16 -3
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/spawn.py +133 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/PKG-INFO +19 -4
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/SOURCES.txt +1 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/LICENSE +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/setup.cfg +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_adapters.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_backup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_concepts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_forget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_helpers.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_env.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_main.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_identity.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_nudges.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_panel.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_permissions.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_pickup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_process_health.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_retention.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_threads.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/config.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/db.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/host.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/server.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.17.3 → threadkeeper-0.17.5}/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.5
|
|
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
|
|
@@ -310,6 +310,12 @@ uses a SQLite `BEGIN IMMEDIATE` reservation: `spawn()` re-checks the budget and
|
|
|
310
310
|
inserts the child task row with its RSS estimate before `Popen`, so two
|
|
311
311
|
concurrent spawns cannot both squeeze through the cap.
|
|
312
312
|
|
|
313
|
+
When `cwd` is inside a Git checkout, `spawn()` also requires the source
|
|
314
|
+
checkout's tracked files to be clean, then starts the child from a unique
|
|
315
|
+
branch/worktree under `THREADKEEPER_TASK_LOG_DIR/worktrees/`. Parallel children
|
|
316
|
+
therefore never share a mutable checkout or Git index. Non-Git directories keep
|
|
317
|
+
their existing behavior; a dirty Git checkout is refused before a child starts.
|
|
318
|
+
|
|
313
319
|
The spawn wrapper also records each completed child's `duration_s`,
|
|
314
320
|
`tokens_in`, `tokens_out`, `tokens_total`, and `cost_usd` when the underlying
|
|
315
321
|
CLI emits a recognizable usage trailer. Optional daily ceilings
|
|
@@ -605,7 +611,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
605
611
|
though an existing same-slug long lesson may be corrected without increasing
|
|
606
612
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
607
613
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
608
|
-
sibling lesson.
|
|
614
|
+
sibling lesson. A clear new directive or debunk also flags older permissive
|
|
615
|
+
lessons on the same concrete practice for patch, cross-link, or supersession
|
|
616
|
+
review.
|
|
609
617
|
|
|
610
618
|
#### 3. Extract daemon
|
|
611
619
|
|
|
@@ -710,6 +718,12 @@ non-forced direct `curator_review()` calls return `not_due` inside the
|
|
|
710
718
|
configured interval and record that status without spawning. A manual
|
|
711
719
|
`curator_review(force=True)` bypasses the interval but still respects the lock.
|
|
712
720
|
|
|
721
|
+
For automation-created skills, the audit keeps foreground consultation separate
|
|
722
|
+
from maintenance: a background-review skill with `fg_uses=0` after 14 days is
|
|
723
|
+
still a false-positive prune candidate even if automatic review or sync loops
|
|
724
|
+
have increased its patch counter. Patches are maintenance activity, not proof
|
|
725
|
+
that a foreground user or agent consulted the skill.
|
|
726
|
+
|
|
713
727
|
Before spawning, the scheduler hashes lessons, concepts, skill bodies, support
|
|
714
728
|
trees, validators, and mirror state. Repeated manual calls over identical bytes
|
|
715
729
|
return `unchanged_inventory`; the scheduled three-day pass still runs because
|
|
@@ -1549,8 +1563,9 @@ Spawn task spool files live in `THREADKEEPER_TASK_LOG_DIR` (default
|
|
|
1549
1563
|
the hardened `~/.threadkeeper` perimeter by default; explicit overrides are
|
|
1550
1564
|
refused when the configured directory is a symlink or is not owned by the
|
|
1551
1565
|
current user. `spawn()` creates captured headless `.log`, stdin prompt spool,
|
|
1552
|
-
and visible `.command` files with no-follow owner-only opens.
|
|
1553
|
-
|
|
1566
|
+
and visible `.command` files with no-follow owner-only opens. Git-backed spawns
|
|
1567
|
+
also create their isolated worktrees there. `consolidate()` garbage-collects
|
|
1568
|
+
task spool files once their task row is no longer retained.
|
|
1554
1569
|
|
|
1555
1570
|
---
|
|
1556
1571
|
|
|
@@ -266,6 +266,12 @@ uses a SQLite `BEGIN IMMEDIATE` reservation: `spawn()` re-checks the budget and
|
|
|
266
266
|
inserts the child task row with its RSS estimate before `Popen`, so two
|
|
267
267
|
concurrent spawns cannot both squeeze through the cap.
|
|
268
268
|
|
|
269
|
+
When `cwd` is inside a Git checkout, `spawn()` also requires the source
|
|
270
|
+
checkout's tracked files to be clean, then starts the child from a unique
|
|
271
|
+
branch/worktree under `THREADKEEPER_TASK_LOG_DIR/worktrees/`. Parallel children
|
|
272
|
+
therefore never share a mutable checkout or Git index. Non-Git directories keep
|
|
273
|
+
their existing behavior; a dirty Git checkout is refused before a child starts.
|
|
274
|
+
|
|
269
275
|
The spawn wrapper also records each completed child's `duration_s`,
|
|
270
276
|
`tokens_in`, `tokens_out`, `tokens_total`, and `cost_usd` when the underlying
|
|
271
277
|
CLI emits a recognizable usage trailer. Optional daily ceilings
|
|
@@ -561,7 +567,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
561
567
|
though an existing same-slug long lesson may be corrected without increasing
|
|
562
568
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
563
569
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
564
|
-
sibling lesson.
|
|
570
|
+
sibling lesson. A clear new directive or debunk also flags older permissive
|
|
571
|
+
lessons on the same concrete practice for patch, cross-link, or supersession
|
|
572
|
+
review.
|
|
565
573
|
|
|
566
574
|
#### 3. Extract daemon
|
|
567
575
|
|
|
@@ -666,6 +674,12 @@ non-forced direct `curator_review()` calls return `not_due` inside the
|
|
|
666
674
|
configured interval and record that status without spawning. A manual
|
|
667
675
|
`curator_review(force=True)` bypasses the interval but still respects the lock.
|
|
668
676
|
|
|
677
|
+
For automation-created skills, the audit keeps foreground consultation separate
|
|
678
|
+
from maintenance: a background-review skill with `fg_uses=0` after 14 days is
|
|
679
|
+
still a false-positive prune candidate even if automatic review or sync loops
|
|
680
|
+
have increased its patch counter. Patches are maintenance activity, not proof
|
|
681
|
+
that a foreground user or agent consulted the skill.
|
|
682
|
+
|
|
669
683
|
Before spawning, the scheduler hashes lessons, concepts, skill bodies, support
|
|
670
684
|
trees, validators, and mirror state. Repeated manual calls over identical bytes
|
|
671
685
|
return `unchanged_inventory`; the scheduled three-day pass still runs because
|
|
@@ -1505,8 +1519,9 @@ Spawn task spool files live in `THREADKEEPER_TASK_LOG_DIR` (default
|
|
|
1505
1519
|
the hardened `~/.threadkeeper` perimeter by default; explicit overrides are
|
|
1506
1520
|
refused when the configured directory is a symlink or is not owned by the
|
|
1507
1521
|
current user. `spawn()` creates captured headless `.log`, stdin prompt spool,
|
|
1508
|
-
and visible `.command` files with no-follow owner-only opens.
|
|
1509
|
-
|
|
1522
|
+
and visible `.command` files with no-follow owner-only opens. Git-backed spawns
|
|
1523
|
+
also create their isolated worktrees there. `consolidate()` garbage-collects
|
|
1524
|
+
task spool files once their task row is no longer retained.
|
|
1510
1525
|
|
|
1511
1526
|
---
|
|
1512
1527
|
|
|
@@ -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.5"
|
|
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" }]
|
|
@@ -152,15 +152,15 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
|
|
|
152
152
|
conn.execute(
|
|
153
153
|
"INSERT INTO skill_usage "
|
|
154
154
|
"(name, created_at, created_by_origin, last_used_at, "
|
|
155
|
-
" use_count, pinned, state) "
|
|
156
|
-
"VALUES (?, ?, 'foreground', ?, 5, 1, 'active')",
|
|
155
|
+
" use_count, foreground_use_count, view_count, pinned, state) "
|
|
156
|
+
"VALUES (?, ?, 'foreground', ?, 5, 4, 3, 1, 'active')",
|
|
157
157
|
("pinned-skill", now - 86400, now - 3600),
|
|
158
158
|
)
|
|
159
159
|
conn.execute(
|
|
160
160
|
"INSERT INTO skill_usage "
|
|
161
161
|
"(name, created_at, created_by_origin, last_used_at, "
|
|
162
|
-
" use_count, state) "
|
|
163
|
-
"VALUES (?, ?, 'background_review', ?, 2, 'active')",
|
|
162
|
+
" use_count, foreground_use_count, view_count, state) "
|
|
163
|
+
"VALUES (?, ?, 'background_review', ?, 2, 1, 6, 'active')",
|
|
164
164
|
("auto-created-skill", now - 172800, now - 7200),
|
|
165
165
|
)
|
|
166
166
|
conn.commit()
|
|
@@ -177,6 +177,8 @@ def test_collect_inventory_counts_lessons_and_skills(tmp_path, monkeypatch):
|
|
|
177
177
|
# background_review skill (not pinned) is NOT protected
|
|
178
178
|
assert "SKILL auto-created-skill [PROTECTED]" not in dump
|
|
179
179
|
assert "SKILL auto-created-skill" in dump
|
|
180
|
+
assert "fg_uses=4 uses=5 views=3 maintenance_patches=0" in dump
|
|
181
|
+
assert "fg_uses=1 uses=2 views=6 maintenance_patches=0" in dump
|
|
180
182
|
assert "STALE LESSONS (dry-run decay ranking)" in dump
|
|
181
183
|
|
|
182
184
|
|
|
@@ -843,6 +845,48 @@ def test_skill_validator_is_exhaustive_and_semantic(tmp_path, monkeypatch):
|
|
|
843
845
|
assert "3. SKILL second-skill" in checklist
|
|
844
846
|
|
|
845
847
|
|
|
848
|
+
def test_curator_prune_rubric_keeps_patched_unconsulted_skill_eligible(
|
|
849
|
+
tmp_path, monkeypatch,
|
|
850
|
+
):
|
|
851
|
+
pkg = _bootstrap(tmp_path, monkeypatch, min_lessons="2")
|
|
852
|
+
pkg["lessons"].append_lesson(title="one", body="b1", source="shadow")
|
|
853
|
+
pkg["lessons"].append_lesson(title="two", body="b2", source="shadow")
|
|
854
|
+
_write_audit_skill(
|
|
855
|
+
pkg["skills_dir"], "patched-background-skill", "# Rule\nDo the test.",
|
|
856
|
+
)
|
|
857
|
+
now = int(time.time())
|
|
858
|
+
conn = pkg["db"].get_db()
|
|
859
|
+
conn.execute(
|
|
860
|
+
"INSERT INTO skill_usage "
|
|
861
|
+
"(name, created_at, created_by_origin, last_patched_at, patch_count, "
|
|
862
|
+
"foreground_use_count, state) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
863
|
+
(
|
|
864
|
+
"patched-background-skill", now - 30 * 86400,
|
|
865
|
+
"background_review", now, 4, 0, "active",
|
|
866
|
+
),
|
|
867
|
+
)
|
|
868
|
+
conn.commit()
|
|
869
|
+
|
|
870
|
+
import threadkeeper.tools.spawn as spawn_mod
|
|
871
|
+
captured: list[dict] = []
|
|
872
|
+
|
|
873
|
+
def fake_spawn(**kwargs):
|
|
874
|
+
captured.append(kwargs)
|
|
875
|
+
return "spawn task_id=patched-background-skill pid=0"
|
|
876
|
+
|
|
877
|
+
monkeypatch.setattr(spawn_mod, "spawn", fake_spawn)
|
|
878
|
+
pkg["curator"].run_curator_pass(force=True)
|
|
879
|
+
|
|
880
|
+
prompt = captured[0]["prompt"]
|
|
881
|
+
assert "origin=background_review AND fg_uses=0" in prompt
|
|
882
|
+
assert "they are not foreground consultation" in prompt
|
|
883
|
+
assert "SKILL patched-background-skill" in prompt
|
|
884
|
+
assert "fg_uses=0" in prompt
|
|
885
|
+
assert "maintenance_patches=4" in prompt
|
|
886
|
+
assert "created=30d_ago" in prompt
|
|
887
|
+
assert "patches=0" not in prompt
|
|
888
|
+
|
|
889
|
+
|
|
846
890
|
def test_skill_validate_tool_returns_post_change_contract(tmp_path, monkeypatch):
|
|
847
891
|
pkg = _bootstrap(tmp_path, monkeypatch)
|
|
848
892
|
primary = pkg["skills_dir"]
|
|
@@ -356,6 +356,74 @@ def test_shadow_lesson_append_semantic_duplicate_patches_incumbent(
|
|
|
356
356
|
assert "serialize AVD boot lock acquisition" in body
|
|
357
357
|
|
|
358
358
|
|
|
359
|
+
def test_new_directive_flags_older_permissive_lesson_for_reconciliation(
|
|
360
|
+
tmp_path, monkeypatch,
|
|
361
|
+
):
|
|
362
|
+
pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
|
|
363
|
+
la = _tool(pkg, "lesson_append")
|
|
364
|
+
|
|
365
|
+
first = la(
|
|
366
|
+
title="module globals for test configuration",
|
|
367
|
+
body=(
|
|
368
|
+
"You can use module globals to pass test configuration between "
|
|
369
|
+
"runner steps when setup is simple."
|
|
370
|
+
),
|
|
371
|
+
source="shadow",
|
|
372
|
+
)
|
|
373
|
+
assert first.startswith("ok")
|
|
374
|
+
|
|
375
|
+
out = la(
|
|
376
|
+
title="inject test configuration explicitly",
|
|
377
|
+
body=(
|
|
378
|
+
"Never use module globals to pass test configuration between "
|
|
379
|
+
"runner steps; inject dependencies explicitly instead."
|
|
380
|
+
),
|
|
381
|
+
source="shadow",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
assert out.startswith("ok slug=inject-test-configuration-explicitly")
|
|
385
|
+
assert "reconciliation=module-globals-for-test-configuration" in out
|
|
386
|
+
assert "review=patch_or_cross_link" in out
|
|
387
|
+
assert pkg["lessons"].count_lessons() == 2
|
|
388
|
+
row = pkg["db"].get_db().execute(
|
|
389
|
+
"SELECT target, summary FROM events WHERE kind='lesson_reconciliation'"
|
|
390
|
+
).fetchone()
|
|
391
|
+
assert row["target"] == "module-globals-for-test-configuration"
|
|
392
|
+
assert "new=inject-test-configuration-explicitly" in row["summary"]
|
|
393
|
+
assert "reason=absolute_directive" in row["summary"]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_new_debunk_flags_older_permissive_lesson_for_reconciliation(
|
|
397
|
+
tmp_path, monkeypatch,
|
|
398
|
+
):
|
|
399
|
+
pkg = _bootstrap(tmp_path, monkeypatch, write_origin="shadow_review")
|
|
400
|
+
la = _tool(pkg, "lesson_append")
|
|
401
|
+
|
|
402
|
+
la(
|
|
403
|
+
title="shared runner environment state",
|
|
404
|
+
body=(
|
|
405
|
+
"It is safe to keep shared runner environment state throughout "
|
|
406
|
+
"a test workflow."
|
|
407
|
+
),
|
|
408
|
+
source="shadow",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
out = la(
|
|
412
|
+
title="shared runner environment state is unreliable",
|
|
413
|
+
body=(
|
|
414
|
+
"Keeping shared runner environment state is unreliable; pass the "
|
|
415
|
+
"environment to each command instead."
|
|
416
|
+
),
|
|
417
|
+
source="shadow",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
assert "reconciliation=shared-runner-environment-state" in out
|
|
421
|
+
row = pkg["db"].get_db().execute(
|
|
422
|
+
"SELECT summary FROM events WHERE kind='lesson_reconciliation'"
|
|
423
|
+
).fetchone()
|
|
424
|
+
assert "reason=debunk" in row["summary"]
|
|
425
|
+
|
|
426
|
+
|
|
359
427
|
def test_shadow_lesson_append_semantic_borderline_surfaces_duplicate(
|
|
360
428
|
tmp_path, monkeypatch,
|
|
361
429
|
):
|
|
@@ -182,11 +182,12 @@ def test_ingest_file_bumps_skill_usage(tmp_path, monkeypatch):
|
|
|
182
182
|
conn.commit()
|
|
183
183
|
assert added == 1
|
|
184
184
|
row = conn.execute(
|
|
185
|
-
"SELECT use_count, last_used_at, created_by_origin "
|
|
185
|
+
"SELECT use_count, foreground_use_count, last_used_at, created_by_origin "
|
|
186
186
|
"FROM skill_usage WHERE name='swift-ios'"
|
|
187
187
|
).fetchone()
|
|
188
188
|
assert row is not None
|
|
189
189
|
assert row["use_count"] == 1
|
|
190
|
+
assert row["foreground_use_count"] == 1
|
|
190
191
|
assert row["last_used_at"] is not None
|
|
191
192
|
assert row["created_by_origin"] == "foreground"
|
|
192
193
|
|
|
@@ -466,6 +466,42 @@ def test_skill_record_bumps_counters(skills_pkg):
|
|
|
466
466
|
assert r["last_viewed_at"] is not None
|
|
467
467
|
|
|
468
468
|
|
|
469
|
+
def test_skill_list_records_views_for_returned_skills(skills_pkg):
|
|
470
|
+
_seed_skill(skills_pkg, name="listed-active")
|
|
471
|
+
_seed_skill(skills_pkg, name="listed-archived")
|
|
472
|
+
conn = skills_pkg["db"].get_db()
|
|
473
|
+
conn.execute(
|
|
474
|
+
"UPDATE skill_usage SET state='archived' WHERE name='listed-archived'"
|
|
475
|
+
)
|
|
476
|
+
conn.commit()
|
|
477
|
+
|
|
478
|
+
listed = _tool(skills_pkg, "skill_list")
|
|
479
|
+
rendered = listed()
|
|
480
|
+
assert "listed-active" in rendered
|
|
481
|
+
assert "views=1" in rendered
|
|
482
|
+
rows = {
|
|
483
|
+
row["name"]: row
|
|
484
|
+
for row in conn.execute(
|
|
485
|
+
"SELECT name, view_count, last_viewed_at FROM skill_usage "
|
|
486
|
+
"WHERE name IN ('listed-active', 'listed-archived')"
|
|
487
|
+
).fetchall()
|
|
488
|
+
}
|
|
489
|
+
assert rows["listed-active"]["view_count"] == 1
|
|
490
|
+
assert rows["listed-active"]["last_viewed_at"] is not None
|
|
491
|
+
assert rows["listed-archived"]["view_count"] == 0
|
|
492
|
+
|
|
493
|
+
assert "listed-archived" in listed(include_archived=True)
|
|
494
|
+
rows = {
|
|
495
|
+
row["name"]: row
|
|
496
|
+
for row in conn.execute(
|
|
497
|
+
"SELECT name, view_count FROM skill_usage "
|
|
498
|
+
"WHERE name IN ('listed-active', 'listed-archived')"
|
|
499
|
+
).fetchall()
|
|
500
|
+
}
|
|
501
|
+
assert rows["listed-active"]["view_count"] == 2
|
|
502
|
+
assert rows["listed-archived"]["view_count"] == 1
|
|
503
|
+
|
|
504
|
+
|
|
469
505
|
def test_skill_list_omits_archived_by_default(skills_pkg):
|
|
470
506
|
_seed_skill(skills_pkg, name="alive")
|
|
471
507
|
_seed_skill(skills_pkg, name="ghost")
|
|
@@ -171,7 +171,7 @@ def test_visible_command_script_is_owner_only(mp_with_cid, monkeypatch):
|
|
|
171
171
|
monkeypatch.setattr(spawn_mod.subprocess, "Popen", _FakePopen)
|
|
172
172
|
|
|
173
173
|
spawn_fn = pkg["mcp"]._tool_manager._tools["spawn"].fn
|
|
174
|
-
out = spawn_fn(prompt="do a thing", visible=True)
|
|
174
|
+
out = spawn_fn(prompt="do a thing", cwd=str(pkg["tmp"]), visible=True)
|
|
175
175
|
assert out.startswith("ok task="), out
|
|
176
176
|
|
|
177
177
|
cmd_files = list(spawn_mod.TASK_LOG_DIR.glob("*.command"))
|
|
@@ -197,7 +197,12 @@ def test_headless_log_file_is_owner_only(mp_with_cid, monkeypatch):
|
|
|
197
197
|
monkeypatch.setattr(spawn_mod.subprocess, "Popen", _FakePopen)
|
|
198
198
|
|
|
199
199
|
spawn_fn = pkg["mcp"]._tool_manager._tools["spawn"].fn
|
|
200
|
-
out = spawn_fn(
|
|
200
|
+
out = spawn_fn(
|
|
201
|
+
prompt="do a thing",
|
|
202
|
+
cwd=str(pkg["tmp"]),
|
|
203
|
+
visible=False,
|
|
204
|
+
capture_output=True,
|
|
205
|
+
)
|
|
201
206
|
assert out.startswith("ok task="), out
|
|
202
207
|
|
|
203
208
|
log_files = list(spawn_mod.TASK_LOG_DIR.glob("*.log"))
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Git worktree isolation on the shared spawn path."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_FAKE_CID = "aaaa0000-bbbb-1111-cccc-222233334444"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _git(repo: Path, *args: str) -> str:
|
|
12
|
+
proc = subprocess.run(
|
|
13
|
+
["git", "-C", str(repo), *args],
|
|
14
|
+
text=True,
|
|
15
|
+
capture_output=True,
|
|
16
|
+
check=True,
|
|
17
|
+
)
|
|
18
|
+
return proc.stdout.strip()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _init_repo(tmp_path: Path) -> Path:
|
|
22
|
+
repo = tmp_path / "repo"
|
|
23
|
+
repo.mkdir()
|
|
24
|
+
_git(repo, "init", "-b", "main")
|
|
25
|
+
_git(repo, "config", "user.email", "test@example.com")
|
|
26
|
+
_git(repo, "config", "user.name", "Test User")
|
|
27
|
+
(repo / "tracked.txt").write_text("base\n", encoding="utf-8")
|
|
28
|
+
_git(repo, "add", "tracked.txt")
|
|
29
|
+
_git(repo, "commit", "-m", "base")
|
|
30
|
+
return repo
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _configure_spawn(monkeypatch, pkg):
|
|
34
|
+
import threadkeeper.identity as identity
|
|
35
|
+
import threadkeeper.spawn_config as spawn_config
|
|
36
|
+
import threadkeeper.tools.spawn as spawn_mod
|
|
37
|
+
|
|
38
|
+
monkeypatch.setattr(spawn_mod, "_claude_bin", lambda: "/bin/true")
|
|
39
|
+
monkeypatch.setattr(identity, "_active_cli", "claude")
|
|
40
|
+
monkeypatch.setattr(
|
|
41
|
+
spawn_config, "resolve_agent", lambda role, active_cli=None: "claude"
|
|
42
|
+
)
|
|
43
|
+
monkeypatch.setattr(spawn_config, "resolve_model", lambda cli, role="": "")
|
|
44
|
+
return spawn_mod
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_git_spawns_get_distinct_branch_worktrees(mp_with_cid, monkeypatch):
|
|
48
|
+
pkg = mp_with_cid(_FAKE_CID)
|
|
49
|
+
repo = _init_repo(pkg["tmp"])
|
|
50
|
+
spawn_mod = _configure_spawn(monkeypatch, pkg)
|
|
51
|
+
child_cwds: list[Path] = []
|
|
52
|
+
|
|
53
|
+
class _FakePopen:
|
|
54
|
+
def __init__(self, _args, **kwargs):
|
|
55
|
+
child_cwds.append(Path(kwargs["cwd"]))
|
|
56
|
+
self.pid = 4200 + len(child_cwds)
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(spawn_mod.subprocess, "Popen", _FakePopen)
|
|
59
|
+
|
|
60
|
+
first = spawn_mod.spawn(
|
|
61
|
+
prompt="first child", cwd=str(repo), visible=False, capture_output=False
|
|
62
|
+
)
|
|
63
|
+
second = spawn_mod.spawn(
|
|
64
|
+
prompt="second child", cwd=str(repo), visible=False, capture_output=False
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert first.startswith("ok task="), first
|
|
68
|
+
assert second.startswith("ok task="), second
|
|
69
|
+
assert len(child_cwds) == 2
|
|
70
|
+
assert child_cwds[0] != child_cwds[1]
|
|
71
|
+
assert all(path != repo and path.is_dir() for path in child_cwds)
|
|
72
|
+
roots = [
|
|
73
|
+
spawn_mod._git_result(["rev-parse", "--show-toplevel"], path)[0]
|
|
74
|
+
for path in child_cwds
|
|
75
|
+
]
|
|
76
|
+
assert roots == [str(path) for path in child_cwds]
|
|
77
|
+
branches = [
|
|
78
|
+
spawn_mod._git_result(["branch", "--show-current"], path)[0]
|
|
79
|
+
for path in child_cwds
|
|
80
|
+
]
|
|
81
|
+
assert len(set(branches)) == 2
|
|
82
|
+
assert all(branch.startswith("threadkeeper/spawn-tk_") for branch in branches)
|
|
83
|
+
assert spawn_mod._git_result(
|
|
84
|
+
["status", "--porcelain", "--untracked-files=no"], repo
|
|
85
|
+
)[0] == ""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_dirty_git_checkout_is_refused_before_child_launch(
|
|
89
|
+
mp_with_cid, monkeypatch,
|
|
90
|
+
):
|
|
91
|
+
pkg = mp_with_cid(_FAKE_CID)
|
|
92
|
+
repo = _init_repo(pkg["tmp"])
|
|
93
|
+
spawn_mod = _configure_spawn(monkeypatch, pkg)
|
|
94
|
+
(repo / "tracked.txt").write_text("uncommitted\n", encoding="utf-8")
|
|
95
|
+
|
|
96
|
+
def _unexpected_launch(*_args, **_kwargs):
|
|
97
|
+
raise AssertionError("dirty checkout must be rejected before Popen")
|
|
98
|
+
|
|
99
|
+
monkeypatch.setattr(spawn_mod.subprocess, "Popen", _unexpected_launch)
|
|
100
|
+
|
|
101
|
+
out = spawn_mod.spawn(
|
|
102
|
+
prompt="must not launch", cwd=str(repo), visible=False, capture_output=False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
assert out == "ERR spawn_dirty_worktree mode=git"
|
|
106
|
+
worktrees, err = spawn_mod._git_result(
|
|
107
|
+
["worktree", "list", "--porcelain"], repo
|
|
108
|
+
)
|
|
109
|
+
assert not err
|
|
110
|
+
assert worktrees.count("worktree ") == 1
|
|
111
|
+
conn = pkg["db"].get_db()
|
|
112
|
+
assert conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 0
|
|
@@ -33,7 +33,7 @@ READ_TOOLS = {
|
|
|
33
33
|
"memory_guard_status", "mp_dashboard", "mp_health", "neighbors", "peers",
|
|
34
34
|
"pending_distillates", "pickup_candidates", "reliability_for",
|
|
35
35
|
"review_candidates", "run_probe", "search", "shadow_review_status",
|
|
36
|
-
"
|
|
36
|
+
"skill_validate", "spawn_budget_status", "spawn_status",
|
|
37
37
|
"sync_peers", "sync_status", "task_logs",
|
|
38
38
|
"task_thread", "tasks", "weak_spots", "whoami",
|
|
39
39
|
}
|
|
@@ -65,7 +65,7 @@ WRITE_TOOLS = {
|
|
|
65
65
|
"open_dialog_window", "open_thread", "presence", "record_attempt",
|
|
66
66
|
"register_concept", "register_probe", "reject_candidate", "release_pickup",
|
|
67
67
|
"respond", "review_thread", "search_via_parent", "session_end",
|
|
68
|
-
"shadow_review_run", "skill_record", "spawn", "spawn_budget_set",
|
|
68
|
+
"shadow_review_run", "skill_list", "skill_record", "spawn", "spawn_budget_set",
|
|
69
69
|
"style_set", "sync_now", "tag_signal", "tournament", "validate_threads",
|
|
70
70
|
"verbatim_user", "vote_distill", "wait", "whisper",
|
|
71
71
|
}
|
|
@@ -248,10 +248,11 @@ promotion and linking plan in REPORT.md for a foreground human instead.
|
|
|
248
248
|
superseded by a newer entry, or a **FALSE POSITIVE** (auto-created
|
|
249
249
|
by the background-review loop but never validated by actual use).
|
|
250
250
|
Specifically flag as PRUNE:
|
|
251
|
-
• origin=background_review AND
|
|
252
|
-
|
|
253
|
-
consulted it
|
|
254
|
-
|
|
251
|
+
• origin=background_review AND fg_uses=0 AND created >14 days ago
|
|
252
|
+
→ strong false-positive signal: no foreground user or agent ever
|
|
253
|
+
consulted it. `maintenance_patches` count automated or other maintenance
|
|
254
|
+
writes; they are not foreground consultation and never reset this
|
|
255
|
+
eligibility signal.
|
|
255
256
|
• SKILL_OUTCOME signals (in the events table) marking the skill
|
|
256
257
|
as 'wrong' more often than 'helped' → user-judgment override.
|
|
257
258
|
Format:
|
|
@@ -18,6 +18,7 @@ import os
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
import re
|
|
20
20
|
import sqlite3
|
|
21
|
+
import time
|
|
21
22
|
from typing import Iterable
|
|
22
23
|
|
|
23
24
|
import yaml
|
|
@@ -245,7 +246,8 @@ def _telemetry_rows(conn: sqlite3.Connection, include_archived: bool) -> dict[st
|
|
|
245
246
|
rows = conn.execute(
|
|
246
247
|
"SELECT name, created_at, created_by_origin, last_used_at, "
|
|
247
248
|
"last_viewed_at, last_patched_at, use_count, view_count, "
|
|
248
|
-
"patch_count, pinned, state, tier
|
|
249
|
+
"patch_count, foreground_use_count, pinned, state, tier "
|
|
250
|
+
"FROM skill_usage" + where
|
|
249
251
|
+ " ORDER BY name"
|
|
250
252
|
).fetchall()
|
|
251
253
|
except sqlite3.OperationalError:
|
|
@@ -327,7 +329,7 @@ def _record_for(
|
|
|
327
329
|
for key in (
|
|
328
330
|
"created_at", "last_used_at", "last_viewed_at",
|
|
329
331
|
"last_patched_at", "use_count", "view_count", "patch_count",
|
|
330
|
-
"pinned",
|
|
332
|
+
"foreground_use_count", "pinned",
|
|
331
333
|
)
|
|
332
334
|
},
|
|
333
335
|
"source_path": str(source) if source else None,
|
|
@@ -533,9 +535,20 @@ def format_skill_checklist(manifest: dict) -> str:
|
|
|
533
535
|
for index, record in enumerate(manifest["skills"], start=1):
|
|
534
536
|
flags = ",".join(record["findings"]) or "clean"
|
|
535
537
|
protected = " [PROTECTED]" if record["protected"] else ""
|
|
538
|
+
telemetry = record["telemetry"]
|
|
539
|
+
created_at = int(telemetry.get("created_at") or 0)
|
|
540
|
+
created_age = (
|
|
541
|
+
max(0, int(time.time()) - created_at) // 86400
|
|
542
|
+
if created_at else "?"
|
|
543
|
+
)
|
|
536
544
|
lines.append(
|
|
537
545
|
f"{index}. SKILL {record['name']}{protected} "
|
|
538
546
|
f"state={record['state']} origin={record['origin']} "
|
|
547
|
+
f"fg_uses={telemetry.get('foreground_use_count') or 0} "
|
|
548
|
+
f"uses={telemetry.get('use_count') or 0} "
|
|
549
|
+
f"views={telemetry.get('view_count') or 0} "
|
|
550
|
+
f"maintenance_patches={telemetry.get('patch_count') or 0} "
|
|
551
|
+
f"created={created_age}d_ago "
|
|
539
552
|
f"source_kind={record['source_kind']} "
|
|
540
553
|
f"source={record['source_path'] or '-'} findings={flags}"
|
|
541
554
|
)
|