diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 384440b..111060c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,20 +1,25 @@ -name: Deploy Documentation +name: Documentation on: push: branches: [main] + pull_request: + branches: [main, develop] permissions: contents: read pages: write id-token: write +# Keyed by ref so concurrent PR doc builds do not serialize behind each other, +# while repeated pushes to the same branch still supersede one another. concurrency: - group: "pages" + group: pages-${{ github.ref }} cancel-in-progress: false jobs: - build: + # Job id is `docs` so it satisfies the required status check of that name. + docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -30,20 +35,24 @@ jobs: - name: Install dependencies run: uv sync --extra docs + # --strict fails on broken internal links, which is how the dangling + # agent-page links were caught rather than shipped. - name: Build docs - run: uv run mkdocs build + run: uv run mkdocs build --strict - name: Upload artifact + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v3 with: path: ./site deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: build + needs: docs steps: - name: Deploy to GitHub Pages id: deployment diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a17c945 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,99 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this +project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). While the +version is below 1.0.0, breaking changes are released in a minor bump. + +## [0.3.0] - 2026-07-24 + +A library-wide audit of all three pillars — skills, agents, and archetypes — measured +against Anthropic's Agent Skills guidance. Always-resident skill context is down 73%, +the agents concept is gone, every archetype generates a project that runs, and the +authoring rules are now enforced in CI rather than by discipline. + +### Removed + +- **The `agents/` directory and the agent concept.** The six "agents" were never Claude + Code subagents: four had no YAML frontmatter (so they installed with their H1 as the + entire description and were effectively untriggerable), all six installed into + `.claude/skills/` rather than `.claude/agents/`, `agent.toml` was never parsed by any + code path, and both "blocking" `action.yml` files ran `pixi` against a repo with no + `pixi.toml`. Roughly 77% of their content duplicated existing skills, often diverging + from them. GSD owns agency; whet owns domain knowledge, and knowledge is a skill. +- **`whet install --agents-only` and `--skills-only` flags**, along with + `discover_agents()` and the `agents_dir` config field. +- **Skills `dvc` and `cv-model-selection`.** Guidance that referenced DVC as a generic + concept was rewritten tool-neutral rather than left pointing at a deleted skill. + +### Added + +- **`model-evaluation`** — detection mAP/IoU via `supervision`, confusion matrices, + per-class and per-size breakdowns, deployment-threshold selection, per-slice failure + analysis, and eval-as-CI regression gates. +- **`pydantic-ai`** — typed LLM/VLM structured outputs and VLM-in-the-loop auto-labeling. +- **`data-pipelines`** — storage-format selection, group-aware leakage-preventing dataset + splitting, schema evolution, and data-quality validation. +- **`whet install --prune`** — removes skills that no longer exist upstream. Scoped by a + `.whet-manifest.json` recording what whet installed, so skills placed in the same + directory by other tools are never touched. +- **`whet install --include-extras`** and a `tier` field in `skill.toml`. Skills outside + the flagship path are opt-in: `aws-sagemaker`, `github-repo-setup`, `gradio`, + `huggingface`, `kubernetes`, `mlflow`, `vscode`. +- **`whet init --with-recommended` / `--no-skills`.** +- **Progressive disclosure** across the library: 29 of 32 skills are now a thin index + plus a `references/` directory loaded on demand — 155 reference files in total. +- **CI enforcement.** A new `archetypes` job renders every archetype and runs the + toolchain against the generated project, plus guards for skill-authoring rules + (line ceiling, reference-link integrity, orphaned references, one-level-deep + references, tables of contents, trigger-first and third-person descriptions, + listing budget) and for archetype composition and documented directory trees. + +### Changed + +- **`whet init` now installs the archetype's skills** (with their `references/`) instead + of printing a `whet add ...` hint. An archetype's value over a folder copy is the skill + set it composes, so leaving that as homework made `[skills]` advisory. +- **Skill `pydantic-strict` renamed to `pydantic`.** +- **All skill descriptions rewritten** as third-person, trigger-first "use this skill + when…" statements with explicit disambiguation between adjacent skills. They were topic + summaries, which under-trigger. +- **Every archetype template now generates a project that runs.** Previously four of six + generated only a four-file stub, and the two with templates produced projects that could + not import their own entry point, crashed on the first training step, or shipped no ONNX + despite being named for it. Template files went from 19 to 125. +- **pixi is the canonical environment manager** for generated projects; manifests migrated + from the deprecated `[project]` table to `[workspace]`. +- **Archetype skill composition** now reflects what each template actually ships — every + archetype requires `pixi` and `code-quality`, and `cv-inference-service` finally + requires `fastapi`. + +### Fixed + +- **The published wheel shipped no skills.** `[tool.hatch.build.targets.wheel]` packaged + only `src/whet`, and the bundled directories were resolved relative to the repository + root — so `uv tool install whet` produced a CLI that reported "No skills found" outside + a source checkout. The wheel now force-includes `skills/`, `archetypes/`, and + `settings/` under `whet/_data/`, and a single resolver prefers the packaged copy while + falling back to the repo layout for development. +- Template files under a `data/` path were silently excluded by the repository's own + `.gitignore`, so a clean checkout produced projects missing `data/raw`, `data/processed`, + and an entire Hydra `data` config group. +- `.ipynb` files were not treated as text by the scaffold engine, so `${package_name}` + inside a notebook was never substituted. +- Adapters now install a skill's `references/` directory (Claude, Antigravity) or inline + it (Cursor, Copilot), so deep-dive links resolve on every platform. +- Documented directory trees in archetype READMEs and docs pages are regenerated from the + templates; 78 documented-but-absent files were removed. + +## [0.2.1] - 2026-07-23 + +Published to PyPI outside this repository's tag-based release workflow; no corresponding +git tag exists. Recorded here for continuity. + +## [0.1.0] - 2026-02-09 + +Initial whet CLI implementation. + +[0.3.0]: https://github.com/ortizeg/whet/releases/tag/v0.3.0 diff --git a/pyproject.toml b/pyproject.toml index 0a92fcc..35b055c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "whet" -version = "0.1.0" +version = "0.3.0" description = "Sharpen your AI coder — install expert skills into AI coding agents" authors = [{name = "Enrique G. Ortiz", email = "ortizeg@gmail.com"}] readme = "README.md" @@ -57,11 +57,17 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/whet"] +# The skills, archetypes, and settings ARE the product; without this the wheel +# ships only the CLI and `whet list` reports "No skills found". +[tool.hatch.build.targets.wheel.force-include] +"skills" = "whet/_data/skills" +"archetypes" = "whet/_data/archetypes" +"settings" = "whet/_data/settings" + [tool.hatch.build.targets.sdist] include = [ "src/whet/", "skills/", - "agents/", "archetypes/", "settings/", ] diff --git a/src/whet/__init__.py b/src/whet/__init__.py index 2cb7ff7..4731432 100644 --- a/src/whet/__init__.py +++ b/src/whet/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations -__version__ = "0.1.0" +__version__ = "0.3.0" diff --git a/src/whet/core/config.py b/src/whet/core/config.py index 5cb2bac..66bf88e 100644 --- a/src/whet/core/config.py +++ b/src/whet/core/config.py @@ -8,6 +8,8 @@ from pydantic import BaseModel, Field +from whet.core.paths import bundled_dir + CONFIG_PATH = Path.home() / ".config" / "whet" / "config.json" @@ -51,10 +53,8 @@ class WhetConfig(BaseModel): """Whet configuration persisted to disk.""" target: Platform = Platform.CLAUDE - skills_dir: Path = Field(default_factory=lambda: Path(__file__).resolve().parents[3] / "skills") - archetypes_dir: Path = Field( - default_factory=lambda: Path(__file__).resolve().parents[3] / "archetypes" - ) + skills_dir: Path = Field(default_factory=lambda: bundled_dir("skills")) + archetypes_dir: Path = Field(default_factory=lambda: bundled_dir("archetypes")) @classmethod def load(cls) -> WhetConfig: diff --git a/src/whet/core/paths.py b/src/whet/core/paths.py new file mode 100644 index 0000000..7d74c2c --- /dev/null +++ b/src/whet/core/paths.py @@ -0,0 +1,31 @@ +"""Locate whet's bundled content (skills, archetypes, settings). + +These directories live at the repository root during development, but must also be +found when whet is installed as a wheel — where the package lands in site-packages +and the repository layout no longer exists above it. The build force-includes them +under `whet/_data/`, so resolution prefers that and falls back to the repo layout. +""" + +from __future__ import annotations + +from pathlib import Path + +# .../src/whet/core/paths.py -> parents[1] is the `whet` package. +_PACKAGE_ROOT = Path(__file__).resolve().parents[1] +# .../src/whet/core/paths.py -> parents[3] is the repository root in a source checkout. +_REPO_ROOT = Path(__file__).resolve().parents[3] + +_BUNDLED = _PACKAGE_ROOT / "_data" + + +def bundled_dir(name: str) -> Path: + """Path to a bundled content directory such as "skills" or "archetypes". + + Prefers the packaged copy so an installed wheel works outside any checkout; + falls back to the repository root so a source checkout keeps working with no + build step. + """ + packaged = _BUNDLED / name + if packaged.is_dir(): + return packaged + return _REPO_ROOT / name diff --git a/src/whet/settings/engine.py b/src/whet/settings/engine.py index 48fec29..6ab23c0 100644 --- a/src/whet/settings/engine.py +++ b/src/whet/settings/engine.py @@ -8,6 +8,8 @@ from pydantic import BaseModel, Field +from whet.core.paths import bundled_dir + class SettingsTemplate(BaseModel, frozen=True): """A settings template with permissions.""" @@ -103,7 +105,7 @@ def write_settings(settings_path: Path, template: SettingsTemplate) -> None: def get_template_path(platform: str) -> Path: """Get the path to a platform's settings template.""" - templates_dir = Path(__file__).resolve().parents[3] / "settings" + templates_dir = bundled_dir("settings") template_file = templates_dir / f"{platform}.json" if template_file.exists(): return template_file diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..2cbae27 --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,69 @@ +"""Guard that the built wheel actually ships the product. + +whet's value is the skills, archetypes, and settings — not the CLI that copies them. +The wheel once packaged only `src/whet`, so `uv tool install whet` produced a CLI +that reported "No skills found" outside a source checkout. These tests assert the +bundled content is both declared for packaging and reachable at runtime. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +if sys.version_info >= (3, 12): + import tomllib +else: + import tomli as tomllib + +from whet.core.paths import bundled_dir + +BUNDLED = ("skills", "archetypes", "settings") + + +def test_wheel_force_includes_bundled_content() -> None: + """Bundled dirs live outside src/whet, so the wheel needs an explicit include.""" + with open("pyproject.toml", "rb") as f: + raw = tomllib.load(f) + + include = raw["tool"]["hatch"]["build"]["targets"]["wheel"]["force-include"] + missing = [name for name in BUNDLED if name not in include] + assert not missing, ( + f"wheel does not force-include {missing}; an installed whet would find no skills" + ) + for name in BUNDLED: + assert include[name] == f"whet/_data/{name}", ( + f"{name} must map to whet/_data/{name} so bundled_dir() resolves it" + ) + + +def test_sdist_includes_bundled_content() -> None: + with open("pyproject.toml", "rb") as f: + raw = tomllib.load(f) + + include = raw["tool"]["hatch"]["build"]["targets"]["sdist"]["include"] + missing = [name for name in BUNDLED if f"{name}/" not in include] + assert not missing, f"sdist does not include {missing}" + + +def test_sdist_does_not_ship_removed_directories() -> None: + """agents/ was deleted; packaging must not still reference it.""" + with open("pyproject.toml", "rb") as f: + raw = tomllib.load(f) + + include = raw["tool"]["hatch"]["build"]["targets"]["sdist"]["include"] + stale = [entry for entry in include if not Path(entry.rstrip("/")).exists()] + assert not stale, f"packaging references directories that no longer exist: {stale}" + + +def test_bundled_dir_resolves_in_a_source_checkout() -> None: + """Development must keep working with no build step.""" + for name in BUNDLED: + resolved = bundled_dir(name) + assert resolved.is_dir(), f"bundled_dir({name!r}) -> {resolved} does not exist" + + +def test_bundled_skills_are_the_real_library() -> None: + """Catch a resolver that finds an empty or wrong directory.""" + skills = list(bundled_dir("skills").glob("*/SKILL.md")) + assert len(skills) >= 25, f"bundled skills dir looks wrong: {len(skills)} SKILL.md files"