diff --git a/presets/catalog.json b/presets/catalog.json index 39bacb4157..6fc1f53e4e 100644 --- a/presets/catalog.json +++ b/presets/catalog.json @@ -1,6 +1,6 @@ { "schema_version": "1.0", - "updated_at": "2026-08-04T00:00:00Z", + "updated_at": "2026-09-10T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json", "presets": { "lean": { @@ -48,6 +48,28 @@ "templates", "compatibility" ] + }, + "clarify-spec-gate": { + "name": "Clarify spec-stage gate", + "id": "clarify-spec-gate", + "version": "1.0.0", + "description": "Opt-in: wrap /speckit.clarify so spec-taxonomy items stay in the spec stage. Defer only implementation method, tech-stack comparison, or task breakdown.", + "author": "github", + "repository": "https://github.com/github/spec-kit", + "license": "MIT", + "bundled": true, + "requires": { + "speckit_version": ">=0.14.4" + }, + "provides": { + "commands": 1, + "templates": 0 + }, + "tags": [ + "clarify", + "spec", + "methodology" + ] } } } diff --git a/presets/clarify-spec-gate/README.md b/presets/clarify-spec-gate/README.md new file mode 100644 index 0000000000..14a7bde5ac --- /dev/null +++ b/presets/clarify-spec-gate/README.md @@ -0,0 +1,28 @@ +# Clarify spec-stage gate + +Opt-in wrap of `/speckit.clarify`. Core already lists the spec taxonomy. This preset adds a stage gate so agents do not dump NFRs, acceptance criteria, edge cases, and similar items into Plan. + +Install it if you want that enforcement. Leave it off if core `/speckit.clarify` is enough. + +## What it does + +It wraps `speckit.clarify` with `{CORE_TEMPLATE}`, so core command updates still land. On top of that it: + +- Treats a taxonomy hit as a spec-stage question +- Defers only implementation method, tech-stack comparison, or task breakdown +- Pauses when more than 60% of unresolved items would be deferred +- Adds a MUST-NOT on spec-taxonomy deferral + +## Installation + +```bash +specify preset add clarify-spec-gate +``` + +## Development + +```bash +specify preset add --dev ./presets/clarify-spec-gate +specify preset resolve speckit.clarify +specify preset remove clarify-spec-gate +``` diff --git a/presets/clarify-spec-gate/commands/speckit.clarify.md b/presets/clarify-spec-gate/commands/speckit.clarify.md new file mode 100644 index 0000000000..ff9105d258 --- /dev/null +++ b/presets/clarify-spec-gate/commands/speckit.clarify.md @@ -0,0 +1,25 @@ +--- +description: Identify underspecified areas in the current feature spec, keeping spec-taxonomy items in this stage. +strategy: wrap +--- + +## Spec-vs-plan stage gate + +This preset tightens deferral. Apply it while you scan the spec (the taxonomy lives in the core command below). + +For every unchecked checklist item and every Partial/Missing taxonomy category, classify before considering deferral: + +1. Match the item against the spec-oriented taxonomy in the core command. +2. A hit on any taxonomy category is a question candidate for this stage. Do not defer NFRs, acceptance/DoD testability, edge cases, UX empty states, domain constraints, or external-dependency failure modes. +3. Defer to planning only when the item is specifically about implementation method, tech-stack comparison, or task breakdown. +4. Mixed items (spec decision plus plan detail): split them and handle the spec part now. + +If more than 60% of unresolved items would be marked Defer, pause, report the ratio, and re-check each against the taxonomy before continuing. + +Do not use a vague "better deferred to planning" catch-all. + +{CORE_TEMPLATE} + +## Spec-taxonomy MUST-NOT + +MUST NOT defer spec-taxonomy items to Plan. Concurrent-user volume, NFR quantification, acceptance-criteria testability, empty-state UX, and external-dependency failure modes are spec questions. How you implement pagination can wait. Whether the API paginates cannot. diff --git a/presets/clarify-spec-gate/preset.yml b/presets/clarify-spec-gate/preset.yml new file mode 100644 index 0000000000..0d50dba2c5 --- /dev/null +++ b/presets/clarify-spec-gate/preset.yml @@ -0,0 +1,26 @@ +schema_version: "1.0" + +preset: + id: "clarify-spec-gate" + name: "Clarify spec-stage gate" + version: "1.0.0" + description: "Opt-in: wrap /speckit.clarify so spec-taxonomy items stay in the spec stage. Defer only implementation method, tech-stack comparison, or task breakdown." + author: "github" + repository: "https://github.com/github/spec-kit" + license: "MIT" + +requires: + speckit_version: ">=0.14.4" + +provides: + templates: + - type: "command" + name: "speckit.clarify" + file: "commands/speckit.clarify.md" + description: "Wrap /speckit.clarify with a spec-vs-plan stage gate and defer-ratio audit" + strategy: "wrap" + +tags: + - "clarify" + - "spec" + - "methodology" diff --git a/pyproject.toml b/pyproject.toml index ee1dc4ab31..042b3c24e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ packages = ["src/specify_cli"] # Bundled presets (installable via `specify preset add ` or `specify init --preset `) "presets/lean" = "specify_cli/core_pack/presets/lean" "presets/constitution-sync" = "specify_cli/core_pack/presets/constitution-sync" +"presets/clarify-spec-gate" = "specify_cli/core_pack/presets/clarify-spec-gate" # Community bundle catalog snapshot (used for offline discovery) "bundles/catalog.community.json" = "specify_cli/core_pack/bundles/catalog.community.json" diff --git a/tests/contract/test_wheel_bundled_presets.py b/tests/contract/test_wheel_bundled_presets.py index 29faff5d59..28ceeb260c 100644 --- a/tests/contract/test_wheel_bundled_presets.py +++ b/tests/contract/test_wheel_bundled_presets.py @@ -51,3 +51,10 @@ def test_constitution_sync_is_bundled_and_shipped(): assert _force_include()["presets/constitution-sync"] == ( "specify_cli/core_pack/presets/constitution-sync" ) + + +def test_clarify_spec_gate_is_bundled_and_shipped(): + assert "clarify-spec-gate" in _bundled_preset_ids() + assert _force_include()["presets/clarify-spec-gate"] == ( + "specify_cli/core_pack/presets/clarify-spec-gate" + ) diff --git a/tests/test_presets.py b/tests/test_presets.py index 17aef20dce..ac802bb535 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -14730,3 +14730,69 @@ def test_resolved_content_embeds_core_and_sync_pass(self, project_dir): assert "## Constitution Template Sync" in content assert "supersedes the \"Scope Guard\" above" in content assert "plan-template.md" in content + + +class TestClarifySpecGatePreset: + """Bundled opt-in wrap of ``/speckit.clarify`` (#1717). + + Core already lists the spec taxonomy. This preset adds the stage-gate + procedure, defer-ratio audit, and MUST-NOT block so teams can opt in + without imposing that methodology on every install. + """ + + PRESET_DIR = Path(__file__).parent.parent / "presets" / "clarify-spec-gate" + + def test_manifest_provides_wrap_of_clarify(self): + manifest = yaml.safe_load((self.PRESET_DIR / "preset.yml").read_text()) + assert manifest["preset"]["id"] == "clarify-spec-gate" + entries = manifest["provides"]["templates"] + assert len(entries) == 1 + entry = entries[0] + assert entry["type"] == "command" + assert entry["name"] == "speckit.clarify" + assert entry["strategy"] == "wrap" + + def test_wrapper_uses_core_template_and_stage_gate(self): + text = (self.PRESET_DIR / "commands" / "speckit.clarify.md").read_text() + assert text.startswith("---\n") + _, frontmatter_block, body = text.split("---", 2) + frontmatter = yaml.safe_load(frontmatter_block) + assert frontmatter["strategy"] == "wrap" + assert "{CORE_TEMPLATE}" in body + assert "strategy: wrap" not in body + assert "Spec-vs-plan stage gate" in body + assert "60%" in body + assert "MUST NOT defer spec-taxonomy items to Plan" in body + + def test_catalog_lists_bundled_preset(self): + manifest = yaml.safe_load((self.PRESET_DIR / "preset.yml").read_text()) + catalog = json.loads((self.PRESET_DIR.parent / "catalog.json").read_text()) + entry = catalog["presets"]["clarify-spec-gate"] + assert entry["bundled"] is True + assert entry["version"] == manifest["preset"]["version"] + assert entry["provides"]["commands"] == 1 + assert entry["provides"]["templates"] == 0 + + def test_wrap_composes_over_core_clarify(self, project_dir): + manager = PresetManager(project_dir) + manager.install_from_directory(self.PRESET_DIR, "0.15.0") + + resolver = PresetResolver(project_dir) + layers = resolver.collect_all_layers("speckit.clarify", "command") + assert len(layers) >= 2, "expected preset wrap layer plus a core base" + assert layers[0]["strategy"] == "wrap" + assert any("clarify-spec-gate" in str(layer["path"]) for layer in layers) + assert layers[-1]["source"] == "core (bundled)" + + def test_resolved_content_embeds_core_and_gate(self, project_dir): + manager = PresetManager(project_dir) + manager.install_from_directory(self.PRESET_DIR, "0.15.0") + + resolver = PresetResolver(project_dir) + content = resolver.resolve_content("speckit.clarify", "command") + assert content is not None + assert "{CORE_TEMPLATE}" not in content + assert "## Outline" in content + assert "Functional Scope & Behavior" in content + assert "## Spec-vs-plan stage gate" in content + assert "MUST NOT defer spec-taxonomy items to Plan" in content