Skip to content

feat(skills): lazy index-first loading + use_skill tool#89

Closed
shuff57 wants to merge 5 commits into
Doorman11991:masterfrom
shuff57:feat/lazy-skills
Closed

feat(skills): lazy index-first loading + use_skill tool#89
shuff57 wants to merge 5 commits into
Doorman11991:masterfrom
shuff57:feat/lazy-skills

Conversation

@shuff57

@shuff57 shuff57 commented Jun 7, 2026

Copy link
Copy Markdown

What

SkillManager currently eager-reads every skill body at startup and injects full bodies of matched skills into context (4k-char cap). This PR makes skills index-first:

  1. Startup scans frontmatter only (2KB / 50-line window per file, full-read fallback) — bodies never load until needed. A 38-skill user library costs an index, not a context window.
  2. Compact index injection: the model sees one line per skill (name + description, ~8 tokens each) instead of full bodies. New optional frontmatter: description, tags, related — all backward compatible, existing skills untouched.
  3. use_skill tool: the model pulls a skill body on demand; the result includes related: skills as names/descriptions (not bodies). Unknown name returns an explicit error listing valid names — small models recover well from that.
  4. Auto-match unchanged in behavior: trigger: match/auto still injects matched bodies (lazily read, cached after first read, same 4k aggregate cap).

Public API preserved

get()/list()/getAutoSkills()/formatForPrompt()/add()/remove() behave identically from callers'' perspective — get() lazy-fills content. All pre-existing skills tests pass unmodified.

Notable fix included

use_skill is registered in BOTH tool routers'' category lists (src/compiled/tool_router.js, src/tools/two_stage_router.js) — without that, routed mode filtered the tool out and the model never saw it. Found via live E2E, pinned in review notes.

Tests

13 new (test/skill_lazy.test.js): index-only startup, lazy get + cache, getIndex fields, formatter output, backward compat without new fields. Full suite green.

Field-verified: model called use_skill for a manual-trigger skill and followed its instructions; keyword auto-match drove generated code to follow the skill body pattern.

Stacked on #88 — new commits here: 797d304, 6c938e4.

🤖 Generated with Claude Code

shuff57 and others added 5 commits June 5, 2026 12:33
Skills following the Claude Code layout (<skill-dir>/<name>/SKILL.md)
or written as plain .md without YAML frontmatter were silently skipped
in the standard skill dirs (.smallcode/skills, ~/.smallcode/skills,
~/.config/smallcode/skills). Both shapes now load; README-style files
(README/CHANGELOG/LICENSE/CONTRIBUTING) are filtered by name.

Fixes Doorman11991#81

Constraint: no warning channel exists in SkillManager, so silent skips had no user-visible signal
Rejected: warn-on-skip only | users following Claude Code conventions expect these layouts to work
Confidence: high
Scope-risk: narrow
Not-tested: fullscreen TUI /skill list rendering (logic shared with classic mode)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Create-mode evolver: deterministic friction extraction from saved
traces (repeated near-duplicate prompts, consecutive tool-retry
loops), LLM judgment routed to the strong tier, and ONE quarantined
skill draft per run written to .smallcode/skills/drafts/.

Drafts never auto-load; /evolve promote <name> moves them live.
Validation gates every write (name format, no frontmatter injection,
trigger rules); name collisions across live+draft+global dirs abort;
every create appends to .smallcode/evolver-audit.jsonl. The per-run
cap is structural — EvolverRun raises on a second create.

Constraint: small models produce noisy judgments, so all fuzzy output passes validate-or-abort before any write
Rejected: plugin delivery | needs TraceRecorder + SkillManager internals unreachable from plugin dirs under binary installs
Confidence: high
Scope-risk: narrow
Directive: keep mechanics LLM-free — judgment stays in the command handler so mechanics remain unit-testable
Not-tested: strong-tier routing with a separately configured SMALLCODE_MODEL_STRONG endpoint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Field regression: rephrased prompts with filler drift (another/please/new) failed to cluster because stopwords diluted Jaccard below threshold. Real prompts from a live session pinned as a test.
SkillManager now reads only frontmatter on startup (_index Map) and
loads bodies on demand via _loadBody(), cached in skills Map. This
cuts per-turn skill injection from ~60k chars (all bodies) to ~240
chars (compact index) for a typical 30-skill install.

New surface: getIndex() flat list, formatSkillIndex/formatSkillResult
in skill_index_formatter.js, use_skill tool (executor + tools.js).
getSkillContext() injects the index always; auto-matched bodies append
after, subject to the existing 4000-char cap.

Public API (get/list/getAutoSkills/formatForPrompt/add/remove/
promoteDraft/listDrafts) is unchanged — all 335 tests pass.

Rejected: inject all bodies always | O(skills) context cost per turn
Constraint: existing tests must pass unmodified
Confidence: high
Scope-risk: moderate
Not-tested: live use_skill call by real model (requires interactive session)
use_skill was defined in TOOLS but absent from both routers' category whitelists, so the model never saw it in routed mode. The skill index is injected every turn, so the tool rides along in every tool-bearing category (~80 tokens).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant