From 7390fe6b2230a72ae08aae58c7b39c8b19262926 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Wed, 27 May 2026 18:07:22 +0100 Subject: [PATCH 01/28] Update `config` --- .agents/_TOC.md | 2 +- .agents/jvm-project.md | 37 ++ .agents/memory/MEMORY.md | 5 +- .../memory/feedback/copilot-review-request.md | 35 ++ .../memory/reference/anthropic-api-caching.md | 52 +++ .agents/memory/reference/cache-warm-window.md | 33 ++ .agents/project-overview.md | 7 - .agents/project.md | 18 + .agents/project.template.md | 18 + .agents/quick-reference-card.md | 10 +- .agents/scripts/pre-pr-gate.sh | 2 + .agents/scripts/protect-version-file.sh | 2 + .agents/scripts/publish-version-gate.sh | 2 + .agents/scripts/sanitize-source-code.sh | 2 + .agents/skills/check-links/SKILL.md | 113 +++--- .agents/skills/pre-pr/SKILL.md | 235 ++++++------ .agents/tasks/check-links.md | 342 ------------------ .agents/tasks/issue-175-set-once-timestamp.md | 58 --- .agents/tasks/prompt-caching-org.md | 165 +++++++++ .agents/tasks/reject-required-on-empty.md | 129 ------- .../setup-cross-tool-agent-instructions.md | 138 +++++++ .claude/settings.json | 2 +- .github/copilot-instructions.md | 26 ++ .github/workflows/check-links.yml | 71 +++- .gitignore | 8 +- .idea/live-templates/README.md | 6 +- .idea/live-templates/User.xml | 2 +- .idea/misc.xml | 17 +- AGENTS.md | 80 +++- CLAUDE.md | 76 +--- config | 2 +- 31 files changed, 885 insertions(+), 810 deletions(-) create mode 100644 .agents/jvm-project.md create mode 100644 .agents/memory/feedback/copilot-review-request.md create mode 100644 .agents/memory/reference/anthropic-api-caching.md create mode 100644 .agents/memory/reference/cache-warm-window.md delete mode 100644 .agents/project-overview.md create mode 100644 .agents/project.md create mode 100644 .agents/project.template.md delete mode 100644 .agents/tasks/check-links.md delete mode 100644 .agents/tasks/issue-175-set-once-timestamp.md create mode 100644 .agents/tasks/prompt-caching-org.md delete mode 100644 .agents/tasks/reject-required-on-empty.md create mode 100644 .agents/tasks/setup-cross-tool-agent-instructions.md create mode 100644 .github/copilot-instructions.md diff --git a/.agents/_TOC.md b/.agents/_TOC.md index dc8cde3ce9..4be0656bdf 100644 --- a/.agents/_TOC.md +++ b/.agents/_TOC.md @@ -1,7 +1,7 @@ # Table of Contents 1. [Quick Reference Card](quick-reference-card.md) -2. [Project overview](project-overview.md) +2. [JVM project requirements](jvm-project.md) — language, build, and review checklist shared by all JVM repos 3. [Coding guidelines](coding-guidelines.md) 4. [Documentation & comments](documentation-guidelines.md) 5. [Documentation tasks](documentation-tasks.md) diff --git a/.agents/jvm-project.md b/.agents/jvm-project.md new file mode 100644 index 0000000000..e3c5d650d1 --- /dev/null +++ b/.agents/jvm-project.md @@ -0,0 +1,37 @@ +# JVM Project Requirements + +General requirements for all JVM projects in the Spine SDK organisation. +Repo-specific `project.md` files link here and add their own context. + +## Language and build + +- **Languages**: Kotlin (primary), Java (secondary). +- **Build**: Gradle with Kotlin DSL. +- **Static analysis**: detekt, ErrorProne, Checkstyle, PMD. +- **Testing**: JUnit 5, Kotest Assertions, Codecov. + +## Code review checklist + +**Correctness and safety** +- Code compiles and passes static analysis (detekt, ErrorProne, Checkstyle, PMD). +- No reflection or unsafe code unless explicitly approved in scope. +- No analytics, telemetry, or tracking code. +- No blocking calls inside coroutines. + +**Kotlin/Java style** +- Kotlin idioms preferred: extension functions, `when` expressions, data/sealed + classes, immutable data structures. +- No `!!` unless provably safe. No unchecked casts. +- No mutable state without justification. +- No string duplication — use constants. + +**Tests** +- New or changed functionality must include tests. +- Use stubs, not mocks. +- Prefer [Kotest assertions][kotest-assertions] over JUnit or Google Truth. + +**Versioning** +- If the repo has `version.gradle.kts`, every PR must include a version bump. + Flag the absence as a required change. + +[kotest-assertions]: https://kotest.io/docs/assertions/assertions.html diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index cfc2e843fa..2c8045c6ed 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -5,7 +5,7 @@ See [README.md](README.md) for the format and routing rules. ## Feedback (validated patterns & corrections) -*(no entries yet)* +- [copilot-review-request](feedback/copilot-review-request.md) — GraphQL `requestReviews` with `botIds: ["BOT_kgDOCnlnWA"]`; REST endpoint silently no-ops on re-requests. ## Project (durable context & rationale) @@ -13,4 +13,5 @@ See [README.md](README.md) for the format and routing rules. ## Reference (external systems) -*(no entries yet)* +- [cache-warm-window](reference/cache-warm-window.md) — How prompt cache entries are shared between sibling-repo sessions and how to maximise overlap. +- [anthropic-api-caching](reference/anthropic-api-caching.md) — Pattern and pricing for adding prompt caching to any direct Anthropic API call. diff --git a/.agents/memory/feedback/copilot-review-request.md b/.agents/memory/feedback/copilot-review-request.md new file mode 100644 index 0000000000..f5dde9b460 --- /dev/null +++ b/.agents/memory/feedback/copilot-review-request.md @@ -0,0 +1,35 @@ +--- +name: copilot-review-request +description: How to request or re-request a Copilot PR review programmatically — GraphQL botIds is the only reliable path +metadata: + type: feedback + since: 2026-05-25 +--- + +Use the GraphQL `requestReviews` mutation with `botIds` for both initial +requests and re-requests: + +```bash +gh api graphql -f query=' +mutation { + requestReviews(input: { + pullRequestId: "PR_NODE_ID", + botIds: ["BOT_kgDOCnlnWA"] + }) { + pullRequest { id number } + } +}' +``` + +- `PR_NODE_ID`: `gh api repos/SpineEventEngine/REPO/pulls/NUMBER --jq '.node_id'` +- `BOT_kgDOCnlnWA`: fixed node ID for the Copilot PR reviewer bot (stable) + +**Why:** The REST endpoint (`POST .../requested_reviewers` with +`reviewers[]=Copilot`) silently no-ops on re-requests — it only works for +the first-ever request on a PR. The GraphQL `userIds` field also fails +because Copilot is a Bot, not a User. `botIds` is the correct field and +works for both initial and re-requests. + +**How to apply:** Any time a Copilot review needs to be requested or +re-requested, use the GraphQL mutation above. Do not use the REST endpoint +or `@copilot review` comments. diff --git a/.agents/memory/reference/anthropic-api-caching.md b/.agents/memory/reference/anthropic-api-caching.md new file mode 100644 index 0000000000..bcb1be4ccd --- /dev/null +++ b/.agents/memory/reference/anthropic-api-caching.md @@ -0,0 +1,52 @@ +--- +name: anthropic-api-caching +description: Pattern and pricing for adding prompt caching to any direct Anthropic API call. +metadata: + type: reference + since: 2026-05-24 +--- + +Use this when adding a direct Anthropic API call (GitHub Actions workflow, +script, or tool) that sends a stable system prompt. + +**Add `cache_control` to the system message block:** + +```python +system=[{ + "type": "text", + "text": "", + "cache_control": {"type": "ephemeral", "ttl": "1h"} +}] +``` + +Use `ttl: "1h"` for any caller whose requests are spaced more than 5 minutes +apart (GitHub Actions jobs, scheduled tasks, skill invocations). Use the +default 5-minute TTL only for tight interactive loops. + +**Pricing (input tokens):** + +| Operation | Cost multiplier | +|---|---| +| Cache write (5-min TTL) | 1.25× base input price | +| Cache write (1-hour TTL) | 2× base input price | +| Cache read (any TTL) | 0.1× base input price | + +A single cache hit within the TTL window recovers the write premium. Multiple +hits within the hour make the 2× write cost negligible. + +**Place stable content before dynamic content.** Cache breakpoints apply to +everything *before* the `cache_control` marker. Dynamic per-request content +(user query, file diff, current date) must come after the last breakpoint. + +**Monitor hits via the usage object:** +```python +print(response.usage.cache_read_input_tokens) # 0 on miss, >0 on hit +print(response.usage.cache_creation_input_tokens) # tokens written to cache +``` + +**Future:** once direct API calls exist in this org, consider a cache pre-warm +job triggered on push to `master` — calls the API with `max_tokens: 0` and +`cache_control: {ttl: "1h"}` so the first session after a config change +hits rather than writes. + +Related: [[cache-warm-window]] diff --git a/.agents/memory/reference/cache-warm-window.md b/.agents/memory/reference/cache-warm-window.md new file mode 100644 index 0000000000..796dd4d303 --- /dev/null +++ b/.agents/memory/reference/cache-warm-window.md @@ -0,0 +1,33 @@ +--- +name: cache-warm-window +description: How prompt cache entries are shared between sibling-repo sessions and how to maximise overlap. +metadata: + type: reference + since: 2026-05-24 +--- + +Claude Code sessions share a prompt cache entry when they send byte-identical +content within the cache TTL window. Because `migrate` copies `CLAUDE.md` and +`.agents/` verbatim, any two sessions on the same config version share the +same cache slot — provided they fall within the TTL. + +**TTL in effect for Console OAuth users:** +- Default: **5 minutes** (applies to all non-subscription auth) +- With `ENABLE_PROMPT_CACHING_1H=1` in `~/.claude/settings.json`: **1 hour** + +Developers must have `ENABLE_PROMPT_CACHING_1H=1` set, otherwise the +window is too short for cross-session hits to occur reliably. +This setting will work ONLY for Claude Code which runs the CLI binary. +It will not work for JetBrains Air or any other IDE plugin which does not +run the Claude Code CLI binary. + +**Cache is per Anthropic workspace.** All developers authenticated via the +same Anthropic organisation Console org share the same cache pool. Do not +create separate Console workspaces per developer — that would isolate their +cache entries. + +**Practical impact:** Realistic concurrency is 1–2 sessions at a time. The +first session after a config change pays the cache-write cost; any session +starting within the next hour (with 1H TTL) reads from cache at 0.1× cost. + +Related: [[anthropic-api-caching]] diff --git a/.agents/project-overview.md b/.agents/project-overview.md deleted file mode 100644 index dfac73f03f..0000000000 --- a/.agents/project-overview.md +++ /dev/null @@ -1,7 +0,0 @@ -# 🛠️ Project overview - -- **Languages**: Kotlin (primary), Java (secondary). -- **Build tool**: Gradle with Kotlin DSL. -- **Static analysis**: detekt, ErrorProne, Checkstyle, PMD. -- **Testing**: JUnit 5, Kotest Assertions, Codecov. -- **Tools used**: Gradle plugins, IntelliJ IDEA Platform, KSP, KotlinPoet, Dokka. diff --git a/.agents/project.md b/.agents/project.md new file mode 100644 index 0000000000..b6882e03af --- /dev/null +++ b/.agents/project.md @@ -0,0 +1,18 @@ + + +# Project: + +## Overview + +*One paragraph: what this repo is, what problem it solves, and its role in the +Spine SDK organisation.* + +## Architecture + +*Role in the org: library / tool / Gradle plugin / application. +Key patterns, public API boundaries, and constraints specific to this repo.* + + diff --git a/.agents/project.template.md b/.agents/project.template.md new file mode 100644 index 0000000000..b6882e03af --- /dev/null +++ b/.agents/project.template.md @@ -0,0 +1,18 @@ + + +# Project: + +## Overview + +*One paragraph: what this repo is, what problem it solves, and its role in the +Spine SDK organisation.* + +## Architecture + +*Role in the org: library / tool / Gradle plugin / application. +Key patterns, public API boundaries, and constraints specific to this repo.* + + diff --git a/.agents/quick-reference-card.md b/.agents/quick-reference-card.md index 27105d2d1b..2e890e4289 100644 --- a/.agents/quick-reference-card.md +++ b/.agents/quick-reference-card.md @@ -1,14 +1,6 @@ # 📝 Quick Reference Card -``` -🔑 Key Information: -- Kotlin/Java project with CQRS architecture -- Follow coding guidelines in Spine Event Engine docs -- Always include tests with code changes -- Version bump required for all PRs -``` - -🚫 **Do not commit, push, or tag** without explicit authorization. See +🚫 **Do not write to git history** (commit/push/tag/rebase/merge/cherry-pick/reset/`gh pr merge`) 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 diff --git a/.agents/scripts/pre-pr-gate.sh b/.agents/scripts/pre-pr-gate.sh index cb80b31251..88de51c2a8 100755 --- a/.agents/scripts/pre-pr-gate.sh +++ b/.agents/scripts/pre-pr-gate.sh @@ -9,6 +9,8 @@ # set -eu +command -v jq >/dev/null 2>&1 || exit 0 + input=$(cat) tool=$(printf '%s' "$input" | jq -r '.tool_name // empty') [ "$tool" != "Bash" ] && exit 0 diff --git a/.agents/scripts/protect-version-file.sh b/.agents/scripts/protect-version-file.sh index cd0fbdd69b..2184468cf9 100755 --- a/.agents/scripts/protect-version-file.sh +++ b/.agents/scripts/protect-version-file.sh @@ -12,6 +12,8 @@ # set -eu +command -v jq >/dev/null 2>&1 || exit 0 + input=$(cat) file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') diff --git a/.agents/scripts/publish-version-gate.sh b/.agents/scripts/publish-version-gate.sh index 466f0a8002..996bd25656 100755 --- a/.agents/scripts/publish-version-gate.sh +++ b/.agents/scripts/publish-version-gate.sh @@ -17,6 +17,8 @@ # set -eu +command -v jq >/dev/null 2>&1 || exit 0 + input=$(cat) tool=$(printf '%s' "$input" | jq -r '.tool_name // empty') [ "$tool" != "Bash" ] && exit 0 diff --git a/.agents/scripts/sanitize-source-code.sh b/.agents/scripts/sanitize-source-code.sh index 821bf8977c..f74c867a93 100755 --- a/.agents/scripts/sanitize-source-code.sh +++ b/.agents/scripts/sanitize-source-code.sh @@ -11,6 +11,8 @@ # set -eu +command -v jq >/dev/null 2>&1 || exit 0 + input=$(cat) file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') diff --git a/.agents/skills/check-links/SKILL.md b/.agents/skills/check-links/SKILL.md index b67942e030..a4c61a0c5b 100644 --- a/.agents/skills/check-links/SKILL.md +++ b/.agents/skills/check-links/SKILL.md @@ -1,27 +1,27 @@ --- name: check-links description: > - Validate the Hugo documentation site under `docs/` for broken links. - Builds the site, starts the Hugo server locally, runs Lychee against the - rendered HTML using the repo's `lychee.toml`, and reports any broken URLs + Validate the Hugo documentation site under `docs/` or `site/` for broken + links. Builds the site, starts the Hugo server locally, runs Lychee against + the rendered HTML using the repo's `lychee.toml`, and reports any broken URLs grouped by source Markdown page. Use locally before pushing changes that - touch `docs/**`, when CI's `Check Links` job fails, or whenever the user - asks to "check doc links". Read-only with respect to the project sources. - Does **not** cover Javadoc/KDoc (out of scope for this skill). + touch `docs/**` or `site/**`, when CI's `Check Links` job fails, or whenever + the user asks to "check doc links". Read-only with respect to the project + sources. Does **not** cover Javadoc/KDoc (out of scope for this skill). --- # Check links in the Hugo docs (repo-specific) You are the documentation link checker for this Spine Event Engine project. -You build the site under `docs/`, serve it locally on port `1414`, run Lychee -against the rendered HTML, and report broken URLs. You mirror what the -`.github/workflows/check-links.yml` workflow does in CI: same Hugo version, -same Lychee version, same Hugo environment (`development`), and the same -`lychee.toml`. Two deliberate differences remain: the skill serves on port -`1414` (CI uses `1313`) to avoid clashing with a developer's local -`hugo server`, and the skill writes a local sentinel that CI does not. -Both differences are harmless because `--base-url` is rewritten to match -the local port and the sentinel is consumed only by the local `pre-pr` +You build the site under `docs/` or `site/` (auto-detected; see step 0), serve +it locally on port `1414`, run Lychee against the rendered HTML, and report +broken URLs. You mirror what the `.github/workflows/check-links.yml` workflow +does in CI: same Hugo version, same Lychee version, same Hugo environment +(`development`), and the same `lychee.toml`. Two deliberate differences remain: +the skill serves on port `1414` (CI uses `1313`) to avoid clashing with a +developer's local `hugo server`, and the skill writes a local sentinel that CI +does not. Both differences are harmless because `--base-url` is rewritten to +match the local port and the sentinel is consumed only by the local `pre-pr` skill. ### Pinned versions @@ -44,8 +44,8 @@ both the skill and CI). ## When to run -- Any change touches `docs/**` (including reference links, `embed-code` - blocks, sidenav YAML files, content under `docs/content/`). +- Any change touches `docs/**` or `site/**` (including reference links, + `embed-code` blocks, sidenav YAML files, content under `/content/`). - A change touches `lychee.toml` itself. - CI reported broken links and you want a fast local repro. - The user asks to "check the doc links" or invokes `/check-links`. @@ -78,12 +78,46 @@ version matches what the CI workflow uses, so behavior is identical. Execute the steps in order. On the first failure, stop, write a `FAIL` sentinel (step 8), and report the failure with the next action. +### 0. Detect site root and work directory + +Before any other step, determine `SITE_DIR` (the Hugo site root) and `WORK_DIR` +(the directory where `npm ci` / `hugo` commands run — mirrors `.github/workflows/check-links.yml`): + +```bash +SITE_DIR="" +for dir in docs site; do + for cfg in hugo.toml hugo.yaml \ + config/hugo.toml config/hugo.yaml \ + config/_default/hugo.toml config/_default/hugo.yaml; do + if [ -f "$dir/$cfg" ]; then + SITE_DIR="$dir" + break 2 + fi + done +done +if [ -z "$SITE_DIR" ]; then + echo "ERROR: No Hugo config found under docs/ or site/." >&2 + exit 1 +fi + +if [ -f "${SITE_DIR}/_preview/package-lock.json" ]; then + WORK_DIR="${SITE_DIR}/_preview" +elif [ -f "${SITE_DIR}/package-lock.json" ]; then + WORK_DIR="${SITE_DIR}" +else + echo "ERROR: No package-lock.json found under ${SITE_DIR}/_preview/ or ${SITE_DIR}/." >&2 + exit 1 +fi +``` + +Use `$SITE_DIR` for content paths and `$WORK_DIR` for build/serve operations in the steps below. + ### 1. Scope check Run `git diff ...HEAD --name-only` (default `` = `master` unless -the user provides another). If the change set has **no** files under `docs/**` -and no changes to `lychee.toml`, and the user did not explicitly ask, decline -and exit cleanly. +the user provides another). If the change set has **no** files under +`$SITE_DIR/**` and no changes to `lychee.toml`, and the user did not +explicitly ask, decline and exit cleanly. ### 2. Preflight binaries @@ -137,9 +171,9 @@ and exit cleanly. ### 3. Install Hugo deps -Run `( cd docs/_preview && npm ci )`. We deliberately use `npm ci` (matching -the CI workflow's `Install Dependencies` step in `check-links.yml`) rather -than `npm install`: +Run `( cd ${WORK_DIR} && npm ci )`. We deliberately use `npm ci` +(matching the CI workflow's `Install Dependencies` step in `check-links.yml`) +rather than `npm install`: - `npm ci` installs exactly the versions pinned by `package-lock.json`; `npm install` is allowed to update the lockfile and may resolve to @@ -149,18 +183,14 @@ than `npm install`: fails fast with a clear error rather than silently healing the lockfile — a divergence we want to surface, not paper over. -The helper script `docs/_script/install-dependencies` exists for interactive -use but does a relative `cd _preview` and therefore only works when invoked -from `docs/` — calling it from the repo root (the skill's default CWD) -would fail with "No such file or directory: _preview". - ### 4. Build the site -Run `( cd docs/_preview && hugo -e development )`. -This emits `docs/_preview/public/**/*.html`. The `-e development` flag matches +Run `( cd ${WORK_DIR} && hugo -e development )`. +This emits `${WORK_DIR}/public/**/*.html`. The `-e development` flag matches what CI uses in `check-links.yml` so the two builds render identical HTML. -(The helper `docs/_script/hugo-build` exists for interactive use but defaults -to `production`; we invoke `hugo` directly to keep the env in lock-step with CI.) +(The helper `${SITE_DIR}/_script/hugo-build` exists for interactive use but +defaults to `production`; we invoke `hugo` directly to keep the env in +lock-step with CI.) ### 5. Start the Hugo server in the background @@ -176,7 +206,7 @@ stale process does not hold port `1414`: pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true rm -f /tmp/check-links.hugo.pid -( cd docs/_preview && nohup hugo server --environment development --port 1414 \ +( cd ${WORK_DIR} && nohup hugo server --environment development --port 1414 \ > /tmp/check-links.hugo.out 2>&1 & echo $! > /tmp/check-links.hugo.pid ) sleep 5 @@ -199,7 +229,7 @@ Port `1414` is chosen to avoid clashing with a developer's local `hugo server` ```bash --config lychee.toml --timeout 60 \ --base-url http://localhost:1414/ \ - 'docs/_preview/public/**/*.html' + "${WORK_DIR}/public/**/*.html" ``` Capture exit code. Any non-zero exit means at least one broken link. @@ -209,8 +239,8 @@ Capture exit code. Any non-zero exit means at least one broken link. Group the broken URLs from Lychee's output by source page. To reverse-map an HTML path to its Markdown source: -`docs/_preview/public/docs/
//index.html` -↔ `docs/content/docs/
/.md` (or `/_index.md`). +`${WORK_DIR}/public/docs/
//index.html` +↔ `${SITE_DIR}/content/docs/
/.md` (or `/_index.md`). Report in this shape: @@ -222,11 +252,11 @@ Lychee: () Pages scanned: Broken URLs: -### docs/content/docs/<...>/.md +### /content/docs/<...>/.md - - — ... -### docs/content/docs/<...>/.md +### /content/docs/<...>/.md - ... ``` @@ -269,10 +299,9 @@ HEAD advance (commit, amend, rebase) invalidates the cache automatically. reader does not mistake them for unrelated side-effects: - `.agents/skills/check-links/.cache/lychee/` — auto-downloaded Lychee binary, when the system Lychee was unavailable. - - `docs/_preview/node_modules/` — installed by `npm ci` in step 3. - - `docs/_preview/public/` — Hugo's rendered HTML (the corpus Lychee - scans). - - `docs/_preview/resources/` — Hugo's asset-pipeline cache. + - `${WORK_DIR}/node_modules/` — installed by `npm ci` in step 3. + - `${WORK_DIR}/public/` — Hugo's rendered HTML (the corpus Lychee scans). + - `${WORK_DIR}/resources/` — Hugo's asset-pipeline cache. - `.lycheecache` at the repo root — Lychee's per-URL result cache (honoured for `max_cache_age = "3d"` per `lychee.toml`). - `/tmp/check-links.hugo.{pid,out}` — server PID file and log, both diff --git a/.agents/skills/pre-pr/SKILL.md b/.agents/skills/pre-pr/SKILL.md index ec9ab410c6..e656b0b452 100644 --- a/.agents/skills/pre-pr/SKILL.md +++ b/.agents/skills/pre-pr/SKILL.md @@ -2,9 +2,11 @@ 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`, + the repository has a root `version.gradle.kts`, run a scope-dependent + build/check command per `.agents/running-builds.md` (docs-only → `dokka`; + code/deps → `build`; proto → `clean build`; no documented command → skipped), + and invoke the relevant reviewers (`kotlin-review`, `review-docs`, + `dependency-audit`, `check-links`) 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 @@ -18,67 +20,55 @@ 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**. +that intentionally do not have `version.gradle.kts`. 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/running-builds.md` — which build/check command to run. - `.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), - `check-links` (skill — runs Lychee against the rendered Hugo site - under `docs/`). ## 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. +Run steps 1–4 fully before aggregating. Collect all findings; do not stop at +the first failure. ### 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. - - **site** — any file under `docs/**` changed, or `lychee.toml` changed - (independent of **docs**; triggers the Hugo link check; pure `README.md` - edits or KDoc-only changes do *not* count as **site**). - - **deps** — any file under `buildSrc/src/main/kotlin/io/spine/dependency/` - changed. +- Changed files: `git diff ...HEAD --name-only` +- Repository root: `git rev-parse --show-toplevel` +- Version gate: check only the repository-root `version.gradle.kts`. + - Absent at both sides → `not-applicable`, continue. + - Present at `HEAD` → enforce in step 2. + - Present at `` but missing at `HEAD` → fail unless the user + explicitly asked to migrate away from Gradle Build Tools versioning. +- Classify changes: + - **proto** — any `*.proto` changed + - **code** — any `*.kt`, `*.kts`, or `*.java` changed + - **docs** — any `*.md` or doc-only source edits changed + - **deps** — any file under `buildSrc/src/main/kotlin/io/spine/dependency/` changed + - **site** — any file under `docs/**` or `lychee.toml` (triggers Hugo link + check; pure `README.md` or KDoc-only changes do *not* count) ### 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`." +- Skip when version gate is `not-applicable`. +- Read `version.gradle.kts` at `HEAD`. Read `` only if the file exists + there; if it does not, the file is newly introduced — record the introduced + version and continue. +- When both sides have the file: if the version is not strictly greater (semver + + Spine snapshot rules in `.agents/version-policy.md`): if + `.agents/skills/bump-version/` exists, **auto-fix immediately** by invoking + `/bump-version` without asking; otherwise record a Must-fix and continue. + Re-read the file after the fix. If the version is still not strictly greater, + record a Must-fix and continue. If the auto-fix succeeded, recompute the + changed-file list (`git diff ...HEAD --name-only`) before proceeding to + Step 3 — the bump commit adds `version.gradle.kts` to the diff. ### 3. Build or check @@ -86,107 +76,108 @@ 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) +- Else **docs**-only → `./gradlew dokka` -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. +If `./gradlew` is absent, read `.agents/running-builds.md` for the +repository-specific command. If that file is also absent, or if none is +documented for the change type, record `build_status=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. +Run the chosen command. On failure, record the first failing task and +continue to step 4 — do not abort. Pass `build_status=FAIL` in the context +given to reviewers so they can discount false positives from non-compiling +code. ### 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 **site** changed: `check-links` (runs in parallel with any other - dispatched reviewers), **unless** the sentinel short-circuit below applies. -- If **deps** changed: `dependency-audit`. - -**`check-links` sentinel short-circuit.** Before dispatching -`check-links`, read `.git/check-links.ok` (if present) and parse the -`head=` and `status=` fields. If `head=` equals the current HEAD SHA (full, -not short) and `status=PASS`, skip the dispatch and record the reviewer as -`APPROVE` with the note "cached from `.git/check-links.ok`". Any HEAD -advance — commit, amend, rebase — automatically invalidates the cache because -the recorded SHA no longer matches. If the file is missing, malformed, or the -`head=` does not match, dispatch the reviewer normally. Other reviewers do not -use this pattern today; only `check-links` does, because its rebuild+serve -cycle is slow (~30 s) and the result is deterministic for a given HEAD. - -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`). +Dispatch relevant reviewers concurrently; collect all verdicts before +aggregating. Before dispatching, check that the skill directory exists under +`.agents/skills/`; if a skill is absent, skip it with a note "not applicable +for this repo" rather than failing. + +- **code** changed → `kotlin-review` +- **docs** or KDoc changed → `review-docs` +- **deps** changed → `dependency-audit` +- **site** changed → `check-links` (unless the sentinel short-circuit below + applies) + +**`check-links` sentinel short-circuit.** Read `.git/check-links.ok` (if +present). If `head=` equals the current **full** HEAD SHA and `status=PASS`, skip +dispatch and record `APPROVE` with note "cached from `.git/check-links.ok`" +(caching its ~30 s rebuild+serve cycle; the result is deterministic for a given +HEAD). Otherwise dispatch normally. + +Pass each reviewer: base ref, changed-file list, build result, version result. +When the version check is `not-applicable`, say so explicitly so reviewers don't flag a +missing version bump. + +**Auto-fix policy for reviewer findings:** + +- Findings from `kotlin-review`, `review-docs`, or `dependency-audit` → record + as Must-fix or Should-fix; do **not** auto-apply. Surface them and wait for + user action. +- If a reviewer reports a missing version bump after Step 2 already ran, the + auto-fix did not take — record a Must-fix and do not silently re-apply. +- `dependency-audit` reports a **version rollback** → do **not** auto-fix. + Surface it as a Must-fix and wait for user confirmation, because a rollback + can be intentional. ### 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**. +- **PASS**: version check passed or `not-applicable`, build succeeded or + `build_status=skipped` (no documented command for the change type), every + reviewer returned `APPROVE` or `APPROVE WITH CHANGES`, and no unaddressed + Must-fix items remain. +- **FAIL**: anything else. ### 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: +Write `.git/pre-pr.ok` at the repo root (never under `.claude/`). The `gh pr +create` hook (`.agents/scripts/pre-pr-gate.sh`) checks `head=` and `status=`; +field names in this block are part of that contract. ``` head= branch= status=PASS|FAIL timestamp= -build= -reviewers= +build= +build_status=PASS|FAIL|skipped +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: +**On PASS** — single line: ``` -## Pre-PR checklist ( vs ) - -| Check | Status | Notes | -|------------------|--------|----------------------------------------| -| Version check | … | , introduced, or N/A | -| Build/check | … | | -| kotlin-review | … | | -| review-docs | … | | -| check-links | … | | -| dep audit | … | | - -**Overall: PASS|FAIL** -Sentinel: .git/pre-pr.ok (status=PASS|FAIL, head=) +Pre-PR: PASS ( vs ) — ready to `gh pr create`. +``` + +**On FAIL** — header line, then only the items that need attention, each +prefixed with the source reviewer or check: + +``` +Pre-PR: FAIL ( vs ) + +Must fix: +- [kotlin-review] +- [review-docs] + +Should fix: +- [dependency-audit] ``` -On `PASS`, end with: "You can now run `gh pr create`." -On `FAIL`, end with the specific blocker and the next action. +Report nothing about checks that passed. If auto-fixes were applied, list +them in one line before the verdict: `Auto-fixed: .` ## 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. +- This skill must NOT create the PR itself. +- This skill must NOT create `version.gradle.kts`. +- The sentinel lives under `.git/` — per-clone, never committed. +- Each reviewer is the source of truth for its own checks; this skill only + orchestrates and aggregates. +- This skill may auto-fix a missing version bump by invoking `/bump-version`; + all other fixes require explicit user confirmation. diff --git a/.agents/tasks/check-links.md b/.agents/tasks/check-links.md deleted file mode 100644 index 7387ca61cb..0000000000 --- a/.agents/tasks/check-links.md +++ /dev/null @@ -1,342 +0,0 @@ ---- -slug: check-links -branch: validate-doc-links -owner: claude -status: in-review -started: 2026-05-22 ---- - -## Context - -The Spine `validation` repo's Hugo site under `docs/` accumulates link rot -silently. A concrete example: `docs/content/docs/validation/developer/runtime-library.md` -references `error_message.proto` at a path that no longer exists in this repo — -the `TemplateString` type it describes actually lives in the sibling -`SpineEventEngine/base-libraries` repo. The same broken reference appears in -`architecture.md`. We need both a local pre-push check and a CI gate, mirroring -the well-tested setup in the sibling `spine.io` repo -(`/Users/sanders/Projects/Spine/spine.io/lychee.toml`, -`/Users/sanders/Projects/Spine/spine.io/.github/workflows/check-links.yml`). - -The new skill is scoped to the Hugo Markdown docs only. A separate skill for -KDoc/Javadoc HTML is out of scope here and will be planned independently. The -`../config` fan-out is also out of scope — it will happen separately in that -repo. - -## Decisions (approved 2026-05-22) - -- **Lychee binary** — prefer installed, fall back to auto-download the version - pinned by `LYCHEE_VERSION_TAG` in `check-links.yml` into - `.agents/skills/check-links/.cache/lychee/`. -- **`pre-pr` wiring** — yes, wire it in. -- **Hugo server port** — 1414 (avoid clashing with developer's default 1313). - -## Plan - -### 1. Fix the two broken doc links - -- [ ] Read both files end-to-end first to understand the surrounding prose: - `docs/content/docs/validation/developer/runtime-library.md`, - `docs/content/docs/validation/developer/architecture.md`. -- [ ] Verify the canonical GitHub URL for `template_string.proto` by browsing - `https://github.com/SpineEventEngine/base-libraries/tree/master/src/main/proto/spine/string/`. -- [ ] In `runtime-library.md`: update the `[error-message-proto]` reference - definition to point at the `SpineEventEngine/base-libraries` URL. Rename the - reference id to `[template-string-proto]` and the prose mention from - "`error_message.proto`" to "`template_string.proto`". -- [ ] In `architecture.md`: same fix. -- [ ] Sanity-check there are no other lingering `error_message.proto` mentions - via `Grep`. - -### 2. Add Lychee config at repo root - -- [ ] Create `lychee.toml` as a near-copy of - `/Users/sanders/Projects/Spine/spine.io/lychee.toml`. Keep its exclude - list unchanged. - -### 3. Add CI workflow - -- [ ] Create `.github/workflows/check-links.yml`, adapted from - `/Users/sanders/Projects/Spine/spine.io/.github/workflows/check-links.yml`. -- [ ] Diffs vs. source: `working-directory` → `docs`/`docs/_preview`; - Lychee glob `'docs/_preview/public/**/*.html'`; `paths:` filter on `pull_request` - for `['docs/**', '.github/workflows/check-links.yml', 'lychee.toml']`. -- [ ] Preserve pinned versions: `HUGO_VERSION=0.161.1`, - `LYCHEE_VERSION_TAG=lychee-v0.24.2`. Preserve both caches. - -### 4. Create `check-links` skill - -- [ ] New file: `.agents/skills/check-links/SKILL.md`. -- [ ] Procedure: scope check → preflight binaries (Lychee prefer-installed, - fallback to download) → install deps → Hugo build → Hugo serve on port - 1414 → Lychee against `docs/_preview/public/**/*.html` → tear down → report grouped - by source `.md` → sentinel `.git/check-links.ok` on PASS. - -### 5. Wire into `pre-pr` skill - -- [ ] Edit `.agents/skills/pre-pr/SKILL.md`: add `docs/**` slice in the - classifier; invoke `check-links` in parallel with `review-docs` when - `docs/**` changed; include in verdict union; append to sentinel - `reviewers=` field; add row to verdict table. - -### 6. Update `.gitignore` - -- [ ] Add `.agents/skills/check-links/.cache/` to `.gitignore` (Lychee binary cache). - -### 7. Verification (end-to-end) - -- [ ] **Local skill smoke test**: temporarily revert the link fix on a scratch - commit; run `/check-links`; expect non-zero exit naming the broken URL - and the source page. Re-apply the fix; rerun; expect clean exit. -- [ ] **CI smoke test**: push the branch; observe the new `check-links` job - appear on the PR; it must be green with the fix applied. -- [ ] **pre-pr integration**: run `/pre-pr` on the branch; confirm the - reviewers table now lists `check-links` and that the sentinel records - it. - -## Critical files - -- `docs/content/docs/validation/developer/runtime-library.md` (link fix) -- `docs/content/docs/validation/developer/architecture.md` (link fix) -- `lychee.toml` (new, repo root) -- `.github/workflows/check-links.yml` (new) -- `.agents/skills/check-links/SKILL.md` (new) -- `.agents/skills/pre-pr/SKILL.md` (edit) -- `.gitignore` (edit — add `.agents/skills/check-links/.cache/`) - -## Log -- 2026-05-22 — drafted, presented to user, approved with all three open - questions resolved (Lychee fallback download / pre-pr wiring yes / port 1414). -- 2026-05-22 — execution started. -- 2026-05-22 — Hugo actually publishes to `docs/_preview/public/**`, not - `docs/public/**` (the latter is a stale dir from an earlier config). Lychee - glob updated everywhere. -- 2026-05-22 — User-supplied canonical URL is - `SpineEventEngine/base-libraries`, not `SpineEventEngine/base`. Both docs - updated; WebFetch confirmed HTTP 200. -- 2026-05-22 — Lychee v0.15.1 ships no arm64 macOS binary, so the skill's - fallback download path can't work on Apple Silicon. Bumped the pin to - `lychee-v0.24.2` in both `check-links.yml` and the skill. Asset names in - that release drop the version-in-filename (e.g. - `lychee-aarch64-apple-darwin.tar.gz`); release tag is `lychee-v0.24.2`. - Extract with `--strip-components=1` so the binary lands at - `.agents/skills/check-links/.cache/lychee/lychee`. -- 2026-05-22 — Verified end-to-end on macOS arm64: downloaded Lychee 0.24.2 - → `hugo` (extended 0.161.1) build → `hugo server --port 1414` → Lychee - against `docs/_preview/public/**/*.html`. **616 links checked, 0 errors.** -- 2026-05-22 — Lychee 0.24.2 deprecated `--base`; switched to `--base-url` - in both the workflow and the skill. -- 2026-05-22 — Self-review pass on SKILL + workflow + lychee.toml. Fixes: - - SKILL: softened the "green here means green there" claim and listed the - two deliberate differences (port 1414 vs 1313, local sentinel). - - SKILL: aligned the build/serve commands to use `-e development` / - `--environment development` so the skill renders exactly what CI renders. - Replaced `docs/_script/hugo-build` with explicit `hugo -e development` - (the helper script defaults to `production`). - - SKILL: added Linux aarch64 row to the platform map; unsupported platforms - (Windows / FreeBSD / 32-bit) now stop with a Must-fix asking the user to - install Lychee manually instead of falling through silently. - - SKILL: install the kill-trap **before** backgrounding the Hugo server so - a failure between the subshell exit and the original trap line cannot - leak a process holding port 1414. Noted the trap-doesn't-survive-Bash - rule explicitly. - - Workflow: added `cancel-in-progress: true` to `concurrency` so each PR - push cancels the previous run instead of queueing. - - Workflow: bumped `actions/setup-node@v3` → `@v4` to match the other - actions which are already on `@v4`. - - Workflow: rekeyed Lychee results cache to - `cache-lychee-${{ runner.os }}-${{ hashFiles('lychee.toml') }}` with a - `restore-keys:` fallback, so exclude-list edits invalidate cached `200`s - deterministically. Included `LYCHEE_VERSION_TAG` in the binary cache key - for the same reason. - - Workflow: added `--strip-components=1` (and `-z`) to the tar extract so - the binary lands at `lychee/lychee` instead of - `lychee/lychee-/lychee` — matching the skill and what the next - "Check links" step expects on a cold cache. - - `lychee.toml`: removed the broad - `raw.githubusercontent.com/SpineEventEngine/*` exclude (it would mask - the very class of bug this skill exists to catch). Existing - `max_retries=3` / `retry_wait_time=2` should absorb transient 429s; a - comment in the file calls out the deliberate omission. -- 2026-05-22 — Second self-review pass. Fixes: - - SKILL: the "same Hugo version, same Lychee version" claim used to be - aspirational — neither value was pinned anywhere in this file. Added a - "Pinned versions" subsection saying `check-links.yml` is the single - source of truth, and rewrote step 2 so the auto-download path **reads - `LYCHEE_VERSION_TAG` out of the workflow at runtime** via `grep|sed`. - Hugo similarly read from the workflow's `HUGO_VERSION` for a "your - Hugo is older than CI" warning. Drift between skill and CI is now - impossible for the version pins. - - SKILL: the previous trap-based teardown had a fatal bug — `trap … EXIT` - fires when *this* shell exits, and Claude Code's Bash tool gives each - invocation its own shell. So running step 5 (start server) and step 6 - (run Lychee) in separate Bash calls killed Hugo before Lychee could - query it. Dropped the trap; switched to `nohup`-only (which detaches - the server from the spawning shell). Step 5 now also `pkill -F`s any - leftover server from a previous crashed run before launching. Step 8 - does the explicit kill with the same `pkill -F` (always runs, even on - Lychee failure, so port 1414 isn't leaked). - - Workflow: added `restore-keys: ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}-` - fallback for the Lychee binary cache so a release-filename tweak can - reuse the existing cached binary for the same version-tag instead of - paying for a fresh download. - - Workflow: made the Hugo server's `--port 1313` explicit so its coupling - with the next step's `--base-url http://localhost:1313/` is visible at - the call site. Comment in the workflow explains the lock-step. - - `lychee.toml`: fixed `no_progress = false` → `true` (the comment said - "don't show progress bar" but the value asked to show it — Lychee's - `no_progress` semantics invert what one might expect). - - `lychee.toml`: documented the `429` acceptance — it's a deliberate - rate-limit tradeoff, not a typo, and the comment explains the rare - downside (a genuinely-broken URL that also returns `429` would pass). -- 2026-05-22 — Third self-review pass. Fixes: - - SKILL: dropped the concrete `HUGO_VERSION=0.161.1` / - `LYCHEE_VERSION_TAG=lychee-v0.24.2` values from the preamble. They - contradicted the "don't edit here" warning — duplicating pins is the - very thing the "single source of truth" framing exists to prevent. - Preamble now just points to `check-links.yml`'s `env:` block. - - SKILL: step 3 replaced `docs/_script/install-dependencies` with inline - `( cd docs/_preview && npm install )`. The helper script does a - relative `cd _preview` so it only works from `docs/`; the skill's - default CWD is the repo root, where the helper would fail with - "No such file or directory: _preview". Annotated step 3 with the - reason. - - SKILL: step 5 captures `$!` to a pid file and then verifies the PID - via `pgrep -F` before relying on it. Without this check a silent Hugo - startup failure (port already bound, missing module, etc.) becomes a - confusing "Lychee fetches an empty port" failure 30s later. Now it's - a clear error with a log tail at the actual failure point. - - SKILL: step 8's sentinel-consumer wording now describes the planned - `pre-pr` short-circuit precisely — `head=` match + `status=PASS` - skips re-dispatch and records APPROVE with note "cached from - `.git/check-links.ok`". Avoids the "orphaned sentinel" issue - where the skill claimed integration that didn't exist. - - `pre-pr/SKILL.md`: implemented the sentinel short-circuit in step 4 - to make the SKILL.md claim true. Explicitly notes that other - reviewers do not use this pattern (only `check-links` does, - because its rebuild+serve cycle is slow and the result is - deterministic for a given HEAD). - - Workflow: removed the noisy "Check if the cache file exists" - diagnostic step. It only existed as a debugging breadcrumb during - initial bring-up and added log noise without any actionable signal — - `actions/cache@v4` already reports cache-hit status. - - `lychee.toml`: header comment now references the canonical - `SpineEventEngine/SpineEventEngine.github.io` repo, not the bare - `spine.io` shorthand. -- 2026-05-22 — Fourth self-review pass. Six fixes: - - SKILL step 2 (Lychee auto-download): added an explicit - `mkdir -p .agents/skills/check-links/.cache/lychee/` substep - before the tar extract. On a fresh clone the cache dir does not - exist (it is git-ignored), and `tar -xzf -C ` would - fail with "no such file or directory" — the previous - end-to-end success worked only because the dir was already - present locally. Renumbered the surrounding substeps. - - SKILL step 3 (install Hugo deps): switched - `( cd docs/_preview && npm install )` to `npm ci` to match the - `Install Dependencies` step in `check-links.yml`. Rationale (in - the SKILL): `npm install` may mutate the lockfile and resolve - different transitive versions than CI; `npm ci` pins to - `package-lock.json` and fails fast on lockfile drift instead of - silently healing it. - - Workflow: removed the standalone `Cache dependencies` step and - folded its job into `actions/setup-node@v4` via - `cache: 'npm'` + `cache-dependency-path: - docs/_preview/package-lock.json`. One source of truth for the - npm cache key, no drift between the two layers. - - Workflow: the Hugo modules cache step pointed at - `path: /tmp/hugo_cache`, but Hugo's default cacheDir on Linux - is `~/.cache/hugo_cache` (or `$TMPDIR/hugo_cache_$USER`) — - nothing ever landed in `/tmp/hugo_cache` and the cache was a - silent no-op every run. Fixed by adding - `HUGO_CACHEDIR: /tmp/hugo_cache` to the job `env:` block so - Hugo actually writes where the cache step restores from. Also - narrowed the cache key from `hashFiles('**/go.sum')` to - `hashFiles('docs/**/go.sum')` since those are the only `go.sum` - files in the repo and the broader glob was misleading. - - Workflow: the Lychee `Download` and `Extract` steps gated on - `steps.cache-lychee.outputs.cache-hit != 'true'`, which is only - `'true'` on an EXACT key match. A restore via `restore-keys` - reports `cache-hit == 'false'` per `actions/cache` docs, so the - download would re-run even when the binary was present — - defeating the fallback that was added in round 1. Switched the - `if:` guards to `hashFiles('lychee/lychee') == ''`, which only - triggers the download when the binary is actually absent. - Comment in the workflow explains the rationale. - - `lychee.toml`: the exclude entries are Rust regexes (per - Lychee docs), not shell globs. The previous entries used - unescaped dots and bare `*` (e.g. `fonts.googleapis.com/*`), - which means "any char between `fonts` and `googleapis`, - zero-or-more slashes at the end" — silent over-match that - could mask real broken links to URLs of the same shape (e.g. - `fontsXgoogleapisYcomZ/...`). Rewrote every pattern with - escaped dots and `/.*` for the path suffix. Switched the TOML - strings from double-quoted to single-quoted (literal strings) - so the backslashes stay literal — TOML basic strings treat - `\.` as an unrecognized escape. Added a header block above the - `exclude = [` line documenting the regex semantics. - **Empirically verified** with `lychee --dump`: - `fonts.googleapis.com/css?family=Roboto` and `x.com/notify` are - correctly excluded by the new config, `github.com/owner/repo` - passes through. Without `--config`, all three pass — confirming - the exclusion is firing from our patterns, not from Lychee - built-ins. -- 2026-05-22 — Re-verified end-to-end after round-4 edits: - `( cd docs/_preview && hugo -e development )` → - `hugo server --environment development --port 1414` (PID survived - across separate Bash calls; `pgrep -F` verification passed) → - `lychee --config lychee.toml --base-url http://localhost:1414/ - 'docs/_preview/public/**/*.html'`. **616 links, 0 errors, exit 0.** -- 2026-05-22 — Fifth self-review pass. Four fixes: - - SKILL "Tooling" section (lines 67–72): removed the literal - `v0.24.2` reference. It contradicted the "Pinned versions" - subsection above (`check-links.yml` is the single source of - truth) and would drift the moment the workflow pin is bumped — - exactly the failure mode the dynamic `grep|sed` read in step 2 - was introduced to prevent. The Tooling sentence now points at - `LYCHEE_VERSION_TAG` in the workflow. - - SKILL "Notes" bullet about outside-`.git/` files: the previous - claim ("only files outside `.git/` are `.cache/` and `/tmp/`") - was wrong. The skill also creates `docs/_preview/node_modules/` - (`npm ci`), `docs/_preview/public/` + `docs/_preview/resources/` - (`hugo -e development`), and `.lycheecache` at the repo root - (Lychee). All git-ignored (verified against `.gitignore` and - `docs/.gitignore`), but listing them prevents future readers - from mistaking a build artifact for an unexpected side-effect. - Reworded "does not modify project sources" → "does not modify - tracked sources" to be technically precise. - - Workflow: the `Start Hugo server` step had no readiness check — - only `nohup … & sleep 5`. A silent startup failure (port bind, - missing Hugo module, build error after `nohup` returns 0) - would surface 60 s later as "every URL unreachable" Lychee - errors. Added a `curl -sf http://localhost:1313/` probe after - the sleep that exits non-zero with a `cat nohup.out / nohup.err` - dump if Hugo isn't actually serving. Mirrors the spirit of the - skill's `pgrep -F` guard (strictly stronger, since `curl` - catches "process alive but not serving" too). - - `lychee.toml`: two exclude entries — `clients4\.google\.com/` - and `ssl\.gstatic\.com/` — didn't have the trailing `/.*` that - the header comment documents as the convention and that every - other entry follows. They still matched via substring regex, - but the inconsistency was a footgun for future edits. Aligned - to `clients4\.google\.com/.*` and `ssl\.gstatic\.com/.*`. - Empirically verified via `lychee --dump` (running from `/tmp` - to bypass CWD config auto-discovery): both URLs are now - excluded by the config, `github.com/owner/repo` still passes. -- 2026-05-22 — Re-verified end-to-end after round-5 edits: - build → `hugo server --port 1414` → `pgrep -F` + `curl -sf - http://localhost:1414/` both pass → Lychee 616/0/exit 0. The - `--dump` probe confirms the two re-aligned exclude entries still - filter their domains. -- 2026-05-22 — Rename pass: workflow `proof-links.yml` → - `check-links.yml` (file + `name:` field `Proof Links` → - `Check Links` + job key `proof-links:` → `check-links:`); skill - `link-check-docs` → `check-links` (directory, frontmatter `name:`, - all internal references); sentinel `.git/link-check-docs.ok` → - `.git/check-links.ok` (skill + `pre-pr/SKILL.md` consumer); - `.gitignore` comments + cache path; this task file's slug, body, - and prior log entries. -- 2026-05-22 — Dropped all references to the future KDoc/Javadoc - link-check skill from `SKILL.md` (description preamble + the - "Related skills" bullet) and from this task file's Context - section. Rationale: that skill doesn't exist yet, naming it now - pre-commits the design before its own planning round. diff --git a/.agents/tasks/issue-175-set-once-timestamp.md b/.agents/tasks/issue-175-set-once-timestamp.md deleted file mode 100644 index a09a19a7e1..0000000000 --- a/.agents/tasks/issue-175-set-once-timestamp.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -slug: issue-175-set-once-timestamp -branch: air/please-work-on-this-issue-https-github.com-spineeventengine-vali-8bd73481-e -owner: claude -status: in-progress -started: 2026-05-22 ---- - -## Goal - -Close [issue #175](https://github.com/SpineEventEngine/validation/issues/175) by adding -a positive integration test that applies `(set_once)` to a field of type -`google.protobuf.Timestamp` — an external message imported from a Protobuf dependency. - -## Context - -The issue has two items: - -1. Negative tests: fail when the option is applied to unsupported field types. -2. Positive test for `(set_once)` on an external message type like `Timestamp`, - guarding the regression that motivated the original report. - -Item 1 is already covered at the reaction level: - -- `context-tests/.../SetOnceOptionSpec.kt` exercises both unsupported categories - (`repeated`, `map`) for `(set_once)`, which is exhaustive because the supported-type - check is purely structural (`!isList && !isMap`). -- `context-tests/.../GoesReactionSpec.kt` + `GoesReactionTestEnv.kt` enumerate **all 13** - unsupported primitive types (`bool` + 12 numeric variants) as both target and companion, - matching `SUPPORTED_PRIMITIVES = {STRING, BYTES}`. - -Item 2 has no coverage today — the `(set_once)` proto fixtures only use the locally -defined `Name` message. `(goes)` already uses `Timestamp` in several of its fixtures, so -the pattern is well-established. - -## Plan - -- [x] Draft this task file and the system plan. -- [x] Add `import "google/protobuf/timestamp.proto";` and a new `Measurement` - message with one `(set_once) = true` `Timestamp` field to - `tests/validating/src/testFixtures/proto/spine/test/tools/validate/set_once_fields.proto`. -- [x] Add two distinct `Timestamp` constants (`BIRTHDAY1`, `BIRTHDAY2`) to - `tests/validating/src/testFixtures/kotlin/io/spine/test/options/setonce/SetOnceTestEnv.kt`. -- [x] Add two nested test classes to - `tests/validating/src/test/kotlin/io/spine/test/options/setonce/SetOnceFieldsITest.kt` - mirroring the existing `…non-default message` / `…default and same-value message` - pair, but for `Measurement`. -- [x] `./gradlew :tests:validating:test --tests "*SetOnceFieldsITest*"` passes (82/82). -- [x] `./gradlew :tests:validating:test` passes (460/460, no regressions). -- [x] Bump `version.gradle.kts` via the `bump-version` skill (SNAPSHOT.441 → .442). -- [ ] Run the `pre-pr` skill (version gate, build/check, reviewers). -- [ ] Delete this file on merge to master. - -## Log - -- 2026-05-22 — drafted, approved by user (turn 4), executing. -- 2026-05-22 — implementation + tests complete; 460/460 pass. Proceeding to version bump. -- 2026-05-22 — version bumped to `2.0.0-SNAPSHOT.442`; dependency reports regenerated and committed. diff --git a/.agents/tasks/prompt-caching-org.md b/.agents/tasks/prompt-caching-org.md new file mode 100644 index 0000000000..71f0c4fb9a --- /dev/null +++ b/.agents/tasks/prompt-caching-org.md @@ -0,0 +1,165 @@ +--- +slug: prompt-caching-org +branch: improve-caching +owner: claude +status: in-review +started: 2026-05-24 +related-memories: [cache-warm-window, anthropic-api-caching] +--- + +## Goal + +Maximise Claude API prompt cache hit rates across the Spine GitHub organisation +(~40 sibling repos) so that repeated session starts and agent invocations read +from cache at 0.1× token cost rather than processing the full prompt fresh. + +## Context + +- Claude Code already applies automatic prompt caching to every API call it + makes. There is no single "enable" switch; the work is about raising the + cache hit rate and keeping it high. +- The `migrate` script overwrites `CLAUDE.md`, `.agents/`, `.claude/`, and + `buildSrc/` in each sibling repo with an exact copy from this repo. This + means all 40 repos hold byte-identical content after a `./config/pull` and + therefore share the same cache entry at any given config version. +- The `openai.yaml` files under each skill are FleetView UI interface metadata + only — they define display name and default prompt, not API call parameters. + `cache_control` cannot go there. +- No GitHub Actions workflow currently calls the Anthropic API directly. +- Current stable prefix: CLAUDE.md (≈ 900 tokens) + quick-reference card + (≈ 200 tokens) ≈ 1,100 tokens. + - This **clears** the 1,024-token minimum for Sonnet 4.6 / Opus. + - This **does not meet** the 4,096-token minimum for Haiku 4.5. +- The team memory system is empty; populating it will grow the stable prefix. +- Cache TTL defaults to 5 minutes. Sessions more than 5 minutes apart miss + the cross-session cache unless the extended 1-hour TTL is used. + +## Plan + +- [ ] **Step 0 — Diagnose why zero caching is happening and enable it** + + The Console Caching dashboard ("TeamDev Management OÜ", All workspaces) shows + no prompt caching in use — no `cache_control` blocks are being sent by any + caller. This is the highest-priority item; the remaining steps only add value + once caching is active. + + Sub-tasks: + + - **0a. Switch to Console OAuth on every developer machine** + + Raw API key auth loses per-developer identity (`email`, `orgId`, `orgName` + all null in `claude auth status`). Console OAuth preserves identity while + still billing to "TeamDev Management OÜ". + + **For each developer:** + 1. Remove `ANTHROPIC_API_KEY` from `~/.claude/settings.json` — it takes + precedence over OAuth in the auth stack and must be absent. + 2. Run `claude` → a browser window opens → log in with Console credentials + (the same account used at console.anthropic.com). + 3. Run `claude auth status` and confirm `email`, `orgId`, `orgName` are + populated. + + **For the org admin (Alexander):** + - Invite the second developer via Console → Settings → Members → Invite. + - Assign the "Developer" or "Claude Code" role. + - They accept the email invite, then follow the three steps above. + + - **0b. Enable 1-hour cache TTL on every developer machine** + + Console OAuth users get the **5-minute** default cache TTL — the 1-hour + TTL is only automatic for claude.ai subscription users. Add the opt-in + to `~/.claude/settings.json` on every developer machine: + + ```json + { + "env": { + "ENABLE_PROMPT_CACHING_1H": "1" + } + } + ``` + + Restart Claude Code after saving. This is the highest-impact change in + the entire plan — without it, cache entries expire every 5 minutes and + cross-session hits are rare. + + - **0c. Verify caching is active** — start a Claude Code session, make a + few turns, wait 2–3 minutes, then check Analytics → Usage in the Console + under "TeamDev Management OÜ". Non-zero `cache_creation_input_tokens` + confirms caching is active. Non-zero `cache_read_input_tokens` on a + subsequent session in the same hour confirms hits are occurring. + + - **0d. Investigate remote skill calls** — FleetView-managed remote skills + (the 7 skills with `openai.yaml`) make their own API calls through the + agent platform. Confirm whether those calls include `cache_control`; if + not, this may require configuration in the FleetView platform outside + this repo. + + Until steps 0a–0b are done on both developer machines, Steps 1–3 improve + future cache hygiene but produce limited cost savings. + +- ~~**Step 1 — Cache-hygiene team memory**~~ — *reverted 2026-05-25: the + batching guidance was too restrictive on `config` changes; removed + `.agents/memory/feedback/cache-hygiene.md` and its references.* + +- [x] **Step 2 — Post-migration cache-warm window (reference memory)** + + Create `.agents/memory/reference/cache-warm-window.md` documenting: + - Realistic concurrency is 1–2 developers working on different repos at the + same time, not the full fleet of 40. + - Default TTL is 5 minutes. If a second session starts within 5 minutes of + the first (on the same config version), it hits the warm entry rather than + writing a new one. + - Extended 1-hour TTL (available in direct API calls via + `cache_control: {ttl: "1h"}`) gives a wider window, at 2× write cost per + token — still profitable after even one hit within the hour. + + Update `.agents/memory/MEMORY.md` index. + +- [x] **Step 3 — API caching pattern reference memory (for future direct calls)** + + No workflow currently calls the Anthropic API directly, but when one is + added, developers need the pattern immediately. + + Create `.agents/memory/reference/anthropic-api-caching.md` documenting: + - Use `cache_control: {type: ephemeral}` on the system message block for + 5-minute TTL (1.25× write / 0.1× read). + - Use `cache_control: {type: ephemeral, ttl: "1h"}` for 1-hour TTL + (2× write / 0.1× read) — right for any workflow job spaced > 5 min apart. + - Place stable content (system instructions, skill definitions, shared + context) **before** any dynamic per-request content so the breakpoint + sits at the end of the stable prefix. + - Monitor: `usage.cache_read_input_tokens` should grow relative to + `usage.cache_creation_input_tokens` as the cache warms. + - Future: once direct API calls exist, consider a cache pre-warm job + triggered on push to `master` — calls the API with `max_tokens: 0` and + `cache_control: {ttl: "1h"}` so the first session after a config change + hits rather than writes. + + Update `.agents/memory/MEMORY.md` index. + +- [x] **Step 4 — API workspace consolidation (already confirmed — verify stays true)** + + A cache entry is visible only to API calls made with a key from the **same + Anthropic workspace** (a named sub-group within your Anthropic Console + organisation). Two requests using keys from different workspaces never share + cache, even if they send identical prompts. + + **Current state (confirmed):** "TeamDev Management OÜ" has a single default + workspace (Environments list is empty). Both developers use Console API keys + from this organisation. Both developers share the same cache pool — no action + needed today. + + **Keep true as the team grows:** do not create separate Environments per + developer or per project unless cache isolation is intentional. Any new API + key issued for a new caller (GitHub Actions, scripts, new developer) should + be issued from the same workspace. + +## Log + +- 2026-05-24 — drafted from codebase audit; awaiting review and approval +- 2026-05-24 — revised per review: added buildSrc to migrate list, removed dependency-audit caching step, corrected concurrency description to 1–2 repos, dropped pre-warm workflow step (pattern preserved in Step 3 memory), clarified per-workspace semantics in Step 4 +- 2026-05-24 — added Step 0 after Console Caching dashboard confirmed zero prompt caching in use; workspace confirmed as single default (no Environments), both devs on same org — Step 4 updated to reflect confirmed state +- 2026-05-24 — Step 0 revised: root cause identified — Console API key users get 5-min TTL by default vs 1-hour for subscription users; ENABLE_PROMPT_CACHING_1H=1 is the fix; warning on first launch is one-time approval only +- 2026-05-24 — Step 0 revised again: switched to Console OAuth (not raw API key) to preserve per-developer identity; ENABLE_PROMPT_CACHING_1H=1 still required for Console OAuth users (5-min TTL default applies to all non-subscription auth) +- 2026-05-24 — Steps 1–4 complete: three memory files created, MEMORY.md index updated, workspace consolidation confirmed; Step 0 remains in progress (Console OAuth setup and verification) +- 2026-05-25 — reverted Step 1: removed `cache-hygiene.md` and references — batching guidance was too restrictive for `config` development cadence diff --git a/.agents/tasks/reject-required-on-empty.md b/.agents/tasks/reject-required-on-empty.md deleted file mode 100644 index c79fa22e50..0000000000 --- a/.agents/tasks/reject-required-on-empty.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -slug: reject-required-on-empty -branch: improve-console-output -owner: claude -status: in-progress -started: 2026-05-22 -related-issues: - - https://github.com/SpineEventEngine/validation/issues/146 ---- - -## Goal - -Make the Validation Compiler reject `(required)` set on any field whose type is -`google.protobuf.Empty` — singular, `repeated`, or `map<_, Empty>` — at -**compile time**, producing a clear error pointing at the offending field. -Today, such fields produce a runtime "always invalid" message because the -generated check compares against the default instance of `Empty`, which always -matches. - -Scope is limited to the **explicit** `(required)` option in this repo. The -parallel rule for *implicitly* required IDs (command messages, entity states) -lives in `SpineEventEngine/core-jvm-compiler` and will be filed as a -follow-up issue linked from #146. - -## Context - -Issue: https://github.com/SpineEventEngine/validation/issues/146 - -Today's behavior (runtime-only) is captured by `RequiredMessageITest.not be -applied to the type 'Empty'` and the `AlwaysInvalid` fixture at -`tests/validating/src/testFixtures/proto/spine/test/tools/validate/required.proto:63`. -The test comment explicitly references this issue as "we should do more and -raise compile-time error". - -Decisions made with the user before drafting (one question at a time): -- (Q1) Implicit-required case (command IDs / entity-state IDs): only this - repo; open a follow-up issue under `core-jvm-compiler` and cross-link it - with #146. -- (Q2) Existing `AlwaysInvalid` runtime fixture: keep the proto definition, - but move it to a "should-fail-compilation" fixture set in `context-tests` - (analogous to `RequiredBoolField`). -- (Q3) Structural placement: inline a new private helper next to - `checkFieldType()` in `RequiredOption.kt`; call it from the existing - `RequiredReaction.whenever()`. -- (Q4) Only reject `Empty` with `(required)` set. Pointless `repeated Empty` - / `map<_, Empty>` *without* `(required)` are out of scope — not the job - of Validation. - -### Key code locations - -- `context/src/main/kotlin/io/spine/tools/validation/option/required/RequiredOption.kt` - — `RequiredReaction.whenever()` (line ~91) and the private - `checkFieldType()` helper (line ~149) live here. The new helper goes next - to `checkFieldType()` and is invoked from `whenever()`. -- `context/src/main/kotlin/io/spine/tools/validation/option/required/RequiredFieldSupport.kt` - — type-support rules; not touched by this change. -- `context-tests/src/testFixtures/proto/spine/validation/required_option_spec.proto` - — where the new "bad" fixtures land (three new messages). -- `context-tests/src/test/kotlin/io/spine/tools/validation/RequiredOptionSpec.kt` - — where the new compile-time tests land. -- `tests/validating/src/testFixtures/proto/spine/test/tools/validate/required.proto:63` - — `AlwaysInvalid` to be removed (replaced by the context-tests fixtures). -- `tests/validating/src/test/kotlin/io/spine/test/options/required/RequiredMessageITest.kt:60-77` - — the `not be applied to the type 'Empty'` test method to be removed - (along with its `AlwaysInvalid` and `Empty` imports). - -### API used to detect `Empty` - -Following the existing `refersToAny()` pattern in -`compiler-api/.../FieldTypeExts.kt`: -- `TypeNameOrBuilder.isAny` checks `packageName == "google.protobuf" && - simpleName == "Any"`. -- `Type.isAny = isMessage && message.isAny`. -- `FieldType.refersToAny()` handles singular / list / map. - -We mirror that locally in `RequiredOption.kt` with a `refersToEmpty()` -private helper. No change to `compiler-api` is needed for this issue. - -## Plan - -- [ ] **Add compile-time check** — in `RequiredOption.kt`: - - Add private helper `checkFieldIsNotEmpty(field, file)` next to - `checkFieldType()`, using `Compilation.check(...)` with a message like: - `"The field `${field.qualifiedName}` of type `${field.type.name}` cannot - be marked as `($REQUIRED)` because `google.protobuf.Empty` has no - fields and its instances are always equal to the default value."` - - Add a private `FieldType.refersToEmpty()` helper that checks singular - message / `list.message` / `map.valueType.message` for - `packageName == "google.protobuf" && simpleName == "Empty"`. - - Call `checkFieldIsNotEmpty(field, file)` from - `RequiredReaction.whenever()` immediately after `checkFieldType(...)`. -- [ ] **Move the runtime fixture to a compile-time fixture set**: - - Delete `AlwaysInvalid` from - `tests/validating/src/testFixtures/proto/spine/test/tools/validate/required.proto`. - - Add three new fixture messages to - `context-tests/src/testFixtures/proto/spine/validation/required_option_spec.proto`: - `RequiredEmptyField` (singular), `RequiredRepeatedEmpty` (repeated), - `RequiredMapWithEmptyValue` (`map`). -- [ ] **Add compile-time tests** in `RequiredOptionSpec.kt`: - - Three tests asserting `assertCompilationFails(...)` on each new - fixture with a substring matching the new error message. - - Reuse the existing `unsupportedFieldType(field)` only if the message - is exactly the same wording; otherwise add a sibling helper - `rejectsEmpty(field)` in the same file. -- [ ] **Remove the runtime test**: - - Delete the `not be applied to the type 'Empty'` test method in - `RequiredMessageITest.kt`. - - Drop the now-unused `import com.google.protobuf.Empty` and - `import io.spine.test.tools.validate.AlwaysInvalid`. -- [ ] **Search for any other live references** to `AlwaysInvalid` in - `tests/validating` proto/code (the `spine.test.validate.AlwaysInvalid` - in `messages.proto` is a DIFFERENT message — string fields — and - must remain untouched). -- [ ] **Pre-PR**: - - Run `bump-version` per repo policy. - - Run the build per `.agents/running-builds.md`. - - `kotlin-review` on the diff. - - `review-docs` on KDoc changes (any new public docs?). -- [ ] **Follow-up issue** under `core-jvm-compiler`: - - File a new issue: "Reject `(required)`-implicit ID fields of type - `google.protobuf.Empty` at compile-time". - - Link it back to #146 in both directions (mention from each issue). - - Body: short rationale, link to this change in `validation`, and the - affected reactions (`RequiredIdReaction`, `EntityStateIdReaction`). - -## Log - -- 2026-05-22 — drafted from user clarifications Q1–Q4 above; awaiting - approval via `ExitPlanMode`. diff --git a/.agents/tasks/setup-cross-tool-agent-instructions.md b/.agents/tasks/setup-cross-tool-agent-instructions.md new file mode 100644 index 0000000000..02672e2c8f --- /dev/null +++ b/.agents/tasks/setup-cross-tool-agent-instructions.md @@ -0,0 +1,138 @@ +--- +slug: setup-cross-tool-agent-instructions +branch: improve-caching +owner: claude +status: in-review +started: 2026-05-24 +--- + +# Task: Consolidate Agent Instructions into AGENTS.md + +## Goal + +Move universal agent instructions from `CLAUDE.md` into `AGENTS.md` so that +Claude Code, GitHub Copilot, and Codex all read identical rules from a single +source. Reduce `CLAUDE.md` to a thin wrapper that imports `AGENTS.md` plus a +small Claude Code-specific section. + +## Current state + +Both files already exist with real content. + +**`AGENTS.md`** currently has: +- Orientation — `project.md` reference, link to `.agents/_TOC.md` +- Commit and history safety — full rule (authoritative) +- Other safety rules — compile check, no auto-deps, no analytics, no reflection +- Moving files — `git mv` rule + +**`CLAUDE.md`** currently has: +- Project Guidelines — quick-reference-card, `project.md`, `jvm-project.md`, + skills, TOC +- Workflow Rules — `EnterPlanMode`, task planning, `api-discovery` skill, + commit rule (duplicate of AGENTS.md) +- Memory — team memory (`.agents/memory/`) + per-developer (auto-memory) +- Verification & Quality +- Core Principles +- Task Flow — plan writing, `ExitPlanMode`, `TaskCreate` +- Final Rule + +## Content split + +**Universal — move to `AGENTS.md`:** + +| Section | Notes | +|---|---| +| Project Guidelines (project.md, jvm-project.md, skills, TOC) | All agents need this orientation | +| Memory → team-shared store only (`.agents/memory/`) | Codex/Copilot have no auto-memory; the team store is universal | +| Verification & Quality | Universal engineering standards | +| Core Principles | Universal | +| Task Flow items 1, 4, 5, 6 (plan write, verify, update memory, delete task) | Universal; omit items 2–3 (ExitPlanMode/TaskCreate) | + +**Claude Code-specific — keep in `CLAUDE.md` only:** + +| Item | Why Claude-only | +|---|---| +| `EnterPlanMode` / `ExitPlanMode` | Claude Code SDK tools | +| `api-discovery` skill / never unzip JARs | Gradle cache path is machine-local | +| Per-developer auto-memory | Claude Code built-in feature | +| `TaskCreate` for live status | Claude Code SDK tool | +| Final Rule meta-note | Claude Code session advice | + +## Steps + +### 1. Expand `AGENTS.md` + +Add the universal sections to `AGENTS.md` after the existing content. Do not +duplicate the commit rule — it is already there. Resulting sections in order: + +1. Welcome / Orientation *(already exists — update to include quick-reference-card and skills references)* +2. Commit and history safety *(already exists — keep as-is)* +3. Other safety rules *(already exists — keep as-is)* +4. Moving files *(already exists — keep as-is)* +5. **Memory** — team-shared store only; omit the per-developer store +6. **Verification & Quality** +7. **Core Principles** +8. **Task planning** — write plan to `.agents/tasks/.md`; verify before marking done; delete task file on merge + +Keep `AGENTS.md` under 120 lines. Every line must change agent behaviour. + +### 2. Rewrite `CLAUDE.md` as a thin wrapper + +Replace the current content with: + +```markdown +@AGENTS.md + +## Claude Code-specific notes + +- Use Plan mode (`EnterPlanMode`) for architecture, refactoring, multi-file + changes, or lengthy documentation. Show the plan (`ExitPlanMode`) before + implementing. +- Track live progress with `TaskCreate`. +- Before reading library source code from `~/.gradle/caches`, follow the + `api-discovery` skill — never `unzip` JARs directly. +- 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. +- This is living team memory. Update it regularly and keep it concise + (<120 lines / ~2.5k tokens). +``` + +### 3. Verify `.github/copilot-instructions.md` + +This file already exists. Confirm it contains an explicit reference to `AGENTS.md` +at the repository root, a pointer to `project.md` for repo context, and the +universal "Do not suggest" safety rules. Add the `AGENTS.md` reference if absent. + +### 4. Verify the setup + +Run these checks and report results: + +- `AGENTS.md` exists at repo root and is under 120 lines (`wc -l AGENTS.md`). +- `CLAUDE.md` first non-empty line is `@AGENTS.md`. +- `.github/copilot-instructions.md` exists and references `.agents/project.md`. +- All modified files are tracked by git (no relevant "Untracked files" in + `git status`). + +### 5. Commit + +Stage only the files modified by this task. Use this commit message: + +``` +refactor: consolidate agent instructions into AGENTS.md + +Move universal rules (orientation, memory, quality, principles, task +planning) from CLAUDE.md into AGENTS.md so Codex, Copilot, and Claude +Code all read from a single source. CLAUDE.md becomes a thin @AGENTS.md +wrapper plus Claude Code-specific notes. +``` + +## Acceptance Criteria + +- Editing `AGENTS.md` is the only required change to update agent behaviour + across all three tools. +- No universal instruction content exists only in `CLAUDE.md`. +- `AGENTS.md` is under 120 lines. +- `CLAUDE.md` first non-empty line is `@AGENTS.md`. +- All checks in step 4 pass. diff --git a/.claude/settings.json b/.claude/settings.json index 22537b6a5d..f7bbfb98ff 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -15,6 +15,7 @@ "Bash(git restore:*)", "Bash(git stash:*)", "Bash(git fetch:*)", + "Bash(git push:*)", "Bash(git rev-parse:*)", "Bash(git ls-files:*)", "Bash(git mv:*)", @@ -34,7 +35,6 @@ "Bash(./config/migrate)" ], "deny": [ - "Bash(git push:*)", "Bash(git reset --hard:*)", "Bash(git clean -fdx:*)", "Bash(rm -rf /:*)", diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..81c8d500ca --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,26 @@ +# GitHub Copilot Instructions + +## Repository context + +This repository is part of the Spine SDK organisation (~40 repos). + +Universal agent instructions are in [`AGENTS.md`](../AGENTS.md) at the +repository root — read it first. + +If `.agents/project.md` exists, read it before reviewing. It provides the +language, architecture, role, and code review checklist for this specific repo. + +Additional guidelines are in `.agents/` — see `.agents/_TOC.md` for the index +(if present; Hugo repos do not include this file). + +## Universal rules + +**Do not suggest:** +- Any git history operation — `git commit`, `git push`, `git tag`, + `git rebase`, `git merge`, `git cherry-pick`, `gh pr merge`, or any other + command that writes to history — leave these to the developer. +- Auto-updating dependency versions outside a dedicated update task. +- Feature flags, backwards-compatibility shims, or fallbacks for scenarios + that cannot occur in the current codebase. +- Analytics, telemetry, or tracking code. +- Reflection or unsafe code without explicit approval. diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index fc2553b1ca..3fa235be0b 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -6,6 +6,7 @@ on: pull_request: paths: - 'docs/**' + - 'site/**' - 'lychee.toml' - '.github/workflows/check-links.yml' workflow_dispatch: @@ -34,7 +35,45 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # Detect the Hugo site root (`docs/` or `site/`) by looking for a Hugo + # config file. Hugo config may live directly in the site root or in a + # `config/` or `config/_default/` subdirectory (both layouts are valid). + # Outputs `present=true|false` and `work_dir` (the directory where + # `npm ci` / `hugo` commands should run — either `$dir/_preview` for + # repos that use a separate preview sub-tree, or `$dir` for repos whose + # Node/Hugo setup lives at the site root). + # When neither directory has a Hugo config, the job short-circuits to a + # success so that this shared workflow stays green on repos that do not + # host a Hugo site at all. + - name: Detect docs site + id: docs + run: | + for dir in docs site; do + for cfg in hugo.toml hugo.yaml \ + config/hugo.toml config/hugo.yaml \ + config/_default/hugo.toml config/_default/hugo.yaml; do + if [ -f "$dir/$cfg" ]; then + if [ -f "$dir/_preview/package-lock.json" ]; then + echo "work_dir=$dir/_preview" >> "$GITHUB_OUTPUT" + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/ (work_dir: $dir/_preview)" + elif [ -f "$dir/package-lock.json" ]; then + echo "work_dir=$dir" >> "$GITHUB_OUTPUT" + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/ (work_dir: $dir)" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo config found in $dir/ but no package-lock.json found — skipping link check." + fi + exit 0 + fi + done + done + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::No Hugo site found under docs/ or site/ — skipping link check." + - name: Setup Hugo + if: steps.docs.outputs.present == 'true' uses: peaceiris/actions-hugo@v3 with: hugo-version: ${{ env.HUGO_VERSION }} @@ -45,30 +84,34 @@ jobs: # standalone `actions/cache@v4` block so there is only one source of # truth for the cache key (no drift between two layers). - name: Setup Node + if: steps.docs.outputs.present == 'true' uses: actions/setup-node@v4 with: node-version: '26' cache: 'npm' - cache-dependency-path: docs/_preview/package-lock.json + cache-dependency-path: ${{ steps.docs.outputs.work_dir }}/package-lock.json # `HUGO_CACHEDIR=/tmp/hugo_cache` (set in `env:` above) makes Hugo # actually write to the path this step restores from. The key hashes - # `docs/_preview/go.sum` + `docs/go.sum`, so adding/removing a Hugo - # module invalidates the cache deterministically. + # both possible go.sum locations so adding/removing a Hugo module + # invalidates the cache deterministically regardless of site root. - name: Cache Hugo Modules + if: steps.docs.outputs.present == 'true' uses: actions/cache@v4 with: path: /tmp/hugo_cache - key: ${{ runner.os }}-hugomod-${{ hashFiles('docs/**/go.sum') }} + key: ${{ runner.os }}-hugomod-${{ hashFiles('docs/**/go.sum', 'site/**/go.sum') }} restore-keys: | ${{ runner.os }}-hugomod- - name: Install Dependencies - working-directory: docs/_preview + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} run: npm ci - - name: Build Validation docs - working-directory: docs/_preview + - name: Build docs preview site + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} run: hugo -e development # Cache Lychee results to avoid hitting rate limits. @@ -77,6 +120,7 @@ jobs: # stale `200 OK` entries for the now-checked URLs would be trusted until # `max_cache_age` expires. - name: Cache Lychee results + if: steps.docs.outputs.present == 'true' uses: actions/cache@v4 with: path: .lycheecache @@ -90,6 +134,7 @@ jobs: # the existing cached binary for the same version-tag instead of paying # for a fresh download. - name: Cache Lychee executable + if: steps.docs.outputs.present == 'true' id: cache-lychee uses: actions/cache@v4 with: @@ -112,14 +157,14 @@ jobs: # neither the exact key nor any restore-key restored the binary. - name: Download Lychee executable uses: robinraju/release-downloader@v1.7 - if: hashFiles('lychee/lychee') == '' + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' with: repository: "lycheeverse/lychee" tag: ${{ env.LYCHEE_VERSION_TAG }} fileName: ${{ env.LYCHEE_RELEASE }} - name: Verify Lychee checksum - if: hashFiles('lychee/lychee') == '' + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' run: | echo "${{ env.LYCHEE_SHA256 }} ${{ env.LYCHEE_RELEASE }}" | sha256sum --check --strict @@ -128,7 +173,7 @@ jobs: # flattens it to `lychee/lychee` — matching what the companion # `check-links` skill does locally and what the next step expects. - name: Extract Lychee executable - if: hashFiles('lychee/lychee') == '' + if: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' run: | mkdir -p lychee && tar -xzf ${{ env.LYCHEE_RELEASE }} --strip-components=1 -C lychee @@ -148,7 +193,8 @@ jobs: # the coupling with `--base-url http://localhost:1313/` in the next # Lychee step is visible — change one, change the other. - name: Start Hugo server - working-directory: docs/_preview + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.work_dir }} run: | nohup hugo server \ --environment development \ @@ -163,7 +209,8 @@ jobs: fi - name: Check links + if: steps.docs.outputs.present == 'true' run: | ./lychee/lychee --config lychee.toml --timeout 60 \ --base-url http://localhost:1313/ \ - 'docs/_preview/public/**/*.html' + '${{ steps.docs.outputs.work_dir }}/public/**/*.html' diff --git a/.gitignore b/.gitignore index f445d5b73b..38e5ad4b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -138,7 +138,6 @@ __pycache__/ # Claude working files /.claude/worktrees/ -/.claude/settings.local.json # Auto-downloaded Lychee binary used by the `check-links` skill. /.agents/skills/check-links/.cache/ @@ -146,3 +145,10 @@ __pycache__/ # Lychee link-checker cache (created by the `check-links` skill and # the `Check Links` workflow when run locally). .lycheecache + +# Hugo docs preview site build artifacts (used by the `check-links` +# skill and the `Check Links` workflow in repos that contain a +# `docs/_preview` Hugo site). +docs/_preview/node_modules/ +docs/_preview/public/ +docs/_preview/resources/ diff --git a/.idea/live-templates/README.md b/.idea/live-templates/README.md index 66713b347f..950066731a 100644 --- a/.idea/live-templates/README.md +++ b/.idea/live-templates/README.md @@ -5,12 +5,12 @@ This directory contains two live template groups: 1. `Spine.xml`: shortcuts for the repeated patterns used in the framework. 2. `User.xml`: a single shortcut to generate TODO comments. -### Instlallation +### Installation Live templates are not picked up by IDEA automatically. They should be added manually. In order to add these templates, perform the following steps: -1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA +1. Copy `*.xml` files from this directory to `templates` directory in the IntelliJ IDEA [settings folder][settings_folder]. 2. Restart IntelliJ IDEA: `File -> Invalidate Caches -> Just restart`. 3. Go to `Preferences -> Editor -> Live Templates`. @@ -23,5 +23,5 @@ In order to add these templates, perform the following steps: 1. Open the corresponding template: `Preferences -> Editor -> Live Templates -> User.todo`. 2. Click on `Edit variables`. 3. Set `USER` variable to your domain email address without `@teamdev.com` ending. For example, - for `jack.sparrow@teamdev.com` use the follwoing expression `"jack.sparrow"`. + for `jack.sparrow@teamdev.com` use the following expression `"jack.sparrow"`. 4. Verify that the template generates expected comments: `// TODO:2022-11-03:jack.sparrow: <...>`. diff --git a/.idea/live-templates/User.xml b/.idea/live-templates/User.xml index cc156507ea..958e2ea9be 100644 --- a/.idea/live-templates/User.xml +++ b/.idea/live-templates/User.xml @@ -1,7 +1,7 @@