Skip to content

fix(skillify): locally migrate legacy over-long skill installs to the 64-char cap#327

Merged
efenocchi merged 2 commits into
mainfrom
fix/skillify-legacy-cap-migration
Jul 21, 2026
Merged

fix(skillify): locally migrate legacy over-long skill installs to the 64-char cap#327
efenocchi merged 2 commits into
mainfrom
fix/skillify-legacy-cap-migration

Conversation

@efenocchi

@efenocchi efenocchi commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

#322 caps skill frontmatter names to codex's 64-char loader limit and migrates prior un-capped installs — but only while processing a MATCHING remote row during a pull. A legacy managed install whose org/workspace isn't the currently-routed one never matches, so it stays un-capped forever. Observed live: every codex session logs invalid name: exceeds maximum length of 64 characters for pre-#322 installs.

Fix

New local, remote-independent migration pass (src/skillify/legacy-cap-migration.ts), wired into autoPullSkills BEFORE the network query — runs offline and logged-out:

  • Scans ONLY manifest-managed global entries; never infers ownership from directory names, never touches project-scoped installs of other projects.
  • Validates author + raw/capped names with the same validators pull uses before building any path.
  • Copy-then-swap: copy to the capped dir → rewrite frontmatter name (body/version preserved) → record the canonical manifest entry (rawName preserved) → fan out symlinks → only then retire the legacy dir/entry/symlinks. A failure at any earlier step leaves the original fully intact; the next SessionStart retries.
  • Same-rawName collision = leftover of a prior migration/pull → reconciled (legacy removed, canonical kept; an interrupted over-long canonical is torn down and re-migrated). Different rawName = true collision → skipped.

Known limitation (intentional): legacy dirs with NO manifest entry are not touched — manifest ownership is the safety boundary. Those still self-heal via a full hivemind skillify pull against their org.

Validation (real harness)

Replicated a manifest-managed legacy install (the real 69-char pg-deeplake-multi-layer-issue-diagnosis-and-workaround-prioritization--sasun) in a sandbox HOME and ran a real interactive claude session with this build via --plugin-dir. The real SessionStart hook migrated it:

  • dir → pg-deeplake-multi-layer-issue-diagnosis-and-workaround-420af--sasun
  • frontmatter name: → 60 chars (deterministic hash suffix, same capSkillName as pull)
  • manifest entry updated with rawName preserving the original.

A real codex exec session was separately shown to load a capped-name skill by name (and to reject the un-capped legacy ones — the exact bug).

  • tsc clean, npm run build clean; targeted suites 107 passed; full suite 5652 passed with per-file coverage thresholds met (new file: 97% stmts / 89% branches).
  • codex review of the diff: initial round's blockers (post-rename orphaning, missing path validation, same-rawName reconciliation, cross-project scope) all addressed as above.

Session Context

Session transcript

Summary by CodeRabbit

  • Bug Fixes

    • On skill pulls, automatically migrates legacy managed installs whose skill names exceed the supported length into the correct capped install layout.
    • Improves safety and resilience by preserving existing installs when issues are detected (conflicts, missing/invalid metadata) and continuing past per-item failures.
    • Ensures the migration completes before skill synchronization begins, including early runs when not signed in.
  • Tests

    • Added an integration-style test suite covering migrations, idempotency, retries after failures, collision handling, and ordering relative to synchronization.

PR #322 caps skill names to codex's 64-char loader limit, but the
migration of pre-cap installs only runs while processing a MATCHING
remote row during a pull. A legacy managed install whose org/workspace
is not the currently-routed one never matches, so it stays un-capped
forever and every codex session logs 'invalid name: exceeds maximum
length of 64 characters' for it (observed live).

Add a local, remote-independent migration pass that runs from
autoPullSkills before the network query (works offline and logged-out):

- Scans ONLY manifest-managed global entries - never infers ownership
  from directory names, never touches project-scoped installs.
- Validates author and names with the same validators pull uses before
  building any path.
- Copy-then-swap: copy to the capped dir, rewrite the frontmatter name,
  record the canonical manifest entry (rawName preserved), fan out
  symlinks, and only then retire the legacy dir + entry - a failure at
  any earlier step leaves the original fully intact for the next retry.
- A collision with the SAME rawName is a leftover from a prior
  migration/pull and is reconciled (legacy removed, canonical kept);
  a different rawName is a true collision and is skipped.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f37c0c96-616d-4a12-bcb1-6b46e33c0f15

📥 Commits

Reviewing files that changed from the base of the PR and between 11bdd39 and 2323373.

📒 Files selected for processing (2)
  • src/skillify/legacy-cap-migration.ts
  • tests/claude-code/legacy-cap-migration.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/skillify/legacy-cap-migration.ts
  • tests/claude-code/legacy-cap-migration.test.ts

📝 Walkthrough

Walkthrough

Adds an offline migration for overlong global skill install names, including collision safety, manifest and symlink reconciliation, retry handling, and tests. autoPullSkills runs the migration before configuration and login checks, while coverage thresholds are added for the new module.

Changes

Legacy capped install migration

Layer / File(s) Summary
Migration primitives
src/skillify/legacy-cap-migration.ts
Defines migration results, installed-name parsing, managed-entry retirement, and filesystem utilities.
Safe capped-directory migration
src/skillify/legacy-cap-migration.ts
Validates global installs, reconciles destinations, copies to capped directories, updates frontmatter, manifests, and symlinks, then removes stale entries.
Pre-pull migration wiring
src/skillify/auto-pull.ts
Runs the migration before routed configuration and login checks while swallowing migration failures.
Migration behavior and recovery coverage
tests/claude-code/legacy-cap-migration.test.ts
Covers migration, idempotency, collisions, validation, scope protection, symlinks, copy failures, and retry reconciliation.
Pull-ordering validation and coverage
tests/claude-code/legacy-cap-migration.test.ts, vitest.config.ts
Verifies migration precedes SQL queries and adds dedicated coverage thresholds.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SessionStart
  participant autoPullSkills
  participant LegacyCapMigration
  participant Manifest
  participant FileSystem
  participant QueryFn
  SessionStart->>autoPullSkills: start automatic pull
  autoPullSkills->>LegacyCapMigration: migrate legacy capped installs
  LegacyCapMigration->>Manifest: inspect managed global entries
  LegacyCapMigration->>FileSystem: copy and rewrite capped install
  LegacyCapMigration->>Manifest: update canonical entry and symlinks
  LegacyCapMigration-->>autoPullSkills: complete or report swallowed failure
  autoPullSkills->>QueryFn: load pull configuration
Loading

Possibly related PRs

Suggested reviewers: kaghni

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is detailed, but it does not follow the required template and is missing the Summary, Version Bump, and Test plan sections. Add the template sections with a brief Summary, version-bump status, and a Test plan checklist covering tests run and any new tests added.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: a local migration for legacy over-long skill installs to the 64-char cap.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/skillify-legacy-cap-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🟢 Lines 98.06% (🎯 90%) 101 / 103
🟢 Statements 96.67% (🎯 90%) 116 / 120
🟢 Functions 94.12% (🎯 90%) 16 / 17
🔴 Branches 82.19% (🎯 90%) 60 / 73
File Coverage — 2 files changed
File Stmts Branches Functions Lines
src/skillify/auto-pull.ts 🟢 95.3% 🔴 72.4% 🟢 90.9% 🟢 97.1%
src/skillify/legacy-cap-migration.ts 🟢 97.4% 🔴 88.6% 🟢 100.0% 🟢 98.5%

Generated for commit 625ce1a.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/claude-code/legacy-cap-migration.test.ts (1)

106-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer exact content equality over .toContain(...) substring checks.

Since writeSkill produces fully deterministic SKILL.md content, these body-preservation checks (e.g. lines 106, 221, 247-249, 269-270, 317, 322, 363, 418, 442, 458, 476) could assert the exact expected content instead of a substring — as already done correctly at line 132 (fileAfterFirst). A substring check wouldn't catch unintended corruption elsewhere in the file (e.g. a stray extra newline or a mangled unrelated frontmatter field).

As per path instructions, "Prefer asserting on specific values (paths, messages) over generic substrings."

Also applies to: 221-222, 247-249, 269-270, 317-317, 322-322, 363-363, 418-418, 442-442, 458-458, 476-476

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/claude-code/legacy-cap-migration.test.ts` around lines 106 - 107,
Replace the listed `.toContain(...)` assertions in the legacy migration tests
with exact equality checks against the complete deterministic SKILL.md content
produced by `writeSkill`. Reuse the established `fileAfterFirst`
expected-content pattern where applicable, ensuring checks cover the full body
and frontmatter rather than only selected substrings.

Source: Path instructions

src/skillify/legacy-cap-migration.ts (1)

202-236: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate recordPull payload construction.

The two recordPull calls (steps b and d) are identical except for symlinks (and a freshly re-computed pulledAt). Consider extracting a small buildCanonicalEntry(symlinks) helper to avoid the drift risk of editing one call and forgetting the other, and to reuse a single pulledAt timestamp for both writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/skillify/legacy-cap-migration.ts` around lines 202 - 236, Extract the
shared `recordPull` payload construction in the migration flow into a
`buildCanonicalEntry(symlinks)` helper, reusing the existing entry fields and
accepting only the symlink list as variable input. Capture one `pulledAt`
timestamp before both writes and use the helper for the step (b) and step (d)
`recordPull` calls, preserving their current symlink behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/skillify/legacy-cap-migration.ts`:
- Around line 145-191: Update the collision-reconciliation logic in migrateEntry
to treat readInstalledName(cappedFile) returning null the same as an overlong
canonical name, routing both through the repair path instead of retiring the
legacy entry. Extend the existing try/catch to cover the entire reconciliation
block, including rmSync, unlinkSymlinks, and removePullEntry, so per-entry
failures are swallowed and do not abort migrateLegacyCappedInstalls.

---

Nitpick comments:
In `@src/skillify/legacy-cap-migration.ts`:
- Around line 202-236: Extract the shared `recordPull` payload construction in
the migration flow into a `buildCanonicalEntry(symlinks)` helper, reusing the
existing entry fields and accepting only the symlink list as variable input.
Capture one `pulledAt` timestamp before both writes and use the helper for the
step (b) and step (d) `recordPull` calls, preserving their current symlink
behavior.

In `@tests/claude-code/legacy-cap-migration.test.ts`:
- Around line 106-107: Replace the listed `.toContain(...)` assertions in the
legacy migration tests with exact equality checks against the complete
deterministic SKILL.md content produced by `writeSkill`. Reuse the established
`fileAfterFirst` expected-content pattern where applicable, ensuring checks
cover the full body and frontmatter rather than only selected substrings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fa6cce33-261c-4d96-b33f-817d918942c1

📥 Commits

Reviewing files that changed from the base of the PR and between 87c285d and 11bdd39.

📒 Files selected for processing (4)
  • src/skillify/auto-pull.ts
  • src/skillify/legacy-cap-migration.ts
  • tests/claude-code/legacy-cap-migration.test.ts
  • vitest.config.ts

Comment thread src/skillify/legacy-cap-migration.ts Outdated
CodeRabbit review on #327 flagged two real defects in the same-rawName
reconciliation block:

- It ran OUTSIDE the per-entry try/catch, so an I/O error in the repair
  teardown (rmSync / unlinkSymlinks / removePullEntry) escaped
  migrateLegacyCappedInstalls and could abort autoPullSkills - breaking
  the swallow-all-per-entry-failures contract.
- An ORPHANED canonical manifest row (dir deleted manually, frontmatter
  unreadable) fell into the already-migrated branch and retired the
  legacy install - dropping the only surviving copy.

The reconciliation now lives inside the try, and a null installed name
routes to the repair path (tear down the broken canonical row, re-migrate
from the intact legacy) instead of retiring the legacy. Tests cover both:
a throw inside reconciliation is swallowed with both copies left intact,
and an orphaned row re-migrates instead of retiring.
@efenocchi
efenocchi merged commit 2b3b767 into main Jul 21, 2026
11 checks passed
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