Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ projects as a git submodule.

The repository carries three things:

- `rules/` — the rules themselves, as plain markdown. Single source of truth.
- `rules/` — common rules and opt-in profiles, as plain markdown. Single source
of truth.
- `hooks/` — scripts wired into agent lifecycle events (Claude Code and Codex).
- `install.sh` / `check.sh` — wire the above into a consuming project, idempotently.

## What belongs here

Only rules that hold for the whole organization. Anything tied to one service —
its packages, its build quirks, its local conventions — stays in that service's
own `AGENTS.md` / `CLAUDE.md`, outside the synced block.
Top-level files in `rules/` hold rules that apply to the whole organization.
Rules shared by one family of services live in `rules/profiles/` and are selected
by the consuming project. Anything tied to one service — its build quirks and
local conventions — stays in that service's own `AGENTS.md` / `CLAUDE.md`,
outside the synced block.

## Adding to a project

Expand All @@ -25,14 +28,40 @@ git submodule add <repo-url> .agent-rules
`install.sh` is idempotent and touches only what it owns:

- registers the Kotlin format hook in `.claude/settings.json` and `.codex/hooks.json`
- writes `@`-imports of `rules/*` into `CLAUDE.md`
- writes `@`-imports of the selected rule files into `CLAUDE.md`
- syncs the rule text into `AGENTS.md` between `<!-- BEGIN agent-rules -->` and
`<!-- END agent-rules -->`

Everything outside those markers is yours and is never rewritten.

Commit the resulting changes together with the submodule pointer.

## Rule profiles

Without configuration, `install.sh` applies only the common rules. A consuming
project can commit `.agent-rules-profile` with one of these values:

- `common` — common rules only;
- `openapi` — common rules and contract-first OpenAPI conventions;
- `adapter` — common rules and external-adapter conventions.

For example:

```text
openapi
```

The profile can be overridden for a single command. The same option is accepted
by `check.sh`:

```bash
./.agent-rules/install.sh --profile openapi
./.agent-rules/check.sh --profile openapi
```

The command-line value takes precedence over `.agent-rules-profile`. Unknown or
empty profile values are rejected.

## Updating

```bash
Expand All @@ -45,8 +74,9 @@ change under a project without a commit in it.

## Keeping projects honest

`check.sh` is `install.sh --check`: it writes nothing and exits non-zero when a
project has drifted from the submodule it pins. Wire it into CI with
`check.sh` runs `install.sh --check` with the configured profile: it writes
nothing and exits non-zero when a project has drifted from the submodule it pins.
Wire it into CI with
`ci/github-actions/agent-rules-drift.yml` — note the `submodules: true` on
checkout, without it the check runs against an empty directory.

Expand Down
2 changes: 1 addition & 1 deletion check.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash
# CI entry point: fails when the project has drifted from the rules it pins.
exec "$(cd -- "$(dirname -- "$0")" && pwd)/install.sh" --check
exec "$(cd -- "$(dirname -- "$0")" && pwd)/install.sh" --check "$@"
36 changes: 0 additions & 36 deletions code-conventions.md

This file was deleted.

24 changes: 0 additions & 24 deletions database-conventions.md

This file was deleted.

75 changes: 65 additions & 10 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Wires the shared rules into the project that mounts this submodule.
#
# ./.agent-rules/install.sh apply
# ./.agent-rules/install.sh --check report drift, write nothing, exit 1 on drift
# ./.agent-rules/install.sh apply common rules
# ./.agent-rules/install.sh --profile openapi apply a rule profile
# ./.agent-rules/install.sh --check [--profile ...] report drift, write nothing
#
# Everything here is idempotent and owns a bounded piece of each file: the hook
# entries it registered, and the text between the agent-rules markers. Whatever
Expand All @@ -18,15 +19,36 @@ HOOK_MARKER="format-kotlin.sh"

CHECK_ONLY=0
DRIFT=0
PROFILE_OVERRIDE=""

case "${1:-}" in
--check) CHECK_ONLY=1 ;;
"") ;;
*)
printf 'usage: %s [--check]\n' "$0" >&2
exit 64
;;
esac
usage() {
printf 'usage: %s [--check] [--profile common|openapi|adapter]\n' "$0" >&2
}

while [ "$#" -gt 0 ]; do
case "$1" in
--check)
CHECK_ONLY=1
shift
;;
--profile)
case "${2:-}" in
""|--*)
printf 'agent-rules: --profile requires a value\n' >&2
usage
exit 64
;;
esac
PROFILE_OVERRIDE="$2"
shift 2
;;
*)
printf 'agent-rules: unknown argument: %s\n' "$1" >&2
usage
exit 64
;;
esac
done

command -v jq >/dev/null 2>&1 || {
printf 'agent-rules: jq is required\n' >&2
Expand All @@ -47,6 +69,30 @@ case "$RULES_DIR" in
;;
esac

PROFILE="common"
PROFILE_FILE="$PROJECT_ROOT/.agent-rules-profile"

if [ -f "$PROFILE_FILE" ]; then
PROFILE="$(cat "$PROFILE_FILE")"
fi

if [ -n "$PROFILE_OVERRIDE" ]; then
PROFILE="$PROFILE_OVERRIDE"
fi

case "$PROFILE" in
common|openapi|adapter) ;;
"")
printf 'agent-rules: profile in %s is empty\n' "$PROFILE_FILE" >&2
exit 64
;;
*)
printf 'agent-rules: unknown profile: %s\n' "$PROFILE" >&2
printf 'agent-rules: expected common, openapi, or adapter\n' >&2
exit 64
;;
esac

report() {
if [ "$CHECK_ONLY" -eq 1 ]; then
printf 'drift: %s\n' "$1" >&2
Expand Down Expand Up @@ -118,6 +164,15 @@ merge_hooks() {

rule_files() {
find "$RULES_DIR/rules" -maxdepth 1 -name '*.md' -not -name 'index.md' | sort

case "$PROFILE" in
openapi)
printf '%s\n' "$RULES_DIR/rules/profiles/openapi.md"
;;
adapter)
printf '%s\n' "$RULES_DIR/rules/profiles/adapter.md"
;;
esac
}

# Path of a rule file relative to the project root, e.g. .agent-rules/rules/x.md
Expand Down
114 changes: 114 additions & 0 deletions rules/code-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Code conventions

## Architecture and dependencies

- Transport resources handle protocol concerns only: they validate the transport
contract, delegate to a service, and translate failures into protocol errors.
- Services implement business scenarios and define the order of operations.
- Complex changes to aggregate parts are delegated to focused handlers instead of
growing a single service class.
- Repositories encapsulate persistence and return database or domain models. They do
not build transport responses.
- External systems are hidden behind local client or service interfaces; generated
stubs and retry mechanics do not leak into business code.
- Dependencies point toward service and domain abstractions; circular dependencies
between packages or modules are not introduced.
- Spring dependencies are provided through constructor injection and stored in
`final`/`val` fields. Java components use Lombok's `@RequiredArgsConstructor`
instead of handwritten constructors when no custom initialization is required.

## Project structure

- Code is organized into the `config`, `config.properties`, `resource`,
`servlet`, `service`, `repository`, `repository.model`, `scheduler`, `client`,
`client.model`, `converter`, and `extensions` packages.
- Standalone classes and models are placed in separate files.
- Types and members use the narrowest practical visibility. Implementation details
are not exposed only to make tests easier to write.

## DTOs and converters

- External API requests and responses are represented by typed DTOs, without
`Map<String, Any>`.
- Transport models are converted before reaching repositories. Simple entities may
use generated persistence models; aggregates use local domain models.
- JSON property names are specified with Jackson annotations only when they differ
from the corresponding field or property name. Closed sets of values are
represented by enums.
- Model conversion, including creation of requests and responses, is performed by
dedicated `@Component` classes implementing Spring's `Converter<S, T>`.
- Converters map data but do not write to the database or call external systems.
- Optional fields are set only when present. An omitted value and an explicitly
empty value remain distinct when the API contract distinguishes them.
- Unsupported conversion directions fail explicitly instead of returning `null`.
- When a contract schema changes, converters and tests are reviewed so every new field
is either mapped or intentionally ignored.

## REST-to-gRPC gateways

- Generated REST interfaces define the transport contract. Controllers and resources
implement them, validate transport concerns, and delegate without duplicating the
contract or containing business orchestration.
- Orchestration services build typed gRPC requests, invoke generated clients, and use
dedicated converters for REST-to-Protobuf and Protobuf-to-REST mapping.
- A request or correlation identifier received at the public boundary is propagated to
every downstream request and included in logs and typed error responses.
- gRPC failures are mapped centrally to the API's declared error model. At minimum,
invalid input, unauthenticated, forbidden, not found, conflict, throttling, deadline,
downstream unavailability, and unexpected internal failures remain distinguishable.
- Transport failures never produce an untyped or accidentally empty error response.

## Kotlin style

- Calls to regular functions and methods use positional arguments.
- Named arguments are allowed for constructors and annotations.
- Constants belonging to a single class are placed in its `private companion object`.
- Shared constants are placed in the appropriate `constants/*.kt` file.
- Nullable values are handled explicitly; `!!` and unchecked casts are not used when
validation or a typed alternative can express the invariant.

## Configuration

- Settings are grouped into typed `@ConfigurationProperties`; required values use
validation constraints and the properties are validated with `@Validated`.
- Invalid required configuration fails application startup. Environment-specific
values and credentials are not hardcoded as production defaults.
- Retry policies, backoff, and asynchronous executors are configured centrally and
injected by name.

## External clients

- The client is responsible for transport, the converter for mapping, and the service
for the business scenario.
- A client owns its generated stub and applies the configured retry policy in one
place.
- Every remote call has an explicit finite timeout. Retry policies are bounded and
apply only when repeating the operation is safe or protected by idempotency.
- Missing recipients or input for an optional side effect causes an early return
without an external call.
- Asynchronous entry points catch and log failures that cannot be returned to the
caller.

## Errors and logging

- Expected domain failures use specific exception types. REST resources, controllers,
and other protocol entry points map them to protocol-specific response codes at the
application boundary; business services do not depend on HTTP or gRPC status types.
- Logs use parameterized placeholders instead of string concatenation and include
available request and domain identifiers.
- Large payloads and user content are logged only at `DEBUG` or `TRACE`.
- Credentials, tokens, personal data, and other sensitive values are redacted at every
log level, including exception messages and structured logging fields.
- Transport resources log request boundaries; services and handlers log business
steps without duplicating the full payload.

## Testing

- Pure converters and external-client orchestration are covered by unit tests,
including optional values, empty collections, invalid input, retries, and early
returns.
- Tests replace external integrations with mock or stub beans and assert the generated
request as well as the returned result.
- Asynchronous tests wait for an observable event instead of using a fixed `sleep`.
- Tests are deterministic, independent of execution order, and assert observable
behavior instead of private implementation details.
3 changes: 0 additions & 3 deletions rules/code-generation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# Code generation

- Generated sources are never edited manually.
- Protobuf field numbers are immutable after publication.
- Removed protobuf fields and names are reserved.
- OpenAPI changes are validated and generated clients are rebuilt in the same pull request.
- Generation must be deterministic and runnable in CI without repository-local state.
Loading