From 13e16b50fcf7ba930f02fdff0a43512be48d763f Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Mon, 8 Jun 2026 10:33:07 -0500 Subject: [PATCH 1/5] feat: add category and effect as first-class fields in extension schema Add `category` and `effect` as optional fields in the extension schema (`extension.yml`) and community catalog (`catalog.community.json`). Schema changes: - Valid categories: docs, code, process, integration, visibility - Valid effects: read-only, read-write - Both fields are optional (backward-compatible with existing extensions) - Validation raises ValidationError for invalid values when present Propagation: - Added `category` and `effect` to all 108 entries in catalog.community.json (populated from the existing docs/community/extensions.md table) - Updated extension template with commented category/effect fields - Updated add-community-extension skill with new JSON template fields - Updated `specify extension info` CLI output to display category/effect - Added properties to ExtensionManifest class Tests: - test_valid_category: all 5 category values pass - test_valid_effect: both effect values pass - test_invalid_category: invalid value raises ValidationError - test_invalid_effect: invalid value raises ValidationError - test_category_and_effect_optional: omitting fields still works Closes #2874 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/add-community-extension/SKILL.md | 5 + docs/community/extensions.md | 3 + extensions/catalog.community.json | 220 +++++++++++++++++- extensions/template/extension.yml | 8 + src/specify_cli/__init__.py | 12 +- src/specify_cli/extensions.py | 29 +++ tests/test_extensions.py | 65 ++++++ 7 files changed, 339 insertions(+), 3 deletions(-) diff --git a/.github/skills/add-community-extension/SKILL.md b/.github/skills/add-community-extension/SKILL.md index 4d56eaa9d0..7b1cc3180d 100644 --- a/.github/skills/add-community-extension/SKILL.md +++ b/.github/skills/add-community-extension/SKILL.md @@ -70,6 +70,8 @@ Use the existing entries as the format template. Required fields: "documentation": "", "changelog": "", "license": "", + "category": "", + "effect": "", "requires": { "speckit_version": "" }, @@ -87,6 +89,9 @@ Use the existing entries as the format template. Required fields: } ``` +**Category** — one of: `docs`, `code`, `process`, `integration`, `visibility` +**Effect** — one of: `read-only`, `read-write` + If the extension has optional tool dependencies, add a `"tools"` array inside `"requires"`: ```json diff --git a/docs/community/extensions.md b/docs/community/extensions.md index 8d41db95c6..7d2438d89a 100644 --- a/docs/community/extensions.md +++ b/docs/community/extensions.md @@ -20,6 +20,9 @@ The following community-contributed extensions are available in [`catalog.commun - `Read-only` — produces reports without modifying files - `Read+Write` — modifies files, creates artifacts, or updates specs +> [!TIP] +> Extension authors can declare `category` and `effect` in their `extension.yml` under the `extension:` block. These fields are also available in `catalog.community.json` for tooling and the CLI (`specify extension info`). + | Extension | Purpose | Category | Effect | URL | |-----------|---------|----------|--------|-----| | Agent Assign | Assign specialized Claude Code agents to spec-kit tasks for targeted execution | `process` | Read+Write | [spec-kit-agent-assign](https://github.com/xymelon/spec-kit-agent-assign) | diff --git a/extensions/catalog.community.json b/extensions/catalog.community.json index 07fb7ef55d..b3ff5e3b3c 100644 --- a/extensions/catalog.community.json +++ b/extensions/catalog.community.json @@ -15,6 +15,8 @@ "documentation": "https://github.com/mnriem/spec-kit-extensions/blob/main/aide/README.md", "changelog": "https://github.com/mnriem/spec-kit-extensions/blob/main/aide/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0" }, @@ -48,6 +50,8 @@ "documentation": "https://github.com/xymelon/spec-kit-agent-assign/blob/main/README.md", "changelog": "https://github.com/xymelon/spec-kit-agent-assign/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -80,6 +84,8 @@ "documentation": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/README.md", "changelog": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.0", "tools": [ @@ -117,6 +123,8 @@ "documentation": "https://github.com/pragya247/spec-kit-orchestrator/blob/main/README.md", "changelog": "https://github.com/pragya247/spec-kit-orchestrator/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.6.1" }, @@ -149,6 +157,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-api-evolve/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-api-evolve/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -186,6 +196,8 @@ "documentation": "https://github.com/bigsmartben/spec-kit-arch/blob/main/README.md", "changelog": "https://github.com/bigsmartben/spec-kit-arch/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.10.dev0" }, @@ -217,6 +229,8 @@ "documentation": "https://github.com/UmmeHabiba1312/spec-kit-architect-preview/blob/main/README.md", "changelog": "https://github.com/UmmeHabiba1312/spec-kit-architect-preview/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -249,6 +263,8 @@ "documentation": "https://github.com/DyanGalih/spec-kit-architecture-guard/blob/main/docs/architecture-overview.md", "changelog": "https://github.com/DyanGalih/spec-kit-architecture-guard/releases", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -283,6 +299,8 @@ "documentation": "https://github.com/stn1slv/spec-kit-archive/blob/main/README.md", "changelog": "https://github.com/stn1slv/spec-kit-archive/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -314,6 +332,8 @@ "documentation": "https://github.com/pragya247/spec-kit-azure-devops/blob/main/README.md", "changelog": "https://github.com/pragya247/spec-kit-azure-devops/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -353,6 +373,8 @@ "documentation": "https://github.com/chordpli/spec-kit-blueprint/blob/main/README.md", "changelog": "https://github.com/chordpli/spec-kit-blueprint/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0" }, @@ -385,6 +407,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-branch-convention/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-branch-convention/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -417,6 +441,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-brownfield/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -438,7 +464,7 @@ "updated_at": "2026-04-10T00:00:00Z" }, "brownkit": { - "name": "BrownKit \u2014 Brownfield Discovery for Spec-Kit", + "name": "BrownKit — Brownfield Discovery for Spec-Kit", "id": "brownkit", "description": "Evidence-driven capability discovery, security and QA risk assessment for existing codebases.", "author": "Maksim Shautsou", @@ -449,6 +475,8 @@ "documentation": "https://github.com/MaksimShevtsov/BrownKit/blob/main/README.md", "changelog": "https://github.com/MaksimShevtsov/BrownKit/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -481,6 +509,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-bugfix/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-bugfix/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -513,6 +543,8 @@ "documentation": "https://github.com/maximiliamus/spec-kit-canon/blob/master/README.md", "changelog": "https://github.com/maximiliamus/spec-kit-canon/blob/master/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.3" }, @@ -548,6 +580,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-catalog-ci/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-catalog-ci/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -580,6 +614,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-changelog/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-changelog/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -612,6 +648,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-ci-guard/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -645,6 +683,8 @@ "documentation": "https://github.com/aaronrsun/spec-kit-checkpoint/blob/main/README.md", "changelog": "https://github.com/aaronrsun/spec-kit-checkpoint/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -674,6 +714,8 @@ "documentation": "https://github.com/dsrednicki/spec-kit-cleanup/blob/main/README.md", "changelog": "https://github.com/dsrednicki/spec-kit-cleanup/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -706,6 +748,8 @@ "documentation": "https://github.com/twbrandon7/spec-kit-conduct-ext/blob/main/README.md", "changelog": "https://github.com/twbrandon7/spec-kit-conduct-ext/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.1" }, @@ -736,6 +780,8 @@ "documentation": "https://github.com/arunt14/spec-kit-critique/blob/main/README.md", "changelog": "https://github.com/arunt14/spec-kit-critique/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -766,6 +812,8 @@ "documentation": "https://github.com/aaronrsun/spec-kit-confluence/blob/main/README.md", "changelog": "https://github.com/aaronrsun/spec-kit-confluence/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -794,6 +842,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-cost/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-cost/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.0" }, @@ -826,6 +876,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-diagram-/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-diagram-/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -858,6 +910,8 @@ "documentation": "https://github.com/raccioly/docguard/blob/main/extensions/spec-kit-docguard/README.md", "changelog": "https://github.com/raccioly/docguard/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -900,6 +954,8 @@ "documentation": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/README.md", "changelog": "https://github.com/KhawarHabibKhan/spec-kit-doctor/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -931,6 +987,8 @@ "documentation": "https://github.com/mnriem/spec-kit-extensions/blob/main/extensify/README.md", "changelog": "https://github.com/mnriem/spec-kit-extensions/blob/main/extensify/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.0" }, @@ -962,6 +1020,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-fix-findings/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-fix-findings/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -994,6 +1054,8 @@ "documentation": "https://github.com/speckit-community/spec-kit-fixit/blob/main/README.md", "changelog": "https://github.com/speckit-community/spec-kit-fixit/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1025,6 +1087,8 @@ "documentation": "https://github.com/sharathsatish/spec-kit-fleet/blob/main/README.md", "changelog": "https://github.com/sharathsatish/spec-kit-fleet/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1055,6 +1119,8 @@ "homepage": "https://github.com/RogerBestMsft/spec-kit-FxToNet", "documentation": "https://github.com/RogerBestMsft/spec-kit-FxToNet/blob/main/README.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -1094,6 +1160,8 @@ "documentation": "https://github.com/Fatima367/spec-kit-github-issues/blob/main/README.md", "changelog": "https://github.com/Fatima367/spec-kit-github-issues/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -1134,6 +1202,8 @@ "documentation": "https://github.com/aaronrsun/spec-kit-issue/blob/main/README.md", "changelog": "https://github.com/aaronrsun/spec-kit-issue/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1166,6 +1236,8 @@ "documentation": "https://github.com/imviancagrace/spec-kit-iterate/blob/main/README.md", "changelog": "https://github.com/imviancagrace/spec-kit-iterate/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1196,6 +1268,8 @@ "documentation": "https://github.com/mbachorik/spec-kit-jira/blob/main/README.md", "changelog": "https://github.com/mbachorik/spec-kit-jira/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1227,6 +1301,8 @@ "documentation": "https://github.com/imviancagrace/spec-kit-learn/blob/main/README.md", "changelog": "https://github.com/imviancagrace/spec-kit-learn/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1258,6 +1334,8 @@ "documentation": "https://github.com/ashbrener/spec-kit-linear/blob/main/README.md", "changelog": "https://github.com/ashbrener/spec-kit-linear/releases", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1291,6 +1369,8 @@ "documentation": "https://github.com/BenBtg/spec-kit-m365/blob/main/README.md", "changelog": "https://github.com/BenBtg/spec-kit-m365/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -1329,6 +1409,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-ext/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-ext/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1362,6 +1444,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-azure-devops/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1394,6 +1478,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-ci/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-ci/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1427,6 +1513,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-github-projects/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1459,6 +1547,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-jira/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-jira/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1491,6 +1581,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-linear/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-linear/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1523,6 +1615,8 @@ "documentation": "https://github.com/GenieRobot/spec-kit-maqa-trello/blob/main/README.md", "changelog": "https://github.com/GenieRobot/spec-kit-maqa-trello/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-write", "requires": { "speckit_version": ">=0.3.0" }, @@ -1555,6 +1649,8 @@ "documentation": "https://github.com/BenBtg/spec-kit-markitdown/blob/main/README.md", "changelog": "https://github.com/BenBtg/spec-kit-markitdown/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -1592,6 +1688,8 @@ "repository": "https://github.com/AI-MDE/spec-kit-mde", "homepage": "https://github.com/AI-MDE/spec-kit-mde", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1623,6 +1721,8 @@ "documentation": "https://github.com/KevinBrown5280/spec-kit-memory-loader/blob/main/README.md", "changelog": "https://github.com/KevinBrown5280/spec-kit-memory-loader/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-only", "requires": { "speckit_version": ">=0.6.0" }, @@ -1654,6 +1754,8 @@ "documentation": "https://github.com/DyanGalih/spec-kit-memory-hub/blob/main/README.md", "changelog": "https://github.com/DyanGalih/spec-kit-memory-hub/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0" }, @@ -1687,6 +1789,8 @@ "documentation": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/memorylint/README.md", "changelog": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/memorylint/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.5.1" }, @@ -1719,6 +1823,8 @@ "documentation": "https://github.com/formin/multi-model-review/blob/main/README.md", "changelog": "https://github.com/formin/multi-model-review/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0", "tools": [ @@ -1769,6 +1875,8 @@ "documentation": "https://github.com/teeyo/spec-kit-multi-sites/blob/main/README.md", "changelog": "https://github.com/teeyo/spec-kit-multi-sites/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1800,6 +1908,8 @@ "documentation": "https://github.com/dmux/spec-kit-onboard/blob/main/README.md", "changelog": "https://github.com/dmux/spec-kit-onboard/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1833,6 +1943,8 @@ "documentation": "https://github.com/sakitA/spec-kit-optimize/blob/main/README.md", "changelog": "https://github.com/sakitA/spec-kit-optimize/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -1865,6 +1977,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-orchestrator/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-orchestrator/releases", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -1897,6 +2011,8 @@ "documentation": "https://github.com/luno/spec-kit-plan-review-gate/blob/main/README.md", "changelog": "https://github.com/luno/spec-kit-plan-review-gate/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -1928,6 +2044,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-pr-bridge-/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -1960,6 +2078,8 @@ "documentation": "https://github.com/mnriem/spec-kit-extensions/blob/main/presetify/README.md", "changelog": "https://github.com/mnriem/spec-kit-extensions/blob/main/presetify/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0" }, @@ -1991,6 +2111,8 @@ "documentation": "https://github.com/bigsmartben/spec-kit-preview/blob/main/README.md", "changelog": "https://github.com/bigsmartben/spec-kit-preview/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.10.dev0" }, @@ -2022,6 +2144,8 @@ "documentation": "https://github.com/d0whc3r/spec-kit-product/wiki", "changelog": "https://github.com/d0whc3r/spec-kit-product/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0" }, @@ -2064,6 +2188,8 @@ "documentation": "https://github.com/VaiYav/speckit-product-forge/blob/main/README.md", "changelog": "https://github.com/VaiYav/speckit-product-forge/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2096,6 +2222,8 @@ "documentation": "https://github.com/arunt14/spec-kit-qa/blob/main/README.md", "changelog": "https://github.com/arunt14/spec-kit-qa/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -2126,6 +2254,8 @@ "documentation": "https://github.com/Sertxito/spec-kit-extension-rag-azure-builder#readme", "changelog": "https://github.com/Sertxito/spec-kit-extension-rag-azure-builder/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.0" }, @@ -2158,6 +2288,8 @@ "documentation": "https://github.com/Rubiss-Projects/spec-kit-ralph/blob/main/README.md", "changelog": "https://github.com/Rubiss-Projects/spec-kit-ralph/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -2199,6 +2331,8 @@ "documentation": "https://github.com/stn1slv/spec-kit-reconcile/blob/main/README.md", "changelog": "https://github.com/stn1slv/spec-kit-reconcile/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2230,6 +2364,8 @@ "documentation": "https://github.com/ashbrener/spec-kit-red-team/blob/main/README.md", "changelog": "https://github.com/ashbrener/spec-kit-red-team/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2262,6 +2398,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-refine/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-refine/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -2294,6 +2432,8 @@ "documentation": "https://github.com/liuyiyu/spec-kit-repoindex/tree/main/docs", "changelog": "https://github.com/liuyiyu/spec-kit-repoindex/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -2331,6 +2471,8 @@ "documentation": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd#readme", "changelog": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.0", "tools": [ @@ -2369,6 +2511,8 @@ "documentation": "https://github.com/arunt14/spec-kit-retro/blob/main/README.md", "changelog": "https://github.com/arunt14/spec-kit-retro/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2399,6 +2543,8 @@ "documentation": "https://github.com/emi-dm/spec-kit-retrospective/blob/main/README.md", "changelog": "https://github.com/emi-dm/spec-kit-retrospective/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2431,6 +2577,8 @@ "documentation": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/README.md", "changelog": "https://github.com/ismaelJimenez/spec-kit-review/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -2465,6 +2613,8 @@ "documentation": "https://github.com/chordpli/spec-kit-ripple/blob/main/README.md", "changelog": "https://github.com/chordpli/spec-kit-ripple/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.2.0" }, @@ -2497,6 +2647,8 @@ "documentation": "https://github.com/jfranc38/spec-kit-schedule/blob/main/README.md", "changelog": "https://github.com/jfranc38/spec-kit-schedule/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -2529,6 +2681,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-scope-/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-scope-/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.4.0" }, @@ -2562,6 +2716,8 @@ "documentation": "https://github.com/DyanGalih/spec-kit-security-review/blob/main/README.md", "changelog": "https://github.com/DyanGalih/spec-kit-security-review/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2594,6 +2750,8 @@ "documentation": "https://ysumanth06.github.io/spec-kit-sf/introduction.html", "changelog": "https://github.com/ysumanth06/spec-kit-sf/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0", "tools": [ @@ -2638,6 +2796,8 @@ "documentation": "https://github.com/arunt14/spec-kit-ship/blob/main/README.md", "changelog": "https://github.com/arunt14/spec-kit-ship/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2668,6 +2828,8 @@ "documentation": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader/blob/main/README.md", "changelog": "https://github.com/KevinBrown5280/spec-kit-spec-reference-loader/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-only", "requires": { "speckit_version": ">=0.6.0" }, @@ -2699,6 +2861,8 @@ "documentation": "https://github.com/aeltayeb/spec-kit-spec-validate/blob/main/README.md", "changelog": "https://github.com/aeltayeb/spec-kit-spec-validate/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.5.0" }, @@ -2731,6 +2895,8 @@ "documentation": "https://github.com/Azure-Samples/Spec2Cloud/blob/main/spec-kit/README.md", "changelog": "https://github.com/Azure-Samples/Spec2Cloud/blob/main/spec-kit/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -2763,6 +2929,8 @@ "documentation": "https://github.com/lihan3238/speckit-superpowers-bridge#readme", "changelog": "https://github.com/lihan3238/speckit-superpowers-bridge/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.10", "tools": [ @@ -2812,6 +2980,8 @@ "documentation": "https://github.com/mvanhorn/speckit-utils/blob/main/README.md", "changelog": "https://github.com/mvanhorn/speckit-utils/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -2844,6 +3014,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-spectest/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -2877,6 +3049,8 @@ "documentation": "https://github.com/jwill824/spec-kit-squad/blob/main/README.md", "changelog": "https://github.com/jwill824/spec-kit-squad/blob/main/docs/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.8.11", "tools": [ @@ -2916,6 +3090,8 @@ "documentation": "https://github.com/arunt14/spec-kit-staff-review/blob/main/README.md", "changelog": "https://github.com/arunt14/spec-kit-staff-review/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -2946,6 +3122,8 @@ "documentation": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/README.md", "changelog": "https://github.com/KhawarHabibKhan/spec-kit-status/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -2978,6 +3156,8 @@ "documentation": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/README.md", "changelog": "https://github.com/Open-Agent-Tools/spec-kit-status/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -3008,6 +3188,8 @@ "documentation": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/superpowers-bridge/README.md", "changelog": "https://github.com/RbBtSn0w/spec-kit-extensions/blob/main/superpowers-bridge/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.3", "tools": [ @@ -3051,6 +3233,8 @@ "documentation": "https://github.com/WangX0111/superspec/blob/main/README.md", "changelog": "https://github.com/WangX0111/superspec/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -3084,6 +3268,8 @@ "documentation": "https://github.com/bgervin/spec-kit-sync/blob/main/README.md", "changelog": "https://github.com/bgervin/spec-kit-sync/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -3116,6 +3302,8 @@ "documentation": "https://github.com/tarunkumarbhati/spec-kit-team-assign/blob/main/README.md", "changelog": "https://github.com/tarunkumarbhati/spec-kit-team-assign/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -3147,6 +3335,8 @@ "documentation": "https://github.com/teeyo/spec-kit-time-machine", "changelog": "https://github.com/teeyo/spec-kit-time-machine/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -3184,6 +3374,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-tinyspec/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -3216,6 +3408,8 @@ "documentation": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/README.md", "changelog": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-only", "requires": { "speckit_version": ">=0.6.0" }, @@ -3248,6 +3442,8 @@ "documentation": "https://github.com/coderandhiker/spec-kit-token-analyzer/blob/main/README.md", "changelog": "https://github.com/coderandhiker/spec-kit-token-analyzer/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.2.0" }, @@ -3279,6 +3475,8 @@ "documentation": "https://github.com/tinesoft/spec-kit-token-budget/blob/main/README.md", "changelog": "https://github.com/tinesoft/spec-kit-token-budget/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -3321,6 +3519,8 @@ "documentation": "https://github.com/leocamello/spec-kit-v-model/blob/main/README.md", "changelog": "https://github.com/leocamello/spec-kit-v-model/blob/main/CHANGELOG.md", "license": "MIT", + "category": "docs", + "effect": "read-write", "requires": { "speckit_version": ">=0.1.0" }, @@ -3353,6 +3553,8 @@ "documentation": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/README.md", "changelog": "https://github.com/ismaelJimenez/spec-kit-verify/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -3385,6 +3587,8 @@ "documentation": "https://github.com/datastone-inc/spec-kit-verify-tasks/blob/main/README.md", "changelog": "https://github.com/datastone-inc/spec-kit-verify-tasks/blob/main/CHANGELOG.md", "license": "MIT", + "category": "code", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0" }, @@ -3416,6 +3620,8 @@ "documentation": "https://github.com/KevinBrown5280/spec-kit-version-guard/blob/main/README.md", "changelog": "https://github.com/KevinBrown5280/spec-kit-version-guard/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-only", "requires": { "speckit_version": ">=0.2.0" }, @@ -3445,6 +3651,8 @@ "homepage": "https://github.com/DevAbdullah90/spec-kit-whatif", "documentation": "https://github.com/DevAbdullah90/spec-kit-whatif/blob/main/README.md", "license": "MIT", + "category": "visibility", + "effect": "read-only", "requires": { "speckit_version": ">=0.6.0" }, @@ -3475,6 +3683,8 @@ "documentation": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/blob/main/README.md", "changelog": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/blob/main/CHANGELOG.md", "license": "MIT", + "category": "visibility", + "effect": "read-write", "requires": { "speckit_version": ">=0.6.0" }, @@ -3510,6 +3720,8 @@ "documentation": "https://github.com/sakitA/spec-kit-workiq/blob/main/README.md", "changelog": "https://github.com/sakitA/spec-kit-workiq/blob/main/CHANGELOG.md", "license": "MIT", + "category": "integration", + "effect": "read-only", "requires": { "speckit_version": ">=0.1.0", "tools": [ @@ -3554,6 +3766,8 @@ "documentation": "https://github.com/Quratulain-bilal/spec-kit-worktree/blob/main/README.md", "changelog": "https://github.com/Quratulain-bilal/spec-kit-worktree/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -3586,6 +3800,8 @@ "documentation": "https://github.com/dango85/spec-kit-worktree-parallel/blob/main/README.md", "changelog": "https://github.com/dango85/spec-kit-worktree-parallel/blob/main/CHANGELOG.md", "license": "MIT", + "category": "process", + "effect": "read-write", "requires": { "speckit_version": ">=0.4.0" }, @@ -3607,4 +3823,4 @@ "updated_at": "2026-04-13T00:00:00Z" } } -} \ No newline at end of file +} diff --git a/extensions/template/extension.yml b/extensions/template/extension.yml index b907e0c786..5e7151df2d 100644 --- a/extensions/template/extension.yml +++ b/extensions/template/extension.yml @@ -13,6 +13,14 @@ extension: # CUSTOMIZE: Brief description (under 200 characters) description: "Brief description of what your extension does" + # CUSTOMIZE: Extension category — describes what the extension operates on + # One of: docs | code | process | integration | visibility + category: "process" + + # CUSTOMIZE: Extension effect — whether it modifies project files + # One of: read-only | read-write + effect: "read-write" + # CUSTOMIZE: Your name or organization name author: "Your Name" diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 23d31cb0cf..96a65e5e9f 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1983,7 +1983,11 @@ def extension_info( author = ext_manifest.data.get("extension", {}).get("author") if author: console.print(f"[dim]Author:[/dim] {author}") - console.print() + if ext_manifest.category: + console.print(f"[dim]Category:[/dim] {ext_manifest.category}") + if ext_manifest.effect: + console.print(f"[dim]Effect:[/dim] {ext_manifest.effect}") + console.print() if ext_manifest.commands: console.print("[bold]Commands:[/bold]") @@ -2033,6 +2037,12 @@ def _print_extension_info(ext_info: dict, manager): console.print(f"[dim]Author:[/dim] {ext_info.get('author', 'Unknown')}") console.print(f"[dim]License:[/dim] {ext_info.get('license', 'Unknown')}") + # Category and Effect + if ext_info.get('category'): + console.print(f"[dim]Category:[/dim] {ext_info['category']}") + if ext_info.get('effect'): + console.print(f"[dim]Effect:[/dim] {ext_info['effect']}") + # Source catalog if ext_info.get("_catalog_name"): install_allowed = ext_info.get("_install_allowed", True) diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index adbbedcb94..b3097e28d9 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -41,6 +41,9 @@ }) EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$") +VALID_CATEGORIES = frozenset({"docs", "code", "process", "integration", "visibility"}) +VALID_EFFECTS = frozenset({"read-only", "read-write"}) + DEFAULT_HOOK_PRIORITY = 10 REINSTALL_COMMAND = "uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git" @@ -201,6 +204,22 @@ def _validate(self): except pkg_version.InvalidVersion: raise ValidationError(f"Invalid version: {ext['version']}") + # Validate optional category field + if "category" in ext: + if ext["category"] not in VALID_CATEGORIES: + raise ValidationError( + f"Invalid extension.category '{ext['category']}': " + f"must be one of {sorted(VALID_CATEGORIES)}" + ) + + # Validate optional effect field + if "effect" in ext: + if ext["effect"] not in VALID_EFFECTS: + raise ValidationError( + f"Invalid extension.effect '{ext['effect']}': " + f"must be one of {sorted(VALID_EFFECTS)}" + ) + # Validate requires section requires = self.data["requires"] if "speckit_version" not in requires: @@ -374,6 +393,16 @@ def description(self) -> str: """Get extension description.""" return self.data["extension"]["description"] + @property + def category(self) -> Optional[str]: + """Get extension category (docs, code, process, integration, visibility).""" + return self.data["extension"].get("category") + + @property + def effect(self) -> Optional[str]: + """Get extension effect (read-only, read-write).""" + return self.data["extension"].get("effect") + @property def requires_speckit_version(self) -> str: """Get required spec-kit version range.""" diff --git a/tests/test_extensions.py b/tests/test_extensions.py index dd231de311..2eef8f4667 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -24,6 +24,8 @@ CatalogEntry, CORE_COMMAND_NAMES, DEFAULT_HOOK_PRIORITY, + VALID_CATEGORIES, + VALID_EFFECTS, ExtensionManifest, ExtensionRegistry, ExtensionManager, @@ -300,6 +302,69 @@ def test_invalid_version(self, temp_dir, valid_manifest_data): with pytest.raises(ValidationError, match="Invalid version"): ExtensionManifest(manifest_path) + def test_valid_category(self, temp_dir, valid_manifest_data): + """Test manifest with valid category values.""" + import yaml + + for category in ("docs", "code", "process", "integration", "visibility"): + valid_manifest_data["extension"]["category"] = category + manifest_path = temp_dir / "extension.yml" + with open(manifest_path, 'w') as f: + yaml.dump(valid_manifest_data, f) + manifest = ExtensionManifest(manifest_path) + assert manifest.category == category + + def test_valid_effect(self, temp_dir, valid_manifest_data): + """Test manifest with valid effect values.""" + import yaml + + for effect in ("read-only", "read-write"): + valid_manifest_data["extension"]["effect"] = effect + manifest_path = temp_dir / "extension.yml" + with open(manifest_path, 'w') as f: + yaml.dump(valid_manifest_data, f) + manifest = ExtensionManifest(manifest_path) + assert manifest.effect == effect + + def test_invalid_category(self, temp_dir, valid_manifest_data): + """Test manifest with invalid category raises ValidationError.""" + import yaml + + valid_manifest_data["extension"]["category"] = "invalid-category" + manifest_path = temp_dir / "extension.yml" + with open(manifest_path, 'w') as f: + yaml.dump(valid_manifest_data, f) + + with pytest.raises(ValidationError, match="Invalid extension.category"): + ExtensionManifest(manifest_path) + + def test_invalid_effect(self, temp_dir, valid_manifest_data): + """Test manifest with invalid effect raises ValidationError.""" + import yaml + + valid_manifest_data["extension"]["effect"] = "write-only" + manifest_path = temp_dir / "extension.yml" + with open(manifest_path, 'w') as f: + yaml.dump(valid_manifest_data, f) + + with pytest.raises(ValidationError, match="Invalid extension.effect"): + ExtensionManifest(manifest_path) + + def test_category_and_effect_optional(self, temp_dir, valid_manifest_data): + """Test that omitting category and effect still passes validation.""" + import yaml + + # Ensure no category/effect in data + valid_manifest_data["extension"].pop("category", None) + valid_manifest_data["extension"].pop("effect", None) + manifest_path = temp_dir / "extension.yml" + with open(manifest_path, 'w') as f: + yaml.dump(valid_manifest_data, f) + + manifest = ExtensionManifest(manifest_path) + assert manifest.category is None + assert manifest.effect is None + def test_invalid_command_name(self, temp_dir, valid_manifest_data): """Test manifest with command name that cannot be auto-corrected raises ValidationError.""" import yaml From 26f0407077d809e2276ed7a1dcff8516619622ea Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Mon, 8 Jun 2026 10:35:40 -0500 Subject: [PATCH 2/5] fix: make category free-form, keep effect validated Category is a free-form string (only validated as non-empty when present), while effect remains restricted to 'read-only' or 'read-write'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/add-community-extension/SKILL.md | 4 ++-- docs/community/extensions.md | 2 +- extensions/template/extension.yml | 2 +- src/specify_cli/extensions.py | 8 +++----- tests/test_extensions.py | 9 ++++----- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/skills/add-community-extension/SKILL.md b/.github/skills/add-community-extension/SKILL.md index 7b1cc3180d..fb63aad955 100644 --- a/.github/skills/add-community-extension/SKILL.md +++ b/.github/skills/add-community-extension/SKILL.md @@ -89,7 +89,7 @@ Use the existing entries as the format template. Required fields: } ``` -**Category** — one of: `docs`, `code`, `process`, `integration`, `visibility` +**Category** — free-form string; common values: `docs`, `code`, `process`, `integration`, `visibility` **Effect** — one of: `read-only`, `read-write` If the extension has optional tool dependencies, add a `"tools"` array inside `"requires"`: @@ -118,7 +118,7 @@ Determine the category and effect from the extension's behavior: | | | `` | | []() | ``` -**Category** — one of: `docs`, `code`, `process`, `integration`, `visibility` +**Category** — free-form; common values: `docs`, `code`, `process`, `integration`, `visibility` **Effect** — `Read-only` (produces reports only) or `Read+Write` (modifies project files) ### 6. Commit, push, and open PR diff --git a/docs/community/extensions.md b/docs/community/extensions.md index 7d2438d89a..b7b4072042 100644 --- a/docs/community/extensions.md +++ b/docs/community/extensions.md @@ -7,7 +7,7 @@ The following community-contributed extensions are available in [`catalog.community.json`](https://github.com/github/spec-kit/blob/main/extensions/catalog.community.json): -**Categories:** +**Categories** (common values, but any string is allowed): - `docs` — reads, validates, or generates spec artifacts - `code` — reviews, validates, or modifies source code diff --git a/extensions/template/extension.yml b/extensions/template/extension.yml index 5e7151df2d..6c80000244 100644 --- a/extensions/template/extension.yml +++ b/extensions/template/extension.yml @@ -14,7 +14,7 @@ extension: description: "Brief description of what your extension does" # CUSTOMIZE: Extension category — describes what the extension operates on - # One of: docs | code | process | integration | visibility + # Common values: docs, code, process, integration, visibility category: "process" # CUSTOMIZE: Extension effect — whether it modifies project files diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index b3097e28d9..6364d0c917 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -41,7 +41,6 @@ }) EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$") -VALID_CATEGORIES = frozenset({"docs", "code", "process", "integration", "visibility"}) VALID_EFFECTS = frozenset({"read-only", "read-write"}) DEFAULT_HOOK_PRIORITY = 10 @@ -204,12 +203,11 @@ def _validate(self): except pkg_version.InvalidVersion: raise ValidationError(f"Invalid version: {ext['version']}") - # Validate optional category field + # Validate optional category field (free-form string) if "category" in ext: - if ext["category"] not in VALID_CATEGORIES: + if not isinstance(ext["category"], str) or not ext["category"].strip(): raise ValidationError( - f"Invalid extension.category '{ext['category']}': " - f"must be one of {sorted(VALID_CATEGORIES)}" + "Invalid extension.category: must be a non-empty string" ) # Validate optional effect field diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 2eef8f4667..e8cbc4e1ee 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -24,7 +24,6 @@ CatalogEntry, CORE_COMMAND_NAMES, DEFAULT_HOOK_PRIORITY, - VALID_CATEGORIES, VALID_EFFECTS, ExtensionManifest, ExtensionRegistry, @@ -303,10 +302,10 @@ def test_invalid_version(self, temp_dir, valid_manifest_data): ExtensionManifest(manifest_path) def test_valid_category(self, temp_dir, valid_manifest_data): - """Test manifest with valid category values.""" + """Test manifest with various category values (free-form string).""" import yaml - for category in ("docs", "code", "process", "integration", "visibility"): + for category in ("docs", "code", "process", "integration", "visibility", "custom-category"): valid_manifest_data["extension"]["category"] = category manifest_path = temp_dir / "extension.yml" with open(manifest_path, 'w') as f: @@ -327,10 +326,10 @@ def test_valid_effect(self, temp_dir, valid_manifest_data): assert manifest.effect == effect def test_invalid_category(self, temp_dir, valid_manifest_data): - """Test manifest with invalid category raises ValidationError.""" + """Test manifest with empty category raises ValidationError.""" import yaml - valid_manifest_data["extension"]["category"] = "invalid-category" + valid_manifest_data["extension"]["category"] = "" manifest_path = temp_dir / "extension.yml" with open(manifest_path, 'w') as f: yaml.dump(valid_manifest_data, f) From e6e23a490fc1ddc3ef5ddc2f91a82860166bb83e Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Mon, 8 Jun 2026 10:54:24 -0500 Subject: [PATCH 3/5] fix: address PR review feedback - Add type guard before 'in' check for effect to prevent TypeError on unhashable YAML values (list/dict) - Comment out category/effect in template so authors must opt in - Use VALID_EFFECTS constant in test instead of hard-coded values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- extensions/template/extension.yml | 4 ++-- src/specify_cli/extensions.py | 4 ++-- tests/test_extensions.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/template/extension.yml b/extensions/template/extension.yml index 6c80000244..a23bdc87da 100644 --- a/extensions/template/extension.yml +++ b/extensions/template/extension.yml @@ -15,11 +15,11 @@ extension: # CUSTOMIZE: Extension category — describes what the extension operates on # Common values: docs, code, process, integration, visibility - category: "process" + # category: "process" # CUSTOMIZE: Extension effect — whether it modifies project files # One of: read-only | read-write - effect: "read-write" + # effect: "read-write" # CUSTOMIZE: Your name or organization name author: "Your Name" diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 6364d0c917..38342366f2 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -212,9 +212,9 @@ def _validate(self): # Validate optional effect field if "effect" in ext: - if ext["effect"] not in VALID_EFFECTS: + if not isinstance(ext["effect"], str) or ext["effect"] not in VALID_EFFECTS: raise ValidationError( - f"Invalid extension.effect '{ext['effect']}': " + f"Invalid extension.effect '{ext.get('effect')}': " f"must be one of {sorted(VALID_EFFECTS)}" ) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index e8cbc4e1ee..d8cd0bcbe6 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -317,7 +317,7 @@ def test_valid_effect(self, temp_dir, valid_manifest_data): """Test manifest with valid effect values.""" import yaml - for effect in ("read-only", "read-write"): + for effect in sorted(VALID_EFFECTS): valid_manifest_data["extension"]["effect"] = effect manifest_path = temp_dir / "extension.yml" with open(manifest_path, 'w') as f: From c90d76cc0d575392ac113061d10c53755b5e8ebb Mon Sep 17 00:00:00 2001 From: Manfred Riem Date: Mon, 8 Jun 2026 11:01:36 -0500 Subject: [PATCH 4/5] fix: update category docstring to reflect free-form semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 38342366f2..daf009e4b0 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -393,7 +393,7 @@ def description(self) -> str: @property def category(self) -> Optional[str]: - """Get extension category (docs, code, process, integration, visibility).""" + """Get extension category (free-form; common values: docs, code, process, integration, visibility).""" return self.data["extension"].get("category") @property From 1e2443267ac1e1fc0dcc8d42ff3f0d6bbb9ea0ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:33:36 +0000 Subject: [PATCH 5/5] docs: clarify canonical extension effect values --- .github/skills/add-community-extension/SKILL.md | 2 +- docs/community/extensions.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/skills/add-community-extension/SKILL.md b/.github/skills/add-community-extension/SKILL.md index fb63aad955..179c11b3e2 100644 --- a/.github/skills/add-community-extension/SKILL.md +++ b/.github/skills/add-community-extension/SKILL.md @@ -119,7 +119,7 @@ Determine the category and effect from the extension's behavior: ``` **Category** — free-form; common values: `docs`, `code`, `process`, `integration`, `visibility` -**Effect** — `Read-only` (produces reports only) or `Read+Write` (modifies project files) +**Effect** — write canonical values `read-only` or `read-write` in `extension.yml` and `catalog.community.json`; use `Read-only`/`Read+Write` only for the docs table display ### 6. Commit, push, and open PR diff --git a/docs/community/extensions.md b/docs/community/extensions.md index b7b4072042..517c32290f 100644 --- a/docs/community/extensions.md +++ b/docs/community/extensions.md @@ -15,10 +15,10 @@ The following community-contributed extensions are available in [`catalog.commun - `integration` — syncs with external platforms - `visibility` — reports on project health or progress -**Effect:** +**Effect** (canonical `extension.yml`/catalog values): -- `Read-only` — produces reports without modifying files -- `Read+Write` — modifies files, creates artifacts, or updates specs +- `read-only` — produces reports without modifying files (displayed as `Read-only` in the table) +- `read-write` — modifies files, creates artifacts, or updates specs (displayed as `Read+Write` in the table) > [!TIP] > Extension authors can declare `category` and `effect` in their `extension.yml` under the `extension:` block. These fields are also available in `catalog.community.json` for tooling and the CLI (`specify extension info`).