Skip to content

Add native skill installation pipeline#50

Merged
albertodebortoli merged 46 commits intomainfrom
native-skill-support
Apr 8, 2026
Merged

Add native skill installation pipeline#50
albertodebortoli merged 46 commits intomainfrom
native-skill-support

Conversation

@albertodebortoli
Copy link
Copy Markdown
Member

Pull Request Title

Add native skill installation pipeline

Description

This PR introduces a fully native skill installation pipeline in Luca, replacing the previous approach that relied on Vercel Labs' npx skills add CLI tool. Skills can now be installed, symlinked, and uninstalled directly via Luca without requiring Node.js or npx on the host.

The native pipeline is now the default for skill operations. The original npx-based path is preserved and accessible via --use-npx for users who prefer it or need it for compatibility reasons.

What is changing and why?

Core motivation: Luca's prior skill installation delegated all work to an external npx skills add command, introducing a hard dependency on Node.js and an extra network round-trip to the npm registry. This PR replaces that with an implementation entirely within Luca that uses git clone under the hood — leveraging whatever authentication is already configured on the host (SSH keys, credential helpers, .netrc) with no extra token setup.

Key changes:

New components (LucaCore)

Component Role
GitRepositorySkillFetcher Performs a shallow git clone into a temp directory, enumerates skill file paths (excluding metadata/infra files), and cleans up on deinit. Acts as a SkillRepositoryFetching actor.
GitHubSkillTreeClient Alternative fetcher using the GitHub Trees API (unauthenticated, public repos only). Used in tests and as an optional backend.
SkillDownloader Orchestrates fetching + filtering + frontmatter parsing. Given a SkillSet, returns [(name, [SkillFile])]. Handles owner/repo, HTTPS, and SSH URL formats.
SkillFrontmatterParser Parses the YAML frontmatter name: field from SKILL.md files to derive canonical skill names.
SkillSymLinker Creates per-agent symlinks from the project skills directory into the Luca cache. Respects each agent's projectSkillsPath.
InstalledSkillsLister Lists installed skill names for a given agent by reading the agent's project skills directory.
SkillUninstaller Removes skill symlinks across all agents, with clear errors when a skill is not found.
AgentRegistry Static catalogue of 45 known AI coding agents (synced from the Vercel Labs agent-skills registry), each with projectSkillsPath and globalSkillsPath.
SkillFile Model representing a downloaded skill file: relative path + raw Data.

Changes to existing components

  • Installer: Extended with skillDownloader and skillSymLinker dependencies. install(installationType:) gains an experimental parameter (now defaults to true = native pipeline). installQuietly now wraps everything in a noora.progressStep for cleaner quiet-mode output.
  • GitIgnoreManager: New ensureGitIgnoreIncludesSkillFolders(agents:) method that appends agent skill folder patterns (e.g. .claude/skills/, .cursor/skills/) to the project's .gitignore so installed skills are not accidentally committed.
  • SkillInstallationType: New .individual(repository:skills:agents:) case for installing skills directly from a repository reference without a Lucafile.
  • SkillsInfoFactory: Updated to resolve the new .individual case.
  • SubprocessRunner / SubprocessRunning: Added GIT_TERMINAL_PROMPT=0 and GIT_ASKPASS=/usr/bin/false env vars to disable interactive git credential prompts during clone operations.

CLI changes (LucaCLI)

  • install:
    • identifier argument now accepts org/repo (skill) in addition to org/repo@version (tool). Auto-detection: presence of @ → tool, absence → skill.
    • --use-npx flag: opt in to the legacy Vercel Labs npx path.
    • --skill <name> (repeatable): install only specific named skills from a repository.
    • --agent <id> (repeatable): override the agents list from the Lucafile.
    • --only-tools / --only-skills now document they apply to spec-based installs only.
  • uninstall: Falls back to SkillUninstaller when the identifier contains no @ version suffix and no tool with that name is found in the cache.
  • installed: No breaking change; minor documentation update.

Documentation

  • README.md extended with a dedicated "Skills" section covering the native pipeline, --use-npx fallback, per-agent targeting, and .gitignore management.

Type of Change

  • Feature
  • Bug fix
  • Maintenance / Refactor
  • Documentation
  • CI / Tooling
  • Other (specify)

How Has This Been Tested?

  • Added / updated unit tests
  • Manually tested locally (describe)
  • Tested on macOS (arch: arm64 / x86_64)
  • Other

New test files (all using Swift Testing):

Test file Coverage
AgentRegistryTests Lookup by id, unknown ids silently dropped, allAgentIds sorted
GitHubSkillTreeClientTests Tree parsing, excluded files, truncated response error
GitRepositorySkillFetcherTests Clone, file enumeration, excluded dirs/files, clone caching, deinit cleanup
SkillDownloaderTests Valid repository formats, name resolution from frontmatter, skill filtering, missing skill error
SkillFrontmatterParserTests YAML name extraction, missing/malformed frontmatter fallback
SkillSymLinkerTests Symlink creation, idempotent re-link, missing cache dir
SkillUninstallerTests Unlink existing skill, skill-not-found error
InstalledSkillsListerTests Lists skill names, skips non-symlink entries
GitIgnoreManagerSkillsTests Appends missing patterns, skips already-present patterns
ErrorDescriptionTests Covers .errorDescription on new error enums

Existing InstallerTests and SkillsInfoFactoryTests extended to cover new code paths.

Manual test: luca install vercel-labs/agent-skills --skill find-skills installs the skill into .claude/skills/find-skills/ and updates .gitignore.

Checklist

  • Swift code builds locally (swift build)
  • Tests pass locally (swift test)
  • Code style / formatting respected
  • Documentation updated (README / comments)
  • Version / tag alignment considered (if release related)
  • PR title follows conventional style (optional)

CI Considerations

  • Affects build time notably
  • Requires new secrets / env vars
  • Alters release process

Breaking Changes?

  • No

The native pipeline is additive. The experimental parameter on Installer.install(installationType:experimental:) defaults to true but is not part of the public ABI (internal use via CLI). The --use-npx flag preserves the previous behaviour for anyone relying on the npx path.

Additional Notes

  • The GitRepositorySkillFetcher is an actor to safely cache clone paths across concurrent calls.
  • GIT_TERMINAL_PROMPT=0 and GIT_ASKPASS=/usr/bin/false are injected into every subprocess to avoid interactive prompts hanging in CI or headless environments.
  • Excluded infrastructure paths: metadata.json, .git/, __pycache__/, __pypackages__/ — mirroring what the Vercel Labs npx tool excludes.
  • The AgentRegistry is synced with the Vercel Labs agent-skills registry as of the branch cut date; it is a static table and will need periodic updates as new agents are added upstream.

- Add Constants.skillsFolder = 'skills' for project-local skills cache folder
- Add FileManagerWrapper.skillsCacheFolder property pointing to {CWD}/.luca/skills/
- Add skillsCacheFolder to FileManaging protocol
- Update FileManagerWrapperMock to implement skillsCacheFolder
Issue 1: Add DocC comment to skillsCacheFolder property in FileManaging protocol to document it as project-local cache.
Issue 2: Update skillsFolder constant comment to clarify it's used for project-local .luca/skills/ directory, not home cache.
Introduces `AgentInfo` (id + projectSkillsPath) and `AgentRegistry` with
a static list of 27 known AI coding agents, plus `agents(for:)` and
`allAgentIds()` helpers. Includes a matching test suite (6 tests).
1. Make AgentInfo and AgentRegistry public by adding public keyword to struct, properties, and static methods
2. Change test_all_hasExpectedCount to exact equality (27 agents instead of >= 20)
3. Add test_allAgentIds_isSorted to verify alphabetical sorting
4. Add test_agents_forIds_hasCorrectPaths spot-check test for known paths

All tests pass, build succeeds.
…tories

Implements GitHubSkillTreeClient using the GitHub Git Trees API to list SKILL.md
blob paths and raw.githubusercontent.com to download individual skill files,
with full test coverage via DataDownloaderMock and a fixture-based tree response.
- Rename protocol to GitHubSkillTreeFetching (gerund -ing convention)
- Extract protocol to its own file GitHubSkillTreeFetching.swift
- Add @unchecked Sendable to GitHubSkillTreeClientMock
- Add treeTruncated error case with guard in skillPaths(owner:repo:)
- Remove redundant CodingKeys from GitHubTreeItem
- Add test_skillPaths_treeTruncated with GitHubTreeTruncated.json fixture
Add SkillFrontmatterParser component that parses YAML frontmatter from SKILL.md
files to extract the skill name. The parser handles various error cases including
missing UTF-8 encoding, missing frontmatter delimiters, invalid YAML syntax, and
missing name field.

- New file: Sources/LucaCore/Core/SkillFrontmatterParser/SkillFrontmatterParser.swift
- New test file: Tests/Core/SkillFrontmatterParserTests.swift
- Error enum: SkillFrontmatterParserError with cases missingNameField and invalidFrontmatter
- Uses Yams library for YAML parsing with proper error handling
- 5 comprehensive tests covering all error paths and the success case
- All 244 tests passing
Implements `SkillDownloading` protocol and `SkillDownloader` struct that parses GitHub repository references (shorthand and HTTPS URLs), fetches SKILL.md paths via `GitHubSkillTreeFetching`, optionally filters by name, and downloads skill content. Root `SKILL.md` names are resolved from YAML frontmatter.
- Promote GitHubSkillTreeClientError to top-level type (decouples SkillDownloader from concrete struct name)
- Introduce SkillFrontmatterParsing protocol and inject it into SkillDownloader (replaces concrete type dependency)
- Fix greedy .git removal in parseRepository to suffix-only strip
- Fix default catch case to re-throw the original error instead of a misleading repositoryNotFound
- Add tests for downloadFailed and frontmatter parse failure fallback to repo name
Introduces SkillSymLinker, which creates symbolic links for installed
skills in each agent's project skills directory (e.g. .claude/skills/)
pointing to the canonical .luca/skills/ cache. Includes protocol,
implementation, file manager protocol, mock, and tests.
Implements Task 7 (InstalledSkillsLister) and Task 8 (SkillUninstaller):
- InstalledSkillsLister lists installed skills from the project-local skills cache, returning a sorted list of subdirectory names under `.luca/skills/`, skipping non-directories and returning [] if the folder doesn't exist
- SkillUninstaller removes a skill's cache folder and all agent symlinks, throwing SkillUninstallerError.skillNotFound when the skill isn't installed
- Both components have narrow *FileManaging protocols registered in the FileManaging umbrella
- Full test coverage: 3 tests for InstalledSkillsLister, 3 tests for SkillUninstaller
- SkillUninstallerMock added for CLI-layer tests
…nfoFactory

- Add .individual case to SkillInstallationType enum for ad-hoc skill installs
- Implement handler in SkillsInfoFactory for .individual case
- Add two new tests covering all-skills and filtered-skills scenarios
Adds SkillDownloader and SkillSymLinker DI slots to Installer, and an
`experimental` parameter to `install(installationType:SkillInstallationType)`.
When `experimental: true`, uses the native pipeline (download SKILL.md files,
write to cache, create symlinks) instead of delegating to the npx-based
SkillInstaller. Adds three tests covering both paths.
- Fix broken DocC symbol reference (remove invalid InstallationType link, update step 4 to mention native/npx paths, add separate Topics entries for tool and skill install methods)
- Replace content.write(to:) with fileManager.createFile(atPath:contents:) to route through the FileManaging dependency; add createFile to FileManaging protocol, FileManagerWrapper, and FileManagerWrapperMock
- Add agent-resolution assertions to experimental pipeline tests (spec test verifies resolved agent IDs, individual test verifies AgentRegistry.all is used when agents is nil)
…into Installer

Adds a new method to GitIgnoreManager that appends .luca/skills/ and each agent's projectSkillsPath to .gitignore when in a git repo. The Installer now calls this at the end of the native (experimental) skills pipeline, and 5 new Swift Testing tests cover all branches.
Move ensureGitIgnoreIncludesSkillFolders out of the per-SkillSet install
loop so it runs once after all skill sets are processed, update the
GitIgnoreManager type-level DocC comment to reflect both tools and skills
folders, and add a test for the no-trailing-newline branch.
Exposes the native skills pipeline behind --experimental in three CLI
commands: install gains --experimental, --skill, and --agent flags;
uninstall and installed gain --only-skills and --experimental flags to
route through SkillUninstaller and InstalledSkillsLister respectively.
- Guard `.individual` skill branch behind `onlySkills` flag to prevent it from firing in `.all` mode
- Throw `invalidCombinationOfArguments` in `validate()` when `--skill`/`--agent` are used without `--experimental`
- Throw `ValidationError` in `UninstallCommand` and `InstalledCommand` when `--only-skills` is used without `--experimental`
- Fix `// mARK: -` typo to `// MARK: -` in InstallCommand.swift
Covers five new error types: GitHubSkillTreeClientError, SkillFrontmatterParser.SkillFrontmatterParserError, SkillDownloader.SkillDownloaderError, SkillSymLinker.SkillSymLinkerError, and SkillUninstaller.SkillUninstallerError. All test methods follow the existing pattern and verify errorDescription is non-nil for each case.
- Move two orphaned @test functions inside SkillDownloaderTests struct
- Delete unused SkillUninstallerMock (no protocol conformance, no consumers)
- Refactor Installer to compute resolvedAgents once before loop in both installQuietly and installVerbose skill paths
- Remove unnecessary FoundationNetworking import from GitHubSkillTreeFetching protocol file
- Fix InstalledSkillsListerTests to use mock fileManager instead of FileManager.default
… downloads

Mirrors Vercel Labs' skill exclusion list: metadata.json is always skipped;
.git, __pycache__, and __pypackages__ directory contents are never downloaded.
Replace the hand-curated agent list with the canonical data from the
Vercel Labs skills README: correct project paths for cursor, cline,
continue, windsurf and others; add globalSkillsPath to AgentInfo; add
27 new agents; remove agents absent from the registry.
albertodebortoli and others added 7 commits April 1, 2026 09:49
Skill repositories are now cloned with `git clone --quiet --depth 1`
via the system git binary, so any authentication already configured on
the host (SSH keys, SSH agents, credential helpers) works transparently.
This makes private repositories and GitHub Enterprise Server instances
accessible without any token configuration.

- Add `GitRepositorySkillFetcher` (actor) as the new default fetcher;
  caches clones by URL and cleans up temp dirs on deinit
- Introduce `SkillRepositoryFetching` protocol (replaces
  `GitHubSkillTreeFetching`) with `repository: String` params so the
  full original URL is passed through unchanged
- Update `GitHubSkillTreeClient` to implement the new protocol, parsing
  the repository reference internally; kept as an alternative for
  API-based access
- Add `gitNotFound` and `cloneFailed` error cases to `SkillDownloader`
- Mark `SubprocessRunning` as `Sendable` for Swift 6 actor isolation
- Remove GITHUB_TOKEN auth added in the previous session

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set GIT_TERMINAL_PROMPT=0 when running git clone so that authentication
failures produce an immediate error rather than hanging indefinitely on
a credential prompt that will never be answered (stdin is /dev/null).

Extends SubprocessRunning/SubprocessRunner with an environment parameter
(merged on top of the inherited env) to support this without coupling
GitRepositorySkillFetcher to process manipulation directly. Existing
callers use the convenience overload and are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a Skills section covering Lucafile configuration, installing from
a repository via the native pipeline, listing, uninstalling, and the
--only-tools / --only-skills flags for mixed Lucafiles.
Install: infer tool (org/repo@version) vs skill (org/repo or URL) from
the identifier format; --only-tools and --only-skills are now reserved
for spec-based installs and cannot be combined with an identifier.

Uninstall: try tool first; if no tool folder is found, fall back to
skill uninstall automatically. --only-skills and --experimental flags
are removed from the uninstall command.
- Replaces --experimental with --use-npx in the install command; native Swift pipeline now runs by default and --use-npx opts into Vercel Labs' npx-based skills tool
- Renames luca installed --only-skills to luca installed --skills
- Removes --only-skills from identifier-based install examples (invalid combination)
- Shows 'No tool or skill named X was found.' when uninstalling an unknown name
@albertodebortoli albertodebortoli added the feature New feature or enhancement label Apr 7, 2026
@albertodebortoli albertodebortoli added this to the 0.14.0 milestone Apr 7, 2026
… flags

- Remove erroneous `!useNpx` guard that prevented `.individual` install type from being returned when `--use-npx` was passed with a repository identifier
- Remove validation that incorrectly blocked `--skill`/`--agent` flags when combined with `--use-npx`; npx skills add supports these flags natively
@albertodebortoli albertodebortoli marked this pull request as ready for review April 7, 2026 21:22
The `--experimental` CLI flag was replaced by `--use-npx`, so the
`experimental: Bool` parameter in `Installer` was an inverted alias
that no longer matched the CLI semantics. Renamed to `useNpx: Bool`
(default `false`) with matching logic: `true` routes to the npx
pipeline, `false` (the default) uses the native pipeline.
test_installSkillsSpec_installsAllSkillSets was calling install without
useNpx: true, so it routed through the native pipeline (SkillDownloader)
instead of the npx path (SkillInstaller), causing a real network lookup.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 98.83495% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../GitHubSkillTreeClient/GitHubSkillTreeClient.swift 98.01% 2 Missing ⚠️
...sitorySkillFetcher/GitRepositorySkillFetcher.swift 97.43% 2 Missing ⚠️
Sources/LucaCore/Core/Installer/Installer.swift 96.29% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Cover uncovered code paths across 8 files (44 missing lines):
- GitHubSkillTreeClient: SSH/HTTPS URL parsing, GitHub Enterprise
  Server URLs, non-HTTP responses, invalid formats
- SkillDownloader: fileReadFailed from skillPaths, 429 rate limit,
  unexpected status code re-throw, non-unexpectedResponse re-throw,
  auxiliary file download failure
- SkillFrontmatterParser: single delimiter, empty frontmatter, YAML
  list instead of dictionary
- SkillSymLinker: directory creation and symlink creation error paths
- SubprocessRunner: environment variable merging
- GitRepositorySkillFetcher: HTTPS/HTTP URL passthrough, __pypackages__
  directory exclusion
- Installer: no-skills spec path, individual install with all agents
- SkillDownloader: cover SSH/HTTPS/HTTP error paths in extractRepoName,
  cover .git suffix stripping for HTTPS URLs, cover HTTP scheme branch
- SkillUninstaller: add mock-based test that covers removeItem(atPath:)
  path for agent symlink cleanup (fixes broken-symlink issue on Linux)
Skills are installed into .luca/skills/ and symlinked into
agent-specific directories, not installed directly into them.
Replace generic vercel-labs/agent-skills references with a more
representative Swift skill example in help text and doc comments.
@albertodebortoli albertodebortoli merged commit 7bc01b9 into main Apr 8, 2026
3 checks passed
@albertodebortoli albertodebortoli deleted the native-skill-support branch April 8, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant