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
9 changes: 8 additions & 1 deletion .agents/_TOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion .agents/coding-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.
2 changes: 1 addition & 1 deletion .agents/project-structure-expectations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```
73 changes: 73 additions & 0 deletions .agents/scripts/pre-pr-gate.sh
Original file line number Diff line number Diff line change
@@ -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 <<EOF
'gh pr create' blocked: pre-PR checks have not run on this clone.

Run /pre-pr first. It runs the applicable build/check command, applies the
version gate only when this repository has a root version.gradle.kts, dispatches
the configured reviewers, then writes $sentinel on success.
EOF
fi

sentinel_status=$(awk -F= '/^status=/{print $2}' "$sentinel")
sentinel_sha=$(awk -F= '/^head=/{print $2}' "$sentinel")
head_sha=$(git -C "$repo_root" rev-parse HEAD)

if [ "$sentinel_status" != "PASS" ]; then
block <<EOF
'gh pr create' blocked: the last /pre-pr run reported status='$sentinel_status'.

Fix the issues and re-run /pre-pr before creating the PR.
Sentinel: $sentinel
EOF
fi

if [ "$sentinel_sha" != "$head_sha" ]; then
block <<EOF
'gh pr create' blocked: /pre-pr was last run at commit
$sentinel_sha
but HEAD is now
$head_sha

Re-run /pre-pr to revalidate the current tree.
EOF
fi

exit 0
46 changes: 46 additions & 0 deletions .agents/scripts/protect-version-file.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
#
# PreToolUse hook: block direct Edit/Write/MultiEdit on `version.gradle.kts`.
# In repositories that have this file, the bump-version skill owns the
# version-bump policy (snapshot numbering, rebuilds, dependency-report updates,
# conflict resolution). Repositories without it must not add it just to satisfy
# hooks or reviewers.
#
# 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')

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.

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
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:]]|$)'
Comment thread
alexander-yevsyukov marked this conversation as resolved.
}

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')
Comment thread
alexander-yevsyukov marked this conversation as resolved.

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 }
Comment thread
alexander-yevsyukov marked this conversation as resolved.
' "$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
Loading
Loading