---
name: fluent-module-scaffold
description: Scaffold a new Fluent Commerce extension module. Generates Maven project structure, module.json, build scripts, initial rule classes, test skeletons, and .gitignore. Triggers on "scaffold module", "new module", "create module", "initialize module".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: <module-name> [--entity-types ORDER,FULFILMENT] [--rules RuleName1,RuleName2] [--account-prefix ACCT]
cursor-tier: manual
planning-gate: mandatory
---

# Module Scaffolder

Generate a complete, buildable Fluent Commerce extension module skeleton from a module name, entity scope, and optional rule list. The output is a directory under `accounts/<PROFILE>/SOURCE/backend/` that passes `/fluent-module-validate` and `/fluent-build` on first run.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-module-scaffold.jsonl` exists
2. If yes, read last 20 records
3. Filter to: FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Pre-Check: New Module or Extend Existing?

**ALWAYS run this decision tree before scaffolding. Do NOT skip.**

```
User asks: "I need a module for X" / "Build me a module" / "Create rules for X"
│
├── 1. Discover existing modules in workspace
│   Search: accounts/<PROFILE>/SOURCE/backend/*/resources/module.json
│   Also check: accounts/<PROFILE>/analysis/code/source-map.json
│   List found modules with their rules and entity types
│
├── 2. Check deployed modules in live environment
│   Use MCP tool: plugin.list (no filter — get all custom rules)
│   Look for <ACCOUNT>.* rules (not FLUENTRETAIL.*)
│   Cross-reference with local SOURCE/backend/ to find modules with source code available
│
├── 3. Evaluate: does the new functionality fit an existing module?
│   ├── YES — functionality belongs in an existing module's domain
│   │   ├── Source code in SOURCE/backend/? → Use /fluent-rule-scaffold to add rules
│   │   ├── No source code? → Ask user to clone the repo first
│   │   │   "Module <name> is deployed but source not found in accounts/<PROFILE>/SOURCE/backend/"
│   │   │   "Please clone the repo: git clone <url> accounts/<PROFILE>/SOURCE/backend/<repo>"
│   │   │   Then use /fluent-custom-code to analyze, then /fluent-rule-scaffold
│   │   └── STOP — do NOT scaffold a new module
│   │
│   ├── NO — genuinely new domain, no existing module covers it
│   │   └── Proceed with scaffolding below
│   │
│   └── UNCLEAR — could go either way
│       └── Ask user: "Should I add rules to existing module <X> or create a new module?"
```

### Source Code Availability Check

Before extending an existing module, verify the source is accessible:

| Source State | Action |
|---|---|
| `accounts/<PROFILE>/SOURCE/backend/<repo>/` exists with `pom.xml` + `module.json` | Ready — use `/fluent-rule-scaffold` |
| Module deployed but no source in `SOURCE/backend/` | Ask user to clone repo into `SOURCE/backend/` |
| Source exists but no `source-map.json` analysis | Run `/fluent-custom-code` first to map packages, base classes, conventions |
| Source exists with stale `source-map.json` | Re-run `/fluent-custom-code` to refresh |

## Planning Gate

### Pre-flight Chain (Three-Level)

Before scaffolding any module, run the three-level pre-flight check:

1. **Spec check:** If `accounts/<PROFILE>/features/<slug>/status.json` exists for this work:
   - Check `spec: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PREREQ_MISSING — Run /fluent-use-case-discover first`
   - Check `plan: "APPROVED"` — If missing, BLOCK: `→ BLOCKED: PLAN_REQUIRED — Run /fluent-feature-plan first`
   - If both approved, skip to implementation referencing the plan's §8 (Detailed Design) section
2. **Multi-artifact work** (module + rules + workflows + settings): STOP. Invoke `/fluent-feature-plan` first — this skill cannot be used for multi-artifact work without a feature plan
3. **Standalone new-module work** (no feature context, no multi-artifact):
   - Check `accounts/<PROFILE>/tasks/` for a task plan with `Status: APPROVED`
   - If approved task plan found, skip to implementation
   - If no plan, continue to the Planning Gate below to write a plan for this skill

**After the pre-check determines a NEW module is needed, write a plan using the template from `PLAN_TEMPLATE.md` in the `fluent-feature-plan` skill.** Every table row must carry a Source column (NEW/EXISTING/MODIFIED/REUSED/OOTB).

**Module-scaffold specific emphasis — ensure these are covered:**

1. **Business Context (Section 1)** — why a new module (not extending existing), decision rationale from pre-check
2. **Entity relationship diagram (Section 3)** — Mermaid diagram showing entity types the module operates on and their edges. Validate syntax per `knowledge/mermaid-syntax-rules.md`
3. **Cross-entity flow (Section 3.2)** — Mermaid `sequenceDiagram` showing how the module's rules will be triggered and what events/mutations they produce. Validate syntax per `knowledge/mermaid-syntax-rules.md`
4. **Impacted workflows (Section 4.1)** — which workflow rulesets will use these rules (existing or new)
5. **Rulesets & rules (Section 4.3)** — initial rules table with entity type, trigger event, OOTB/custom, inline/scheduled, description, parameters
6. **Settings (Section 4.4)** — settings the rules will need, with context and expected format
7. **GraphQL operations (Section 4.7)** — queries and mutations the rules will execute
8. **Detailed Design (Section 5)** — Maven directory structure, SDK version, parent POM, dependencies, package naming convention

**Write the plan to:** `accounts/<PROFILE>/tasks/<YYYY-MM-DD>-module-scaffold-<slug>.md`. Set `Status: PENDING`.

Present the full plan content to the user and wait for approval before generating any files. On approval, update the file to `Status: APPROVED`. If the user says "just do it", proceed directly (still write the file for audit trail).

## Handoff Protocol

### Signals emitted by this skill

| Signal | Condition | Example |
|--------|-----------|---------|
| `-> READY: <path>` | Module directory scaffolded | `-> READY: accounts/MY_PROFILE/SOURCE/backend/fc-module-curbside/` |
| `-> NEXT: /fluent-<skill>` | Ready for rule scaffolding | `-> NEXT: /fluent-rule-scaffold` |
| `-> BLOCKED: <reason>` | Cannot proceed | `-> BLOCKED: PLAN_REQUIRED — Write a plan via /fluent-feature-plan` |
| `-> SKIP: <reason>` | Existing module covers the domain | `-> SKIP: Extend existing module fc-module-my-extensions. Use /fluent-rule-scaffold` |

### Error codes

| Code | Condition | Recovery |
|------|-----------|----------|
| `PLAN_REQUIRED` | Scaffolding attempted without approved plan | Run `/fluent-feature-plan` first |
| `VALIDATION_FAILED` | Module name conflicts or invalid entity types | Fix input parameters |

## When to Use

- Creating a brand-new Fluent Commerce extension module from scratch
- Starting a greenfield project that needs a complete Maven + module.json + build scripts structure
- Bootstrapping a module for a new account or retailer where no custom code exists yet
- Generating an empty module shell to be populated incrementally with `/fluent-rule-scaffold`

## Ownership Boundary

This skill owns:

- Creating the complete module directory structure
- Generating the parent POM and all submodule POMs (types, util, rules)
- Generating `module.json` with correct Fluent Commerce manifest format
- Generating initial rule classes and test skeletons (if `--rules` specified)
- Generating build scripts (bash `.sh` and PowerShell `.ps1`)
- Generating `.gitignore`
- Account prefix detection for rule registration

This skill does **not** own:

- Adding rules to an existing module --> `/fluent-rule-scaffold`
- Building and packaging --> `/fluent-build`
- Validating module structure --> `/fluent-module-validate`
- Deploying modules --> `/fluent-module-deploy`
- Analyzing existing custom code --> `/fluent-custom-code`

## Inputs

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `module-name` | Yes | -- | Module identifier (e.g., `acme-returns`). Used for directory names, POM artifactId, module.json name. Must be lowercase alphanumeric with hyphens only. |
| `--entity-types` | No | `ORDER` | Comma-separated entity types the module's rules will operate on. Valid: `ORDER`, `FULFILMENT`, `FULFILMENT_OPTIONS`, `ARTICLE`, `CONSIGNMENT`, `WAVE`, `LOCATION`, `PRODUCT`, `VIRTUAL_CATALOGUE`, `INVENTORY_POSITION`, `BILLING_ACCOUNT`, `RETURN_ORDER` |
| `--rules` | No | -- | Comma-separated rule class names to scaffold (each gets a Java class + test). Names must be PascalCase. |
| `--account-prefix` | No | Auto-detect from profile | Account context prefix for rule registration in module.json (e.g., `MY_PROFILE`). Used as the OSGi symbolic name prefix. |
| `--group-id` | No | `com.fluentcommerce` | Maven groupId for all POMs |
| `--initial-version` | No | `1.0.0-SNAPSHOT` | Initial POM and module.json version |
| `--profile` | No | Active profile | Fluent CLI profile for account prefix detection and module path resolution |

## Pre-Check: Module Already Exists?

Before creating anything, check if the target directory already exists:

```
accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<module-name>/
```

If it exists, **abort** with message:
```
Module directory already exists: accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<module-name>/
Use /fluent-rule-scaffold to add rules to an existing module, or choose a different module name.
```

Also check for name collisions against deployed modules:

```
# Query deployed modules via Fluent CLI
fluent module list -p <PROFILE>
```

If a module with the same name is already deployed, warn (but do not abort -- the user may be re-creating local source for an existing deployed module).

## Account Prefix Detection

The account prefix is used in `module.json` rule registration keys and the OSGi symbolic name. Detection order:

```
1. If --account-prefix provided, use it directly

2. Else query registered rules via MCP:
   plugin.list (compact: true)
   → Parse first custom rule key: "<ACCOUNT>.<context>.<RuleName>"
   → Extract <ACCOUNT> prefix (the part before the first dot that is NOT "FLUENTRETAIL")

3. Else derive from PROFILE name:
   MY_PROFILE → MY_PROFILE
   ACME_PROD  → ACME_PROD
   (uppercase the profile name)

4. Fallback: "UNKNOWN"
   → Warn user: "Could not detect account prefix. Update module.json manually."
```

### Module Alias Derivation

The `<module-alias>` is derived from `<module-name>` by removing the common prefix pattern. It is used for submodule directory names and artifact suffixes:

```
module-name:  acme-returns
module-alias: acme-returns

module-name:  my-extensions
module-alias: my-extensions
```

The alias is the same as the module name. It appears in paths like `plugins/rules/<module-alias>/`.

### OSGi Symbolic Name

Derived from the account prefix and module alias with special characters removed:

```
account-prefix: MY_PROFILE
module-alias:   my-returns
osgi-name:      _.myreturns

account-prefix: ACME_PROD
module-alias:   acme-extensions
osgi-name:      _.acmeextensions
```

Pattern: `_.<alias-with-hyphens-removed>`

## Generated Directory Structure

```
accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<module-name>/
+-- plugins/
|   +-- pom.xml                                         # Parent POM (multi-module, packaging=pom)
|   +-- types/
|   |   +-- pom.xml                                     # Types aggregator POM (packaging=pom)
|   |   +-- <module-alias>/
|   |       +-- pom.xml                                 # Types implementation POM
|   |       +-- src/
|   |           +-- main/java/com/fluentcommerce/types/  # (empty, ready for DTOs)
|   +-- util/
|   |   +-- pom.xml                                     # Util aggregator POM (packaging=pom)
|   |   +-- <module-alias>/
|   |       +-- pom.xml                                 # Util implementation POM
|   |       +-- src/
|   |           +-- main/java/com/fluentcommerce/util/   # (empty, ready for helpers)
|   +-- rules/
|       +-- pom.xml                                     # Rules aggregator POM (packaging=pom)
|       +-- <module-alias>/
|           +-- pom.xml                                 # Rules implementation POM (OSGi bundle)
|           +-- src/
|               +-- main/java/com/fluentcommerce/rule/
|               |   +-- <package>/                      # Sub-package per entity type
|               |       +-- <RuleName1>.java            # (if --rules specified)
|               |       +-- <RuleName2>.java
|               +-- test/java/com/fluentcommerce/rule/
|                   +-- <package>/
|                       +-- <RuleName1>Test.java        # (if --rules specified)
|                       +-- <RuleName2>Test.java
+-- resources/
|   +-- module.json                                     # Fluent Commerce module manifest
|   +-- settings/                                       # (empty, ready for default settings JSON)
+-- global/                                             # (empty, for GraphQL schema.json)
+-- scripts/
|   +-- build-module.sh                                 # Bash build + packaging script
|   +-- build-module.ps1                                # PowerShell build + packaging script
+-- dist/                                               # (empty, output dir for ZIP artifacts)
+-- .gitignore
```

### POM Hierarchy

The Maven project uses a 3-level POM hierarchy matching real Fluent Commerce modules:

```
plugins/pom.xml (parent, packaging=pom)
  +-- types/pom.xml (aggregator, packaging=pom)
  |     +-- types/<alias>/pom.xml (implementation)
  +-- util/pom.xml (aggregator, packaging=pom)
  |     +-- util/<alias>/pom.xml (implementation)
  +-- rules/pom.xml (aggregator, packaging=pom)
        +-- rules/<alias>/pom.xml (implementation, OSGi bundle)
```

The rules implementation POM is the only one that produces a deployable JAR. It depends on the types and util artifacts.

## File Templates

### Parent POM (`plugins/pom.xml`)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>${GROUP_ID}</groupId>
    <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
    <version>${INITIAL_VERSION}</version>
    <packaging>pom</packaging>

    <name>FC Module ${MODULE_TITLE} - Plugins Parent</name>
    <description>Parent POM for fc-module-${MODULE_NAME} plugin builds</description>

    <modules>
        <module>types</module>
        <module>util</module>
        <module>rules</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <apollo-client-maven-plugin.version>1.0.0.15</apollo-client-maven-plugin.version>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>com.fluentcommerce</groupId>
                    <artifactId>apollo-client-maven-plugin</artifactId>
                    <version>${apollo-client-maven-plugin.version}</version>
                    <configuration>
                        <basePackage>com.fluentcommerce.graphql</basePackage>
                        <introspectionFile>${project.basedir}/../../../global/schema.json</introspectionFile>
                    </configuration>
                    <executions>
                        <execution>
                            <id>generate-classes</id>
                            <goals>
                                <goal>generate</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>replace-customtypes</id>
                            <goals>
                                <goal>replace</goal>
                            </goals>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>com.squareup.okio</groupId>
                            <artifactId>okio</artifactId>
                            <version>1.17.6</version>
                        </dependency>
                        <dependency>
                            <groupId>com.squareup.okhttp3</groupId>
                            <artifactId>okhttp</artifactId>
                            <version>3.14.9</version>
                        </dependency>
                    </dependencies>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>
```

### Types Aggregator POM (`plugins/types/pom.xml`)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>${GROUP_ID}</groupId>
        <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
        <version>${INITIAL_VERSION}</version>
    </parent>

    <artifactId>fc-module-${MODULE_NAME}-types</artifactId>
    <packaging>pom</packaging>

    <name>FC Module ${MODULE_TITLE} - Types</name>
    <description>Parent POM for fc-module-${MODULE_NAME} types plugins</description>

    <modules>
        <module>${MODULE_ALIAS}</module>
    </modules>

</project>
```

### Types Implementation POM (`plugins/types/<module-alias>/pom.xml`)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>${GROUP_ID}</groupId>
        <artifactId>fc-module-${MODULE_NAME}-types</artifactId>
        <version>${INITIAL_VERSION}</version>
    </parent>

    <artifactId>fc-types-${MODULE_NAME}</artifactId>

    <name>FC Module ${MODULE_TITLE} - Types Implementation</name>
    <description>DTOs and data types for ${MODULE_TITLE} module</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <lombok.version>1.18.30</lombok.version>
        <jackson.version>2.10.0</jackson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <target>1.8</target>
                    <source>1.8</source>
                    <fork>true</fork>
                    <compilerArgs>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
                    </compilerArgs>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

### Util Aggregator POM (`plugins/util/pom.xml`)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>${GROUP_ID}</groupId>
        <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
        <version>${INITIAL_VERSION}</version>
    </parent>

    <artifactId>fc-module-${MODULE_NAME}-util</artifactId>
    <packaging>pom</packaging>

    <name>FC Module ${MODULE_TITLE} - Util</name>
    <description>Parent POM for fc-module-${MODULE_NAME} util plugins</description>

    <repositories>
        <repository>
            <id>maven-s3-public-repo</id>
            <name>FluentCommerce Public Repo</name>
            <url>https://public.maven.dev.fluentcommerce.com/releases</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>maven-s3-public-repo</id>
            <name>FluentCommerce Public Repo</name>
            <url>https://public.maven.dev.fluentcommerce.com/releases</url>
        </pluginRepository>
    </pluginRepositories>

    <modules>
        <module>${MODULE_ALIAS}</module>
    </modules>

</project>
```

### Util Implementation POM (`plugins/util/<module-alias>/pom.xml`)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>${GROUP_ID}</groupId>
        <artifactId>fc-module-${MODULE_NAME}-util</artifactId>
        <version>${INITIAL_VERSION}</version>
    </parent>

    <artifactId>fc-util-${MODULE_NAME}</artifactId>

    <name>FC Module ${MODULE_TITLE} - Utilities Implementation</name>
    <description>Utilities and helper classes for ${MODULE_TITLE} module</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <rubix-plugin-base.version>1.2.0.679</rubix-plugin-base.version>
        <fluent-api-client.version>1.2.0.118</fluent-api-client.version>
        <fluent-apollo-client-plugin.version>1.0.0.15</fluent-apollo-client-plugin.version>
        <apollo.graphql.version>0.4.2</apollo.graphql.version>
        <lombok.version>1.18.30</lombok.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <jackson.version>2.10.0</jackson.version>
        <google.guava.version>19.0</google.guava.version>
        <commons.collections4.version>4.3</commons.collections4.version>
    </properties>

    <dependencies>
        <!-- Module Dependencies -->
        <dependency>
            <groupId>${GROUP_ID}</groupId>
            <artifactId>fc-types-${MODULE_NAME}</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- Fluent Commerce dependencies -->
        <dependency>
            <groupId>com.fluentcommerce</groupId>
            <artifactId>rubix-plugin-sdk</artifactId>
            <version>${rubix-plugin-base.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fluentcommerce</groupId>
            <artifactId>fluent-api-client</artifactId>
            <version>${fluent-api-client.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- 3rd party libraries -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${google.guava.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>${commons.collections4.version}</version>
        </dependency>

        <!-- Apollo GraphQL -->
        <dependency>
            <groupId>com.apollographql.apollo</groupId>
            <artifactId>apollo-runtime</artifactId>
            <version>${apollo.graphql.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-jsonSchema</artifactId>
            <version>${jackson.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>com.fluentcommerce</groupId>
                <artifactId>apollo-client-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <target>1.8</target>
                    <source>1.8</source>
                    <fork>true</fork>
                    <compilerArgs>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
                    </compilerArgs>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

### Rules Aggregator POM (`plugins/rules/pom.xml`)

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>${GROUP_ID}</groupId>
        <artifactId>fc-module-${MODULE_NAME}-plugins</artifactId>
        <version>${INITIAL_VERSION}</version>
    </parent>

    <artifactId>fc-module-${MODULE_NAME}-rules</artifactId>
    <packaging>pom</packaging>

    <name>FC Module ${MODULE_TITLE} - Rules</name>
    <description>Parent POM for fc-module-${MODULE_NAME} rules plugins</description>

    <modules>
        <module>${MODULE_ALIAS}</module>
    </modules>

</project>
```

### Rules Implementation POM (`plugins/rules/<module-alias>/pom.xml`)

This is the most important POM -- it produces the deployable OSGi bundle JAR.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>${GROUP_ID}</groupId>
        <artifactId>fc-module-${MODULE_NAME}-rules</artifactId>
        <version>${INITIAL_VERSION}</version>
    </parent>

    <artifactId>fc-module-${MODULE_NAME}</artifactId>

    <pluginRepositories>
        <pluginRepository>
            <id>maven-s3-public-repo</id>
            <name>FluentCommerce Public Repo</name>
            <url>https://public.maven.dev.fluentcommerce.com/releases</url>
        </pluginRepository>
    </pluginRepositories>

    <repositories>
        <repository>
            <id>maven-s3-public-repo</id>
            <name>FluentCommerce Public Repo</name>
            <url>https://public.maven.dev.fluentcommerce.com/releases</url>
        </repository>
    </repositories>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <rubix.artifact>${project.groupId}.${project.artifactId}</rubix.artifact>
        <rubix.artifact.vendor>Fluent Commerce</rubix.artifact.vendor>
        <rubix.osgi.symbolic.name>${OSGI_SYMBOLIC_NAME}</rubix.osgi.symbolic.name>
        <rubix.osgi.activator/>
        <rubix.osgi.export.package/>
        <rubix.osgi.export.service/>
        <rubix-plugin-base.version>1.2.0.679</rubix-plugin-base.version>
        <fluent-api-client.version>1.2.0.118</fluent-api-client.version>
        <fluent-apollo-client-plugin.version>1.0.0.15</fluent-apollo-client-plugin.version>
        <slf4j.version>1.7.22</slf4j.version>

        <!-- 3rd party dependency versions -->
        <apollo.graphql.version>0.4.2</apollo.graphql.version>
        <commons.collections4.version>4.3</commons.collections4.version>
        <lombok.version>1.18.30</lombok.version>
        <commons-lang3.version>3.4</commons-lang3.version>
        <jackson.version>2.10.0</jackson.version>
        <google.guava.version>19.0</google.guava.version>
        <okhttp.version>3.14.9</okhttp.version>

        <!-- test dependency versions -->
        <junit-jupiter.version>5.11.0</junit-jupiter.version>
        <mockito.version>4.11.0</mockito.version>

        <!-- maven plugin versions -->
        <lombok.maven.plugin.version>1.16.8.0</lombok.maven.plugin.version>
        <maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
        <maven.dependency.plugin.version>2.8</maven.dependency.plugin.version>
        <maven.resources.plugin.version>2.6</maven.resources.plugin.version>
        <maven.bundle.plugin.version>5.1.9</maven.bundle.plugin.version>
    </properties>

    <dependencies>

        <!-- Module Dependencies -->
        <dependency>
            <groupId>${GROUP_ID}</groupId>
            <artifactId>fc-types-${MODULE_NAME}</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>${GROUP_ID}</groupId>
            <artifactId>fc-util-${MODULE_NAME}</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- INCLUDED DEPENDENCIES -->
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-jsonSchema</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!-- PROVIDED DEPENDENCIES -->
        <dependency>
            <groupId>com.fluentcommerce</groupId>
            <artifactId>rubix-plugin-sdk</artifactId>
            <version>${rubix-plugin-base.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>${slf4j.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.fluentcommerce</groupId>
            <artifactId>fluent-api-client</artifactId>
            <version>${fluent-api-client.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${google.guava.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.apollographql.apollo</groupId>
            <artifactId>apollo-api</artifactId>
            <version>${apollo.graphql.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- 3rd party libs -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>${commons.collections4.version}</version>
        </dependency>

        <!-- TEST -->
        <dependency>
            <groupId>com.squareup.okio</groupId>
            <artifactId>okio</artifactId>
            <version>1.17.6</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.fluentcommerce</groupId>
            <artifactId>rubix-test-mockery</artifactId>
            <version>${rubix-plugin-base.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit-jupiter.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit-jupiter.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit-jupiter.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-maven-plugin</artifactId>
                <version>${lombok.maven.plugin.version}</version>
                <executions>
                    <execution>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>delombok</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.plugin.version}</version>
                <configuration>
                    <target>1.8</target>
                    <source>1.8</source>
                    <fork>true</fork>
                    <compilerArgs>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
                    </compilerArgs>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>${maven.dependency.plugin.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>${maven.resources.plugin.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>${maven.bundle.plugin.version}</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>cleanVersions</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>bundle-manifest</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>manifest</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>package-bundle</id>
                        <phase>package</phase>
                        <goals>
                            <goal>bundle</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <instructions>
                        <Bundle-Name>${project.name}</Bundle-Name>
                        <Bundle-SymbolicName>${rubix.osgi.symbolic.name}</Bundle-SymbolicName>
                        <Bundle-Activator>${rubix.osgi.activator}</Bundle-Activator>
                        <Bundle-Vendor>${rubix.artifact.vendor}</Bundle-Vendor>
                        <Bundle-Version>${project.version}</Bundle-Version>
                        <Export-Package>${rubix.osgi.export.package}</Export-Package>
                        <Export-Service>${rubix.osgi.export.service}</Export-Service>
                        <Import-Package>!android.*,!org.conscrypt,!org.openjsse.*,!okhttp3.*,!okio.*,*</Import-Package>
                        <Embed-Dependency>*;scope=compile|runtime;inline=true</Embed-Dependency>
                        <Bundle-ClassPath>.,{maven-dependencies}</Bundle-ClassPath>
                        <Embed-Transitive>true</Embed-Transitive>
                        <Rubix-Rules>$(classes;CONCRETE;ANNOTATION;com.fluentretail.rubix.rule.meta.RuleInfo)</Rubix-Rules>
                        <Rubix-Account>_</Rubix-Account>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

### Module Manifest (`resources/module.json`)

```json
{
    "_schema": "1.0.0",
    "name": "fluent-commerce/fc-module-${MODULE_NAME}",
    "version": "${INITIAL_VERSION}",
    "title": "Fluent Commerce - ${MODULE_TITLE} Module",
    "description": "${MODULE_TITLE} extension module providing custom rules and utilities for the Rubix platform",
    "authors": [
        {
            "name": "Fluent Commerce",
            "email": "support@fluentcommerce.com",
            "web": "https://fluentcommerce.com"
        }
    ],
    "dependencies": [
        {
            "name": "fluent-commerce/core",
            "type": "module",
            "version": "2.x.x"
        }
    ],
    "contracts": [],
    "rules": [
        "${RULE_NAME_1}",
        "${RULE_NAME_2}"
    ],
    "compatibility": {
        "minRubixVersion": "1.2.0",
        "rubixPluginSdk": "1.2.0.679",
        "fluentApiClient": "1.2.0.118"
    }
}
```

**Important:** If no `--rules` are provided, the `"rules"` array must be empty `[]`. Each entry is a simple string matching the `@RuleInfo(name = "...")` annotation value. Do NOT include package prefixes or account prefixes in this array.

### Config Prefix System

The Fluent CLI uses a prefix system in `module.config.json` to scope variable values to specific contexts. Understanding this system is essential for extension modules that include workflows and settings.

#### How Config Prefixes Work

Config keys follow the pattern: `<prefix>:<variable.name>`

The CLI filters keys based on the asset context being processed and selects the most specific match.

#### Available Prefixes

| Prefix | Applies To | Specificity | Example |
|--------|-----------|-------------|---------|
| `default:` | All assets (workflows, settings, data) | 0 (lowest, fallback) | `"default:network.ref": "NET_DEFAULT"` |
| `workflow:` | Workflows only | 0 | `"workflow:notification.enabled": "true"` |
| `workflow:<type>:` | Specific entity type workflows | 1 | `"workflow:order:network.ref": "NET_ORDER"` |
| `workflow:<type>:<subtype>:` | Specific entity type + subtype | 2 (highest) | `"workflow:order:hd:carrier.ref": "HD_CARRIER"` |
| `setting:` | Settings only | 0 | `"setting:api.timeout": "30000"` |

**Specificity rules:** More colons after the prefix = higher specificity = wins. For a workflow `ORDER::HD`:
- `workflow:order:hd:network.ref` (2 levels) beats
- `workflow:order:network.ref` (1 level) beats
- `default:network.ref` (0 levels)

#### Context-Based Filtering

| Asset Folder | Recognized Prefixes |
|-------------|---------------------|
| `assets/workflows/` | `default:`, `workflow:`, `workflow:<type>:`, `workflow:<type>:<subtype>:` |
| `assets/settings/` | `default:`, `setting:` |
| All other asset folders | `default:` only |

#### Built-in Auto-Injected Variables

These variables are available in all asset files without defining them in `module.config.json`:
- `account.id` -- Fluent account ID
- `retailer.id` -- Target retailer numeric ID
- `retailer.ref` -- Target retailer reference string
- `retailer.name` -- Target retailer name

Note: `account.host` is NOT auto-injected. If needed, define it explicitly: `"default:account.host": "acme.sandbox.api.fluentretail.com"`.

#### Variable Syntax

Use `[[variable]]` tokens in any JSON or CSV asset file:

```json
{
  "ref": "NET_HD_[[retailer.id]]",
  "networkRef": "[[network.ref]]",
  "retailerId": [[retailer.id]]
}
```

**Important:** Unresolved `[[tokens]]` are left as-is -- the CLI does not error on missing substitutions. Always scan for unreplaced tokens before deploying:

```bash
rg '\[\[.*\]\]' resources/assets/
```

#### Example module.config.json

When the scaffold creates `resources/module.config.json`, it generates an empty object `{}`. Populate it with prefixed keys as needed:

```json
{
  "default:carrier.ref": "CARRIER_STD",
  "default:network.ref": "NET_DEFAULT",
  "workflow:order:hd:network.ref": "NET_HD",
  "workflow:order:hd:carrier.ref": "HD_CARRIER",
  "workflow:order:cc:network.ref": "NET_CC",
  "setting:webhook.order.url": "https://example.com/webhook/order"
}
```

The build scripts (`scripts/build-module.sh` and `scripts/build-module.ps1`) already handle copying `module.config.json` into the distribution ZIP alongside `module.json`. When deploying with `fluent module install`, the CLI reads this file and substitutes `[[variable]]` tokens in all asset files before uploading.

### Rule Class Template

```java
package com.fluentcommerce.rule.${PACKAGE};

import com.fluentcommerce.common.BaseRule;
import com.fluentcommerce.common.ContextWrapper;
import com.fluentretail.rubix.rule.meta.ParamString;
import com.fluentretail.rubix.rule.meta.RuleInfo;
import lombok.extern.slf4j.Slf4j;

/**
 * ${RULE_DESCRIPTION}
 */
@RuleInfo(
    name = "${RULE_NAME}",
    description = "${RULE_DESCRIPTION}"
)
@Slf4j
public class ${RULE_NAME} extends BaseRule {

    private static final String CLASS_NAME = ${RULE_NAME}.class.getSimpleName();

    @Override
    public void run(ContextWrapper context) {
        String accountId = context.getEvent().getAccountId();

        log.info("[{}] [{}] - Processing event: {}", accountId, CLASS_NAME,
                context.getEvent().getName());
        context.addLog("Processing " + CLASS_NAME);

        try {
            // === Rule logic here ===

            context.addLog(CLASS_NAME + " completed successfully");
        } catch (Exception e) {
            log.error("[{}] [{}] - Error: {}", accountId, CLASS_NAME, e.getMessage(), e);
            context.addLog("Error in " + CLASS_NAME + ": " + e.getMessage());
        }
    }
}
```

**Fallback rule class (when no BaseRule/ContextWrapper exists in the module):**

If this is a brand-new module without an existing `BaseRule` class, use the direct Rubix SDK API instead:

```java
package com.fluentcommerce.rule.${PACKAGE};

import com.fluentretail.rubix.event.Event;
import com.fluentretail.rubix.rule.meta.RuleInfo;
import com.fluentretail.rubix.v2.context.Context;
import com.fluentretail.rubix.v2.rule.Rule;
import lombok.extern.slf4j.Slf4j;

/**
 * ${RULE_DESCRIPTION}
 */
@RuleInfo(
    name = "${RULE_NAME}",
    description = "${RULE_DESCRIPTION}"
)
@Slf4j
public class ${RULE_NAME} implements Rule {

    private static final String CLASS_NAME = ${RULE_NAME}.class.getSimpleName();

    @Override
    public void run(Context context) {
        Event event = context.getEvent();
        String accountId = event.getAccountId();

        log.info("[{}] [{}] - Processing event: {} for entity {}",
                accountId, CLASS_NAME, event.getName(), event.getEntityRef());

        try {
            // === Rule logic here ===

            log.info("[{}] [{}] - Completed successfully", accountId, CLASS_NAME);
        } catch (Exception e) {
            log.error("[{}] [{}] - Error: {}", accountId, CLASS_NAME, e.getMessage(), e);
        }
    }
}
```

**Which template to use:** For new modules with no shared `BaseRule`, use the fallback (`implements Rule`). For modules that already have a `BaseRule` and `ContextWrapper` in their `common` package (or in a util dependency), use the primary template (`extends BaseRule`). The scaffolder should check for the existence of `BaseRule.java` under `plugins/util/` or `plugins/rules/` and choose accordingly. Default for greenfield: use the fallback.

### Test Class Template

```java
package com.fluentcommerce.rule.${PACKAGE};

import com.fluentretail.rubix.event.Event;
import com.fluentretail.rubix.rule.meta.RuleInfo;
import com.fluentretail.rubix.v2.context.Context;
import com.fluentretail.rubix.v2.action.Action;
import com.fluentretail.rubix.v2.action.MutationAction;
import com.fluentretail.rubix.v2.action.EventAction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class ${RULE_NAME}Test {

    private ${RULE_NAME} rule;

    @Mock private Context context;
    @Mock private Event event;
    @Mock private Action action;
    @Mock private MutationAction mutationAction;
    @Mock private EventAction eventAction;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        rule = new ${RULE_NAME}();

        when(context.getEvent()).thenReturn(event);
        when(context.action()).thenReturn(action);
        when(action.mutation()).thenReturn(mutationAction);
        when(action.eventAction()).thenReturn(eventAction);
        when(event.getAccountId()).thenReturn("TEST_ACCOUNT");
        when(event.getName()).thenReturn("TestEvent");
        when(event.getEntityRef()).thenReturn("TEST-001");
    }

    @Test
    @DisplayName("Rule has correct @RuleInfo annotation")
    void hasRuleInfo() {
        RuleInfo info = ${RULE_NAME}.class.getAnnotation(RuleInfo.class);
        assertNotNull(info);
        assertEquals("${RULE_NAME}", info.name());
    }

    @Test
    @DisplayName("Processes event successfully")
    void processesSuccessfully() {
        // Should not throw
        assertDoesNotThrow(() -> rule.run(context));
    }

    @Test
    @DisplayName("Handles exception gracefully")
    void handlesExceptionGracefully() {
        when(event.getAccountId()).thenThrow(new RuntimeException("Simulated failure"));

        // Rule should not propagate the exception
        assertDoesNotThrow(() -> rule.run(context));
    }
}
```

### Build Script - Bash (`scripts/build-module.sh`)

```bash
#!/bin/bash
set -euo pipefail

VERBOSE=false
BUILD_PLUGINS=true
BUILD_MODULE=true

parse_options() {
  while getopts "mpx" opt; do
    case $opt in
    m) BUILD_MODULE=false ;;
    p) BUILD_PLUGINS=false ;;
    x) VERBOSE=true ;;
    *)
      print_usage
      exit 1
      ;;
    esac
  done
}

print_usage() {
  echo "Usage: $0 [-m] [-p] [-x]"
  echo "  -m  Skip module zip creation"
  echo "  -p  Skip plugin build and package existing artifacts"
  echo "  -x  Enable verbose mode"
}

log() {
  if [ "$VERBOSE" = true ]; then
    echo "$@"
  fi
}

require_command() {
  local command_name=$1
  if ! command -v "$command_name" >/dev/null 2>&1; then
    echo "Error: required command '$command_name' was not found in PATH"
    exit 1
  fi
}

clean_folder() {
  local dist_dir=$1
  if [ -d "$dist_dir" ]; then
    log "Directory $dist_dir exist, clearing..."
    rm -rf "${dist_dir:?}/"
  fi
}

copy_assets() {
  local resources_dir=$1
  local dist_dir=$2
  local dist_assets_dir=$3

  log "Resources Dir: '$resources_dir', Destination Dir: '$dist_dir'"

  if [ ! -d "$resources_dir" ]; then
    echo "Error: Resources directory '$resources_dir' does not exist"
    exit 1
  fi

  if [ -e "$resources_dir/module.json" ]; then
    cp "$resources_dir/module.json" "$dist_dir/"
  else
    log "No module.json found, skipping."
  fi

  if [ -e "$resources_dir/module.config.json" ]; then
    cp "$resources_dir/module.config.json" "$dist_dir/"
  else
    log "No module.config.json found, skipping."
  fi

  mkdir -p "$dist_assets_dir"
  for entry in "$resources_dir"/*; do
    [ -e "$entry" ] || continue
    entry_name=$(basename "$entry")
    case "$entry_name" in
      module.json|module.config.json|README.md|CHANGELOG.md|LICENSE.md|workflows-backup)
        log "Skipping non-asset entry: '$entry_name'"
        continue
        ;;
    esac
    cp -r "$entry" "$dist_assets_dir/"
  done
}

copy_jar_files() {
  local plugin_folder=$1
  local destination=$2

  if [ -d "$plugin_folder" ]; then
    log "Plugin folder exists, copying plugin files from '$plugin_folder' to '$destination'"
    mkdir -p "$destination"
  else
    log "No plugins to copy from: '$plugin_folder'"
    return 0
  fi

  for TARGET_FOLDER in "$plugin_folder"/*/target; do
    log "TARGET_FOLDER: '$TARGET_FOLDER'"
    if [ -d "$TARGET_FOLDER" ]; then
      for JAR_FILE in "$TARGET_FOLDER"/*.jar; do
        log "JAR FILE: '$JAR_FILE'"
        if [ -f "$JAR_FILE" ]; then
          echo "Copying $JAR_FILE to $destination"
          cp "$JAR_FILE" "$destination"
        fi
      done
    fi
  done
}

zip_module() {
  local dist_dir=$1
  local module_name=$2

  if [ -f "$dist_dir/$module_name.zip" ]; then
    rm "$dist_dir/$module_name.zip"
  fi

  local zip_file="$module_name.zip"
  (cd "$dist_dir/$module_name" && zip -r "$zip_file" . -q)
  mv "$dist_dir/$module_name/$zip_file" "$dist_dir"

  log "Contents of '$dist_dir' zipped into '$zip_file'."
}

main() {
  parse_options "$@"
  require_command sed
  require_command cp
  require_command basename
  if [ "$BUILD_PLUGINS" = true ]; then
    require_command mvn
  fi
  if [ "$BUILD_MODULE" = true ]; then
    require_command zip
  fi

  SCRIPT_DIR=$(dirname "$(realpath "$0")")
  MODULE_BASE_DIR="$SCRIPT_DIR/.."
  PLUGIN_DIR="$SCRIPT_DIR/../plugins"
  PLUGIN_RULES_DIR="$PLUGIN_DIR/rules"
  DIST_DIR="$MODULE_BASE_DIR/dist"
  RESOURCES_DIR="$MODULE_BASE_DIR/resources"

  log "Script Directory: $SCRIPT_DIR"
  log "Module Directory: $MODULE_BASE_DIR"

  if [ $BUILD_PLUGINS = true ]; then
    log "Building plugins..."
    cd "$PLUGIN_DIR" || exit 1
    mvn clean package || {
      echo "Maven build failed"
      exit 1
    }
  else
    log "Skipping building plugins..."
  fi

  module_name=$(sed -n 's/.*"name": *"\([^"]*\)".*/\1/p' "$RESOURCES_DIR/module.json" | head -n 1)
  sanitized_name=$(echo "$module_name" | sed 's|/|-|g')
  module_version=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$RESOURCES_DIR/module.json" | head -n 1)
  combined_name="$sanitized_name-$module_version"
  module_dist_dir="$DIST_DIR/$combined_name"
  module_dist_assets_dir="$module_dist_dir/assets"
  module_dist_plugins_dir="$module_dist_assets_dir/rules"

  clean_folder "$DIST_DIR"
  mkdir -p "$module_dist_dir"

  for f in CHANGELOG.md README.md LICENSE.md; do
    if [ -e "$MODULE_BASE_DIR/$f" ]; then
      cp "$MODULE_BASE_DIR/$f" "$module_dist_dir/"
    else
      log "No $f found, skipping."
    fi
  done

  copy_assets "$RESOURCES_DIR" "$module_dist_dir" "$module_dist_assets_dir"
  copy_jar_files "$PLUGIN_RULES_DIR" "$module_dist_plugins_dir"

  if [ $BUILD_MODULE = true ]; then
    log "Building module..."
    zip_module "$DIST_DIR" "$combined_name"
  else
    log "Skipping building module zip..."
  fi
}

main "$@"
```

### Build Script - PowerShell (`scripts/build-module.ps1`)

```powershell
#!/usr/bin/env pwsh

<#
.SYNOPSIS
    Build module script for FluentCommerce modules

.DESCRIPTION
    This script builds rule plugins and packages the module assets into a distributable zip file.

.PARAMETER SkipModule
    Skip zipping the module

.PARAMETER SkipPlugins
    Skip building rule plugins

.PARAMETER VerboseLogging
    Enable verbose logging

.EXAMPLE
    .\build-module.ps1 -VerboseLogging

.EXAMPLE
    .\build-module.ps1 -SkipPlugins
#>

[CmdletBinding()]
param(
    [Alias('m')]
    [switch]$SkipModule,

    [Alias('p')]
    [switch]$SkipPlugins,

    [Alias('x')]
    [switch]$VerboseLogging
)

$Script:BuildPlugins = -not $SkipPlugins
$Script:BuildModule = -not $SkipModule
$Script:VerboseMode = $VerboseLogging

function Write-Log {
    param([string]$Message)
    if ($Script:VerboseMode) {
        Write-Host $Message
    }
}

function Remove-FolderIfExists {
    param([string]$Path)
    if (Test-Path $Path) {
        Write-Log "Directory $Path exists, clearing..."
        Remove-Item -Path $Path -Recurse -Force
    }
}

function Copy-Assets {
    param(
        [string]$ResourcesDir,
        [string]$DistDir,
        [string]$DistAssetsDir
    )
    Write-Log "Resources Dir: '$ResourcesDir', Destination Dir: '$DistDir'"

    if (-not (Test-Path $ResourcesDir)) {
        Write-Error "Resources directory '$ResourcesDir' does not exist"
        exit 1
    }

    $moduleJsonPath = Join-Path $ResourcesDir "module.json"
    if (Test-Path $moduleJsonPath) {
        Copy-Item $moduleJsonPath $DistDir
    }

    $moduleConfigPath = Join-Path $ResourcesDir "module.config.json"
    if (Test-Path $moduleConfigPath) {
        Copy-Item $moduleConfigPath $DistDir
    }

    if (-not (Test-Path $DistAssetsDir)) {
        New-Item -ItemType Directory -Path $DistAssetsDir -Force | Out-Null
    }

    $excludedEntries = @("module.json", "module.config.json", "README.md", "CHANGELOG.md", "LICENSE.md", "workflows-backup")
    Get-ChildItem -Path $ResourcesDir -Force | ForEach-Object {
        if ($excludedEntries -contains $_.Name) {
            Write-Log "Skipping non-asset entry: '$($_.Name)'"
        } else {
            Copy-Item -Path $_.FullName -Destination $DistAssetsDir -Recurse -Force
        }
    }
}

function Copy-JarFiles {
    param(
        [string]$PluginFolder,
        [string]$Destination
    )
    if (Test-Path $PluginFolder) {
        Write-Log "Plugin folder exists, copying from '$PluginFolder' to '$Destination'"
        New-Item -ItemType Directory -Path $Destination -Force | Out-Null
    } else {
        Write-Log "No plugins to copy from: '$PluginFolder'"
        return
    }

    $targetFolders = Get-ChildItem -Path $PluginFolder -Directory | Where-Object { Test-Path (Join-Path $_.FullName "target") }
    foreach ($folder in $targetFolders) {
        $targetFolder = Join-Path $folder.FullName "target"
        if (Test-Path $targetFolder) {
            $jarFiles = Get-ChildItem -Path $targetFolder -Filter "*.jar"
            foreach ($jarFile in $jarFiles) {
                Write-Host "Copying $($jarFile.FullName) to $Destination"
                Copy-Item $jarFile.FullName $Destination
            }
        }
    }
}

function New-ModuleZip {
    param(
        [string]$DistDir,
        [string]$ModuleName
    )
    $zipPath = Join-Path $DistDir "$ModuleName.zip"
    if (Test-Path $zipPath) {
        Remove-Item $zipPath -Force
    }

    $sourceDir = Join-Path $DistDir $ModuleName
    Compress-Archive -Path "$sourceDir\*" -DestinationPath $zipPath -Force
    Write-Log "Contents zipped into '$ModuleName.zip'."
}

function Get-JsonValue {
    param(
        [string]$FilePath,
        [string]$PropertyName
    )
    if (-not (Test-Path $FilePath)) { return $null }
    try {
        $json = Get-Content $FilePath -Raw | ConvertFrom-Json
        return $json.$PropertyName
    } catch {
        Write-Log "Failed to parse JSON from $FilePath"
        return $null
    }
}

function Main {
    if ($PSScriptRoot) {
        $scriptDir = $PSScriptRoot
    } elseif ($MyInvocation.MyCommand.Path) {
        $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
    } else {
        $scriptDir = (Get-Location).Path
    }
    $moduleBaseDir = Split-Path -Parent $scriptDir
    $pluginDir = Join-Path $moduleBaseDir "plugins"
    $pluginRulesDir = Join-Path $pluginDir "rules"
    $distDir = Join-Path $moduleBaseDir "dist"
    $resourcesDir = Join-Path $moduleBaseDir "resources"

    if ($Script:BuildPlugins) {
        Write-Log "Building plugins..."
        Push-Location $pluginDir
        try {
            & mvn clean package
            if ($LASTEXITCODE -ne 0) {
                Write-Error "Maven build failed"
                exit 1
            }
        } finally {
            Pop-Location
        }
    }

    $moduleJsonPath = Join-Path $resourcesDir "module.json"
    $moduleName = Get-JsonValue $moduleJsonPath "name"
    $moduleVersion = Get-JsonValue $moduleJsonPath "version"
    if (-not $moduleName -or -not $moduleVersion) {
        Write-Error "Could not extract module name or version from module.json"
        exit 1
    }

    $sanitizedName = $moduleName -replace "/", "-"
    $combinedName = "$sanitizedName-$moduleVersion"
    $moduleDistDir = Join-Path $distDir $combinedName
    $moduleDistAssetsDir = Join-Path $moduleDistDir "assets"
    $moduleDistPluginsDir = Join-Path $moduleDistAssetsDir "rules"

    Remove-FolderIfExists $distDir
    New-Item -ItemType Directory -Path $moduleDistDir -Force | Out-Null

    $rootFiles = @("CHANGELOG.md", "README.md", "LICENSE.md")
    foreach ($file in $rootFiles) {
        $sourcePath = Join-Path $moduleBaseDir $file
        if (Test-Path $sourcePath) {
            Copy-Item $sourcePath $moduleDistDir
        }
    }

    Copy-Assets $resourcesDir $moduleDistDir $moduleDistAssetsDir
    Copy-JarFiles $pluginRulesDir $moduleDistPluginsDir

    if ($Script:BuildModule) {
        Write-Log "Building module..."
        New-ModuleZip $distDir $combinedName
    }

    Write-Host "Build completed successfully!"
}

Main
```

### `.gitignore`

```gitignore
# Maven build output
target/
*.class

# IDE files
.idea/
*.iml
.project
.classpath
.settings/
.vscode/

# Distribution output
dist/

# OS files
.DS_Store
Thumbs.db

# Logs
*.log

# Dependencies (should be resolved by Maven)
node_modules/

# Schema (downloaded separately)
global/schema.json
```

## Entity Type to Package Mapping

When `--entity-types` is specified, rule classes are placed in sub-packages that match the entity type:

| Entity Type | Java Package | Sub-package name |
|-------------|-------------|------------------|
| `ORDER` | `com.fluentcommerce.rule.order` | `order` |
| `FULFILMENT` | `com.fluentcommerce.rule.fulfilment` | `fulfilment` |
| `FULFILMENT_OPTIONS` | `com.fluentcommerce.rule.fulfilmentoption` | `fulfilmentoption` |
| `ARTICLE` | `com.fluentcommerce.rule.article` | `article` |
| `CONSIGNMENT` | `com.fluentcommerce.rule.consignment` | `consignment` |
| `WAVE` | `com.fluentcommerce.rule.wave` | `wave` |
| `LOCATION` | `com.fluentcommerce.rule.location` | `location` |
| `PRODUCT` | `com.fluentcommerce.rule.variantproduct` | `variantproduct` |
| `VIRTUAL_CATALOGUE` | `com.fluentcommerce.rule.catalogue` | `catalogue` |
| `INVENTORY_POSITION` | `com.fluentcommerce.rule.inventory` | `inventory` |
| `RETURN_ORDER` | `com.fluentcommerce.rule.returnorder` | `returnorder` |
| (default/common) | `com.fluentcommerce.rule.common` | `common` |

When multiple entity types are specified, place each rule in the package matching its primary entity type. If a rule applies to multiple entity types, use `common`.

## Execution Flow

```
1. VALIDATE inputs
   a. module-name format: must match /^[a-z][a-z0-9-]*$/ (lowercase, hyphens allowed, no leading hyphen)
   b. rule names (if --rules): must match /^[A-Z][a-zA-Z0-9]*$/ (PascalCase)
   c. entity types (if --entity-types): must be in the valid set above

2. PRE-CHECK: Module already exists?
   a. Check accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<module-name>/ exists
   b. If exists → ABORT with guidance
   c. Optionally check deployed modules via: fluent module list -p <PROFILE>

3. DETECT account prefix
   a. If --account-prefix provided → use it
   b. Else try MCP: plugin.list (compact: true)
      → Extract <ACCOUNT> from first non-FLUENTRETAIL rule key
   c. Else derive from PROFILE name (uppercase)
   d. Fallback: "UNKNOWN" with warning

4. DETECT SDK versions (optional)
   a. Check if another module exists under accounts/<PROFILE>/SOURCE/backend/
   b. If found, read its rules/*/pom.xml for rubix-plugin-base.version and fluent-api-client.version
   c. If not found, use defaults: rubix-plugin-base=1.2.0.679, fluent-api-client=1.2.0.118

5. COMPUTE derived values
   a. MODULE_ALIAS = <module-name> (e.g., acme-returns)
   b. MODULE_TITLE = title-case of module-name with hyphens as spaces (e.g., Hm Returns)
   c. OSGI_SYMBOLIC_NAME = "_." + <module-alias with hyphens removed> (e.g., _.hmreturns)
   d. PACKAGE = entity type sub-package from mapping table above

6. CREATE directory structure
   a. Create all directories listed in the structure section
   b. Ensure all parent directories exist before writing files

7. GENERATE files (use Write tool, not bash echo)
   a. plugins/pom.xml — parent POM with all variable substitutions
   b. plugins/types/pom.xml — types aggregator
   c. plugins/types/<alias>/pom.xml — types implementation
   d. plugins/util/pom.xml — util aggregator
   e. plugins/util/<alias>/pom.xml — util implementation
   f. plugins/rules/pom.xml — rules aggregator
   g. plugins/rules/<alias>/pom.xml — rules implementation (OSGi bundle)
   h. resources/module.json — module manifest
   i. scripts/build-module.sh — bash build script (set executable: chmod +x)
   j. scripts/build-module.ps1 — powershell build script
   k. .gitignore

8. FOR EACH rule in --rules (if specified):
   a. Determine target package from entity type
   b. Generate Java rule class using the appropriate template (BaseRule or Rule)
   c. Generate test class
   d. Add rule name to module.json rules[] array

9. CREATE empty placeholder directories
   a. resources/settings/
   b. global/
   c. dist/
   d. Empty src/main/java/ trees for types and util (if no rules specified)

10. MAKE build script executable
    bash: chmod +x scripts/build-module.sh

11. VERIFY structure (optional, if Maven is available)
    cd plugins/ && mvn validate
    This confirms POM structure is valid without actually building.

12. REPORT generated files
    List all files created with their full paths.
    Print next steps:
    - "Module scaffolded at: accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<module-name>/"
    - "Next: Add rules with /fluent-rule-scaffold or build with /fluent-build"
    - "Validate structure: /fluent-module-validate"
```

## SDK Version Detection

The `rubix-plugin-base.version` and `fluent-api-client.version` determine Fluent Commerce SDK compatibility. Detection order:

```
1. Check existing modules under accounts/<PROFILE>/SOURCE/backend/
   → Read any rules/*/pom.xml for <rubix-plugin-base.version> and <fluent-api-client.version>
   → Reuse the same versions for consistency

2. Check deployed modules via MCP (environment.discover with modules section)
   → Extract compatibility block versions from deployed module metadata

3. Fall back to known stable versions:
   rubix-plugin-base.version = 1.2.0.679
   fluent-api-client.version = 1.2.0.118
   apollo-client-maven-plugin.version = 1.0.0.15
```

## Integration with Other Skills

| Skill | Relationship |
|-------|-------------|
| `/fluent-rule-scaffold` | For adding rules to an EXISTING module. After module-scaffold creates the module structure, use rule-scaffold to add individual rules incrementally. Rule-scaffold handles: OOTB-vs-custom decision, BaseRule vs Rule template selection, module.json wiring, and test generation. |
| `/fluent-build` | Immediately usable after scaffold. Run `mvn clean install` from `plugins/`, then `scripts/build-module.sh` (or `.ps1`) for ZIP packaging. |
| `/fluent-module-validate` | Should pass structural validation on the scaffolded output with 0 FAILs. Expected WARNs: no GraphQL schema (not yet downloaded), no distribution (not yet built). |
| `/fluent-module-deploy` | Deploy the built ZIP to a target retailer after build succeeds. |
| `/fluent-custom-code` | Analyzes the generated source for behavior mapping and constraint extraction. |

### Post-Scaffold Workflow

```
/fluent-module-scaffold acme-returns --entity-types ORDER,RETURN_ORDER --rules CreateReturnFromOrder,ProcessReturnApproval
    |
    v
/fluent-rule-scaffold AddReturnNotes --entity-type RETURN_ORDER --package returnorder
    |
    v
/fluent-module-validate accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-acme-returns/
    |
    v
/fluent-build build
    |
    v
/fluent-module-deploy
```

## Edge Cases

### Module name conflicts

Check `accounts/<PROFILE>/SOURCE/backend/` for any directory containing the module name:

```bash
ls accounts/<PROFILE>/SOURCE/backend/ | grep -i "<module-name>"
```

If found, abort. Do NOT overwrite an existing module.

### No rules specified

When `--rules` is omitted:
- `module.json` has `"rules": []`
- No Java classes are generated
- Empty `src/main/java/com/fluentcommerce/rule/` directory is created
- Empty `src/test/java/com/fluentcommerce/rule/` directory is created
- The module is still buildable (produces an empty JAR)

### Windows path handling

- All POM `<module>` references use forward slashes (Maven standard)
- Build scripts have both `.sh` (Git Bash, WSL, macOS, Linux) and `.ps1` (PowerShell) variants
- Use Node.js for cross-platform directory creation when bash `mkdir -p` is unavailable
- Use `path.join()` semantics in documentation, not hardcoded separators

### Module name validation

Reject module names that:
- Start with a digit or hyphen
- Contain uppercase letters (convention: lowercase only)
- Contain spaces or special characters beyond hyphens
- Are empty or longer than 50 characters

### GraphQL schema not available

The `global/schema.json` file is required for Apollo codegen (types submodule) but may not be available at scaffold time. The scaffolder:
- Creates the `global/` directory empty
- Notes in the output that the schema must be downloaded before a full build
- The types submodule will fail codegen without it, but `mvn validate` will pass

### Duplicate rule names

If `--rules` contains duplicates, deduplicate silently and warn:

```
Warning: Duplicate rule name 'MyRule' removed from list.
```

## Session Tracking

When invoked, log the following to the session tracking protocol (consumed by `/fluent-session`):

**On entry:**
```json
{ "skill": "<this-skill-name>", "timestamp": "<ISO-8601>", "arguments": { "<key>": "<value>", "...": "..." } }
```

**On exit:**
```json
{ "skill": "<this-skill-name>", "outcome": "<completed|failed|skipped>", "changesProduced": ["<seq-numbers>"], "toolCallsProduced": ["<seq-numbers>"], "nextRecommended": "<from-handoff-section>" }
```

All MCP tool calls made during execution should include `"skill": "<this-skill-name>"` in their tracking record for attribution. The `arguments` object should capture the key parameters actually passed (profile, retailer, entity type, etc.) — not a fixed schema. The `nextRecommended` value comes from the Handoff section below.

## Handoff

| Output Artifact | Consumed By | Path |
|----------------|-------------|------|
| Maven project skeleton | `/fluent-rule-scaffold` (adds rules) | `accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<name>/` |
| Maven project skeleton | `/fluent-build` (compiles) | `accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<name>/plugins/` |
| module.json manifest | `/fluent-module-validate` | `accounts/<PROFILE>/SOURCE/backend/fluentcommerce-fc-module-<name>/resources/module.json` |

## Error Reporting

Errors from this skill follow the standard format:

| Field | Description |
|-------|-------------|
| `phase` | Skill phase where error occurred (e.g., `pre-check`, `validation`, `generation`, `verification`) |
| `severity` | `CRITICAL` (blocks downstream), `HIGH` (needs fix), `MEDIUM` (advisory), `LOW` (informational) |
| `message` | Human-readable error description |
| `resolution` | Suggested fix or downstream skill to invoke |

## Gotchas

- **OSGi symbolic name** must be unique per account. Convention: `_.<aliasNoHyphens>` (e.g., `_.hmreturns`). If two modules share the same symbolic name, deployment will fail with a cryptic OSGi error.
- **`<Rubix-Account>_</Rubix-Account>`** in the bundle plugin config means "register to the deploying account." Do not change this to a hardcoded account name.
- **Java source level 1.8** is required by the Rubix runtime. Do not use Java 11+ features in rule code even if the build JDK is newer.
- **`module.json` `name` field** uses the format `fluent-commerce/fc-module-<name>` by convention (with the `fluent-commerce/` prefix). This is the canonical identifier used by `fluent module install`.
- **`module.json` `rules` array** contains simple rule names (e.g., `"MyRule"`), NOT fully-qualified class names or account-prefixed keys. The OSGi bundle plugin discovers `@RuleInfo` annotations at build time.
- **Apollo codegen** requires Node.js 18 for the Maven plugin. On Windows with nvm-windows (nvm4w), use: `nvm use 18.x.x` (full version number required).
- **Intermediate POM hierarchy** matters. The parent POM declares `<module>types</module>`, which references `plugins/types/pom.xml` (an aggregator), which in turn declares `<module>${MODULE_ALIAS}</module>` pointing to the implementation POM. Skipping the aggregator level will break the build.

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-module-scaffold"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-module-scaffold.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user asked a question, no actual write operation happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
