diff --git a/install.py b/install.py index e4f3b54..cc79333 100644 --- a/install.py +++ b/install.py @@ -31,6 +31,8 @@ "campaign", "fleet", "learn", + "learn-status", + "forget", "plan", "research", "polish", @@ -54,6 +56,11 @@ ] COMMAND_FILES = [ "do.md", +] + +# Legacy commands removed in favor of routing through skills directly. +# Listed here so uninstall cleans them up from ~/.claude/commands/. +_LEGACY_COMMANDS = [ "experiment.md", "run.md", "campaign.md", @@ -69,6 +76,7 @@ "loop.md", "gc.md", "pr.md", + "polish.md", ] HOOK_EVENTS = { @@ -117,7 +125,6 @@ def _patch_settings() -> None: event_hooks = hooks.get(event, []) for script in scripts: command = f"uv run --project {REPO_DIR} python3 {hooks_dir / script}" - # Check if already registered (also match legacy python3-only commands) # Check if already registered (match in hooks array or legacy top-level command) already = any( # New format: hooks array @@ -218,7 +225,14 @@ def install() -> None: if source.exists(): _symlink(source, CLAUDE_DIR / "agents" / f"autodidact-{agent}") - # Symlink commands (no prefix — user-facing) + # Clean up legacy command symlinks (now routed through skills) + for cmd in _LEGACY_COMMANDS: + target = CLAUDE_DIR / "commands" / cmd + if target.is_symlink(): + target.unlink() + print(f" -> Removed legacy commands/{cmd}") + + # Symlink commands (only /do entry point) for cmd in COMMAND_FILES: source = REPO_DIR / "commands" / cmd if source.exists(): @@ -272,7 +286,7 @@ def uninstall() -> None: target.unlink() print(f" -> Removed agents/autodidact-{agent}") - for cmd in COMMAND_FILES: + for cmd in COMMAND_FILES + _LEGACY_COMMANDS: target = CLAUDE_DIR / "commands" / cmd if target.is_symlink(): target.unlink() diff --git a/skills/do/skill.md b/skills/do/skill.md index b7acfac..f1235b6 100644 --- a/skills/do/skill.md +++ b/skills/do/skill.md @@ -28,9 +28,11 @@ The Python router (`src/router.py`) handles Tiers 0-2 automatically via the `use **Non-orchestration skills** (match these first): - `autodidact-experiment` — User wants iterative optimization against a metric - `autodidact-plan` — User needs to clarify, research, or plan (all three are one pipeline) - - `review` — User wants code review (command-only, no autodidact- prefix) + - `autodidact-polish` — User wants code review (also triggered by "review") - `autodidact-handoff` — User wants to create a session transfer document - `autodidact-learn` — User wants to teach autodidact something + - `autodidact-learn-status` — User wants to see knowledge inventory / learning stats + - `autodidact-forget` — User wants to decay or remove specific learnings **Orchestration skills** (use the complexity matrix below): | Signal | Route | Confidence cue | @@ -42,7 +44,7 @@ The Python router (`src/router.py`) handles Tiers 0-2 automatically via the `use **Decision priority**: `direct` > `autodidact-fleet` (if parallelizable) > `autodidact-run` (if sequential) > `autodidact-campaign` (if scope exceeds one session). Prefer simpler orchestration when uncertain. - **IMPORTANT**: Always use the `autodidact-` prefix for skill names to ensure autodidact skills are invoked, not project-scoped alternatives. Exceptions: `direct` (signal, not a skill), `review`, `forget`, and `learn_status` (command-only, no autodidact- prefix). + **IMPORTANT**: Always use the `autodidact-` prefix for skill names to ensure autodidact skills are invoked, not project-scoped alternatives. Exception: `direct` (signal value, not a skill). 3. **For `direct` classification**: Just do the task. No orchestration overhead. diff --git a/skills/forget/skill.md b/skills/forget/skill.md new file mode 100644 index 0000000..a844123 --- /dev/null +++ b/skills/forget/skill.md @@ -0,0 +1,65 @@ +--- +description: Decay or remove specific learnings from the autodidact database. +--- + +# /forget — Knowledge Removal + +## Identity + +You are the knowledge decay agent for autodidact. You help the user remove or reduce confidence in specific learnings that are outdated, incorrect, or no longer useful. + +## Orientation + +The learning database (`~/.claude/autodidact/learning.db`) stores all knowledge captured by autodidact. Each learning has a topic, key, value, confidence score, and metadata. Sometimes knowledge becomes stale or wrong and needs to be decayed or deleted. + +## Protocol + +1. **Find matching learnings**: Search the DB for what the user wants to forget: + ```bash + python3 -c " + import sys, json; sys.path.insert(0, 'REPO_PATH') + from src.db import LearningDB + db = LearningDB() + results = db.query_fts('SEARCH_TERMS') + for r in results: + print(json.dumps(r, default=str)) + db.close() + " + ``` + +2. **Show matches** and ask for confirmation. Present each match with its topic, key, value, and current confidence. + +3. **Offer two actions**: + - **Decay**: Reduce confidence by 0.3 (learning fades but isn't deleted): + ```bash + python3 -c " + import sys; sys.path.insert(0, 'REPO_PATH') + from src.db import LearningDB + db = LearningDB() + db.decay(learning_id=LEARNING_ID, amount=0.3) + db.close() + " + ``` + - **Delete**: Remove entirely from the DB via direct SQL: + ```bash + python3 -c " + import sys; sys.path.insert(0, 'REPO_PATH') + from src.db import LearningDB + db = LearningDB() + db.conn.execute('DELETE FROM learnings WHERE id = ?', (LEARNING_ID,)) + db.conn.commit() + db.close() + " + ``` + +4. **Execute** the chosen action and confirm by re-querying the DB to verify the change. + +## Quality Gates + +- [ ] User confirmed which learnings to act on +- [ ] Action (decay or delete) was confirmed before execution +- [ ] Result was verified after execution + +## Exit Protocol + +Confirm what was decayed or deleted, showing the before/after confidence for decayed items. diff --git a/skills/learn-status/skill.md b/skills/learn-status/skill.md new file mode 100644 index 0000000..f8ae21d --- /dev/null +++ b/skills/learn-status/skill.md @@ -0,0 +1,46 @@ +--- +description: Show confidence stats and knowledge inventory from the autodidact learning database. +--- + +# /learn-status — Knowledge Inventory + +## Identity + +You are the knowledge inventory agent for autodidact. You provide a comprehensive view of the current state of the learning database, including stats, top learnings, graduation candidates, and routing gaps. + +## Orientation + +The learning database tracks all knowledge autodidact has accumulated. This skill provides a dashboard view of that knowledge. + +## Protocol + +1. **Gather data** from the learning DB: + ```bash + python3 -c " + import sys, json; sys.path.insert(0, 'REPO_PATH') + from src.db import LearningDB + db = LearningDB() + stats = db.stats() + top = db.get_top_learnings(limit=15) + candidates = db.get_graduation_candidates() + gaps = db.get_routing_gaps(limit=5) + print(json.dumps({'stats': stats, 'top_learnings': top, 'graduation_candidates': candidates, 'routing_gaps': gaps}, indent=2, default=str)) + db.close() + " + ``` + +2. **Present results** in a readable format: + 1. **Summary**: Total learnings, average confidence, graduated count + 2. **Top Learnings**: Highest confidence items (topic, key, value, confidence) + 3. **Graduation Candidates**: Items ready to be promoted (confidence >= 0.9, observations >= 5) + 4. **Routing Gaps**: Recent unmatched prompts (if any) + 5. **Token Economics**: When RTK is installed, show total commands, total saved tokens, avg savings %, estimated $ saved (from `rtk gain --daily`). When not installed: "Install RTK for token analytics" + +## Quality Gates + +- [ ] All five sections are presented +- [ ] Data is current (freshly queried, not cached) + +## Exit Protocol + +Present the dashboard. No further action needed. diff --git a/skills/learn/skill.md b/skills/learn/skill.md index f432beb..1986fac 100644 --- a/skills/learn/skill.md +++ b/skills/learn/skill.md @@ -34,6 +34,22 @@ The learning database (`~/.claude/autodidact/learning.db`) stores all knowledge 3. Confirm what was recorded and at what confidence level. +### Mining (`/learn mine `) + +1. Call `mine_and_record()` from `src.session_miner` with the given project path and the active LearningDB: + ```bash + python3 -c " + import sys, json; sys.path.insert(0, 'REPO_PATH') + from src.db import LearningDB + from src.session_miner import mine_and_record + db = LearningDB() + result = mine_and_record('PROJECT_PATH', db) + print(json.dumps(result, default=str, indent=2)) + db.close() + " + ``` +2. Display the summary: sessions_scanned, commands_found, patterns_found, learnings_recorded + ### Querying (`/learn query `) 1. Run FTS5 search against the learning DB: diff --git a/src/router.py b/src/router.py index c34ab45..5cd05a2 100644 --- a/src/router.py +++ b/src/router.py @@ -25,7 +25,7 @@ class RouterResult: SKILL_MODEL_MAP: dict[str, str] = { # Haiku tier — cheap, fast - "learn_status": "haiku", + "learn-status": "haiku", "forget": "haiku", "direct": "haiku", # Sonnet tier — standard work @@ -34,7 +34,6 @@ class RouterResult: "plan": "sonnet", "run": "sonnet", "fleet": "sonnet", - "review": "sonnet", "polish": "sonnet", "handoff": "sonnet", "experiment": "sonnet", @@ -61,13 +60,13 @@ class RouterResult: (r"^/?(do\s+)?marshal\b", "run"), # legacy alias (r"^/?(do\s+)?campaign\b", "campaign"), (r"^/?(do\s+)?archon\b", "campaign"), # legacy alias + (r"^/?(do\s+)?learn[-_]?status\b", "learn-status"), (r"^/?(do\s+)?learn\b", "learn"), - (r"^/?(do\s+)?review\b", "review"), + (r"^/?(do\s+)?review\b", "polish"), (r"^/?(do\s+)?polish\b", "polish"), (r"^/?(do\s+)?handoff\b", "handoff"), (r"^/?(do\s+)?sync.?thoughts\b", "sync-thoughts"), (r"^/?(do\s+)?forget\b", "forget"), - (r"^/?(do\s+)?learn.?status\b", "learn_status"), (r"^/?(do\s+)?experiment\b", "experiment"), (r"^/do\s+loop\b", "loop"), # requires /do prefix to avoid matching "loop through..." (r"^/?loop$", "loop"), # bare "loop" with no arguments @@ -301,14 +300,12 @@ def _tier25_plan_analysis(cwd: str) -> RouterResult | None: ("requirements", 0.3), ("scope", 0.2), ], - "review": [ + "polish": [ ("review", 0.5), ("code review", 0.6), ("check quality", 0.4), ("audit", 0.3), ("inspect", 0.3), - ], - "polish": [ ("polish", 0.6), ("clean up", 0.4), ("simplify", 0.4), @@ -346,7 +343,7 @@ def _tier25_plan_analysis(cwd: str) -> RouterResult | None: ("merge request", 0.5), ("submit pr", 0.5), ], - "learn_status": [ + "learn-status": [ ("token savings", 0.6), ("token economics", 0.6), ("rtk", 0.5), @@ -416,6 +413,8 @@ def _tier2_keyword_heuristic(prompt: str) -> RouterResult | None: "fleet", "campaign", "learn", + "learn-status", + "forget", "handoff", "experiment", "loop", @@ -431,8 +430,7 @@ def _tier2_keyword_heuristic(prompt: str) -> RouterResult | None: def _qualify_skill(name: str) -> str: """Add the autodidact- prefix for installed skills. - Signal values (``direct``, ``classify``) and command-only entries - (``review``, ``forget``, ``learn_status``) are returned bare. + Signal values (``direct``, ``classify``) are returned bare. """ if name in _AUTODIDACT_SKILLS: return f"autodidact-{name}" diff --git a/tests/test_router.py b/tests/test_router.py index 7ce523a..f5b3396 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -64,9 +64,14 @@ def test_archon_legacy_alias(self) -> None: self.assertEqual(r.tier, 0) def test_learn_status(self) -> None: - """learn_status is command-only, no autodidact- prefix.""" r = classify("/do learn_status") - self.assertEqual(r.skill, "learn_status") + self.assertEqual(r.skill, "autodidact-learn-status") + self.assertEqual(r.tier, 0) + + def test_learn_vs_learn_status_ambiguity(self) -> None: + """'learn status codes' should route to learn, not learn-status.""" + r = classify("/do learn status codes are important") + self.assertEqual(r.skill, "autodidact-learn") self.assertEqual(r.tier, 0) def test_case_insensitive(self) -> None: @@ -104,10 +109,9 @@ def test_loop_does_not_match_natural_language(self) -> None: r = classify("loop through the array") self.assertNotEqual(r.skill, "autodidact-loop") - def test_forget_command_only(self) -> None: - """forget is command-only, no autodidact- prefix.""" + def test_forget_routes_to_forget_skill(self) -> None: r = classify("/do forget") - self.assertEqual(r.skill, "forget") + self.assertEqual(r.skill, "autodidact-forget") self.assertEqual(r.tier, 0) def test_gc_direct(self) -> None: @@ -189,10 +193,9 @@ def test_plan_routes_to_plan(self) -> None: self.assertEqual(r.skill, "autodidact-plan") self.assertEqual(r.tier, 2) - def test_review_routes_to_review(self) -> None: - """review is command-only, no autodidact- prefix.""" + def test_review_routes_to_polish(self) -> None: r = classify("code review the changes") - self.assertEqual(r.skill, "review") + self.assertEqual(r.skill, "autodidact-polish") self.assertEqual(r.tier, 2) def test_polish_keyword_routes(self) -> None: @@ -201,14 +204,13 @@ def test_polish_keyword_routes(self) -> None: self.assertEqual(r.tier, 2) def test_token_savings_routes_to_learn_status(self) -> None: - """RTK/token-savings queries route to learn_status (no prefix).""" r = classify("show my token savings") - self.assertEqual(r.skill, "learn_status") + self.assertEqual(r.skill, "autodidact-learn-status") self.assertEqual(r.tier, 2) def test_rtk_routes_to_learn_status(self) -> None: r = classify("rtk stats and learning stats") - self.assertEqual(r.skill, "learn_status") + self.assertEqual(r.skill, "autodidact-learn-status") self.assertEqual(r.tier, 2) def test_commit_keyword_routes_to_gc(self) -> None: @@ -305,7 +307,7 @@ def test_keyword_match_takes_precedence(self) -> None: tmpdir, ("## Plan: Small fix\n### Phase 1: Fix\n- [ ] Edit `src/router.py`\n") ) r = classify("code review the changes", cwd=tmpdir) - self.assertEqual(r.skill, "review") + self.assertEqual(r.skill, "autodidact-polish") self.assertEqual(r.tier, 2) def test_most_recent_plan_is_used(self) -> None: @@ -422,7 +424,7 @@ def test_campaign_gets_opus(self) -> None: self.assertEqual(r.model, "opus") def test_learn_status_gets_haiku(self) -> None: - r = classify("/do learn_status") + r = classify("/do learn-status") self.assertEqual(r.model, "haiku") def test_forget_gets_haiku(self) -> None: