Skip to content

fix(skills): restore the production skill catalog and stop --full from wiping it - #10

Merged
deve1993 merged 3 commits into
mainfrom
fix/skill-catalog-recovery
Aug 19, 2026
Merged

deve1993 merged 3 commits into
mainfrom
fix/skill-catalog-recovery

Conversation

@deve1993

Copy link
Copy Markdown
Collaborator

Why

Production routing has been degraded for ~10 days. skill_stats reported a healthy 293-skill catalog, but only 27 were active — and only active skills are ever listed or routed:

type total active
domain 129 19
process 114 0
command 23 0
agent 22 3
lifecycle 5 5

The 19 surviving domain skills were the 17 marketing skills from #9 plus nextjs and _routing-index, so skill_route answered every query from those:

"add stripe payments checkout"   → attribution, marketing-plan, sms, marketing-loops, prospecting
"setup Payload CMS multi-tenant" → lead-magnets, attribution, ads, marketing-plan, prospecting

The same queries against a healthy catalog return payments + stripe-subscriptions-webhooks and payload + mongodb + cms. The ranking algorithm was never at fault — the data was.

skill_read kept working for the missing skills, which is why nothing looked broken.

Root cause

A --full prune whose discovery set covered only a slice of the bundle. The guard added in 5438d7a only refused on skills.length === 0, so a catastrophically partial discovery passed straight through and deprecated everything else. The survivors match the prune's own protection clause exactly (category NOT IN ('System','Lifecycle')) — _routing-index and the 5 lifecycle skills.

Changes

Prevention — prune now refuses when it would deprecate >25% of the eligible active catalog and ≥10 skills. The absolute floor keeps small catalogs working (1 of 2 is a legitimate 50% prune). --force is the explicit override.

Recovery — new --reactivate, the exact inverse of --full: restores deprecated skills that ARE in the bundle. Genuinely retired skills never appear in the discovery set so they stay deprecated; pending security-gate quarantines are never auto-approved.

Detection — stats() counted every status while list/route serve only active. That contradiction is how this stayed invisible. It now reports active / byStatus / activeBy*, and skill_stats prints a warning banner when the active catalog drops below half the total.

Deploy gap — 29 domain skills existed only in the .claude/skill workspace mirror and were never COPYed into the image. Confirmed against prod: skill_read("stripe-subscriptions-webhooks") → not found. Now in the bundle.

CRLF frontmatter — seo-page and seo-programmatic used CRLF, so ^---\n failed and both lost their entire frontmatter, landing in the DB as "Domain skill: <name>" with none of the trigger keywords routing ranks on. Parser now tolerates CRLF; both files normalised.

Cross-zone duplicates — the bundle ships 29 names in both data/skill/ and data/lifecycle-skills/; whichever copy was walked last silently decided the skill's type. Now collapsed deterministically and reported.

Minor — expo-ui-* renamed to kebab-case (they were keyed under "Expo UI SwiftUI", unreachable by the obvious name); typecheck script added to codegraph (pnpm -r typecheck skipped it); /build/ gitignored.

Verification

Build 3/3, typecheck 3/3 (now including codegraph), 363 tests pass. Four new regression tests cover the partial-discovery guard, --reactivate semantics, CRLF frontmatter, and cross-zone collapse.

End-to-end, against a workspace built exactly the way entrypoint.sh builds one in the container:

  1. Imported the bundle → 315 active
  2. Recreated the incident (deprecated all but the marketing skills) → 24 active
  3. Ran --reactivate → 315 active, the 2 security-gate quarantines correctly untouched
  4. Routing verified correct on payments / Payload / 3D / Coolify queries

Deploying this

Merging + redeploying shows the 29 missing skills and installs the guard, but does not by itself repair the catalog — the boot import passes status: NULL and the upsert deliberately keeps existing status rather than resurrecting deprecated rows. Recovery is an explicit step, in the container:

sqlite3 /data/.codegraph/graph.db ".dump" | gzip > /data/.codegraph/pre-reactivate.sql.gz

DATA_DIR=/data
mkdir -p "$DATA_DIR/.opencode" "$DATA_DIR/.agents/skills"
ln -sf /app/data/skill   "$DATA_DIR/.opencode/skill"
ln -sf /app/data/agents  "$DATA_DIR/.opencode/agents"
ln -sf /app/data/command "$DATA_DIR/.opencode/command"
for d in /app/data/lifecycle-skills/*/; do
  name=$(basename "$d"); mkdir -p "$DATA_DIR/.agents/skills/$name"
  ln -sf "$d/SKILL.md" "$DATA_DIR/.agents/skills/$name/SKILL.md"
done

node dist/cli.js import-skills "$DATA_DIR" --reactivate

rm -rf "$DATA_DIR/.opencode" "$DATA_DIR/.agents"

The symlinks are required — without them discovery is empty and --reactivate restores 0 (harmless, but useless). Docs updated in DEPLOY-SKILLS.md.

Left out deliberately

  • .claude/skill/pixarts/ holds 6 real skills that are never imported (walkDir doesn't recurse into nested dirs). They self-name pixarts/*, which the project's naming rule disallows — renaming them is a content decision, not a bug fix.
  • webxr-spatial, shopify-expert sit in pending on a skill-guard BLOCK verdict. By design those need human review.

🤖 Generated with Claude Code

OpenCode Bot and others added 3 commits August 19, 2026 11:38
…covery

A `--full` run whose discovery set covered only a slice of the bundle
soft-deprecated everything else. In production that left 27 of 293 skills
active: `skill_route` answered every query from the ~19 survivors, so
unrelated skills came back for every task while `skill_read` still worked
fine for the missing ones — which is why it went unnoticed for days.

The old guard only refused on `skills.length === 0`, so a catastrophically
PARTIAL discovery sailed straight through.

* Prune now refuses when it would deprecate >25% of the eligible active
  catalog and at least 10 skills; `--force` is the explicit override. The
  absolute floor keeps small/dev catalogs working (1 of 2 is a legitimate
  50% prune).
* New `--reactivate`: the exact inverse. Restores `deprecated` skills that
  ARE in the bundle. Genuinely retired skills never appear in the discovery
  set so they stay deprecated, and `pending` security-gate quarantines are
  never auto-approved. A redeploy alone cannot repair this — the boot import
  passes `status: NULL` and the upsert deliberately keeps existing status.
* Collapse cross-zone duplicates before writing. The bundle ships 29 names
  in both data/skill/ and data/lifecycle-skills/; previously whichever copy
  was walked last silently decided the skill's type. Now deterministic and
  reported.
* parseFrontmatter normalises line endings. Every pattern was anchored on
  \n, so a CRLF SKILL.md failed the opening `^---\n` match and lost its
  ENTIRE frontmatter — the skill fell back to its directory name and a
  placeholder description, stripping the trigger keywords routing ranks on.
* skills stats() reports `active` / `byStatus` / `activeBy*` alongside the
  all-status totals it returned before. Reporting only all-status numbers is
  how a mass deprecation stayed invisible: stats said 293 while list/route
  could see 27. skill_stats now prints a warning banner when the active
  catalog drops below half the total.
* codegraph gained a `typecheck` script — `pnpm -r typecheck` silently
  skipped it before (CI called tsc directly, so this was ergonomics only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ills

seo-page and seo-programmatic were stored with CRLF line endings, so the
importer's `^---\n` match failed and both landed in the DB as
"Domain skill: <name>" with no trigger keywords — effectively unroutable.
The parser now tolerates CRLF (previous commit); these two files are
normalised so the repo is consistent either way.

expo-ui-swiftui and expo-ui-jetpack-compose self-reported title-case names
("Expo UI SwiftUI"), so they were keyed in the DB under a name no caller
would guess and `skill_read("expo-ui-swiftui")` missed them. Renamed to
match their directories; CATEGORY_MAP keeps the old keys so already-imported
rows retain their Mobile category instead of falling back to Other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only packages/codegraph/data/ is COPYed into the image, but these 29 domain
skills existed solely in the .claude/skill workspace mirror, so they were
never deployed. Confirmed against production: skill_read
"stripe-subscriptions-webhooks" returned not found, and 3D/R3F queries had
nothing relevant to match.

Includes stripe-subscriptions-webhooks, supabase-auth-ssr, sentry-nextjs,
resend-react-email, turborepo-monorepo, tanstack-query-next-actions,
rhf-zod-server-actions, vitest-next-conventions, claude-api-patterns,
the react-three-fiber / threejs / glsl / webgpu / webxr 3D set, and the
ai-image-generation / ai-video-generation / tts-voiceover media set.

The two mirrors are hand-maintained with no sync step, which is how they
drifted; this closes the gap for the skills currently missing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@deve1993
deve1993 merged commit 70aced2 into main Aug 19, 2026
1 check 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