← 기사 목록
日本語https://zenn.dev/topics/ai/feed

複数プロジェクトのテンプレを束ねる kata

추출된 키워드

47
メタテンプレート CLI·5kata·5Claude Code·4Rust·4CLI·4layered template·4pj-base·4AGENTS.md·4pj-rust·4pj-rust-cli·4merge-section·4merge-toml·4Gemini Code Assist·3Renovate·3CodeRabbit·3merge-yaml·3toml_edit·3serde_yaml·3GitHub Actions·3Tera·3teravars·3pj-presets·3vars.toml·3rustfmt.toml·3shun·3rvpm·3todoke·3yui·3renri·3ボイラープレート·3Makefile.toml·3clippy·3Codex·3rust-toolchain.toml·3APM·3AI エージェント·3renovate.json·3CLAUDE.md·3GEMINI.md·3Claude·3Gemini·3tokio·2cruft·2cookiecutter·2copier·2cross-compile·2cargo publish·2

원문

25,893
複数プロジェクトのテンプレを束ねる kata

複数プロジェクトのテンプレを束ねる kata

Claude Code と一緒に Rust 製の CLI を作るのが楽しくて、ここ最近で shun / rvpm / todoke / yui / renri といった CLI がどんどん増えていきました。これらにまったく同じボイラープレートを適用しつづけるためのメタテンプレート CLI、kata (型) を作りました。

型 —

the woodblock pattern. 各プロジェクトに同じ型を押し当てる、版木のイメージです。

https://github.com/yukimemi/kata

なぜ作ったのか

同じような CLI が増えてくると、共通ボイラープレートのメンテがどんどんしんどくなります。具体的にしんどかったのはこのあたりです。

  • Makefile.toml
    check
    /
    clippy
    /
    test
    タスクの並び
  • .github/workflows/ci.yml
    の OS マトリクスと action のバージョン pin
  • .github/workflows/release.yml
    の cross-compile + cargo publish のテンプレ
  • rustfmt.toml
    /
    clippy.toml
    /
    rust-toolchain.toml
    の方針
  • apm.yml
    APM経由の AI エージェント用 skill (
    renri
    など) を入れる定型
  • renovate.json
    の auto-merge ルール
  • そして極めつけに
    AGENTS.md
    /
    CLAUDE.md
    /
    GEMINI.md

最後の

AGENTS.md
がいちばんやっかいでした。Claude / Gemini / Codex に渡している「PR レビューはこう回す」「worktree workflow はこう」「Rust の lint/format ポリシーはこう」みたいな会話で蓄積されたノウハウを、新しいプロジェクトを生やすたびに、あるいは規約を 1 行直すたびに、N 個のリポジトリにぜんぶ手でコピペする必要があったのです。

「Gemini Code Assist と CodeRabbit 両方のレビュー待つようにしよう」と気付いたら 7 個のリポジトリの

AGENTS.md
を全部開いて編集する、というのを何度かやりました。1 回ならいいんですけど、規約は一度決めて終わりじゃなくて、運用しながら何度も微調整したくなる。

copier / cookiecutter / cruft といった既存ツールは「最初のプロジェクト生成」と「機械的な再適用」までは面倒を見てくれるんですが、

AGENTS.md
のように
  • リポジトリ共通のセクション (PR レビュー規約、worktree workflow)
  • プロジェクト固有のセクション (このプロジェクトのアーキ概要、特殊な事情)

1 枚のファイルに同居しているようなものを update し続ける機能はありませんでした。AI に判定を委譲するモードもありません。

そこで作ったのが kata です。「型 (テンプレート) を版木のように押し当てて、押し当てきれないところだけ AI に判断させる」 という発想で設計しました。

kata の特徴

  • layered template
    pj-base
    +
    pj-rust
    +
    pj-rust-cli
    を順に重ねて、後勝ち。言語非依存な部分を
    pj-base
    に集約できる
  • how
    ×
    when
    の二軸
    how
    (
    overwrite
    /
    merge-section
    /
    merge-toml
    /
    merge-yaml
    /
    ai
    /
    script
    ) と
    when
    (
    once
    /
    always
    /
    manual
    ) が独立。
    how="ai", when="once"
    how="script", when="always"
    がどちらも自然に書ける
  • marker-bracketed merge-section
    AGENTS.md
    <!-- kata:agents:base:begin -->
    <!-- kata:agents:base:end -->
    間だけを kata が管理。プロジェクト固有の節は外側にいくらでも書ける
  • path-based merge-toml
    Makefile.toml
    tasks.check
    /
    tasks.clippy
    /
    tasks.test
    だけを kata が所有して、
    tasks.install-local
    のような独自タスクは触らない
  • AI 委譲
    how = "ai"
    なファイルは、インストール済みの
    claude
    /
    gemini
    /
    codex
    CLI に template の diff と現在の中身を投げて、chezmoi 風の
    [a]ccept / [e]dit / [s]kip / [d]efer
    で確認
  • truth は PJ 側— どのテンプレートをどの rev で適用したかは各 PJ の
    .kata/applied.toml
    に記録される。グローバル設定は単なる PJ パスのレジストリ
  • 並列実行— tokio で PJ をファンアウト、AI 呼び出しは semaphore で抑制してエージェント CLI の同時起動が爆発しないようにする
  • CI 同期
    kata-apply.yml
    を pj-base が配布。daily で
    kata update + kata apply
    を回して、テンプレ上流の変更を PR として自動取り込み

インストール

cargo install kata

kata --version
で動作確認できれば OK です。

クイックスタート

# 新しい Rust CLI プロジェクトに rust-cli プリセットを適用
mkdir my-rust-cli && cd my-rust-cli
kata init github.com/yukimemi/pj-presets:rust-cli --non-interactive

# テンプレが進化したら再適用 (idempotent)
kata apply --non-interactive

# 適用前のドライラン
kata status

# 何が tracked か確認
kata list

これで

Makefile.toml
/
apm.yml
/
renri.toml
/
.github/workflows/ci.yml
/
release.yml
/
AGENTS.md
/
CLAUDE.md
/
GEMINI.md
/
rustfmt.toml
/
clippy.toml
/
rust-toolchain.toml
/
renovate.json
などがまとめてプロジェクトに落ちてきます。

preset = テンプレートの束

pj-presets:rust-cli
は単に「どのテンプレートを、どの順に重ねるか」を書いた小さなファイルです。
# pj-presets/rust-cli.toml
name = "rust-cli"

[[templates]]
source = "github.com/yukimemi/pj-base"

[[templates]]
source = "github.com/yukimemi/pj-rust"

[[templates]]
source = "github.com/yukimemi/pj-rust-cli"

順に書かれた順番で適用され、同じファイルが衝突したら後勝ちです。これによって

  • pj-base
    — 言語非依存 (LICENSE,
    .gitignore
    ,
    AGENTS.md
    の共通節,
    apm.yml
    ,
    renri.toml
    の base,
    kata-apply.yml
    ……)
  • pj-rust
    — Rust 共通 (
    Makefile.toml
    , CI matrix,
    rust-toolchain.toml
    ,
    rustfmt.toml
    ,
    clippy.toml
    )
  • pj-rust-cli
    — CLI 用追加 (
    release.yml
    の cross-compile + cargo publish)

という役割分担ができて、たとえば「Rust ライブラリだから CLI 用 release は要らない」というケースには

pj-rust-lib
を組み合わせた別 preset を用意するだけで済みます。

ライブラリ向けに

rust-lib
、Web フロント向けに
web-react
、Firebase まで含む
web-react-firebase
も用意していて、preset 単位で気軽に「型」を切り替えられます。

how
when
を独立に持つということ

kata の設計でいちばんこだわったのが

how
(適用方法) と
when
(タイミング) を別の軸として持つことです。
how
何をするか
overwrite
テンプレ通りにファイルを上書き
merge-section
<!-- kata:*:begin -->
end
の間だけ差し替え
merge-toml
toml_edit
で指定パスだけマージ
merge-yaml
serde_yaml
で YAML の指定パスだけマージ
ai
claude / gemini / codex に判断委譲
script
任意のシェルコマンドを実行
when
いつ適用するか
once
初回だけ。以降は
.kata/applied.toml
once_applied = true
で skip
always
毎回適用 (
kata apply
のたびに同期)
manual
明示的に
--file
を指定したときだけ

この 2 軸が独立なので、たとえば

  • release.yml
    overwrite, when=once
    — 初回だけ配って、以降はプロジェクト側で自由に編集
  • ci.yml
    overwrite, when=always
    — CI は常に上流追従
  • Makefile.toml
    merge-toml, when=always
    — kata 所有のタスクだけ追従
  • AGENTS.md
    merge-section, when=always
    — マーカーの中だけ追従
  • apm.yml
    overwrite, when=once
    — 初回テンプレ、以降はプロジェクト側
  • LICENSE
    overwrite, when=once
    — 初回だけ

という細かいポリシーを 1 つの manifest で表現できます。「mode」として 1 つの enum にまとめなかったのは、

how="ai", when="once"
(ROADMAP.md の初期生成だけ AI に任せる) のような組み合わせを潰したくなかったからです。

AGENTS.md の merge-section が刺さるところ

kata を入れて一番恩恵を感じているのが

AGENTS.md
の扱いなので、ここは少し詳しく書きます。

pj-base
側の
AGENTS.md.base
(テンプレ) には共通規約だけが書かれています。
## Shared conventions

This file is the agent-agnostic source of truth (per the
[agents.md](https://agents.md) convention)...

### Git workflow
- **No direct push to `main`.** Open a PR.
- Branch names: `feat/...`, `fix/...`, `chore/...`.
- **PR titles + bodies in English.**
...

### PR review cycle
- Every PR runs reviews from **Gemini Code Assist** and **CodeRabbit**...
- **After opening a PR, immediately enter the review-monitoring loop...**
...

### Worktree workflow
Use [`renri`](https://github.com/yukimemi/renri) for any commit-bound change...

これを

pj-base/template.toml
でこう宣言しています。
[[file]]
src = "AGENTS.md.base"
dst = "AGENTS.md"
how = "merge-section"
when = "always"
marker = { begin = "<!-- kata:agents:base:begin -->", end = "<!-- kata:agents:base:end -->" }

kata apply
を実行すると、各プロジェクトの
AGENTS.md
の対応マーカーの中だけがテンプレ内容で置き換わります。マーカーの外にはそのプロジェクトの固有の事情 (アーキテクチャ、設計判断、ドメイン用語、Phase n の状況……) をいくらでも書いておけて、それは
kata apply
で一切触られません。

さらに layered なので、

pj-rust
<!-- kata:agents:rust:* -->
pj-rust-cli
<!-- kata:agents:rust-cli:* -->
それぞれ自分のマーカーブロックを所有します。これによって 1 枚の
AGENTS.md
の中に「言語非依存 / Rust 共通 / Rust CLI 専用 / プロジェクト固有」の 4 層が綺麗に同居する、という構造になります。

Makefile.toml の merge-toml で「kata 所有タスク」だけ追従させる

AGENTS.md
の marker と並んでよく使うのが、
Makefile.toml
merge-toml
です。
pj-rust/template.toml
ではこう宣言しています。
[[file]]
src = "Makefile.toml"
how = "merge-toml"
when = "always"
# kata owns these specific tasks; everything else is left untouched
paths = [
    "tasks.default",
    "tasks.check",
    "tasks.fmt",
    "tasks.fmt-check",
    "tasks.clippy",
    "tasks.test",
    "tasks.lock-check",
    "tasks.publish-dry",
    "tasks.hook-install",
    "tasks.apm-install",
    "tasks.setup",
    "tasks.on-add",
]

paths
で「kata が所有する TOML のパス」を明示します。
tasks.check
/
tasks.clippy
/
tasks.test
のような kata-managed task は毎回上流から上書きされますが、consumer 側で勝手に追加した
tasks.install-local
とか
tasks.deploy
のような独自タスクは paths に含まれないので一切触られない という挙動になります。

merge-toml
toml_edit
paths
で指定した key だけを replace していくので、
  • 同じ key の値は更新される (例:
    tasks.check.script
    の中身が変わる)
  • paths に出てこない key は consumer の手書きが完全に保持
  • インデント / コメント / 順序も
    toml_edit
    レベルで保持

という、

overwrite
だと潰してしまう手書き内容を尊重した同期ができます。

GHA の action バージョンは
.kata/vars.toml
に切り出して Renovate に任せる

merge-toml
×
when = "once"
のもう 1 つの実用例が、GitHub Actions の version pin を .kata/vars.toml (Tera 変数ファイル) に切り出す パターンです。

考えたい問題はこうです。

  • ci.yml
    /
    release.yml
    /
    kata-apply.yml
    の version pin (
    actions/checkout@v6.0.2
    等) をRenovate に自動 bumpさせたい
  • しかし workflow 本体は
    overwrite, when=always
    で kata-managed なので、Renovate が直接
    ci.yml
    を編集しても次の
    kata apply
    で潰されてしまう

解決策は、pin だけを .kata/vars.toml に切り出して、workflow 本体は Tera テンプレでそれを参照 することです。

pj-base/vars.toml
(universal pin):
[actions]
checkout = "actions/checkout@v6.0.2"
create_pull_request = "peter-evans/create-pull-request@v8.1.1"

pj-rust/vars.rust.toml
で Rust 専用の pin を追加マージ:
[actions]
swatinem_rust_cache = "Swatinem/rust-cache@v2"

template.toml
側で
.kata/vars.toml
の所有関係を宣言:
# pj-base — universal pin を初回だけ seed
[[file]]
src = "vars.toml"
dst = ".kata/vars.toml"
how = "overwrite"
when = "once"

# pj-rust — Rust 専用 pin を merge-toml で重ねる (初回だけ)
[[file]]
src = "vars.rust.toml"
dst = ".kata/vars.toml"
how = "merge-toml"
when = "once"
paths = ["actions.swatinem_rust_cache"]

両方とも

when = "once"
なので、初回 apply で seed したあとは consumer の .kata/vars.toml を kata は一切触らない、という所有関係になります。
ci.yml.tera
の中では:
- uses: {{ vars.actions.checkout }}
- uses: {{ vars.actions.swatinem_rust_cache }}

として参照されているので、

.kata/vars.toml
の pin が変われば次の
kata apply
ci.yml
全体が新しい version で再レンダリングされる、という流れになります。

その上で consumer 側の .kata/vars.toml を Renovate の customManager が scan していて、新しい action リリースが出たら

を作ってくれます。Renovate が触るのは

.kata/vars.toml
の pin 値を bump する PR
.kata/vars.toml
の 1 行だけ、workflow 本体は kata-apply の再レンダリングで反映、という分業になります。

まとめると、

  • workflow の構造変更(新ステップ追加、jobs の整理など) → 上流テンプレへの push → daily
    kata-apply
    で降りてくる
  • action の version bump→ consumer の
    .kata/vars.toml
    を Renovate が自走で書き換える → 次の
    kata apply
    ci.yml
    が再レンダリング

という、役割分担の明快な並列同期

merge-toml
when = "once"
の組み合わせだけで組めます。

ところで、上の

ci.yml.tera
{{ vars.actions.checkout }}
のように書けているのは Rust 製のテンプレートエンジン Tera のおかげです。
.tera
サフィックスの付いたファイルが apply 時に Tera で render されて、suffix を落とした名前で consumer に書き出される、というシンプルな仕組み。
{% if is_windows() %}
のような分岐も
{{ env.HOME }}
のような環境変数参照も全部 Tera の機能で、kata 側はほぼ何も再発明していません。

実はこの「設定を Tera で書ける」感覚は、rvpmtodoke といった他の Rust 製 CLI でも同じスタックで提供していて、内部では teravars (Tera + vars + include + system context を統一した薄いラッパー) を共有しています。

rvpm
で見慣れた
{% if is_windows() %}
がそのまま kata の template でも通る、というのが地味に効いていて、Rust で「設定が宣言的に書ける小さな CLI」を作るときの定番スタックとして teravars はかなりおすすめです。

ここが本題: pj-base を直せば全プロジェクトに反映される

これが kata を作って一番うれしかったところです。

「あ、

AGENTS.md
のこの一節、ちょっと書き方が悪かったな。Claude が誤解しがちだから直したい」
PR review cycle
の節、CodeRabbit の rate-limit notice の扱いを追記したい」
Worktree workflow
の節に新しい
renri prune
の説明を入れたい」

こういう気付きは規約を運用してるとめちゃくちゃ頻繁にあります。kata を入れる前は N 個の AGENTS.md を全部開いて同じ編集を N 回繰り返す ことになっていました。

kata を入れた今はこうなっています。

# pj-base 側でだけ直す
cd ~/src/github.com/yukimemi/pj-base
$EDITOR AGENTS.md.base
git commit -am "docs(agents): clarify PR review cycle"
git push

# あとは何もしなくていい — 翌日になれば、
# kata-apply ワークフローが全 PJ に PR を作って auto-merge する

具体的には、各プロジェクトの

.github/workflows/kata-apply.yml
(これも pj-base 配布) が毎日 03:17 UTC に走って、
  • 上流テンプレ (
    pj-base
    /
    pj-rust
    /
    pj-rust-cli
    ) の最新 rev を取得
  • kata update
    で applied.toml の rev を更新
  • kata apply --non-interactive --no-ai
    で再レンダリング
  • 差分が出たら
    kata-apply/auto
    ブランチに PR を作成
  • CI が緑なら auto-merge

を全リポジトリで自動的に回します。規約や設定の更新が該当するテンプレレイヤへの 1 push だけで N プロジェクトに伝播していくわけです。

AGENTS.md
の共通節なら
pj-base
Makefile.toml
/
rustfmt.toml
/
clippy.toml
/
ci.yml
といった Rust 共通の規約なら
pj-rust
release.yml
の cross-compile + cargo publish なら
pj-rust-cli
、というレイヤ分担そのままに、それぞれの上流に 1 push すれば全 consumer PJ が翌日には追従します。

ワークフロー本体もテンプレ管理なので、ワークフロー自体の改善 (新しい action バージョン、ステップの追加) も同じ仕組みで自動的に各プロジェクトに降りていきます。テンプレが自分自身のメンテも見るかたち。

CI で kata-apply を回す

このフローの肝は CI で kata apply を回せることです。設計時点でここを最優先に考えていて、

  • --non-interactive
    でプロンプトを完全にスキップできる
  • --no-ai
    で AI ファイルを丸ごとスキップ (= 機械的なテンプレ同期だけ自動で回せる)
  • --non-interactive --yes
    で「全部 accept」モードもあり (= 信頼できるテンプレなら AI も自動 accept)

という 3 つのフラグで CI 適性が担保されています。

pj-base
が配布する
kata-apply.yml.tera
の中身は essentially こんな感じです (一部抜粋)。
name: kata-apply

on:
  schedule:
    - cron: "17 3 * * *"     # 03:17 UTC daily
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

concurrency:
  group: kata-apply
  cancel-in-progress: false

jobs:
  apply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6.0.2
        with:
          token: ${{ secrets.KATA_APPLY_TOKEN }}    # ← PAT 必須

      - name: Install kata
        run: |
          KATA_VERSION="$(curl -fsSL https://api.github.com/repos/yukimemi/kata/releases/latest | jq -r .tag_name)"
          curl -fsSL "https://github.com/yukimemi/kata/releases/download/${KATA_VERSION}/kata-x86_64-unknown-linux-gnu.tar.gz" \
            | tar xz -C /tmp
          sudo mv /tmp/kata /usr/local/bin/kata

      - name: kata update + apply
        run: |
          kata update
          kata apply --non-interactive --no-ai

      - 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: kata-apply/auto
          title: "chore(kata): auto-apply"

      - 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 }}

要点だけハイライトしておきます。

  • KATA_APPLY_TOKEN
    GITHUB_TOKEN
    じゃダメ
    GITHUB_TOKEN
    経由で開いた PR は GitHub のループ防止仕様で下流ワークフロー (CI) を triggers しません。auto-merge の前提が CI 緑判定なので、CI が走らないと永遠にマージされません。適切な権限 (
    contents: write
    +
    pull-requests: write
    ) を付けた PAT (fine-grained でも classic でも可) を
    KATA_APPLY_TOKEN
    リポジトリシークレットとして設定する、というのが consumer 側の唯一のセットアップ作業です
  • ブランチは
    kata-apply/auto
    固定
    create-pull-request
    delete-branch: true
    と組み合わせると、毎日新しい差分を同じブランチに rolling で上書きしていく動きになるので、PR が大量にスタックしません
  • CI が落ちたら PR は open のまま残る。auto-merge は CI 緑になったときだけ発火するので、何か壊れたら人間が見るタイミングが自然に生まれます
  • cron: "17 3 * * *"
    は意図的な off-peak かつ off-the-hour pin
    0 0 * * *
    0 9 * * *
    みたいに :00 ちょうどでスケジュールする人が地球上に多すぎて、GitHub Actions の runner プールが :00 / :30 で枯渇する (cron-storm) という現象があります。
    :17
    のような半端な分にずらすと runner 確保がスムーズ。さらに
    3 UTC
    は日本時間 12:00 / 米西海岸の夜 / 欧州早朝で、runner 自体も比較的空いている時間帯なので、daily な機械的同期にはちょうどいい枠です

「全プロジェクトに同じワークフローが入っている」という事実が、全プロジェクトに同じ自動同期がかかっている という安心感に繋がっていて、これは入れた価値が大きかったです。

AI 委譲モード

how = "ai"
を指定したファイルは、インストール済みの AI CLI に判断を投げます。

template.toml にこう書くと:

[[file]]
src = "ROADMAP.md.tera"
dst = "ROADMAP.md"
how = "ai"
when = "always"
agent = "auto"                # claude > codex > gemini で最初に見つかったやつ
prompt = """
Merge the template's structural changes into the project's
ROADMAP.md. Preserve project-specific phases and dated entries.
"""

kata は template の diff、現在の dst の中身、prompt をまとめて指定エージェントの CLI (

claude -p
,
gemini -p
,
codex exec
のいずれか) に投げます。返ってきた full body または patch に対して、chezmoi 風の対話プロンプトが出ます。
proposed change for ROADMAP.md:
  + Phase 5 — opencode adapter
  + ## Crate structure (regenerated section)
  ...

[a]ccept / [e]dit / [s]kip / [d]efer ?
  • a
    = そのまま採用
  • e
    =
    $EDITOR
    で開いて手で直してから採用
  • s
    = この回はスキップ (次回
    kata apply
    でもう一度提案される)
  • d
    =
    defer
    (今回は見送り、ただし「次回必ず再提案」を
    applied.toml
    に記録)

--non-interactive
だけだと安全側に倒れて AI ファイルはスキップ、
--non-interactive --yes
だと全部 accept という CI 完全自動モードになります。

backend は trait で抽象化されていて、

claude
/
gemini
/
codex
の 3 つを実装済みです。
agent = "auto"
は PATH を見て上から順にフォールバックしていく挙動。
MockAiAgent
も組み込まれていて、テストでは決定的な応答が返せます。

並列度の制御もあって、AI 呼び出しはグローバル semaphore (default 4) で絞られます。

kata apply --all
で 10 個のプロジェクトを並列で回したときに、エージェント CLI の同時起動が爆発しないようにするためです。

.kata/applied.toml
が source of truth

kata の状態は全部 PJ 側の

.kata/applied.toml
に書かれます。グローバル設定 (
~/.config/kata/config.toml
) は単なる PJ パスのレジストリで、何が適用されているかは知りません。

applied.toml
はだいたいこんな感じです。
preset = "github.com/yukimemi/pj-presets:rust-cli"
applied_at = "2026-05-17T04:40:43Z"

[[templates]]
source = "github.com/yukimemi/pj-base"
rev = "f04151faf4f0678be9621bb724c8f3120a5e4d8b"
version = "0.10.0"

[[templates]]
source = "github.com/yukimemi/pj-rust"
rev = "9f103ca5aaf39e1dcf1a4d84b11685821aabc62f"
version = "0.5.0"

[[templates]]
source = "github.com/yukimemi/pj-rust-cli"
rev = "9263751c3f94f3415147e546db33170157fb1503"
version = "0.2.0"

[files."AGENTS.md"]
content_hash = "e4f146a1c66d11e6b6c707f52507b3e37da2fe52d8d7cf13f75edcf9ad5d3a7f"

[files."Makefile.toml"]
content_hash = "27e3f57b9efd6c177843d9b0d248f40af5843739bcde6ceaf985f2ce518ecfcf"

[files."LICENSE"]
once_applied = true

これを git に commit しておくと、

  • teammateが clone →
    kata apply
    で同じ状態が再現できる
  • CI
    applied.toml
    を見るので、ローカル設定なしで CI が完結する
  • rev が pin されているので、上流の HEAD が動いても勝手に当たらない (
    kata update
    で明示的に上げる)

という運用になります。「状態は対象 (PJ) 側に置く、グローバル設定は単なるレジストリに留める」という設計を kata でも採用しました。

kata apply
が冪等であること

設計でずっと気をつけたのが「何度走らせても結果が変わらない」ことです。

apply
が走るたびに差分が出るようなツールは CI で回せないので、
  • content_hash
    applied.toml
    に記録して、変化がないファイルはそもそも書き換えない
  • once_applied = true
    のファイルは 2 回目以降は完全 skip
  • merge-section
    /
    merge-toml
    のマージはべき等なように実装 (同じ入力で何度マージしても同じ出力)
  • AI モードも
    --non-interactive --no-ai
    で完全に固定動作 (= AI モードを除いて再現可能)

という不変条件を守るようにしてあります。

おかげで

kata apply --non-interactive --no-ai
を毎日 CI で回すと、変更が必要なときだけ PR が立ち、なければ何も起きないという静かな動きになります。

関連プロジェクト

kata の周りに必要な template repo は以下です。すべて単体で意味があるので、好きな組み合わせで preset を組めます。

repo役割

pj-base
.gitignore
, AGENTS.md 共通節, kata-apply ワークフロー, …)
pj-rust
pj-rust-cli
pj-rust-lib
pj-pnpm
pj-react-web
pj-firebase
pj-presets
rust-cli
/
rust-lib
/
web-react
/
web-react-firebase
のバンドル

kata 自身も dogfood で

pj-presets:rust-cli
を適用しています — README の表が示す通り、
Makefile.toml
も CI も
AGENTS.md
も全部 kata-apply 経由で同期されています。

おわりに

kata を入れる前は、

AGENTS.md
を 1 行書き換えるのに 7 リポジトリの編集が必要でした。今は pj-base に 1 push すれば、翌日には全プロジェクトに PR が立って auto-merge されています。Claude / Gemini / Codex に渡しているノウハウが全プロジェクトで瞬時に揃う、というのが体験として相当よかったです。

「規約を更新する心理的コストが下がると、規約をもっと細かく洗練させたくなる」というポジティブフィードバックが回り始めていて、Claude Code との PR レビューサイクル運用 (

/loop
での 60s ポーリング、CodeRabbit の rate-limit notice の扱い、version-bump-only PR の特例……) みたいな細かい知見が、書いた次の日には全 PJ の Claude に届くようになりました。

Rust + Tera + tokio + AI CLI 委譲という shun / rvpm / todoke のときから使い倒している組み合わせに、

teravars
(Tera engine + vars + include の共通エンジン) を載せた構成で、いつもの定番スタックの延長で書けたのも開発体験として良かったところです。

複数のリポジトリのボイラープレートに疲れている方、特に AGENTS.md などの AI への指示書 を複数プロジェクトに散らかしてしまっている方は、ぜひ kata を試してみてください — 型を押して、版木を当てて、揃えていきましょう