threadkeeper 0.13.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.13.0 → threadkeeper-0.13.1}/PKG-INFO +6 -4
  2. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/README.md +4 -3
  3. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/pyproject.toml +2 -1
  4. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_menubar_app.py +33 -0
  5. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/README.md +4 -3
  6. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/menubar_app.py +32 -9
  7. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/PKG-INFO +6 -4
  8. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/requires.txt +1 -0
  9. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/LICENSE +0 -0
  10. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/setup.cfg +0 -0
  11. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_adapters.py +0 -0
  12. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_agent_status.py +0 -0
  13. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_auto_update.py +0 -0
  14. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_brief_footprint.py +0 -0
  15. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_brief_sections.py +0 -0
  16. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_candidate_reviewer.py +0 -0
  17. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_config_settings.py +0 -0
  18. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_core_memory.py +0 -0
  19. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_curator.py +0 -0
  20. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dashboard.py +0 -0
  21. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_delegated_search.py +0 -0
  22. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic.py +0 -0
  23. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic_feed_tools.py +0 -0
  24. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic_miner.py +0 -0
  25. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic_observation_resolve.py +0 -0
  26. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic_recompute.py +0 -0
  27. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic_tier.py +0 -0
  28. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_dialectic_validator.py +0 -0
  29. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_error_paths.py +0 -0
  30. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_evolve_applier.py +0 -0
  31. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_evolve_apply_2.py +0 -0
  32. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_evolve_apply_3.py +0 -0
  33. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_evolve_daemon.py +0 -0
  34. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_extract_daemon.py +0 -0
  35. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_extract_dedup.py +0 -0
  36. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_i18n_multilang.py +0 -0
  37. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_identity.py +0 -0
  38. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_ingest_status.py +0 -0
  39. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_lessons.py +0 -0
  40. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_memory_guard.py +0 -0
  41. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_missed_spawns.py +0 -0
  42. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_nudges.py +0 -0
  43. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_onnx_embeddings.py +0 -0
  44. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_panel.py +0 -0
  45. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_probe_daemon.py +0 -0
  46. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_process_health.py +0 -0
  47. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_search_fts_punctuation.py +0 -0
  48. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_shadow_review.py +0 -0
  49. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_skill_hint.py +0 -0
  50. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_skill_passive_tier.py +0 -0
  51. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_skill_tier.py +0 -0
  52. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_skill_use_parser.py +0 -0
  53. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_skill_watcher.py +0 -0
  54. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_skills.py +0 -0
  55. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_budget.py +0 -0
  56. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_codex_stdin.py +0 -0
  57. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_config.py +0 -0
  58. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_hint.py +0 -0
  59. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_reap.py +0 -0
  60. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_slim.py +0 -0
  61. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_spawn_wrap.py +0 -0
  62. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_thread_janitor.py +0 -0
  63. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_threads.py +0 -0
  64. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_tools_smoke.py +0 -0
  65. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_validate_threads.py +0 -0
  66. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_vec_search.py +0 -0
  67. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/tests/test_verify_ingest.py +0 -0
  68. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/__init__.py +0 -0
  69. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/_mcp.py +0 -0
  70. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/_setup.py +0 -0
  71. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/_spawn_wrap.py +0 -0
  72. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/__init__.py +0 -0
  73. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/_hook_helpers.py +0 -0
  74. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/antigravity.py +0 -0
  75. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/base.py +0 -0
  76. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/claude_code.py +0 -0
  77. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/claude_desktop.py +0 -0
  78. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/codex.py +0 -0
  79. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/copilot.py +0 -0
  80. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/gemini.py +0 -0
  81. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/adapters/vscode.py +0 -0
  82. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/agent_status.py +0 -0
  83. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  84. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  85. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  86. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  87. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/auto_update.py +0 -0
  88. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/brief.py +0 -0
  89. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/candidate_reviewer.py +0 -0
  90. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/config.py +0 -0
  91. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/curator.py +0 -0
  92. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/db.py +0 -0
  93. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/dialectic_miner.py +0 -0
  94. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/dialectic_validator.py +0 -0
  95. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/embeddings.py +0 -0
  96. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/evolve_applier.py +0 -0
  97. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/evolve_daemon.py +0 -0
  98. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/extract_daemon.py +0 -0
  99. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/helpers.py +0 -0
  100. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/i18n.py +0 -0
  101. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/identity.py +0 -0
  102. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/ingest.py +0 -0
  103. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/lessons.py +0 -0
  104. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/memory_guard.py +0 -0
  105. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/migrate_embeddings.py +0 -0
  106. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/nudges.py +0 -0
  107. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/probe_daemon.py +0 -0
  108. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/process_health.py +0 -0
  109. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/review_prompts.py +0 -0
  110. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/search_proxy.py +0 -0
  111. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/server.py +0 -0
  112. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/shadow_review.py +0 -0
  113. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/skill_watcher.py +0 -0
  114. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/spawn_budget.py +0 -0
  115. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/spawn_config.py +0 -0
  116. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/thread_janitor.py +0 -0
  117. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/__init__.py +0 -0
  118. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/agent_status.py +0 -0
  119. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/candidate_reviewer.py +0 -0
  120. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/concepts.py +0 -0
  121. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/consolidate.py +0 -0
  122. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/core_memory.py +0 -0
  123. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/correlation.py +0 -0
  124. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/curator.py +0 -0
  125. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/dashboard.py +0 -0
  126. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialectic.py +0 -0
  127. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialectic_feed.py +0 -0
  128. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/dialog.py +0 -0
  129. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/distill.py +0 -0
  130. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/evolve_applier.py +0 -0
  131. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/extract.py +0 -0
  132. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/graph.py +0 -0
  133. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/invariants.py +0 -0
  134. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/lessons.py +0 -0
  135. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/memory_guard.py +0 -0
  136. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/missed_spawns.py +0 -0
  137. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/panel.py +0 -0
  138. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/peers.py +0 -0
  139. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/pickup.py +0 -0
  140. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/probes.py +0 -0
  141. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/process_health.py +0 -0
  142. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/session.py +0 -0
  143. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/shadow_review.py +0 -0
  144. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/skills.py +0 -0
  145. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/spawn.py +0 -0
  146. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/style.py +0 -0
  147. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/threads.py +0 -0
  148. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/tools/validate.py +0 -0
  149. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper/verify_ingest.py +0 -0
  150. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/SOURCES.txt +0 -0
  151. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/dependency_links.txt +0 -0
  152. {threadkeeper-0.13.0 → threadkeeper-0.13.1}/threadkeeper.egg-info/entry_points.txt +0 -0
  153. {threadkeeper-0.13.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.13.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"
@@ -251,9 +252,10 @@ while keeping `gemini` as legacy, and model selectors use dropdowns with exact
251
252
  CLI model ids/labels instead of free-text fields. Probe backlog is due objective
252
253
  probes only, not every registered probe, so a healthy cooldown shows `0 due
253
254
  probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
254
- automatically installs and launches it on MCP startup, and restarts the app when
255
- the installed bundle has changed while an older menu-bar process is still
256
- 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
257
259
  `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
258
260
 
259
261
  ### Auto Update
@@ -210,9 +210,10 @@ while keeping `gemini` as legacy, and model selectors use dropdowns with exact
210
210
  CLI model ids/labels instead of free-text fields. Probe backlog is due objective
211
211
  probes only, not every registered probe, so a healthy cooldown shows `0 due
212
212
  probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
213
- automatically installs and launches it on MCP startup, and restarts the app when
214
- the installed bundle has changed while an older menu-bar process is still
215
- 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
216
217
  `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
217
218
 
218
219
  ### Auto Update
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.13.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]
@@ -130,6 +130,35 @@ def test_menubar_source_falls_back_to_packaged_assets(fresh_mp, tmp_path, monkey
130
130
  assert menubar_app._source_dir() == menubar_app._package_source_dir()
131
131
 
132
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
+
133
162
  def test_install_app_builds_from_task_log_scratch_without_executable_bit(
134
163
  fresh_mp,
135
164
  tmp_path,
@@ -173,6 +202,10 @@ def test_install_app_builds_from_task_log_scratch_without_executable_bit(
173
202
  assert calls[0][2] == task_logs / "menubar-build" / "source"
174
203
  assert (installed / "Contents" / "Info.plist").exists()
175
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
+ )
176
209
  assert not (src / "build").exists()
177
210
 
178
211
 
@@ -39,9 +39,10 @@ keeps the menu-bar helper from becoming the memory-pressure offender.
39
39
 
40
40
  On macOS, `python -m threadkeeper.server` installs and launches this app
41
41
  automatically when the MCP server starts. The startup hook is idempotent: it
42
- rebuilds only when the installed app is missing or older than the source,
43
- registers the LaunchAgent, and restarts the app when a rebuild or stale running
44
- 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.
45
46
 
46
47
  Disable automatic startup with:
47
48
 
@@ -6,6 +6,7 @@ failures are non-fatal.
6
6
  """
7
7
  from __future__ import annotations
8
8
 
9
+ import hashlib
9
10
  import os
10
11
  import platform
11
12
  import shutil
@@ -25,6 +26,8 @@ from .config import (
25
26
  APP_NAME = "ThreadKeeperAgentStatus"
26
27
  APP_BUNDLE = f"{APP_NAME}.app"
27
28
  LAUNCH_LABEL = "local.threadkeeper.agent-status"
29
+ SOURCE_FILES = ("ThreadKeeperAgentStatus.swift", "Info.plist", "build.sh")
30
+ SOURCE_FINGERPRINT_FILE = "threadkeeper-source.sha256"
28
31
 
29
32
  _attempted = False
30
33
 
@@ -53,7 +56,7 @@ def _prepare_build_source(src: Path) -> Path:
53
56
  if build_src.exists():
54
57
  shutil.rmtree(build_src)
55
58
  build_src.mkdir(parents=True, exist_ok=True)
56
- for name in ("ThreadKeeperAgentStatus.swift", "Info.plist", "build.sh"):
59
+ for name in SOURCE_FILES:
57
60
  shutil.copy2(src / name, build_src / name)
58
61
  return build_src
59
62
 
@@ -164,13 +167,25 @@ def _terminate_running_app() -> None:
164
167
  _log("terminate_timeout")
165
168
 
166
169
 
167
- def _source_mtime(src: Path) -> float:
168
- newest = 0.0
169
- for name in ("ThreadKeeperAgentStatus.swift", "Info.plist", "build.sh"):
170
- p = src / name
171
- if p.exists():
172
- newest = max(newest, p.stat().st_mtime)
173
- return newest
170
+ def _source_fingerprint(src: Path) -> str:
171
+ digest = hashlib.sha256()
172
+ for name in SOURCE_FILES:
173
+ path = src / name
174
+ digest.update(name.encode("utf-8"))
175
+ digest.update(b"\0")
176
+ digest.update(path.read_bytes())
177
+ digest.update(b"\0")
178
+ return digest.hexdigest()
179
+
180
+
181
+ def _source_fingerprint_path(app: Path) -> Path:
182
+ return app / "Contents" / "Resources" / SOURCE_FINGERPRINT_FILE
183
+
184
+
185
+ def _write_source_fingerprint(src: Path, app: Path) -> None:
186
+ marker = _source_fingerprint_path(app)
187
+ marker.parent.mkdir(parents=True, exist_ok=True)
188
+ marker.write_text(f"{_source_fingerprint(src)}\n", encoding="utf-8")
174
189
 
175
190
 
176
191
  def _app_is_current(src: Path, app: Path) -> bool:
@@ -178,7 +193,14 @@ def _app_is_current(src: Path, app: Path) -> bool:
178
193
  plist = app / "Contents" / "Info.plist"
179
194
  if not binary.exists() or not plist.exists():
180
195
  return False
181
- return binary.stat().st_mtime >= _source_mtime(src)
196
+ marker = _source_fingerprint_path(app)
197
+ if not marker.exists():
198
+ return False
199
+ try:
200
+ return marker.read_text(encoding="utf-8").strip() == _source_fingerprint(src)
201
+ except OSError as e:
202
+ _log(f"source_fingerprint_check_failed err={e}")
203
+ return False
182
204
 
183
205
 
184
206
  def _ensure_status_command() -> None:
@@ -236,6 +258,7 @@ def _install_app(src: Path, app: Path) -> bool:
236
258
  if app.exists():
237
259
  shutil.rmtree(app)
238
260
  shutil.copytree(built, app)
261
+ _write_source_fingerprint(src, app)
239
262
  except OSError as e:
240
263
  _log(f"copy_app_failed err={e}")
241
264
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.13.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"
@@ -251,9 +252,10 @@ while keeping `gemini` as legacy, and model selectors use dropdowns with exact
251
252
  CLI model ids/labels instead of free-text fields. Probe backlog is due objective
252
253
  probes only, not every registered probe, so a healthy cooldown shows `0 due
253
254
  probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
254
- automatically installs and launches it on MCP startup, and restarts the app when
255
- the installed bundle has changed while an older menu-bar process is still
256
- 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
257
259
  `THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
258
260
 
259
261
  ### Auto Update
@@ -1,6 +1,7 @@
1
1
  mcp>=1.0.0
2
2
  pydantic>=2
3
3
  pydantic-settings>=2
4
+ pyyaml>=6.0
4
5
 
5
6
  [dev]
6
7
  pytest>=8.0
File without changes
File without changes