threadkeeper 0.5.0__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/PKG-INFO +18 -15
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/README.md +17 -14
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/pyproject.toml +1 -1
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_skill_hint.py +1 -1
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_skills.py +66 -1
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/brief.py +2 -2
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/review_prompts.py +4 -4
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/shadow_review.py +3 -3
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/skills.py +116 -41
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/threads.py +12 -3
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper.egg-info/PKG-INFO +18 -15
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/LICENSE +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/setup.cfg +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_adapters.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_curator.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_identity.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_lessons.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_nudges.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_process_health.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_threads.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/gemini.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/candidate_reviewer.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/config.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/db.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/server.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper.egg-info/SOURCES.txt +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper.egg-info/requires.txt +0 -0
- {threadkeeper-0.5.0 → threadkeeper-0.5.3}/threadkeeper.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threadkeeper
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|
|
@@ -82,10 +82,10 @@ make it more than a memory store:
|
|
|
82
82
|
harvester, candidate-reviewer, weekly Curator) materialize
|
|
83
83
|
class-level skills as the agents work. Adapted to multi-CLI:
|
|
84
84
|
SKILL.md is the primary write target and gets mirrored to every
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
loader.
|
|
85
|
+
known/configured skills root simultaneously (`~/.claude/skills/`,
|
|
86
|
+
`~/.codex/skills/`, existing `~/.agents/skills/`, extra roots from
|
|
87
|
+
`THREADKEEPER_EXTRA_SKILLS_DIRS`, and `~/.threadkeeper/skills/`),
|
|
88
|
+
with lessons.md as a fallback for CLIs without a native skills loader.
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
@@ -223,8 +223,8 @@ shows agents focused on their primary task rarely do).
|
|
|
223
223
|
brief() SKILL.md + lessons.md ─► skill_usage │
|
|
224
224
|
│ │ │ │
|
|
225
225
|
│ ▼ ▼ │
|
|
226
|
-
│ (every
|
|
227
|
-
│ skills/
|
|
226
|
+
│ (every configured │ │
|
|
227
|
+
│ skills/ root) │ │
|
|
228
228
|
│ │ │ │
|
|
229
229
|
│ └──────► [5] Curator daemon ───┘
|
|
230
230
|
│ (cron, every 7d)
|
|
@@ -246,11 +246,11 @@ shows agents focused on their primary task rarely do).
|
|
|
246
246
|
| 5 | Curator daemon | every 7 days (env knob) | every existing lesson + recently-touched skill | REPORT-`<date>`.md (advisory) or direct PATCH/PRUNE/CONSOLIDATE |
|
|
247
247
|
|
|
248
248
|
All five write into the universal Skill format (`SKILL.md` under each
|
|
249
|
-
|
|
250
|
-
`~/.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
MCP).
|
|
249
|
+
known/configured skills root — `~/.claude/skills/`, `~/.codex/skills/`,
|
|
250
|
+
existing `~/.agents/skills/`, optional `THREADKEEPER_EXTRA_SKILLS_DIRS`,
|
|
251
|
+
plus the canonical `~/.threadkeeper/skills/` mirror), with
|
|
252
|
+
`~/.threadkeeper/lessons.md` as a CLI-agnostic fallback for clients
|
|
253
|
+
without a native skills loader (Gemini, Copilot, bare MCP).
|
|
254
254
|
|
|
255
255
|
#### 1. Auto-review on close_thread
|
|
256
256
|
|
|
@@ -260,9 +260,12 @@ thread's notes. The prompt is rubric-form (Q1–Q5 yes/no) with explicit
|
|
|
260
260
|
positive examples for incident-vs-rule classification. The fork also
|
|
261
261
|
receives a "recently active skills" block so it prefers PATCHing
|
|
262
262
|
existing umbrellas over creating new ones (*active-update bias*).
|
|
263
|
-
Child appends a lesson via `lesson_append`,
|
|
264
|
-
|
|
265
|
-
`mark_skill_materialized`.
|
|
263
|
+
Child appends a lesson via `lesson_append`, writes/patches a skill via
|
|
264
|
+
`skill_manage` or writes a skill file directly, then closes with
|
|
265
|
+
`mark_skill_materialized`. If `skill_path` points at a `SKILL.md` (or a
|
|
266
|
+
skill directory), thread-keeper immediately mirrors that whole skill
|
|
267
|
+
into every configured skills root. Opt in with
|
|
268
|
+
`THREADKEEPER_AUTO_REVIEW=1`.
|
|
266
269
|
|
|
267
270
|
#### 2. Shadow-review daemon
|
|
268
271
|
|
|
@@ -48,10 +48,10 @@ make it more than a memory store:
|
|
|
48
48
|
harvester, candidate-reviewer, weekly Curator) materialize
|
|
49
49
|
class-level skills as the agents work. Adapted to multi-CLI:
|
|
50
50
|
SKILL.md is the primary write target and gets mirrored to every
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
loader.
|
|
51
|
+
known/configured skills root simultaneously (`~/.claude/skills/`,
|
|
52
|
+
`~/.codex/skills/`, existing `~/.agents/skills/`, extra roots from
|
|
53
|
+
`THREADKEEPER_EXTRA_SKILLS_DIRS`, and `~/.threadkeeper/skills/`),
|
|
54
|
+
with lessons.md as a fallback for CLIs without a native skills loader.
|
|
55
55
|
|
|
56
56
|
---
|
|
57
57
|
|
|
@@ -189,8 +189,8 @@ shows agents focused on their primary task rarely do).
|
|
|
189
189
|
brief() SKILL.md + lessons.md ─► skill_usage │
|
|
190
190
|
│ │ │ │
|
|
191
191
|
│ ▼ ▼ │
|
|
192
|
-
│ (every
|
|
193
|
-
│ skills/
|
|
192
|
+
│ (every configured │ │
|
|
193
|
+
│ skills/ root) │ │
|
|
194
194
|
│ │ │ │
|
|
195
195
|
│ └──────► [5] Curator daemon ───┘
|
|
196
196
|
│ (cron, every 7d)
|
|
@@ -212,11 +212,11 @@ shows agents focused on their primary task rarely do).
|
|
|
212
212
|
| 5 | Curator daemon | every 7 days (env knob) | every existing lesson + recently-touched skill | REPORT-`<date>`.md (advisory) or direct PATCH/PRUNE/CONSOLIDATE |
|
|
213
213
|
|
|
214
214
|
All five write into the universal Skill format (`SKILL.md` under each
|
|
215
|
-
|
|
216
|
-
`~/.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
MCP).
|
|
215
|
+
known/configured skills root — `~/.claude/skills/`, `~/.codex/skills/`,
|
|
216
|
+
existing `~/.agents/skills/`, optional `THREADKEEPER_EXTRA_SKILLS_DIRS`,
|
|
217
|
+
plus the canonical `~/.threadkeeper/skills/` mirror), with
|
|
218
|
+
`~/.threadkeeper/lessons.md` as a CLI-agnostic fallback for clients
|
|
219
|
+
without a native skills loader (Gemini, Copilot, bare MCP).
|
|
220
220
|
|
|
221
221
|
#### 1. Auto-review on close_thread
|
|
222
222
|
|
|
@@ -226,9 +226,12 @@ thread's notes. The prompt is rubric-form (Q1–Q5 yes/no) with explicit
|
|
|
226
226
|
positive examples for incident-vs-rule classification. The fork also
|
|
227
227
|
receives a "recently active skills" block so it prefers PATCHing
|
|
228
228
|
existing umbrellas over creating new ones (*active-update bias*).
|
|
229
|
-
Child appends a lesson via `lesson_append`,
|
|
230
|
-
|
|
231
|
-
`mark_skill_materialized`.
|
|
229
|
+
Child appends a lesson via `lesson_append`, writes/patches a skill via
|
|
230
|
+
`skill_manage` or writes a skill file directly, then closes with
|
|
231
|
+
`mark_skill_materialized`. If `skill_path` points at a `SKILL.md` (or a
|
|
232
|
+
skill directory), thread-keeper immediately mirrors that whole skill
|
|
233
|
+
into every configured skills root. Opt in with
|
|
234
|
+
`THREADKEEPER_AUTO_REVIEW=1`.
|
|
232
235
|
|
|
233
236
|
#### 2. Shadow-review daemon
|
|
234
237
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "threadkeeper"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.3"
|
|
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" }]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""brief() skill_hint nudge — fires when a recently-closed thread is rich
|
|
2
|
-
enough to be worth materializing as a
|
|
2
|
+
enough to be worth materializing as a mirrored reusable skill.
|
|
3
3
|
|
|
4
4
|
After complex tasks, the agent should turn distilled insights into
|
|
5
5
|
reusable skills, not let them sit only in notes. The nudge surfaces
|
|
@@ -535,7 +535,7 @@ def test_skill_manage_mirrors_skill_across_detected_clis(
|
|
|
535
535
|
skills_pkg, monkeypatch,
|
|
536
536
|
):
|
|
537
537
|
"""skill_manage(create) must write SKILL.md not only to the canonical
|
|
538
|
-
|
|
538
|
+
primary skills root but also to every configured skills_dir() —
|
|
539
539
|
so one materialization reaches Claude AND Codex at once."""
|
|
540
540
|
# Pin two mirror targets that we own (in tmp_path) so the test is
|
|
541
541
|
# hermetic — don't touch the developer's ~/.codex/ or ~/.threadkeeper/.
|
|
@@ -568,6 +568,71 @@ def test_skill_manage_mirrors_skill_across_detected_clis(
|
|
|
568
568
|
canonical.read_text()
|
|
569
569
|
|
|
570
570
|
|
|
571
|
+
def test_mirror_targets_include_known_skill_roots_without_install_detection(
|
|
572
|
+
skills_pkg, monkeypatch,
|
|
573
|
+
):
|
|
574
|
+
"""Mirror planning should include native skill roots even when an
|
|
575
|
+
adapter's executable/config detection would be false. Skill roots are
|
|
576
|
+
cheap paths; creation should not depend on whether the CLI has already
|
|
577
|
+
written a config file."""
|
|
578
|
+
from threadkeeper.tools import skills as st
|
|
579
|
+
import threadkeeper.adapters as adapters
|
|
580
|
+
|
|
581
|
+
class FakeAdapter:
|
|
582
|
+
def skills_dir(self):
|
|
583
|
+
return skills_pkg["tmp"] / "future_cli_skills"
|
|
584
|
+
|
|
585
|
+
monkeypatch.setattr(adapters, "ADAPTERS", [FakeAdapter()])
|
|
586
|
+
|
|
587
|
+
targets = st._mirror_targets("future-skill")
|
|
588
|
+
assert skills_pkg["tmp"] / "future_cli_skills" / "future-skill" in targets
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_mark_skill_materialized_mirrors_external_skill_dir(
|
|
592
|
+
skills_pkg, monkeypatch,
|
|
593
|
+
):
|
|
594
|
+
"""If an agent created a skill directly in Codex/Claude and then calls
|
|
595
|
+
mark_skill_materialized(skill_path=...), thread-keeper should copy the
|
|
596
|
+
whole skill dir to canonical + mirrors."""
|
|
597
|
+
mirror_codex = skills_pkg["tmp"] / "fake_codex_skills"
|
|
598
|
+
mirror_agents = skills_pkg["tmp"] / "fake_agents_skills"
|
|
599
|
+
|
|
600
|
+
from threadkeeper.tools import skills as st
|
|
601
|
+
|
|
602
|
+
def fake_mirror_targets(name):
|
|
603
|
+
return [mirror_codex / name, mirror_agents / name]
|
|
604
|
+
|
|
605
|
+
monkeypatch.setattr(st, "_mirror_targets", fake_mirror_targets)
|
|
606
|
+
|
|
607
|
+
external = skills_pkg["tmp"] / "external_codex" / "external-sync"
|
|
608
|
+
(external / "agents").mkdir(parents=True)
|
|
609
|
+
(external / "SKILL.md").write_text(
|
|
610
|
+
"---\n"
|
|
611
|
+
"name: external-sync\n"
|
|
612
|
+
"description: Use when testing external skill sync.\n"
|
|
613
|
+
"---\n\n"
|
|
614
|
+
"# external-sync\n",
|
|
615
|
+
encoding="utf-8",
|
|
616
|
+
)
|
|
617
|
+
(external / "agents" / "openai.yaml").write_text(
|
|
618
|
+
"interface:\n"
|
|
619
|
+
" display_name: \"External Sync\"\n",
|
|
620
|
+
encoding="utf-8",
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
open_t = _tool(skills_pkg, "open_thread")
|
|
624
|
+
mark = _tool(skills_pkg, "mark_skill_materialized")
|
|
625
|
+
tid = open_t(question="external skill sync")
|
|
626
|
+
|
|
627
|
+
assert mark(thread_id=tid, skill_path=str(external / "SKILL.md")) == "ok"
|
|
628
|
+
|
|
629
|
+
canonical = skills_pkg["skills_root"] / "external-sync"
|
|
630
|
+
assert (canonical / "SKILL.md").exists()
|
|
631
|
+
assert (canonical / "agents" / "openai.yaml").exists()
|
|
632
|
+
assert (mirror_codex / "external-sync" / "SKILL.md").exists()
|
|
633
|
+
assert (mirror_agents / "external-sync" / "agents" / "openai.yaml").exists()
|
|
634
|
+
|
|
635
|
+
|
|
571
636
|
def test_skill_manage_delete_removes_mirrors(skills_pkg, monkeypatch):
|
|
572
637
|
"""delete must propagate to every mirror target — otherwise stale
|
|
573
638
|
SKILL.md files keep auto-triggering in Codex / canonical store."""
|
|
@@ -355,7 +355,7 @@ def render_brief(conn: sqlite3.Connection, query: str = "", k: int = 6) -> str:
|
|
|
355
355
|
# ── skill_hint ────────────────────────────────────────────────────────
|
|
356
356
|
# Behavioral nudge: after a rich thread closes, the lessons inside
|
|
357
357
|
# it (insights + repeated moves) should
|
|
358
|
-
# be materialized as a reusable
|
|
358
|
+
# be materialized as a reusable skill mirrored across configured roots,
|
|
359
359
|
# not just sit in notes. Trigger only on threads recently closed AND
|
|
360
360
|
# rich enough to be worth a class-level skill — never on one-off chatter.
|
|
361
361
|
#
|
|
@@ -413,7 +413,7 @@ def render_brief(conn: sqlite3.Connection, query: str = "", k: int = 6) -> str:
|
|
|
413
413
|
warn = "⚠️ " if consecutive_ignored >= 3 else ""
|
|
414
414
|
out.append(
|
|
415
415
|
f" → {warn}closed thread is rich (≥5 notes, ≥2 insight/move). "
|
|
416
|
-
"MATERIALIZE: invoke skill-creator to write
|
|
416
|
+
"MATERIALIZE: invoke skill-creator or skill_manage to write "
|
|
417
417
|
"<class-level-name>/SKILL.md from the insights — don't let "
|
|
418
418
|
"learnings sit only in notes"
|
|
419
419
|
)
|
|
@@ -139,7 +139,7 @@ MEMORY_REVIEW_PROMPT = (
|
|
|
139
139
|
SKILL_REVIEW_PROMPT = (
|
|
140
140
|
"Review the closed thread above and materialize any class-level "
|
|
141
141
|
"lessons.\n\n"
|
|
142
|
-
"PRIMARY output: a SKILL.md
|
|
142
|
+
"PRIMARY output: a SKILL.md via "
|
|
143
143
|
"skill_manage(action='create'|'patch'|'write_file'|'delete'). The "
|
|
144
144
|
"Skill format is the universal format — Claude Code, Claude "
|
|
145
145
|
"Desktop, Codex, the Anthropic IDE plugins, and any MCP-aware tool "
|
|
@@ -149,8 +149,8 @@ SKILL_REVIEW_PROMPT = (
|
|
|
149
149
|
"FALLBACK output (only when target CLI has no skills/ directory — "
|
|
150
150
|
"Gemini / Copilot / generic MCP clients without a skill loader): "
|
|
151
151
|
"lesson_append(title, body, summary, source=thread_id) writes into "
|
|
152
|
-
"~/.threadkeeper/lessons.md. Use this only if
|
|
153
|
-
"
|
|
152
|
+
"~/.threadkeeper/lessons.md. Use this only if a SKILL.md is not "
|
|
153
|
+
"appropriate; otherwise the mirrored SKILL.md is strictly better.\n\n"
|
|
154
154
|
+ RUBRIC_QUESTIONS + "\n\n"
|
|
155
155
|
"PREFERENCE ORDER (pick the earliest action that fits):\n"
|
|
156
156
|
" 1. PATCH an existing skill. If the conversation referenced (or "
|
|
@@ -192,7 +192,7 @@ COMBINED_REVIEW_PROMPT = (
|
|
|
192
192
|
"/ note as appropriate.\n\n"
|
|
193
193
|
" **Skills** — how to handle this class of task. PRIMARY: "
|
|
194
194
|
"skill_manage(action='create'|'patch'|'write_file'|'delete') → "
|
|
195
|
-
"
|
|
195
|
+
"a mirrored SKILL.md under every configured skills root. The Skill format auto-triggers "
|
|
196
196
|
"via frontmatter description and is consumed by every modern "
|
|
197
197
|
"agentic CLI (Claude Code/Desktop, Codex CLI/desktop, IDE plugins) "
|
|
198
198
|
"— strictly better than an opt-in lessons.md scan. FALLBACK: "
|
|
@@ -51,9 +51,9 @@ from .review_prompts import POSITIVE_EXAMPLES
|
|
|
51
51
|
|
|
52
52
|
SHADOW_REVIEW_PROMPT = f"""\
|
|
53
53
|
You are a SHADOW LEARNING OBSERVER for thread-keeper. You read a slice
|
|
54
|
-
of recent dialog from across ALL
|
|
54
|
+
of recent dialog from across ALL agent sessions on this machine and
|
|
55
55
|
decide whether any CLASS-LEVEL learning emerged that's worth a durable
|
|
56
|
-
skill
|
|
56
|
+
skill.
|
|
57
57
|
|
|
58
58
|
CLASS-LEVEL signals (materialize):
|
|
59
59
|
{SHADOW_CLASS_SIGNAL_EXAMPLES}\
|
|
@@ -84,7 +84,7 @@ PROCEDURE
|
|
|
84
84
|
- body: markdown rationale + procedure
|
|
85
85
|
- summary: optional one-line TL;DR
|
|
86
86
|
b. OPTIONAL: also call `mcp__thread-keeper__skill_manage(...)` to mirror
|
|
87
|
-
|
|
87
|
+
SKILL.md into every configured skills root when frontmatter
|
|
88
88
|
auto-triggering adds value beyond the lesson alone.
|
|
89
89
|
c. Output `MATERIALIZED: <slug>` on success.
|
|
90
90
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Multi-CLI skills lifecycle: create/edit/patch/delete + usage telemetry +
|
|
2
2
|
curator + background-review fork.
|
|
3
3
|
|
|
4
4
|
Bridges thread-keeper (working memory across sessions) and Claude's
|
|
5
|
-
~/.claude/skills/ store
|
|
5
|
+
primary ~/.claude/skills/ store plus mirrored CLI skill roots
|
|
6
|
+
(procedural memory). The Learning loop:
|
|
6
7
|
|
|
7
8
|
rich thread closes → brief() surfaces skill_hint nudge →
|
|
8
9
|
agent calls review_thread(...) → spawned child reads notes,
|
|
@@ -24,6 +25,7 @@ Validator enforces the agentskills.io-compatible frontmatter shape:
|
|
|
24
25
|
from __future__ import annotations
|
|
25
26
|
|
|
26
27
|
import json
|
|
28
|
+
import os
|
|
27
29
|
import re
|
|
28
30
|
import shutil
|
|
29
31
|
import sqlite3
|
|
@@ -143,55 +145,90 @@ def _archive_dir() -> Path:
|
|
|
143
145
|
|
|
144
146
|
|
|
145
147
|
# ──────────────────────────────────────────────────────────────────────────
|
|
146
|
-
# Multi-mirror — propagate
|
|
147
|
-
#
|
|
148
|
-
# the canonical ~/.threadkeeper/skills/
|
|
149
|
-
# per-mirror failures are logged but
|
|
148
|
+
# Multi-mirror — propagate a whole skill directory across every known
|
|
149
|
+
# native skills/ root so a single materialization reaches Claude, Codex,
|
|
150
|
+
# shared ~/.agents skills, and the canonical ~/.threadkeeper/skills/
|
|
151
|
+
# fallback at once. Best-effort: per-mirror failures are logged but
|
|
152
|
+
# don't fail the canonical write.
|
|
150
153
|
# ──────────────────────────────────────────────────────────────────────────
|
|
151
154
|
|
|
152
|
-
def
|
|
153
|
-
"""
|
|
155
|
+
def _extra_skill_roots() -> list[Path]:
|
|
156
|
+
"""Additional skills roots outside CLI adapters.
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
THREADKEEPER_EXTRA_SKILLS_DIRS is os.pathsep-separated. We also mirror
|
|
159
|
+
into ~/.agents/skills when that shared OpenAI-agent skill root already
|
|
160
|
+
exists on the machine.
|
|
161
|
+
"""
|
|
162
|
+
roots: list[Path] = []
|
|
163
|
+
raw = os.environ.get("THREADKEEPER_EXTRA_SKILLS_DIRS", "").strip()
|
|
164
|
+
if raw:
|
|
165
|
+
for item in raw.split(os.pathsep):
|
|
166
|
+
item = item.strip()
|
|
167
|
+
if item:
|
|
168
|
+
roots.append(Path(item).expanduser())
|
|
169
|
+
agents_root = Path("~/.agents/skills").expanduser()
|
|
170
|
+
if agents_root.exists():
|
|
171
|
+
roots.append(agents_root)
|
|
172
|
+
return roots
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _skill_roots() -> list[Path]:
|
|
176
|
+
"""Every root that should receive mirrored skill directories.
|
|
177
|
+
|
|
178
|
+
Use all adapters, not only installed adapters. Skill roots are cheap
|
|
179
|
+
filesystem paths, and relying on install-detection can miss a CLI that
|
|
180
|
+
is present but has not written its config yet.
|
|
181
|
+
"""
|
|
182
|
+
from ..adapters import ADAPTERS
|
|
160
183
|
from ..config import DB_PATH
|
|
161
184
|
|
|
162
|
-
|
|
163
|
-
seen: set[Path] =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if
|
|
171
|
-
|
|
172
|
-
seen.add(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
roots: list[Path] = []
|
|
186
|
+
seen: set[Path] = set()
|
|
187
|
+
|
|
188
|
+
def add(root: Path | None) -> None:
|
|
189
|
+
if root is None:
|
|
190
|
+
return
|
|
191
|
+
root = root.expanduser()
|
|
192
|
+
key = root.resolve()
|
|
193
|
+
if key in seen:
|
|
194
|
+
return
|
|
195
|
+
seen.add(key)
|
|
196
|
+
roots.append(root)
|
|
197
|
+
|
|
198
|
+
add(CLAUDE_SKILLS_DIR)
|
|
199
|
+
for adapter in ADAPTERS:
|
|
200
|
+
add(adapter.skills_dir())
|
|
201
|
+
for root in _extra_skill_roots():
|
|
202
|
+
add(root)
|
|
203
|
+
add(DB_PATH.parent / "skills")
|
|
204
|
+
return roots
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _mirror_targets(name: str) -> list[Path]:
|
|
208
|
+
"""All non-primary skill directories that should hold <name>/."""
|
|
209
|
+
primary = CLAUDE_SKILLS_DIR.resolve()
|
|
210
|
+
return [root / name for root in _skill_roots() if root.resolve() != primary]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _copy_skill_tree(src: Path, dst: Path) -> None:
|
|
214
|
+
"""Replace dst with a recursive copy of src unless they are identical."""
|
|
215
|
+
if src.resolve() == dst.resolve():
|
|
216
|
+
return
|
|
217
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
if dst.exists():
|
|
219
|
+
shutil.rmtree(dst)
|
|
220
|
+
shutil.copytree(src, dst)
|
|
180
221
|
|
|
181
222
|
|
|
182
223
|
def _mirror_skill_dir(name: str) -> None:
|
|
183
224
|
"""Copy CLAUDE_SKILLS_DIR/<name>/ → every mirror target (recursive).
|
|
184
225
|
Pre-existing mirror is replaced atomically. Best-effort per target."""
|
|
185
|
-
import shutil as _sh
|
|
186
226
|
src = _skill_dir(name)
|
|
187
227
|
if not src.exists():
|
|
188
228
|
return
|
|
189
229
|
for dst in _mirror_targets(name):
|
|
190
230
|
try:
|
|
191
|
-
|
|
192
|
-
if dst.exists():
|
|
193
|
-
_sh.rmtree(dst)
|
|
194
|
-
_sh.copytree(src, dst)
|
|
231
|
+
_copy_skill_tree(src, dst)
|
|
195
232
|
except Exception:
|
|
196
233
|
# Per-mirror failure (permission denied / disk full / etc.)
|
|
197
234
|
# doesn't fail the canonical write. The user can re-sync
|
|
@@ -199,6 +236,43 @@ def _mirror_skill_dir(name: str) -> None:
|
|
|
199
236
|
pass
|
|
200
237
|
|
|
201
238
|
|
|
239
|
+
def mirror_skill_from_path(skill_path: str) -> Optional[str]:
|
|
240
|
+
"""Mirror an existing external skill dir into every configured root.
|
|
241
|
+
|
|
242
|
+
`skill_path` may point at SKILL.md or at the skill directory. The
|
|
243
|
+
frontmatter `name` is authoritative; the source directory does not
|
|
244
|
+
need to be under CLAUDE_SKILLS_DIR. Returns the mirrored skill name,
|
|
245
|
+
or None if the path is absent/invalid. Best-effort mirrors are still
|
|
246
|
+
handled by _mirror_skill_dir after the canonical copy lands.
|
|
247
|
+
"""
|
|
248
|
+
raw = skill_path.strip()
|
|
249
|
+
if not raw:
|
|
250
|
+
return None
|
|
251
|
+
p = Path(raw).expanduser()
|
|
252
|
+
src = p.parent if p.name == "SKILL.md" else p
|
|
253
|
+
md = src / "SKILL.md"
|
|
254
|
+
if not md.is_file():
|
|
255
|
+
return None
|
|
256
|
+
try:
|
|
257
|
+
body = md.read_text(encoding="utf-8")
|
|
258
|
+
except OSError:
|
|
259
|
+
return None
|
|
260
|
+
fields, err = _parse_frontmatter(body)
|
|
261
|
+
if err or not fields:
|
|
262
|
+
return None
|
|
263
|
+
name = fields["name"]
|
|
264
|
+
if err := _validate_name(name):
|
|
265
|
+
return None
|
|
266
|
+
if err := _validate_skill_md(body, name):
|
|
267
|
+
return None
|
|
268
|
+
try:
|
|
269
|
+
_copy_skill_tree(src, _skill_dir(name))
|
|
270
|
+
except Exception:
|
|
271
|
+
return None
|
|
272
|
+
_mirror_skill_dir(name)
|
|
273
|
+
return name
|
|
274
|
+
|
|
275
|
+
|
|
202
276
|
def _unmirror_skill(name: str) -> None:
|
|
203
277
|
"""Remove <name>/ from every mirror target. Best-effort per target."""
|
|
204
278
|
import shutil as _sh
|
|
@@ -359,7 +433,7 @@ _VALID_OUTCOMES: set[str] = {"helped", "partial", "wrong"}
|
|
|
359
433
|
|
|
360
434
|
@mcp.tool()
|
|
361
435
|
def skill_record(name: str, kind: str = "use", outcome: str = "") -> str:
|
|
362
|
-
"""Record usage telemetry for a
|
|
436
|
+
"""Record usage telemetry for a mirrored skill.
|
|
363
437
|
|
|
364
438
|
`kind`: 'use' | 'view' | 'patch' | 'create'. Bumps the corresponding
|
|
365
439
|
counter + timestamp in skill_usage. The curator reads these to decide
|
|
@@ -425,9 +499,10 @@ def skill_manage(action: str,
|
|
|
425
499
|
new_string: str = "",
|
|
426
500
|
sub_path: str = "",
|
|
427
501
|
description: str = "") -> str:
|
|
428
|
-
"""Create, edit, patch, or delete
|
|
502
|
+
"""Create, edit, patch, or delete skills under the primary skills root.
|
|
429
503
|
|
|
430
|
-
Atomic write with frontmatter validation before disk hits
|
|
504
|
+
Atomic primary write with frontmatter validation before disk hits, then
|
|
505
|
+
best-effort mirror into every configured skill root.
|
|
431
506
|
|
|
432
507
|
Actions:
|
|
433
508
|
create — write a brand-new skill. Requires `name` + `description`
|
|
@@ -627,7 +702,7 @@ def _action_delete(name: str) -> str:
|
|
|
627
702
|
|
|
628
703
|
@mcp.tool()
|
|
629
704
|
def skill_list(include_archived: bool = False) -> str:
|
|
630
|
-
"""List
|
|
705
|
+
"""List skills with telemetry. Format:
|
|
631
706
|
<name> tier=<hypothesis|observed|validated> origin=<...>
|
|
632
707
|
state=<active|stale|archived> uses=N fg_uses=N
|
|
633
708
|
views=N patches=N wrong=N pinned=0/1 last_active=<age>
|
|
@@ -763,7 +838,7 @@ def curator_run(stale_after_days: int = 30,
|
|
|
763
838
|
Lifecycle:
|
|
764
839
|
active → stale when last activity > stale_after_days
|
|
765
840
|
stale → archived when last activity > archive_after_days
|
|
766
|
-
(also moves directory to
|
|
841
|
+
(also moves primary directory to .archive/)
|
|
767
842
|
|
|
768
843
|
Tier-aware adjustments (the discrete trust signal trumps raw activity):
|
|
769
844
|
• tier='validated' skills are NEVER stale-aged or archived — proven
|
|
@@ -140,20 +140,29 @@ def close_thread(thread_id: str, outcome: str) -> str:
|
|
|
140
140
|
@mcp.tool()
|
|
141
141
|
def mark_skill_materialized(thread_id: str, skill_path: str = "") -> str:
|
|
142
142
|
"""Close the Learning loop: record that a closed thread's insights were
|
|
143
|
-
written into a
|
|
143
|
+
written into a skill.
|
|
144
144
|
|
|
145
145
|
Stops the brief()'s `skill_hint` nudge from firing for this thread. Also
|
|
146
146
|
appends a `move` note pointing at the skill path so future briefs surface
|
|
147
147
|
the link.
|
|
148
148
|
|
|
149
149
|
Pass the absolute path to the SKILL.md (or skill directory) when known;
|
|
150
|
-
leave empty if you only want to silence the hint without recording a path.
|
|
150
|
+
leave empty if you only want to silence the hint without recording a path.
|
|
151
|
+
When a path is provided, thread-keeper also mirrors that skill directory
|
|
152
|
+
into every configured native skills root (Claude, Codex, shared agents,
|
|
153
|
+
and ~/.threadkeeper/skills) on a best-effort basis."""
|
|
151
154
|
conn = get_db()
|
|
152
155
|
_ensure_session(conn)
|
|
153
156
|
if not conn.execute("SELECT 1 FROM threads WHERE id=?", (thread_id,)).fetchone():
|
|
154
157
|
return f"ERR thread_not_found={thread_id}"
|
|
155
158
|
now = int(time.time())
|
|
156
159
|
path = skill_path.strip()
|
|
160
|
+
if path:
|
|
161
|
+
try:
|
|
162
|
+
from .skills import mirror_skill_from_path
|
|
163
|
+
mirror_skill_from_path(path)
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
157
166
|
summary = path or "(no path recorded)"
|
|
158
167
|
conn.execute(
|
|
159
168
|
"INSERT INTO events (session_id, kind, target, summary, created_at) "
|
|
@@ -163,7 +172,7 @@ def mark_skill_materialized(thread_id: str, skill_path: str = "") -> str:
|
|
|
163
172
|
)
|
|
164
173
|
note_body = (
|
|
165
174
|
f"materialized into {path}" if path
|
|
166
|
-
else "materialized into a
|
|
175
|
+
else "materialized into a skill (path not recorded)"
|
|
167
176
|
)
|
|
168
177
|
emb = _embed(note_body)
|
|
169
178
|
cur = conn.execute(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: threadkeeper
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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
|
|
@@ -82,10 +82,10 @@ make it more than a memory store:
|
|
|
82
82
|
harvester, candidate-reviewer, weekly Curator) materialize
|
|
83
83
|
class-level skills as the agents work. Adapted to multi-CLI:
|
|
84
84
|
SKILL.md is the primary write target and gets mirrored to every
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
loader.
|
|
85
|
+
known/configured skills root simultaneously (`~/.claude/skills/`,
|
|
86
|
+
`~/.codex/skills/`, existing `~/.agents/skills/`, extra roots from
|
|
87
|
+
`THREADKEEPER_EXTRA_SKILLS_DIRS`, and `~/.threadkeeper/skills/`),
|
|
88
|
+
with lessons.md as a fallback for CLIs without a native skills loader.
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
@@ -223,8 +223,8 @@ shows agents focused on their primary task rarely do).
|
|
|
223
223
|
brief() SKILL.md + lessons.md ─► skill_usage │
|
|
224
224
|
│ │ │ │
|
|
225
225
|
│ ▼ ▼ │
|
|
226
|
-
│ (every
|
|
227
|
-
│ skills/
|
|
226
|
+
│ (every configured │ │
|
|
227
|
+
│ skills/ root) │ │
|
|
228
228
|
│ │ │ │
|
|
229
229
|
│ └──────► [5] Curator daemon ───┘
|
|
230
230
|
│ (cron, every 7d)
|
|
@@ -246,11 +246,11 @@ shows agents focused on their primary task rarely do).
|
|
|
246
246
|
| 5 | Curator daemon | every 7 days (env knob) | every existing lesson + recently-touched skill | REPORT-`<date>`.md (advisory) or direct PATCH/PRUNE/CONSOLIDATE |
|
|
247
247
|
|
|
248
248
|
All five write into the universal Skill format (`SKILL.md` under each
|
|
249
|
-
|
|
250
|
-
`~/.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
MCP).
|
|
249
|
+
known/configured skills root — `~/.claude/skills/`, `~/.codex/skills/`,
|
|
250
|
+
existing `~/.agents/skills/`, optional `THREADKEEPER_EXTRA_SKILLS_DIRS`,
|
|
251
|
+
plus the canonical `~/.threadkeeper/skills/` mirror), with
|
|
252
|
+
`~/.threadkeeper/lessons.md` as a CLI-agnostic fallback for clients
|
|
253
|
+
without a native skills loader (Gemini, Copilot, bare MCP).
|
|
254
254
|
|
|
255
255
|
#### 1. Auto-review on close_thread
|
|
256
256
|
|
|
@@ -260,9 +260,12 @@ thread's notes. The prompt is rubric-form (Q1–Q5 yes/no) with explicit
|
|
|
260
260
|
positive examples for incident-vs-rule classification. The fork also
|
|
261
261
|
receives a "recently active skills" block so it prefers PATCHing
|
|
262
262
|
existing umbrellas over creating new ones (*active-update bias*).
|
|
263
|
-
Child appends a lesson via `lesson_append`,
|
|
264
|
-
|
|
265
|
-
`mark_skill_materialized`.
|
|
263
|
+
Child appends a lesson via `lesson_append`, writes/patches a skill via
|
|
264
|
+
`skill_manage` or writes a skill file directly, then closes with
|
|
265
|
+
`mark_skill_materialized`. If `skill_path` points at a `SKILL.md` (or a
|
|
266
|
+
skill directory), thread-keeper immediately mirrors that whole skill
|
|
267
|
+
into every configured skills root. Opt in with
|
|
268
|
+
`THREADKEEPER_AUTO_REVIEW=1`.
|
|
266
269
|
|
|
267
270
|
#### 2. Shadow-review daemon
|
|
268
271
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|