diff --git a/.github/workflows/validate-manifest.yml b/.github/workflows/validate-manifest.yml
index a155482..0b7fe42 100644
--- a/.github/workflows/validate-manifest.yml
+++ b/.github/workflows/validate-manifest.yml
@@ -4,7 +4,8 @@ on:
pull_request:
paths:
- 'skills/**'
- - 'scripts/generate_manifest.py'
+ - 'assets/**'
+ - 'scripts/skills.py'
- 'manifest.json'
push:
branches:
@@ -24,4 +25,4 @@ jobs:
python-version: '3.11'
- name: Validate manifest is up to date
- run: python3 scripts/generate_manifest.py validate
+ run: python3 scripts/skills.py validate
diff --git a/CLAUDE.md b/CLAUDE.md
index 9a84e00..19236df 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -37,11 +37,12 @@ Then apply these patterns:
- Pattern 2
```
-### Manifest
+### Skills management
```bash
-python3 scripts/generate_manifest.py # generate
-python3 scripts/generate_manifest.py validate # check in CI
+python3 scripts/skills.py # sync assets + generate manifest (default)
+python3 scripts/skills.py sync # copy shared assets into each skill dir
+python3 scripts/skills.py validate # check assets + manifest are up to date (CI)
```
## Security
diff --git a/README.md b/README.md
index 4a62124..d49a6ae 100644
--- a/README.md
+++ b/README.md
@@ -62,16 +62,16 @@ This approach:
### Manifest Management
-Generate manifest after adding/updating skills:
+Sync assets and generate manifest after adding/updating skills:
```bash
-python3 scripts/generate_manifest.py
+python3 scripts/skills.py
```
-Validate that manifest is up to date (for CI):
+Validate that assets and manifest are up to date (for CI):
```bash
-python3 scripts/generate_manifest.py validate
+python3 scripts/skills.py validate
```
The manifest is used by the CLI to discover available skills.
diff --git a/assets/databricks.png b/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/assets/databricks.png differ
diff --git a/assets/databricks.svg b/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index b28e975..ffd9ca8 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,14 +1,17 @@
{
"version": "2",
- "updated_at": "2026-04-02T10:44:39Z",
+ "updated_at": "2026-04-07T12:45:02Z",
"skills": {
"databricks-apps": {
"version": "0.1.1",
"description": "Databricks Apps development and deployment",
"experimental": false,
- "updated_at": "2026-04-02T10:44:20Z",
+ "updated_at": "2026-04-07T12:41:53Z",
"files": [
"SKILL.md",
+ "agents/openai.yaml",
+ "assets/databricks.png",
+ "assets/databricks.svg",
"references/appkit/appkit-sdk.md",
"references/appkit/frontend.md",
"references/appkit/lakebase.md",
@@ -26,9 +29,12 @@
"version": "0.1.0",
"description": "Core Databricks skill for CLI, auth, and data exploration",
"experimental": false,
- "updated_at": "2026-04-02T10:44:20Z",
+ "updated_at": "2026-04-07T12:41:53Z",
"files": [
"SKILL.md",
+ "agents/openai.yaml",
+ "assets/databricks.png",
+ "assets/databricks.svg",
"data-exploration.md",
"databricks-cli-auth.md",
"databricks-cli-install.md"
@@ -38,9 +44,12 @@
"version": "0.0.0",
"description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources",
"experimental": false,
- "updated_at": "2026-04-02T10:44:20Z",
+ "updated_at": "2026-04-07T12:44:49Z",
"files": [
"SKILL.md",
+ "agents/openai.yaml",
+ "assets/databricks.png",
+ "assets/databricks.svg",
"references/alerts.md",
"references/bundle-structure.md",
"references/deploy-and-run.md",
@@ -53,27 +62,36 @@
"version": "0.1.0",
"description": "Databricks Jobs orchestration and scheduling",
"experimental": false,
- "updated_at": "2026-04-02T10:44:20Z",
+ "updated_at": "2026-04-07T12:41:53Z",
"files": [
- "SKILL.md"
+ "SKILL.md",
+ "agents/openai.yaml",
+ "assets/databricks.png",
+ "assets/databricks.svg"
]
},
"databricks-lakebase": {
"version": "0.1.0",
"description": "Databricks Lakebase database development",
"experimental": false,
- "updated_at": "2026-04-02T10:44:20Z",
+ "updated_at": "2026-04-07T12:41:53Z",
"files": [
- "SKILL.md"
+ "SKILL.md",
+ "agents/openai.yaml",
+ "assets/databricks.png",
+ "assets/databricks.svg"
]
},
"databricks-pipelines": {
"version": "0.1.0",
"description": "Databricks Pipelines (DLT) for ETL and streaming",
"experimental": false,
- "updated_at": "2026-04-02T10:44:20Z",
+ "updated_at": "2026-04-07T12:41:53Z",
"files": [
"SKILL.md",
+ "agents/openai.yaml",
+ "assets/databricks.png",
+ "assets/databricks.svg",
"references/auto-cdc-python.md",
"references/auto-cdc-sql.md",
"references/auto-cdc.md",
diff --git a/scripts/generate_manifest.py b/scripts/skills.py
similarity index 52%
rename from scripts/generate_manifest.py
rename to scripts/skills.py
index 13c8301..b319438 100644
--- a/scripts/generate_manifest.py
+++ b/scripts/skills.py
@@ -1,14 +1,65 @@
#!/usr/bin/env python3
-"""Generate or validate manifest.json from skill directories."""
+"""Manage skills: sync shared assets, generate manifest, validate."""
import argparse
import json
import re
+import shutil
import sys
from datetime import datetime, timezone
from pathlib import Path
+SHARED_ASSETS = [
+ "assets/databricks.svg",
+ "assets/databricks.png",
+]
+
+SKILL_METADATA = {
+ "databricks-core": {
+ "description": "Core Databricks skill for CLI, auth, and data exploration",
+ "experimental": False,
+ },
+ "databricks-apps": {
+ "description": "Databricks Apps development and deployment",
+ "experimental": False,
+ },
+ "databricks-jobs": {
+ "description": "Databricks Jobs orchestration and scheduling",
+ "experimental": False,
+ },
+ "databricks-lakebase": {
+ "description": "Databricks Lakebase database development",
+ "experimental": False,
+ },
+ "databricks-dabs": {
+ "description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources",
+ "experimental": False,
+ },
+ "databricks-pipelines": {
+ "description": "Databricks Pipelines (DLT) for ETL and streaming",
+ "experimental": False,
+ },
+ "databricks-proto-first": {
+ "description": "Proto-first schema design for Databricks apps",
+ "experimental": False,
+ },
+}
+
+
+def iter_skill_dirs(repo_root: Path):
+ """Yield skill directories that contain SKILL.md."""
+ skills_dir = repo_root / "skills"
+ for item in sorted(skills_dir.iterdir()):
+ if not item.is_dir():
+ continue
+ if item.name.startswith(".") or item.name == "scripts":
+ continue
+ if not (item / "SKILL.md").exists():
+ continue
+ yield item
+
+
def extract_version_from_skill(skill_path: Path) -> str:
"""Extract version from SKILL.md frontmatter metadata."""
skill_md = skill_path / "SKILL.md"
@@ -17,7 +68,6 @@ def extract_version_from_skill(skill_path: Path) -> str:
content = skill_md.read_text()
- # parse YAML frontmatter
if not content.startswith("---"):
raise ValueError(f"SKILL.md in {skill_path} missing frontmatter")
@@ -27,7 +77,6 @@ def extract_version_from_skill(skill_path: Path) -> str:
frontmatter = content[3:end_idx]
- # extract version from metadata block
version_match = re.search(r'version:\s*["\']?([^"\'\n]+)["\']?', frontmatter)
if version_match:
return version_match.group(1).strip()
@@ -52,86 +101,111 @@ def get_skill_updated_at(skill_path: Path) -> str:
)
-SKILL_METADATA = {
- "databricks-core": {
- "description": "Core Databricks skill for CLI, auth, and data exploration",
- "experimental": False,
- },
- "databricks-apps": {
- "description": "Databricks Apps development and deployment",
- "experimental": False,
- },
- "databricks-jobs": {
- "description": "Databricks Jobs orchestration and scheduling",
- "experimental": False,
- },
- "databricks-lakebase": {
- "description": "Databricks Lakebase database development",
- "experimental": False,
- },
- "databricks-dabs": {
- "description": "Declarative Automation Bundles (DABs) for deploying and managing Databricks resources",
- "experimental": False,
- },
- "databricks-pipelines": {
- "description": "Databricks Pipelines (DLT) for ETL and streaming",
- "experimental": False,
- },
- "databricks-proto-first": {
- "description": "Proto-first schema design for Databricks apps",
- "experimental": False,
- },
-}
+# ---------------------------------------------------------------------------
+# Sync
+# ---------------------------------------------------------------------------
+
+def sync_assets(repo_root: Path) -> int:
+ """Copy shared assets from repo root into each skill directory.
+
+ Only writes when content differs. Uses shutil.copy2 to preserve mtime
+ from the source so that skill updated_at timestamps stay stable.
+
+ Returns count of files written.
+ """
+ for asset_rel in SHARED_ASSETS:
+ source = repo_root / asset_rel
+ if not source.exists():
+ raise ValueError(f"Missing shared asset '{asset_rel}' at repo root.")
+
+ synced = 0
+ for skill_dir in iter_skill_dirs(repo_root):
+ for asset_rel in SHARED_ASSETS:
+ source = repo_root / asset_rel
+ dest = skill_dir / asset_rel
+ dest.parent.mkdir(parents=True, exist_ok=True)
+
+ if dest.exists() and dest.read_bytes() == source.read_bytes():
+ continue
+ shutil.copy2(source, dest)
+ synced += 1
+
+ return synced
+
+
+def check_assets_synced(repo_root: Path) -> list[str]:
+ """Validate that all shared assets are present and up-to-date.
+
+ Returns a list of error messages (empty means all good).
+ """
+ errors: list[str] = []
+ for asset_rel in SHARED_ASSETS:
+ source = repo_root / asset_rel
+ if not source.exists():
+ errors.append(f"Missing shared asset '{asset_rel}' at repo root.")
+ continue
+
+ source_bytes = source.read_bytes()
+ for skill_dir in iter_skill_dirs(repo_root):
+ dest = skill_dir / asset_rel
+ if not dest.exists():
+ errors.append(f"Missing '{asset_rel}' in skill '{skill_dir.name}'.")
+ elif dest.read_bytes() != source_bytes:
+ errors.append(f"Stale '{asset_rel}' in skill '{skill_dir.name}'.")
+
+ return errors
+
+
+# ---------------------------------------------------------------------------
+# Manifest generation
+# ---------------------------------------------------------------------------
def generate_manifest(repo_root: Path) -> dict:
"""Generate manifest from skill directories."""
- # Load existing manifest to preserve base_revision fields
manifest_path = repo_root / "manifest.json"
existing_skills = {}
if manifest_path.exists():
existing_skills = json.loads(manifest_path.read_text()).get("skills", {})
skills = {}
- skills_dir = repo_root / "skills"
-
- for item in sorted(skills_dir.iterdir()):
- if not item.is_dir():
- continue
- if item.name.startswith(".") or item.name == "scripts":
- continue
-
- skill_md = item / "SKILL.md"
- if not skill_md.exists():
- continue
-
+ for skill_dir in iter_skill_dirs(repo_root):
files = sorted(
- str(f.relative_to(item))
- for f in item.rglob("*")
+ str(f.relative_to(skill_dir))
+ for f in skill_dir.rglob("*")
if f.is_file()
)
- if item.name not in SKILL_METADATA:
- raise ValueError(f"Missing SKILL_METADATA entry for skill '{item.name}'. Add it to SKILL_METADATA dict.")
+ if skill_dir.name not in SKILL_METADATA:
+ raise ValueError(
+ f"Missing SKILL_METADATA entry for skill '{skill_dir.name}'. "
+ "Add it to SKILL_METADATA dict."
+ )
- metadata = SKILL_METADATA[item.name]
+ openai_yaml = skill_dir / "agents" / "openai.yaml"
+ if not openai_yaml.exists():
+ raise ValueError(
+ f"Missing agents/openai.yaml in skill '{skill_dir.name}'. "
+ "Each skill must include Codex marketplace metadata."
+ )
+
+ metadata = SKILL_METADATA[skill_dir.name]
skill_entry = {
- "version": extract_version_from_skill(item),
+ "version": extract_version_from_skill(skill_dir),
"description": metadata.get("description", ""),
"experimental": metadata.get("experimental", False),
- "updated_at": get_skill_updated_at(item),
+ "updated_at": get_skill_updated_at(skill_dir),
"files": files,
}
if metadata.get("min_cli_version"):
skill_entry["min_cli_version"] = metadata["min_cli_version"]
- # Preserve base_revision from existing manifest
- existing = existing_skills.get(item.name, {})
+ existing = existing_skills.get(skill_dir.name, {})
if "base_revision" in existing:
skill_entry["base_revision"] = existing["base_revision"]
- skills[item.name] = skill_entry
+ skills[skill_dir.name] = skill_entry
return {
"version": "2",
@@ -140,6 +214,10 @@ def generate_manifest(repo_root: Path) -> dict:
}
+# ---------------------------------------------------------------------------
+# Validation
+# ---------------------------------------------------------------------------
+
def normalize_manifest(manifest: dict) -> dict:
"""Normalize manifest for comparison by excluding volatile fields."""
normalized = manifest.copy()
@@ -167,7 +245,6 @@ def validate_manifest(repo_root: Path) -> bool:
current_manifest = json.loads(manifest_path.read_text())
expected_manifest = generate_manifest(repo_root)
- # compare without timestamps
current_normalized = normalize_manifest(current_manifest)
expected_normalized = normalize_manifest(expected_manifest)
@@ -179,34 +256,71 @@ def validate_manifest(repo_root: Path) -> bool:
print(json.dumps(current_normalized, indent=2), file=sys.stderr)
return False
- print("manifest.json is up to date")
return True
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
def main() -> None:
- parser = argparse.ArgumentParser(description="Generate or validate manifest.json")
+ parser = argparse.ArgumentParser(
+ description="Manage skills: sync shared assets, generate manifest, validate."
+ )
parser.add_argument(
"mode",
nargs="?",
default="generate",
- choices=["generate", "validate"],
- help="Mode: generate (creates manifest.json, default) or validate (checks if up to date)",
+ choices=["sync", "generate", "validate"],
+ help=(
+ "sync: copy shared assets into each skill directory. "
+ "generate: sync + create manifest.json (default). "
+ "validate: check assets and manifest are up to date."
+ ),
)
args = parser.parse_args()
repo_root = Path(__file__).parent.parent
match args.mode:
+ case "sync":
+ synced = sync_assets(repo_root)
+ print(f"Synced {synced} asset(s)")
+
case "generate":
+ synced = sync_assets(repo_root)
+ print(f"Synced {synced} asset(s)")
+
manifest = generate_manifest(repo_root)
manifest_path = repo_root / "manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
print(f"Generated {manifest_path}")
- print(f"Found {len(manifest['skills'])} skill(s): {', '.join(manifest['skills'].keys())}")
+ print(
+ f"Found {len(manifest['skills'])} skill(s): "
+ f"{', '.join(manifest['skills'].keys())}"
+ )
case "validate":
- is_valid = validate_manifest(repo_root)
- sys.exit(0 if is_valid else 1)
+ ok = True
+
+ asset_errors = check_assets_synced(repo_root)
+ if asset_errors:
+ print("ERROR: Shared assets are out of sync:", file=sys.stderr)
+ for err in asset_errors:
+ print(f" - {err}", file=sys.stderr)
+ ok = False
+
+ if not validate_manifest(repo_root):
+ ok = False
+
+ if not ok:
+ print(
+ "\nRun `python3 scripts/skills.py generate` to fix.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ print("Everything is up to date.")
if __name__ == "__main__":
diff --git a/skills/databricks-apps/agents/openai.yaml b/skills/databricks-apps/agents/openai.yaml
new file mode 100644
index 0000000..1e3827e
--- /dev/null
+++ b/skills/databricks-apps/agents/openai.yaml
@@ -0,0 +1,7 @@
+interface:
+ display_name: "Databricks Apps"
+ short_description: "Apps development and deployment"
+ icon_small: "./assets/databricks.svg"
+ icon_large: "./assets/databricks.png"
+ brand_color: "#FF3621"
+ default_prompt: "Use $databricks-apps for Databricks Apps development and deployment."
diff --git a/skills/databricks-apps/assets/databricks.png b/skills/databricks-apps/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/skills/databricks-apps/assets/databricks.png differ
diff --git a/skills/databricks-apps/assets/databricks.svg b/skills/databricks-apps/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/skills/databricks-apps/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/skills/databricks-core/agents/openai.yaml b/skills/databricks-core/agents/openai.yaml
new file mode 100644
index 0000000..1b5f562
--- /dev/null
+++ b/skills/databricks-core/agents/openai.yaml
@@ -0,0 +1,7 @@
+interface:
+ display_name: "Databricks"
+ short_description: "CLI, auth, and data exploration"
+ icon_small: "./assets/databricks.svg"
+ icon_large: "./assets/databricks.png"
+ brand_color: "#FF3621"
+ default_prompt: "Use $databricks-core for Databricks CLI, auth, and data exploration."
diff --git a/skills/databricks-core/assets/databricks.png b/skills/databricks-core/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/skills/databricks-core/assets/databricks.png differ
diff --git a/skills/databricks-core/assets/databricks.svg b/skills/databricks-core/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/skills/databricks-core/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/skills/databricks-dabs/agents/openai.yaml b/skills/databricks-dabs/agents/openai.yaml
new file mode 100644
index 0000000..185f6e5
--- /dev/null
+++ b/skills/databricks-dabs/agents/openai.yaml
@@ -0,0 +1,7 @@
+interface:
+ display_name: "Databricks DABs"
+ short_description: "Declarative Automation Bundles for deploying and managing Databricks resources"
+ icon_small: "./assets/databricks.svg"
+ icon_large: "./assets/databricks.png"
+ brand_color: "#FF3621"
+ default_prompt: "Use $databricks-dabs for creating, deploying, and managing Databricks resources through Declarative Automation Bundles."
diff --git a/skills/databricks-dabs/assets/databricks.png b/skills/databricks-dabs/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/skills/databricks-dabs/assets/databricks.png differ
diff --git a/skills/databricks-dabs/assets/databricks.svg b/skills/databricks-dabs/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/skills/databricks-dabs/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/skills/databricks-jobs/agents/openai.yaml b/skills/databricks-jobs/agents/openai.yaml
new file mode 100644
index 0000000..9dd4b36
--- /dev/null
+++ b/skills/databricks-jobs/agents/openai.yaml
@@ -0,0 +1,7 @@
+interface:
+ display_name: "Databricks Jobs"
+ short_description: "Jobs orchestration and scheduling"
+ icon_small: "./assets/databricks.svg"
+ icon_large: "./assets/databricks.png"
+ brand_color: "#FF3621"
+ default_prompt: "Use $databricks-jobs for Databricks Jobs orchestration and scheduling."
diff --git a/skills/databricks-jobs/assets/databricks.png b/skills/databricks-jobs/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/skills/databricks-jobs/assets/databricks.png differ
diff --git a/skills/databricks-jobs/assets/databricks.svg b/skills/databricks-jobs/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/skills/databricks-jobs/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/skills/databricks-lakebase/agents/openai.yaml b/skills/databricks-lakebase/agents/openai.yaml
new file mode 100644
index 0000000..541b208
--- /dev/null
+++ b/skills/databricks-lakebase/agents/openai.yaml
@@ -0,0 +1,7 @@
+interface:
+ display_name: "Databricks Lakebase"
+ short_description: "Lakebase database development"
+ icon_small: "./assets/databricks.svg"
+ icon_large: "./assets/databricks.png"
+ brand_color: "#FF3621"
+ default_prompt: "Use $databricks-lakebase for Databricks Lakebase database development."
diff --git a/skills/databricks-lakebase/assets/databricks.png b/skills/databricks-lakebase/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/skills/databricks-lakebase/assets/databricks.png differ
diff --git a/skills/databricks-lakebase/assets/databricks.svg b/skills/databricks-lakebase/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/skills/databricks-lakebase/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/skills/databricks-pipelines/agents/openai.yaml b/skills/databricks-pipelines/agents/openai.yaml
new file mode 100644
index 0000000..a6d7111
--- /dev/null
+++ b/skills/databricks-pipelines/agents/openai.yaml
@@ -0,0 +1,7 @@
+interface:
+ display_name: "Databricks Pipelines"
+ short_description: "Pipelines for ETL and streaming"
+ icon_small: "./assets/databricks.svg"
+ icon_large: "./assets/databricks.png"
+ brand_color: "#FF3621"
+ default_prompt: "Use $databricks-pipelines for Databricks Pipelines ETL and streaming."
diff --git a/skills/databricks-pipelines/assets/databricks.png b/skills/databricks-pipelines/assets/databricks.png
new file mode 100644
index 0000000..263fe98
Binary files /dev/null and b/skills/databricks-pipelines/assets/databricks.png differ
diff --git a/skills/databricks-pipelines/assets/databricks.svg b/skills/databricks-pipelines/assets/databricks.svg
new file mode 100644
index 0000000..9d19110
--- /dev/null
+++ b/skills/databricks-pipelines/assets/databricks.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file