From 53aa05a9809d2b03d92f4c4ef5c3d43b9fdcb728 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Wed, 25 Feb 2026 01:13:47 +0000 Subject: [PATCH 1/2] Skill engine: weighted scoring, negative patterns, context guards, max 3 cap Replace binary keyword matching with confidence-scored suggestions. Phrases carry weights (0.0-1.0), negative patterns suppress false positives, and context guards require confirming words for low-confidence matches. Results ranked by score and capped at 3 per prompt. --- .devcontainer/CHANGELOG.md | 7 + .../plugins/skill-engine/README.md | 22 +- .../skill-engine/scripts/skill-suggester.py | 747 +++++++++++------- 3 files changed, 483 insertions(+), 293 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 032f14d..39e5f87 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -19,6 +19,13 @@ ### Changed +#### Skill Engine: Auto-Suggestion +- **Weighted scoring** — Skill suggestion phrases now carry confidence weights (0.0–1.0) instead of binary match/no-match. Specific phrases like "build a fastapi app" score 1.0; ambiguous phrases like "start building" score 0.2 +- **Negative patterns** — Skills can define substrings that instantly disqualify them. Prevents `fastapi` from triggering when discussing `pydantic-ai`, and `docker` from triggering for `docker-py` prompts +- **Context guards** — Low-confidence matches (score < 0.6) require a confirming context word elsewhere in the prompt. "health check" only suggests `docker` if "docker", "container", or "compose" also appears +- **Ranked results, capped at 3** — Suggestions are sorted by score (then priority tier), and only the top 3 are returned. Eliminates 6+ skill suggestion floods +- **Priority tiers** — Explicit commands (priority 10) outrank technology skills (7), which outrank patterns (5) and generic skills (3) when scores tie + #### Claude Code Installation - **Claude Code now installs as a native binary** — uses Anthropic's official installer (`https://claude.ai/install.sh`) via new `./features/claude-code-native` feature, replacing the npm-based `ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5` - **In-session auto-updater now works without root** — native binary at `~/.local/bin/claude` is owned by the container user, so `claude update` succeeds without permission issues diff --git a/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md b/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md index a142e0c..6af6c8d 100644 --- a/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md +++ b/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/README.md @@ -39,11 +39,14 @@ Two capabilities: ### Auto-Suggestion -The `skill-suggester.py` hook matches user prompts against keyword maps defined in each skill. When a match is found, it injects a suggestion via `additionalContext` so Claude knows a relevant skill is available. +The `skill-suggester.py` hook scores user prompts against keyword maps for each skill using weighted matching. Suggestions are ranked by confidence and capped at **3 skills maximum** per prompt. -Keywords are defined per skill as: -- **Phrases** — Multi-word patterns (e.g., "docker compose", "REST API") -- **Terms** — Single keywords (e.g., "FastAPI", "pytest") +Each skill defines: +- **Phrases** — `(substring, weight)` tuples. Weight 0.0–1.0 reflects specificity (e.g., `("build a fastapi app", 1.0)` vs `("pydantic model", 0.3)`) +- **Terms** — Whole-word regex patterns, all scored at 0.6 +- **Negative patterns** — Substrings that instantly disqualify a skill (e.g., `"pydanticai"` suppresses `fastapi`) +- **Context guards** — Required co-occurring words for low-confidence matches. When the best score is below 0.6, at least one guard word must appear in the prompt or the match is dropped +- **Priority** — Integer tie-breaker (10 = commands, 7 = tech, 5 = patterns, 3 = generic) ## How It Works @@ -56,9 +59,12 @@ User submits a prompt | +-> skill-suggester.py | - +-> Scan prompt against all skill keyword maps - +-> Match found? -> Inject skill suggestion as additionalContext - +-> No match? -> Silent (no output) + +-> Check negative patterns (instant disqualify) + +-> Score phrases (best weight) and terms (0.6) + +-> Enforce context guards on low-confidence matches + +-> Rank by score desc, priority desc + +-> Return top 3 as additionalContext + +-> No matches above threshold? -> Silent (no output) ``` ### Skill Structure @@ -127,7 +133,7 @@ skill-engine/ +-- hooks/ | +-- hooks.json # UserPromptSubmit hook registration +-- scripts/ -| +-- skill-suggester.py # Keyword-based skill auto-suggestion +| +-- skill-suggester.py # Weighted scoring skill auto-suggestion +-- skills/ | +-- api-design/ # 22 skill directories | +-- ast-grep-patterns/ diff --git a/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py b/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py index 7363c61..4e0ecd3 100644 --- a/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py +++ b/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py @@ -5,415 +5,544 @@ - UserPromptSubmit: {"prompt": "..."} -> {"additionalContext": "..."} - SubagentStart: {"subagent_type": "Plan", "prompt": "..."} -> {"additionalContext": "..."} -Matches user prompts against skill keyword maps (phrases + terms) and suggests -relevant skills. Outputs nothing when no skills match. +Uses weighted scoring with negative patterns and context guards to suggest +the most relevant skills. Returns at most MAX_SKILLS suggestions, ranked +by confidence score. """ import json import re import sys -SKILLS = { +# Maximum number of skills to suggest per prompt. +MAX_SKILLS = 3 + +# Minimum score for a match to appear in final results. Set to 0 so that +# even low-weight phrases can survive if they pass context guard checks. +# The context guard + MAX_SKILLS cap handle quality control instead. +MIN_SCORE = 0.0 + +# Fixed score assigned to whole-word term matches (regex \b...\b). +TERM_WEIGHT = 0.6 + +# Threshold below which context guards are enforced. Matches scoring below +# this value must have at least one context guard word present in the prompt, +# otherwise the match is discarded as low-confidence. +CONTEXT_GUARD_THRESHOLD = 0.6 + +# --------------------------------------------------------------------------- +# Skill definitions +# +# Each skill has: +# phrases — list of (substring, weight) tuples. Weight 0.0-1.0 +# reflects how confidently the phrase indicates the skill. +# terms — list of whole-word regex terms (case-insensitive). +# All term matches receive TERM_WEIGHT. +# negative — (optional) list of substrings that instantly disqualify +# the skill, even if phrases/terms matched. +# context_guards — (optional) list of substrings. When the best match +# score is below CONTEXT_GUARD_THRESHOLD, at least one +# guard must be present in the prompt or the match is +# dropped. +# priority — integer tie-breaker. Higher = preferred when scores +# are equal. 10 = explicit commands, 7 = technology, +# 5 = practice/pattern, 3 = meta/generic. +# --------------------------------------------------------------------------- + +SKILLS: dict[str, dict] = { + # ------------------------------------------------------------------ + # Technology skills (priority 7) + # ------------------------------------------------------------------ "fastapi": { "phrases": [ - "build a fastapi app", - "rest api with fastapi", - "fastapi", - "fast api", - "add sse streaming", - "dependency injection in fastapi", - "define pydantic models", - "stream llm responses", - "add middleware to fastapi", - "pydantic model", - ], - "terms": ["fastapi", "pydantic", "uvicorn", "starlette", "sse-starlette"], + ("build a fastapi app", 1.0), + ("rest api with fastapi", 1.0), + ("fastapi", 0.9), + ("fast api", 0.9), + ("add sse streaming", 0.5), + ("dependency injection in fastapi", 1.0), + ("define pydantic models", 0.4), + ("stream llm responses", 0.3), + ("add middleware to fastapi", 1.0), + ("pydantic model", 0.3), + ], + "terms": ["fastapi", "uvicorn", "starlette", "sse-starlette"], + "negative": ["pydanticai", "pydantic-ai", "pydantic ai"], + "context_guards": [ + "fastapi", "fast api", "api", "endpoint", "route", + "uvicorn", "rest", "server", "web", + ], + "priority": 7, }, "sqlite": { "phrases": [ - "sqlite", - "set up a sqlite database", - "wal mode", - "fts5", - "full-text search", - "better-sqlite3", - "cloudflare d1", - "store json in sqlite", - "write ctes", - "window functions", + ("sqlite", 0.9), + ("set up a sqlite database", 1.0), + ("wal mode", 0.9), + ("fts5", 0.9), + ("full-text search", 0.3), + ("better-sqlite3", 1.0), + ("cloudflare d1", 0.8), + ("store json in sqlite", 1.0), + ("write ctes", 0.3), + ("window functions", 0.3), ], "terms": ["aiosqlite", "better-sqlite3"], + "negative": ["elasticsearch", "algolia", "meilisearch", "postgres", "mysql"], + "context_guards": [ + "sqlite", "sql", "database", "db", "query", "table", + ], + "priority": 7, }, "claude-code-headless": { "phrases": [ - "headless mode", - "claude -p", - "stream-json", - "claude code headless", - "run claude in ci", - "claude in pipeline", - "parse stream-json output", - "track costs programmatically", - "permissions for scripts", + ("headless mode", 0.8), + ("claude -p", 0.9), + ("stream-json", 0.9), + ("claude code headless", 1.0), + ("run claude in ci", 1.0), + ("claude in pipeline", 0.9), + ("parse stream-json output", 1.0), + ("track costs programmatically", 0.7), + ("permissions for scripts", 0.3), ], "terms": ["--output-format stream-json", "--permission-mode"], + "context_guards": ["claude", "headless", "ci", "pipeline", "script"], + "priority": 7, }, "claude-agent-sdk": { "phrases": [ - "agent sdk", - "claude agent sdk", - "build an agent with the claude agent sdk", - "canusetool", - "sdk permissions", - "create mcp tools", - "define subagents", - "configure sdk hooks", - "stream sdk messages", + ("agent sdk", 0.9), + ("claude agent sdk", 1.0), + ("build an agent with the claude agent sdk", 1.0), + ("canusetool", 1.0), + ("sdk permissions", 0.7), + ("create mcp tools", 0.7), + ("define subagents", 0.8), + ("configure sdk hooks", 0.8), + ("stream sdk messages", 0.8), ], "terms": ["claude-agent-sdk", "claude_agent_sdk", "createSdkMcpServer"], + "priority": 7, }, "pydantic-ai": { "phrases": [ - "pydantic ai", - "pydantic-ai", - "pydanticai", - "build a pydanticai agent", - "add tools to an agent", - "stream responses with pydanticai", - "test a pydanticai agent", - "connect pydanticai to svelte", - "configure model fallbacks", + ("pydantic ai", 0.9), + ("pydantic-ai", 1.0), + ("pydanticai", 1.0), + ("build a pydanticai agent", 1.0), + ("add tools to an agent", 0.5), + ("stream responses with pydanticai", 1.0), + ("test a pydanticai agent", 1.0), + ("connect pydanticai to svelte", 1.0), + ("configure model fallbacks", 0.5), ], "terms": ["pydanticai", "RunContext", "VercelAIAdapter", "FallbackModel"], - }, - "testing": { - "phrases": [ - "write tests", - "write a test", - "add tests", - "add a test", - "pytest fixture", - "vitest config", - "testing library", - "mock dependencies", - "test endpoint", - "test component", - "test sse streaming", - "unit test", - "integration test", + "context_guards": [ + "pydantic", "agent", "ai", "model", "tool", "llm", ], - "terms": ["pytest", "vitest", "pytest-anyio", "httpx AsyncClient"], + "priority": 7, }, "docker-py": { "phrases": [ - "docker-py", - "docker py", - "docker sdk", - "docker engine api", - "docker from python", - "docker api", - "manage docker containers from python", - "create containers programmatically", - "stream container logs", - "monitor container health from python", + ("docker-py", 1.0), + ("docker py", 1.0), + ("docker sdk", 0.9), + ("docker engine api", 0.9), + ("docker from python", 1.0), + ("docker api", 0.7), + ("manage docker containers from python", 1.0), + ("create containers programmatically", 0.8), + ("stream container logs", 0.7), + ("monitor container health from python", 1.0), ], "terms": ["aiodocker", "DockerClient"], + "priority": 7, }, "svelte5": { "phrases": [ - "svelte component", - "sveltekit", - "svelte kit", - "svelte rune", - "svelte 5", - "svelte5", - "migrate from svelte 4", - "manage state with $state", - "drag and drop to svelte", + ("svelte component", 0.8), + ("sveltekit", 0.9), + ("svelte kit", 0.9), + ("svelte rune", 1.0), + ("svelte 5", 1.0), + ("svelte5", 1.0), + ("migrate from svelte 4", 1.0), + ("manage state with $state", 1.0), + ("drag and drop to svelte", 0.9), ], "terms": ["sveltekit", "svelte", "svelte-dnd-action", "@ai-sdk/svelte"], + "priority": 7, }, "docker": { "phrases": [ - "dockerfile", - "docker compose", - "docker-compose", - "compose file", - "multi-stage build", - "health check", - "healthcheck", - "docker compose watch", - "optimize docker image", + ("dockerfile", 0.9), + ("docker compose", 0.9), + ("docker-compose", 0.9), + ("compose file", 0.8), + ("multi-stage build", 0.8), + ("health check", 0.3), + ("healthcheck", 0.3), + ("docker compose watch", 1.0), + ("optimize docker image", 1.0), ], "terms": ["dockerfile", "compose.yaml", "BuildKit"], + "negative": ["docker-py", "docker py", "docker sdk", "docker from python", + "aiodocker", "DockerClient"], + "context_guards": [ + "docker", "container", "compose", "image", "dockerfile", + ], + "priority": 7, + }, + + # ------------------------------------------------------------------ + # Practice / pattern skills (priority 5) + # ------------------------------------------------------------------ + "testing": { + "phrases": [ + ("write tests", 0.4), + ("write a test", 0.4), + ("add tests", 0.4), + ("add a test", 0.4), + ("pytest fixture", 0.9), + ("vitest config", 0.9), + ("testing library", 0.6), + ("mock dependencies", 0.7), + ("test endpoint", 0.5), + ("test component", 0.5), + ("test sse streaming", 0.8), + ("unit test", 0.5), + ("integration test", 0.6), + ], + "terms": ["pytest", "vitest", "pytest-anyio", "httpx AsyncClient"], + "priority": 5, }, "skill-building": { "phrases": [ - "build a skill", - "create a skill", - "write a skill", - "skill.md", - "skill instructions", - "skill authoring", - "design a skill", - "improve a skill description", - "optimize skill content", + ("build a skill", 0.9), + ("create a skill", 0.9), + ("write a skill", 0.9), + ("skill.md", 1.0), + ("skill instructions", 0.8), + ("skill authoring", 1.0), + ("design a skill", 0.8), + ("improve a skill description", 0.9), + ("optimize skill content", 0.8), ], "terms": [], + "priority": 5, }, "debugging": { "phrases": [ - "debug logs", - "check logs", - "check container logs", - "find error", - "investigate failure", - "what went wrong", - "why did this crash", - "diagnose the issue", - "look at the logs", - "read the logs", - "read docker logs", - "analyze error", + ("debug logs", 0.7), + ("check logs", 0.4), + ("check container logs", 0.9), + ("find error", 0.3), + ("investigate failure", 0.7), + ("what went wrong", 0.3), + ("why did this crash", 0.8), + ("diagnose the issue", 0.8), + ("look at the logs", 0.4), + ("read the logs", 0.4), + ("read docker logs", 0.9), + ("analyze error", 0.5), ], "terms": ["diagnose", "troubleshoot", "OOMKilled", "ECONNREFUSED"], + "context_guards": [ + "log", "crash", "fail", "bug", "container", "stack", + "trace", "exception", "runtime", "service", "process", + ], + "priority": 5, }, "refactoring-patterns": { "phrases": [ - "refactor this", - "clean up code", - "clean up this function", - "extract a method", - "fix code smells", - "reduce code duplication", - "simplify this class", - "break up this large function", - "remove dead code", + ("refactor this", 0.4), + ("clean up code", 0.4), + ("clean up this function", 0.6), + ("extract a method", 0.8), + ("fix code smells", 0.9), + ("reduce code duplication", 0.8), + ("simplify this class", 0.6), + ("break up this large function", 0.8), + ("remove dead code", 0.7), ], "terms": ["refactor", "refactoring", "code smell", "feature envy", "god class"], + "priority": 5, }, "security-checklist": { "phrases": [ - "security review", - "security issues", - "security vulnerabilities", - "check for vulnerabilities", - "scan for secrets", - "audit security", - "review for injection", - "owasp compliance", - "hardcoded credentials", + ("security review", 0.8), + ("security issues", 0.5), + ("security vulnerabilities", 0.8), + ("check for vulnerabilities", 0.7), + ("scan for secrets", 0.9), + ("audit security", 0.9), + ("review for injection", 0.9), + ("owasp compliance", 1.0), + ("hardcoded credentials", 0.8), ], "terms": ["owasp", "injection", "xss", "cve", "trivy", "gitleaks"], + "priority": 5, }, "git-forensics": { "phrases": [ - "git history", - "who changed this", - "when did this break", - "git blame", - "bisect a regression", - "recover a lost commit", - "search git history", - "find when code was removed", - "trace the history", - "use git reflog", + ("git history", 0.7), + ("who changed this", 0.7), + ("when did this break", 0.7), + ("git blame", 0.9), + ("bisect a regression", 1.0), + ("recover a lost commit", 0.9), + ("search git history", 0.9), + ("find when code was removed", 0.8), + ("trace the history", 0.5), + ("use git reflog", 1.0), ], "terms": ["bisect", "blame", "pickaxe", "reflog", "git log -S"], + "context_guards": ["git", "commit", "branch", "history", "repo"], + "priority": 5, }, "specification-writing": { "phrases": [ - "write a spec", - "write requirements", - "define requirements", - "acceptance criteria", - "user stories", - "use ears format", - "given/when/then", - "write given/when/then scenarios", - "structure requirements", + ("write a spec", 0.8), + ("write requirements", 0.7), + ("define requirements", 0.7), + ("acceptance criteria", 0.6), + ("user stories", 0.6), + ("use ears format", 1.0), + ("given/when/then", 0.8), + ("write given/when/then scenarios", 1.0), + ("structure requirements", 0.7), ], "terms": ["specification", "ears", "gherkin", "given when then"], + "priority": 5, }, "performance-profiling": { "phrases": [ - "profile this code", - "profile performance", - "find bottleneck", - "find the bottleneck", - "benchmark this", - "create a flamegraph", - "find memory leaks", - "why is this slow", - "measure execution time", - "reduce latency", + ("profile this code", 0.9), + ("profile performance", 0.9), + ("find bottleneck", 0.7), + ("find the bottleneck", 0.7), + ("benchmark this", 0.8), + ("create a flamegraph", 1.0), + ("find memory leaks", 0.8), + ("why is this slow", 0.4), + ("measure execution time", 0.7), + ("reduce latency", 0.4), ], "terms": ["cProfile", "py-spy", "scalene", "flamegraph", "hyperfine"], - }, - "api-design": { - "phrases": [ - "api design", - "rest api design", - "design an api", - "design rest endpoints", - "api versioning", - "pagination strategy", - "design error responses", - "rate limiting", - "openapi documentation", + "context_guards": [ + "profile", "profiler", "benchmark", "performance", "slow", + "latency", "bottleneck", "memory", ], - "terms": ["openapi", "swagger", "rfc7807", "rfc 7807"], + "priority": 5, }, "ast-grep-patterns": { "phrases": [ - "ast-grep", - "ast grep", - "structural search", - "syntax-aware search", - "find code patterns", - "search with ast-grep", - "use tree-sitter", + ("ast-grep", 1.0), + ("ast grep", 1.0), + ("structural search", 0.8), + ("syntax-aware search", 0.9), + ("find code patterns", 0.5), + ("search with ast-grep", 1.0), + ("use tree-sitter", 0.8), ], "terms": ["sg run", "ast-grep", "tree-sitter"], + "context_guards": ["ast", "syntax", "pattern", "structural", "tree-sitter"], + "priority": 5, }, "dependency-management": { "phrases": [ - "check dependencies", - "audit dependencies", - "outdated packages", - "dependency health", - "license check", - "unused dependencies", - "vulnerability scan", - "find unused dependencies", + ("check dependencies", 0.5), + ("audit dependencies", 0.8), + ("outdated packages", 0.8), + ("dependency health", 0.8), + ("license check", 0.6), + ("unused dependencies", 0.8), + ("vulnerability scan", 0.7), + ("find unused dependencies", 0.9), ], "terms": ["pip-audit", "npm audit", "cargo audit", "govulncheck"], + "context_guards": [ + "dependency", "dependencies", "package", "packages", + "npm", "pip", "cargo", "audit", + ], + "priority": 5, + }, + "team": { + "phrases": [ + ("spawn a team", 1.0), + ("create a team", 0.8), + ("team of agents", 0.9), + ("use a swarm", 0.8), + ("work in parallel", 0.4), + ("coordinate multiple agents", 0.9), + ("split this across agents", 0.9), + ("team up", 0.5), + ], + "terms": ["TeamCreate", "SendMessage"], + # Note: "parallel", "swarm", "coordinate", "team" omitted — overlap phrases + "context_guards": ["agent", "agents", "teammate", "teammates"], + "priority": 5, + }, + + # ------------------------------------------------------------------ + # Meta / generic skills (priority 3) + # ------------------------------------------------------------------ + "api-design": { + "phrases": [ + ("api design", 0.6), + ("rest api design", 0.8), + ("design an api", 0.7), + ("design rest endpoints", 0.9), + ("api versioning", 0.8), + ("pagination strategy", 0.7), + ("design error responses", 0.8), + ("rate limiting", 0.6), + ("openapi documentation", 0.9), + ], + "terms": ["openapi", "swagger", "rfc7807", "rfc 7807"], + "priority": 3, }, "documentation-patterns": { "phrases": [ - "write a readme", - "write documentation", - "add docstrings", - "add jsdoc", - "document the api", - "create architecture docs", - "update the docs", + ("write a readme", 0.6), + ("write documentation", 0.4), + ("add docstrings", 0.8), + ("add jsdoc", 0.9), + ("document the api", 0.7), + ("create architecture docs", 0.8), + ("update the docs", 0.3), ], "terms": ["docstring", "jsdoc", "tsdoc", "rustdoc", "Sphinx"], + # Note: "docs" omitted — it overlaps with the phrase "update the docs" + "context_guards": [ + "documentation", "docstring", "readme", "jsdoc", "api doc", + "rustdoc", "tsdoc", "sphinx", + ], + "priority": 3, }, "migration-patterns": { "phrases": [ - "migrate from", - "upgrade to", - "version upgrade", - "framework migration", - "bump python", - "upgrade pydantic", - "migrate express", - "modernize the codebase", - "commonjs to esm", + ("migrate from", 0.4), + ("upgrade to", 0.3), + ("version upgrade", 0.4), + ("framework migration", 0.8), + ("bump python", 0.6), + ("upgrade pydantic", 0.7), + ("migrate express", 0.8), + ("modernize the codebase", 0.7), + ("commonjs to esm", 1.0), ], "terms": ["migrate", "migration"], + # Note: "upgrade", "version", "modernize" omitted — overlap with phrases + "context_guards": [ + "framework", "breaking", "compatibility", "deprecated", + "legacy", "esm", "commonjs", + ], + "priority": 3, }, + + # ------------------------------------------------------------------ + # Spec-workflow command skills (priority 10) + # ------------------------------------------------------------------ "spec-build": { "phrases": [ - "implement the spec", - "build from spec", - "start building", - "spec-build", - "implement this feature", - "build what the spec describes", - "run spec-build", + ("implement the spec", 0.9), + ("build from spec", 0.9), + ("building from spec", 0.9), + ("building from the spec", 0.9), + ("start building", 0.2), + ("spec-build", 1.0), + ("implement this feature", 0.2), + ("build what the spec describes", 1.0), + ("run spec-build", 1.0), ], "terms": ["spec-build"], + "context_guards": ["spec", "specification", ".specs"], + "priority": 10, }, "spec-review": { "phrases": [ - "review the spec", - "check spec adherence", - "verify implementation", - "spec-review", - "does code match spec", - "audit implementation", - "run spec-review", - "regression check", + ("review the spec", 0.8), + ("check spec adherence", 0.9), + ("verify implementation", 0.3), + ("spec-review", 1.0), + ("does code match spec", 0.9), + ("audit implementation", 0.4), + ("run spec-review", 1.0), + ("regression check", 0.3), ], "terms": ["spec-review"], + "context_guards": ["spec", "specification", ".specs"], + "priority": 10, }, "spec-check": { "phrases": [ - "check spec health", - "audit specs", - "which specs are stale", - "find missing specs", - "review spec quality", - "run spec-check", - "are my specs up to date", + ("check spec health", 0.9), + ("audit specs", 0.9), + ("which specs are stale", 1.0), + ("find missing specs", 0.9), + ("review spec quality", 0.9), + ("run spec-check", 1.0), + ("are my specs up to date", 0.9), ], "terms": ["spec-check"], + "priority": 10, }, "spec-init": { "phrases": [ - "initialize specs", - "specs directory", - "set up specs", - "bootstrap specs", - "start using specs", - "create spec directory", - "init specs", - "set up .specs", + ("initialize specs", 0.9), + ("specs directory", 0.7), + ("set up specs", 0.8), + ("bootstrap specs", 0.9), + ("start using specs", 0.8), + ("create spec directory", 0.9), + ("init specs", 0.9), + ("set up .specs", 1.0), ], "terms": ["spec-init"], + "priority": 10, }, "spec-new": { "phrases": [ - "create a spec", - "new spec", - "new feature spec", - "write a spec for", - "spec this feature", - "start a new spec", - "plan a feature", - "add a spec", + ("create a spec", 0.8), + ("new spec", 0.8), + ("new feature spec", 0.9), + ("write a spec for", 0.9), + ("spec this feature", 0.9), + ("start a new spec", 0.9), + ("plan a feature", 0.2), + ("add a spec", 0.8), ], "terms": ["spec-new"], + "context_guards": ["spec", "specification", ".specs"], + "priority": 10, }, "spec-refine": { "phrases": [ - "refine the spec", - "review spec assumptions", - "validate spec decisions", - "approve the spec", - "walk me through the spec", - "check spec for assumptions", - "iterate on the spec", + ("refine the spec", 0.9), + ("review spec assumptions", 0.9), + ("validate spec decisions", 0.9), + ("approve the spec", 0.8), + ("walk me through the spec", 0.8), + ("check spec for assumptions", 0.9), + ("iterate on the spec", 0.8), ], "terms": ["spec-refine"], + "priority": 10, }, "spec-update": { "phrases": [ - "update the spec", - "mark spec as implemented", - "as-built update", - "finish the spec", - "close the spec", - "update spec status", - "sync spec with code", + ("update the spec", 0.8), + ("mark spec as implemented", 0.9), + ("as-built update", 0.9), + ("finish the spec", 0.7), + ("close the spec", 0.7), + ("update spec status", 0.8), + ("sync spec with code", 0.8), ], "terms": ["spec-update"], - }, - "team": { - "phrases": [ - "spawn a team", - "create a team", - "team of agents", - "use a swarm", - "work in parallel", - "coordinate multiple agents", - "split this across agents", - "team up", - ], - "terms": ["TeamCreate", "SendMessage"], + "priority": 10, }, "worktree": { "phrases": [ @@ -432,7 +561,10 @@ }, } +# --------------------------------------------------------------------------- # Pre-compile term patterns for whole-word matching +# --------------------------------------------------------------------------- + _TERM_PATTERNS: dict[str, re.Pattern[str]] = {} for _skill, _cfg in SKILLS.items(): for _term in _cfg["terms"]: @@ -442,23 +574,68 @@ ) +# --------------------------------------------------------------------------- +# Scoring engine +# --------------------------------------------------------------------------- + + +def _score_skill(cfg: dict, prompt: str, lowered: str) -> float: + """Return a confidence score for how well *prompt* matches *cfg*. + + Returns 0.0 when the skill should not be suggested. + """ + + # 1. Negative patterns — instant disqualification + for neg in cfg.get("negative", []): + if neg in lowered: + return 0.0 + + # 2. Phrase scoring — take the highest matching weight + best_phrase: float = 0.0 + for phrase, weight in cfg["phrases"]: + if phrase in lowered: + if weight > best_phrase: + best_phrase = weight + + # 3. Term scoring — fixed weight for any whole-word match + term_score: float = 0.0 + for term in cfg["terms"]: + if _TERM_PATTERNS[term].search(prompt): + term_score = TERM_WEIGHT + break + + base = max(best_phrase, term_score) + if base < MIN_SCORE: + return 0.0 + + # 4. Context guard — low-confidence matches need confirmation + if base < CONTEXT_GUARD_THRESHOLD: + guards = cfg.get("context_guards") + if guards and not any(g in lowered for g in guards): + return 0.0 + + return base + + def match_skills(prompt: str) -> list[str]: - """Return sorted list of skill names matching the prompt.""" + """Return up to MAX_SKILLS skill names, ranked by confidence score.""" lowered = prompt.lower() - matched: list[str] = [] + scored: list[tuple[str, float, int]] = [] for skill, cfg in SKILLS.items(): - # Check phrases (substring match on lowercased prompt) - if any(phrase in lowered for phrase in cfg["phrases"]): - matched.append(skill) - continue + score = _score_skill(cfg, prompt, lowered) + if score > 0.0: + scored.append((skill, score, cfg.get("priority", 0))) + + # Sort by score descending, then priority descending for ties + scored.sort(key=lambda x: (x[1], x[2]), reverse=True) + + return [name for name, _, _ in scored[:MAX_SKILLS]] - # Check terms (whole-word regex match) - if any(_TERM_PATTERNS[term].search(prompt) for term in cfg["terms"]): - matched.append(skill) - matched.sort() - return matched +# --------------------------------------------------------------------------- +# Hook entry point +# --------------------------------------------------------------------------- def main() -> None: From 1a1ef13fbea6577ac016a3b18bd798fc934b02f4 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Wed, 25 Feb 2026 05:38:28 +0000 Subject: [PATCH 2/2] Fix CodeRabbit review: lowercase negative pattern, add missing docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "DockerClient" → "dockerclient" in docker skill's negative list (mixed-case never matched against lowered input) - Add docstring to main() to meet 80% coverage threshold --- .../plugins/skill-engine/scripts/skill-suggester.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py b/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py index 4e0ecd3..2b778fc 100644 --- a/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py +++ b/.devcontainer/plugins/devs-marketplace/plugins/skill-engine/scripts/skill-suggester.py @@ -188,7 +188,7 @@ ], "terms": ["dockerfile", "compose.yaml", "BuildKit"], "negative": ["docker-py", "docker py", "docker sdk", "docker from python", - "aiodocker", "DockerClient"], + "aiodocker", "dockerclient"], "context_guards": [ "docker", "container", "compose", "image", "dockerfile", ], @@ -639,6 +639,7 @@ def match_skills(prompt: str) -> list[str]: def main() -> None: + """Read a hook event from stdin, score skills, and print suggestions to stdout.""" raw = sys.stdin.read().strip() if not raw: return