threadkeeper 0.12.0__tar.gz → 0.13.1__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.12.0 → threadkeeper-0.13.1}/PKG-INFO +49 -12
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/README.md +47 -11
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/pyproject.toml +2 -1
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_menubar_app.py +67 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_config.py +4 -4
- threadkeeper-0.13.1/tests/test_verify_ingest.py +162 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/README.md +13 -6
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +241 -57
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/menubar_app.py +32 -9
- threadkeeper-0.13.1/threadkeeper/verify_ingest.py +313 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/PKG-INFO +49 -12
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/SOURCES.txt +2 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/requires.txt +1 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/LICENSE +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/setup.cfg +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_adapters.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_curator.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_identity.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_lessons.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_nudges.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_panel.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_process_health.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skills.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_threads.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/gemini.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/config.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/db.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/server.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.12.0 → threadkeeper-0.13.1}/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.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Gemini, Copilot, 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
|
|
@@ -25,6 +25,7 @@ License-File: LICENSE
|
|
|
25
25
|
Requires-Dist: mcp>=1.0.0
|
|
26
26
|
Requires-Dist: pydantic>=2
|
|
27
27
|
Requires-Dist: pydantic-settings>=2
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
29
|
Provides-Extra: semantic
|
|
29
30
|
Requires-Dist: fastembed>=0.3; extra == "semantic"
|
|
30
31
|
Requires-Dist: numpy>=1.24.0; extra == "semantic"
|
|
@@ -221,7 +222,7 @@ tk-agent-status --cleanup-memory
|
|
|
221
222
|
```
|
|
222
223
|
|
|
223
224
|
`apps/macos-agent-status/` contains a small macOS menu-bar app that polls this
|
|
224
|
-
command every
|
|
225
|
+
command every 15 seconds and shows every autonomous learning loop: enabled/off,
|
|
225
226
|
running/idle/ready, last pass, backlog, and active child RSS when that loop has
|
|
226
227
|
spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
|
|
227
228
|
`threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
|
|
@@ -239,17 +240,22 @@ memory button, self-restarts when its own RSS crosses
|
|
|
239
240
|
notification permission, and sends a notification when a newly completed
|
|
240
241
|
autonomous child task produces a useful result in `recent_results`; the first
|
|
241
242
|
poll only marks existing results as seen, so old completions do not spam
|
|
242
|
-
notifications.
|
|
243
|
+
notifications. Status polling and cleanup commands run off the main actor, so
|
|
244
|
+
opening the popover does not wait for `tk-agent-status --json`. The header gear
|
|
245
|
+
opens a separate Settings window for
|
|
243
246
|
`~/.threadkeeper/.env`: common knobs are grouped into guided controls, the raw
|
|
244
247
|
`.env` remains editable for advanced values, three local presets can be saved
|
|
245
248
|
and loaded, and Save & Restart writes the file then asks existing
|
|
246
249
|
`threadkeeper.server` processes to exit so MCP hosts reconnect with the new
|
|
247
|
-
configuration.
|
|
250
|
+
configuration. Spawn CLI selectors collapse `agy` into canonical `antigravity`
|
|
251
|
+
while keeping `gemini` as legacy, and model selectors use dropdowns with exact
|
|
252
|
+
CLI model ids/labels instead of free-text fields. Probe backlog is due objective
|
|
248
253
|
probes only, not every registered probe, so a healthy cooldown shows `0 due
|
|
249
254
|
probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
|
|
250
|
-
automatically installs and launches it on MCP startup
|
|
251
|
-
|
|
252
|
-
running
|
|
255
|
+
automatically installs and launches it on MCP startup. The installed app records
|
|
256
|
+
a source fingerprint, so package upgrades rebuild the helper even when an older
|
|
257
|
+
bundle has a newer file timestamp, then restart any stale running menu-bar
|
|
258
|
+
process. Set
|
|
253
259
|
`THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
|
|
254
260
|
|
|
255
261
|
### Auto Update
|
|
@@ -633,11 +639,15 @@ keys are lowercased:
|
|
|
633
639
|
# default agent for roles with no explicit pin ("" / unset = use the active CLI)
|
|
634
640
|
THREADKEEPER_SPAWN__DEFAULT=claude
|
|
635
641
|
# per-role CLI: THREADKEEPER_SPAWN__LOOP__<ROLE>=<cli>
|
|
642
|
+
# supported CLI keys: claude, codex, antigravity (agy executable), gemini (legacy), copilot
|
|
636
643
|
THREADKEEPER_SPAWN__LOOP__SHADOW_OBSERVER=claude # heaviest reasoning → keep on Claude
|
|
637
644
|
THREADKEEPER_SPAWN__LOOP__CURATOR=codex # weekly audit → Codex is fine
|
|
638
645
|
THREADKEEPER_SPAWN__LOOP__CANDIDATE_REVIEWER=auto # "auto" = follow active CLI
|
|
639
646
|
# model pin per CLI or per role: THREADKEEPER_SPAWN__MODEL__<KEY>=<model>
|
|
640
647
|
THREADKEEPER_SPAWN__MODEL__CLAUDE=opus
|
|
648
|
+
THREADKEEPER_SPAWN__MODEL__CODEX=gpt-5.5
|
|
649
|
+
THREADKEEPER_SPAWN__MODEL__AGY="Gemini 3.1 Pro (High)"
|
|
650
|
+
THREADKEEPER_SPAWN__MODEL__GEMINI=gemini-3.1-pro-preview
|
|
641
651
|
THREADKEEPER_SPAWN__MODEL__DIALECTIC_VALIDATOR=opus
|
|
642
652
|
```
|
|
643
653
|
|
|
@@ -645,7 +655,9 @@ Resolution per role: `SPAWN__LOOP__<role>` → `SPAWN__DEFAULT` → active CLI
|
|
|
645
655
|
`claude`; `"auto"` (or unset) defers to the active CLI. Real environment
|
|
646
656
|
variables override the `.env`. Force host detection with
|
|
647
657
|
`THREADKEEPER_ACTIVE_CLI=claude` (or `codex`, `antigravity`/`agy`,
|
|
648
|
-
`gemini`, `copilot`).
|
|
658
|
+
`gemini`, `copilot`). `agy` is normalized to `antigravity`; `gemini` remains a
|
|
659
|
+
legacy Gemini CLI adapter for old installs/enterprise paths. See `.env.example`
|
|
660
|
+
for the full knob list.
|
|
649
661
|
|
|
650
662
|
Adapters without headless support (Claude Desktop, VS Code) can't be
|
|
651
663
|
spawn targets — `spawn_status()` reports them as "no adapter" and any
|
|
@@ -745,12 +757,34 @@ unchanged.
|
|
|
745
757
|
## Verifying ingest across CLIs
|
|
746
758
|
|
|
747
759
|
```bash
|
|
748
|
-
python scripts/tk_verify_ingest.py
|
|
760
|
+
python scripts/tk_verify_ingest.py # both checks below
|
|
761
|
+
python scripts/tk_verify_ingest.py --contract # parse/ingest contract only
|
|
762
|
+
python scripts/tk_verify_ingest.py --live # production verdict only
|
|
763
|
+
python scripts/tk_verify_ingest.py --live --json # machine-readable
|
|
749
764
|
```
|
|
750
765
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
766
|
+
Two read-only checks:
|
|
767
|
+
|
|
768
|
+
- **Contract test** (`--contract`) — walks every installed CLI adapter,
|
|
769
|
+
parses recent transcripts into an isolated tempdir DB, reports
|
|
770
|
+
per-source message counts and flags any adapter that parsed messages
|
|
771
|
+
but silently failed to persist them. Answers *"does the pipeline
|
|
772
|
+
work?"*
|
|
773
|
+
- **Production verification** (`--live`) — reads the **live**
|
|
774
|
+
`dialog_messages` table read-only and scores the three acceptance
|
|
775
|
+
criteria from [roadmap issue #1](https://github.com/po4erk91/thread-keeper/issues/1):
|
|
776
|
+
(1) every targeted CLI *slot* has production rows, (2) shadow-review
|
|
777
|
+
sees more than one adapter in the same recent window, (3) the learning
|
|
778
|
+
loop has fired on non-Claude sessions. Emits a `PASS` / `PARTIAL` /
|
|
779
|
+
`FAIL` verdict. The four slots are `claude-code`, `codex`, `copilot`,
|
|
780
|
+
and `google` — where the Google slot is satisfied by *either* the
|
|
781
|
+
legacy `gemini` adapter or its successor Antigravity (`agy`), since
|
|
782
|
+
both live under `~/.gemini`.
|
|
783
|
+
|
|
784
|
+
`--strict` makes the process exit non-zero unless the live verdict is
|
|
785
|
+
`PASS`, so it can gate CI; `PARTIAL` (e.g. a box that doesn't run all
|
|
786
|
+
four CLIs) is a valid real-world state and exits 0 by default. The
|
|
787
|
+
reusable verdict logic lives in `threadkeeper/verify_ingest.py`.
|
|
754
788
|
|
|
755
789
|
---
|
|
756
790
|
|
|
@@ -776,6 +810,7 @@ threadkeeper/
|
|
|
776
810
|
├── db.py # SQLite schema + sqlite-vec loader
|
|
777
811
|
├── identity.py # session, self-cid, daemon launchers
|
|
778
812
|
├── ingest.py # adapter-driven transcript ingest
|
|
813
|
+
├── verify_ingest.py # cross-CLI production verification verdict
|
|
779
814
|
├── brief.py # render_brief / render_context
|
|
780
815
|
├── shadow_review.py # autonomous learning observer
|
|
781
816
|
├── i18n.py # 10 locales of regex + prompt bundles
|
|
@@ -814,3 +849,5 @@ locale. Look for the `good-first-issue` label.
|
|
|
814
849
|
## License
|
|
815
850
|
|
|
816
851
|
MIT — see [LICENSE](LICENSE).
|
|
852
|
+
|
|
853
|
+
<!-- mcp-name: io.github.po4erk91/thread-keeper -->
|
|
@@ -180,7 +180,7 @@ tk-agent-status --cleanup-memory
|
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
`apps/macos-agent-status/` contains a small macOS menu-bar app that polls this
|
|
183
|
-
command every
|
|
183
|
+
command every 15 seconds and shows every autonomous learning loop: enabled/off,
|
|
184
184
|
running/idle/ready, last pass, backlog, and active child RSS when that loop has
|
|
185
185
|
spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
|
|
186
186
|
`threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
|
|
@@ -198,17 +198,22 @@ memory button, self-restarts when its own RSS crosses
|
|
|
198
198
|
notification permission, and sends a notification when a newly completed
|
|
199
199
|
autonomous child task produces a useful result in `recent_results`; the first
|
|
200
200
|
poll only marks existing results as seen, so old completions do not spam
|
|
201
|
-
notifications.
|
|
201
|
+
notifications. Status polling and cleanup commands run off the main actor, so
|
|
202
|
+
opening the popover does not wait for `tk-agent-status --json`. The header gear
|
|
203
|
+
opens a separate Settings window for
|
|
202
204
|
`~/.threadkeeper/.env`: common knobs are grouped into guided controls, the raw
|
|
203
205
|
`.env` remains editable for advanced values, three local presets can be saved
|
|
204
206
|
and loaded, and Save & Restart writes the file then asks existing
|
|
205
207
|
`threadkeeper.server` processes to exit so MCP hosts reconnect with the new
|
|
206
|
-
configuration.
|
|
208
|
+
configuration. Spawn CLI selectors collapse `agy` into canonical `antigravity`
|
|
209
|
+
while keeping `gemini` as legacy, and model selectors use dropdowns with exact
|
|
210
|
+
CLI model ids/labels instead of free-text fields. Probe backlog is due objective
|
|
207
211
|
probes only, not every registered probe, so a healthy cooldown shows `0 due
|
|
208
212
|
probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
|
|
209
|
-
automatically installs and launches it on MCP startup
|
|
210
|
-
|
|
211
|
-
running
|
|
213
|
+
automatically installs and launches it on MCP startup. The installed app records
|
|
214
|
+
a source fingerprint, so package upgrades rebuild the helper even when an older
|
|
215
|
+
bundle has a newer file timestamp, then restart any stale running menu-bar
|
|
216
|
+
process. Set
|
|
212
217
|
`THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
|
|
213
218
|
|
|
214
219
|
### Auto Update
|
|
@@ -592,11 +597,15 @@ keys are lowercased:
|
|
|
592
597
|
# default agent for roles with no explicit pin ("" / unset = use the active CLI)
|
|
593
598
|
THREADKEEPER_SPAWN__DEFAULT=claude
|
|
594
599
|
# per-role CLI: THREADKEEPER_SPAWN__LOOP__<ROLE>=<cli>
|
|
600
|
+
# supported CLI keys: claude, codex, antigravity (agy executable), gemini (legacy), copilot
|
|
595
601
|
THREADKEEPER_SPAWN__LOOP__SHADOW_OBSERVER=claude # heaviest reasoning → keep on Claude
|
|
596
602
|
THREADKEEPER_SPAWN__LOOP__CURATOR=codex # weekly audit → Codex is fine
|
|
597
603
|
THREADKEEPER_SPAWN__LOOP__CANDIDATE_REVIEWER=auto # "auto" = follow active CLI
|
|
598
604
|
# model pin per CLI or per role: THREADKEEPER_SPAWN__MODEL__<KEY>=<model>
|
|
599
605
|
THREADKEEPER_SPAWN__MODEL__CLAUDE=opus
|
|
606
|
+
THREADKEEPER_SPAWN__MODEL__CODEX=gpt-5.5
|
|
607
|
+
THREADKEEPER_SPAWN__MODEL__AGY="Gemini 3.1 Pro (High)"
|
|
608
|
+
THREADKEEPER_SPAWN__MODEL__GEMINI=gemini-3.1-pro-preview
|
|
600
609
|
THREADKEEPER_SPAWN__MODEL__DIALECTIC_VALIDATOR=opus
|
|
601
610
|
```
|
|
602
611
|
|
|
@@ -604,7 +613,9 @@ Resolution per role: `SPAWN__LOOP__<role>` → `SPAWN__DEFAULT` → active CLI
|
|
|
604
613
|
`claude`; `"auto"` (or unset) defers to the active CLI. Real environment
|
|
605
614
|
variables override the `.env`. Force host detection with
|
|
606
615
|
`THREADKEEPER_ACTIVE_CLI=claude` (or `codex`, `antigravity`/`agy`,
|
|
607
|
-
`gemini`, `copilot`).
|
|
616
|
+
`gemini`, `copilot`). `agy` is normalized to `antigravity`; `gemini` remains a
|
|
617
|
+
legacy Gemini CLI adapter for old installs/enterprise paths. See `.env.example`
|
|
618
|
+
for the full knob list.
|
|
608
619
|
|
|
609
620
|
Adapters without headless support (Claude Desktop, VS Code) can't be
|
|
610
621
|
spawn targets — `spawn_status()` reports them as "no adapter" and any
|
|
@@ -704,12 +715,34 @@ unchanged.
|
|
|
704
715
|
## Verifying ingest across CLIs
|
|
705
716
|
|
|
706
717
|
```bash
|
|
707
|
-
python scripts/tk_verify_ingest.py
|
|
718
|
+
python scripts/tk_verify_ingest.py # both checks below
|
|
719
|
+
python scripts/tk_verify_ingest.py --contract # parse/ingest contract only
|
|
720
|
+
python scripts/tk_verify_ingest.py --live # production verdict only
|
|
721
|
+
python scripts/tk_verify_ingest.py --live --json # machine-readable
|
|
708
722
|
```
|
|
709
723
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
724
|
+
Two read-only checks:
|
|
725
|
+
|
|
726
|
+
- **Contract test** (`--contract`) — walks every installed CLI adapter,
|
|
727
|
+
parses recent transcripts into an isolated tempdir DB, reports
|
|
728
|
+
per-source message counts and flags any adapter that parsed messages
|
|
729
|
+
but silently failed to persist them. Answers *"does the pipeline
|
|
730
|
+
work?"*
|
|
731
|
+
- **Production verification** (`--live`) — reads the **live**
|
|
732
|
+
`dialog_messages` table read-only and scores the three acceptance
|
|
733
|
+
criteria from [roadmap issue #1](https://github.com/po4erk91/thread-keeper/issues/1):
|
|
734
|
+
(1) every targeted CLI *slot* has production rows, (2) shadow-review
|
|
735
|
+
sees more than one adapter in the same recent window, (3) the learning
|
|
736
|
+
loop has fired on non-Claude sessions. Emits a `PASS` / `PARTIAL` /
|
|
737
|
+
`FAIL` verdict. The four slots are `claude-code`, `codex`, `copilot`,
|
|
738
|
+
and `google` — where the Google slot is satisfied by *either* the
|
|
739
|
+
legacy `gemini` adapter or its successor Antigravity (`agy`), since
|
|
740
|
+
both live under `~/.gemini`.
|
|
741
|
+
|
|
742
|
+
`--strict` makes the process exit non-zero unless the live verdict is
|
|
743
|
+
`PASS`, so it can gate CI; `PARTIAL` (e.g. a box that doesn't run all
|
|
744
|
+
four CLIs) is a valid real-world state and exits 0 by default. The
|
|
745
|
+
reusable verdict logic lives in `threadkeeper/verify_ingest.py`.
|
|
713
746
|
|
|
714
747
|
---
|
|
715
748
|
|
|
@@ -735,6 +768,7 @@ threadkeeper/
|
|
|
735
768
|
├── db.py # SQLite schema + sqlite-vec loader
|
|
736
769
|
├── identity.py # session, self-cid, daemon launchers
|
|
737
770
|
├── ingest.py # adapter-driven transcript ingest
|
|
771
|
+
├── verify_ingest.py # cross-CLI production verification verdict
|
|
738
772
|
├── brief.py # render_brief / render_context
|
|
739
773
|
├── shadow_review.py # autonomous learning observer
|
|
740
774
|
├── i18n.py # 10 locales of regex + prompt bundles
|
|
@@ -773,3 +807,5 @@ locale. Look for the `good-first-issue` label.
|
|
|
773
807
|
## License
|
|
774
808
|
|
|
775
809
|
MIT — see [LICENSE](LICENSE).
|
|
810
|
+
|
|
811
|
+
<!-- mcp-name: io.github.po4erk91/thread-keeper -->
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "threadkeeper"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.1"
|
|
8
8
|
description = "Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Gemini, Copilot, 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" }]
|
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
|
29
29
|
"mcp>=1.0.0",
|
|
30
30
|
"pydantic>=2",
|
|
31
31
|
"pydantic-settings>=2",
|
|
32
|
+
"pyyaml>=6.0",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
35
|
[project.optional-dependencies]
|
|
@@ -46,6 +46,8 @@ def test_menubar_status_item_uses_idle_chip_and_running_gears():
|
|
|
46
46
|
assert 'button.title = ""' in swift
|
|
47
47
|
assert 'button.title = " TK' not in swift
|
|
48
48
|
assert 'return "TK ' not in swift
|
|
49
|
+
assert "statusPollInterval: TimeInterval = 15.0" in swift
|
|
50
|
+
assert "Timer.scheduledTimer(withTimeInterval: statusPollInterval" in swift
|
|
49
51
|
assert "Timer(timeInterval: gearSpinInterval" in swift
|
|
50
52
|
assert "gearFrameStepDegrees = 17.0" in swift
|
|
51
53
|
assert "largeGearDiameter: CGFloat = 12.0" in swift
|
|
@@ -59,6 +61,9 @@ def test_menubar_status_item_uses_idle_chip_and_running_gears():
|
|
|
59
61
|
assert "store.snapshot.runningCount > 0" not in swift
|
|
60
62
|
assert "button.image = gearFrames" in swift
|
|
61
63
|
assert "TimelineView" not in swift
|
|
64
|
+
assert "refreshInFlight" in swift
|
|
65
|
+
assert "Task.detached(priority: .utility)" in swift
|
|
66
|
+
assert "nonisolated private static func runStatusCommand" in swift
|
|
62
67
|
assert "store.openEnvSettings()" in swift
|
|
63
68
|
assert '.help("Settings")' in swift
|
|
64
69
|
assert '.help("Refresh")' not in swift
|
|
@@ -67,6 +72,19 @@ def test_menubar_status_item_uses_idle_chip_and_running_gears():
|
|
|
67
72
|
assert '.help("Clean memory")' in swift
|
|
68
73
|
|
|
69
74
|
|
|
75
|
+
def test_menubar_popover_shows_before_status_refresh():
|
|
76
|
+
repo = Path(__file__).resolve().parents[1]
|
|
77
|
+
swift = (
|
|
78
|
+
repo / "apps" / "macos-agent-status" / "ThreadKeeperAgentStatus.swift"
|
|
79
|
+
).read_text(encoding="utf-8")
|
|
80
|
+
|
|
81
|
+
start = swift.index("@objc private func togglePopover")
|
|
82
|
+
end = swift.index(" private func updateStatusButton", start)
|
|
83
|
+
body = swift[start:end]
|
|
84
|
+
|
|
85
|
+
assert body.index("popover.show(") < body.index("store.refresh()")
|
|
86
|
+
|
|
87
|
+
|
|
70
88
|
def test_menubar_env_settings_window_edits_env_and_presets():
|
|
71
89
|
repo = Path(__file__).resolve().parents[1]
|
|
72
90
|
swift = (
|
|
@@ -81,6 +99,22 @@ def test_menubar_env_settings_window_edits_env_and_presets():
|
|
|
81
99
|
assert "(1...3).map" in swift
|
|
82
100
|
assert "EnvPresetCard" in swift
|
|
83
101
|
assert "mergeEnvText(raw:" in swift
|
|
102
|
+
assert "EnvSettingsTab" in swift
|
|
103
|
+
assert "case .raw:" in swift
|
|
104
|
+
assert "saveRaw(restart:" in swift
|
|
105
|
+
assert ".onChange(of: envStore.rawEnvText)" not in swift
|
|
106
|
+
assert "syncRawEditsIntoForm" not in swift
|
|
107
|
+
assert 'ChoiceOption("antigravity", label: "antigravity (agy)")' in swift
|
|
108
|
+
assert 'ChoiceOption("agy")' not in swift
|
|
109
|
+
assert 'ChoiceOption("gemini", label: "gemini (legacy)")' in swift
|
|
110
|
+
assert "antigravityModelChoices" in swift
|
|
111
|
+
assert "geminiLegacyModelChoices" in swift
|
|
112
|
+
assert '"Gemini 3.1 Pro (High)"' in swift
|
|
113
|
+
assert '"Gemini 3.5 Flash (Medium)"' in swift
|
|
114
|
+
assert '"gemini-3.1-pro-preview"' in swift
|
|
115
|
+
assert '"gemini-3.1-pro"' not in swift
|
|
116
|
+
assert "THREADKEEPER_SPAWN__MODEL__CODEX" in swift
|
|
117
|
+
assert "THREADKEEPER_SPAWN__MODEL__GEMINI" in swift
|
|
84
118
|
assert "THREADKEEPER_DISABLE_BG_DAEMONS" in swift
|
|
85
119
|
assert "THREADKEEPER_EVOLVE_APPLY_INTERVAL_S" in swift
|
|
86
120
|
assert "THREADKEEPER_SPAWN__MODEL__EVOLVE_APPLIER" in swift
|
|
@@ -96,6 +130,35 @@ def test_menubar_source_falls_back_to_packaged_assets(fresh_mp, tmp_path, monkey
|
|
|
96
130
|
assert menubar_app._source_dir() == menubar_app._package_source_dir()
|
|
97
131
|
|
|
98
132
|
|
|
133
|
+
def test_app_current_requires_matching_source_fingerprint(tmp_path):
|
|
134
|
+
import threadkeeper.menubar_app as menubar_app
|
|
135
|
+
|
|
136
|
+
src = tmp_path / "source"
|
|
137
|
+
src.mkdir()
|
|
138
|
+
for name in menubar_app.SOURCE_FILES:
|
|
139
|
+
(src / name).write_text(f"{name}\n", encoding="utf-8")
|
|
140
|
+
|
|
141
|
+
app = tmp_path / menubar_app.APP_BUNDLE
|
|
142
|
+
binary = app / "Contents" / "MacOS" / menubar_app.APP_NAME
|
|
143
|
+
plist = app / "Contents" / "Info.plist"
|
|
144
|
+
binary.parent.mkdir(parents=True)
|
|
145
|
+
plist.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
binary.write_text("old binary\n", encoding="utf-8")
|
|
147
|
+
plist.write_text("<plist></plist>\n", encoding="utf-8")
|
|
148
|
+
|
|
149
|
+
assert menubar_app._app_is_current(src, app) is False
|
|
150
|
+
|
|
151
|
+
marker = menubar_app._source_fingerprint_path(app)
|
|
152
|
+
marker.parent.mkdir(parents=True)
|
|
153
|
+
marker.write_text(menubar_app._source_fingerprint(src) + "\n", encoding="utf-8")
|
|
154
|
+
|
|
155
|
+
assert menubar_app._app_is_current(src, app) is True
|
|
156
|
+
|
|
157
|
+
(src / "ThreadKeeperAgentStatus.swift").write_text("// changed\n", encoding="utf-8")
|
|
158
|
+
|
|
159
|
+
assert menubar_app._app_is_current(src, app) is False
|
|
160
|
+
|
|
161
|
+
|
|
99
162
|
def test_install_app_builds_from_task_log_scratch_without_executable_bit(
|
|
100
163
|
fresh_mp,
|
|
101
164
|
tmp_path,
|
|
@@ -139,6 +202,10 @@ def test_install_app_builds_from_task_log_scratch_without_executable_bit(
|
|
|
139
202
|
assert calls[0][2] == task_logs / "menubar-build" / "source"
|
|
140
203
|
assert (installed / "Contents" / "Info.plist").exists()
|
|
141
204
|
assert (installed / "Contents" / "MacOS" / menubar_app.APP_NAME).exists()
|
|
205
|
+
marker = menubar_app._source_fingerprint_path(installed)
|
|
206
|
+
assert marker.read_text(encoding="utf-8").strip() == menubar_app._source_fingerprint(
|
|
207
|
+
src
|
|
208
|
+
)
|
|
142
209
|
assert not (src / "build").exists()
|
|
143
210
|
|
|
144
211
|
|
|
@@ -114,12 +114,12 @@ def test_resolve_model_from_dotenv(tmp_path, monkeypatch):
|
|
|
114
114
|
envf = tmp_path / "tk.env"
|
|
115
115
|
envf.write_text(
|
|
116
116
|
"THREADKEEPER_SPAWN__MODEL__CODEX=gpt-5.4\n"
|
|
117
|
-
|
|
117
|
+
'THREADKEEPER_SPAWN__MODEL__AGY="Gemini 3.1 Pro (High)"\n'
|
|
118
118
|
"THREADKEEPER_SPAWN__MODEL__GEMINI=gemini-2.5-pro\n"
|
|
119
119
|
)
|
|
120
120
|
sc = _reset(monkeypatch, tmp_path, env_file=str(envf))
|
|
121
121
|
assert sc.resolve_model("codex") == "gpt-5.4"
|
|
122
|
-
assert sc.resolve_model("antigravity") == "
|
|
122
|
+
assert sc.resolve_model("antigravity") == "Gemini 3.1 Pro (High)"
|
|
123
123
|
assert sc.resolve_model("gemini") == "gemini-2.5-pro"
|
|
124
124
|
assert sc.resolve_model("claude") == "" # no entry
|
|
125
125
|
|
|
@@ -216,12 +216,12 @@ def test_antigravity_spawn_argv_uses_p_flag(tmp_path, monkeypatch):
|
|
|
216
216
|
for name in [m for m in list(sys.modules) if m.startswith("threadkeeper")]:
|
|
217
217
|
del sys.modules[name]
|
|
218
218
|
from threadkeeper.adapters.antigravity import ADAPTER
|
|
219
|
-
argv = ADAPTER.spawn_argv("hello", model="
|
|
219
|
+
argv = ADAPTER.spawn_argv("hello", model="Gemini 3.1 Pro (High)")
|
|
220
220
|
if argv is None:
|
|
221
221
|
pytest.skip("agy binary not installed in test env")
|
|
222
222
|
assert "-p" in argv
|
|
223
223
|
assert "--model" in argv
|
|
224
|
-
assert "
|
|
224
|
+
assert "Gemini 3.1 Pro (High)" in argv
|
|
225
225
|
|
|
226
226
|
|
|
227
227
|
def test_gemini_spawn_argv_uses_p_flag(tmp_path, monkeypatch):
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Tests for the cross-CLI production verification harness (issue #1).
|
|
2
|
+
|
|
3
|
+
The verdict logic is pure, so most of this exercises ``evaluate_coverage``
|
|
4
|
+
and ``evaluate_verdict`` directly. One test drives the read-only SQL layer
|
|
5
|
+
against an in-memory sqlite so the live-DB query path is covered without a
|
|
6
|
+
real ~/.threadkeeper store.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
|
|
12
|
+
from threadkeeper.verify_ingest import (
|
|
13
|
+
CANONICAL_SLOTS,
|
|
14
|
+
collect_live_signals,
|
|
15
|
+
evaluate_coverage,
|
|
16
|
+
evaluate_verdict,
|
|
17
|
+
format_report,
|
|
18
|
+
slot_for_source,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_slot_mapping_groups_gemini_and_antigravity():
|
|
23
|
+
# Gemini legacy and Antigravity (agy) both satisfy the single Google slot.
|
|
24
|
+
assert slot_for_source("gemini") == "google"
|
|
25
|
+
assert slot_for_source("antigravity") == "google"
|
|
26
|
+
assert slot_for_source("claude-code") == "claude-code"
|
|
27
|
+
assert slot_for_source("vscode") is None # not a canonical slot
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_coverage_status_verified_thin_absent():
|
|
31
|
+
cov = evaluate_coverage(
|
|
32
|
+
{"claude-code": 200, "codex": 50, "copilot": 2, "gemini": 0},
|
|
33
|
+
thin_threshold=5,
|
|
34
|
+
)
|
|
35
|
+
assert cov["claude-code"]["status"] == "verified"
|
|
36
|
+
assert cov["codex"]["status"] == "verified"
|
|
37
|
+
assert cov["copilot"]["status"] == "thin" # 2 rows, below threshold
|
|
38
|
+
assert cov["google"]["status"] == "absent" # gemini=0, no antigravity rows
|
|
39
|
+
# every canonical slot is represented even when no source mapped to it
|
|
40
|
+
assert set(cov) == set(CANONICAL_SLOTS)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_coverage_antigravity_fills_google_slot():
|
|
44
|
+
cov = evaluate_coverage({"antigravity": 42}, thin_threshold=5)
|
|
45
|
+
assert cov["google"]["status"] == "verified"
|
|
46
|
+
assert cov["google"]["sources"] == {"antigravity": 42}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_verdict_pass_when_all_criteria_met():
|
|
50
|
+
rep = evaluate_verdict(
|
|
51
|
+
source_counts={
|
|
52
|
+
"claude-code": 100, "codex": 100, "copilot": 100, "antigravity": 100,
|
|
53
|
+
},
|
|
54
|
+
window_sources=["claude-code", "codex", "antigravity"],
|
|
55
|
+
shadow_passes=10,
|
|
56
|
+
)
|
|
57
|
+
assert rep["verdict"] == "PASS"
|
|
58
|
+
assert rep["criteria"]["all_sources_present"]["pass"] is True
|
|
59
|
+
assert rep["criteria"]["cross_adapter_window"]["pass"] is True
|
|
60
|
+
assert rep["criteria"]["learning_loop_non_claude"]["pass"] is True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_verdict_partial_three_of_four_slots():
|
|
64
|
+
# This is the real shape on a dev box: claude/codex/copilot present,
|
|
65
|
+
# google slot empty, but cross-adapter window + non-claude loop confirmed.
|
|
66
|
+
rep = evaluate_verdict(
|
|
67
|
+
source_counts={"claude-code": 200000, "codex": 11000, "copilot": 10},
|
|
68
|
+
window_sources=["claude-code", "codex"],
|
|
69
|
+
shadow_passes=2567,
|
|
70
|
+
)
|
|
71
|
+
assert rep["verdict"] == "PARTIAL"
|
|
72
|
+
assert rep["criteria"]["all_sources_present"]["pass"] is False
|
|
73
|
+
assert rep["criteria"]["all_sources_present"]["verified_slots"] == [
|
|
74
|
+
"claude-code", "codex", "copilot",
|
|
75
|
+
]
|
|
76
|
+
assert rep["criteria"]["cross_adapter_window"]["pass"] is True
|
|
77
|
+
assert rep["criteria"]["learning_loop_non_claude"]["pass"] is True
|
|
78
|
+
assert "codex" in rep["criteria"]["learning_loop_non_claude"]["sources"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_verdict_fail_single_adapter_only():
|
|
82
|
+
# Only Claude Code has data and the window — not a cross-CLI demonstration.
|
|
83
|
+
rep = evaluate_verdict(
|
|
84
|
+
source_counts={"claude-code": 5000},
|
|
85
|
+
window_sources=["claude-code"],
|
|
86
|
+
shadow_passes=100,
|
|
87
|
+
)
|
|
88
|
+
assert rep["verdict"] == "FAIL"
|
|
89
|
+
assert rep["criteria"]["cross_adapter_window"]["pass"] is False
|
|
90
|
+
assert rep["criteria"]["learning_loop_non_claude"]["pass"] is False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_verdict_fail_when_loop_never_ran():
|
|
94
|
+
rep = evaluate_verdict(
|
|
95
|
+
source_counts={"claude-code": 100, "codex": 100},
|
|
96
|
+
window_sources=["claude-code", "codex"],
|
|
97
|
+
shadow_passes=0, # learning loop has never fired
|
|
98
|
+
)
|
|
99
|
+
# cross-adapter window passes, but loop criterion fails and only 2 slots
|
|
100
|
+
# verified → PARTIAL (loop is one signal, window is the other).
|
|
101
|
+
assert rep["verdict"] == "PARTIAL"
|
|
102
|
+
assert rep["criteria"]["learning_loop_non_claude"]["pass"] is False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _seed_live_db(conn: sqlite3.Connection) -> None:
|
|
106
|
+
conn.execute(
|
|
107
|
+
"CREATE TABLE dialog_messages (source TEXT, created_at INTEGER)"
|
|
108
|
+
)
|
|
109
|
+
conn.execute("CREATE TABLE events (kind TEXT)")
|
|
110
|
+
rows = [
|
|
111
|
+
("claude-code", 1_000_000),
|
|
112
|
+
("claude-code", 1_000_500),
|
|
113
|
+
("codex", 1_000_600), # interleaved with claude in the window
|
|
114
|
+
("copilot", 100), # ancient — outside the recent window
|
|
115
|
+
]
|
|
116
|
+
conn.executemany(
|
|
117
|
+
"INSERT INTO dialog_messages (source, created_at) VALUES (?, ?)", rows
|
|
118
|
+
)
|
|
119
|
+
conn.executemany(
|
|
120
|
+
"INSERT INTO events (kind) VALUES (?)",
|
|
121
|
+
[("shadow_review_pass",)] * 3 + [("ingest_pass",)],
|
|
122
|
+
)
|
|
123
|
+
conn.commit()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_collect_live_signals_reads_window_and_passes():
|
|
127
|
+
conn = sqlite3.connect(":memory:")
|
|
128
|
+
conn.row_factory = sqlite3.Row
|
|
129
|
+
_seed_live_db(conn)
|
|
130
|
+
|
|
131
|
+
sig = collect_live_signals(conn, window_hours=24)
|
|
132
|
+
assert sig["source_counts"] == {
|
|
133
|
+
"claude-code": 2, "codex": 1, "copilot": 1,
|
|
134
|
+
}
|
|
135
|
+
# newest is 1_000_600; copilot@100 is far outside a 24h window of it.
|
|
136
|
+
assert set(sig["window_sources"]) == {"claude-code", "codex"}
|
|
137
|
+
assert sig["shadow_passes"] == 3
|
|
138
|
+
assert sig["newest_ts"] == 1_000_600
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_collect_live_signals_tolerates_missing_events_table():
|
|
142
|
+
conn = sqlite3.connect(":memory:")
|
|
143
|
+
conn.row_factory = sqlite3.Row
|
|
144
|
+
conn.execute("CREATE TABLE dialog_messages (source TEXT, created_at INTEGER)")
|
|
145
|
+
conn.execute("INSERT INTO dialog_messages VALUES ('codex', 5)")
|
|
146
|
+
conn.commit()
|
|
147
|
+
sig = collect_live_signals(conn)
|
|
148
|
+
assert sig["shadow_passes"] == 0 # no events table → graceful 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_format_report_renders_verdict_and_slots():
|
|
152
|
+
rep = evaluate_verdict(
|
|
153
|
+
source_counts={"claude-code": 200000, "codex": 11000, "copilot": 10},
|
|
154
|
+
window_sources=["claude-code", "codex"],
|
|
155
|
+
shadow_passes=2567,
|
|
156
|
+
)
|
|
157
|
+
rep["db_path"] = "/tmp/x.sqlite"
|
|
158
|
+
rep["signals"] = {"window_hours": 24}
|
|
159
|
+
text = format_report(rep)
|
|
160
|
+
assert "VERDICT: PARTIAL" in text
|
|
161
|
+
assert "claude-code" in text
|
|
162
|
+
assert "learning_loop_non_claude" in text
|
{threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/README.md
RENAMED
|
@@ -5,7 +5,7 @@ The status-bar item itself is AppKit `NSStatusItem`; the popover content is
|
|
|
5
5
|
SwiftUI. That lets the app update the menu-bar image directly instead of relying
|
|
6
6
|
on SwiftUI `MenuBarExtra` label animation.
|
|
7
7
|
|
|
8
|
-
It polls `tk-agent-status --json` every
|
|
8
|
+
It polls `tk-agent-status --json` every 15 seconds and shows:
|
|
9
9
|
|
|
10
10
|
- an icon-only menu-bar status item, with loop counts in the popover and
|
|
11
11
|
tooltip,
|
|
@@ -20,10 +20,14 @@ It polls `tk-agent-status --json` every 5 seconds and shows:
|
|
|
20
20
|
- active spawned-child RSS when a loop has a worker running,
|
|
21
21
|
- a Clean memory button that runs `tk-agent-status --cleanup-memory`,
|
|
22
22
|
- a Settings gear that opens a separate `~/.threadkeeper/.env` editor with
|
|
23
|
-
guided controls,
|
|
23
|
+
guided controls, exact dropdowns for spawn CLI/model choices, raw text
|
|
24
|
+
editing, three saved presets, and Save & Restart,
|
|
24
25
|
- macOS notifications for newly completed autonomous child tasks that produced
|
|
25
26
|
a useful result.
|
|
26
27
|
|
|
28
|
+
Status polling and cleanup commands run in the background, so opening the
|
|
29
|
+
popover does not wait for `tk-agent-status --json`.
|
|
30
|
+
|
|
27
31
|
The first poll primes the seen-result list, so the app does not notify for old
|
|
28
32
|
completed tasks that existed before it started.
|
|
29
33
|
|
|
@@ -35,9 +39,10 @@ keeps the menu-bar helper from becoming the memory-pressure offender.
|
|
|
35
39
|
|
|
36
40
|
On macOS, `python -m threadkeeper.server` installs and launches this app
|
|
37
41
|
automatically when the MCP server starts. The startup hook is idempotent: it
|
|
38
|
-
rebuilds
|
|
39
|
-
|
|
40
|
-
process means the menu-bar
|
|
42
|
+
rebuilds when the installed app is missing or its recorded source fingerprint no
|
|
43
|
+
longer matches the bundled/source Swift files, registers the LaunchAgent, and
|
|
44
|
+
restarts the app when a rebuild or stale running process means the menu-bar
|
|
45
|
+
process is still using older code.
|
|
41
46
|
|
|
42
47
|
Disable automatic startup with:
|
|
43
48
|
|
|
@@ -55,7 +60,9 @@ The Settings gear edits `~/.threadkeeper/.env` by default, or the path in
|
|
|
55
60
|
`THREADKEEPER_ENV_FILE` when the app was launched with that override. Save &
|
|
56
61
|
Restart writes the file, runs the safe cleanup command, and sends TERM to
|
|
57
62
|
running `threadkeeper.server` processes so MCP hosts reconnect with the new
|
|
58
|
-
environment.
|
|
63
|
+
environment. In the spawn routing controls, `antigravity` is the stored CLI
|
|
64
|
+
value and `agy` is only the executable alias; `gemini` remains available as the
|
|
65
|
+
legacy Gemini CLI adapter.
|
|
59
66
|
|
|
60
67
|
## Build
|
|
61
68
|
|