From ccf8dc246a7c20ceeedbe26b39123153125e3d0a Mon Sep 17 00:00:00 2001 From: Ben Buttigieg <70525+BenBtg@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:54:29 +0100 Subject: [PATCH 1/3] fix(presets): seed constitution from preset constitution-template (#3272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The constitution is the only template materialized to a live file (.specify/memory/constitution.md) rather than resolved on demand, yet ensure_constitution_from_template hardcoded a copy from the core template and ignored PresetResolver. Combined with init seeding the constitution before preset installation, a preset's constitution-template (e.g. strategy: replace with a ratified constitution) could never go live. Changes: - ensure_constitution_from_template now resolves constitution-template through PresetResolver, so a preset/override/extension wins and core is the fallback. - init seeds the constitution after preset installation so init --preset uses the resolved stack. - install_from_directory re-seeds memory/constitution.md from the resolved preset template, guarded to only act when the memory file is missing or still contains generic placeholder tokens — authored constitutions are never overwritten. Covers preset add and install_from_zip. - Tests for preset seeding, placeholder re-seed, authored-constitution preservation, override resolution, and resolver-aware init seeding. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/commands/init.py | 24 ++++-- src/specify_cli/presets/__init__.py | 64 ++++++++++++++++ tests/test_presets.py | 111 ++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 6 deletions(-) diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index dd815b8c5d..af4ae3e845 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -33,10 +33,19 @@ def _stdin_is_interactive() -> bool: def ensure_constitution_from_template( project_path: Path, tracker: StepTracker | None = None ) -> None: - """Copy constitution template to memory if it doesn't exist.""" + """Copy the resolved constitution template to memory if it doesn't exist. + + Resolution walks the full priority stack (project overrides → installed + presets → extensions → core) via :class:`PresetResolver`, so a preset that + ships a ``constitution-template`` (e.g. ``strategy: replace`` with a ratified + constitution) seeds the memory file verbatim. When nothing overrides it, the + resolver falls through to the core template, preserving legacy behavior. + """ + from ..presets import PresetResolver + memory_constitution = project_path / ".specify" / "memory" / "constitution.md" - template_constitution = ( - project_path / ".specify" / "templates" / "constitution-template.md" + template_constitution = PresetResolver(project_path).resolve( + "constitution-template", "template" ) if memory_constitution.exists(): @@ -45,7 +54,7 @@ def ensure_constitution_from_template( tracker.skip("constitution", "existing file preserved") return - if not template_constitution.exists(): + if template_constitution is None or not template_constitution.exists(): if tracker: tracker.add("constitution", "Constitution setup") tracker.error("constitution", "template not found") @@ -447,8 +456,6 @@ def init( "shared-infra", f"scripts ({selected_script}) + templates" ) - ensure_constitution_from_template(project_path, tracker=tracker) - try: bundled_wf = _locate_bundled_workflow("speckit") if bundled_wf: @@ -576,6 +583,11 @@ def init( continuing="Continuing without the optional preset.", ) + # Seed the constitution AFTER preset installation so that a + # preset-provided constitution-template (resolved via the + # priority stack) wins over the core template. + ensure_constitution_from_template(project_path, tracker=tracker) + tracker.complete("final", "project ready") except (typer.Exit, SystemExit): raise diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index 863b6ef7dc..b6ea849052 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -34,6 +34,17 @@ from ..shared_infra import verify_archive_sha256 +# Tokens that mark an unmodified, generic constitution that has not yet been +# authored. Used to decide whether seeding/re-seeding memory/constitution.md +# from a preset-provided template is safe (i.e. won't clobber authored content). +_CONSTITUTION_PLACEHOLDER_TOKENS = ("[PROJECT_NAME]", "[PRINCIPLE_1_NAME]") + + +def _constitution_is_placeholder(content: str) -> bool: + """Return True if a constitution body is still the generic placeholder.""" + return any(token in content for token in _CONSTITUTION_PLACEHOLDER_TOKENS) + + def _substitute_core_template( body: str, cmd_name: str, @@ -1615,8 +1626,61 @@ def install_from_directory( stacklevel=2, ) + # Seed/re-seed memory/constitution.md from a preset-provided + # constitution-template. The constitution is the only template that is + # materialized to a live file rather than resolved on demand, so a + # preset that ships one (e.g. strategy: replace with a ratified + # constitution) must be propagated here. Guard against clobbering an + # already-authored constitution by only seeding when the memory file is + # missing or still contains generic placeholder tokens. + self._seed_constitution_from_preset(manifest) + return manifest + def _seed_constitution_from_preset(self, manifest: PresetManifest) -> None: + """Seed memory/constitution.md from a preset constitution-template. + + Only runs when the preset declares a ``type: template`` entry named + ``constitution-template`` and the live memory file is either missing or + still the generic placeholder. Authored constitutions are never + overwritten. + """ + provides_constitution = any( + t.get("type") == "template" and t.get("name") == "constitution-template" + for t in manifest.templates + ) + if not provides_constitution: + return + + memory_constitution = ( + self.project_root / ".specify" / "memory" / "constitution.md" + ) + if memory_constitution.exists(): + try: + existing = memory_constitution.read_text(encoding="utf-8") + except OSError: + return + if not _constitution_is_placeholder(existing): + # Legitimately authored constitution; leave it untouched. + return + + resolved = PresetResolver(self.project_root).resolve( + "constitution-template", "template" + ) + if resolved is None or not resolved.exists(): + return + + try: + memory_constitution.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(resolved, memory_constitution) + except OSError as exc: + import warnings + + warnings.warn( + f"Failed to seed constitution from preset {manifest.id}: {exc}.", + stacklevel=2, + ) + def install_from_zip( self, zip_path: Path, diff --git a/tests/test_presets.py b/tests/test_presets.py index 054018b7a0..2d4cefd59e 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -2778,6 +2778,55 @@ def test_self_test_no_commands_without_agent_dirs(self, project_dir): metadata = manager.registry.get("self-test") assert metadata["registered_commands"] == {} + def test_self_test_seeds_constitution_when_memory_absent(self, project_dir): + """Installing a preset seeds memory/constitution.md from its template.""" + manager = PresetManager(project_dir) + install_self_test_preset(manager) + + memory = project_dir / ".specify" / "memory" / "constitution.md" + assert memory.exists(), "constitution.md was not seeded from the preset" + assert "preset:self-test" in memory.read_text(), ( + "constitution.md was not seeded from the self-test preset template" + ) + + def test_self_test_reseeds_placeholder_constitution(self, project_dir): + """A placeholder memory constitution is re-seeded from the preset template.""" + memory = project_dir / ".specify" / "memory" / "constitution.md" + memory.parent.mkdir(parents=True, exist_ok=True) + memory.write_text("# [PROJECT_NAME] Constitution\n\n### [PRINCIPLE_1_NAME]\n") + + manager = PresetManager(project_dir) + install_self_test_preset(manager) + + content = memory.read_text() + assert "preset:self-test" in content, "placeholder constitution was not re-seeded" + assert "[PROJECT_NAME]" not in content + + def test_self_test_preserves_authored_constitution(self, project_dir): + """An authored (placeholder-free) constitution is never overwritten.""" + memory = project_dir / ".specify" / "memory" / "constitution.md" + memory.parent.mkdir(parents=True, exist_ok=True) + authored = "# Acme Constitution\n\n### I. Ship It\nAuthored by a human.\n" + memory.write_text(authored) + + manager = PresetManager(project_dir) + install_self_test_preset(manager) + + assert memory.read_text() == authored, "authored constitution was overwritten" + + def test_self_test_override_resolves_constitution_template(self, project_dir): + """The preset override of constitution-template resolves to the preset file.""" + templates_dir = project_dir / ".specify" / "templates" + (templates_dir / "constitution-template.md").write_text("# Core constitution\n") + + manager = PresetManager(project_dir) + install_self_test_preset(manager) + + resolver = PresetResolver(project_dir) + result = resolver.resolve("constitution-template", "template") + assert result is not None + assert "preset:self-test" in result.read_text() + def test_extension_command_skipped_when_extension_missing(self, project_dir, temp_dir): """Test that extension command overrides are skipped if the extension isn't installed.""" claude_dir = project_dir / ".claude" / "skills" @@ -6149,3 +6198,65 @@ def fake_open(url, timeout=None, extra_headers=None): ) assert resolved == "https://ghes.example/api/v3/repos/o/r/releases/assets/9" assert captured == ["https://ghes.example/api/v3/repos/o/r/releases/tags/v2"] + + +# ===== ensure_constitution_from_template resolver-awareness ===== + + +class TestEnsureConstitutionResolverAware: + """`ensure_constitution_from_template` must resolve through PresetResolver. + + The constitution is the only template materialized to a live file rather + than resolved on demand. These tests pin the regression from issue #3272: + a preset-provided ``constitution-template`` must seed memory, while the + core template is used when no preset overrides it. + """ + + def _core_constitution(self, project_dir): + templates_dir = project_dir / ".specify" / "templates" + templates_dir.mkdir(parents=True, exist_ok=True) + (templates_dir / "constitution-template.md").write_text( + "# [PROJECT_NAME] Constitution\n\n### [PRINCIPLE_1_NAME]\n" + ) + + def test_seeds_from_core_when_no_preset(self, project_dir): + from specify_cli.commands.init import ensure_constitution_from_template + + self._core_constitution(project_dir) + ensure_constitution_from_template(project_dir) + + memory = project_dir / ".specify" / "memory" / "constitution.md" + assert memory.exists() + assert "[PROJECT_NAME]" in memory.read_text() + + def test_seeds_from_preset_when_installed(self, project_dir): + from specify_cli.commands.init import ensure_constitution_from_template + + self._core_constitution(project_dir) + manager = PresetManager(project_dir) + install_self_test_preset(manager) + + # Remove the memory file seeded during install to test ensure() in + # isolation; it must re-seed from the preset, not the core template. + memory = project_dir / ".specify" / "memory" / "constitution.md" + memory.unlink() + + ensure_constitution_from_template(project_dir) + + assert memory.exists() + content = memory.read_text() + assert "preset:self-test" in content + assert "[PROJECT_NAME]" not in content + + def test_preserves_existing_memory(self, project_dir): + from specify_cli.commands.init import ensure_constitution_from_template + + self._core_constitution(project_dir) + memory = project_dir / ".specify" / "memory" / "constitution.md" + memory.parent.mkdir(parents=True, exist_ok=True) + authored = "# Acme Constitution\nAuthored.\n" + memory.write_text(authored) + + ensure_constitution_from_template(project_dir) + + assert memory.read_text() == authored From f954e30a66725035ee4bd8fca0f5ac463cedcae1 Mon Sep 17 00:00:00 2001 From: Ben Buttigieg <70525+BenBtg@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:30:34 +0100 Subject: [PATCH 2/3] fix(presets): compose constitution-template when seeding memory Take on review feedback from Copilot and gglachant: - constitution seeding previously copied the top layer file path verbatim even when the winning layer used a composing strategy (prepend/append/wrap), which could leave {CORE_TEMPLATE} unresolved. - both seeding paths now inspect resolver layers and only copy verbatim for replace; non-replace strategies materialize composed content via PresetResolver.resolve_content(). - add regression tests for wrap strategy composition in both PresetManager seeding and ensure_constitution_from_template. - add a drift-guard test pinning _CONSTITUTION_PLACEHOLDER_TOKENS to the placeholders in templates/constitution-template.md. Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/commands/init.py | 24 ++++-- src/specify_cli/presets/__init__.py | 18 +++-- tests/test_presets.py | 110 ++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 13 deletions(-) diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index af4ae3e845..00ea68806b 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -33,20 +33,19 @@ def _stdin_is_interactive() -> bool: def ensure_constitution_from_template( project_path: Path, tracker: StepTracker | None = None ) -> None: - """Copy the resolved constitution template to memory if it doesn't exist. + """Materialize the resolved constitution template to memory if missing. Resolution walks the full priority stack (project overrides → installed presets → extensions → core) via :class:`PresetResolver`, so a preset that ships a ``constitution-template`` (e.g. ``strategy: replace`` with a ratified - constitution) seeds the memory file verbatim. When nothing overrides it, the - resolver falls through to the core template, preserving legacy behavior. + constitution) can seed the memory file. When nothing overrides it, the + resolver falls through to the core template. """ from ..presets import PresetResolver memory_constitution = project_path / ".specify" / "memory" / "constitution.md" - template_constitution = PresetResolver(project_path).resolve( - "constitution-template", "template" - ) + resolver = PresetResolver(project_path) + layers = resolver.collect_all_layers("constitution-template", "template") if memory_constitution.exists(): if tracker: @@ -54,7 +53,7 @@ def ensure_constitution_from_template( tracker.skip("constitution", "existing file preserved") return - if template_constitution is None or not template_constitution.exists(): + if not layers: if tracker: tracker.add("constitution", "Constitution setup") tracker.error("constitution", "template not found") @@ -62,7 +61,16 @@ def ensure_constitution_from_template( try: memory_constitution.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(template_constitution, memory_constitution) + top_layer = layers[0] + if top_layer["strategy"] == "replace": + shutil.copy2(top_layer["path"], memory_constitution) + else: + composed_content = resolver.resolve_content( + "constitution-template", "template" + ) + if composed_content is None: + raise FileNotFoundError("constitution template not found") + memory_constitution.write_text(composed_content, encoding="utf-8") if tracker: tracker.add("constitution", "Constitution setup") tracker.complete("constitution", "copied from template") diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index b6ea849052..184f3d6b8c 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -1664,15 +1664,23 @@ def _seed_constitution_from_preset(self, manifest: PresetManifest) -> None: # Legitimately authored constitution; leave it untouched. return - resolved = PresetResolver(self.project_root).resolve( - "constitution-template", "template" - ) - if resolved is None or not resolved.exists(): + resolver = PresetResolver(self.project_root) + layers = resolver.collect_all_layers("constitution-template", "template") + if not layers: return try: memory_constitution.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(resolved, memory_constitution) + top_layer = layers[0] + if top_layer["strategy"] == "replace": + shutil.copy2(top_layer["path"], memory_constitution) + else: + composed_content = resolver.resolve_content( + "constitution-template", "template" + ) + if composed_content is None: + return + memory_constitution.write_text(composed_content, encoding="utf-8") except OSError as exc: import warnings diff --git a/tests/test_presets.py b/tests/test_presets.py index 2d4cefd59e..3c18af8800 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -2827,6 +2827,66 @@ def test_self_test_override_resolves_constitution_template(self, project_dir): assert result is not None assert "preset:self-test" in result.read_text() + def test_constitution_seed_composes_wrap_strategy(self, project_dir, temp_dir): + """Seeding memory composes wrap constitution-template layers.""" + templates_dir = project_dir / ".specify" / "templates" + templates_dir.mkdir(parents=True, exist_ok=True) + (templates_dir / "constitution-template.md").write_text( + "# Core Constitution\n\n## Core Principle\n" + ) + + preset_dir = temp_dir / "constitution-wrap" + (preset_dir / "templates").mkdir(parents=True) + (preset_dir / "templates" / "constitution-template.md").write_text( + "# Wrapper Constitution\n\n{CORE_TEMPLATE}\n\n## Wrapper Footer\n" + ) + (preset_dir / "preset.yml").write_text( + yaml.dump( + { + "schema_version": "1.0", + "preset": { + "id": "constitution-wrap", + "name": "Constitution Wrap", + "version": "1.0.0", + "description": "Wrap constitution template for testing", + }, + "requires": {"speckit_version": ">=0.1.0"}, + "provides": { + "templates": [ + { + "type": "template", + "name": "constitution-template", + "file": "templates/constitution-template.md", + "strategy": "wrap", + "description": "Wrapped constitution template", + } + ] + }, + } + ) + ) + + manager = PresetManager(project_dir) + manager.install_from_directory(preset_dir, "0.1.5") + + memory = project_dir / ".specify" / "memory" / "constitution.md" + content = memory.read_text() + assert "{CORE_TEMPLATE}" not in content + assert "# Wrapper Constitution" in content + assert "## Core Principle" in content + + def test_constitution_placeholder_tokens_are_pinned_to_core_template(self): + """Guard placeholder token drift between code and core template.""" + from specify_cli.presets import _CONSTITUTION_PLACEHOLDER_TOKENS + + expected_tokens = {"[PROJECT_NAME]", "[PRINCIPLE_1_NAME]"} + assert set(_CONSTITUTION_PLACEHOLDER_TOKENS) == expected_tokens + + core_template = Path(__file__).parent.parent / "templates" / "constitution-template.md" + content = core_template.read_text(encoding="utf-8") + for token in expected_tokens: + assert token in content + def test_extension_command_skipped_when_extension_missing(self, project_dir, temp_dir): """Test that extension command overrides are skipped if the extension isn't installed.""" claude_dir = project_dir / ".claude" / "skills" @@ -6219,6 +6279,39 @@ def _core_constitution(self, project_dir): "# [PROJECT_NAME] Constitution\n\n### [PRINCIPLE_1_NAME]\n" ) + def _wrap_constitution_preset(self, temp_dir): + preset_dir = temp_dir / "ensure-wrap-preset" + (preset_dir / "templates").mkdir(parents=True) + (preset_dir / "templates" / "constitution-template.md").write_text( + "# Ensure Wrapper\n\n{CORE_TEMPLATE}\n\n## Tail\n" + ) + (preset_dir / "preset.yml").write_text( + yaml.dump( + { + "schema_version": "1.0", + "preset": { + "id": "ensure-wrap", + "name": "Ensure Wrap", + "version": "1.0.0", + "description": "Wrap strategy for ensure() coverage", + }, + "requires": {"speckit_version": ">=0.1.0"}, + "provides": { + "templates": [ + { + "type": "template", + "name": "constitution-template", + "file": "templates/constitution-template.md", + "strategy": "wrap", + "description": "Wrapped constitution", + } + ] + }, + } + ) + ) + return preset_dir + def test_seeds_from_core_when_no_preset(self, project_dir): from specify_cli.commands.init import ensure_constitution_from_template @@ -6260,3 +6353,20 @@ def test_preserves_existing_memory(self, project_dir): ensure_constitution_from_template(project_dir) assert memory.read_text() == authored + + def test_composes_wrap_strategy_when_ensuring(self, project_dir, temp_dir): + from specify_cli.commands.init import ensure_constitution_from_template + + self._core_constitution(project_dir) + manager = PresetManager(project_dir) + manager.install_from_directory(self._wrap_constitution_preset(temp_dir), "0.1.5") + + # Ensure we validate ensure() behavior directly. + memory = project_dir / ".specify" / "memory" / "constitution.md" + memory.unlink() + ensure_constitution_from_template(project_dir) + + content = memory.read_text() + assert "{CORE_TEMPLATE}" not in content + assert "# Ensure Wrapper" in content + assert "[PROJECT_NAME]" in content From aa0927287314a8eb45c3f48584db73c409ea0eea Mon Sep 17 00:00:00 2001 From: Ben Buttigieg <70525+BenBtg@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:53:21 +0100 Subject: [PATCH 3/3] refactor(presets): unify constitution template materialization Address latest Copilot feedback on the constitution seeding path: - moved resolver/layer I/O behind the existing-memory fast path in init - corrected tracker output for composed materialization - deduplicated materialization logic shared by init and preset install seeding into presets._materialize_constitution_template() Behavior is unchanged for replace strategies (copy verbatim) and remains composed for prepend/append/wrap via resolve_content(). Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/commands/init.py | 35 ++++++++------------ src/specify_cli/presets/__init__.py | 50 ++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index 00ea68806b..8c39014ceb 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -3,7 +3,6 @@ from __future__ import annotations import os -import shutil import sys from pathlib import Path from typing import Any @@ -41,11 +40,9 @@ def ensure_constitution_from_template( constitution) can seed the memory file. When nothing overrides it, the resolver falls through to the core template. """ - from ..presets import PresetResolver + from ..presets import _materialize_constitution_template memory_constitution = project_path / ".specify" / "memory" / "constitution.md" - resolver = PresetResolver(project_path) - layers = resolver.collect_all_layers("constitution-template", "template") if memory_constitution.exists(): if tracker: @@ -53,27 +50,21 @@ def ensure_constitution_from_template( tracker.skip("constitution", "existing file preserved") return - if not layers: - if tracker: - tracker.add("constitution", "Constitution setup") - tracker.error("constitution", "template not found") - return - try: - memory_constitution.parent.mkdir(parents=True, exist_ok=True) - top_layer = layers[0] - if top_layer["strategy"] == "replace": - shutil.copy2(top_layer["path"], memory_constitution) - else: - composed_content = resolver.resolve_content( - "constitution-template", "template" - ) - if composed_content is None: - raise FileNotFoundError("constitution template not found") - memory_constitution.write_text(composed_content, encoding="utf-8") + materialization = _materialize_constitution_template( + project_path, memory_constitution + ) + if materialization is None: + if tracker: + tracker.add("constitution", "Constitution setup") + tracker.error("constitution", "template not found") + return if tracker: tracker.add("constitution", "Constitution setup") - tracker.complete("constitution", "copied from template") + if materialization == "copied": + tracker.complete("constitution", "copied from template") + else: + tracker.complete("constitution", "composed from template") else: console.print("[cyan]Initialized constitution from template[/cyan]") except Exception as e: diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index 184f3d6b8c..fb596f9424 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -45,6 +45,35 @@ def _constitution_is_placeholder(content: str) -> bool: return any(token in content for token in _CONSTITUTION_PLACEHOLDER_TOKENS) +def _materialize_constitution_template( + project_root: Path, + memory_constitution: Path, +) -> str | None: + """Materialize constitution-template content into memory/constitution.md. + + Returns: + "copied" when the winning layer is ``replace`` and the source file is + copied verbatim; "composed" when a composing strategy is materialized + via ``resolve_content``; ``None`` when no constitution template resolves. + """ + resolver = PresetResolver(project_root) + layers = resolver.collect_all_layers("constitution-template", "template") + if not layers: + return None + + memory_constitution.parent.mkdir(parents=True, exist_ok=True) + top_layer = layers[0] + if top_layer["strategy"] == "replace": + shutil.copy2(top_layer["path"], memory_constitution) + return "copied" + + composed_content = resolver.resolve_content("constitution-template", "template") + if composed_content is None: + return None + memory_constitution.write_text(composed_content, encoding="utf-8") + return "composed" + + def _substitute_core_template( body: str, cmd_name: str, @@ -1664,23 +1693,12 @@ def _seed_constitution_from_preset(self, manifest: PresetManifest) -> None: # Legitimately authored constitution; leave it untouched. return - resolver = PresetResolver(self.project_root) - layers = resolver.collect_all_layers("constitution-template", "template") - if not layers: - return - try: - memory_constitution.parent.mkdir(parents=True, exist_ok=True) - top_layer = layers[0] - if top_layer["strategy"] == "replace": - shutil.copy2(top_layer["path"], memory_constitution) - else: - composed_content = resolver.resolve_content( - "constitution-template", "template" - ) - if composed_content is None: - return - memory_constitution.write_text(composed_content, encoding="utf-8") + result = _materialize_constitution_template( + self.project_root, memory_constitution + ) + if result is None: + return except OSError as exc: import warnings