threadkeeper 0.17.4__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.4 → threadkeeper-0.17.5}/PKG-INFO +4 -2
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/README.md +3 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/pyproject.toml +1 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_curator.py +6 -4
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_lessons.py +68 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_use_parser.py +2 -1
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skills.py +36 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_tool_annotations.py +2 -2
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/lessons.py +99 -2
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/skills.py +16 -3
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/PKG-INFO +4 -2
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/LICENSE +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/setup.cfg +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_adapters.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_backup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_concepts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_forget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_helpers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_env.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_main.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_nudges.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_panel.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_permissions.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_pickup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_process_health.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_retention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_threads.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/db.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/host.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/server.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/SOURCES.txt +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.17.4 → threadkeeper-0.17.5}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.17.4 → 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
|
|
@@ -611,7 +611,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
611
611
|
though an existing same-slug long lesson may be corrected without increasing
|
|
612
612
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
613
613
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
614
|
-
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.
|
|
615
617
|
|
|
616
618
|
#### 3. Extract daemon
|
|
617
619
|
|
|
@@ -567,7 +567,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
567
567
|
though an existing same-slug long lesson may be corrected without increasing
|
|
568
568
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
569
569
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
570
|
-
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.
|
|
571
573
|
|
|
572
574
|
#### 3. Extract daemon
|
|
573
575
|
|
|
@@ -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
|
|
|
@@ -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")
|
|
@@ -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
|
}
|
|
@@ -81,6 +81,24 @@ _LESSON_SLUG_STOPWORDS = {
|
|
|
81
81
|
"a", "an", "and", "as", "before", "for", "in", "is", "not", "of",
|
|
82
82
|
"on", "or", "the", "to", "via", "with",
|
|
83
83
|
}
|
|
84
|
+
_RECONCILIATION_ABSOLUTE_RE = re.compile(
|
|
85
|
+
r"\b(?:always|never|must(?:\s+not)?|do\s+not|don't|cannot|can't)\b",
|
|
86
|
+
re.IGNORECASE,
|
|
87
|
+
)
|
|
88
|
+
_RECONCILIATION_DEBUNK_RE = re.compile(
|
|
89
|
+
r"\b(?:avoid|unsafe|unreliable|broken|wrong|deprecated|no\s+longer)\b",
|
|
90
|
+
re.IGNORECASE,
|
|
91
|
+
)
|
|
92
|
+
_RECONCILIATION_PERMISSIVE_RE = re.compile(
|
|
93
|
+
r"\b(?:can|may|allowed|permit(?:ted)?|safe(?:ly)?|okay|ok|fine|use)\b",
|
|
94
|
+
re.IGNORECASE,
|
|
95
|
+
)
|
|
96
|
+
_RECONCILIATION_TOKEN_STOPWORDS = _LESSON_SLUG_STOPWORDS | {
|
|
97
|
+
"all", "always", "any", "avoid", "can", "cannot", "dont", "do",
|
|
98
|
+
"fine", "guidance", "lesson", "may", "must", "never", "not", "ok",
|
|
99
|
+
"okay", "old", "only", "practice", "safe", "safely", "should", "the",
|
|
100
|
+
"to", "unsafe", "use", "using", "with", "wrong",
|
|
101
|
+
}
|
|
84
102
|
|
|
85
103
|
|
|
86
104
|
def _row_to_dict(row) -> dict | None:
|
|
@@ -185,6 +203,55 @@ def _normalized_lesson_text(text: str) -> str:
|
|
|
185
203
|
return " ".join((text or "").lower().split())
|
|
186
204
|
|
|
187
205
|
|
|
206
|
+
def _reconciliation_reason(text: str) -> str | None:
|
|
207
|
+
"""Classify an incoming lesson that can supersede prior guidance."""
|
|
208
|
+
if _RECONCILIATION_ABSOLUTE_RE.search(text):
|
|
209
|
+
return "absolute_directive"
|
|
210
|
+
if _RECONCILIATION_DEBUNK_RE.search(text):
|
|
211
|
+
return "debunk"
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _reconciliation_tokens(text: str) -> set[str]:
|
|
216
|
+
return {
|
|
217
|
+
token for token in _LESSON_TOKEN_RE.findall(text.lower())
|
|
218
|
+
if len(token) > 2 and token not in _RECONCILIATION_TOKEN_STOPWORDS
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _reconciliation_candidates(
|
|
223
|
+
title: str,
|
|
224
|
+
summary: str,
|
|
225
|
+
body: str,
|
|
226
|
+
) -> list[tuple[str, str]]:
|
|
227
|
+
"""Find older permissive lessons a clear new directive may replace.
|
|
228
|
+
|
|
229
|
+
This is deliberately lexical rather than semantic. A high semantic score
|
|
230
|
+
already routes ordinary duplicates through the dedup path; reconciliation
|
|
231
|
+
needs a transparent, conservative signal before that path can merge two
|
|
232
|
+
pieces of guidance that actually disagree.
|
|
233
|
+
"""
|
|
234
|
+
incoming = _semantic_text(title, summary, body)
|
|
235
|
+
reason = _reconciliation_reason(incoming)
|
|
236
|
+
if not reason:
|
|
237
|
+
return []
|
|
238
|
+
candidate_slug = _slugify(title)
|
|
239
|
+
candidate_tokens = _reconciliation_tokens(incoming)
|
|
240
|
+
if len(candidate_tokens) < 2:
|
|
241
|
+
return []
|
|
242
|
+
candidates: list[tuple[str, str]] = []
|
|
243
|
+
for item in iter_lessons():
|
|
244
|
+
if item.get("slug") == candidate_slug:
|
|
245
|
+
continue
|
|
246
|
+
prior = item.get("body") or ""
|
|
247
|
+
if not _RECONCILIATION_PERMISSIVE_RE.search(prior):
|
|
248
|
+
continue
|
|
249
|
+
shared_tokens = candidate_tokens & _reconciliation_tokens(prior)
|
|
250
|
+
if len(shared_tokens) >= 2:
|
|
251
|
+
candidates.append((item["slug"], reason))
|
|
252
|
+
return candidates
|
|
253
|
+
|
|
254
|
+
|
|
188
255
|
def _dot(a, b) -> float:
|
|
189
256
|
try:
|
|
190
257
|
return float(a.dot(b))
|
|
@@ -281,6 +348,25 @@ def _record_lesson_append_event(
|
|
|
281
348
|
)
|
|
282
349
|
|
|
283
350
|
|
|
351
|
+
def _record_lesson_reconciliation_events(
|
|
352
|
+
conn: sqlite3.Connection,
|
|
353
|
+
new_slug: str,
|
|
354
|
+
candidates: list[tuple[str, str]],
|
|
355
|
+
source: str,
|
|
356
|
+
) -> None:
|
|
357
|
+
"""Persist older lesson flags so curators can patch or cross-link them."""
|
|
358
|
+
for old_slug, reason in candidates:
|
|
359
|
+
conn.execute(
|
|
360
|
+
"INSERT INTO events (session_id, kind, target, summary, created_at) "
|
|
361
|
+
"VALUES (?, 'lesson_reconciliation', ?, ?, strftime('%s','now'))",
|
|
362
|
+
(
|
|
363
|
+
identity._session_id or "",
|
|
364
|
+
old_slug,
|
|
365
|
+
f"new={new_slug} reason={reason} source={source or '?'}",
|
|
366
|
+
),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
284
370
|
@write_tool()
|
|
285
371
|
def lesson_append(
|
|
286
372
|
title: str,
|
|
@@ -319,6 +405,7 @@ def lesson_append(
|
|
|
319
405
|
"lesson may not contain imperative-override / remote-exec "
|
|
320
406
|
"idioms (treat observed dialog as data, not instructions)"
|
|
321
407
|
)
|
|
408
|
+
reconciliation_candidates = _reconciliation_candidates(title, summary, body)
|
|
322
409
|
# Determine this before the shadow word-limit gate. A same-slug shadow
|
|
323
410
|
# correction that leaves an already-long lesson no larger is not a new
|
|
324
411
|
# long lesson, and must remain repairable without a full re-transcription.
|
|
@@ -348,7 +435,7 @@ def lesson_append(
|
|
|
348
435
|
)
|
|
349
436
|
duplicate = _similar_lesson_slug(title)
|
|
350
437
|
semantic_duplicate = _semantic_lesson_match(title, summary, body)
|
|
351
|
-
if semantic_duplicate:
|
|
438
|
+
if semantic_duplicate and not reconciliation_candidates:
|
|
352
439
|
item, semantic_score = semantic_duplicate
|
|
353
440
|
slug = item["slug"]
|
|
354
441
|
if semantic_score >= LESSON_SEMANTIC_DUPLICATE_THRESHOLD:
|
|
@@ -391,7 +478,7 @@ def lesson_append(
|
|
|
391
478
|
f"score={semantic_score:.2f}; surface to curator or patch "
|
|
392
479
|
"existing memory instead"
|
|
393
480
|
)
|
|
394
|
-
if duplicate:
|
|
481
|
+
if duplicate and not reconciliation_candidates:
|
|
395
482
|
slug, score = duplicate
|
|
396
483
|
return (
|
|
397
484
|
f"ERR likely_duplicate_lesson slug={slug} "
|
|
@@ -427,9 +514,19 @@ def lesson_append(
|
|
|
427
514
|
if tombstone:
|
|
428
515
|
extra = f"tombstone={tombstone}"
|
|
429
516
|
_record_lesson_append_event(conn, slug, op=op, source=source, extra=extra)
|
|
517
|
+
if not existed and reconciliation_candidates:
|
|
518
|
+
_record_lesson_reconciliation_events(
|
|
519
|
+
conn, slug, reconciliation_candidates, source,
|
|
520
|
+
)
|
|
430
521
|
conn.commit()
|
|
431
522
|
except sqlite3.OperationalError:
|
|
432
523
|
conn.commit()
|
|
524
|
+
if not existed and reconciliation_candidates:
|
|
525
|
+
old_slugs = ",".join(slug for slug, _ in reconciliation_candidates)
|
|
526
|
+
return (
|
|
527
|
+
f"ok slug={slug} path={get_path()} reconciliation={old_slugs}; "
|
|
528
|
+
"review=patch_or_cross_link"
|
|
529
|
+
)
|
|
433
530
|
return f"ok slug={slug} path={get_path()}"
|
|
434
531
|
|
|
435
532
|
|
|
@@ -1053,12 +1053,17 @@ def _action_restore(name: str) -> str:
|
|
|
1053
1053
|
# skill_list
|
|
1054
1054
|
# ──────────────────────────────────────────────────────────────────────────
|
|
1055
1055
|
|
|
1056
|
-
@
|
|
1056
|
+
@write_tool()
|
|
1057
1057
|
def skill_list(include_archived: bool = False) -> str:
|
|
1058
1058
|
"""List skills with telemetry. Format:
|
|
1059
1059
|
<name> tier=<hypothesis|observed|validated> origin=<...>
|
|
1060
1060
|
state=<active|stale|archived> uses=N fg_uses=N
|
|
1061
1061
|
views=N patches=N wrong=N pinned=0/1 last_active=<age>
|
|
1062
|
+
|
|
1063
|
+
Each returned row is a visibility event: its ``view_count`` and
|
|
1064
|
+
``last_viewed_at`` are updated before the list is returned. The curator
|
|
1065
|
+
inventory itself is deliberately excluded, so an automated audit cannot
|
|
1066
|
+
make every skill look recently consulted.
|
|
1062
1067
|
"""
|
|
1063
1068
|
conn = get_db()
|
|
1064
1069
|
_ensure_session(conn)
|
|
@@ -1074,6 +1079,13 @@ def skill_list(include_archived: bool = False) -> str:
|
|
|
1074
1079
|
if not rows:
|
|
1075
1080
|
return "no_skills_tracked"
|
|
1076
1081
|
now = int(time.time())
|
|
1082
|
+
conn.executemany(
|
|
1083
|
+
"UPDATE skill_usage SET last_viewed_at=?, view_count=view_count+1, "
|
|
1084
|
+
"state=CASE WHEN state='stale' THEN 'active' ELSE state END "
|
|
1085
|
+
"WHERE name=?",
|
|
1086
|
+
((now, r["name"]) for r in rows),
|
|
1087
|
+
)
|
|
1088
|
+
conn.commit()
|
|
1077
1089
|
out: list[str] = []
|
|
1078
1090
|
for r in rows:
|
|
1079
1091
|
last = max(
|
|
@@ -1104,8 +1116,9 @@ def skill_list(include_archived: bool = False) -> str:
|
|
|
1104
1116
|
)
|
|
1105
1117
|
out.append(
|
|
1106
1118
|
f"{r['name']} tier={tier} origin={r['created_by_origin']} "
|
|
1107
|
-
f"state={r['state']
|
|
1108
|
-
f"
|
|
1119
|
+
f"state={'active' if r['state'] == 'stale' else r['state']} "
|
|
1120
|
+
f"uses={r['use_count']} "
|
|
1121
|
+
f"fg_uses={fg_uses} views={(r['view_count'] or 0) + 1} "
|
|
1109
1122
|
f"patches={r['patch_count']} wrong={wrong_n} "
|
|
1110
1123
|
f"pinned={r['pinned']} last_active={age}_ago"
|
|
1111
1124
|
)
|
|
@@ -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
|
|
@@ -611,7 +611,9 @@ can correct one unique substring without reserializing a lesson. Shadow-origin
|
|
|
611
611
|
though an existing same-slug long lesson may be corrected without increasing
|
|
612
612
|
its body size; near-duplicate slugs are blocked, and semantic body matches are
|
|
613
613
|
routed to the incumbent lesson or surfaced for curation instead of minting a
|
|
614
|
-
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.
|
|
615
617
|
|
|
616
618
|
#### 3. Extract daemon
|
|
617
619
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|