threadkeeper 0.9.0__tar.gz → 0.9.2__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.9.0 → threadkeeper-0.9.2}/PKG-INFO +19 -14
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/README.md +18 -13
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/pyproject.toml +10 -1
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_candidate_reviewer.py +36 -0
- threadkeeper-0.9.2/tests/test_menubar_app.py +69 -0
- threadkeeper-0.9.2/threadkeeper/assets/macos-agent-status/Info.plist +25 -0
- threadkeeper-0.9.2/threadkeeper/assets/macos-agent-status/README.md +65 -0
- threadkeeper-0.9.2/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +482 -0
- threadkeeper-0.9.2/threadkeeper/assets/macos-agent-status/build.sh +27 -0
- threadkeeper-0.9.2/threadkeeper/assets/macos-agent-status/install.sh +58 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/candidate_reviewer.py +117 -43
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/menubar_app.py +28 -2
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper.egg-info/PKG-INFO +19 -14
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper.egg-info/SOURCES.txt +6 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/LICENSE +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/setup.cfg +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_adapters.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_curator.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_identity.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_lessons.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_nudges.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_panel.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_process_health.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_skills.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_threads.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/gemini.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/config.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/db.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/server.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.9.0 → threadkeeper-0.9.2}/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.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: Multi-agent shared brain across Claude Code/Desktop, Codex, 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
|
|
@@ -211,18 +211,20 @@ tk-agent-status --json
|
|
|
211
211
|
`apps/macos-agent-status/` contains a small macOS menu-bar app that polls this
|
|
212
212
|
command every 5 seconds and shows every autonomous learning loop: enabled/off,
|
|
213
213
|
running/idle/ready, last pass, backlog, and active child RSS when that loop has
|
|
214
|
-
spawned a worker.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
214
|
+
spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
|
|
215
|
+
`threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
|
|
216
|
+
does not need a git checkout for the widget to build. Active loops are sorted
|
|
217
|
+
first (`running`, then `ready`), so background work stays at the top of the
|
|
218
|
+
panel. The app also requests macOS notification permission and sends a
|
|
219
|
+
notification when a newly completed autonomous child task produces a useful
|
|
220
|
+
result in `recent_results`; the first poll only marks existing results as seen,
|
|
221
|
+
so old completions do not spam notifications. Probe backlog is due objective
|
|
222
|
+
probes only, not every registered probe, so a healthy cooldown shows `0 due
|
|
223
|
+
probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
|
|
224
|
+
automatically installs and launches it on MCP startup. Set
|
|
225
|
+
`THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
|
|
226
|
+
|
|
227
|
+
Manual fallback from a source checkout:
|
|
226
228
|
|
|
227
229
|
```sh
|
|
228
230
|
cd apps/macos-agent-status
|
|
@@ -365,7 +367,10 @@ Hard limits: max 2 new skills per pass, `[PROTECTED]` (pinned +
|
|
|
365
367
|
foreground-authored) skills off-limits. Closes the gap between
|
|
366
368
|
heuristic harvest and SKILL.md materialization — previously pending
|
|
367
369
|
candidates accumulated indefinitely waiting for an agent to call
|
|
368
|
-
`accept_candidate()` manually.
|
|
370
|
+
`accept_candidate()` manually. The loop is machine-wide single-flight:
|
|
371
|
+
while one reviewer child is running, other foreground servers/ticks report
|
|
372
|
+
`candidate_review_running` instead of spawning another child for the same
|
|
373
|
+
queue.
|
|
369
374
|
|
|
370
375
|
#### 5. Autonomous Curator
|
|
371
376
|
|
|
@@ -170,18 +170,20 @@ tk-agent-status --json
|
|
|
170
170
|
`apps/macos-agent-status/` contains a small macOS menu-bar app that polls this
|
|
171
171
|
command every 5 seconds and shows every autonomous learning loop: enabled/off,
|
|
172
172
|
running/idle/ready, last pass, backlog, and active child RSS when that loop has
|
|
173
|
-
spawned a worker.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
spawned a worker. PyPI wheels and sdists also bundle the same Swift source under
|
|
174
|
+
`threadkeeper/assets/macos-agent-status/`, so a normal `pipx`/`uv tool` install
|
|
175
|
+
does not need a git checkout for the widget to build. Active loops are sorted
|
|
176
|
+
first (`running`, then `ready`), so background work stays at the top of the
|
|
177
|
+
panel. The app also requests macOS notification permission and sends a
|
|
178
|
+
notification when a newly completed autonomous child task produces a useful
|
|
179
|
+
result in `recent_results`; the first poll only marks existing results as seen,
|
|
180
|
+
so old completions do not spam notifications. Probe backlog is due objective
|
|
181
|
+
probes only, not every registered probe, so a healthy cooldown shows `0 due
|
|
182
|
+
probes` instead of looking stuck. On macOS, `python -m threadkeeper.server`
|
|
183
|
+
automatically installs and launches it on MCP startup. Set
|
|
184
|
+
`THREADKEEPER_MENUBAR_AUTO_LAUNCH=0` to disable that behavior.
|
|
185
|
+
|
|
186
|
+
Manual fallback from a source checkout:
|
|
185
187
|
|
|
186
188
|
```sh
|
|
187
189
|
cd apps/macos-agent-status
|
|
@@ -324,7 +326,10 @@ Hard limits: max 2 new skills per pass, `[PROTECTED]` (pinned +
|
|
|
324
326
|
foreground-authored) skills off-limits. Closes the gap between
|
|
325
327
|
heuristic harvest and SKILL.md materialization — previously pending
|
|
326
328
|
candidates accumulated indefinitely waiting for an agent to call
|
|
327
|
-
`accept_candidate()` manually.
|
|
329
|
+
`accept_candidate()` manually. The loop is machine-wide single-flight:
|
|
330
|
+
while one reviewer child is running, other foreground servers/ticks report
|
|
331
|
+
`candidate_review_running` instead of spawning another child for the same
|
|
332
|
+
queue.
|
|
328
333
|
|
|
329
334
|
#### 5. Autonomous Curator
|
|
330
335
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "threadkeeper"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
8
8
|
description = "Multi-agent shared brain across Claude Code/Desktop, Codex, 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" }]
|
|
@@ -78,6 +78,15 @@ tk-agent-status = "threadkeeper.agent_status:main"
|
|
|
78
78
|
include = ["threadkeeper*"]
|
|
79
79
|
exclude = ["tests*", "scripts*"]
|
|
80
80
|
|
|
81
|
+
[tool.setuptools.package-data]
|
|
82
|
+
threadkeeper = [
|
|
83
|
+
"assets/macos-agent-status/Info.plist",
|
|
84
|
+
"assets/macos-agent-status/README.md",
|
|
85
|
+
"assets/macos-agent-status/ThreadKeeperAgentStatus.swift",
|
|
86
|
+
"assets/macos-agent-status/build.sh",
|
|
87
|
+
"assets/macos-agent-status/install.sh",
|
|
88
|
+
]
|
|
89
|
+
|
|
81
90
|
[tool.pytest.ini_options]
|
|
82
91
|
testpaths = ["tests"]
|
|
83
92
|
addopts = "-q --strict-markers"
|
|
@@ -7,6 +7,7 @@ fork.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import os
|
|
10
11
|
import sys
|
|
11
12
|
import time
|
|
12
13
|
from pathlib import Path
|
|
@@ -205,6 +206,41 @@ def test_run_spawns_when_threshold_met(tmp_path, monkeypatch):
|
|
|
205
206
|
assert "Bash" not in allowed
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
def test_single_flight_when_reviewer_child_running(tmp_path, monkeypatch):
|
|
210
|
+
"""Candidate review consumes one global queue; don't spawn duplicates."""
|
|
211
|
+
pkg = _bootstrap(tmp_path, monkeypatch, min_n="3")
|
|
212
|
+
conn = pkg["db"].get_db()
|
|
213
|
+
for i in range(4):
|
|
214
|
+
_seed_pending(conn, "verbatim", f"candidate {i}", age_s=60 + i)
|
|
215
|
+
conn.execute(
|
|
216
|
+
"INSERT INTO tasks "
|
|
217
|
+
"(id, pid, parent_cid, spawned_cid, cwd, prompt, started_at) "
|
|
218
|
+
"VALUES ('tk_running_review', ?, 'p', 'c', '/x', ?, ?)",
|
|
219
|
+
(
|
|
220
|
+
os.getpid(),
|
|
221
|
+
"You are a CANDIDATE REVIEWER for thread-keeper's extract queue.",
|
|
222
|
+
int(time.time()) - 30,
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
conn.commit()
|
|
226
|
+
|
|
227
|
+
import threadkeeper.tools.spawn as spawn_mod
|
|
228
|
+
|
|
229
|
+
def fail_spawn(**kwargs): # pragma: no cover - should not be called
|
|
230
|
+
raise AssertionError("spawn should not run while reviewer is active")
|
|
231
|
+
|
|
232
|
+
monkeypatch.setattr(spawn_mod, "spawn", fail_spawn)
|
|
233
|
+
|
|
234
|
+
out = pkg["candidate_reviewer"].run_review_pass(force=True)
|
|
235
|
+
|
|
236
|
+
assert out == "candidate_review_running n=1 (single-flight)"
|
|
237
|
+
row = conn.execute(
|
|
238
|
+
"SELECT summary FROM events WHERE kind='candidate_review_pass' "
|
|
239
|
+
"ORDER BY id DESC LIMIT 1"
|
|
240
|
+
).fetchone()
|
|
241
|
+
assert "candidate_review_running n=1" in row["summary"]
|
|
242
|
+
|
|
243
|
+
|
|
208
244
|
# ──────────────────────────────────────────────────────────────────────
|
|
209
245
|
# Daemon lifecycle
|
|
210
246
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_menubar_packaged_assets_are_available(fresh_mp):
|
|
8
|
+
import threadkeeper.menubar_app as menubar_app
|
|
9
|
+
|
|
10
|
+
src = menubar_app._package_source_dir()
|
|
11
|
+
|
|
12
|
+
assert src.name == "macos-agent-status"
|
|
13
|
+
assert (src / "ThreadKeeperAgentStatus.swift").exists()
|
|
14
|
+
assert (src / "Info.plist").exists()
|
|
15
|
+
assert (src / "build.sh").exists()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_menubar_source_falls_back_to_packaged_assets(fresh_mp, tmp_path, monkeypatch):
|
|
19
|
+
import threadkeeper.menubar_app as menubar_app
|
|
20
|
+
|
|
21
|
+
monkeypatch.setattr(menubar_app, "_dev_source_dir", lambda: tmp_path / "missing")
|
|
22
|
+
|
|
23
|
+
assert menubar_app._source_dir() == menubar_app._package_source_dir()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_install_app_builds_from_task_log_scratch_without_executable_bit(
|
|
27
|
+
fresh_mp,
|
|
28
|
+
tmp_path,
|
|
29
|
+
monkeypatch,
|
|
30
|
+
):
|
|
31
|
+
import threadkeeper.menubar_app as menubar_app
|
|
32
|
+
|
|
33
|
+
src = tmp_path / "source"
|
|
34
|
+
src.mkdir()
|
|
35
|
+
(src / "ThreadKeeperAgentStatus.swift").write_text("// swift\n", encoding="utf-8")
|
|
36
|
+
(src / "Info.plist").write_text("<plist></plist>\n", encoding="utf-8")
|
|
37
|
+
build = src / "build.sh"
|
|
38
|
+
build.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
|
39
|
+
build.chmod(0o644)
|
|
40
|
+
|
|
41
|
+
task_logs = tmp_path / "tasks"
|
|
42
|
+
monkeypatch.setattr(menubar_app, "TASK_LOG_DIR", task_logs)
|
|
43
|
+
monkeypatch.setattr(menubar_app, "_app_is_current", lambda src, app: False)
|
|
44
|
+
calls = []
|
|
45
|
+
|
|
46
|
+
def fake_run(args, timeout=60, cwd=None):
|
|
47
|
+
calls.append((args, timeout, Path(cwd)))
|
|
48
|
+
app_dir = Path(cwd) / "build" / menubar_app.APP_BUNDLE
|
|
49
|
+
bin_dir = app_dir / "Contents" / "MacOS"
|
|
50
|
+
bin_dir.mkdir(parents=True)
|
|
51
|
+
(app_dir / "Contents" / "Info.plist").write_text(
|
|
52
|
+
"<plist></plist>\n",
|
|
53
|
+
encoding="utf-8",
|
|
54
|
+
)
|
|
55
|
+
(bin_dir / menubar_app.APP_NAME).write_text("binary\n", encoding="utf-8")
|
|
56
|
+
return SimpleNamespace(returncode=0, stdout=f"{app_dir}\n")
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(menubar_app, "_run", fake_run)
|
|
59
|
+
|
|
60
|
+
installed = tmp_path / "Applications" / menubar_app.APP_BUNDLE
|
|
61
|
+
assert menubar_app._install_app(src, installed) is True
|
|
62
|
+
|
|
63
|
+
assert calls
|
|
64
|
+
assert calls[0][0][0] == "/bin/bash"
|
|
65
|
+
assert calls[0][0][1] == str(task_logs / "menubar-build" / "source" / "build.sh")
|
|
66
|
+
assert calls[0][2] == task_logs / "menubar-build" / "source"
|
|
67
|
+
assert (installed / "Contents" / "Info.plist").exists()
|
|
68
|
+
assert (installed / "Contents" / "MacOS" / menubar_app.APP_NAME).exists()
|
|
69
|
+
assert not (src / "build").exists()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
3
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4
|
+
<plist version="1.0">
|
|
5
|
+
<dict>
|
|
6
|
+
<key>CFBundleExecutable</key>
|
|
7
|
+
<string>ThreadKeeperAgentStatus</string>
|
|
8
|
+
<key>CFBundleIdentifier</key>
|
|
9
|
+
<string>local.threadkeeper.agent-status</string>
|
|
10
|
+
<key>CFBundleName</key>
|
|
11
|
+
<string>ThreadKeeperAgentStatus</string>
|
|
12
|
+
<key>CFBundlePackageType</key>
|
|
13
|
+
<string>APPL</string>
|
|
14
|
+
<key>CFBundleShortVersionString</key>
|
|
15
|
+
<string>0.1.0</string>
|
|
16
|
+
<key>CFBundleVersion</key>
|
|
17
|
+
<string>1</string>
|
|
18
|
+
<key>LSMinimumSystemVersion</key>
|
|
19
|
+
<string>13.0</string>
|
|
20
|
+
<key>LSUIElement</key>
|
|
21
|
+
<true/>
|
|
22
|
+
<key>NSHighResolutionCapable</key>
|
|
23
|
+
<true/>
|
|
24
|
+
</dict>
|
|
25
|
+
</plist>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ThreadKeeper Agent Status
|
|
2
|
+
|
|
3
|
+
Small macOS menu-bar app for live thread-keeper autonomous learning loop status.
|
|
4
|
+
|
|
5
|
+
It polls `tk-agent-status --json` every 5 seconds and shows:
|
|
6
|
+
|
|
7
|
+
- running/enabled loop count in the menu bar,
|
|
8
|
+
- every autonomous learning loop,
|
|
9
|
+
- a stable role description for what each loop/agent is responsible for,
|
|
10
|
+
- running / idle / ready / off state,
|
|
11
|
+
- active loops first (`running`, then `ready`),
|
|
12
|
+
- last pass summary,
|
|
13
|
+
- backlog count,
|
|
14
|
+
- active spawned-child RSS when a loop has a worker running,
|
|
15
|
+
- macOS notifications for newly completed autonomous child tasks that produced
|
|
16
|
+
a useful result.
|
|
17
|
+
|
|
18
|
+
The first poll primes the seen-result list, so the app does not notify for old
|
|
19
|
+
completed tasks that existed before it started.
|
|
20
|
+
|
|
21
|
+
## Automatic startup
|
|
22
|
+
|
|
23
|
+
On macOS, `python -m threadkeeper.server` installs and launches this app
|
|
24
|
+
automatically when the MCP server starts. The startup hook is idempotent: it
|
|
25
|
+
rebuilds only when the installed app is missing or older than the source,
|
|
26
|
+
registers the LaunchAgent, and opens the app if it is not already running.
|
|
27
|
+
|
|
28
|
+
Disable automatic startup with:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
THREADKEEPER_MENUBAR_AUTO_LAUNCH=0
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Build
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
./build.sh
|
|
38
|
+
open build/ThreadKeeperAgentStatus.app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install at login
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
./install.sh
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The app is installed to `~/Applications/ThreadKeeperAgentStatus.app` and a
|
|
48
|
+
LaunchAgent is registered at
|
|
49
|
+
`~/Library/LaunchAgents/local.threadkeeper.agent-status.plist`.
|
|
50
|
+
|
|
51
|
+
If `tk-agent-status` is not already installed on PATH, `install.sh` creates a
|
|
52
|
+
small fallback wrapper at `~/.local/bin/tk-agent-status` that runs the local
|
|
53
|
+
repo module through `.venv/bin/python`.
|
|
54
|
+
|
|
55
|
+
## Command lookup
|
|
56
|
+
|
|
57
|
+
The app looks for `tk-agent-status` in:
|
|
58
|
+
|
|
59
|
+
- `/opt/homebrew/bin/tk-agent-status`
|
|
60
|
+
- `/usr/local/bin/tk-agent-status`
|
|
61
|
+
- `~/.local/bin/tk-agent-status`
|
|
62
|
+
- `PATH` via `/usr/bin/env`
|
|
63
|
+
|
|
64
|
+
Set `THREADKEEPER_AGENT_STATUS_COMMAND` when launching the app if the command
|
|
65
|
+
lives somewhere else.
|