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
28 changes: 28 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"permissions": {
"allow": [
"Bash(git:*)",
"Bash(python:*)",
"Bash(python3:*)",
"Bash(pip:*)",
"Bash(poetry:*)",
"Bash(ls:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(jq:*)",
"Bash(echo:*)",
"Read(*)"
],
"deny": [
"Read(.env)",
"Bash(rm -rf /*)",
"Bash(git reset --hard:*)",
"Bash(git rebase:*)",
"Bash(git merge:*)",
"Bash(git branch -D:*)",
"Bash(gh pr merge:*)",
"Bash(gh pr close:*)",
"Bash(gh pr review:*)"
]
}
}
80 changes: 80 additions & 0 deletions .claude/skills/scaffold-example/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: scaffold-example
description: Scaffold a brand-new Comet example in this repo from the canonical template under templates/integration-example/. Use whenever the user wants to create, add, or start a new example, demo, or integration on Comet — phrasings like "add a Comet example for X", "scaffold an integration example", "create a new example for <framework>", "start a hello-world for <library>", "set up a new example project". Do NOT hand-build the folder structure or copy files manually — this skill stamps out the example and renames it correctly. Require a name and a one-line description of what it does; refuse to invent an example from nothing.
---

# Scaffold a new Comet example

This repo has one canonical example shape, a real, runnable template under
[`templates/integration-example/`](../../../templates/integration-example): a `uv` project
(`pyproject.toml`), a single `comet_ml` script (`example_integration.py`), and a README in the house
structure (Title → Documentation → See it → Setup → Run the example). Getting it right by hand is
fiddly and easy to drift on. This skill copies the template and renames it, so you start from
something that already runs.

## When to refuse

You need two things:

- A **name** (kebab-case, e.g. `pytorch-amp-example`, `keras-mnist-dnn`). It becomes the folder, the
Python module, and the Comet project name (`comet-example-<name>`).
- A **one-line description** of what it does.

If either is missing, ask one short question. Don't invent an example — an empty scaffold with a
guessed purpose wastes the user's time.

## Step 1 — Generate the example

Run the bundled generator. It copies the template, renames its sentinel identity
(`example_integration` / `example-integration`) to the new name, and refuses to overwrite an
existing folder.

```bash
python .claude/skills/scaffold-example/scripts/scaffold.py <name> \
--description "<one line>" \
--dest <parent-dir>
```

`--dest` is the parent directory (default `integrations`). New examples live under
`integrations/<category>/<framework>/`, so pass e.g.
`--dest integrations/model-training/pytorch`. Categories in use: `model-training`,
`model-evaluation`, `model-optimization`, `model-deployment`, `workflow-orchestration`,
`reinforcement-learning`, `llm`, `data-management`. The script prints the folder / script / project
names it chose and a next-steps checklist.

The name is normalised: the folder and Comet project use kebab-case, the Python file uses
snake_case (so it stays importable). The description is written into the `pyproject.toml`.

## Step 2 — Make it real

The scaffold runs as-is but is generic. Fill the parts marked `TODO`:

- **`<name>.py`** — replace the stub loop with the real instrumentation: your framework's training
or inference, plus the `comet_ml` logging that matters (`log_parameters`, `log_metrics`,
`log_model`, …). Keep the `comet_ml.login(...)` + `comet_ml.start()` + `experiment.end()` frame.
- **`pyproject.toml`** — add the framework dependency alongside the pinned `comet_ml`.
- **`README.md`** — fill the title, intro (paste the description), the docs link, and a public
Comet project link if one exists. This is required for every example (see
[AGENTS.md](../../../AGENTS.md)).

Keep the repo conventions: credentials from env vars only, `# WHY:`-only comments, no emojis.

## Step 3 — Verify

From the new example directory:

```bash
uv sync
uv run python <name>.py # logs a real run (needs COMET_API_KEY)
COMET_MODE=offline uv run python <name>.py # runs without an account, if practical
```

To get the example tested, add it to the matrix in
[`.github/workflows/test-examples.yml`](../../../.github/workflows/test-examples.yml) — the
`notebooks` list for a `.ipynb`, or the `example` list for a script. See
[CONTRIBUTING.md](../../../CONTRIBUTING.md#ci).

## What this skill does NOT do

- **No git commit / PR.** Leave the new files in the working tree for the user to review.
- **No domain logic.** It scaffolds; you (with the user) write the actual instrumentation, deps, and README.
148 changes: 148 additions & 0 deletions .claude/skills/scaffold-example/scripts/scaffold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Scaffold a new Comet example by copying and renaming the canonical template.

One template lives under `templates/integration-example/`: a `pyproject.toml`, a single `comet_ml`
script, and a README in the house structure. It is a real, runnable example under a
sentinel identity (`example_integration` / `example-integration`). This script copies it to the
target directory and rewrites those sentinels to the new name — a pure identifier rename, no
template language. Run it, then fill in the TODO stubs.

python scaffold.py pytorch-amp-example --description "..." --dest integrations/model-training/pytorch
"""

import argparse
import re
import shutil
import sys
from pathlib import Path

TEMPLATE_DIR = ("templates", "integration-example")
PKG_SENTINEL = "example_integration" # snake_case: the .py module name
PROJECT_SENTINEL = "example-integration" # kebab-case: folder + comet project name

EXCLUDE = shutil.ignore_patterns(
".venv", "uv.lock", "__pycache__", "*.pyc", ".ruff_cache", ".env", "*.log", ".tmp"
)


def to_snake(name: str) -> str:
s = re.sub(r"[^0-9a-zA-Z]+", "_", name.strip().lower()).strip("_")
if not s:
raise ValueError(f"cannot derive a name from {name!r}")
if s[0].isdigit():
s = f"x_{s}"
return s


def to_kebab(snake: str) -> str:
return snake.replace("_", "-")


def is_text(path: Path) -> bool:
try:
path.read_text(encoding="utf-8")
return True
except (UnicodeDecodeError, OSError):
return False


def main() -> int:
parser = argparse.ArgumentParser(
description="Scaffold a new Comet example from templates/integration-example."
)
parser.add_argument("name", help="Example name, e.g. 'pytorch-amp-example'.")
parser.add_argument(
"--description",
default=None,
help="One-line description (written to pyproject.toml).",
)
parser.add_argument(
"--dest",
default="integrations",
help="Parent directory for the new example (default: integrations). "
"Relative paths are resolved from the repo root; absolute paths are used as-is.",
)
parser.add_argument(
"--repo-root",
default=None,
help="Repo root (default: inferred from this script).",
)
args = parser.parse_args()

repo_root = (
Path(args.repo_root).resolve()
if args.repo_root
else Path(__file__).resolve().parents[4]
)
template = repo_root.joinpath(*TEMPLATE_DIR)

pkg = to_snake(args.name)
kebab = to_kebab(pkg)

if not template.is_dir():
print(f"error: template not found at {template}", file=sys.stderr)
return 1

dest_parent = (
Path(args.dest) if Path(args.dest).is_absolute() else repo_root / args.dest
)
dest = dest_parent / kebab
if dest.exists():
print(f"error: {dest} already exists — refusing to overwrite.", file=sys.stderr)
return 1

dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(template, dest, ignore=EXCLUDE)

# Rename the module file (script kind).
mod_file = dest / f"{PKG_SENTINEL}.py"
if mod_file.is_file():
mod_file.rename(dest / f"{pkg}.py")

# Rewrite sentinels in every text file. Snake and kebab sentinels don't overlap
# (underscore vs hyphen), so independent replacement is safe.
for path in dest.rglob("*"):
if not path.is_file() or not is_text(path):
continue
text = path.read_text(encoding="utf-8")
new = text.replace(PKG_SENTINEL, pkg).replace(PROJECT_SENTINEL, kebab)
if new != text:
path.write_text(new, encoding="utf-8")

# Description -> pyproject.toml.
if args.description:
pyproject = dest / "pyproject.toml"
text = pyproject.read_text(encoding="utf-8")
text = re.sub(
r'^description = ".*"$',
f'description = "{args.description}"',
text,
count=1,
flags=re.MULTILINE,
)
pyproject.write_text(text, encoding="utf-8")

try:
rel = dest.relative_to(repo_root)
except ValueError:
rel = dest
print(f"Scaffolded {rel}")
print(f" folder: {kebab}/")
print(f" script: {pkg}.py")
print(f" comet project: comet-example-{kebab}")
if args.description:
print(f" description: {args.description}")
print("\nNext steps:")
print(f" 1. cd {rel} && uv sync")
print(
f" 2. Fill in {pkg}.py (real logic), pyproject.toml deps, and the README sections."
)
print(
f" 3. Run it: uv run python {pkg}.py (or: COMET_MODE=offline uv run python {pkg}.py)"
)
print(" 4. To test it in CI, add it to .github/workflows/test-examples.yml.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,8 @@ data/*
# pytest
.pytest_cache/
MagicMock/

# Tooling / agent harness
.ruff_cache/
.tmp/
*-workspace/ # skill eval scratch (e.g. skill-creator benchmarks) — never commit
Loading
Loading