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
10 changes: 1 addition & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
# IDE
.DS_Store
.idea/
*.iml
.vscode/

# macOS
.DS_Store

# Editor scratch
*.swp
*~
140 changes: 79 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,104 +1,122 @@
# code-generation-rules

Shared engineering rules and agent tooling for the organization, mounted into
projects as a git submodule.
Shared agent guidance and deterministic coding hooks for Vality services.

The repository carries three things:
The repository is mounted into a consuming project as `.agent-rules`. The always-on
agent context stays intentionally small: durable cross-project invariants and routes to
more specific guidance. Detailed conventions are read only for tasks that need them.

- `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.
Repository-local code, tests, build configuration, `AGENTS.md`, and `CLAUDE.md` remain
the primary source for local architecture and implementation patterns.

## What belongs here
## Repository layout

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.
- `guidance/core.md` — compact guidance installed into the persistent agent context.
- `skills/provider-adapter/` — workflow and focused references for external
payment/provider integrations.
- `references/` — task-specific guidance for database, generated contracts, OpenAPI,
and cross-layer code decisions.
- `hooks/` — deterministic lifecycle checks; currently Kotlin formatting/linting.
- `agents/` — Claude Code and Codex hook fragments.
- `install.sh` — installs or refreshes the managed agent block and hooks.
- `check.sh` — verifies that the consuming repository is synchronized with the pinned
submodule revision.
- `ci/github-actions/agent-rules-drift.yml` — optional CI drift check.

## Adding to a project
## Install

From the consuming repository:

```bash
git submodule add <repo-url> .agent-rules
./.agent-rules/install.sh
```

`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 the selected rule files into `CLAUDE.md`
- syncs the rule text into `AGENTS.md` between `<!-- BEGIN agent-rules -->` and
`<!-- END agent-rules -->`
The installer is idempotent. It owns only:

Everything outside those markers is yours and is never rewritten.
- content between `<!-- BEGIN agent-rules -->` and `<!-- END agent-rules -->` in
`AGENTS.md` and `CLAUDE.md`;
- the hook entries registered in `.codex/hooks.json` and `.claude/settings.json`.

Commit the resulting changes together with the submodule pointer.
Content outside the managed block is preserved.

## Rule profiles
Requirements: `git` and `jq`. The Kotlin hook uses Maven only when the target project
contains `ktlint-maven-plugin`.

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

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

For example:
For a persistent project profile, commit `.agent-rules-profile` with exactly one of:

```text
common
openapi
adapter
```

The profile can be overridden for a single command. The same option is accepted
by `check.sh`:
- `common` — compact core plus common task routes.
- `openapi` — common routes plus OpenAPI guidance.
- `adapter` — common routes plus the provider-adapter workflow.

A profile can be overridden for one invocation:

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

The command-line value takes precedence over `.agent-rules-profile`. Unknown or
empty profile values are rejected.
For normal repository use, prefer committing `.agent-rules-profile` so `check.sh` and
CI resolve the same profile without extra flags.

## Updating
## Update

```bash
git submodule update --remote .agent-rules
./.agent-rules/install.sh
git diff
```

Review and commit the submodule pointer together with generated changes to the managed
agent configuration.

## CI drift check

Run:

```bash
./.agent-rules/check.sh
```

Review the diff, then commit. The bump is explicit per project — rules never
change under a project without a commit in it.
The command writes nothing. It exits non-zero if the managed blocks or hook
configuration do not match the pinned `.agent-rules` revision.

## Keeping projects honest
A GitHub Actions example is available at:

```text
ci/github-actions/agent-rules-drift.yml
```

`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.
The consuming workflow must checkout submodules.

## The Kotlin format hook
## Agent routing

`hooks/format-kotlin.sh` runs on the agent's `Stop` event — once per turn, after
the code is generated, in both Claude Code and Codex.
The installed persistent block does not copy the contents of `references/` or
`skills/` into every task. It tells the coding agent when to read them:

When the turn touched Kotlin, it runs `ktlint:format` and `ktlint:check` in one
maven invocation. Both goals are needed: `format` fixes what it can but exits
successfully while staying silent about the rest, so only `check` surfaces the
violations that need a human-shaped fix. Those are handed back to the agent,
which then has to correct them before the turn can end.
- schema/migration/repository/transaction work → `references/database.md`;
- generated-source or Protobuf work → `references/code-generation.md`;
- cross-layer architecture/client/converter decisions not settled by local code →
`references/code-conventions.md`;
- OpenAPI work in the `openapi` profile → `references/openapi.md`;
- external payment/provider integrations in the `adapter` profile →
`skills/provider-adapter/SKILL.md`.

It is deliberately quiet and cheap: with no changed `.kt`/`.kts` files, or in a
project with no ktlint, it exits in well under a tenth of a second without
starting a JVM.
The provider-adapter workflow starts from existing production adapters in the target
repository and loads its narrower references only when the concrete flow needs them.

Note that `ktlint:format` covers the whole module, not just the changed files.
In a project where CI already enforces `ktlint:check`, everything committed is
formatted anyway, so this is a no-op on untouched code.
## Project-specific environment

If a project needs specific environment to run its build (a particular
`JAVA_HOME`, a locale), put it in `.agent-rules.env` in the project root — the
hook sources it when present. That file belongs to the project, not here.
If the build hook needs project-local environment such as `JAVA_HOME`, the consuming
repository may provide `.agent-rules.env` at its root. The hook sources that file when
present. Keep secrets out of this file unless the consuming repository already has an
appropriate secret-injection mechanism and the file itself is not committed.
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.
# CI entry point: fails when the installed compact guidance/hook configuration drifts.
exec "$(cd -- "$(dirname -- "$0")" && pwd)/install.sh" --check "$@"
9 changes: 2 additions & 7 deletions ci/github-actions/agent-rules-drift.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
# Copy into .github/workflows/ of a project that mounts .agent-rules.
#
# Fails the pull request when the project's agent configuration no longer
# matches the rules revision it pins — usually because the submodule was bumped
# without re-running install.sh.
# Copy into .github/workflows/ of a consuming project.
name: agent-rules drift

on:
Expand All @@ -16,8 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# Without this the check runs against an empty .agent-rules directory.
submodules: true

- name: Check agent rules are in sync
- name: Check agent guidance is in sync
run: ./.agent-rules/check.sh
20 changes: 20 additions & 0 deletions guidance/core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Shared agent guidance

Treat repository-local code, tests, build configuration, and local `AGENTS.md` /
`CLAUDE.md` content as the primary evidence for how this project is built.

Keep these organization-level invariants:

- Do not edit generated sources. Change their source contract or generator instead.
- Preserve published wire/storage compatibility unless the task explicitly requires a
coordinated breaking change or migration.
- Never hardcode, expose, or log credentials, tokens, PANs, bank-account data, or
other payment-sensitive values.
- Prefer an established local implementation pattern over introducing a new framework,
package layout, abstraction, or dependency without a concrete need.
- Run the project's existing focused tests, linters, generators, and compatibility
checks that cover the changed area.
- Do not load or apply detailed guidance that is unrelated to the current task.

Detailed references are defaults and checklists, not permission to override stronger
repository evidence or explicit task requirements.
27 changes: 2 additions & 25 deletions hooks/format-kotlin.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
#!/usr/bin/env bash
# Formats Kotlin sources touched during the turn.
#
# Wired to the Stop event of Claude Code and Codex alike: both hand the hook a
# JSON event on stdin, and both read exit code 2 with stderr as text to give
# back to the model. So one script serves both.
#
# Contract:
# exit 0 — nothing to do, or everything formatted cleanly
# exit 2 — ktlint found violations it cannot fix; stderr goes back to the agent
#
# It never fails the session for its own reasons: no Kotlin changes, no ktlint,
# no maven, no repository — all of these exit 0. Written against bash 3.2, which
# is still what ships with macOS.
# exit 0 — nothing to do or formatting/checking succeeded
# exit 2 — ktlint found violations it could not fix; stderr is returned to agent

set -uo pipefail

Expand All @@ -25,14 +16,10 @@ REPO_ROOT="$(hook_repo_root)" || exit 0
[ -n "$REPO_ROOT" ] || exit 0

CHANGED="$(hook_changed_files "$REPO_ROOT" '*.kt' '*.kts')"
# The common case is a turn that touched no Kotlin. Leave before paying for a JVM.
[ -n "$CHANGED" ] || exit 0

hook_load_project_env "$REPO_ROOT"

# Resolves the module directories to format: for each changed file, the nearest
# ancestor holding a pom.xml. A leaf module inherits the plugin from its parent,
# so running there is enough.
maven_module_dirs() {
printf '%s\n' "$CHANGED" | while IFS= read -r file; do
[ -n "$file" ] || continue
Expand All @@ -54,9 +41,6 @@ project_has_ktlint_maven() {
grep -q .
}

# Keeps the ktlint violation lines and drops maven's own failure boilerplate and
# JVM warnings, so the agent gets the findings rather than a wall of noise. Falls
# back to the raw output if the run failed for some reason other than lint.
extract_violations() {
local raw filtered
raw="$(cat)"
Expand All @@ -70,8 +54,6 @@ extract_violations() {
}

run_maven_ktlint() {
# No subshell below: the loop is fed by a heredoc precisely so that a
# failure inside it survives into the return value.
local status=0
local dirs dir output

Expand All @@ -85,8 +67,6 @@ run_maven_ktlint() {

while IFS= read -r dir; do
[ -n "$dir" ] || continue
# Both goals in one invocation: format is silent about what it cannot
# fix — only check reports that — and a single mvn run means a single JVM.
if ! output="$(cd "$dir" && mvn --batch-mode -q -Dstyle.color=never ktlint:format ktlint:check 2>&1)"; then
status=1
printf '%s\n' "$output" | extract_violations
Expand All @@ -98,10 +78,7 @@ EOF
return "$status"
}

# The runner is picked per project. Adding Gradle or the standalone CLI later
# means adding a branch here, not touching anything else.
if ! project_has_ktlint_maven; then
# Plenty of repositories mount this submodule without being Kotlin projects.
exit 0
fi

Expand Down
25 changes: 4 additions & 21 deletions hooks/lib/common.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# Shared helpers for agent lifecycle hooks.
#
# Hooks run inside somebody's coding session. The overriding rule for everything
# here: never break the session. Anything unexpected means "exit 0 quietly", not
# "fail loudly".
# Hooks run inside a coding session. Unexpected hook/environment conditions should
# not break the session; deterministic findings from a check may still be returned
# to the agent.

HOOK_PAYLOAD=""
HOOK_SESSION_ID=""
Expand All @@ -14,18 +14,12 @@ hook_log() {
printf '%s\n' "$*" >&2
}

# Reads the event JSON from stdin and populates HOOK_* variables.
#
# jq is used when available; the fallback covers the two scalar fields we
# actually need, so a machine without jq still gets a working hook.
hook_read_payload() {
HOOK_PAYLOAD="$(cat)"
[ -n "$HOOK_PAYLOAD" ] || return 0

if command -v jq >/dev/null 2>&1; then
# One jq for all three fields: this runs on every turn, so the process
# spawns are worth counting.
IFS=' ' read -r HOOK_SESSION_ID HOOK_CWD HOOK_STOP_ACTIVE <<EOF
IFS=$'\t' read -r HOOK_SESSION_ID HOOK_CWD HOOK_STOP_ACTIVE <<EOF
$(printf '%s' "$HOOK_PAYLOAD" | jq -r '[.session_id // "", .cwd // "", (.stop_hook_active // false | tostring)] | @tsv' 2>/dev/null)
EOF
else
Expand All @@ -46,15 +40,12 @@ hook_scalar_fallback() {
head -n 1
}

# Echoes the repository root, or returns non-zero when there is no repository.
hook_repo_root() {
local dir="${HOOK_CWD:-$PWD}"
[ -d "$dir" ] || dir="$PWD"
git -C "$dir" rev-parse --show-toplevel 2>/dev/null
}

# Projects declare their own build environment (JAVA_HOME, locale) here. The
# file belongs to the project; this repository only agrees to read it.
hook_load_project_env() {
local env_file="$1/.agent-rules.env"
[ -f "$env_file" ] || return 0
Expand All @@ -64,9 +55,6 @@ hook_load_project_env() {
set +a
}

# Echoes working-tree files matching the given globs, one per line, relative to
# the repository root: everything changed against HEAD plus untracked files.
# Build and generated output is filtered out.
hook_changed_files() {
local root="$1"
shift
Expand All @@ -88,11 +76,6 @@ hook_changed_files() {
done
}

# Loop guard for hooks that block on Stop.
#
# Blocking makes the agent run again, which fires Stop again. The agent's own
# stop_hook_active flag covers Claude Code; this covers the general case by
# refusing to block twice in a row on an identical message.
hook_should_block() {
local message="$1"
local state_dir="${TMPDIR:-/tmp}/agent-rules-hooks"
Expand Down
Loading