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
14 changes: 14 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "{{NAME}}",
"owner": {
"name": "Neil Foster",
"url": "https://github.com/neilgfoster"
},
"plugins": [
{
"name": "{{NAME}}",
"source": "./plugin",
"description": "{{DESCRIPTION}}"
}
]
}
26 changes: 26 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
PR title should be a Conventional Commit (e.g. `feat: ...`, `fix: ...`, `docs: ...`).
Keep it minimal — this is a template repo. Delete sections that genuinely don't apply.
-->

## What and why

<!-- One paragraph: what this changes and why. -->

## Conventions

- [ ] Runtime stays **stdlib-only, zero-dependency, zero-backend** (`urllib`/`json`; ruff/pytest are
dev tooling only).
- [ ] No secrets/tokens in the repo — they live outside it in an XDG path (`0600`).
- [ ] Any new/changed skill follows `docs/AGENT-FRIENDLY.md` (description + CLI I/O are the contract).
- [ ] The template stays minimal — layout + exemplary patterns only, no speculative framework.

## Verification

```sh
ruff check . && ruff format --check .
python3 -m pytest -q
```

- [ ] `ruff check .` and `ruff format --check .` pass.
- [ ] `python3 -m pytest -q` passes.
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: ci

on:
push:
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dev tooling
run: pip install ruff pytest
- name: Lint
run: ruff check .
- name: Format check
run: ruff format --check .
- name: Test
run: python3 -m pytest -q
44 changes: 44 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: release

# Cut a GitHub Release from a vX.Y.Z tag. The version in
# plugin/.claude-plugin/plugin.json is the single source of truth; this workflow refuses to
# release if the tag and that version disagree, then publishes notes from CHANGELOG.md.

on:
push:
tags: ["v*.*.*"]

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Verify tag matches plugin.json version
run: |
tag="${GITHUB_REF_NAME#v}"
ver=$(python3 -c "import json; print(json.load(open('plugin/.claude-plugin/plugin.json'))['version'])")
if [ "$tag" != "$ver" ]; then
echo "tag $GITHUB_REF_NAME does not match plugin.json version $ver" >&2
exit 1
fi

- name: Extract release notes from CHANGELOG
run: |
ver="${GITHUB_REF_NAME#v}"
awk -v v="$ver" '
$0 ~ "^## \\[" v "\\]" {grab=1; next}
grab && /^## \[/ {exit}
grab {print}
' CHANGELOG.md > RELEASE_NOTES.md
if [ ! -s RELEASE_NOTES.md ]; then
echo "No CHANGELOG section for $ver." > RELEASE_NOTES.md
fi

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md
16 changes: 4 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
__pycache__/
*.pyc
.DS_Store
.pytest_cache/
.ruff_cache/
*.egg-info/
.venv/

# Secrets/tokens NEVER live in the repo — they belong outside it (XDG, 0600).
# These patterns are belt-and-braces only; the real guarantee is storing tokens
Expand All @@ -9,18 +13,6 @@ __pycache__/
secrets/
.env

# --- Local spec-driven-development scaffolding (kept out of the repo) ---
# Spec Kit / SDD working artifacts stay local so the template's footprint stays minimal.
# Remove the relevant lines if you decide to commit these artifacts.
.tredl/
.specify/
specs/
.claude/skills/speckit-*/
.agents/skills/speckit-*/
.github/agents/speckit*
.github/prompts/speckit*
# --- end ---

# --- tredl (solo trial) ---
# tredl is installed transparently for one person: the Spec Kit and tredl artifacts below stay
# local, so adopting tredl leaves (almost) no footprint on the team. When the value is proven,
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

All notable changes to this plugin are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this plugin uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-1.0, breaking changes may land on a
minor bump and are called out explicitly.

The `version` in `plugin/.claude-plugin/plugin.json` is the single source of truth; the release
workflow refuses to publish a tag that disagrees with it.

## [Unreleased]

<!--
Add notes here under Added / Changed / Fixed / Removed. On release, move them under a new
## [X.Y.Z] - YYYY-MM-DD heading and bump plugin/.claude-plugin/plugin.json to match.
-->
30 changes: 24 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,39 @@ Two situations you might be in:

## Where things are

**Two tiers.** The repo root builds/tests/distributes the plugin; everything under `plugin/` is the
shippable payload that installs. `${CLAUDE_PLUGIN_ROOT}` resolves to `plugin/`.

| Path | Purpose |
|---|---|
| `.claude-plugin/plugin.json` | Plugin manifest (`{{NAME}}`/`{{DESCRIPTION}}` placeholders) |
| `skills/<subject>-<verb>/SKILL.md` | Agent-facing commands; one exemplary skill ships in the template |
| `src/<plugin>/client.py` | Stdlib kernel — importable AND runnable (`python3 -m <plugin>.client …`) |
| `.claude-plugin/marketplace.json` | **Root** marketplace manifest; `source: ./plugin`, name must match the plugin's |
| `plugin/.claude-plugin/plugin.json` | Plugin manifest (`{{NAME}}`/`{{DESCRIPTION}}` placeholders) |
| `plugin/skills/<subject>-<verb>/SKILL.md` | Agent-facing commands; one exemplary skill ships in the template |
| `plugin/src/<plugin>/client.py` | Stdlib kernel — importable AND runnable (`python3 -m <plugin>.client …`) |
| `plugin/hooks/hooks.json` | Optional PreToolUse example (delete if unused) |
| `pyproject.toml`, `tests/`, `.github/` | Dev tooling (ruff/pytest), manifest/kernel guards, CI+release |
| `CHANGELOG.md`, `CONTRIBUTING.md` | Keep-a-Changelog + contributor standards |
| `docs/AGENT-FRIENDLY.md` | **Required reading** — the MCP/agent-tool design principles |
| `README.md` | Human-facing overview + instantiation steps |

## How to instantiate a real plugin

1. Copy this repo; rename `src/example` → `src/<plugin>` (update `APP` in `client.py`).
2. Rename `skills/example-subject-verb` → real `<subject>-<verb>` skills.
3. Fill `{{NAME}}` / `{{DESCRIPTION}}` in `.claude-plugin/plugin.json`.
1. Copy this repo; rename `plugin/src/example` → `plugin/src/<plugin>` (update `APP` in `client.py`
and `known-first-party` in `pyproject.toml`).
2. Rename `plugin/skills/example-subject-verb` → real `<subject>-<verb>` skills.
3. Fill `{{NAME}}` / `{{DESCRIPTION}}` in `.claude-plugin/marketplace.json` and
`plugin/.claude-plugin/plugin.json` (marketplace entry name must equal the plugin name).
4. Build each skill against `docs/AGENT-FRIENDLY.md`.

## Verify before claiming done

```sh
ruff check . && ruff format --check .
python3 -m pytest -q
```

`.github/workflows/ci.yml` runs exactly these; green CI is part of the Definition of Done.

<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
Expand Down
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Contributing

This is a minimal plugin template, so contribution discipline is light. The few rules that earn their
keep:

## Standards

- **Python 3.11+, stdlib only at runtime.** `urllib` for HTTP, `json` for parsing. `ruff` and
`pytest` are dev tooling — they never ship in `plugin/`. No third-party runtime dependencies, no
backend/server process. The constraint is the point: portable, auditable, no install friction.
- **Two-tier layout.** `plugin/` is the shippable payload (its own `.claude-plugin/plugin.json` +
`skills/`, `src/`, `hooks/`); the repo root is the build/distribution repo
(`.claude-plugin/marketplace.json`, `pyproject.toml`, `tests/`, `.github/`, docs). Inside the
plugin, reference bundled files via `${CLAUDE_PLUGIN_ROOT}/...` — it resolves to `plugin/`.
- **Command naming: `<subject>-<verb>`.** Skills are named subject-then-verb (e.g.
`example-subject-verb`). Keep the convention.
- **Secrets live OUTSIDE the repo** — a user-level XDG path
(`${XDG_STATE_HOME:-~/.local/state}/<plugin>/`) with `0600` perms. Never committed, not even
encrypted. Do not add an in-repo secrets path or a git-crypt dependency.
- **Agent-friendly by design.** Every skill follows `docs/AGENT-FRIENDLY.md` — read it before adding
or changing a skill. The skill `description` and the CLI's inputs/outputs **are** the tool contract
an agent reads.
- **Keep the template minimal.** Layout + exemplary patterns only. Real abstractions are harvested
later from working plugins, never invented here speculatively.
- **Conventional Commit messages and PR titles.**

## Verify before claiming done

```sh
ruff check . && ruff format --check .
python3 -m pytest -q
```

CI (`.github/workflows/ci.yml`) runs exactly these; green CI is part of the Definition of Done.

## Cutting a release

SemVer, tag-driven. The version in `plugin/.claude-plugin/plugin.json` is the single source of truth.

1. Bump `version` in `plugin/.claude-plugin/plugin.json`.
2. In `CHANGELOG.md`, move `## [Unreleased]` notes under a new `## [X.Y.Z] - YYYY-MM-DD` heading.
3. `ruff check . && ruff format --check . && python3 -m pytest -q`.
4. Merge, then `git tag vX.Y.Z && git push origin vX.Y.Z`. The release workflow publishes it.
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,44 @@ harvested later, from working plugins, not invented up front.

## Layout

Two tiers: the **repo root** builds/tests/distributes the plugin; **`plugin/`** is the shippable
payload that gets installed.

```
.claude-plugin/plugin.json # manifest ({{NAME}}/{{DESCRIPTION}} placeholders)
skills/<subject>-<verb>/SKILL.md # one exemplary skill demonstrating the conventions
src/<plugin>/ # stdlib Python kernel (importable + runnable by skills)
client.py # urllib+json idiom + output-shaping patterns (stub)
hooks/hooks.json # optional PreToolUse example (delete if unused)
docs/AGENT-FRIENDLY.md # REQUIRED READING before adding a skill
.claude-plugin/marketplace.json # marketplace manifest — points at ./plugin ({{NAME}} placeholder)
plugin/ # THE PLUGIN PAYLOAD (this is what installs)
.claude-plugin/plugin.json # plugin manifest ({{NAME}}/{{DESCRIPTION}} placeholders)
skills/<subject>-<verb>/SKILL.md# one exemplary skill demonstrating the conventions
src/<plugin>/ # stdlib Python kernel (importable + runnable by skills)
client.py # urllib+json idiom + output-shaping patterns (stub)
hooks/hooks.json # optional PreToolUse example (delete if unused)
pyproject.toml # ruff + pytest config (dev tooling only — never ships)
tests/ # guards the manifest install path + the runnable kernel
.github/workflows/{ci,release}.yml# lint+test on PR; tag-driven GitHub Release
CHANGELOG.md CONTRIBUTING.md # Keep-a-Changelog + contributor standards
docs/AGENT-FRIENDLY.md # REQUIRED READING before adding a skill
```

Inside the plugin, reference bundled files via `${CLAUDE_PLUGIN_ROOT}/...` — it resolves to `plugin/`.

## How to instantiate

1. Copy this repo to `~/source/neilgfoster/<plugin>` and `git init`.
2. Rename `src/example` → `src/<plugin>`; update `APP` in `client.py`.
3. Rename `skills/example-subject-verb` → real `<subject>-<verb>` skills.
4. Fill the `{{NAME}}` / `{{DESCRIPTION}}` placeholders in `.claude-plugin/plugin.json`.
2. Rename `plugin/src/example` → `plugin/src/<plugin>`; update `APP` in `client.py` and
`known-first-party` in `pyproject.toml`.
3. Rename `plugin/skills/example-subject-verb` → real `<subject>-<verb>` skills.
4. Fill the `{{NAME}}` / `{{DESCRIPTION}}` placeholders in `.claude-plugin/marketplace.json` and
`plugin/.claude-plugin/plugin.json` (the marketplace entry name must equal the plugin name).
5. Build features spec-first (Spec-Driven Development):
`/speckit-specify` → `clarify` → `plan` → `tasks` → `implement`.

## Verify before done

```sh
ruff check . && ruff format --check .
python3 -m pytest -q
```

## Non-negotiable conventions

- **stdlib only, zero dependencies, no backend.** `urllib` for HTTP, `json` for parsing.
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 12 additions & 4 deletions src/example/client.py → plugin/src/example/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

Replace the stub `list` command with real operations. Keep the patterns.
"""

import argparse
import json
import os
Expand Down Expand Up @@ -40,10 +41,17 @@
"inputSchema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "default": 25,
"description": "Max items to return (pagination)."},
"format": {"type": "string", "enum": ["concise", "detailed"], "default": "concise",
"description": "concise = agent-legible summary; detailed = adds IDs."},
"limit": {
"type": "integer",
"default": 25,
"description": "Max items to return (pagination).",
},
"format": {
"type": "string",
"enum": ["concise", "detailed"],
"default": "concise",
"description": "concise = agent-legible summary; detailed = adds IDs.",
},
},
"required": [],
},
Expand Down
17 changes: 17 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Dev tooling only. The plugin payload under plugin/ stays stdlib-only at runtime;
# ruff and pytest never ship — they guard the build repo. Rename "example" to your
# plugin package when you instantiate (see known-first-party below).
[tool.ruff]
target-version = "py311"
line-length = 110
src = ["plugin/src", "tests"]

[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B"]

[tool.ruff.lint.isort]
known-first-party = ["example"]

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["plugin/src"]
28 changes: 28 additions & 0 deletions tests/test_client_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""The bundled stdlib kernel must stay importable and runnable.

`describe` is the discovery entry point (the zero-backend MCP tools/list equivalent); if it stops
emitting a valid catalog, agents can no longer enumerate the plugin's verbs.
"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

CLIENT = Path(__file__).resolve().parent.parent / "plugin" / "src" / "example" / "client.py"


def test_describe_emits_a_valid_tool_catalog():
proc = subprocess.run(
[sys.executable, str(CLIENT), "describe"],
capture_output=True,
text=True,
check=False,
)
assert proc.returncode == 0, f"describe failed: {proc.stderr}"
catalog = json.loads(proc.stdout)
assert isinstance(catalog.get("tools"), list) and catalog["tools"], "describe must list tools"
for tool in catalog["tools"]:
assert tool.get("name") and tool.get("description") and tool.get("inputSchema")
Loading
Loading