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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .agents/_TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +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)
15. [Dependency update](skills/dependency-update/SKILL.md)
16. [Documentation review](skills/review-docs/SKILL.md)
17. [Pre-PR checklist](skills/pre-pr/SKILL.md)
18. [Kotlin code review](skills/kotlin-review/SKILL.md)
19. [Dependency audit](skills/dependency-audit/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)
16 changes: 16 additions & 0 deletions .agents/memory/MEMORY.md
Original file line number Diff line number Diff line change
@@ -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)*
89 changes: 89 additions & 0 deletions .agents/memory/README.md
Original file line number Diff line number Diff line change
@@ -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)
---

<one-paragraph rule or fact>

**Why:** <reason — incident, constraint, team convention>

**How to apply:** <when this kicks in; what to do or avoid>

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).
Empty file.
Empty file added .agents/memory/project/.gitkeep
Empty file.
Empty file.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@
# conflict resolution). Repositories without it must not add it just to satisfy
# hooks or reviewers.
#
# Input: hook JSON on stdin.
# Exit: 0 to allow, 2 to block with stderr message surfaced to Claude.
# Input: hook JSON on stdin. Claude edit tools pass `tool_input.file_path`;
# Codex `apply_patch` passes the patch text in `tool_input.command`.
# Exit: 0 to allow, 2 to block with stderr message surfaced to the agent.
#
set -eu

input=$(cat)
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')

case "$file" in
*/version.gradle.kts|version.gradle.kts)
touches_version_file() {
if [ "$file" = "version.gradle.kts" ] || [ "${file%/version.gradle.kts}" != "$file" ]; then
return 0
fi

printf '%s\n' "$command" \
| grep -qE '^\*\*\* (Add|Update|Delete) File: (.+/)?version\.gradle\.kts$'
}

if touches_version_file; then
cat >&2 <<'EOF'
Direct edits to version.gradle.kts are blocked by a project hook.

Expand All @@ -31,7 +41,6 @@ See:
- .agents/skills/bump-version/SKILL.md
EOF
exit 2
;;
esac
fi

exit 0
93 changes: 93 additions & 0 deletions .agents/scripts/publish-version-gate.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
'./gradlew' blocked: branch differs from the base ref but
version.gradle.kts is not bumped. Publishing would overwrite the Maven
Local artifact at the base version, which integration tests in consumer
repos may rely on.

Run /version-bumped to auto-recover (it invokes /bump-version and re-runs
the check), or /bump-version directly.

Underlying check (.agents/skills/version-bumped/scripts/version-bumped.sh) reported:
$err_payload
EOF
exit 2
47 changes: 47 additions & 0 deletions .agents/scripts/sanitize-source-code.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
#
# PostToolUse hook: enforce the source-code formatting rules from
# .agents/coding-guidelines.md after Edit/Write/MultiEdit:
# - strip trailing whitespace
# - replace 2+ consecutive blank lines with a single blank line
#
# Input: hook JSON on stdin. Claude Code passes `tool_input.file_path`;
# Codex `apply_patch` passes the patch text in `tool_input.command`.
# Exit: 0 always (post-tool-use; never block).
#
set -eu

input=$(cat)
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')

sanitize_file() {
local path="$1"

[ -z "$path" ] && return 0
[ ! -f "$path" ] && return 0

case "$path" in
*.java|*.kt|*.kts) ;;
*) return 0 ;;
esac

tmp=$(mktemp)
awk '
{ sub(/[ \t]+$/, "") }
/^$/ { blank++; if (blank > 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
8 changes: 8 additions & 0 deletions .agents/skills/bump-gradle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
17 changes: 11 additions & 6 deletions .agents/skills/dependency-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,18 @@ When the run completes, emit a Markdown report with these sections:
End with the suggested next steps:

1. Review the diff (`git diff buildSrc/src/main/kotlin/io/spine/dependency/`).
2. Run `./gradlew build` (or `./gradlew clean build` if `.proto` files
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).
3. If any `local/` artifacts moved, run `./gradlew buildDependants` (the
`ConfigTester` task) to confirm downstream repos still build.
4. Commit. The conventional message is
`chore(deps): refresh external versions` (or a more specific subject if
the diff is small).
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

Expand Down
8 changes: 8 additions & 0 deletions .agents/skills/java-to-kotlin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 9 additions & 1 deletion .agents/skills/move-files/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/pre-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ reviewers=<comma-separated reviewer names that were invoked>
version=<old->new, introduced:<new>, or "not-applicable">
```

The `gh pr create` hook (`.claude/scripts/pre-pr-gate.sh`) checks this
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.
Expand Down
Loading
Loading