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
4 changes: 2 additions & 2 deletions great_docs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
"skill": {
"enabled": True,
"file": None, # Path to a hand-written SKILL.md (overrides auto-generation)
"well_known": True, # Also serve at /.well-known/skills/default/SKILL.md
"well_known": True, # Also serve at /.well-known/agent-skills/{name}/SKILL.md + index.json
"gotchas": [], # List of gotcha strings for the Gotchas section
"best_practices": [], # List of best-practice strings
"decision_table": [], # Manual rows: [{"need": "...", "use": "..."}]
Expand Down Expand Up @@ -567,7 +567,7 @@ def skill_file(self) -> str | None:

@property
def skill_well_known(self) -> bool:
"""Check if .well-known/skills/default/SKILL.md should be generated."""
"""Check if .well-known/agent-skills/ discovery files should be generated."""
return self.get("skill.well_known", True)

@property
Expand Down
102 changes: 65 additions & 37 deletions great_docs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11199,9 +11199,8 @@ def _inject_version_selector(self, quarto_yml: Path) -> None:
"""
Inject the version selector widget into the Quarto config.

Adds the version-selector.js script and a `<meta name="gd-version-map">`
tag containing the serialized `_version_map.json` data so the widget
can resolve versions client-side.
Adds the version-selector.js script and a `<meta name="gd-version-map">` tag containing the
serialized `_version_map.json` data so the widget can resolve versions client-side.
"""
import html as html_mod

Expand Down Expand Up @@ -11721,15 +11720,16 @@ def _generate_skill_md(self) -> None:
"""
Generate a SKILL.md file conforming to the Agent Skills specification.

Creates a skill file that gives AI coding agents structured context about the
documented packageits capabilities, API decision table, gotchas, and links
to comprehensive documentation.
Creates a skill file that gives AI coding agents structured context about the documented
package: its capabilities, API decision table, gotchas, and links to comprehensive
documentation.

If the user has provided a hand-written SKILL.md via `skill.file` in
`great-docs.yml`, that file is copied verbatim instead of generating one.
If the user has provided a hand-written SKILL.md via `skill.file` in `great-docs.yml`, that
file is copied verbatim instead of generating one.

The generated file is written to `<docs>/skill.md` and optionally copied to
`<docs>/.well-known/skills/default/SKILL.md` for auto-discovery.
The generated file is written to `<docs>/skill.md` and optionally published at
`<docs>/.well-known/agent-skills/<name>/SKILL.md` with a discovery manifest at
`<docs>/.well-known/agent-skills/index.json` for `npx skills add` auto-discovery.
"""
import shutil

Expand Down Expand Up @@ -11939,25 +11939,24 @@ def _generate_skill_md(self) -> None:

def _generate_skills_page(self, skill_path: "Path", *, skill_dir: "Path | None" = None) -> None:
"""
Generate a `skills.qmd` page that renders the raw SKILL.md content in a
styled, human-readable format.
Generate a `skills.qmd` page that renders the raw SKILL.md content in a styled,
human-readable format.

The page displays the skill's YAML frontmatter as a highlighted block and
the Markdown body with color-coded headings and monospaced fonta halfway
point between raw Markdown and fully rendered HTML.
The page displays the skill's YAML frontmatter as a highlighted block and the Markdown body
with color-coded headings and monospaced font: a halfway point between raw Markdown and
fully rendered HTML.

When *skill_dir* points to a curated skill directory that contains companion
subdirectories (`references/`, `scripts/`, `assets/`), a directory
tree is rendered before the SKILL.md and each `.md` / `.sh` file is
displayed in its own text area with anchor links.
When *skill_dir* points to a curated skill directory that contains companion subdirectories
(`references/`, `scripts/`, `assets/`), a directory tree is rendered before the SKILL.md and
each `.md` / `.sh` file is displayed in its own text area with anchor links.

Parameters
----------
skill_path
Path to the skill.md file to render.
skill_dir
Optional path to the curated skill directory containing SKILL.md and
its companion subdirectories.
Optional path to the curated skill directory containing SKILL.md and its companion
subdirectories.
"""
import re

Expand Down Expand Up @@ -12257,7 +12256,13 @@ def _t(key: str, fallback: str) -> str:

def _place_well_known_skill(self, skill_path: "Path") -> None:
"""
Copy the SKILL.md to .well-known/skills/default/ for auto-discovery.
Copy the SKILL.md to .well-known/ directories for auto-discovery.

Places the skill at two well-known locations:

1. `.well-known/agent-skills/{name}/SKILL.md` with an `index.json` discovery manifest: the
preferred path used by `npx skills add`.
2. `.well-known/skills/default/SKILL.md`: legacy fallback.

Parameters
----------
Expand All @@ -12269,11 +12274,37 @@ def _place_well_known_skill(self, skill_path: "Path") -> None:
if not self._config.skill_well_known:
return

# Parse frontmatter to extract skill name and description
content = skill_path.read_text(encoding="utf-8")
fm, _ = self._split_frontmatter(content)
skill_name = fm.get("name", "default")
skill_description = fm.get("description", "")
if isinstance(skill_description, str):
skill_description = skill_description.strip()

# --- Preferred: .well-known/agent-skills/{name}/SKILL.md + index.json ---
agent_skills_dir = self.project_path / ".well-known" / "agent-skills" / skill_name
agent_skills_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(skill_path, agent_skills_dir / "SKILL.md")

index_data = {
"skills": [
{
"name": skill_name,
"description": skill_description,
"files": ["SKILL.md"],
}
]
}
index_path = self.project_path / ".well-known" / "agent-skills" / "index.json"
with open(index_path, "w", encoding="utf-8") as f:
json.dump(index_data, f, indent=2)
f.write("\n")

# --- Legacy: .well-known/skills/default/SKILL.md ---
well_known_dir = self.project_path / ".well-known" / "skills" / "default"
well_known_dir.mkdir(parents=True, exist_ok=True)

dest = well_known_dir / "SKILL.md"
shutil.copy2(skill_path, dest)
shutil.copy2(skill_path, well_known_dir / "SKILL.md")

# ══════════════════════════════════════════════════════════════════════════
# SEO GENERATION METHODS
Expand Down Expand Up @@ -12337,8 +12368,8 @@ def _generate_sitemap_xml(self) -> None:
"""
Generate a sitemap.xml file for search engine indexing.

Creates an XML sitemap at _site/sitemap.xml with proper priorities
and change frequencies based on page type.
Creates an XML sitemap at _site/sitemap.xml with proper priorities and change frequencies
based on page type.
"""
if not self._config.sitemap_enabled:
return # pragma: no cover
Expand Down Expand Up @@ -12476,9 +12507,9 @@ def _resolve_social_card_image_url(self) -> str | None:
"""
Resolve the social card image to an absolute URL.

If the configured image is a local file path, copies it to the build
directory and returns the site-relative path. If it's already a URL,
returns it as-is. If no image is configured, returns None.
If the configured image is a local file path, copies it to the build directory and returns
the site-relative path. If it's already a URL, returns it as-is. If no image is configured,
returns `None`.

Returns
-------
Expand Down Expand Up @@ -12617,9 +12648,7 @@ def _annotate(pages: list[dict[str, object]]) -> list[dict[str, object]]:
total_seconds += ver_total
versions_payload[tag] = {
"seconds": ver_total,
"pages": sorted(
_annotate(timings), key=lambda t: t["seconds"], reverse=True
),
"pages": sorted(_annotate(timings), key=lambda t: t["seconds"], reverse=True),
}
payload["total_seconds"] = round(total_seconds, 3)
payload["versions"] = versions_payload
Expand Down Expand Up @@ -12696,8 +12725,8 @@ def _get_user_guide_text_for_llms(self) -> str:
"""
Get User Guide content formatted for llms-full.txt.

Reads all user guide .qmd files in order and extracts their content,
stripping YAML frontmatter but preserving the document structure.
Reads all user guide .qmd files in order and extracts their content, stripping YAML
frontmatter but preserving the document structure.

Returns
-------
Expand Down Expand Up @@ -13501,8 +13530,7 @@ class Result:
mock_modified = _expand_mock_cells(self.project_path)
if mock_modified:
log.detail(
f"Expanded {len(mock_modified)} mock-code cell(s): "
+ ", ".join(mock_modified)
f"Expanded {len(mock_modified)} mock-code cell(s): " + ", ".join(mock_modified)
)

# Get environment with QUARTO_PYTHON set
Expand Down
20 changes: 18 additions & 2 deletions tests/test_great_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -38310,7 +38310,7 @@ def test_generate_skill_md_basic():


def test_generate_skill_md_well_known():
"""Test that SKILL.md is copied to .well-known/ directory."""
"""Test that SKILL.md is copied to .well-known/ directories with index.json."""
with tempfile.TemporaryDirectory() as tmp_dir:
docs = GreatDocs(project_path=tmp_dir)

Expand Down Expand Up @@ -38339,14 +38339,30 @@ def test_generate_skill_md_well_known():

docs._generate_skill_md()

# Check .well-known placement
# Check legacy .well-known/skills/default placement
well_known = great_docs_dir / ".well-known" / "skills" / "default" / "SKILL.md"
assert well_known.exists()

# Content should match skill.md
skill_md = great_docs_dir / "skill.md"
assert skill_md.read_text() == well_known.read_text()

# Check preferred .well-known/agent-skills/{name}/SKILL.md placement
agent_skill = great_docs_dir / ".well-known" / "agent-skills" / "test-package" / "SKILL.md"
assert agent_skill.exists()
assert skill_md.read_text() == agent_skill.read_text()

# Check index.json discovery manifest
index_json = great_docs_dir / ".well-known" / "agent-skills" / "index.json"
assert index_json.exists()
import json

index_data = json.loads(index_json.read_text())
assert "skills" in index_data
assert len(index_data["skills"]) == 1
assert index_data["skills"][0]["name"] == "test-package"
assert "SKILL.md" in index_data["skills"][0]["files"]


def test_generate_skill_md_disabled():
"""Test that SKILL.md is not generated when skill.enabled is false."""
Expand Down
2 changes: 1 addition & 1 deletion user_guide/05-configuration.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ reference:

Great Docs supports the [Agent Skills](https://agentskills.io/) open standard, which gives AI coding agents (Claude Code, GitHub Copilot, Cursor, Codex, Gemini CLI, and 30+ others) structured context about your package so they can write better code when using it.

During the build, a `skill.md` file is placed in your docs directory (and at `.well-known/skills/default/SKILL.md` for auto-discovery). Users of your package can then install the skill into their agent of choice:
During the build, a `skill.md` file is placed in your docs directory and published at `.well-known/agent-skills/<name>/SKILL.md` with a discovery manifest at `.well-known/agent-skills/index.json` (plus a legacy copy at `.well-known/skills/default/SKILL.md`). Users of your package can then install the skill into their agent of choice:

```bash
npx skills add https://your-docs-site.com
Expand Down
Loading