threadkeeper 0.17.2__tar.gz → 0.17.3__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.2 → threadkeeper-0.17.3}/PKG-INFO +14 -7
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/README.md +13 -6
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/pyproject.toml +1 -1
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_agent_status.py +38 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_notify_daemon.py +42 -2
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/agent_status.py +42 -11
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/notify.py +40 -4
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper.egg-info/PKG-INFO +14 -7
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/LICENSE +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/setup.cfg +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_adapters.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_backup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_concepts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_curator.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_forget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_helpers.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_host_env.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_host_main.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_identity.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_lessons.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_memory_eval.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_nudges.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_panel.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_permissions.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_pickup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_process_health.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_retention.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_skills.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_threads.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_tool_annotations.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/config.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/db.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/forget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/host.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/retention.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/server.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper.egg-info/SOURCES.txt +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.17.2 → threadkeeper-0.17.3}/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.3
|
|
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
|
|
@@ -1058,7 +1058,10 @@ Three detection sources per tick:
|
|
|
1058
1058
|
from an exhausted subscription; the real outcome only lands in
|
|
1059
1059
|
`tasks.return_code`. The reason is read from the child's log tail.
|
|
1060
1060
|
3. **Materialization** — `skill_materialized` / `skill_create` (skill) and
|
|
1061
|
-
`lesson_append` (lesson).
|
|
1061
|
+
`lesson_append` (lesson). Each notification contains one readable artifact
|
|
1062
|
+
name: the skill's first heading (falling back to its directory name), or
|
|
1063
|
+
the lesson name with slug separators replaced by spaces. Paths, provenance,
|
|
1064
|
+
and operation metadata stay out of the notification text.
|
|
1062
1065
|
|
|
1063
1066
|
A per-loop cooldown collapses a lapsed-subscription storm into one actionable
|
|
1064
1067
|
alert; the first run seeds its cursor to the current position, so historical
|
|
@@ -1089,12 +1092,16 @@ will register the app in **System Settings ▸ Notifications** and show banners;
|
|
|
1089
1092
|
banners are titled **Thread-Keeper** (`CFBundleDisplayName`). The first launch
|
|
1090
1093
|
after an update shows a one-time permission prompt.
|
|
1091
1094
|
|
|
1092
|
-
`agent_status` feeds the app two lists — `recent_results` (
|
|
1093
|
-
|
|
1094
|
-
tagged with a `notify` flag computed from the toggles
|
|
1095
|
+
`agent_status` feeds the app two lists — `recent_results` (named skill/lesson
|
|
1096
|
+
write events plus completed-task history) and `recent_failures` (the two failure
|
|
1097
|
+
sources above) — each item tagged with a `notify` flag computed from the toggles
|
|
1098
|
+
below. The app **lists**
|
|
1095
1099
|
every item in its menu but only **posts a banner** for flagged ones, so turning a
|
|
1096
|
-
category off silences the banner without hiding the history.
|
|
1097
|
-
|
|
1100
|
+
category off silences the banner without hiding the history. Each materialization
|
|
1101
|
+
uses its own skill or lesson toggle; generic task reports are history-only.
|
|
1102
|
+
Foreground writes appear too, without waiting for a background child to finish.
|
|
1103
|
+
A pathless skill mark only silences its reminder and has no materialization
|
|
1104
|
+
banner. Enabling a toggle never replays backlog.
|
|
1098
1105
|
|
|
1099
1106
|
**Settings in the app.** The menu-bar app's **Settings ▸ Notifications** tab
|
|
1100
1107
|
edits these same `THREADKEEPER_NOTIFY_*` keys visually — switches for the
|
|
@@ -1014,7 +1014,10 @@ Three detection sources per tick:
|
|
|
1014
1014
|
from an exhausted subscription; the real outcome only lands in
|
|
1015
1015
|
`tasks.return_code`. The reason is read from the child's log tail.
|
|
1016
1016
|
3. **Materialization** — `skill_materialized` / `skill_create` (skill) and
|
|
1017
|
-
`lesson_append` (lesson).
|
|
1017
|
+
`lesson_append` (lesson). Each notification contains one readable artifact
|
|
1018
|
+
name: the skill's first heading (falling back to its directory name), or
|
|
1019
|
+
the lesson name with slug separators replaced by spaces. Paths, provenance,
|
|
1020
|
+
and operation metadata stay out of the notification text.
|
|
1018
1021
|
|
|
1019
1022
|
A per-loop cooldown collapses a lapsed-subscription storm into one actionable
|
|
1020
1023
|
alert; the first run seeds its cursor to the current position, so historical
|
|
@@ -1045,12 +1048,16 @@ will register the app in **System Settings ▸ Notifications** and show banners;
|
|
|
1045
1048
|
banners are titled **Thread-Keeper** (`CFBundleDisplayName`). The first launch
|
|
1046
1049
|
after an update shows a one-time permission prompt.
|
|
1047
1050
|
|
|
1048
|
-
`agent_status` feeds the app two lists — `recent_results` (
|
|
1049
|
-
|
|
1050
|
-
tagged with a `notify` flag computed from the toggles
|
|
1051
|
+
`agent_status` feeds the app two lists — `recent_results` (named skill/lesson
|
|
1052
|
+
write events plus completed-task history) and `recent_failures` (the two failure
|
|
1053
|
+
sources above) — each item tagged with a `notify` flag computed from the toggles
|
|
1054
|
+
below. The app **lists**
|
|
1051
1055
|
every item in its menu but only **posts a banner** for flagged ones, so turning a
|
|
1052
|
-
category off silences the banner without hiding the history.
|
|
1053
|
-
|
|
1056
|
+
category off silences the banner without hiding the history. Each materialization
|
|
1057
|
+
uses its own skill or lesson toggle; generic task reports are history-only.
|
|
1058
|
+
Foreground writes appear too, without waiting for a background child to finish.
|
|
1059
|
+
A pathless skill mark only silences its reminder and has no materialization
|
|
1060
|
+
banner. Enabling a toggle never replays backlog.
|
|
1054
1061
|
|
|
1055
1062
|
**Settings in the app.** The menu-bar app's **Settings ▸ Notifications** tab
|
|
1056
1063
|
edits these same `THREADKEEPER_NOTIFY_*` keys visually — switches for the
|
|
@@ -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.3"
|
|
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" }]
|
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
_FAKE_CID = "33334444-5555-6666-7777-888899990000"
|
|
9
11
|
|
|
@@ -468,6 +470,42 @@ def test_agent_status_recent_results_for_useful_completed_tasks(mp_with_cid):
|
|
|
468
470
|
assert snap["recent_results"][0]["summary"] == (
|
|
469
471
|
"Processed 2 candidates into durable notes."
|
|
470
472
|
)
|
|
473
|
+
assert snap["recent_results"][0]["notify"] is False
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@pytest.mark.parametrize("poll,skill,lesson", [
|
|
477
|
+
(30, True, True), (30, True, False), (30, False, True), (0, True, True),
|
|
478
|
+
])
|
|
479
|
+
def test_materialization_results_name_actual_writes(mp_with_cid, poll, skill, lesson):
|
|
480
|
+
pkg = mp_with_cid(_FAKE_CID)
|
|
481
|
+
pkg["config"].NOTIFY_POLL_S = poll
|
|
482
|
+
pkg["config"].NOTIFY_SKILL_MATERIALIZED = skill
|
|
483
|
+
pkg["config"].NOTIFY_LESSON = lesson
|
|
484
|
+
result = _txt(_tool(pkg, "skill_manage")(
|
|
485
|
+
action="create", name="api-contract-testing",
|
|
486
|
+
description="Test API contracts.", content="# API contract testing\n\nUse schemas.",
|
|
487
|
+
))
|
|
488
|
+
assert result.startswith("ok"), result
|
|
489
|
+
result = _txt(_tool(pkg, "lesson_append")(
|
|
490
|
+
title="Verify state transitions", body="Assert the state after the action.",
|
|
491
|
+
source="T1",
|
|
492
|
+
))
|
|
493
|
+
assert result.startswith("ok"), result
|
|
494
|
+
_insert_completed_task(pkg, "tk_generic_done",
|
|
495
|
+
"You are a CANDIDATE REVIEWER for thread-keeper's extract queue.",
|
|
496
|
+
"Processed 2 candidates into durable notes.\n")
|
|
497
|
+
|
|
498
|
+
from threadkeeper.agent_status import agent_status_snapshot
|
|
499
|
+
|
|
500
|
+
items = agent_status_snapshot(refresh=False)["recent_results"]
|
|
501
|
+
by_role = {item["role"]: item for item in items}
|
|
502
|
+
assert by_role["skill"]["summary"] == "API contract testing"
|
|
503
|
+
assert by_role["lesson"]["summary"] == "Verify state transitions"
|
|
504
|
+
assert by_role["skill"]["notify"] is bool(poll and skill)
|
|
505
|
+
assert by_role["lesson"]["notify"] is bool(poll and lesson)
|
|
506
|
+
assert by_role["candidate_reviewer"]["notify"] is False
|
|
507
|
+
assert all(item["id"].startswith("materialization:")
|
|
508
|
+
for item in items if item["role"] in ("skill", "lesson"))
|
|
471
509
|
|
|
472
510
|
|
|
473
511
|
def test_agent_status_mcp_json_output(mp_with_cid):
|
|
@@ -228,8 +228,48 @@ def test_positives_on_fire(tmp_path, monkeypatch, caplog):
|
|
|
228
228
|
with caplog.at_level(logging.WARNING, logger="threadkeeper.notify"):
|
|
229
229
|
out = notify.run_notify_pass(force=True)
|
|
230
230
|
assert out == "ok fired=2", out
|
|
231
|
-
assert "skill materialized" in caplog.text
|
|
232
|
-
assert "lesson added
|
|
231
|
+
assert "skill materialized | Foo" in caplog.text
|
|
232
|
+
assert "lesson added | My lesson" in caplog.text
|
|
233
|
+
assert "/skills/" not in caplog.text
|
|
234
|
+
assert "op=create" not in caplog.text and "source=shadow" not in caplog.text
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.mark.parametrize("directory", [False, True])
|
|
238
|
+
def test_skill_notification_uses_heading(tmp_path, monkeypatch, directory):
|
|
239
|
+
m = _bootstrap(tmp_path, monkeypatch, skill="true")
|
|
240
|
+
md = tmp_path / "api-contract-testing" / "SKILL.md"
|
|
241
|
+
md.parent.mkdir()
|
|
242
|
+
md.write_text("---\nname: api-contract-testing\ndescription: |\n"
|
|
243
|
+
" Internal description\n---\n\n# API contract testing\n"
|
|
244
|
+
"\nLong implementation details that do not belong in a banner.\n")
|
|
245
|
+
notify = m["notify"]
|
|
246
|
+
conn = m["db"].get_db()
|
|
247
|
+
sent = []
|
|
248
|
+
monkeypatch.setattr(notify, "_dispatch", lambda *args: sent.append(args))
|
|
249
|
+
assert notify.run_notify_pass(force=True) == "seed"
|
|
250
|
+
_ev(conn, "skill_materialized", target="T1",
|
|
251
|
+
summary=str(md.parent if directory else md))
|
|
252
|
+
assert notify.run_notify_pass(force=True) == "ok fired=1"
|
|
253
|
+
assert sent == [("Thread-keeper: skill materialized", "API contract testing")]
|
|
254
|
+
assert notify.run_notify_pass(force=True) == "ok fired=0"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.parametrize("body", [b"---\nname: foo\n---\nNo heading.", b"\xff"])
|
|
258
|
+
def test_skill_notification_falls_back_to_name(tmp_path, monkeypatch, body):
|
|
259
|
+
notify = _bootstrap(tmp_path, monkeypatch)["notify"]
|
|
260
|
+
md = tmp_path / "api-contract-testing" / "SKILL.md"
|
|
261
|
+
md.parent.mkdir()
|
|
262
|
+
md.write_bytes(body)
|
|
263
|
+
assert notify._fmt_skill({"kind": "skill_create", "target": "api-contract-testing",
|
|
264
|
+
"summary": str(md)})[1] == "Api contract testing"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_pathless_skill_mark_does_not_claim_a_materialization(tmp_path, monkeypatch):
|
|
268
|
+
m = _bootstrap(tmp_path, monkeypatch, skill="true")
|
|
269
|
+
notify, conn = m["notify"], m["db"].get_db()
|
|
270
|
+
assert notify.run_notify_pass(force=True) == "seed"
|
|
271
|
+
_ev(conn, "skill_materialized", target="T1", summary="(no path recorded)")
|
|
272
|
+
assert notify.run_notify_pass(force=True) == "ok fired=0"
|
|
233
273
|
|
|
234
274
|
|
|
235
275
|
# ── disabled ────────────────────────────────────────────────────────────────
|
|
@@ -878,17 +878,47 @@ def _extract_useful_result(task_id: str) -> str:
|
|
|
878
878
|
return ""
|
|
879
879
|
|
|
880
880
|
|
|
881
|
-
def
|
|
881
|
+
def _recent_materializations(conn, now: int, limit: int) -> list[dict[str, Any]]:
|
|
882
|
+
"""Name actual writes, including foreground writes without a child task."""
|
|
882
883
|
from . import config
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
# notifications are enabled (NOTIFY_POLL_S master gate) and a positive toggle
|
|
886
|
-
# is on, but the menu always lists them (`notify` flag).
|
|
884
|
+
from .notify import _fmt_skill, _fmt_lesson
|
|
885
|
+
|
|
887
886
|
enabled = float(getattr(config, "NOTIFY_POLL_S", 0) or 0) > 0
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
887
|
+
rows = conn.execute(
|
|
888
|
+
"SELECT id, kind, target, summary, created_at FROM events "
|
|
889
|
+
"WHERE kind IN ('skill_create', 'skill_materialized', 'lesson_append') "
|
|
890
|
+
"AND created_at>=? ORDER BY created_at DESC, id DESC LIMIT ?",
|
|
891
|
+
(now - _RESULT_WINDOW_S, int(limit)),
|
|
892
|
+
).fetchall()
|
|
893
|
+
results = []
|
|
894
|
+
for row in rows:
|
|
895
|
+
skill = row["kind"] != "lesson_append"
|
|
896
|
+
title, summary = (_fmt_skill if skill else _fmt_lesson)(dict(row))
|
|
897
|
+
if not summary:
|
|
898
|
+
continue
|
|
899
|
+
age_s = max(0, now - row["created_at"])
|
|
900
|
+
results.append({
|
|
901
|
+
"id": f"materialization:{row['id']}",
|
|
902
|
+
"task_id": "",
|
|
903
|
+
"role": "skill" if skill else "lesson",
|
|
904
|
+
"loop_id": "",
|
|
905
|
+
"loop_name": "Skill" if skill else "Lesson",
|
|
906
|
+
"title": title,
|
|
907
|
+
"summary": summary,
|
|
908
|
+
"ended_at": row["created_at"],
|
|
909
|
+
"age_s": age_s,
|
|
910
|
+
"age": fmt_age(age_s),
|
|
911
|
+
"notify": enabled and bool(
|
|
912
|
+
config.NOTIFY_SKILL_MATERIALIZED if skill else config.NOTIFY_LESSON
|
|
913
|
+
),
|
|
914
|
+
})
|
|
915
|
+
return results
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _recent_results(conn, now: int, limit: int = 10) -> list[dict[str, Any]]:
|
|
919
|
+
# Event-backed materializations carry names and per-category toggles.
|
|
920
|
+
# Generic completion reports remain in history without duplicate banners.
|
|
921
|
+
materializations = _recent_materializations(conn, now, limit)
|
|
892
922
|
role_loop = _role_to_loop()
|
|
893
923
|
rows = conn.execute(
|
|
894
924
|
"SELECT id, prompt, ended_at, return_code FROM tasks "
|
|
@@ -919,11 +949,12 @@ def _recent_results(conn, now: int, limit: int = 10) -> list[dict[str, Any]]:
|
|
|
919
949
|
"ended_at": ended_at,
|
|
920
950
|
"age_s": age_s,
|
|
921
951
|
"age": fmt_age(age_s),
|
|
922
|
-
"notify":
|
|
952
|
+
"notify": False,
|
|
923
953
|
})
|
|
924
954
|
if len(results) >= limit:
|
|
925
955
|
break
|
|
926
|
-
return results
|
|
956
|
+
return sorted(materializations + results, key=lambda r: r["ended_at"],
|
|
957
|
+
reverse=True)[:limit]
|
|
927
958
|
|
|
928
959
|
|
|
929
960
|
def _recent_failures(conn, now: int, limit: int = 10) -> list[dict[str, Any]]:
|
|
@@ -27,6 +27,8 @@ from __future__ import annotations
|
|
|
27
27
|
|
|
28
28
|
import logging
|
|
29
29
|
import os
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
import re
|
|
30
32
|
import sqlite3
|
|
31
33
|
import subprocess
|
|
32
34
|
import threading
|
|
@@ -349,11 +351,43 @@ def _fmt_child(c: dict) -> tuple[str, str]:
|
|
|
349
351
|
|
|
350
352
|
|
|
351
353
|
def _fmt_skill(s: dict) -> tuple[str, str]:
|
|
352
|
-
|
|
354
|
+
path = (s.get("summary") or "").strip()
|
|
355
|
+
name = s.get("target", "") if s.get("kind") == "skill_create" else ""
|
|
356
|
+
if path and path != "(no path recorded)":
|
|
357
|
+
md = Path(path).expanduser()
|
|
358
|
+
if md.name == "SKILL.md":
|
|
359
|
+
name = md.parent.name
|
|
360
|
+
else:
|
|
361
|
+
name = md.name
|
|
362
|
+
md = md / "SKILL.md"
|
|
363
|
+
try:
|
|
364
|
+
# A title is near the top. Never load an entire skill (or follow
|
|
365
|
+
# paths mentioned in its body) just to render a notification.
|
|
366
|
+
body = ""
|
|
367
|
+
if md.is_file():
|
|
368
|
+
with md.open(encoding="utf-8") as stream:
|
|
369
|
+
body = stream.read(8192)
|
|
370
|
+
if body.startswith("---\n"):
|
|
371
|
+
body = body.split("\n---", 1)[-1]
|
|
372
|
+
heading = re.search(r"^#\s+(.+?)(?:\s+#+)?\s*$", body, re.MULTILINE)
|
|
373
|
+
if heading:
|
|
374
|
+
name = heading.group(1)
|
|
375
|
+
except (OSError, ValueError):
|
|
376
|
+
pass
|
|
377
|
+
return ("Thread-keeper: skill materialized", _artifact_name(name))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _artifact_name(name: str) -> str:
|
|
381
|
+
"""One readable name, without slug separators or Markdown decoration."""
|
|
382
|
+
name = " ".join((name or "").replace("`", "").split())
|
|
383
|
+
if " " not in name:
|
|
384
|
+
name = re.sub(r"[-_]+", " ", name)
|
|
385
|
+
name = name[:1].upper() + name[1:]
|
|
386
|
+
return name[:200]
|
|
353
387
|
|
|
354
388
|
|
|
355
389
|
def _fmt_lesson(l: dict) -> tuple[str, str]:
|
|
356
|
-
return ("Thread-keeper: lesson added",
|
|
390
|
+
return ("Thread-keeper: lesson added", _artifact_name(l["target"]))
|
|
357
391
|
|
|
358
392
|
|
|
359
393
|
# ── pass + daemon lifecycle ─────────────────────────────────────────────────
|
|
@@ -401,8 +435,10 @@ def run_notify_pass(force: bool = False, *, scheduled: bool = False) -> str:
|
|
|
401
435
|
_dispatch(*_fmt_child(c))
|
|
402
436
|
fired += 1
|
|
403
437
|
for s in skills:
|
|
404
|
-
|
|
405
|
-
|
|
438
|
+
title, body = _fmt_skill(s)
|
|
439
|
+
if body: # A pathless mark only silences a nudge; no named artifact.
|
|
440
|
+
_dispatch(title, body)
|
|
441
|
+
fired += 1
|
|
406
442
|
for l in lessons:
|
|
407
443
|
_dispatch(*_fmt_lesson(l))
|
|
408
444
|
fired += 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threadkeeper
|
|
3
|
-
Version: 0.17.
|
|
3
|
+
Version: 0.17.3
|
|
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
|
|
@@ -1058,7 +1058,10 @@ Three detection sources per tick:
|
|
|
1058
1058
|
from an exhausted subscription; the real outcome only lands in
|
|
1059
1059
|
`tasks.return_code`. The reason is read from the child's log tail.
|
|
1060
1060
|
3. **Materialization** — `skill_materialized` / `skill_create` (skill) and
|
|
1061
|
-
`lesson_append` (lesson).
|
|
1061
|
+
`lesson_append` (lesson). Each notification contains one readable artifact
|
|
1062
|
+
name: the skill's first heading (falling back to its directory name), or
|
|
1063
|
+
the lesson name with slug separators replaced by spaces. Paths, provenance,
|
|
1064
|
+
and operation metadata stay out of the notification text.
|
|
1062
1065
|
|
|
1063
1066
|
A per-loop cooldown collapses a lapsed-subscription storm into one actionable
|
|
1064
1067
|
alert; the first run seeds its cursor to the current position, so historical
|
|
@@ -1089,12 +1092,16 @@ will register the app in **System Settings ▸ Notifications** and show banners;
|
|
|
1089
1092
|
banners are titled **Thread-Keeper** (`CFBundleDisplayName`). The first launch
|
|
1090
1093
|
after an update shows a one-time permission prompt.
|
|
1091
1094
|
|
|
1092
|
-
`agent_status` feeds the app two lists — `recent_results` (
|
|
1093
|
-
|
|
1094
|
-
tagged with a `notify` flag computed from the toggles
|
|
1095
|
+
`agent_status` feeds the app two lists — `recent_results` (named skill/lesson
|
|
1096
|
+
write events plus completed-task history) and `recent_failures` (the two failure
|
|
1097
|
+
sources above) — each item tagged with a `notify` flag computed from the toggles
|
|
1098
|
+
below. The app **lists**
|
|
1095
1099
|
every item in its menu but only **posts a banner** for flagged ones, so turning a
|
|
1096
|
-
category off silences the banner without hiding the history.
|
|
1097
|
-
|
|
1100
|
+
category off silences the banner without hiding the history. Each materialization
|
|
1101
|
+
uses its own skill or lesson toggle; generic task reports are history-only.
|
|
1102
|
+
Foreground writes appear too, without waiting for a background child to finish.
|
|
1103
|
+
A pathless skill mark only silences its reminder and has no materialization
|
|
1104
|
+
banner. Enabling a toggle never replays backlog.
|
|
1098
1105
|
|
|
1099
1106
|
**Settings in the app.** The menu-bar app's **Settings ▸ Notifications** tab
|
|
1100
1107
|
edits these same `THREADKEEPER_NOTIFY_*` keys visually — switches for the
|
|
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
|
|
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
|