diff --git a/.agents/_TOC.md b/.agents/_TOC.md index 83375f417f..dc8cde3ce9 100644 --- a/.agents/_TOC.md +++ b/.agents/_TOC.md @@ -13,4 +13,11 @@ 11. [Advanced safety rules](advanced-safety-rules.md) 12. [Refactoring guidelines](refactoring-guidelines.md) 13. [Common tasks](common-tasks.md) -14. [Java to Kotlin conversion](skills/java-to-kotlin/SKILL.md) +14. [Team memory](memory/MEMORY.md) +15. [Task plans](tasks/README.md) +16. [Java to Kotlin conversion](skills/java-to-kotlin/SKILL.md) +17. [Dependency update](skills/dependency-update/SKILL.md) +18. [Documentation review](skills/review-docs/SKILL.md) +19. [Pre-PR checklist](skills/pre-pr/SKILL.md) +20. [Kotlin code review](skills/kotlin-review/SKILL.md) +21. [Dependency audit](skills/dependency-audit/SKILL.md) diff --git a/.agents/coding-guidelines.md b/.agents/coding-guidelines.md index 3297d8ae49..12ede97cdc 100644 --- a/.agents/coding-guidelines.md +++ b/.agents/coding-guidelines.md @@ -33,7 +33,7 @@ - Reflection unless specifically requested ## Text formatting - - ✅ Remove double empty lines in the code. + - ✅ Replace double empty lines with a single empty line in the code. - ✅ Remove trailing space characters in the code. [spine-docs]: https://github.com/SpineEventEngine/documentation/wiki diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md new file mode 100644 index 0000000000..cfc2e843fa --- /dev/null +++ b/.agents/memory/MEMORY.md @@ -0,0 +1,16 @@ +# Team memory index + +One line per memory. Scan at the start of every session. +See [README.md](README.md) for the format and routing rules. + +## Feedback (validated patterns & corrections) + +*(no entries yet)* + +## Project (durable context & rationale) + +*(no entries yet)* + +## Reference (external systems) + +*(no entries yet)* diff --git a/.agents/memory/README.md b/.agents/memory/README.md new file mode 100644 index 0000000000..899d9e5585 --- /dev/null +++ b/.agents/memory/README.md @@ -0,0 +1,89 @@ +# Team memory — `.agents/memory/` + +Validated patterns, durable project context, and pointers to external +systems. Checked into git so the whole team — and any agent working in +this repo — benefits from accumulated knowledge. + +This complements Claude Code's built-in per-developer auto-memory: +team-shareable knowledge lives here; personal preferences and ephemeral +state live in the auto-memory. + +## Layout + + .agents/memory/ + ├── MEMORY.md # Index — scan at start of every session + ├── README.md # This file — read when adding/updating memories + ├── feedback/ # Validated patterns & corrections + ├── project/ # Durable project context & rationale + └── reference/ # External systems & resources + +One file per memory. Filename = the memory's kebab-case slug. + +## File format + + --- + name: tests-no-db-mocks + description: One-line summary — used to surface relevance, so be specific. + metadata: + type: feedback # feedback | project | reference + since: 2026-05-19 # date added (ISO) + --- + + + + **Why:** + + **How to apply:** + + Related: [[other-memory-slug]] + +`Why:` and `How to apply:` are required for `feedback` and `project` +memories — they let future readers judge edge cases. `reference` +memories may be shorter (link + one-line purpose). + +Link related memories with `[[slug]]` (the target file's `name:`). + +## Routing — repo vs. auto-memory + +| Kind of fact | Goes to | +|---|---| +| Personal preference, role, style | auto-memory (`user`) | +| Personal habit feedback | auto-memory (`feedback`) | +| Team coding/test/PR rule | **`feedback/`** | +| Durable project rationale | **`project/`** | +| Ephemeral project state (freezes, OOO, deadlines) | auto-memory (`project`) — would rot in git | +| Team-shared external resource | **`reference/`** | +| Personal external resource | auto-memory (`reference`) | + +**Litmus test:** *would a teammate joining the project next month benefit +from knowing this?* If no, it belongs in auto-memory. + +## Write protocol + +1. Write the file **uncommitted** in the working tree. +2. **Surface the change** in the same turn so the human can review. +3. **Do not auto-commit** memory edits as part of an unrelated PR — memory + changes should be reviewable on their own. +4. **Correct in place** when an existing memory turns out wrong; `git blame` + carries the history. +5. **Propose deletion explicitly** when a memory has gone stale, rather + than silently editing it out. + +## Updating the index + +After adding or removing a memory file, update `MEMORY.md`. One line under +the matching section: + + - [slug](category/slug.md) — description from frontmatter + +Keep the index short — long descriptions belong in the file body. + +## Anti-patterns — do not store + +- Anything derivable from the code (module structure, paths, conventions + visible in source). Use `grep` / `Read`. +- Recent-activity summaries or PR lists — `git log` is authoritative. +- Fix recipes for specific bugs — the commit message belongs in the commit. +- Anything already documented in `.agents/` reference docs — keep one + source of truth. +- Personal preferences (see routing). diff --git a/.agents/memory/feedback/.gitkeep b/.agents/memory/feedback/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.agents/memory/project/.gitkeep b/.agents/memory/project/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.agents/memory/reference/.gitkeep b/.agents/memory/reference/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.agents/project-structure-expectations.md b/.agents/project-structure-expectations.md index 81b8e1a62a..22a3ab7d61 100644 --- a/.agents/project-structure-expectations.md +++ b/.agents/project-structure-expectations.md @@ -17,5 +17,5 @@ build.gradle.kts # Kotlin-based build configuration settings.gradle.kts # Project structure and settings README.md # Project overview AGENTS.md # Entry point for LLM agent instructions -version.gradle.kts # Declares the project version. +version.gradle.kts # Declares the project version in versioned Gradle Build Tools repos. ``` diff --git a/.agents/scripts/pre-pr-gate.sh b/.agents/scripts/pre-pr-gate.sh new file mode 100755 index 0000000000..cb80b31251 --- /dev/null +++ b/.agents/scripts/pre-pr-gate.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# +# PreToolUse hook: block `gh pr create` unless /pre-pr has successfully run +# for the current HEAD. The hook is intentionally unaware of the repository's +# versioning or build system; the /pre-pr skill decides which checks apply. +# +# Input: hook JSON on stdin (tool_name, tool_input.command). +# Exit: 0 to allow, 2 to block (stderr is surfaced to Claude). +# +set -eu + +input=$(cat) +tool=$(printf '%s' "$input" | jq -r '.tool_name // empty') +[ "$tool" != "Bash" ] && exit 0 + +cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') + +# Split the command on shell separators (`;`, `&`, `|` — `&&`/`||` collapse +# to repeated newlines, which is fine) and check each segment. Only block +# when a segment STARTS (after optional whitespace) with `gh pr create`. +# This avoids false positives like `echo "gh pr create"` or test fixtures +# that mention the string, while still catching `cd dir && gh pr create` +# and `cat body | gh pr create`. `tr` is used (not `sed s///`) because +# BSD `sed` on macOS does not interpret `\n` in the replacement string. +if ! printf '%s' "$cmd" \ + | tr ';&|' '\n\n\n' \ + | grep -qE '^[[:space:]]*gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)'; then + exit 0 +fi + +repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +sentinel="$repo_root/.git/pre-pr.ok" + +block() { + cat >&2 + exit 2 +} + +if [ ! -f "$sentinel" ]; then + block <&2 <<'EOF' +Direct edits to version.gradle.kts are blocked by a project hook. + +If this repository already has a root version.gradle.kts, use the bump-version +skill instead: + /bump-version [snapshot|minor|major] + +If this repository does not have a root version.gradle.kts, do not add one just +to satisfy /pre-pr; the version check is not applicable. + +See: + - .agents/version-policy.md + - .agents/skills/bump-version/SKILL.md +EOF + exit 2 +fi + +exit 0 diff --git a/.agents/scripts/publish-version-gate.sh b/.agents/scripts/publish-version-gate.sh new file mode 100755 index 0000000000..466f0a8002 --- /dev/null +++ b/.agents/scripts/publish-version-gate.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# PreToolUse hook: block any `./gradlew` invocation that could publish to +# Maven Local without a version bump on the current branch. Wraps the +# Layer-1 deterministic check at `version-bumped.sh`. +# +# This is intentionally broad: it fires on `build`, `publish`, +# `publishToMavenLocal`, and any `:publish*` task. Many repos in this +# constellation chain `publishToMavenLocal` into `build` because +# integration tests consume those local artifacts, so `build` itself is +# publish-risky. False positives (blocking a pure compile) are preferable +# to overwriting a previously published snapshot that consuming repos +# rely on. +# +# Input: hook JSON on stdin (tool_name, tool_input.command). +# Exit: 0 to allow, 2 to block (stderr is surfaced to Claude). +# +set -eu + +input=$(cat) +tool=$(printf '%s' "$input" | jq -r '.tool_name // empty') +[ "$tool" != "Bash" ] && exit 0 + +cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') + +# Split the command on shell separators (`;`, `&`, `|`) and inspect each +# segment. Only block when a segment, after optional whitespace, invokes +# `./gradlew` (or `./config/gradlew`) with a publish-risky task. Avoids +# false positives on `echo "./gradlew build"` or fixtures. +risky_segment() { + local seg="$1" + # Must start with a gradlew invocation. + printf '%s' "$seg" | grep -qE '^[[:space:]]*\.?/?(config/)?gradlew([[:space:]]|$)' || return 1 + # Must mention a publish-risky task. `build` is risky because it can + # finalize publishToMavenLocal in this config. The leading + # `(:[A-Za-z0-9_.-]+)*:?` covers qualified task paths + # (e.g. `:module:build`, `:a:b:publishToMavenLocal`) and a single + # leading-colon form (`:publishMavenJavaPublicationToMavenLocal`). + # `publish[^[:space:]]*` then catches every publish-task variant. + printf '%s' "$seg" | grep -qE '(^|[[:space:]])(:[A-Za-z0-9_.-]+)*:?(build|publish[^[:space:]]*|publishToMavenLocal|publishAllPublicationsToMavenLocal)([[:space:]]|$)' +} + +block_needed=0 +# `|| [ -n "$segment" ]` makes the loop process the final segment when the +# input has no trailing newline (which is the case for `printf '%s'`). +while IFS= read -r segment || [ -n "$segment" ]; do + if risky_segment "$segment"; then + block_needed=1 + break + fi +done < <(printf '%s' "$cmd" | tr ';&|' '\n\n\n') + +[ "$block_needed" -eq 0 ] && exit 0 + +repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +script="$repo_root/.agents/skills/version-bumped/scripts/version-bumped.sh" + +# If the helper is missing (e.g. partial clone), don't pretend we gated. +if [ ! -x "$script" ]; then + exit 0 +fi + +# `&& rc=0 || rc=$?` captures the exit code regardless of success/failure. +# After `if cmd; then ... fi`, $? reflects the if-fi structural exit (0), +# not the failed test's exit code — so we cannot use the if-fi form here. +err_file="/tmp/version-bumped.$$.err" +VERSION_BUMPED_QUIET=1 "$script" 2>"$err_file" && rc=0 || rc=$? +if [ "$rc" -eq 0 ]; then + rm -f "$err_file" + exit 0 +fi +err_payload=$(cat "$err_file" 2>/dev/null || true) +rm -f "$err_file" + +# Layer-1 returned a configuration error — do not block, surface the note. +if [ "$rc" -ne 1 ]; then + printf '%s\n' "$err_payload" >&2 + exit 0 +fi + +cat >&2 < 1) next; print; next } + { blank = 0; print } + ' "$path" > "$tmp" && mv "$tmp" "$path" +} + +if [ -n "$file" ]; then + sanitize_file "$file" + exit 0 +fi + +printf '%s\n' "$command" \ + | sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \ + | sort -u \ + | while IFS= read -r path; do + sanitize_file "$path" + done diff --git a/.agents/skills/bump-gradle/SKILL.md b/.agents/skills/bump-gradle/SKILL.md index e5d09269fd..1fa6fd50b9 100644 --- a/.agents/skills/bump-gradle/SKILL.md +++ b/.agents/skills/bump-gradle/SKILL.md @@ -115,3 +115,11 @@ Always check that page at task time. Do not rely on remembered Gradle versions. Leave unrelated pre-existing user changes alone and mention them separately in the final response. + +8. Ensure `version.gradle.kts` is bumped. + + Before this branch can be built or published locally, the project + version must be strictly greater than the version on the base ref. + Invoke `/version-bumped` — it is a no-op if a bump has already + happened earlier on the branch, and otherwise calls `/bump-version` + to perform the increment. diff --git a/.agents/skills/dependency-audit/SKILL.md b/.agents/skills/dependency-audit/SKILL.md new file mode 100644 index 0000000000..06a812488b --- /dev/null +++ b/.agents/skills/dependency-audit/SKILL.md @@ -0,0 +1,133 @@ +--- +name: dependency-audit +description: > + Audit changes to dependency declarations under + `buildSrc/src/main/kotlin/io/spine/dependency/` — catches accidental + version downgrades, BOM mismatches, missing deprecation markers when + artifacts are renamed or removed, copyright drift, and convention drift. + Use whenever a diff touches that directory, or when asked to "audit + this dependency bump". Read-only; does not run builds. +--- + +# Dependency audit (repo-specific) + +You are the dependency auditor for a Spine Event Engine repo. All managed +dependencies live under: + + buildSrc/src/main/kotlin/io/spine/dependency/ + +organized by sub-package: + +- `lib/` — third-party runtime libraries (Kotlin, Guava, Protobuf, gRPC, …). +- `local/` — Spine SDK artifacts (Base, CoreJvm, ModelCompiler, …). +- `test/` — testing libraries (JUnit, Kotest, AssertK, Truth, Jacoco, Kover). +- `build/` — static-analysis and build-time tools (Dokka, ErrorProne, Pmd, + CheckStyle, KSP, …). +- `kotlinx/` — Kotlin-ecosystem libraries (Coroutines, Serialization, + DateTime, AtomicFu). +- `boms/` — BOM declarations. + +Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBom` +(see `dependency/Dependency.kt`). The shape is: + + object Kotest { + const val version = "6.1.11" + const val group = "io.kotest" + const val assertions = "$group:kotest-assertions-core:$version" + // … + } + +## How to run an audit + +1. **Fetch the full diff once.** Run + `git diff ...HEAD -- 'buildSrc/src/main/kotlin/io/spine/dependency/**'` + (or `--staged` if the user is mid-commit). The unified diff already + contains the old and new lines you need for version-sanity and BOM + checks — do not call `--stat` first and then re-read each file. If the + diff is empty, ask the user which files to audit. + +2. **Lean on the diff; `Read` on demand.** Version, BOM, copyright, and + deprecation deltas are all visible in the unified diff. Only `Read` a + file when (a) it is newly added, or (b) a hunk references a + `version`/`group` constant defined outside the hunk and you need + surrounding context. **Budget:** if more than 5 files changed, do not + `Read` individual files — work from the diff and use targeted `Grep` + for cross-cutting questions. + +3. **Batch independent work into one turn.** Issue the version-sanity (A), + convention-drift (D), and cross-cutting (E) tool calls *in parallel* + within a single response. Collect every finding and emit the report + once — **do not stop at the first failure**. + +4. **Batch greps.** For deprecation/caller checks (C) and snapshot-pin + checks (A), build one ripgrep over the union of symbols instead of one + command per symbol. Examples: + - `rg -n '\b(name1|name2|name3)\b' --type kt` to find callers of any + removed `const val`. + - `rg -L 'Copyright \(c\) 2026' ` to flag every stale + header in one call. + - `rg -n ':' --type kt --type gradle` once per + library to check for hardcoded pins. + +## Checks + +### A. Version sanity +- **No silent downgrade.** Compare the old and new `version` value as semver. + A decrease (`2.0.0 -> 1.9.0`) or a snapshot regression (`-SNAPSHOT.183` -> + `.182`) is a Must-fix unless the commit message explicitly justifies it. +- **Snapshot vs. release consistency.** If `version` switches from a release + (`2.0.0`) to a snapshot (`2.0.1-SNAPSHOT.001`), confirm the consuming code + isn't pinned to the release elsewhere via `grep -r ':'`. +- **BOM ↔ component agreement.** For objects extending `DependencyWithBom`, + check that `bom` references the same version as `version` (e.g. Kotlin's + `kotlin-bom:$runtimeVersion`). + +### B. Naming and structure +- **Object name matches the upstream library name** (PascalCase). New files + must follow the convention of neighbors (e.g. `lib/Foo.kt` declares + `object Foo`). +- **No type names in property names** (`fooList`, `barObject`) — this is in + `.agents/coding-guidelines.md`. +- **Module constants use `"$group::$version"`**, not hardcoded + Maven coordinates. Catch copy-paste like `"io.kotest:kotest-assertions-core:6.1.11"`. + +### C. Deprecation discipline +When an artifact is **renamed or removed**: +- The old `const val` must stay with `@Deprecated("…", ReplaceWith("…"))` + or `@Deprecated("…")` (see `Kotest.frameworkApi` and `Kotest.datatest` for + the established style). +- If the diff deletes a `const val` outright, grep the repo with + `git grep ''` to confirm no caller is left behind. If callers exist, + this is a Must-fix. + +### D. Convention drift +- **Copyright header year.** Every changed file should have a current-year + copyright line. If a file was edited but its copyright says `2024`, flag it + (the user can run `/update-copyright` to fix). +- **GitHub URL comment.** New `lib/` and `kotlinx/` files conventionally + start with `// https://github.com//` above the object. + Recommend it if missing. +- **`@Suppress("unused", "ConstPropertyName")` on the object.** This is the + established style for constant-heavy declarations. + +### E. Cross-cutting checks +- **`local/` deps don't leak.** Spine SDK artifacts in `local/` should not be + declared in `lib/` or `test/` (and vice versa). +- **No mixing Groovy and Kotlin DSL.** All Gradle code in `buildSrc/` must be + `.kt` or `.gradle.kts`. Catch any `.gradle` file slipping in. + +## Output format + +Three sections, in this order: + +- **Must fix** — version downgrades, missing deprecation markers on removed + symbols, broken callers, BOM/version mismatches. +- **Should fix** — convention drift, missing deprecation `ReplaceWith`, + missing copyright update, missing URL comment, naming oddities. +- **Nits** — formatting, ordering, doc-comment polish. + +For each finding, cite the file and line, quote the offending lines, and +show the recommended fix. + +End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or +`REQUEST CHANGES`. diff --git a/.agents/skills/dependency-update/SKILL.md b/.agents/skills/dependency-update/SKILL.md new file mode 100644 index 0000000000..7e70bc126c --- /dev/null +++ b/.agents/skills/dependency-update/SKILL.md @@ -0,0 +1,283 @@ +--- +name: dependency-update +description: > + Walk every dependency declaration under + `buildSrc/src/main/kotlin/io/spine/dependency/`, discover the latest accepted + version of each artifact from the URL hinted in its file (or from Maven + metadata if no URL is present), and update the `version` constant in place. + External dependency scopes accept only released versions; the `local` scope + also accepts snapshots and pre-releases published from sibling Spine repos. + Use when asked to refresh dependency versions, bump libraries, run a + dependency audit, or "see what's stale". +--- + +# Update dependencies + +## Goal + +Bring every dependency object under +`buildSrc/src/main/kotlin/io/spine/dependency/` to its latest accepted version. +For every scope except `local/`, that means the latest **released** version: +snapshots, release candidates, milestones, alpha/beta, EAP, and `-dev` builds +are **excluded**. + +`local/` is the deliberate exception. It holds Spine SDK dependencies published +from sibling Spine repositories, and it may move to newer snapshots or +pre-releases such as `2.0.0-SNAPSHOT.388` or `2.1.0-RC1`. + +The authoritative version source for each artifact is the web page already +referenced in its file. When the file has no URL, use the Maven metadata +fallback described below. For non-`local/` artifacts, a discovered Maven +Central URL is **added back to the file** as a line comment so the next run has +a hint. + +## Inputs + +- No arguments → scan all of `buildSrc/src/main/kotlin/io/spine/dependency/`. +- One or more paths or sub-package names (`lib`, `local`, `test`, `build`, + `kotlinx`, `boms`) → restrict the scan to those. +- `--dry-run` → discover and report, but do not edit. + +## Pre-flight + +1. Run `git status --short`. If the worktree is dirty in files this skill will + touch, stop and ask the user. Otherwise preserve unrelated changes. +2. Confirm `buildSrc/src/main/kotlin/io/spine/dependency/` exists. +3. Note the current branch — every change this skill makes is a candidate for + a single `chore(deps): refresh external versions` commit at the end; the + skill itself does NOT commit. The user decides. + +## Per-file workflow + +For each `*.kt` file in scope: + +### 1. Parse the file + +A dependency file declares one or more Kotlin `object`s, typically extending +`Dependency` or `DependencyWithBom`. The shape is: + + object Kotest { + const val version = "6.1.11" + const val group = "io.kotest" + const val assertions = "$group:kotest-assertions-core:$version" + // … + } + +Extract: + +- `objectName` — the outer `object` identifier. +- `version` — the literal version string. Some files have **multiple** version + constants (`runtimeVersion`, `embeddedVersion`, `annotationsVersion`); treat + each separately. The one driving the artifact is typically `override val + version = …` or the `const val version = …` declared at the top. +- `group` — the Maven group. +- `module` artifact names — each `const val foo = "$group:foo:$version"` line + contributes one artifact name. Use the first one to query Maven Central if + needed for non-`local/` artifacts, or Spine SDK Maven repositories for + `local/` artifacts. +- `versionUrl` — a URL hint. Look in this order: + 1. Line comments above the object: `^//\s*(https?://\S+)`. + 2. KDoc `@see ` inside the object's KDoc. + 3. Plain `@see https?://…` inside the KDoc. + 4. If none: leave `versionUrl` empty and use the Maven metadata fallback + below. + +Skip files that contain only abstract base classes or helpers (`Dependency.kt`, +`DependencyWithBom.kt`, `BomsPlugin.kt`, anything without a concrete artifact +declaration). + +### 2. Find the latest accepted version + +The discovery rule depends on the URL shape. For files under +`dependency/local/`, check the Spine SDK Maven metadata before GitHub, even +when the file has a GitHub URL; snapshots are usually visible in Maven +metadata, not in GitHub's latest-release redirect. + +**A. GitHub repository URL** (`https://github.com//`): + +- Outside `local/`, resolve + `https://github.com///releases/latest`. GitHub redirects to the + latest non-prerelease tag. Read the redirected location or the rendered HTML + to extract the tag. +- In `local/`, do **not** rely on `/releases/latest`, because it hides + pre-releases. Use GitHub releases and tags only after checking Spine SDK + Maven metadata. When you do use GitHub, include pre-release entries and keep + version-like tags that match the artifact. +- Tags often have a `v` prefix. Strip it. +- If the repo publishes per-component tags (e.g. + `kotlinx-coroutines-1.10.2`), prefer the tag whose name matches the + artifact's module identifier. Otherwise take the topmost release. + +**B. Maven Central artifact URL** +(`https://search.maven.org/artifact//` or +`https://repo1.maven.org/maven2///`): + +- Hit Maven Central's REST API: + `https://search.maven.org/solrsearch/select?q=g:+AND+a:&rows=20&core=gav` +- Outside `local/`, filter the `response.docs[].v` values by the pre-release + rule (below). +- In `local/`, keep snapshots and pre-releases in the candidate list. +- Take the highest by semver comparison. + +**C. Spine SDK Maven repositories for `local/` artifacts**: + +- For files under `dependency/local/`, query Maven metadata in the current + Spine SDK Artifact Registry repositories before falling back elsewhere: + - `https://europe-maven.pkg.dev/spine-event-engine/releases` + - `https://europe-maven.pkg.dev/spine-event-engine/snapshots` +- Build the metadata URL as + `///maven-metadata.xml`, where `groupPath` is the + Maven group after first resolving symbolic aliases used in dependency files + (for example, `Spine.group` -> `io.spine` and `Spine.toolsGroup` -> + `io.spine.tools`) and then replacing dots with slashes. +- Read `...` entries. For `local/`, do not + reject `SNAPSHOT`, RC, milestone, alpha, beta, EAP, pre, or dev versions. +- If both release and snapshot repositories have candidates, compare all of + them together and take the highest version. + +**D. Project homepage** (e.g. `https://kotest.io/`, `https://junit.org/`, +`https://www.detekt.dev/`): + +- Try to find a "latest release" or "download" link on the page. If the page + is a thin landing page with no usable version data, fall through to E. + +**E. No URL or unusable URL — Maven metadata fallback**: + +- Outside `local/`, query Maven Central as in B using the file's `group` and + the first module artifact name (the part after `$group:`). +- In `local/`, query the Spine SDK Maven metadata first. Use Maven Central only + if the artifact is absent from those repositories. +- If a non-`local/` Maven Central fallback query returns results, **also insert + a line comment** + `// https://search.maven.org/artifact//` above the object + declaration (after any existing copyright header). This back-fills the URL + hint for next time. Match the existing comment style (one line, no trailing + punctuation). +- If all fallback queries have no result, leave the file untouched and add it + to the `Manual review` section of the final report. + +### 3. Filter pre-releases outside `local/` + +Apply this filter only to files outside `dependency/local/`. + +For `local/` files, snapshots and pre-releases are accepted candidates. Do not +put them in `Filtered pre-releases`; put them in the `local/` confirmation +section of the final report instead. + +Reject any version string matching, case-insensitively: + + -SNAPSHOT$ + -RC[\d\-.]*$ e.g. -RC1, -RC.2 + -M\d+$ e.g. -M3 + -alpha[\d\-.]*$ + -beta[\d\-.]*$ + -EAP[\d\-.]*$ + -pre[\d\-.]*$ + -dev[\d\-.]*$ + \.Beta\d*$ Spring-style trailing tokens + \.Alpha\d*$ + \.RC\d*$ + \.M\d+$ + +Apply the regex to the **suffix after the numeric version**. The version +`2.0.0-SNAPSHOT.182` is a snapshot and must be rejected as a target outside +`local/`, but it is valid for `local/` dependency objects. This skill only +edits dependency files, never `version.gradle.kts` (that belongs to the +`bump-version` skill). + +### 4. Compare versions + +Use semver comparison: + +- Split on `.` and `-`. +- Numeric segments compare numerically; non-numeric segments compare + lexicographically. +- A version without any pre-release suffix is greater than one with the same + numeric prefix but a pre-release suffix. + +Only update when `latest > current`. Equal or lower → no change. + +### 5. Apply the edit + +- Replace the `version` literal with the new value. Use a precise replacement + anchored on the full line (`const val version = ""` → + `const val version = ""`). Do not blindly replace the version string, + because the same string can appear in module URLs constructed via + interpolation (`"$group:…:$version"`) — those will pick up the new value + automatically. +- If the file uses a renamed version constant (`runtimeVersion`, + `compilerVersion`, etc.) that feeds `override val version = compilerVersion`, + update the **source** constant, not the alias. +- For `DependencyWithBom` objects, verify the `bom` line still resolves + correctly. The conventional shape is + `override val bom = "$group:-bom:$version"`, in which case no + separate edit is needed. If the BOM version is hard-coded, update it too. +- Preserve indentation, comment style, and surrounding blank lines exactly. + +### 6. Watch for `local/` artifacts + +`local/` holds Spine SDK dependencies (Base, CoreJvm, ModelCompiler, …) that +are published from sibling Spine repos. This scope accepts snapshots and +pre-releases because these artifacts often advance through internal snapshot +builds before a stable SDK release. + +Still **flag every `local/` update in the report**, and note whether the target +is a release, snapshot, or pre-release. The user can then decide whether to +bump the SDK in lockstep with the rest of the project. Spine SDK artifacts +often need to move together; one-off bumps can cause runtime ABI mismatches. + +## Report + +When the run completes, emit a Markdown report with these sections: + +- **Updated** — table of `file | objectName | old → new | source URL`. +- **Already current** — file/object pairs whose version was already the + newest accepted version. +- **Skipped (no URL, metadata empty)** — manual review needed. +- **Filtered pre-releases** — newer versions found but rejected because they + were RC/SNAPSHOT/alpha/etc. Applies only outside `local/`. +- **`local/` bumps to confirm** — every `local/` change called out separately, + including snapshot and pre-release targets. + +End with the suggested next steps: + +1. Review the diff (`git diff buildSrc/src/main/kotlin/io/spine/dependency/`). +2. Invoke `/version-bumped`. Every feature branch must advance + `version.gradle.kts` strictly above the base before any + `./gradlew build` (which may transitively `publishToMavenLocal`). The + skill is a no-op when a bump already happened earlier on the branch + and otherwise calls `/bump-version` to perform the increment. +3. Run `./gradlew build` (or `./gradlew clean build` if `.proto` files + participate). +4. Commit. Match the shape of the actual change: + - Single `local/` bump (most common): `` Bump Spine Base -> `2.0.0-SNAPSHOT.190` `` + - Coordinated external set: `Bump Protobuf and gRPC` (one commit; + mention both). + - Bulk external refresh (rare): `Refresh external dependencies`. + +## Safety + +- Do not commit. Do not push. Editing files is the limit of this skill's + authority. +- Never edit `version.gradle.kts` — that's the `bump-version` skill's + responsibility. +- Never auto-resolve a Maven Central query that returns multiple matching + artifacts with different groups (e.g. a library that exists under both + `io.netty` and `io.netty.incubator`). Ask the user. +- If a discovered "latest" version is more than one **major** ahead of the + current value (e.g. `1.x` → `3.x`), flag it as a major bump in the report + and apply the edit only if the user confirms, or only when running + non-interactively with `--include-majors`. Major bumps frequently break + ABI. + +## Failure modes to expect + +- **GitHub rate limit** on the unauthenticated REST API. The `/releases/latest` + HTML page does not require auth and is the preferred fallback. +- **Per-component tags** in a monorepo. Match by artifact name, don't take the + topmost tag blindly. +- **Repositories that publish to JCenter only** — JCenter is sunset; if Maven + Central is empty, the dependency may need migration. Flag it. +- **Vendor-specific version schemes** (e.g. dates: `2025.10.01`) — the + semver comparator above will still order these correctly; just don't + mis-classify them as pre-releases. diff --git a/.agents/skills/dependency-update/agents/openai.yaml b/.agents/skills/dependency-update/agents/openai.yaml new file mode 100644 index 0000000000..a61198d327 --- /dev/null +++ b/.agents/skills/dependency-update/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Dependency Update" + short_description: "Refresh dependency versions, allowing snapshots only for local Spine SDK artifacts" + default_prompt: "Use $dependency-update to walk every dependency object under buildSrc/src/main/kotlin/io/spine/dependency/, find the latest accepted version for each, and update the version constants in place. External scopes use released non-snapshot versions only; dependency/local/ may use snapshots and pre-releases from sibling Spine repos. Use the URL referenced in each file as the source of truth; fall back to Maven metadata and back-fill missing hints when useful." diff --git a/.agents/skills/java-to-kotlin/SKILL.md b/.agents/skills/java-to-kotlin/SKILL.md index d3abdc2f7b..b9835f8f7a 100644 --- a/.agents/skills/java-to-kotlin/SKILL.md +++ b/.agents/skills/java-to-kotlin/SKILL.md @@ -49,3 +49,11 @@ description: > * Convert `@throws` to `@throws` with the same description. * Convert `{@link}` to `[name][fully.qualified.Name]` format. * Convert `{@code}` to inline code with backticks (`). + +## Final step: ensure the version is bumped + +After the conversion is verified, invoke `/version-bumped` so the branch +carries a strictly greater `version.gradle.kts` than the base ref before +any `./gradlew build` (which may transitively `publishToMavenLocal` and +overwrite the previously published snapshot consumer repos depend on). +The skill is a no-op when a bump already happened earlier on the branch. diff --git a/.agents/skills/kotlin-review/SKILL.md b/.agents/skills/kotlin-review/SKILL.md new file mode 100644 index 0000000000..c55c8c49c9 --- /dev/null +++ b/.agents/skills/kotlin-review/SKILL.md @@ -0,0 +1,62 @@ +--- +name: kotlin-review +description: > + Review Kotlin (and Java) changes in this repo against the Spine coding + guidelines, safety rules, and testing policy. Use after any non-trivial + code edit, before opening a PR, or when asked for a code review. + Read-only; does not run builds. +--- + +# Kotlin code review (repo-specific) + +You are the Kotlin reviewer for this repository. The authoritative standards +live in `.agents/`: + +- `.agents/coding-guidelines.md` — Kotlin idioms, formatting, what to prefer/avoid. +- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` — hard constraints + (no reflection without approval, no analytics/telemetry, no blocking calls in + coroutines, no auto-updating external dependencies). +- `.agents/testing.md` — Kotest assertions preferred, stubs not mocks. +- `.agents/project-structure-expectations.md` — module/source-set layout. +- `.agents/version-policy.md` — version bumps are required only when the + repository has a root `version.gradle.kts`. + +## Review procedure + +1. Read the diff. Use `git diff --staged` or `git diff ...HEAD` depending on + what the user describes. Do NOT review the full repo — only what changed. +2. Read each affected file fully, not just the diff hunks. Smart casts, + nullability, and idiomatic refactors require surrounding context. +3. Check against `.agents/coding-guidelines.md`: + - Kotlin idioms (extension functions, `when`, smart casts, data/sealed classes). + - Immutability by default. + - No `!!` without justification. + - No type names in variable names. + - No string duplication — use companion-object constants. + - No mixing Groovy/Kotlin DSL in build logic. + - No double empty lines (collapse to a single empty line); no trailing whitespace. +4. Check safety rules: reflection, telemetry, blocking-in-coroutines, dependency + bumps that weren't requested. +5. Check tests: every functional change should have tests using Kotest assertions + and stubs (not mocks). +6. Check the version gate: + - If the repository has a root `version.gradle.kts`, confirm it was + incremented when the change is user-visible. + - If root `version.gradle.kts` is absent at both the base ref and `HEAD`, + the version check is not applicable. Do not report a missing version bump + or ask for the file to be created. + +## Output format + +Return three sections, in this order: + +- **Must fix** — violations of safety rules, broken builds, missing version + bump when the version gate applies, missing tests for functional changes. +- **Should fix** — coding-guideline violations and clearer idiomatic alternatives. + Cite the specific guideline. +- **Nits** — style and naming suggestions. + +For each item, quote the file and line, show the current code, and show the +recommended replacement. If there's nothing in a section, write "None." + +End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or `REQUEST CHANGES`. diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md index 2885f4828f..b92b05d3f5 100644 --- a/.agents/skills/move-files/SKILL.md +++ b/.agents/skills/move-files/SKILL.md @@ -25,7 +25,8 @@ description: > changes. 3. Move safely. - - Prefer `git mv` for tracked files in the repo. + - Always use `git mv` for tracked files in the repo. If sandboxing blocks + it, request approval; do not use delete/create as a fallback. - Use filesystem moves only for untracked/generated/out-of-git files. - Create parent directories first. - For case-only renames, move through a temporary name. @@ -40,6 +41,13 @@ description: > - Run `git status --short` and confirm the delta matches the move. - Run focused validation for moved files, or state what could not run. +6. Ensure the version is bumped. + Invoke `/version-bumped` so the branch carries a strictly greater + `version.gradle.kts` than the base ref before any `./gradlew build` + (which can transitively `publishToMavenLocal` and overwrite + consumer-facing snapshots). The skill is a no-op if a bump already + happened earlier on the branch. + ## Repo Notes Follow `.agents/project-structure-expectations.md` for module/source-set/test moves. diff --git a/.agents/skills/pre-pr/SKILL.md b/.agents/skills/pre-pr/SKILL.md new file mode 100644 index 0000000000..85a3b0a29d --- /dev/null +++ b/.agents/skills/pre-pr/SKILL.md @@ -0,0 +1,173 @@ +--- +name: pre-pr +description: > + Run the pre-PR checklist for this repo: apply the version gate only when + the repository has a root `version.gradle.kts`, run the configured + build/check command per `.agents/running-builds.md`, and invoke the + configured reviewers (`kotlin-review`, `review-docs`, `dependency-audit`) + against the branch diff. On success, write a sentinel file at + `.git/pre-pr.ok` so the `gh pr create` hook can verify the checklist ran + for the current HEAD. Use before opening a PR, or when CI rejected a + branch and you want a fast local repro. +--- + +# Pre-PR checklist (repo-specific) + +You are the pre-PR gate for this repository. You compose the existing +reviewers and the documented repository rules into a single pass that must +succeed before a pull request is opened. + +This skill supports both versioned Gradle Build Tools projects and repositories +that intentionally do not have `version.gradle.kts` (for example, shared +configuration repositories). Do not create `version.gradle.kts` just to satisfy +this checklist. When the file is absent from the project root, the version-bump +check is **not applicable**. + +The authoritative standards live in `.agents/`: + +- `.agents/version-policy.md` — applies only when the repository has a root + `version.gradle.kts`. +- `.agents/running-builds.md` — which build/check command to run based on what + changed. It may be Gradle or another repository-specific command. +- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` — hard + constraints checked by the reviewers. +- The reviewer skills/agents themselves: `kotlin-review` (Claude agent), + `review-docs` (skill + Claude agent), `dependency-audit` (Claude agent). + +## Procedure + +Execute the steps in order. If a step fails, stop, write a `FAIL` sentinel +(see step 6), and report the failure — do not run the remaining steps. + +### 1. Determine scope and repository capabilities + +- Base ref: `master` unless the user provides a different one. +- Diff command: `git diff ...HEAD --name-only` for the file list, + `git diff ...HEAD --stat` for the summary. +- Repository root: `git rev-parse --show-toplevel`. +- Version gate: + - Check only the repository-root `version.gradle.kts`. + - If `version.gradle.kts` is absent at both `` and `HEAD`, record the + version check as `N/A` and continue. Do not ask the user to run + `/bump-version`. + - If `version.gradle.kts` exists at `HEAD`, enforce the version check in + step 2. + - If `version.gradle.kts` exists at `` but is missing at `HEAD`, fail + unless the user explicitly asked to migrate the repository away from + Gradle Build Tools versioning. +- Classify the changes: + - **proto** — any `*.proto` file changed. + - **code** — any `*.kt`, `*.kts`, or `*.java` file changed. + - **docs** — any `*.md` file or doc-only edits inside sources changed. + - **deps** — any file under `buildSrc/src/main/kotlin/io/spine/dependency/` + changed. + +### 2. Version-bump check + +- If the version gate is `N/A`, skip this step with note: + "`version.gradle.kts` is absent; this repository is not a versioned Gradle + Build Tools project." +- Otherwise, read `version.gradle.kts` at `HEAD` and, when present, at + ``. +- Confirm the version string is strictly greater (semver + Spine snapshot + rules — see `.agents/version-policy.md`) when both sides have the file. +- If the file is newly introduced at `HEAD`, report the introduced version and + continue. +- If unchanged or decreased, stop with a Must-fix: "Run `/bump-version`." + +### 3. Build or check + +Pick the target per `.agents/running-builds.md`: + +- **proto** changed → `./gradlew clean build` +- Else **code** changed → `./gradlew build` +- Else **docs**-only → `./gradlew dokka` (tests not required) + +If the repository does not have `./gradlew`, do not fail solely because Gradle +is unavailable. Read `.agents/running-builds.md` for the repository-specific +non-Gradle command that matches the change type, and run that instead. If no +build/check command is documented for the change type, record `build=skipped` +with the reason and continue. + +Run the chosen command. Surface the first failing module/task/check. On +failure, stop and write a `FAIL` sentinel. + +### 4. Reviewers (run in parallel) + +Dispatch the relevant reviewers concurrently and collect their verdicts: + +- Always: `kotlin-review` (if **code** changed) and `review-docs` (if + **docs** or KDoc changed). +- If **deps** changed: `dependency-audit`. + +Pass each reviewer the base ref, changed-file list, build/check result, and +version-check result. When the version check is `N/A`, say explicitly: +"This repository has no root `version.gradle.kts`; a version bump is not +applicable and must not be reported as missing." + +Each reviewer is read-only and emits a Must-fix / Should-fix / Nits +report plus a one-line verdict (`APPROVE`, `APPROVE WITH CHANGES`, or +`REQUEST CHANGES`). + +### 5. Aggregate + +- Overall **PASS** when: + - Version check passed or was `N/A`, + - Build succeeded, + - Every dispatched reviewer returned `APPROVE` or `APPROVE WITH CHANGES` + *and* no Must-fix items remain unaddressed in this session. +- Otherwise **FAIL**. + +### 6. Sentinel + +Write `.git/pre-pr.ok` at the repo root (NOT under `.claude/` — the +sentinel must travel with the local clone, not be checked in). Format: + +``` +head= +branch= +status=PASS|FAIL +timestamp= +build= +reviewers= +version=new, introduced:, or "not-applicable"> +``` + +The `gh pr create` hook (`.agents/scripts/pre-pr-gate.sh`) checks this +file's `head=` and `status=` fields. Extra fields are allowed. The sentinel is +invalidated automatically when HEAD advances — the hook compares the recorded +`head=` against the current HEAD SHA. + +## Output format + +Report in this shape: + +``` +## Pre-PR checklist ( vs ) + +| Check | Status | Notes | +|---------------|--------|----------------------------------------| +| Version check | … | , introduced, or N/A | +| Build/check | … | | +| kotlin-review | … | | +| review-docs | … | | +| dep audit | … | | + +**Overall: PASS|FAIL** +Sentinel: .git/pre-pr.ok (status=PASS|FAIL, head=) +``` + +On `PASS`, end with: "You can now run `gh pr create`." +On `FAIL`, end with the specific blocker and the next action. + +## Notes + +- This skill must NOT create the PR itself. It only gates whether the + workspace is ready. +- This skill must NOT create `version.gradle.kts`. Repositories without a root + `version.gradle.kts` are valid; their version check is `N/A`. +- The sentinel lives under `.git/` (untracked by definition) so it is + per-clone and never committed. +- Each reviewer remains the source of truth for its own checks; this + skill does not duplicate their rules — it only orchestrates and + aggregates. diff --git a/.agents/skills/review-docs/SKILL.md b/.agents/skills/review-docs/SKILL.md new file mode 100644 index 0000000000..d936fa28a2 --- /dev/null +++ b/.agents/skills/review-docs/SKILL.md @@ -0,0 +1,129 @@ +--- +name: review-docs +description: > + Review documentation changes — KDoc/Javadoc inside Kotlin/Java sources and + Markdown docs (`README.md`, `docs/**`) — against Spine documentation + conventions. Use when a diff touches doc comments or Markdown, before + opening a doc-affecting PR, or when asked for a documentation review. + Read-only; does not run builds. +--- + +# Review documentation (repo-specific) + +You are the documentation reviewer for a Spine Event Engine project. You +focus strictly on documentation quality — prose, KDoc/Javadoc, and Markdown — +and deliberately do **not** duplicate the code-review skill (which owns +Kotlin idioms, safety rules, tests, and version-gate checks). + +The authoritative standards live in `.agents/`: + +- `.agents/documentation-guidelines.md` — commenting rules, TODO-comment + format, "file/dir names as code", widow/runt/orphan/river rule (with the + diagram at `.agents/widow-runt-orphan.jpg`). +- `.agents/documentation-tasks.md` — KDoc-example requirement on APIs; + Javadoc → KDoc conversion rules (`

` removal, etc.). +- `.agents/skills/writer/SKILL.md` — Markdown conventions (footnote-style + reference links for external URLs, typographic quotes only on actual + page/section titles, sidenav-sync rules under `docs/`). +- `.agents/running-builds.md` — for doc-only Kotlin/Java changes the right + build is `./gradlew dokka` (no tests required). + +## Review procedure + +1. **Scope the diff.** Obtain the change set via `git diff --staged` or + `git diff ...HEAD` depending on what the user describes. Restrict + to files matching: + - `**/*.kt`, `**/*.kts`, `**/*.java` (for KDoc/Javadoc inside sources) + - `**/*.md` (Markdown docs) + Do **not** review the full repo — only what changed. + +2. **Read each affected file fully, not just the hunks.** Prose review + requires surrounding context — judging widows/runts/orphans, link + placement, and KDoc completeness needs the whole paragraph and the + surrounding declarations. + +3. **Stay in scope.** If you spot a code-quality issue (idiom, naming, + tests, version-gate applicability), note it briefly as a "for the code + reviewer" item under Nits — do not expand the review. + +## Checks + +### A. KDoc / Javadoc inside sources + +- **Public and internal APIs carry KDoc.** Per `documentation-tasks.md`, + KDoc should include at least one usage example for non-trivial APIs. + Missing KDoc on a new or modified public/internal symbol is a Should-fix. +- **No Javadoc residue in Kotlin.** When converting from Java: + - `

` tags on a text line removed (`"

This"` → `"This"`). + - `

` on its own line replaced with a blank line. + - HTML entities (`&`, `<`, …) converted to literals where appropriate. +- **Inline comments in production code are minimized.** Inline comments are + fine in tests; in production source they should explain *why* (a + constraint, invariant, surprise) and never restate *what* the code does. +- **TODO comments follow the Spine format.** Linked from + `documentation-guidelines.md` to the wiki "TODO-comments" page. A bare + `// TODO: …` without owner/issue reference is a Should-fix. +- **File and directory names rendered as code.** Within KDoc/Javadoc prose, + `path/to/file.kt` and `module-name` must use backticks. + +### B. Markdown docs + +- **Footnote-style reference links** for external `https://` URLs (per the + `writer` skill). Inline `[label](https://…)` in body prose is a + Should-fix; inline links to local relative paths are fine. +- **Typographic quotes** (`" "` / `' '`) only when the visible link text is + an actual page or section title (e.g., the "Getting started" page). + Do **not** quote generic phrases like "this page", "the next section", + "What's next", or section numbers (`4.3`). +- **Sidenav sync.** If the diff adds/removes/renames/moves a page under + `docs/content/docs/

/`, the matching current-version + `sidenav.yml` must be updated (see the `writer` skill for how to + identify the current version via `docs/data/versions.yml`). A missing + sidenav update is a Must-fix. +- **Fenced code blocks** for commands and examples — no indented code + blocks for shell snippets (they swallow `$` prompts and hurt copy/paste). +- **Heading hierarchy.** No skipped levels (`#` → `###`); exactly one `#` + per file. + +### C. Prose flow (Spine-specific) + +- **Avoid widows, runts, orphans, and rivers** — the rule from + `documentation-guidelines.md` with the diagram at + `.agents/widow-runt-orphan.jpg`. Operationally: + - **Widow / runt**: a paragraph's last line containing only one short + word (or a hyphenated fragment). Reflow the prior line. + - **Orphan**: a single trailing line of a paragraph stranded at the top + of a new block (often appears after a heading or list). Reflow. + - **River**: a vertical "gap" of aligned spaces running down justified + text. Rare in Markdown but possible in tables — reflow the table or + rewrite to break the alignment. + Quote the offending paragraph and propose a rewording that fixes it. + +### D. Terminology and tone + +- **Match code identifiers verbatim.** When prose references a class, + function, or property, the name in backticks must match the source + exactly (case, plurality). +- **Consistent terminology across the diff.** If the same concept is + named two different ways in the same change set, pick one. + +## Output format + +Three sections, in this order: + +- **Must fix** — broken/missing KDoc on a newly-introduced public API, + missing sidenav sync, broken cross-references, Javadoc residue + (`

` tags) left in Kotlin KDoc, broken Markdown links. +- **Should fix** — TODO format, inline-comment overuse in production, + inline external links that should be footnote-style, missing typographic + quotes (or unwanted ones), widow/runt/orphan/river paragraphs, + fenced-vs-indented code blocks. +- **Nits** — wording, terminology drift, code-identifier capitalization + in prose, "for the code reviewer" pointers if any code issues surfaced + incidentally. + +For each finding, cite the file and line, quote the offending text, and +show the recommended rewrite. If a section is empty, write "None." + +End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or +`REQUEST CHANGES`. diff --git a/.agents/skills/version-bumped/SKILL.md b/.agents/skills/version-bumped/SKILL.md new file mode 100644 index 0000000000..86ca53df04 --- /dev/null +++ b/.agents/skills/version-bumped/SKILL.md @@ -0,0 +1,99 @@ +--- +name: version-bumped +description: > + Verify the current branch has bumped `version.gradle.kts` strictly above + the base ref; invoke `/bump-version` to auto-recover if not. Composable: + other modifying skills (`dependency-update`, `bump-gradle`, + `java-to-kotlin`, `move-files`) call this as their final step so a + `./gradlew build` or `publishToMavenLocal` can never overwrite a + previously published Maven Local artifact that integration tests in + consumer repos depend on. +--- + +# Ensure version is bumped + +This skill is the agent-facing wrapper around +`.agents/skills/version-bumped/scripts/version-bumped.sh`. The script is the source of truth for +"has this branch advanced the version vs base?"; this skill just runs it +and, if it fails, invokes `/bump-version` and re-runs to confirm. + +The same logic is enforced as a hook +(`.agents/scripts/publish-version-gate.sh`) that fires before any +`./gradlew … (build|publish|publishToMavenLocal)` invocation, so even +direct gradle calls cannot bypass it. This skill exists for the +cooperative path — other skills calling it before they finish, so the +user is never surprised by a blocked gradle command later. + +The premise is simple: any feature branch is a candidate for publishing, +even when the only change is the version bump itself (sometimes the bump +is the entire change, used to retry a publish that failed because Maven +repositories were overloaded). So if the branch differs from base at all, +the version must advance. + +## When to use + +- Automatically: as the final step of any skill that may change files on + the branch. +- Manually (`/version-bumped`): before running `./gradlew build` or + `./gradlew publishToMavenLocal` on a feature branch when you are not + sure whether the version has already been bumped. + +## Procedure + +1. Run the deterministic check: + + ```bash + .agents/skills/version-bumped/scripts/version-bumped.sh + ``` + + Honor `VERSION_BUMPED_BASE` if the user has set a non-default base ref + (e.g. `origin/master`, or a release branch). + +2. Interpret the exit code: + + - **0** — Done. Either the repository has no root `version.gradle.kts` + (the version check is `N/A`), the branch has no diff vs base, or the + version is already strictly greater. Report a one-line confirmation + and stop. + - **1** — Block. The script's stderr explains which check failed. + Proceed to step 3. + - **2** — Configuration error (no merge-base, parse failure on + `version.gradle.kts`). Do **not** invoke `/bump-version` + automatically. Surface the script's stderr to the user and stop. + +3. On exit 1, invoke `/bump-version` to perform the actual bump. That + skill owns the policy (snapshot numbering, the commit subject, the + rebuild, dependency-report regeneration, and the conflict rule). Do + not duplicate its work here. + +4. After `/bump-version` finishes, re-run the deterministic check. If it + now passes, report the new version on the branch. If it still fails, + surface the stderr unchanged and stop — do not loop. + +## Why this skill is separate from `/bump-version` + +`/bump-version` is the **action** (it edits `version.gradle.kts`, +commits, rebuilds, may commit reports). `/version-bumped` is the +**guard** (read-only check, optional auto-recovery). Skills that want to +say "make sure the branch has a bumped version" should call +`/version-bumped`, not `/bump-version`, because the guard is a no-op when +the bump is already done — calling `/bump-version` unconditionally would +double-bump on every chained skill invocation. + +## Relationship to `checkVersionIncrement` + +The Gradle task `checkVersionIncrement` (in `buildSrc/.../publish/`) +asks a different question: *"is this version already in the remote +Maven metadata?"* It runs on GitHub Actions feature-branch pushes and +fetches the Spine SDK Artifact Registry. The two checks are +complementary — neither subsumes the other. + +## See also + +- `.agents/version-policy.md` — when the version gate applies. +- `.agents/skills/bump-version/SKILL.md` — the bump procedure itself. +- `.agents/skills/pre-pr/SKILL.md` — uses the same check at PR time + (step 2). +- `.agents/skills/version-bumped/scripts/version-bumped.sh` — the deterministic check. +- `.agents/scripts/publish-version-gate.sh` — the hook that enforces the + rule on `./gradlew` invocations. diff --git a/.agents/skills/version-bumped/scripts/version-bumped.sh b/.agents/skills/version-bumped/scripts/version-bumped.sh new file mode 100755 index 0000000000..20ad722783 --- /dev/null +++ b/.agents/skills/version-bumped/scripts/version-bumped.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# +# Verifies that a feature branch which differs from the base ref also +# bumps `version.gradle.kts` strictly above the base version. Mirrors the +# universal "every branch advances the version" policy: a branch with any +# changes is a candidate for publishing — sometimes the only change is the +# bump itself, used to retry a publish that failed because Maven +# repositories were overloaded. +# +# Exit codes: +# 0 — OK: repo has no root `version.gradle.kts`, OR branch has no diff +# vs base, OR working-tree version is strictly greater than base +# version. +# 1 — Block: branch differs from base but version is unchanged or +# decreased. Stderr points to `/bump-version`. +# 2 — Configuration error (bad base ref, parse failure). Stderr explains. +# +# Inputs (env, all optional): +# VERSION_BUMPED_BASE Base ref to compare against. Default: master, +# then main if master is absent. +# VERSION_BUMPED_QUIET When `1`, suppress the "OK" line on stdout. +# The publish-version-gate hook sets this. +# +# Notes: +# * Companion to the Gradle task `checkVersionIncrement` (see +# `buildSrc/.../publish/CheckVersionIncrement.kt`). The Gradle task +# asks "is this version already in remote Maven metadata?" — this +# script asks the simpler local question "has this branch advanced +# the version vs base?". The two checks are complementary; neither +# subsumes the other. +# * The working tree is included in the change-detection so the gate +# reflects what `./gradlew build` would actually publish. +# +set -eu + +repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { + echo "version-bumped: not inside a git repository" >&2 + exit 2 +} +cd "$repo_root" + +version_file="version.gradle.kts" + +# --- N/A: not a versioned project ---------------------------------------- +if [ ! -f "$version_file" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: N/A (no root version.gradle.kts)" + exit 0 +fi + +# --- Resolve base ref ---------------------------------------------------- +base="${VERSION_BUMPED_BASE:-}" +if [ -z "$base" ]; then + if git show-ref --verify --quiet refs/heads/master; then + base=master + elif git show-ref --verify --quiet refs/heads/main; then + base=main + else + echo "version-bumped: no master or main branch found; set VERSION_BUMPED_BASE" >&2 + exit 2 + fi +fi + +if ! git rev-parse --verify --quiet "$base" >/dev/null; then + echo "version-bumped: base ref '$base' does not resolve" >&2 + exit 2 +fi + +# When we are on the base branch itself, there is nothing to gate. +current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") +if [ "$current_branch" = "$base" ] || [ "$current_branch" = "${base##*/}" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: on base branch ($current_branch); nothing to gate" + exit 0 +fi + +merge_base=$(git merge-base HEAD "$base" 2>/dev/null) || { + echo "version-bumped: cannot find merge-base of HEAD and '$base'" >&2 + exit 2 +} + +# --- Detect any branch divergence vs base (committed/worktree/untracked) - +committed=$(git diff --name-only "$merge_base"..HEAD 2>/dev/null || true) +worktree=$(git diff --name-only HEAD 2>/dev/null || true) +untracked=$(git ls-files --others --exclude-standard 2>/dev/null || true) + +if [ -z "$committed" ] && [ -z "$worktree" ] && [ -z "$untracked" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: no changes vs $base" + exit 0 +fi + +# --- Parse versionToPublish from a Gradle file content ------------------- +# Handles two shapes (per .agents/skills/bump-version/SKILL.md step 2): +# 1. val versionToPublish: String by extra("X") +# 2. val sourceVar: String by extra("X") +# val versionToPublish by extra(sourceVar) +parse_version() { + local content="$1" + local v + v=$(printf '%s' "$content" \ + | grep -E 'val[[:space:]]+versionToPublish[^=]*by[[:space:]]+extra\("' \ + | head -n1 \ + | sed -nE 's/.*extra\("([^"]+)".*/\1/p') + if [ -n "$v" ]; then + printf '%s' "$v" + return 0 + fi + local varName + varName=$(printf '%s' "$content" \ + | grep -E 'val[[:space:]]+versionToPublish[^=]*by[[:space:]]+extra\(' \ + | head -n1 \ + | sed -nE 's/.*extra\(([A-Za-z_][A-Za-z0-9_]*)\).*/\1/p') + if [ -n "$varName" ]; then + v=$(printf '%s' "$content" \ + | grep -E "val[[:space:]]+${varName}[^=]*by[[:space:]]+extra\(\"" \ + | head -n1 \ + | sed -nE 's/.*extra\("([^"]+)".*/\1/p') + if [ -n "$v" ]; then + printf '%s' "$v" + return 0 + fi + fi + return 1 +} + +head_content=$(cat "$version_file" 2>/dev/null || true) +head_version=$(parse_version "$head_content" || true) +if [ -z "$head_version" ]; then + echo "version-bumped: cannot parse versionToPublish from working-tree $version_file" >&2 + exit 2 +fi + +# Base content may legitimately not exist (file newly introduced). +base_content=$(git show "$base:$version_file" 2>/dev/null || true) +if [ -z "$base_content" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: $version_file newly introduced at $head_version; treating as bumped" + exit 0 +fi + +base_version=$(parse_version "$base_content" || true) +if [ -z "$base_version" ]; then + echo "version-bumped: cannot parse versionToPublish from $base:$version_file" >&2 + exit 2 +fi + +# --- Strict-greater comparison via `sort -V` ----------------------------- +if [ "$head_version" = "$base_version" ]; then + cmp="equal" +elif [ "$(printf '%s\n%s\n' "$base_version" "$head_version" | sort -V | tail -n1)" = "$head_version" ]; then + cmp="greater" +else + cmp="lesser" +fi + +if [ "$cmp" = "greater" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: OK ($base_version -> $head_version)" + exit 0 +fi + +cat >&2 <.md # One file per task; status in frontmatter + +Filename = the task's kebab-case slug. Multiple active tasks per +branch are allowed — use distinct slugs. + +## File format + + --- + slug: add-team-memory + branch: tune-claude + owner: claude # or a human/agent handle + status: in-progress # see status values below + started: 2026-05-19 + related-memories: # optional — links into .agents/memory/ + - team-memory-routing + --- + + ## Goal + + + ## Context + + + ## Plan + - [x] Step 1 + - [ ] Step 2 + - notes / sub-bullets + - [ ] Step 3 + + ## Log + - 2026-05-19 14:02 — drafted, awaiting approval + - 2026-05-19 14:15 — approved, executing + - 2026-05-19 14:42 — step 3 blocked on … + +The checklist uses `- [ ]` / `- [x]` so another agent can claim and +complete unchecked items by ticking them and adding a `Log` line. + +### `status` values + +| value | meaning | +|---|---| +| `draft` | written but not yet approved | +| `approved` | approved, not yet started | +| `in-progress` | execution under way | +| `blocked` | paused; reason in `Log` | +| `in-review` | work done, awaiting review | +| `done` | complete — file is then deleted (see lifecycle) | + +## Workflow + +1. **Discover** — at task start, scan `.agents/tasks/` for + in-progress or blocked plans on the current branch. Resume + rather than restart. +2. **Draft** — write `.md` with `status: draft` and the + plan checklist. +3. **Approval gate** — `EnterPlanMode` → `ExitPlanMode`. The plan + presented to the human references the file path; the human may + edit the file directly before approving. +4. **Mirror** — on approval, flip `status: approved` → `in-progress` + and populate `TaskCreate` from the top-level checklist for live + in-session progress. +5. **Execute + sync** — use `TaskUpdate` for fine-grained progress. + Edit the file only at meaningful checkpoints: step done, blocker, + scope change, new note. +6. **Complete** — flip `status: done`. The file is raw material for + the PR description. +7. **Delete on merge** — once the branch lands on master, delete the + task file in the same commit or shortly after. `git log --follow` + recovers it if ever needed. + +## Cross-agent coordination + +- Other agents (or other CC sessions) `Read` the file to pick up + state. They MUST update `status`, tick checkboxes, and append + `Log` lines rather than rewriting the plan silently. +- If two agents work the same task in parallel, partition by + checkbox — each agent claims unchecked items by tagging the line + (e.g. `- [ ] (owner: reviewer-bot) Run dependency-audit`) or by + appending a `Log` line. +- The **file** is the contract. In-session `TaskCreate` state is + per-session and not authoritative. + +## When to create a task file + +Create one whenever the work is non-trivial: + +- Changes spanning multiple files or modules (features, refactors). +- Lengthy documentation work — multi-page guides, restructuring + `docs/`, migration notes, tutorials. The task file plans and + tracks the effort; the docs-related skills (`writer`, + `write-docs`, `review-docs`) handle individual page work inside + the plan steps. +- Cross-agent or cross-session work (e.g. one agent drafts, another + reviews). +- Anything that may span sessions and needs durable state. + +Do **not** create a task file for: + +- Trivial changes (single-file edits, typo fixes, version bumps) — + pure overhead. +- Deliverables themselves — code lives in source, docs in `docs/`, + design records where the project keeps them. Task files describe + the *work*, not the artifact. +- Status reports of work already done — that's a `Log` entry on an + existing task, or the PR description. +- Personal reminders / todo lists — use the built-in task list. + +## Relationship to other stores + +- **`.agents/memory/`** — enduring lessons that survive *across* + tasks. If a task yields a generalizable rule, add the memory and + link from the task's `related-memories`. +- **Built-in auto-memory** — personal and ephemeral. Task files do + not carry personal preferences. diff --git a/.agents/version-policy.md b/.agents/version-policy.md index 95ac4513e7..3e8abd5495 100644 --- a/.agents/version-policy.md +++ b/.agents/version-policy.md @@ -1,15 +1,19 @@ # Version policy -The project follows the [Spine SDK Versioning policy][wiki-versioning]. -The version is kept in `version.gradle.kts` at the project root and follows -[Semantic Versioning 2.0.0][semver] with Spine-specific extensions -(snapshot `NUMBER`, patch, and flavor suffixes). +When a repository has `version.gradle.kts` at the project root, it follows the +[Spine SDK Versioning policy][wiki-versioning]. The version is kept in that +file and follows [Semantic Versioning 2.0.0][semver] with Spine-specific +extensions (snapshot `NUMBER`, patch, and flavor suffixes). -PRs without a version bump fail CI. +For repositories with root `version.gradle.kts`, PRs without a version bump +fail CI. Repositories without that file are not versioned Gradle Build Tools +projects; their version check is not applicable, and agents must not create +`version.gradle.kts` just to satisfy `/pre-pr`. -For the bump procedure — version-number selection, the commit-message -convention, the rebuild, dependency-report updates, and conflict resolution — -use the [`bump-version`](skills/bump-version/SKILL.md) skill. +For the bump procedure in repositories that have root `version.gradle.kts` — +version-number selection, the commit-message convention, the rebuild, +dependency-report updates, and conflict resolution — use the +[`bump-version`](skills/bump-version/SKILL.md) skill. [semver]: https://semver.org/ [wiki-versioning]: https://github.com/SpineEventEngine/documentation/wiki/Versioning diff --git a/.claude/agents/dependency-audit.md b/.claude/agents/dependency-audit.md new file mode 100644 index 0000000000..9db010fe55 --- /dev/null +++ b/.claude/agents/dependency-audit.md @@ -0,0 +1,19 @@ +--- +name: dependency-audit +description: Audits changes to dependency declarations under `buildSrc/src/main/kotlin/io/spine/dependency/` — catches accidental version downgrades, BOM mismatches, missing deprecation markers, copyright drift, and convention drift. Use proactively whenever a diff touches that directory, or when the user asks "audit this dependency bump". Read-only; does not run builds. +tools: Read, Grep, Glob, Bash +model: claude-haiku-4-5-20251001 +--- + +Follow the `dependency-audit` skill exactly: + +- Skill: `.agents/skills/dependency-audit/SKILL.md` +- The skill owns the per-area checks (version sanity, naming and structure, + deprecation discipline, convention drift, cross-cutting) and the output + format (Must fix / Should fix / Nits + one-line verdict). +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff`, + `git grep`, and related read-only inspection. Do not run builds. +- **Be fast.** Fetch the full unified diff once, work from it, and `Read` + individual files only when the skill's step 2 budget allows. Issue + independent `Grep`/`Bash` calls in parallel within a single response; + do not halt at the first failure — collect all findings and report once. diff --git a/.claude/agents/kotlin-review.md b/.claude/agents/kotlin-review.md new file mode 100644 index 0000000000..74583aa33b --- /dev/null +++ b/.claude/agents/kotlin-review.md @@ -0,0 +1,17 @@ +--- +name: kotlin-review +description: Reviews Kotlin (and Java) changes against Spine coding guidelines, safety rules, and testing policy. Use proactively after any non-trivial code edit, before opening a PR, or when the user asks for a code review. Read-only; does not run builds. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +Follow the `kotlin-review` skill exactly: + +- Skill: `.agents/skills/kotlin-review/SKILL.md` +- The skill owns the procedure, the checks (Kotlin idioms, safety rules, + testing policy, version-gate applicability), and the output format + (Must fix / Should fix / Nits + one-line verdict). +- Stay in scope: code only. If a documentation issue surfaces, note it + briefly as a Nit pointing at the `review-docs` agent. +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` + and related read-only inspection. Do not run builds. diff --git a/.claude/agents/review-docs.md b/.claude/agents/review-docs.md new file mode 100644 index 0000000000..0481b240b1 --- /dev/null +++ b/.claude/agents/review-docs.md @@ -0,0 +1,18 @@ +--- +name: review-docs +description: Reviews documentation changes — KDoc/Javadoc inside Kotlin/Java sources and Markdown docs (`README.md`, `docs/**`) — against Spine documentation conventions. Use proactively when a diff touches doc comments or Markdown, before opening a doc-affecting PR, or when the user asks for a documentation review. Read-only; does not run builds. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +Follow the `review-docs` skill exactly: + +- Skill: `.agents/skills/review-docs/SKILL.md` +- The skill owns the review procedure, the per-area checks (KDoc/Javadoc, + Markdown, prose flow, terminology), and the output format + (Must fix / Should fix / Nits + one-line verdict). +- Scope yourself to documentation only. If you spot a code-quality issue, + surface it briefly as a Nit pointing at the `kotlin-review` agent — + do not expand the review. +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` + and related read-only inspection. Do not run builds. diff --git a/.claude/commands/bump-gradle.md b/.claude/commands/bump-gradle.md new file mode 100644 index 0000000000..f9078802c9 --- /dev/null +++ b/.claude/commands/bump-gradle.md @@ -0,0 +1,13 @@ +--- +description: Upgrade the Gradle wrapper to the latest release. +argument-hint: "[gradle-version]" +allowed-tools: Read, Edit, Bash(./gradlew:*), Bash(git status:*), Bash(git diff:*), WebFetch +--- + +Follow the `bump-gradle` skill exactly: + +- Skill: `.agents/skills/bump-gradle/SKILL.md` +- Read the skill first. +- Use https://docs.gradle.org/current/release-notes.html as the source of truth for the latest version. Do NOT rely on remembered Gradle versions. +- If the user supplied a version: $ARGUMENTS, use it; otherwise read it from the release notes. +- Commit the wrapper change and dependency report change in separate commits per the skill. diff --git a/.claude/commands/bump-version.md b/.claude/commands/bump-version.md new file mode 100644 index 0000000000..82e18b599c --- /dev/null +++ b/.claude/commands/bump-version.md @@ -0,0 +1,13 @@ +--- +description: Bump the project version in version.gradle.kts per Spine SDK versioning policy. +argument-hint: "[snapshot|minor|major]" +allowed-tools: Read, Edit, Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(./gradlew:*) +--- + +Follow the `bump-version` skill exactly: + +- Skill: `.agents/skills/bump-version/SKILL.md` +- Read the skill first; it owns the policy (snapshot numbering, version conflicts, rebuilding reports). +- Increment requested by the user: $ARGUMENTS (treat as "snapshot" if empty). +- Inspect `git status --short` before editing; preserve unrelated user changes. +- Stop and ask the user before committing. diff --git a/.claude/commands/dependency-update.md b/.claude/commands/dependency-update.md new file mode 100644 index 0000000000..9c54da1498 --- /dev/null +++ b/.claude/commands/dependency-update.md @@ -0,0 +1,16 @@ +--- +description: Refresh external dependency versions in buildSrc to their latest non-snapshot release. +argument-hint: "[--dry-run] [paths...]" +allowed-tools: Read, Edit, Write, Grep, Glob, WebFetch, Bash(git status:*), Bash(git diff:*), Bash(./gradlew build:*), Bash(./gradlew clean build:*) +--- + +Follow the `dependency-update` skill exactly: + +- Skill: `.agents/skills/dependency-update/SKILL.md` +- Scope / flags: $ARGUMENTS +- Walk every dependency object under `buildSrc/src/main/kotlin/io/spine/dependency/`. +- Source of truth per artifact: the URL in the file's comment (line `// https://...` or KDoc `@see`). If no URL, fall back to Maven Central AND back-fill the URL comment. +- Filter out snapshots, RCs, alphas, betas, milestones, EAPs, and `-dev` builds. +- Apply the edit. Do NOT commit; emit the report described in the skill. +- Flag `local/` (Spine SDK) updates separately so the user can decide whether to bump in lockstep. +- After the run, suggest the user review the diff and run `./gradlew build` (or `./gradlew clean build` if proto files participate). For `local/` bumps, suggest `./gradlew buildDependants`. diff --git a/.claude/commands/java-to-kotlin.md b/.claude/commands/java-to-kotlin.md new file mode 100644 index 0000000000..6f2c072f93 --- /dev/null +++ b/.claude/commands/java-to-kotlin.md @@ -0,0 +1,13 @@ +--- +description: Convert Java files to idiomatic Kotlin, including Javadoc -> KDoc. +argument-hint: "" +allowed-tools: Read, Edit, Write, Bash(./gradlew:*), Bash(git status:*), Grep, Glob +--- + +Follow the `java-to-kotlin` skill exactly: + +- Skill: `.agents/skills/java-to-kotlin/SKILL.md` +- Target: $ARGUMENTS +- Preserve behavior. Convert Javadoc to KDoc, `@Nullable` to nullable Kotlin types, getters/setters to properties, static methods to companion objects or top-level functions. +- After each file, run `./gradlew compileKotlin` (or the relevant module's compile task) to verify. +- Honor `.agents/coding-guidelines.md` for Kotlin idioms. diff --git a/.claude/commands/move-files.md b/.claude/commands/move-files.md new file mode 100644 index 0000000000..25885f9d77 --- /dev/null +++ b/.claude/commands/move-files.md @@ -0,0 +1,12 @@ +--- +description: Move or rename files/directories, updating all references and build metadata. +argument-hint: " " +allowed-tools: Read, Edit, Bash(git mv:*), Bash(git status:*), Bash(git ls-files:*), Grep, Glob +--- + +Follow the `move-files` skill exactly: + +- Skill: `.agents/skills/move-files/SKILL.md` +- Operation: $ARGUMENTS +- Preflight (run `git status --short`, classify scope) -> Search for all old identifiers -> Move with `git mv` -> Repair references (imports, build metadata, docs) -> Verify. +- Report: Moved[], UpdatedRefs[], Verification[], Risks[]. diff --git a/.claude/commands/pre-pr.md b/.claude/commands/pre-pr.md new file mode 100644 index 0000000000..24499cc517 --- /dev/null +++ b/.claude/commands/pre-pr.md @@ -0,0 +1,32 @@ +--- +description: Run the applicable pre-PR checklist (version gate, build/check, reviewers) and write a sentinel so `gh pr create` is unblocked. +argument-hint: "[base-ref]" +allowed-tools: Read, Write, Grep, Glob, Agent, Bash +--- + +Follow the `pre-pr` skill exactly: + +- Skill: `.agents/skills/pre-pr/SKILL.md` +- Base ref: $ARGUMENTS (treat empty as `master`). +- Detect whether the repository-root `version.gradle.kts` exists. If it is + absent at both the base ref and `HEAD`, the version check is `N/A`; do not + create the file and do not ask for `/bump-version`. +- Run the build/check command selected by the skill and + `.agents/running-builds.md`. The command may be Gradle or non-Gradle. +- Dispatch the reviewers as Claude subagents in parallel — send a single + message with multiple Agent tool uses: + - `kotlin-review` when `.kt|.kts|.java` files changed. + - `review-docs` when `.md` files or KDoc inside sources changed. + - `dependency-audit` when any file under + `buildSrc/src/main/kotlin/io/spine/dependency/` changed. +- Pass the version-check status to reviewers. If it is `N/A`, tell them: + "This repository has no root `version.gradle.kts`; a version bump is not + applicable and must not be reported as missing." +- Each reviewer is read-only; do not pass it edit tools. +- On any reviewer returning `REQUEST CHANGES`, treat the overall result + as `FAIL` and stop before writing the sentinel as `PASS`. +- Sentinel location: `$(git rev-parse --show-toplevel)/.git/pre-pr.ok`, + format per the skill (`head=`, `branch=`, `status=`, `timestamp=`, + `build=`, `reviewers=`, `version=`). Use `git rev-parse HEAD` for the + SHA and `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. +- Do NOT run `gh pr create`. That is the user's next step. diff --git a/.claude/commands/review-docs.md b/.claude/commands/review-docs.md new file mode 100644 index 0000000000..f8043f0ea1 --- /dev/null +++ b/.claude/commands/review-docs.md @@ -0,0 +1,21 @@ +--- +description: Review documentation changes (KDoc/Javadoc and Markdown) against Spine documentation conventions. +argument-hint: "[base-ref | --staged | paths...]" +allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git ls-files:*) +--- + +Follow the `review-docs` skill exactly: + +- Skill: `.agents/skills/review-docs/SKILL.md` +- Scope / flags: $ARGUMENTS + - Empty: review the current branch's diff against `master` (`git diff master...HEAD`). + - `--staged`: review staged changes only (`git diff --staged`). + - A base ref (e.g. `master`, `origin/master`, a commit SHA): review `git diff ...HEAD`. + - Explicit paths: limit the review to those paths in addition to the diff scope. +- The skill owns the procedure, the per-area checks (KDoc/Javadoc, Markdown, + prose flow, terminology), and the output format (Must fix / Should fix / + Nits + one-line verdict). +- Stay in scope: documentation only. If a code-quality issue surfaces, + note it briefly as a Nit pointing at `/review` (or the `kotlin-review` + agent) — do not expand the review. +- Read-only: do not edit files, do not run builds. diff --git a/.claude/commands/run-build.md b/.claude/commands/run-build.md new file mode 100644 index 0000000000..8a8d84ca0b --- /dev/null +++ b/.claude/commands/run-build.md @@ -0,0 +1,12 @@ +--- +description: Build the project the right way based on what changed (proto vs. Kotlin/Java vs. docs). +allowed-tools: Bash(./gradlew:*), Bash(git status:*), Bash(git diff:*) +--- + +Decide which build to run by looking at `git status --short` and `git diff --name-only`: + +- If any `.proto` files changed: `./gradlew clean build` +- Else if Kotlin or Java source changed: `./gradlew build` +- Else if only docs/comments changed (KDoc / Javadoc / Markdown): `./gradlew dokka`. Tests are NOT required for doc-only changes. + +Report the chosen command and its result. See `.agents/running-builds.md`. diff --git a/.claude/commands/update-copyright.md b/.claude/commands/update-copyright.md new file mode 100644 index 0000000000..076fb6133a --- /dev/null +++ b/.claude/commands/update-copyright.md @@ -0,0 +1,12 @@ +--- +description: Refresh copyright headers from the IntelliJ profile, replacing today.year with the current year. +argument-hint: "[paths...]" +allowed-tools: Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*), Read +--- + +Follow the `update-copyright` skill exactly: + +- Skill: `.agents/skills/update-copyright/SKILL.md` +- Run: `python3 .agents/skills/update-copyright/scripts/update_copyright.py $ARGUMENTS` +- If $ARGUMENTS is empty, run once with `--dry-run`, show the output to the user, then run without `--dry-run`. +- Never add a header to a file that doesn't already have one. diff --git a/.claude/commands/write-docs.md b/.claude/commands/write-docs.md new file mode 100644 index 0000000000..b9b9a742b2 --- /dev/null +++ b/.claude/commands/write-docs.md @@ -0,0 +1,14 @@ +--- +description: Write or update Markdown / KDoc documentation per Spine documentation conventions. +argument-hint: "" +allowed-tools: Read, Edit, Write, Grep, Glob +--- + +Follow the `writer` skill exactly: + +- Skill: `.agents/skills/writer/SKILL.md` +- Topic / target: $ARGUMENTS +- Decide audience first (end user, contributor, maintainer, tooling). +- Prefer updating an existing doc over creating a new one. +- Keep `docs/data/docs/

//sidenav.yml` in sync when adding, removing, moving, or renaming pages under `docs/content/docs/
/`. +- Honor `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..0053d24c55 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(./gradlew:*)", + "Bash(./config/gradlew:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git switch:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git restore:*)", + "Bash(git stash:*)", + "Bash(git fetch:*)", + "Bash(git rev-parse:*)", + "Bash(git ls-files:*)", + "Bash(git mv:*)", + "Bash(git submodule status:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(find:*)", + "Bash(rg:*)", + "Bash(grep:*)", + "Bash(mkdir:*)", + "Bash(touch:*)", + "Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)", + "Bash(./config/pull)", + "Bash(./config/migrate)" + ], + "deny": [ + "Bash(git push:*)", + "Bash(git reset --hard:*)", + "Bash(git clean -fdx:*)", + "Bash(rm -rf /:*)", + "Bash(rm -rf ~:*)", + "Bash(gh pr merge:*)", + "Bash(gh release create:*)" + ], + "ask": [ + "Bash(git commit:*)", + "Bash(git rebase:*)", + "Bash(git merge:*)", + "Bash(git cherry-pick:*)", + "Bash(./gradlew publish:*)", + "Bash(./gradlew uploadArtifacts:*)", + "Bash(./gradlew clean:*)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/protect-version-file.sh" + } + ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/pre-pr-gate.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/publish-version-gate.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/sanitize-source-code.sh" + } + ] + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index 142af9873c..6251d1c93a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,3 +3,13 @@ For detailed agent guidelines and documentation, please see: **[Agent Documentation](./.agents/_TOC.md)** + +## Moving Files + +When moving or renaming tracked files, always use `git mv`. + +Do not simulate a move by deleting the old file and creating a new file. Preserve +Git history unless the user explicitly asks for a fresh file replacement. + +If `git mv` fails because of permissions or sandbox restrictions, request +approval to run `git mv`; do not fall back to delete/create. diff --git a/CLAUDE.md b/CLAUDE.md index 38753d02a6..6d374c6727 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,17 +1,61 @@ -# Project Configuration for Claude Code +# CLAUDE.md — Project Conventions -## Agent Guidelines -Please read and follow all guidelines in the project's agent documentation: +## Project Guidelines -- Start with the table of contents: `.agents/_TOC.md`. -- Follow all linked documents from the TOC. -- Apply all coding standards, formatting rules, and project conventions found in these documents. +- Quick-reference baseline: `.agents/quick-reference-card.md` — always read this first. +- For specific tasks (code review, PR prep, dependency updates, docs, etc.): prefer the matching + skill from `.agents/skills/`. +- Full standards reference: `.agents/_TOC.md` — consult when a skill + doesn't cover the needed context. -## Key Points for Claude Code -- All guidelines in the `.agents` directory apply to Claude Code interactions. -- Pay special attention to Kotlin formatting requirements (trailing newlines, detekt compliance). -- Follow project-specific conventions documented in the agent guidelines. +## Workflow Rules -## Priority -The `.agents` directory contains the authoritative project standards. -These take precedence over default behaviors. +- Use Plan mode (`EnterPlanMode`) for architecture, refactoring, multi-file changes, or lengthy documentation. +- Write the plan to `.agents/tasks/.md` before coding. See `.agents/tasks/README.md` for format and lifecycle. +- If something goes wrong — STOP and re-plan immediately. +- One focused task per subagent. + +## Memory + +Two stores, split by audience: + +- **Team-shared memory** lives in `.agents/memory/` (checked into git). Use it + for feedback rules, durable project rationale, and external system pointers. + See `.agents/memory/README.md` for layout and write protocol. +- **Per-developer memory** lives in the built-in auto-memory dir. Use it for + personal preferences, ephemeral project state, and per-machine resources. + +Litmus test: *would a teammate benefit from this next month?* → repo. +Otherwise → auto-memory. + +Review `.agents/memory/MEMORY.md` at the start of every session. +Ruthlessly iterate until mistakes stop repeating. + +## Verification & Quality + +- Never mark a task done without proof (tests, logs, diff vs main). +- Ask: "Would a senior/staff engineer approve this?" +- For non-trivial changes: pause and consider a more elegant solution. +- Fix bugs autonomously — find root cause, no hand-holding, no band-aids. + +## Core Principles + +- Simplicity first: minimal code impact, minimal surface area. +- No laziness: always find root causes. +- Minimal side effects: avoid new bugs. +- Prefer early returns and clear naming. +- Challenge your own work before presenting it. + +## Task Flow + +1. Draft plan → `.agents/tasks/.md` (see README there). +2. Show plan (`ExitPlanMode`) before implementing. +3. Execute + track progress (file at checkpoints, `TaskCreate` for live status). +4. Verify + summarize changes. +5. Update memory if lessons emerged. +6. Delete the task file on merge to master. + +## Final Rule + +This is living team memory. Update it regularly and keep it concise +(<120 lines / ~2.5k tokens). diff --git a/base/src/main/proto/spine/options.proto b/base/src/main/proto/spine/options.proto index 31f02bfa67..7700740784 100644 --- a/base/src/main/proto/spine/options.proto +++ b/base/src/main/proto/spine/options.proto @@ -91,9 +91,34 @@ extend google.protobuf.FieldOptions { // // When this option is set: // - // 1. For message or enum fields, the field must be set to a non-default instance. - // 2. For `string` and `bytes` fields, the value must be set to a non-empty string or an array. - // 3. For repeated fields and maps, at least one element must be present. + // 1. For message fields, the field must not be set to the default instance + // (a message with all fields unset). + // 2. For enum fields, the field must not be set to the constant whose numeric + // value is zero — the first constant declared, as required by proto3. + // 3. For `string` and `bytes` fields, the value must be a non-empty string or + // a non-empty byte sequence, respectively. + // 4. For `repeated` and `map` fields, the field is considered missing when it is + // empty, or when any of its elements (for `map`, any of its values) is itself + // "missing". A per-element "missing" check is applied as follows: + // + // - `string` elements/values: an empty string is treated as missing — analogously + // to how an empty string is rejected for a singular `(required) string` field. + // - `bytes` elements/values: an empty byte sequence is treated as missing — + // analogously to how an empty payload is rejected for a singular + // `(required) bytes` field. + // - message elements/values: the default instance is treated as missing — + // analogously to a singular `(required)` message field. + // - enum elements/values: a constant whose numeric value is zero is treated as + // missing — analogously to a singular `(required)` enum field. + // - Numeric and `bool` elements/values: no per-element check is performed; only + // the presence of at least one element is required. The notion of "missing" is + // not meaningful for these types in a collection context. + // + // For `map` fields, only values are inspected; keys are not checked. Keys + // identify entries and are structurally present whenever an entry exists, so + // `(required)` — which constrains field *content* — applies only to the value + // side of each entry. Consequently, an empty-string key is allowed as long as + // its associated value is not missing. // // Other field types are not supported by the option. // diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt index 463cbf2b1f..6a0a489cc7 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Base.kt @@ -33,8 +33,8 @@ package io.spine.dependency.local */ @Suppress("ConstPropertyName", "unused") object Base { - const val version = "2.0.0-SNAPSHOT.387" - const val versionForBuildScript = "2.0.0-SNAPSHOT.387" + const val version = "2.0.0-SNAPSHOT.389" + const val versionForBuildScript = "2.0.0-SNAPSHOT.389" const val group = Spine.group private const val prefix = "spine" const val libModule = "$prefix-base" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt index 330917d7c1..9f65ab2468 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Compiler.kt @@ -72,7 +72,7 @@ object Compiler : Dependency() { * The version of the Compiler dependencies. */ override val version: String - private const val fallbackVersion = "2.0.0-SNAPSHOT.043" + private const val fallbackVersion = "2.0.0-SNAPSHOT.044" /** * The distinct version of the Compiler used by other build tools. @@ -81,7 +81,7 @@ object Compiler : Dependency() { * transitive dependencies, this is the version used to build the project itself. */ val dogfoodingVersion: String - private const val fallbackDfVersion = "2.0.0-SNAPSHOT.043" + private const val fallbackDfVersion = "2.0.0-SNAPSHOT.044" /** * The artifact for the Compiler Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index 0a52ff7826..3cf7c7e194 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -44,14 +44,14 @@ object CoreJvmCompiler { const val group = Spine.toolsGroup /** - * The version used to in the build classpath. + * The version used in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.063" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.065" /** * The version to be used for integration tests. */ - const val version = "2.0.0-SNAPSHOT.063" + const val version = "2.0.0-SNAPSHOT.065" /** * The ID of the Gradle plugin. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt index 4e285fa9d2..3ff845647a 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt @@ -40,7 +40,7 @@ import io.spine.dependency.Dependency ) object Time : Dependency() { override val group = Spine.group - override val version = "2.0.0-SNAPSHOT.238" + override val version = "2.0.0-SNAPSHOT.242" private const val infix = "spine-time" fun lib(version: String): String = "$group:$infix:$version" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index 600deeda75..f4edf9cb5c 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -36,7 +36,7 @@ object Validation { /** * The version of the Validation library artifacts. */ - const val version = "2.0.0-SNAPSHOT.415" + const val version = "2.0.0-SNAPSHOT.431" /** * The last version of Validation compatible with ProtoData. diff --git a/config b/config index c3ab20a7fc..a6d52be171 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit c3ab20a7fc8cf1464ff45651a19935b5b7aeaea7 +Subproject commit a6d52be171841f3d6dee8e372792541bb37fb67c diff --git a/docs/dependencies/dependencies.md b/docs/dependencies/dependencies.md index aadbf54dc0..4b37924424 100644 --- a/docs/dependencies/dependencies.md +++ b/docs/dependencies/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.389` +# Dependencies of `io.spine:spine-annotations:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 26.0.2. @@ -764,14 +764,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 15 22:53:38 WEST 2026** using +This report was generated on **Wed May 20 21:19:43 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.389` +# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -1616,14 +1616,14 @@ This report was generated on **Fri May 15 22:53:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 15 22:53:38 WEST 2026** using +This report was generated on **Wed May 20 21:19:43 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.389` +# Dependencies of `io.spine:spine-environment:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -2446,14 +2446,14 @@ This report was generated on **Fri May 15 22:53:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 15 22:53:38 WEST 2026** using +This report was generated on **Wed May 20 21:19:43 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.389` +# Dependencies of `io.spine:spine-format:2.0.0-SNAPSHOT.390` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -3356,6 +3356,6 @@ This report was generated on **Fri May 15 22:53:38 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 15 22:53:38 WEST 2026** using +This report was generated on **Wed May 20 21:19:44 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/dependencies/pom.xml b/docs/dependencies/pom.xml index 20ec1c5ad5..032ff76cee 100644 --- a/docs/dependencies/pom.xml +++ b/docs/dependencies/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine base-libraries -2.0.0-SNAPSHOT.389 +2.0.0-SNAPSHOT.390 2015 diff --git a/version.gradle.kts b/version.gradle.kts index c7cfc4b551..5a3f3015aa 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -24,4 +24,4 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -val versionToPublish: String by extra("2.0.0-SNAPSHOT.389") +val versionToPublish: String by extra("2.0.0-SNAPSHOT.390")