Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gen.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"source_commit": "1c24dc7326ccac3583eae4035f2281a2656c68cd"
"source_commit": "57d814360e4e10c618760acd0605983b8be3594f"
}
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ Every skill — stable (`skills/<name>/`) or experimental (`experimental/<name>/

**`SKILL.md`** is what every coding agent reads. Frontmatter carries `name`, `description`, and optional `metadata.version` + `parent`.

**Referencing other files**: agents load skill files by path, so write a real path, never a bare file name. Markdown links resolve relative to the file they are in (`[x](references/x.md)` from `SKILL.md`, `[y](y.md)` between references). Backticked mentions of the skill's own files use the path from the skill root (`` `references/x.md` ``). `validate` rejects references that resolve to no file.

**`agents/openai.yaml`** is Codex CLI's plugin-marketplace metadata format: `display_name`, `short_description`, `icon_small`, `icon_large`, `brand_color`, `default_prompt`. It controls how the skill renders in Codex's in-app marketplace. Other agents ignore this file. The repo ships it for every skill so the manifest is a single feed for all agents.

**`assets/databricks.{svg,png}`** are the icons referenced by `agents/openai.yaml`'s `icon_small` / `icon_large`. Identical across all skills (the Databricks logo); `scripts/skills.py` copies them in from the repo-root `/assets/`.
Expand Down
3 changes: 3 additions & 0 deletions scripts/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@
_DESCRIPTION_LINE_RE,
_description_from_frontmatter,
check_skill_frontmatter,
_MD_LINK_RE,
_MD_CODE_REF_RE,
check_skill_file_references,
_HOOK_PY_RE,
_check_hook_wiring,
check_cursor_plugin,
Expand Down
11 changes: 11 additions & 0 deletions scripts/skillsgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
check_cursor_plugin,
check_plugin_components,
check_routing_tables,
check_skill_file_references,
check_skill_frontmatter,
)

Expand Down Expand Up @@ -125,6 +126,16 @@ def main() -> None:
print(f" - {err}", file=sys.stderr)
ok = False

reference_errors = check_skill_file_references(repo_root)
if reference_errors:
print(
"ERROR: skill files reference paths that do not exist:",
file=sys.stderr,
)
for err in reference_errors:
print(f" - {err}", file=sys.stderr)
ok = False

if not validate_manifest(repo_root):
ok = False

Expand Down
55 changes: 55 additions & 0 deletions scripts/skillsgen/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,61 @@ def check_skill_frontmatter(repo_root: Path) -> list[str]:
return errors


_MD_LINK_RE = re.compile(r"\]\(([^)\s]+?\.md)(?:#[^)]*)?\)")
_MD_CODE_REF_RE = re.compile(r"`([^`\s]+?\.md)`")


def check_skill_file_references(repo_root: Path) -> list[str]:
"""Flag `.md` references inside a skill that point at no file.

Agents load skill files by path, so a reference they cannot turn into a
real path makes them guess, often into the wrong skill (a bare
`appkit-cheatsheet.md` in databricks-app-design was read as
databricks-apps/references/appkit/appkit-cheatsheet.md). Two shapes:

- a markdown link must resolve relative to the file containing it;
- a backticked name of one of the skill's own files must be a path that
resolves from the skill root or from the containing file. Backticked
names the skill does not ship (a user's `agent.md`, AppKit's
`./docs/...`) are not skill references and are skipped.

Fenced code blocks are skipped. Returns a list of error strings.
"""
errors: list[str] = []
for skill_dir in iter_all_skill_dirs(repo_root):
md_files = sorted(skill_dir.rglob("*.md"))
own_paths_by_name: dict[str, list[str]] = {}
for md in md_files:
own_paths_by_name.setdefault(md.name, []).append(
md.relative_to(skill_dir).as_posix()
)
for md in md_files:
rel = md.relative_to(repo_root)
in_fence = False
for lineno, line in enumerate(md.read_text(encoding="utf-8").splitlines(), 1):
if line.lstrip().startswith("```"):
in_fence = not in_fence
continue
if in_fence:
continue
for target in _MD_LINK_RE.findall(line):
if "://" in target:
continue
if not (md.parent / target).resolve().is_file():
errors.append(f"{rel}:{lineno}: link target '{target}' does not exist.")
for ref in _MD_CODE_REF_RE.findall(line):
candidates = own_paths_by_name.get(Path(ref).name)
if not candidates:
continue
if (skill_dir / ref).resolve().is_file() or (md.parent / ref).resolve().is_file():
continue
errors.append(
f"{rel}:{lineno}: '{ref}' is not a path to a file in this skill; "
f"use {' or '.join(repr(c) for c in candidates)}."
)
return errors


# Any hooks/*.py mentioned in a hooks wiring file, regardless of how the
# platform prefixes the path (${CLAUDE_PLUGIN_ROOT}/, plugin-root-relative, …).
_HOOK_PY_RE = re.compile(r"hooks/[\w.-]+\.py")
Expand Down
6 changes: 3 additions & 3 deletions skills/databricks-app-design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ Design advice that doesn't name a real component is incomplete. Always end at a

## Workflow
1. **Frame** — audience, the decision/question, refresh cadence, device, primary task. One sentence.
2. **Genre** — pick the closest from `dashboard-patterns.md` (static / analytic / magazine / infographic / repository / embedded mini). State it.
2. **Genre** — pick the closest from `references/dashboard-patterns.md` (static / analytic / magazine / infographic / repository / embedded mini). State it.
3. **Compose** — choose content + composition patterns (data abstraction, meta-info, layout, interaction, color). Make the tradeoff explicit: what's summarized, hidden, paginated, or made interactive — and why.
4. **Apply notation** — run the relevant `ibcs-notation.md` rules: message-in-title, scenario marks (actual/PY/plan/forecast), honest scales, semantic color. On any chart-vocabulary conflict, **IBCS wins** (see the conflict note in that file).
5. **Bind to components** — map every element to a primitive that's actually **exported from `@databricks/appkit` / `@databricks/appkit-ui`** (see `appkit-cheatsheet.md`); never cite a component AppKit doesn't ship. There's no prebuilt KPI/trend/distribution card — compose those from primitives, following the notation rules. Use `colorPalette` + semantic tokens, never hardcoded hex. Bind data with `useAnalyticsQuery`/`queryKey` + `sql.*` params.
4. **Apply notation** — run the relevant `references/ibcs-notation.md` rules: message-in-title, scenario marks (actual/PY/plan/forecast), honest scales, semantic color. On any chart-vocabulary conflict, **IBCS wins** (see the conflict note in that file).
5. **Bind to components** — map every element to a primitive that's actually **exported from `@databricks/appkit` / `@databricks/appkit-ui`** (see `references/appkit-cheatsheet.md`); never cite a component AppKit doesn't ship. There's no prebuilt KPI/trend/distribution card — compose those from primitives, following the notation rules. Use `colorPalette` + semantic tokens, never hardcoded hex. Bind data with `useAnalyticsQuery`/`queryKey` + `sql.*` params.
6. **Cover the states** — every data view must handle loading / empty / error / partial (see checklist).
7. **Review** — run the checklists in both reference files; lead critiques with the highest-impact comprehension or integrity issue, citing the affected component/file.

Expand Down
4 changes: 2 additions & 2 deletions skills/databricks-app-design/references/genie-ai-trust.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ useEffect(() => { fetch("/api/whoami").then(r => (r.ok ? r.json() : null)).then(
```
**Whether queries actually run as that user (OBO) depends on config:** a default `genie()` runs as the
app's *service principal*. To run on-behalf-of the user, declare `user_api_scopes: [dashboards.genie]`
in `databricks.yml` (see the parent `databricks-apps` `genie.md`). Only claim OBO in the UI when that's
in `databricks.yml` (see the parent skill's [genie.md](../../databricks-apps/references/appkit/genie.md)). Only claim OBO in the UI when that's
wired — otherwise disclose service-principal execution (see #5).

Register the plugin with `genie()` (no args); it reads `DATABRICKS_GENIE_SPACE_ID` from the env, per the
parent `genie.md`. (Multi-space config: `npx @databricks/appkit docs ./docs/plugins/genie.md`.)
parent skill's [genie.md](../../databricks-apps/references/appkit/genie.md). (Multi-space config: `npx @databricks/appkit docs ./docs/plugins/genie.md`.)

## 2. Show the generated SQL (never hide how the answer was produced)
`useGenieChat` messages carry attachments with the generated query (exact shape is versioned — see
Expand Down
52 changes: 52 additions & 0 deletions tests/skills_generator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,58 @@ def test_short_plain_multiline_description_ok(self):
self.assertEqual(skills.check_skill_frontmatter(root), [])


class SkillFileReferenceTest(unittest.TestCase):
def _make_skill(self, root: Path, body: str, reference: str = "") -> None:
skill_dir = root / "skills" / "databricks-demo"
(skill_dir / "references").mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
f"---\nname: databricks-demo\ndescription: Demo.\n---\n{body}\n"
)
(skill_dir / "references" / "cheatsheet.md").write_text(reference)
(skill_dir / "references" / "patterns.md").write_text("")

def test_repo_skills_are_clean(self):
self.assertEqual(skills.check_skill_file_references(_REPO), [])

def test_bare_name_of_own_file_flagged(self):
# The databricks-app-design failure: agents resolved the bare name
# into the wrong skill.
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(root, "See `cheatsheet.md`.")
errors = skills.check_skill_file_references(root)
self.assertEqual(len(errors), 1, errors)
self.assertIn("'references/cheatsheet.md'", errors[0])

def test_root_and_file_relative_paths_ok(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(
root,
"See `references/cheatsheet.md` and [patterns](references/patterns.md#top).",
reference="Pairs with `patterns.md` and [patterns](patterns.md).",
)
self.assertEqual(skills.check_skill_file_references(root), [])

def test_broken_link_flagged(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(root, "", reference="See [x](references/patterns.md).")
errors = skills.check_skill_file_references(root)
self.assertEqual(len(errors), 1, errors)
self.assertIn("link target 'references/patterns.md'", errors[0])

def test_unrelated_names_urls_and_fences_ignored(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(
root,
"Edit `server/agent.md`; see [docs](https://example.com/cheatsheet.md).\n"
"```\ncat cheatsheet.md `cheatsheet.md`\n```",
)
self.assertEqual(skills.check_skill_file_references(root), [])


class BundleTest(unittest.TestCase):
"""The per-provider bundles under plugins/databricks/<provider>/."""

Expand Down
Loading