Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions install.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"campaign",
"fleet",
"learn",
"learn-status",
"forget",
"plan",
"research",
"polish",
Expand All @@ -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",
Expand All @@ -69,6 +76,7 @@
"loop.md",
"gc.md",
"pr.md",
"polish.md",
]

HOOK_EVENTS = {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions skills/do/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.

Expand Down
65 changes: 65 additions & 0 deletions skills/forget/skill.md
Original file line number Diff line number Diff line change
@@ -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.
46 changes: 46 additions & 0 deletions skills/learn-status/skill.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions skills/learn/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <project-path>`)

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 <search terms>`)

1. Run FTS5 search against the learning DB:
Expand Down
18 changes: 8 additions & 10 deletions src/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,7 +34,6 @@ class RouterResult:
"plan": "sonnet",
"run": "sonnet",
"fleet": "sonnet",
"review": "sonnet",
"polish": "sonnet",
"handoff": "sonnet",
"experiment": "sonnet",
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -416,6 +413,8 @@ def _tier2_keyword_heuristic(prompt: str) -> RouterResult | None:
"fleet",
"campaign",
"learn",
"learn-status",
"forget",
"handoff",
"experiment",
"loop",
Expand All @@ -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}"
Expand Down
28 changes: 15 additions & 13 deletions tests/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Loading