feat(skills): SkillRegistry, ClaudeAdapter, GeminiAdapter, skills:sync & skills:list commands#113
feat(skills): SkillRegistry, ClaudeAdapter, GeminiAdapter, skills:sync & skills:list commands#113bedus-creation wants to merge 10 commits into
Conversation
Implements tasks #139, #140, #141 — the fastapi-startkit[skills] core: **Task #139 — SkillRegistry + provider discovery** - `skills/registry.py`: SkillRegistry scans only providers currently registered in the Application container, discovering skills/ dirs co-located with each provider.py. SKILL.md YAML front-matter (name, description) is parsed into Skill dataclass objects. - `skills/provider.py`: SkillsServiceProvider registers the registry under 'skills.registry' in the container and boots the CLI commands. **Task #140 — Adapter layer (ClaudeAdapter + GeminiAdapter)** - `skills/adapters/base.py`: BaseAdapter ABC with render() and prune(). - `skills/adapters/claude.py`: ClaudeAdapter writes idempotent .claude/skills/<name>/SKILL.md files with YAML front-matter. --prune removes directories for skills no longer in the registry. - `skills/adapters/gemini.py`: GeminiAdapter splices a <!-- skills:start/end --> block into GEMINI.md without touching any user-authored content outside the markers. Idempotent. **Task #141 — Cleo commands** - `skills/commands/sync.py`: `skills:sync` — resolves SkillRegistry, selects adapter(s) via --target (claude|gemini|all, default all), calls render(), optionally prune(). - `skills/commands/list.py`: `skills:list` — shows provider, name, sync status (claude/gemini), and description for every discovered skill. **Tests**: 34 new pytest tests covering registry discovery, front-matter parsing, both adapters (idempotency, prune, marker preservation), and both commands (target routing, prune flag, status display). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code Review — Conditional Approval (pending fix in PR #114)PR #113 is well-structured and consistent with the existing provider/container patterns. All 34 tests pass. The architecture is correct and extensible. ✅ What looks good
🟡 One concern (minor, does not block this PR)
@staticmethod
def _skills_dir_for(provider) -> Path | None:
"""Return the ``skills/`` directory co-located with *provider*'s module file.
For a provider defined in ``mypackage/providers/my_provider.py``, this
returns ``mypackage/providers/skills/``. Skills placed anywhere else
(e.g. at the package root) will NOT be discovered.
"""VerdictApprove this PR on its own merits once PR #114 fixes its file placement bug. The contract is correct; the canonical SKILL.md files just need to move to the right location. |
…e.html
Replace per-provider skills/ directory scanning with a single central
.ai/deployments/core.html file as the source of truth for all skills.
Architecture change:
- SkillRegistry now reads from {base_path}/.ai/deployments/core.html
instead of scanning each registered provider for a skills/ dir
- core.html uses <section data-skill="..." data-description="...">
blocks — plain HTML, Jinja2-compatible for future template rendering
- _CoreHtmlParser (html.parser, zero extra deps) extracts section
blocks, preserving body content verbatim
- SkillsServiceProvider.boot() publishes stubs/core.html to
.ai/deployments/core.html via `artisan provider:publish --provider=skills`
- Adapters (ClaudeAdapter, GeminiAdapter) unchanged — they still deploy
Skill objects to .claude/skills/<name>/SKILL.md and GEMINI.md
Added: skills/stubs/core.html with three example skills (fastapi-routing,
orm-queries, console-commands) ready to publish to any project.
Tests: 43 passing (9 new registry tests added for HTML parser + path
behaviour; command tests updated to write core.html instead of mocking
provider module structure).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Redesigns the skills module to match the Laravel Boost convention and adds
a full per-topic rules system.
**Skills — .ai/fastapi-startkit/skill/{name}/SKILL.md**
- SkillRegistry now scans .ai/fastapi-startkit/skill/*/SKILL.md (individual
folders per skill, YAML frontmatter: name, description, license, metadata)
- Falls back to directory name when frontmatter has no 'name' key
- Published stub: .ai/fastapi-startkit/skill/fastapi-best-practices/SKILL.md
with a 19-section Quick Reference pointing to rules/ files
**Rules — rules/{name}.md**
- RulesRegistry scans rules/*.md (plain Markdown, optional frontmatter)
- Rule name = file stem (http-client.md → "http-client")
- ClaudeRulesAdapter → .claude/rules/<name>.md (idempotent, prune)
- GeminiRulesAdapter → GEMINI.md <!-- rules:start/end --> block
- rules:sync command (--target claude|gemini|all, --prune)
- rules:list command (shows name, Claude/Gemini status, path)
- Published stub: rules/http-client.md with Python/httpx best practices:
explicit timeout + connect_timeout, exponential backoff, raise_for_status(),
asyncio.gather() for concurrent requests, respx fakes in tests
**SkillsServiceProvider** now registers both skills.registry and rules.registry
and boots all four commands (skills:sync, skills:list, rules:sync, rules:list).
Tests: 48 passing (14 new for rules registry, commands, and adapters).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove duplicate `ClaudeAdapter`/`GeminiAdapter` and `ClaudeRulesAdapter`/`GeminiRulesAdapter` imports from `handle()` in both sync commands — these are only needed in `_resolve_adapters()`. Note: `_skills_dir_for()` referenced in the reviewer comment no longer exists — it was removed as part of the redesign from per-provider skills/ directories to the centralised .ai/fastapi-startkit/skill/*/SKILL.md layout. The SkillRegistry._load() static method now handles file discovery directly. ruff check src/ → All checks passed. pytest tests/skills/ → 48 passed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-review — APPROVED ✅Redesign confirmed correct. All 48 tests pass (34 skills + 14 new rules tests). F401 lint fix applied. Summary:
Minor (non-blocking): This PR is approved. Merge once PR #114 is fixed and rebased against the new layout. |
Architecture Note — Rules layout should nest inside skills (pre-merge change)Updating my approval to flag one design issue before merge: the Current vs. correctCurrent PR #113: Should match Laravel Boost: Why this matters before merge
Required changes to
|
…oost convention)
Rules now live co-located with the SKILL.md they document, matching the
Laravel Boost layout exactly:
.ai/fastapi-startkit/skill/<skill-name>/
SKILL.md
rules/
<rule-name>.md
Changes:
- RulesRegistry scans `.ai/fastapi-startkit/skill/*/rules/*.md` (was `rules/*.md`)
- Rule dataclass gains `skill_name` field (owning skill dir name)
- RulesRegistry.for_skill(name) helper for scoped lookups
- ClaudeRulesAdapter output: .claude/skills/<skill>/rules/<rule>.md
(mirrors source nesting; pending final confirmation from reviewer 361)
- GeminiRulesAdapter groups rules under their skill heading in the
<!-- rules:start/end --> block
- Prune lifecycle: ClaudeAdapter.prune() removes the whole skill dir
(shutil.rmtree) which automatically takes its nested rules/ with it;
ClaudeRulesAdapter.prune() handles stale individual rule files within
still-present skills
- Stubs: stubs/rules/http-client.md moved to
stubs/.ai/fastapi-startkit/skill/fastapi-best-practices/rules/http-client.md
- SkillsServiceProvider.publishes() updated to new nested paths
- tests/skills/test_commands.py updated throughout; added
test_prune_removes_skill_dir_removes_its_rules_too
ruff check src/ → All checks passed.
pytest tests/skills/ → 49 passed, 0 failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop `import pytest` from test_adapters.py, `from pathlib import Path` and the inline `Skill` import from test_commands.py, and `import textwrap` plus the unused `Skill` from test_registry.py's multi-import. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Code Review — PR #113 CHANGES REQUIRED Pytest passes (34 new tests, no regressions) — the implementation is solid. However Ruff blocks with 5 F401 unused import errors in test files:
All are auto-fixable: run Verdict: BLOCKED — fix unused imports, then ready to approve |
ClaudeRulesAdapter: output path corrected to .claude/rules/<skill>/<rule>.md so rules are actually read by Claude Code (the old .claude/skills/<skill>/rules/ path was silently ignored). prune() now independently scans .claude/rules/ instead of .claude/skills/. RulesListCommand: synced-status check updated to .claude/rules/ path. Tests: all path assertions updated; test_prune_removes_skill_dir_removes_its_rules_too replaced with test_prune_removes_stale_rule_skill_subdir which asserts that ClaudeRulesAdapter.prune() correctly targets .claude/rules/. Stubs: SKILL.md and http-client.md rewritten as terse actionable checklists (no filler commentary, minimal code examples only where they disambiguate). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ions only - Delete fastapi/, inertia-controllers/, laravel-best-practices/, orm-models/ (all replaced by the single verbatim fastapi-startkit skill on PR #113) - Revert fastapi_startkit/pyproject.toml to main (removes [skills] extra added in an earlier commit — not part of this PR's scope) - Strip filler from broadcasting/SKILL.md: remove "When to use" section, intro paragraph, and docstring comment in channel callback example - Remove placeholder `# ...do work` comment from console-commands/SKILL.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…actices + core.html Replace the placeholder fastapi-best-practices stub with the user-authored fastapi-startkit SKILL.md covering routing, controller shape, resource() shortcut, ORM model + controller usage, Pydantic request classes, JsonApiResource responses, and the action pattern. Delete skills/stubs/core.html (zero code references, leftover junk). Update SkillsServiceProvider.publishes() to reference the new skill only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop the extra skill/ level from the discovery path. Skills now live at .ai/fastapi-startkit/<skill-name>/SKILL.md (was .ai/fastapi-startkit/skill/<skill-name>/SKILL.md). - SKILLS_BASE_PATH updated in registry.py and rules/registry.py - Stub file moved from stubs/.ai/.../skill/fastapi-startkit/ to stubs/.ai/.../fastapi-startkit/ accordingly - SkillsServiceProvider.publishes() destination updated - Test path assertion and docstrings updated ruff + pytest: 49/49 green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Implements tasks #139, #140, and #141 — the core
fastapi-startkit[skills]feature.Task #139 — Skill Registry + Provider Discovery
skills/registry.py:SkillRegistryscans only providers currently registered in theApplicationcontainer. Each provider declares skills via askills/directory co-located with itsprovider.py.SKILL.mdfiles are read and YAML front-matter (name,description) is parsed into aSkilldataclass.skills/provider.py:SkillsServiceProviderregisters the registry under'skills.registry'in the container and boots the CLI commands.Task #140 — Adapter Layer (ClaudeAdapter + GeminiAdapter)
skills/adapters/base.py:BaseAdapterABC withrender()andprune()— extensible for future Codex adapter.skills/adapters/claude.py:ClaudeAdapterwrites idempotent.claude/skills/<name>/SKILL.mdfiles with YAML front-matter.--pruneremoves directories for skills no longer in the registry.skills/adapters/gemini.py:GeminiAdaptersplices a<!-- skills:start --> … <!-- skills:end -->block intoGEMINI.md— never touches content outside the markers. Idempotent.Task #141 — Cleo Commands
skills:sync— resolvesSkillRegistry, selects adapter(s) via--target(claude|gemini|all, defaultall), callsrender(), optionally callsprune()when--pruneis passed.skills:list— shows provider key, skill name, Claude/Gemini sync status (synced/pending), and truncated description for every discovered skill.Test Plan
pytesttests intests/skills/— all passingtest_registry.py— front-matter parsing, discovery, caching,get(),reset()test_adapters.py— idempotency, prune, marker preservation (Gemini), append when no markerstest_commands.py— target routing, prune flag, unknown target error, status display🤖 Generated with Claude Code