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
6 changes: 6 additions & 0 deletions .agents/quick-reference-card.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@
- Always include tests with code changes
- Version bump required for all PRs
```

🚫 **Do not commit, push, or tag** without explicit authorization. See
[`safety-rules.md`](safety-rules.md) → *Commits and history-writing*.
Authorization comes only from a skill's `## Commit authorization`
section or from the user's current prompt — never from prior turns or
memory.
42 changes: 42 additions & 0 deletions .agents/safety-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,45 @@
- ❌ Never use reflection or unsafe code without an explicit approval.
- ❌ No analytics or telemetry code.
- ❌ No blocking calls inside coroutines.

## Commits and history-writing

**Default: do not write to git history.** This is a hard rule for every
agent — the main thread, every subagent, every skill. It overrides any
local convenience or "the change looks done" instinct.

The rule covers all of these operations:

- `git commit`, `git commit-tree`
- `git push`, `git push --force`
- `git tag`
- `git rebase`, `git merge`, `git cherry-pick` against shared history
- `git reset` that discards committed work
- `gh release create`, `gh pr merge`

Authorization to perform one of these operations exists only when **one**
of the following is true *right now*:

1. **Skill-declared.** The currently active skill's `SKILL.md` contains
a `## Commit authorization` section that explicitly authorizes the
operation and constrains it (which files may be staged, the exact
commit subject, the maximum number of commits). The mere mention of
a commit message inside skill prose is **not** authorization — the
section heading must be present.
2. **User-instructed.** The user's *current* prompt explicitly tells
the agent to perform the operation. Examples that qualify:
"commit this", "make a commit with subject X", "push the branch",
"tag this release". Authorization from previous turns, from
`CLAUDE.md`, or from any memory file does **not** carry over.

If neither holds, the agent:

1. Stages relevant changes with `git add` (only if helpful for review).
2. Prints the proposed commit subject (if any) and `git diff --staged`.
3. **Stops.** The user runs the commit themselves, or replies with
explicit authorization in the next prompt.

The project's `.claude/settings.json` keeps `Bash(git commit:*)` in
`permissions.ask` as defense-in-depth, but the primary enforcement is
this rule — agents must not propose commit attempts that rely on the
user clicking the prompt.
44 changes: 44 additions & 0 deletions .agents/scripts/update-copyright.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
#
# PostToolUse hook: refresh the copyright header of source files touched by
# Edit/Write/MultiEdit. Delegates to
# .agents/skills/update-copyright/scripts/update_copyright.py, which:
# - operates only on recognized source extensions,
# - never adds a header to a file that does not already have one,
# - rewrites `today.year` to the current year per the IntelliJ profile.
#
# 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')

root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
script="$root/.agents/skills/update-copyright/scripts/update_copyright.py"

[ -f "$script" ] || exit 0

update_path() {
local path="$1"
[ -z "$path" ] && return 0
[ ! -f "$path" ] && return 0
python3 "$script" --root "$root" "$path" >/dev/null 2>&1 || true
}

if [ -n "$file" ]; then
update_path "$file"
exit 0
fi

printf '%s\n' "$command" \
| sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \
| sort -u \
| while IFS= read -r path; do
update_path "$path"
done

exit 0
23 changes: 23 additions & 0 deletions .agents/skills/bump-gradle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ https://docs.gradle.org/current/release-notes.html#upgrade-instructions

Always check that page at task time. Do not rely on remembered Gradle versions.

## Commit authorization

This skill is authorized to run `git commit` **up to two times** per
invocation, under these constraints:

1. **Gradle wrapper commit.** Stage only the Gradle wrapper files
(`gradle/wrapper/gradle-wrapper.properties`,
`gradle/wrapper/gradle-wrapper.jar`, `gradlew`, `gradlew.bat`, plus
files directly required by the wrapper update). Subject:
`` Bump Gradle -> `GRADLE_VERSION` `` with the actual version
substituted. Skip if no wrapper-owned file changed.

2. **Dependency-report commit** (separate from the wrapper commit). Stage
only generated dependency-report files (`docs/dependencies/pom.xml`,
`docs/dependencies/dependencies.md`). Subject:
`Update dependency reports`. Skip if the build did not regenerate
those files.

No `git push`, `git tag`, `git rebase`, `git commit --amend`, or any other
history-writing operation. Those require a separate authorization
(`.agents/safety-rules.md` → *Commits and history-writing*). Do not create
empty commits, and do not bundle unrelated changes into either commit.

## Checklist

1. Work from the target repository root.
Expand Down
17 changes: 17 additions & 0 deletions .agents/skills/bump-version/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ skill's target repository, CI runs the `Version Guard` workflow, which invokes
project version already exists in the Maven repository. It does not compare git
branches or inspect commit subjects; the checks below are agent-side guardrails.

## Commit authorization

This skill is authorized to run `git commit` **exactly once** per invocation,
under these constraints:

- Stage only `version.gradle.kts`. Any other modified files are out of scope
for this skill's commit and must remain unstaged.
- Use the exact subject `` Bump version -> `<new>` `` (see step 4 of the
Checklist) with the actual new version value substituted.
- No `git push`, `git tag`, `git rebase`, `git commit --amend`, or any other
history-writing operation. Those require a separate authorization
(`.agents/safety-rules.md` → *Commits and history-writing*).

If the bump cannot be performed cleanly (no diff to commit, conflicting
staged files, build failures preceding the commit), report and stop — do not
create the commit.

## Checklist

1. Work from the target repository root.
Expand Down
37 changes: 25 additions & 12 deletions .agents/skills/dependency-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBo

## How to run an audit

1. **Fetch the full diff once.** Run
`git diff <base>...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.
1. **Fetch the full diff once.** Default base is `origin/master`:
`git diff origin/master...HEAD -- 'buildSrc/src/main/kotlin/io/spine/dependency/**'`
(use `--staged` if the user is mid-commit, or a different base only if
the user names one). 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
Expand All @@ -66,8 +67,17 @@ Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBo
removed `const val`.
- `rg -L 'Copyright \(c\) 2026' <changed-files>` to flag every stale
header in one call.
- `rg -n '<lib>:<oldVersion>' --type kt --type gradle` once per
library to check for hardcoded pins.
- `rg -L '@Suppress\("unused", "ConstPropertyName"\)' <changed-files>`
to flag missing object-level suppression in one call.
- `rg -n '(lib1:oldv1|lib2:oldv2)' --type kt --type gradle` — one
alternation across libraries, not one command per library.

5. **Fast path for pure version bumps.** If every hunk only modifies an
existing `version` (or `bom`) string literal — no added/removed
`const val`, no new files, no renames — run only Checks A and D.
Skip B, C, and E entirely. This is the dominant `/dependency-update`
shape; do not waste tool calls re-validating naming or deprecation
discipline when nothing structural changed.

## Checks

Expand All @@ -77,7 +87,9 @@ Each file declares a Kotlin `object` extending `Dependency` or `DependencyWithBo
`.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 '<libName>:<oldVersion>'`.
isn't pinned to the release elsewhere. Use the batched ripgrep recipe
in step 4 — one alternation across all switched libraries, not one
command per library.
- **BOM ↔ component agreement.** For objects extending `DependencyWithBom`,
check that `bom` references the same version as `version` (e.g. Kotlin's
`kotlin-bom:$runtimeVersion`).
Expand All @@ -96,9 +108,10 @@ 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 '<oldName>'` to confirm no caller is left behind. If callers exist,
this is a Must-fix.
- If the diff deletes one or more `const val`s outright, confirm no caller
is left behind. Use the batched ripgrep recipe in step 4 — one
alternation over all removed symbol names, not one `git grep` per
name. If any caller survives, this is a Must-fix.

### D. Convention drift
- **Copyright header year.** Every changed file should have a current-year
Expand Down
Loading
Loading