diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 3da7319..295223b 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -37,6 +37,35 @@ jobs:
sys.exit(1 if fail else 0)
EOF
+ - name: Agent Plugins v1.0.0 manifests match and stay within the closed schema
+ run: |
+ python3 - <<'EOF'
+ import json, sys, pathlib
+ ALLOWED = {
+ '$schema', 'name', 'version', 'description', 'author',
+ 'homepage', 'repository', 'license', 'keywords', 'extensions',
+ }
+ SCHEMA_URL = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'
+ mp = json.loads(pathlib.Path('.claude-plugin/marketplace.json').read_text())
+ fail = False
+ for entry in mp['plugins']:
+ src = entry['source'].lstrip('./')
+ manifest = pathlib.Path(src) / 'plugin.json'
+ if not manifest.exists():
+ continue # optional: not every plugin has to opt into the open spec
+ pj = json.loads(manifest.read_text())
+ extra = set(pj) - ALLOWED
+ if extra:
+ print(f"FAIL {manifest}: fields outside the closed schema: {sorted(extra)}"); fail = True
+ if pj.get('$schema') != SCHEMA_URL:
+ print(f"FAIL {manifest}: \\$schema is {pj.get('$schema')!r}, expected {SCHEMA_URL!r}"); fail = True
+ if pj.get('name') != entry['name']:
+ print(f"FAIL {manifest}: name {pj.get('name')!r} != marketplace {entry['name']!r}"); fail = True
+ if pj.get('version') != entry.get('version'):
+ print(f"FAIL {manifest}: version {pj.get('version')!r} != marketplace {entry.get('version')!r}"); fail = True
+ sys.exit(1 if fail else 0)
+ EOF
+
- name: Every skill has frontmatter with name and description
run: |
python3 - <<'EOF'
diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml
index b1c24e4..d63ecd7 100644
--- a/.github/workflows/version-bump.yml
+++ b/.github/workflows/version-bump.yml
@@ -104,8 +104,9 @@ jobs:
# maintainerd-core--v0.1.1 -> "maintainerd-core 0.1.1", joined with commas.
SUMMARY=$(printf '%s\n' "${TAGS[@]}" | sed 's/--v/ /' | paste -sd, - | sed 's/,/, /g')
# Stage the manifests by name — the script touches nothing else, and `-A` would
- # sweep in anything a future step happens to leave in the tree.
- git add .claude-plugin/marketplace.json plugins/*/.claude-plugin/plugin.json
+ # sweep in anything a future step happens to leave in the tree. plugins/*/plugin.json
+ # is the Agent Plugins v1.0.0 manifest bump-version.py also edits where present.
+ git add .claude-plugin/marketplace.json plugins/*/.claude-plugin/plugin.json plugins/*/plugin.json
# No staged change means every version here was bumped by hand in the PR. That
# still wants a tag, so fall through to tagging rather than treating it as a
diff --git a/README.md b/README.md
index 97a8972..de9ab60 100644
--- a/README.md
+++ b/README.md
@@ -95,15 +95,22 @@ maintainerd/
scripts/sync-references.sh
scripts/bump-version.py
plugins/
- core/ .claude-plugin/plugin.json skills/{bootstrap,doctor}/ references/{config-schema,model-tiers}.md
- repo-ops/ .claude-plugin/plugin.json skills/{create-pr,address-review,release,daily-changelog,daily-update}/
- audits/ .claude-plugin/plugin.json skills/{audit-architecture,audit-tests,audit-security,audit-deps,audit-design-docs,audit-product-docs}/ references/pattern-promotion.md
- research/ .claude-plugin/plugin.json skills/{research-radar}/
- journal/ .claude-plugin/plugin.json skills/{worklog}/
- auto-dev/ .claude-plugin/plugin.json skills/{create-issue,auto-dev,review-queue}/
- deps-flow/ .claude-plugin/plugin.json skills/{dependabot}/
+ core/ .claude-plugin/plugin.json plugin.json skills/{bootstrap,doctor}/ references/{config-schema,model-tiers}.md
+ repo-ops/ .claude-plugin/plugin.json plugin.json skills/{create-pr,address-review,release,daily-changelog,daily-update}/
+ audits/ .claude-plugin/plugin.json plugin.json skills/{audit-architecture,audit-tests,audit-security,audit-deps,audit-design-docs,audit-product-docs}/ references/pattern-promotion.md
+ research/ .claude-plugin/plugin.json plugin.json skills/{research-radar}/
+ journal/ .claude-plugin/plugin.json plugin.json skills/{worklog}/
+ auto-dev/ .claude-plugin/plugin.json plugin.json skills/{create-issue,auto-dev,review-queue}/
+ deps-flow/ .claude-plugin/plugin.json plugin.json skills/{dependabot}/
```
+Each plugin ships two manifests: `.claude-plugin/plugin.json` is Claude Code's own format;
+the root-level `plugin.json` conforms to the [Agent Plugins v1.0.0](https://agent-plugins.org/specification)
+open spec, for clients that speak that instead. Both describe the same plugin — same `name`,
+same `version` — and both read the same `skills/` directory, since that layout already matches
+what the open spec expects. `bump-version.py` and `validate.yml` keep them in lockstep; edit the
+`.claude-plugin` copy as the source of truth and let the script propagate the version.
+
### Shared reference docs
`config-schema.md` and `model-tiers.md` are authored once in `plugins/core/references/` and
@@ -151,9 +158,10 @@ the push didn't touch don't move, so installs of those stay put.
Two things to know when working in this repo:
-- **The version lives in two files that must agree** — `plugins/
/.claude-plugin/plugin.json`
- and the plugin's entry in `.claude-plugin/marketplace.json`. `validate.yml` fails the build if
- they drift. Use the script rather than editing either by hand:
+- **The version lives in every manifest that must agree** — `plugins//.claude-plugin/plugin.json`,
+ the plugin's entry in `.claude-plugin/marketplace.json`, and — where present —
+ `plugins//plugin.json`. `validate.yml` fails the build if any of them drift. Use the
+ script rather than editing any of them by hand:
```bash
./scripts/bump-version.py --level minor repo-ops
diff --git a/plugins/audits/plugin.json b/plugins/audits/plugin.json
new file mode 100644
index 0000000..2e3300d
--- /dev/null
+++ b/plugins/audits/plugin.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "audits",
+ "version": "0.2.5",
+ "description": "Scheduled findings that open PRs/issues, deliberately capped and conservative: audit-architecture (tech-debt sweep), audit-tests (test-suite health), audit-security (vulnerable deps, committed secrets incl. git history, permissive defaults — code patterns are left to Claude Code's built-in /security-review), audit-deps (dependency health: outdated, deprecated, unused, lockfile drift, licenses), audit-design-docs (validate design docs against code), and audit-product-docs (validate user-facing docs against code). Detection rules and invariants come from the repo's .claude/maintainerd.json and guidelines files.",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/audits#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "audit",
+ "tech-debt",
+ "tests",
+ "security",
+ "dependencies",
+ "documentation",
+ "maintainer"
+ ]
+}
diff --git a/plugins/auto-dev/plugin.json b/plugins/auto-dev/plugin.json
new file mode 100644
index 0000000..0499c76
--- /dev/null
+++ b/plugins/auto-dev/plugin.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "auto-dev",
+ "version": "0.2.7",
+ "description": "Autonomous issue-to-PR pipeline. create-issue is the front door — it turns a rough request into a well-formed, buildable issue by investigating the codebase. auto-dev runs one tick of triage -> plan -> build -> address-review against an auto:* GitHub label state machine and never merges; review-queue is the maintainer's console for approving plans, answering questions, and merging. Label names, comment markers, and branch prefixes are configurable via .claude/maintainerd.json so the pipeline drops into any repo.",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/auto-dev#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "automation",
+ "issues",
+ "issue-intake",
+ "pull-request",
+ "pipeline",
+ "maintainer"
+ ]
+}
diff --git a/plugins/core/plugin.json b/plugins/core/plugin.json
new file mode 100644
index 0000000..968c963
--- /dev/null
+++ b/plugins/core/plugin.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "maintainerd-core",
+ "version": "0.2.4",
+ "description": "Foundation for the maintainerd maintainer toolkit. Ships bootstrap (generates a repo's .claude/maintainerd.json config contract and starter guidelines) and doctor (validates that setup — config, paths, commands, labels, guidelines, rosters — and reports what's wrong), plus the canonical config schema reference that every other maintainerd plugin reads.",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/core#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "config",
+ "bootstrap",
+ "doctor",
+ "diagnostics",
+ "maintainer",
+ "scaffolding"
+ ]
+}
diff --git a/plugins/deps-flow/plugin.json b/plugins/deps-flow/plugin.json
new file mode 100644
index 0000000..b3b7b96
--- /dev/null
+++ b/plugins/deps-flow/plugin.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "deps-flow",
+ "version": "0.2.4",
+ "description": "Autonomous Dependabot queue management — the one maintainerd plugin that merges. The dependabot skill gates every bot-authored dependency PR on all checks green, no requested changes, and a configured semver policy (patch/minor by default, majors held for the human), merges one PR per non-overlapping file group per pass, waits for Dependabot to rebase the rest (nudging with @dependabot rebase only when a rebase is genuinely stuck), and repeats until the queue drains. It never fixes a broken update: it diagnoses the CI failure, files one issue with the evidence, labels the PR blocked, and moves on. Install only in repos where a bot is allowed to merge; every knob lives in .claude/maintainerd.json's depsFlow block.",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/deps-flow#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "dependabot",
+ "dependencies",
+ "merge",
+ "automation",
+ "pull-request",
+ "maintainer"
+ ]
+}
diff --git a/plugins/journal/plugin.json b/plugins/journal/plugin.json
new file mode 100644
index 0000000..2131fff
--- /dev/null
+++ b/plugins/journal/plugin.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "journal",
+ "version": "0.2.5",
+ "description": "User-scoped narration and record-keeping that spans repos. worklog captures a day's shipped work — from merged PRs enriched by the live session — into the user's Obsidian vault as a session-summary note, a project-hub link, and a daily-note line. Unlike the repo-scoped plugins, its vault setting lives in a user-level ~/.claude/maintainerd.json, with an optional per-repo project pointer. A home for future journal skills (weekly-review, decision-log).",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/journal#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "journal",
+ "worklog",
+ "obsidian",
+ "notes",
+ "maintainer"
+ ]
+}
diff --git a/plugins/repo-ops/plugin.json b/plugins/repo-ops/plugin.json
new file mode 100644
index 0000000..61dfa06
--- /dev/null
+++ b/plugins/repo-ops/plugin.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "repo-ops",
+ "version": "0.2.4",
+ "description": "Baseline maintainer dev flow as config-driven skills: create-pr (opens PRs only after running the repo's format/lint/build/test pre-flight), address-review (drive the iterative response loop on bot + human PR feedback to approval — fix, push, reply to every inline thread and a PR-level summary), release (cut a versioned release: gather changes since the last tag, update notes, run the gate, bump, tag, publish), daily-changelog (one markdown file per day from merged PRs), and daily-update (runs the repo's per-day housekeeping skills and bundles their output into one PR). Reads .claude/maintainerd.json.",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/repo-ops#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "pull-request",
+ "review-feedback",
+ "release",
+ "changelog",
+ "automation",
+ "maintainer"
+ ]
+}
diff --git a/plugins/research/plugin.json b/plugins/research/plugin.json
new file mode 100644
index 0000000..9a693ce
--- /dev/null
+++ b/plugins/research/plugin.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
+ "name": "research",
+ "version": "0.2.4",
+ "description": "Proactive research skills that surface relevant outside knowledge and open a PR or digest. research-radar periodically scans arXiv for papers relevant to the work in this repo and writes a dated digest. A home for future research tooling (deep research, GitHub/code search, prior-art scans). Themes and output paths come from .claude/maintainerd.json.",
+ "author": {
+ "name": "Allen Hutchison",
+ "url": "https://github.com/allenhutchison"
+ },
+ "homepage": "https://github.com/Vycari/maintainerd/tree/main/plugins/research#readme",
+ "repository": "https://github.com/Vycari/maintainerd",
+ "license": "MIT",
+ "keywords": [
+ "research",
+ "arxiv",
+ "digest",
+ "discovery",
+ "maintainer"
+ ]
+}
diff --git a/scripts/bump-version.py b/scripts/bump-version.py
index 6662ab6..93218d5 100755
--- a/scripts/bump-version.py
+++ b/scripts/bump-version.py
@@ -10,9 +10,11 @@
install keeps serving the old content forever. Bumping is therefore not a release
ceremony here, it is how the change reaches anyone.
-The version lives in two places that CI (`.github/workflows/validate.yml`) requires to
-agree: `plugins//.claude-plugin/plugin.json` and the plugin's entry in
-`.claude-plugin/marketplace.json`. This script edits both or neither.
+The version lives in the places that CI (`.github/workflows/validate.yml`) requires to
+agree: `plugins//.claude-plugin/plugin.json` (Claude Code's manifest), the plugin's
+entry in `.claude-plugin/marketplace.json`, and — where present — `plugins//plugin.json`
+(the Agent Plugins v1.0.0 manifest, kept for clients that speak the open spec instead of
+Claude Code's format). This script edits all of them or none.
scripts/bump-version.py core repo-ops # bump these (patch)
scripts/bump-version.py --level minor audits # bump one, minor
@@ -101,6 +103,7 @@ def plugin_index(marketplace):
"dir": os.path.basename(source),
"version": entry.get("version"),
"manifest": os.path.join(source, ".claude-plugin", "plugin.json"),
+ "manifest_open_spec": os.path.join(source, "plugin.json"),
}
index[record["name"]] = record
index[record["dir"]] = record
@@ -277,6 +280,18 @@ def main():
f"{record['version']!r} — fix the mismatch before bumping"
)
+ open_spec_path = os.path.join(ROOT, record["manifest_open_spec"])
+ has_open_spec = os.path.exists(open_spec_path)
+ if has_open_spec:
+ with open(open_spec_path) as handle:
+ open_spec_current = json.load(handle).get("version")
+ if open_spec_current != current:
+ fail(
+ f"{record['name']}: {record['manifest_open_spec']} version "
+ f"{open_spec_current!r} != {record['manifest']} {current!r} — "
+ "fix the mismatch before bumping"
+ )
+
# An author who deliberately bumped in the PR (a minor or major that this
# script's default patch would flatten) already published a new version for this
# range. Bumping again on merge would silently turn their 0.2.0 into 0.2.1.
@@ -313,6 +328,8 @@ def main():
new = bump(current, args.level)
edits[manifest_path] = replace_plugin_version(manifest_path, current, new)
+ if has_open_spec:
+ edits[open_spec_path] = replace_plugin_version(open_spec_path, current, new)
marketplace_text = replace_marketplace_version(
marketplace_text, record["name"], current, new
)