Stop Copying AI Skills: Version IBM Bob Instructions with Maven
This tutorial shows Java developers how to turn reusable Quarkus agent skills into versioned artifacts and consume them across projects.
Copy-pasting AI skill files feels harmless when you have one project. You drop a SKILL.md into .bob/skills, IBM Bob starts behaving like it understands Quarkus, and you move on. The trouble shows up later: the same skill in five repositories, each with slightly different instructions, commands, and assumptions about your stack. You only notice when two checkouts disagree during the same review.
Most teams file this under documentation. In practice it behaves like dependency management, and you stop treating it like documentation the day Bob (or any other IDE/Shell combination you are using) starts generating patches you actually merge. Once agent behavior matters for daily work, those instructions sit in your build and delivery path. If they drift, your agent drifts. One checkout gets jakarta.ws.rs right, another keeps old patterns, a third nudges the model toward the wrong extension.
This gets worse on teams that ship to production because agent instructions are not neutral. They push which commands run, which files change, and which defaults stick. A stale skill gives you ugly code. It can also teach the assistant the wrong native build command, the wrong REST stack, or the wrong packaging convention. Past “Bob is a bit off” you get wasted review time, a messy delivery flow, and expensive tokens for bad answers.
Java developers already know what to do with reusable stuff: package it, version it, ship it like any other JAR, pull it in with Maven. SkillsJars does the same for agent skills. You write framework-specific SKILL.md files once, pack them into a JAR, install or publish the artifact where your builds can see it, and extract into the folder IBM Bob reads when someone opens the project. Same muscle memory as any internal library, only the payload is Markdown.
Next we run the full loop with local mvn install (Maven Central optional): a quarkus-dev-skills JAR at 1.0.0-SNAPSHOT, three Quarkus skills inside, and a consumer app under shipment-service/ that pulls that JAR. Bob gets the same guidance everywhere without copy-paste. If you get lost, the quarkus-dev-skills/ tree in the repo is the ground truth for these steps.
Prerequisites
You should be fine with Maven, a normal Quarkus project layout, and Markdown written for an agent. You also need:
Java 25 installed
Maven 3.9+ or the Maven Wrapper available
IBM Bob installed in your IDE
Network access to resolve the SkillsJars Maven plugin (
com.skillsjars:maven-plugin) from the public plugin repositoriesBasic understanding of Maven
pom.xmlfiles
Project Setup
Start from a plain Maven project for the skills artifact. It is not a Quarkus app: its only job is to ship reusable skill files. Deleting src/main/java in the next step still feels wrong the first time; for this artifact, empty Java trees are normal.
Create the project or start from my Github repository:
mvn archetype:generate \
-DgroupId=com.example.skills \
-DartifactId=quarkus-dev-skills \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=falseNow move into the project and remove the Java source folders because this artifact ships Markdown-based skills, not Java classes:
cd quarkus-dev-skills
rm -rf src
mkdir -p skills/quarkus-scaffolding
mkdir -p skills/quarkus-extensions
mkdir -p skills/quarkus-nativeThe structure should now look like this (this repository also keeps the demo consumer next to the packaging project):
quarkus-dev-skills/
├── article.md
├── pom.xml
├── skills/
│ ├── quarkus-scaffolding/
│ │ └── SKILL.md
│ ├── quarkus-extensions/
│ │ └── SKILL.md
│ └── quarkus-native/
│ └── SKILL.md
└── shipment-service/
├── AGENTS.md
├── pom.xml
└── src/
The SkillsJars Maven plugin scans the top-level skills/ directory and treats each immediate child folder as one skill. Get the directory names right once; everything downstream reads from there.
Implementation
Below you will find very incomplete examples. There is ongoing Quarkus work around shared coding-agent guidance in pull request quarkusio/quarkus#53038, which adds an initial structure for reusable coding rules and explicitly references AGENTS.md as the emerging open format for agent instructions. So keep an eye out for more coming from the team in the future. The broader format and conventions are documented at agents.md.
Security warning: Agent Skills should be treated like executable guidance, not harmless documentation. They can run commands, read files, and change code in ways you did not expect. SkillsJars says that they do a basic security scan before publication, but that is only a baseline check. It does not replace a proper security review of the skills before your team uses them.
Writing the scaffolding skill
The first skill pins how Bob creates Quarkus resources and related classes: endpoint, service, maybe an entity, with predictable packages and imports.
Create skills/quarkus-scaffolding/SKILL.md:
---
name: quarkus-scaffolding
description: >
Example playbook for new REST resources, CDI services, Panache entities,
and repositories in a Quarkus 3 app. Load only when scaffolding those
pieces — not a substitute for project-wide AGENTS.md or rules.
allowed-tools: Bash Read Edit
license: Apache-2.0
---
# Quarkus scaffolding (examples only)
**Progressive skill:** use when adding endpoints or persistence types. Repo-wide conventions belong in always-on guidance ([`AGENTS.md`](https://agents.md/), `.cursor/rules`, etc.); specialized steps live in skills so they are not stuffed into every prompt. Quarkus is converging on that split — markdown rules plus `.agents/skills/` — see [quarkus#53038](https://github.com/quarkusio/quarkus/pull/53038). If this repo already defines layout or naming, follow that first.
## Package layout (typical)
- `.../resource/` — JAX-RS
- `.../service/` — CDI beans
- `.../entity/` — JPA + Panache
- `.../repository/` — Panache repositories
## CLI vs hand-written classes
The Quarkus CLI creates **apps** and manages **extensions** (e.g. `quarkus create app`, `quarkus extension add`). It does not standardize “add one Java class” inside an existing module — create new types in the IDE or by copying the skeleton below.
## Resource shape (`jakarta.*`, not `javax.*`)
```java
package com.example.app.resource;
import com.example.app.service.WidgetService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@Path("/api/v1/widgets")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class WidgetResource {
@Inject
WidgetService service;
@GET
public Object list() {
return service.list();
}
@POST
public Object create(Object request) {
return service.create(request);
}
}
```
## Panache (minimal)
- Default surrogate key → `PanacheEntity`.
- Custom or composite id → `PanacheEntityBase`.
- Annotate with `@Entity` and `@Table(name = "some_table")` (snake_case is a common convention for `name`).
## Imports
- CDI: `jakarta.inject`, `jakarta.enterprise.context`
- REST: `jakarta.ws.rs`
- Panache ORM: `io.quarkus.hibernate.orm.panache`
- Avoid Spring annotations unless the project uses Spring-on-Quarkus explicitly.
The front matter does most of the routing. The name must match the directory name. The description helps the model decide if the skill fits the task: write it for matching, not like internal documentation. If you would not say the description out loud to a teammate choosing a skill, rewrite it.
allowed-tools caps what the agent can do without another prompt. It also keeps the blast radius small: only list tools you want. For scaffolding, Bash plus file editing is enough. Wider lists mean a bigger mess if the skill fires in the wrong place. I keep lists short on purpose; you can always add more when a task really needs them.
Writing the extension management skill
Add a second skill so Bob stays on the Quarkus CLI and BOM patterns. Generic assistants often invent Maven coordinates, pin versions the BOM should own, or mix old and new REST stacks. This file narrows that path.
Create skills/quarkus-extensions/SKILL.md:
---
name: quarkus-extensions
description: >
Example playbook for adding, listing, and removing Quarkus extensions
via CLI or build tools. Load when the task is dependencies/capabilities,
not routine coding; platform/BOM policy stays in AGENTS.md/rules.
allowed-tools: Bash Read Edit
license: Apache-2.0
---
# Quarkus extensions (examples only)
**Progressive skill:** use when someone needs REST, data, messaging, health, tracing, etc. at the **build** level. Version and BOM constraints that apply to every change belong in always-on guidance ([agents.md](https://agents.md/), rules); this file is task-sized, like the layout Quarkus is heading toward with rules + `.agents/skills/` — see [quarkus#53038](https://github.com/quarkusio/quarkus/pull/53038).
## CLI (preferred)
`ext` is shorthand for `extension` ([CLI tooling](https://quarkus.io/guides/cli-tooling)).
Browse what you can add (installable extensions, name filter):
```bash
quarkus ext ls -i -s jdbc
```
Add or remove (names can be short; globs like `smallrye-*` work):
```bash
quarkus ext add rest-jackson
quarkus ext rm rest-jackson
```
## No CLI: Maven / Gradle
Maven:
```bash
./mvnw quarkus:add-extension -Dextensions='rest-jackson,kafka'
```
Gradle:
```bash
./gradlew listExtensions
./gradlew addExtension --extensions='hibernate-validator'
```
## Last resort: edit the build by hand
Maven — **no** version when the Quarkus BOM is imported:
```xml
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
```
Gradle (same idea: BOM manages versions):
```groovy
implementation 'io.quarkus:quarkus-rest-jackson'
```
Use coordinates from [quarkus.io/extensions](https://quarkus.io/extensions/) or CLI list output — **do not guess** artifact IDs.
## Names you see a lot (illustrative)
- `rest` / `rest-jackson`
- `hibernate-orm-panache`
- `jdbc-postgresql`
- `messaging-kafka`
- `smallrye-health`
- `opentelemetry`
## Rules of thumb
- Prefer the **REST** stack (`rest`, `rest-jackson`) for new JAX-RS-style apps unless the project already standardizes on something else.
- Do not mix **incompatible** stacks (e.g. Spring MVC + RESTEasy) without an explicit reason.
- Keep every `io.quarkus` extension on the **same platform/BOM** the project already uses.So when someone says “add Kafka,” you get Quarkus extension IDs and the CLI flow, not a random client dependency pulled from a blog post. If your team really wants plain Kafka clients, say so in the skill and own that choice.
The same JAR everywhere means the same instructions in every checkout. For an agent that touches many repos, boring repeatability beats clever prose. That is the whole point of the exercise.
Writing the native build skill
Native builds get a separate skill because wrong text hurts fast: local GraalVM vs container builds, reflection registration, integration tests vs JVM tests.
Create skills/quarkus-native/SKILL.md:
---
name: quarkus-native
description: >
Example playbook for native executables and native container images
(Maven/Gradle). Load when debugging native builds or reflection — not
for everyday JVM dev; keep always-on project policy in AGENTS.md/rules.
allowed-tools: Bash Read Edit
license: Apache-2.0
---
# Quarkus native (examples only)
**Progressive skill:** native compilation is slow and toolchain-specific; invoke this only when the task is “build native,” “fix native runtime,” or CI image parity. Always-on constraints (e.g. “we only ship container-build”) belong in [agents.md](https://agents.md/) / rules; task detail here matches the direction in [quarkus#53038](https://github.com/quarkusio/quarkus/pull/53038).
## Maven (typical)
Local toolchain (GraalVM / Mandrel already on `PATH`):
```bash
./mvnw package -Dnative
```
No local native compiler — build inside the builder image:
```bash
./mvnw package -Dnative -Dquarkus.native.container-build=true
```
Native binary **and** container image (needs a `quarkus-container-image-*` extension):
```bash
./mvnw package -Dnative \
-Dquarkus.native.container-build=true \
-Dquarkus.container-image.build=true
```
## Gradle (typical)
```bash
./gradlew build -Dquarkus.native.enabled=true
```
Container-based native compile:
```bash
./gradlew build -Dquarkus.native.enabled=true -Dquarkus.native.container-build=true
```
## Reflection / missing classes at runtime
Prefer registering the types that actually need reflection:
```java
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class ShipmentDto {
public String id;
public String status;
}
```
Fallback: extra native-image args (e.g. external JSON) via config:
```properties
quarkus.native.additional-build-args=-H:ReflectionConfigurationFiles=reflection-config.json
```
## Native integration tests
```bash
./mvnw verify -Dnative
```
Gradle (generates native image, then runs tests):
```bash
./gradlew testNative
```
`@QuarkusIntegrationTest` exercises the **artifact the build produced** (JAR vs native binary vs container), not the in-process JVM test runtime.
Do not pack every native-image trick into one file. A long wall of text gives the model more tokens but weaker signal. Keep the skill tight; put long appendices in another file if you need them. Native is painful enough without a fifty-screen skill nobody loads.
Configuring the skills artifact POM
Wire the packaging project so Maven turns these Markdown files into a skills JAR. Replace your pom.xml with this version (same as quarkus-dev-skills/pom.xml in the tree). Yes, it is a full paste; for the demo that is faster than diffing line by line in prose.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.skills</groupId>
<artifactId>quarkus-dev-skills</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Quarkus Developer Skills</name>
<description>Reusable agent skills for IBM Bob working in Quarkus projects (demo / tutorial).</description>
<url>https://github.com/myfear/the-main-thread/quarkus-dev-skills</url>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<skillsjars.skill.quarkus-scaffolding.allowed-tools>Bash Read Edit</skillsjars.skill.quarkus-scaffolding.allowed-tools>
<skillsjars.skill.quarkus-extensions.allowed-tools>Bash Read Edit</skillsjars.skill.quarkus-extensions.allowed-tools>
<skillsjars.skill.quarkus-native.allowed-tools>Bash Read Edit</skillsjars.skill.quarkus-native.allowed-tools>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.skillsjars</groupId>
<artifactId>maven-plugin</artifactId>
<version>0.0.6</version>
<executions>
<execution>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>This POM is the contract between your Markdown and the plugin. The skillsjars.skill.<skill-name>.allowed-tools properties must match the front matter in each SKILL.md. If they drift, the build fails, which is friendlier than silently shipping skills with the wrong tool policy.
Paths inside the JAR. The package goal copies each skill under META-INF/skills/... as in the plugin README. With <scm><url> on github.com, the plugin takes the GitHub org and repo from that URL. Without it, you get Maven groupId segments (com/example/skills/<skill>/... in PackageMojoTest). This tree uses a placeholder example-org/quarkus-dev-skills URL in <scm> so the paths match the SkillsJars.com examples. That URL is only a teaching label.
Consumer coordinates. Skills that SkillsJars.com republishes use groupId com.skillsjars and an artifactId like org__repo__skill (skillsjars.com). For a JAR you built yourself, consumers use your groupId and artifactId, here com.example.skills:quarkus-dev-skills, after mvn install or after you push to an internal repo.
Building and inspecting the skills JAR
Package the artifact and confirm the skill files landed under the right META-INF paths.
Build and install into your local repository (~/.m2) so the consumer can resolve the SNAPSHOT:
mvn installInspect the resulting JAR:
jar tf target/quarkus-dev-skills-1.0.0-SNAPSHOT.jar | grep /SKILL.mdYou should see output similar to this (with the example <scm> URL above):
META-INF/skills/example-org/quarkus-dev-skills/quarkus-extensions/SKILL.md
META-INF/skills/example-org/quarkus-dev-skills/quarkus-scaffolding/SKILL.md
META-INF/skills/example-org/quarkus-dev-skills/quarkus-native/SKILL.mdThe extract goal reads exactly these paths. If the jar tf output is empty, Bob gets nothing from the consumer build, so stop and fix packaging before going further.
A clean JAR only proves layout, not quality. You can ship a perfect archive and still teach the wrong extension. Versioning ships the same bits everywhere; someone still has to read the skill text. I treat jar tf as a smoke test, not a proof that Bob will behave.
Creating the consumer Quarkus application
Add a Quarkus app that consumes the skills JAR so the layout looks like a real project.
Create the Quarkus application under the packaging project (from quarkus-dev-skills/):
quarkus create app com.example:shipment-service \
--extension=quarkus-rest-jackson,quarkus-hibernate-orm-panache,quarkus-jdbc-postgresql,quarkus-smallrye-healthMove into the project:
cd shipment-serviceAlign the consumer with Java 25 if your Quarkus codestart picked a newer --release (shipment-service/pom.xml here uses maven.compiler.release 25).
The app is only there to consume skills. There is no real business logic; that is intentional. Pick extensions so the prompts in Verification look like normal Quarkus work instead of a toy Hello World.
Configuring the consumer project to extract skills
Add the SkillsJars plugin to the Quarkus app and declare the skills artifact as a plugin dependency. Skills stay off the runtime classpath; extraction reads the JAR at build time for Bob’s folder.
Update the consumer project pom.xml and add the plugin inside the <build><plugins> section. Reference the same coordinates you installed with mvn install in the packaging project:
<plugin>
<groupId>com.skillsjars</groupId>
<artifactId>maven-plugin</artifactId>
<version>0.0.6</version>
<dependencies>
<dependency>
<groupId>com.example.skills</groupId>
<artifactId>quarkus-dev-skills</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</plugin>The live shipment-service/pom.xml in this tree keeps the Quarkus-generated compiler, Surefire, and Failsafe plugins and appends this SkillsJars plugin after them.
The skills JAR never joins the application dependency graph. Your runtime image does not grow agent instructions just because someone uses Bob on a laptop. That boundary matters if ops is nervous about “AI stuff” on the classpath.
Extracting the skills into Bob’s project directory
Run the extraction goal and write skills into the directory IBM Bob watches in the project.
Run (from shipment-service/):
./mvnw skillsjars:extract -Ddir=.bob/skillsThe extract goal scans META-INF/skills/ in the JAR, finds each skill root, and writes one folder per skill under the path you pass to -Ddir. The folder name starts with skillsjars__, then the path inside the JAR with / turned into __. See ExtractMojo in the plugin sources. If you expected a straight mirror of the paths inside the JAR, the folder names will look odd until you read that class once.
After extraction you should see three sibling directories (example with the placeholder <scm> from the POM):
.bob/skills/
├── skillsjars__example-org__quarkus-dev-skills__quarkus-extensions/
│ └── SKILL.md
├── skillsjars__example-org__quarkus-dev-skills__quarkus-native/
│ └── SKILL.md
└── skillsjars__example-org__quarkus-dev-skills__quarkus-scaffolding/
└── SKILL.mdSkills stay sealed in the JAR until extract puts them next to the code Maven built. Bump the artifact version, run extract again, and the skill folders refresh. You stop guessing which stale folder someone copied six months ago.
It is probably a good idea to add the skills folder to .gitignore and not commit them with your code.
Making setup explicit with AGENTS.md
Document how skills land in the repo so the next person does not have to hunt for tribal knowledge.
Add an AGENTS.md file at the root of the consumer project. The checked-in copy matches this (note the mvn install step in the parent directory so the SNAPSHOT exists locally):
# AGENTS.md
## Setup
After cloning this repository, install the shared skills artifact into your local Maven repository (from the sibling packaging project), then extract skills:
```bash
cd ../
mvn -f pom.xml -q install
cd shipment-service
./mvnw skillsjars:extract -Ddir=.bob/skills
```
The first step publishes `com.example.skills:quarkus-dev-skills:1.0.0-SNAPSHOT` to `~/.m2`. The second step unpacks `META-INF/skills/...` from that JAR into `.bob/skills/` using the SkillsJars Maven plugin ([plugin README](https://github.com/skillsjars/skillsjars-maven-plugin/blob/main/README.md)).
Extracted directories are not automatically gitignored. Check and add them to .gitignore. Re-run extraction whenever you bump the skills artifact version.
## Project context
* Java 25
* Quarkus REST with Jackson
* Panache with PostgreSQL
* Health endpoints enabled
* Prefer modern Quarkus REST stack
* Native builds use container-based compilation when neededAGENTS.md covers bootstrap for humans and a short context block for the agent. Keep that list short; long filler buries the commands people actually need.
Some teams commit the extracted files so PRs show skill changes; that works, but diffs get noisy. I prefer gitignore plus explicit extract in AGENTS.md so human edits and generated trees do not step on each other. Your team might reasonably choose otherwise; say which one you picked.
What happens when skills drift
The usual failure is drift. Someone updates native build text in the skills repo. Another repo still has an old extract on disk. Bob answers differently in each checkout, and you argue about the assistant instead of the code. Packaging and versions help only if you bump versions like normal dependency work.
Make the version visible in review. Bump the artifact in the consumer pom.xml, run extract again, and read the .bob/skills diff if you commit those files so behavior changes show up in git. In this repo extract output should be gitignored, so bump the version in the packaging pom.xml and in the consumer plugin dependency together and tell people to reinstall and re-extract. I have watched teams “fix” Bob locally while CI still had last month’s skills; aligning those two numbers is the boring part that actually fixes it.
Tool permissions are a real security boundary
A skill with allowed-tools: Bash Read Edit can run shell commands and edit files. That is the point, and that is also where accidents happen. A sloppy skill, or the right skill in the wrong place, can change more than you meant.
Keep the tool list small. Skip network, broad shell, or “run anything” patterns unless you really need them. Skills are closer to scripts than to comments. Review them like scripts.
Versioning does not replace code review
A versioned skills artifact ships the same bits everywhere: local ~/.m2, internal Nexus, SkillsJars.com, same idea. It does not check whether those bits are correct. If a skill names the wrong Quarkus extension, every consumer picks up the same mistake.
Versioning still helps. Patch for typos and small fixes. Minor when you add a skill or real content. Major when Bob’s behavior on real tasks will change. Downstream teams get a number to plan around. They still need to read the diff.
Context overload weakens skill quality
Do not cram everything into one giant skill. Huge files turn to mush. The model sees more lines and picks the wrong ones. Small focused files usually win.
One skill per problem area works here: scaffolding, extensions, native. If you need a long appendix later, add another file. Do not hide it all in the skill that should fire on a short prompt.
Verification
Verify Bob behavior with concrete prompts
Open the Quarkus consumer in your IDE with IBM Bob enabled and try these prompts in Code mode. This is the part no amount of Maven XML replaces: you are checking whether the words in the skills survive contact with a real model.
Prompt one:
Create a ShipmentResource with GET /api/v1/shipments and POST /api/v1/shipments.Check that:
Bob creates the resource in a
resourcepackageThe code uses
jakarta.ws.rsimportsA matching service class is suggested or created
The generated code reads as Quarkus, not Spring
Prompt two:
Add Kafka support to this project.Watch for:
Bob uses a Quarkus extension workflow
It does not invent random Maven versions
It picks Quarkus extension IDs, not random generic dependencies
Prompt three:
Build a native container image for this project.Expect:
Bob suggests a container-based native build when appropriate
It distinguishes between local native toolchains and container builds
It does not collapse everything into one vague “use GraalVM” answer
Those checks are about how Bob behaves. The JAR and extract steps can be perfect and the skill still does nothing useful if the text does not stick. Ship packaging first, then iterate on words; both are allowed to be wrong, but usually not in the same release.
Conclusion
SkillsJars fits the same habit you already have for libraries: package once, version it, let Maven resolve it, extract into .bob/skills on the consumer. One good SKILL.md is quick to write. The long fight is the same as with any shared library: keeping one truth across many repos without silent fork drift.
After that you can split skills or add reference files so Bob loads a thin layer first and pulls depth only when the task needs it. That is optional polish; the baseline win is already “same JAR, same extract, same words.”


