diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 497d2e5..5c33c26 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -10,11 +10,15 @@ on:  # yamllint disable-line rule:truthy
 jobs:
   build:
     uses: metaborg/actions/.github/workflows/gradle-build-matrix.yaml@main
+    with:
+      gradle-command: |
+        gradle build
+# Publish snapshots
   publish-snapshot:
     uses: metaborg/actions/.github/workflows/gradle-publish.yaml@main
     with:
       gradle-command: |
-        gradle :publish -Pgitonium.isSnapshot=true
+        gradle publish -Pgitonium.isSnapshot=true
       gradle-version-command: |
         gradle -q :convention-plugin:printVersion -Pgitonium.isSnapshot=true
     if: "github.event_name == 'push' && github.ref == 'refs/heads/main'"
@@ -22,11 +26,12 @@ jobs:
     secrets:
       METABORG_ARTIFACTS_USERNAME: ${{ secrets.METABORG_ARTIFACTS_USERNAME }}
       METABORG_ARTIFACTS_PASSWORD: ${{ secrets.METABORG_ARTIFACTS_PASSWORD }}
+# Publish releases
   publish-release:
     uses: metaborg/actions/.github/workflows/gradle-publish.yaml@main
     with:
       gradle-command: |
-        gradle :publish
+        gradle publish
       gradle-version-command: |
         gradle -q :convention-plugin:printVersion
     if: "github.event_name == 'push' && startsWith(github.ref, 'refs/tags/release-')"
@@ -34,4 +39,3 @@ jobs:
     secrets:
       METABORG_ARTIFACTS_USERNAME: ${{ secrets.METABORG_ARTIFACTS_USERNAME }}
       METABORG_ARTIFACTS_PASSWORD: ${{ secrets.METABORG_ARTIFACTS_PASSWORD }}
-
diff --git a/.gitignore b/.gitignore
index 569ad27..c3b0106 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,5 @@ local.properties
 .cache
 .DS_Store
 *.lock
+jte-classes/
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..7a152d8
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,133 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6ee1fa3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,55 @@
+# Metaborg Gradle
+
+## How to Contribute
+Thank you for wanting to contribute to this project! :tada::+1:
+
+> **Note**:
+> We may not deal with your issue or pull request in a timely manner, or at all.
+> We also reserve the right to change your contribution in any way we deem fit
+> for this project, or even outright reject it.
+
+#### **You have a question?**
+Search the [Discussions][1] and [Stackoverflow][3] to see whether your question
+has already been answered, or ask your question there.
+Please do **not** make an issue on the Github repository.
+
+
+#### **You found a bug**
+Search the [Issues][2] to ensure the bug has not been reported before.
+
+If the bug is new, open a new issue with a _clear title and description_.
+Please indicate:
+- what you did,
+- what you expected to happen, and
+- what actually happened.
+
+Try to include as much relevant information as you have.
+For example, a code sample or executable test case are very helpful.
+
+
+#### **You wrote a patch with a cosmetic change**
+Please do not submit pull requests for cosmetic changes,
+such as whitespace and formatting changes.
+We will reject them.
+
+
+#### **You wrote a patch with a bug fix**
+Thank you! Please open a GitHub pull request with the patch.
+
+
+#### **You wrote a patch that adds a new feature or changes an existing one**
+Please open an issue _first_, so we can discuss the change.
+
+
+#### **You want to contribute to the documentation or test suite**
+Thank you! Please open a GitHub pull request with the patch.
+
+---
+
+Thanks! :heart: :heart: :heart:
+
+[Programming Languages Group](https://pl.ewi.tudelft.nl/), [Delft University of Technology](https://www.tudelft.nl/)
+
+[1]: https://github.com/metaborg/metaborg-gradle/discussions
+[2]: https://github.com/metaborg/metaborg-gradle/issues
+[3]: https://stackoverflow.com/
diff --git a/README.md b/README.md
index 6970097..2e43fae 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Metaborg Gradle Plugins
+# Metaborg Gradle
 [![Build][github-badge:build]][github:build]
 [![License][license-badge]][license]
 [![GitHub Release][github-badge:release]][github:release]
@@ -6,39 +6,21 @@
 
 The Metaborg Gradle convention and development plugins, and the Metaborg dependency management and Gradle platform.
 
-[![Documentation][documentation-button]][documentation]
-
-| Gradle Plugin                           | Latest Release                                                                     | Latest Snapshot                                                                      |
-|-----------------------------------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
-| `org.metaborg.convention.settings`      | [![Release][mvn-rel-badge:convention.settings]][mvn:convention.settings]           | [![Snapshot][mvn-snap-badge:convention.settings]][mvn:convention.settings]           |
-| `org.metaborg.convention.java`          | [![Release][mvn-rel-badge:convention.java]][mvn:convention.java]                   | [![Snapshot][mvn-snap-badge:convention.java]][mvn:convention.java]                   |
-| `org.metaborg.convention.maven-publish` | [![Release][mvn-rel-badge:convention.maven-publish]][mvn:convention.maven-publish] | [![Snapshot][mvn-snap-badge:convention.maven-publish]][mvn:convention.maven-publish] |
-| `org.metaborg.convention.root-project`  | [![Release][mvn-rel-badge:convention.root-project]][mvn:convention.root-project]   | [![Snapshot][mvn-snap-badge:convention.root-project]][mvn:convention.root-project]   |
-
-| Artifact                         | Latest Release                                                       | Latest Snapshot                                                        |
-|----------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|
-| `org.metaborg:catalog`           | [![Release][mvn-rel-badge:catalog]][mvn:catalog]                     | [![Snapshot][mvn-snap-badge:catalog]][mvn:catalog]                     |
-| `org.metaborg:platform`          | [![Release][mvn-rel-badge:platform]][mvn:platform]                   | [![Snapshot][mvn-snap-badge:platform]][mvn:platform]                   |
-| `org.metaborg:platform-latest`   | [![Release][mvn-rel-badge:platform-latest]][mvn:platform-latest]     | [![Snapshot][mvn-snap-badge:platform-latest]][mvn:platform-latest]     |
-| `org.metaborg:platform-snapshot` | [![Release][mvn-rel-badge:platform-snapshot]][mvn:platform-snapshot] | [![Snapshot][mvn-snap-badge:platform-snapshot]][mvn:platform-snapshot] |
- 
-
-## Gradle Convention
-The `org.metaborg.convention` plugins applies any conventional configuration to Metaborg build and projects. It has the following plugins:
-
-- `org.metaborg.convention.settings`: Configures a build (in `settings.gradle.kts`) by applying a version catalog and the Develocity plugin.
-- `org.metaborg.convention.java`: Configures a project as a Java project (library or application).
-- `org.metaborg.convention.maven-publish`: Configures the Maven publications for a project.
-- `org.metaborg.convention.root-project`: Configures the root project of a Gradle multi-project build.
-
 
-## Gradle Dependency Management
-The `org.metaborg:catalog` artifact provides recommended versions for dependencies, and should be used in projects that are part of Spoofax.
+[![Documentation][documentation-button]][documentation]
 
-The `org.metaborg:platform` artifact enforces particular versions for Spoofax dependencies, and should be used by consumers of Spoofax libraries.
 
-For special use cases, the `org.metaborg:platform-latest` and `org.metaborg:platform-snapshot` artifacts provide any latest releases and snapshots of Spoofax dependencies, respectively. These may not have been tested together. Therefore, it is recommended to use a particular release of `org.metaborg:platform` in production instead.
+| Artifact | Latest Release | Latest Snapshot |
+|----------|----------------|-----------------|
+| `org.metaborg:catalog` | [![Release][mvn-rel-badge:org.metaborg:catalog]][mvn:org.metaborg:catalog] | [![Snapshot][mvn-snap-badge:org.metaborg:catalog]][mvn:org.metaborg:catalog] |
+| `org.metaborg:platform` | [![Release][mvn-rel-badge:org.metaborg:platform]][mvn:org.metaborg:platform] | [![Snapshot][mvn-snap-badge:org.metaborg:platform]][mvn:org.metaborg:platform] |
 
+| Gradle Plugin | Latest Release | Latest Snapshot |
+|---------------|----------------|-----------------|
+| `org.metaborg.convention.settings` | [![Release][mvn-rel-badge:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin]][mvn:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin] | [![Snapshot][mvn-snap-badge:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin]][mvn:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin] |
+| `org.metaborg.convention.java` | [![Release][mvn-rel-badge:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin]][mvn:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin] | [![Snapshot][mvn-snap-badge:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin]][mvn:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin] |
+| `org.metaborg.convention.maven-publish` | [![Release][mvn-rel-badge:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin]][mvn:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin] | [![Snapshot][mvn-snap-badge:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin]][mvn:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin] |
+| `org.metaborg.convention.root-project` | [![Release][mvn-rel-badge:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin]][mvn:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin] | [![Snapshot][mvn-snap-badge:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin]][mvn:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin] |
 
 ## License
 Copyright 2024 [Programming Languages Group](https://pl.ewi.tudelft.nl/), [Delft University of Technology](https://www.tudelft.nl/)
@@ -47,41 +29,31 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
 
 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an **"as is" basis, without warranties or conditions of any kind**, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 
-
-
 [github-badge:build]: https://img.shields.io/github/actions/workflow/status/metaborg/metaborg-gradle/build.yaml
 [github:build]: https://github.com/metaborg/metaborg-gradle/actions
 [license-badge]: https://img.shields.io/github/license/metaborg/metaborg-gradle
-[license]: https://github.com/metaborg/metaborg-gradle/blob/main/LICENSE
+[license]: https://github.com/metaborg/metaborg-gradle/blob/main/LICENSE.md
 [github-badge:release]: https://img.shields.io/github/v/release/metaborg/metaborg-gradle?display_name=release
 [github:release]: https://github.com/metaborg/metaborg-gradle/releases
 [documentation-badge]: https://img.shields.io/badge/docs-latest-brightgreen
 [documentation]: https://spoofax.dev/metaborg-gradle/
 [documentation-button]: https://img.shields.io/badge/Documentation-blue?style=for-the-badge&logo=googledocs&logoColor=white
 
-[mvn:convention.settings]:                  https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.settings~org.metaborg.convention.settings.gradle.plugin~~~
-[mvn:convention.java]:                      https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.java~org.metaborg.convention.java.gradle.plugin~~~
-[mvn:convention.maven-publish]:             https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.maven-publish~org.metaborg.convention.maven-publish.gradle.plugin~~~
-[mvn:convention.root-project]:              https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.root-project~org.metaborg.convention.root-project.gradle.plugin~~~
-[mvn:catalog]:                              https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg~catalog~~~
-[mvn:platform]:                             https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg~platform~~~
-[mvn:platform-latest]:                      https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg~platform-latest~~~
-[mvn:platform-snapshot]:                    https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg~platform-snapshot~~~
-
-[mvn-rel-badge:convention.settings]:        https://img.shields.io/nexus/r/org.metaborg.convention.settings/org.metaborg.convention.settings.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:convention.java]:            https://img.shields.io/nexus/r/org.metaborg.convention.java/org.metaborg.convention.java.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:convention.maven-publish]:   https://img.shields.io/nexus/r/org.metaborg.convention.maven-publish/org.metaborg.convention.maven-publish.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:convention.root-project]:    https://img.shields.io/nexus/r/org.metaborg.convention.root-project/org.metaborg.convention.root-project.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:catalog]:                    https://img.shields.io/nexus/r/org.metaborg/catalog?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:platform]:                   https://img.shields.io/nexus/r/org.metaborg/platform?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:platform-latest]:            https://img.shields.io/nexus/r/org.metaborg/platform-latest?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-rel-badge:platform-snapshot]:          https://img.shields.io/nexus/r/org.metaborg/platform-snapshot?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-
-[mvn-snap-badge:convention.settings]:       https://img.shields.io/nexus/s/org.metaborg.convention.settings/org.metaborg.convention.settings.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:convention.java]:           https://img.shields.io/nexus/s/org.metaborg.convention.java/org.metaborg.convention.java.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:convention.maven-publish]:  https://img.shields.io/nexus/s/org.metaborg.convention.maven-publish/org.metaborg.convention.maven-publish.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:convention.root-project]:   https://img.shields.io/nexus/s/org.metaborg.convention.root-project/org.metaborg.convention.root-project.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:catalog]:                   https://img.shields.io/nexus/s/org.metaborg/catalog?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:platform]:                  https://img.shields.io/nexus/s/org.metaborg/platform?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:platform-latest]:           https://img.shields.io/nexus/s/org.metaborg/platform-latest?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
-[mvn-snap-badge:platform-snapshot]:         https://img.shields.io/nexus/s/org.metaborg/platform-snapshot?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn:org.metaborg:catalog]: https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg~catalog~~~
+[mvn-rel-badge:org.metaborg:catalog]: https://img.shields.io/nexus/r/org.metaborg/catalog?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:org.metaborg:catalog]: https://img.shields.io/nexus/s/org.metaborg/catalog?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn:org.metaborg:platform]: https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg~platform~~~
+[mvn-rel-badge:org.metaborg:platform]: https://img.shields.io/nexus/r/org.metaborg/platform?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:org.metaborg:platform]: https://img.shields.io/nexus/s/org.metaborg/platform?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin]: https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.settings~org.metaborg.convention.settings.gradle.plugin~~~
+[mvn-rel-badge:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin]: https://img.shields.io/nexus/r/org.metaborg.convention.settings/org.metaborg.convention.settings.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:org.metaborg.convention.settings:org.metaborg.convention.settings.gradle.plugin]: https://img.shields.io/nexus/s/org.metaborg.convention.settings/org.metaborg.convention.settings.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin]: https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.java~org.metaborg.convention.java.gradle.plugin~~~
+[mvn-rel-badge:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin]: https://img.shields.io/nexus/r/org.metaborg.convention.java/org.metaborg.convention.java.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:org.metaborg.convention.java:org.metaborg.convention.java.gradle.plugin]: https://img.shields.io/nexus/s/org.metaborg.convention.java/org.metaborg.convention.java.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin]: https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.maven-publish~org.metaborg.convention.maven-publish.gradle.plugin~~~
+[mvn-rel-badge:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin]: https://img.shields.io/nexus/r/org.metaborg.convention.maven-publish/org.metaborg.convention.maven-publish.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:org.metaborg.convention.maven-publish:org.metaborg.convention.maven-publish.gradle.plugin]: https://img.shields.io/nexus/s/org.metaborg.convention.maven-publish/org.metaborg.convention.maven-publish.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin]: https://artifacts.metaborg.org/#nexus-search;gav~org.metaborg.convention.root-project~org.metaborg.convention.root-project.gradle.plugin~~~
+[mvn-rel-badge:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin]: https://img.shields.io/nexus/r/org.metaborg.convention.root-project/org.metaborg.convention.root-project.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:org.metaborg.convention.root-project:org.metaborg.convention.root-project.gradle.plugin]: https://img.shields.io/nexus/s/org.metaborg.convention.root-project/org.metaborg.convention.root-project.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
diff --git a/depman/gradle/libs.versions.toml b/depman/gradle/libs.versions.toml
index bf3290f..e5c1865 100644
--- a/depman/gradle/libs.versions.toml
+++ b/depman/gradle/libs.versions.toml
@@ -102,6 +102,7 @@ junit4-benchmarks               = "0.7.2"                   # https://labs.carro
 
 [plugins]
 kotlin-jvm                                  = { id = "org.jetbrains.kotlin.jvm",                                        version.ref = "kotlin" }
+kotlin-serialization                        = { id = "org.jetbrains.kotlin.plugin.serialization",                       version.ref = "kotlin" }
 gitonium                                    = { id = "org.metaborg.gitonium",                                           version.ref = "gitonium" }
 coronium-bundle                             = { id = "org.metaborg.coronium.bundle",                                    version.ref = "coronium" }
 foojay-resolver-convention                  = { id = "org.gradle.toolchains.foojay-resolver-convention",                version.ref = "foojay" }
diff --git a/repo.yaml b/repo.yaml
new file mode 100644
index 0000000..9b41cf5
--- /dev/null
+++ b/repo.yaml
@@ -0,0 +1,39 @@
+---
+repoOwner: "metaborg"
+repoName: "metaborg-gradle"
+mainBranch: "main"
+
+title: "Metaborg Gradle"
+description: |
+  The Metaborg Gradle convention and development plugins, and the Metaborg dependency management and Gradle platform.
+documentationLink: https://spoofax.dev/metaborg-gradle/
+inceptionYear: "2024"
+
+libraries:
+  - group: "org.metaborg"
+    name: "catalog"
+    description: "Version catalog"
+  - group: "org.metaborg"
+    name: "platform"
+    description: "Spoofax platform"
+
+plugins:
+  - id: "org.metaborg.convention.settings"
+    description: "Settings convention plugin"
+  - id: "org.metaborg.convention.java"
+    description: "Java convention plugin"
+  - id: "org.metaborg.convention.maven-publish"
+    description: "Maven publish convention plugin"
+  - id: "org.metaborg.convention.root-project"
+    description: "Root project convention plugin"
+
+developers:
+  - id: "Virtlink"
+    name: "Daniel A. A. Pelsmaeker"
+    email: "developer@pelsmaeker.net"
+
+files:
+  githubWorkflows:
+    publishRelease: true
+    publishSnapshot: true
+    printVersionTask: ":convention-plugin:printVersion"
\ No newline at end of file
diff --git a/repoman/build.gradle.kts b/repoman/build.gradle.kts
new file mode 100644
index 0000000..f7083f9
--- /dev/null
+++ b/repoman/build.gradle.kts
@@ -0,0 +1,57 @@
+import org.metaborg.convention.Developer
+
+// Workaround for issue: https://youtrack.jetbrains.com/issue/KTIJ-19369
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+    application
+    id("org.metaborg.convention.java")
+    id("org.metaborg.convention.maven-publish")
+    id("org.metaborg.convention.junit")
+    alias(libs.plugins.kotlin.jvm)
+    alias(libs.plugins.kotlin.serialization)
+    alias(libs.plugins.gitonium)
+}
+
+version = gitonium.version
+group = "org.metaborg"
+description = "Repository manager for Metaborg/Spoofax projects."
+
+dependencies {
+    implementation("com.github.ajalt.clikt:clikt:4.4.0")            // CLI interface
+    implementation("gg.jte:jte:3.1.12")                             // Templating engine
+    implementation("gg.jte:jte-kotlin:3.1.12")                      // Templating engine (Kotlin support)
+    implementation("com.charleskorn.kaml:kaml:0.59.0")              // Deserialize YAML files
+
+    testImplementation  (libs.kotest)
+    testImplementation  (libs.kotest.assertions)
+    testImplementation  (libs.kotest.datatest)
+    testImplementation  (libs.kotest.property)
+}
+
+application {
+    mainClass.set("org.metaborg.repoman.Program")
+}
+
+javaConvention {
+    javaVersion.set(JavaLanguageVersion.of(17))
+}
+
+publishing {
+    publications {
+        create<MavenPublication>("mavenJava") {
+            from(components["java"])
+        }
+    }
+}
+
+mavenPublishConvention {
+    repoOwner.set("metaborg")
+    repoName.set("metaborg-git")
+
+    metadata {
+        inceptionYear.set("2024")
+        developers.set(listOf(
+            Developer("Virtlink", "Daniel A. A. Pelsmaeker", "developer@pelsmaeker.net"),
+        ))
+    }
+}
diff --git a/repoman/examplemeta.yaml b/repoman/examplemeta.yaml
new file mode 100644
index 0000000..ef7363d
--- /dev/null
+++ b/repoman/examplemeta.yaml
@@ -0,0 +1,2 @@
+---
+name: "My name"
\ No newline at end of file
diff --git a/repoman/settings.gradle.kts b/repoman/settings.gradle.kts
new file mode 100644
index 0000000..e6efa3b
--- /dev/null
+++ b/repoman/settings.gradle.kts
@@ -0,0 +1,19 @@
+dependencyResolutionManagement {
+    repositories {
+        maven("https://artifacts.metaborg.org/content/groups/public/")
+        mavenCentral()
+    }
+}
+
+pluginManagement {
+    repositories {
+        maven("https://artifacts.metaborg.org/content/groups/public/")
+        gradlePluginPortal()
+    }
+}
+
+plugins {
+    id("org.metaborg.convention.settings") version "latest.integration"
+}
+
+rootProject.name = "repoman"
diff --git a/repoman/src/main/kotlin/org/metaborg/repoman/GenerateCommand.kt b/repoman/src/main/kotlin/org/metaborg/repoman/GenerateCommand.kt
new file mode 100644
index 0000000..c6ca570
--- /dev/null
+++ b/repoman/src/main/kotlin/org/metaborg/repoman/GenerateCommand.kt
@@ -0,0 +1,232 @@
+package org.metaborg.repoman
+
+import com.charleskorn.kaml.Yaml
+import com.charleskorn.kaml.decodeFromStream
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.options.default
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.types.path
+import gg.jte.ContentType
+import gg.jte.TemplateEngine
+import gg.jte.output.FileOutput
+import gg.jte.resolve.ResourceCodeResolver
+import org.metaborg.repoman.meta.RepoMetadata
+import java.io.IOException
+import java.nio.file.Path
+import kotlin.io.path.*
+
+/**
+ * Generates or updates the files in a repository, including:
+ * - README.md
+ * - LICENSE.md
+ * - CONTRIBUTING.md
+ * - CODE_OF_CONDUCT.md
+ * - CHANGELOG.md
+ * - .gitignore
+ * - ./gradlew (Gradle wrapper)
+ * - docs/ (MkDocs Material)
+ * - .github/workflows/ (GitHub CI/CD)
+ */
+object GenerateCommand: CliktCommand(
+    name = "generate",
+    help = "Generates a README.md file for a specific repository."
+) {
+    /** The file with the repository metadata. */
+    val metadataFile: Path? by option("-m", "--meta", help = "The file with the repository metadata")
+        .path(canBeFile = true, canBeDir = false, mustExist = true)
+
+    /** The directory with the repository. */
+    val repoDir: Path? by option("-r", "--repo", help = "The directory with the repository")
+        .path(canBeFile = false, canBeDir = true, mustExist = true)
+
+    /** The Gradle binary to invoke. */
+    val gradleBin: String? by option("--gradle-bin", help = "The Gradle binary to invoke")
+        .default("gradle")
+
+    /** Whether to force updating files even if they exist. Use this to just update all files and manually
+     * use version control to sort out what to actually update. */
+    val forceUpdate: Boolean by option("-f", "--force-update", help = "Force updating files even if they exist")
+        .flag(default = false)
+
+
+    override fun run() {
+        val repoDir = repoDir ?: Path.of(System.getProperty("user.dir"))
+        val metadata = readMetadata(repoDir)
+        val resolver = ResourceCodeResolver("templates", Program::class.java.classLoader)
+        val engine = TemplateEngine.create(resolver, ContentType.Plain)
+        engine.setTrimControlStructures(true)
+        val generator = Generator(repoDir, engine, metadata)
+
+        generator.generateReadme()
+        generator.generateLicense()
+        generator.generateContributing()
+        generator.generateCodeOfConduct()
+        generator.generateChangelog()
+        generator.generateGitignore()
+        generator.generateGradleWrapper()
+        generator.generateGradleRootProject()
+        generator.generateGithubWorkflows()
+        println("Done!")
+    }
+
+    private fun readMetadata(repoDir: Path): RepoMetadata {
+        println("Reading metadata...")
+
+        val metadataFile = metadataFile ?: repoDir.resolve("repo.yaml")
+        val metadata = Yaml.default.decodeFromStream<RepoMetadata>(metadataFile.inputStream())
+
+        return metadata
+    }
+
+    class Generator(
+        private val repoDir: Path,
+        private val engine: TemplateEngine,
+        private val metadata: RepoMetadata,
+    ) {
+        fun generateReadme() {
+            val generate = metadata.files.readme.generate
+            val update = metadata.files.readme.update || forceUpdate
+            generate("README.md", generate, update)
+        }
+
+        fun generateLicense() {
+            val generate = metadata.files.license.generate
+            val update = metadata.files.license.update || forceUpdate
+            generate("LICENSE.md", generate, update)
+        }
+
+        fun generateContributing() {
+            val generate = metadata.files.contributing.generate
+            val update = metadata.files.contributing.update || forceUpdate
+            generate("CONTRIBUTING.md", generate, update)
+        }
+
+        fun generateCodeOfConduct() {
+            val generate = metadata.files.codeOfConduct.generate
+            val update = metadata.files.codeOfConduct.update || forceUpdate
+            generate("CODE_OF_CONDUCT.md", generate, update)
+        }
+
+        fun generateChangelog() {
+            val generate = metadata.files.changelog.generate
+            val update = metadata.files.changelog.update || forceUpdate
+            generate("CHANGELOG.md", generate, update)
+        }
+
+        fun generateGitignore() {
+            val generate = metadata.files.gitignore.generate
+            val update = metadata.files.gitignore.update || forceUpdate
+            generate("gitignore", generate, update, path = ".gitignore")
+        }
+
+        fun generateGradleWrapper() {
+            val generate = metadata.files.gradleWrapper.generate
+            val update = metadata.files.gradleWrapper.update || forceUpdate
+            generateGradleWrapper(generate, update)
+        }
+
+        fun generateGradleRootProject() {
+            val generate = metadata.files.gradleRootProject.generate
+            val update = metadata.files.gradleRootProject.update || forceUpdate
+            generate("settings.gradle.kts", generate, update)
+            generate("build.gradle.kts", generate, update)
+        }
+
+        fun generateGithubWorkflows() {
+            val generate = metadata.files.githubWorkflows.generate
+            val update = metadata.files.githubWorkflows.update || forceUpdate
+            generate("github/workflows/build.yaml", generate, update, path = ".github/workflows/build.yaml")
+            if (metadata.files.githubWorkflows.buildDocs) {
+                generate("github/workflows/documentation.yaml", generate, update, path = ".github/workflows/documentation.yaml")
+            }
+        }
+
+        private fun generate(templateName: String, generate: Boolean, update: Boolean, path: String = templateName) {
+            val outputFile = repoDir.resolve(path)
+            val outputFileExisted = outputFile.exists()
+            if (!outputFileExisted && !generate) {
+                println("$path: Not generated")
+                return
+            } else if (outputFileExisted && !update) {
+                println("$path: Not updated")
+                return
+            }
+
+            FileOutput(outputFile).use { output ->
+                engine.render("$templateName.kte", metadata, output)
+            }
+
+            if (!outputFileExisted) {
+                println("$path: Generated")
+            } else {
+                println("$path: Updated")
+            }
+        }
+
+        @OptIn(ExperimentalPathApi::class)
+        private fun generateGradleWrapper(generate: Boolean, update: Boolean) {
+            // We use the gradle/wrapper/gradle-wrapper.properties file to determine whether a Gradle wrapper
+            //  is present and configured, as this is the file that might be customized by the user. The other
+            //  files can safely be regenerated.
+            val gradleWrapperProperties = repoDir.resolve("gradle/wrapper/gradle-wrapper.properties")
+            val gradleWrapperPropertiesExisted = gradleWrapperProperties.exists()
+            if (!gradleWrapperPropertiesExisted && !generate) {
+                println("Gradle wrapper: Not generated")
+                return
+            } else if (gradleWrapperPropertiesExisted && !update) {
+                println("Gradle wrapper: Not updated")
+                return
+            }
+
+            // Generate the wrapper in a temporary directory and copy it to the repository
+            val tmpDir = createTempDirectory()
+            val settingsFile = tmpDir.resolve("settings.gradle.kts")
+            settingsFile.createFile()
+            val processBuilder = ProcessBuilder().apply {
+                command(
+                    gradleBin,
+                    "wrapper",
+                    "--gradle-version=${metadata.files.gradleWrapper.gradleVersion}",
+                    "--distribution-type=${metadata.files.gradleWrapper.gradleDistributionType}",
+                    "--quiet",
+                )
+                directory(tmpDir.toFile())
+                // Merge STDERR into STDOUT
+                redirectErrorStream(true)
+            }
+            try {
+                // THROWS: IOException, SecurityException, UnsupportedOperationException
+                val process = processBuilder.start()
+                // NOTE: We don't close streams that we didn't open.
+                val stdout = process.inputStream.bufferedReader().readText()
+                // THROWS: InterruptedException
+                val exitCode = process.waitFor()
+                if (exitCode != 0) throw IOException(stdout.trim())
+            } catch (ex: IOException) {
+                println("Gradle wrapper: Failed to generate: ${ex.message}")
+                return
+            } catch (ex: SecurityException) {
+                println("Gradle wrapper: Failed to generate: ${ex.message}")
+                return
+            } catch (ex: UnsupportedOperationException) {
+                println("Gradle wrapper: Failed to generate: ${ex.message}")
+                return
+            } catch (ex: InterruptedException) {
+                println("Gradle wrapper: Failed to generate: ${ex.message}")
+                return
+            }
+
+            // Remove the temporary Gradle settings file
+            settingsFile.deleteExisting()
+            // Copy the generated files back to the repository directory, overwriting what's there
+            tmpDir.copyToRecursively(repoDir, followLinks = false, overwrite = true)
+
+            if (!gradleWrapperPropertiesExisted) {
+                println("Gradle wrapper: Generated")
+            } else {
+                println("Gradle wrapper: Updated")
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/repoman/src/main/kotlin/org/metaborg/repoman/Markdown.kt b/repoman/src/main/kotlin/org/metaborg/repoman/Markdown.kt
new file mode 100644
index 0000000..73b3b01
--- /dev/null
+++ b/repoman/src/main/kotlin/org/metaborg/repoman/Markdown.kt
@@ -0,0 +1,4 @@
+package org.metaborg.repoman
+
+/** A Markdown string. */
+typealias Markdown = String
\ No newline at end of file
diff --git a/repoman/src/main/kotlin/org/metaborg/repoman/Program.kt b/repoman/src/main/kotlin/org/metaborg/repoman/Program.kt
new file mode 100644
index 0000000..999e02a
--- /dev/null
+++ b/repoman/src/main/kotlin/org/metaborg/repoman/Program.kt
@@ -0,0 +1,23 @@
+package org.metaborg.repoman
+
+import com.github.ajalt.clikt.core.NoOpCliktCommand
+import com.github.ajalt.clikt.core.subcommands
+
+object Program {
+    @JvmStatic
+    fun main(args: Array<String>) {
+        CLI.main(args)
+    }
+}
+
+/** The command-line root base class. */
+object CLI: NoOpCliktCommand(name = "repoman") {
+    init {
+        // TODO: Set versionOption()
+        subcommands(
+            GenerateCommand,
+        )
+    }
+}
+
+
diff --git a/repoman/src/main/kotlin/org/metaborg/repoman/meta/RepoMetadata.kt b/repoman/src/main/kotlin/org/metaborg/repoman/meta/RepoMetadata.kt
new file mode 100644
index 0000000..69c3b5e
--- /dev/null
+++ b/repoman/src/main/kotlin/org/metaborg/repoman/meta/RepoMetadata.kt
@@ -0,0 +1,234 @@
+package org.metaborg.repoman.meta
+
+import kotlinx.serialization.Serializable
+import org.metaborg.repoman.Markdown
+
+/** Default values. */
+object Defaults {
+    /** The default main branch in Gitonium. */
+    const val MAIN_BRANCH = "main"
+    /** THe default release tag prefix in Gitonium. */
+    const val RELEASE_TAG_PREFIX = "release-"
+}
+
+/** Repository metadata. */
+@Serializable
+data class RepoMetadata(
+    /** The owner of the repo on GitHub. For example: `"metaborg"` */
+    val repoOwner: String = "metaborg",
+    /** The name of the repo on GitHub (required). For example: `"resource"` */
+    val repoName: String,
+    /** The name of the main branch. For example: `"master"` */
+    val mainBranch: String = Defaults.MAIN_BRANCH,
+    /** The release tag prefix to use. For example: `"devenv-release/"` */
+    val releaseTagPrefix: String = Defaults.RELEASE_TAG_PREFIX,
+    /** The default Maven group of the artifacts in the build. For example: `"org.metaborg.devenv"` */
+    val mavenGroup: String = "org.metaborg",
+
+    /** The title of the repo (required). For example: `"Metaborg Resource"` */
+    val title: String,
+    /** A short description of the repo; or `null`. For example: `"A utility library for working with resources."` */
+    val description: Markdown? = null,
+    /** A link to the hosted documentation of the repo; or `null`. For example: `"https://spoofax.dev/gitonium/"` */
+    val documentationLink: String? = null,
+    /** The inception year of the repository (required). */
+    val inceptionYear: String,
+    /** The current year in which the repository is still maintained. */
+    val currentYear: String = "2024",
+
+    /** A list of Maven libraries published by the repo. */
+    val libraries: List<MavenArtifact> = emptyList(),
+    /** A list of Spoofax languages published by the repo. */
+    val languages: List<MavenArtifact> = emptyList(),
+    /** A list of Gradle plugins published by the repo. */
+    val plugins: List<GradlePlugin> = emptyList(),
+
+    /** An ordered list of (main) developers that worked on the repo. */
+    val developers: List<Developer> = emptyList(),
+
+    /** The configurations for the generated files. */
+    val files: Files = Files(),
+)
+
+/** Metadata for the files to generate. */
+@Serializable
+data class Files(
+    /** The metadata for the README.md file. */
+    val readme: Readme = Readme(),
+    /** The metadata for the LICENSE.md file. */
+    val license: License = License(),
+    /** The metadata for the CONTRIBUTING.md file. */
+    val contributing: Contributing = Contributing(),
+    /** The metadata for the CODE_OF_CONDUCT.md file. */
+    val codeOfConduct: CodeOfConduct = CodeOfConduct(),
+    /** The metadata for the CHANGELOG.md file. */
+    val changelog: Changelog = Changelog(),
+    /** The metadata for the .gitignore file. */
+    val gitignore: Gitignore = Gitignore(),
+    /** The metadata for the Gradle wrapper files. */
+    val gradleWrapper: GradleWrapper = GradleWrapper(),
+    /** The metadata for the Gradle root project files. */
+    val gradleRootProject: GradleRootProject = GradleRootProject(),
+    /** The metadata for the GitHub workflows. */
+    val githubWorkflows: GithubWorkflows = GithubWorkflows(),
+)
+
+/** Metadata for the README.md file. */
+@Serializable
+data class Readme(
+    /** Whether to generate the file. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+    /** Content to include in the main body of the readme; or `null`. */
+    val body: Markdown? = null,
+)
+
+/** Metadata for the LICENSE.md file. */
+@Serializable
+data class License(
+    /** Whether to generate the file. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+)
+
+/** Metadata for the CONTRIBUTING.md file. */
+@Serializable
+data class Contributing(
+    /** Whether to generate the file. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+)
+
+/** Metadata for the CODE_OF_CONDUCT.md file. */
+@Serializable
+data class CodeOfConduct(
+    /** Whether to generate the file. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+)
+
+/** Metadata for the CHANGELOG.md file. */
+@Serializable
+data class Changelog(
+    /** Whether to generate the file. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = false,
+)
+
+/** Metadata for the .gitignore file. */
+@Serializable
+data class Gitignore(
+    /** Whether to generate the file. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+    /** Extra entries to include at the bottom of the .gitignore file; or `null`. */
+    val extra: String? = null,
+)
+
+/** Metadata for the .gradlew files. */
+@Serializable
+data class GradleWrapper(
+    /** Whether to generate the files. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+    /** The version of the Gradle wrapper to generate. */
+    val gradleVersion: String = "7.6.4",
+    /** The kind of Gradle distribution type to use, either `"bin"` or `"all"`. */
+    val gradleDistributionType: String = "bin",
+)
+
+/** Metadata for the Gradle root project files. */
+@Serializable
+data class GradleRootProject(
+    /** Whether to generate the files. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = false,
+    /** The name of the root project. */
+    val rootProjectName: String? = null,
+    /** Included builds. */
+    val includedBuilds: List<IncludedBuild> = emptyList(),
+    /** Included projects. */
+    val includedProjects: List<IncludedProject> = emptyList(),
+    /** The version of the Metaborg Gradle convention to use. */
+    val conventionVersion: String = "latest.integration",
+    /** Whether to create `publish` tasks that delegate to the included builds and subprojects. */
+    val createPublishTasks: Boolean = false,
+)
+
+/** Metadata for the GitHub workflows. */
+@Serializable
+data class GithubWorkflows(
+    /** Whether to generate the files. */
+    val generate: Boolean = true,
+    /** Whether to update the file. */
+    val update: Boolean = true,
+    /** Whether to publish releases using GitHub CI (instead of Jenkins or something else). */
+    val publishRelease: Boolean = false,
+    /** Whether to publish snapshots using GitHub CI. */
+    val publishSnapshot: Boolean = false,
+    /** The Gradle `:build` task to use. */
+    val buildTask: String = "build",        // NOTE: No `:` prefix to allow Gradle to figure it out itself.
+    /** The Gradle `:publish` task to use. */
+    val publishTask: String = "publish",    // NOTE: No `:` prefix to allow Gradle to figure it out itself.
+    /** The Gradle `:printVersion` task to use. */
+    val printVersionTask: String = ":printVersion",
+    /** Whether to build and publish the documentation using GitHub CI. */
+    val buildDocs: Boolean = false,
+)
+
+/** A Maven artifact. */
+@Serializable
+data class MavenArtifact(
+    /** The group ID of the artifact. For example: `"org.metaborg"` */
+    val group: String,
+    /** The name of the artifact. For example: `"resource"` */
+    val name: String,
+    /** A short description. For example: `"Resource management library."` */
+    val description: Markdown? = null,
+)
+
+/** A Gradle plugin. */
+@Serializable
+data class GradlePlugin(
+    /** The ID of the plugin. For example: `"org.metaborg.convention.maven-publish"`. */
+    val id: String,
+    /** A short description. For example: `"Resource management library."` */
+    val description: Markdown? = null,
+)
+
+/** A developer. */
+@Serializable
+data class Developer(
+    /** The ID of the developer, usually their GitHub nickname. */
+    val id: String,
+    /** The (full) name of the developer. */
+    val name: String,
+    /** The e-mail address of the developer. */
+    val email: String,
+)
+
+/** An included build. */
+@Serializable
+data class IncludedBuild(
+    /** The name of the included build; or `null` to use the default. */
+    val name: String? = null,
+    /** The path to the included build. */
+    val path: String,
+)
+
+/** An included project. */
+@Serializable
+data class IncludedProject(
+    /** The name of the included project. */
+    val name: String,
+    /** The path to the included project; or `null` to use the default. */
+    val path: String? = null,
+)
\ No newline at end of file
diff --git a/repoman/src/main/resources/templates/CHANGELOG.md.kte b/repoman/src/main/resources/templates/CHANGELOG.md.kte
new file mode 100644
index 0000000..27e9776
--- /dev/null
+++ b/repoman/src/main/resources/templates/CHANGELOG.md.kte
@@ -0,0 +1,17 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+
+## [Unreleased]
+
+
+## [0.0.1] - 2000-01-01
+
+
+[unreleased]: https://github.com/${meta.repoOwner}/${meta.repoName}/compare/release-0.0.1...HEAD
+[0.0.1]: https://github.com/${meta.repoOwner}/${meta.repoName}/releases/tag/release-0.0.1
+
diff --git a/repoman/src/main/resources/templates/CODE_OF_CONDUCT.md.kte b/repoman/src/main/resources/templates/CODE_OF_CONDUCT.md.kte
new file mode 100644
index 0000000..7a152d8
--- /dev/null
+++ b/repoman/src/main/resources/templates/CODE_OF_CONDUCT.md.kte
@@ -0,0 +1,133 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
+
diff --git a/repoman/src/main/resources/templates/CONTRIBUTING.md.kte b/repoman/src/main/resources/templates/CONTRIBUTING.md.kte
new file mode 100644
index 0000000..dacca16
--- /dev/null
+++ b/repoman/src/main/resources/templates/CONTRIBUTING.md.kte
@@ -0,0 +1,57 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+# ${meta.title}
+
+## How to Contribute
+Thank you for wanting to contribute to this project! :tada::+1:
+
+> **Note**:
+> We may not deal with your issue or pull request in a timely manner, or at all.
+> We also reserve the right to change your contribution in any way we deem fit
+> for this project, or even outright reject it.
+
+#### **You have a question?**
+Search the [Discussions][1] and [Stackoverflow][3] to see whether your question
+has already been answered, or ask your question there.
+Please do **not** make an issue on the Github repository.
+
+
+#### **You found a bug**
+Search the [Issues][2] to ensure the bug has not been reported before.
+
+If the bug is new, open a new issue with a _clear title and description_.
+Please indicate:
+- what you did,
+- what you expected to happen, and
+- what actually happened.
+
+Try to include as much relevant information as you have.
+For example, a code sample or executable test case are very helpful.
+
+
+#### **You wrote a patch with a cosmetic change**
+Please do not submit pull requests for cosmetic changes,
+such as whitespace and formatting changes.
+We will reject them.
+
+
+#### **You wrote a patch with a bug fix**
+Thank you! Please open a GitHub pull request with the patch.
+
+
+#### **You wrote a patch that adds a new feature or changes an existing one**
+Please open an issue _first_, so we can discuss the change.
+
+
+#### **You want to contribute to the documentation or test suite**
+Thank you! Please open a GitHub pull request with the patch.
+
+---
+
+Thanks! :heart: :heart: :heart:
+
+[Programming Languages Group](https://pl.ewi.tudelft.nl/), [Delft University of Technology](https://www.tudelft.nl/)
+
+[1]: https://github.com/${meta.repoOwner}/${meta.repoName}/discussions
+[2]: https://github.com/${meta.repoOwner}/${meta.repoName}/issues
+[3]: https://stackoverflow.com/
diff --git a/repoman/src/main/resources/templates/LICENSE.md.kte b/repoman/src/main/resources/templates/LICENSE.md.kte
new file mode 100644
index 0000000..362ac12
--- /dev/null
+++ b/repoman/src/main/resources/templates/LICENSE.md.kte
@@ -0,0 +1,194 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_&lt;<http://www.apache.org/licenses/>&gt;_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+  this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+  changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+  all copyright, patent, trademark, and attribution notices from the Source form
+  of the Work, excluding those notices that do not pertain to any part of the
+  Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+  Derivative Works that You distribute must include a readable copy of the
+  attribution notices contained within such NOTICE file, excluding those notices
+  that do not pertain to any part of the Derivative Works, in at least one of the
+  following places: within a NOTICE text file distributed as part of the
+  Derivative Works; within the Source form or documentation, if provided along
+  with the Derivative Works; or, within a display generated by the Derivative
+  Works, if and wherever such third-party notices normally appear. The contents of
+  the NOTICE file are for informational purposes only and do not modify the
+  License. You may add Your own attribution notices within Derivative Works that
+  You distribute, alongside or as an addendum to the NOTICE text from the Work,
+  provided that such additional attribution notices cannot be construed as
+  modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+    Copyright [yyyy] [name of copyright owner]
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
diff --git a/repoman/src/main/resources/templates/README.md.kte b/repoman/src/main/resources/templates/README.md.kte
new file mode 100644
index 0000000..a18d063
--- /dev/null
+++ b/repoman/src/main/resources/templates/README.md.kte
@@ -0,0 +1,88 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+# ${meta.title}
+[![Build][github-badge:build]][github:build]
+[![License][license-badge]][license]
+[![GitHub Release][github-badge:release]][github:release]
+@if(meta.documentationLink != null)
+[![Documentation][documentation-badge]][documentation]
+@endif
+
+
+${meta.description}
+
+@if(meta.documentationLink != null)
+[![Documentation][documentation-button]][documentation]
+
+@endif
+
+@if(meta.languages.isNotEmpty())
+| Language | Latest Release | Latest Snapshot |
+|----------|----------------|-----------------|
+@for(entry in meta.languages)
+| `${entry.group}:${entry.name}` | [![Release][mvn-rel-badge:${entry.group}:${entry.name}]][mvn:${entry.group}:${entry.name}] | [![Snapshot][mvn-snap-badge:${entry.group}:${entry.name}]][mvn:${entry.group}:${entry.name}] |
+@endfor
+
+@endif
+
+
+@if(meta.libraries.isNotEmpty())
+| Artifact | Latest Release | Latest Snapshot |
+|----------|----------------|-----------------|
+@for(entry in meta.libraries)
+| `${entry.group}:${entry.name}` | [![Release][mvn-rel-badge:${entry.group}:${entry.name}]][mvn:${entry.group}:${entry.name}] | [![Snapshot][mvn-snap-badge:${entry.group}:${entry.name}]][mvn:${entry.group}:${entry.name}] |
+@endfor
+
+@endif
+
+
+@if(meta.plugins.isNotEmpty())
+| Gradle Plugin | Latest Release | Latest Snapshot |
+|---------------|----------------|-----------------|
+@for(entry in meta.plugins)
+| `${entry.id}` | [![Release][mvn-rel-badge:${entry.id}:${entry.id}.gradle.plugin]][mvn:${entry.id}:${entry.id}.gradle.plugin] | [![Snapshot][mvn-snap-badge:${entry.id}:${entry.id}.gradle.plugin]][mvn:${entry.id}:${entry.id}.gradle.plugin] |
+@endfor
+
+@endif
+
+
+@if(meta.files.readme.body != null)
+${meta.files.readme.body}
+
+@endif
+
+## License
+Copyright ${if (meta.inceptionYear != meta.currentYear) meta.inceptionYear + "-" + meta.currentYear else meta.inceptionYear} [Programming Languages Group](https://pl.ewi.tudelft.nl/), [Delft University of Technology](https://www.tudelft.nl/)
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at <https://www.apache.org/licenses/LICENSE-2.0>
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an **"as is" basis, without warranties or conditions of any kind**, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+[github-badge:build]: https://img.shields.io/github/actions/workflow/status/${meta.repoOwner}/${meta.repoName}/build.yaml
+[github:build]: https://github.com/${meta.repoOwner}/${meta.repoName}/actions
+[license-badge]: https://img.shields.io/github/license/${meta.repoOwner}/${meta.repoName}
+[license]: https://github.com/${meta.repoOwner}/${meta.repoName}/blob/${meta.mainBranch}/LICENSE.md
+[github-badge:release]: https://img.shields.io/github/v/release/${meta.repoOwner}/${meta.repoName}?display_name=release
+[github:release]: https://github.com/${meta.repoOwner}/${meta.repoName}/releases
+@if(meta.documentationLink != null)
+[documentation-badge]: https://img.shields.io/badge/docs-latest-brightgreen
+[documentation]: ${meta.documentationLink}
+[documentation-button]: https://img.shields.io/badge/Documentation-blue?style=for-the-badge&logo=googledocs&logoColor=white
+@endif
+
+
+@for(entry in meta.languages)
+[mvn:${entry.group}:${entry.name}]: https://artifacts.metaborg.org/#nexus-search;gav~${entry.group}~${entry.name}~~~
+[mvn-rel-badge:${entry.group}:${entry.name}]: https://img.shields.io/nexus/r/${entry.group}/${entry.name}?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:${entry.group}:${entry.name}]: https://img.shields.io/nexus/s/${entry.group}/${entry.name}?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+@endfor
+@for(entry in meta.libraries)
+[mvn:${entry.group}:${entry.name}]: https://artifacts.metaborg.org/#nexus-search;gav~${entry.group}~${entry.name}~~~
+[mvn-rel-badge:${entry.group}:${entry.name}]: https://img.shields.io/nexus/r/${entry.group}/${entry.name}?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:${entry.group}:${entry.name}]: https://img.shields.io/nexus/s/${entry.group}/${entry.name}?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+@endfor
+@for(entry in meta.plugins)
+[mvn:${entry.id}:${entry.id}.gradle.plugin]: https://artifacts.metaborg.org/#nexus-search;gav~${entry.id}~${entry.id}.gradle.plugin~~~
+[mvn-rel-badge:${entry.id}:${entry.id}.gradle.plugin]: https://img.shields.io/nexus/r/${entry.id}/${entry.id}.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+[mvn-snap-badge:${entry.id}:${entry.id}.gradle.plugin]: https://img.shields.io/nexus/s/${entry.id}/${entry.id}.gradle.plugin?server=https%3A%2F%2Fartifacts.metaborg.org&label=%20
+@endfor
diff --git a/repoman/src/main/resources/templates/build.gradle.kts.kte b/repoman/src/main/resources/templates/build.gradle.kts.kte
new file mode 100644
index 0000000..7ed0f40
--- /dev/null
+++ b/repoman/src/main/resources/templates/build.gradle.kts.kte
@@ -0,0 +1,56 @@
+@import org.metaborg.repoman.meta.Defaults
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+import org.metaborg.convention.Developer
+import org.metaborg.convention.MavenPublishConventionExtension
+
+// Workaround for issue: https://youtrack.jetbrains.com/issue/KTIJ-19369
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+    id("org.metaborg.convention.root-project")
+    alias(libs.plugins.gitonium)
+}
+
+@if(meta.files.gradleRootProject.createPublishTasks)
+rootProjectConvention {
+    // Add `publishAll` and `publish` tasks that delegate to the subprojects and included builds.
+    registerPublishTasks.set(true)
+}
+@endif
+
+allprojects {
+    apply(plugin = "org.metaborg.gitonium")
+
+@if(meta.mainBranch != Defaults.MAIN_BRANCH || meta.releaseTagPrefix != Defaults.RELEASE_TAG_PREFIX)
+    // Configure Gitonium before setting the version
+    gitonium {
+    @if(meta.mainBranch != Defaults.MAIN_BRANCH)
+        mainBranch.set("${meta.mainBranch}")
+    @endif
+    @if(meta.releaseTagPrefix != Defaults.RELEASE_TAG_PREFIX)
+        tagPrefix.set("${meta.releaseTagPrefix}")
+    @endif
+    }
+@endif
+
+    version = gitonium.version
+    group = "${meta.mavenGroup}"
+
+    pluginManager.withPlugin("org.metaborg.convention.maven-publish") {
+        extensions.configure(MavenPublishConventionExtension::class.java) {
+            repoOwner.set("${meta.repoOwner}")
+            repoName.set("${meta.repoName}")
+
+            metadata {
+                inceptionYear.set("${meta.inceptionYear}")
+@if(meta.developers.isNotEmpty())
+                developers.set(listOf(
+    @for(entry in meta.developers)
+                    Developer("${entry.id}", "${entry.name}", "${entry.email}"),
+    @endfor
+                ))
+@endif
+            }
+        }
+    }
+}
diff --git a/repoman/src/main/resources/templates/github/workflows/build.yaml.kte b/repoman/src/main/resources/templates/github/workflows/build.yaml.kte
new file mode 100644
index 0000000..030c5a5
--- /dev/null
+++ b/repoman/src/main/resources/templates/github/workflows/build.yaml.kte
@@ -0,0 +1,47 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+---
+name: 'Build & Publish'
+
+on:  # yamllint disable-line rule:truthy
+  push:
+  pull_request:
+    branches:
+      - ${meta.mainBranch}
+
+jobs:
+  build:
+    uses: metaborg/actions/.github/workflows/gradle-build-matrix.yaml@main
+    with:
+      gradle-command: |
+        gradle ${meta.files.githubWorkflows.buildTask}
+@if(meta.files.githubWorkflows.publishSnapshot)
+# Publish snapshots
+  publish-snapshot:
+    uses: metaborg/actions/.github/workflows/gradle-publish.yaml@main
+    with:
+      gradle-command: |
+        gradle ${meta.files.githubWorkflows.publishTask} -Pgitonium.isSnapshot=true
+      gradle-version-command: |
+        gradle -q ${meta.files.githubWorkflows.printVersionTask} -Pgitonium.isSnapshot=true
+    if: "github.event_name == 'push' && github.ref == 'refs/heads/${meta.mainBranch}'"
+    needs: [build]
+    secrets:
+      METABORG_ARTIFACTS_USERNAME: ${'$'}{{ secrets.METABORG_ARTIFACTS_USERNAME }}
+      METABORG_ARTIFACTS_PASSWORD: ${'$'}{{ secrets.METABORG_ARTIFACTS_PASSWORD }}
+@endif
+@if(meta.files.githubWorkflows.publishRelease)
+# Publish releases
+  publish-release:
+    uses: metaborg/actions/.github/workflows/gradle-publish.yaml@main
+    with:
+      gradle-command: |
+        gradle ${meta.files.githubWorkflows.publishTask}
+      gradle-version-command: |
+        gradle -q ${meta.files.githubWorkflows.printVersionTask}
+    if: "github.event_name == 'push' && startsWith(github.ref, 'refs/tags/${meta.releaseTagPrefix}')"
+    needs: [build]
+    secrets:
+      METABORG_ARTIFACTS_USERNAME: ${'$'}{{ secrets.METABORG_ARTIFACTS_USERNAME }}
+      METABORG_ARTIFACTS_PASSWORD: ${'$'}{{ secrets.METABORG_ARTIFACTS_PASSWORD }}
+@endif
\ No newline at end of file
diff --git a/repoman/src/main/resources/templates/github/workflows/documentation.yaml.kte b/repoman/src/main/resources/templates/github/workflows/documentation.yaml.kte
new file mode 100644
index 0000000..cda4fba
--- /dev/null
+++ b/repoman/src/main/resources/templates/github/workflows/documentation.yaml.kte
@@ -0,0 +1,14 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+---
+name: 'Documentation'
+
+on:  # yamllint disable-line rule:truthy
+  push:
+    branches:
+      - ${meta.mainBranch}
+  workflow_dispatch: {} # Allow running this workflow manually (Actions tab)
+
+jobs:
+  documentation:
+    uses: metaborg/actions/.github/workflows/mkdocs-material.yaml@main
diff --git a/repoman/src/main/resources/templates/gitignore.kte b/repoman/src/main/resources/templates/gitignore.kte
new file mode 100644
index 0000000..76d70b9
--- /dev/null
+++ b/repoman/src/main/resources/templates/gitignore.kte
@@ -0,0 +1,78 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+# Java
+*.class
+*.log
+*.jar
+*.war
+*.nar
+*.ear
+
+
+# Gradle
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+
+# IntelliJ
+.idea/*
+!.idea/icon.svg
+!.idea/icon_dark.svg
+*.iws
+out/
+*.iml
+*.ipr
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+
+# Eclipse
+.metadata
+.classpath
+.project
+.apt_generated
+.settings
+.springBeans
+.sts4-cache
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+.factorypath
+.recommenders/
+.apt_generated/
+.apt_generated_test/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+
+# NetBeans
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+
+# VS Code
+.vscode/
+
+# Misc
+.cache
+.DS_Store
+*.lock
+jte-classes/
+
+@if(meta.files.gitignore.extra != null)
+# Extra
+${meta.files.gitignore.extra}
+@endif
\ No newline at end of file
diff --git a/repoman/src/main/resources/templates/settings.gradle.kts.kte b/repoman/src/main/resources/templates/settings.gradle.kts.kte
new file mode 100644
index 0000000..5920185
--- /dev/null
+++ b/repoman/src/main/resources/templates/settings.gradle.kts.kte
@@ -0,0 +1,38 @@
+@import org.metaborg.repoman.meta.RepoMetadata
+@param meta: RepoMetadata
+dependencyResolutionManagement {
+    repositories {
+        maven("https://artifacts.metaborg.org/content/groups/public/")
+        mavenCentral()
+    }
+}
+
+pluginManagement {
+    repositories {
+        maven("https://artifacts.metaborg.org/content/groups/public/")
+        gradlePluginPortal()
+    }
+}
+
+plugins {
+    id("org.metaborg.convention.settings") version "${meta.files.gradleRootProject.conventionVersion}"
+}
+
+@if(meta.files.gradleRootProject.rootProjectName != null)
+rootProject.name = "${meta.files.gradleRootProject.rootProjectName}"
+@endif
+
+@for(entry in meta.files.gradleRootProject.includedProjects)
+include(":${entry.name}")
+    @if(entry.path != null)
+project(":${entry.name}").projectDir = file("${entry.path}")
+    @endif
+@endfor
+
+@for(entry in meta.files.gradleRootProject.includedBuilds)
+    @if(entry.name != null)
+includeBuild("${entry.path}") { name = "${entry.name}" }
+    @else
+includeBuild("${entry.path}")
+    @endif
+@endfor
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 94e472e..b575dc9 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,3 +21,4 @@ plugins {
 includeBuild("convention-plugin/")
 includeBuild("depman/")
 includeBuild("example/")
+includeBuild("repoman/")