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.
Files changed (153) hide show
  1. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/PKG-INFO +49 -12
  2. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/README.md +47 -11
  3. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/pyproject.toml +2 -1
  4. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_menubar_app.py +67 -0
  5. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_config.py +4 -4
  6. threadkeeper-0.13.1/tests/test_verify_ingest.py +162 -0
  7. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/README.md +13 -6
  8. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +241 -57
  9. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/menubar_app.py +32 -9
  10. threadkeeper-0.13.1/threadkeeper/verify_ingest.py +313 -0
  11. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/PKG-INFO +49 -12
  12. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/SOURCES.txt +2 -0
  13. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/requires.txt +1 -0
  14. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/LICENSE +0 -0
  15. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/setup.cfg +0 -0
  16. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_adapters.py +0 -0
  17. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_agent_status.py +0 -0
  18. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_auto_update.py +0 -0
  19. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_brief_footprint.py +0 -0
  20. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_brief_sections.py +0 -0
  21. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_candidate_reviewer.py +0 -0
  22. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_config_settings.py +0 -0
  23. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_core_memory.py +0 -0
  24. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_curator.py +0 -0
  25. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dashboard.py +0 -0
  26. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_delegated_search.py +0 -0
  27. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic.py +0 -0
  28. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_feed_tools.py +0 -0
  29. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_miner.py +0 -0
  30. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_observation_resolve.py +0 -0
  31. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_recompute.py +0 -0
  32. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_tier.py +0 -0
  33. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_dialectic_validator.py +0 -0
  34. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_error_paths.py +0 -0
  35. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_applier.py +0 -0
  36. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_apply_2.py +0 -0
  37. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_apply_3.py +0 -0
  38. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_evolve_daemon.py +0 -0
  39. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_extract_daemon.py +0 -0
  40. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_extract_dedup.py +0 -0
  41. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_i18n_multilang.py +0 -0
  42. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_identity.py +0 -0
  43. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_ingest_status.py +0 -0
  44. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_lessons.py +0 -0
  45. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_memory_guard.py +0 -0
  46. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_missed_spawns.py +0 -0
  47. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_nudges.py +0 -0
  48. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_onnx_embeddings.py +0 -0
  49. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_panel.py +0 -0
  50. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_probe_daemon.py +0 -0
  51. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_process_health.py +0 -0
  52. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_search_fts_punctuation.py +0 -0
  53. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_shadow_review.py +0 -0
  54. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_hint.py +0 -0
  55. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_passive_tier.py +0 -0
  56. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_tier.py +0 -0
  57. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_use_parser.py +0 -0
  58. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skill_watcher.py +0 -0
  59. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_skills.py +0 -0
  60. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_budget.py +0 -0
  61. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_codex_stdin.py +0 -0
  62. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_hint.py +0 -0
  63. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_reap.py +0 -0
  64. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_slim.py +0 -0
  65. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_spawn_wrap.py +0 -0
  66. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_thread_janitor.py +0 -0
  67. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_threads.py +0 -0
  68. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_tools_smoke.py +0 -0
  69. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_validate_threads.py +0 -0
  70. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/tests/test_vec_search.py +0 -0
  71. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/__init__.py +0 -0
  72. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/_mcp.py +0 -0
  73. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/_setup.py +0 -0
  74. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/_spawn_wrap.py +0 -0
  75. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/__init__.py +0 -0
  76. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/_hook_helpers.py +0 -0
  77. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/antigravity.py +0 -0
  78. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/base.py +0 -0
  79. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/claude_code.py +0 -0
  80. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/claude_desktop.py +0 -0
  81. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/codex.py +0 -0
  82. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/copilot.py +0 -0
  83. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/gemini.py +0 -0
  84. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/adapters/vscode.py +0 -0
  85. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/agent_status.py +0 -0
  86. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  87. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  88. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  89. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/auto_update.py +0 -0
  90. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/brief.py +0 -0
  91. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/candidate_reviewer.py +0 -0
  92. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/config.py +0 -0
  93. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/curator.py +0 -0
  94. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/db.py +0 -0
  95. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/dialectic_miner.py +0 -0
  96. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/dialectic_validator.py +0 -0
  97. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/embeddings.py +0 -0
  98. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/evolve_applier.py +0 -0
  99. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/evolve_daemon.py +0 -0
  100. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/extract_daemon.py +0 -0
  101. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/helpers.py +0 -0
  102. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/i18n.py +0 -0
  103. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/identity.py +0 -0
  104. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/ingest.py +0 -0
  105. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/lessons.py +0 -0
  106. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/memory_guard.py +0 -0
  107. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/migrate_embeddings.py +0 -0
  108. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/nudges.py +0 -0
  109. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/probe_daemon.py +0 -0
  110. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/process_health.py +0 -0
  111. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/review_prompts.py +0 -0
  112. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/search_proxy.py +0 -0
  113. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/server.py +0 -0
  114. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/shadow_review.py +0 -0
  115. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/skill_watcher.py +0 -0
  116. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/spawn_budget.py +0 -0
  117. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/spawn_config.py +0 -0
  118. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/thread_janitor.py +0 -0
  119. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/__init__.py +0 -0
  120. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/agent_status.py +0 -0
  121. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/candidate_reviewer.py +0 -0
  122. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/concepts.py +0 -0
  123. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/consolidate.py +0 -0
  124. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/core_memory.py +0 -0
  125. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/correlation.py +0 -0
  126. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/curator.py +0 -0
  127. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dashboard.py +0 -0
  128. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialectic.py +0 -0
  129. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialectic_feed.py +0 -0
  130. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialog.py +0 -0
  131. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/distill.py +0 -0
  132. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/evolve_applier.py +0 -0
  133. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/extract.py +0 -0
  134. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/graph.py +0 -0
  135. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/invariants.py +0 -0
  136. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/lessons.py +0 -0
  137. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/memory_guard.py +0 -0
  138. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/missed_spawns.py +0 -0
  139. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/panel.py +0 -0
  140. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/peers.py +0 -0
  141. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/pickup.py +0 -0
  142. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/probes.py +0 -0
  143. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/process_health.py +0 -0
  144. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/session.py +0 -0
  145. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/shadow_review.py +0 -0
  146. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/skills.py +0 -0
  147. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/spawn.py +0 -0
  148. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/style.py +0 -0
  149. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/threads.py +0 -0
  150. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper/tools/validate.py +0 -0
  151. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/dependency_links.txt +0 -0
  152. {threadkeeper-0.12.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/entry_points.txt +0 -0
  153. {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.12.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 5 seconds and shows every autonomous learning loop: enabled/off,
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. The header gear opens a separate Settings window for
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. Probe backlog is due objective
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, and restarts the app when
251
- the installed bundle has changed while an older menu-bar process is still
252
- running. Set
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`). See `.env.example` for the full knob list.
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
- Walks every installed CLI adapter, parses recent transcripts in an
752
- isolated tempdir DB, reports per-source message counts and any silent
753
- parse failures. Read-only with respect to live state.
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 5 seconds and shows every autonomous learning loop: enabled/off,
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. The header gear opens a separate Settings window for
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. Probe backlog is due objective
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, and restarts the app when
210
- the installed bundle has changed while an older menu-bar process is still
211
- running. Set
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`). See `.env.example` for the full knob list.
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
- Walks every installed CLI adapter, parses recent transcripts in an
711
- isolated tempdir DB, reports per-source message counts and any silent
712
- parse failures. Read-only with respect to live state.
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.12.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
- "THREADKEEPER_SPAWN__MODEL__AGY=gemini-3.1-pro\n"
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") == "gemini-3.1-pro"
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="gemini-3.1-pro")
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 "gemini-3.1-pro" in argv
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
@@ -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 5 seconds and shows:
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, raw text editing, three saved presets, and Save & Restart,
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 only when the installed app is missing or older than the source,
39
- registers the LaunchAgent, and restarts the app when a rebuild or stale running
40
- process means the menu-bar process is still using older code.
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