Skip to content

feat(skills): versioned skill cache paths and repo-level version inheritance#99

Merged
albertodebortoli merged 7 commits into
mainfrom
skill-versioning
Jul 22, 2026
Merged

feat(skills): versioned skill cache paths and repo-level version inheritance#99
albertodebortoli merged 7 commits into
mainfrom
skill-versioning

Conversation

@albertodebortoli

@albertodebortoli albertodebortoli commented Jul 22, 2026

Copy link
Copy Markdown
Member

Description

This PR introduces versioned skill management — skills are now pinned to a specific git ref (tag, branch, or commit SHA), stored and cached under a versioned path, and that version can be declared once at the repository level rather than repeated on every skill entry.

Motivation

Without versioning, every luca install had to re-clone a skill repository to determine whether anything had changed, because there was no record of which revision was installed. This made installs slow and unreliable. It also made it impossible to have two sets of skills from the same repository at different refs (e.g. a stable tag for most skills and a specific SHA for one experimental skill).

No lockfile

A lockfile was considered and deliberately rejected. Luca's original design philosophy argues against lockfiles for managed tools: skills are symlinked directly into each agent's folder (.claude/skills/, etc.), so agents always access the exact installed version. Users never need to know which SHA is live — the symlink is the truth. Introducing a lockfile would replicate the same information in a second place without adding safety, while adding friction to the workflow. The versioning pattern here mirrors how tool versions are managed (declared in the Lucafile, cached under ~/.luca/tools/{name}/{version}/), keeping the mental model consistent.

What changed

Version is now mandatory for skills.
Every skill must resolve to a version — either declared inline on the skill entry or inherited from the repos: dictionary entry. Attempting to install without one is an error with a clear message.

Skills are cached at versioned paths.
Global cache: ~/.luca/skills/{name}/{version}/
Project-local cache: .luca/skills/{name}/{version}/

If the versioned folder already exists, the download is skipped entirely. Old flat-layout installations (.luca/skills/{name}/SKILL.md) are auto-migrated on the next install.

Symlinks target versioned folders.
.claude/skills/{name}.luca/skills/{name}/{version}/
The symlink name is unchanged so agents see no difference.

Repo aliases can carry a version field.
All skills referencing the alias inherit its version, with per-skill version taking precedence:

repos:
  ios-skills:
    url: git@github.com:org/ios-skills.git
    version: v2.1.0          # all skills below inherit this
  infra-tools: git@github.com:org/infra-skills.git   # bare string, no default version

skills:
  - name: swift-concurrency
    repository: ios-skills   # inherits v2.1.0 from repo entry

  - name: swift-testing-expert
    repository: ios-skills
    version: abc1234          # overrides repo version for this skill only

  - name: deploy-scripts
    repository: infra-tools
    version: v3.0.0           # must specify when repo has no default

Same repo, different SHAs — now supported.
To avoid two skills from the same repository at different refs to trigger a spurious version-conflict error, the grouping key is (repository, version) so each unique (URL, SHA) pair produces an independent SkillSet — cloned and cached separately:

repos:
  ios-skills:         { url: git@github.com:org/ios-skills.git, version: 947ad594 }
  ios-skills-preview: { url: git@github.com:org/ios-skills.git, version: 3445c638 }

skills:
  - name: swift-concurrency
    repository: ios-skills         # cloned at 947ad594

  - name: swift-concurrency-next
    repository: ios-skills-preview # cloned at 3445c638 — separate checkout

luca installed --skills shows versions.
Output now lists each installed version per skill name:

Installed skills:
  swift-concurrency       v2.1.0, abc1234
  swift-testing-expert    v2.1.0
  deploy-scripts          v3.0.0

Type of Change

  • Feature
  • Bug fix

How Has This Been Tested?

  • Added / updated unit tests
  • Tested on macOS (arch: arm64)

Checklist

  • Swift code builds locally (swift build)
  • Tests pass locally (swift test)
  • Code style / formatting respected
  • Documentation updated (README / comments)

Breaking Changes?

  • Yes — version is now required on every skill (or on its repo alias).

Migration: Add a version: field to each skill, or promote it to the repo alias entry to avoid repetition. The flat → versioned cache migration is transparent: on the first luca install after upgrading, any existing ~/.luca/skills/{name}/SKILL.md file is removed before the versioned copy is written. No manual cleanup required.

Skill.version is now non-optional — omitting it in a Lucafile throws
SkillDecodingError.missingVersion, which SpecLoader wraps as
SpecLoaderError.invalidSpec. All fixture files and tests updated
accordingly; the previously-optional behaviour test is removed.
…dividual installs

SkillSet.version is now String (non-optional). SkillsInfoFactory throws
SkillsInfoFactoryError.missingVersion when an individual install is
attempted without a ref. InstallCommand.validate() provides an early CLI
error for that case. All test callsites updated to supply a version.
…ned cache folder

Symlink source path gains a /{version} component:
  .luca/skills/{name}/{version}/   (project-local)
  ~/.luca/skills/{name}/{version}/ (global)
…lready installed

Skills are now cached at .luca/skills/{name}/{version}/ (mirroring tool
cache paths). Named skills that already have their versioned folder skip
the git clone entirely and only re-create symlinks. A migration step
removes any old flat-layout SKILL.md before writing the versioned folder.
…lledCommand shows versions

installedSkills() now returns [String: [String]] (skill name → sorted
version list) by iterating the two-level name/version directory tree.
InstalledCommand displays each skill name followed by its installed
versions, matching the tools listing style.
…sions

repos: entries now support an object form with url + version fields.
Skills referencing an alias without their own version: inherit the repo's
version (skill-level overrides repo-level). If two skills from the same
resolved URL end up with conflicting versions, SkillsInfoFactory throws
versionConflict rather than silently dropping the second version.

Fixes the silent-discard bug where mixed per-skill versions on the same
repository caused one SHA to be silently ignored.
@albertodebortoli albertodebortoli added this to the 0.22.0 milestone Jul 22, 2026
@albertodebortoli albertodebortoli added the feature New feature or enhancement label Jul 22, 2026
…at different SHAs

Two different repo aliases pointing to the same URL but at different commit SHAs
previously triggered a spurious versionConflict error. Skills are now grouped by a
(repository, version) composite key so each unique (URL, SHA) pair produces an
independent SkillSet — checked out and cached separately.

Removes SkillsInfoFactoryError.versionConflict entirely; the restriction was
wrong because independent aliases intentionally express separate checkouts.
@albertodebortoli
albertodebortoli merged commit 3c3afa4 into main Jul 22, 2026
2 checks passed
@albertodebortoli
albertodebortoli deleted the skill-versioning branch July 22, 2026 12:09
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.05691% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
Sources/LucaFoundation/Models/Skill.swift 41.66% 7 Missing ⚠️
Sources/LucaFoundation/Models/RepoEntry.swift 72.72% 3 Missing ⚠️
Sources/ManagerCore/Core/Installer/Installer.swift 97.14% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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