diff --git a/.agents/advanced-safety-rules.md b/.agents/advanced-safety-rules.md new file mode 100644 index 0000000..e410581 --- /dev/null +++ b/.agents/advanced-safety-rules.md @@ -0,0 +1,6 @@ +# 🚨 Advanced safety rules + +- Do **not** auto-update external dependencies without explicit request. +- Do **not** inject analytics or telemetry code. +- Flag any usage of unsafe constructs (e.g., reflection, I/O on the main thread). +- Avoid generating blocking calls inside coroutines. diff --git a/.agents/documentation-guidelines.md b/.agents/documentation-guidelines.md new file mode 100644 index 0000000..6c9c1ba --- /dev/null +++ b/.agents/documentation-guidelines.md @@ -0,0 +1,14 @@ +# Documentation & comments + +## Commenting guidelines +- Avoid inline comments in production code unless necessary. +- Inline comments are helpful in tests. +- When using TODO comments, follow the format on the [dedicated page][todo-comments]. +- File and directory names should be formatted as code. + +## Avoid widows, runts, orphans, or rivers + +Agents should **AVOID** text flow patters illustrated +on [this diagram](widow-runt-orphan.jpg). + +[todo-comments]: https://github.com/SpineEventEngine/documentation/wiki/TODO-comments diff --git a/.agents/documentation-tasks.md b/.agents/documentation-tasks.md new file mode 100644 index 0000000..8ac4660 --- /dev/null +++ b/.agents/documentation-tasks.md @@ -0,0 +1,20 @@ +# πŸ“„ Documentation tasks + +1. Ensure all public and internal APIs have KDoc examples. +2. Add in-line code blocks for clarity in tests. +3. Convert inline API comments in Java to KDoc in Kotlin: + ```java + // Literal string to be inlined whenever a placeholder references a non-existent argument. + private final String missingArgumentMessage = "[MISSING ARGUMENT]"; + ``` + transforms to: + ```kotlin + /** + * Literal string to be inlined whenever a placeholder references a non-existent argument. + */ + private val missingArgumentMessage = "[MISSING ARGUMENT]" + ``` + +4. Javadoc -> KDoc conversion tasks: + - Remove `

` tags in the line with text: `"

This"` -> `"This"`. + - Replace `

` with empty line if the tag is the only text in the line. diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md new file mode 100644 index 0000000..2c8045c --- /dev/null +++ b/.agents/memory/MEMORY.md @@ -0,0 +1,17 @@ +# Team memory index + +One line per memory. Scan at the start of every session. +See [README.md](README.md) for the format and routing rules. + +## Feedback (validated patterns & corrections) + +- [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) + +*(no entries yet)* + +## Reference (external systems) + +- [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/README.md b/.agents/memory/README.md new file mode 100644 index 0000000..899d9e5 --- /dev/null +++ b/.agents/memory/README.md @@ -0,0 +1,89 @@ +# Team memory β€” `.agents/memory/` + +Validated patterns, durable project context, and pointers to external +systems. Checked into git so the whole team β€” and any agent working in +this repo β€” benefits from accumulated knowledge. + +This complements Claude Code's built-in per-developer auto-memory: +team-shareable knowledge lives here; personal preferences and ephemeral +state live in the auto-memory. + +## Layout + + .agents/memory/ + β”œβ”€β”€ MEMORY.md # Index β€” scan at start of every session + β”œβ”€β”€ README.md # This file β€” read when adding/updating memories + β”œβ”€β”€ feedback/ # Validated patterns & corrections + β”œβ”€β”€ project/ # Durable project context & rationale + └── reference/ # External systems & resources + +One file per memory. Filename = the memory's kebab-case slug. + +## File format + + --- + name: tests-no-db-mocks + description: One-line summary β€” used to surface relevance, so be specific. + metadata: + type: feedback # feedback | project | reference + since: 2026-05-19 # date added (ISO) + --- + + + + **Why:** + + **How to apply:** + + Related: [[other-memory-slug]] + +`Why:` and `How to apply:` are required for `feedback` and `project` +memories β€” they let future readers judge edge cases. `reference` +memories may be shorter (link + one-line purpose). + +Link related memories with `[[slug]]` (the target file's `name:`). + +## Routing β€” repo vs. auto-memory + +| Kind of fact | Goes to | +|---|---| +| Personal preference, role, style | auto-memory (`user`) | +| Personal habit feedback | auto-memory (`feedback`) | +| Team coding/test/PR rule | **`feedback/`** | +| Durable project rationale | **`project/`** | +| Ephemeral project state (freezes, OOO, deadlines) | auto-memory (`project`) β€” would rot in git | +| Team-shared external resource | **`reference/`** | +| Personal external resource | auto-memory (`reference`) | + +**Litmus test:** *would a teammate joining the project next month benefit +from knowing this?* If no, it belongs in auto-memory. + +## Write protocol + +1. Write the file **uncommitted** in the working tree. +2. **Surface the change** in the same turn so the human can review. +3. **Do not auto-commit** memory edits as part of an unrelated PR β€” memory + changes should be reviewable on their own. +4. **Correct in place** when an existing memory turns out wrong; `git blame` + carries the history. +5. **Propose deletion explicitly** when a memory has gone stale, rather + than silently editing it out. + +## Updating the index + +After adding or removing a memory file, update `MEMORY.md`. One line under +the matching section: + + - [slug](category/slug.md) β€” description from frontmatter + +Keep the index short β€” long descriptions belong in the file body. + +## Anti-patterns β€” do not store + +- Anything derivable from the code (module structure, paths, conventions + visible in source). Use `grep` / `Read`. +- Recent-activity summaries or PR lists β€” `git log` is authoritative. +- Fix recipes for specific bugs β€” the commit message belongs in the commit. +- Anything already documented in `.agents/` reference docs β€” keep one + source of truth. +- Personal preferences (see routing). diff --git a/.agents/memory/feedback/.gitkeep b/.agents/memory/feedback/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.agents/memory/feedback/copilot-review-request.md b/.agents/memory/feedback/copilot-review-request.md new file mode 100644 index 0000000..f5dde9b --- /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/project/.gitkeep b/.agents/memory/project/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.agents/memory/reference/.gitkeep b/.agents/memory/reference/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.agents/memory/reference/anthropic-api-caching.md b/.agents/memory/reference/anthropic-api-caching.md new file mode 100644 index 0000000..bcb1be4 --- /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 0000000..796dd4d --- /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.md b/.agents/project.md new file mode 100644 index 0000000..77c3f9c --- /dev/null +++ b/.agents/project.md @@ -0,0 +1,59 @@ +# Project: documentation + +## Overview + +This repository is the **documentation aggregator and content host** for the +Spine SDK. It owns the Hugo site setup that gathers documentation from sibling +SDK repos (currently `SpineEventEngine/validation`) as Hugo modules, and it +also stores original Markdown content under `docs/`. The repo additionally +serves as the GitHub Wiki source for committer-facing documentation about +contributing to the framework. + +The public site at [spine.io](https://spine.io) is built from +`SpineEventEngine/SpineEventEngine.github.io`, which **imports this repo's +`docs/` directory as a Hugo module**. Edits made here flow to the public site +when `SpineEventEngine.github.io` bumps its module pin. + +## Architecture + +**Role in the org:** documentation aggregator + content host. + +**Stack.** A Hugo + Node project at heart. Gradle is a thin task-runner +wrapper around Hugo, Node, and Go tooling β€” not a JVM build in any meaningful +sense, so JVM coding conventions do not apply here. + +- `docs/` β€” published content, Hugo site root, exported as a Hugo module to + `SpineEventEngine.github.io`. +- `docs/_preview/` β€” local-only Hugo setup for running the site during + authoring (`./gradlew :runSite`, or `hugo server` from this directory). +- `docs/_code/examples/*` β€” git submodules pinned at + `spine-examples/{airport, blog, hello, kanban, todo-list}`. These are the + **canonical source of embedded code samples**; this repo does not modify + them. +- `config/` β€” git submodule pointing at `SpineEventEngine/config`. Provides + shared agent guidance, skills, and build config consumed across Spine SDK + repos. Applied via the `Apply config` step. +- Theme: components, layouts, and styles come from the `site-commons` Hugo + theme (`github.com/SpineEventEngine/site-commons`). + +**Doc modules pulled in via `docs/hugo.toml`.** Currently only the +`validation` repo contributes docs as a Hugo module. The README lists +`framework` and `compiler` as examples of how to add more; they are not +wired in today. + +**Key conventions and constraints (not obvious from the code):** + +- **Theme changes mirror to spine.io.** Any non-trivial change to + `site-commons` usage here must also be applied in the main `spine.io` site + repo, or the live site will diverge from preview. +- **Embedded code must round-trip.** Code blocks in pages are generated from + the `docs/_code` submodules by the [`embed-code`][embed-code] tool. Do not + hand-edit embedded code blocks in Markdown; run `./gradlew :embedCode` and + verify with `./gradlew :checkSamples`. See [`EMBEDDING.md`](../EMBEDDING.md). +- **Submodules are pinned.** `docs/_code/examples/*` and `config` are pinned + intentionally β€” do not bump them as a side effect of unrelated work. +- **Link checking is required pre-PR.** Run the `check-links` skill before + opening any PR that touches `docs/**` or `site/**`; CI runs the same check + via `lychee.toml` against the rendered HTML. + +[embed-code]: https://github.com/SpineEventEngine/embed-code-go diff --git a/.agents/project.template.md b/.agents/project.template.md new file mode 100644 index 0000000..b6882e0 --- /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/safety-rules.md b/.agents/safety-rules.md new file mode 100644 index 0000000..e7fece3 --- /dev/null +++ b/.agents/safety-rules.md @@ -0,0 +1,49 @@ +# Safety rules + +- βœ… All code must compile and pass static analysis. +- βœ… Do not auto-update external dependencies. +- ❌ 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. diff --git a/.agents/scripts/pre-pr-gate.sh b/.agents/scripts/pre-pr-gate.sh new file mode 100755 index 0000000..cb80b31 --- /dev/null +++ b/.agents/scripts/pre-pr-gate.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# +# PreToolUse hook: block `gh pr create` unless /pre-pr has successfully run +# for the current HEAD. The hook is intentionally unaware of the repository's +# versioning or build system; the /pre-pr skill decides which checks apply. +# +# Input: hook JSON on stdin (tool_name, tool_input.command). +# Exit: 0 to allow, 2 to block (stderr is surfaced to Claude). +# +set -eu + +input=$(cat) +tool=$(printf '%s' "$input" | jq -r '.tool_name // empty') +[ "$tool" != "Bash" ] && exit 0 + +cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty') + +# Split the command on shell separators (`;`, `&`, `|` β€” `&&`/`||` collapse +# to repeated newlines, which is fine) and check each segment. Only block +# when a segment STARTS (after optional whitespace) with `gh pr create`. +# This avoids false positives like `echo "gh pr create"` or test fixtures +# that mention the string, while still catching `cd dir && gh pr create` +# and `cat body | gh pr create`. `tr` is used (not `sed s///`) because +# BSD `sed` on macOS does not interpret `\n` in the replacement string. +if ! printf '%s' "$cmd" \ + | tr ';&|' '\n\n\n' \ + | grep -qE '^[[:space:]]*gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)'; then + exit 0 +fi + +repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +sentinel="$repo_root/.git/pre-pr.ok" + +block() { + cat >&2 + exit 2 +} + +if [ ! -f "$sentinel" ]; then + block < 1) next; print; next } + { blank = 0; print } + ' "$path" > "$tmp" && mv "$tmp" "$path" +} + +if [ -n "$file" ]; then + sanitize_file "$file" + exit 0 +fi + +printf '%s\n' "$command" \ + | sed -nE 's/^\*\*\* (Add|Update) File: (.*)$/\2/p' \ + | sort -u \ + | while IFS= read -r path; do + sanitize_file "$path" + done diff --git a/.agents/scripts/update-copyright.sh b/.agents/scripts/update-copyright.sh new file mode 100755 index 0000000..b25282f --- /dev/null +++ b/.agents/scripts/update-copyright.sh @@ -0,0 +1,48 @@ +#!/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 -u + +# Required tools β€” silently no-op if either is missing so the hook never blocks. +command -v jq >/dev/null 2>&1 || exit 0 +command -v python3 >/dev/null 2>&1 || exit 0 + +input=$(cat) +file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true) +command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true) + +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 diff --git a/.agents/skills/check-links/SKILL.md b/.agents/skills/check-links/SKILL.md new file mode 100644 index 0000000..5571e13 --- /dev/null +++ b/.agents/skills/check-links/SKILL.md @@ -0,0 +1,320 @@ +--- +name: check-links +description: > + 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/**` 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/` 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 + +`.github/workflows/check-links.yml` is the **single source of truth** for the +Hugo and Lychee pins. This file does not duplicate the current values +because duplicates inevitably drift; see the workflow's `env:` block for +the canonical `HUGO_VERSION` and `LYCHEE_VERSION_TAG`. The auto-download +step (Β§2) reads `LYCHEE_VERSION_TAG` out of the workflow at runtime, so a +workflow bump propagates automatically. Hugo is not auto-installed; the +skill uses whichever `hugo` is on `$PATH` and only warns (does not block) +if the installed version is older than the workflow's `HUGO_VERSION` β€” +Hugo's HTML output is stable enough across minor versions that a small +skew does not invalidate link-check results. + +The authoritative shared config is `lychee.toml` at the repo root. Do not +fork its exclude list β€” fix the source link or, if the failing URL is a known +flaky external endpoint, add it to `lychee.toml` once (the change applies to +both the skill and CI). + +## When to run + +- 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`. + +If none of the above is true, decline with a one-line note rather than +running the (~30 s) build+check. + +## Tooling + +The skill needs four binaries: + +| Tool | Purpose | Install hint | +|--------|------------------------------------------|-------------------------------| +| Hugo | Build and serve the site | `brew install hugo` (extended)| +| Node | Hugo theme dependencies (`npm ci`) | `brew install node` | +| npm | Same | bundled with Node | +| Lychee | Link checker | `brew install lychee` | + +For **Lychee**, prefer a pre-installed binary on `$PATH`. If none is found, +download the pinned release (see `LYCHEE_VERSION_TAG` in +`.github/workflows/check-links.yml` β€” the dynamic-read pattern in step 2 below +keeps this version in lock-step with CI) into +`.agents/skills/check-links/.cache/lychee/` and use that path. The pinned +version matches what the CI workflow uses, so behavior is identical. + +`.agents/skills/check-links/.cache/` is git-ignored (see `.gitignore`). + +## Procedure + +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 + +Before any other step, determine `SITE_DIR` β€” the directory that contains the +Hugo config file: + +```bash +SITE_DIR="" +for dir in docs site; do + for cfg in hugo.toml 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 +``` + +Use `$SITE_DIR` everywhere a directory path is needed 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 +`$SITE_DIR/**` and no changes to `lychee.toml`, and the user did not +explicitly ask, decline and exit cleanly. + +### 2. Preflight binaries + +- `hugo version` β†’ must succeed; capture the version. If missing, stop with + Must-fix: "Install Hugo extended (`brew install hugo`)." If installed but + older than the workflow's `HUGO_VERSION` (parse with + `grep -E '^[[:space:]]+HUGO_VERSION:' .github/workflows/check-links.yml | sed -E 's/.*: *"?([^"]+)"?$/\1/'`), warn but + continue. +- `node -v` and `npm -v` β†’ must succeed. If missing, stop with Must-fix: + "Install Node (`brew install node`) at the major version pinned by + `node-version:` in `.github/workflows/check-links.yml`." +- `lychee --version` β†’ if it succeeds, record the path and version. +- If `lychee` is missing: + 1. Read the canonical pin from the workflow file so the skill cannot drift + from CI: + ```bash + LYCHEE_VERSION_TAG=$( + grep -E '^[[:space:]]+LYCHEE_VERSION_TAG:' .github/workflows/check-links.yml \ + | sed -E 's/.*: *"?([^"]+)"?$/\1/' + ) + ``` + Expected shape: `lychee-vX.Y.Z` (the leading `lychee-` is part of the + upstream release tag, not a typo). + 2. Determine platform via `uname -s` / `uname -m`. Map to the matching + Lychee asset (recent releases β€” `v0.24.2` and later β€” drop the + version from the asset filename): + - `Darwin` + `arm64` β†’ `lychee-aarch64-apple-darwin.tar.gz` + - `Darwin` + `x86_64` β†’ `lychee-x86_64-apple-darwin.tar.gz` + - `Linux` + `x86_64` β†’ `lychee-x86_64-unknown-linux-gnu.tar.gz` + - `Linux` + `aarch64` β†’ `lychee-aarch64-unknown-linux-gnu.tar.gz` + - any other combination (e.g. Windows, FreeBSD, 32-bit) β†’ stop with + Must-fix: "Unsupported platform for Lychee auto-download β€” install + Lychee manually (`brew install lychee` / `cargo install lychee`) + and rerun." + 3. Ensure the cache directory exists *before* the download β€” + `mkdir -p .agents/skills/check-links/.cache/lychee/` β€” + because the path is git-ignored and absent on a fresh clone, + and `tar -xzf … -C

` will fail with "no such file or + directory" if the target does not exist yet. This mirrors the + `mkdir -p lychee` that `check-links.yml` does before its own + extract step. + 4. Download from + `https://github.com/lycheeverse/lychee/releases/download/${LYCHEE_VERSION_TAG}/` + into `.agents/skills/check-links/.cache/lychee/` and extract + with `tar -xzf --strip-components=1 -C .agents/skills/check-links/.cache/lychee/` + so the binary lands at + `.agents/skills/check-links/.cache/lychee/lychee`. + 5. Use `.agents/skills/check-links/.cache/lychee/lychee` for the rest of this run. + 6. Print a one-line note: "Using auto-downloaded Lychee. For faster runs, + install with `brew install lychee`." + +### 3. Install Hugo deps + +Run `( cd ${SITE_DIR}/_preview && 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 + different transitive versions than CI, which defeats the "render + identical HTML to CI" goal. +- If `package.json` and `package-lock.json` drift out of sync, `npm ci` + fails fast with a clear error rather than silently healing the + lockfile β€” a divergence we want to surface, not paper over. + +The helper script `${SITE_DIR}/_script/install-dependencies` exists for +interactive use but does a relative `cd _preview` and therefore only works +when invoked from `${SITE_DIR}/` β€” 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 ${SITE_DIR}/_preview && hugo -e development )`. +This emits `${SITE_DIR}/_preview/public/**/*.html`. The `-e development` flag +matches what CI uses in `check-links.yml` so the two builds render identical +HTML. (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 + +The server must survive across multiple `Bash` tool calls (steps 5 β†’ 6 β†’ 8 +typically run in separate shells), so we rely on `nohup` alone β€” a `trap … +EXIT` would fire when *this* shell exits and kill the server before Lychee +can query it. Teardown happens explicitly in step 8. + +Before launching, kill any leftover server from a previous crashed run so a +stale process does not hold port `1414`: + +```bash +pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true +rm -f /tmp/check-links.hugo.pid + +( cd ${SITE_DIR}/_preview && nohup hugo server --environment development --port 1414 \ + > /tmp/check-links.hugo.out 2>&1 & echo $! > /tmp/check-links.hugo.pid ) +sleep 5 + +# Verify the captured PID is alive before relying on it. `$!` for +# `nohup foo &` is reliable on bash but not portable across shells; the +# pgrep check turns a silent "Lychee fetches an empty port" failure into +# a clear error. +if ! pgrep -F /tmp/check-links.hugo.pid > /dev/null 2>&1; then + echo "ERROR: Hugo server failed to start. Tail of log:" >&2 + tail -20 /tmp/check-links.hugo.out >&2 || true + exit 1 +fi +``` + +Port `1414` is chosen to avoid clashing with a developer's local `hugo server` +(default `1313`). The `--environment development` flag matches CI's build env. + +### 6. Run Lychee + +```bash + --config lychee.toml --timeout 60 \ + --base-url http://localhost:1414/ \ + "${SITE_DIR}/_preview/public/**/*.html" +``` + +Capture exit code. Any non-zero exit means at least one broken link. + +### 7. Report + +Group the broken URLs from Lychee's output by source page. To reverse-map +an HTML path to its Markdown source: + +`${SITE_DIR}/_preview/public/docs/
//index.html` +↔ `${SITE_DIR}/content/docs/
/.md` (or `/_index.md`). + +Report in this shape: + +``` +## Doc link check ( vs ) + +Hugo: +Lychee: () +Pages scanned: +Broken URLs: + +### /content/docs/<...>/.md +- β€” +- β€” ... + +### /content/docs/<...>/.md +- ... +``` + +If `K == 0`, report a single line: "All links OK." + +### 8. Tear down and sentinel + +- Kill the Hugo server (and clean up its pid file): + + ```bash + pkill -F /tmp/check-links.hugo.pid 2>/dev/null || true + rm -f /tmp/check-links.hugo.pid /tmp/check-links.hugo.out + ``` + + Run this even if Lychee failed β€” leaving a server on port `1414` would + poison the next invocation. +- Write `.git/check-links.ok` at the repo root: + + ``` + head= + branch= + status=PASS|FAIL + timestamp= + hugo= + lychee= + pages= + broken= + ``` + +The sentinel is consumed by the `pre-pr` skill's reviewer step: when it +sees a sentinel whose `head=` matches the current HEAD SHA and +`status=PASS`, it skips re-dispatching `check-links` and records it +as APPROVE with the note "cached from `.git/check-links.ok`". Any +HEAD advance (commit, amend, rebase) invalidates the cache automatically. + +## Notes + +- This skill does **not** modify tracked sources. It does, however, write + several git-ignored build artifacts during a run β€” listed here so a future + 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. + - `${SITE_DIR}/_preview/node_modules/` β€” installed by `npm ci` in step 3. + - `${SITE_DIR}/_preview/public/` β€” Hugo's rendered HTML (the corpus Lychee + scans). + - `${SITE_DIR}/_preview/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 + removed in step 8's teardown. + + Every path above is matched by an existing `.gitignore` entry; none is + committed. +- The `lychee.toml` exclude list is the single source of truth for flaky + external endpoints. If a real link must be excluded, add it there and + explain why in a comment so CI and local runs stay in sync. +- The skill assumes the docs build succeeds. A Hugo build error is treated + the same as a link failure β€” surface it and stop. +- The `include_verbatim = false` setting in `lychee.toml` skips links inside + code blocks. That is intentional today; flip it on if you specifically need + to validate examples. + +## Related skills + +- `review-docs` β€” prose, KDoc/Javadoc, and Markdown style review. Runs in + parallel with `check-links` when invoked by `pre-pr`. +- `pre-pr` β€” composes the above and gates `gh pr create`. diff --git a/.agents/skills/move-files/SKILL.md b/.agents/skills/move-files/SKILL.md new file mode 100644 index 0000000..b92b05d --- /dev/null +++ b/.agents/skills/move-files/SKILL.md @@ -0,0 +1,57 @@ +--- +name: move-files +description: > + Move or rename any files/directories in a repo: preserve history, update all + references and build metadata, verify no stale paths remain. +--- + +# Move Files + +## Workflow + +1. Preflight. + - Run `git status --short`. + - Map each `source -> destination`. + - Classify scope: simple same-module moves stay targeted; package, module, or + cross-module moves need broader inspection. + - Ask before ambiguous mappings, destination conflicts, or unclear semantic + package/module changes. + +2. Search before moving. + - Search all old identifiers: paths, names, resource refs, doc links. + - For Gradle/module/source-set moves, check `settings.gradle.kts`, + `build.gradle.kts`, and `buildSrc`. + - For Kotlin/Java, update package declarations only when package intent + changes. + +3. Move safely. + - Always use `git mv` for tracked files in the repo. If sandboxing blocks + it, request approval; do not use delete/create as a fallback. + - Use filesystem moves only for untracked/generated/out-of-git files. + - Create parent directories first. + - For case-only renames, move through a temporary name. + +4. Repair references. + - Update all references: imports, build metadata, docs, resources, and scripts. + - Start search scope narrow: affected directory, then module, then repo-wide. + - Prefer precise edits; avoid broad replacements on generic names. + +5. Verify. + - Re-run targeted searches for old tokens. + - Run `git status --short` and confirm the delta matches the move. + - Run focused validation for moved files, or state what could not run. + +6. Ensure the version is bumped. + Invoke `/version-bumped` so the branch carries a strictly greater + `version.gradle.kts` than the base ref before any `./gradlew build` + (which can transitively `publishToMavenLocal` and overwrite + consumer-facing snapshots). The skill is a no-op if a bump already + happened earlier on the branch. + +## Repo Notes + +Follow `.agents/project-structure-expectations.md` for module/source-set/test moves. + +## Report + +Return: `Moved[]`, `UpdatedRefs[]`, `Verification[]`, `Risks[]`. diff --git a/.agents/skills/move-files/agents/openai.yaml b/.agents/skills/move-files/agents/openai.yaml new file mode 100644 index 0000000..ba90a9f --- /dev/null +++ b/.agents/skills/move-files/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Move Files" + short_description: "Move files safely across a repo" + default_prompt: "Use $move-files to relocate files or directories in this repository while preserving history, updating references, and verifying the result." diff --git a/.agents/skills/pre-pr/SKILL.md b/.agents/skills/pre-pr/SKILL.md new file mode 100644 index 0000000..2c81dfd --- /dev/null +++ b/.agents/skills/pre-pr/SKILL.md @@ -0,0 +1,181 @@ +--- +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`, + `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 + branch and you want a fast local repro. +--- + +# Pre-PR checklist (repo-specific) + +You are the pre-PR gate for this repository. You compose the existing +reviewers and the documented repository rules into a single pass that must +succeed before a pull request is opened. + +This skill supports both versioned Gradle Build Tools projects and repositories +that intentionally do not have `version.gradle.kts`. 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. +- `.agents/safety-rules.md` and `.agents/advanced-safety-rules.md` β€” hard + constraints checked by the reviewers. + +## Procedure + +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. +- 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 + +- 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 + +Pick the target per `.agents/running-builds.md`: + +- **proto** changed β†’ `./gradlew clean build` +- Else **code** changed β†’ `./gradlew build` +- Else **docs**-only β†’ `./gradlew dokka` + +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. 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 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 + +- **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 (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= +build_status=PASS|FAIL|skipped +reviewers= +version=new, introduced:, or "not-applicable"> +``` + +## Output format + +**On PASS** β€” single line: + +``` +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] +``` + +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. +- 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/skills/review-docs/SKILL.md b/.agents/skills/review-docs/SKILL.md new file mode 100644 index 0000000..d936fa2 --- /dev/null +++ b/.agents/skills/review-docs/SKILL.md @@ -0,0 +1,129 @@ +--- +name: review-docs +description: > + Review documentation changes β€” KDoc/Javadoc inside Kotlin/Java sources and + Markdown docs (`README.md`, `docs/**`) β€” against Spine documentation + conventions. Use when a diff touches doc comments or Markdown, before + opening a doc-affecting PR, or when asked for a documentation review. + Read-only; does not run builds. +--- + +# Review documentation (repo-specific) + +You are the documentation reviewer for a Spine Event Engine project. You +focus strictly on documentation quality β€” prose, KDoc/Javadoc, and Markdown β€” +and deliberately do **not** duplicate the code-review skill (which owns +Kotlin idioms, safety rules, tests, and version-gate checks). + +The authoritative standards live in `.agents/`: + +- `.agents/documentation-guidelines.md` β€” commenting rules, TODO-comment + format, "file/dir names as code", widow/runt/orphan/river rule (with the + diagram at `.agents/widow-runt-orphan.jpg`). +- `.agents/documentation-tasks.md` β€” KDoc-example requirement on APIs; + Javadoc β†’ KDoc conversion rules (`

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

` tags on a text line removed (`"

This"` β†’ `"This"`). + - `

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

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

` tags) left in Kotlin KDoc, broken Markdown links. +- **Should fix** β€” TODO format, inline-comment overuse in production, + inline external links that should be footnote-style, missing typographic + quotes (or unwanted ones), widow/runt/orphan/river paragraphs, + fenced-vs-indented code blocks. +- **Nits** β€” wording, terminology drift, code-identifier capitalization + in prose, "for the code reviewer" pointers if any code issues surfaced + incidentally. + +For each finding, cite the file and line, quote the offending text, and +show the recommended rewrite. If a section is empty, write "None." + +End with a one-line verdict: `APPROVE`, `APPROVE WITH CHANGES`, or +`REQUEST CHANGES`. diff --git a/.agents/skills/update-copyright/SKILL.md b/.agents/skills/update-copyright/SKILL.md new file mode 100644 index 0000000..6afc4c7 --- /dev/null +++ b/.agents/skills/update-copyright/SKILL.md @@ -0,0 +1,16 @@ +--- +name: update-copyright +description: > + Update source file copyright headers from the IntelliJ IDEA copyright profile, + replacing `today.year` with the current year. + Automatically apply when source files are modified in a change set. +--- + +# Copyright Update + +**Command:** `python3 .agents/skills/update-copyright/scripts/update_copyright.py` + +1. Scope: explicit files/dirs from the user, or all tracked source files if none given. +2. No explicit paths β†’ run with `--dry-run` first, then without. +3. Relay stdout (notice source, file count, changed paths) to the user. +4. Never add a copyright header to a file that does not already have one. diff --git a/.agents/skills/update-copyright/agents/openai.yaml b/.agents/skills/update-copyright/agents/openai.yaml new file mode 100644 index 0000000..246dd64 --- /dev/null +++ b/.agents/skills/update-copyright/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Copyright Update" + short_description: "Refresh source copyright headers" + default_prompt: "Use $update-copyright to refresh source file copyright headers from the IntelliJ IDEA copyright profile in this repository." diff --git a/.agents/skills/update-copyright/scripts/update_copyright.py b/.agents/skills/update-copyright/scripts/update_copyright.py new file mode 100755 index 0000000..2dbf8bb --- /dev/null +++ b/.agents/skills/update-copyright/scripts/update_copyright.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +"""Update source copyright headers from IntelliJ IDEA copyright profiles.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import html +import re +import subprocess +import sys +from pathlib import Path +from xml.etree import ElementTree as ET + + +BLOCK_EXTENSIONS = { + ".c", + ".cc", + ".cpp", + ".cs", + ".css", + ".cxx", + ".dart", + ".go", + ".gradle", + ".groovy", + ".h", + ".hh", + ".hpp", + ".java", + ".js", + ".jsx", + ".kt", + ".kts", + ".less", + ".m", + ".mm", + ".proto", + ".rs", + ".scala", + ".scss", + ".swift", + ".ts", + ".tsx", +} +HASH_EXTENSIONS = { + ".bash", + ".bzl", + ".properties", + ".pl", + ".py", + ".rb", + ".sh", + ".toml", + ".yaml", + ".yml", + ".zsh", +} +XML_EXTENSIONS = { + ".fxml", + ".pom", + ".wsdl", + ".xml", + ".xsd", + ".xsl", + ".xslt", +} +EXCLUDED_DIRS = { + ".agents", + ".git", + ".gradle", + ".idea", + ".kotlin", + "build", + "generated", + "out", + "tmp", +} +EXCLUDED_FILES = { + "gradlew", + "gradlew.bat", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Update source copyright headers from " + ".idea/copyright/profiles_settings.xml." + ) + ) + parser.add_argument( + "paths", + nargs="*", + help="Files or directories to update. Defaults to tracked source files.", + ) + parser.add_argument( + "--root", + type=Path, + default=Path.cwd(), + help="Repository root. Defaults to the current working directory.", + ) + parser.add_argument( + "--year", + default=str(dt.date.today().year), + help="Year to substitute for today.year. Defaults to the current year.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Report files that would change without writing them.", + ) + parser.add_argument( + "--check", + action="store_true", + help="Exit with status 1 if any file would change; do not write files.", + ) + return parser.parse_args() + + +def profile_filename(profile_name: str) -> str: + stem = re.sub(r"[^A-Za-z0-9]+", "_", profile_name).strip("_") + if not stem: + raise ValueError("The default copyright profile name is empty.") + return f"{stem}.xml" + + +def load_notice(root: Path, year: str) -> tuple[str, Path]: + settings_path = root / ".idea" / "copyright" / "profiles_settings.xml" + if not settings_path.is_file(): + raise FileNotFoundError(f"Missing {settings_path}") + + settings_root = ET.parse(settings_path).getroot() + settings = settings_root.find(".//settings") + if settings is None: + raise ValueError(f"{settings_path} does not contain a settings tag.") + + default_profile = settings.get("default") + if not default_profile: + raise ValueError(f"{settings_path} settings tag has no default attribute.") + + profile_path = settings_path.parent / profile_filename(default_profile) + if not profile_path.is_file(): + raise FileNotFoundError( + f"Default profile {default_profile!r} resolves to missing {profile_path}" + ) + + profile_root = ET.parse(profile_path).getroot() + notice = None + for option in profile_root.findall(".//option"): + if option.get("name") == "notice": + notice = option.get("value") + break + if notice is None: + raise ValueError(f"{profile_path} has no option named 'notice'.") + + decoded = html.unescape(notice) + decoded = decoded.replace("${today.year}", year) + decoded = decoded.replace("$today.year", year) + decoded = decoded.replace("today.year", year) + return decoded.rstrip(), profile_path + + +def style_for(path: Path) -> str | None: + name = path.name + suffix = path.suffix.lower() + if name.endswith((".sh.template", ".bash.template", ".zsh.template")): + return "hash" + if suffix in BLOCK_EXTENSIONS: + return "block" + if suffix in HASH_EXTENSIONS: + return "hash" + if suffix in XML_EXTENSIONS: + return "xml" + return None + + +def is_excluded(path: Path) -> bool: + if path.name in EXCLUDED_FILES: + return True + parts = path.parts + if len(parts) >= 2 and parts[0] == "gradle" and parts[1] == "wrapper": + return True + return any(part in EXCLUDED_DIRS for part in parts) + + +def tracked_files(root: Path) -> list[Path]: + try: + result = subprocess.run( + ["git", "-C", str(root), "ls-files", "-z"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except (FileNotFoundError, subprocess.CalledProcessError): + return [ + path.relative_to(root) + for path in root.rglob("*") + if path.is_file() and not is_excluded(path.relative_to(root)) + ] + + paths = [] + for item in result.stdout.decode("utf-8").split("\0"): + if not item: + continue + path = Path(item) + if (root / path).is_file(): + paths.append(path) + return paths + + +def expand_requested_paths(root: Path, requested: list[str]) -> list[Path]: + if not requested: + paths = tracked_files(root) + else: + paths = [] + for item in requested: + path = (root / item).resolve() + if not path.exists(): + raise FileNotFoundError(f"Path does not exist: {item}") + if not path.is_relative_to(root): + raise ValueError( + f"Path is outside the repository root: {item!r} " + f"(resolved to {path}, root is {root})" + ) + if path.is_dir(): + for child in path.rglob("*"): + if child.is_file(): + paths.append(child.relative_to(root)) + else: + paths.append(path.relative_to(root)) + + unique = sorted(set(paths), key=lambda p: p.as_posix()) + return [ + path + for path in unique + if style_for(path) is not None and not is_excluded(path) + ] + + +def newline_for(text: str) -> str: + return "\r\n" if "\r\n" in text else "\n" + + +def build_header(notice: str, style: str, newline: str) -> str: + lines = notice.splitlines() + if style == "block": + body = newline.join(f" * {line}" if line else " *" for line in lines) + return f"/*{newline}{body}{newline} */{newline}{newline}" + if style == "hash": + body = newline.join(f"# {line}" if line else "#" for line in lines) + return f"{body}{newline}{newline}" + if style == "xml": + body = newline.join(f" ~ {line}" if line else " ~" for line in lines) + return f"{newline}{newline}" + raise ValueError(f"Unsupported comment style: {style}") + + +def split_leading_directive(text: str, style: str, newline: str) -> tuple[str, str]: + if style == "hash" and text.startswith("#!"): + line_end = text.find("\n") + if line_end == -1: + return text + newline + newline, "" + prefix = text[: line_end + 1] + newline + return prefix, strip_leading_blank_lines(text[line_end + 1 :]) + + if style == "xml" and text.startswith("") + if close != -1: + line_end = text.find("\n", close) + if line_end == -1: + return text + newline + newline, "" + prefix = text[: line_end + 1] + newline + return prefix, strip_leading_blank_lines(text[line_end + 1 :]) + + return "", strip_leading_blank_lines(text) + + +def strip_leading_blank_lines(text: str) -> str: + return re.sub(r"^(?:[ \t]*\r?\n)+", "", text) + + +def strip_existing_header(text: str, style: str) -> tuple[str, bool]: + if style == "block" and text.startswith("/*"): + close = text.find("*/") + if close != -1: + candidate = text[: close + 2] + if is_copyright_header(candidate): + return strip_leading_blank_lines(text[close + 2 :]), True + + if style == "xml" and text.startswith("") + if close != -1: + candidate = text[: close + 3] + if is_copyright_header(candidate): + return strip_leading_blank_lines(text[close + 3 :]), True + + if style == "hash": + lines = text.splitlines(keepends=True) + end = 0 + for line in lines: + stripped = line.strip() + if stripped == "" or stripped.startswith("#"): + end += len(line) + continue + break + candidate = text[:end] + if candidate and is_copyright_header(candidate): + return strip_leading_blank_lines(text[end:]), True + + return text, False + + +def is_copyright_header(text: str) -> bool: + limited = text[:5000] + return "Copyright" in limited and ( + "Licensed under" in limited or "All rights reserved" in limited + ) + + +def updated_text(text: str, notice: str, style: str) -> str: + original = text + bom = "\ufeff" if text.startswith("\ufeff") else "" + if bom: + text = text[1:] + newline = newline_for(text) + prefix, body = split_leading_directive(text, style, newline) + body, had_header = strip_existing_header(body, style) + if not had_header: + return original + return bom + prefix + build_header(notice, style, newline) + body + + +def update_file(root: Path, path: Path, notice: str, dry_run: bool) -> bool: + absolute = root / path + style = style_for(path) + if style is None: + return False + + try: + text = absolute.read_text(encoding="utf-8") + except FileNotFoundError: + print(f"Skipping missing file: {path}", file=sys.stderr) + return False + except UnicodeDecodeError: + print(f"Skipping non-UTF-8 file: {path}", file=sys.stderr) + return False + + next_text = updated_text(text, notice, style) + if next_text == text: + return False + + if not dry_run: + with absolute.open("w", encoding="utf-8", newline="") as file: + file.write(next_text) + return True + + +def main() -> int: + args = parse_args() + root = args.root.resolve() + notice, profile_path = load_notice(root, args.year) + try: + paths = expand_requested_paths(root, args.paths) + except (FileNotFoundError, ValueError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + dry_run = args.dry_run or args.check + + changed = [ + path + for path in paths + if update_file(root, path, notice, dry_run=dry_run) + ] + + rel_profile = profile_path.relative_to(root) + action = "Would update" if dry_run else "Updated" + print(f"Notice source: {rel_profile}") + print(f"{action} {len(changed)} file(s).") + for path in changed: + print(path.as_posix()) + + if args.check and changed: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.agents/skills/update-copyright/tests/test_update_copyright.py b/.agents/skills/update-copyright/tests/test_update_copyright.py new file mode 100644 index 0000000..8770b32 --- /dev/null +++ b/.agents/skills/update-copyright/tests/test_update_copyright.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "update_copyright.py" + + +class UpdateCopyrightTest(unittest.TestCase): + def test_default_run_leaves_plain_source_without_header_unchanged(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + self.write_profile(root) + source = root / "Foo.java" + original = "class Foo {}\n" + source.write_text(original, encoding="utf-8") + + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True) + + result = self.run_script(root) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Updated 0 file(s).", result.stdout) + self.assertEqual(result.stderr, "") + self.assertEqual(source.read_text(encoding="utf-8"), original) + + def test_existing_header_is_updated(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + self.write_profile(root) + source = root / "Foo.java" + source.write_text( + "/*\n" + " * Copyright 2024 ACME\n" + " * All rights reserved\n" + " */\n" + "\n" + "class Foo {}\n", + encoding="utf-8", + ) + + result = self.run_script(root, "--year", "2026", "Foo.java") + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Updated 1 file(s).", result.stdout) + self.assertIn("Foo.java", result.stdout) + self.assertEqual(result.stderr, "") + self.assertEqual( + source.read_text(encoding="utf-8"), + "/*\n" + " * Copyright 2026 ACME\n" + " * All rights reserved\n" + " */\n" + "\n" + "class Foo {}\n", + ) + + def test_default_run_skips_tracked_files_deleted_from_working_tree(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + root = Path(temp_dir) + self.write_profile(root) + source = root / "Foo.java" + source.write_text("class Foo {}\n", encoding="utf-8") + + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + subprocess.run(["git", "add", "Foo.java"], cwd=root, check=True) + source.unlink() + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--root", + str(root), + "--dry-run", + ], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("Would update 0 file(s).", result.stdout) + self.assertEqual(result.stderr, "") + + @staticmethod + def run_script(root: Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [ + sys.executable, + str(SCRIPT), + "--root", + str(root), + *args, + ], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + @staticmethod + def write_profile(root: Path) -> None: + copyright_dir = root / ".idea" / "copyright" + copyright_dir.mkdir(parents=True) + (copyright_dir / "profiles_settings.xml").write_text( + '' + '' + "\n", + encoding="utf-8", + ) + (copyright_dir / "Default.xml").write_text( + '' + "" + '" + "\n", + encoding="utf-8", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.agents/skills/version-bumped/SKILL.md b/.agents/skills/version-bumped/SKILL.md new file mode 100644 index 0000000..86ca53d --- /dev/null +++ b/.agents/skills/version-bumped/SKILL.md @@ -0,0 +1,99 @@ +--- +name: version-bumped +description: > + Verify the current branch has bumped `version.gradle.kts` strictly above + the base ref; invoke `/bump-version` to auto-recover if not. Composable: + other modifying skills (`dependency-update`, `bump-gradle`, + `java-to-kotlin`, `move-files`) call this as their final step so a + `./gradlew build` or `publishToMavenLocal` can never overwrite a + previously published Maven Local artifact that integration tests in + consumer repos depend on. +--- + +# Ensure version is bumped + +This skill is the agent-facing wrapper around +`.agents/skills/version-bumped/scripts/version-bumped.sh`. The script is the source of truth for +"has this branch advanced the version vs base?"; this skill just runs it +and, if it fails, invokes `/bump-version` and re-runs to confirm. + +The same logic is enforced as a hook +(`.agents/scripts/publish-version-gate.sh`) that fires before any +`./gradlew … (build|publish|publishToMavenLocal)` invocation, so even +direct gradle calls cannot bypass it. This skill exists for the +cooperative path β€” other skills calling it before they finish, so the +user is never surprised by a blocked gradle command later. + +The premise is simple: any feature branch is a candidate for publishing, +even when the only change is the version bump itself (sometimes the bump +is the entire change, used to retry a publish that failed because Maven +repositories were overloaded). So if the branch differs from base at all, +the version must advance. + +## When to use + +- Automatically: as the final step of any skill that may change files on + the branch. +- Manually (`/version-bumped`): before running `./gradlew build` or + `./gradlew publishToMavenLocal` on a feature branch when you are not + sure whether the version has already been bumped. + +## Procedure + +1. Run the deterministic check: + + ```bash + .agents/skills/version-bumped/scripts/version-bumped.sh + ``` + + Honor `VERSION_BUMPED_BASE` if the user has set a non-default base ref + (e.g. `origin/master`, or a release branch). + +2. Interpret the exit code: + + - **0** β€” Done. Either the repository has no root `version.gradle.kts` + (the version check is `N/A`), the branch has no diff vs base, or the + version is already strictly greater. Report a one-line confirmation + and stop. + - **1** β€” Block. The script's stderr explains which check failed. + Proceed to step 3. + - **2** β€” Configuration error (no merge-base, parse failure on + `version.gradle.kts`). Do **not** invoke `/bump-version` + automatically. Surface the script's stderr to the user and stop. + +3. On exit 1, invoke `/bump-version` to perform the actual bump. That + skill owns the policy (snapshot numbering, the commit subject, the + rebuild, dependency-report regeneration, and the conflict rule). Do + not duplicate its work here. + +4. After `/bump-version` finishes, re-run the deterministic check. If it + now passes, report the new version on the branch. If it still fails, + surface the stderr unchanged and stop β€” do not loop. + +## Why this skill is separate from `/bump-version` + +`/bump-version` is the **action** (it edits `version.gradle.kts`, +commits, rebuilds, may commit reports). `/version-bumped` is the +**guard** (read-only check, optional auto-recovery). Skills that want to +say "make sure the branch has a bumped version" should call +`/version-bumped`, not `/bump-version`, because the guard is a no-op when +the bump is already done β€” calling `/bump-version` unconditionally would +double-bump on every chained skill invocation. + +## Relationship to `checkVersionIncrement` + +The Gradle task `checkVersionIncrement` (in `buildSrc/.../publish/`) +asks a different question: *"is this version already in the remote +Maven metadata?"* It runs on GitHub Actions feature-branch pushes and +fetches the Spine SDK Artifact Registry. The two checks are +complementary β€” neither subsumes the other. + +## See also + +- `.agents/version-policy.md` β€” when the version gate applies. +- `.agents/skills/bump-version/SKILL.md` β€” the bump procedure itself. +- `.agents/skills/pre-pr/SKILL.md` β€” uses the same check at PR time + (step 2). +- `.agents/skills/version-bumped/scripts/version-bumped.sh` β€” the deterministic check. +- `.agents/scripts/publish-version-gate.sh` β€” the hook that enforces the + rule on `./gradlew` invocations. diff --git a/.agents/skills/version-bumped/scripts/version-bumped.sh b/.agents/skills/version-bumped/scripts/version-bumped.sh new file mode 100755 index 0000000..f050a5b --- /dev/null +++ b/.agents/skills/version-bumped/scripts/version-bumped.sh @@ -0,0 +1,276 @@ +#!/usr/bin/env bash +# +# Verifies that a feature branch which differs from the base ref also +# bumps `version.gradle.kts` strictly above the base version. Mirrors the +# universal "every branch advances the version" policy: a branch with any +# changes is a candidate for publishing β€” sometimes the only change is the +# bump itself, used to retry a publish that failed because Maven +# repositories were overloaded. +# +# Exit codes: +# 0 β€” OK: repo has no root `version.gradle.kts`, OR branch has no diff +# vs base, OR working-tree version is strictly greater than base +# version. +# 1 β€” Block: branch differs from base but version is unchanged or +# decreased. Stderr points to `/bump-version`. +# 2 β€” Configuration error (bad base ref, parse failure). Stderr explains. +# +# Inputs (env, all optional): +# VERSION_BUMPED_BASE Base ref to compare against. Default: master, +# then main if master is absent. +# VERSION_BUMPED_KEY Name of the `extra` property holding the +# publishing version (e.g. `versionToPublish`, +# `validationVersion`, `bootstrapVersion`). When +# set, bypasses auto-discovery. Useful for repos +# that don't follow the `version = extra["…"]` +# pattern in `build.gradle.kts`. +# VERSION_BUMPED_QUIET When `1`, suppress the "OK" line on stdout. +# The publish-version-gate hook sets this. +# +# Publishing-key discovery: +# The publishing version's variable name varies across Spine repos +# (`versionToPublish`, `validationVersion`, `compilerVersion`, …). +# `version.gradle.kts` may also declare other `val xxxVersion by extra(...)` entries +# that are *dependency* versions of other Spine modules β€” not this +# project's own publishing version β€” so the key cannot be picked by +# inspecting `version.gradle.kts` alone. +# +# The canonical source is `build.gradle.kts`, which assigns +# `version = extra["KEY"]!!`. This script scans for that pattern, +# picks the unique key, and parses its value from `version.gradle.kts`. +# If `build.gradle.kts` does not contain such a line, the script falls +# back to `versionToPublish`. Set `VERSION_BUMPED_KEY` to override. +# +# Notes: +# * Companion to the Gradle task `checkVersionIncrement` (see +# `buildSrc/.../publish/CheckVersionIncrement.kt`). The Gradle task +# asks "is this version already in remote Maven metadata?" β€” this +# script asks the simpler local question "has this branch advanced +# the version vs base?". The two checks are complementary; neither +# subsumes the other. +# * The working tree is included in the change-detection so the gate +# reflects what `./gradlew build` would actually publish. +# +set -eu + +repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { + echo "version-bumped: not inside a git repository" >&2 + exit 2 +} +cd "$repo_root" + +version_file="version.gradle.kts" + +# --- N/A: not a versioned project ---------------------------------------- +if [ ! -f "$version_file" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: N/A (no root version.gradle.kts)" + exit 0 +fi + +# --- Resolve base ref ---------------------------------------------------- +base="${VERSION_BUMPED_BASE:-}" +if [ -z "$base" ]; then + if git show-ref --verify --quiet refs/heads/master; then + base=master + elif git show-ref --verify --quiet refs/heads/main; then + base=main + else + echo "version-bumped: no master or main branch found; set VERSION_BUMPED_BASE" >&2 + exit 2 + fi +fi + +if ! git rev-parse --verify --quiet "$base" >/dev/null; then + echo "version-bumped: base ref '$base' does not resolve" >&2 + exit 2 +fi + +# When we are on the base branch itself, there is nothing to gate. +current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") +if [ "$current_branch" = "$base" ] || [ "$current_branch" = "${base##*/}" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: on base branch ($current_branch); nothing to gate" + exit 0 +fi + +merge_base=$(git merge-base HEAD "$base" 2>/dev/null) || { + echo "version-bumped: cannot find merge-base of HEAD and '$base'" >&2 + exit 2 +} + +# --- Detect any branch divergence vs base (committed/worktree/untracked) - +committed=$(git diff --name-only "$merge_base"..HEAD 2>/dev/null || true) +worktree=$(git diff --name-only HEAD 2>/dev/null || true) +untracked=$(git ls-files --others --exclude-standard 2>/dev/null || true) + +if [ -z "$committed" ] && [ -z "$worktree" ] && [ -z "$untracked" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: no changes vs $base" + exit 0 +fi + +# --- Discover the publishing-version key --------------------------------- +# Source of truth is `build.gradle.kts` (or `build.gradle`). Two shapes are +# recognised, in order: +# +# a) version = extra["KEY"] +# b) version = IDENTIFIER (with `val IDENTIFIER ... by extra` nearby) +# +# Single or double quotes are accepted in shape (a). If multiple distinct +# keys appear across shapes, the script refuses to guess and asks the user +# to set VERSION_BUMPED_KEY. +# +# Return codes: +# 0 β€” printed a unique key on stdout +# 1 β€” no candidates found (caller should fall back) +# 2 β€” ambiguous; diagnostic already on stderr +discover_key() { + local files keys_a keys_b keys count + files="" + [ -f build.gradle.kts ] && files="build.gradle.kts" + [ -f build.gradle ] && files="$files build.gradle" + [ -z "$files" ] && return 1 + # Shape (a): version = extra["KEY"] + # Anchored to start-of-line (modulo leading whitespace) so that comments + # like `// version = extra["x"]` and identifiers like `fooversion = ...` + # don't produce false matches. + # shellcheck disable=SC2086 + keys_a=$(grep -hE '^[[:space:]]*version[[:space:]]*=[[:space:]]*extra[[:space:]]*\[[[:space:]]*["'"'"'][^"'"'"']+["'"'"']' $files 2>/dev/null \ + | sed -nE 's/.*extra[[:space:]]*\[[[:space:]]*["'"'"']([^"'"'"']+)["'"'"'].*/\1/p') + # Shape (b): version = IDENTIFIER (bare Kotlin identifier, no '[' or '"'). + # Only accept the identifier if the same file also declares + # `val IDENTIFIER[: String]? by extra` β€” otherwise it's a plain local + # variable (common in Groovy `build.gradle`), not an `extra` property we + # can resolve in `version.gradle.kts`. + local candidates_b cand + # shellcheck disable=SC2086 + candidates_b=$(grep -hE '^[[:space:]]*version[[:space:]]*=[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*$' $files 2>/dev/null \ + | sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*$/\1/p') + keys_b="" + for cand in $candidates_b; do + # shellcheck disable=SC2086 + if grep -hE "^[[:space:]]*val[[:space:]]+${cand}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra([^A-Za-z0-9_]|\$)" $files >/dev/null 2>&1; then + keys_b="${keys_b}${cand} +" + fi + done + keys=$(printf '%s\n%s' "$keys_a" "$keys_b" | sed '/^$/d' | sort -u) + [ -z "$keys" ] && return 1 + count=$(printf '%s\n' "$keys" | wc -l | tr -d ' ') + if [ "$count" -gt 1 ]; then + { + echo "version-bumped: ambiguous publishing key in build scripts:" + while IFS= read -r k; do printf ' %s\n' "$k"; done <<< "$keys" + echo " Set VERSION_BUMPED_KEY to disambiguate." + } >&2 + return 2 + fi + printf '%s' "$keys" +} + +key="${VERSION_BUMPED_KEY:-}" +if [ -z "$key" ]; then + set +e + key=$(discover_key) + rc=$? + set -e + if [ "$rc" = "2" ]; then + exit 2 + fi + if [ "$rc" != "0" ] || [ -z "$key" ]; then + key="versionToPublish" + fi +fi + +# --- Parse a `val KEY by extra(...)` from a Gradle file content ---------- +# Handles three shapes (per .agents/skills/bump-version/SKILL.md step 2): +# 1. val KEY[: String]? by extra("X") β€” literal extra +# 2. val SRC[: String]? by extra("X") β€” alias chain via extra +# val KEY[: String]? by extra(SRC) +# 3. val SRC[: String]? = "X" β€” alias chain via plain val +# val KEY[: String]? by extra(SRC) +# The key name is parameterized so that any project-specific name works +# (versionToPublish, validationVersion, bootstrapVersion, botVersion, …). +parse_version() { + local content="$1" name="$2" + local v varName + # Shape 1: literal. + v=$(printf '%s' "$content" \ + | grep -E "val[[:space:]]+${name}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(\"" \ + | head -n1 \ + | sed -nE 's/.*extra\("([^"]+)".*/\1/p') + if [ -n "$v" ]; then + printf '%s' "$v" + return 0 + fi + # Shapes 2 & 3: extract the alias source identifier. + varName=$(printf '%s' "$content" \ + | grep -E "val[[:space:]]+${name}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(" \ + | head -n1 \ + | sed -nE 's/.*extra\(([A-Za-z_][A-Za-z0-9_]*)\).*/\1/p') + if [ -n "$varName" ]; then + # Shape 2: source is `val SRC ... by extra("X")`. + v=$(printf '%s' "$content" \ + | grep -E "val[[:space:]]+${varName}([[:space:]]*:[[:space:]]*String)?[[:space:]]+by[[:space:]]+extra\(\"" \ + | head -n1 \ + | sed -nE 's/.*extra\("([^"]+)".*/\1/p') + if [ -n "$v" ]; then + printf '%s' "$v" + return 0 + fi + # Shape 3: source is `val SRC[: String]? = "X"`. + v=$(printf '%s' "$content" \ + | grep -E "val[[:space:]]+${varName}([[:space:]]*:[[:space:]]*String)?[[:space:]]*=[[:space:]]*\"" \ + | head -n1 \ + | sed -nE 's/.*=[[:space:]]*"([^"]+)".*/\1/p') + if [ -n "$v" ]; then + printf '%s' "$v" + return 0 + fi + fi + return 1 +} + +head_content=$(cat "$version_file" 2>/dev/null || true) +head_version=$(parse_version "$head_content" "$key" || true) +if [ -z "$head_version" ]; then + echo "version-bumped: cannot parse '$key' from working-tree $version_file" >&2 + exit 2 +fi + +# Base content may legitimately not exist (file newly introduced). +base_content=$(git show "$base:$version_file" 2>/dev/null || true) +if [ -z "$base_content" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: $version_file newly introduced at $head_version; treating as bumped" + exit 0 +fi + +base_version=$(parse_version "$base_content" "$key" || true) +if [ -z "$base_version" ]; then + echo "version-bumped: cannot parse '$key' from $base:$version_file" >&2 + exit 2 +fi + +# --- Strict-greater comparison via `sort -V` ----------------------------- +if [ "$head_version" = "$base_version" ]; then + cmp="equal" +elif [ "$(printf '%s\n%s\n' "$base_version" "$head_version" | sort -V | tail -n1)" = "$head_version" ]; then + cmp="greater" +else + cmp="lesser" +fi + +if [ "$cmp" = "greater" ]; then + [ "${VERSION_BUMPED_QUIET:-0}" = "1" ] || echo "version-bumped: OK ($key: $base_version -> $head_version)" + exit 0 +fi + +cat >&2 < + Write, edit, and restructure user-facing and developer-facing documentation. + Use when asked to create/update docs such as `README.md`, `docs/**`, and + other Markdown documentation, including keeping docs navigation data in sync; + when drafting tutorials, guides, troubleshooting pages, or migration notes; and + when improving inline API documentation (KDoc) and examples. +--- + +# Write documentation (repo-specific) + +## Decide the target and audience + +- Identify the target reader: end user, contributor, maintainer, or tooling/automation. +- Identify the task type: new doc, update, restructure, or documentation audit. +- Identify the acceptance criteria: β€œwhat is correct when the reader is done?” + +## Choose where the content should live + +- Prefer updating an existing doc over creating a new one. +- Place content in the most discoverable location: + - `README.md`: project entry point and β€œwhat is this?”. + - `docs/`: longer-form docs (follow existing conventions in that tree). + - Source KDoc: API usage, examples, and semantics that belong with the code. + +## Keep docs navigation in sync + +- When adding, removing, moving, or renaming a page under + `docs/content/docs/

/`, keep the current version's matching + `sidenav.yml` in sync. +- Use `docs/data/versions.yml` to identify the current documentation version for + that section. The current version is the entry with `is_main: true`; its + `version_id` maps to `docs/data/docs/
//sidenav.yml`. +- Do not update historical version entries or their navigation files unless the + user explicitly asks to edit that historical version. +- Map page files to `file_path` values relative to the current version's + `content_path`, without `.md`; `_index.md` maps to its directory path, such as + `01-getting-started/_index.md` -> `01-getting-started`. +- Keep each `page` label aligned with the page frontmatter `title` unless the + existing navigation intentionally uses a shorter reader-facing label. +- Preserve the existing ordering, nesting, keys, comments, and YAML quoting + style. Remove nav entries for deleted pages and update `file_path` values for + moved pages. +- If a docs content change should not appear in navigation, say so explicitly in + the final response. + +## Follow local documentation conventions + +- Follow `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. +- Use fenced code blocks for commands and examples; format file/dir names as code. +- When referencing a documentation page or section in body prose, use typographic + double quotation marks only if the visible reference text is the actual page or + section title, such as the β€œGetting started” page or the β€œTroubleshooting” + section. The title normally starts with a capital letter. Do not add these + quotes around generic or descriptive links such as β€œthis page”, β€œthe next + section”, β€œdeclaring constraints”, or `4.3`, even if they point to a page or + section. Do not add these quotes in β€œWhat’s next” sections or navigation + elements. Keep file paths, identifiers, frontmatter values, navigation labels, + and Markdown link labels in their expected syntax. +- In Markdown files, prefer footnote-style reference links for external `https://` + targets instead of inline links. Write readable body text like + `[label][short-id]`, then place the URL definition near the end of the file, + such as `[short-id]: https://example.com/long/path`. Keep reference IDs short + and descriptive. Inline links are still fine for local relative paths. +- Avoid widows, runts, orphans, and rivers by reflowing paragraphs when needed. + +## Make docs actionable + +- Prefer steps the reader can execute (commands + expected outcome). +- Prefer concrete examples over abstract descriptions. +- Include prerequisites (versions, OS, environment) when they are easy to miss. +- Use consistent terminology (match code identifiers and existing docs). + +## KDoc-specific guidance + +- For public/internal APIs, include at least one example snippet demonstrating common usage. +- When converting from Javadoc/inline comments to KDoc: + - Remove HTML like `

` and preserve meaning. + - Prefer short paragraphs and blank lines over HTML formatting. + +## Validate changes + +- For code changes, follow `.agents/running-builds.md`. +- For documentation-only changes in Kotlin/Java sources, prefer `./gradlew dokka`. diff --git a/.agents/skills/writer/agents/openai.yaml b/.agents/skills/writer/agents/openai.yaml new file mode 100644 index 0000000..44eaa4e --- /dev/null +++ b/.agents/skills/writer/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Writer" + short_description: "Write and update user/developer docs" + default_prompt: "Write or revise documentation in this repository (for example: README.md, docs/**, CONTRIBUTING.md, and API documentation/KDoc). Follow local documentation guidelines in .agents/*.md, keep changes concise and actionable, and include concrete examples and commands where appropriate." + diff --git a/.agents/skills/writer/assets/templates/doc-page.md b/.agents/skills/writer/assets/templates/doc-page.md new file mode 100644 index 0000000..f405b71 --- /dev/null +++ b/.agents/skills/writer/assets/templates/doc-page.md @@ -0,0 +1,23 @@ +# Title + +## Goal + +State what the reader will accomplish. + +## Prerequisites + +- List versions/tools the reader needs. + +## Steps + +1. Do the first thing. +2. Do the next thing. + +## Verify + +Show how the reader can confirm success. + +## Troubleshooting + +- Common failure: likely cause β†’ fix. + diff --git a/.agents/skills/writer/assets/templates/kdoc-example.md b/.agents/skills/writer/assets/templates/kdoc-example.md new file mode 100644 index 0000000..fdbd9b6 --- /dev/null +++ b/.agents/skills/writer/assets/templates/kdoc-example.md @@ -0,0 +1,11 @@ +````kotlin +/** + * Explain what this API does in one sentence. + * + * ## Example + * ```kotlin + * // Show the typical usage pattern. + * val result = doThing() + * ``` + */ +```` diff --git a/.agents/skills/writer/assets/templates/kotlin-java-example.md b/.agents/skills/writer/assets/templates/kotlin-java-example.md new file mode 100644 index 0000000..5517516 --- /dev/null +++ b/.agents/skills/writer/assets/templates/kotlin-java-example.md @@ -0,0 +1,13 @@ +{{< code-tabs langs="Kotlin, Java">}} + +{{< code-tab lang="Kotlin" >}} +```kotlin +``` +{{< /code-tab >}} + +{{< code-tab lang="Java" >}} +```java +``` +{{< /code-tab >}} + +{{< /code-tabs >}} diff --git a/.agents/tasks/README.md b/.agents/tasks/README.md new file mode 100644 index 0000000..325f52c --- /dev/null +++ b/.agents/tasks/README.md @@ -0,0 +1,128 @@ +# Task plans β€” `.agents/tasks/` + +Durable task plans. Checked into git so the whole team β€” and any +agent working in this repo β€” can review, resume, or pick up +sub-tasks across sessions. + +This complements Claude Code's built-in Plan mode and in-session +task list: the file here is the durable source of truth; the +built-in tools gate approval and render live progress. + +## Layout + + .agents/tasks/ + β”œβ”€β”€ README.md # This file + └── .md # One file per task; status in frontmatter + +Filename = the task's kebab-case slug. Multiple active tasks per +branch are allowed β€” use distinct slugs. + +## File format + + --- + slug: add-team-memory + branch: tune-claude + owner: claude # or a human/agent handle + status: in-progress # see status values below + started: 2026-05-19 + related-memories: # optional β€” links into .agents/memory/ + - team-memory-routing + --- + + ## Goal + + + ## Context + + + ## Plan + - [x] Step 1 + - [ ] Step 2 + - notes / sub-bullets + - [ ] Step 3 + + ## Log + - 2026-05-19 14:02 β€” drafted, awaiting approval + - 2026-05-19 14:15 β€” approved, executing + - 2026-05-19 14:42 β€” step 3 blocked on … + +The checklist uses `- [ ]` / `- [x]` so another agent can claim and +complete unchecked items by ticking them and adding a `Log` line. + +### `status` values + +| value | meaning | +|---|---| +| `draft` | written but not yet approved | +| `approved` | approved, not yet started | +| `in-progress` | execution under way | +| `blocked` | paused; reason in `Log` | +| `in-review` | work done, awaiting review | +| `done` | complete β€” file is then deleted (see lifecycle) | + +## Workflow + +1. **Discover** β€” at task start, scan `.agents/tasks/` for + in-progress or blocked plans on the current branch. Resume + rather than restart. +2. **Draft** β€” write `.md` with `status: draft` and the + plan checklist. +3. **Approval gate** β€” `EnterPlanMode` β†’ `ExitPlanMode`. The plan + presented to the human references the file path; the human may + edit the file directly before approving. +4. **Mirror** β€” on approval, flip `status: approved` β†’ `in-progress` + and populate `TaskCreate` from the top-level checklist for live + in-session progress. +5. **Execute + sync** β€” use `TaskUpdate` for fine-grained progress. + Edit the file only at meaningful checkpoints: step done, blocker, + scope change, new note. +6. **Complete** β€” flip `status: done`. The file is raw material for + the PR description. +7. **Delete on merge** β€” once the branch lands on master, delete the + task file in the same commit or shortly after. `git log --follow` + recovers it if ever needed. + +## Cross-agent coordination + +- Other agents (or other CC sessions) `Read` the file to pick up + state. They MUST update `status`, tick checkboxes, and append + `Log` lines rather than rewriting the plan silently. +- If two agents work the same task in parallel, partition by + checkbox β€” each agent claims unchecked items by tagging the line + (e.g. `- [ ] (owner: reviewer-bot) Run dependency-audit`) or by + appending a `Log` line. +- The **file** is the contract. In-session `TaskCreate` state is + per-session and not authoritative. + +## When to create a task file + +Create one whenever the work is non-trivial: + +- Changes spanning multiple files or modules (features, refactors). +- Lengthy documentation work β€” multi-page guides, restructuring + `docs/`, migration notes, tutorials. The task file plans and + tracks the effort; the docs-related skills (`writer`, + `write-docs`, `review-docs`) handle individual page work inside + the plan steps. +- Cross-agent or cross-session work (e.g. one agent drafts, another + reviews). +- Anything that may span sessions and needs durable state. + +Do **not** create a task file for: + +- Trivial changes (single-file edits, typo fixes, version bumps) β€” + pure overhead. +- Deliverables themselves β€” code lives in source, docs in `docs/`, + design records where the project keeps them. Task files describe + the *work*, not the artifact. +- Status reports of work already done β€” that's a `Log` entry on an + existing task, or the PR description. +- Personal reminders / todo lists β€” use the built-in task list. + +## Relationship to other stores + +- **`.agents/memory/`** β€” enduring lessons that survive *across* + tasks. If a task yields a generalizable rule, add the memory and + link from the task's `related-memories`. +- **Built-in auto-memory** β€” personal and ephemeral. Task files do + not carry personal preferences. diff --git a/.agents/tasks/prohibit-automatic-commits.md b/.agents/tasks/prohibit-automatic-commits.md new file mode 100644 index 0000000..ff067c5 --- /dev/null +++ b/.agents/tasks/prohibit-automatic-commits.md @@ -0,0 +1,92 @@ +--- +slug: prohibit-automatic-commits +branch: prohibit-automatic-commits +owner: claude +status: in-review +started: 2026-05-20 +--- + +## Goal + +Make it a durable, team-wide rule that AI agents (Claude Code main thread, +every subagent, every skill) MUST NOT run `git commit` (or other +history-writing git/gh operations) unless authorization is *explicit and +current*. Authorization comes from one of two sources only: + +1. The currently active skill's `SKILL.md` contains an explicit + `## Commit authorization` section. +2. The user's current prompt explicitly instructs the operation + (e.g. "commit this", "push the branch"). + +Agents must otherwise stage changes and stop, letting the user review and +decide. This preserves today's auto-commit behavior for `bump-version` +and `bump-gradle`, which will declare authorization in their SKILL.md. + +## Context + +- Today's pain: Claude Code commits routinely, even when the user wants + to review diffs locally first. +- The project's `.claude/settings.json` already has `Bash(git commit:*)` + in `permissions.ask`. That asks the user per-commit but does not + redirect agent behavior β€” the agent still proposes commits constantly. + The fix is at the *instruction* layer, not the permission layer. +- Skills that legitimately commit today: `bump-version`, `bump-gradle`. +- Skills that do not commit but prescribe commit messages for the human: + `dependency-update` (already says "Do not commit. Do not push."). +- The user accepted removal of the global `~/.claude/settings.json` hook + added earlier this session. Enforcement lives in `.agents/` instructions + only. + +## Plan + +- [x] **1. Add the canonical rule to `.agents/safety-rules.md`.** + Added section *Commits and history-writing*. Lists default (no + history writes), two authorization sources, the fallback behavior + (stage + show diff + stop), and the operations covered. Names the + `## Commit authorization` marker. + +- [x] **2. Surface the rule in `.agents/quick-reference-card.md`.** + Added one-line pointer to `safety-rules.md` β†’ *Commits and + history-writing*. + +- [x] **3. Add a workflow rule to `CLAUDE.md`.** + Added bullet under *Workflow Rules* referencing + `.agents/safety-rules.md`. + +- [x] **4. Declare authorization in `bump-version/SKILL.md`.** + Added a top-level `## Commit authorization` section above the + Checklist: exactly one commit, stage only `version.gradle.kts`, + subject `` Bump version -> `` ``, no push/tag/amend. + +- [x] **5. Declare authorization in `bump-gradle/SKILL.md`.** + Added a top-level `## Commit authorization` section above the + Checklist: up to two commits (wrapper + dependency reports), exact + subjects, no push/tag/amend. + +- [x] **6. Cross-check the non-authorizing skills.** + `dependency-update/SKILL.md` already explicit ("Do not commit. Do + not push.") β€” left as is. `pre-pr/SKILL.md` does not commit β€” left + as is. Other skills scanned (see Log). + +- [x] **7. Verification.** See Log entry β€” all three grep checks pass. + +## Out of scope + +- Project `.claude/settings.json` `ask` rule for `Bash(git commit:*)`: + leave as defense-in-depth (zero cost when the agent obeys the rule). +- `~/.claude/settings.json` global hook: already reverted earlier this + session per user direction. + +## Log + +- 2026-05-20 β€” drafted, awaiting plan approval. +- 2026-05-20 β€” approved by user. Executed steps 1–6. +- 2026-05-20 β€” verification: + - `grep -RIn '^## Commit authorization' .agents/skills/` returns exactly + `bump-gradle/SKILL.md` and `bump-version/SKILL.md` βœ“ + - `safety-rules.md` referenced from `CLAUDE.md`, `quick-reference-card.md`, + `bump-version/SKILL.md`, `bump-gradle/SKILL.md` βœ“ + - Literal `git commit` strings live only in the two authorizing skills βœ“ + - `dependency-update/SKILL.md` still says "Do not commit. Do not push."; + `pre-pr/SKILL.md` still writes a sentinel and does not commit βœ“ +- Status: `in-review` β€” awaiting user sign-off, then delete on merge to master. diff --git a/.agents/tasks/prompt-caching-org.md b/.agents/tasks/prompt-caching-org.md new file mode 100644 index 0000000..71f0c4f --- /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/setup-cross-tool-agent-instructions.md b/.agents/tasks/setup-cross-tool-agent-instructions.md new file mode 100644 index 0000000..02672e2 --- /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/.agents/widow-runt-orphan.jpg b/.agents/widow-runt-orphan.jpg new file mode 100644 index 0000000..284b02a Binary files /dev/null and b/.agents/widow-runt-orphan.jpg differ diff --git a/.claude/agents/review-docs.md b/.claude/agents/review-docs.md new file mode 100644 index 0000000..0481b24 --- /dev/null +++ b/.claude/agents/review-docs.md @@ -0,0 +1,18 @@ +--- +name: review-docs +description: Reviews documentation changes β€” KDoc/Javadoc inside Kotlin/Java sources and Markdown docs (`README.md`, `docs/**`) β€” against Spine documentation conventions. Use proactively when a diff touches doc comments or Markdown, before opening a doc-affecting PR, or when the user asks for a documentation review. Read-only; does not run builds. +tools: Read, Grep, Glob, Bash +model: inherit +--- + +Follow the `review-docs` skill exactly: + +- Skill: `.agents/skills/review-docs/SKILL.md` +- The skill owns the review procedure, the per-area checks (KDoc/Javadoc, + Markdown, prose flow, terminology), and the output format + (Must fix / Should fix / Nits + one-line verdict). +- Scope yourself to documentation only. If you spot a code-quality issue, + surface it briefly as a Nit pointing at the `kotlin-review` agent β€” + do not expand the review. +- Read-only: use `Read`, `Grep`, `Glob`, and `Bash` solely for `git diff` + and related read-only inspection. Do not run builds. diff --git a/.claude/commands/move-files.md b/.claude/commands/move-files.md new file mode 100644 index 0000000..25885f9 --- /dev/null +++ b/.claude/commands/move-files.md @@ -0,0 +1,12 @@ +--- +description: Move or rename files/directories, updating all references and build metadata. +argument-hint: " " +allowed-tools: Read, Edit, Bash(git mv:*), Bash(git status:*), Bash(git ls-files:*), Grep, Glob +--- + +Follow the `move-files` skill exactly: + +- Skill: `.agents/skills/move-files/SKILL.md` +- Operation: $ARGUMENTS +- Preflight (run `git status --short`, classify scope) -> Search for all old identifiers -> Move with `git mv` -> Repair references (imports, build metadata, docs) -> Verify. +- Report: Moved[], UpdatedRefs[], Verification[], Risks[]. diff --git a/.claude/commands/pre-pr.md b/.claude/commands/pre-pr.md new file mode 100644 index 0000000..24499cc --- /dev/null +++ b/.claude/commands/pre-pr.md @@ -0,0 +1,32 @@ +--- +description: Run the applicable pre-PR checklist (version gate, build/check, reviewers) and write a sentinel so `gh pr create` is unblocked. +argument-hint: "[base-ref]" +allowed-tools: Read, Write, Grep, Glob, Agent, Bash +--- + +Follow the `pre-pr` skill exactly: + +- Skill: `.agents/skills/pre-pr/SKILL.md` +- Base ref: $ARGUMENTS (treat empty as `master`). +- Detect whether the repository-root `version.gradle.kts` exists. If it is + absent at both the base ref and `HEAD`, the version check is `N/A`; do not + create the file and do not ask for `/bump-version`. +- Run the build/check command selected by the skill and + `.agents/running-builds.md`. The command may be Gradle or non-Gradle. +- Dispatch the reviewers as Claude subagents in parallel β€” send a single + message with multiple Agent tool uses: + - `kotlin-review` when `.kt|.kts|.java` files changed. + - `review-docs` when `.md` files or KDoc inside sources changed. + - `dependency-audit` when any file under + `buildSrc/src/main/kotlin/io/spine/dependency/` changed. +- Pass the version-check status to reviewers. If it is `N/A`, tell them: + "This repository has no root `version.gradle.kts`; a version bump is not + applicable and must not be reported as missing." +- Each reviewer is read-only; do not pass it edit tools. +- On any reviewer returning `REQUEST CHANGES`, treat the overall result + as `FAIL` and stop before writing the sentinel as `PASS`. +- Sentinel location: `$(git rev-parse --show-toplevel)/.git/pre-pr.ok`, + format per the skill (`head=`, `branch=`, `status=`, `timestamp=`, + `build=`, `reviewers=`, `version=`). Use `git rev-parse HEAD` for the + SHA and `date -u +%Y-%m-%dT%H:%M:%SZ` for the timestamp. +- Do NOT run `gh pr create`. That is the user's next step. diff --git a/.claude/commands/review-docs.md b/.claude/commands/review-docs.md new file mode 100644 index 0000000..f8043f0 --- /dev/null +++ b/.claude/commands/review-docs.md @@ -0,0 +1,21 @@ +--- +description: Review documentation changes (KDoc/Javadoc and Markdown) against Spine documentation conventions. +argument-hint: "[base-ref | --staged | paths...]" +allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git ls-files:*) +--- + +Follow the `review-docs` skill exactly: + +- Skill: `.agents/skills/review-docs/SKILL.md` +- Scope / flags: $ARGUMENTS + - Empty: review the current branch's diff against `master` (`git diff master...HEAD`). + - `--staged`: review staged changes only (`git diff --staged`). + - A base ref (e.g. `master`, `origin/master`, a commit SHA): review `git diff ...HEAD`. + - Explicit paths: limit the review to those paths in addition to the diff scope. +- The skill owns the procedure, the per-area checks (KDoc/Javadoc, Markdown, + prose flow, terminology), and the output format (Must fix / Should fix / + Nits + one-line verdict). +- Stay in scope: documentation only. If a code-quality issue surfaces, + note it briefly as a Nit pointing at `/review` (or the `kotlin-review` + agent) β€” do not expand the review. +- Read-only: do not edit files, do not run builds. diff --git a/.claude/commands/update-copyright.md b/.claude/commands/update-copyright.md new file mode 100644 index 0000000..076fb61 --- /dev/null +++ b/.claude/commands/update-copyright.md @@ -0,0 +1,12 @@ +--- +description: Refresh copyright headers from the IntelliJ profile, replacing today.year with the current year. +argument-hint: "[paths...]" +allowed-tools: Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*), Read +--- + +Follow the `update-copyright` skill exactly: + +- Skill: `.agents/skills/update-copyright/SKILL.md` +- Run: `python3 .agents/skills/update-copyright/scripts/update_copyright.py $ARGUMENTS` +- If $ARGUMENTS is empty, run once with `--dry-run`, show the output to the user, then run without `--dry-run`. +- Never add a header to a file that doesn't already have one. diff --git a/.claude/commands/write-docs.md b/.claude/commands/write-docs.md new file mode 100644 index 0000000..b9b9a74 --- /dev/null +++ b/.claude/commands/write-docs.md @@ -0,0 +1,14 @@ +--- +description: Write or update Markdown / KDoc documentation per Spine documentation conventions. +argument-hint: "" +allowed-tools: Read, Edit, Write, Grep, Glob +--- + +Follow the `writer` skill exactly: + +- Skill: `.agents/skills/writer/SKILL.md` +- Topic / target: $ARGUMENTS +- Decide audience first (end user, contributor, maintainer, tooling). +- Prefer updating an existing doc over creating a new one. +- Keep `docs/data/docs/

//sidenav.yml` in sync when adding, removing, moving, or renaming pages under `docs/content/docs/
/`. +- Honor `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4fdcab8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(git switch:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git restore:*)", + "Bash(git stash:*)", + "Bash(git fetch:*)", + "Bash(git rev-parse:*)", + "Bash(git ls-files:*)", + "Bash(git mv:*)", + "Bash(git submodule status:*)", + "Bash(hugo:*)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(find:*)", + "Bash(rg:*)", + "Bash(grep:*)", + "Bash(mkdir:*)", + "Bash(touch:*)", + "Bash(python3 .agents/skills/update-copyright/scripts/update_copyright.py:*)", + "Bash(./config/pull)", + "Bash(./config/migrate)" + ], + "deny": [ + "Bash(git push:*)", + "Bash(git reset --hard:*)", + "Bash(git clean -fdx:*)", + "Bash(rm -rf /:*)", + "Bash(rm -rf ~:*)", + "Bash(gh pr merge:*)", + "Bash(gh release create:*)" + ], + "ask": [ + "Bash(git commit:*)", + "Bash(git rebase:*)", + "Bash(git merge:*)", + "Bash(git cherry-pick:*)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/sanitize-source-code.sh" + }, + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.agents/scripts/update-copyright.sh" + } + ] + } + ] + } +} diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..aa71b2f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,66 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. + +# Common formats +*.html text +*.xml text +*.css text +*.scss text +*.svg text +*.js text +*.properties text +*.rtf text +*.yaml text +*.yml text +*.md text + +LICENSE text + +# SQL scripts +*.sql text + +# Java sources +*.java text + +# Kotlin sources +*.kt text +*.kts text + +# Python sources +*.py text + +# Gradle build files +*.gradle text + +# Google protocol buffers +*.proto text + +# Miscellaneous +*.rb text + +# Declare files that will always have CRLF line endings on checkout. +*.bat text eol=crlf + +# Declare files that will always have LF line endings on checkout. +*.sh text eol=lf +gradlew text eol=lf +pull text eol=lf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary +*.swf binary +*.jar binary +*.desc binary + +*.scpt binary +*.scssc binary + +# Encrypted files +*.enc binary +*.gpg binary +*.weis binary diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..81c8d50 --- /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 new file mode 100644 index 0000000..7a51f8f --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,205 @@ +name: Check Links + +# Trigger only when the docs site, the link checker config, or this workflow +# itself changes β€” unrelated PRs do not need to pay the build+check cost. +on: + pull_request: + paths: + - 'docs/**' + - 'site/**' + - 'lychee.toml' + - '.github/workflows/check-links.yml' + workflow_dispatch: + +env: + HUGO_VERSION: 0.161.1 + LYCHEE_RELEASE: "lychee-x86_64-unknown-linux-gnu.tar.gz" + LYCHEE_VERSION_TAG: "lychee-v0.24.2" + # SHA256 of the above tarball, pinned at download time. Update alongside + # LYCHEE_VERSION_TAG whenever the binary is upgraded. + LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" + # Force Hugo to write its module cache where the cache step actually + # restores from. Hugo's default on Linux is `~/.cache/hugo_cache` + # (or `$TMPDIR/hugo_cache_$USER`), neither of which matches the + # `path: /tmp/hugo_cache` cache step below β€” without this env var, + # the cache would silently never hit. + HUGO_CACHEDIR: /tmp/hugo_cache + +jobs: + check-links: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Detect the Hugo site root (`docs/` or `site/`) by looking for a Hugo + # config file. Outputs `present=true|false` and `site_dir=docs|site`. + # 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; do + if [ -f "$dir/$cfg" ]; then + echo "site_dir=$dir" >> "$GITHUB_OUTPUT" + if [ -f "$dir/_preview/package-lock.json" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo site found under $dir/" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::Hugo config found in $dir/ but $dir/_preview/package-lock.json is missing β€” 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 }} + extended: true + + # `actions/setup-node@v4` ships with built-in npm caching that hashes + # the lockfile and restores `~/.npm`. We use that instead of a + # 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: ${{ steps.docs.outputs.site_dir }}/_preview/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 + # 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', 'site/**/go.sum') }} + restore-keys: | + ${{ runner.os }}-hugomod- + + - name: Install Dependencies + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.site_dir }}/_preview + run: npm ci + + - name: Build docs preview site + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.site_dir }}/_preview + run: hugo -e development + + # Cache Lychee results to avoid hitting rate limits. + # Key on the lychee.toml hash so that exclude-list edits (e.g. removing + # an exclude pattern) invalidate the cache deterministically; otherwise + # 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 + key: cache-lychee-${{ runner.os }}-${{ hashFiles('lychee.toml') }} + restore-keys: | + cache-lychee-${{ runner.os }}- + + # The cache key includes LYCHEE_VERSION_TAG so a version bump + # automatically pulls a fresh binary instead of reusing the old one. + # The restore-keys fallback lets a release-filename tweak (rare) reuse + # 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: + path: lychee + key: ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}-${{ env.LYCHEE_RELEASE }} + restore-keys: | + ${{ runner.os }}-${{ env.LYCHEE_VERSION_TAG }}- + + # We use Lychee directly instead of a GitHub Action because it + # must have access to the local Hugo server, which is not visible + # from the Docker-based action. + # + # `if:` gating uses `hashFiles('lychee/lychee')` rather than + # `steps.cache-lychee.outputs.cache-hit != 'true'`. Per `actions/cache` + # docs, `cache-hit` is only `'true'` on an EXACT key match β€” a restore + # via `restore-keys` reports `cache-hit == 'false'`, even though the + # binary is present in the workspace. Re-downloading in that case + # would defeat the point of the fallback. `hashFiles` returns an empty + # string when the file is absent, so this guard runs the download iff + # neither the exact key nor any restore-key restored the binary. + - name: Download Lychee executable + uses: robinraju/release-downloader@v1.7 + 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: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + run: | + echo "${{ env.LYCHEE_SHA256 }} ${{ env.LYCHEE_RELEASE }}" | sha256sum --check --strict + + # The v0.24.2 tarball contains a top-level directory + # (e.g. `lychee-x86_64-unknown-linux-gnu/lychee`), so `--strip-components=1` + # 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: steps.docs.outputs.present == 'true' && hashFiles('lychee/lychee') == '' + run: | + mkdir -p lychee && + tar -xzf ${{ env.LYCHEE_RELEASE }} --strip-components=1 -C lychee + + # 1. In the generated HTML, some inner links will have absolute URLs and + # the link checker will attempt to fetch them. That's why we need + # a server. Sadly, link checkers have no settings to address this. + # 2. Output redirection is necessary for nohup in GitHub Actions. + # 3. Sleep + `curl` readiness check make sure the server is actually + # serving HTTP before the next step runs Lychee. Without the curl + # probe a silent startup failure (port already bound, missing + # Hugo module, build error surfacing after `nohup` returns 0) + # would manifest 60 s later as "every URL unreachable" Lychee + # errors instead of pointing at the real cause. Mirrors the + # `pgrep -F` guard in the companion `check-links` skill. + # 4. `--port 1313` is set explicitly (not relying on Hugo's default) so + # the coupling with `--base-url http://localhost:1313/` in the next + # Lychee step is visible β€” change one, change the other. + - name: Start Hugo server + if: steps.docs.outputs.present == 'true' + working-directory: ${{ steps.docs.outputs.site_dir }}/_preview + run: | + nohup hugo server \ + --environment development \ + --port 1313 \ + > nohup.out 2> nohup.err < /dev/null & + sleep 5 + if ! curl -sf http://localhost:1313/ > /dev/null; then + echo "ERROR: Hugo server did not respond on port 1313." >&2 + echo "--- stdout ---" >&2; cat nohup.out >&2 || true + echo "--- stderr ---" >&2; cat nohup.err >&2 || true + exit 1 + fi + + - name: Check links + if: steps.docs.outputs.present == 'true' + run: | + ./lychee/lychee --config lychee.toml --timeout 60 \ + --base-url http://localhost:1313/ \ + '${{ steps.docs.outputs.site_dir }}/_preview/public/**/*.html' diff --git a/.gitignore b/.gitignore index 3d7b70f..472e7ab 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ _code/build # Gradle build files build/ generated/ + +# The `check-links` cache directory and Lychee cache. +/.agents/skills/check-links/.cache/ +/.lycheecache diff --git a/.gitmodules b/.gitmodules index 2a7d81a..e7f6727 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "docs/_code/examples/todo-list"] path = docs/_code/examples/todo-list url = https://github.com/spine-examples/todo-list.git +[submodule "config"] + path = config + url = https://github.com/SpineEventEngine/config diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..f60c273 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,104 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index a55e7a1..6e6eec1 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ + \ No newline at end of file diff --git a/.idea/copyright/TeamDev_Open_Source.xml b/.idea/copyright/TeamDev_Open_Source.xml index 12ca8fa..cea7fed 100644 --- a/.idea/copyright/TeamDev_Open_Source.xml +++ b/.idea/copyright/TeamDev_Open_Source.xml @@ -1,6 +1,6 @@ - - \ No newline at end of file + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml index 7e1663a..0b8f9a1 100644 --- a/.idea/copyright/profiles_settings.xml +++ b/.idea/copyright/profiles_settings.xml @@ -1,7 +1,7 @@ - + - \ No newline at end of file + diff --git a/.idea/dictionaries/common.xml b/.idea/dictionaries/common.xml new file mode 100644 index 0000000..d1c3a7b --- /dev/null +++ b/.idea/dictionaries/common.xml @@ -0,0 +1,71 @@ + + + + afghani + arraybuffer + aspx + bytebuffer + callees + closeables + cqrs + dartdocs + dataset + datastore + datastores + deserialized + dirham + enrichable + enrichments + escaper + flushables + googleapis + gradle + grpc + handshaker + hohpe + idempotency + jspecify + kotest + lempira + liskov + melnik + memoized + memoizes + memoizing + mergeable + mikhaylov + millisecs + multitenancy + multitenant + nullable + onclose + oneof + onmessage + onopen + parameterizing + plugable + processmanager + procman + proto's + protodata + protos + sfixed + stderr + stringifier + stringifiers + substituter + switchman + testutil + threeten + tuples + unicast + unregister + unregistering + unregisters + unregistration + websocket + workflows + yevsyukov + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index df7825d..7be402d 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,915 @@ \ No newline at end of file diff --git a/.idea/live-templates/README.md b/.idea/live-templates/README.md new file mode 100644 index 0000000..66713b3 --- /dev/null +++ b/.idea/live-templates/README.md @@ -0,0 +1,27 @@ +### Live Templates + +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 + +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 + [settings folder][settings_folder]. +2. Restart IntelliJ IDEA: `File -> Invalidate Caches -> Just restart`. +3. Go to `Preferences -> Editor -> Live Templates`. +4. Verify `User` and `Spine` template groups are present. + +[settings_folder]: https://www.jetbrains.com/help/idea/directories-used-by-the-ide-to-store-settings-caches-plugins-and-logs.html#config-directory + +### Configuring `User.todo` template + +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"`. +4. Verify that the template generates expected comments: `// TODO:2022-11-03:jack.sparrow: <...>`. diff --git a/.idea/live-templates/Spine.xml b/.idea/live-templates/Spine.xml new file mode 100644 index 0000000..369b72d --- /dev/null +++ b/.idea/live-templates/Spine.xml @@ -0,0 +1,58 @@ + + + + + + + + + + diff --git a/.idea/live-templates/User.xml b/.idea/live-templates/User.xml new file mode 100644 index 0000000..cc15650 --- /dev/null +++ b/.idea/live-templates/User.xml @@ -0,0 +1,11 @@ + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 039e6a7..ad582f4 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,44 @@ - - \ No newline at end of file diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 0000000..5160f49 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,21 @@ +# Guidelines for Junie and AI Agent from JetBrains + +Read the `../.agents/_TOC.md` file to understand: + - the agent responsibilities, + - project overview, + - coding guidelines, + - other relevant topics. + +Also follow the Junie-specific rules described below. + +## Junie Assistance Tips + +When working with Junie AI on the Spine family of projects: + +1. **Project Navigation**: Use `search_project` to find relevant files and code segments. +2. **Code Understanding**: Request file structure with `get_file_structure` before editing. +3. **Code Editing**: Make minimal changes with `search_replace` to maintain project consistency. +4. **Testing**: Verify changes with `run_test` on relevant test files. +5. **Documentation**: Follow KDoc style for documentation. +6. **Kotlin Idioms**: Prefer Kotlin-style solutions over Java-style approaches. +7. **Version Updates**: Remember to update `version.gradle.kts` for PRs. diff --git a/.junie/skills b/.junie/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.junie/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1268e51 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,79 @@ +# πŸ‘‹ Welcome, Agents! + +## Orientation + +If `.agents/project.md` exists in this repository, read it first β€” it describes +the language, architecture, and role of this specific repo within the Spine SDK +organisation. To create one, copy `.agents/project.template.md` (or the +relevant language template) and fill it in. If `project.md` links to a shared +requirements file (e.g. `jvm-project.md`), read that too. + +- Start every session by reading `.agents/quick-reference-card.md` (if present). +- For specific tasks (code review, PR prep, dependency updates, docs, etc.), + prefer the matching skill from `.agents/skills/`. +- Full standards reference: `.agents/_TOC.md` (if present) β€” consult when a + skill doesn't cover the needed context. + +## Commit and history safety + +**Do not commit, push, tag, rebase, merge, cherry-pick, or otherwise write to git history** +unless one of the following is true *right now*: + +1. The currently active skill's `SKILL.md` has a `## Commit authorization` section + that explicitly permits the operation. +2. The user's *current* prompt explicitly requests the operation. + +Authorization does not carry over between turns or sessions. When in doubt: stage +changes, show the diff, and stop β€” let the user commit. + +See [`.agents/safety-rules.md`](.agents/safety-rules.md) β†’ *Commits and history-writing*. + +## Other safety rules + +- All code must compile and pass static analysis. +- Do not auto-update external dependencies outside a dedicated update task. +- No analytics, telemetry, or tracking code. +- No reflection or unsafe code without explicit approval. + +See [`.agents/safety-rules.md`](.agents/safety-rules.md) for the full list. + +## Moving files + +When moving or renaming tracked files, always use `git mv`. Do not simulate a +move by deleting the old file and creating a new one β€” preserve Git history +unless the user explicitly asks for a fresh replacement. + +If `git mv` fails due to permissions or sandbox restrictions, request approval; +do not fall back to delete/create. + +## Memory + +Team-shared memory lives in `.agents/memory/` (checked into git). Use it for +feedback rules, durable project rationale, and external system pointers. +See `.agents/memory/README.md` for layout and write protocol. + +Review `.agents/memory/MEMORY.md` at the start of every session. +Ruthlessly iterate until mistakes stop repeating. + +## Verification & Quality + +- Never mark a task done without proof (tests, logs, diff vs main). +- Ask: "Would a senior/staff engineer approve this?" +- For non-trivial changes: pause and consider a more elegant solution. +- Fix bugs autonomously β€” find root cause, no hand-holding, no band-aids. + +## Core Principles + +- Simplicity first: minimal code impact, minimal surface area. +- No laziness: always find root causes. +- Minimal side effects: avoid new bugs. +- Prefer early returns and clear naming. +- Challenge your own work before presenting it. + +## Task planning + +- Write plans to `.agents/tasks/.md` before coding. + See `.agents/tasks/README.md` for format and lifecycle. +- Verify changes before marking a task done. +- Update memory if lessons emerged. +- Delete the task file on merge to master. diff --git a/CLAUDE.md b/CLAUDE.md index ad8bf44..2ddd0b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,74 +1,16 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What this repo is - -A Hugo-based documentation source for [spine.io](https://spine.io). The `docs/` directory is consumed as a Hugo module by the `SpineEventEngine/SpineEventEngine.github.io` site. The `docs/_preview/` directory exists only to render the site locally β€” it is not shipped. - -## Prerequisites - -JDK 8 (x86_64), Go 1.22+ (per `docs/go.mod`), Node.js 18+, Hugo Extended `v0.150.0`+. The `embedCode` and `checkSamples` Gradle tasks hardcode `embed-code-macos` (ARM) from `docs/_bin/`; on an x86_64 Mac, invoke `embed-code-macos-x86_64` directly (see bypass snippet below) β€” the Gradle wrappers won't work. CI uses `embed-code-ubuntu` (the `check-samples` script branches on `$GITHUB_ACTIONS`). - -## Common commands - -Run from repo root: - -```shell -./gradlew :runSite # install deps + hugo server (local preview) -./gradlew :buildSite # install deps + hugo build (no server) -./gradlew :embedCode # update git submodules + embed code snippets into markdown -./gradlew :checkSamples # verify embedded snippets match source (CI uses this) -./gradlew :buildAll # build all included example projects via composite build -``` - -Bypassing Gradle (you must run the prerequisite steps yourself, or you'll skip what the Gradle tasks do): - -```shell -# equivalent to :runSite β€” installDependencies runs npm install via docs/_script/install-dependencies -./docs/_script/install-dependencies && cd docs/_preview && hugo server - -# equivalent to :embedCode β€” the script updates submodules before invoking the binary. -# Use ./embed-code-macos on ARM Macs, ./embed-code-macos-x86_64 on Intel Macs. -git submodule update --remote --merge --recursive -cd docs/_bin && ./embed-code-macos \ - -config-path="../_settings/v1.embed-code.yml" -mode="embed" -``` - -Each Gradle task is a thin wrapper around a script in `docs/_script/`. When debugging a task, read that script first. - -## Architecture - -### Two-directory split - -- `docs/` β€” content shipped as a Hugo module. Has its own `go.mod` (`github.com/SpineEventEngine/documentation/docs`) and `hugo.toml`. Everything outside `docs/` is build tooling. -- `docs/_preview/` β€” local preview rig. Its `hugo.toml` imports `../..` (the repo root) plus `github.com/SpineEventEngine/site-commons`. Edit `docs/_preview/hugo.toml` to enable/disable other doc modules (validation, compiler, framework) when previewing aggregation locally. - -### Theme and module aggregation - -Layouts/components come from the `site-commons` Hugo theme (pulled via Hugo Modules, not git submodules). The site is also a **documentation aggregator**: it imports docs from sibling repos (`SpineEventEngine/validation/docs`, `compiler/docs`, etc.) and renders them under a unified sidenav. `params.moduleOrder` in `hugo.toml` controls sidenav order; `disable = true` on a module import excludes it from the build. - -To pull theme/module updates: `cd docs/_preview && hugo mod clean && hugo mod get -u github.com/SpineEventEngine/site-commons` (or `./...` for all). Commit the resulting `go.mod`/`go.sum` changes β€” keep `go.sum` minimal (two entries per theme). - -### Code embedding (`docs/_code/`) - -Snippets in markdown pages are inserted by the [`embed-code-go`](https://github.com/SpineEventEngine/embed-code-go) tool, not written inline. - -- `docs/_code/examples/` β€” full example projects, each a **git submodule** of a `spine-examples/*` repo (airport, blog, hello, kanban, todo-list). The composite build in `settings.gradle.kts` includes `airport`, `hello`, and the local `samples` build. -- `docs/_code/samples/` β€” a local Gradle subproject (included as a composite build via `includeBuild("./docs/_code/samples")`) whose Java/Proto sources under `src/main/` are embedded as snippets into pages. When adding new snippets here, you may also need to update `samples/build.gradle` and its build will run as part of `:buildAll`. -- `docs/_settings/v1.embed-code.yml` β€” embed-code config (paths into `docs/_code` and `docs/content/docs/1`). - -Workflow: update snippet at source β†’ `./gradlew :embedCode` β†’ review the `.md`/`.html` diff β†’ commit. After adding a new submodule under `docs/_code/examples/`, also register it in `settings.gradle.kts` (`includeBuild(...)`) and the project must expose a top-level `buildAll` Gradle task. - -### Content versioning - -Versions live side-by-side under `docs/content/docs//`, configured in `docs/data/versions.yml`. The `is_main` version may live at the root (`docs/content/docs/`) instead of in a `/` subdir β€” switching the main version requires moving directories *and* updating `content_path`/`route_url` in `versions.yml`. Each version also needs a sidenav at `docs/data/docs//sidenav.yml`. Full procedure: see `SPINE_RELEASE.md`. - -### Link conventions in markdown - -- Internal links must **not** start with `/` and **must** end with `/` (avoids redirects). The versioning system rewrites them, e.g. `docs/guides/requirements/` β†’ `/docs/1.9.0/guides/requirements/` for main, `/docs/2/guides/...` for version 2. -- For the version label inside a URL, use `{{% version %}}` (current page's version) or `{{% version "1" %}}` (specific). For repo URLs, use `{{% get-site-data "repositories." %}}` β€” pulls from `site-commons`' `data/repositories.yml`. - -## Authoring guide - -The user-facing content authoring guide lives in the `SpineEventEngine.github.io` repo (`AUTHORING.md`), not here. +@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`. +- In JVM repos: 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). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0a1b5f2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +developers@spine.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2185ef6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +How to contribute +================== +Thank you for wanting to contribute to Spine. The following links will help you get started: + * [Wiki home][wiki-home] β€” the home of the framework developer's documentation. + * [Getting started with Spine in Java][quick-start] β€” this guide will walk you through + a minimal client-server β€œHello World!” application in Java. + * [Introduction][docs-intro] β€” this section of the Spine Documentation will help you understand + the foundation of the framework. + +Pull requests +------------- +The work on an improvement starts with creating an issue that describes a bug or a feature. The issue will be used for communications on the proposed improvements. +If code changes are going to be introduced, the issue should also have a link to the corresponding Pull Request. + +Code contributions should: + * Be accompanied by tests. + * Be licensed under the Apache v2.0 license with the appropriate copyright header for each file. + * Formatted according to the code style. See [Wiki home][wiki-home] for the links to + style guides of the programming languages used in the framework. + +Contributor License Agreement +----------------------------- +Contributions to the code of Spine Event Engine framework and its libraries must be accompanied by +Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you own + the intellectual property, then you'll need to sign an individual CLA. + + * If you work for a company which wants you to contribute your work, + then an authorized person from your company will need to sign a corporate CLA. + +Please [contact us][legal-email] for arranging the paper formalities. + +[wiki-home]: https://github.com/SpineEventEngine/SpineEventEngine.github.io/wiki +[quick-start]: https://spine.io/docs/quick-start +[docs-intro]: https://spine.io/docs/introduction +[legal-email]: mailto:legal@teamdev.com diff --git a/config b/config new file mode 160000 index 0000000..56b5c90 --- /dev/null +++ b/config @@ -0,0 +1 @@ +Subproject commit 56b5c9070ad0efcadc3a96256e4c4937b0528e4e diff --git a/docs/_preview/go.mod b/docs/_preview/go.mod index 77bcc49..428834c 100644 --- a/docs/_preview/go.mod +++ b/docs/_preview/go.mod @@ -4,7 +4,7 @@ go 1.22.0 require ( github.com/SpineEventEngine/site-commons v0.0.0-20260522171914-2a606d89558f // indirect - github.com/SpineEventEngine/validation/docs v0.0.0-20260522175555-cf09b4c706ea // indirect + github.com/SpineEventEngine/validation/docs v0.0.0-20260523180634-dd65da5abb65 // indirect github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20400 // indirect github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000 // indirect github.com/twbs/bootstrap v5.3.8+incompatible // indirect diff --git a/docs/_preview/go.sum b/docs/_preview/go.sum index cdf50f2..ee2e4e7 100644 --- a/docs/_preview/go.sum +++ b/docs/_preview/go.sum @@ -4,6 +4,8 @@ github.com/SpineEventEngine/validation/docs v0.0.0-20260205202311-181ba8844107 h github.com/SpineEventEngine/validation/docs v0.0.0-20260205202311-181ba8844107/go.mod h1:STHyjXejVvPmfrxujfDvhofmjg55mMk+fwI3TVL0b4Y= github.com/SpineEventEngine/validation/docs v0.0.0-20260522175555-cf09b4c706ea h1:YEMSlr5KJXkZdK7epMSnN+6HSr2K41n8O3c8OhtF8pM= github.com/SpineEventEngine/validation/docs v0.0.0-20260522175555-cf09b4c706ea/go.mod h1:4RnP1hlrfBI7ZlTsJkllaOEyluhzmz4mOBrRgbc/tts= +github.com/SpineEventEngine/validation/docs v0.0.0-20260523180634-dd65da5abb65 h1:gLlFsu7ZznIurecioKUHTP+sKD4EapeKFhtEqL/JvdU= +github.com/SpineEventEngine/validation/docs v0.0.0-20260523180634-dd65da5abb65/go.mod h1:4RnP1hlrfBI7ZlTsJkllaOEyluhzmz4mOBrRgbc/tts= github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20400 h1:L6+F22i76xmeWWwrtijAhUbf3BiRLmpO5j34bgl1ggU= github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20400/go.mod h1:uekq1D4ebeXgduLj8VIZy8TgfTjrLdSl6nPtVczso78= github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000 h1:GZxx4Hc+yb0/t3/rau1j8XlAxLE4CyXns2fqQbyqWfs= diff --git a/docs/go.mod b/docs/go.mod index fb6fad4..8d98efb 100644 --- a/docs/go.mod +++ b/docs/go.mod @@ -1,3 +1,5 @@ module github.com/SpineEventEngine/documentation/docs go 1.22.0 + +require github.com/SpineEventEngine/validation/docs v0.0.0-20260523180634-dd65da5abb65 // indirect diff --git a/docs/go.sum b/docs/go.sum new file mode 100644 index 0000000..455c4b7 --- /dev/null +++ b/docs/go.sum @@ -0,0 +1,2 @@ +github.com/SpineEventEngine/validation/docs v0.0.0-20260523180634-dd65da5abb65 h1:gLlFsu7ZznIurecioKUHTP+sKD4EapeKFhtEqL/JvdU= +github.com/SpineEventEngine/validation/docs v0.0.0-20260523180634-dd65da5abb65/go.mod h1:4RnP1hlrfBI7ZlTsJkllaOEyluhzmz4mOBrRgbc/tts= diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..6b7d309 --- /dev/null +++ b/lychee.toml @@ -0,0 +1,76 @@ +# Lychee configuration for the `check-links` skill and the +# `Check Links` GitHub workflow. +# +# Mirrors the configuration used by the sibling +# `SpineEventEngine/SpineEventEngine.github.io` repository, with the same +# exclude list of flaky external endpoints. + +# Exclude URLs and mail addresses from checking (supports regex). +# +# The entries are interpreted as Rust regexes, NOT shell globs: +# - `.` is escaped (`\.`) because an unescaped dot matches any character +# and would silently over-match (e.g. `fonts.googleapis.com` would +# also match `fontsXgoogleapisYcomZ`, masking real broken links). +# - `/.*` replaces the shell-style `/*` so a trailing path of any length +# matches (zero-or-more of any character, not zero-or-more slashes). +# TOML literal strings (single-quoted) are used so backslashes stay +# literal β€” basic strings (double quotes) would treat `\.` as an unknown +# escape and either error out or strip the backslash. +exclude = [ + # Links that return errors during checks, but work for the user. + 'fonts\.googleapis\.com/.*', + 'fonts\.gstatic\.com/.*', + 'chromium\.googlesource\.com/.*', + 'chromereleases\.googleblog\.com/.*', + 'clients4\.google\.com/.*', + 'ssl\.gstatic\.com/.*', + 'googletagmanager\.com/.*', + 'x\.com/.*', + + 'stackoverflow\.com/questions/.*', + 'openjdk\.org/.*', + 'npmjs\.com/.*', + 'medium\.com/.*', + 'levelup\.gitconnected\.com/.*', +] +# Deliberately NOT excluded: `raw.githubusercontent.com/SpineEventEngine/*`. +# Catching broken refs into our own raw GitHub content is part of the reason +# this skill exists. If rate-limit flake appears, prefer narrowing the +# pattern to the specific failing path rather than re-adding the broad rule. +# Existing `max_retries`/`retry_wait_time` below should absorb transient 429s. + +# Exclude these filesystem paths from getting checked. +exclude_path = [] + +# Do NOT check links inside `` and `
` blocks or
+# Markdown code blocks (verbatim/code sections are excluded).
+include_verbatim = false
+
+# Verbose program output
+verbose = "error"
+
+# Don't show the interactive progress bar while checking links. Both
+# consumers (CI and the `check-links` skill) run non-interactively, so
+# the progress bar adds noise to logs without value.
+no_progress = true
+
+# Comma-separated list of accepted status codes for valid links.
+# `429` (Too Many Requests) is accepted as a tradeoff against CI flake: some
+# providers rate-limit unauthenticated link probes from CI runners even when
+# the URL is healthy. Combined with `max_retries`/`retry_wait_time` below,
+# this avoids spurious failures on healthy-but-rate-limited URLs. The
+# downside: a URL that is genuinely broken AND returns `429` (rare) will
+# pass. Revisit this if false negatives accumulate.
+accept = ["200..=204", "429"]
+
+# Link caching to avoid checking the same links on multiple runs.
+cache = true
+
+# Discard all cached requests older than this duration.
+max_cache_age = "3d"
+
+# Maximum number of allowed retries before a link is declared dead.
+max_retries = 3
+
+# Minimum wait time in seconds between retries of failed requests.
+retry_wait_time = 2