diff --git a/.ddev/commands/web/tag b/.ddev/commands/web/tag
index dfc8c2cb334ff66747ed9112c8e14b392b834b50..391653d0d8fe9765e91dcda20da0946168d6b1fd 100755
--- a/.ddev/commands/web/tag
+++ b/.ddev/commands/web/tag
@@ -13,8 +13,12 @@ if [ -z "$VERSION" ]; then
   exit 1
 fi
 
-# Find all `composer.json` files and change their `*` version constraints.
-find . -maxdepth 3 -type f -name composer.json -exec sed --in-place "s/\"\*\"/\"~$VERSION\"/g" {} ';'
+COMPONENTS=$(find $PWD -maxdepth 2 -type d -name 'drupal_cms_*' -or -name project_template)
+
+# Change all components' version constraints.
+for dir in $COMPONENTS; do
+  adjust-constraints $dir "~$VERSION"
+done
 
 # Generate pre-parsed versions of the recipes available to the installer,
 # and store them in a serialized file format for one-time use. This greatly
@@ -23,24 +27,13 @@ cd $DDEV_DOCROOT
 ./profiles/drupal_cms_installer/build-cache.sh
 cd -
 
-# Bump the minimum stability of the project template and ensure it correctly
-# conflicts with `drupal/drupal`.
-cd project_template
-composer config minimum-stability beta
-jq --indent 4 '.conflict."drupal/drupal" = "*"' composer.json > tag.json
-mv -f tag.json composer.json
-cd -
-
-# For visbility, output every line we're executing, as interpreted by the shell.
-set -x
-
-# Remove any untracked files.
-git clean -d --force
+# Bump the minimum stability of the project template.
+composer config minimum-stability stable --working-dir=project_template
 
 # Stage all changes, but if we're just testing this script, don't actually commit
 # or tag anything.
-git add .
-if [[ "$VERSION" == "test" ]]; then
+git add $COMPONENTS
+if [[ $2 == "test" ]]; then
   exit 0
 fi
 
@@ -49,5 +42,7 @@ git commit --message=$VERSION
 git tag $VERSION
 
 # Make another commit that puts this branch back into its pre-tagged state.
-find . -maxdepth 2 -type d -name 'drupal_cms_*' -or -name project_template -exec git checkout HEAD^1 {} ';'
+for dir in $COMPONENTS; do
+  git checkout HEAD^1 $dir
+done
 git commit --all --message="Back to dev."
diff --git a/.ddev/homeadditions/bin/adjust-constraints b/.ddev/homeadditions/bin/adjust-constraints
new file mode 100755
index 0000000000000000000000000000000000000000..a195e7e7c3fdc36fb073515a344744dfcf70e7b3
--- /dev/null
+++ b/.ddev/homeadditions/bin/adjust-constraints
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+# Alters dependency constraints for Drupal CMS components required by a
+# given `composer.json` file.
+# Usage: adjust-constraints path/to/component <constraint>
+# Example: adjust-constraints project_template "~1.2.3"
+
+DIR=$1
+
+DEPENDENCIES=$(jq -r '.require | keys | .[] | select(startswith("drupal/drupal_cms_"))' $DIR/composer.json)
+
+echo "Adjusting constraints in $DIR."
+
+for package in $DEPENDENCIES; do
+  composer require --quiet --no-update --working-dir=$DIR "$package:$2"
+done
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0f8d1418ec80d271c64e878feb55af521c99cdc7..f3de319b18cda06fef7e594161af61e2c5bed95e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -108,6 +108,10 @@ default:
 build test project:
   stage: test
   script:
+    # If this is a merge request branch, ensure that any recipe which requires the
+    # target branch (e.g., `"drupal/drupal_cms_ai": "1.x-dev"`) will know to refer
+    # to the detached HEAD we're operating from.
+    - if [ $CI_PIPELINE_SOURCE == "merge_request_event" ]; then find . -maxdepth 2 -type d -name 'drupal_cms_*' -exec composer config extra.branch-alias.dev-$CI_COMMIT_SHA $CI_MERGE_REQUEST_TARGET_BRANCH_NAME-dev --working-dir={} ';' ; fi
     - *create-project
     # Generate `composer.json` by merging our dev requirements into the project template.
     - .ddev/homeadditions/bin/generate-composer-json > $BUILD_DIR/composer.json
diff --git a/project_template/composer.json b/project_template/composer.json
index 00f60908579e08a7bca5c55b71e647a035abee29..f779170f19ef5adb2d841053860f3f8aca557db1 100644
--- a/project_template/composer.json
+++ b/project_template/composer.json
@@ -19,20 +19,24 @@
         "drupal/core-composer-scaffold": "^11.1.1",
         "drupal/core-project-message": "^11.1.1",
         "drupal/core-recommended": "^11.1.1",
-        "drupal/drupal_cms_starter": "*",
-        "drupal/drupal_cms_analytics": "*",
-        "drupal/drupal_cms_accessibility_tools": "*",
-        "drupal/drupal_cms_ai": "*",
-        "drupal/drupal_cms_blog": "*",
-        "drupal/drupal_cms_case_study": "*",
-        "drupal/drupal_cms_events": "*",
-        "drupal/drupal_cms_forms": "*",
-        "drupal/drupal_cms_news": "*",
-        "drupal/drupal_cms_person": "*",
-        "drupal/drupal_cms_project": "*",
-        "drupal/drupal_cms_seo_tools": "*",
-        "drush/drush": "^13",
-        "drupal/project_browser": "@alpha"
+        "drupal/dashboard": "@beta",
+        "drupal/drupal_cms_accessibility_tools": "1.0.x-dev",
+        "drupal/drupal_cms_ai": "1.0.x-dev",
+        "drupal/drupal_cms_analytics": "1.0.x-dev",
+        "drupal/drupal_cms_blog": "1.0.x-dev",
+        "drupal/drupal_cms_case_study": "1.0.x-dev",
+        "drupal/drupal_cms_events": "1.0.x-dev",
+        "drupal/drupal_cms_forms": "1.0.x-dev",
+        "drupal/drupal_cms_news": "1.0.x-dev",
+        "drupal/drupal_cms_page": "1.0.x-dev",
+        "drupal/drupal_cms_person": "1.0.x-dev",
+        "drupal/drupal_cms_project": "1.0.x-dev",
+        "drupal/drupal_cms_seo_tools": "1.0.x-dev",
+        "drupal/drupal_cms_starter": "1.0.x-dev",
+        "drupal/klaro": "@rc",
+        "drupal/project_browser": "@alpha",
+        "drupal/webform": "@beta",
+        "drush/drush": "^13"
     },
     "conflict": {
         "drupal/drupal": "*"
diff --git a/recipes/drupal_cms_accessibility_tools/composer.json b/recipes/drupal_cms_accessibility_tools/composer.json
index 083c7db68c7ba5d1b517c0526515efd9c8079db9..71dd6972538c4cd48e74fab29da889d16e443177 100644
--- a/recipes/drupal_cms_accessibility_tools/composer.json
+++ b/recipes/drupal_cms_accessibility_tools/composer.json
@@ -5,7 +5,7 @@
     "license": ["GPL-2.0-or-later"],
     "require": {
         "drupal/core": ">=10.3",
-        "drupal/drupal_cms_page": "*",
+        "drupal/drupal_cms_page": "1.0.x-dev",
         "drupal/editoria11y": "^2.2"
     }
 }
diff --git a/recipes/drupal_cms_ai/composer.json b/recipes/drupal_cms_ai/composer.json
index 18361abf366509a72d037e89cd6c8a5a5d166976..75d4e22b0fe1586588e111f3a1a2e7cd32b39949 100644
--- a/recipes/drupal_cms_ai/composer.json
+++ b/recipes/drupal_cms_ai/composer.json
@@ -10,7 +10,7 @@
     "drupal/ai_image_alt_text": "^1",
     "drupal/ai_provider_anthropic": "^1",
     "drupal/ai_provider_openai": "^1",
-    "drupal/drupal_cms_privacy_basic": "*",
+    "drupal/drupal_cms_privacy_basic": "1.0.x-dev",
     "league/commonmark": "^2.4"
   }
 }
diff --git a/recipes/drupal_cms_analytics/composer.json b/recipes/drupal_cms_analytics/composer.json
index c3efee2a694ab73fabf2fcb2dc5640daec63609c..b2da6f3b8c2cdb22888b6d2e17b78ca7db653e49 100644
--- a/recipes/drupal_cms_analytics/composer.json
+++ b/recipes/drupal_cms_analytics/composer.json
@@ -3,6 +3,6 @@
     "type": "metapackage",
     "description": "Adds recipes for tracking website traffic.",
     "require": {
-        "drupal/drupal_cms_google_analytics": "*"
+        "drupal/drupal_cms_google_analytics": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_authentication/composer.json b/recipes/drupal_cms_authentication/composer.json
index ef7bdc1ad626dfce0b571e29b446cfa1c131c080..61a8fcb20b204496804703460d01d46ada77857a 100644
--- a/recipes/drupal_cms_authentication/composer.json
+++ b/recipes/drupal_cms_authentication/composer.json
@@ -6,7 +6,7 @@
     "require": {
         "drupal/bpmn_io": "^2.0.3",
         "drupal/core": ">=10.3",
-        "drupal/eca": "^2.1.0",
+        "drupal/eca": "^2.1",
         "drupal/login_emailusername": "^3",
         "drupal/token": "^1"
     }
diff --git a/recipes/drupal_cms_blog/composer.json b/recipes/drupal_cms_blog/composer.json
index c1030e4036aa091a818a276ebfa01ff4f4a5823d..4d9aa147e5799cf373e76b486023334d7180745e 100644
--- a/recipes/drupal_cms_blog/composer.json
+++ b/recipes/drupal_cms_blog/composer.json
@@ -6,7 +6,7 @@
     "require": {
         "drupal/core": ">=10.4",
         "drupal/add_content_by_bundle": "^1.2.2",
-        "drupal/drupal_cms_page": "*",
+        "drupal/drupal_cms_page": "1.0.x-dev",
         "drupal/better_exposed_filters": "^7",
         "drupal/selective_better_exposed_filters": "^3"
     }
diff --git a/recipes/drupal_cms_case_study/composer.json b/recipes/drupal_cms_case_study/composer.json
index 8226f37606e6336b54bfdd728cbb1a2e01a2a464..48c7939df7748ec5df2278b1dfc3b36227ae210f 100644
--- a/recipes/drupal_cms_case_study/composer.json
+++ b/recipes/drupal_cms_case_study/composer.json
@@ -6,6 +6,6 @@
     "require": {
         "drupal/core": ">=10.4",
         "drupal/add_content_by_bundle": "^1.2.2",
-        "drupal/drupal_cms_page": "*"
+        "drupal/drupal_cms_page": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_content_type_base/composer.json b/recipes/drupal_cms_content_type_base/composer.json
index 7a35b782df836f7bab613ea64b69baa32f0a0972..5df238a1a01b0a24375ac8b5b601dbd7e8e0b690 100644
--- a/recipes/drupal_cms_content_type_base/composer.json
+++ b/recipes/drupal_cms_content_type_base/composer.json
@@ -7,8 +7,8 @@
         "drupal/autosave_form": "^1.7",
         "drupal/bpmn_io": "^2.0.3",
         "drupal/core": ">=10.4",
-        "drupal/drupal_cms_image": "*",
-        "drupal/eca": "^2.1.0",
+        "drupal/drupal_cms_image": "1.0.x-dev",
+        "drupal/eca": "^2.1",
         "drupal/linkit": "^7",
         "drupal/pathauto": "^1.13",
         "drupal/token": "^1",
diff --git a/recipes/drupal_cms_events/composer.json b/recipes/drupal_cms_events/composer.json
index 3e58bb6c566f0b3e3997a95c749342d52fc61af8..aa2198f9ac884ec01db977d68fbc83153163c3f2 100644
--- a/recipes/drupal_cms_events/composer.json
+++ b/recipes/drupal_cms_events/composer.json
@@ -7,9 +7,9 @@
     "drupal/core": ">=10.4",
     "drupal/add_content_by_bundle": "^1.2.2",
     "drupal/address": "^2",
-    "drupal/addtocal_augment": "^1.2@rc",
-    "drupal/drupal_cms_page": "*",
-    "drupal/drupal_cms_privacy_basic": "*",
+    "drupal/addtocal_augment": "^1.2.3",
+    "drupal/drupal_cms_page": "1.0.x-dev",
+    "drupal/drupal_cms_privacy_basic": "1.0.x-dev",
     "drupal/geocoder": "^4.10",
     "drupal/geofield": "^1.47",
     "drupal/leaflet": "^10.2.33",
diff --git a/recipes/drupal_cms_forms/composer.json b/recipes/drupal_cms_forms/composer.json
index de1f282884099745c963596085740c53149d100e..95c11293213fd08aade36404bbad6e7b1b5aad0a 100644
--- a/recipes/drupal_cms_forms/composer.json
+++ b/recipes/drupal_cms_forms/composer.json
@@ -4,8 +4,8 @@
     "type": "drupal-recipe",
     "license": ["GPL-2.0-or-later"],
     "require": {
-        "drupal/drupal_cms_anti_spam": "*",
-        "drupal/drupal_cms_page": "*",
+        "drupal/drupal_cms_anti_spam": "1.0.x-dev",
+        "drupal/drupal_cms_page": "1.0.x-dev",
         "drupal/core": ">=10.4",
         "drupal/webform": "^6.3-beta1"
     }
diff --git a/recipes/drupal_cms_google_analytics/composer.json b/recipes/drupal_cms_google_analytics/composer.json
index c242c2ddb35b7300046a0bf965f360c9d69ecda6..ad4de9bc807b74a0801e69487cb59595ff5b5550 100644
--- a/recipes/drupal_cms_google_analytics/composer.json
+++ b/recipes/drupal_cms_google_analytics/composer.json
@@ -5,6 +5,6 @@
     "require": {
         "drupal/core": ">=10.4",
         "drupal/google_tag": "^2.0.7",
-        "drupal/drupal_cms_privacy_basic": "*"
+        "drupal/drupal_cms_privacy_basic": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_news/composer.json b/recipes/drupal_cms_news/composer.json
index a38f4e2fe042e9bbccc062503b8ea6c8d2837ba1..486f63cd6edfc3b475ad9e1f0527ac6e95380388 100644
--- a/recipes/drupal_cms_news/composer.json
+++ b/recipes/drupal_cms_news/composer.json
@@ -6,7 +6,7 @@
     "require": {
         "drupal/core": ">=10.4",
         "drupal/add_content_by_bundle": "^1.2.2",
-        "drupal/drupal_cms_page": "*",
+        "drupal/drupal_cms_page": "1.0.x-dev",
         "drupal/better_exposed_filters": "^7",
         "drupal/selective_better_exposed_filters": "^3"
     }
diff --git a/recipes/drupal_cms_page/composer.json b/recipes/drupal_cms_page/composer.json
index 6232b87dbc0bc8b0d2a2e4ee6e3808826d1cc46e..9f4d88f2266f908de505a9037c5bf393f2c27340 100644
--- a/recipes/drupal_cms_page/composer.json
+++ b/recipes/drupal_cms_page/composer.json
@@ -5,6 +5,6 @@
     "license": ["GPL-2.0-or-later"],
     "require": {
         "drupal/core": ">=10.4",
-        "drupal/drupal_cms_content_type_base": "*"
+        "drupal/drupal_cms_content_type_base": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_person/composer.json b/recipes/drupal_cms_person/composer.json
index 1c6e1ea24b28ac0944afcb871b95a9930906101c..5ba5790a3cee9d7efa95098b396762b0386d2702 100644
--- a/recipes/drupal_cms_person/composer.json
+++ b/recipes/drupal_cms_person/composer.json
@@ -5,6 +5,6 @@
     "license": ["GPL-2.0-or-later"],
     "require": {
         "drupal/core": ">=10.4",
-        "drupal/drupal_cms_content_type_base": "*"
+        "drupal/drupal_cms_content_type_base": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_privacy_basic/composer.json b/recipes/drupal_cms_privacy_basic/composer.json
index 705fbda80793ae525f8730a240718ec1442d06ba..329283e26de54472b9cca0ca92d1d834c8f8f072 100644
--- a/recipes/drupal_cms_privacy_basic/composer.json
+++ b/recipes/drupal_cms_privacy_basic/composer.json
@@ -7,7 +7,7 @@
         "drupal/bpmn_io": "^2.0.3",
         "drupal/core": ">=10.4",
         "drupal/eca": "^2.1",
-        "drupal/drupal_cms_page": "*",
+        "drupal/drupal_cms_page": "1.0.x-dev",
         "drupal/klaro": "^3-rc16",
         "drupal/menu_link_attributes": "^1.5"
     }
diff --git a/recipes/drupal_cms_project/composer.json b/recipes/drupal_cms_project/composer.json
index 2debc56050a9a854d2f0ddb2508f3b7bb5d801a8..e2cabb54fdeb63388c8d0f4756b0c4bfd0f06dcc 100644
--- a/recipes/drupal_cms_project/composer.json
+++ b/recipes/drupal_cms_project/composer.json
@@ -6,6 +6,6 @@
     "require": {
         "drupal/core": ">=10.4",
         "drupal/add_content_by_bundle": "^1.2.2",
-        "drupal/drupal_cms_page": "*"
+        "drupal/drupal_cms_page": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_remote_video/composer.json b/recipes/drupal_cms_remote_video/composer.json
index 312e265b3b02d6a11ab0ab6978b6f394e0dc97c2..04a7e73ed2651da409a5769e0681f4fe9946e299 100644
--- a/recipes/drupal_cms_remote_video/composer.json
+++ b/recipes/drupal_cms_remote_video/composer.json
@@ -5,6 +5,6 @@
     "license": ["GPL-2.0-or-later"],
     "require": {
         "drupal/core": ">=10.4",
-        "drupal/drupal_cms_privacy_basic": "*"
+        "drupal/drupal_cms_privacy_basic": "1.0.x-dev"
     }
 }
diff --git a/recipes/drupal_cms_starter/composer.json b/recipes/drupal_cms_starter/composer.json
index 53372dd0342cb0275c05f88b2138e0d5d5038e9f..37ac0d85c70de38c118f37e4466c39863cd1f256 100644
--- a/recipes/drupal_cms_starter/composer.json
+++ b/recipes/drupal_cms_starter/composer.json
@@ -9,15 +9,15 @@
         "drupal/core": ">=10.4",
         "drupal/eca": "^2.1",
         "drupal/dashboard": "^2-beta1",
-        "drupal/drupal_cms_admin_ui": "*",
-        "drupal/drupal_cms_anti_spam": "*",
-        "drupal/drupal_cms_authentication": "*",
-        "drupal/drupal_cms_olivero": "*",
-        "drupal/drupal_cms_page": "*",
-        "drupal/drupal_cms_privacy_basic": "*",
-        "drupal/drupal_cms_remote_video": "*",
-        "drupal/drupal_cms_search": "*",
-        "drupal/drupal_cms_seo_basic": "*",
+        "drupal/drupal_cms_admin_ui": "1.0.x-dev",
+        "drupal/drupal_cms_anti_spam": "1.0.x-dev",
+        "drupal/drupal_cms_authentication": "1.0.x-dev",
+        "drupal/drupal_cms_olivero": "1.0.x-dev",
+        "drupal/drupal_cms_page": "1.0.x-dev",
+        "drupal/drupal_cms_privacy_basic": "1.0.x-dev",
+        "drupal/drupal_cms_remote_video": "1.0.x-dev",
+        "drupal/drupal_cms_search": "1.0.x-dev",
+        "drupal/drupal_cms_seo_basic": "1.0.x-dev",
         "drupal/easy_email_express": "^1.0.2",
         "drupal/project_browser": "^2-alpha7",
         "drupal/token": "^1"