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 .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"name": "compound-knowledge",
"source": "./plugins/compound-knowledge",
"description": "Capture solved problems, retrieve past solutions, and graduate accumulated knowledge into living context. Skills: compound-capture, compound-retrieve, compound-graduate.",
"version": "0.8.2",
"version": "0.8.3",
"author": {
"name": "Joshua Oliphant"
},
Expand Down Expand Up @@ -128,7 +128,7 @@
"name": "understand",
"source": "./plugins/understand",
"description": "Process information for real understanding with the explain-back skill: explain from memory, grade against the real source, struggle-then-teach per gap, output Mochi cards and a resumable session record. An antidote to the illusion of clarity.",
"version": "0.1.1",
"version": "0.1.2",
"author": {
"name": "Joshua Oliphant"
},
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
checks:
name: Repo-wide checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Run version, shared-sync, and test checks
run: uv run --group dev python scripts/check_all.py

review-diff-coverage:
name: review-diff coverage gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Run review-diff suite with its 100% coverage gate
working-directory: plugins/review-diff
run: uv run --group dev pytest
16 changes: 11 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ claude-plugins/
│ ├── review-diff/ # Local web-based diff review fed back to Claude Code
│ └── stick-shift/ # Manually-driven ("disassembled") SDLC via slash commands
├── scripts/
│ ├── check_marketplace_versions.py # Asserts marketplace.json matches each plugin.json
│ ├── check_all.py # One-command repo health check (versions + sync + tests)
│ ├── check_marketplace_versions.py # Asserts marketplace.json ⇄ plugin.json (versions + registration)
│ ├── sync_shared.py # Asserts/regenerates per-plugin copies of shared artifacts
│ └── shared/ # Canonical sources for artifacts duplicated across plugins
├── evals/ # Skill-trigger eval datasets (fixtures, not a plugin)
├── ai_docs/ # Background research/reference docs (not shipped in plugins)
├── docs/ # Planning and specifications
├── README.md # Marketplace installation instructions
Expand Down Expand Up @@ -129,9 +131,10 @@ python scripts/check_marketplace_versions.py # exits non-zero on any drift
### Shared-source sync (generate from one source)

Some artifacts must be physically present in multiple self-contained plugins but should never
diverge — e.g. `feedback_manager.py` (shipped by 5 plugins) and `prompt_design_principles.md`
(shared by `mochi-creator` and `understand`). The canonical copy lives under `scripts/shared/`;
each plugin's copy is generated from it.
diverge — e.g. `feedback_manager.py` (shipped by 5 plugins), `config_loader.py` (the
`.claude/<name>.local.md` reader shared by `compound-knowledge` and `understand`), and
`prompt_design_principles.md` (shared by `mochi-creator` and `understand`). The canonical copy
lives under `scripts/shared/`; each plugin's copy is generated from it.

```bash
python scripts/sync_shared.py # check for drift (exits non-zero on mismatch)
Expand Down Expand Up @@ -177,7 +180,10 @@ When changing a plugin:
1. Bump the version in `plugins/{name}/.claude-plugin/plugin.json`
2. Copy that version into the matching entry in `.claude-plugin/marketplace.json`
3. Only bump `metadata.version` if the marketplace itself changed (plugin added/removed)
4. Run `python scripts/check_marketplace_versions.py` to confirm they match
4. Run `python scripts/check_marketplace_versions.py` to confirm they match. This
check is bidirectional: it also fails if a directory under `plugins/` has no
marketplace entry, or if a non-plugin directory (no `.claude-plugin/plugin.json`)
is sitting under `plugins/`.

## Plugin Installation

Expand Down
26 changes: 26 additions & 0 deletions evals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# evals/

Skill-trigger eval datasets — **not** a plugin, which is why they live here and
not under `plugins/`.

Each `*-eval.json` file is a list of `{ "query": ..., "should_trigger": bool }`
cases for one skill or workflow. They check that a skill's description fires on
the requests it should and stays quiet on the ones it shouldn't:

```json
[
{"query": "help me create Mochi flashcards for the key concepts", "should_trigger": true},
{"query": "write documentation for our API endpoints", "should_trigger": false}
]
```

| File | Covers |
|---|---|
| `mochi-creator-eval.json` | mochi-creator card creation |
| `compound-capture-eval.json` / `compound-retrieve-eval.json` | compound-knowledge capture / retrieve |
| `hexagonal-agents-eval.json` | hexagonal-agents scaffolding |
| `bdd-spec-eval.json` / `bdd-generate-eval.json` / `tdd-workflow-eval.json` / `beads-workflow-eval.json` | autonomous-sdlc workflows |
| `verification-stack-eval.json` | observability-harness verification |

These are fixtures for evaluating skill-description quality; they are not shipped
inside any plugin.
File renamed without changes.
2 changes: 1 addition & 1 deletion plugins/compound-knowledge/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "compound-knowledge",
"version": "0.8.2",
"version": "0.8.3",
"description": "Capture solved problems, retrieve past solutions, and graduate accumulated knowledge into living context. Skills: compound-capture (post-solve documentation), compound-retrieve (pre-work knowledge search), compound-graduate (promote lessons into CLAUDE.md/AGENTS.md).",
"license": "MIT",
"author": {
Expand Down
48 changes: 48 additions & 0 deletions plugins/compound-knowledge/scripts/config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# ABOUTME: Shared loader for a plugin's `.claude/<name>.local.md` key:value config.
# ABOUTME: Canonical source — synced into each plugin's scripts/ via scripts/sync_shared.py.
"""Read `key: value` lines from a plugin's `.claude/<name>.local.md` config file.

Resolution order is project-level (`<project>/.claude/<name>.local.md`) then
user-level (`<home>/.claude/<name>.local.md`), else empty. Callers layer their
own typing, defaults, and path normalization on top of the raw dict returned
here — this module only owns the file format and the project>user>default
precedence, which every consuming plugin shares.
"""

from __future__ import annotations

import re
from pathlib import Path

_KV = re.compile(r"^([a-z_]+)\s*:\s*(.+)$")


def parse_config(path: Path) -> dict:
"""Parse `key: value` lines from a .local.md config file (missing file → {})."""
settings: dict = {}
if not path.exists():
return settings
for line in path.read_text(encoding="utf-8").split("\n"):
match = _KV.match(line.strip())
if match:
settings[match.group(1)] = match.group(2).strip()
return settings


def find_config(project_root: Path, home: Path, config_name: str) -> tuple[dict, str]:
"""Return (settings, source) using project-level then user-level config."""
project_cfg = project_root / ".claude" / config_name
if project_cfg.exists():
return parse_config(project_cfg), "project"
user_cfg = home / ".claude" / config_name
if user_cfg.exists():
return parse_config(user_cfg), "user"
return {}, "default"


def cli_roots(argv: list) -> tuple[Path, Path]:
"""Resolve (project_root, home) from argv[1:] for the standard resolver CLI."""
project_root = Path(argv[1]).expanduser().resolve() if len(argv) > 1 else Path.cwd()
home = Path(argv[2]).expanduser().resolve() if len(argv) > 2 else Path.home()
return project_root, home
32 changes: 4 additions & 28 deletions plugins/compound-knowledge/scripts/resolve_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,12 @@
"""

import json
import re
import sys
from pathlib import Path

CONFIG_NAME = "compound-knowledge.local.md"
_KV = re.compile(r"^([a-z_]+)\s*:\s*(.+)$")


def _parse_config(path: Path) -> dict:
"""Parse `key: value` lines from a .local.md config file."""
settings: dict = {}
if not path.exists():
return settings
for line in path.read_text(encoding="utf-8").split("\n"):
match = _KV.match(line.strip())
if match:
settings[match.group(1)] = match.group(2).strip()
return settings
import config_loader


def _find_config(project_root: Path, home: Path):
"""Return (settings, source) using project-level then user-level config."""
project_cfg = project_root / ".claude" / CONFIG_NAME
if project_cfg.exists():
return _parse_config(project_cfg), "project"
user_cfg = home / ".claude" / CONFIG_NAME
if user_cfg.exists():
return _parse_config(user_cfg), "user"
return {}, "default"
CONFIG_NAME = "compound-knowledge.local.md"


def _norm(path: str) -> str:
Expand All @@ -43,7 +20,7 @@ def _norm(path: str) -> str:

def resolve(project_root: Path, home: Path) -> dict:
"""Resolve write_path, read_paths, and vault_root from config or defaults."""
settings, source = _find_config(project_root, home)
settings, source = config_loader.find_config(project_root, home, CONFIG_NAME)

# write_path: explicit > solutions_path alias > default
if "write_path" in settings:
Expand Down Expand Up @@ -73,8 +50,7 @@ def resolve(project_root: Path, home: Path) -> dict:


def main(argv: list) -> int:
project_root = Path(argv[1]).expanduser().resolve() if len(argv) > 1 else Path.cwd()
home = Path(argv[2]).expanduser().resolve() if len(argv) > 2 else Path.home()
project_root, home = config_loader.cli_roots(argv)
print(json.dumps(resolve(project_root, home), indent=2))
return 0

Expand Down
2 changes: 1 addition & 1 deletion plugins/understand/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "understand",
"version": "0.1.1",
"version": "0.1.2",
"description": "Process information for real understanding. The explain-back skill makes you explain from memory, grades against the real source, teaches only after you attempt, and outputs Mochi cards plus a resumable session record. An antidote to the illusion of clarity.",
"license": "MIT",
"author": {
Expand Down
48 changes: 48 additions & 0 deletions plugins/understand/scripts/config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# ABOUTME: Shared loader for a plugin's `.claude/<name>.local.md` key:value config.
# ABOUTME: Canonical source — synced into each plugin's scripts/ via scripts/sync_shared.py.
"""Read `key: value` lines from a plugin's `.claude/<name>.local.md` config file.

Resolution order is project-level (`<project>/.claude/<name>.local.md`) then
user-level (`<home>/.claude/<name>.local.md`), else empty. Callers layer their
own typing, defaults, and path normalization on top of the raw dict returned
here — this module only owns the file format and the project>user>default
precedence, which every consuming plugin shares.
"""

from __future__ import annotations

import re
from pathlib import Path

_KV = re.compile(r"^([a-z_]+)\s*:\s*(.+)$")


def parse_config(path: Path) -> dict:
"""Parse `key: value` lines from a .local.md config file (missing file → {})."""
settings: dict = {}
if not path.exists():
return settings
for line in path.read_text(encoding="utf-8").split("\n"):
match = _KV.match(line.strip())
if match:
settings[match.group(1)] = match.group(2).strip()
return settings


def find_config(project_root: Path, home: Path, config_name: str) -> tuple[dict, str]:
"""Return (settings, source) using project-level then user-level config."""
project_cfg = project_root / ".claude" / config_name
if project_cfg.exists():
return parse_config(project_cfg), "project"
user_cfg = home / ".claude" / config_name
if user_cfg.exists():
return parse_config(user_cfg), "user"
return {}, "default"


def cli_roots(argv: list) -> tuple[Path, Path]:
"""Resolve (project_root, home) from argv[1:] for the standard resolver CLI."""
project_root = Path(argv[1]).expanduser().resolve() if len(argv) > 1 else Path.cwd()
home = Path(argv[2]).expanduser().resolve() if len(argv) > 2 else Path.home()
return project_root, home
32 changes: 4 additions & 28 deletions plugins/understand/scripts/resolve_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
"""

import json
import re
import sys
from pathlib import Path

import config_loader

CONFIG_NAME = "understand.local.md"
_KV = re.compile(r"^([a-z_]+)\s*:\s*(.+)$")

DEFAULTS = {
"mochi_deck": "",
Expand All @@ -21,37 +21,14 @@
}


def _parse_config(path: Path) -> dict:
"""Parse `key: value` lines from a .local.md config file."""
settings: dict = {}
if not path.exists():
return settings
for line in path.read_text(encoding="utf-8").split("\n"):
match = _KV.match(line.strip())
if match:
settings[match.group(1)] = match.group(2).strip()
return settings


def _find_config(project_root: Path, home: Path):
"""Return (settings, source) using project-level then user-level config."""
project_cfg = project_root / ".claude" / CONFIG_NAME
if project_cfg.exists():
return _parse_config(project_cfg), "project"
user_cfg = home / ".claude" / CONFIG_NAME
if user_cfg.exists():
return _parse_config(user_cfg), "user"
return {}, "default"


def _as_bool(value: str) -> bool:
"""Interpret a config string as a boolean."""
return value.strip().lower() in ("true", "yes", "1", "on")


def resolve(project_root: Path, home: Path) -> dict:
"""Resolve understand-plugin settings from config or defaults."""
settings, source = _find_config(project_root, home)
settings, source = config_loader.find_config(project_root, home, CONFIG_NAME)
merged = dict(DEFAULTS)
merged.update({k: v for k, v in settings.items() if k in DEFAULTS})

Expand All @@ -77,8 +54,7 @@ def resolve(project_root: Path, home: Path) -> dict:


def main(argv: list) -> int:
project_root = Path(argv[1]).expanduser().resolve() if len(argv) > 1 else Path.cwd()
home = Path(argv[2]).expanduser().resolve() if len(argv) > 2 else Path.home()
project_root, home = config_loader.cli_roots(argv)
print(json.dumps(resolve(project_root, home), indent=2))
return 0

Expand Down
20 changes: 19 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
[project]
name = "claude-plugins"
version = "0.1.0"
description = "Add your description here"
description = "Claude Code plugin marketplace (oliphant-plugins) and its dev/CI tooling."
readme = "README.md"
requires-python = ">=3.11"
dependencies = []

[tool.uv]
package = false

[dependency-groups]
# Repo-wide dev tooling. Individual plugins that need more (e.g. review-diff's
# coverage gate) declare their own dependency-group in their own pyproject.toml.
dev = ["pytest>=8", "ruff>=0.6"]

[tool.pytest.ini_options]
# `uv run --group dev pytest` from the repo root discovers every plugin's tests.
# review-diff's coverage-gated config lives in its own pyproject and only applies
# when pytest is invoked from that directory; at the repo root those tests still
# run (without the coverage gate), so nothing is excluded here.
testpaths = ["plugins"]

[tool.ruff]
line-length = 100
Loading
Loading