Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,6 @@ dmypy.json

# Pyre type checker
.pyre/

# Installed agent skills (see skills/install.py)
.claude/
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ dev = [
"pytest~=7.2.2",
"mypy==1.9.0",
"ruff==0.15.6",
# skills installer dashboard (skills/installer/tui.py); needs Python >=3.8.1 (textual 6.2.x; 6.3+ requires >=3.9)
"textual>=6.2, <7",
# type stubs
"types-lxml",
"types-python-dateutil>=2.8, <3",
Expand Down
89 changes: 89 additions & 0 deletions skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Agent skills

Playbooks and skills meant to be **executed by AI coding agents**, not human contributors.

The repo stays **agent-neutral**: we track the skill *sources* here, but we do **not** commit any
agent's config directory (`.claude/` is gitignored). Each developer installs the skills they want into
their own agent with the installer below.

## Skills

| Skill | What it does |
|-------|--------------|
| [`review-publisher`](review-publisher/SKILL.md) | Reviews a publisher PR (new publisher or parser-version change): crawls live articles to verify the extracted `ArticleBody` mirrors the real article, checks `VALID_UNTIL` / version bumps / `validate=False` / `free_access`, and drafts a single GitHub review. |

`install.py` (the installer launcher) and the `installer/` package behind it, plus this
`README.md`, round out the folder.

## Layout

Each skill is a **self-contained folder** — a `SKILL.md`, the `PLAYBOOK.md` it points at, and any helper
scripts under `scripts/` — so the whole thing can be copied anywhere and still work. The installer copies
the **entire folder**, so anything you drop in ships automatically; if the folder has a `requirements.txt`
at its root, the installer also pip-installs it into your current interpreter. Install is **atomic**: if
that pip step fails, the copied folder is rolled back (the cell flashes `!`, then shows `·`) so an install
never half-lands — `✓` always means the skill *and* its deps are in.

Two path conventions keep a copied folder working:

- `SKILL.md` links to `PLAYBOOK.md` as a **sibling**, so the link survives being copied into `.claude/`.
- **`${CLAUDE_SKILL_DIR}` is substituted by Claude Code in `SKILL.md` content only** — it is *not* an
environment variable, and other bundled files are read verbatim. So `SKILL.md` states the skill's own
directory once (via the placeholder) and tells the agent to substitute it into the `<skill>` paths
the `PLAYBOOK.md` commands use. A bare `scripts/…` path would wrongly assume the working dir is the
skill dir; a `${CLAUDE_SKILL_DIR}` outside SKILL.md would expand to nothing in a shell.

References from `PLAYBOOK.md` to *other* repo docs use **repo-root-relative** links
(e.g. `/docs/attribute_guidelines.md`): they resolve on GitHub and from the repo root, which is where
the review skill runs anyway.

## Install

The installer is a full-screen **status dashboard**. Run it with no arguments:

```bash
python skills/install.py
```

It shows a live matrix of *skills* × *scopes* for the selected agent. Each row is a skill; the
**Project** and **User** columns show whether it's installed (`✓`) or not (`·`). Move the cursor with
`↑`/`↓`, then toggle a scope to act on the highlighted skill — `p` for project, `u` for user. Toggling
installs the skill if it's absent and uninstalls it if it's present; the matrix updates in place, so you
never have to re-run. `r` refreshes from disk and `q` quits.

The two scopes:

- **project** installs to `./.claude/skills/`. The skill is scoped to this repo, where it's relevant,
and never pushed (`.claude/` is in the committed `.gitignore`).
- **user** installs to `~/.claude/skills/`, available in every project. Nothing touches the repo. (The
review skill references repo files, so it only makes sense while your working directory is the Fundus
repo regardless.)

The installer needs the `textual` package, which ships in the project's `dev` extra:

```bash
pip install -e .[dev]
```

Re-install (toggle off, then on) after editing a skill source to refresh the installed copy. Restart
your agent so it picks up a newly installed skill.

## Supported agents

| Agent | Status | Target |
|-------|--------|--------|
| **Claude Code** (`claude`) | supported | `.claude/skills/<name>/` |
| Codex, Cursor, … | not yet | no 1:1 `SKILL.md` concept; would be a best-effort mapping (e.g. a Codex prompt or a `.cursor/rules` file). Add to `available_agents()` in `installer/core.py` when wanted. |

## Adding a skill

Create `skills/<name>/` with a `SKILL.md` (frontmatter `name`/`description`) and a `PLAYBOOK.md`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having not too much experience with Markdown, I didn't know what exactly you meant with frontmatter until looking into the review-skill PR. Maybe a template or short example might make adding the first skill simpler.

it links to as a sibling; put helper scripts under `scripts/`. The installer **discovers skills from
the folder layout** (any directory here containing a `SKILL.md`), so there is nothing to register —
run `python skills/install.py` and the new skill shows up as a row. If the skill needs packages
beyond Fundus, drop a `requirements.txt` at its root — the installer pip-installs it on install (and
leaves it in place on uninstall).

Keep the path conventions above: state the skill's own directory in `SKILL.md` via
`${CLAUDE_SKILL_DIR}` (it is substituted nowhere else), and point cross-doc links at repo-root paths
(`/docs/...`).
49 changes: 49 additions & 0 deletions skills/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Install Fundus agent skills into your coding agent's config directory.

The skill *sources* live under ``skills/``, tracked in the repo, so the repo itself stays
agent-neutral (we don't commit any agent's config dir). Each skill is a self-contained
folder (``SKILL.md`` + ``PLAYBOOK.md`` + any bundled ``scripts/``); installing copies the
whole folder into your agent's config dir, which is gitignored (project scope) or lives
outside the repo entirely (user scope). Re-install after editing a source to refresh the
installed copy.

Run it with no arguments for a full-screen status dashboard::

python skills/install.py

The dashboard is a live matrix of *skills* × *scopes* for the selected agent — toggle a
scope on the highlighted skill to (un)install it in place. It needs the ``textual`` package
(it ships in the project's ``dev`` extra: ``pip install -e .[dev]``).

This file is a thin launcher; the install logic lives in the ``installer`` package next to
it (``installer/core.py`` is stdlib-only, ``installer/tui.py`` is the Textual dashboard).
"""

from __future__ import annotations

import sys
from pathlib import Path


def main() -> int:
# Allow `python skills/install.py` to find the sibling `installer` package regardless
# of the current working directory.
sys.path.insert(0, str(Path(__file__).resolve().parent))
try:
from installer.tui import run
except ModuleNotFoundError as exc:
if exc.name in {"textual", "rich"}:
print(
"The skill installer dashboard needs the 'textual' package.\n"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It might be more helpful to not selectively test for textual and one of it's dependencies here, but rather either:

  • always suggest running the dev extra install or
  • check for all necessary dependencies (i.e. platformdirs is also essential) and
    • explicitely name the missing package or
    • rephrase the error message to say needs the 'textual' package and all dependencies.

"Install the project's dev extra: pip install -e .[dev]",
file=sys.stderr,
)
return 1
raise
run()
return 0


if __name__ == "__main__":
raise SystemExit(main())
30 changes: 30 additions & 0 deletions skills/installer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Fundus skill installer.

:mod:`installer.core` is stdlib-only (discovery + install logic); :mod:`installer.tui` adds
the Textual dashboard. Importing this package pulls in only the core, so it stays usable
without textual installed.
"""

from __future__ import annotations

from .core import (
REPO_ROOT,
SCOPES,
SKILLS_DIR,
Agent,
InstallResult,
Skill,
available_agents,
discover_skills,
)

__all__ = [
"REPO_ROOT",
"SCOPES",
"SKILLS_DIR",
"Agent",
"InstallResult",
"Skill",
"available_agents",
"discover_skills",
]
Loading