jdaugherty commented on issue #16348:
URL: https://github.com/apache/grails-core/issues/16348#issuecomment-6150508865

   From the hackathon: Revision 9, 2026-10-13 
   
   Contents
   
   1. [1. Verdict on the proposal](#verdict)
   2. [2. Target architecture](#architecture)
      1. [2.1 One command tree](#one-cli)
      2. [2.2 Feature packs](#engine)
      3. [2.3 Catalog and fetch](#metadata)
      4. [2.4 The .grails directory](#project-state)
      5. [2.5 The console](#console)
      6. [2.6 feature add](#feature-add)
      7. [2.7 Hosted Forge](#hosted)
   3. [3. What profiles offered](#gaps)
   4. [4. Removal inventory](#removal)
   5. [5. Delivery](#phases)
   6. [6. Open decisions](#decisions)
   7. [7. Testing and docs](#testing)
   8. [A. Command tree](#appendix-a)
   9. [B. Facts relied on](#appendix-b)
   10. [Change log](#changelog)
   
   apache/grails-core #16348
   
   # One generator: Forge, feature packs, and a profile-free console
   
   **Base** feat/forge-grails-composite (PR #16551, target 8.1.x) **Verified** 
2026-10-13 **PR state** approved, conflicting with the 8.1.x tip **Revision** 
9, 2026-10-13 (see [change log](#changelog))
   
   This plan evaluates the proposal to make `grails-forge-cli` the only 
application generator, let plugins extend generation without compiling code 
into Forge or running Gradle, remove profiles, and keep `grails-shell-cli` as 
the interactive console. File pointers are relative to the PR #16551 branch. 
Merging that PR, after resolving its conflict with the current `8.1.x` tip, is 
step zero.
   
   ## 1. Verdict on the proposal
   
   The direction is right. Profiles bought two things in Grails 3: a skeleton 
engine and an extensibility story, since anyone could publish a profile jar 
without a Grails pull request. Forge is now the better skeleton engine 
(exclusive options, Hibernate 7 BOM switching, preview and diff, a website, 
version slots) but has no extensibility story. Every feature is a compiled 
`@Component` in `grails-forge-core` found by component scan, and the shipped 
CLI is one 48 MB shadow jar with no extension directory 
(`grails-forge/grails-cli/build.gradle:117`). A declarative, data-only feature 
format that plugins ship inside their jars restores what profiles offered 
without compiling anything into Forge and without Gradle at generation time.
   
   | Proposal item | Verdict | Adjustment |
   | --- | --- | --- |
   | 1. forge-cli searches plugin metadata and selects plugins | Yes | The 
metadata is `apache/grails-plugins-metadata`: 256 reviewed plugin entries with 
coordinates, labels, docs and an indexer that already downloads each released 
jar to read its descriptor. Nothing in the framework reads it yet: today's 
shell `list-plugins` command still scrapes an HTML directory listing of the 
legacy `org.grails.plugins` repository 
(`grails-profiles/base/commands/list-plugins.groovy:7`), and Forge reads only 
its own `pom.xml` for third-party versions. Both go away. The plan makes Forge 
read `grails-plugins-index.json` from the repository's `main` branch and adds 
one artifact: a **feature pack** (per-plugin descriptor plus templates) that 
the indexer learns to detect. |
   | 2. forge-cli updates existing apps with a plugin's feature | Yes, scoped | 
Make it `grails feature add`, built on the existing `FeatureDiffer` (web-only 
today) and applied as a three-way patch with a dry run. Not a general rewriter. 
The `update-cli-config` command the code tells users to run was never 
implemented (`grails-forge-cli/.../CodeGenConfig.groovy:181`); a `project 
migrate` command replaces it. |
   | 3. Remove `create-app` from grails-shell-cli | Yes | Keep the command 
name; it becomes Forge's. Delete the dispatcher (`DelegatingShellApplication`), 
`-t/--type`, and `GRAILS_PREFERRED_SHELL`, which still defaults to the profile 
shell (`:56`). |
   | 4. Remove profiles | Yes, staged | The widest blast radius. Every app 
build has a `profile` configuration with a default 
`org.apache.grails.profiles:web` dependency 
(`grails-gradle/plugins/.../GrailsGradlePlugin.groovy:461-490`); 
`grails.profile` is injected as an active Spring profile at runtime 
(`grails-core/.../GrailsApplicationPostProcessor.groovy:451`); 109 in-repo 
`application.yml` files set it; the BOM publishes profile constraints; the 
release verifier inspects the base profile jar 
(`etc/bin/verify-jar-artifacts.sh:208-236`). Nothing is removed in a minor 
release: the modules, shell code, `profile` Gradle configuration and runtime 
`grails.profile` injection all go in 9.0.x together, with `project migrate` and 
the upgrade guide carrying existing applications. |
   | 5. grails-shell-cli stays as the interactive console | Yes, but a rewrite 
| The shell codes almost nothing itself. Inside a project every command, 
including `help`, `gradle` and plugin `ApplicationCommand`s, is dispatched 
through the resolved profile (`grails-shell-cli/.../GrailsCli.groovy:385`, 
`AbstractProfile.groovy:461-499`), and roughly 40 user-facing commands 
(run-app, test-app, clean, compile, war, console, every artefact `create-*`) 
are profile scripts. The console needs a profile-free command catalog and 
built-in lifecycle commands. |
   | `.grails/` directory, gitignored | Partly | Use `.grails/`, but **commit** 
the manifest (`.grails/project.yml`) and ignore only caches and run state. 
`feature add`, codegen gating, IDE detection and teammates' clones all need the 
manifest. Forge already renders a `.gitignore` 
(`grails-forge-core/grails-app/views/forge/feature/build/gitignore.gsp`), so 
the ignore rules are a template edit. |
   
   ## 2. Target architecture
   
   Section 2 describes the end state. Section 5 says what can land in `8.1.x` 
(additions, and replacements that function the same) and what waits for `9.0.x` 
(every removal).
   
   ### 2.1 One `grails` command tree
   
   - Picocli root = Forge's `ForgeCommand`, renamed `grails`. Subcommands: 
`create-app`, `create-restapi`, `create-plugin`, `create-web-plugin`, 
`create-rest-api-plugin` (#14932), `feature list|search|info|diff|add`, `plugin 
search|info` (catalog aliases), the artefact `create-*` commands, `project 
init|migrate`, and the console commands (`run-app`, `test-app`, `gradle`, 
`open`, `help`, plugin commands). With no arguments inside a project it opens 
the interactive console; outside a project it prints help.
   - The console's project commands are registered into the same Picocli tree 
at startup from the three sources in section 2.5, so completion and help are 
unified. Forge's `InteractiveShell` already fixes its completion set at startup 
(`grails-forge-cli/.../ForgeCli.groovy:103-106`); that stays.
   - Delete `DelegatingShellApplication.groovy`, the `grails-shell-cli` and 
`grails-forge-cli` start scripts 
(`grails-forge/grails-cli/build.gradle:166-192`), and the `-t` handling (9.0.x; 
through 8.x they stay and the new tree sits beside them).
   - Startup budget. The profile shell resolves `grails-bom` through Aether in 
a constructor even for `grails --help` outside a project 
(`grails-shell-cli/.../GrailsDependencyVersions.groovy:65-82`) and resolves the 
project classpath through the Tooling API before `help` inside one. Removing 
the Spring Boot CLI port (`run`, `S/boot`, `S/compiler`, `S/archive`, 
maven-resolver) and all profile resolution is the main speed win. The Forge CLI 
still boots a full Grails context per invocation (37 auto-configurations merged 
into the shadow jar); set a measured target such as 500 ms for `--help` and 
track it with the init-script timing recipe used for #16337.
   
   ### 2.2 Generation engine: compiled features plus declarative feature packs
   
   Forge keeps compiled `Feature` beans for framework-owned concerns: 
application types, build files, BOM switching (`GrailsBase`), `OneOfFeature` 
groups, options. It gains a `DeclarativeFeature` adapter that turns a 
descriptor plus a template directory into a `Feature` when the registry is 
built. Packs are loaded from three places:
   
   1. built-in packs bundled in `grails-forge-core` resources (official plugins 
migrate from compiled classes to packs wherever the feature is "dependencies 
plus config plus files");
   2. packs fetched from plugin jars at generation time (third-party, section 
2.3);
   3. a local directory (`--feature-pack-dir`) for authoring and testing a pack 
before publishing.
   
   Descriptor, `META-INF/grails-forge/feature.yml`:
   
   ```
   format: 1
   name: acme-audit
   title: Acme Audit Trail
   description: Adds the Acme audit plugin and its default configuration
   category: Logging                      # a Forge Category string
   documentation: https://acme.example/docs/grails
   applicationTypes: [web, rest-api]      # default: all
   requires: [gorm-hibernate7]            # auto-selected, like 
processSelectedFeatures
   conflicts: [gorm-mongodb]              # fails validatePreProcessing with a 
message
   oneOf: audit                           # optional exclusive group
   dependencies:
     - scope: implementation
       coordinates: com.acme:acme-audit-grails
       version: ${pack.version}           # default; omit for BOM-managed 
coordinates
     - scope: runtimeOnly
       coordinates: com.acme:acme-audit-h2
       when: { feature: h2 }              # conditional on another selected 
feature
       excludes: [org.slf4j:slf4j-simple] # new: Dependency has no exclusions 
today
   platforms:
     - coordinates: com.acme:acme-bom
       version: 2.3.0
   buildPlugins:
     - id: com.acme.audit
       version: 2.3.0
   build:                                 # typed, merged build-file 
contributions (see "Build-file contributions")
     properties:
       acme.audit.verbose: 'false'        # gradle.properties
     extensions:
       acmeAudit: |                       # one acmeAudit { } block, merged 
with other packs' entries
         retention = 90
       tasks.bootRun: |                   # tasks.named('bootRun') { }
         systemProperty 'acme.audit.console', 'true'
   configuration:                         # merged through Forge's 
configuration model
     acme.audit.enabled: true
     environments:
       production:
         acme.audit.flush: 30s
   files:                                 # relative to 
META-INF/grails-forge/templates/
     - from: AuditConfig.groovy.gsp
       to: grails-app/conf/{packagePath}/AuditConfig.groovy
     - from: audit.css                    # no suffix: copied verbatim, 
binary-safe
       to: grails-app/assets/stylesheets/audit.css
   readme: |
     Audit events are written to the `audit_event` table. See the documentation 
link.
   instructions: |                        # printed after create-app, as 
profiles did
     Run `./gradlew dbmGormDiff` to generate the audit tables migration.
   artefacts:                             # adds `grails create-audit-listener 
<Name>`
     - command: create-audit-listener
       description: Creates an audit listener and its specification
       files:
         - from: artefact/AuditListener.groovy.gsp
           to: src/main/groovy/{packagePath}/{className}AuditListener.groovy
         - from: artefact/AuditListenerSpec.groovy.gsp
           to: src/test/groovy/{packagePath}/{className}AuditListenerSpec.groovy
   ```
   
   Mapping onto what exists in `grails-forge-core`:
   
   | Descriptor key | Forge call | Notes |
   | --- | --- | --- |
   | `dependencies`, `platforms` | `GeneratorContext.addDependency` with 
`Dependency.builder()` scopes, `.pom(true)` for platforms 
(`build/dependencies/Dependency.groovy:187-266`, 
`GradleDependency.groovy:102-122`) | Add `excludes` to the model. Declarative 
features run at `FeaturePhase.DEFAULT`; anything after `BUILD` is silently 
dropped by the build-file snapshot (`feature/build/gradle/Gradle.groovy:73`). |
   | `buildPlugins` | `GradlePlugin.builder().id().version()` |  |
   | `build.properties` | `getBuildProperties().put` 
(`GeneratorContext.groovy:145`) | Snapshotted at `BUILD` like dependencies 
(`Gradle.groovy:80`). |
   | `build.extensions`, `build.blocks`, `build.imports`, `build.settings` | a 
new keyed contribution API on `GradleBuild`, replacing the single 
`renderExtensions()` slot (`GradleBuild.groovy:122-134`) and 
`renderSettingsExtensions()` | See "Build-file contributions". |
   | `configuration` | `getConfiguration()`, `getConfiguration(env)` 
(`GeneratorContext.groovy:152-188`) |  |
   | `files` | `addTemplate` with `GspTemplate` over a pack-scoped page 
locator, or `BinaryTemplate` for verbatim files | Destination tokens are the 
existing `{packageName}`, `{packagePath}`, `{className}`, `{naturalName}`, 
`{propertyName}`, `{name}` (`DefaultTemplateRenderer.groovy:44-62`). |
   | `readme` | `addHelpTemplate` |  |
   | `requires`, `conflicts`, `oneOf` | `FeatureContext.addFeature`, a 
`FeatureValidator`, a synthetic `OneOfFeature` group |  |
   | `artefacts` | new entries in the codegen command set, which is a static 
list today (`ForgeCli.CODE_GEN_COMMANDS`, `ForgeCli.groovy:45-57`) | Must 
become dynamic. |
   
   **Template language: GSP source, compiled by the CLI.** External packs are a 
CLI-only capability; start.grails.org hosts the framework's own features and 
never renders a third-party pack (section 2.7). That removes the reason to keep 
pack templates logic-less, so packs use the same language as Forge's own 62 
templates: GSP, with `<%@ page trimLogicLines="true" expressionCodec="none" %>` 
and the same model (`project`, `features`, `options`). Bundled official packs 
are precompiled by the monorepo build as today. A third-party pack ships GSP 
*source*, and the CLI compiles it at generation time through 
`GroovyPagesTemplateEngine` with development mode on for the pack's directory 
(today `ForgePages` forces it off, `template/ForgePages.groovy:62-67`), caching 
the compiled classes under `~/.grails/forge/cache/<gav>/classes/<forge 
version>/`. Source packs are tied to GSP syntax, not to a Groovy or GSP binary 
version, so one pack serves every Grails line its plugin supports. Files 
without 
 a `.gsp` suffix are copied as bytes. A pack therefore runs the plugin author's 
code at generation time; that is the same trust the user already extends to the 
plugin's Gradle plugin and runtime, and the CLI says so when it fetches a pack.
   
   What this buys: converting an official compiled feature into a pack is a 
file move plus a descriptor, not a template rewrite; plugin authors write the 
template language they already know; and the 
`features.contains('gorm-mongodb')` branches in today's templates carry over 
unchanged. What it costs: a few hundred milliseconds of Groovy compilation per 
template on first use; type errors in a statically compiled page (`<%@ model 
%>`) surface at generation time rather than at the author's build; and no 
static validation of a pack beyond its descriptor.
   
   Mustache (jmustache, logic-less, no code execution) stays the fallback if a 
logic-less format is ever needed, for example if the hosted service were ever 
to render community packs; the descriptor's `format` version leaves room for a 
`templateEngine` field. An earlier revision of this plan recommended Mustache 
for exactly that hosted-safety reason; with the hosted service official-only, 
the trade favours GSP.
   
   **Precedents.** Quarkus extensions declare `metadata.codestart` in 
`quarkus-extension.yaml`, ship the codestart in a `codestarts` classifier jar 
(`codestart.yml` plus Qute templates), and users select them with `quarkus 
create app -x group:artifact:version`. Spring Initializr keeps its dependency 
catalog declarative (`initializr.dependencies` with `compatibilityRange`, 
`bom`, `repository`) and reserves code (`ProjectContributor`, 
`BuildCustomizer`) for the service itself. Grails 3 profiles' `feature.yml` 
plus `skeleton/` is the same shape as this proposal; it lived in the wrong 
engine.
   
   **Conditional logic in packs.** Forge features routinely add code depending 
on which other features are selected. On this branch 22 of the 118 feature 
classes and 10 of the 47 GSP templates branch on other features, in three 
patterns, and each has a declarative home. The rule: logic lives in the 
descriptor as data that Forge evaluates, and templates stay logic-less.
   
   1. *Auto-adding a collaborator* (`processSelectedFeatures`): Hibernate 7 
adds H2 when no driver feature is selected 
(`database/GrailsDataHibernate7.groovy:64`), Geb adds Spock, Scaffolding adds 
GSP, Spring Security UI adds Spring Security. Most of the 22 classes do only 
this. It is the descriptor's `requires` list, evaluated during selection, 
before anything renders. The "add a default if nothing of this kind is present" 
variant needs a group form:
   
      ```
      requires:
        - gsp
        - oneOf: { group: database-driver, default: h2 }
      ```
   2. *Conditional contributions* (`apply`): `TestContainers` adds 
`testcontainers-spock` only when Spock is present and `testcontainers-mongodb` 
only when a Mongo feature is (`database/TestContainers.groovy:81-87`). That is 
a `when` predicate on a dependency, file or configuration entry, so the entry 
exists or does not; no template logic is involved:
   
      ```
      dependencies:
        - { scope: testImplementation, coordinates: 
org.testcontainers:testcontainers-spock, when: { feature: spock } }
        - { scope: testImplementation, coordinates: 
org.testcontainers:testcontainers-mongodb, when: { anyOf: [gorm-mongodb, 
mongo-sync] } }
      files:
        - { from: MongoSpec.groovy.gsp, to: 
src/integration-test/groovy/{packagePath}/MongoSpec.groovy, when: { feature: 
gorm-mongodb } }
      ```
   3. *Branching inside a file*: the security `user.gsp` emits a `collection 
'users'` mapping for MongoDB and a `table 'users'` mapping for either Hibernate 
(`feature/security/template/user.gsp:41-50`). Pack templates are GSP, so this 
is written exactly as it is today, and selection finishes before rendering 
starts, so the `features` list a template sees is final:
   
      ```
      <% if (features.contains('gorm-mongodb')) { %>
          static mapping = {
              collection 'users'
          }
      <% } else if (features.contains('gorm-hibernate5') || 
features.contains('gorm-hibernate7')) { %>
          static mapping = {
              table 'users'
          }
      <% } %>
      ```
   
      A descriptor may still name a condition once as a flag and reuse it in 
several files (`<% if (flags.sql) { %>`), which keeps templates short, but 
nothing requires it:
   
      ```
      flags:
        sql: { anyOf: [gorm-hibernate5, gorm-hibernate7] }
      ```
   
   One predicate language serves `when`, `requires` and `flags`: `feature`, 
`anyOf`, `allOf`, `not`, `applicationType`, and `option` (for example `option: 
{ data: hibernate7 }`). Predicates matter most for the parts a pack does not 
write as a template: the build file and `application.yml` are generated by 
Forge from the dependency and configuration models, so a conditional dependency 
or configuration key has to be declared, not templated. The render model is the 
one today's GSPs use (`project` with `name`, `className`, `propertyName`, 
`naturalName`, `packagePath`; `features`; `options`) plus the pack's `flags` 
and any `params` the pack declares (the user and role class names of an 
`s2-quickstart`-style pack).
   
   Two things in the inventory stay compiled: features that take over a slot 
another feature owns (`GrailsBase` choosing the BOM, the Gradle plugin picking 
`grails-web` versus `grails-plugin`, `buildGradle.gsp` branching per feature 
name) and features that reshape another feature's templates. A pack declares 
`conflicts` and `requires`; it does not rewrite someone else's output.
   
   **How the template decision changed.** Revisions 1 to 5 of this plan 
recommended Mustache, on the grounds that a pack had to be safe to render on 
start.grails.org. Once external packs became a CLI-only capability (section 
2.7), the remaining arguments were weighed again:
   
   - *Precompiled GSP in the plugin jar* was rejected on its own merits: 
compiled pages bind to one Groovy and GSP version, so a pack built on Grails 
8.0 is not guaranteed to load in the 8.1 CLI and will not load in a Groovy 6 
CLI, and precompiling drags `grails-web-gsp` plus the servlet API into every 
plugin's build. Source packs compiled by the CLI avoid both.
   - *Code execution* is accepted in the CLI. A user who selects a plugin 
already runs that plugin's Gradle plugin at build time and its code at runtime; 
a pack adds nothing new to that trust, and the CLI names the pack and its 
checksum before first use.
   - *Reviewability and tooling* are weaker with GSP: a pack cannot be 
validated or previewed without executing it. `feature diff` and `--dry-run` 
still work, because rendering in memory is how Forge previews anyway.
   - *Whitespace* is handled by the `trimLogicLines` page directive that 
already exists for Forge's templates.
   
   Mustache, Handlebars or Pebble remain available later through a 
`templateEngine` field if a logic-less format is wanted for a hosted use. 
Framework-owned features keep their precompiled GSPs regardless.
   
   **Build-file contributions.** `build.gradle` is the file every pack wants to 
touch, and it is where the current design is weakest. Three mechanisms coexist 
today: the model (dependencies, plugins, repositories, buildscript, 
properties); one generic slot, `${gradleBuild.renderExtensions()}` after the 
dependencies block (`feature/build/gradle/templates/buildGradle.gsp:107`), fed 
by `GradlePlugin.extension(Writable)`; and eight branches hard-coded in the 
central template on feature names (`gradle-build-src`, `asciidoctor`, `jrebel`, 
`spock`, `geb-with-local-browsers`, `grails-compile-static`, `gsp`, 
`asset-pipeline-grails`, `buildGradle.gsp:40-122`) plus the BOM choice in 
`buildSrcBuildGradle.gsp:50-58`. The hard-coded branches are the "everyone 
edits the central file" problem in miniature, and a pack cannot use them at 
all. There is also a silent bug: `Dependency.Builder.extension(Writable)` is 
accepted and then dropped (`build/dependencies/Dependency.groovy:315,361-373`), 
so the `s
 ourceSets` block `DatabaseMigrationPlugin` tries to add never reaches the 
build file (`feature/migration/DatabaseMigrationPlugin.groovy:72`).
   
   The rule for packs: no pack ships a `build.gradle` template or a text patch. 
The build file stays generated from a model, and packs contribute through typed 
channels that Forge merges:
   
   | Channel | Renders | Merge rule |
   | --- | --- | --- |
   | `dependencies`, `platforms` | lines in `dependencies {}` | same group, 
artifact and scope collapse (existing `Dependency` equality); the same 
coordinate with two explicit versions is an error naming both packs |
   | `buildPlugins` (`id`, `version`, `apply`) | a `plugins {}` entry or an 
`apply plugin:` line | same id collapses; different versions error |
   | `build.repositories`, `build.buildscript` | `repositories {}`, 
`buildscript {}` | by URL or coordinate |
   | `build.properties` | `gradle.properties` | same key and value collapse; 
different values error |
   | `build.imports` | import lines at the top of the file | set |
   | `build.settings` | `settings.gradle` fragments, for example 
`pluginManagement` repositories | ordered |
   | `build.extensions.<name>` | one `<name> { }` block per key with the bodies 
concatenated; `tasks.<task>` keys render `tasks.named('<task>') { }` | merged 
across packs and framework features, in feature order |
   | `build.blocks` | a free-form fragment at an anchor: `afterPlugins`, 
`afterDependencies`, `end` | ordered, never merged; the escape hatch for 
`sourceSets {}` or a custom task |
   
   Every fragment is a GSP with the pack's model, so it can branch on features. 
Ordering is deterministic: feature order, then feature name, then pack name. 
Today ties within a phase come from a `HashSet` walk 
(`FeatureContext.groovy:90`), so the same selection can render blocks in a 
different order between runs; the sort fixes that as a side effect.
   
   The framework's own features move to the same channels in phase 1: the 
asset-pipeline feature contributes `assets {}`, compile-static contributes 
`grails {}`, Spock and Geb contribute `tasks.test`, asciidoctor and jrebel 
contribute their blocks, and `GrailsBase` contributes the BOM. The central 
template then owns structure only. That is the proof that the channels are 
sufficient, and it is what makes `feature add` tractable: the diff between the 
baseline build and the pack-applied build is a set of insertions inside 
existing blocks rather than a rewrite, which the three-way merge in section 2.6 
applies cleanly as long as the user kept the block.
   
   Limits: a pack cannot change the BOM selection, reorder the framework's 
plugins, change the Java toolchain, or remove another feature's lines; 
`conflicts` is the tool for incompatibility. The Gradle plugin's validation 
warns when a `build.blocks` fragment could have been a typed channel, so 
free-form blocks stay rare.
   
   ### 2.3 Plugin metadata: catalog plus pack
   
   **Where a pack lives, and the Gradle plugin that publishes it.** Inside the 
plugin's main jar under `META-INF/grails-forge/`. Authoring and publishing are 
automated by the Grails Gradle plugins, the same way `grails-plugin-cli` 
automates `-cli` companions today: a plugin author keeps the pack in 
`src/main/forge/` (`feature.yml` plus `templates/`), and the `grails-plugin` 
Gradle plugin, when that directory exists, does the rest:
   
   - validates `feature.yml` against the descriptor schema at build time 
(unknown keys, bad predicates, destination paths that escape the project, 
references to unknown feature names from the bundled index);
   - compiles every `.gsp` in the pack with the same GSP compiler Forge uses, 
as a check task, so type errors in a `<%@ model %>` page fail the author's 
build instead of a user's `create-app`; the shipped artifact stays source;
   - packages the directory into `META-INF/grails-forge/` of the runtime jar 
and stamps a `Grails-Forge-Pack: <name>` manifest attribute, which the indexer 
and the CLI can read without opening the whole jar;
   - registers a `forgePackTest` task that generates a sample application for 
each supported application type with the pack applied, through forge-core in 
process, and runs `./gradlew classes` on it (opt-in, since it needs the local 
Maven publish chain the Forge tests already use);
   - with `grails-publish` applied, needs nothing more: the pack travels with 
the plugin's existing publication.
   
   A `grails create-feature-pack` command scaffolds `src/main/forge/` in an 
existing plugin. The alternative layout, a `forge` classifier artifact as 
Quarkus uses, downloads less but adds a publication to sign and release; the 
fetcher prefers a `-forge.jar` classifier when present and falls back to the 
main jar, so the Gradle plugin can switch to it later without changing the 
format.
   
   **Catalog.** The catalog already exists. `apache/grails-plugins-metadata` is 
the data source of the plugin page: one YAML file per plugin at 
`grails-plugins/<group path>/<artifactId>.yml`, submitted by pull request and 
reviewed by the Grails team.
   
   ```
   name: Cache
   desc: Provides AST transformations for caching method calls
   coords: org.apache.grails:grails-cache
   owner: Apache Software Foundation
   vcs: https://github.com/apache/grails-core
   docs: https://grails.apache.org/docs/latest/guide/cache.html
   maven-repo: https://repo1.maven.org/maven2
   labels: [cache]
   licenses: [Apache-2.0]
   versions:                     # maintained by the indexer, newest first
     - version: 8.0.0
       date: 2026-10-04T18:46:18Z
       grailsVersion: 8.0.0-SNAPSHOT > *
   ```
   
   Every eight hours `index-plugins.groovy` reads each plugin's 
`maven-metadata.xml`, downloads every new version's jar by its exact URL, opens 
`META-INF/grails-plugin.xml` inside it to read `grailsVersion`, rewrites the 
YAML, and regenerates `grails-plugins-index.json` (a JSON array of those maps) 
through a pull request. The website's `PluginsTask` fetches that file at build 
time. As of 2026-10-13 the index has 256 entries (157 under 
`org.grails.plugins`, 29 under `org.apache.grails`, 12 under `io.github.gpc`, 9 
under `cloud.wondrify`), 40 marked `deprecated`, and 61 with at least one 
version whose `grailsVersion` starts with 7 or 8.
   
   The plan extends this pipeline instead of adding a second catalog:
   
   1. **The indexer detects packs.** While it has a version's jar open it also 
looks for `META-INF/grails-forge/feature.yml` and records `forge: { name, 
applicationTypes }` plus the jar's `sha256` on that version entry. Same 
download, no extra cost. A plugin opts in by shipping the file; the YAML 
submission process does not change.
   2. **Forge reads the index.** forge-cli and forge-web consume 
`grails-plugins-index.json`: `labels` map to Forge categories through a small 
table, `desc` and `docs` fill description and documentation, `deprecated` hides 
an entry from `feature search` unless asked for, and `versions[].grailsVersion` 
(the `7.0.0 > *` range syntax from the plugin descriptor) filters candidates 
against the CLI's Grails version; the newest compatible non-snapshot version 
wins by default. The index for the matching release is bundled into 
`grails-forge-core` as the offline fallback and refreshed straight from the 
repository: 
`https://raw.githubusercontent.com/apache/grails-plugins-metadata/main/grails-plugins-index.json`.
 Whatever is merged to `main` is the current list; the indexer's pull requests 
land there every eight hours, so there is no separate publishing step and no 
mirror. The website already reads the same file, through the pre-ASF `grails/` 
organisation path that GitHub redirects; switching i
 t to `apache/` is a one-line fix.
   3. **Official plugins need no second source.** The 29 `org.apache.grails` 
entries are already listed; the BOM stays the version authority for official 
coordinates inside generated builds, so those packs omit versions.
   4. **Explicit coordinates remain the escape hatch** for a plugin that is not 
in the index: `grails create-app shop --features 
com.acme:acme-audit-grails:2.3.0`. Users can add private indexes and 
repositories in `~/.grails/forge.yml`.
   
   Weaknesses of the index that the plan has to live with or fix: 
`grailsVersion` is whatever the plugin descriptor says (grails-cache 8.0.0 
reports `8.0.0-SNAPSHOT > *`), `docs` is missing on 134 entries, `labels` are 
free-form, there is no checksum today, and the older `sync-latest-version` 
action still refers to a `grails-plugins.json` file that no longer exists.
   
   **Fetching without a build.** The indexer already shows the pattern: 
`createArtifactUrl` builds the exact jar URL from `maven-repo`, `coords` and 
the version, and one zip entry is read from the download. The pack fetcher does 
the same with plain HTTPS GETs, no dependency graph: try `<repo>/<group 
path>/<artifact>/<version>/<artifact>-<version>-forge.jar`, then the main jar; 
read only the `META-INF/grails-forge/**` entries from the stream; cache under 
`~/.grails/forge/cache/<group>/<artifact>/<version>/`. Repositories: the 
entry's `maven-repo` (honouring per-version `coords` and `maven-repo` 
overrides), then Maven Central, then `GRAILS_REPO_URL` (the existing semicolon 
list, already validated by `GrailsRepositoryOverrides`), then 
`~/.grails/forge.yml` entries with credentials. Snapshots read 
`maven-metadata.xml` for the timestamped file. Proxies come from the JVM's 
proxy properties and `~/.m2/settings.xml`; a small dependency-free reader of 
that file avoids pulling Maven Resolver b
 ack into the CLI. `--offline` and `--refresh` flags; the catalog cache has a 
24-hour TTL.
   
   **Security model.** External packs exist only in the CLI. HTTPS only; sha256 
verification against the index when it carries one; destination paths must be 
relative and must not escape the project (`..`, absolute, symlinks); entry size 
and count caps on the zip; existing files are never overwritten at `create-app` 
time; and because a GSP pack executes the plugin author's code at generation 
time, the CLI prints the pack's coordinates and checksum before first use, and 
`--offline` serves only cached packs. The hosted Forge (start.grails.org) never 
loads a pack from outside its own build: it serves the framework's features, 
some of which may be implemented as bundled packs, and never fetches 
coordinates on request. This belongs in `THREAT_MODEL.md`.
   
   ### 2.4 Project state: `.grails/`
   
   ```
   .grails/
     project.yml   committed; replaces grails-forge-cli.yml and grails.profile
     cache/        ignored; packs resolved for this project
     run/          ignored; run-app pid, port and stopping markers 
(build/run-app.pid today)
     history       ignored; console history (~/.grails_history is global today)
   ```
   
   ```
   format: 1
   grailsVersion: 8.1.0
   applicationType: web
   defaultPackage: com.acme.shop
   options: { jdk: 21, servlet: tomcat, data: hibernate7, reloading: devtools }
   features:
     - name: gsp
     - name: grails-spring-security
     - name: acme-audit
       coordinates: com.acme:acme-audit-grails
       version: 2.3.0
       applied: 2026-10-13
   ```
   
   Why commit it: `feature add` must know the current feature set and options 
to compute a diff; artefact codegen is gated on it (today the gate is 
`grails-forge-cli.yml` in the current directory only, no parent walk, 
`ForgeCli.groovy:115-128`); IDEs can detect a Grails project and its type from 
it; CI can validate it. Precedents: Micronaut's `micronaut-cli.yml`, 
`.sdkmanrc`, `.mvn/`. Everything else under `.grails/` is per-machine and goes 
into the generated `.gitignore`.
   
   Two keys stay in `application.yml` because runtime code reads them: 
`grails.codegen.defaultPackage` 
(`grails-testing-support-web/.../WebSetupSpecInterceptor.groovy:111`, 
`grails-console/.../GrailsApplicationScriptRunner.groovy:69`) and, through 8.x, 
`grails.profile`, because `GrailsApplicationPostProcessor` adds it as an active 
Spring profile with a `web` default; 9.0 removes that injection and Forge stops 
writing the key.
   
   `grails project migrate` creates `.grails/project.yml` for an existing app 
from `grails-forge-cli.yml` or legacy `grails-cli.yml` (its `profile:` key maps 
to a type exactly as `CodeGenConfig` does today), or from `grails.profile` plus 
a scan of `build.gradle` for known coordinates, and removes the `profile "..."` 
line from the build.
   
   ### 2.5 The console after profiles
   
   Command sources, in precedence order (first registration wins, as today):
   
   1. **Built-in lifecycle commands**, moved from the base profile's Groovy 
scripts into the console as Picocli commands over the existing Tooling API 
plumbing (`grails-shell-cli/.../gradle/GradleUtil.groovy`, `GradleInvoker`, 
`GradleAsyncInvoker`): `run-app`/`stop-app` (keep `--https`, `--port`, 
`--debug-jvm`, pid handling via `RunningApplicationProcess`), `test-app`, 
`compile`, `clean`, `assemble`/`war`/`package`, `console`, `shell`, 
`dependency-report`, `run-command`, `run-script`, `stats`, `bug-report`.
   2. **Gradle passthrough**: `grails gradle <args>` with completion from 
`ReadGradleTasks` (exists). Fix: arguments are passed as one string today 
(`GradleCommand.groovy:62-65`).
   3. **Plugin `ApplicationCommand`s** from `-cli` companions through 
`ApplicationContextCommandFactory` and `GradleTaskCommandAdapter` (exists, 
profile-free apart from the `ProfileCommand` interface). Fix the two known 
gaps: legacy Grails 7 commands are listed but have no named Gradle task 
(`GrailsCliGradlePlugin.groovy:411-414`), so route them to `runCommand`; 
app-local `grails-app/commands` are invisible because only `grailsCliClasspath` 
jars are scanned (`:450-480`).
   4. **Artefact codegen**, one implementation in Forge, called in-process: the 
five existing commands plus `create-unit-test`, `create-integration-test`, 
`create-functional-test`, `create-restful-controller`, 
`create-domain-resource`, `create-command`, `create-script`, and pack-provided 
artefacts. Template lookup order: `src/main/templates/<path>` in the app, then 
the pack, then Forge's built-in GSPs. Extend scaffolding's `install-templates` 
idea so users can copy Forge's artefact templates into `src/main/templates`.
   5. **Script commands** (`src/main/scripts/*.groovy` in the app, 
`META-INF/commands/*` in plugin jars, the `GroovyScriptCommand` DSL): unchanged 
in 8.x; in 9.0 the profile lookups go (`methodMissing` chaining through 
`profile.hasCommand`, `GroovyScriptCommand.groovy:170-184`, and the profile 
branch of `TemplateRendererImpl.groovy:241-266`) and the scripts stay, 
deprecated, until the major after 9.0. Six in-repo commands exist only as 
scripts and need conversion to `ApplicationCommand`s or pack artefacts in 9.0: 
geb `create-functional-test`, quartz `create-job`, `s2-create-acl-domains`, 
`s2ui-create-challenge-questions`, `s2ui-override`, and dbmigration's 
`dbm-create-changelog` / `dbm-changelog-to-groovy`, whose `-cli` jar depends on 
grails-shell-cli for `org.grails.cli.profile.ExecutionContext` 
(`grails-data-hibernate7/dbmigration/build.gradle:87-92`).
   6. **Catalog services**: help, completion, abbreviation expansion (`rA` for 
`run-app`), "did you mean", and the legacy-command hint move from 
`AbstractProfile.groovy:361-499` into a profile-free `CommandCatalog`.
   
   Project detection walks up from the current directory to find 
`.grails/project.yml` or `grails-app` (today: current directory only, 
`GrailsCli.groovy:351-355`).
   
   **Command execution model.** Commands already live outside profiles today, 
and three of the ways are code. Profiles are not what commands depend on; they 
are what the shell's dispatcher depends on.
   
   | Where a command is defined today | Author | Runs where | Needs a profile? |
   | --- | --- | --- | --- |
   | Profile jar `commands/*.yml` or `.groovy` | profile author | shell JVM | 
yes, by definition |
   | App `src/main/scripts/*.groovy` | app developer | shell JVM | only because 
the shell dispatches every in-project command through the profile object 
(`GrailsCli.groovy:385`) |
   | Plugin jar `META-INF/commands/*` (from `src/main/scripts`) | plugin author 
| shell JVM, found on the app's test runtime classpath | same |
   | `ApplicationCommand` in a plugin's `-cli` companion (`src/cli/groovy`) | 
plugin author | one Gradle `JavaExec` task per command, created by 
`GrailsCliGradlePlugin`; the shell wraps each one in `GradleTaskCommandAdapter` 
| no on the Gradle path; the shell listing still goes through the profile |
   | `ApplicationCommand` in the app (`grails-app/commands`) | app developer | 
`./gradlew runCommand -Pargs=...` only; no task, invisible to the shell | no |
   | Legacy `grails.dev.commands.ApplicationCommand` with 
`legacyCommandSupport` | Grails 7 plugin | `runCommand` only; the shell lists 
it but calls a task that does not exist | no |
   
   The runtime half of this system is `grails-console`, which the Gradle plugin 
auto-provisions into `grailsCli` because every command task's main class comes 
from it. `GrailsApplicationContextCommandRunner` (per-command tasks and 
`runCommand`) boots the application through `DevelopmentGrailsApplication`, 
which swaps the context class to `DevelopmentWebApplicationContext`: a 
`GenericWebApplicationContext` over a `MockServletContext` pointed at 
`src/main/webapp`. The command gets the full context, plugins, GORM, data 
sources and `BootStrap`, unless it carries `skipBootstrap = true` (the 
`SkipBootstrap` trait, currently copied into scaffolding and spring-security). 
No Tomcat, no port. `GrailsApplicationScriptRunner` (`runScript`), 
`GrailsSwingConsole` (`console`) and `GrailsShell` (`shell`) boot the same way 
with console-flavoured contexts; the profile commands named `console` and 
`shell` only invoked those Gradle tasks.
   
   No command in the repository needs a running server: a search of every 
`src/cli` source set finds nothing that starts one or makes HTTP calls. 
`generate-open-api` builds its model from beans (`GrailsOpenApiGenerator`, 
`GroupedOpenApiContributor`); `dbm-*`, `schema-export`, `url-mappings-report`, 
`config-report`, `generate-*` and `s2-*` all work on the context. The 
server-bound operations are lifecycle: `run-app` is `bootRun` under the Tooling 
API, asynchronous in interactive mode, with port polling, the pid file the 
application writes through `grails.cli.pid.file`, and `--https` keystore 
generation; `stop-app` reads that pid; `test-app` runs `integrationTest`, where 
the test JVM starts the server itself.
   
   After profiles, every command maps to exactly one of five execution modes:
   
   | Mode | Runs where | Examples |
   | --- | --- | --- |
   | Codegen, no application | the `grails` JVM, Forge in process | `create-*` 
artefacts, pack artefacts, `feature add`, `project migrate` |
   | Gradle task passthrough | Gradle, Tooling API | `gradle <args>`, 
`compile`, `clean`, `assemble`, `war` |
   | Context-bound command | Gradle `JavaExec` plus 
`GrailsApplicationContextCommandRunner`, full context, mock servlet, no server 
| `dbm-*`, `schema-export`, `url-mappings-report`, `generate-open-api` |
   | Server-bound lifecycle | Gradle, Tooling API, asynchronous | `run-app`, 
`stop-app`, `test-app` |
   | Interactive session | Gradle `JavaExec` into `grails-console` | `console`, 
`shell`, `run-script` |
   
   Two additions follow from the table:
   
   1. *A home for fast, code-bearing plugin commands.* Profile and plugin 
scripts ran instantly in the shell JVM. An `ApplicationCommand` always pays a 
Gradle invocation plus a full application boot, even for pure file generation 
such as `s2-quickstart`, `create-scaffold-*` and `install-templates`; 
`SkipBootstrap` only skips `BootStrap.groovy`. Two answers, both needed: pack 
artefacts for file generation without code, and a `requiresApplicationContext` 
flag on the `ApplicationCommand` contract (default `true`) so the console runs 
context-free commands in its own JVM from `grailsCliClasspath`. The shell 
already builds that class loader and instantiates every command just to list 
them (`ApplicationContextCommandRegistry`), so the cost is already paid; Gradle 
runs such a command in a plain `JavaExec` without booting the application. This 
also makes app-local `grails-app/commands` runnable from the console.
   2. *Server-bound commands are out of scope.* Nothing needs one, and the 
runner cannot provide one. The rule is that a command never starts the server; 
`run-app` followed by a script covers the rest. The contract stays extensible: 
a `requiresServer` mode that boots `GrailsApp` with the embedded container on a 
random port can be added later without changing existing commands.
   
   Housekeeping the inventory found: move `SkipBootstrap` into 
`grails-core-cli`; route legacy commands to `runCommand`; list app-local 
commands; keep the Tooling API connection open across console commands instead 
of reconnecting per call (`GradleUtil.groovy:54-76`).
   
   **IDE integration.** The IntelliJ plugin is now 
`apache/grails-intellij-plugin` (donated by JetBrains, imported from 
`intellij-obsolete-plugins`), so its CLI coupling is work this plan schedules 
rather than a risk to confirm. What it does today, from the source:
   
   - Commands run in a JVM the IDE starts itself, main class 
`org.grails.cli.GrailsCli`, classpath = the 
`org.apache.grails:grails-shell-cli:<grailsVersion>` artifact resolved through 
a detached Gradle configuration by the plugin's own tooling model builder 
(`libs/gradle-tooling/.../GrailsModuleModelBuilderImpl.java:54-78`, 
`plugin/.../runner/impl/Grails3GradleCommandExecutor.java:54`), with 
`--plain-output` appended. The `grails` launcher and `grails-forge-cli` are not 
involved.
   - The command list is hard-coded: 34 names from the profile era 
(`bug-report`, `clean`, `compile`, `console`, `create-*`, `dependency-report`, 
`generate-*`, `gradle`, `install`, `install-templates`, `list-plugins`, 
`plugin-info`, `run-app`, `schema-export`, `shell`, `stats`, `stop-app`, 
`test-app`, `url-mappings-report`, `war`) in 
`plugin/.../commands/Grails3CommandProvider.java:31-70`.
   - The Grails run configuration is `run-app` through the same executor 
(`runner/GrailsRunConfiguration.java:70`), so the IDE's Run button depends on 
the base profile's `run-app` script and its pid file.
   - Project detection is the `grails-app` directory 
(`structure/GrailsApplicationManager.java:167`).
   - The new-project wizard calls the hosted Forge API 
(`https://latest.grails.org`: `/select-options`, `/versions`, 
`/application-types/{type}/features`, `/create`) in 
`feature/forge/GrailsForgeModuleBuilder.kt:67-221`.
   - The plugin's own `IMPROVEMENT-PLAN.md` section 2.6 already intends to move 
the run configuration to Gradle `bootRun`, rebuild the command layer from 
profile YAML plus forge-cli commands, and keep the wizard on the hosted API 
with an offline fallback via `grails-forge-cli`. The profile half of that plan 
is superseded by this one.
   
   What changes on the Grails side so the plugin can follow:
   
   1. *A stable programmatic entry point.* The unified CLI keeps one main class 
and one artifact the IDE can resolve by coordinates for the project's Grails 
version, with `--plain-output` and `--non-interactive` honoured (the shell 
declares `--non-interactive` today and ignores it). Through 8.x the shell, its 
artifact and `org.grails.cli.GrailsCli` stay untouched, so the current plugin 
keeps working; 9.0 removes them, and the plugin release that supports the new 
entry point ships with 9.0.0. The plugin resolves the CLI artifact per project 
Grails version, so 8.x projects are unaffected by that release.
   2. *A machine-readable command catalog.* `grails help --json` (name, 
description, synonyms, arguments, flags, completer kind, execution mode) 
replaces the hard-coded list, so plugin `-cli` commands, pack artefacts and 
app-local commands appear in the IDE without a plugin release. The same catalog 
feeds shell completion, so it is not IDE-only work.
   3. *Project detection and type.* The plugin reads `.grails/project.yml` 
first (application type, default package, features), falling back to 
`grails-app`.
   4. *Run configuration.* `bootRun` directly, as the plugin's own plan says, 
or the CLI's `run-app` in non-interactive mode; either way the pid and port 
files move to `.grails/run/`.
   5. *Wizard.* Unchanged for official features, since the hosted API keeps 
serving them. External packs and `feature add` are CLI operations the IDE can 
surface by invoking the same entry point, which is why they need no web API.
   
   Sequencing: the plugin release is lock-step with 9.0.0, which is why step 5 
lists it.
   
   ### 2.6 `feature add` on an existing app
   
   1. Read `.grails/project.yml`.
   2. Generate two projects in memory with the same type, options and Grails 
version: A with the recorded features, B with the recorded features plus the 
requested ones. This is what the web `DiffController` does through 
`FeatureDiffer`.
   3. Turn the difference into a unified diff. New files are written directly. 
Hunks against existing files (`build.gradle`, `application.yml`, 
`settings.gradle`, `README.md`) are applied as a three-way merge with context 
(java-diff-utils or JGit's apply); a hunk that does not apply is reported with 
the rejected text, and `--dry-run` / `--diff` print the patch instead of 
applying it.
   4. Record the feature in `project.yml`.
   
   This is deliberately additive. Removing a feature prints the inverse diff 
only. The approach is simpler than parsing Gradle scripts and more useful than 
Micronaut's `feature-diff`, which only prints the diff. `feature diff` also 
becomes a CLI command; today the diff exists only as a web endpoint.
   
   ### 2.7 Hosted Forge and the UI
   
   - The hosted service hosts the framework's own features only. External 
packs, the plugin index and `feature add` are CLI capabilities; the web API and 
`apache/grails-forge-ui` gain no plugin search and load nothing from outside 
their build.
   - Official features that become bundled packs render exactly as compiled 
features do (precompiled GSP), so the API, the preview and diff endpoints and 
the version slots need no change. `FeatureDTO` may expose `documentation`, 
which today is only written into the README.
   
   ## 3. What profiles offered and what replaces it
   
   | Capability | Today | After | Status |
   | --- | --- | --- | --- |
   | `create-app` with profiles | shell plus Aether, default path | Forge | 
replaced |
   | Company or third-party skeletons (custom profile jars, `--profile g:a:v`) 
| profile jar | **Preset packs**: a pack with `kind: preset` naming a type, 
option defaults, a feature list and extra files; `grails create-app --preset 
com.acme:acme-starter:1.0 shop` | replaced, new work |
   | `create-profile` and the seven profile-authoring commands | profile 
profile | Not in v1; `grails create-feature-pack <name>` scaffolds a pack 
skeleton later | dropped, small gap |
   | `list-profiles`, `profile-info`, `list-plugins`, `plugin-info` | shell; 
the last two scrape the legacy repository | `feature list`, `feature info`, 
`plugin search`, `plugin info`, backed by `grails-plugins-index.json` | 
replaced |
   | `--features` replaces defaults; unknown names warn | profiles | Forge 
semantics: additive, unknown names fail. Add `--exclude-features` so a default 
such as a Geb variant can be dropped | gap, new flag |
   | `rest-api-plugin` application type | profile only | Add to Forge (#14932) 
| planned gap |
   | Profile features with no Forge equivalent | `less-asset-pipeline`, 
`rx-mongodb`, rest-api `security` 
(`grails-spring-security-rest:7.0.0-SNAPSHOT`), `maven-publish` | 
less-asset-pipeline becomes a pack owned by the asset-pipeline plugin; 
rx-mongodb is dropped (legacy coordinates, RxJava 1/2 deprecated on 9.0.x); 
spring-security-rest ships a pack once it is on 8.x; publishing becomes the 
plugin type's default via grails-publish | mixed |
   | Lifecycle commands (run-app, test-app, war, ...) | base profile scripts | 
Console built-ins | replaced |
   | Artefact `create-*` with web and rest-api variants, test variants | 
profile YAML plus templates | Forge codegen; new templates for the eight 
missing commands | replaced, work |
   | rest-api `generate-all` / `generate-views` producing **gson** views | 
rest-api Groovy scripts | Scaffolding's `-cli` `generate-*` produce GSP only. 
Add gson scaffolding templates to grails-views-gson 
(`src/main/templates/scaffolding/*.gson`) and a `--views gson` option or a 
views-gson `-cli` command | real gap |
   | `src/main/templates` override of artefact templates | shell lookup | Forge 
codegen honours it first | replaced |
   | App scripts and plugin script commands | shell | Unchanged in 8.x; 
profile-free and deprecated in 9.0; `ApplicationCommand` is the path | 
deprecated |
   | `~/.grails/settings.groovy` profile repositories and defaults | shell | 
`~/.grails/forge.yml` repositories and catalogs; proxies from JVM properties 
and `settings.xml` | replaced |
   | Post-create `instructions` | `profile.yml` (unused in-repo) | Pack 
`instructions` | replaced |
   | YAML `execute` / `gradle` command steps | profile YAML | Not needed: 
Gradle tasks and `ApplicationCommand`s | dropped |
   | Welcome page shows `grails.profile` | `index.gsp:141-144` | Show 
`info.app.*`; the type lives in `project.yml`, which the runtime does not read 
| adjust |
   | `grails.profile` as an active Spring profile | runtime, default `web` | 
Unchanged in 8.x, removed in 9.0 with an upgrade note | 9.0 removal |
   | IntelliJ Grails plugin (`apache/grails-intellij-plugin`) | launches 
`org.grails.cli.GrailsCli` from a resolved `grails-shell-cli` jar; hard-coded 
list of 34 shell commands; `run-app` run configuration through the same path; 
new-project wizard on the hosted Forge API | In-scope work, section 2.5 "IDE 
integration": stable CLI entry point and artifact, machine-readable command 
catalog, `.grails/project.yml` detection, `bootRun` run configuration, wizard 
unchanged for official features | replaced, coordinated release |
   | `profile` Gradle configuration in existing builds | default dependency, 
resolved by the tooling model | Unchanged in 8.x; removed in 9.0, where 
`project migrate` deletes the line and a declared `profile` dependency fails 
configuration with a pointer to the upgrade guide | 9.0 removal |
   | Hibernate 7 BOM switch, servlet, JDK, exclusive groups, preview, diff, 
website | Forge only | Kept | gain |
   
   ## 4. Removal inventory
   
   Grouped by area; each item is a known dependency on profiles or the 
dispatcher.
   
   **Forge** (`grails-forge/`): `feature/grailsProfiles/GrailsProfiles.groovy` 
(replace with a `ProjectManifest` feature that writes `.grails/project.yml` and 
`grails.codegen.defaultPackage`); `Scope.PROFILE`, `Phase.PROFILE`, 
`GradleConfiguration.PROFILE`; `CodeGenConfig` legacy `profile:` handling 
(moves to `project migrate`); the drift spec `GrailsGspSpec.groovy:326-350`, 
which asserts byte identity with `grails-profiles/web` skeleton files; 
`GrailsProfilesSpec`; `grails-forge-web/grails-app/conf/application.yml:18`; 
stale comments in `grails-cli/build.gradle:70` and 
`grails-cli-shadow/build.gradle:50`; the `.gitignore` DirectoryScanner 
workaround in `grails-forge/settings.gradle:88-93`.
   
   **Shell** (`grails-shell-cli/`): everything under `profile/repository/`, 
`Profile`, `AbstractProfile`, `ResourceProfile`, `FileSystemProfile`, 
`Feature`, `DefaultFeature`, `ProfileRepository*`, `ProfileRepoConfig`, 
`ProfileUtil`, the `Create*Command`, `ListProfilesCommand`, 
`ProfileInfoCommand`, `ArgumentCompletingCommand`, `io/GradleDependency`, their 
service-file entries, `GrailsCli` members 
`:87-89,129-133,195-201,260-271,307-311,324-332,386-394,518-534`; the Spring 
Boot CLI port and the maven-resolver dependencies 
(`grails-shell-cli/build.gradle:86-100`). Re-home `CompilerAutoConfiguration`, 
which the hibernate boot-plugins compile against 
(`grails-data-hibernate5/boot-plugin/build.gradle:50`). Rewrite rather than 
delete: `CommandRegistry.findCommands`, the four `CommandFactory` 
implementations, `GradleTaskCommandAdapter`, `HelpCommand`, 
`GroovyScriptCommand`, `TemplateRendererImpl`, `RenderStep`, 
`DefaultStepFactory`'s `ProfileCommand` gate (`:44-47`), `populateContextLoader`
  (keep the classpath part, drop `:571,582-584`).
   
   **Gradle plugins** (`grails-gradle/`): 
`GrailsGradlePlugin.groovy:166,461-490,782-784`; 
`GrailsPluginGradlePlugin.groovy:138-140`; 
`GrailsClasspathToolingModelBuilder.groovy:35,51-56` and 
`GrailsClasspath.getProfileDependencies`; the `grails-profile` and 
`grails-publish-profile` plugins (`plugins/build.gradle:143-160`, `profiles/*`, 
`ProfileCompilerTask`; `GroovyScriptCommandTransform` stays while scripts are 
supported); `ProfileCompilerTaskSpec`; `BomPlatformFunctionalSpec.groovy:76-98` 
and its fixture 
`test-projects/bom-platform-default-dependencies/build.gradle:98`.
   
   **Root build**: the seven `grails-profiles/*` modules 
(`settings.gradle:465-485`), `grails-spring-security/rest/testapp-profile` 
(convert its features to packs or test fixtures), 
`gradle/publish-root-config.gradle:172-191`, `build.gradle:76,163-165` 
(`profileProjects`, unused), `PublishPlugin.groovy:58,146-151,191-197`, 
`gradle/rat-root-config.gradle:73-76,161-166`, `settings.gradle:661-666` and 
`end-to-end/settings.gradle:132-137` (DirectoryScanner workaround), 
`grails-bom/base/build.gradle:78-94` and the `forcedGroupPrefixes` entries in 
all five BOMs, `etc/bin/verify-jar-artifacts.sh:208-236`, 
`etc/bin/reset-verify.sh:47`, 
`.github/dependency-graph/external-references.yml:42-48,197-203` (the purls are 
wrong today anyway), `RENAME.md:164-182`, 
`etc/bin/rename_gradle_artifacts.sh:305-347`, `INSTALL:98`.
   
   **In-repo consumers**: `profile "..."` in seven 
`grails-test-examples/*/build.gradle`; `grails.profile` in 109 
`application.yml` files (harmless at runtime, remove for hygiene); `def 
profiles = ['web']` in plugin descriptors (dead: `DefaultGrailsPlugin.profiles` 
is never assigned, `DefaultGrailsPlugin.java:117,139-147`) and in Forge's 
`plugin.gsp:44-47`.
   
   **Docs** (`grails-doc/src/en/guide/`): the whole `profiles` chapter 
(`toc.yml:91-106`, twelve pages) is replaced by a "Feature packs and presets" 
chapter; `commandLine.adoc:20-71,112-122`, `interactiveMode.adoc:19-41`, 
`creatingCustomScripts.adoc`, `reusingGrailsScripts.adoc` and 
`gradleBuild.adoc` are deleted or rewritten; `creatingProject.adoc:19-37`, 
`creatingCustomCommands.adoc:21-69,219-272` (the `executionContext.environment` 
example at `:248` is wrong today), `gradleDependencies.adoc:27,61`, 
`gradleTasks.adoc`, `gradlePlugins.adoc:63-64`, 
`downloadingAndInstalling.adoc:24-93`, `creatingAnApplication.adoc`, 
`aHelloWorldExample.adoc`, `usingInteractiveMode.adoc`, 
`creatingArtefacts.adoc`, `generatingAnApplication.adoc`, 
`REST/restProfile.adoc`, `plugins/creatingAndInstallingPlugins.adoc:390-466`, 
`plugins/repositories.adoc:21-40`, 
`plugins/providingBasicArtefacts.adoc:21-68,127-141`; the 47 command reference 
pages (five are profile-only); `index.adoc:736` includes a path that
  does not exist. `GRAILS_PREFERRED_SHELL` and the wrapper environment 
variables are undocumented today and need a home in the new chapter.
   
   ## 5. Delivery
   
   Project policy: nothing is removed in a minor release. Every removal in this 
plan (the profile modules and Gradle plugins, the shell's `create-*`, 
`list-profiles` and `profile-info`, the dispatcher with `-t` and 
`GRAILS_PREFERRED_SHELL`, the Spring Boot CLI port, the `profile` Gradle 
configuration, the `grails.profile` active-profile injection, the alias start 
scripts, `org.grails.cli.GrailsCli`) therefore lands in `9.0.x`, together. What 
`8.1.x` can carry is additions, and replacements of an implementation that 
functions the same: the declarative engine, packs, the index reader, 
`.grails/`, `feature add`, the unified `grails` command tree added alongside 
the profile shell, and existing commands re-implemented on Forge or on the 
console where the result is equivalent for the user. Whether `8.1.x` carries 
that work or everything waits for `9.0.x` and ships at once is decision 6; the 
steps are the same either way, only the line moves.
   
   0
   
   #### Prerequisites
   
   8.1.x
   
   Merge PR #16551 after resolving the conflict with the current `8.1.x` tip; 
merge `8.1.x` up to `9.0.x`.
   
   **Exit:** Forge builds and tests green inside the composite build on both 
lines.
   
   1
   
   #### Declarative engine
   
   8.1.x, additive, Forge only
   
   `DeclarativeFeature`, descriptor schema and validator, `ForgePages` 
development-mode compilation for pack directories with a compiled-class cache, 
`Dependency.excludes`, dynamic codegen command registration, 
`--feature-pack-dir`. The keyed build-file contribution API (`extensions`, 
`blocks`, `imports`, `settings`) with deterministic ordering; the eight 
feature-name branches in `buildGradle.gsp` and the BOM choice in 
`buildSrcBuildGradle.gsp` move into the owning features; the dropped 
`Dependency.extension` bug fixed. Convert three to five simple compiled 
features (quartz, cache-ehcache, asciidoctor, web-console) to bundled packs as 
proof, with golden-output specs proving identical generation.
   
   **Exit:** packs render byte-identical output to the classes they replace; 
`buildGradle.gsp` contains no `features.contains` branch; hosted Forge 
unchanged.
   
   2
   
   #### Catalog, fetch and authoring
   
   8.1.x, additive, plus grails-gradle and grails-plugins-metadata
   
   Extend `index-plugins.groovy` to detect `META-INF/grails-forge/feature.yml` 
(or the `Grails-Forge-Pack` manifest attribute) and record `forge` and `sha256` 
per version; an index reader in forge-core with the label-to-category table and 
`grailsVersion` compatibility filter; `feature list/search/info` and `plugin 
search/info`; the pack fetcher with cache, repositories, proxies, checksums, 
path guards and the first-use notice; `--features g:a:v`; the `src/main/forge/` 
support in the `grails-plugin` Gradle plugin (descriptor validation, GSP check 
compilation, packaging, manifest attribute, `forgePackTest`); a fixture plugin 
in `grails-gradle` `test-projects/` that uses it and is published to 
`build/local-maven` and consumed by test-core through a local index file.
   
   **Exit:** `grails create-app x --features <gav>` works offline from the 
cache and online from a local Maven repository; `plugin search cache` lists the 
real index entry; the fixture plugin's pack is built and validated by the 
Gradle plugin alone.
   
   3
   
   #### `.grails/` and `feature add`
   
   8.1.x, additive
   
   `project.yml` writer and reader, `.gitignore` entries, `project migrate`, 
`feature diff` CLI, `feature add` with dry run and three-way apply; codegen 
gating accepts `project.yml` and `grails-forge-cli.yml`; Forge keeps writing 
`grails-forge-cli.yml` on 8.x so the existing codegen commands are unchanged.
   
   **Exit:** round trip in test-core: create, `feature add`, build, run.
   
   4
   
   #### Unified command tree and equivalent replacements
   
   8.1.x, additions and same-behaviour replacements
   
   The `grails` launcher gains the Picocli tree (`feature`, `plugin`, 
`project`, `help --json`, the Forge create and codegen commands) alongside the 
profile shell; the dispatcher, `-t` and `GRAILS_PREFERRED_SHELL` keep working. 
Replacements allowed because they function the same: `create-app`, 
`create-plugin` and `create-web-plugin` implemented by Forge once the 
dependency drift between the two generators is closed (#14944, #14933), with 
`-t shell` still reaching the profile implementation; the shell's artefact 
`create-*` delegated to Forge codegen where the files produced are the same 
(the web `create-controller` also creates the views directory); lifecycle 
commands re-implemented as console built-ins behind the same names and flags 
while the profile scripts remain loadable. Profiles, `grails.profile` and the 
`profile` dependency are still emitted and resolved.
   
   **Exit:** parity specs: for every replaced command, the same invocation 
produces the same files and effects on an 8.1 application before and after; the 
new commands work alongside.
   
   5
   
   #### Console rework and removals
   
   9.0.x
   
   Profile-free `CommandCatalog`; built-in lifecycle commands; codegen 
delegated to Forge; script commands kept with the profile lookups removed and 
deprecated; dispatcher, `-t`, `GRAILS_PREFERRED_SHELL`, alias start scripts, 
the Spring Boot CLI port and `org.grails.cli.GrailsCli` removed; 
`grails-profiles/*`, the profile Gradle plugins, shell profile code, BOM 
constraints and release-script checks deleted; the `profile` Gradle 
configuration deleted (a declared `profile` dependency fails configuration with 
a pointer to the upgrade guide and `project migrate`); the `grails.profile` 
active-profile injection removed; Forge stops emitting `grails.profile`, the 
`profile` dependency and `grails-forge-cli.yml`; `rest-api-plugin` type, the 
eight missing artefact commands, gson scaffolding templates, the six 
script-only commands converted; a `grails-intellij-plugin` release supporting 
the 9.0 entry point ships with 9.0.0; startup measured.
   
   **Exit:** no `org.apache.grails.profiles` coordinate anywhere in the build; 
`grails` inside and outside a project needs no profile and no Aether; all shell 
specs rewritten against the catalog; a migrated 8.x application builds and 
runs; an unmigrated one fails with the pointer.
   
   6
   
   #### Docs, website, UI
   
   8.1.x for additions, 9.0.x for removals
   
   A "Feature packs and presets" chapter and `upgrading81x` notes with the 
additions; the "Command line" chapter rewrite, the profiles chapter removal, 
the command reference pages and `upgrading90x` notes with the removals; the 
website's index URL switched to the `apache/` organisation path; SDKMAN and 
distribution scripts; a `THREAT_MODEL.md` section; "ship a feature pack" in the 
`grails-plugins-metadata` README and the plugin authoring guide.
   
   **Exit:** the 8.1 guide documents packs next to profiles; the 9.0 guide 
builds without the profiles chapter.
   
   Deferred to the major after 9.0: removal of script commands 
(`src/main/scripts`, `META-INF/commands`, `GroovyScriptCommand`), which 9.0 
keeps profile-free and deprecated.
   
   Rough sizing: steps 1 to 3 are each a few weeks of Forge-only work; step 5 
is the largest single item because it rewrites the shell's dispatch and tests 
and carries every removal; step 6 is sustained documentation work that can run 
alongside the others.
   
   ## 6. Open decisions
   
   | # | Decision | Recommendation |
   | --- | --- | --- |
   | 1 | Pack template language | GSP source, compiled by the CLI at generation 
time; bundled official packs precompiled. One language for Forge and for packs, 
and existing templates convert by moving. Mustache stays the fallback if a 
logic-less format is ever needed for a hosted use. |
   | 2 | Pack location and authoring | Main jar, `META-INF/grails-forge/`, 
produced by the `grails-plugin` Gradle plugin from `src/main/forge/` with 
validation, GSP check compilation and `forgePackTest`. Add a `forge` classifier 
later only if download size becomes a problem. |
   | 3 | Catalog source of truth | `apache/grails-plugins-metadata` as it is, 
with its indexer extended to flag feature packs and record checksums; Forge 
reads `grails-plugins-index.json` from the repository's `main` branch, with the 
release's copy bundled as the offline fallback. No second catalog, no mirror. |
   | 4 | Is `.grails/project.yml` committed? | Yes. Ignore `cache/`, `run/`, 
`history`. |
   | 5 | Script commands (`src/main/scripts`, `META-INF/commands`) | Unchanged 
in 8.x; profile-free and deprecated in 9.0 once the six script-only commands 
are converted; removed in the major after 9.0. |
   | 6 | Release lines | Settled by policy: every removal lands in 9.0.x 
together; nothing is removed in 8.1.x. Open: whether 8.1.x ships the additions 
and same-behaviour replacements of steps 1 to 4, or everything waits for 9.0.x. 
|
   | 7 | Third-party packs on start.grails.org | Settled 2026-10-13: none. The 
hosted service hosts the framework's own features; external packs are CLI-only. 
|
   | 8 | Fetcher implementation | Plain HTTPS plus a small `settings.xml` 
reader; do not reintroduce Maven Resolver. |
   | 9 | `grails.profile` as a Spring active profile | Unchanged in 8.x; 
removed in 9.0 with an upgrade note. |
   | 10 | Naming | "feature pack" for the per-plugin descriptor, "preset" for a 
type-level bundle, "catalog" for the index. |
   | 11 | Programmatic entry point for the IDE | One main class and one 
artifact resolvable by coordinates per Grails version, `--plain-output` and 
`--non-interactive` honoured, `grails help --json` as the command catalog; 
`org.grails.cli.GrailsCli` and the `grails-shell-cli` artifact stay untouched 
through 8.x and go in 9.0, with a plugin release that supports the new entry 
point shipping with 9.0.0 (the plugin resolves the CLI per project version, so 
8.x projects are unaffected). |
   | 12 | Free-form build-file fragments | Allowed at three anchors 
(`afterPlugins`, `afterDependencies`, `end`) as an escape hatch; typed channels 
preferred, and the Gradle plugin warns when a fragment could have been one. No 
pack ships a `build.gradle` template or a text patch. |
   
   ## 7. Testing and documentation
   
   - `grails-forge-core`: specs for descriptor parsing and validation, 
dependency and platform mapping, environment configuration merging, GSP source 
compilation from a pack directory with the class cache, verbatim copies, 
path-escape rejection, `requires`/`conflicts`/`oneOf` behaviour through 
`ContextFactory`, and golden-output comparisons for every converted feature 
using `CommandOutputFixture.generate`.
   - `grails-gradle`: functional tests for the `src/main/forge/` support 
(descriptor validation failures, GSP check compilation failures, packaging and 
manifest attribute, `forgePackTest`), on the fixture plugin.
   - `test-core`: TestKit builds of generated apps that include bundled packs; 
a fixture plugin in `test-projects/forge-feature-pack-plugin` published to 
`build/local-maven` and consumed both by coordinates and through a catalog 
file; `feature add` round trips.
   - Console: `CommandCatalog` precedence, synonyms, abbreviations and hints; 
interactive dispatch with the existing `TestTerminal`; the legacy-command and 
app-local-command fixes in `grails-gradle` functional tests.
   - End-to-end: generate, `feature add`, build, run, and `project migrate` on 
an 8.0-generated app.
   - Docs: a "Command line" chapter (install, create, options, console, Gradle 
tasks, custom commands via `-cli` companions) and a "Feature packs and presets" 
chapter (descriptor reference, catalog, authoring and testing a pack, 
publishing, security notes), plus `upgrading81x` entries for the additions and 
same-behaviour replacements and `upgrading90x` entries for every removed 
surface.
   
   ## Appendix A: command tree
   
   ```
   grails
     create-app | create-restapi | create-plugin | create-web-plugin | 
create-rest-api-plugin
         [--features a,b,g:a:v] [--exclude-features x] [--preset g:a:v]
         [--data|--servlet|--jdk|--reloading] [--inplace] [--list-features]
     feature   list | search <text> | info <name> | diff <name> | add 
<name|gav> [--dry-run|--diff]
     plugin    search <text> | info <name>                      (catalog 
aliases)
     project   init | migrate
     create-controller | create-service | create-domain-class | create-taglib | 
create-interceptor
     create-unit-test | create-integration-test | create-functional-test | 
create-restful-controller
     create-domain-resource | create-command | create-script | add-property | 
<pack artefacts>
     run-app | stop-app | test-app | compile | clean | assemble | war | package 
| console | shell
     dependency-report | run-command | run-script | stats | bug-report | 
install-templates
     gradle <args> | open <path> | help [command]
     <every ApplicationCommand from -cli companions: dbm-*, schema-export, 
generate-*, s2-*, ...>
   ```
   
   ## Appendix B: facts the plan relies on
   
   - Forge has no declarative mechanism; features are `@Component`s found by 
scanning `org.grails.forge` (`ForgeCoreConfiguration.groovy:35-50`), registered 
by `FeatureRegistry` (`feature/FeatureRegistry.groovy:43-64`).
   - The build file snapshots dependencies at `FeaturePhase.BUILD` 
(`GradleBuildCreator.groovy:34-74`); later additions are lost.
   - `Dependency` has no exclusions; versions come from the BOM, a literal, or 
a `pom.xml` lookup (`PomDependencyVersionResolver.groovy:103-119`).
   - Codegen commands are a static list (`ForgeCli.groovy:45-57`) gated only by 
a marker file in the current directory (`:115-128`).
   - `FeatureDiffer` exists in core but is exposed only through the web 
`DiffController`.
   - Inside a project the shell dispatches everything through the profile 
(`GrailsCli.groovy:385`); the profile jars come from the Gradle `profile` 
configuration via the `GrailsClasspath` tooling model 
(`GrailsClasspathToolingModelBuilder.groovy:43-60`); Aether is used only 
outside a project.
   - The shell writes nothing into a project except under `build/`; no 
`.grails/` project directory exists anywhere in the repository.
   - `grails.codegen.defaultPackage` has runtime readers; `grails.profile` 
becomes an active Spring profile with a `web` default.
   - `-cli` companions are discovered from the `Grails-Cli-Artifact` manifest 
attribute and `META-INF/grails-cli.factories` without loading classes 
(`GrailsCliGradlePlugin.groovy:203-307,451-476`); that is the model the pack 
fetcher follows.
   - The IntelliJ plugin (`apache/grails-intellij-plugin`, `main`, 2026-10-11) 
starts its own JVM with main class `org.grails.cli.GrailsCli` on the resolved 
`grails-shell-cli` artifact (`Grails3GradleCommandExecutor.java:54`, 
`GrailsModuleModelBuilderImpl.java:54-78`), hard-codes 34 shell command names 
(`Grails3CommandProvider.java:31-70`), runs `run-app` the same way 
(`GrailsRunConfiguration.java:70`), detects projects by `grails-app` 
(`GrailsApplicationManager.java:167`), and creates projects through the hosted 
Forge API (`GrailsForgeModuleBuilder.kt:67-221`).
   - The plugin portal's data source is `apache/grails-plugins-metadata`: one 
YAML per plugin under `grails-plugins/<group path>/<artifactId>.yml` (`name`, 
`desc`, `coords`, `owner`, `vcs`, `docs`, `maven-repo`, `labels`, `licenses`, 
`versions`, optional `deprecated`), `index-plugins.groovy` run every eight 
hours by `.github/workflows/updateIndex.yml`, which downloads each new 
version's jar by exact URL and reads `META-INF/grails-plugin.xml`, and 
`grails-plugins-index.json` consumed by `apache/grails-static-website` 
(`buildSrc/.../PluginsTask.groovy:67`). 256 entries on 2026-10-13.
   
   ## Change log
   
   | Revision | Date | Change |
   | --- | --- | --- |
   | 9 | 2026-10-13 | Release policy applied: nothing is removed in a minor. 
Section 5 rewritten as "Delivery": steps 1 to 4 are additions and 
same-behaviour replacements that may ship in 8.1.x (open, decision 6); every 
removal, including the `profile` configuration, `grails.profile` injection, 
dispatcher, Spring Boot CLI port and `org.grails.cli.GrailsCli`, lands together 
in 9.0.x; the no-op configuration shim and the `GrailsCli` façade are gone. 
Section 1, 2.1, 2.4, 2.5, section 3 rows, decisions 5, 6, 9 and 11, and testing 
updated. |
   | 8 | 2026-10-13 | "Build-file contributions" in 2.2: typed, merged channels 
(`build.properties`, `extensions`, `blocks`, `imports`, `settings`) with merge 
rules and deterministic ordering; the central template's feature-name branches 
and the dropped `Dependency.extension` bug move into phase 1; descriptor 
example, mapping table and decision 12 updated. |
   | 7 | 2026-10-13 | IntelliJ plugin is `apache/grails-intellij-plugin`: new 
"IDE integration" under 2.5 with how it couples to the shell today and what the 
unified CLI must provide (stable entry point and artifact, `help --json` 
catalog, `.grails/project.yml` detection, `bootRun`). Section 3 row, phase 4, 
decision 11 and appendix B updated. |
   | 6 | 2026-10-13 | Hosted Forge is official-only; external packs are 
CLI-only. Pack template language switched from Mustache to GSP source compiled 
by the CLI, with the decision history kept in 2.2. The `grails-plugin` Gradle 
plugin automates pack validation, GSP check compilation, packaging and 
`forgePackTest` from `src/main/forge/`. New "Command execution model" under 
2.5. Sections 2.3, 2.7, phases 1, 2 and 6, decisions 1, 2 and 7, and testing 
updated. |
   | 5 | 2026-10-13 | Revision stamp in the header and this change log. |
   | 4 | 2026-10-13 | Section 2.2: "Why not GSP or another dynamic engine", 
with the two ways GSP could be opened to packs, the reasons against, and the 
middle options. |
   | 3 | 2026-10-13 | Section 2.2: "Conditional logic in packs" (`requires`, 
`when`, `flags`, Mustache sections). Section 2.3: Forge reads 
`grails-plugins-index.json` straight from the repository's `main` branch; the 
mirror step was dropped from phase 6 and decision 3. |
   | 2 | 2026-10-13 | Section 2.3 rebuilt on `apache/grails-plugins-metadata` 
as the catalog: YAML schema, indexer behaviour, index statistics, pack 
detection in the indexer, no second catalog. Section 1, section 3, decision 3, 
phases 2 and 6, appendix B updated to match. |
   | 1 | 2026-10-13 | First draft: verdict, architecture, gap analysis, removal 
inventory, phases, decisions, testing, appendices. |
   
   Plan for apache/grails-core issue #16348, drafted against PR #16551. 
Related: #15301, #14081, #14944, #14932.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to