diff --git a/.gitattributes b/.gitattributes index 9e9ee61..b75ceb8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ # kata:gitattributes:base:begin -# Universal LF — yukimemi/* projects are cross-platform (web, +# Universal LF — projects using these templates are cross-platform (web, # Rust, Go) and never intentionally rely on CRLF, so normalize # every text file to LF on commit. Binary files are auto-detected # (`text=auto`), so this is safe for images, PDFs, etc. diff --git a/.github/workflows/apm-bump.yml b/.github/workflows/apm-bump.yml new file mode 100644 index 0000000..5812581 --- /dev/null +++ b/.github/workflows/apm-bump.yml @@ -0,0 +1,124 @@ +name: apm-bump + +# Auto-refresh apm.lock.yaml on a weekly cadence. +# +# Why this exists: +# - Before pj-base#?? / pj-rust#??, the renri `[[hooks.post_create]]` +# hook ran `apm install --update` on every `renri add`. That +# rewrites apm.lock.yaml whenever upstream renri (or any other +# apm dep) has moved — which, during active renri development, +# happens often. Developers ended up `jj restore`-ing the lock +# on every worktree create just to keep feature-branch diffs +# clean. +# - Solution split into two parts: +# (a) the renri hook now runs plain `apm install` (no --update), +# which is idempotent against the existing lock — fresh +# worktrees install the recorded skills without touching the +# lock. Lives in pj-base's `renri.toml.base` (this layer) +# and pj-rust's `Makefile.toml` (`tasks.on-add` chain). +# (b) **this workflow** does the upstream-refresh. Weekly cron +# runs `apm install --update`; if the lock changes, it opens +# (or updates) a rolling `apm-bump/auto` PR. Auto-merged +# when CI passes. Same shape as `kata-apply.yml.tera`'s +# Renovate-equivalent for kata-managed files. +# +# Schedule: Mondays 04:00 UTC (~13:00 JST). Weekly is plenty for +# skill drift; tighten via `workflow_dispatch` when needed. +# +# `.tera` suffix: same dual purpose as the other workflow +# templates here — keeps GHA from auto-running the source inside +# pj-base, and opts the file into kata's Tera rendering for the +# `{{ vars.actions.* }}` action-version pins. +# +# Setup requirement on every consumer (one-time): +# `KATA_APPLY_TOKEN` repo secret — classic PAT with `repo` + +# `workflow` scope. PAT-owned PRs trigger downstream workflows +# (the auto-merge gate needs CI to fire); GITHUB_TOKEN-opened +# PRs would skip CI by GitHub design. +# +# `when = "always"` (in template.toml): every consumer wants +# identical refresh behaviour; fixes to the workflow itself flow +# automatically on next apply. + +on: + schedule: + # 04:00 UTC weekly Mondays. Off-peak; minute 0 is fine for a + # once-a-week job (no cron-storm worry the way daily jobs have). + - cron: "0 4 * * 1" + workflow_dispatch: + +permissions: + # `peter-evans/create-pull-request` needs both: contents to + # write the branch, pull-requests to open/update the PR. + contents: write + pull-requests: write + +concurrency: + # Serialise: if a scheduled run and a manual dispatch overlap, + # let them run sequentially so the second sees the first's + # commit (same shape as kata-apply.yml.tera). + group: apm-bump + cancel-in-progress: false + +jobs: + bump: + runs-on: ubuntu-latest + steps: + # PAT (KATA_APPLY_TOKEN), same reason kata-apply.yml.tera + # uses it: PRs pushed via GITHUB_TOKEN don't trigger CI, + # which would block the auto-merge gate. PAT-owned identity + # restores the trigger chain. + - uses: actions/checkout@v6.0.2 + with: + token: ${{ secrets.KATA_APPLY_TOKEN }} + + - name: Install apm + # aka.ms/apm-unix is the Unix one-liner installer published + # by Microsoft/apm. Same source the local-dev docs use + # ("install via aka.ms/apm-unix or your platform's package + # manager"). + run: | + set -euo pipefail + curl -fsSL https://aka.ms/apm-unix | sh + echo "$HOME/.apm/bin" >> "$GITHUB_PATH" + + - name: Refresh apm.lock.yaml + # No-op when apm.yml is absent — some PJs may not ship one + # yet, and the workflow shouldn't fail because of that. + # The actual `apm install --update` then resolves every + # `dependencies.apm:` entry to upstream HEAD and rewrites + # the lock. Targets match the local Makefile.toml task + # (copilot/claude/gemini) so worktrees and CI agree on + # which agent skill dirs get populated. + run: | + set -euo pipefail + if [ ! -f apm.yml ]; then + echo "no apm.yml; nothing to refresh" + exit 0 + fi + apm install --update -t copilot,claude,gemini + + - name: Open / update PR if there are changes + id: cpr + uses: peter-evans/create-pull-request@v8.1.1 + with: + token: ${{ secrets.KATA_APPLY_TOKEN }} + branch: apm-bump/auto + delete-branch: true + title: "chore(apm): refresh apm.lock.yaml" + commit-message: "chore(apm): refresh apm.lock.yaml" + body: | + Automated `apm install --update` (weekly schedule + manual dispatch). + + Auto-merged when CI passes; left open if CI fails. + + + author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + + - name: Enable auto-merge + if: steps.cpr.outputs.pull-request-number != '' + env: + GH_TOKEN: ${{ secrets.KATA_APPLY_TOKEN }} + run: | + gh pr merge --auto --squash ${{ steps.cpr.outputs.pull-request-number }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20dba67..9b1c0a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,14 @@ jobs: # blocking renovate auto-merge for downstream consumers). # Main keeps saving so every PR rebuild gets a warm cache. save-if: ${{ github.ref == 'refs/heads/main' }} + # The GH Actions cache backend transient-flakes the restore + # step on windows-latest just often enough to gate release + # pipelines (verified on yukimemi/kanade@v0.6.2 — the cache + # restore died mid-tar, killed the Windows build matrix, and + # blocked the publish). Fall through to a cold cargo build + # when the restore errors out: ~3–5 min slower on a single + # job, but self-healing instead of release-blocking. + continue-on-error: true - run: cargo check --all-targets test: @@ -48,6 +56,10 @@ jobs: - uses: Swatinem/rust-cache@v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} + # See the check job above for why continue-on-error is set + # on every rust-cache restore (transient GH Actions cache + # backend flake on windows-latest). + continue-on-error: true # `cargo test --all-targets` covers lib / bins / tests / benches # / examples but EXCLUDES doc tests, so run --doc separately. # Doc-test step is gated on `src/lib.rs` presence: `cargo test @@ -88,6 +100,10 @@ jobs: - uses: Swatinem/rust-cache@v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} + # See the check job above for why continue-on-error is set + # on every rust-cache restore (transient GH Actions cache + # backend flake on windows-latest). + continue-on-error: true - run: cargo clippy --all-targets -- -D warnings lockfile: @@ -102,6 +118,10 @@ jobs: - uses: Swatinem/rust-cache@v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} + # See the check job above for why continue-on-error is set + # on every rust-cache restore (transient GH Actions cache + # backend flake on windows-latest). + continue-on-error: true - run: cargo check --locked --all-targets coverage: @@ -121,6 +141,10 @@ jobs: - uses: Swatinem/rust-cache@v2.9.1 with: save-if: ${{ github.ref == 'refs/heads/main' }} + # See the check job above for why continue-on-error is set + # on every rust-cache restore (transient GH Actions cache + # backend flake on windows-latest). + continue-on-error: true - uses: taiki-e/install-action@v2 with: tool: cargo-llvm-cov diff --git a/.kata/applied.toml b/.kata/applied.toml index 404734f..3e8c3ec 100644 --- a/.kata/applied.toml +++ b/.kata/applied.toml @@ -1,19 +1,19 @@ preset = "github.com/yukimemi/pj-presets:rust-cli" -applied_at = "2026-05-17T04:40:43.234375838Z" +applied_at = "2026-05-24T04:45:12.151282216Z" [[templates]] source = "github.com/yukimemi/pj-base" -rev = "f04151faf4f0678be9621bb724c8f3120a5e4d8b" -version = "0.10.0" +rev = "3e3c7ee971897ae6cc71e78d597c838412974f19" +version = "0.12.1" [[templates]] source = "github.com/yukimemi/pj-rust" -rev = "9f103ca5aaf39e1dcf1a4d84b11685821aabc62f" -version = "0.5.0" +rev = "f78e190bac618fc3870b3c4a6e4187eb01c3cc42" +version = "0.7.0" [[templates]] source = "github.com/yukimemi/pj-rust-cli" -rev = "9263751c3f94f3415147e546db33170157fb1503" +rev = "58fae0574cbf118e61312c7599c412bfd7d3d3ac" version = "0.2.0" [files.".agents/skills/renri/SKILL.md"] @@ -29,13 +29,16 @@ once_applied = true content_hash = "486c974de88903113ed99b4e643432b7050e88f1031276f5263e8da7b43fe11e" [files.".gitattributes"] -content_hash = "1a4b579b1b643a41dbf97d8834ff1f3f1fe532ff863d0f861431cf3ca47d31e2" +content_hash = "6d380d1ecc2f882c2011429d548a6f3c774b74f0c757d884453e4667172acc2a" + +[files.".github/workflows/apm-bump.yml"] +content_hash = "79b12afa028841f8bce3c0caffd60dfe491642dba685142e33ed22a8cfcb9f9d" [files.".github/workflows/auto-tag.yml"] content_hash = "0796dfb281c7447610035360622ddada137a8e2d89efa574aea89374676d768b" [files.".github/workflows/ci.yml"] -content_hash = "741862d937397ea8fa0c02529cb4e7bc81266a8e628d317e590fa4687c2a3ec5" +content_hash = "594c14cf24dd2aa17d8c50d0db6d56321cb729ad4fc660888d7e6fbe36ca9211" [files.".github/workflows/kata-apply.yml"] content_hash = "bc0e3def04b634949297d60322999a27f53bffc71b6cb662ae58ac8087e78bed" @@ -51,7 +54,7 @@ content_hash = "4bebd1c3417c33f9d1c51f011694f6d82b098bbb1a5a102ba56e97ab01a866ef once_applied = true [files."AGENTS.md"] -content_hash = "e4f146a1c66d11e6b6c707f52507b3e37da2fe52d8d7cf13f75edcf9ad5d3a7f" +content_hash = "2a38543fca1f22128c41fe51c40159e8ea7f3331ae221fb99cddfca0194ece32" [files."CLAUDE.md"] content_hash = "a76b3af252969b8120128d7ce44f2b6cfdcf88c7420ee00675732f542d047529" @@ -63,7 +66,7 @@ content_hash = "3670f70a2f0be4013926bab6e878b90a42ad64c17899080aef4d193b246fa390 once_applied = true [files."Makefile.toml"] -content_hash = "27e3f57b9efd6c177843d9b0d248f40af5843739bcde6ceaf985f2ce518ecfcf" +content_hash = "930554e54d2c4090b4e0ba6014dd0692a05884ff6af803897f9d1c1647818b94" [files."OPENCODE.md"] content_hash = "11993dc7feec5fca8e83934f135de33879b88f100c48aeba0729de2f57a11a48" @@ -75,7 +78,7 @@ once_applied = true once_applied = true [files."clippy.toml"] -content_hash = "89dbb3b4fd63c479f61f077d2383ab5c0998ea838f271ed57fd09281975deda0" +content_hash = "c750cb285075d0cb0c5c9ea77cd67baf7b0c4af864603cbe6c0c6692e75ed6fb" [files."opencode.json"] content_hash = "2ada1cf4347db9e98fadd67ca72cd9e29042c5bf6ab719d666b5d71f97663e81" @@ -90,4 +93,4 @@ content_hash = "f4751570494ba62997cfb47e3abee2e707d3e01d924d1e8b940905a4e7c974f3 content_hash = "9eac3c057320afbad00022718e0c14731122b4c3f28abd2dc702846244c39f66" [files."rustfmt.toml"] -content_hash = "8a0bd07a7aaaf3e9fed5dd297b04a5f96c4a969d895375aa4e00247a95aa616c" +content_hash = "0d4916b2ce83f5270265e2fcc01b308e34e255d37fde92946382f8dbeb4427bb" diff --git a/AGENTS.md b/AGENTS.md index c35e07e..c6595de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -234,7 +234,7 @@ in `yukimemi/pj-base/AGENTS.md.base`, not in this file. [teravars]: https://github.com/yukimemi/teravars -## yukimemi/* shared conventions +## Shared conventions This file is the agent-agnostic source of truth (per the [agents.md](https://agents.md) convention). The matching @@ -246,10 +246,15 @@ here so each tool's auto-load behaviour still finds something. - **No direct push to `main`.** Open a PR. - Exception: trivial typo / whitespace / docs wording fixes. - - Exception: standalone version bumps. - Branch names: `feat/...`, `fix/...`, `chore/...`. - **PR titles + bodies in English. Commit messages in English.** -- Tag-based releases: `git tag vX.Y.Z && git push origin vX.Y.Z`. +- **Releases are PR-driven, tagging is automatic.** Bump + `[workspace.package].version` (workspace) or `[package].version` + (single crate) in a `chore/release-vX.Y.Z` PR. On merge to `main`, + `.github/workflows/auto-tag.yml` (kata-managed) detects the bump, + pushes the `vX.Y.Z` tag, and that tag fires `release.yml` for + binary builds + crates.io publish. **Do not run `git tag` by + hand** — the bot tag will collide and the manual push fails. ### PR review cycle @@ -257,6 +262,45 @@ here so each tool's auto-load behaviour still finds something. **CodeRabbit**. Wait for both bots to post, address their comments (push fixes to the PR branch), and merge only after feedback is resolved. +- **After opening a PR, immediately enter the review-monitoring + loop — do not ask the user whether to start it.** Drive the + cadence with `/loop` — fixed-interval mode (e.g. + `/loop 60s …`) schedules ticks via `CronCreate`; dynamic mode + (no interval, `/loop …`) self-paces via `ScheduleWakeup`. The + agent actively pulls fresh state each tick with + `gh pr view --json state,reviews,comments,statusCheckRollup` + and `gh api repos///pulls//comments` (the + latter covers inline review comments, which `gh pr view` + does not surface) and reacts to new bot feedback. Passive + watchers (background `gh` polls, file watchers, hooks) cannot + trigger active follow-up, so they are not a substitute — + without an active wake-up the agent never re-reads the PR. +- **Default polling interval: 60s.** Gemini Code Assist / + CodeRabbit historically reply within ~1–3 minutes of a push or + thread reply, so a 60s tick catches them on the next wake-up + without burning cache: 60s sits well inside the 5-minute + prompt-cache TTL, so the conversation context stays cached + across ticks. Do **not** stretch the interval to 300s — that + is the worst-of-both window (you pay the cache miss without + amortizing it). If the PR is idle but a bot re-review is still + expected (e.g. a CodeRabbit rate-limit refill window), step + **up** to 1200–1800s instead. +- **Stop the loop entirely when only owner approval is missing.** + Once review bots are quiet (or quiet-by-exception — version-bump + skip, Renovate/Dependabot skip), CI is green, and there is no + other expected follow-up, the *only* remaining action is human + approval. GitHub already notifies the owner; the agent + re-entering on every cron tick to find the same "still waiting + on owner" state burns cache and adds no value. Stop scheduling + further wake-ups (`CronDelete` in fixed-interval mode; simply + omit the next `ScheduleWakeup` in dynamic mode) and report the + wait state to the user. The owner restarts the loop after their + next push if a fresh bot pass is wanted, or merges directly. + (A CodeRabbit rate-limit window doesn't qualify on its own — a + re-review is still expected once the quota refills, so step up + to 1200–1800s instead and let it ride. Stopping is only correct + when the owner has explicitly chosen to skip the bot pass per + the rate-limit exception below.) - **Reply to reviewers after pushing a fix.** Reply on the corresponding review thread with an **@-mention** (`@gemini-code-assist` / `@coderabbitai`). Silent fixes are @@ -268,6 +312,20 @@ here so each tool's auto-load behaviour still finds something. - **Merge gate**: review bots quiet AND owner explicit approval. - Bot-authored PRs (Renovate / Dependabot) skip the bot-review gate; CI green + owner approval is enough. +- **Version-bump-only PRs** (a single `chore/release-vX.Y.Z` + branch whose entire diff is `[workspace.package].version` / + `[package].version` + the matching inter-crate refs + + `Cargo.lock`) **also skip the bot-review gate.** There is + nothing for the bots to find in a version bump, and the + release pipeline downstream of merge (auto-tag → release.yml) + is time-sensitive. CI green + owner approval is enough. +- **Treat CodeRabbit rate-limit notices as "quiet" for the + merge gate.** If CodeRabbit only posts a "Review limit + reached" quota-exhaustion message (no findings, no inline + comments), it has produced no review content — there is + nothing to address. Re-trigger with `@coderabbitai review` + once the quota refills if you want a real pass; for small or + time-sensitive PRs, merge on owner approval without waiting. ### Worktree workflow @@ -301,7 +359,7 @@ upstream template repo (`yukimemi/pj-base` / `yukimemi/pj-rust` / ### Rust workflow -This repo follows the yukimemi/* Rust toolchain conventions. The +This repo follows the shared Rust toolchain conventions. The language-agnostic conventions block above (`kata:agents:base:*`) covers git workflow, PR review cycle, and worktree usage. @@ -333,7 +391,7 @@ the reason in the relevant module. `rustfmt.toml` and `clippy.toml` are kata-managed (sourced from `yukimemi/pj-rust`). Edits to those files in this repo won't survive the next `kata apply`; if a setting is wrong, push the -fix to `yukimemi/pj-rust` so every yukimemi/* Rust project picks +fix to `yukimemi/pj-rust` so every Rust project using these templates picks it up. ### CI workflow @@ -348,6 +406,53 @@ apply, so don't bump them locally — Renovate is configured (via the kata-distributed `renovate.json`) to ignore `.github/workflows/ci.yml` and `.github/workflows/release.yml` in each PJ to avoid the bump→clobber loop. + +### Releasing: version bump PR + auto-tag + +Releases are triggered from `main` by a Cargo.toml version +change. `.github/workflows/auto-tag.yml` is kata-managed (source: +`yukimemi/pj-rust/.github/workflows/auto-tag.yml.tera`). It +watches `main` and, whenever a commit lands that changes the +top-level `version = "..."` in `Cargo.toml`, it pushes a matching +`vX.Y.Z` tag — no manual `git tag` step is needed. The tag push +then fires `release.yml`; see `kata:agents:rust-lib:*` or +`kata:agents:rust-cli:*` for what release.yml does in each +crate shape. + +Cut a release via a small PR — never `git push` the bump +straight to `main`, even though the base block lists version +bumps as an exception to "no direct push". `auto-tag.yml` only +fires on `main`-branch pushes, so the bump must land via a merge +either way; using a PR also gives CI a chance to gate the +release. Enable automerge so CI green = release start: + +```sh +git switch -c chore/bump-X.Y.Z +# Edit `package.version` in Cargo.toml, then: +cargo build # let Cargo.lock follow +git commit -am "chore: bump version to X.Y.Z" +git push -u origin chore/bump-X.Y.Z +gh pr create --fill +gh pr merge --auto --squash --delete-branch +``` + +Once CI is green the PR auto-merges. `auto-tag.yml` then pushes +`vX.Y.Z`, which fires `release.yml`. + +**Repo settings to set once:** enable +`delete_branch_on_merge=true` (Settings → General → +"Automatically delete head branches"). The `--delete-branch` +flag on `gh pr merge --auto` is effectively a no-op — gh +returns as soon as automerge is enabled, so the deletion has to +happen server-side, which requires the repo setting. + +**Why `KATA_APPLY_TOKEN`:** GitHub refuses to fire downstream +workflows from tags pushed by the default `GITHUB_TOKEN`, so +`auto-tag.yml` pushes with `KATA_APPLY_TOKEN` (the same PAT +`kata-apply.yml` already uses). Each consumer repo needs a +`KATA_APPLY_TOKEN` secret set; if a version-bump merge silently +doesn't fire `release.yml`, the missing PAT is the first thing +to check. ### Rust CLI release flow @@ -358,15 +463,14 @@ This is a Rust CLI crate, so the release pipeline is publish-aware. `release.yml.template` for the same don't-auto-execute reason ci.yml uses). -```sh -# Bump `package.version` in Cargo.toml (run `cargo build` so -# Cargo.lock follows), then: -git commit -am "chore: bump version to X.Y.Z" -git tag -a vX.Y.Z -m "vX.Y.Z" -git push origin main vX.Y.Z -``` +Releases are triggered by a Cargo.toml version bump landing on +`main`. The bump flow itself (PR with automerge → `auto-tag.yml` +pushes `vX.Y.Z` → `release.yml` runs) is documented in +`kata:agents:rust:*` under "Releasing: version bump PR + +auto-tag" — that block also covers the `KATA_APPLY_TOKEN` and +`delete_branch_on_merge` setup. What `release.yml` then does for +a **CLI** crate: -The workflow then: 1. Cross-compiles binaries for x86_64 Linux / Windows / macOS, plus aarch64 macOS (Apple Silicon) — full triples `x86_64-unknown-linux-gnu`, `x86_64-pc-windows-msvc`, @@ -376,7 +480,7 @@ The workflow then: `CARGO_REGISTRY_TOKEN` repo secret. Set the `CARGO_REGISTRY_TOKEN` secret once per repo (`gh secret -set CARGO_REGISTRY_TOKEN`) before the first tag push. If the +set CARGO_REGISTRY_TOKEN`) before the first release. If the crate is internal-only and shouldn't go to crates.io, either drop the `publish` job locally (release.yml is `when = "once"` so the edit survives subsequent applies) or set `package.publish = false` @@ -384,7 +488,7 @@ in `Cargo.toml`. The binary name is derived from the GitHub repo name at runtime (`${{ github.event.repository.name }}`), so the workflow is -identical across yukimemi/* CLIs unless your `[[bin]] name` in +identical across CLIs using these templates unless your `[[bin]] name` in `Cargo.toml` deliberately differs from the repo name — in that case override `BIN_NAME` in the workflow's `env:` block. diff --git a/Makefile.toml b/Makefile.toml index 57859c1..2cf02ed 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -106,7 +106,7 @@ end # Fast-path: if apm.lock.yaml already records the current # upstream HEAD of yukimemi/renri (the only APM dependency for -# yukimemi/* projects), don't re-run `apm install --update` — +# projects using these templates), don't re-run `apm install --update` — # git ls-remote is ~100ms, an actual reinstall is several # seconds. This makes `cargo make on-add` cheap on every # `renri add` after the first. @@ -175,4 +175,13 @@ end [tasks.on-add] description = "Wired to renri's [[hooks.post_create]] — runs in a freshly created worktree" -dependencies = ["apm-install-update", "vcs-fetch"] +# `apm-install` (no `--update`) is idempotent against the existing +# `apm.lock.yaml` — fresh worktrees install the recorded skills +# without rewriting the lock. Upstream-refresh is delegated to +# pj-base's `.github/workflows/apm-bump.yml` (weekly cron + manual +# dispatch); same Renovate-equivalent pattern as `kata-apply.yml`. +# Avoids the per-`renri add` lock churn that `apm install --update` +# produced whenever upstream renri (or any other apm dep) had +# moved — see yukimemi/pj-base#8 for the companion change in +# `renri.toml.base`. +dependencies = ["apm-install", "vcs-fetch"] diff --git a/clippy.toml b/clippy.toml index 2afd996..e4b41d7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,4 +1,4 @@ -# yukimemi/* Rust clippy policy. Centralised in +# Shared Rust clippy policy. Centralised in # `yukimemi/pj-rust`; every Rust project picks this up via # `kata apply` (overwrite always). Local edits don't survive # `kata apply` — push the change here. diff --git a/rustfmt.toml b/rustfmt.toml index e815750..1ab7a33 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ -# yukimemi/* Rust formatting policy. Centralised in +# Shared Rust formatting policy. Centralised in # `yukimemi/pj-rust`; every Rust project picks this up via # `kata apply` (overwrite always). Local edits don't survive # `kata apply` — push the change here.