diff --git a/packages/codegraph/scripts/seed-seo-skills.mjs b/packages/codegraph/scripts/seed-seo-skills.mjs
new file mode 100644
index 0000000..e53dc91
--- /dev/null
+++ b/packages/codegraph/scripts/seed-seo-skills.mjs
@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+/*
+ * Synapse — seed/refresh the SEO skill set into a graph.db.
+ *
+ * Propagates the SEO skills merged from claude-seo (MIT) — 11 enriched + 6 new + 3
+ * reference — into a target database. Use it to push them to a server whose DB you
+ * can't reach over MCP (e.g. the Coolify production container):
+ *
+ * node packages/codegraph/scripts/seed-seo-skills.mjs [SKILLBRAIN_ROOT]
+ *
+ * SKILLBRAIN_ROOT defaults to "/data" (prod layout → /data/.codegraph/graph.db).
+ * Pass "." to seed the local workspace DB.
+ *
+ * Run it AFTER deploying the code that contains migration 035 + the stable-rowid
+ * upsert, so FTS stays consistent. It opens the DB (which applies pending migrations),
+ * backs it up, upserts every skill in ./seed-seo-skills/, and rebuilds FTS as a
+ * belt-and-suspenders. Existing skills keep their status/confidence/usage (the upsert
+ * preserves them); brand-new ones default to status=active, confidence=5.
+ */
+
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { openDb, closeDb, SkillsStore } from '@skillbrain/storage'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const SEED_DIR = path.join(__dirname, 'seed-seo-skills')
+const ROOT = process.argv[2] || '/data'
+
+function parseFrontmatter(content, fallbackName) {
+ const fm = content.match(/^---\n([\s\S]*?)\n---/)
+ const block = fm ? fm[1] : ''
+ const pick = (key) => {
+ const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))
+ return m ? m[1].trim().replace(/^["']|["']$/g, '') : ''
+ }
+ return {
+ name: (pick('name') || fallbackName).trim(),
+ description: pick('description'),
+ }
+}
+
+async function main() {
+ if (!fs.existsSync(SEED_DIR)) {
+ console.error(`Seed dir not found: ${SEED_DIR}`)
+ process.exit(1)
+ }
+ const files = fs.readdirSync(SEED_DIR).filter((f) => f.endsWith('.md')).sort()
+ if (files.length === 0) {
+ console.error('No .md skill files in seed dir.')
+ process.exit(1)
+ }
+
+ const dbPath = path.join(ROOT, '.codegraph', 'graph.db')
+ console.log(`==> Target DB: ${dbPath}`)
+ console.log(`==> Seeding ${files.length} SEO skills`)
+
+ // openDb applies pending migrations (incl. 035 FTS triggers) before we touch anything.
+ const db = openDb(ROOT)
+
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
+ const backup = `${dbPath}.bak-pre-seo-seed-${stamp}`
+ await db.backup(backup)
+ console.log(`==> Backup: ${backup}`)
+
+ const store = new SkillsStore(db)
+ const now = new Date().toISOString()
+ let inserted = 0
+ let updated = 0
+
+ for (const file of files) {
+ const content = fs.readFileSync(path.join(SEED_DIR, file), 'utf-8')
+ const fallback = file.replace(/\.md$/, '')
+ const { name, description } = parseFrontmatter(content, fallback)
+ const existed = !!store.get(name)
+ store.upsert(
+ {
+ name,
+ category: 'SEO',
+ description: description || name,
+ content,
+ type: 'domain',
+ tags: Array.from(new Set(['seo', ...name.split('-')])),
+ lines: content.split('\n').length,
+ updatedAt: now,
+ // status omitted → preserve existing on update, default 'active' on insert
+ },
+ { reason: 'seo-skills-merge (claude-seo)' },
+ )
+ if (existed) { updated++; console.log(` ~ updated ${name}`) }
+ else { inserted++; console.log(` + inserted ${name}`) }
+ }
+
+ // Belt-and-suspenders: ensure the FTS index is consistent.
+ try { db.prepare(`INSERT INTO skills_fts(skills_fts) VALUES('rebuild')`).run() } catch { /* ok */ }
+
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM skills WHERE status='active'`).get().n
+ closeDb(db)
+
+ console.log(`\n==> Done. inserted=${inserted} updated=${updated}. Active skills now: ${total}.`)
+ console.log(` Rollback if needed: cp '${backup}' '${dbPath}' (stop the server first)`)
+}
+
+main().catch((err) => { console.error(err); process.exit(1) })
diff --git a/packages/codegraph/scripts/seed-seo-skills/seo-audit.md b/packages/codegraph/scripts/seed-seo-skills/seo-audit.md
new file mode 100644
index 0000000..37b1d76
--- /dev/null
+++ b/packages/codegraph/scripts/seed-seo-skills/seo-audit.md
@@ -0,0 +1,208 @@
+---
+name: seo-audit
+description: >
+ Full-site SEO audit orchestrator. Crawls the site, detects business type,
+ fans out to the seo-* specialist skills, then aggregates an evidence-backed
+ 0-100 health score and a prioritized action plan (Critical → Low). Use when
+ user says "audit", "full SEO check", "analyze my site", "website health
+ check", or "SEO report". Triggers on: seo audit, full seo check, site health
+ score, action plan, crawl my site, technical+content+schema audit.
+version: 1.1.0
+---
+
+# Full-Site SEO Audit (Orchestrator)
+
+This is the **umbrella** skill. It does not do the deep work itself — it crawls,
+classifies the site, **delegates** to the specialist `seo-*` skills, then merges
+their findings into one scored report. Every finding it emits carries **evidence**,
+a **falsifiability check**, and a **leading indicator** so the report is verifiable
+rather than vibes.
+
+## When to use
+
+- Someone asks for a whole-site audit / health check / SEO report.
+- You need a single prioritized backlog across technical, content, schema, perf, etc.
+- Before a redesign or migration (capture a baseline), or after launch (verify).
+
+For a single concern, skip this and go straight to the specialist (e.g. just schema →
+`seo-schema`). Use this when the ask is "how healthy is the whole site".
+
+## Process
+
+1. **Render the homepage.** Fetch raw + rendered HTML and extracted text (use your
+ own crawler/headless tooling). Note SPA vs SSR — for our stack this is Next.js 15
+ App Router, so most routes are RSC/SSR; flag any client-only routes that ship
+ empty initial HTML.
+2. **Detect business type / vertical.** SaaS, local service, e-commerce, publisher,
+ marketing site. This decides which conditional specialists fan out and how weights
+ shift (see Scoring).
+3. **Crawl** internal links (config below), respect `robots.txt`, dedupe by canonical.
+4. **Fan out to specialists** (run as parallel subagents if available, else inline
+ in priority order). Each returns per-finding evidence + a category sub-score.
+5. **Aggregate** into the 0-100 SEO Health Score using the weighted rubric.
+6. **Prioritize** every finding into Critical → High → Medium → Low.
+7. **Report**: executive summary, per-category sections, action plan grouped by phase.
+
+## Specialist delegation map
+
+Always run the core set. Add conditional specialists based on detected vertical /
+available data. Names below are the actual sibling skills in this catalog.
+
+| Specialist skill | Owns | Run when |
+|-------------------------|---------------------------------------------------------------------|---------------------|
+| `seo-technical` | robots.txt, sitemaps, canonicals, indexability, redirects, headers, CWV reality | always |
+| `seo-content` | E-E-A-T, search intent, thin/duplicate content, readability | always |
+| `seo-page` | On-page: title, meta description, headings, internal links per page | always |
+| `seo-schema` | JSON-LD detection, validation, missing-type opportunities | always |
+| `seo-images` | alt text, dimensions/CLS, format (AVIF/WebP), `next/image` usage | always |
+| `seo-geo` / `ai-seo` | AI-crawler access, llms.txt, citability, answer-engine readiness | always |
+| `seo-sitemap-advanced` | sitemap structure, quality gates, index/sub-sitemaps, missing pages | sites > ~50 pages |
+| `seo-hreflang` | hreflang correctness, x-default, return tags (next-intl locales) | multi-locale sites |
+| `programmatic-seo` / `seo-programmatic` | template/page-pattern quality, dup risk at scale | templated/large catalogs |
+| `seo-competitor-pages` | gap vs competitor pages for target queries | strategy/ranking goals |
+| `seo-for-devs` | implementation fixes mapped to Next.js/Payload code | when handing fixes to devs |
+
+Performance (LCP/INP/CLS) is part of `seo-technical`'s CWV reality check; capture
+field data if a CrUX/RUM source exists, otherwise label numbers as lab estimates.
+
+## Crawl configuration
+
+```
+Max pages: 500
+Respect robots.txt: yes
+Follow redirects: yes (max 3 hops)
+Timeout per page: 30s
+Concurrent requests: 5
+Delay between requests: 1s
+Canonical dedupe: yes (collapse param/duplicate URLs)
+```
+
+On large sites, cap at the page limit, report what was crawled, and estimate total
+scope rather than silently truncating.
+
+## Scoring (0-100, weighted)
+
+Each specialist returns a category sub-score (0-100). Combine with these weights.
+Shift weights by vertical (e.g. e-commerce → raise Schema; publisher → raise Content;
+local → raise the geo/local signal). State the weights you used in the report.
+
+| Category | Weight | Default rubric anchor |
+|--------------------------|:------:|-------------------------------------------------------|
+| Technical SEO | 22% | crawlable, indexable, no canonical/redirect chaos |
+| Content Quality | 23% | intent match, E-E-A-T signals, no thin/dup pages |
+| On-Page SEO | 20% | unique titles/meta, clean heading tree, internal links |
+| Schema / Structured Data | 10% | valid JSON-LD for the page types that warrant it |
+| Performance (CWV) | 10% | LCP < 2.5s, INP < 200ms, CLS < 0.1 (p75 field if avail) |
+| AI Search Readiness | 10% | AI crawlers allowed, citable structure, llms.txt |
+| Images | 5% | alt text present, sized to avoid CLS, modern formats |
+
+**Sub-score banding:** 90-100 excellent · 70-89 good · 50-69 needs work ·
+30-49 poor · 0-29 critical. The overall score is the weighted mean, rounded.
+
+## Evidence + falsifiability per finding (claude-seo's core value)
+
+Do not emit a finding without all four fields. This is what makes the audit honest
+and re-checkable instead of generic advice.
+
+- **Claim** — the specific problem, with the exact URL(s)/selector/value.
+- **Evidence** — what was observed (the rendered title, the HTTP status, the LCP
+ element, the missing JSON-LD field). Quote the real value, not a paraphrase.
+- **Falsifiability** — "How would we know this finding is wrong?" State the concrete
+ observation that would overturn it (e.g. "wrong if GSC shows these URLs indexed
+ within 14 days despite the noindex we flagged").
+- **Leading indicator** — the metric that should move first if the fix works, and
+ roughly when (e.g. "impressions in GSC for the target query cluster within 2-4
+ weeks", "INP p75 in CrUX next 28-day window", "valid items in Rich Results Test").
+
+Per-finding record shape:
+
+```json
+{
+ "title": "Homepage