Teach AI assistants your Java 21 / Spring Boot 3.5 patterns, PostgreSQL / Oracle / Liquibase conventions, and GitLab CI / Kubernetes infrastructure — the full path from application code to production deployment.
| Part | Topic | Focus |
|---|---|---|
| Java & Spring Boot | Records, layers, Maven, testing | Stop pre-Java-16 defaults |
| Database | Schema, Liquibase, query tuning | Prevent unsafe migrations |
| DevOps | CI pipelines, Helm, K8s debugging | Catch infrastructure errors early |
Left unconfigured, AI assistants default to the Java they were most
heavily trained on — which is overwhelmingly pre-Java 16 code.
That means Lombok @Data instead of records, raw Thread instead
of virtual threads, instanceof + cast chains instead of pattern
matching, and Hibernate/JPA instead of Spring Data JDBC.
This has a concrete cost. Every suggestion you reject and retype is a half-minute of friction. Multiplied across a team of 10 developers doing 30 AI interactions a day, that is hours of throwaway review per sprint. The configuration work in this section is a one-time investment that pays back every single session.
There is a second, less obvious cost: inconsistency. An AI that
does not know your layer rules will happily inject a repository into
a controller, return null instead of Optional, or add a @Transactional
annotation in the wrong layer. Those suggestions are not just wrong —
they are subtly wrong, the kind of thing that survives code review and
becomes a support incident later.
The goal of this section is to eliminate that entire class of problem by
front-loading your project's patterns into AGENTS.md, skills, and commands.
Modern Java has changed significantly. Without explicit guidance, AI assistants often generate Java 8-style code. Your configuration should enforce these patterns:
// AI should generate this (Java 16+)
public record CreatePersonRequest(
@NotNull String firstName,
@NotNull String lastName,
@NotNull Sex sex
) {
public CreatePersonRequest {
if (firstName == null || firstName.isBlank()) {
throw new IllegalArgumentException("firstName is required");
}
}
}
// NOT this (pre-Java 16)
@Data
@AllArgsConstructor
public class CreatePersonRequest {
private String firstName;
private String lastName;
private Sex sex;
}public sealed interface PersonEvent
permits PersonCreated, PersonUpdated, PersonDeleted {
UUID personId();
Instant occurredAt();
}
public record PersonCreated(UUID personId, Instant occurredAt, String name)
implements PersonEvent {}// AI should generate this (Java 21)
return switch (status) {
case ACTIVE -> processActive(person);
case INACTIVE -> processInactive(person);
case BLOCKED -> throw new IllegalStateException("Blocked persons cannot be processed");
};
// NOT this
if (status == Status.ACTIVE) {
return processActive(person);
} else if (status == Status.INACTIVE) {
return processInactive(person);
} else {
throw new IllegalStateException("...");
}Virtual threads are GA in Java 21 (JEP 444). No special flags needed.
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}Note — Structured Concurrency (
StructuredTaskScope) status by JDK version:
JDK Status Flag required 21 Preview (JEP 453) --enable-preview22 Second preview (JEP 462) --enable-preview23 Third preview (JEP 480) --enable-preview24+ GA (JEP 505) None The
StructuredTaskScopeexamples in this section use the GA API shape. If your team is on JDK 21–23, add--enable-previewto your Maven Surefire and compiler plugin configuration, or useExecutors.newVirtualThreadPerTaskExecutor()with a plainCompletableFuturechain as the stable alternative.
String query = """
SELECT p.id, p.first_name, p.last_name
FROM person p
WHERE p.status = :status
ORDER BY p.last_name
""";Document your exact layer boundaries in AGENTS.md:
## Architecture Layers
### Controller Layer
- Annotation: @RestController
- Responsibilities: HTTP handling, request validation, response mapping
- MUST use DTOs (records) for request/response
- MUST NOT contain business logic
- MUST NOT inject Repository beans directly
- MUST use OpenAPI annotations (@Tag, @Operation, @ApiResponse)
### Service Layer
- Interface + Impl pattern: PersonService / PersonServiceImpl
- Annotation: @Service on implementation
- MUST use @Transactional for write operations
- MUST receive and return DTOs, not entities
- MUST use constructor injection via @RequiredArgsConstructor
### Repository Layer
- Annotation: @Repository
- Extends CrudRepository or uses Spring Data JDBC
- Named queries follow Spring conventions: findByEmail, findAllByStatus
- Complex queries use @Query with named parameters
### Entity Layer
- Plain classes with @Table annotation (Spring Data JDBC)
- NEVER exposed through REST API
- Use @Id for primary key
- Relationships via AggregateReference (Spring Data JDBC)
### Mapper Layer
- MapStruct: @Mapper(componentModel = "spring")
- Declarative mapping only (no procedural logic)
- Separate mappers per entity/aggregateInclude your Maven conventions in AGENTS.md:
## Maven
### Plugin Versions
- Spring Boot Maven Plugin: inherited from parent
- MapStruct: 1.5.5.Final
- Lombok: inherited from Spring Boot BOM
- JaCoCo: 0.8.12
### Dependency Rules
- NEVER add dependency versions if managed by Spring Boot BOM
- Use spring-boot-starter-* where available
- Test dependencies: scope=test only
- Lombok: scope=provided, annotationProcessorPaths configured
### Build Profiles
- default: full build with tests
- fast: -DskipTests -Dmaven.javadoc.skip=true
- ci: includes JaCoCo coverage + dependency-checkWhy it matters: Without an explicit ordering standard, AI assistants
produce inconsistent annotation order across files in the same project.
That is pure noise in code review — the reviewer has to determine whether
@Transactional appearing before @Service is intentional. Enforcing
a single order in AGENTS.md eliminates that class of comment entirely.
Convention: Lombok → Spring stereotype → Spring configuration → Custom
// Class-level: Lombok -> Spring stereotype -> Config
@Slf4j // Lombok
@RequiredArgsConstructor // Lombok
@Service // Spring stereotype
@Transactional(readOnly = true) // Spring config
public class PersonServiceImpl implements PersonService {
// Method-level: Transaction -> Override -> Custom
@Transactional // Spring
@Override // Java
public PersonResponse create(@NonNull @Valid CreatePersonRequest request) {
log.debug("create: request={}", request);
// ...
}
}## Testing Conventions
### Test Class Structure
- Extend AbstractComponentTest for integration tests
- Use @Nested classes to group tests by method
- Each @Nested class tests one public method
### Naming
- Test class: {ClassName}Test (e.g., PersonServiceTest)
- Test method: methodName_stateUnderTest_ExpectedBehavior
### Data
- Use ObjectMother pattern for builders
- Never hardcode UUIDs or timestamps
- Use @Sql or Liquibase for database state setup
### Assertions
- Use AssertJ for fluent assertions
- Use MockMvc for controller tests
- Use @Transactional on tests to auto-rollback
### Coverage
- Minimum 80% line coverage
- 100% coverage on service layer business logic
- Controller tests must verify HTTP status codes and response structureA complete AGENTS.md for a Java project should include:
- Project metadata (Java version, Spring Boot version, package)
- Build commands (clean, test, coverage, single test)
- Architecture layers with hard rules
- Code style (imports, formatting, naming)
- Annotation ordering
- DTO strategy (records, validation)
- Error handling patterns
- Logging conventions
- Testing patterns (naming, data, assertions)
- Maven dependency rules
See templates/AGENTS.md for a complete example.
Create these skills for your project:
| Skill Name | Purpose |
|---|---|
code-review |
Review against project conventions |
generate-tests |
Create JUnit 5 tests with your patterns |
spring-endpoint |
Scaffold full endpoint (Controller -> Test) |
refactor-java21 |
Modernize code to Java 21 idioms |
mapstruct-mapper |
Generate MapStruct mappers for entities |
Applying prompt engineering principles (see Section 3: Prompting) is especially important for multi-layer Java features where the AI must produce coherent code across controllers, services, repositories, and entities:
-
Decompose multi-layer features (Principle 3) -- Instead of "create the Order feature," break it into sequential prompts: schema first, then entities, then repository, then service, then controller. Each prompt builds on the previous output and stays within a focused scope.
-
Use "think step by step" for complex logic (Principle 12) -- When asking the AI to implement business rules, validation chains, or transactional workflows, prepend "Think step by step" to get the model to reason through edge cases before writing code.
Think step by step about the order cancellation flow: 1. What states allow cancellation? 2. What happens to inventory reservations? 3. What events should be emitted? 4. What should the rollback behavior be? Then implement OrderServiceImpl.cancel(). -
Provide few-shot examples of your patterns (Principle 7) -- When generating a new endpoint, include an existing endpoint from your codebase as a reference example. The model will mirror the patterns (annotation ordering, DTO style, error handling) of the example rather than inventing its own.
-
Use affirmative architecture rules (Principle 4) -- In your
AGENTS.mdarchitecture section, phrase every rule as an affirmative directive. "Controllers return DTO records" is more reliably followed than "Controllers don't return entities."
These four refactoring patterns target areas where AI assistants excel with Java 21. Each includes a before/after example and a ready-to-use prompt.
Traditional POJOs with boilerplate can be collapsed into records while preserving Jackson serialization and validation behavior.
Before (pre-Java 16):
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PersonDto {
private UUID id;
private String firstName;
private String lastName;
private String email;
private LocalDate birthDate;
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}After (Java 21 record):
public record PersonDto(
UUID id,
@NotBlank String firstName,
@NotBlank String lastName,
@Email String email,
@Past LocalDate birthDate
) {
public PersonDto {
firstName = firstName.strip();
lastName = lastName.strip();
}
}Prompt:
Refactor PersonDto from a class to a Java 21 record. Preserve Jackson
serialization compatibility. Add a compact canonical constructor that
strips whitespace from firstName and lastName. Move Bean Validation
annotations to record components. Show me any callers that need updating.
Replace fragile instanceof + cast chains with sealed interfaces and exhaustive pattern matching switches.
Before:
public BigDecimal calculateFee(Payment payment) {
if (payment instanceof CreditCardPayment) {
CreditCardPayment cc = (CreditCardPayment) payment;
return cc.getAmount().multiply(CREDIT_CARD_FEE_RATE);
} else if (payment instanceof BankTransferPayment) {
BankTransferPayment bt = (BankTransferPayment) payment;
return bt.getAmount().compareTo(BANK_TRANSFER_THRESHOLD) > 0
? FLAT_FEE : BigDecimal.ZERO;
} else if (payment instanceof CryptoPayment) {
CryptoPayment crypto = (CryptoPayment) payment;
return crypto.getGasFee();
} else {
throw new IllegalArgumentException("Unknown payment type: " + payment);
}
}After:
public sealed interface Payment permits CreditCardPayment, BankTransferPayment, CryptoPayment {
BigDecimal amount();
}
public record CreditCardPayment(BigDecimal amount, String cardLast4) implements Payment {}
public record BankTransferPayment(BigDecimal amount, String iban) implements Payment {}
public record CryptoPayment(BigDecimal amount, BigDecimal gasFee) implements Payment {}
public BigDecimal calculateFee(Payment payment) {
return switch (payment) {
case CreditCardPayment cc -> cc.amount().multiply(CREDIT_CARD_FEE_RATE);
case BankTransferPayment bt -> bt.amount().compareTo(BANK_TRANSFER_THRESHOLD) > 0
? FLAT_FEE : BigDecimal.ZERO;
case CryptoPayment crypto -> crypto.gasFee();
};
}Prompt:
Refactor the PaymentProcessor.calculateFee() method to use a sealed
interface + pattern matching switch. The current code uses instanceof
checks for CreditCardPayment, BankTransferPayment, and CryptoPayment.
Convert each payment type to a record implementing the sealed interface.
The switch must be exhaustive (no default branch).
Replace fixed thread pools with virtual threads and structured concurrency for I/O-bound workloads.
Prerequisite on JDK 21–23 — see the version table at the top of the Virtual Threads section. Add
--enable-previewto your Maven Compiler and Surefire plugins, or useExecutors.newVirtualThreadPerTaskExecutor()withCompletableFuturefor a flag-free Java 21 alternative.
Before:
@Service
public class OrderServiceImpl implements OrderService {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
@Override
public List<OrderResult> processOrders(List<Order> orders) {
List<Future<OrderResult>> futures = new ArrayList<>();
for (Order order : orders) {
futures.add(executor.submit(() -> processOne(order)));
}
List<OrderResult> results = new ArrayList<>();
for (Future<OrderResult> f : futures) {
try {
results.add(f.get());
} catch (Exception e) {
results.add(OrderResult.failed(e));
}
}
return results;
}
}After:
@Service
public class OrderServiceImpl implements OrderService {
@Override
public List<OrderResult> processOrders(List<Order> orders)
throws InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
List<StructuredTaskScope.Subtask<OrderResult>> subtasks = orders.stream()
.map(order -> scope.fork(() -> processOne(order)))
.toList();
scope.join().throwIfFailed();
return subtasks.stream()
.map(StructuredTaskScope.Subtask::get)
.toList();
}
}
}Prompt:
Refactor OrderService.processOrders() to use virtual threads with
StructuredTaskScope. The current implementation uses a fixed thread
pool of 10 platform threads. Requirements:
1. Use StructuredTaskScope.ShutdownOnFailure for error handling.
2. Remove the fixed thread pool -- virtual threads are created per task.
3. Ensure proper resource cleanup with try-with-resources.
4. Keep the same method signature and return type.
Replace mutation-heavy for-loops with declarative stream pipelines that are easier to parallelize and test.
Before:
public MonthlyReport generateMonthlyReport(List<Order> orders, YearMonth month) {
BigDecimal totalRevenue = BigDecimal.ZERO;
int completedCount = 0;
int cancelledCount = 0;
Map<String, BigDecimal> revenueByCategory = new HashMap<>();
for (Order order : orders) {
if (!order.getCreatedAt().toLocalDate().getMonth().equals(month.getMonth())) {
continue;
}
if (order.getStatus() == COMPLETED) {
completedCount++;
totalRevenue = totalRevenue.add(order.getTotal());
String cat = order.getCategory();
revenueByCategory.merge(cat, order.getTotal(), BigDecimal::add);
} else if (order.getStatus() == CANCELLED) {
cancelledCount++;
}
}
return new MonthlyReport(totalRevenue, completedCount, cancelledCount, revenueByCategory);
}After:
public MonthlyReport generateMonthlyReport(List<Order> orders, YearMonth month) {
var monthlyOrders = orders.stream()
.filter(o -> YearMonth.from(o.getCreatedAt()).equals(month))
.toList();
var totalRevenue = monthlyOrders.stream()
.filter(o -> o.getStatus() == COMPLETED)
.map(Order::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
var completedCount = monthlyOrders.stream()
.filter(o -> o.getStatus() == COMPLETED)
.count();
var cancelledCount = monthlyOrders.stream()
.filter(o -> o.getStatus() == CANCELLED)
.count();
var revenueByCategory = monthlyOrders.stream()
.filter(o -> o.getStatus() == COMPLETED)
.collect(Collectors.groupingBy(
Order::getCategory,
Collectors.reducing(BigDecimal.ZERO, Order::getTotal, BigDecimal::add)
));
return new MonthlyReport(totalRevenue, (int) completedCount, (int) cancelledCount, revenueByCategory);
}Prompt:
Refactor the report generation in ReportService.generateMonthlyReport()
from imperative loops to a Stream pipeline. The current code uses for-loops
with mutable accumulators. Requirements:
1. Filter by month using YearMonth.from().
2. Use Collectors.groupingBy for revenue-by-category.
3. Keep the same MonthlyReport return type.
4. Do not introduce parallel streams unless the dataset exceeds 10K items.
When asking the AI to generate tests, include these conventions in
your prompt or AGENTS.md to avoid rework:
| Convention | Rule |
|---|---|
| Test class naming | {ClassName}Test (e.g., PersonServiceTest) |
| Test method naming | methodName_stateUnderTest_expectedBehavior |
| Structure | Given/When/Then with // given, // when, // then |
| Assertions | AssertJ (assertThat(...)) over JUnit assertions |
| Mocking | Mockito with @ExtendWith(MockitoExtension.class) |
| Integration tests | @SpringBootTest with @Testcontainers |
| Controller tests | @WebMvcTest({ControllerClass}.class) slice |
| Repository tests | @DataJdbcTest with Testcontainers PostgreSQL |
| Test data | ObjectMother pattern -- never hardcode UUIDs/timestamps |
| Coverage target | 80% line overall, 100% on service business logic |
Example prompt incorporating these conventions:
Generate tests for PersonServiceImpl. Follow these rules:
- Test class: PersonServiceImplTest
- Naming: methodName_stateUnderTest_expectedBehavior
- Structure: given/when/then with section comments
- Use @ExtendWith(MockitoExtension.class), @Mock, @InjectMocks
- AssertJ assertions only (no assertEquals)
- Cover: happy path, null input, empty result, duplicate detection
- Use ObjectMother.personEntity() for test data
java.util.SequencedCollection is a new interface (JEP 431) that all
ordered collections (List, LinkedHashSet, Deque, etc.) implement.
It adds six methods so you never have to reach for index arithmetic or
iterator().next() to access the first or last element:
List<String> names = List.of("Alice", "Bob", "Carol");
// Before Java 21 (verbose, error-prone)
String first = names.get(0);
String last = names.get(names.size() - 1);
// Java 21
String first = names.getFirst(); // throws NoSuchElementException on empty
String last = names.getLast();
// Reversed view (no copying)
SequencedCollection<String> reversed = names.reversed();Prompt your AI with: "Use SequencedCollection methods (getFirst/getLast/reversed) instead of index-based access in {method}".
| Java 21 Feature | AI Prompt Template | When to Use |
|---|---|---|
| Records | "Refactor {Class} to a Java record. Preserve Jackson compatibility. Show callers that need updating" | Any DTO, value object, or event payload |
| Sealed classes | "Extract sealed interface from {Class} hierarchy. Make the switch exhaustive (no default)" | Domain type hierarchies, event types |
| Pattern matching switch | "Replace instanceof chain in {method} with pattern matching switch using sealed types" | Polymorphic dispatch, type-based routing |
| Virtual threads | "Refactor {method} to use virtual threads with StructuredTaskScope. Remove the fixed thread pool" | I/O-bound parallel work, HTTP clients |
| Text blocks | "Convert multi-line string concatenation in {method} to a text block" | SQL queries, JSON templates, HTML |
| Stream + Collectors | "Refactor for-loop in {method} to a Stream pipeline with Collectors.groupingBy" | Aggregation, filtering, transformation |
Optional improvements |
"Replace null checks in {method} with Optional.map/flatMap chain" | Nullable return values, optional params |
SequencedCollection |
"Use SequencedCollection methods (getFirst/getLast/reversed) instead of index-based access in {method}" | First/last element access in lists |
Database changes are the only changes in a software project that can lose production data permanently. A bad deployment rolls back in 90 seconds; a bad migration that drops a column or runs without a transaction cannot be undone by a redeploy.
AI assistants generate plausible-looking SQL and Liquibase changesets quickly, but "plausible" is not "safe". Without explicit guidance they will:
- Use
VARCHARwhere your schema convention usesTEXT, causing silent truncation at the ORM layer. - Omit
runInTransaction="false"onCREATE INDEX CONCURRENTLY, making the changeset fail at startup. - Generate
HikariCPpool sizes using rules of thumb that do not fit your connection limits. - Write JDBC-style queries when your team has standardised on Spring Data JDBC repositories.
- Suggest
@Querywith JPQL when your project uses native SQL exclusively.
This section exists because one bad AI-generated migration reaching production costs more than everything you saved in prompt round-trips that sprint. The configuration patterns here are a safety net, not a convenience.
## Database
### Technology
- Primary: PostgreSQL 15+
- Legacy: Oracle 19c (some services)
- Migrations: Liquibase (XML format)
- ORM: Spring Data JDBC (not JPA/Hibernate)
- Connection pool: HikariCP (Spring Boot default)
### Schema Conventions
- Table names: snake_case, plural (persons, orders, order_items)
- Column names: snake_case (first_name, created_at)
- Primary key: `id` (UUID type, generated by application)
- Foreign keys: {referenced_table_singular}_id (person_id)
- Timestamps: created_at, updated_at (TIMESTAMPTZ in PostgreSQL)
- Soft delete: deleted_at (nullable TIMESTAMPTZ)
- Indexes: idx_{table}_{column(s)} (idx_persons_email)
- Unique constraints: uq_{table}_{column(s)}
- Check constraints: chk_{table}_{description}
### Data Types (PostgreSQL)
| Concept | PostgreSQL Type | Java Type |
|--------------|--------------------|-------------------|
| ID | UUID | java.util.UUID |
| String | VARCHAR(n) | String |
| Long text | TEXT | String |
| Integer | INTEGER | int / Integer |
| Money | NUMERIC(19,4) | BigDecimal |
| Boolean | BOOLEAN | boolean |
| Timestamp | TIMESTAMPTZ | Instant |
| Date | DATE | LocalDate |
| Enum | VARCHAR(50) | enum (Java) |
| JSON | JSONB | String / JsonNode |
### Data Types (Oracle Equivalents)
| PostgreSQL | Oracle Equivalent |
|---------------|--------------------|
| UUID | RAW(16) |
| VARCHAR(n) | VARCHAR2(n) |
| TEXT | CLOB |
| BOOLEAN | NUMBER(1) |
| TIMESTAMPTZ | TIMESTAMP WITH TIME ZONE |
| JSONB | CLOB + IS JSON |
| SERIAL | GENERATED ALWAYS AS IDENTITY |
### Liquibase Conventions
- Format: XML
- Changelog master: db/changelog/db.changelog-master.xml
- Changelog files: db/changelog/changes/YYYY-MM-DD-NNN-description.xml
- Author: developer username
- Always include rollback
- One logical change per changeset
- Use preconditions for safety
- `CREATE INDEX CONCURRENTLY` requires `runInTransaction="false"` on the changeSet — PostgreSQL forbids CONCURRENTLY inside a transaction; omitting this attribute crashes Liquibase startup<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="2026-04-04-001-add-email-to-persons"
author="developer">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="persons" columnName="email"/>
</not>
</preConditions>
<addColumn tableName="persons">
<column name="email" type="VARCHAR(255)">
<constraints nullable="true"/>
</column>
</addColumn>
<createIndex indexName="idx_persons_email"
tableName="persons">
<column name="email"/>
</createIndex>
<rollback>
<dropIndex indexName="idx_persons_email"
tableName="persons"/>
<dropColumn tableName="persons" columnName="email"/>
</rollback>
</changeSet>
</databaseChangeLog>| Operation | Liquibase Tag | Notes |
|---|---|---|
| Create table | <createTable> |
Always include PK and timestamps |
| Add column | <addColumn> |
Include precondition |
| Drop column | <dropColumn> |
Dangerous -- precondition required |
| Add index | <createIndex> |
Name: idx_{table}_{cols} |
| Add foreign key | <addForeignKeyConstraint> |
Name: fk_{src}_{ref} |
| Add unique | <addUniqueConstraint> |
Name: uq_{table}_{cols} |
| Data migration | <sql> |
Use CDATA, include rollback |
| Rename column | <renameColumn> |
Check all queries first |
## Migration Safety Rules
### ALWAYS
- Include rollback statements
- Use preconditions to prevent double-execution
- Test migrations against a fresh database AND existing data
- Run ./mvnw liquibase:validate before committing
### NEVER
- Drop a column without a precondition check
- Rename a table in production without a multi-step migration
- Add NOT NULL without a default value (breaks existing rows)
- Modify existing changeset IDs (Liquibase checksums will fail)
- Use auto-generated IDs (use descriptive IDs like dates)## SQL Performance Rules
### Indexing
- Every foreign key column must be indexed
- Columns used in WHERE clauses frequently should be indexed
- Composite indexes: most selective column first
- Don't over-index: each index slows writes
### Query Patterns
- Use EXPLAIN ANALYZE to verify query plans
- Prefer JOIN over subquery for correlated queries
- Use EXISTS instead of IN for large subsets
- Avoid SELECT * -- list required columns explicitly
- Use LIMIT/OFFSET or keyset pagination (not OFFSET for large tables)
### Spring Data JDBC Queries
- Use method naming for simple queries: findByEmail(String email)
- Use @Query for complex queries with named parameters
- Never concatenate SQL strings
- Use RowMapper for custom result mapping### PostgreSQL Tips
- Use JSONB for flexible schema fields (not JSON)
- Use partial indexes for filtered queries:
CREATE INDEX idx_active_persons ON persons(email) WHERE deleted_at IS NULL;
- Use CONCURRENTLY for index creation in production
- Use pg_stat_user_tables to find missing indexes
- Use VACUUM ANALYZE after large data changes
- Connection pool: HikariCP with maximumPoolSize = (CPU cores * 2) + disk spindles### Oracle Tips
- Use Oracle hints sparingly and document when used
- Prefer FETCH FIRST N ROWS instead of ROWNUM for pagination
- Use bind variables (Spring parameterized queries handle this)
- Check execution plans with EXPLAIN PLAN FOR
- Use AWR reports for production query analysis
- RAW(16) for UUID storage (convert in application layer)---
name: db-migration
description: >
Generate Liquibase database migration changelogs. Use when asked to
create migrations, add columns, create tables, add indexes, or modify
database schema. Handles PostgreSQL and Oracle syntax.
---
## Instructions
When generating Liquibase changelogs:
1. Use XML format
2. Changeset ID: YYYY-MM-DD-NNN-description
3. Author: use "ai-assistant" (developer will update)
4. Always include preconditions
5. Always include rollback
6. Use project naming conventions for tables, columns, indexes
7. Place file in: src/main/resources/db/changelog/changes/
8. Add include to db.changelog-master.xml
## PostgreSQL Types
Use PostgreSQL types by default. Add Oracle alternatives in comments.
## Safety Checks
Before generating a destructive migration (DROP, RENAME):
1. Warn the user about data loss risk
2. Suggest a safer multi-step approach
3. Include preconditions to prevent accidental executionA step-by-step workflow for optimizing SQL queries with AI assistance:
-
Capture the slow query: Get the actual SQL from application logs or
pg_stat_statements:SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;
-
Get the execution plan: Run
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)and copy the full output:EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT p.id, p.first_name, p.last_name, p.email FROM persons p WHERE p.status = 'ACTIVE' AND p.created_at > '2025-01-01' ORDER BY p.last_name LIMIT 20;
-
Ask AI to analyze: Paste the plan and add context:
"Analyze this PostgreSQL execution plan. Identify the bottleneck and suggest index changes. The
personstable has 2M rows, roughly 60% are ACTIVE. We use Spring Data JDBC." -
Review suggested indexes: Before applying, verify against existing indexes:
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'persons';
-
Generate migration: Use the
/migrationcommand:"/migration — add a composite index on persons(status, created_at, last_name) for the active-persons query optimization"
-
Benchmark: Re-run the query and compare execution plans. Confirm the planner uses the new index.
Slow query — fetching recently created active persons:
-- Before: Seq Scan on persons (cost=0.00..98754.00 rows=612000 width=128)
-- Planning Time: 0.2ms Execution Time: 1340ms
SELECT id, first_name, last_name, email
FROM persons
WHERE status = 'ACTIVE'
AND created_at > '2025-01-01'
ORDER BY last_name
LIMIT 20;AI-suggested optimization:
-- Composite index covering the WHERE and ORDER BY
CREATE INDEX CONCURRENTLY idx_persons_status_created_last
ON persons (status, created_at, last_name);
-- After: Index Scan using idx_persons_status_created_last
-- Planning Time: 0.3ms Execution Time: 2.1msGenerated Liquibase changeset:
<!--
IMPORTANT: CREATE INDEX CONCURRENTLY cannot run inside a transaction.
runInTransaction="false" is mandatory — Liquibase will fail at startup
if you omit it and PostgreSQL rejects the statement.
-->
<changeSet id="2026-04-10-001-idx-persons-active-query"
author="ai-assistant"
runInTransaction="false">
<preConditions onFail="MARK_RAN">
<not>
<indexExists indexName="idx_persons_status_created_last"/>
</not>
</preConditions>
<sql>
CREATE INDEX CONCURRENTLY idx_persons_status_created_last
ON persons (status, created_at, last_name);
</sql>
<rollback>
-- CONCURRENTLY cannot run in a transaction either, so we use the
-- blocking form for rollback (acceptable: rollbacks run in maintenance windows).
DROP INDEX IF EXISTS idx_persons_status_created_last;
</rollback>
</changeSet>Six common anti-patterns and the prompts to detect them:
Occurs when a parent query triggers individual queries for each child.
Prompt: "Review this Spring Data JDBC repository and service layer. Check for N+1 query patterns — places where we load a list of entities and then query related data in a loop. Suggest
@Querywith JOINs or batch loading instead."
Every foreign key column should be indexed for JOIN performance.
Prompt: "Compare my schema against this rule: every column ending in
_id(foreign key) must have an index. List columns that violate this rule and generate Liquibase changesets to add the missing indexes."
Fetches unnecessary columns, wastes network and memory.
Prompt: "Scan all
@Queryannotations and SQL statements in the repository layer. Flag any that useSELECT *and rewrite them to select only the columns needed by the calling service method."
Columns that should never be null lack database-level enforcement.
Prompt: "Review the persons table schema. Identify columns that logically should never be null (e.g., first_name, last_name, status, created_at) but are currently nullable. Generate a Liquibase migration to add NOT NULL constraints with appropriate default values for existing rows."
Using wrong types leads to implicit casts and index bypasses.
Prompt: "Check the schema for data type mismatches: VARCHAR used for dates or timestamps, INTEGER used for monetary values, TEXT used where VARCHAR with a length constraint would be safer. Suggest corrections with migration changesets."
Changesets without rollback blocks make releases irreversible.
Prompt: "Scan all Liquibase changelog files in
src/main/resources/db/changelog/changes/. List any changesets that are missing<rollback>blocks and generate the missing rollback statements for each."
The /schema-review command performs a comprehensive schema health check:
/schema-review
Analyze the database schema defined in the Liquibase changelogs
under `src/main/resources/db/changelog/changes/` and the current
database DDL. Check for these issues:
### Naming Convention Violations
- Tables must be snake_case, plural (persons, order_items)
- Columns must be snake_case (first_name, created_at)
- Primary keys must be named `id`
- Foreign keys must follow {referenced_table_singular}_id pattern
- Indexes: idx_{table}_{columns}
- Unique constraints: uq_{table}_{columns}
### Missing Indexes
- Every foreign key column (_id suffix) must have an index
- Columns used in WHERE clauses of frequent queries should be indexed
- Composite indexes should have the most selective column first
### Nullable Columns That Should Be NOT NULL
- Primary keys (id)
- Timestamps (created_at, updated_at)
- Status/type columns (status, type)
- Core business fields (first_name, last_name for persons)
### Missing Foreign Key Constraints
- Columns ending in _id should have a corresponding foreign key constraint
- Verify ON DELETE behavior (CASCADE vs RESTRICT vs SET NULL)
### Table/Column Documentation Gaps
- Every table should have a COMMENT ON TABLE
- Non-obvious columns should have a COMMENT ON COLUMN
- Enum-like VARCHAR columns should document valid values
Output:
1. A table of all issues found (severity, table, column, issue, fix)
2. Liquibase changesets to fix each issue
3. Summary statistics (tables checked, issues by severity)| Add column | "/migration — add {column} ({type}) to {table}" | AI may omit precondition or rollback |
| Add index | "Create an index on {table}({columns}) for optimizing {query description}" | AI may not use CONCURRENTLY for production indexes |
| Optimize query | "Analyze this EXPLAIN plan: {plan}. Table has {N} rows." | AI may suggest indexes without checking existing ones |
| Schema review | "/schema-review" | AI may miss cross-table relationship issues |
| Generate migration | "/migration — {description}" | AI may use wrong changelog ID format |
| N+1 check | "Check repository {name} for N+1 query patterns" | AI may miss indirect N+1 through service-layer loops |
| Oracle migration | "Generate this migration for Oracle 19c: {PG changeset}" | AI may forget RAW(16) for UUID or NUMBER(1) for BOOLEAN |
DevOps YAML is deceptively simple to write and expensive to debug.
A gitlab-ci.yml with a wrong stage name silently orphans a job.
A Kubernetes manifest with the wrong resource units deploys, runs
fine in staging, and OOMKills in production under real traffic.
These are not hypothetical risks — they are the most common class of incident in teams that adopt AI-generated infrastructure without a verification layer. The failure mode is insidious: the AI is approximately correct, which means the error survives review.
There are two structural problems AI assistants have with DevOps work:
-
Your environment is unique. Every company has different shared CI template paths, different Helm chart conventions, different Kubernetes namespace and resource quota policies. Generic AI training data is useless for this. Until you teach the AI your specific environment, every suggestion needs full manual verification.
-
The feedback loop is slow. A wrong Java class fails in seconds. A wrong CI pipeline fails in 10–20 minutes, after it has already consumed a runner. A wrong Kubernetes manifest may not fail at all until load hits it. AI is most useful here when it generates debuggable artefacts, not when it produces opaque YAML.
This section shows you how to close both gaps: give the AI enough context about your infrastructure that its suggestions are accurate, and use it as a debugging partner that explains its reasoning step by step.
Add a GitLab CI section to AGENTS.md:
## GitLab CI
### Pipeline Structure
- Configuration: .gitlab-ci.yml in project root
- Uses shared templates via `include:`
- Project-specific variables defined under `variables:`
### Template Pattern
\```yaml
include:
- project: 'platform/ci-templates'
ref: main
file:
- '/templates/java-maven.yml'
- '/templates/docker-build.yml'
- '/templates/helm-deploy.yml'
variables:
JAVA_VERSION: "21"
CHART_NAME: "my-service"
MAVEN_OPTS: "-Xmx1024m"
\```
### Stages
1. build: Compile and package (./mvnw clean package)
2. test: Run tests with coverage (./mvnw test jacoco:report)
3. docker: Build and push container image
4. deploy-dev: Deploy to dev via Helm
5. deploy-staging: Deploy to staging (manual trigger)
6. deploy-prod: Deploy to prod (manual trigger + approval)
### Rules
- Never hardcode secrets in .gitlab-ci.yml
- Use CI/CD variables for environment-specific values
- Always include the shared templates; do not duplicate their logic
- Use `rules:` (not `only:`/`except:`) for job conditionsCreate a skill for generating CI pipeline configurations:
---
name: gitlab-ci
description: >
Generate or modify GitLab CI pipeline configurations. Use when asked
to create pipelines, add CI stages, fix CI failures, or configure
deployment jobs. Covers .gitlab-ci.yml syntax and best practices.
---
## Instructions
When generating GitLab CI configurations:
1. Always use `include:` for shared templates
2. Use `rules:` instead of `only:`/`except:`
3. Define variables at the top level
4. Use anchors (&, *) for repeated configuration
5. Never embed secrets -- use $CI_VARIABLE_NAME
6. Add `needs:` for parallel job execution
7. Use `artifacts:` to pass data between stages
8. Include `cache:` for Maven dependencies
## Template
\```yaml
include:
- project: 'platform/ci-templates'
ref: main
file: '/templates/java-maven.yml'
variables:
CHART_NAME: "${SERVICE_NAME}"
stages:
- build
- test
- deploy
\```Create /ci-fix to help debug pipeline failures:
Analyze the GitLab CI pipeline failure I'm describing.
1. Identify the failing stage and job
2. Determine root cause (compilation, test, Docker, deployment)
3. Suggest a fix with the exact YAML change needed
4. If the fix requires a new CI variable, document it
Consider common issues:
- Maven dependency resolution failures (check mirrors/proxies)
- Docker build context issues (check .dockerignore)
- Helm deployment failures (check values files and secrets)
- Network issues (check runner tags and network policies)Add Kubernetes patterns to AGENTS.md:
## Kubernetes / Helm
### Helm Chart Structure
\```
.helm/
├── Chart.yaml
├── values.yaml # Default values
├── values-dev.yaml # Dev overrides
├── values-staging.yaml # Staging overrides
├── values-prod.yaml # Production overrides
└── templates/
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── configmap.yaml
├── secret.yaml
└── hpa.yaml
\```
### Deployment Requirements
- Always include liveness and readiness probes
- Set resource requests AND limits
- Use securityContext (non-root, readOnlyRootFilesystem)
- Environment variables from ConfigMap/Secret references
- Pod disruption budget for production
- Horizontal Pod Autoscaler for production
### Probe Configuration
\```yaml
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
initialDelaySeconds: 10
periodSeconds: 5
\```
### Resource Defaults
\```yaml
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
\```
### Naming Convention
- Release name: {service-name}-{environment}
- Labels: app.kubernetes.io/name, app.kubernetes.io/instance
- Annotations: team, contact, cost-center---
name: helm-template
description: >
Generate or modify Helm chart templates for Kubernetes deployments.
Use when asked to create K8s manifests, add probes, configure
resources, or set up ingress. Follows project Helm conventions.
---
## Instructions
When generating Helm templates:
1. Use .helm/ directory structure (not charts/)
2. Always include liveness and readiness probes using Spring Actuator
3. Set resource requests and limits
4. Use securityContext with non-root user
5. Environment variables via configMapRef and secretRef
6. Use {{ .Values.xxx }} for all configurable values
7. Include proper labels (app.kubernetes.io/*)
8. Add comments for non-obvious configurationsCreate /k8s-debug for troubleshooting:
Help me debug a Kubernetes deployment issue.
Analyze the symptoms I describe and suggest:
1. Diagnostic kubectl commands to run
2. Common root causes for the symptom
3. Fixes ranked by likelihood
Common patterns:
- CrashLoopBackOff: Check logs, probe config, resource limits, JVM heap
- OOMKilled: Increase memory limit, check JVM -Xmx setting
- ImagePullBackOff: Check image name, registry credentials, tag
- Pending pod: Check node resources, node selector, tolerations
- Readiness probe failure: Check actuator endpoint, startup timeTeach the AI how your environments differ:
## Environment Differences
| Setting | Dev | Staging | Production |
|-----------------|----------------|------------------|------------------|
| Replicas | 1 | 2 | 3+ |
| Resources (CPU) | 100m / 500m | 200m / 1 | 500m / 2 |
| Resources (Mem) | 256Mi / 512Mi | 512Mi / 1Gi | 1Gi / 2Gi |
| HPA | disabled | min:2, max:4 | min:3, max:10 |
| Ingress TLS | self-signed | Let's Encrypt | corporate CA |
| Log level | DEBUG | INFO | WARN |
| DB pool size | 5 | 10 | 20 |When a CI pipeline fails, follow this structured workflow to get the most accurate help from your AI assistant:
-
Copy the error output from GitLab CI — include the full job log, not just the last line. Relevant context often appears earlier.
-
Include the relevant
.gitlab-ci.ymlsection — the AI needs to see the job definition, variables, and anyinclude:references. -
Ask with context:
"This GitLab CI job failed in the 'test' stage. Here's the error output and the job definition. Diagnose the root cause and suggest a fix."
[ERROR] Failed to execute goal org.apache.maven.plugins: maven-surefire-plugin:3.2.5:test ... Timeouttest: stage: test script: - ./mvnw test jacoco:report artifacts: reports: junit: target/surefire-reports/*.xml
| Failure Type | Symptoms | Key Context to Include |
|---|---|---|
| Maven dependency resolution | Could not resolve dependencies, Non-resolvable parent POM |
pom.xml dependencies section, settings.xml mirror config |
| Docker build | COPY failed, RUN apt-get ... E: Unable to locate package |
Dockerfile, .dockerignore, base image version |
| Kubernetes deployment | ImagePullBackOff, ErrImagePull |
Image name/tag, registry URL, deployment YAML |
| Helm template rendering | Error: template ... not defined, cannot load values |
values.yaml, relevant template file, Chart.yaml |
| Test failures in CI only | Tests pass locally but fail in CI | CI runner environment, JVM version, MAVEN_OPTS, timezone settings |
My GitLab CI pipeline failed. Here's the context:
**Stage**: {stage name}
**Job**: {job name}
**Runner tags**: {runner tags if relevant}
**Error output**:
\```
{paste error log}
\```
**Job definition** (.gitlab-ci.yml):
\```yaml
{paste job YAML}
\```
**Relevant files** (if applicable):
- Dockerfile / pom.xml / Helm values
Diagnose the root cause. Suggest a fix with the exact file changes needed.
If the fix requires a new CI variable, specify the variable name and
expected value.How to generate a complete deployment pipeline with AI assistance:
"I need a GitLab CI pipeline for a Spring Boot 3.5 app that:
- Builds with Maven 3.9+ and Java 21
- Runs unit tests with JaCoCo coverage (minimum 80%)
- Builds a Docker image and pushes to our GitLab Container Registry
- Deploys to K8s via Helm with environment-specific values
- Has manual approval gates for staging and production
- Caches Maven dependencies between runs
- Uses our shared templates from platform/ci-templates"
The AI should produce a .gitlab-ci.yml similar to:
include:
- project: 'platform/ci-templates'
ref: main
file:
- '/templates/java-maven.yml'
- '/templates/docker-build.yml'
- '/templates/helm-deploy.yml'
variables:
JAVA_VERSION: "21"
CHART_NAME: "my-service"
MAVEN_OPTS: "-Xmx1024m -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
stages:
- build
- test
- docker
- deploy-staging
- deploy-production
cache:
key: "${CI_COMMIT_REF_SLUG}"
paths:
- .m2/repository/
build:
stage: build
script:
- ./mvnw clean package -DskipTests
artifacts:
paths:
- target/*.jar
expire_in: 1 hour
test:
stage: test
script:
- ./mvnw test jacoco:report
- awk -F ',' '{ missed += $4; covered += $5 }
END { pct = 100 * covered / (missed + covered);
printf "Coverage: %.1f%%\n", pct;
if (pct < 80) exit 1 }' target/site/jacoco/jacoco.csv
artifacts:
reports:
junit: target/surefire-reports/*.xml
paths:
- target/site/jacoco/
docker:
stage: docker
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy-staging:
stage: deploy-staging
script:
- helm upgrade --install $CHART_NAME-staging .helm/
--values .helm/values-staging.yaml
--set image.tag=$CI_COMMIT_SHA
--namespace staging
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
deploy-production:
stage: deploy-production
script:
- helm upgrade --install $CHART_NAME-prod .helm/
--values .helm/values-prod.yaml
--set image.tag=$CI_COMMIT_SHA
--namespace production
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
allow_failure: falseAsk the AI to generate values files for each environment:
"Generate Helm values files for staging and production using our environment differences table (replicas, resources, HPA, log level, DB pool size). Use the project's values structure."
For production deployments, ensure the pipeline includes:
when: manualon the production deploy joballow_failure: falseto block the pipeline until approved- Consider adding a
needs:dependency on staging deployment - Optionally configure GitLab protected environments for additional control
Understanding the failure modes lets you prompt defensively and review AI output faster.
| Mistake | Why It Happens | How to Catch It |
|---|---|---|
Job references a stage not in stages: list |
AI forgets to sync the stage list with new jobs | gitlab-ci-lint or yq '.stages' .gitlab-ci.yml |
| Cache key too broad | AI uses $CI_COMMIT_REF_SLUG — different branches never share cache |
Check key: is $CI_COMMIT_REF_SLUG-$CI_JOB_NAME |
needs: creates a cycle |
AI adds needs: dependencies without checking direction |
Run gitlab-ci-lint — it catches DAG cycles |
artifacts: expire_in omitted |
Artifacts accumulate indefinitely, filling runner storage | Always specify expire_in: 1 hour for build artefacts |
Docker build uses latest tag |
AI defaults to :latest — no traceability, breaks rollback |
Always use $CI_COMMIT_SHA as the image tag |
Production deploy is not when: manual |
AI generates when: on_success by default |
Verify every production job has when: manual |
| Resource requests absent from Helm templates | AI omits resources: — pod gets BestEffort QoS class |
Always verify requests and limits are both set |
initialDelaySeconds too short |
AI guesses 30 s; JVM warm-up on cold nodes often takes 60–90 s | Test actual startup time with kubectl logs -f |
Prompt to validate generated pipelines:
Review this .gitlab-ci.yml for common mistakes:
1. Are all job stage names present in the top-level stages list?
2. Does every job that builds an artifact set expire_in?
3. Are production deploy jobs gated with when: manual?
4. Are there any needs: dependencies that form a cycle?
5. Is the Docker image tagged with the commit SHA, not latest?
{paste .gitlab-ci.yml content}
How to use AI during a production incident to accelerate diagnosis:
Collect these outputs before asking the AI:
# Pod logs (last 100 lines)
kubectl logs -n prod deployment/my-service --tail=100
# Pod description (events, resource usage, restart count)
kubectl describe pod -n prod <pod-name>
# Resource usage
kubectl top pod -n prod -l app=my-service
# Recent events in the namespace
kubectl get events -n prod --sort-by='.lastTimestamp' | tail -20"This Spring Boot service in Kubernetes is crashing with OOMKilled. Here are the logs, pod description, and resource metrics.
Pod logs:
{paste logs}Pod describe (relevant section):
{paste describe output — especially Last State, Restart Count, Limits}Resource usage:
{paste kubectl top output}What's the likely cause? How should I adjust the JVM and container resources? Current settings: container limit 1Gi, JVM -Xmx not set."
| Symptom | Likely Cause | AI Can Help With |
|---|---|---|
| OOMKilled | JVM heap exceeds container memory limit | Calculating correct -Xmx relative to container limit (75% rule) |
| CrashLoopBackOff | Application fails to start, probe timeout | Analyzing startup logs, adjusting initialDelaySeconds |
| High latency | Connection pool exhaustion, slow queries | Analyzing thread dumps, HikariCP metrics |
| 5xx errors after deploy | Incompatible schema migration, config error | Comparing new vs old config, checking migration status |
| Pod stuck Pending | Insufficient cluster resources, node affinity | Checking node capacity, suggesting resource adjustments |
Once root cause is identified, ask the AI to generate the fix:
"The root cause is that JVM heap is not bounded and grows beyond the 1Gi container limit. Generate:
- Updated Helm values with correct resource limits and JVM flags
- A ConfigMap entry for JAVA_TOOL_OPTIONS
- The kubectl commands to apply the hotfix now"
| Debug CI failure | "This CI job failed in '{stage}'. Error: {error}. Job YAML: {yaml}" | Full error log, job definition, referenced template names |
| Generate Helm chart | "Generate a Helm chart for {service} following our conventions" | .helm/ structure, probe paths, resource defaults, environment table |
| Debug K8s issue | "/k8s-debug — {symptom}. Logs: {logs}. Describe: {describe}" | Pod logs, describe output, recent events, resource metrics |
| Add CI stage | "Add a {stage-name} stage to this pipeline: {existing yaml}" | Where it fits in stage order, dependencies via needs: |
| Hotfix deploy | "Generate kubectl commands to patch deployment {name} in {namespace}" | Current image tag, new image tag, rollback strategy |
| Security scan | "Add dependency-check and SAST stages to this pipeline" | Approved scanner images, fail thresholds, report formats |
| Helm values | "/helm-values — generate {env} values for {service}" | Environment differences table, service-specific overrides |
Proceed to Section 16: Team Collaboration to learn how to share AI configurations and onboard developers as a team.
- Section 9: AGENTS.md — project context for all three parts
- Section 10: Skills —
db-migration,ci-fix,k8s-debugskills - Section 11: Custom Commands —
/migration,/k8s-debug - Section 6: Context — token budget for large schema/CI prompts
- templates/sample-project/ — reference implementation