diff --git a/.gitignore b/.gitignore index 438f867..77ee16a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ ./hosts.allow ./hosts.deny +config/nvim/lazy-lock.json +config/nvim/log +zshrc.local +# Claude Code: machine-local / company-internal config must never be tracked (public repo) +claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bebe1d8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +Personal dotfiles and Docker-based development environment for vankichi. The repo serves two purposes: +1. Dotfiles (zsh, tmux, git, neovim, starship) symlinked to `$HOME` +2. Multi-stage Docker images that assemble a full dev container (Go, Rust, Dart, K8s tools, etc.) + +## Key Commands + +```bash +# Symlink dotfiles to $HOME (uses ~/.config paths) +make new_link + +# Remove symlinks +make new_clean + +# Build the main dev container image +make build # simple docker build +make prod_build # multiplatform buildx (linux/amd64,linux/arm64) + +# Build individual layer images +make build_go # reads versions/GO_VERSION +make build_rust +make build_env # reads versions/NGT_VERSION, TENSORFLOW_C_VERSION +make build_k8s +make build_base +make build_all # builds all layers then prod + +# Update version files from upstream releases +make version/go +make version/ngt +make version/tensorflow +make version/flutter + +# Run / manage the dev container (defined in alias file) +source ./alias && devrun # or just: make run +devin # exec into running container +devkill # stop and remove container +devres # kill + rerun + +# Multiplatform buildx setup +make init_buildx # register qemu binfmt +make create_buildx # create buildx builder "vankichi-builder" +``` + +## Architecture + +- **Dockerfile** — Final dev image. Multi-stage: pulls from `vankichi/{go,rust,docker,dart,kube,env}` images, copies binaries, then installs dotfiles. +- **dockers/** — Individual Dockerfiles for each layer image (base, env, go, rust, k8s, docker, dart, gcloud, glibc, nim). +- **versions/** — Plain text files (e.g., `GO_VERSION`) consumed as `--build-arg` during Docker builds. Updated via `make version/*` targets. +- **Makefile.d/version.mk** — Version-fetching targets, included by the main Makefile. +- **alias** — Shell functions (`devrun`, `devin`, `devkill`) for container lifecycle. `devrun` mounts dotfiles, Go src, Docker socket, SSH keys, etc. into the container; platform-aware (macOS vs Linux). +- **config/nvim/** — Lua-based Neovim config (lazy.nvim plugin manager). +- **config/sheldon/plugins.toml** — Sheldon zsh plugin manager config. + +## Docker Image Hierarchy + +``` +base.Dockerfile → env.Dockerfile → Dockerfile (final) + ↑ copies from: go, rust, docker, dart, kube +``` + +All images are pushed to Docker Hub under the `vankichi/` namespace. Builds use `docker buildx` for multiplatform support (`linux/amd64,linux/arm64`). diff --git a/Dockerfile b/Dockerfile index e185c0d..10995ca 100755 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ ENV CARGO_PATH $HOME/.cargo ENV NVIM_HOME $HOME/.config/nvim ENV VIM_PLUG_HOME $NVIM_HOME/plugged/vim-plug ENV LIBRARY_PATH /usr/local/lib:$LIBRARY_PATH -ENV ZPLUG_HOME $HOME/.zplug +# ENV ZPLUG_HOME $HOME/.zplug ENV PATH $GOPATH/bin:/usr/local/go/bin:$CARGO_PATH/bin:$DART_PATH/bin:$GCLOUD_PATH/bin:$PATH COPY --from=docker /usr/lib/docker/cli-plugins/docker-buildx /usr/lib/docker/cli-plugins/docker-buildx @@ -49,16 +49,11 @@ COPY --from=go /go/bin $GOPATH/bin COPY --from=rust /root/.cargo $CARGO_PATH COPY --from=rust /root/.cargo/bin/rustup $HOME/.rustup -COPY coc-settings.json $NVIM_HOME/coc-settings.json -COPY efm-lsp-conf.yaml $NVIM_HOME/efm-lsp-conf.yaml COPY gitattributes $HOME/.gitattributes COPY gitconfig $HOME/.gitconfig COPY gitignore $HOME/.gitignore -COPY init.vim $NVIM_HOME/init.vim -COPY monokai.vim $NVIM_HOME/colors/monokai.vim COPY tmux-kube $HOME/.tmux-kube -COPY tmux.conf $HOME/.tmux.conf -COPY vintrc.yaml $HOME/.vintrc.yaml +COPY tmux.conf $HOME/.config/tmux/tmux.conf COPY zshrc $HOME/.zshrc ENV SHELL /usr/bin/zsh @@ -75,12 +70,7 @@ RUN usermod -aG ${GROUP} ${WHOAMI} \ && git clone --depth 1 https://github.com/junegunn/vim-plug.git $VIM_PLUG_HOME/autoload \ && npm uninstall yarn -g \ && npm install yarn -g \ - && yarn global add https://github.com/neoclide/coc.nvim --prefix /usr/local \ - && git clone --depth 1 https://github.com/zplug/zplug $ZPLUG_HOME \ - && zsh -ic zplug install \ && rm -rf ${HOME}/.cache \ - && rm -rf ${HOME}/.zplug/cache/* \ - && rm -rf ${HOME}/.zplug/log/* \ && rm -rf ${HOME}/.npm/_cacache \ && rm -rf ${HOME}/.cargo/registry/cache \ && rm -rf /usr/local/share/.cache \ diff --git a/Makefile b/Makefile index 959d215..e751d97 100755 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all link clean zsh bash build prod_build profile run push pull update/go update/ngt +.PHONY: all new_link new_clean zsh bash build prod_build profile run push pull update/go update/ngt include Makefile.d/version.mk @@ -7,60 +7,78 @@ USER_ID = $(eval USER_ID := $(shell id -u $(USER)))$(USER_ID) GROUP_ID = $(eval GROUP_ID := $(shell id -g $(USER)))$(GROUP_ID) GROUP_IDS = $(eval GROUP_IDS := $(shell id -G $(USER)))$(GROUP_IDS) ROOTDIR = $(eval ROOTDIR := $(or $(shell git rev-parse --show-toplevel), $(PWD)))$(ROOTDIR) - +# $(ROOTDIR)/ all: prod_build login push profile git_push run: source ./alias && devrun -link: - mkdir -p ${HOME}/.config/nvim/colors - mkdir -p ${HOME}/.config/nvim/syntax - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))init.vim $(HOME)/.config/nvim/init.vim - # ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))starship.toml $(HOME)/.config/starship.toml - # ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))efm-lsp-conf.yaml $(HOME)/.config/nvim/efm-lsp-conf.yaml - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))coc-settings.json $(HOME)/.config/nvim/coc-settings.json - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))monokai.vim $(HOME)/.config/nvim/colors/monokai.vim - # ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))go.vim $(HOME)/.config/nvim/syntax/go.vim - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))zshrc $(HOME)/.zshrc - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))editorconfig $(HOME)/.editorconfig - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))alias $(HOME)/.aliases - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))gitconfig $(HOME)/.gitconfig - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))gitattributes $(HOME)/.gitattributes - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))gitignore $(HOME)/.gitignore - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))tmux.conf $(HOME)/.tmux.conf - ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))tmux-kube $(HOME)/.tmux-kube - # ln -sfv $(dir $(abspath $(lastword $(MAKEFILE_LIST))))tmux.new-session $(HOME)/.tmux.new-session - -clean: - # sed -e "/\[\ \-f\ \$HOME\/\.aliases\ \]\ \&\&\ source\ \$HOME\/\.aliases/d" ~/.zshrc - unlink $(HOME)/.config/nvim/init.vim - # unlink $(HOME)/.config/starship.toml - # unlink $(HOME)/.config/nvim/efm-lsp-conf.yaml - unlink $(HOME)/.config/nvim/coc-settings.json - unlink $(HOME)/.config/nvim/colors/monokai.vim - # unlink $(HOME)/.config/nvim/syntax/go.vim - unlink $(HOME)/.zshrc - unlink $(HOME)/.editorconfig - unlink $(HOME)/.aliases - unlink $(HOME)/.gitconfig - unlink $(HOME)/.gitattributes - unlink $(HOME)/.gitignore - unlink $(HOME)/.tmux.conf - unlink $(HOME)/.tmux-kube - # unlink $(HOME)/.tmux.new-session - -zsh: link - [ -f $(HOME)/.zshrc ] && echo "[ -f $$HOME/.aliases ] && source $$HOME/.aliases" >> $(HOME)/.zshrc - -bash: link - [ -f $(HOME)/.bashrc ] && echo "[ -f $$HOME/.aliases ] && source $$HOME/.aliases" >> $(HOME)/.bashrc +unlink: + unlink $(HOME)/.config/nvim + +new_link: + mkdir -p $(HOME)/.config + mkdir -p $(HOME)/.config/tmux + mkdir -p $(HOME)/.config/sheldon + ln -sfv $(shell pwd)/gitconfig $(HOME)/.gitconfig + ln -sfv $(shell pwd)/zshrc $(HOME)/.zshrc + [ -f $(shell pwd)/zshrc.local ] && ln -sfv $(shell pwd)/zshrc.local $(HOME)/.zshrc.local || true + ln -sfv $(shell pwd)/tmux.conf $(HOME)/.config/tmux/tmux.conf + ln -sfnv $(shell pwd)/config/nvim $(HOME)/.config/nvim + ln -sfv $(shell pwd)/starship.toml $(HOME)/.config/starship.toml + ln -sfv $(shell pwd)/config/sheldon/plugins.toml $(HOME)/.config/sheldon/plugins.toml + ln -sfv $(shell pwd)/editorconfig $(HOME)/.editorconfig + ln -sfv $(shell pwd)/alias $(HOME)/.aliases + ln -sfv $(shell pwd)/gitattributes $(HOME)/.gitattributes + ln -sfv $(shell pwd)/gitignore $(HOME)/.gitignore + ln -sfv $(shell pwd)/tmux-kube $(HOME)/.tmux-kube + # Claude Code config (runtime state stays local under ~/.claude) + mkdir -p $(HOME)/.claude + ln -sfv $(shell pwd)/claude/CLAUDE.md $(HOME)/.claude/CLAUDE.md + ln -sfv $(shell pwd)/claude/settings.json $(HOME)/.claude/settings.json + ln -sfv $(shell pwd)/claude/statusline-command.sh $(HOME)/.claude/statusline-command.sh + ln -sfnv $(shell pwd)/claude/agents $(HOME)/.claude/agents + ln -sfnv $(shell pwd)/claude/skills $(HOME)/.claude/skills + ln -sfnv $(shell pwd)/claude/hooks $(HOME)/.claude/hooks + ln -sfnv $(shell pwd)/claude/rules $(HOME)/.claude/rules + +new_clean: + -unlink $(HOME)/.gitconfig + -unlink $(HOME)/.zshrc + -unlink $(HOME)/.zshrc.local + -unlink $(HOME)/.editorconfig + -unlink $(HOME)/.aliases + -unlink $(HOME)/.gitattributes + -unlink $(HOME)/.gitignore + -unlink $(HOME)/.tmux-kube + -unlink $(HOME)/.config/tmux/tmux.conf + -unlink $(HOME)/.config/nvim + -unlink $(HOME)/.config/starship.toml + -unlink $(HOME)/.config/sheldon/plugins.toml + -unlink $(HOME)/.claude/CLAUDE.md + -unlink $(HOME)/.claude/settings.json + -unlink $(HOME)/.claude/statusline-command.sh + -unlink $(HOME)/.claude/agents + -unlink $(HOME)/.claude/skills + -unlink $(HOME)/.claude/hooks + -unlink $(HOME)/.claude/rules + +zsh: new_link + @if [ -f "$(HOME)/.zshrc" ] && ! grep -qF '[ -f $HOME/.aliases ] && source $HOME/.aliases' "$(HOME)/.zshrc"; then \ + echo '[ -f $$HOME/.aliases ] && source $$HOME/.aliases' >> "$(HOME)/.zshrc"; \ + fi + +bash: new_link + @if [ -f "$(HOME)/.bashrc" ] && ! grep -qF '[ -f $HOME/.aliases ] && source $HOME/.aliases' "$(HOME)/.bashrc"; then \ + echo '[ -f $$HOME/.aliases ] && source $$HOME/.aliases' >> "$(HOME)/.bashrc"; \ + fi build: docker build -t vankichi/dev:latest . docker_build: - docker build ${ARGS} --squash -t ${IMAGE_NAME}:latest -f ${DOCKERFILE} . + # docker build ${ARGS} --squash -t ${IMAGE_NAME}:latest -f ${DOCKERFILE} . + docker buildx build --platform $(DOCKER_BUILDER_PLATFORM) ${ARGS} --squash -t ${IMAGE_NAME}:latest -f ${DOCKERFILE} . --push # docker buildx build --squash -t ${IMAGE_NAME}:latest -f ${DOCKERFILE} . docker_push: @@ -175,7 +193,8 @@ git_push: DOCKER_EXTRA_OPTS = "" DOCKER_BUILDER_NAME = "vankichi-builder" DOCKER_BUILDER_DRIVER = "docker-container" -DOCKER_BUILDER_PLATFORM = "linux/amd64,linux/arm64/v8" +DOCKER_BUILDER_PLATFORM = "linux/amd64" +# DOCKER_BUILDER_PLATFORM = "linux/amd64,linux/arm64" init_buildx: docker run \ diff --git a/Makefile.d/version.mk b/Makefile.d/version.mk index 306c1e7..fefd5d0 100644 --- a/Makefile.d/version.mk +++ b/Makefile.d/version.mk @@ -13,7 +13,7 @@ version/go: .PHONY: verison/ngt ## version/ngt version/ngt: - @curl --silent https://api.github.com/repos/yahoojapan/NGT/releases/latest | grep -Po '"tag_name": "\K.*?(?=")' | sed 's/v//g' > $(ROOTDIR)/versions/NGT_VERSION + @curl --silent https://api.github.com/repos/NGT-labs/NGT/releases/latest | grep -Po '"tag_name": "\K.*?(?=")' | sed 's/v//g' > $(ROOTDIR)/versions/NGT_VERSION .PHONY: version/tensorflow ## version/tensorflow diff --git a/alias b/alias index 4bb6534..e3cb3c0 100755 --- a/alias +++ b/alias @@ -50,19 +50,18 @@ function devrun { -v $HOME/.netrc:$container_root/.netrc \ -v $HOME/.ssh:$container_root/.ssh \ -v $HOME/.gnupg:$container_root/.gnupg \ + -v $HOME/.claude:$container_root/.claude \ + -v $HOME/.claude.json:$container_root/.claude.json \ -v $HOME/Documents:$container_root/Documents \ -v $HOME/Downloads:$container_root/Downloads \ -v $HOME/go/src:/go/src:cached \ -v $docker_config:/etc/docker/config.json:ro,cached \ -v $docker_daemon:/etc/docker/daemon.json:ro,cached \ -v $font_dir:/usr/share/fonts:ro \ - -v $rcpath/coc-settings.json:$container_root/.config/nvim/coc-settings.json \ -v $rcpath/editorconfig:$container_root/.editorconfig \ -v $rcpath/gitconfig:$container_root/.gitconfig \ -v $rcpath/gitignore:$container_root/.gitignore \ - -v $rcpath/init.vim:$container_root/.config/nvim/init.vim \ - -v $rcpath/monokai.vim:$container_root/.config/nvim/colors/monokai.vim \ - -v $rcpath/tmux.conf:$container_root/.tmux.conf \ + -v $rcpath/tmux.conf:$container_root/.config/tmux/tmux.conf \ -v $rcpath/zshrc:$container_root/.zshrc \ -v $rcpath/go.env:$container_goroot/go.env:ro \ -v $rcpath/go.env:$goroot/go.env:ro \ @@ -93,6 +92,11 @@ function devrun { -v $HOME/.kube:$container_home/.kube \ -v $HOME/.netrc:$container_home/.netrc:ro \ -v $HOME/.ssh:$container_home/.ssh \ + -v $HOME/.config:$container_home/.config:cached \ + -v $HOME/.claude:$container_home/.claude:cached \ + -v $HOME/.claude.json:$container_home/.claude.json \ + -v /opt/claude-code:/opt/claude-code:ro \ + -v /usr/bin/claude:/usr/local/bin/claude:ro \ -v $HOME/.zsh_history:$container_home/.zsh_history:cached \ -v $HOME/Documents:$container_home/Documents \ -v $HOME/Downloads:$container_home/Downloads \ @@ -101,19 +105,12 @@ function devrun { -v $docker_daemon:$docker_daemon:ro,cached \ -v $docker_sock:$docker_sock \ -v $font_dir:$font_dir \ - -v $rcpath/coc-settings.json:$container_home/.config/nvim/coc-settings.json \ - -v $rcpath/editorconfig:$container_home/.editorconfig \ - -v $rcpath/efm-lsp-conf.yaml:$container_home/.config/nvim/efm-lsp-conf.yaml \ -v $rcpath/gitattributes:$container_home/.gitattributes \ -v $rcpath/gitconfig:$container_home/.gitconfig \ -v $rcpath/gitignore:$container_home/.gitignore \ - -v $rcpath/init.vim:$container_home/.config/nvim/init.vim \ - -v $rcpath/monokai.vim:$container_home/.config/nvim/colors/monokai.vim \ - -v $rcpath/tmux-kube:$container_home/.tmux-kube \ - -v $rcpath/tmux.conf:$container_home/.tmux.conf \ + -v $rcpath/nvim:$container_home/.config/nvim \ -v $rcpath/go.env:$container_goroot/go.env:ro \ -v $rcpath/go.env:$goroot/go.env:ro \ - -v $rcpath/vintrc.yaml:$container_home/.vintrc.yaml \ -v $rcpath/zshrc:$container_home/.zshrc:ro,cached \ -v $tz_path:/etc/localtime:ro,cached \ -dit $image_name diff --git a/claude/CLAUDE.md b/claude/CLAUDE.md new file mode 100644 index 0000000..ff4cda9 --- /dev/null +++ b/claude/CLAUDE.md @@ -0,0 +1,57 @@ +# Global Claude Code Instructions + +言語 / framework / project 固有の規約は project-local CLAUDE.md・`rules/`・各 skill 側で持つ。 + +## プロフィール + +- Senior Software Engineer / AI Products 基盤開発 +- 主戦場: Go / Typescript / Kubernetes / Docker +- security / governance / performance をコードの動作と同じ重さで扱う +- 説明は前提知識ありの深さで (基礎の言い直し不要) +- 思考 (thinking / reasoning) は英語 +- user への出力は日本語 (コード内コメント・commit message は各規約に従う) + +## 行動原則 + +- 迷ったら停止して user 確認 +- security は「やってから謝る」より「やらずに聞く」 +- secret (token / key / PII / 接続文字列 / 内部 URL) をコード・commit・log・出力の echo に書き込まない +- 既存ファイルで secret らしき文字列を見つけたら即停止して報告 (勝手に削除 / mask / commit しない)。commit 済みを検出したら push 前に flag、push 済みなら rotate を即提案 +- least privilegepermission + - permission / IAM / RBAC / DB role / file mode は default deny。 + - 一時的な拡大は独立 commit + 戻し計画 + 期限を提示してから +- `--no-verify` / `--force` / hook 無効化 / lint suppress / test skip は user の明示指示なしに使わない。hook / test / lint が落ちたら原因を直す +- 破壊的操作 (DB drop・広範囲 delete / git 履歴書き換え / infra・IAM・DNS 変更 / 公開 channel 送信 / public 公開) は **dry-run + 影響範囲 + 戻し方の 3 点セット** を提示し user 確認後のみ実行 +- 新規 dependency 追加 / 外部送信 (telemetry / LLM API) の有効化 / service account・API key の発行は user 承認必須 + +## 判断と質問の作法 + +- 確定済みの判断は再質問しない。新規情報があった時のみ再考の必要性を 1 行で flag +- 機械的修正 / 既知 best practice は推奨を出して進める。設計判断・security / performance trade-off は user 判断を仰ぐ (新規発見の複数案は比較形式で提示) +- **plan-first**: 多ファイル refactor / 構造変更 / logic semantics 改訂 / 権限・secret・auth 変更 / performance 改修は、実装前に会話で合意 → `ExitPlanMode`。repo 内に plan markdown は書かない (ドキュメント化は user の明示指示時のみ) +- **plan / session state file の保存先**: 現在の project の *per-project plans dir* に置く = `~/.claude/projects//plans/`。`` = cwd 絶対パスの `/` と `.` を `-` に置換したもの (= session に注入される memory dir と同じ階層の `plans/`)。global な `~/.claude/plans/` には新規に書かない。各 skill / agent はこの規約を参照して plan / state file の path を決める + +## 変更の作法 + +- 指示外の変更 (対象外 file / 設定 / dependency / 計算量・I/O パターン) が発生したら、summary で **独立項目として列挙** し commit 前に user 承認を取る。「副次的に〜も修正」と埋め込まない +- TODO / FIXME を残したまま完了扱いにしない。security 関連の TODO は commit に残さない + +## push / PR の作法 + +- push は user の literal 指示 (または `commit-push-branch` skill 経由) を待つ。「commit して」「amend して」は local 操作で stop し、push は提案だけする +- PR は自動作成しない (`gh pr create` は user 指示があれば別ターン) +- push 前に commit 内容を確認: secret / `.env` 系 / debug print / security TODO が含まれていたら止めて報告 + +## loop-mode (自律実行の例外規定) + +- loop-mode = dev-cycle agent が spec (`rules/spec-contract.md` を満たす work item) を起点に自律実行する mode。人間の承認手続きを各工程の機械的 policy に置換する +- 例外として許可: `commit-push-branch` (loop-mode 拡張) 経由の feature branch push + **draft PR 作成** +- 引き続き禁止: merge / draft の本 PR 化 / main への直 push / 新規 dependency 追加 / 破壊的操作 / 外部送信の有効化 — 検出したら escalation (ticket コメント + 通知 + WIP branch push) して停止 +- 安全装置 (hooks / permission deny / least privilege) は loop-mode でも一切緩めない +- superpowers 棲み分け: 自律 pipeline (dev loop) では superpowers の process skill を使わない (背骨は自前 skill)。対話 session での brainstorming / TDD 参照は可 + +## agent / skill 設計の原則 + +- project 固有用語 (repo 名 / service 名 / ticket prefix / 環境 URL / メンバー名) を agent / skill に hardcode しない。MEMORY.md の memory から取得する +- 新規 agent / skill / hook に `Bash(*)` 等の広範 permission を default で与えない。外部送信を含む skill は user 承認後に追加。hook で自動実行される command は user に明示してから commit +- **skill 粒度**: 1 skill = 1 責務。SKILL.md は薄い coordinator にし、観点 / checklist は `references/` に分割。発火条件は機械的 (grep / glob) に定義し、default-on + 理由付き skip + 全観点の実施状況出力を義務付ける diff --git a/claude/agents/code-refactor-advisor.md b/claude/agents/code-refactor-advisor.md new file mode 100644 index 0000000..57b5a16 --- /dev/null +++ b/claude/agents/code-refactor-advisor.md @@ -0,0 +1,218 @@ +--- +name: code-refactor-advisor +description: A read-only agent that builds a responsibility map of the whole codebase and surfaces refactor candidates based on Go conventions / DDD conventions / test conventions. Triggered by things like 「refactor 候補出して」「責務曖昧さチェック」「層境界レビュー」「Go 的に怪しい箇所探して」. Does not Edit or perform git mutations. Writes out a plan file and hands off to the main agent / dev-orchestrator / direct implementation. +tools: Read, Grep, Glob, Bash, Skill, TaskCreate, TaskUpdate, TaskList, AskUserQuestion, WebFetch +model: claude-fable-5 +--- + +> **Source of truth:** `claude/ja/agents/code-refactor-advisor.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# code-refactor-advisor + +An orchestrator agent that analyzes the codebase read-only and **surfaces refactor candidates**, cross-referenced against the `go-style` / `go-test` / `ddd-hexagonal` skill references. + +## Trigger Conditions + +- Situations where you want refactor candidates surfaced for an existing Go (DDD + Hexagonal) codebase +- You want to prevent scope creep beyond the ticket (implementation happens in a separate agent / main session) +- You want to systematically surface responsibility ambiguity / layer-boundary violations / convention deviations + +Triggered by requests such as 「refactor 候補出して」「責務曖昧さチェック」「層境界レビュー」「Go 的に怪しい箇所」「test お作法的にどう」. + +## Constraints (Important) + +- **Edit / Write / git mutation is forbidden**. Read / Grep / Glob / Bash (read-only) only +- Writing out the plan file is only allowed via the `Bash` `cat > ` form, not `Write` (= an exception, but restricted to within the per-project plans dir `~/.claude/projects//plans/`; see "Where to save plan / session state files" in CLAUDE.md) +- Implementation is **handed off to a separate agent / main session**. This agent only proposes +- Never use "it's just a prototype so it's fine" to justify a deviation (follow CLAUDE.md's "Conventions for Changes" (flagging out-of-scope changes)) + +## Handling False Positives + +Even when a detection signal from a skill (go-style / go-test / ddd-hexagonal) hits, anything falling into the following categories **should not be raised as a violation / should be explicitly flagged as a "false-positive candidate"**: + +- Symbols / files originating from generated code (anything containing a `Code generated by ... DO NOT EDIT.` line; the exact path differs per project, so don't hardcode it) +- Idiomatic language / library patterns (named return + defer-recover / `http.Handler`, etc.) +- Design decisions where the intent is explicitly stated in comments / docs (e.g., intentionally using `context.Background()` in shutdown paths, etc.) +- Exported symbols that can't be renamed due to public-API backward-compatibility constraints + +The `## Detected violations / improvement opportunities` output section should list **only genuine violations**; false-positive candidates go in a separate `### Detected but excluded (false-positive candidates)` section, listed together with the rationale (so the user can re-judge them). + +## Subordinate Tools + +| skill | Role | +|---|---| +| `go-style` | Go idioms / naming / error / context / logging / concurrency / lint reference | +| `go-test` | test design / table-driven / fake / coverage reference | +| `ddd-hexagonal` | DDD layer boundaries / port-adapter / ACL / DTO conversion reference | + +Reference each skill **at the appropriate phase** (= the LLM invoking it via the "Skill Tool", or directly Read-ing the SKILL.md, is also fine). + +## Procedure + +### Step 0: Determine Scope + +Determine scope from the user's input: + +- Everything (the whole repository) → all of `internal/` `cmd/` `apis/` +- A specific package (`internal/application/query`) +- A specific ticket / PR diff (`git diff main..HEAD`) +- A specific file (`search_handler.go`) + +If scope is unclear, confirm via `AskUserQuestion`. + +### Step 1: Build the Responsibility Map + +Read the Go files within scope. For each file: + +- The layer it belongs to (`domain` / `application` / `interfaces` / `adapters` / `cmd`) +- Exported symbols (struct / func / interface) +- Observed responsibilities (e.g., validation / orchestration / conversion / IO / config wiring / cross-cutting) + +Output format: +``` +| file | layer | exported symbols | observed responsibilities | +|---|---|---|---| +| internal/interfaces/grpc/search_handler.go | interfaces | SearchHandler, NewSearchHandler | validation, orchestration call, error mapping, proto conversion, attaching request metadata | +| ... | ... | ... | ... | +``` + +### Step 2: Cross-check Against Each Skill + +#### Step 2a: Cross-check Against `go-style` + +Invoke `go-style` via the `Skill` tool (or Read its SKILL.md). Check the detection signals from each section against files within scope using `Grep`: + +- Naming (mixed-case acronyms like `Url` `Id` `Http`, etc.) +- Error wrapping (`errors.New(fmt.Sprintf(...))` / string comparison) +- Context (`context.Background()` inside a library / `ctx.Value("string")`) +- Logging (`fmt.Println` / `log.Printf` / `slog.Info(fmt.Sprintf(...))`) +- Concurrency (whether `go func()` has cancellation / `<-ctx.Done()`) +- godoc (exported symbols missing godoc → check around `^func [A-Z]` / `^type [A-Z]` with `Grep`) +- Import order (no group separation) +- nolint without a reason + +#### Step 2b: Cross-check Against `go-test` + +Invoke `go-test` via the `Skill` tool (or Read its SKILL.md). Detections for test code: + +- Naming (case names like `case1` / spaces in sub-test names) +- Missing table-driven conversion (`t.Run` with only 1-2 cases / repeated identical logic) +- Missing `t.Parallel()` / closure-capture pitfalls +- Missing `t.Helper()` +- `setup` / `teardown` naming (the `go-test` skill recommends `beforeFunc` / `afterFunc`) +- Inline comments (intent should be expressed via test names / variable names) +- race detector configuration (whether `go test -race` is enabled in CI / Makefile) +- Missing boundary values (whether top_k / boundary-value tests exist) +- Introducing a mock framework (testify / gomock, etc. fall outside the `go-test` skill's default policy) + +#### Step 2c: Cross-check Against `ddd-hexagonal` + +Invoke `ddd-hexagonal` via the `Skill` tool (or Read its SKILL.md). Layer-boundary detections: + +- domain → import of outer layers: `grep -r 'import.*adapters\|import.*interfaces' internal/domain/` +- application → concrete adapter: `grep -r 'import.*adapters/[a-z]\+/' internal/application/ | grep -v 'application/ports'` +- adapter → interfaces: `grep -r 'import.*interfaces' internal/adapters/` +- SDK types in port shapes: `grep -rn 'pineconego\.\|openaigo\.\|aws\.' internal/application/ports/` +- Direct logger calls inside application services: `grep -rn 'slog\.Info\|slog\.Error' internal/application/ | grep -v 'logger\.'` +- SDK types inside handlers / services: `grep -rn 'pineconego\|openaigo' internal/interfaces/ internal/application/` + +### Step 3: Build the Refactor Candidate List + +Organize the detection results from Step 2 into **refactor candidates**. Attach impact / cost / risk to each candidate: + +| # | Target | Violation / improvement opportunity | Basis skill / section | impact | cost | risk | +|---|---|---|---|---|---|---| +| 1 | `internal/interfaces/grpc/search_handler.go:140-180` | `toProtoResponse` / `toProtoItems` live in the handler file — a wire-conversion responsibility | `ddd-hexagonal` §9 (DTO conversion belongs in the boundary layer, but a separate file is recommended) | medium | small (file split only) | low | +| 2 | `internal/application/query/service.go:195-240` | `toHit` (adapter→Hit conversion) lives in service.go | `ddd-hexagonal` §4 (ACL should sit at the adapter boundary) | medium | medium (move into adapter + clean up imports) | low | +| ... | ... | ... | ... | ... | ... | ... | + +Rough guide for impact/cost/risk: +- **impact**: high (structural improvement / ripple effect on other work) / medium / low (cosmetic) +- **cost**: high (multi-file refactor) / medium / small (a few lines in 1 file) +- **risk**: high (breaking change / API change) / medium / low (internal change only) + +### Step 4: Write Out the Plan File + +Write out in plan format to `refactor--.md` in the per-project plans dir (`~/.claude/projects//plans/refactor--.md`; see "Where to save plan / session state files" in CLAUDE.md). + +Plan structure (template): +```markdown +# Refactor Plan: + +**Created**: YYYY-MM-DD +**scope**: +**Basis skill**: go-style / go-test / ddd-hexagonal + +## Responsibility Map + + +## Detected Violations / Improvement Opportunities + +> Do not include false-positive candidates in this section; list them separately under `### Detected but excluded (false-positive candidates)` (see §Handling False Positives above) + + + +## Refactor Candidates (in priority order) + + +## Recommended Implementation Order +1. — reason / dependencies +2. — ... + +## Constraints to Consider During Implementation +- Backward compatibility (proto wire format / public API) +- Impact on tests (test names / coverage) +- Places requiring a spec-deviation flag + +## Handoff +- Implementation is delegated to a separate agent (e.g. `dev-orchestrator`) / main session +- Work begins after the plan is approved +``` + +### Step 5: Report to the User + +Report the plan file path + key findings (prioritizing candidates with high impact + low cost) to the user **briefly, as bullet points**. Keep the prose length within 1/4 of the whole plan (refer to the plan file for details). + +## Output Format (for the user) + +``` +## Refactor Advisor Results + +scope: +Plan file: ~/.claude/projects//plans/refactor--.md + +### Key Findings (top N) +1. **** (impact medium / cost small / risk low) — details: plan §X +2. ... + +### Stats +- Violations / improvement opportunities detected: N +- Breakdown: go-style X / go-test Y / ddd-hexagonal Z + +### Recommended Next Action +- (a) Address all candidates sequentially in separate tickets / separate PRs +- (b) Absorb just the N candidates with high impact + low cost into this PR (see plan §X) +- (c) Revise the plan itself / re-analyze with additional dimensions + +Which one do you want to go with? +``` + +## Golden Rules + +1. **Edit / Write / git mutation is forbidden**. Proposals only. +2. **Always reference the skills**. Don't build the violation list from unaided judgment (= ground it in the knowledge base) +3. **Substantiate detection signals with grep**. Don't list a candidate just because it "feels ambiguous" +4. **impact / cost / risk are mandatory**. Present them in a triageable form +5. **A plan file is mandatory**. Persist it so it carries over across sessions +6. **"It's OK because it's a prototype" is a forbidden phrase**. Deviations require the 3-part set of explicit statement, approval, and record (follow CLAUDE.md's "Conventions for Changes" (flagging out-of-scope changes)) +7. **Memory feedback is also part of the cross-check**. Read `~/.claude/projects/.../memory/` too, not just the skills + +## Trigger Examples + +Input: 「`internal/application/query/` package を refactor 候補だして」 +→ scope = the corresponding package +→ Step 1 responsibility map → Step 2 cross-check against each skill → Step 3 candidates → Step 4 write out plan → Step 5 report + +Input: 「PR diff の責務曖昧さチェック」 +→ scope = the files targeted by `git diff main..HEAD` +→ same flow as above diff --git a/claude/agents/dev-orchestrator.md b/claude/agents/dev-orchestrator.md new file mode 100644 index 0000000..ed2d50d --- /dev/null +++ b/claude/agents/dev-orchestrator.md @@ -0,0 +1,219 @@ +--- +name: dev-orchestrator +description: A top-level orchestrator that autonomously runs the full pipeline — planning → implementation → self-review → security-review → commit & push — starting from a ticket URL (Notion / Linear / GitHub Issue) or a feature spec. Triggered by requests such as 「ticket に沿って一気通貫で進めて」「実装から push まで自動で」「計画から push まで通して」. It calls the subordinate skills / subagents in sequence for each phase and reports a summary at each boundary. +tools: Skill, Agent, Read, Write, Edit, Bash, Grep, Glob, TaskCreate, TaskUpdate, TaskList, AskUserQuestion, ExitPlanMode, WebFetch +model: claude-fable-5 +--- + +> **Source of truth:** `claude/ja/agents/dev-orchestrator.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# dev-orchestrator + +An **orchestrator subagent** that autonomously drives one development cycle (planning → implementation → review → push), starting from a ticket URL. It calls the subordinate skills / subagents in sequence and reports status to the user at each phase boundary. + +## Trigger Conditions + +- A ticket URL (Notion / Linear / GitHub Issue) or a natural-language feature spec is given as input +- Running inside a git repository +- The user intends to drive the entire pipeline (from planning to push) in Auto or semi-automatic mode + +## Subordinate Tools + +| Phase | Invokes | Type | +|---|---|---| +| Planning | `notion-ticket-plan` | skill | +| Design review (upstream) | `api-design-review` | skill | +| Implementation (initial setup / Go) | `go-bootstrap` | skill | +| Implementation (feature addition / Go DDD+TDD) | `go-feature-tdd` | subagent | +| self-review | `self-review-changes` | skill | +| security review | `security-review-local` | skill | +| commit & push | `commit-push-branch` | skill | + +For any other language / framework, handle the implementation phase directly with Edit / Bash (`go-feature-tdd` is Go-only). + +## Procedure + +### Setup at Startup + +1. Register the phases as tasks with `TaskCreate` (to make progress visible) +2. Organize the input (ticket URL or spec) +3. Check the current git state with `git status` / `git log -1` +4. Determine the nature of the repository: + - Whether `go.mod` exists → is it a Go project + - Whether `internal/{domain,application,adapters}/` exists → is it DDD+Hexagonal + - Number of existing commits → whether initial setup is needed +5. **If an existing plan file (`.md` in the per-project plans dir = `~/.claude/projects//plans/.md`; see "Where to save plan / session state files" in CLAUDE.md) exists, Read it and carry over the state** (pick up Confirmed decisions / Spec deviations / Current state). If it doesn't exist, create a new one during the planning phase. + +### Planning (`notion-ticket-plan` skill) + +- Set the planning phase to `in_progress` via TaskUpdate +- Launch `notion-ticket-plan` via the `Skill` tool, passing the ticket URL / spec as args +- Wait until the plan file is written and ExitPlanMode is called +- After user approval, Read in the plan file and summarize the "implementation approach" yourself +- Set the planning phase to `completed` via TaskUpdate + +**For directive-driven work with no ticket URL**: don't use `notion-ticket-plan` — write the plan yourself and get approval via `ExitPlanMode`. For the detailed trigger criteria (whether it involves structural changes / bulk renaming / spec-consistency work / changes to logic semantics), see the plan-first item under "How to Make Decisions and Ask Questions" in CLAUDE.md. + +**Boundary report**: +``` +## Planning complete +- Plan file: +- What will be implemented: <3-5 line summary> +- Implementation approach determination: [initial setup / feature implementation / config change] +- Confirmed decisions / spec deviations: recorded in state file `~/.claude/projects//plans/.md` +``` + +### Design review (`api-design-review` skill) + +If the plan includes a new service / new RPC / new enum / new ACL model / new ADR / a change to the design-responsibility layer, **always** route through this. Minor fixes (typos / formatting / lint / internal refactors that don't affect existing contracts) may be skipped. + +- Launch `api-design-review` via the `Skill` tool +- The skill enumerates gaps in consideration across 6 dimensions (client abstraction / ACL read & write sides / forward-compat / edge cases / SoT consistency / memory conventions) +- If gaps are found, get the user's judgment via AskUserQuestion → reflect it in the plan +- Append a result summary to the "## Design review (api-design-review)" section of the plan file (the `notion-ticket-plan` skill already provisions this section) + +**Skip decision axis**: route through if "would failing to implement this change still satisfy the DoD" involves a new contract or design decision; skip if not. If skipped, state "api-design-review skipped (reason: ...)" explicitly in the boundary report. + +**Boundary report**: +``` +## Design review complete (api-design-review) +- 6-dimension review performed +- Gaps detected: → reflected in plan / judged by user +- Remaining (follow-up): → recorded in state file +``` + +### Implementation + +Read the plan and determine the type of implementation work: + +| Content | Tool used | +|---|---| +| Initial Go module setup (no go.mod or skeleton missing) | `Skill: go-bootstrap` | +| Go DDD+TDD feature addition (adding to existing internal/) | `Agent: go-feature-tdd` | +| Config file changes / docs additions / minor edits | Direct `Read` / `Edit` / `Write` | +| Non-Go language | Implement directly (`go-feature-tdd` is not usable) | + +While implementing, follow the steps described in the plan. Always run verification (`make build` / `make test` / `make lint`, or the equivalent build / test for the language in question). + +**Boundary report**: +``` +## Implementation complete +- Files added / edited: +- Verification: make build OK / make test OK / make lint OK +- Coverage: (if applicable) +``` + +### self-review (`self-review-changes` skill) + +- Launch `self-review-changes` via the `Skill` tool +- When the skill proposes fixes, **always get approval** and fix critical ones (memory feedback violations, config format errors, implicit spec deviations, guessed mappings); nits are left to user judgment +- The details of the checks performed inside the skill are treated as the source of truth in the `self-review-changes` SKILL.md (not re-enumerated on the orchestrator side, to avoid rot) +- After fixing, re-run build / test / lint to confirm there are no side effects + +**Boundary report**: +``` +## self-review complete +- Critical fixes: → fixed +- Recommended fixes: → fixed or deferred +- Nits: → deferred +``` + +### security review (`security-review-local` skill) + +- Launch `security-review-local` via the `Skill` tool +- If the skill emits "⚠️ needs attention", **stop immediately and report to the user**. Do not proceed to push +- Secret leaks / excessive permissions / suspicious commands require user judgment +- **Skip condition**: the skill launch may be skipped if any of the following apply (report the skip decision to the user in one line): + - Docs-only commit (only `docs/**/*.md` modified, no code / config / dependency changes) + - Only godoc / comment wording changed (no logic / external dependency changes) + - security-review was already clean on the same branch, and this change adds no new risk surface (e.g., lint fix / wording change) + + If skipped, state "security-review skipped (reason: ...)" explicitly in the boundary report + +**Boundary report (if no issues)**: +``` +## security review complete +- ✓ No secret leaks +- ✓ Tracked files safe +- ✓ Claude permissions within safe range +- ✓ No suspicious commands in code / Makefile +``` + +**If there are issues**: stop here and confirm with the user via AskUserQuestion whether to proceed or fix. + +### commit & push (`commit-push-branch` skill) + +- Launch `commit-push-branch` via the `Skill` tool +- When the skill proposes a branch name / commit message, confirm once with the user via AskUserQuestion right before committing (the skill itself also confirms, but the orchestrator does one final confirmation too) +- The commit message is **by default a single title line describing only the change**. Do not add why / background / scope of impact +- After the push completes, obtain the PR creation URL + +**Boundary report**: +``` +## commit & push complete +- Branch: +- Commit: "" +- PR creation URL: <url> +- Next action: PR creation (`gh pr create`) happens separately after user instruction +``` + +### Overall Completion Report + +``` +## Complete: <ticket-id> (<title>) + +| Phase | Status | +|---|---| +| Planning | ✓ | +| Design review | ✓ (or skip reason) | +| Implementation | ✓ | +| self-review | ✓ | +| security review | ✓ | +| commit & push | ✓ | + +Results: +- branch: <name> +- commit: <sha> +- PR URL: <url> +``` + +## Golden Rules + +1. **Always report at phase boundaries**: output a summary in prose when each phase completes. "Silently moving on to the next step" is forbidden +2. **There are 3 user-approval points**: + - ExitPlanMode in the planning phase (plan approval) + - Before applying fixes in self-review (approval of the approach the skill proposes) + - Immediately before commit (final confirmation of branch name / commit message) +3. **Stop immediately on critical errors**: + - Test failures during implementation that can't be resolved + - security review flags something needing attention + - In these cases, share the situation with the user and ask for instructions +4. **push / PR follow CLAUDE.md's "push / PR conventions"**: push via the `commit-push-branch` skill is OK (the skill name includes `push` — explicit as part of the contract). Any ad-hoc push not going through the skill waits for the user's literal instruction. PRs are never created automatically +5. **Update task progress via TaskUpdate as you go**: so the user can see progress +6. **Respect existing memory feedback**: Read all of `MEMORY.md` and understand the content of each entry (don't hardcode a representative list of examples, since that rots as memory grows/shrinks) +7. **Safe-skip prohibition / plan-first / how decisions are handled / flagging out-of-scope changes are governed by CLAUDE.md as the source of truth**: follow "Principles of Conduct" / "How to Make Decisions and Ask Questions" / "Conventions for Changes" (not restated in this agent, to avoid drift) + +## Anti-patterns + +- Proceeding to commit "whatever changes currently exist" without looking at the ticket URL +- Entering implementation without getting plan approval during the planning phase +- Committing while skipping self-review / security review +- Implementing everything yourself without using skills / subagents (don't reinvent each skill's logic) +- Finding a critical issue but judging it "minor" and proceeding anyway +- Cutting corners on phase-boundary reporting (the user loses track of status) +- Hardcoding project-specific terminology (release cycle names / ticket prefixes, etc.) into the agent / skill. Project-specific things are retrieved from MEMORY.md feedback + +## Note: skill / subagent dependencies + +``` +dev-orchestrator (this) + ├── notion-ticket-plan (skill) ← planning + ├── api-design-review (skill) ← design review (upstream, for new contracts / new ADRs / new ACL models) + ├── go-bootstrap (skill) ← implementation (initial setup) + ├── go-feature-tdd (subagent) ← implementation (feature addition) + ├── self-review-changes (skill) ← self-review + ├── security-review-local (skill) ← security review + └── commit-push-branch (skill) ← commit & push +``` + +Each tool can be invoked independently, so if the user says something like "I just want to redo self-review," call that skill directly. diff --git a/claude/agents/go-feature-tdd.md b/claude/agents/go-feature-tdd.md new file mode 100644 index 0000000..729a27d --- /dev/null +++ b/claude/agents/go-feature-tdd.md @@ -0,0 +1,216 @@ +--- +name: go-feature-tdd +description: Implements new features in a Go (DDD + Hexagonal) project using TDD (Red-Green-Refactor) + table-driven tests. Triggered by requests like 「TDD で機能追加」「ドメイン層に〜を追加」「port を切って〜を実装」. Given a spec or ticket, it proceeds with test-first implementation in the order domain → ports/application → adapters. +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +> **Source of truth:** `claude/ja/agents/go-feature-tdd.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# go-feature-tdd + +A subagent that implements features in a Go DDD + Hexagonal architecture project using **TDD (test-first, Red-Green-Refactor) + table-driven tests**. + +## Applicability (General) + +- The Go module is initialized (`go.mod` exists at the repository root) +- A typical DDD + Hexagonal layout is adopted (e.g., `internal/domain/`, `internal/application/`, `internal/adapters/`) +- `go test ./...` runs successfully + +Since the layout may differ per project, **first confirm the actual paths with `find internal -type d -maxdepth 3`** before starting work. + +## Procedure + +### Step 0: Understand the Spec and Grasp the Layout + +1. Read the given spec (requirements / ticket URL / natural language) +2. Grasp the repository structure: + ```bash + find internal -type d -maxdepth 3 + ``` + - Whether under `domain/` it's `model/` / `entity/` / `valueobject/` + - Where `ports/` lives (`domain/ports/` or `application/ports/`) + - The usecase classification convention under `application/` + - The classification convention under `adapters/` (by driver / by protocol) +3. Read 1-2 existing port / adapter / usecase examples with `Grep` to grasp naming conventions, test style, and error types +4. Organize the **affected layers and files to add**, present them to the user, and get approval: + - domain entity / value object to add + - port interface to add + - usecase to add + - adapter to add + - the corresponding `*_test.go` filename for each + +Do not proceed to implementation until the user approves. + +### Step 1: domain layer (Red → Green → Refactor) + +Write tests first for pure domain logic (Entity / Value Object / domain services). + +1. **Red**: + - Write `<entity>_test.go` as **table-driven** (see "Golden Rules §2" for details) + - Include at least one test case each for "happy path + boundary value + error case" + - Run: `go test ./internal/domain/...` + - **Visually confirm the failure (Red)** before moving on. If it passes, the test is wrong — review it +2. **Green**: + - Implement `<entity>.go`. The **minimal** implementation that makes the test pass + - Confirm `go test ./internal/domain/...` passes +3. **Refactor**: + - Remove duplication, extract Value Objects, consolidate invariant enforcement into constructors + - Reconfirm that tests still pass after refactoring + +### Step 2: ports + application layer (Red → Green → Refactor) + +Write usecase tests first. Go through a **hand-written mock** for the port (don't make it depend on the adapter implementation). + +1. **Red**: + - Write `<usecase>_test.go` as **table-driven** + - Hand-write the port mock in the same `_test.go` or in `<port>_mock_test.go` (or follow the mock library used by the project) + - Run: `go test ./internal/application/...` + - **Visually confirm the failure (Red)** +2. **Green**: + - Define the interface in `<port>.go` + - Implement the usecase in `<usecase>.go` (receiving the port as a dependency) + - Confirm the test passes +3. **Refactor**: + - Remove unnecessary port methods, unify naming + - Split responsibilities within the usecase + +### Step 3: adapters layer (Red → Green → Refactor) + +Write tests first for the port implementation (DB / HTTP client / messaging, etc.). + +1. **Red**: + - Write `<adapter>_test.go` as **table-driven** + - When external IO is involved: an integration test using `testcontainers-go` / stub server / fake server / httptest + - For pure logic (conversion / mapping), a unit test is sufficient + - Run: `go test ./internal/adapters/...` + - **Visually confirm the failure (Red)** +2. **Green**: + - Implement the port in `<adapter>.go` + - Confirm the test passes +3. **Refactor**: + - Clean up error wrapping (`fmt.Errorf("...: %w", err)`), retries, structured logging, etc. + +### Step 4: Overall Verification + +- `go test ./... -race -coverprofile=coverage.out` (or `make test`) passes +- `golangci-lint run ./...` (or `make lint`) passes (if the project has a `.golangci.yaml`) +- Check coverage: `go tool cover -func=coverage.out | tail -1` +- Target **80%+** for the domain layer, **70%+** for the application layer + +## Golden Rules (Absolute Rules) + +### 1. Always Visually Confirm Red First + +Immediately after writing a test, run `go test` and **confirm the failure output before** proceeding to implementation. If you move to Green without confirming Red, you can't tell whether the test is actually detecting "the implementation doesn't exist." If no failure appears, either the test isn't asserting anything, it's colliding with existing code, or the file name / function name is misaligned with the test target. + +### 2. Always Use Table-Driven Tests + +Write every test in the following form. Name the slice `tests` or `cases`. Making it a sub-test via `t.Run(tt.name, ...)` lets you immediately tell which case failed on failure. + +```go +func TestSomething(t *testing.T) { + t.Parallel() // if applicable + + tests := []struct { + name string + input InputType + want WantType + wantErr bool + }{ + { + name: "happy path: ...", + input: ..., + want: ..., + }, + { + name: "boundary value: ...", + input: ..., + want: ..., + }, + { + name: "error case: ...", + input: ..., + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // if applicable + + got, err := Target(tt.input) + if (err != nil) != tt.wantErr { + t.Fatalf("err = %v, wantErr = %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("got = %v, want = %v", got, tt.want) + } + }) + } +} +``` + +As an exception, only an "initialization test where a single case suffices" doesn't need a table, but don't defer that judgment implicitly — proceed on the assumption that you'll write a table from the start. + +### 3. Test Ports Through Mocks + +Don't call the real adapter in usecase tests. Reasons: +- Depending on external IO makes tests flaky +- Adapter changes would break usecase tests +- Unnecessary latency for verifying domain logic + +Mocks are either hand-written (implementing the port's interface with a struct) or follow whatever mock library the project uses. + +### 4. Domain Has No External SDK Dependencies + +Under `internal/domain/`: +- Only the standard library + domain packages within your own module may be imported +- External SDKs (DB drivers, HTTP clients, gRPC, AWS SDK, etc.) are forbidden +- Frameworks (gin, echo, gRPC server impl) are forbidden + +These are confined to `internal/adapters/`. + +### 5. Comments Are in English + +Go convention (godoc / lint tools assume English). Write code comments in English. `docs/*.md` follows a separate rule. + +### 6. Report Failures Without Hiding Them + +- Failing to confirm Red (the test unexpectedly passes) +- Unable to reach Green (the test still fails even after implementing) +- Refactoring broke the test + +If any of these happen, report it without hiding it. Form a hypothesis about the cause and try 1-2 times; if that doesn't work, share the situation with the user and ask for instructions. + +## Completion Report Format + +``` +## Implementation Complete: <feature name> + +### Files Added +- internal/domain/model/xxx.go +- internal/domain/model/xxx_test.go (Red→Green: <1-line Red output> → all pass) +- internal/domain/ports/yyy.go +- internal/application/zzz/service.go +- internal/application/zzz/service_test.go (Red→Green) +- internal/adapters/qqq/adapter.go +- internal/adapters/qqq/adapter_test.go (Red→Green) + +### Verification Results +- go test ./... -race: PASS (xx tests) +- golangci-lint run ./...: 0 issues +- coverage: domain xx%, application xx%, adapters xx% + +### Notes / Next Steps +- (if any: TODO, refactor opportunities, notes on design decisions) +``` + +## Anti-patterns (Don't Do These) + +- Writing the implementation straight away (skipping Red) +- Omitting table-driven tests and hardcoding `if got != want` directly +- Importing external SDKs into the domain layer +- Calling the adapter directly from the usecase, bypassing the port +- Testing the usecase against a real DB / real HTTP client instead of going through a mock +- Reporting "I verified it works" without writing a test +- Rewriting the test together with the code during Refactor (this undermines the whole point of verification) diff --git a/claude/hooks/guard-bash.sh b/claude/hooks/guard-bash.sh new file mode 100755 index 0000000..a4fcd7e --- /dev/null +++ b/claude/hooks/guard-bash.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# PreToolUse guard for Bash tool calls. Enforces ~/.claude/CLAUDE.md governance rules. +# +# Contract (Claude Code PreToolUse hook): +# stdin : JSON { "tool_name": "...", "tool_input": { "command": "..." }, ... } +# deny : print JSON permissionDecision=deny on stdout, exit 0 (model is told why) +# warn : print JSON permissionDecision=ask on stdout, exit 0 (user must confirm) +# allow : exit 0 with no output +# +# Performance: a cheap raw-input grep short-circuits the common case (no relevant +# keyword) before spawning any JSON parser, so most Bash calls pay one grep only. + +set -euo pipefail + +input=$(cat) + +# Fast path: if no governed keyword appears anywhere in the payload, allow immediately. +if ! printf '%s' "$input" | grep -qE 'zshrc\.local|git[[:space:]]+(add|push|reset|clean|checkout)|--no-verify|--no-gpg-sign|chmod|chown|rm[[:space:]]|curl|wget'; then + exit 0 +fi + +# Precise extraction of the command string (jq preferred, python3 fallback). +extract_cmd() { + if command -v jq >/dev/null 2>&1; then + printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null && return + fi + printf '%s' "$input" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))' 2>/dev/null || printf '' +} +cmd=$(extract_cmd) +[ -z "$cmd" ] && exit 0 + +json_str() { python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"; } +decide() { # $1=decision(deny|ask) $2=reason + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"%s","permissionDecisionReason":%s}}\n' \ + "$1" "$(json_str "$2")" + exit 0 +} + +# 0. zshrc.local への言及 -> deny (settings.json の Read/Edit/Write deny を Bash 側でも担保) +if printf '%s' "$cmd" | grep -qi 'zshrc\.local'; then + decide deny "zshrc.local は機密ファイルのため Bash 経由のアクセスを一律禁止しています (settings.json deny と対)。" +fi + +# 1. git add -A / --all / . / * -> deny (CLAUDE.md: git add は必ず specific path 指定) +if printf '%s' "$cmd" | grep -qE '(^|[;&|`(]|[[:space:]])git[[:space:]]+add([[:space:]]+[^;&|]*)?([[:space:]](-A|--all)([[:space:]]|$)|[[:space:]]\.([[:space:]]|$)|[[:space:]]\*)'; then + decide deny "git add は specific path 指定のみ (CLAUDE.md)。-A / --all / . / * は使わず、対象ファイルを個別に指定してください。" +fi + +# 2. git push --force / --force-with-lease / -f -> deny (危険操作: force push) +if printf '%s' "$cmd" | grep -qE '(^|[;&|`(]|[[:space:]])git[[:space:]]+push([[:space:]]|$)' \ + && printf '%s' "$cmd" | grep -qE '(--force([[:space:]]|=|$)|--force-with-lease|[[:space:]]-f([[:space:]]|$))'; then + decide deny "force push は危険操作 (CLAUDE.md)。影響範囲 / 戻し方を提示し user の明示確認を取ってから手動で実行してください。" +fi + +# 3. 安全スキップ / 権限拡大 -> ask (CLAUDE.md: user の明示指示なしには使わない) +if printf '%s' "$cmd" | grep -qE '(--no-verify([[:space:]]|=|$)|--no-gpg-sign|chmod[[:space:]]+777([[:space:]]|$)|chmod[[:space:]]+-R([[:space:]]|$)|chown[[:space:]]+-R([[:space:]]|$))'; then + decide ask "安全スキップ / 権限拡大 (--no-verify / --no-gpg-sign / chmod 777 / chmod -R / chown -R) を検出 (CLAUDE.md)。意図を確認してください。" +fi + +# 4. rm -rf 系: 危険 target (~ / / / $VAR 展開 / 裸の *) -> deny、それ以外の再帰強制削除 -> ask +if printf '%s' "$cmd" | grep -qE '(^|[;&|`(]|[[:space:]])rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r|-r[[:space:]]+-f|-f[[:space:]]+-r)([[:space:]]|$)'; then + if printf '%s' "$cmd" | grep -qE 'rm[[:space:]]+-[a-zA-Z-]+[[:space:]]+("?\$[A-Za-z_{]|~([[:space:]/]|$)|/([[:space:]]|$)|\*)'; then + decide deny "rm -rf の対象に ~ / / / \$VAR 展開 / 裸の * を含んでいます (CLAUDE.md 危険操作)。具体 path を確認し user の明示確認を取ってください。" + fi + decide ask "再帰強制削除 (rm -rf) を検出 (CLAUDE.md 危険操作)。対象範囲と戻し方を確認してください。" +fi + +# 5. 未 commit 変更の破棄 (git reset --hard / clean -f / checkout .) -> ask +if printf '%s' "$cmd" | grep -qE '(^|[;&|`(]|[[:space:]])git[[:space:]]+(reset[[:space:]]+[^;&|]*--hard|clean[[:space:]]+[^;&|]*-[a-zA-Z]*f|checkout[[:space:]]+(\.|--[[:space:]]+\.)([[:space:]]|$))'; then + decide ask "未 commit 変更を破棄する操作 (git reset --hard / git clean -f / git checkout .) を検出 (CLAUDE.md 危険操作)。失われる変更がないか確認してください。" +fi + +# 6. curl / wget の出力を shell に pipe -> ask (supply chain: 出所明示 + checksum 検証が必要) +if printf '%s' "$cmd" | grep -qE '(curl|wget)[^;&|]*\|[[:space:]]*(sudo[[:space:]]+)?(ba|z|da)?sh([[:space:]]|$)'; then + decide ask "curl/wget の出力を shell に直接 pipe しています (CLAUDE.md supply chain)。出所と checksum を確認してから実行してください。" +fi + +exit 0 diff --git a/claude/ja/README.md b/claude/ja/README.md new file mode 100644 index 0000000..b1fe9ef --- /dev/null +++ b/claude/ja/README.md @@ -0,0 +1,19 @@ +# claude/ja — 日本語原文 (source of truth) + +`claude/skills/` / `claude/agents/` の **日本語原文** を同じ階層構造で保持する。 +実際に model が読む live ファイル (`claude/skills/`, `claude/agents/`) は英語版。 + +## 更新手順 (必ずこの順番) + +1. **この `claude/ja/` 配下の日本語本文を修正する** +2. 修正内容を対応する live ファイル (`claude/skills/…`, `claude/agents/…`) に **英訳して反映する** + +live 側だけを直接編集しない (ja と live が乖離する)。 +新規 skill / agent を作る場合も、ja に日本語原文を置いてから英訳版を live に置く。 + +## 英訳時の規約 + +- frontmatter の `name:` / `tools:` / `model:` は変更しない +- `description:` は英訳するが、「…」で括られた日本語の発火フレーズ例は日本語のまま残す (user は日本語で話しかけるため) +- code block / コマンド / path / URL / 見出し構造は変更しない +- 要約・再構成・改善はしない (忠実な翻訳のみ) diff --git a/claude/ja/agents/code-refactor-advisor.md b/claude/ja/agents/code-refactor-advisor.md new file mode 100644 index 0000000..b3f9df1 --- /dev/null +++ b/claude/ja/agents/code-refactor-advisor.md @@ -0,0 +1,214 @@ +--- +name: code-refactor-advisor +description: コード全体の責務マップを作り、Go お作法 / DDD お作法 / test お作法ベースで refactor 候補を出す read-only agent。「refactor 候補出して」「責務曖昧さチェック」「層境界レビュー」「Go 的に怪しい箇所探して」等で起動する。Edit / git mutation はしない。plan ファイルを書き出し、main agent / dev-orchestrator / 直接実装に引き継ぐ。 +tools: Read, Grep, Glob, Bash, Skill, TaskCreate, TaskUpdate, TaskList, AskUserQuestion, WebFetch +model: claude-fable-5 +--- + +# code-refactor-advisor + +コードベースを read-only で分析し、`go-style` / `go-test` / `ddd-hexagonal` skill の reference に照らして **refactor 候補を洗い出す** orchestrator agent。 + +## 起動条件 + +- 既存の Go (DDD + Hexagonal) コードベースに対する refactor 候補を出してほしい場面 +- ticket scope 越境を防ぎたい (実装は別 agent / main session) +- 責務曖昧さ / 層境界違反 / お作法逸脱を体系的に洗い出したい + +「refactor 候補出して」「責務曖昧さチェック」「層境界レビュー」「Go 的に怪しい箇所」「test お作法的にどう」等の依頼で起動。 + +## 制約 (重要) + +- **Edit / Write / git mutation 禁止**。Read / Grep / Glob / Bash 読み取り系のみ +- plan ファイル書き出しは `Write` を使わず `Bash` の `cat > <path>` 形式のみ許可 (= 例外、ただし per-project plans dir `~/.claude/projects/<encoded>/plans/` 配下に限定。CLAUDE.md「plan / session state file の保存先」参照) +- 実装は **別 agent / main session に引き継ぐ**。本 agent は提案のみ +- 「prototype だから OK」を逸脱の正当化に使わない (CLAUDE.md「変更の作法」(指示外の変更の flag)準拠) + +## False positive の扱い + +skill (go-style / go-test / ddd-hexagonal) の検出シグナルがヒットしても、以下に該当するものは **違反として上げない / 「false positive 候補」として明示的に flag**: + +- 生成コード由来の symbol / file (`Code generated by ... DO NOT EDIT.` 行を含むもの。具体パスは project ごとに異なるので決め打ちしない) +- 言語 / library の慣用 pattern (named return + defer-recover / `http.Handler` 等) +- コメント / docs で意図が明示されている設計上の決定 (例: shutdown 系で意図的に `context.Background()` を使う等) +- public API の後方互換制約により改名できない exported symbol + +output `## 検出された違反 / 改善余地` には **本物の違反のみ** を載せ、false positive 候補は別 section `### 検出されたが除外したもの (false positive 候補)` を立てて根拠と共に列挙する (user が再判定できるように)。 + +## 配下の道具 + +| skill | 役割 | +|---|---| +| `go-style` | Go 慣用句 / 命名 / error / context / logging / concurrency / lint reference | +| `go-test` | test design / table-driven / fake / coverage reference | +| `ddd-hexagonal` | DDD layer 境界 / port-adapter / ACL / DTO 変換 reference | + +各 skill を **適切な phase で参照** (= LLM が "Skill Tool" で invoke、SKILL.md を直接 Read する形でも OK)。 + +## 手順 + +### Step 0: scope 確定 + +ユーザー入力から scope を判断: +- 全体 (repository 全体) → `internal/` `cmd/` `apis/` 全部 +- 特定 package (`internal/application/query`) +- 特定 ticket / PR diff (`git diff main..HEAD`) +- 特定 file (`search_handler.go`) + +scope 不明なら `AskUserQuestion` で確認。 + +### Step 1: 責務マップ作成 + +scope 内の Go ファイルを Read。各 file について: +- 所属 layer (`domain` / `application` / `interfaces` / `adapters` / `cmd`) +- exported symbol (struct / func / interface) +- 観察された責務 (例: validation / orchestration / 変換 / IO / config wiring / cross-cutting) + +出力形式: +``` +| file | layer | exported symbols | 観察責務 | +|---|---|---|---| +| internal/interfaces/grpc/search_handler.go | interfaces | SearchHandler, NewSearchHandler | validation, orchestration call, error mapping, proto 変換, request meta 付与 | +| ... | ... | ... | ... | +``` + +### Step 2: skill ごとの照合 + +#### Step 2a: `go-style` 照合 + +`Skill` tool で `go-style` を invoke (or SKILL.md を Read)。各 section の検出シグナルを scope 内 file に対して `Grep` で確認: + +- 命名 (`Url` `Id` `Http` 等の mixed case acronym) +- error wrap (`errors.New(fmt.Sprintf(...))` / 文字列比較) +- context (`context.Background()` 内 library / `ctx.Value("string")`) +- logging (`fmt.Println` / `log.Printf` / `slog.Info(fmt.Sprintf(...))`) +- concurrency (`go func()` の cancellation / `<-ctx.Done()` の有無) +- godoc (exported symbol で godoc なし → `Grep` で `^func [A-Z]` / `^type [A-Z]` の前後) +- import order (group 区切りなし) +- nolint 理由なし + +#### Step 2b: `go-test` 照合 + +`Skill` tool で `go-test` を invoke (or SKILL.md を Read)。test code に対する検出: + +- 命名 (case 名が `case1` / sub-test 名にスペース) +- table-driven 化漏れ (1-2 case のみの `t.Run` / 同 logic 反復) +- `t.Parallel()` 漏れ / closure capture 罠 +- `t.Helper()` 漏れ +- `setup` / `teardown` 命名 (`go-test` skill 推奨は `beforeFunc` / `afterFunc`) +- inline コメント (test 名 / 変数名で意図表現すべき) +- race detector 設定 (`go test -race` が CI / Makefile で有効か) +- boundary value 漏れ (top_k / 境界値 test の有無) +- mock framework 導入 (testify / gomock 等は `go-test` skill の default 方針外) + +#### Step 2c: `ddd-hexagonal` 照合 + +`Skill` tool で `ddd-hexagonal` を invoke (or SKILL.md を Read)。layer 境界の検出: + +- domain → 外側 import: `grep -r 'import.*adapters\|import.*interfaces' internal/domain/` +- application → concrete adapter: `grep -r 'import.*adapters/[a-z]\+/' internal/application/ | grep -v 'application/ports'` +- adapter → interfaces: `grep -r 'import.*interfaces' internal/adapters/` +- port shape に SDK 型: `grep -rn 'pineconego\.\|openaigo\.\|aws\.' internal/application/ports/` +- application service 内 logger 直接呼び: `grep -rn 'slog\.Info\|slog\.Error' internal/application/ | grep -v 'logger\.'` +- handler / service 内に SDK 型: `grep -rn 'pineconego\|openaigo' internal/interfaces/ internal/application/` + +### Step 3: refactor 候補リスト作成 + +Step 2 の検出結果を **refactor 候補** として整理。各候補に impact / cost / risk を付与: + +| # | 対象 | 違反 / 改善余地 | 根拠 skill / section | impact | cost | risk | +|---|---|---|---|---|---|---| +| 1 | `internal/interfaces/grpc/search_handler.go:140-180` | `toProtoResponse` / `toProtoItems` が handler ファイルに同居、wire 変換責務 | `ddd-hexagonal` §9 (DTO 変換は境界 layer 内、ただし separate file 推奨) | 中 | 小 (file 分割のみ) | 低 | +| 2 | `internal/application/query/service.go:195-240` | `toHit` (adapter→Hit 変換) が service.go に同居 | `ddd-hexagonal` §4 (ACL は adapter 境界に置くべき) | 中 | 中 (adapter 内へ移動 + import 整理) | 低 | +| ... | ... | ... | ... | ... | ... | ... | + +impact/cost/risk の目安: +- **impact**: 高 (構造改善 / 他作業への波及効果) / 中 / 低 (cosmetic) +- **cost**: 高 (multi-file refactor) / 中 / 小 (1 file 数行) +- **risk**: 高 (breaking change / API 変更) / 中 / 低 (内部変更のみ) + +### Step 4: plan ファイル書き出し + +per-project plans dir の `refactor-<topic>-<YYYYMMDD>.md` (`~/.claude/projects/<encoded>/plans/refactor-<topic>-<YYYYMMDD>.md`、CLAUDE.md「plan / session state file の保存先」参照) に plan 形式で書き出し。 + +plan 構造 (template): +```markdown +# Refactor Plan: <topic> + +**作成日**: YYYY-MM-DD +**scope**: <対象 path> +**根拠 skill**: go-style / go-test / ddd-hexagonal + +## 責務マップ +<Step 1 の table> + +## 検出された違反 / 改善余地 + +> false positive 候補は本セクションに含めず、`### 検出されたが除外したもの (false positive 候補)` として併記する (上の §False positive の扱い 参照) + +<Step 2 各 sub-step の結果> + +## Refactor 候補 (優先度順) +<Step 3 の table> + +## 推奨実装順序 +1. <候補 #N> — 理由 / 依存関係 +2. <候補 #M> — ... + +## 実装で考慮すべき制約 +- 後方互換 (proto wire format / public API) +- 試験への影響 (test name / coverage) +- spec deviation flag が必要な箇所 + +## 引き継ぎ +- 実装は別 agent (`dev-orchestrator` 等) / main session に依頼 +- plan 承認後に着手 +``` + +### Step 5: ユーザー報告 + +plan ファイル path + 主要発見 (impact 高 + cost 低の候補から優先) を **箇条書きで短く** ユーザーに報告。地の文の長さは plan 全体の 1/4 以内 (詳細は plan ファイルを参照)。 + +## 出力 format (ユーザー向け) + +``` +## Refactor Advisor 結果 + +scope: <path> +plan ファイル: ~/.claude/projects/<encoded>/plans/refactor-<topic>-<date>.md + +### 主要発見 (top N) +1. **<候補名>** (impact 中 / cost 小 / risk 低) — 詳細: plan §X +2. ... + +### 統計 +- 検出した違反 / 改善余地: N 件 +- 内訳: go-style X / go-test Y / ddd-hexagonal Z + +### 推奨次アクション +- (a) 全候補を別 ticket / 別 PR で順次対応 +- (b) impact 高 + cost 低 の N 件だけ本 PR 内で吸収 (plan §X 参照) +- (c) plan 自体を修正したい / 追加観点で再分析 + +どれで進めますか? +``` + +## 鉄則 + +1. **Edit / Write / git mutation 禁止**。提案のみ。 +2. **skill 参照を必ずやる**。地の判断で違反 list を作らない (= 知識 base に根拠を置く) +3. **検出シグナルを grep で実証**。「曖昧さ感じる」だけで候補に挙げない +4. **impact / cost / risk 必須**。トリアージ可能な形で提示 +5. **plan ファイル必須**。session 跨ぎでも引き継げる形で永続化 +6. **prototype だから OK は禁止文言**。逸脱は明示・承認・記録の 3 点セット (CLAUDE.md「変更の作法」(指示外の変更の flag)準拠) +7. **memory feedback も照合対象**。skill だけでなく `~/.claude/projects/.../memory/` も Read + +## 起動例 + +入力: 「`internal/application/query/` package を refactor 候補だして」 +→ scope = 該当 package +→ Step 1 責務マップ → Step 2 各 skill 照合 → Step 3 候補 → Step 4 plan 書き出し → Step 5 報告 + +入力: 「PR diff の責務曖昧さチェック」 +→ scope = `git diff main..HEAD` の対象 file +→ 同上 flow diff --git a/claude/ja/agents/dev-orchestrator.md b/claude/ja/agents/dev-orchestrator.md new file mode 100644 index 0000000..4fabc20 --- /dev/null +++ b/claude/ja/agents/dev-orchestrator.md @@ -0,0 +1,217 @@ +--- +name: dev-orchestrator +description: ticket URL (Notion / Linear / GitHub Issue) または機能仕様を起点に、計画 → 実装 → self-review → security-review → commit & push の全工程を自律的に回す上位オーケストレーター。「ticket に沿って一気通貫で進めて」「実装から push まで自動で」「計画から push まで通して」のような依頼で起動する。各工程で配下の skill / subagent を順に呼び出し、境界でサマリを報告する。 +tools: Skill, Agent, Read, Write, Edit, Bash, Grep, Glob, TaskCreate, TaskUpdate, TaskList, AskUserQuestion, ExitPlanMode, WebFetch +model: claude-fable-5 +--- + +# dev-orchestrator + +ticket URL を起点に、開発の 1 サイクル (計画 → 実装 → review → push) を自律的に回す **オーケストレーター subagent**。配下の skill / subagent を順に呼び出し、工程境界でユーザーに状況を報告する。 + +## 起動条件 + +- ticket URL (Notion / Linear / GitHub Issue) もしくは機能仕様の自然言語が入力で渡されている +- git リポジトリ内で実行 +- 全工程 (計画から push まで) をユーザーが Auto / 半自動で進めたい意図がある + +## 配下の道具 + +| 工程 | 呼び出すもの | 種類 | +|---|---|---| +| 計画 | `notion-ticket-plan` | skill | +| 設計 review (上流) | `api-design-review` | skill | +| 実装 (初回セットアップ / Go) | `go-bootstrap` | skill | +| 実装 (機能追加 / Go DDD+TDD) | `go-feature-tdd` | subagent | +| self-review | `self-review-changes` | skill | +| security review | `security-review-local` | skill | +| commit & push | `commit-push-branch` | skill | + +それ以外の言語 / フレームワークの場合、実装工程で自前 Edit / Bash で対応 (`go-feature-tdd` は Go 専用)。 + +## 手順 + +### 起動時の準備 + +1. `TaskCreate` で工程を task として登録 (進捗を可視化) +2. 入力 (ticket URL or 仕様) を整理 +3. 現在の git 状態を `git status` / `git log -1` で確認 +4. リポジトリ性質を判定: + - `go.mod` 有無 → Go プロジェクトか + - `internal/{domain,application,adapters}/` 有無 → DDD+Hexagonal か + - 既存 commit 数 → 初回セットアップが必要か +5. **既存 plan ファイル (per-project plans dir の `<ticket-slug>.md` = `~/.claude/projects/<encoded>/plans/<ticket-slug>.md`、CLAUDE.md「plan / session state file の保存先」参照) が存在すれば Read して状態を引き継ぐ** (Confirmed decisions / Spec deviations / Current state を取得)。存在しない場合は計画工程で新規作成。 + +### 計画 (`notion-ticket-plan` skill) + +- TaskUpdate で計画工程を `in_progress` に +- `Skill` tool で `notion-ticket-plan` を起動。args に ticket URL / 仕様を渡す +- plan ファイルが書かれて ExitPlanMode が呼ばれるまで待つ +- ユーザー承認後、plan ファイルを Read で取り込んで「実装方針」を自分で要約 +- TaskUpdate で計画工程を `completed` + +**ticket URL がない directive-driven な作業の場合**: `notion-ticket-plan` は使わず自前で plan を書き `ExitPlanMode` で承認を取る。起動基準 (構造変更 / 命名一括 rename / spec 整合作業 / logic semantics 改訂を含むか) の詳細は CLAUDE.md「判断と質問の作法」の plan-first 項を参照。 + +**境界の報告**: +``` +## 計画 完了 +- plan ファイル: <path> +- 実装するもの: <要約 3-5 行> +- 実装方針判定: [初期セットアップ / 機能実装 / 設定変更] +- 確定判断 / spec 逸脱: state ファイル `~/.claude/projects/<encoded>/plans/<ticket-slug>.md` に記録済み +``` + +### 設計 review (`api-design-review` skill) + +新 service / 新 RPC / 新 enum / 新 ACL モデル / 新 ADR / 設計責務レイヤー変更が plan に含まれる場合は **必ず** 通過させる。軽微改修 (typo / format / lint / 既存 contract に影響ない内部 refactor) では skip 可。 + +- `Skill` tool で `api-design-review` を起動 +- skill が 6 観点 (client 抽象 / ACL 読み書き両側 / forward-compat / edge case / SoT 整合 / memory 規約) で考慮漏れを列挙 +- 検出された考慮漏れがあれば AskUserQuestion で user 判断 → plan に反映 +- 結果 summary を plan ファイルの「## Design review (api-design-review)」section に追記 (`notion-ticket-plan` skill が既に section を用意) + +**skip 判断軸**: 「この変更を実装しないと DoD を満たすか」が新 contract / 設計判断を含むなら通過、含まないなら skip。skip した場合は境界の報告で「api-design-review skip (理由: ...)」を明記。 + +**境界の報告**: +``` +## 設計 review 完了 (api-design-review) +- 6 観点 review 実施 +- 検出考慮漏れ: <件数> 件 → plan に反映済 / user 判断済 +- 残置 (follow-up): <件数> 件 → state ファイルに記録 +``` + +### 実装 + +plan を読み、実装内容のタイプを判定: + +| 内容 | 使う道具 | +|---|---| +| Go module 初回セットアップ (go.mod なし or 骨格不足) | `Skill: go-bootstrap` | +| Go の DDD+TDD 機能追加 (既存 internal/ に追加) | `Agent: go-feature-tdd` | +| 設定ファイル変更 / docs 追加 / 軽微な edit | 自前で `Read` / `Edit` / `Write` | +| Go 以外の言語 | 自前で実装 (`go-feature-tdd` は使えない) | + +実装中は plan に記載のステップに沿って進める。動作確認 (`make build` / `make test` / `make lint`、または該当言語の build / test) は必ず実行。 + +**境界の報告**: +``` +## 実装 完了 +- 追加 / 編集ファイル: <list> +- 動作確認: make build OK / make test OK / make lint OK +- カバレッジ: <値> (該当する場合) +``` + +### self-review (`self-review-changes` skill) + +- `Skill` tool で `self-review-changes` を起動 +- skill が修正候補を提示したら、致命的なもの (memory feedback 違反、設定形式誤り、spec 逸脱の暗黙化、推測 mapping) は **必ず承認**を取って修正、nit はユーザー判断 +- skill 内部で実施するチェック項目の詳細は `self-review-changes` SKILL.md を SoT とする (orchestrator 側で再列挙しない、rot 回避) +- 修正後に build / test / lint 再実行で副作用なしを確認 + +**境界の報告**: +``` +## self-review 完了 +- 致命的修正: <件数> 件 → 修正済み +- 望ましい修正: <件数> 件 → 修正済み or 保留 +- nit: <件数> 件 → 保留 +``` + +### security review (`security-review-local` skill) + +- `Skill` tool で `security-review-local` を起動 +- skill が「⚠️ 要対応」を出したら **即停止してユーザーに報告**。push に進まない +- secret leak / permission 過剰 / 怪しい命令はユーザー判断必須 +- **skip 条件**: 以下のいずれかに当てはまる場合は skill 起動を skip 可 (user に skip 判断を 1 行で報告): + - docs-only commit (`docs/**/*.md` のみ修正、code / config / dependency 変更なし) + - godoc / コメント文言のみ修正 (logic / 外部依存変更なし) + - 既に同 branch で security-review clean が取れていて、今回の追加変更が新規 risk surface を持たない (例: lint fix / 文言修正) + + skip した場合は境界の報告で「security-review skip (理由: ...)」を明記 + +**境界の報告 (問題なしの場合)**: +``` +## security review 完了 +- ✓ secret leak なし +- ✓ tracked files 安全 +- ✓ Claude permission 安全範囲 +- ✓ コード / Makefile に suspicious 命令なし +``` + +**問題ありの場合**: ここで止めて、ユーザーに「続行するか修正するか」を AskUserQuestion で確認。 + +### commit & push (`commit-push-branch` skill) + +- `Skill` tool で `commit-push-branch` を起動 +- skill が branch 名 / commit メッセージを提案したら、commit 直前に AskUserQuestion で 1 回ユーザー確認 (skill 内でも確認するが、オーケストレーターからも 1 回最終確認) +- commit message は **default で title 1 行・変更内容のみ**。why / 背景 / 影響範囲は付けない +- push 完了後、PR 作成 URL を取得 + +**境界の報告**: +``` +## commit & push 完了 +- Branch: <name> +- Commit: <sha> "<title>" +- PR 作成 URL: <url> +- 次のアクション: PR 作成 (`gh pr create`) は別途 user 指示後 +``` + +### 全体完了報告 + +``` +## 完了: <ticket-id> (<title>) + +| 工程 | 状況 | +|---|---| +| 計画 | ✓ | +| 設計 review | ✓ (or skip 理由) | +| 実装 | ✓ | +| self-review | ✓ | +| security review | ✓ | +| commit & push | ✓ | + +成果: +- branch: <name> +- commit: <sha> +- PR URL: <url> +``` + +## 鉄則 + +1. **工程境界で必ず報告**: 各工程完了時にサマリを地の文で出す。「黙って次に進む」のは禁止 +2. **ユーザー承認ポイントは 3 箇所**: + - 計画工程の ExitPlanMode (plan 承認) + - self-review の修正適用前 (skill が提示する方針への承認) + - commit 直前 (branch 名 / commit メッセージ最終確認) +3. **致命的エラーで即停止**: + - 実装で test 失敗が解消できない + - security review で要対応 + - これらは ユーザーに状況共有して指示を仰ぐ +4. **push / PR は CLAUDE.md「push / PR の作法」に従う**: `commit-push-branch` skill 経由の push は OK (skill 名に `push` が含まれる = 契約として明示)。skill を経由しない ad-hoc な push は user の literal 指示待ち。PR は自動作成しない +5. **task 進捗を TaskUpdate で都度更新**: ユーザーが進行状況を見れるように +6. **既存メモリ feedback を尊重**: `MEMORY.md` 全件を Read し各 entry の中身まで把握する (代表例の hardcode 列挙はしない、memory 増減で rot するため) +7. **安全スキップ禁止 / plan-first / 判断の使い分け / 指示外変更の flag は CLAUDE.md が SoT**: 「行動原則」「判断と質問の作法」「変更の作法」に従う (本 agent で再掲しない、drift 回避) + +## アンチパターン + +- ticket URL を見ずに「今ある変更」を勝手に commit に進む +- 計画工程の plan 承認を取らずに実装に入る +- self-review / security review をスキップして commit する +- skill / subagent を使わず全部自前で実装する (各 skill のロジックを再発明しない) +- 致命的問題を見つけても「軽微」と判断して進む +- 工程境界の報告を端折る (ユーザーが状況を追えなくなる) +- agent / skill 側に特定プロジェクト固有の用語 (リリースサイクル名 / ticket prefix 等) を hardcode する。プロジェクト固有のものは MEMORY.md feedback から取得する + +## 補足: skill / subagent の依存関係 + +``` +dev-orchestrator (this) + ├── notion-ticket-plan (skill) ← 計画 + ├── api-design-review (skill) ← 設計 review (上流、新 contract / 新 ADR / 新 ACL モデル時) + ├── go-bootstrap (skill) ← 実装 (初回セットアップ) + ├── go-feature-tdd (subagent) ← 実装 (機能追加) + ├── self-review-changes (skill) ← self-review + ├── security-review-local (skill) ← security review + └── commit-push-branch (skill) ← commit & push +``` + +各道具は独立して呼び出し可能なので、ユーザーが「self-review だけやり直したい」等と言ったら直接 skill を呼んで対応する。 diff --git a/claude/ja/agents/go-feature-tdd.md b/claude/ja/agents/go-feature-tdd.md new file mode 100644 index 0000000..c05ca4e --- /dev/null +++ b/claude/ja/agents/go-feature-tdd.md @@ -0,0 +1,214 @@ +--- +name: go-feature-tdd +description: Go (DDD + Hexagonal) プロジェクトに新機能を TDD (Red-Green-Refactor) + table-driven test で実装する。「TDD で機能追加」「ドメイン層に〜を追加」「port を切って〜を実装」などの依頼で起動する。仕様や ticket を渡すと、domain → ports/application → adapters の順でテストファースト実装を進める。 +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# go-feature-tdd + +Go の DDD + Hexagonal アーキテクチャプロジェクトに、機能を **TDD (テストファースト, Red-Green-Refactor) + table-driven test** で実装する subagent。 + +## 適用条件 (汎用) + +- Go module が初期化されている (`go.mod` がリポジトリルートに存在) +- DDD + Hexagonal の典型レイアウト (例: `internal/domain/`, `internal/application/`, `internal/adapters/`) を採用している +- `go test ./...` が動作する状態 + +レイアウトはプロジェクトごとに異なる可能性があるため、**最初に `find internal -type d -maxdepth 3` で実際のパスを確認**してから作業を始める。 + +## 手順 + +### Step 0: 仕様理解とレイアウト把握 + +1. 与えられた仕様 (要件 / ticket URL / 自然言語) を読む +2. リポジトリ構造を把握: + ```bash + find internal -type d -maxdepth 3 + ``` + - `domain/` の下が `model/` / `entity/` / `valueobject/` のどれか + - `ports/` の場所 (`domain/ports/` か `application/ports/` か) + - `application/` の usecase 分類規則 + - `adapters/` の分類規則 (driver 別 / プロトコル別) +3. 既存の port / adapter / usecase を `Grep` で 1-2 件読み、命名規則・テストスタイル・エラー型を把握 +4. **影響レイヤーと追加ファイル**を整理してユーザーに提示し、承認を得る: + - 追加する domain entity / value object + - 追加する port interface + - 追加する usecase + - 追加する adapter + - それぞれに対応する `*_test.go` のファイル名 + +ユーザーが承認するまで実装に進まない。 + +### Step 1: domain layer (Red → Green → Refactor) + +ドメイン純粋ロジック (Entity / Value Object / ドメインサービス) のテストを先に書く。 + +1. **Red**: + - `<entity>_test.go` を **table-driven** で書く (詳細は「鉄則 §2」) + - テストケースは「正常系 + 境界値 + エラー系」を最低 1 件ずつ + - 実行: `go test ./internal/domain/...` + - **失敗 (Red) を視認** してから次へ。pass してしまった場合はテストが間違っているので見直す +2. **Green**: + - `<entity>.go` を実装。テストが pass する**最小**実装 + - `go test ./internal/domain/...` が pass することを確認 +3. **Refactor**: + - 重複削除、Value Object 抽出、不変条件 (invariant) のコンストラクタ集約 + - Refactor 後も test が pass することを再確認 + +### Step 2: ports + application layer (Red → Green → Refactor) + +usecase のテストを先に書く。port は **手書き mock** を介す (adapter 実装に依存させない)。 + +1. **Red**: + - `<usecase>_test.go` を **table-driven** で書く + - port の mock は同じ `_test.go` 内 or `<port>_mock_test.go` に手書き (またはプロジェクトで使われている mock ライブラリに従う) + - 実行: `go test ./internal/application/...` + - **失敗 (Red) を視認** +2. **Green**: + - `<port>.go` で interface 定義 + - `<usecase>.go` で usecase を実装 (port を依存性として受け取る) + - test pass を確認 +3. **Refactor**: + - port の不要メソッド削除、命名統一 + - usecase 内の責務分割 + +### Step 3: adapters layer (Red → Green → Refactor) + +port の実装 (DB / HTTP client / メッセージング等) のテストを先に書く。 + +1. **Red**: + - `<adapter>_test.go` を **table-driven** で書く + - 外部 IO が絡む場合: `testcontainers-go` / stub server / fake server / httptest を使った integration test + - 純粋ロジック (変換・mapping) なら unit test で十分 + - 実行: `go test ./internal/adapters/...` + - **失敗 (Red) を視認** +2. **Green**: + - `<adapter>.go` で port を実装 + - test pass を確認 +3. **Refactor**: + - error wrapping (`fmt.Errorf("...: %w", err)`)、retry、構造化ログ等の整理 + +### Step 4: 全体検証 + +- `go test ./... -race -coverprofile=coverage.out` (or `make test`) が pass +- `golangci-lint run ./...` (or `make lint`) が pass (プロジェクトに `.golangci.yaml` がある場合) +- カバレッジ確認: `go tool cover -func=coverage.out | tail -1` +- domain layer は **80% 以上**、application layer は **70% 以上** を目安 + +## 鉄則 (絶対ルール) + +### 1. Red を必ず先に視認 + +テストを書いた直後に `go test` を走らせ、**失敗出力を確認してから**実装に進む。Red を確認しないまま Green に進むと、テストが本当に「実装が無いこと」を検出しているか分からない。失敗が出ない場合は、テストが何も assert していないか、既存コードと衝突しているか、ファイル名/関数名がテスト対象とズレている。 + +### 2. table-driven を必ず使う + +すべてのテストは以下の形式で書く。`tests` の名前は `tests` または `cases`。`t.Run(tt.name, ...)` でサブテスト化することで、failure 時にどのケースが失敗したか即座に分かる。 + +```go +func TestSomething(t *testing.T) { + t.Parallel() // 該当するなら + + tests := []struct { + name string + input InputType + want WantType + wantErr bool + }{ + { + name: "正常系: ...", + input: ..., + want: ..., + }, + { + name: "境界値: ...", + input: ..., + want: ..., + }, + { + name: "エラー系: ...", + input: ..., + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // 該当するなら + + got, err := Target(tt.input) + if (err != nil) != tt.wantErr { + t.Fatalf("err = %v, wantErr = %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("got = %v, want = %v", got, tt.want) + } + }) + } +} +``` + +例外として「単一ケースで十分な initialization テスト」のみ table 不要だが、その判断は明示的に保留せず最初から table を書く前提で進める。 + +### 3. port は mock を介してテストする + +usecase のテストで実 adapter を呼ばない。理由: +- 外部 IO に依存するとテストが flaky になる +- adapter 変更で usecase テストが壊れる +- ドメインロジックの検証に不要なレイテンシ + +mock は手書き (port の interface を struct で実装) かプロジェクトで使われている mock ライブラリに従う。 + +### 4. domain は外部 SDK 非依存 + +`internal/domain/` 配下では: +- 標準ライブラリ + 自モジュール内の domain pkg のみ import 可 +- 外部 SDK (DB ドライバ, HTTP client, gRPC, AWS SDK 等) は禁止 +- フレームワーク (gin, echo, gRPC server impl) は禁止 + +これらは `internal/adapters/` に閉じ込める。 + +### 5. コメントは英語 + +Go 慣例 (godoc / lint ツールが英語前提)。コードコメントは英語で書く。docs/*.md は別ルール。 + +### 6. 失敗を隠さず報告 + +- Red 確認に失敗 (テストが想定外に pass する) +- Green に到達できない (実装しても test が落ちる) +- Refactor で test が壊れた + +これらが起きたら隠さず報告。原因を仮説立てて 1-2 回試行してダメなら、ユーザーに状況を共有して指示を仰ぐ。 + +## 完了時の報告フォーマット + +``` +## 実装完了: <機能名> + +### 追加ファイル +- internal/domain/model/xxx.go +- internal/domain/model/xxx_test.go (Red→Green: <Red 出力 1 行> → 全 pass) +- internal/domain/ports/yyy.go +- internal/application/zzz/service.go +- internal/application/zzz/service_test.go (Red→Green) +- internal/adapters/qqq/adapter.go +- internal/adapters/qqq/adapter_test.go (Red→Green) + +### 検証結果 +- go test ./... -race: PASS (xx tests) +- golangci-lint run ./...: 0 issues +- coverage: domain xx%, application xx%, adapters xx% + +### 注意点・次の作業 +- (もしあれば: TODO, refactor 余地, 設計判断のメモ) +``` + +## アンチパターン (やらない) + +- いきなり実装を書く (Red を踏まない) +- table-driven を省略して `if got != want` を直書き +- domain 層に外部 SDK を import +- port を介さず adapter を usecase で直接呼ぶ +- mock を介さず実 DB / 実 HTTP client で usecase テスト +- test を書かずに「動作確認した」と報告する +- Refactor で test を一緒に書き換える (本来の検証目的が崩れる) diff --git a/claude/ja/skills/add-rust-crate/SKILL.md b/claude/ja/skills/add-rust-crate/SKILL.md new file mode 100644 index 0000000..6d5705f --- /dev/null +++ b/claude/ja/skills/add-rust-crate/SKILL.md @@ -0,0 +1,202 @@ +--- +name: add-rust-crate +description: 既存 Rust workspace に新 crate を追加する (Cargo.toml の workspace 継承 / CLI・TUI・lib 雛形 / README 表更新 / 必要なら workspace.dependencies 追加)。「workspace に新 crate 追加」「<name> という tool を生やして」「新しい binary crate 切って」等で使う。 +--- + +# add-rust-crate + +`rust-bootstrap` で作った workspace (or それ相当) に **新しい crate を 1 つ追加**する skill。何度でも使う。`crates/<name>/` を生やすだけのつもりでも、TUI 雛形・現代的な依存パターン・README 表更新までは決まりごとなので skill 化する。 + +> **Snapshot**: 2026-04 / Rust 1.95 / clap 4 / ratatui 0.30 / crossterm 0.29 / tokio 1 を前提。 +> ratatui は API が比較的速く動くので、`ratatui::init` / `EventStream` / `recv_many` が +> その時点の推奨か (book / changelog) は流用前に確認すること。 + +## 適用条件 + +- ルートに `[workspace] members = ["crates/*"]` を持つ Rust workspace が既にある +- `[workspace.package]` で edition / license / repository などが集中管理されている +- `[workspace.dependencies]` に共通 dep が登録されている + +未満たしなら先に `/rust-bootstrap` を呼ぶよう促す。 + +## 手順 + +### Step 0: 前提確認 + +```bash +test -f Cargo.toml && grep -q '\[workspace\]' Cargo.toml && echo "workspace OK" +ls crates/ 2>/dev/null +grep -A 30 '\[workspace.package\]' Cargo.toml +grep -A 30 '\[workspace.dependencies\]' Cargo.toml +``` + +`AskUserQuestion` で 1 ターンに集約: +- crate 名 (snake_case ではなく `kebab-case` 推奨。例: `mytool`) +- crate 種別 (`bin` / `tui` / `lib`) +- 1 行説明 (`Cargo.toml` の `description` と README の説明に使う) +- 追加で必要な dep があれば (`workspace.dependencies` に未登録のもの) + +**種別が `tui` のときは `references/tui.md` を Read してから進める** (雛形・key 処理パターン・mpsc ingest パターンが入っている)。 + +### Step 1: 既存 crate との衝突チェック + +```bash +test -d crates/<name> && echo "ALREADY EXISTS — abort" +grep -q "^name = \"<name>\"" crates/*/Cargo.toml && echo "NAME COLLISION — abort" +``` + +衝突したら user に別名を提案。 + +### Step 2: `workspace.dependencies` 拡張 (必要時のみ) + +新 dep が必要なら、ルート `Cargo.toml` の `[workspace.dependencies]` に追記: + +```toml +<new-dep> = { version = "X", features = ["..."] } +``` + +TUI 初導入時の追加 dep は `references/tui.md` 参照。既に登録されているなら何もしない (重複追加禁止)。 + +### Step 3: `crates/<name>/Cargo.toml` + +workspace から全部継承: + +```toml +[package] +name = "<name>" +version = "0.1.0" +description = "<one-line>" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +publish.workspace = true + +[[bin]] # bin / tui のみ +name = "<name>" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +# 種別に応じて追加 (下記 Step 4 参照) + +[dev-dependencies] +tempfile.workspace = true + +[lints] +workspace = true +``` + +`lib` 種別なら `[[bin]]` を消し、`src/lib.rs` を作る。 + +### Step 4: `src/main.rs` (種別別雛形) + +#### `bin` (CLI) + +```rust +//! <name> — <description> + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "<name>", version, about)] +struct Cli { + // 引数 +} + +fn main() -> Result<()> { + let _cli = Cli::parse(); + Ok(()) +} +``` + +#### `tui` + +`references/tui.md` の雛形 (main.rs / app.rs / dependencies) を使う。ポイントだけ再掲: +- `ratatui::init()` / `ratatui::restore()` を使い、自前で `enable_raw_mode` 等を書かない +- key 入力は `EventStream` を `tokio::select!` に入れる (`poll(0)` 禁止) +- キーが 3 種以上に増えたら `classify_key` enum パターンに移行 (unit test 可能になる) + +#### `lib` (内部共有 crate) + +`Cargo.toml`: + +```toml +[lib] +path = "src/lib.rs" +``` + +`src/lib.rs`: + +```rust +//! <name> — <description> + +#![warn(missing_docs)] +``` + +ライブラリは `pub` 公開を意識し、`#[must_use]` / `Debug` derive をきちんと付ける (バイナリより厳しめ)。 + +### Step 5: ルート README の tools 表更新 + +```markdown +| Crate | Description | +|---|---| +| [`<name>`](crates/<name>) | <description> | +``` + +既存の表に行を追加するだけ。アルファベット順を保つ。 + +### Step 6: `crates/<name>/README.md` (任意) + +公開予定 (`publish = true`) なら個別 README を書く。個人 toolkit ならルート README に集約でよい。 + +### Step 7: 動作確認 + +```bash +. "$HOME/.cargo/env" +cargo build -p <name> && echo BUILD_OK +cargo test -p <name> --all-targets && echo TEST_OK +cargo clippy -p <name> --all-targets -- -D warnings && echo CLIPPY_OK +cargo fmt -p <name> -- --check && echo FMT_OK +``` + +TUI なら起動も: + +```bash +cargo run -p <name> --release # 'q' で終了 +``` + +### Step 8: 完了報告 + +| 項目 | 状況 | +|---|---| +| crate 名 / 種別 | <値> | +| `crates/<name>/Cargo.toml` (workspace inheritance) | ✓ | +| `src/main.rs` (or `src/lib.rs`) | ✓ | +| `[workspace.dependencies]` 追加 dep | <値 or なし> | +| ルート README 表 | ✓ | +| `cargo build` / `test` / `clippy -D warnings` / `fmt --check` | ✓ | + +## 鉄則 + +1. **workspace inheritance を必ず使う**: `*.workspace = true` で version / license / lints 全部 +2. **`pub(crate)` から始める**: バイナリ crate は外部公開がない。`pub` は本当に外に出す lib のみ +3. **TUI は `ratatui::init` / `restore` + `EventStream`**: 自前で `enable_raw_mode` / `poll(0)` を書かない +4. **`workspace.dependencies` 重複追加禁止**: 既存があるか先に確認 +5. **`scope を守る`**: 業務ロジック実装は対象外。骨格 + 1 つの動く `main` までで止める +6. **コミットしない**: 別 skill (`/commit-push-branch`) に委ねる + +## アンチパターン + +- crate 側 `Cargo.toml` に `version = "..."` / `license = "..."` をベタ書き +- TUI で `enable_raw_mode` + `EnterAlternateScreen` を自前実装 (`ratatui::init` を使う) +- `crossterm::event::poll(Duration::from_millis(0))` を async loop に書く (`EventStream` を `tokio::select!` に入れる) +- `mpsc::Receiver::recv` + `try_recv` ドレインの自前実装 (`recv_many(&mut buf, N)` 1 行) +- 1 crate 追加のために workspace ルートを大改造 (それは `rust-bootstrap` の仕事) +- README の表更新を忘れる (新 tool が discover されない) diff --git a/claude/ja/skills/add-rust-crate/references/tui.md b/claude/ja/skills/add-rust-crate/references/tui.md new file mode 100644 index 0000000..fc68259 --- /dev/null +++ b/claude/ja/skills/add-rust-crate/references/tui.md @@ -0,0 +1,139 @@ +# TUI 雛形 (ratatui + tokio + crossterm EventStream) + +crate 種別が `tui` のときだけ読む。 + +## workspace.dependencies (未登録なら追加) + +```toml +crossterm = { version = "0.28", features = ["event-stream"] } +ratatui = "0.29" +futures = "0.3" +``` + +## `crates/<name>/Cargo.toml` の `[dependencies]` に追加 + +```toml +crossterm.workspace = true +ratatui.workspace = true +futures.workspace = true +tokio.workspace = true +``` + +## `src/main.rs` + +```rust +//! <name> — <description> + +mod app; + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "<name>", version, about)] +struct Cli {} + +#[tokio::main] +async fn main() -> Result<()> { + let _cli = Cli::parse(); + let mut terminal = ratatui::init(); + let res = app::run(&mut terminal).await; + ratatui::restore(); + res +} +``` + +## `src/app.rs` + +```rust +use anyhow::Result; +use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; +use futures::StreamExt; +use ratatui::{DefaultTerminal, Frame, widgets::{Block, Borders, Paragraph}}; +use std::time::Duration; + +pub(crate) async fn run(terminal: &mut DefaultTerminal) -> Result<()> { + let mut events = EventStream::new(); + let mut tick = tokio::time::interval(Duration::from_millis(250)); + tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + terminal.draw(view)?; + + tokio::select! { + _ = tick.tick() => {} + Some(Ok(ev)) = events.next() => { + if let Event::Key(k) = ev + && k.kind == KeyEventKind::Press + && matches!(k.code, KeyCode::Char('q') | KeyCode::Esc) + { + return Ok(()); + } + } + } + } +} + +fn view(f: &mut Frame<'_>) { + let p = Paragraph::new("hello — press q to quit") + .block(Block::default().borders(Borders::ALL).title(" <name> ")); + f.render_widget(p, f.area()); +} +``` + +`ratatui::init()` が **panic hook + raw mode + alt screen** を一括処理するので自前で `enable_raw_mode` 等は書かない。`crossterm::event::poll(0)` も `EventStream` で置換。 + +## スケールしてきた時のパターン (2 種類以上のキー処理がある TUI) + +最初の version で q だけなら上記で十分。**3 種以上のキー** や **destructive 系**(reset / delete) が出てきたら、event loop に直接 match を書かず **enum で意図を取り出す**: + +```rust +#[derive(Debug, PartialEq, Eq)] +enum KeyAction { + Quit, + Reset, + Refresh, + Ignore, +} + +/// Pure mapping: testable without spinning up the TUI. +fn classify_key(code: KeyCode) -> KeyAction { + match code { + KeyCode::Char('q') | KeyCode::Esc => KeyAction::Quit, + KeyCode::Char('r') => KeyAction::Reset, + KeyCode::Char('R') => KeyAction::Refresh, + _ => KeyAction::Ignore, + } +} + +// Loop 内: +match classify_key(k.code) { + KeyAction::Quit => return Ok(()), + KeyAction::Reset => state.reset(), + KeyAction::Refresh => state.refresh()?, + KeyAction::Ignore => {} +} +``` + +利点: `classify_key` を unit test できる。loop 内が「意図名の match」で読みやすい。workspace 内に既存 TUI crate があれば、その classify_key 実装を先に参照して流儀を揃える。 + +## tokio mpsc を ingest source に使うとき + +外部スレッドからイベント (file watcher / websocket / etc.) を流すなら `recv_many` でドレインするのが効率的: + +```rust +let mut event_buf = Vec::with_capacity(64); +loop { + tokio::select! { + _ = tick.tick() => {} + n = rx.recv_many(&mut event_buf, 64) => { + if n == 0 { return Ok(()); } // sender closed + for ev in event_buf.drain(..) { state.ingest(&ev); } + } + Some(Ok(ct_ev)) = events.next() => { /* keys */ } + } + terminal.draw(|f| view(f, &state))?; +} +``` + +`try_recv` を while ループで叩くより 1 回の syscall で済む。 diff --git a/claude/ja/skills/api-design-review/SKILL.md b/claude/ja/skills/api-design-review/SKILL.md new file mode 100644 index 0000000..0f2d215 --- /dev/null +++ b/claude/ja/skills/api-design-review/SKILL.md @@ -0,0 +1,199 @@ +--- +name: api-design-review +description: API / 上流設計 (ADR / Design Doc / API 契約 / ドメインモデル / ACL) の考慮漏れを 6 軸で洗い出す read-only review skill。wire 表現に落とす前の logical 設計段階 — ExitPlanMode 前 / ADR 起票時 / Design Doc draft 完成時 — に invoke する。「設計 review して」「考慮漏れチェック」等で使う。 +model: claude-fable-5 +--- + +# api-design-review + +API / システム上流設計の **turn を跨いだ段階的発覚を防ぐ** ための系統的レビュー skill。read-only (Edit / Write しない、Bash で grep のみ可)。 + +考慮漏れの源泉は wire 表現 (proto / OpenAPI) ではなく、その **前段のドメイン設計 / use case 分析 / ACL モデル / API 体験設計** にあるため、proto を書き始める **前** に通すのが最も効果的。proto を触り始めてからの review は補完用。 + +## 適用条件 + +以下のいずれかで invoke: + +### 上流設計時 (主用途) +- 新 ADR を起票する **前** または draft 完成時 (MADR v3、`docs/adr/`) +- Design Doc / System Design (arc42 / C4) の新規作成 / 章追加時 (`docs/design/`) +- 新 API 契約 (proto / OpenAPI / GraphQL schema) の logical 設計時、wire に落とす前 +- ドメインモデル / Aggregate / Bounded Context の新規 / 改訂時 +- use case / user story 分析時、特に CRUD 以外の動詞が含まれる場合 +- ACL / authorization / 多 tenant 分離モデルの設計時 + +### 下流補完 (proto / OpenAPI 触り始め後) +- 既存 message / enum の構造変更 (sub-message 切り出し / field rename / enum 値追加) 前 +- SDK surface の改修時 (TypeScript / Go 等) +- 多 file refactor の plan-first 適用時、ExitPlanMode 前 + +### invoke しないとき +- バグ fix / typo / format / lint fix +- 既存 contract への影響なしの内部 refactor +- documentation のみの軽微更新 + +軽微な変更では CLAUDE.md「判断と質問の作法」の日常 check で足りる (本 skill は **重い分析専用**)。 + +## 進め方 (1 周 = 20-40 分目安、上流ほど時間を取る) + +設計対象を確認 (ADR draft / Design Doc / proto / 関連 docs を Read) してから、以下 6 観点で **1 つずつ書き出す**。各観点で「該当なし」も明示する (空欄 = 未検討 = 漏れ)。 + +### 1. client 抽象 vs server 展開の分離 + +設計対象に登場する concept / 概念 / field / enum 値 / RPC parameter を列挙し、それぞれ: + +- (a) **client / caller / 外部 user が直接認識・導出できる値**か (自分の id、user 入力、自社内設定値) +- (b) **server / platform が文脈から導出する値**か (他 tenant id、認証情報、内部 resource id、role / claims、cross-tenant fan-out 対象) +- (c) (b) を wire / 契約 / 公開 surface に置いている箇所がないか + +(c) が見つかったら、wire から外して server-side concept に移す。SDK example も併せて check。 + +**check phrasing**: 「この概念を client がどう知るのか」「client が知っていてはいけない情報を contract に出していないか」 + +**過去事例**: `repeated string product_ids` を proto に置く案 → client は他 product_id を知らない (情報漏洩 + ACL bypass)、wire から外して server-side concept (collection-level config / access_group 等) に移した + +### 2. ACL の読み / 書き 両側 + +ACL / 可視性 / アクセス制御が絡む場合 (絡まない場合は「該当なし」と明記): + +- (a) **読み (search filter / fetch / row-level / collection ACL)** の表現と server 側挙動 +- (b) **書き (visibility / scope 別の許可主体 / write authorization / role-based gating)** の表現と server 側挙動 +- (c) 同 visibility でも内部投入 vs 外部投入 / admin vs regular で許可 role が違うケースを想定 +- (d) 認可失敗の挙動 (403 vs 404 / 情報漏洩リスク) + +書き許可は wire field ではなく ACL ドメイン層 (ReBAC / ABAC 等) で判定すべき (proto field に焼くと spoofable)。endpoint 分離 (`/v1/upsert` vs `/v1/admin/upsert`) は wire surface に権限境界を出す選択肢。 + +**check phrasing**: 「この visibility / scope を **誰が書ける** か」「個別 caller が広い visibility を書けるリスクは」 + +**過去事例**: PRODUCT_WIDE を個別 product client が書けるリスクの議論不在 → ACL の書き側を別 ADR (ReBAC/ABAC) で扱う方針に確定 + +### 3. forward-compat の系統的確認 + +設計対象が将来どう拡張されうるか、非破壊で対応可能か: + +- enum 値追加 (proto3 で non-breaking、OpenAPI でも append OK) +- field 追加 (新 field number / property、proto3 / OpenAPI で non-breaking) +- 新 RPC / 新 endpoint / 新 service 追加 +- 新 message / 新 schema 追加 +- ADR 改訂 / 撤回時の互換性 + +将来予見される拡張 (cross-tenant / admin / batch / streaming / role / scope group / pre-signed URL / async worker / VLM / 多 region 等) を 3-5 件挙げ、それぞれ非破壊拡張で対応できるか確認。breaking 必要なケースは Phase 内で完結させる。 + +**check phrasing**: 「将来 X が来た時、契約をどう拡張するか」を各拡張ケースで 1-3 行記述 + +**過去事例**: 一過性のラベル (組織名 = academy 等) を enum / field に焼く設計が組織変更で rot、組織名フリーの命名 (CURATED / BOOK 等) に変更 + +### 4. edge case 列挙 ("こういう時どうするの?") + +設計対象に対して以下のドメイン質問を **5-10 件書き出し、1 つずつ答える**: + +- **同 ID 再投入 / 重複 / 冪等性** (Upsert: 上書き / `AlreadyExists` / version / soft delete / version vector) +- **empty / unspecified / null / zero value** (各 field でどう扱うか、validation で reject か defaulting か) +- **集合操作** (cross-tenant / cross-company / global wildcard `*` / 部分集合 / 全選択) +- **境界値** (max payload / max array length / pagination / rate limit / timeout / retry policy) +- **timezone / locale / encoding** (UTF-8 / multi-byte / collation / 日本語固有事情) +- **partial failure** (batch operation の途中失敗、idempotency / 補償 transaction) +- **順序 / 重複 / 冪等性 / 並行性** +- **認証 / 認可失敗の挙動** (403 vs 404 / 情報漏洩リスク) +- **形式変換 / 推測** (mime_type の auto-inference / explicit / fallback / 推測失敗時) +- **依存サービス障害時の挙動** (degraded / circuit breaker / fallback) + +**check phrasing**: ドメイン寄りに「X でこういう時どうするの?」を 5-10 件出す。回答が「未検討」「将来検討」になった場合は、design phase で答えを出すべき領域 + +**過去事例**: source_type の「組織が消えたらどうする?」「内部 FAQ vs 外部 FAQ をどう分ける?」が turn 4 で発覚 → 分類軸を format に絞る判断に + +### 5. 既存 SoT との整合 (grep-first) + +新命名 / 新構造を出す **前** に既存 SoT を全 grep: + +- 旧 field 名 / 旧 method 名 / 旧 enum 値 / 旧 ADR 番号 / 旧 design doc 用語が docs / api.md / metadata literal / SDK example / proto / Go code / Markdown notes に残っていないか +- 修正範囲を **全 file 一気に** 把握 (turn を跨いだ段階的発覚を防ぐ) +- design 完了時に grep 検証条件を 5-10 件用意 (`grep -nE "..."` の literal、PR description / commit description / ADR appendix で記録) + +Bash で: +```bash +grep -rnE "<old-name-pattern>" docs/ apis/ internal/ cmd/ cli/ +``` + +の hit list を design 結果に含めて、修正範囲を確定させる。 + +**check phrasing**: 「旧名 を grep して 0 hits になる条件は何か」「新名 が何箇所追加されるか」 + +**過去事例**: `accessScopes` 旧 surface が docs §11.2 SDK example に残置、turn 4 で発覚 → 設計時 grep で先に全箇所把握すべきだった + +### 6. memory 規約準拠 (毎回チェック) + +設計対象が以下 規約 のいずれにも違反していないか 1 周 review (SoT は CLAUDE.md / 各 skill): + +| 規約 | 違反しがちな箇所 | +|---|---| +| Phase / ticket をコメントに残さない | proto / Go コメントに Phase / ticket / PR 参照 | +| commit title は簡潔 (1 行) | commit title が長文 | +| コード系コメントは英語 | *.go / proto / Makefile / shell コメントが日本語 | +| push は user 明示指示後 (CLAUDE.md) | push 提案が user 確認なし | +| test 内 inline コメント不可 | test 内 inline コメント | +| docs 冒頭に前提節を立てない | docs 冒頭の scope 前提節 | +| 個別 ticket plan を repo に置かない | docs/plan/ に個別 ticket plan | +| コメントは原文 literal 範囲 (推測 mapping 禁止) | コメントで原文 literal 範囲を超えた推測 mapping | +| 多 file 改修は plan-first (CLAUDE.md) | 多 file 改修なのに plan 飛ばし | +| spec 逸脱は明示・承認・記録 (CLAUDE.md) | spec literal 逸脱が plan に書かれていない | +| substantive edit は subagent 経由 | 主体 agent が直接 Edit (substantive) | +| PR は自動作成しない (CLAUDE.md) | PR 自動作成 | +| subagent brief は state-file 参照型 | subagent brief に context 反復 | +| 設計 phase checklist の遵守 | 本 checklist 自体の準拠漏れ | +| プロダクト用語の正確性 | プロダクト名・用語の誤称 | +| アーキ前提の遵守 | アーキテクチャ前提に反する用語混入 | + +unused import / 機械的整合の漏れも本観点で check。 + +## 出力フォーマット + +skill 完了時、以下を成果物として user に提示: + +```markdown +# api-design-review 結果 + +## 設計対象 +<対象 ADR / Design Doc / proto / API spec / domain model の path or 名前> + +## 観点別レビュー + +### 1. client 抽象 vs server 展開 +- 検出: <内容、または「該当なし」> +- 対応案: <提案、または「現状で OK」> + +### 2. ACL 読み書き両側 +- 検出: ... + +### 3. forward-compat +- 検出: ... + +### 4. edge case 列挙 +質問形式で 5-10 件、回答付き + +### 5. 既存 SoT 整合 (grep 結果) +- `grep -rnE "..."` → hit list + +### 6. memory 規約準拠 +- 違反候補: <内容、または「クリア」> + +## まとめ +- 設計を進めて OK (該当箇所なし): ◯ +- 修正必要 (具体箇所): X 件 → ... +- user 判断要 (trade-off 提示): Y 件 → ... +``` + +main agent / dev-orchestrator / Plan agent / tech-docs-writer / notion-ticket-plan に引き継ぐ場合、上記を state-file に書き出すと subagent 再起動時の brief が薄くなる (state-file 参照型 brief)。 + +## 適用しないこと + +- 実装 / Edit / Write は行わない (read-only review) +- git mutation / push / PR 作成しない +- skill 内で AskUserQuestion しない (結果を main agent に返し、main agent が user 判断を仰ぐ) + +## 関連 artifact + +- 日常 check (軽量版): CLAUDE.md「判断と質問の作法」 +- 関連 skill: `tech-docs-writer` (ADR / Design Doc 起票時、本 skill を内部で通過)、`notion-ticket-plan` (ticket 解析 / plan 起票時、本 skill 通過)、`ddd-hexagonal` (層境界・依存方向、本 skill 観点 1 と関連)、`code-refactor-advisor` (実装面の refactor 候補、本 skill の implementation pass version) +- 主 agent への引き継ぎ: state-file 参照型 brief +- agent 側組み込み: `dev-orchestrator` agent の plan phase で本 skill を通過 (組み込み済) diff --git a/claude/ja/skills/arc42-c4/SKILL.md b/claude/ja/skills/arc42-c4/SKILL.md new file mode 100644 index 0000000..f1ec465 --- /dev/null +++ b/claude/ja/skills/arc42-c4/SKILL.md @@ -0,0 +1,54 @@ +--- +name: arc42-c4 +description: Reference for architecture design docs combining arc42 (§1-12) with the C4 model (L1-L4) — which diagram goes in which section, top-level vs subsystem split. Use for "which section gets what", "§5 vs §6 vs §7", "ADR inside or separate". Reference skill, not a procedure. +--- + +# arc42-c4 + +arc42 (sections) and C4 (diagrams) are independent conventions; neither spec says which C4 diagram belongs in which arc42 section. **This skill pins that mapping as a house standard** to kill per-doc ambiguity. Project-specific doc maps live in the project's top-level design doc, not here. + +Use when writing / reviewing an arc42 + C4 design doc, or deciding the ADR / runbook / README boundary. Supplies section judgment to `tech-docs-writer`; a review lens to `api-design-review`. + +## arc42 §1-12 + +1 Introduction & Goals · 2 Constraints · 3 Context & Scope · 4 Solution Strategy · 5 Building Block View · 6 Runtime View · 7 Deployment View · 8 Crosscutting Concepts · 9 Architecture Decisions · 10 Quality Requirements · 11 Risks & Tech Debt · 12 Glossary + +- **§5 vs §6 vs §7** (same blocks, different axis): §5 = static structure ("is X a part?") · §6 = behavior over time ("what order do parts talk for use case Y?", insight-bearing scenarios only) · §7 = physical placement ("which node runs X?"). +- **§2 vs §4 vs §10** (same fact can appear in all three): §2 = limits you couldn't choose · §4 = fundamental choices you made · §10 = measurable quality scenarios. PostgreSQL: "policy mandates it" → §2; "we chose it for integrity" → §4; "reads < 50ms p95" → §10. §10 holds ends not means; §9 holds decision index not full ADRs. + +## arc42 × C4 mapping (house standard — decisive) + +| C4 | arc42 | +|----|-------| +| L1 System Context | **§3** | +| L2 Container | **§5** (top level) | +| L3 Component | **§5** (lower level / subsystem doc) | +| L4 Code | §5 deepest, or omit | +| dynamic diagram | **§6** | +| deployment diagram | **§7** | + +The 4 numbered C4 levels are **all static**; runtime → §6, placement → §7. **Dedup:** container structure in §5, container-on-infra mapping in §7 — cross-reference, never paste twice. + +## Multi-doc split (top-level ⇄ subsystem) + +Fold line = container boundary. **Top-level doc** owns L1 + L2 (the subsystem map / hand-off seam). **Subsystem doc** (one per container) owns L3 (+ L4 if used). The Container diagram is the join: top lists all containers, each subsystem points at its own then expands to components. Subsystem docs may use a **mini** arc42 subset (full §1-12 only at top level). + +## Boundary with adjacent conventions + +arc42 = durable design-time "shape + rationale". Different audience / cadence → separate doc, arc42 only **links**. + +| Convention | Location | arc42 touchpoint | +|-----------|----------|------------------| +| MADR (ADR) | `docs/adr/NNNN-*.md` | **§9 = index only** (id/title/status/link); full rationale, alternatives, consequences in the file | +| Diataxis (README/guides) | `docs/readme/` | link from §8 | +| SRE Playbook (runbook) | `docs/runbook/` | link from §7 / §11 | + +Cut an ADR when a decision is costly to reverse, contested, or constrains the future. + +## Common mistakes + +Flow in §5, or §7 re-pasting §5's diagram · treating a C4 level as "runtime" (all 4 are static) · freely-chosen tech filed under §2 · means in §10 instead of measurable ends · full ADR body in §9 · L1/L2 duplicated in subsystem docs · re-litigating the C4↔arc42 mapping as "just convention" (it's pinned above). + +## Related skills + +`tech-docs-writer` (writes the doc) · `api-design-review` (reviews it) · `ddd-hexagonal` (§5 / §8 layer boundaries). diff --git a/claude/ja/skills/claude-session-jsonl/SKILL.md b/claude/ja/skills/claude-session-jsonl/SKILL.md new file mode 100644 index 0000000..a7cce51 --- /dev/null +++ b/claude/ja/skills/claude-session-jsonl/SKILL.md @@ -0,0 +1,203 @@ +--- +name: claude-session-jsonl +description: Claude Code のセッションログ (~/.claude/projects/**/*.jsonl) のスキーマ参照と、token / cost / tool 集計 tool を作るレシピ。「Claude の使用状況を観測したい」「セッションログから集計」「ccusage 相当を作る」「JSONL の中身教えて」等で使う。言語別サンプル parser は references/ にある。 +--- + +# claude-session-jsonl + +Claude Code がローカルに残すセッションログ (JSONL) の構造を **正確に** 知っていることを前提にした集計 tool は何度も書きたくなる (ccwatch / ccusage / cclog / ccexport ...)。毎回手で `head -1 *.jsonl | jq` から始めると時間が溶けるので、確認済みのスキーマと典型的な落とし穴を 1 箇所に固める。 + +> **Snapshot**: 2026-04 時点 / Claude Code 2.1.x が書き出す JSONL を観測してまとめたもの。 +> Claude Code がスキーマを更新したら (event type 増加、usage フィールド変更等) ここも更新。 +> 単価表は Anthropic の公式ページが原本、ここはキャッシュ。 + +## いつ使うか + +- 「Claude の使用状況を tool 化したい」 +- 「セッション jsonl をパースして X を出したい」 +- 「ccusage / ccwatch みたいなのを別言語で作りたい」 +- 「context window 使用率を計算したい」 + +実装言語は問わない (Rust / TypeScript / Python が主)。本 skill はスキーマ説明 + 設計判断 + 各言語の最小 parser を提供する。**実装そのものは別の手段** (`/rust-bootstrap` → `/add-rust-crate` → 実装 / TS なら手作業) に渡す。 + +## ファイル配置 + +``` +~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl +``` + +- **`<encoded-cwd>`** は `cwd` の `/` を `-` に置換した文字列。例: `/Users/foo/repo` → `-Users-foo-repo` +- **`<session-uuid>`** は v4 UUID (例: `ec65e22c-0dab-4eff-b119-c2e2cb02aa8a`) +- 1 ファイル = 1 セッション。`/clear` で新ファイル +- 同 cwd で複数セッションが共存しうる (古い + 進行中) +- ファイルは **append-only**。途中行の書き換えは原則ない (rotation は新 UUID 別ファイル化) + +最新セッションを拾う = 「**最終更新時刻 (`mtime`) が一番新しい `*.jsonl`**」を取れば良い。 + +## スキーマ (event types) + +各行は 1 つの JSON オブジェクト。`type` フィールドで分岐。 + +| `type` | 用途 | 集計に使う? | +|---|---|---| +| `assistant` | アシスタント発話 (token usage / tool 呼び出し / model 名がここ) | **YES (主役)** | +| `user` | ユーザ発話 / コマンド出力 (`/`-command の caveat 含む) | 必要なら | +| `system` | システムメッセージ (slash command の stdout 等) | 通常不要 | +| `attachment` | 添付/イベント (deferred tool delta 等) | 通常不要 | +| `file-history-snapshot` | ファイル履歴スナップショット (Edit のロールバック用) | 不要 | +| `last-prompt` | 最後の user prompt (再生成用キャッシュ) | 不要 | + +**他の type が出ても無視する** (`#[serde(other)]` / catch-all) のが正解。Claude Code の更新で新 type が増えたとき壊れない。 + +## 集計に必須: `assistant` event + +形 (一部省略): + +```json +{ + "type": "assistant", + "uuid": "997e63ff-e2c6-...", + "parentUuid": "d6c11651-...", + "sessionId": "ec65e22c-...", + "timestamp": "2026-04-27T04:32:12.600Z", + "message": { + "id": "msg_018Znq...", + "model": "claude-opus-4-7", + "role": "assistant", + "type": "message", + "content": [ + { "type": "thinking", "thinking": "...", "signature": "..." }, + { "type": "text", "text": "hello" }, + { "type": "tool_use", "name": "Bash", "input": { ... } } + ], + "usage": { + "input_tokens": 6, + "output_tokens": 1103, + "cache_creation_input_tokens": 9667, + "cache_read_input_tokens": 15206, + "service_tier": "standard", + "cache_creation": { "ephemeral_1h_input_tokens": 9667, "ephemeral_5m_input_tokens": 0 }, + "iterations": [ ... ] + }, + "stop_reason": "end_turn" + }, + "requestId": "req_011...", + "cwd": "/Users/foo/repo", + "version": "2.1.x", + "gitBranch": "main" +} +``` + +### 重要フィールド + +| パス | 型 | 用途 | +|---|---|---| +| `message.model` | string | 例: `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`. 大文字小文字混在 (`[1m]` で 1M context variant) | +| `message.usage.input_tokens` | u64 | このターンで billable な新規 input | +| `message.usage.output_tokens` | u64 | アシスタント生成の output | +| `message.usage.cache_creation_input_tokens` | u64 | キャッシュ書き込み合計 (5min + 1hr) | +| `message.usage.cache_creation.ephemeral_5m_input_tokens` | u64 | 5min ephemeral 書き込み (1.25× input) | +| `message.usage.cache_creation.ephemeral_1h_input_tokens` | u64 | 1hr ephemeral 書き込み (**2.00× input**) | +| `message.usage.cache_read_input_tokens` | u64 | キャッシュヒット (最安, 0.10× input) | +| `message.content[].type` | string | `tool_use` / `text` / `thinking` ほか | +| `message.content[].name` | string | `tool_use` 限定。`Bash` / `Edit` / `Read` / `Grep` / `Write` / `Agent` / `WebFetch` 等 | +| `timestamp` | RFC3339 string | レイテンシ計算 / sliding window / active 検出 | + +> ⚠ `cache_creation_input_tokens` (top-level 合計) と `cache_creation.{5m,1h}` (内訳) の両方が出る。**コスト計算には内訳を使うこと**。Claude Code の実セッションは現在ほぼ全部 1hr ephemeral で、この差は数十%以上のコスト誤差になる。 + +### 派生量 (頻出) + +``` +context_size = input_tokens + cache_creation_input_tokens + cache_read_input_tokens +context_window = 200_000 (default) | 1_000_000 (model 名に [1m] / -1m) +context_pct = min(context_size / context_window, 1.0) +cache_hit_ratio = cache_read_input_tokens / context_size +session_cost_usd = sum over assistants of pricing(model).cost_usd(usage) +tokens_per_minute = (input + output + cache_creation) / elapsed_secs * 60 +cost_per_hour = session_cost_usd / elapsed_secs * 3600 +``` + +## モデル別単価 (USD per million tokens, 2026-04 時点) + +> ⚠ Anthropic のページで定期的に更新される値。tool 化したらコメントで「2026-04 時点」を残し、ハードコードを 1 箇所に集約 (例: `pricing.rs`) しておくこと。 + +| family | input | output | cache_w 5min | cache_w 1hr | cache_read | +|---|---:|---:|---:|---:|---:| +| Opus | 15.00 | 75.00 | **18.75** (1.25×) | **30.00** (2.00×) | 1.50 (0.10×) | +| Sonnet | 3.00 | 15.00 | 3.75 | 6.00 | 0.30 | +| Haiku | 1.00 | 5.00 | 1.25 | 2.00 | 0.10 | + +cost 計算式: + +``` +cost = input × input_rate + + output × output_rate + + cache_5m × cache_w_5m_rate # cache_creation.ephemeral_5m_input_tokens + + cache_1h × cache_w_1h_rate # cache_creation.ephemeral_1h_input_tokens + + cache_read × cache_read_rate +``` + +`cache_creation` フィールドが欠けている古いログは「全部 5min」とみなしてフォールバックすると後方互換が取れる。 + +未知モデル名は **Sonnet にフォールバック** が無難 (中間値・現行 default tier)。 + +## 設計判断 — 落とし穴と推奨 + +1. **ファイル監視は poll-based + reopen + seek + read_to_end + remainder buffer**。`notify` crate は append-only ログには rotation/atomic-replace でハマるので避ける。`tokio::io::BufReader::lines()` over manually-seeked file も EOF 扱いが面倒で結局再 seek が要る。**自前 200ms ポーリング + `\n` で split が最もロバスト**。 +2. **行は `\n` で確実に区切られる**。途中で UTF-8 codepoint が割れる心配は不要 (codepoint は完結状態で書き込まれる)。 +3. **ファイル shrink (≒ rotation) で offset を 0 に reset**。`/clear` 等で新ファイル化されると別 UUID なので、watcher は「新しい mtime のファイルを再選択」する設計が良い (本 skill 範囲外)。 +4. **`last-prompt` は最終 prompt のキャッシュ用**で集計には使わない (内容は user 発話と重複)。 +5. **`thinking` ブロックの token 数は `output_tokens` に含まれている** (別カウントしない)。 +6. **`iterations` 配列**は server-side の細分化情報。集計では一番外の `usage` を信用すれば十分。 +7. **model 名の判定は `to_ascii_lowercase().contains("opus" | "sonnet" | "haiku")`** で family を取る。完全一致は壊れる (`claude-opus-4-7`, `claude-opus-4-7[1m]`, `claude-3-opus-...` 等が混在)。 +8. **encoded-cwd を逆引きする必要があるか?** ある (どのリポジトリのセッションか表示する場合)。`-` を `/` に戻すだけだが、もとの cwd に `-` が含まれていた場合は不可逆 — `assistant.cwd` が原本としてイベント内に入っているのでそちらを参照するのが安全。 +9. **集計はセッション単位** が基本。「全セッション横断で today の累計コスト」を出したい場合は `<projects-dir>/**/*.jsonl` の各ファイル全行を読み、`timestamp` で当日フィルタ → usage 合算する (重め)。 + +## 最小 parser (言語別) + +実装言語が決まったら **該当する reference だけ** Read する (全言語を読まない): + +| 言語 | reference | 要点 | +|---|---|---| +| Rust | `references/parser-rust.md` | serde tagged enum + `#[serde(other)]` catch-all | +| TypeScript | `references/parser-typescript.md` | zod discriminatedUnion + catch-all | +| Python | `references/parser-python.md` | dict ベース + dataclass Usage | + +## 動作確認用コマンド + +```bash +# セッションファイル一覧 (最新順) +ls -lt ~/.claude/projects/*/ | head -20 + +# 最新ファイルの event type 分布 +jq -r .type < $(ls -t ~/.claude/projects/*/*.jsonl | head -1) | sort | uniq -c + +# 最新ファイルの usage を 1 行ずつ +jq -c 'select(.type=="assistant") | {model: .message.model, usage: .message.usage}' \ + < $(ls -t ~/.claude/projects/*/*.jsonl | head -1) | head -3 + +# 当日全セッションのモデル別 output_tokens 合計 +jq -r 'select(.type=="assistant" and .timestamp > "'$(date -u +%Y-%m-%d)'") | + [.message.model, .message.usage.output_tokens] | @tsv' \ + ~/.claude/projects/*/*.jsonl | + awk -F'\t' '{m[$1]+=$2} END {for (k in m) printf "%-30s %d\n", k, m[k]}' +``` + +## 鉄則 + +1. **未知 type は捨てる**。Claude Code の更新で増えても壊さない設計を default にする +2. **`thinking` は output_tokens に含まれている**。二重カウントしない +3. **モデル名判定は substring で family を取る**。完全一致しない +4. **単価表は 1 箇所に集中**。「2026-04 時点」のコメント必須 +5. **集計はファイル単位 → セッション単位**。複数日にまたがる解析でも `timestamp` フィルタを先にかける +6. **read-only**。本 skill のコードはセッションファイルを書き換えない (検証用 jq も読み取りのみ) + +## アンチパターン + +- **`notify` crate でログ tail**: rotation で取りこぼす +- **`type` を完全一致で enum 解釈**: 未知 type で panic / unwrap +- **`assistant` 以外の event の `usage` を読む**: そもそも存在しない +- **モデル名を完全一致で hash 引き**: バージョン suffix で全部 miss +- **`cache_*` を input_tokens に含める計算**: 単価が違う 4 種を混ぜると 5x 程度のずれが出る +- **encoded-cwd を逆引きで cwd 復元**: `-` の曖昧さで誤動作。`assistant.cwd` を読む +- **`read_to_string` でファイル丸ごと**: 大きなセッションでメモリ膨張。`BufReader` + line iter でストリーム diff --git a/claude/ja/skills/claude-session-jsonl/references/parser-python.md b/claude/ja/skills/claude-session-jsonl/references/parser-python.md new file mode 100644 index 0000000..0ec000f --- /dev/null +++ b/claude/ja/skills/claude-session-jsonl/references/parser-python.md @@ -0,0 +1,45 @@ +# 最小 parser (Python) + +dict ベースで `type != "assistant"` を捨てる。dataclass は usage のみ。 + +```python +import json +from dataclasses import dataclass, field + +@dataclass +class Usage: + input_tokens: int = 0 + output_tokens: int = 0 + cache_creation_input_tokens: int = 0 + cache_read_input_tokens: int = 0 + + @property + def context_size(self) -> int: + return ( + self.input_tokens + + self.cache_creation_input_tokens + + self.cache_read_input_tokens + ) + +def parse_line(line: str): + line = line.strip() + if not line: + return None + d = json.loads(line) + if d.get("type") != "assistant": + return None + msg = d.get("message", {}) + u = msg.get("usage") or {} + tools = [c.get("name") for c in msg.get("content", []) if c.get("type") == "tool_use"] + return { + "model": msg.get("model"), + "tools": tools, + "usage": Usage( + input_tokens=u.get("input_tokens", 0), + output_tokens=u.get("output_tokens", 0), + cache_creation_input_tokens=u.get("cache_creation_input_tokens", 0), + cache_read_input_tokens=u.get("cache_read_input_tokens", 0), + ), + "timestamp": d.get("timestamp"), + } +``` diff --git a/claude/ja/skills/claude-session-jsonl/references/parser-rust.md b/claude/ja/skills/claude-session-jsonl/references/parser-rust.md new file mode 100644 index 0000000..9a9a624 --- /dev/null +++ b/claude/ja/skills/claude-session-jsonl/references/parser-rust.md @@ -0,0 +1,66 @@ +# 最小 parser (Rust) + +serde の tagged enum + `#[serde(other)]` catch-all で未知 type を安全に無視する。 + +```rust +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub(crate) enum Event { + Assistant(AssistantEvent), + #[serde(other)] + Other, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct AssistantEvent { + pub message: AssistantMessage, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct AssistantMessage { + #[serde(default)] pub model: Option<String>, + #[serde(default)] pub content: Vec<ContentBlock>, + #[serde(default)] pub usage: Option<Usage>, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub(crate) enum ContentBlock { + ToolUse { #[serde(default)] name: String }, + #[serde(other)] Other, +} + +#[derive(Debug, Default, Deserialize, Clone, Copy)] +pub(crate) struct Usage { + #[serde(default)] pub input_tokens: u64, + #[serde(default)] pub output_tokens: u64, + #[serde(default)] pub cache_creation_input_tokens: u64, // 5min + 1hr 合計 + #[serde(default)] pub cache_read_input_tokens: u64, + #[serde(default)] pub cache_creation: Option<CacheCreation>, +} + +#[derive(Debug, Default, Deserialize, Clone, Copy)] +pub(crate) struct CacheCreation { + #[serde(default)] pub ephemeral_5m_input_tokens: u64, + #[serde(default)] pub ephemeral_1h_input_tokens: u64, +} + +impl Usage { + /// (5min, 1hr) breakdown for cost calc. Old logs without `cache_creation` + /// are treated as all-5min for backward compat. + pub fn cache_creation_split(&self) -> (u64, u64) { + match self.cache_creation { + Some(c) => (c.ephemeral_5m_input_tokens, c.ephemeral_1h_input_tokens), + None => (self.cache_creation_input_tokens, 0), + } + } +} + +pub(crate) fn parse_line(line: &str) -> serde_json::Result<Option<Event>> { + let t = line.trim(); + if t.is_empty() { return Ok(None); } + Ok(Some(serde_json::from_str(t)?)) +} +``` diff --git a/claude/ja/skills/claude-session-jsonl/references/parser-typescript.md b/claude/ja/skills/claude-session-jsonl/references/parser-typescript.md new file mode 100644 index 0000000..295610e --- /dev/null +++ b/claude/ja/skills/claude-session-jsonl/references/parser-typescript.md @@ -0,0 +1,38 @@ +# 最小 parser (TypeScript) + +zod の discriminatedUnion + `.or(z.object({ type: z.string() }))` catch-all で未知 type を安全に無視する。 + +```ts +import { z } from "zod"; + +const Usage = z.object({ + input_tokens: z.number().default(0), + output_tokens: z.number().default(0), + cache_creation_input_tokens: z.number().default(0), + cache_read_input_tokens: z.number().default(0), +}); + +const ContentBlock = z.discriminatedUnion("type", [ + z.object({ type: z.literal("tool_use"), name: z.string() }), + z.object({ type: z.literal("text"), text: z.string().optional() }), + z.object({ type: z.literal("thinking"), thinking: z.string().optional() }), +]).or(z.object({ type: z.string() })); // catch-all + +const AssistantEvent = z.object({ + type: z.literal("assistant"), + message: z.object({ + model: z.string().optional(), + content: z.array(ContentBlock).default([]), + usage: Usage.optional(), + }), +}); + +const Event = z.discriminatedUnion("type", [AssistantEvent]) + .or(z.object({ type: z.string() })); // catch-all + +export function parseLine(line: string) { + const t = line.trim(); + if (!t) return null; + return Event.parse(JSON.parse(t)); +} +``` diff --git a/claude/ja/skills/commit-push-branch/SKILL.md b/claude/ja/skills/commit-push-branch/SKILL.md new file mode 100644 index 0000000..589c519 --- /dev/null +++ b/claude/ja/skills/commit-push-branch/SKILL.md @@ -0,0 +1,183 @@ +--- +name: commit-push-branch +description: 作業ツリーの変更を過去の commit スタイル (type prefix / ticket ID / Co-Authored-By) に倣ったメッセージで、新 branch を切って commit & push する。「branch 切って commit & push して」「PR 用に push」等で使う。 +--- + +# commit-push-branch + +新しい branch を切って、過去スタイルに倣った commit を作り、push まで行う skill。PR 作成は別 (`gh pr create`)。 + +## 適用条件 + +- git リポジトリ内 +- 作業ツリーに commit すべき変更がある (`git status` で何か出る) +- リモート `origin` が設定済み + +## 手順 + +### Step 1: 状態確認 + +```bash +git status +git diff --stat +git diff --cached +git log -3 --format='%H%n%B%n---' # 過去スタイル把握 +``` + +### Step 2: 過去スタイル抽出 + +`git log -3` の出力から: +- **type prefix の慣用** (`chore:`, `feat:`, `fix:`, `docs:`, `refactor:`, `test:`) +- **タイトルの言語** (英語 / 日本語) +- **ticket ID の置き方** (`(PROJ-123)` / `Refs: PROJ-123` / `(#123)` 等、リポジトリ慣例次第) +- **HEREDOC で多行記述** されているか +- **Co-Authored-By 行** が付いているか +- **PR merge style** (squash か merge commit か) + +### Step 3: type と branch 名の決定 + +| 変更内容 | type | branch prefix | +|---|---|---| +| 新機能 | `feat` | `feat/` | +| バグ修正 | `fix` | `fix/` | +| インフラ / 設定 / build / ツール | `chore` | `chore/` | +| ドキュメントのみ | `docs` | `docs/` | +| リファクタ (機能変化なし) | `refactor` | `refactor/` | +| テスト追加 | `test` | `test/` | + +branch 名のパターン: + +| ケース | 形式 | 例 | +|---|---|---| +| ticket あり | `<prefix>/<ticket-id>-<description-slug>` | `chore/proj-123-add-feature` | +| ticket 無し | `<prefix>/<description-slug>` | `docs/api-error-codes-cleanup` | + +description slug は kebab-case で短く (3-5 語)。ticket ID だけで branch を識別せず、必ず内容を表す slug を付ける。 + +過去 PR が ticket ID を含む慣習なら ticket ID を付与、ticket 無しの慣習 (主に `docs/` / `chore/` 系で見られる) なら slug only。**判定は Step 2「過去スタイル抽出」の結果に従う** (本 table はあくまでパターン例で、Step 2 を override しない)。 + +### Step 4: branch 作成 + +```bash +git checkout -b <prefix>/<ticket-id>-<slug> +``` + +既存 branch にぶつかったら `-N` などで suffix。 + +### Step 5: 明示的 add + +`-A` / `-a` を**使わない**。新規ファイル / 編集ファイルを明示的に列挙: + +```bash +git add <file1> <file2> <dir1>/ <dir2>/ +git status # 確認 +``` + +`.env` / `*.pem` / `credentials*` 等の secret パターンが含まれないこと確認。 + +### Step 6: commit + +**default は title 1 行のみ**。`-m "<title>"` で十分。HEREDOC + body は default にしない。 + +```bash +git commit -m "<type>: <変更内容>" +# または scope 付き +git commit -m "<type>(<scope>): <変更内容>" +``` + +title に書くもの = **変更内容のみ**。why / 背景 / 影響範囲 / rot リスク / ticket 文脈は title にも body にも書かない (PR description / Notion ticket / git 履歴で追える)。 + +良い例: +- `chore: ブートログから phase フィールドを削除` +- `docs(api): SearchMeta を RequestMeta にリネームし common.proto へ切り出す` +- `chore: translate Makefile comments and error messages to English` + +悪い例 (verbose / why 入り): +- `chore: ブートログから phase フィールドを削除\n\nphase 値はログに焼き付けると rot するだけで利得がない。` +- `chore: 前 commit で導入した slog.Info の "phase" 引数を削除して rot を回避` + +#### body / HEREDOC を使う例外 + +下記のときだけ body を書く: +- **breaking change** → `BREAKING CHANGE: <impact>` 行を含める +- **本当に非自明な why** があり、PR description には残せない事情がある (稀) +- **過去スタイルが HEREDOC + body 必須** で揃っている (Step 2 の調査結果による) + +例外時のテンプレ: + +```bash +git commit -m "$(cat <<'EOF' +<type>: <変更内容> + +<最小限の why。1-2 行。> + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> +EOF +)" +``` + +注意: +- HEREDOC は `'EOF'` (シングルクォート) で展開抑止 +- `Co-Authored-By:` は **過去 commit に倣ってあれば付ける、無ければ付けない**。短文 commit でも `-m "..." -m "Co-Authored-By: ..."` のように 2 行指定すれば付与可能 (リポジトリ慣例次第) +- ticket ID `(PROJ-123)` 等の付与も過去スタイル次第。title が長くなるなら省略しても可 + +#### GPG signing hang への対処 + +`commit.gpgsign=true` 設定の環境では `git commit` が pinentry 待ちで hang することがある (agent 環境は TTY が無く pinentry GUI が起動できない、または gpg-agent cache 切れで passphrase 待ち)。 + +対処手順: + +1. **`git commit` 実行は Bash tool の timeout を 30 秒に設定** (default の 2 分待たない) +2. **hang / timeout 検出時**: `git status` で staged 状態が維持されていることを確認 (commit は失敗、ファイルは staged のまま) +3. **`ps aux | grep -E "gpg|git commit" | grep -v grep`** で hung プロセスがあるか確認、必要なら user に kill を依頼 +4. **user に手動 commit を案内**: + - 別ターミナルで `echo test | gpg --clearsign > /dev/null` を実行 → pinentry でパスフレーズ入力 → cache が温まる + - もしくは下記コマンドを user 側で直接実行: + ```bash + git commit -m "<type>: <変更内容>" -m "Co-Authored-By: <name> <email>" + ``` +5. **手動 commit 完了後、user から「commit done」等の合図を受けたら skill 側で push に進む** (`git log --oneline -1` で確認後) +6. cache が温まれば以降の commit / push は agent 側でも通る (続く Step 7 push も skill 側で実行可) + +注意: +- skill 側で勝手に `--no-gpg-sign` や `commit.gpgsign=false` で workaround しない (鉄則 #2「`--no-verify` 禁止」と同精神 — 安全スキップは user 明示要請があるときだけ) +- staged 状態は hang しても破壊されないので慌てて reset しない + +### Step 7: push + +```bash +git push -u origin <branch-name> +``` + +main / master への直 push は警告 (作業 branch であることを Step 4 で保証している前提)。 + +### Step 8: 完了報告 + +| 項目 | 値 | +|---|---| +| Branch | `<prefix>/<ticket-id>-<slug>` | +| Commit | `<short-sha>` (`<type>: ...`) | +| Files | `<n>` files (+<additions>/-<deletions>) | +| PR 作成 URL | (push 出力から抽出: `https://github.com/<org>/<repo>/pull/new/<branch>`) | + +PR 作成は別タスク。ユーザーが指示したら `gh pr create` で続ける。 + +## 鉄則 + +1. **新 commit を作る**: `--amend` を使わない (前の commit を破壊する可能性) +2. **`--no-verify` 禁止**: pre-commit hook を尊重。失敗したら原因を直す +3. **branch を main に直 push しない**: 必ず作業 branch +4. **`git add -A` / `-a` を使わない**: 明示列挙で secret 混入を防ぐ +5. **過去スタイル尊重**: 前 3 commit の type / 言語 / ticket 表記 / Co-Authored-By 慣例に揃える +6. **PR は user 指示後**: skill は push まで。`gh pr create` はユーザーから指示があれば +7. **default は title 1 行・変更内容のみ**: why / 背景 / 影響範囲は書かない。body は breaking change / 本当に非自明な why のときだけ + +## アンチパターン + +- `git commit -am` (untracked が漏れる + 全 staged を盲目 commit) +- `git push --force` を作業 branch でも安易に使う +- HEREDOC を `EOF` (クォート無し) で書いて変数展開される +- 過去スタイルを確認せずに英語タイトルで commit (リポジトリは日本語タイトル慣例だった等) +- `Co-Authored-By` を独断で付ける / 付けない (リポジトリ慣例に合わせる) +- title に why や rot リスクや「〜のため」を書く (変更内容のみ書く) +- 単純な commit でも HEREDOC + 数行 body を default で書く (1 行で済むなら 1 行) diff --git a/claude/ja/skills/ddd-hexagonal/SKILL.md b/claude/ja/skills/ddd-hexagonal/SKILL.md new file mode 100644 index 0000000..d98e7df --- /dev/null +++ b/claude/ja/skills/ddd-hexagonal/SKILL.md @@ -0,0 +1,268 @@ +--- +name: ddd-hexagonal +description: DDD + Hexagonal architecture の reference skill。layer 境界 / 依存方向 / Port-Adapter / ACL / Aggregate / Repository / DTO 変換 / cross-cutting を扱う。「層曖昧」「責務違反」「port の切り方」等の問いや設計 review 時に参照する。手順 skill ではない。 +--- + +# ddd-hexagonal + +DDD + Hexagonal architecture の慣用句・原則を集めた reference skill。設計判断 / review / refactor 候補出しの判断基準として参照する。 + +## 適用条件 + +- DDD + Hexagonal を採用したプロジェクト (`internal/{domain,application,interfaces,adapters}/` レイアウト) +- code-refactor-advisor agent からの参照 +- 層境界 / port 設計 / adapter 切り方の判断場面 + +## 1. Layer 定義 + +| Layer | path | 責務 | 依存可能な内側 layer | +|---|---|---|---| +| **Domain** | `internal/domain/` | Entity / Value Object / Aggregate / Domain Service / Domain Event / sentinel error。**外部に対する依存は持たない** (純粋ロジック) | (最内側、依存先なし) | +| **Application** | `internal/application/` | Use Case orchestration / Application Service / Port (interface 定義) / DTO | Domain | +| **Interfaces** | `internal/interfaces/` | Inbound adapter (gRPC handler / HTTP handler / CLI / interceptors / wire shape ⇄ application DTO 変換) | Application + Domain | +| **Adapters** | `internal/adapters/` | Outbound adapter (DB / vendor SDK / external API 実装、Port を実装) | Application (Port 実装のため) + Domain (型参照のため) | + +検出シグナル: +- domain が adapter / application を import している (依存方向違反) +- adapter が interfaces を import している (横方向依存、reuse 不可) +- application が adapter を直接 import (= port 抽象化欠如) + +## 2. 依存方向 (内向き原則) + +``` +Interfaces ──┐ ┌── Adapters + ├──> Application ──> Domain <──────┤ + └─────────────────────────────────┘ +``` + +- **依存は常に内向き** (外側 → 内側)。内側 (Domain) は外側 (Application / Adapters) を知らない +- 外側との通信は **Port (interface)** 経由。Application が Port を定義、Adapter が実装 +- 内側 → 外側の通信が必要な場合は **Domain Event + 外側で subscribe** などの仕組みで反転 + +検出シグナル: +- Domain code 内に `import ".../adapters/..."` / `import ".../interfaces/..."` +- Application code 内に `import ".../adapters/concrete-vendor"` (= concrete adapter 直 import、port 経由でない) + +## 3. Port-Adapter pattern + +- **Port** = interface (Application 層に定義)。例: `EmbeddingPort` / `VectorStorePort` +- **Adapter** = Port の実装 (Adapters 層)。例: `openai.Adapter` (= `EmbeddingPort` 実装) / `pinecone.Adapter` (= `VectorStorePort` 実装) +- Port は **vendor-neutral**。SDK 固有型を漏らさない (e.g. `[]float32` を渡す、`openai.EmbeddingResponse` は port shape に登場させない) +- Port の signature には **必要最小限のメソッド**。adapter で使わない method は port に入れない (`I` 接尾辞 / `xxxer` 命名で 1 method port も OK) + +例: +```go +// Application 層: port 定義 +type EmbeddingPort interface { + Embed(ctx context.Context, req EmbedRequest) (*EmbedResult, error) +} + +// Adapter 層: port 実装 +package openai +type Adapter struct { ... } +func (a *Adapter) Embed(ctx context.Context, req ports.EmbedRequest) (*ports.EmbedResult, error) { + // SDK 固有 logic は adapter 内に閉じる +} +``` + +検出シグナル: +- Port shape に SDK 固有型が登場 (`*openai.EmbeddingResponse` / `*pinecone.QueryResponse` 等) +- Adapter が Port を実装していない (= adapter として位置付け不明、ad-hoc 直接 import される) +- Application code が adapter を concrete type で受け取っている (= mock 不可、test で fake 注入できない) + +## 4. Anti-Corruption Layer (ACL) + +- 外部 system / vendor との境界で **語彙 / 型 / 例外モデル** を変換する変換層 +- adapter 内の boundary で **vendor 固有 → port-neutral** 変換 + **vendor 固有 error → port sentinel error** wrap +- 反対方向 (interface → application) も同じ。wire shape (proto / JSON) → application DTO 変換 + +例: +```go +// Adapter 内 ACL +func (a *Adapter) Embed(ctx context.Context, req ports.EmbedRequest) (*ports.EmbedResult, error) { + sdkResp, err := a.client.Embeddings.New(ctx, openai.EmbeddingNewParams{...}) + if err != nil { + return nil, fmt.Errorf("openai: %w: %w", ports.ErrEmbeddingProviderUnavailable, err) + } + // sdkResp.Data → []ports.Vector への変換 + return toPortResult(sdkResp), nil +} +``` + +検出シグナル: +- handler が SDK 型を引数 / 戻り値で扱っている (= ACL 欠如、application が vendor lock-in) +- Application 層が `pinecone.Client` を受け取っている (Port 経由でない) +- vendor 固有 error が application 層 / handler に直接漏れている (sentinel wrap なし) + +## 5. Ubiquitous Language (UL) + +- domain 用語は **business / docs / code で同じ語彙**を使う +- 例: docs で「ナレッジチャンク」と呼ぶなら code も `Chunk` (`Document` / `Item` 等の別名混在を避ける) +- wire shape (proto field 名 / JSON key) も UL に揃える +- 部署 / 関係者で語彙が割れている場合 → docs 側で先に統一する (ADR で語彙確定) + +検出シグナル: +- 同じ概念に対して package ごとに異なる名前 (`User` / `Member` / `Account` / `Principal` 混在) +- proto field 名と Go struct field 名が異なる (`product_ids` ⇔ `ProductIDList`) +- docs と code で用語が違う (docs: 「コレクション」 / code: `Index` `Collection` 混在) + +## 6. Aggregate / Entity / Value Object + +- **Entity**: identity (ID) を持つ object。`Chunk{ID, Text, ...}` 等 +- **Value Object**: identity なし、value の equality で判断。`Vector []float32` / `EmbedRequest{Inputs []string}` 等 +- **Aggregate**: 整合性境界。Aggregate Root を経由してのみ内部 entity を操作する +- Aggregate Root は **business invariant を保護**。例: `Document` を root とすれば `Document.AddChunk(c)` で chunk_sequence の連続性を保証 + +注意: +- 過剰な Aggregate / Value Object 化は YAGNI。**business rule が無い simple data** は plain struct で十分 +- Phase 1 prototype 期は `Hit{ChunkID, Text, ...}` のような plain struct で start、business rule が出てきたら昇格 + +検出シグナル: +- Entity と Value Object の区別が無い (= 全部 plain struct、ID 比較で identity 扱いされていない) +- Aggregate Root を skip して内部 entity が外部から直接編集されている + +## 7. Application Service vs Domain Service + +- **Application Service**: use case 単位の orchestration。複数 port / domain entity を呼び出して 1 つの business operation を達成 +- **Domain Service**: 単一 entity に閉じない domain logic (entity 同士の演算 / domain rule 検証) +- 両方 stateless。Application Service は dependency 注入 (port 群 + logger)、Domain Service は domain object のみ受け取る + +例: +```go +// Application Service (推奨 pattern) +type Service struct { + embedding ports.EmbeddingPort + vectorStore ports.VectorStorePort +} +func (s *Service) Search(ctx, in SearchInput) (*SearchOutput, error) { + // Embed → vector search → hit 変換 (use case orchestration) +} +``` + +検出シグナル: +- Domain Service が port を import (依存方向違反) +- Application Service が validation だけしている (= business orchestration が無い、handler に統合できる) +- Application Service が 1 method しかない (= use case 1 つだけ、struct にする意義が薄い場合あり) + +## 8. Repository pattern + +- DB / persistent store への抽象は **Repository** (port の特殊形) +- entity 単位で粒度を切る: `ChunkRepository` (CRUD on Chunk) / `DocumentRepository` +- 検索専用 / read model は別 Repository に切ることもある (`ChunkSearchRepository`) +- **Vendor-neutral**。SQL / 表名 / SDK 詳細を漏らさない + +例: +```go +type ChunkRepository interface { + Save(ctx context.Context, c Chunk) error + GetByID(ctx context.Context, id string) (*Chunk, error) + Delete(ctx context.Context, id string) error +} +``` + +検出シグナル: +- Repository に SQL string が引数 / 戻り値に登場 +- Repository が CRUD ではなく business logic を含む (= Application Service と責務混同) + +## 9. DTO / wire shape vs domain shape + +- 各層境界で **DTO 変換**を挟む: + - `Interfaces` 境界: wire shape (proto message / JSON request) ⇄ Application DTO (`SearchInput` / `SearchOutput`) + - `Application` 境界: Application DTO ⇄ Domain entity / Value Object + - `Adapters` 境界: Domain ⇄ vendor SDK shape (Anti-Corruption Layer) +- DTO 変換 helper は境界 layer に置く (`internal/interfaces/grpc/search_handler.go` 内 `toProtoResponse` 等) +- DTO 変換は pure function 推奨 (test 容易) + +検出シグナル: +- Domain entity が proto field tag を持つ / JSON tag を持つ (= wire shape と domain shape が混在) +- Wire shape のまま application 層を流れている (= DTO 変換欠如) + +## 10. Cross-cutting concerns + +- **Logging / Tracing / Auth / Rate Limit / Recovery** は cross-cutting で、**interfaces / adapters 境界に置く** +- gRPC: `UnaryServerInterceptor` / HTTP: middleware +- Application / Domain layer に直接 import しない (= business logic と分離) +- 例: `internal/observability/` に context key / logger factory を置き、各 interceptor が consume + +検出シグナル: +- Application Service / Domain Service の中で `slog.Info(...)` が直接呼ばれている (= cross-cutting がbusiness logic に混入) +- auth check が handler 内 inline で書かれている (= interceptor / middleware に切り出すべき) + +## 11. Domain Event (optional) + +- aggregate 内の状態変化を event として publish、外側 layer で subscribe +- 反対方向依存を回避するため: domain は event を発行するのみ、subscribe は application / adapter +- 過剰導入注意: Phase 1 prototype 期は不要、Phase 2 で audit / metrics / 連携が増えてきたら検討 + +## 12. 検出シグナル総括 (refactor 候補洗い出し用) + +agent から呼ばれたとき、以下の grep パターンで違反を検出: + +| 違反 | 検出 grep | +|---|---| +| Domain → 外側 import | `grep -r 'import.*adapters\|import.*interfaces' internal/domain/` | +| Application → concrete adapter import | `grep -r 'import.*adapters/[a-z]\+/' internal/application/ \| grep -v 'application/ports'` | +| Adapter → interfaces import | `grep -r 'import.*interfaces' internal/adapters/` | +| Port shape に SDK 固有型 | `grep -rn 'pineconego\.\|openaigo\.\|aws\.' internal/application/ports/` | +| Application Service 内 logger 直接呼び | `grep -rn 'slog\.Info\|slog\.Error' internal/application/ \| grep -v 'logger\.'` | +| handler / service 内に SDK 型直 import | `grep -rn 'pineconego\|openaigo' internal/interfaces/ internal/application/` | +| Repository に SQL string | `grep -rn 'SELECT\|INSERT\|UPDATE\|DELETE' internal/application/ports/` | + +## 13. Configuration injection (functional option + ConfigMap) + +設定値の override は **3 段階** で expose する: + +| Priority | 経路 | 用途 | +|---|---|---| +| 1 | **default const** (`defaultXxx`、code hardcode) | Phase 1 動作確認、安全な base value | +| 2 | **functional option** (`WithXxx(...)` constructor 時 inject) | deployment 単位 override、global 設定の wiring path | +| 3 | **proto field** (request 単位 inject) | **例外**、「どうしても個別対応必要」レベル (user 承認必須) | + +通常運用は (1) + (2)。proto 拡張は last resort。 + +例: +```go +const defaultRenderDPI = 150 +func WithDPI(dpi int) PDFOption { return func(pp *PDFParser) { pp.dpi = dpi } } + +// cmd/api-server/main.go (wiring) +parser := adapters.NewPDFParser(vlm, adapters.WithDPI(cfg.Parser.DPI)) // ConfigMap 値 +``` + +検出シグナル (refactor 候補): +- adapter / service に `defaultXxx` const なし → 数値リテラルが code 内に散在 +- `WithXxx(...)` Option が export されていない → cmd 側で値を override 不可 +- adapter struct field が public → caller 直接代入で immutable 性破綻 +- proto field に adapter internal 値が露出 → wire surface 膨張 + +project に functional option 採用の ADR があればそちらも参照。 + +--- + +## False positive 判定基準 + +以下に該当する場合は本 skill の検出シグナルがヒットしても **違反扱いしない**: + +- **生成コード由来**: protoc / buf / openapi-generator / `go generate` などで生成された symbol / file (典型: ファイル冒頭に `Code generated by ... DO NOT EDIT.` 行を含む)。生成元の schema 側で改善されるべきで、生成後の Go コードを手で直さない +- **言語 / library の慣用 pattern**: `func(...) (resp any, err error)` の named return + defer-recover、`http.Handler` などの固定 signature、`context.Context` を第一引数で取る規約等は本 skill の規則より優先 +- **意図的設計の例外**: コード / docs で意図が明示されている設計上の決定 (例: shutdown context を signal-aware ctx から detach するための `context.Background()` の library 内使用) +- **public API 互換性**: 後方互換のため変えられない exported symbol (改名提案より移行戦略の提案を優先) + +疑わしい場合は除外せず、output で「false positive 候補」として flag し user 判断を仰ぐ。 + +## このスキルが出力すべきもの + +code-refactor-advisor agent から呼ばれた場合: +- 対象 codebase に対する **層別 責務マップ** (file × layer × 責務) +- **層境界 / 依存方向違反** list (file:line + 違反内容) +- **Port / Adapter / ACL 欠如** の指摘 +- **Ubiquitous Language drift** の指摘 +- 修正方針 (層移動 / port 抽出 / DTO 変換層追加 / etc.) + +## 参照 + +- Eric Evans, "Domain-Driven Design" +- Vaughn Vernon, "Implementing Domain-Driven Design" +- Alistair Cockburn, "Hexagonal architecture": https://alistair.cockburn.us/hexagonal-architecture/ +- Mark Seemann, "Dependency Injection in .NET" (依存方向の議論) +- project の ADR (DDD + Hexagonal 採用判断) があれば参照 diff --git a/claude/ja/skills/go-bootstrap/SKILL.md b/claude/ja/skills/go-bootstrap/SKILL.md new file mode 100644 index 0000000..8ab1acc --- /dev/null +++ b/claude/ja/skills/go-bootstrap/SKILL.md @@ -0,0 +1,232 @@ +--- +name: go-bootstrap +description: 新規 / 既存 Go プロジェクトに動く骨格を一括セットアップする (module 初期化 / ディレクトリ骨格 / .golangci.yaml / Makefile / .gitignore / golangci-lint 導入)。「Go プロジェクトのセットアップ」「Go module を切って」「lint と Makefile 用意して」等の依頼で使う。 +--- + +# go-bootstrap + +新規 Go プロジェクトをゼロから「`make build` / `make test` / `make lint` が pass する状態」までセットアップする skill。1 リポジトリにつき 1 回しか使わない想定。 + +## 適用条件 + +- Go (1.22+ 想定、Toolchain Directive 利用) がローカルに導入されている +- リポジトリ ルートに `go.mod` がまだ無い (or 再セットアップで OK) +- DDD + Hexagonal を採用する (`internal/{domain,application,adapters}/`)。違うレイアウトのときはユーザーに確認 + +## 手順 + +### Step 0: 前提確認 + +```bash +go version # 1.22 以上 +ls -la # 既存ファイル把握 +test -f go.mod && cat go.mod # 既存 module 確認 +``` + +確認事項 (AskUserQuestion で 1 ターンに集約): +- module path (例: `github.com/<org>/<repo>`) +- Go バージョン (デフォルト: ローカルの最新安定版) +- バイナリ構成 (例: `cmd/<bin1>` 単独 / `cmd/<bin1>` + `cmd/<bin2>` / その他。`<bin1>` 等は project に合わせて命名) +- スコープ外: buf / gRPC / IaC は別 skill (本 skill では骨格のみ) + +### Step 1: `go mod init` + +```bash +go mod init <module-path> +``` + +`go.mod` を Read して module path と go directive を確認。 + +### Step 2: cmd/ 骨格 + +各バイナリに最小スタブを Write: + +```go +// Package main is the entry point for the <bin-name> binary. +package main + +import "log/slog" + +func main() { + slog.Info("<bin-name> starting") +} +``` + +コメントは英語、`log/slog` を採用。 + +### Step 3: internal / apis / deploy 骨格 + +`.gitkeep` で空ディレクトリを管理: + +- `internal/domain/.gitkeep` +- `internal/application/.gitkeep` +- `internal/adapters/.gitkeep` +- `apis/proto/server/v1/.gitkeep` (proto 利用予定なら) +- `deploy/.gitkeep` (IaC 配置予定なら) + +### Step 4: `.golangci.yaml` (v2 形式) + +```yaml +version: "2" + +run: + timeout: 5m + +linters: + default: standard + enable: + - misspell + - revive + - gocritic + exclusions: + paths: + - ^gen/ + - ^cli/go/gen/ + rules: + - path: _test\.go + linters: + - errcheck + - gocritic + +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - <module-path> +``` + +`paths` は **regex** として解釈されるため `^gen/` のように先頭一致させる。 + +### Step 5: Makefile + +```makefile +.DEFAULT_GOAL := help + +.PHONY: help build test lint generate tidy + +help: ## このヘルプを表示 + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' + +build: ## 全バイナリをビルド (go build ./...) + go build ./... + +test: ## race + coverage 付きでテスト実行 + go test -race -coverprofile=coverage.out ./... + +lint: ## golangci-lint を実行 + @if ! command -v golangci-lint >/dev/null 2>&1; then \ + echo "golangci-lint が未インストールです。インストール: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest"; \ + exit 1; \ + fi + golangci-lint run ./... + +generate: ## コード生成 (go generate) + go generate ./... + +tidy: ## go.mod / go.sum を整理 + go mod tidy +``` + +### Step 6: `.gitignore` 整備 (既存に追記) + +既存 `.gitignore` がなければ作成、あれば末尾に追加: + +``` +# ===== Go ===== +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ +dist/ + +# Test & coverage +*.test +*.out +*.prof +coverage.* +coverage/ + +# Go workspace +go.work +go.work.sum + +# Dependency directory (legacy) +vendor/ + +# ===== Generated code ===== +/gen/ +``` + +`!.env.example` のような既存除外が secret 管理にあれば尊重。 + +### Step 7: CLAUDE.md / README.md 同期 + +`CLAUDE.md` があれば「## 開発コマンド」セクションを追加 (重複しないか先に確認): + +```markdown +## 開発コマンド + +主要コマンドは `Makefile` 経由で実行する。 + +| コマンド | 用途 | +|---|---| +| `make help` | ターゲット一覧を表示 | +| `make build` | `go build ./...` | +| `make test` | race + coverage 付きで全テスト実行 | +| `make lint` | `golangci-lint run` (v2 系) | +| `make generate` | `go generate ./...` | +| `make tidy` | `go mod tidy` | + +`golangci-lint` は v2 系を使用。`go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest` で `$(go env GOPATH)/bin` に入る。 +``` + +### Step 8: golangci-lint インストール (未導入時のみ) + +```bash +which golangci-lint || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest +golangci-lint --version +``` + +### Step 9: 動作確認 + +```bash +make build && echo BUILD_OK +make test && echo TEST_OK +make lint && echo LINT_OK +``` + +revive の `package-comments` で警告が出る場合、cmd の `main.go` に英語のパッケージコメントを追加する (Step 2 のテンプレに従っていれば既に付いているはず)。 + +### Step 10: 完了報告 + +DoD と各ステップの対応表で報告: + +| 項目 | 状況 | +|---|---| +| `go build ./...` | ✓ | +| `make test` / `make lint` | ✓ | +| module path | <確認値> | +| ディレクトリ骨格 (cmd/internal/apis/proto/deploy) | ✓ | +| `.golangci.yaml` (v2) | ✓ | +| Makefile (build/test/lint/generate/tidy) | ✓ | + +## 鉄則 + +1. **既存ファイル尊重**: 既存 `.gitignore` / `CLAUDE.md` / `README.md` は上書きせず追記 +2. **コメントは英語**: Go ファイルのコメントは英語 +3. **scope を守る**: buf / gRPC / VectorStorePort 等の機能実装は対象外。次の skill / agent に渡す +4. **コミットしない**: コミットは別 skill (`/commit-push-branch`) に委ねる + +## アンチパターン + +- `go mod init` を確認なしで上書き +- `.gitignore` を既存内容無視で全置換 +- `cmd/*/main.go` のコメントを日本語で書く +- `.golangci.yaml` を v1 形式で書く (現在は v2 が主流) +- 動作確認 (`make build/test/lint`) を skip diff --git a/claude/ja/skills/go-style/SKILL.md b/claude/ja/skills/go-style/SKILL.md new file mode 100644 index 0000000..a49f270 --- /dev/null +++ b/claude/ja/skills/go-style/SKILL.md @@ -0,0 +1,203 @@ +--- +name: go-style +description: Go の慣用句・命名・error handling・context・logging・concurrency・lint / format の reference skill。「Go お作法的にどう」「命名規約」「error wrap」等の問いや code review 時に参照する。手順 skill ではない。 +--- + +# go-style + +Go の慣用句・お作法を集めた reference skill。実装 / review / refactor 候補出しの判断基準として参照する。 + +## 適用条件 + +- Go コード (`*.go` / `Makefile` / `*.proto` の Go 生成系) を扱う任意の場面 +- code-refactor-advisor agent からの参照 +- ユーザーが「Go 的にどう書く」「命名どうする」を尋ねる場面 + +## 1. Package layout + +- `cmd/<bin>/main.go` でバイナリエントリ。1 バイナリ = 1 サブディレクトリ +- `internal/` 配下は他リポジトリから import 不可。基本的にここに実装を置く +- `pkg/` は外部 import を意図する場合のみ。reuse の見込みが無いなら `internal/` に置く +- DDD + Hexagonal 採用時: `internal/{domain,application,interfaces,adapters}/` (層境界の詳細は `ddd-hexagonal` skill) +- `apis/` で proto / OpenAPI / SDK 公開 surface を分離 (推奨規約) +- 1 package = 1 責務。`util` / `common` / `helper` のような catch-all package は anti-pattern + +検出シグナル: +- `internal/util/` 配下に複数責務の関数が同居 +- `cmd/` の中に business logic +- `pkg/` を使っているのに外部 import 実例なし + +## 2. 命名 + +- exported (大文字始まり) ⇔ unexported (小文字始まり) の判断は **公開する必要があるか** だけで決める。「将来公開するかも」で大文字にしない +- acronym は **全大文字保持**: + - 3 文字以上: `URL` `ID` `HTTP` `API` (`Url` / `Id` / `Http` は NG) + - 2 文字: `AI` / `IO` / `UI` / `OS` / `DB` も全大文字 (`Ai` / `Io` / `Ui` は NG) + - 複数 acronym 含む商標 / 一般化された綴り: `OpenAI` / `OpenAPI` / `gRPC` の standard 綴りに従う + - 商標が unexported 識別子の先頭に来る場合: 商標部分も lowercase 化して acronym 規則と整合させる (`openaiKey` / `openapiSpec`)。`openAIKey` のような acronym 内部 case mix は NG +- receiver 名は **同じ型なら全 method で同じ短い名前** (`s *Server` / `c *Client`)。`this` / `self` は禁止 +- interface 名は単一 method なら `-er` suffix (`Reader` / `Writer`)。多 method は名詞 (`FileSystem`) +- error 型・変数は `Err` / `err` 接頭辞 (`var ErrNotFound = ...` / `func ... (err error)`) +- enum 値は型名接頭辞 (`type Model string` + `ModelTextEmbedding3Large Model = "..."`) +- test 名は `TestX_When_..._Should_...` または `TestX_RejectsEmpty` 等の意図ベース (詳細は `go-test` skill) + +検出シグナル: +- `Url` / `Id` / `Http` / `Ai` / `Io` / `Ui` のような mixed case acronym +- `openAIKey` のような商標 acronym と他文字の case mix (商標を unexported 先頭で使う場合は `openaiKey` 等に lowercase 化) +- receiver が `this *X` / `self *X` +- 1 型に対して `s` `srv` `server` のように receiver 名が file ごとにバラバラ +- 「将来公開するかも」だけで exported にしている symbol + +## 3. Error handling + +- error は **値**。最後の戻り値、`if err != nil { return ..., err }` を省略しない +- wrap には `fmt.Errorf("context: %w", err)` を使う。`errors.Is` / `errors.As` で sentinel / typed match できる状態を保つ +- sentinel error は package level の `var Err... = errors.New("pkg: human message")`。message に `package:` prefix を付けると wrap chain が読みやすい +- 文字列比較で error 判別しない (`err.Error() == "..."` 禁止) +- panic は **真に異常な状態のみ** (例: program 起動時の不変量違反)。通常 flow では使わない +- error message は **小文字始まり、句点なし** (`io: short read` / `not "Io: short read."`) +- wrap した chain は **API 境界で「sanitize された外向き message」と「内部 wrap chain」を分ける**。client に upstream 詳細を leak しない (内部詳細は logger で出す) + +検出シグナル: +- `errors.New(fmt.Sprintf(...))` (= `fmt.Errorf` を使うべき) +- error の文字列比較 +- handler / gRPC server の error response に upstream SDK error が漏れている +- panic / `log.Fatal` が library code 内に存在 (cmd/ 以外) + +## 4. Context + +- `context.Context` は **関数の第 1 引数**。`ctx context.Context` で名前統一 +- ctx を struct field に保持しない (request scoped と struct lifetime が混ざる) +- ctx の伝搬は明示的 (関数呼び出しで pass、暗黙の global 不可) +- `context.Background()` は entry point (main / test / signal handler) のみ。library 内では caller の ctx を使う +- `ctx.Value` は cross-cutting metadata (request_id / trace span / auth principal) のみ。引数で渡せる値は引数で渡す +- ctx key は **package private struct type** で衝突回避 (`type requestIDKey struct{}`) +- cancellation / deadline はライブラリ側で `<-ctx.Done()` を観察し、長い IO は `select` でブロック解除 + +検出シグナル: +- `context.Background()` を library 内で呼んでいる +- `ctx.Value("string-key")` (= 衝突 risk のある string key) +- struct field に `ctx context.Context` +- ctx を引数の最後 / 中央に置いている + +## 5. Logging + +- 標準 `log/slog` を採用。構造化 JSON 推奨 (`slog.NewJSONHandler`) +- level 使い分け: `Error` (action 必要) / `Warn` (異常だが処理継続) / `Info` (通常運用) / `Debug` (開発時のみ) +- key-value pair で構造化: `slog.String("key", "value")` / `slog.Int(...)` / `slog.Duration(...)` +- ctx を `LogAttrs(ctx, ...)` で渡し、interceptor / middleware に span / request_id を付与可能にする +- **PII / 認証情報を log しない** (API key / password / token / personal identifier) +- log message は固定文字列、可変値は attr で渡す (cardinality 制御 + grep しやすさ) +- error log では wrap 元の root cause も attr で出す (`slog.String("error", err.Error())`) + +検出シグナル: +- `fmt.Println` / `log.Print*` (標準 log や fmt) で運用 log を出している +- `slog.Info(fmt.Sprintf("user %s logged in", userID))` 等の interpolated message +- log message に API key / password / 個人識別子が混入 + +## 6. Concurrency + +- goroutine は **必ず終了経路を確保** (cancellation / done channel / WaitGroup / errgroup) +- goroutine leak の代表的原因: blocked send/recv on unbuffered channel, ctx 観察忘れ, errgroup `g.Wait()` 忘れ +- mutex は **保護対象を明示** (struct field 直前にコメント、または `mu sync.Mutex` の直後に保護対象を並べる) +- channel は **送信者が close する**。受信者 close は禁止 (panic / race risk) +- channel direction を関数 signature で限定 (`<-chan T` / `chan<- T`) +- `errgroup.Group` は context 連動 cancellation + 最初の error 取得 + `g.Wait()` で sync。複数 IO の並行実行に推奨 +- `sync.Once` は init 用途。複数回必要な状態は別 pattern (mutex / atomic) +- shared mutable state は最小化、可能なら immutable copy + channel pass + +検出シグナル: +- goroutine の中で `ctx.Done()` も channel 受信もしていない (= leak 候補) +- mutex の保護対象が不明 (`mu sync.Mutex` しかない) +- channel direction が両方向 (`chan T`) で signature に書かれている +- goroutine 内 panic を defer recover していない (= 上位 goroutine ごと crash) + +## 7. Lint / format + +- `gofmt` / `goimports` 強制 (`goimports` の local prefix は各リポジトリの module path に合わせる) +- `golangci-lint` v2 系 (`.golangci.yaml` で config)。CI で必須 +- 推奨 linter: `errcheck` / `govet` / `staticcheck` / `ineffassign` / `unused` / `gosimple` / `gofmt` / `goimports` +- 警告 (`// nolint:`) は **理由コメント必須** (`// nolint:errcheck // intentional fire-and-forget`) +- 自動 fixable は CI 前に local で fix (`gofmt -w` / `goimports -w` / `golangci-lint run --fix`) + +検出シグナル: +- `// nolint:` が理由なし +- import 順序が stdlib / 3rd-party / local 混在 +- `errcheck` 警告無視 (戻り値 error を `_` に捨てる、必要なら `// intentional`) + +## 8. godoc / コメント + +- exported symbol には godoc 必須 (`// FuncName ...` で symbol 名から始まる文) +- package コメントは `// Package <name> ...` で各 package 1 ファイルに置く (大抵 doc.go or 主要 file) +- コメントは **WHY を書く**。WHAT は code が語る (well-named identifier がある前提) +- 「将来 X したい」は `TODO:` で書く。Phase / ticket ID をコメントに残さない +- `Deprecated:` は対象 symbol の godoc に明示 (`// Deprecated: use NewX instead.`) +- multi-line block comment より行 comment (`//`) を好む + +検出シグナル: +- exported func / type / var で godoc なし +- コメントが WHAT (code を翻訳しているだけ) しか書いていない +- `Phase 0` / リリースサイクル名付き ticket のような時系列 / ticket scope 言及 + +## 9. Import order + +```go +import ( + // Group 1: standard library + "context" + "fmt" + + // Group 2: third-party + "github.com/spf13/cobra" + + // Group 3: local (current module) + "github.com/<org>/<module>/internal/foo" +) +``` + +- 3 group を空行で区切る (goimports の `-local` 設定で自動) +- 各 group 内 alphabetical sort +- alias は **必要最小限** (衝突回避 / 短縮目的のみ)。`pgsql "github.com/lib/pq"` は OK、`f "fmt"` は NG + +検出シグナル: +- group 区切りなし (1 つの import block に全部混ざる) +- 不要 alias (`io2 "io"` 等) + +## 10. Type safety / nil 安全 + +- `interface{}` / `any` は本当に型不定なときのみ。型が決まっているなら明示 type +- nil pointer dereference 防御は **constructor で nil reject** (`func NewX(...) (*X, error) { if ... == nil { return nil, ErrNil }`) +- nil map への代入 panic、nil slice の append は OK (混同しない) +- pointer receiver と value receiver は **混在禁止** (1 type に対して統一、mutating method があるなら全 method pointer receiver) + +検出シグナル: +- `any` / `interface{}` が業務 logic で使われている (passthrough なら可) +- pointer receiver と value receiver が同じ type で混在 +- nil check 漏れた map 代入 + +--- + +## False positive 判定基準 + +以下に該当する場合は本 skill の検出シグナルがヒットしても **違反扱いしない**: + +- **生成コード由来**: protoc / buf / openapi-generator / `go generate` などで生成された symbol / file (典型: ファイル冒頭に `Code generated by ... DO NOT EDIT.` 行を含む)。生成元の schema 側で改善されるべきで、生成後の Go コードを手で直さない +- **言語 / library の慣用 pattern**: `func(...) (resp any, err error)` の named return + defer-recover、`http.Handler` などの固定 signature、`context.Context` を第一引数で取る規約等は本 skill の規則より優先 +- **意図的設計の例外**: コード / docs で意図が明示されている設計上の決定 (例: shutdown context を signal-aware ctx から detach するための `context.Background()` の library 内使用) +- **public API 互換性**: 後方互換のため変えられない exported symbol (改名提案より移行戦略の提案を優先) + +疑わしい場合は除外せず、output で「false positive 候補」として flag し user 判断を仰ぐ。 + +## このスキルが出力すべきもの + +code-refactor-advisor agent から呼ばれた場合、以下を提供する想定: +- 対象コードに対する **section 別の違反 / 改善余地 list** +- 各指摘に対する **検出シグナル** (どの pattern で気付いたか) と **根拠 section** (この skill 内の section 番号) +- 修正方針 (お作法に揃える / 例外として残す / 別途議論) + +## 参照 + +- Go Code Review Comments: https://go.dev/wiki/CodeReviewComments +- Effective Go: https://go.dev/doc/effective_go +- Go Proverbs: https://go-proverbs.github.io/ +- Uber Go Style Guide: https://github.com/uber-go/guide/blob/master/style.md diff --git a/claude/ja/skills/go-test/SKILL.md b/claude/ja/skills/go-test/SKILL.md new file mode 100644 index 0000000..fa13fab --- /dev/null +++ b/claude/ja/skills/go-test/SKILL.md @@ -0,0 +1,316 @@ +--- +name: go-test +description: Go の test design / test code 慣用句の reference skill。命名 / table-driven / t.Parallel / race detector / fake・mock / boundary value / coverage 解釈を扱う。「test 名どうする」「table-driven にする」「coverage 何 % まで」等の問いで参照する。手順 skill ではない。 +--- + +# go-test + +Go の test design / test code 慣用句を集めた reference skill。実装 / review / refactor 候補出しの判断基準として参照する。 + +## 適用条件 + +- Go の test code (`*_test.go`) を扱う任意の場面 +- code-refactor-advisor agent からの参照 +- ユーザーが「test 何書く / どう書く」を尋ねる場面 + +## 1. 命名 + +- 関数名は **意図ベース** (`TestX_RejectsEmpty` / `TestX_When_..._Should_...`)。`TestX1` `TestX2` のような index ベースは禁止 +- table-driven の case 名は **意図を含む** (`{name: "EmptyProductID", ...}` / 単に `{name: "case1"}` は NG) +- sub-test 名にスペース / 特殊文字を入れない (`t.Run("foo bar")` は `foo_bar` に正規化される、grep しにくい) +- Helper 命名: `newFixture(t)` / `newAdapterWithFake(t, fake)` のように **意図を表す** prefix (`new` / `make`) + +検出シグナル: +- test 内に説明 inline コメントがある (= test 名 / 変数名で意図を表現できていない) +- case 名が `case1` `case2` のように index ベース +- `t.Run("with spaces")` のような名前 + +## 2. Table-driven test + +```go +tests := []struct { + name string + input X + want Y + wantErr error +}{ + {name: "HappyPath", input: ..., want: ...}, + {name: "RejectsEmpty", input: X{}, wantErr: ErrEmpty}, +} + +for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := SUT(tc.input) + if !errors.Is(err, tc.wantErr) { + t.Fatalf("err = %v, want %v", err, tc.wantErr) + } + if tc.wantErr != nil { return } + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("got = %v, want %v", got, tc.want) + } + }) +} +``` + +- struct field は **必要最小限**。全 case で同じ値なら struct 外に定数で +- `wantErr` は **sentinel error** (errors.Is 検証可能) を入れる。文字列比較は禁止 +- nil error 期待は `wantErr: nil` を明示 (省略でも動くが意図が伝わらない) + +検出シグナル: +- table-driven で 1 case しかない (= 通常の test func で十分) +- struct field の半数以上が空値 / 同じ値 (= struct 過剰) +- `wantErr string` で文字列比較 + +## 3. `t.Parallel()` + +- **デフォルトで有効化推奨**。test 全体の wall time 短縮 + race detector 効果増 +- table-driven 内の `t.Run(tc.name, func(t *testing.T) { t.Parallel(); ... })` で **closure 内 tc capture に注意**: + ```go + for _, tc := range tests { + tc := tc // Go 1.22+ は不要だが <= 1.21 なら必須 + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ... + }) + } + ``` + Go 1.22+ から for loop 変数 scope が iteration 単位になり capture 罠は解消されたが、CI が古い Go で動く可能性があるなら明示的に local 変数化が安全 +- parallel 不可な test (global state を触る / 順序依存) は `t.Parallel()` を呼ばない +- `t.Parallel()` 呼んだ test 同士は並行実行、呼ばない test は parallel test 完了後に逐次実行 + +検出シグナル: +- table-driven test で全 case が `t.Parallel()` を呼んでいない (= wall time 損失) +- closure capture で `tc` を bind せずに外側の loop 変数を参照 (古い Go で bug) +- global state 触っているのに `t.Parallel()` 呼んでいる (race / flaky) + +## 4. `t.Helper()` + +- helper 関数 (assertion / fixture 構築) の **冒頭で `t.Helper()` 呼ぶ**。失敗時の line 報告が呼び出し側になり debug 容易 +- 純粋な data 生成 helper には不要 (実際に `t.Errorf` / `t.Fatal` を呼ばないなら) + +検出シグナル: +- `t.Errorf` / `t.Fatal` を呼ぶ helper で `t.Helper()` 呼んでいない + +## 5. Setup / Teardown + +- リソース cleanup は **`t.Cleanup()` で inline 登録** (Go 1.14+ の慣用) +- 共通 fixture は helper constructor (`newFixture(t)`) に集約、内部で `t.Cleanup` 登録 +- struct field で table case 個別の前処理を持たせるときの命名は **`beforeFunc` / `afterFunc`** (`setup` / `teardown` の語は使わない) +- struct field 方式自体は default 非推奨 (boilerplate / closure capture 罠 / `t.Parallel()` と相性悪い) +- 例外: case ごとに genuinely 異なる前処理が必要なときのみ `beforeFunc` を struct field に追加。`afterFunc` は通常省略 (`beforeFunc` 内で `t.Cleanup` で済む)、必要なときだけ入れる + +例: +```go +tests := []struct { + name string + beforeFunc func(t *testing.T) *Fixture + want Result +}{ + { + name: "EmptyDB", + beforeFunc: func(t *testing.T) *Fixture { + return newFixture(t) + }, + want: Result{...}, + }, +} +``` + +検出シグナル: +- `defer cleanup()` だけで cleanup している (test 全体 fail 時に走らない、`t.Cleanup` の方が安全) +- struct field 名が `setup` / `teardown` (推奨は `beforeFunc` / `afterFunc`、project 規約があればそちら優先) +- 全 case で同じ setup function を持たせている (= 共通 helper に切り出すべき) + +## 6. Race detector + +- **常時 ON**。`go test -race` で実行 (例: `make test` に `-race` を含めて常時 ON) +- 並行コードのデータ race は static 解析で検出不可、CI で常時走らせる価値が高い +- コスト: ~10x 遅い + memory ~5-10x 増。unit test scale なら問題視しない +- benchmark (`go test -bench`) は race ON だと意味のある測定不可 → 別実行 +- 例外的に race を外したい場合は build tag (`//go:build !race`) + +検出シグナル: +- CI / Makefile で `go test` のみ (= race なし) を default にしている +- `go test -bench` を `-race` 付きで走らせている (測定が無意味) + +## 7. Test design technique + +### Black-box vs White-box + +- **Black-box**: input → output の契約検証、内部実装に依存しない。`package x_test` (external test package) で書くと implementation symbols に触れず public API のみで test できる +- **White-box**: 分岐 / 内部状態を意図的に通す。`package x` (same package) で unexported symbol に触れる +- **使い分け**: API 契約の test は black-box、実装の coverage 補完は white-box。両方共存可 + +### Boundary value analysis + +- off-by-one / 上限下限 / 0 / 空 / nil / max / min を意図的に test +- 例: `top_k` field なら 0 / 1 / 上限値 / 上限+1 / 負数 / 大きな数 +- **文字列 boundary は rune count vs byte count を明示**: + - Go の `len(string)` は **byte count**、文字数 (rune count) は `utf8.RuneCountInString` / `[]rune(s)` + - 上限を持つ string API は spec 側でどちらの単位かを決め、test もそれに合わせる + - マルチバイト文字 (日本語 / 絵文字) は 1 rune ≒ 3-4 byte。byte 上限の場合は **マルチバイトのみの境界 test が必須** (ASCII での境界 test だけでは検証漏れ) + - 結合文字 / 異体字 selector を含む文字列は rune count ≠ grapheme cluster count。grapheme 単位で扱う API は別途 `golang.org/x/text/unicode/norm` 等を使う + +### Equivalence partitioning + +- 同値クラスに 1 件代表 test。全網羅を狙わず代表値で trade-off +- 例: `query` 文字列なら「ASCII」「マルチバイト」「絵文字 / 結合文字」「空文字 / 空白のみ」「上限文字数 / +1」 + +### Negative test + +- 正常系 (HappyPath) だけでなく **rejection path を網羅**: + - validation 失敗 (各 field の境界違反) + - 上流 dependency error (provider unavailable / timeout) + - context cancellation + - permission / ACL 違反 + +### Property-based test + +- `testing/quick` (標準) / `pgregory.net/rapid` (3rd-party) で性質ベース test +- 適用は限定的: pure function / decoder-encoder の roundtrip / ordering 保持などに有効 +- 業務 logic には value-based test の方が読みやすい + +検出シグナル: +- HappyPath のみで rejection / boundary が test されていない +- `top_k` のような数値 field で 0 / 上限 / 上限+1 が test 漏れ +- マルチバイト文字列入力 test がない (日本語 query で fail する潜在 bug) + +## 8. Fake / Mock / Stub の使い分け + +- **Fake**: 動く軽量実装 (in-memory db / in-memory adapter)。複雑 logic でも動作再現できる +- **Stub**: 固定 response を返すだけ。input は無視 +- **Mock**: input 検証 + 期待呼び出し回数を assert (xUnit style) + +推奨方針 (project 規約があればそちら優先): +- **手書き fake 推奨** (testify / gomock 等の generation tool は使わない) +- 理由: dependency 最小 / fake の挙動が src コード上で完結 / 過剰 mock 回避 +- interface に narrow なものを切り (port pattern)、test 用に簡潔な struct で実装 + +例 (推奨 pattern): +```go +type fakeVectorStore struct { + searchCalls []vectorSearchCall + respBy map[string][]ports.VectorSearchResult + err error +} + +func (f *fakeVectorStore) Search(_ context.Context, c string, req ports.VectorSearchRequest) ([]ports.VectorSearchResult, error) { + f.searchCalls = append(f.searchCalls, vectorSearchCall{c, req}) + if f.err != nil { return nil, f.err } + if r, ok := f.respBy[c]; ok { return r, nil } + return nil, nil +} +``` + +検出シグナル: +- testify / gomock / mockery 等の mock framework が新規導入されている +- mock の expectation が「呼ばれた回数」だけ検証して input / output が unchecked + +## 9. Test fixture / golden file / testdata + +- 大きい input / 期待 output は `testdata/` directory に配置 (Go 慣用、build から除外される) +- golden file pattern: 期待 output を `testdata/golden/<name>.json` 等に保存、test 内で diff + - update flag (`-update` 等の独自 flag) で実装変更時に一括更新 +- fixture 共有は `testdata/fixtures/` 配下、test helper で読み込み + +検出シグナル: +- 巨大な期待値 string が test code 中に inline (= testdata に切り出すべき) +- testdata に build tag が付いていない、もしくは Go の build から除外されていない (= `testdata` ディレクトリ名は Go が自動除外) + +## 10. Test 区分 / build tag + +- **Unit test**: 同じ package 内、外部 IO なし、ms オーダー。`*_test.go` で常時実行 +- **Integration test**: 外部 dependency (DB / API) と接続、CI で別 stage。build tag で分離: + ```go + //go:build integration + + package x_test + ``` + 実行: `go test -tags=integration ./...` +- **E2E test**: 全体起動 + black-box。`tests/e2e/` 等の外側 directory + build tag + +検出シグナル: +- Pinecone / OpenAI に直接 net 接続する test が unit test (`go test ./...`) で走る (= flaky / 外部依存) +- 別 build tag が付いていないが external IO する test + +## 11. HTTP / gRPC test pattern + +### HTTP + +- `httptest.NewServer` で in-process server 起動、SDK の base URL を upcast +- request 検証は handler 内で `r.URL` / `r.Body` を assert +- response は `w.WriteHeader` + `w.Write([]byte(...))` で canned + +### gRPC + +- `bufconn.Listen` で in-memory listener、`grpc.NewClient(... grpc.WithContextDialer(bufDialer)) `で client wire +- service 全体起動が重ければ handler を直接 method 呼びでも可 (interceptor の test も含めたいなら bufconn) + +検出シグナル: +- HTTP test で actual TCP port を bind している (= flaky / 並行 test 衝突) +- gRPC test で実 net listener を起動 + +## 12. Time / Context determinism + +- `time.Now()` を直接呼ばず、`now func() time.Time` を struct field / param に注入 +- test では `now: func() time.Time { return time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) }` で固定 +- ctx は test 内で `context.Background()` (entry point) または `context.WithTimeout` で deadline 制御 +- random seed も注入 / `math/rand/v2` の Local 化で deterministic + +検出シグナル: +- production code が `time.Now()` を直接呼ぶ (= test で固定不可) +- random / UUID 生成が global state 由来 (= 決定的にできない) + +## 13. Coverage 解釈 + +- line coverage は guide rail、**信仰しない** +- branch coverage / boundary coverage / 重要 path 重視 +- 100% 目指すと artificial test (`if false { ... }` を消すための pointless test) が生まれる +- DoD ベース: 機能契約 (acceptance criteria) を満たす test を書く、内部 100% を目指さない +- 目安: business logic / handler / adapter は 80%+、generated code / main は coverage 対象外 + +検出シグナル: +- coverage を上げるためだけの assertion なし test +- handler / service の HappyPath だけで coverage が稼がれているのに rejection path が test 抜け + +## 14. Flakiness 回避 + +主な flaky 原因: +- **time-based**: `time.Sleep` 待ち、`time.Now()` の暗黙依存 → 注入で固定 +- **ordering**: map iteration 順序依存 → sorted slice に変換してから assert +- **net IO**: actual port bind → httptest / bufconn を使う +- **goroutine race**: `t.Cleanup` で全 goroutine 終了を待つ、channel close で sync +- **shared state**: `t.Parallel()` 下で global / package var を mutate → test 内 local state に切り替え + +検出シグナル: +- test 内に `time.Sleep` +- map iteration 結果を順序付き比較 (`reflect.DeepEqual([]Map, expectedSlice)` は順序保証なし) +- test 終了後に goroutine が残る + +--- + +## False positive 判定基準 + +以下に該当する場合は本 skill の検出シグナルがヒットしても **違反扱いしない**: + +- **生成コード由来**: protoc / buf / openapi-generator / `go generate` などで生成された symbol / file (典型: ファイル冒頭に `Code generated by ... DO NOT EDIT.` 行を含む)。生成元の schema 側で改善されるべきで、生成後の Go コードを手で直さない +- **言語 / library の慣用 pattern**: `func(...) (resp any, err error)` の named return + defer-recover、`http.Handler` などの固定 signature、`context.Context` を第一引数で取る規約等は本 skill の規則より優先 +- **意図的設計の例外**: コード / docs で意図が明示されている設計上の決定 (例: shutdown context を signal-aware ctx から detach するための `context.Background()` の library 内使用) +- **public API 互換性**: 後方互換のため変えられない exported symbol (改名提案より移行戦略の提案を優先) + +疑わしい場合は除外せず、output で「false positive 候補」として flag し user 判断を仰ぐ。 + +## このスキルが出力すべきもの + +code-refactor-advisor agent から呼ばれた場合: +- 対象 test code に対する **section 別の違反 / 改善余地 list** +- 各指摘の **検出シグナル** + **根拠 section 番号** +- 修正方針 (table-driven 化 / fake 切り替え / golden file 化 / boundary case 追加 / etc.) + +## 参照 + +- Go testing package doc: https://pkg.go.dev/testing +- Go Code Review Comments (Tests section): https://go.dev/wiki/CodeReviewComments +- Effective Go (Test section): https://go.dev/doc/effective_go +- Subtests and sub-benchmarks: https://go.dev/blog/subtests diff --git a/claude/ja/skills/grill-me/SKILL.md b/claude/ja/skills/grill-me/SKILL.md new file mode 100644 index 0000000..bd04394 --- /dev/null +++ b/claude/ja/skills/grill-me/SKILL.md @@ -0,0 +1,10 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/claude/ja/skills/k8s-style/SKILL.md b/claude/ja/skills/k8s-style/SKILL.md new file mode 100644 index 0000000..5027509 --- /dev/null +++ b/claude/ja/skills/k8s-style/SKILL.md @@ -0,0 +1,403 @@ +--- +name: k8s-style +description: K8s manifest / workload 設計 / RBAC / SecurityContext / NetworkPolicy / probe / resource / Kustomize / image 運用の reference skill。「K8s 的にどう」「manifest 規約」「RBAC 最小権限」「probe どう書く」等の問いで参照する。手順 skill ではない。 +--- + +# k8s-style + +Kubernetes 関連の慣用句・お作法を集めた reference skill。manifest 作成 / review / refactor 候補出しの判断基準として参照する。 + +## 適用条件 + +- K8s manifest (`*.yaml` / `*.yml` で `apiVersion` / `kind` を含む) を扱う任意の場面 +- Kustomize (`kustomization.yaml`) / Helm chart (`Chart.yaml` / `templates/` / `values.yaml`) を扱う場面 +- code-refactor-advisor agent / security-review-local skill からの参照 +- 「K8s 的にどう書く」「probe どうする」「最小権限」を尋ねられる場面 + +--- + +## 1. Manifest の基本姿勢 + +- 1 YAML = 1 リソース、または **論理的に 1 単位** (Deployment + Service + HPA + PDB) でまとめる +- `kubectl apply -f` を default にする (`kubectl create` は冪等でないので CI で使わない) +- 変更前は **必ず `kubectl diff`** で差分確認 (推奨規約) +- production への apply は CD 経由 (GitOps: ArgoCD / Flux) を default。`kubectl apply` 直接実行は禁忌 +- imperative command (`kubectl run` / `kubectl expose` / `kubectl edit`) で作ったリソースを残さない (Git に存在しない state は禁忌) + +検出シグナル: +- manifest に存在しないリソースが cluster 上にある (drift) +- `kubectl edit` の痕跡 (annotation の `last-applied-configuration` と Git の不一致) + +--- + +## 2. API version / 非推奨 API + +- 非推奨 / 削除済み API を使わない。`apiVersion` は **最新の stable** に揃える + - `Deployment` / `DaemonSet` / `StatefulSet` / `ReplicaSet` → `apps/v1` + - `Job` / `CronJob` → `batch/v1` + - `Ingress` → `networking.k8s.io/v1` (`extensions/v1beta1` / `v1beta1` は削除済み) + - `HorizontalPodAutoscaler` → `autoscaling/v2` + - `PodDisruptionBudget` → `policy/v1` + - `PodSecurityPolicy` は削除済み → **Pod Security Admission (PSA)** に移行 +- `pluto` で deprecated API を CI 検知 + +--- + +## 3. 命名 / Label / Annotation + +### 命名 (resource name) +- lowercase + `-` (RFC 1123)。`_` は不可 +- 長さは 63 文字以下 (Service / Pod の hostname になるため) +- 環境を name に埋めない (namespace で分離)。`my-app-prod` ではなく `my-app` + namespace `prod` + +### 推奨ラベル +**全リソースに付ける** (`app.kubernetes.io/*` prefix): + +| label | 例 | 必須度 | +|---|---|---| +| `app.kubernetes.io/name` | `my-app` | 必須 | +| `app.kubernetes.io/instance` | `my-app-prod` | 推奨 | +| `app.kubernetes.io/version` | `1.2.3` | 推奨 | +| `app.kubernetes.io/component` | `api` / `worker` | 推奨 | +| `app.kubernetes.io/part-of` | `my-platform` | 任意 | +| `app.kubernetes.io/managed-by` | `kustomize` / `argocd` | 推奨 | + +### selector の label +- Deployment の `spec.selector.matchLabels` は **immutable**。後から変えると update できなくなる +- Service の `selector` は `app.kubernetes.io/name` + `app.kubernetes.io/instance` で安定させる +- 環境変動する label (commit SHA / build number) は selector に入れない + +### annotation +- カスタム annotation は **逆 DNS** で namespace 化 (`example.com/my-key`) +- `kubectl.kubernetes.io/*` / `meta.helm.sh/*` 等の controller 管理 annotation は手で書かない + +検出シグナル: +- 名前に大文字 / underscore +- recommended labels が無い +- selector に rolling-deploy で変わる値が含まれる + +--- + +## 4. Resource requests / limits + +### 必須設定 +- **必ず `resources.requests` と `resources.limits` を設定** (推奨規約) +- 未設定だと best-effort QoS で最初に kill される + +### CPU vs Memory +- **CPU limit は注意して使う**: throttling が発生する。requests のみで運用するパターンも一般的 +- **Memory limit は必ず設定**: OOM kill のほうが node 全体への巻き込みより安全 +- requests ≤ limits。`requests = limits` で **Guaranteed QoS** (重要 workload) + +### サイズ感 +- 推測で決めない。`kubectl top` / Prometheus / VPA recommender で **計測値に基づく** +- 初期値は控えめに → 観測して調整 (over-provision はコストと scheduling 効率に響く) +- batch / job 系は短時間 spike を見込む + +検出シグナル: +- `resources:` が空 / `limits` のみ / `requests` のみ +- memory `limits` が requests の 10 倍以上 (枠の無駄) +- CPU `limits` が 100m 未満で latency 要件あり (確実に throttle) + +--- + +## 5. Probe (liveness / readiness / startup) + +3 種類を **使い分ける**。全部同じ endpoint を当てるのは anti-pattern: + +| probe | 役割 | 失敗時の挙動 | +|---|---|---| +| `startup` | 起動完了の判定。重い init (DB migration / cache warmup) があるなら必須 | 失敗で **kill して再起動** | +| `liveness` | 死活確認。**deadlock / hang のみ拾う** | 失敗で **kill して再起動** | +| `readiness` | trafic 受け入れ可否。依存先 (DB / cache) との接続状況 | 失敗で **Service から外す** (kill しない) | + +### ベストプラクティス +- **liveness は最小限**: アプリ内部状態 / 依存 DB を見ない。「process が応答するか」だけ + - liveness で DB を見ると、DB 障害で全 pod が再起動して悪化する +- **readiness は依存先を含めて良い**: DB 接続 / cache 接続 / dependent service の health を反映 +- **startup probe で liveness の `initialDelaySeconds` 競争を避ける**: 起動が遅いアプリは startup で猶予を取り、liveness の `failureThreshold` を短くする +- 各 probe 専用 endpoint を持つ (`/healthz` / `/readyz` / `/startupz`) + +検出シグナル: +- liveness が DB を叩いている +- 3 つの probe が同じ endpoint +- `initialDelaySeconds` が異常に大きい (300s 等) + +--- + +## 6. SecurityContext / Pod Security + +### Pod / Container レベルで必須 + +```yaml +spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: app + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] +``` + +### 禁忌 +- `privileged: true` +- `hostNetwork: true` / `hostPID: true` / `hostIPC: true` +- `hostPath` mount (特に `/` / `/etc` / `/var/run/docker.sock`) +- `runAsUser: 0` (root) +- `allowPrivilegeEscalation: true` +- `capabilities.add: ["SYS_ADMIN"]` 等の強権限 + +### Pod Security Admission (PSA) +namespace label で **restricted** / **baseline** / **privileged** を強制。production / 一般 workload は `restricted`: + +```yaml +metadata: + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest +``` + +検出シグナル: +- `securityContext` 不在 / `hostPath` 使用 / root user 実行 / `capabilities.drop` 未指定 + +--- + +## 7. RBAC (最小権限) + +### default deny から開始 +- ServiceAccount は **必ず明示** (default SA を使わない) +- pod が K8s API を叩く必要がないなら `automountServiceAccountToken: false` +- Role / ClusterRole は **必要な verb / resource のみ** + +### 禁忌 +- `verbs: ["*"]` / `resources: ["*"]` / `apiGroups: ["*"]` +- `cluster-admin` の bind (人にも、ましてや SA には付けない) +- 広範な ClusterRoleBinding (Role + RoleBinding で namespace 限定にできないか先に検討) + +### token 自動マウント +- 必要な場合のみ projected token (TokenRequest API) で短命化 + +検出シグナル: +- `serviceAccountName` 未指定 (default 使用) +- ClusterRole に wildcard verb / resource +- RoleBinding で済むのに ClusterRoleBinding + +--- + +## 8. NetworkPolicy + +### default deny を導入 + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all +spec: + podSelector: {} + policyTypes: ["Ingress", "Egress"] +``` + +各 namespace に **default deny ingress / egress** を 1 個入れて、必要な通信を allow rule で開ける。 + +### よくある落とし穴 +- **DNS (kube-dns) への egress を忘れる** → name resolution が落ちて全 pod 死亡。必ず allow: + ```yaml + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + ``` +- egress を全開 (`{}`) にしてるとデータ exfiltration を防げない +- cluster に network policy controller (Calico / Cilium 等) が居ないと NetworkPolicy は **無視される**。導入前提を確認 + +検出シグナル: +- NetworkPolicy が 1 つも無い namespace / 全開 egress / DNS allow 欠落 + +--- + +## 9. Pod / Workload 設計 + +### Deployment vs StatefulSet vs DaemonSet vs Job +- **stateless app** → Deployment +- **stable identity / persistent volume が必要** → StatefulSet (DB / Kafka / etcd 等) +- **全 node に 1 pod** → DaemonSet (log collector / node exporter) +- **1 回完結 / 定期実行** → Job / CronJob + +### Pod 設計 +- 1 Pod = 1 main process。複数 process を 1 container に詰めない +- sidecar (proxy / log shipper) は **役割が明確な時のみ**。なんとなく追加しない +- init container は **起動前にだけ必要な処理** に限定 (migration / config 取得) + +### Replica / 可用性 +- production の Deployment は `replicas >= 2` + **PodDisruptionBudget**: + +```yaml +apiVersion: policy/v1 +kind: PodDisruptionBudget +spec: + minAvailable: 1 # or maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: my-app +``` + +- 複数 zone がある cluster なら **topologySpreadConstraints** で zone 分散 +- rolling 中に全 pod が同時に落ちないよう `maxUnavailable` を控えめに + +### Graceful shutdown +- `terminationGracePeriodSeconds` を SIGTERM 後の cleanup 時間に合わせる (default 30s) +- アプリ側で SIGTERM 受信時に readiness を false にしてから drain (preStop hook で sleep するパターン) + +検出シグナル: +- production で `replicas: 1` / PDB なし / preStop hook なしで in-flight request が drop + +--- + +## 10. ConfigMap / Secret + +### ConfigMap +- 設定値は ConfigMap、機密は Secret (混ぜない) +- `envFrom` で一括注入すると env が肥大化 / 名前衝突する。**必要な key だけ `valueFrom`** +- immutable ConfigMap (`immutable: true`) でホットリロード抑止 (controller 負荷低減) + +### Secret +- base64 は **暗号化ではない**。Secret は etcd で平文同等 (encryption-at-rest 設定なければ) +- 本番では **External Secrets Operator / Sealed Secrets / Vault / cloud secret manager** を使う +- Secret を環境変数で注入すると `/proc/<pid>/environ` から見える → 機微なものは **volume mount + readOnly + tmpfs** を検討 +- Secret を Git に commit しない (base64 encode 済みも同じ) + +検出シグナル: +- ConfigMap に password / token らしき key +- Secret が **平の YAML** で commit +- 全環境で同じ Secret 値 + +--- + +## 11. Image / Tag / Pull policy + +### Tag +- `:latest` を **使わない** (再現性が無い / rolling で何が deploy されたか追えない) +- semver tag (`v1.2.3`) + digest (`@sha256:...`) の併用が最安全 +- production manifest は **digest pin** を default にすると tag を上書きされても安全 + +### Pull policy +- tag が `:latest` だと implicit に `Always` → tag pin した時は **明示的に `IfNotPresent`** を書く +- private レジストリは `imagePullSecrets` を明示 + +### イメージサイズ / 脆弱性 +- distroless / chainguard / minimal base を default +- multi-stage build で build deps を残さない +- `trivy` / `grype` で CVE スキャン、SBOM を CI に組み込む + +検出シグナル: +- `:latest` tag +- pull policy 未指定 + 固定 tag +- root user で動く image + +--- + +## 12. Kustomize (default) / Helm + +**default は Kustomize**。理由: +- YAML のまま読める (template engine の癖が無い) +- 自社アプリの環境差分用途では overlay で十分 +- 部分上書きが patch で局所的にでき、values で全穴を開ける必要がない + +Helm は **OSS chart を import する時** / **配布が必要な時** に限定して使う。 + +### Kustomize 規約 +- `base/` + `overlays/{dev,staging,prod}/` の構造 +- overlay では **patch を最小限**。値だけなら `configMapGenerator` / `secretGenerator` で上書き +- `kustomize build overlays/prod | kubectl diff -f -` で本番差分確認 → CI で `kustomize build` をレンダリング +- patch は **strategic merge patch (`patches:` with `target:`)** を default。JSON Patch は強い変更のみ +- `commonLabels` / `commonAnnotations` で全リソースに label 一括付与 +- `namePrefix` / `nameSuffix` で環境別接尾辞 (selector の immutability に注意) +- 親 `base/` を環境固有の値で汚さない (base は環境非依存に保つ) + +### Helm を使う場合の規約 +- `Chart.yaml` の `apiVersion: v2` +- chart version と app version を区別 (`version` / `appVersion`) +- values.yaml は **commented / typed** にして利用者が読める形 +- `helm template` で render → `kubectl diff` で確認してから `helm upgrade` +- OSS chart を取り込む時も Kustomize の `helmCharts:` で values inflate → overlay patch、という構成が扱いやすい + +検出シグナル: +- 自社アプリ用に新規 Helm chart を切ろうとしている (Kustomize で済まないか先に検討) +- overlay で patch が肥大化 (base 設計を見直す signal) +- base が環境値で汚れている + +--- + +## 13. Lint / Validation + +CI に必ず仕込む: + +| tool | 役割 | +|---|---| +| `kubeconform` | schema validation (apiVersion / kind の整合) | +| `kube-linter` | 設定 anti-pattern (probe 無し / privileged / no resources 等) | +| `conftest` (OPA / Rego) | カスタムポリシー (社内規約の強制) | +| `trivy config` | manifest の security misconfig 検出 | +| `kustomize build` | render 可能性 + 結果を kubeconform / kube-linter に流す | +| `pluto` | 廃止 API 検出 | + +**最低限**: `kubeconform` + `kube-linter` + `trivy config` の 3 つは入れる。`kustomize build` の出力を CI でこれらに流すのが定番フロー。 + +--- + +## 14. Observability + +- 全 workload に **metrics endpoint** (`/metrics` Prometheus format) を生やす +- pod に `prometheus.io/scrape` annotation か、ServiceMonitor (Prometheus Operator) で配線 +- log は stdout / stderr に **構造化 JSON で出す**。file には書かない (ephemeral container は消える) +- trace は OpenTelemetry SDK + sidecar / DaemonSet collector → backend (Tempo / Jaeger) +- 重要 SLI は **Recording Rule** で事前計算 (request 時の計算重を避ける) +- 高 cardinality な label / tag を metrics に入れない (時系列 DB を破壊する) + +--- + +## 15. Cost / 効率 + +- 不要な resources requests = 隠れたコスト。VPA recommender で計測値ベースに +- HorizontalPodAutoscaler (`autoscaling/v2`) で **負荷追従**。CPU だけでなく custom metrics も検討 +- Cluster Autoscaler / Karpenter で node 自動増減 +- Spot / Preemptible node を batch / dev に活用 (production の stateless にも条件付きで) + +--- + +## 16. 関連 skill / agent + +- security レビュー → `security-review-local` skill +- docs (ADR / Runbook / Design) → `tech-docs-writer` skill +- 層境界 / 責務 (アプリ側) → `ddd-hexagonal` skill +- Go アプリ規約 → `go-style` / `go-test` skill + +--- + +## チェックリスト (manifest review 時) + +- [ ] `apiVersion` は stable / 非推奨でない +- [ ] recommended labels (`app.kubernetes.io/*`) が全 resource に揃っている +- [ ] `resources.requests` / `limits` が設定されている (memory limit 必須) +- [ ] `liveness` / `readiness` / `startup` を使い分けている (liveness は依存先を見ない) +- [ ] `securityContext` で `runAsNonRoot` / `readOnlyRootFilesystem` / `capabilities.drop: ["ALL"]` +- [ ] `serviceAccountName` 明示 + 必要最小の RBAC + `automountServiceAccountToken: false` (不要なら) +- [ ] NetworkPolicy で default deny + 必要な allow (DNS allow 忘れに注意) +- [ ] production `replicas >= 2` + PDB + topologySpreadConstraints +- [ ] image tag pin (digest 推奨) + `pullPolicy` 明示 + distroless 系 base +- [ ] Secret が平文で commit されていない (External Secrets / Sealed Secrets / Vault) +- [ ] Kustomize の base が環境非依存 / overlay patch が最小 +- [ ] `kubeconform` / `kube-linter` / `trivy config` を CI で通している diff --git a/claude/ja/skills/notion-ticket-plan/SKILL.md b/claude/ja/skills/notion-ticket-plan/SKILL.md new file mode 100644 index 0000000..433d55c --- /dev/null +++ b/claude/ja/skills/notion-ticket-plan/SKILL.md @@ -0,0 +1,201 @@ +--- +name: notion-ticket-plan +description: Notion / Linear / GitHub Issue の ticket URL を入力に、関連 docs / ADR を探索して実装プランを plan ファイルに書き出し plan モード承認を取る (planning 専用、実装はしない)。「ticket に沿って計画して」等で使う。 +model: claude-fable-5 +--- + +# notion-ticket-plan + +ticket (Notion / Linear / GitHub Issue) URL を起点に、リポジトリの docs / ADR / 既存コードを探索し、**実装プランを plan ファイルに書き出して承認を取る**ところまでを担う skill。実装は行わない。 + +## 適用条件 + +- Plan モードで動くのが基本 (実装は別 skill / 別ターン) +- リポジトリに `docs/` や `ADR/` 等の設計ドキュメントがあると効果的 (なくても動く) + +## 手順 + +### Step 1: ticket 取得 + +ticket URL から内容を読む。`Definition of Done`、スコープ、関連 ADR / 設計 doc への言及をメモする。 + +| ticket source | tool | +|---|---| +| Notion | `mcp__claude_ai_Notion__notion-fetch` | +| GitHub Issue / PR | `gh issue view` / `gh pr view` (Bash) | +| Linear | MCP があればそれ、なければ URL を WebFetch | +| その他 URL | WebFetch | + +抽出する観点: +- DoD (受入基準) +- スコープ内 / スコープ外 +- 関連する別 ticket (前後関係) +- deadline / status + +### Step 1.5: DoD ↔ 既存 docs の literal 突合 + +DoD と既存 docs の両方が古い可能性があるため、両者を**並列に読み比較**して矛盾を抽出する。 + +1. DoD literal を抽出 (型名 / method 集合 / 配置 path / 設計責務 / 命名) +2. 関連 docs (`docs/design/*.md`、ADR) の literal を抽出 +3. **矛盾点を網羅的に列挙**: + - 命名 (例: `product_id` 単数 vs `product_ids` list) + - method 集合 (例: 6 method vs 7 method) + - 配置 path (例: `domain/ports` vs `application/ports`) + - 設計責務レイヤー (例: ACL を adapter で組むか application で組むか) +4. **AskUserQuestion で採用 literal を user に確定してもらう** + - DoD と docs どちらが新しい / 信頼できるか user 判断 + - 必要なら docs 側の修正を別タスクとして flag +5. 確定 literal を Step 6 の plan ファイルの "Confirmed decisions" に記録 +6. **各矛盾箇所を Tier 1〜4 に分類** (docs / 実装 update の scope 判断) + - **Tier 1**: 純粋な literal alignment (path / file name / type 名等の機械的揃え) → 本 ticket scope 内、実装と同 commit で修正 + - **Tier 2**: 確定 design の追従 (method 削除 / signature 更新等) → 本 ticket scope 内、実装と同 commit で修正 + - **Tier 3**: 設計判断必要 (caller 側の責務変更を伴う等) + - 判断軸: 「この変更を実装しないと DoD を満たすか」 + - **DoD 必須なら同一 PR 内で実装**(設計判断は user に仰ぐ、実装も含めて scope 内) + - DoD scope 外なら別 ticket、本 ticket では flag のみ + - **Tier 4**: pseudocode / コード例の全面更新 (cosmetic だが分量大) → 別 commit / 別 ticket、本 ticket では flag のみ + +**重要**: DoD を rule にしない。docs を rule にしない。**両者を並列提示して user に判断**してもらう。 + +### Step 2: 関連 docs 探索 (Explore subagent を最大 3 並列で起動) + +Explore subagent を使って以下を並列で探索: + +- 実装計画ファイル (`docs/plan/*.md` 等) の該当 ticket 周りの記述 +- ADR (`docs/adr/*.md`) で関連する決定事項 +- 設計 doc (`docs/design/*.md`) +- README / CLAUDE.md の関連情報 +- 既存コード (該当機能の周辺) +- 既存 .gitignore / 設定ファイルでスコープに影響するもの + +各 Explore に「具体的なファイルパス + 質問リスト」を渡す。返答は file:line で引用させる。 + +### Step 3: Plan agent で設計 + +Plan subagent を起動。前ステップで得た情報を背景として渡し、実装プランを書かせる。プロンプトに含める要素: + +- ticket DoD (Step 1) +- 関連 ADR 抜粋 (Step 2) +- 既存ファイルの状況 +- 確定済み前提 +- 検討してほしい論点 (バージョン選定、命名、緩和ルール等) +- スコープ外項目 (別 ticket への引き継ぎ) + +複雑なタスクなら最大 3 並列で異なる観点 (簡潔さ vs 拡張性 vs 保守性) を取らせる。 + +### Step 4: 主要ファイル直接読み込み + +Plan agent の返答をもとに、修正対象ファイル / 関連設定ファイルを Read で直接確認する。Plan の前提が現状と合っているか検証。 + +### Step 5: 必要なら AskUserQuestion で論点詰め + +不確定論点 (バージョン値、ライブラリ選択、tone) があれば 1-2 問に絞って AskUserQuestion。AskUserQuestion を「plan 承認」用には絶対に使わない (それは ExitPlanMode の役目)。 + +### Step 5.5: api-design-review skill で 6 観点 review + +plan を書き出す **前** に、`api-design-review` skill を invoke して以下 6 観点で考慮漏れを catch: + +1. client 抽象 vs server 展開の分離 +2. ACL の読み / 書き 両側 +3. forward-compat (enum / field / RPC 追加が非破壊で可能か) +4. edge case 列挙 ("こういう時どうするの?") +5. 既存 SoT との整合 (grep-first) +6. memory 規約準拠 + +検出された考慮漏れがあれば AskUserQuestion で user 判断を仰ぎ、plan に反映してから Step 6 へ。reviewer 視点での「あとから気づく」を抑制する。skill 結果は plan ファイルに「## Design review (api-design-review)」section として記録。 + +skill invoke 不要の判断軸: 単純な追加 / 既存 contract に影響ない内部 refactor / バグ fix / 軽微な改修。新 service / 新 RPC / 新 enum / 新 ACL モデル / 新 ADR / 設計責務レイヤーの変更が含まれる ticket では **必須通過**。 + +### Step 6: plan ファイル書き出し + +- Plan モード提示の plan file path に最終プランを書く +- **plan ファイルは per-project plans dir の `<ticket-slug>.md` (`~/.claude/projects/<encoded>/plans/<ticket-slug>.md`、CLAUDE.md「plan / session state file の保存先」参照) を canonical な session state file として扱う**。後続の agent も Read して状態を引き継ぐ +- 構成: + +``` +# <Phase> / <Ticket ID>: <タイトル> — 実装プラン + +## Context +なぜこの変更が必要か (DoD ベース) + +## Confirmed decisions +Step 1.5 で確定した literal を列挙。再実装時の参照源、後続 agent が context bootstrap に使う。永続的な設計判断はここに置く (二度と変えない、spec 側を update する対象)。 +| 判断項目 | 採用 literal | 根拠 (DoD / docs どちら) | + +## Scope decisions (DoD 由来の意図的限定) +DoD で「stub のみで OK」「後続 ticket で実装」と明示されている範囲限定。逸脱ではなく意図的なので PR description には flag しない。 +| 範囲限定項目 | DoD 根拠 | 後続 ticket | + +## Spec deviations (PR description で flag、reviewer 確認対象) +DoD / docs と実装で割れる「永続的な構造選択」のみを列挙。Scope decisions / Phase 2+ migration は別カテゴリに振り分け、ここには **reviewer に「これで OK か」と確認したい項目だけ** を残す。 +| # | 逸脱内容 | 是正方針 (spec update / 維持 / 後日見直し) | + +## Phase 2+ migration (follow-up ticket 化) +prototype 段階の暫定実装で、production 移行時に refactor 予定のもの。Notion ticket comment 等に follow-up として記録、本 PR では実装しない。 +| 暫定実装 | 将来形 | follow-up ticket / link | + +## Carryover (既存問題、別 ticket) +本 ticket scope 外の既存問題で、本 PR では触らないが視認しておきたいもの。 +| 既存問題 | 影響範囲 | 対応 ticket | + +## Documentation updates (Tier 分類) +Step 1.5 で抽出した矛盾箇所を Tier 別に整理。本 ticket での扱いを明示する。 +| 対象 doc | 修正内容 | Tier (1/2/3/4) | 扱い (同 commit / 同 PR / 別 ticket) | + +## Current state (随時更新) +進行状態。後続 agent / 後日の自分が Read 1 回で context bootstrap できるように。 +- Stage X 完了 / Y 進行中 +- 直近 commit: <hash> +- Pending questions: ... + +## Design review (api-design-review) +Step 5.5 で実行した api-design-review skill の結果 sumamry。考慮漏れ catch を監査可能に記録。 +- 6 観点それぞれの検出有無 +- 修正反映済の項目 +- user 判断で resolved の項目 +- 残置 (follow-up ticket) 項目 + +## 主要な設計判断 +| 判断項目 | 決定 | 根拠 | + +## 実装ステップ (実行順) +### Step 1: ... +- 新規 / 編集ファイル + 内容概要 + +## DoD と実装ステップの対応 +| Notion DoD 項目 | 対応 Step | 検証方法 | + +## 想定される落とし穴 + +## 検証手順 (実装完了後) + +## 次のチケットへの引き継ぎ (スコープ外) + +## 参照 +- ticket URL +- docs/... のパス +``` + +### Step 7: ExitPlanMode で承認を取る + +ExitPlanMode を呼んで plan 承認をリクエスト。allowedPrompts に実装で必要な Bash カテゴリを書く (例: `[{tool: "Bash", prompt: "go コマンド実行"}]`)。 + +## 鉄則 + +1. **plan モード厳守**: ファイル編集は plan ファイルのみ。それ以外は read-only +2. **ExitPlanMode で承認を取る**: 「いいですか?」「進めて良いですか?」と地の文で聞かない +3. **過去のメモリ feedback を尊重**: prototype 段階の前提節省略、用語規約 (例: tenant→company)、コメント言語など +4. **plan は scan-friendly**: 表 + 箇条書きを多用。長文段落は避ける +5. **scope 外を明示**: 別 ticket / 後続 phase で扱うものを必ず分離 +6. **state file を maintain する**: 重要判断確定 / spec 逸脱発見のたびに plan ファイルを update。後続の agent / 後日の自分が Read 1 回で context bootstrap できる状態を保つ +7. **docs update の Tier 別運用**: + - Tier 1+2 (機械的揃え / 確定 design 追従): 実装と同 commit で修正 + - Tier 3 (設計判断必要): DoD 必須なら同一 PR 内で実装、DoD scope 外なら別 ticket + - Tier 4 (pseudocode 全面更新): 別 commit / 別 ticket + + plan ファイルの "Documentation updates" section に対象 doc / 修正内容 / Tier / 扱いを記録する + +## 完了時の動き + +ユーザーが ExitPlanMode を承認したら skill 終了。実装は別 skill (例: `/go-bootstrap`, `go-feature-tdd` subagent) や次ターンで行う。 diff --git a/claude/ja/skills/retrospect/SKILL.md b/claude/ja/skills/retrospect/SKILL.md new file mode 100644 index 0000000..ee082d3 --- /dev/null +++ b/claude/ja/skills/retrospect/SKILL.md @@ -0,0 +1,59 @@ +--- +name: retrospect +description: dev cycle / 作業 session の最後に、詰まった点・やり直し・新規判明した規約や環境の癖を insights として 1 件記録する軽量 skill。「retrospect して」「振り返り記録して」等で使う。分析・集約・改善 PR 化はしない (improve-harness の責務)。 +--- + +# retrospect + +cycle 末の軽量記録。**1 分で書ける粒度を守る**。分析・集約・改善 PR 化は improve-harness の責務で、本 skill は記録のみ。 + +## 手順 + +### Step 1: 要点収集 + +直前の cycle / 作業から以下を洗い出す (該当なしなら記録せず終了してよい): + +- 詰まった点 / やり直したこと +- user からの訂正・指摘 +- 新規に判明した規約・環境の癖 +- skill / agent / rules の欠陥や不足 + +### Step 2: category 判定 + +| category | 意味 | +|---|---| +| skill-gap | skill の手順・観点の不足や欠陥 | +| rule-gap | CLAUDE.md / rules/ の規約不足・曖昧さ | +| env-quirk | 環境・tool の癖 (再現する挙動) | +| spec-gap | spec contract / spec 内容の不備 | + +### Step 3: insights ファイルに記録 + +対象 project の per-project dir に **1 insight = 1 ファイル**で書く: + +- path: `~/.claude/projects/<encoded>/insights/<YYYYMMDD>-<slug>.md` +- `<encoded>` = cwd 絶対パスの `/` と `.` を `-` に置換 (CLAUDE.md「plan / session state file の保存先」と同じ規約) + +```markdown +--- +date: <YYYY-MM-DD> +category: skill-gap | rule-gap | env-quirk | spec-gap +ticket: <ticket URL / id / なし> +target: <対象の skill / agent / rules ファイル (あれば)> +--- + +## 事象 + +<何が起きたか 1-3 行> + +## 提案 + +<どう直すべきか 1-3 行 (不明なら「未定」)> +``` + +## 鉄則 + +1. 記録のみ。分析・修正・PR 化はしない +2. 1 insight = 1 ファイル。まとめ書きしない +3. secret / ticket 本文の丸ごと転記をしない (参照は id / URL) +4. 該当なしの cycle は無理に書かない (ノイズを溜めない) diff --git a/claude/ja/skills/rust-bootstrap/SKILL.md b/claude/ja/skills/rust-bootstrap/SKILL.md new file mode 100644 index 0000000..f16b376 --- /dev/null +++ b/claude/ja/skills/rust-bootstrap/SKILL.md @@ -0,0 +1,129 @@ +--- +name: rust-bootstrap +description: 新規 / 既存ディレクトリに Rust workspace の動く骨格を一括セットアップする (crates/* レイアウト、lints 集中管理、Makefile、deny.toml、CI、rustup 導入まで。task runner は make のみ)。「Rust の workspace 切って」「Rust プロジェクトのセットアップ」「複数 tool 入れる Rust リポジトリにして」等で使う。 +--- + +# rust-bootstrap + +Rust workspace をゼロから「`make ci` (fmt-check + clippy -D warnings + test) が pass する状態」までセットアップする skill。1 リポジトリにつき 1 回しか使わない想定。 + +**ファイルテンプレートは `references/templates.md` に全部ある。Step 2 に着手する前に 1 回 Read し、placeholder を Step 0 の確認値で置換して使う。** + +> **Snapshot**: 2026-04 / Rust 1.95 stable / edition 2024 / resolver 3 を前提に書かれてる。 +> 半年以上経った場合、Cargo.toml テンプレや CI workflow のバージョン (dtolnay/rust-toolchain, Swatinem/rust-cache, EmbarkStudios/cargo-deny-action 等) は確認してから流用すること。 + +## 適用条件 + +- バイナリツール群を束ねる workspace を想定 (ライブラリ公開ではない) +- Rust 1.90+ / edition 2024 / resolver 3 を採用 +- レイアウトは `crates/<name>/` に複数 crate (将来追加前提)。違うレイアウトのときはユーザーに確認 + +## 手順 + +### Step 0: 前提確認 + +```bash +which cargo && cargo --version && rustc --version # 未導入なら Step 1 で rustup +ls -la # 既存ファイル把握 +test -f Cargo.toml && cat Cargo.toml # 既存 manifest 確認 +``` + +確認事項 (AskUserQuestion で 1 ターンに集約): +- workspace 名 (= リポジトリ名でよいか) +- ライセンス (`MIT` / `Apache-2.0` / `MIT OR Apache-2.0` のどれか — エコシステム慣習は dual) +- 最初の crate 名 (例: `mytool`) +- repository URL (`https://github.com/<user>/<repo>`) +- バイナリの主用途 (CLI / TUI / daemon — TUI なら ratatui 雛形) + +### Step 1: rustup インストール (未導入時のみ) + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile default +. "$HOME/.cargo/env" +``` + +`. "$HOME/.cargo/env"` を以降の Bash 呼び出し前に挟むこと (PATH を継承させるため)。 + +### Step 2〜9: ファイル生成 (テンプレは `references/templates.md`) + +| Step | ファイル | 要点 | +|---|---|---| +| 2 | ルート `Cargo.toml` | virtual workspace。`[workspace.package]` でメタ集中管理 / `[workspace.dependencies]` / lints / distribution-grade release profile。TUI なら crossterm + ratatui + futures を dependencies に追加 | +| 3 | `rust-toolchain.toml` | stable + rustfmt + clippy (CI と手元のズレ防止) | +| 4 | `crates/<name>/` | Cargo.toml は全部 workspace 継承 (`*.workspace = true`)、main.rs は clap + anyhow の最小 CLI | +| 5 | `rustfmt.toml` | edition 2024 / max_width 100 | +| 6 | `Makefile` | help / build / release / test / clippy / fmt / check / deny / ci / install / uninstall / clean / update | +| 7 | `deny.toml` | advisories + licenses allow-list + bans + sources (supply-chain 監査) | +| 8 | `.cargo/config.toml` | Windows static CRT + `cargo ci` / `cargo xclippy` alias | +| 9 | `.github/workflows/ci.yml` | lint (fmt + clippy) / test (3 OS matrix) / deny の 3 job | + +**install 戦略 (Step 6 の設計判断)**: `cargo install --path` は毎回 release build がトリガーされるが、`make install` は workspace 一括の release build を 1 回走らせて `cp` で配るので、複数 binary の workspace では速い。default の `PREFIX = ~/.local/bin` は sudo 不要 + XDG 標準。`/opt/homebrew/bin` は Homebrew 管理外のバイナリを混ぜると update 時に混乱の素なので避ける。 + +### Step 10: `LICENSE` + +選んだライセンスに応じて配置。`MIT OR Apache-2.0` なら `LICENSE-MIT` と `LICENSE-APACHE` 両方。MIT のみなら `LICENSE`。 + +### Step 11: `.gitignore` + +``` +/target +**/*.rs.bk +.DS_Store +``` + +`Cargo.lock` は **コミットする** (binary workspace の慣習)。`.gitignore` に入れない。 + +### Step 12: ルート `README.md` + +tools 表 + `make help` / `make ci` / `make install` の説明 (テンプレは `references/templates.md`)。 + +### Step 13: CLAUDE.md / 既存 README 同期 + +`CLAUDE.md` があれば「## 開発コマンド」を追記 (追記分のテンプレは `references/templates.md`)。既存 README は上書きせず追記。 + +### Step 14: 動作確認 + +```bash +. "$HOME/.cargo/env" +cargo build --workspace && echo BUILD_OK +cargo test --workspace --all-targets && echo TEST_OK +cargo clippy --workspace --all-targets --all-features -- -D warnings && echo CLIPPY_OK +cargo fmt --all -- --check && echo FMT_OK +``` + +`make ci` で同じ 3 つを一括実行できる。 + +### Step 15: 完了報告 + +| 項目 | 状況 | +|---|---| +| `cargo build --workspace` | ✓ | +| `cargo test` / `cargo clippy -D warnings` / `cargo fmt --check` | ✓ | +| workspace 名 / 最初の crate | <確認値> | +| ライセンス | <確認値> | +| `Cargo.toml` (workspace.package + dependencies + lints + profile) | ✓ | +| `rust-toolchain.toml` / `rustfmt.toml` / `Makefile` / `deny.toml` / `.cargo/config.toml` | ✓ | +| `.github/workflows/ci.yml` | ✓ | +| LICENSE / README.md | ✓ | + +## 鉄則 + +1. **既存ファイル尊重**: 既存 `.gitignore` / `CLAUDE.md` / `README.md` は上書きせず追記 +2. **コメントは英語**: Rust ファイルのコメントは英語 (`//`, `///`, `//!`) +3. **`Cargo.lock` はコミットする**: バイナリ workspace の現代的慣習 +4. **scope を守る**: 機能実装 (TUI イベントループ・サーバ実装等) は対象外。次の skill (`add-rust-crate`) や手作業に渡す +5. **コミットしない**: コミットは別 skill (`/commit-push-branch`) に委ねる +6. **`pub` より `pub(crate)`**: バイナリ crate は外部公開がないので可視性は最小から始める +7. **task runner は `make` のみ**: `make` は macOS / Linux 標準同梱で追加 dep ゼロ。`just` / `task` / `xtask` などは新規 dep なので default では入れない (ユーザーが明示的に希望した場合のみ)。Makefile に対する利点は cross-platform / 引数 / `--list` 程度で、追加 dep を払うほど大きくない + +## アンチパターン + +- `Cargo.toml` に各 crate ごとの version/license/repo をベタ書き (workspace inheritance を使わない) +- `[profile.release]` を crate 側 Cargo.toml に書く (Cargo は workspace ルートしか見ない) +- `.gitignore` に `Cargo.lock` を入れる (library crate の慣習を binary に持ち込まない) +- `resolver = "2"` のまま放置 (edition 2024 + Rust 1.84+ なら 3 を選ぶ) +- `pedantic` clippy を allow なしで有効化 (`cast_precision_loss` など意図的なものは workspace lints で allow + 理由コメント) +- ライブラリ公開向けの `#[non_exhaustive]` / `C-CRATE-DOC` を全 type に強制 (binary crate には過剰) +- `rust-toolchain.toml` を作らない (CI と手元の toolchain ズレの温床) +- `cargo-dist` を最初から仕込む (個人 toolkit には過剰、リリースを手で作るタイミングで導入) +- 「`just` がデファクト / 現代的」のような研究エージェントの **印象論** を鵜呑みにして新規 dep を入れる (実態は ripgrep / fd / bat / cargo / rustc / uv / ruff いずれも `just` を使っていない) diff --git a/claude/ja/skills/rust-bootstrap/references/templates.md b/claude/ja/skills/rust-bootstrap/references/templates.md new file mode 100644 index 0000000..21d38ef --- /dev/null +++ b/claude/ja/skills/rust-bootstrap/references/templates.md @@ -0,0 +1,336 @@ +# rust-bootstrap file templates + +SKILL.md の Step 番号に対応するファイルテンプレート集。placeholder (`<name>` / `<license>` / `<repo-url>` / `<user>` / `<one-line>`) は Step 0 の確認値で置換する。 + +## Step 2: workspace `Cargo.toml` + +ルートに virtual workspace (`[package]` なし)。`[workspace.package]` で全 crate のメタを集中管理: + +```toml +[workspace] +resolver = "3" +members = ["crates/*"] + +[workspace.package] +edition = "2024" +rust-version = "1.90" +license = "<license>" # 例: "MIT" or "MIT OR Apache-2.0" +authors = ["<user>"] +repository = "<repo-url>" +homepage = "<repo-url>" +keywords = ["cli"] +categories = ["command-line-utilities"] +publish = false # crates.io 公開しない + +[workspace.dependencies] +anyhow = "1" +# `env` feature lets `#[arg(env = "FOO")]` fall back to env var when CLI flag absent. +clap = { version = "4", features = ["derive", "env"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros", "sync", "time", "io-util"] } +tempfile = "3" + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" + +# Distribution-grade release profile. +[profile.release] +lto = "fat" +codegen-units = 1 +strip = "symbols" +panic = "abort" +opt-level = 3 +incremental = false + +[profile.dist] +inherits = "release" +``` + +**TUI を作るなら** `[workspace.dependencies]` に追加: + +```toml +crossterm = { version = "0.28", features = ["event-stream"] } +ratatui = "0.29" +futures = "0.3" +``` + +## Step 3: `rust-toolchain.toml` + +```toml +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] +``` + +## Step 4: 最初の crate (`crates/<name>/`) + +`crates/<name>/Cargo.toml`: + +```toml +[package] +name = "<name>" +version = "0.1.0" +description = "<one-line>" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +publish.workspace = true + +[[bin]] +name = "<name>" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true + +[dev-dependencies] +tempfile.workspace = true + +[lints] +workspace = true +``` + +`crates/<name>/src/main.rs`: + +```rust +//! <name> — <one-line description> + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "<name>", version, about)] +struct Cli {} + +fn main() -> Result<()> { + let _cli = Cli::parse(); + Ok(()) +} +``` + +## Step 5: `rustfmt.toml` + +```toml +edition = "2024" +max_width = 100 +use_field_init_shorthand = true +use_try_shorthand = true +``` + +## Step 6: `Makefile` + +```makefile +.DEFAULT_GOAL := help + +# Override: `make install PREFIX=/opt/homebrew/bin` +PREFIX ?= $(HOME)/.local/bin + +.PHONY: help build release test clippy fmt-check fmt check deny ci install uninstall clean update + +help: ## このヘルプを表示 + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' + +build: ## デバッグビルド (workspace 全 crate) + cargo build --workspace --all-targets + +release: ## リリースビルド (lto=fat / panic=abort) + cargo build --workspace --release --locked + +test: ## テスト実行 (workspace 全 crate) + cargo test --workspace --all-targets --locked + +clippy: ## clippy (-D warnings) + cargo clippy --workspace --all-targets --all-features -- -D warnings + +fmt-check: ## フォーマット確認 + cargo fmt --all -- --check + +fmt: ## フォーマット適用 + cargo fmt --all + +check: ## コンパイル確認のみ + cargo check --workspace --all-targets --locked + +deny: ## supply-chain 監査 (要 cargo-deny) + cargo deny check + +ci: fmt-check clippy test ## fmt-check + clippy + test 一括 + +install: release ## release build → $(PREFIX) にコピー (default: ~/.local/bin) + @mkdir -p $(PREFIX) + @for crate in crates/*/; do \ + name=$$(basename $$crate); \ + cp target/release/$$name $(PREFIX)/$$name; \ + if [ "$$(uname)" = "Darwin" ]; then \ + codesign --force --sign - $(PREFIX)/$$name >/dev/null 2>&1; \ + fi; \ + echo "installed $$name -> $(PREFIX)/$$name"; \ + done + +uninstall: ## $(PREFIX) からバイナリ削除 + @for crate in crates/*/; do \ + name=$$(basename $$crate); \ + rm -f $(PREFIX)/$$name; \ + echo "removed $(PREFIX)/$$name"; \ + done + +clean: ## ビルド成果物削除 + cargo clean + +update: ## Cargo.lock 更新 + cargo update +``` + +## Step 7: `deny.toml` (cargo-deny) + +```toml +[graph] +all-features = true + +[advisories] +version = 2 +yanked = "deny" +ignore = [] + +[licenses] +version = 2 +allow = [ + "MIT", "MIT-0", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", "BSD-3-Clause", "ISC", + "Unicode-3.0", "Unicode-DFS-2016", "Zlib", "CC0-1.0", "MPL-2.0", +] +confidence-threshold = 0.93 + +[bans] +multiple-versions = "warn" +wildcards = "deny" +deny = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +``` + +## Step 8: `.cargo/config.toml` + +```toml +[target.'cfg(all(target_env = "msvc", target_os = "windows"))'] +rustflags = ["-C", "target-feature=+crt-static"] + +[alias] +ci = "test --workspace --all-targets --locked" +xclippy = "clippy --workspace --all-targets --all-features -- -D warnings" +``` + +## Step 9: `.github/workflows/ci.yml` + +```yaml +name: ci +on: + push: { branches: [main] } + pull_request: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: { components: rustfmt, clippy } + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all --check + - run: cargo clippy --workspace --all-targets --all-features -- -D warnings + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: { shared-key: ${{ matrix.os }} } + - run: cargo test --workspace --all-targets --locked + - run: cargo build --workspace --release --locked + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 +``` + +## Step 12: ルート `README.md` + +tools 表を含む簡易版: + +```markdown +# <repo> + +A Cargo workspace of small terminal tools. + +## Tools + +| Crate | Description | +|---|---| +| [`<name>`](crates/<name>) | <one-line> | + +## Build & install + +\```sh +make help # 全レシピ +make ci # fmt-check + clippy + test +make install # release build → ~/.local/bin/ +make install PREFIX=/opt/homebrew/bin +make uninstall +\``` + +`~/.local/bin` が PATH に通っていない場合は `~/.zshrc` 等に +`export PATH="$HOME/.local/bin:$PATH"` を追加。 + +## Adding a new tool + +1. Create `crates/<new-name>/Cargo.toml` inheriting from workspace +2. Add a row to the table above +3. `make ci` が通ることを確認 +``` + +## Step 13: CLAUDE.md「開発コマンド」追記分 + +```markdown +## 開発コマンド + +| コマンド | 用途 | +|---|---| +| `make` (= `make help`) | レシピ一覧 | +| `make ci` | fmt-check + clippy + test | +| `make release` | release ビルド (`lto = "fat"`, `panic = "abort"`) | +| `make install` | 全 crate を `$(PREFIX)` へコピー (default: `~/.local/bin`) | +| `make install PREFIX=/opt/homebrew/bin` | install 先を変更 | +| `make uninstall` | install したバイナリを削除 | +| `make deny` | supply-chain audit (`cargo deny check`、要 `cargo install cargo-deny`) | + +新 crate を追加するときは `/add-rust-crate` skill を使う。 +``` diff --git a/claude/ja/skills/security-review-local/SKILL.md b/claude/ja/skills/security-review-local/SKILL.md new file mode 100644 index 0000000..fd24aff --- /dev/null +++ b/claude/ja/skills/security-review-local/SKILL.md @@ -0,0 +1,128 @@ +--- +name: security-review-local +description: ローカルリポジトリと Claude Code 設定のセキュリティ監査。secret leak / .env 実値混入 / permission 過剰許可 / suspicious 命令 / supply chain リスクを横断チェックする。「security review して」「secret 漏れてない?」等で使う。 +--- + +# security-review-local + +ローカルリポジトリの security 観点を横断監査する skill。コミット前 / push 前 / 設定変更後に走らせる。 + +> **builtin `/security-review` との使い分け**: builtin は pending changes (code diff) の脆弱性 review。本 skill はそれに加えて **Claude Code 設定の permission 過剰付与 / tracked secret 名 / .env 実値混入 / Makefile suspicious 命令 / supply chain** をローカル横断で監査する。コード差分の脆弱性なら `/security-review`、リポジトリ + Claude 設定の漏洩・権限監査なら本 skill。 + +## 適用条件 + +- git リポジトリ内で実行 +- `.gitignore` / `.env*` / `.claude/settings*.json` などが対象になる + +## チェック項目 + +### 1. .gitignore の secret パターン除外 + +`.env`, `*.env`, `*.pem`, `*.key`, `id_rsa*`, `credentials*`, `secret*`, `*.p12`, `*.pfx`, `*.kdbx` がカバーされているか確認: + +```bash +git check-ignore -v .env .env.local secrets/foo.txt credentials.json id_rsa.pem 2>&1 +``` + +各テストパスが `.gitignore:<line>:<pattern>` でマッチすれば OK。マッチしない → `.gitignore` に追記が必要。 + +### 2. tracked files に secret 系命名がないか + +```bash +git ls-files | xargs -I {} sh -c 'case "{}" in *.env|*.pem|*.key|*credentials*|*secret*|*.p12|*.pfx) echo "WARNING: {}";; esac' +``` + +`.env.example` / `.envrc.example` のような **テンプレ** は許容。実 secret ファイル (`.env`, `id_rsa`) が tracked になっていたら即 unstage。 + +### 3. .env* テンプレの実値混入チェック + +```bash +ls -la .env* 2>/dev/null +``` + +`.env.example` 等が tracked なら Read して、`OPENAI_API_KEY=sk-...` のような実値が入っていないか確認。形式は `KEY=` (空) が望ましい。 + +### 4. Claude Code 設定の permission 確認 + +```bash +cat .claude/settings.json +cat ~/.claude/settings.json +``` + +allow リストに以下が**含まれていない**ことを確認: + +| ⚠️ 含まれていたら危険 | 理由 | +|---|---| +| `Bash(rm:*)` / `Bash(rm -rf:*)` | 破壊的 | +| `Bash(curl:*)` / `Bash(wget:*)` | 任意 URL アクセス | +| `Bash(cat:*)` (broad) | `~/.ssh/id_rsa` 等任意ファイル読み出し | +| `Bash(grep:*)` (broad) | 任意ファイル grep | +| `Write(*)` / `Edit(*)` (broad) | プロジェクト外含む任意書き込み | +| `Bash(ssh:*)` / `Bash(scp:*)` | リモートアクセス | +| `Bash(*)` | 完全な任意実行 | + +範囲限定された read-only 系 (`Bash(go test:*)`, `Bash(git status)`, `Bash(ls:*)`) は問題なし。 + +### 5. コード / Makefile の suspicious コマンド + +```bash +grep -rE "(curl|wget|eval\(|http://|https://[^ )]*\.(sh|env)|nc -e|/dev/tcp)" \ + Makefile .golangci.yaml cmd/ internal/ scripts/ 2>/dev/null +``` + +ヒットしたら詳細確認。CI スクリプトでの `curl https://...install.sh | sh` は要レビュー。 + +### 6. Go コードの log 出力に secret + +```bash +grep -rnE 'slog\.(Info|Debug|Warn|Error).*\b(API_KEY|SECRET|TOKEN|PASSWORD|ENV)' cmd/ internal/ 2>/dev/null +grep -rnE 'fmt\.(Print|Println|Printf).*\b(API_KEY|SECRET|TOKEN|PASSWORD)' cmd/ internal/ 2>/dev/null +grep -rn 'os\.Environ()' cmd/ internal/ 2>/dev/null # 環境変数全ダンプ +``` + +ヒットしたら、log mask / redact が必要か検討。 + +### 7. 外部依存の supply chain + +```bash +cat go.mod +go mod why <suspicious-pkg> # 必要なら +``` + +- `require` ブロックの依存先が信頼できる org か (例: 怪しい author の typosquat) +- `replace` ディレクティブが local path を指していないか (commit してはいけない) +- `go.sum` が gitignore されていないか (整合性検証に必須) + +### 8. .claude/settings.local.json (個人設定) + +```bash +git check-ignore .claude/settings.local.json +``` + +ignored であることを確認。tracked になっていたら個人 token 等の漏洩リスク。 + +## レポート形式 + +``` +## セキュリティ監査結果 (<時刻>) + +### ✓ 問題なし +- .gitignore: .env / *.pem / *.key / credentials* / secrets/ 全て ignored +- tracked files: secret 系命名なし (git ls-files スキャン) +- .env.example: 全キー空 (実値未混入) +- .claude/settings.json allow list: 安全範囲 +- 外部依存: <数> 件、全て信頼できる org +- log 出力: sensitive ダンプなし + +### ⚠️ 要対応 +| # | 項目 | リスク | 修正方針 | +|---|---|---|---| +| 1 | ... | ... | ... | +``` + +## 鉄則 + +1. **報告は具体的に**: 「○○が問題」だけでなく、どこを直すかまで提示 +2. **false-positive を恐れない**: 怪しいものは listed する。「問題なし」を装わない +3. **修正は別 skill に委ねる**: ここは監査専用。修正は `/self-review-changes` 等で +4. **過去の incident を覚えておく**: 同じ漏洩パターンを次回も見落とさない diff --git a/claude/ja/skills/self-review-changes/SKILL.md b/claude/ja/skills/self-review-changes/SKILL.md new file mode 100644 index 0000000..ae43811 --- /dev/null +++ b/claude/ja/skills/self-review-changes/SKILL.md @@ -0,0 +1,193 @@ +--- +name: self-review-changes +description: 直前の編集差分 (作業ツリー or staged) を self-review し、正確性 / 整合性 / best practice / memory feedback との整合をチェックする。修正方針を提示し user 承認後に Edit する。「self review して」「review して」「修正箇所ないか確認して」等で使う。 +--- + +# self-review-changes + +直前の編集差分を再走査し、修正候補を洗い出して承認を取ってから直す skill。**4 Phase**: Mindset → Observation → Analysis → Action。 + +> **plugin との使い分け**: `/code-review`・`/simplify` plugin は diff の bug / 簡素化に特化。本 skill はそれに加えて **設定の正確性 / docs 整合 / memory feedback 整合 / Copilot 11-category** まで含む広域 self-review。コードの bug だけなら `/code-review`、変更全体を規約込みで見直すなら本 skill。 + +## 適用条件 + +- 何らかのファイル編集が直前ターンで行われている (commit 前 or 直近 commit 後) +- 修正対象範囲が明確 (`git diff` で見える範囲) + +--- + +## Phase 0: Mindset shift (実装者 → 外部 reviewer) + +self-review の最大バイアスは「自分の intent が見えて actual gap が見えない」こと。 Phase 1 以降に進む前に **「初めてこの code を見た外部 reviewer」** として読む宣言を内面で立てる。 コメントは binding contract と扱い、 「私はこう意図した」「typical input では起きない」「内部 caller だから OK」 は禁句。 wire form / nil / empty / 境界値 / 異常 tokenizer output / cancellation 中 / 不正 spec などを adversarial に想定する。 Phase 2、 特に 2.6-2.9 でこの mindset を maintain。 + +--- + +## Phase 1: Observation (差分把握 + 関連情報取得) + +### 1.1 差分把握 + +```bash +git status +git diff --stat +git diff [--cached] +git show HEAD --stat && git show HEAD # 直近 commit 後 +``` + +### 1.2 全変更 file を Read で並列確認 + +各 file を Read tool で並列読み込み (Bash の `cat` ではなく)。 + +### 1.3 関連 memory を Read + +- `MEMORY.md` index を Read、編集内容に関連する feedback / project entry を判定して中身まで read +- **毎回必読**: + - 本 skill Phase 2.1 / 2.1.1 (Copilot 頻出 11 category + Rebut パターン)、Phase 2.6-2.9 で機械的 check + - CLAUDE.md「変更の作法」(指示外の変更の flag) (Phase 2.3) + - 推測 mapping 禁止 (原文 literal の範囲だけ / Phase 2.5) + +--- + +## Phase 2: Analysis (観点別 check) + +### 2.1 設計観点 — Copilot 頻出 11 category (最重要) + +Copilot review で頻出する 11 category (PR #26 + PR #30 の累計 26 件 review 分析より集約)。 以下に展開する。 + +**PR #26 由来 (5 category)**: + +| # | Category | 重要観点 | +|---|---|---| +| 1 | **Documentation accuracy** | helper / API rename したら関連 docs を全 grep / sample の `_` error drop / doc comment と実装の挙動一致 | +| 2 | **Input validation** | public API (`WithXxx` / `NewXxx` / handler arg) で 0 / 負値 / nil / empty / traversal を check、validation 配置は constructor 末尾 | +| 3 | **Edge case** | wire form parameters 付き (MIME `mime.ParseMediaType`) / `strings.TrimSpace` empty check / batch 全要素 fail / empty 出力 catch | +| 4 | **Concurrency / cancellation** | `ctx.Deadline` 尊重 / goroutine spawn 避け / `ctx.Err()` 都度 check / **ctx を最深部まで伝播** (pre-processing loop も含む) | +| 5 | **Error contract** | sentinel 意味契約 (transient vs permanent) / 4xx は retry なし / boundary error の port-level wrap | + +**PR #30 由来の拡張 (6 category)**: + +| # | Category | 重要観点 | +|---|---|---| +| 6 | **Symmetric constructor defense** | constructor 1 つに guard 追加したら、 `grep "^func New"` で同 PR 内の他 constructor も同じ guard を持つか確認 (Phase 2.6 で機械的 check) | +| 7 | **Doc/impl literal drift** | strong claim ("strictly", "preserves", "Returns X") を含む comment を impl と byte-level で照合 (Phase 2.9 で機械的 check) | +| 8 | **Sentinel-on-error 漏洩** | 依存 interface が 0 / "" / nil / -1 を error 経路で返す契約か godoc 確認、 consumer 側で defense (Phase 2.7 で機械的 check) | +| 9 | **Exported function type contract** | `type X func(...) error` を export、 docstring が "all options X" と claim → custom 実装も同じ contract を満たすか、 factory で wrap (Phase 2.7 で機械的 check) | +| 10 | **Test assertion trivially passes** | repeated fixture content / loose `Contains` / nil-only check が「実装が壊れていても通る」シナリオ (Phase 2.8 で機械的 check) | +| 11 | **Slice OOB / panic guard (test)** | test 内 `slice[len-N:]` の N 直前に `len >= N` guard、 input が短い時の panic | + +### 2.1.1 Rebut パターン (false positive を識別) + +以下は project context 上「適用不可」と判断して reply で resolve する Rebut パターン: + +- **Go 1.22+ loop variable shadow**: `go.mod` の go directive が 1.22+ なら shadow 不要 +- **internal port boundary defense 再提案**: SD で撤回した場合、 PR description を referenced して resolve +- **同じ警告の重複投稿**: 別 test に同 pattern が当てはまらない場合は false positive と明示 + +### 2.2 file type 別の機械的 check + +- **Go (`*.go`)**: `gofmt` / `goimports` / godoc 規約 (`// Package <name>` / `// FuncName ...`) / `fmt.Errorf("...: %w", err)` / context arg 最初 / 不要 export +- **設定 (`.golangci.yaml` / `Makefile` / `*.yaml` / `*.json`)**: 形式バージョン (e.g. golangci-lint v1 vs v2) / regex glob (`gen` 中間一致 vs `^gen/` ルート限定) / インデント +- **Markdown / Docs**: 相対 link / コードブロック言語指定 / 表セル数一貫性 / heading 階層 +- **Shell / Makefile / Dockerfile**: shell injection (`$VAR` クォート) / 危険コマンド (`rm -rf`, `curl | sh`) +- **Proto (`.proto`)**: package / option / field number / 後方互換性 (`buf breaking`) + +### 2.3 spec literal 整合 (spec doc を扱う作業のみ) + +spec (`docs/adr/*.md`, `docs/design/*.md`, OpenAPI, Proto) を扱う場合: + +1. spec 中の literal (型名 / enum / field / file name) を grep で列挙 +2. 実装側の同 surface を grep で列挙 +3. **逸脱項目** を明文化、各逸脱に「理由」「是正方針 (a) 仕様に合わせる / (b) 仕様 update / (c) 別 ADR」を添えて user 提示 + +「prototype だから OK」を暗黙正当化に使わない。明示・承認・記録の 3 点セット (CLAUDE.md「変更の作法」(指示外の変更の flag))。 + +### 2.4 memory 規約整合 + +Phase 1.3 で把握した関連 memory に対して、編集差分が違反していないか check: + +- 用語規約 (memory の用語規約に準拠) +- 文書スタイル (前提節を立てない、prototype 期の緩和) +- コメント言語 (`*.go` / Makefile / proto / shell の英語) +- コメント内に Phase / ticket ID 表記の混入 +- commit message スタイル (短く、変更内容のみ) + +### 2.5 cross-reference / forbidden tokens grep + +変更 file 全体に grep: + +- **一時情報の混入**: `ticket`, `in a later`, `future ticket`, `see (commit|PR) #`, ticket ID 形式 (`#?\d+`, `[A-Z]+-\d+`)。検査 pattern の literal は project の `MEMORY.md` feedback から取得、skill 側 hardcode しない +- **cross-reference 実在確認**: コメント内 section 参照 (`§[0-9]+\.[0-9]+` 等) の引用語彙が原文と一致しているか +- **推測 mapping**: 原文に書かれていない対応関係をコメントで補完していないか + +### 2.6 対称性 audit (Symmetric defense check) + +差分で追加した guard / validation / nil check と同種の対称対象 (`grep "^func New" $(git diff --name-only)` で同 PR 内 constructor 全列挙、 `opts ...Option` を受ける関数も grep) が同じ guard を持つか確認、 欠落があれば flag。 + +例: PR #30 C9 — `ports.NewChunkSpec` に nil opt reject を入れたら `chunker.New` の opts も同じ guard が必要 (1 箇所 fix で他を見逃した教訓)。 + +### 2.7 Interface 契約 trace (Sentinel / Exported type) + +変更 file 内で使った external interface の godoc を再 read、 **戻り値の sentinel ケース** (0 / "" / nil / -1 / 特定 error sentinel) と consumer 側の分岐 (`if x <= X`, `errors.Is(...)`) で flow が一致するか確認。 exported function type を export しているなら、 docstring の "all" / "always" claim が custom 実装にも保たれるか、 factory で normalize しているか check。 + +例: PR #30 C10 — `chunking.Tokenizer.CountTokens` は encoding error 時に 0 を返す、 consumer の `tokens <= MaxTokens` 判定でこの 0 が「fits」と誤判定されないよう defense 必要。 C13 — exported `ChunkSpecOption` の custom 実装が contract を破る経路を factory で wrap。 + +### 2.8 Test adversarial review (Trivial pass の摘出) + +各 test の assertion について 「**実装の挙動を逆にしても pass する mutation はあるか?**」 と問う。 input に repeated content / 同一値 / nil / empty が含まれる場合は特に注意、 `strings.Contains` / `len(got) > 0` / `errors.Is(...)` 系の assertion が trivially 通る経路を探す。 + +例: PR #30 C7 — 80 個の同一 sentence repeat で carry-over の overlap を `strings.Contains` で check → carry-over が壊れても trivially true。 distinct marker (`[文NNN]`) を入れて `HasPrefix` で boundary 検証する形が正解。 + +### 2.9 Doc last-write-wins (コメント update 漏れ摘出) + +実装変更後、 変更 file 内の全コメントを疑って再 read。 strong claim を grep (`grep -nE "(strictly|preserves|guarantees|ensures|always|never|returns|panics)"`)、 各 claim を impl の literal 動作と byte-level で照合 (例: "strictly under X" は `<` か `<=` か / "preserves Y" は upstream が Y を trim していないか / "returns Z on W" は実際の戻り値は何か)。 乖離は doc を impl に合わせるか、 設計意図に基づいて impl を doc に合わせる。 + +例: PR #30 C11/C12 — `carryOverlap` の "kept strictly under overlap" は impl が equality 許容なので「at most overlap」に修正、 `joinUnits` の "preserves original surface text" は SplitSentences が whitespace を trim しているので「reproduces sentence content but not byte-for-byte identical」に修正。 + +--- + +## Phase 3: Action (修正方針 → 承認 → 修正 → 再検証) + +### 3.1 修正候補整理 (table 形式で提示) + +``` +## Self-review 結果 + +| # | file:line | 問題 | 修正方針 | 重要度 | +|---|---|---|---|---| +| 1 | .golangci.yaml:14 | paths 値が部分一致 regex で過剰マッチ | `^gen/` に変更 | 望ましい | +| 2 | cmd/x/main.go:1 | コメント日本語 | 英語に書き換え | 致命的 (memory feedback 違反) | + +## 問題なし +- go.mod (module path / go directive 正しい) +- Makefile (ターゲット動作確認済み) +``` + +重要度: **致命的 / 望ましい / nit** の 3 段階で区別。 + +### 3.2 ユーザー承認 + +「進めて」を待つ。selective approval (一部のみ承認) も受け入れる。 + +### 3.3 修正 Edit (並列) + +承認された候補を Edit tool で並列実施。 + +### 3.4 再検証 + +修正後の副作用 check: +- `make test`, `make lint` 再実行 +- 関連 grep を再走 (修正で別 location に同問題が出ていないか) + +--- + +## 鉄則 + +1. **Phase 0 の mindset shift を skip しない**: 実装者バイアスを意識的に外す。 「自分の intent」 で読まず、 「外部 reviewer として code とコメントだけ」 で判断 +2. **修正方針を先に提示**: 黙って Edit しない +3. **致命的 / 望ましい / nit を区別**: user が取捨選択できるように +4. **memory feedback は関連 entry を中身まで read**: index 行だけで判断しない +5. **Copilot 頻出 11 category を毎回 check** (Phase 2.1): docs accuracy / validation / edge case / cancellation / error contract / 対称性 / doc drift / sentinel 漏洩 / exported func type contract / test trivial pass / slice OOB +6. **対称性 / interface 契約 / test adversarial / doc last-write-wins の 4 機械 check を skip しない** (Phase 2.6-2.9): 真の blindspot は ここに集中する +7. **spec 逸脱は明示・承認・記録**: 暗黙正当化に「prototype だから」を使わない (CLAUDE.md「変更の作法」(指示外の変更の flag)) +8. **推測 mapping 禁止**: コメントの cross-reference / 用語対応は原文 literal の範囲だけ +9. **副作用検証**: 修正後にビルド / テスト / lint を再実行 +10. **隠さない**: 自分が直前ターンで作ったものでも問題があれば指摘する。 「typical input では起きない」「内部 caller だから OK」を理由に check を skip しない diff --git a/claude/ja/skills/sync-k8s-manifest-from-code/SKILL.md b/claude/ja/skills/sync-k8s-manifest-from-code/SKILL.md new file mode 100644 index 0000000..a743489 --- /dev/null +++ b/claude/ja/skills/sync-k8s-manifest-from-code/SKILL.md @@ -0,0 +1,126 @@ +--- +name: sync-k8s-manifest-from-code +description: code repo (Go application) の interface 変更 (config / env / port / probe / resources) を K8s Kustomize manifest repo に伝播する (検出 + 3 環境 Edit + git add まで。commit & push はしない)。「manifest 同期」「config / env 変えたから manifest 直して」等で起動。対象 repo 情報は MEMORY.md の reference memory から取得する。 +allowed-tools: Read, Edit, Grep, Glob, Bash(git status:*, git diff:*, git add:*, ls:*, find:*, cat:*) +--- + +# sync-k8s-manifest-from-code + +application code repo と K8s Kustomize manifest repo が分離されている運用で、**code → manifest 方向** の sync (検出 → Edit → stage) を半自動化する skill。逆方向 (manifest → code) は対象外。 + +## 適用パターン + +以下に該当する project を想定: + +- code repo に YAML config schema (struct) を持ち、**unknown field を起動時 reject** する設計 (= ConfigMap drift が即 crash につながる) +- env (機密 / 非機密) を起動時に required 検証 (= env 不足で `os.Exit(1)`) +- manifest repo は Kustomize `bases/` (= prod SoT) + `overlays/{dev, stg, ...}/` の構成 +- 1 service あたり `config/{app.yaml, config.env, sealed-secret.yaml, secret.yaml}` のような config 分離 + +## 起動時の対象 project 解決 (重要) + +global skill なので **project 固有用語を本 SKILL.md に hardcode しない**。起動時、対象 project の以下を MEMORY.md の reference memory から取得する: + +| 取得項目 | 例 | +|---|---| +| code repo path | `~/.../some-code-repo` | +| manifest repo path + service directory | `~/.../some-mani-repo/<service-dir>` | +| config schema 位置 | `internal/config/config.go` | +| env 定義位置 | `internal/config/secrets.go` | +| config example | `cmd/<binary>/config.example.yaml` | +| binaries (sync 動作) | `[<A>: full sync, <B>: detect-warn]` | +| overlay 構成 | `[bases, overlays/dev, overlays/stg, ...]` | +| 制約 / 順序 | manifest 先行 → code 後追い マスト 等 | + +対応する reference memory が見つからない場合、user に質問して取得 → reference memory として保存する候補を提示する (即書きはしない、user 承認を取る)。 + +参照可能な reference memory の例: +- 例: `[[<service>-sync]]` のような reference memory + +## 検出ロジック (project-agnostic) + +reference memory から得た位置情報を元に、code repo の以下を読む (priority 順): + +1. **config struct (YAML schema)** の field 追加 / 変更 / 削除 + - 反映先: 全 overlay の `config/app.yaml` + - unknown field reject 設計なら **manifest 先行マスト** +2. **env 定義** (required env 追加 / rename / 削除) + - 反映先 (機密): `config/sealed-secret.yaml` (envFrom) の key、または `config/secret.yaml` の key + - 反映先 (非機密): `config/config.env` + - 未設定で起動失敗する設計なら **manifest 先行マスト** +3. **config example** の値 / 構造変更 + - 反映先: 各環境 `config/app.yaml` の対応 key +4. **main.go の listen port / probe endpoint** + - 反映先: `bases/deployment.yaml` (containerPort, livenessProbe, readinessProbe) + `bases/svc.yaml` (port) +5. **resource consumption に効く変更** (heavy lib 追加 / 大 cache / 大 buffer / 並列度変更) + - 反映先候補: `bases/{deployment, hpa, pdb}.yaml` + - **数値は user 判断必須** (skill は flag のみ、自動で決めない) +6. **proto / API listen port** (gRPC / REST) + - 反映先: `bases/{deployment, svc}.yaml` ports +7. **`go.mod` の major dep 追加** (Redis / DB / 外部 API client 等) + - 反映先: NetworkPolicy (無ければ flag のみ) / Secret (新規認証情報があれば key 追加) +8. **detect-warn 指定 binary** (reference memory で指定) の変更 + - 検出のみ、Edit しない、warn 出力 +9. **新規 binary** (`cmd/<new>/` の新規追加) + - 検出のみ、新規 directory 初期化は対象外、warn 出力 + +## 動作フロー + +1. **pre-check** + - manifest repo の working tree が **clean** か `git status` で確認 (dirty なら停止、他作業に混ぜない) + - code repo の対象 ref (default = working tree HEAD) を確認 + - reference memory から対象 project 解決 +2. **detect** + - 上記検出ロジックで差分を抽出 + - 結果を要約 table (どのカテゴリ / どの値 / 反映先候補) で出力 +3. **plan-present (user 承認待ち)** + - 反映先候補を提示 + - 触る overlay の選定が分岐する項目 (resource 等) は **比較形式で user 判断** を仰ぐ + - **manifest 先行 → code 後追いの順序** を必ず user に明示 (env / unknown field 追加時は強調) +4. **edit** + - 承認された変更を manifest repo の working tree に Edit + - 既存 YAML / sealed-secret の indent / 順序 / コメントを保持 + - 既存 overlay override (`bases/` の値を overlay が上書きしているもの) を尊重 +5. **stage** + - 触ったファイルだけ specific path で `git add` (guard-bash.sh hook が `git add -A` / `git add .` を deny) +6. **stop** + - `git diff --staged` の summary を出力 + - skill はここで停止、user が確認 → `/commit-push-branch` で別途 commit & push + +## 制約 / 対象外 + +| 項目 | 扱い | +|---|---| +| commit / push / PR 作成 | **絶対実行しない**。allowed-tools に `git push` / `gh` を含めない | +| kubeseal (SealedSecret 値暗号化) | 対象外。key の追加までを行い、値は user が後で kubeseal | +| 新規 binary 用 directory 初期化 | 対象外。warn のみ | +| 逆方向 sync (manifest → code) | 別 skill 扱い (例: `.upstream-sha` 系の運用) | +| resource 数値の自動決定 | 対象外。heuristic で flag のみ、最終値は user 決定 | + +## 検出漏れの扱い + +検出は heuristic なので、出力末尾に **「拾えなかった可能性のある領域」** を必ず明示する: + +- 新規 volume mount / hostAlias / annotation +- NetworkPolicy / PodSecurityContext / SecurityContext 細部 +- Sidecar / init container 追加 +- annotation ベースの動作変更 (Argo Rollouts, Linkerd, Istio 等) + +"他にも影響箇所があるかも、目視確認推奨" を 1 行で添える。 + +## 失敗時の停止条件 (勝手に進まない) + +以下を検出したら **stop + user 報告**: + +- manifest repo の working tree が dirty +- code repo / manifest repo の path が解決できない (reference memory が無い / path が変わった) +- 反映先ファイルが存在しない (overlay 構成が想定と違う) +- security 観点で要 user 確認の変更 (`Action: "*"` / `privileged` / `hostNetwork` / `hostPID` / public exposure 拡張 / SA に大きい role 付与) +- SealedSecret の **値** (encrypted) 変更が必要 +- detect-warn 指定 binary 単独の変更だが、full sync 対象 binary に影響が及ぶ可能性 + +## 関連 + +- 対象 project の reference memory (例 `[[<service>-sync]]`) — repo path / config 位置 / binary を保持 +- `commit-push-branch` skill — 本 skill 完了後の commit & push (branch 切り + 過去スタイル踏襲) +- `security-review-local` skill — manifest 編集後の security 観点 review (推奨併用) diff --git a/claude/ja/skills/tech-docs-writer/SKILL.md b/claude/ja/skills/tech-docs-writer/SKILL.md new file mode 100644 index 0000000..786c8ac --- /dev/null +++ b/claude/ja/skills/tech-docs-writer/SKILL.md @@ -0,0 +1,122 @@ +--- +name: tech-docs-writer +description: 社内向け技術ドキュメント (ADR / Design Doc / API 仕様 / README / Runbook / システム設計書) を日本語 Markdown で作成し docs/{adr,api,readme,runbook,design}/ に保存する。「設計ドキュメント書いて」「ADR 起こして」「API 仕様書」「Runbook 作成」「アーキテクチャ設計」等で必ず使う。 +--- + +# 技術ドキュメント作成スキル + +社内開発者向けの技術ドキュメントを日本語Markdownで作成する。 +5種別のデファクトスタンダードに沿ってテンプレートを出し分ける。 + +| 種別 | 準拠規格 | 保存先 | +|------|---------|--------| +| ADR / Design Doc (単一の決定) | MADR v3 | `docs/adr/NNNN-<slug>.md` | +| API仕様 | OpenAPI 3.1 の考え方 | `docs/api/<resource>.md` | +| README / ガイド | Diataxis | `docs/readme/<topic>.md` or ルート `README.md` | +| Runbook | Google SRE Playbook | `docs/runbook/<alert-or-topic>.md` | +| システム設計書 | arc42 + C4 + MADR | `docs/design/<system-name>.md` | + +## 1. 起動〜種別判定 + +ユーザー発話やコンテキストから種別を判定する。曖昧な時は作成前に必ず確認する(推測で作らない)。 + +| 入力シグナル | 種別 | +|------------|------| +| 「決定記録」「ADR」「なぜこの技術選定」「トレードオフ」(単一の決定) | ADR / Design Doc | +| 「API仕様」「エンドポイント」「OpenAPI」「REST/gRPC」「リクエスト/レスポンス」 | API仕様 | +| 「README」「セットアップ」「使い方」「クイックスタート」 | README / ガイド | +| 「Runbook」「Playbook」「障害対応」「オンコール」「アラート対応」 | Runbook | +| 「システム設計書」「アーキテクチャ設計」「arc42」「全体設計」「システム全体の設計」 | システム設計書 | + +### ADRとシステム設計書の使い分け + +| 対象 | 該当種別 | +|------|---------| +| 単一の意思決定 (「Xを採用する/しない」) | ADR | +| システム/サブシステム全体を俯瞰する設計 (目的/構成要素/動的振る舞い/NFR/デプロイ) | システム設計書 | + +迷う場合は、「決定事項」中心か「全体像」中心かで判断する。 + +## 2. 必須ヒアリング項目 + +仕様が曖昧な状態では絶対に書き始めない。不足情報は `AskUserQuestion` で確認する。 + +共通: タイトル/対象読者の前提/関連PR・Issue/保存先ディレクトリの存在 + +種別ごとの確認項目は対応するreferenceファイルに記載: + +- ADR / Design Doc → `references/adr.md` +- API仕様 → `references/api.md` +- README / ガイド → `references/readme.md` +- Runbook → `references/runbook.md` +- システム設計書 → `references/system-design.md` + +種別が決まった段階で対応するreferenceファイルをReadすること。各referenceにはテンプレート本文と書き方のコツが入っている。 + +## 3. 入力ソース別の情報抽出 + +「コード/差分から自動生成」モードのときは以下を利用する。 + +### Git diff / PR +```bash +git log --oneline -20 +git diff <base>..<head> +gh pr view <number> --json title,body,commits,files +``` +コミット粒度で決定の経緯を追い、ADRの Context や Considered Options に反映する。 + +### Goコード (godoc/コメント) +- パッケージコメント・エクスポートシンボルのdocコメントを第一情報源とする +- インターフェース定義 → APIの契約 / 設計境界の抽出 +- `//go:generate` ディレクティブは実装の意図を示すヒント + +### Kubernetes マニフェスト / Helm +- `Deployment`/`StatefulSet`のリソース要件、probe設定 → Runbookの前提条件 +- `values.yaml`の公開パラメータ → READMEの設定ガイド +- アラートルール(`PrometheusRule`) → RunbookのSymptom節の出発点 + +## 4. ドキュメント作成フロー + +1. 種別判定 (§1) と必須項目ヒアリング (§2) +2. 対応する `references/<type>.md` をRead +3. コードソースが指定されている場合は §3 で情報抽出 +4. テンプレートに沿って日本語でドラフト作成 +5. **ADR / Design Doc / システム設計書 の場合は draft 完成時に `api-design-review` skill を必ず invoke** して 6 観点 (client 抽象 / ACL 両側 / forward-compat / edge case / SoT 整合 / memory 規約) で網羅性チェック。検出された考慮漏れは draft に反映してから保存。API 仕様 / README / Runbook は同 6 観点を軽量に通過するだけで OK (skill invoke は任意) +6. ファイル保存: `docs/<type>/<filename>.md` へ書き出し + - ADRは連番 `0001-`, `0002-` … (既存の最大番号+1) + - システム設計書は `docs/design/<system-name>.md` + - ディレクトリが未作成なら作成する +7. 図が有用な箇所には Mermaid を積極的に使う (シーケンス図/フローチャート/C4) + +## 5. 文体ルール + +- リスト項目・表セル・概要行は体言止め優先 (動詞で閉じない) + - 悪い例: 「P99レイテンシが100ms以下であること」「監視ダッシュボードを確認する」 + - 良い例: 「P99レイテンシ100ms以下」「監視ダッシュボードの確認」 +- 文章で書く必要がある箇所 (Context/背景/解説) は通常の常体 (〜である/〜する) で可 +- 体言止めと常体を同一箇条書き内で混在させない +- 箇条書きは短く (原則1項目1行・最大2行) + +## 6. 品質チェック(書き終わったら自己レビュー) + +- [ ] タイトルが一読で主旨を示す(「新機能の対応」のような曖昧な表題を避ける) +- [ ] 読み手(社内開発者)が前提知識なしで結論に辿り着ける +- [ ] リスト・表は体言止めで統一 (§5 文体ルール) +- [ ] 推測・未確認事項を「決定事項」のように書いていない (不明点は `TBD` と明記) +- [ ] 機微情報(個人情報/認証情報/内部URL以外の秘匿値)を誤って貼っていない +- [ ] ADR以外でも決めた事と決めていない事を明確に分離している +- [ ] ファイル末尾に更新履歴 or 関連リンクがある +- [ ] ADR / Design Doc / システム設計書 の場合、`api-design-review` skill を通過済(§4 step 5)、検出考慮漏れは反映済 + +## 7. やってはいけない(プロジェクト規範) + +- 仕様が曖昧なまま書き始めない。不足情報は必ずユーザーに聞く。 +- 推測で決めつけない。未確定事項は `TBD` と明記する。 +- 秘密情報を出力しない。パスワード/トークン/個人情報が検出されたら、その箇所は `<REDACTED>` に置換する。 + +## 出力後のユーザー報告 + +作成後は以下を簡潔に報告する: +- 保存したファイルパス (`computer://` リンク) +- 選んだ種別とその根拠 +- 残っている `TBD` の一覧 (あれば) diff --git a/claude/ja/skills/tech-docs-writer/references/adr.md b/claude/ja/skills/tech-docs-writer/references/adr.md new file mode 100644 index 0000000..95b925f --- /dev/null +++ b/claude/ja/skills/tech-docs-writer/references/adr.md @@ -0,0 +1,105 @@ +# ADR / Design Doc テンプレート (MADR v3 準拠) + +ADRは「決定の記録」、Design Docは「提案〜決定のプロセス」を書く。テンプレートは共通で、Design Docの場合は `Status: Proposed` で書き始め、合意後に `Accepted` に更新する。 + +## 必須ヒアリング項目 + +- 決定のタイトル (例: 「ベクトル検索エンジンにValdを採用する」) +- Status (`Proposed` / `Accepted` / `Rejected` / `Deprecated` / `Superseded by [ADR-xxxx]`) +- 意思決定者 (個人名/チーム名) +- 背景 (なぜ今この決定が必要か) +- 検討した選択肢 (最低2つ以上。1つしかないなら"決定"ではない) +- 選定基準 (パフォーマンス/運用コスト/既存スタックとの整合性 など) +- 予期される帰結 (ポジティブ/ネガティブ両方) + +必須項目が埋まらない場合は作成しない。特に「検討した選択肢」が1つしかない場合は、ADRではなく実装メモに分類すべきかを確認する。 + +## テンプレート本文 + +```markdown +# ADR-NNNN: <決定のタイトル> + +- **Status**: <Proposed | Accepted | Rejected | Deprecated | Superseded by ADR-XXXX> +- **Date**: YYYY-MM-DD +- **Deciders**: <意思決定者名/チーム名> +- **Consulted**: <相談した人・チーム (任意)> +- **Informed**: <通知先 (任意)> + +## Context and Problem Statement + +<なぜこの決定が必要か。解きたい課題を1〜3段落で。背景の事実と制約を分けて書く。> + +## Decision Drivers + +体言止めで簡潔に。動詞で閉じない。 + +- <例: P99レイテンシ100ms以下> +- <例: Go SDKの公式提供> +- <例: Kubernetes上でのHA構成の容易さ> + +## Considered Options + +1. **<選択肢A>** +2. **<選択肢B>** +3. **<選択肢C>** + +## Decision Outcome + +**Chosen option**: "<選択肢X>" + +理由: <選定基準に照らしてなぜこれを選んだか。2〜5文> + +### Consequences + +体言止めで簡潔に。 + +- Good: <例: P99レイテンシの大幅改善> +- Good: <例: 既存Goスタックとの統合容易性> +- Bad: <例: 新技術導入に伴う学習コスト> +- Bad: <例: 既存Elasticsearchからの移行作業> + +### Confirmation + +<決定が守られていることをどう検証するか。例: CIのベンチマーク、監視指標、コードレビュー時のチェック項目> + +## Pros and Cons of the Options + +### <選択肢A> + +<概要1文> + +- Good: <...> +- Good: <...> +- Neutral: <...> +- Bad: <...> + +### <選択肢B> + +<同上> + +### <選択肢C> + +<同上> + +## More Information + +<参考リンク、関連ADR、補足資料> +``` + +## 書き方のコツ + +1. Context は"事実"を書く。「良いライブラリがない」ではなく「現行の X は Y の機能をサポートしていない」のように検証可能な形で。 +2. Consequences は両面書く。Good だけ並ぶADRは疑わしい。Bad が思いつかないなら決定の影響範囲を理解できていない可能性がある。 +3. Design Doc 用途では "Open Questions" セクションを `## More Information` の前に追加する。合意取得前の論点を可視化するため。 +4. Mermaid図は Context か Decision Outcome に配置する。Before/After のアーキテクチャ図が特に有効。 + +## ファイル名と連番 + +- パス: `docs/adr/NNNN-<kebab-case-slug>.md` +- 連番: `docs/adr/` 配下の既存最大番号 + 1 (ゼロ埋め4桁) +- slug: タイトルを英語kebab-caseに変換 (例: `0012-adopt-vald-for-vector-search.md`) + +## 参考 + +- MADR 公式: https://adr.github.io/madr/ +- GitHub: https://github.com/adr/madr diff --git a/claude/ja/skills/tech-docs-writer/references/api.md b/claude/ja/skills/tech-docs-writer/references/api.md new file mode 100644 index 0000000..41059a6 --- /dev/null +++ b/claude/ja/skills/tech-docs-writer/references/api.md @@ -0,0 +1,166 @@ +# API仕様書テンプレート (OpenAPI 3.1 の考え方に基づく Markdown) + +OpenAPIのYAML/JSONをそのまま出力するのではなく、人間が読める日本語のMarkdown API仕様書を作る。ただし情報粒度は OpenAPI 3.1 に揃えることで、必要になったときに機械可読形式へ変換しやすくする。 + +## 必須ヒアリング項目 + +- APIの種類 (REST / gRPC / GraphQL) +- サービス名・バージョン +- Base URL (環境ごと: 開発/ステージング/本番) +- 認証方式 (Bearer Token / API Key / mTLS / OAuth2 / なし) +- 対象エンドポイント一覧 (最低1つ) +- 各エンドポイントのパラメータ/リクエスト/レスポンスの形 +- エラー体系 (共通エラーレスポンス形式、ステータスコード方針) +- レート制限の有無 + +不明点が多い場合はまずエンドポイント1本を完成させ、テンプレートを示してから残りを埋めるアプローチを提案する。 + +## テンプレート本文 + +```markdown +# <サービス名> API 仕様書 + +- **バージョン**: v1.0.0 +- **最終更新**: YYYY-MM-DD +- **オーナー**: <チーム名> + +## 概要 + +<このAPIの目的を3〜5文で。解決する課題と主要ユースケース。> + +## 環境 + +| 環境 | Base URL | +|------|----------| +| 開発 | `https://api-dev.example.com` | +| ステージング | `https://api-stg.example.com` | +| 本番 | `https://api.example.com` | + +## 認証 + +<認証方式とトークン取得手順> + +```http +Authorization: Bearer <token> +``` + +## 共通仕様 + +### リクエストヘッダ + +| ヘッダ | 必須 | 説明 | +|--------|------|------| +| `Authorization` | ✓ | 認証トークン | +| `Content-Type` | ✓ | `application/json` | +| `X-Request-ID` | - | トレース用UUID (省略時はサーバで採番) | + +### エラーレスポンス + +全エンドポイント共通: + +```json +{ + "error": { + "code": "INVALID_ARGUMENT", + "message": "人間向けメッセージ", + "details": [] + } +} +``` + +| HTTPステータス | code | 意味 | +|---------------|------|------| +| 400 | `INVALID_ARGUMENT` | リクエストパラメータ不正 | +| 401 | `UNAUTHENTICATED` | 認証情報なし/不正 | +| 403 | `PERMISSION_DENIED` | 権限不足 | +| 404 | `NOT_FOUND` | リソースなし | +| 409 | `CONFLICT` | 競合 | +| 429 | `RESOURCE_EXHAUSTED` | レート制限 | +| 500 | `INTERNAL` | サーバ内部エラー | + +### レート制限 + +- <制限値>。超過時は `429` と `Retry-After` ヘッダを返却。 + +## エンドポイント + +### <リソース名> + +#### `POST /v1/<resource>` + +<1行の要約> + +##### リクエスト + +```http +POST /v1/<resource> HTTP/1.1 +Host: api.example.com +Authorization: Bearer <token> +Content-Type: application/json + +{ + "name": "string", + "count": 0 +} +``` + +| フィールド | 型 | 必須 | 説明 | 制約 | +|-----------|-----|------|------|------| +| `name` | string | ✓ | リソース名 | 1〜64文字 | +| `count` | integer | - | 個数 | 0以上, 既定値0 | + +##### レスポンス (200 OK) + +```json +{ + "id": "res_01HXXXX", + "name": "example", + "count": 0, + "created_at": "2026-04-21T12:00:00Z" +} +``` + +| フィールド | 型 | 説明 | +|-----------|-----|------| +| `id` | string | ULID | +| `name` | string | 入力と同じ | +| `count` | integer | 入力と同じ | +| `created_at` | string (RFC3339) | 作成時刻 | + +##### エラー + +| ステータス | code | 条件 | +|-----------|------|------| +| 400 | `INVALID_ARGUMENT` | `name` が空 / 長さ超過 | +| 409 | `CONFLICT` | 同名のリソースが既に存在 | + +##### 備考 + +<冪等性キーの扱い、リトライ推奨条件、関連エンドポイントなど> + +#### `GET /v1/<resource>/{id}` + +...(同じ構造で記述)... + +## 変更履歴 + +| 日付 | バージョン | 変更内容 | +|------|-----------|---------| +| YYYY-MM-DD | v1.0.0 | 初版 | +``` + +## 書き方のコツ + +1. エラーは"起こりうるもの"に絞って列挙。`500` を全エンドポイントに列挙するのは情報量ゼロ。そのエンドポイント固有のエラー条件を書く。 +2. リクエスト/レスポンスは必ず具体例(JSON)を併記する。スキーマ表だけだと誤読される。 +3. 日時は RFC3339 / タイムゾーンを必ず明示。`2026-04-21T12:00:00Z` のように。 +4. gRPCの場合は `.proto` のservice/method単位で見出しを切り、リクエスト/レスポンスのmessage定義を併記する。 + +## 保存先 + +- REST: `docs/api/<service-name>.md` または `docs/api/<resource>.md` +- 複数エンドポイントがある場合は `docs/api/<service>/<resource>.md` と階層化 + +## 参考 + +- OpenAPI 3.1 仕様: https://spec.openapis.org/oas/v3.1.0 diff --git a/claude/ja/skills/tech-docs-writer/references/readme.md b/claude/ja/skills/tech-docs-writer/references/readme.md new file mode 100644 index 0000000..aeb4048 --- /dev/null +++ b/claude/ja/skills/tech-docs-writer/references/readme.md @@ -0,0 +1,200 @@ +# README / ガイドテンプレート (Diataxis 準拠) + +Diataxis は Tutorial / How-to / Reference / Explanation の4象限で文書を分類する。 +READMEはこの4種を1ファイルに詰め込まない。リポジトリ直下 `README.md` は「入口」に徹し、詳細は `docs/readme/` 配下に分離する。 + +## 種別判定 + +| 何を書きたいか | 象限 | 想定読者 | +|---------------|------|---------| +| 初めて触る人が動くところまで | Tutorial | 新規オンボーディング | +| 特定の課題の解き方 | How-to | 既にプロジェクトを知っている開発者 | +| 設定項目・コマンド一覧 | Reference | 実装中に辞書的に引く開発者 | +| 背景・アーキテクチャの説明 | Explanation | アーキテクチャを理解したい人 | + +1つのドキュメントに2種以上を混在させない。「チュートリアルの途中で突然Reference表が挟まる」構成は読みにくい。 + +## 必須ヒアリング項目 + +- 対象読者 (新規オンボーディング / 既存開発者 / 運用担当) +- どの象限を書くか +- プロジェクト名・リポジトリURL +- 前提環境 (言語バージョン / OS / 依存サービス) +- Tutorial/How-to の場合: 達成したい最終状態 (動くアプリ / 出力されるファイル) +- Reference の場合: 列挙する対象 (CLIコマンド / 設定キー / 環境変数) + +## Root README.md のテンプレート (入口用) + +```markdown +# <プロジェクト名> + +<1〜2文でプロジェクトの目的> + +[![CI](<badge-url>)](<link>) [![License](<badge>)](<link>) + +## これは何 + +<もう少し詳しい説明。3〜5文。何を解決するか、何を解決しないか。> + +## クイックスタート + +```bash +git clone <repo> +cd <repo> +make setup +make run +``` + +詳細は [セットアップガイド](docs/readme/getting-started.md) を参照。 + +## ドキュメント + +- [セットアップガイド (Tutorial)](docs/readme/getting-started.md) +- [よくあるタスク (How-to)](docs/readme/how-to.md) +- [設定リファレンス (Reference)](docs/readme/configuration.md) +- [アーキテクチャ解説 (Explanation)](docs/readme/architecture.md) +- [API仕様](docs/api/) +- [運用Runbook](docs/runbook/) + +## 開発者向け + +- コントリビュートガイド: [CONTRIBUTING.md](CONTRIBUTING.md) +- 変更履歴: [CHANGELOG.md](CHANGELOG.md) + +## ライセンス + +<license-name> +``` + +## Tutorial テンプレート (初心者向けの学習) + +```markdown +# <プロジェクト名> セットアップガイド + +このチュートリアルを最後まで進めると、ローカルで <X> が動く状態になります。 +所要時間の目安: <N>分。 + +## 前提 + +- <OS/言語バージョン/必要ツール> + +## ステップ1: <動詞で始める見出し> + +<コマンド> + +```bash +$ command +``` + +**確認**: <期待される出力や状態を明示> + +## ステップ2: ... + +... + +## 完成 + +<何が達成できたか、次に何を読めばよいか> + +## うまくいかない場合 + +<よくあるつまずきポイント2〜3つ> +``` + +## How-to テンプレート (特定タスクの手順) + +```markdown +# <動詞ではじまるタイトル: 例「新しいAPIエンドポイントを追加する」> + +## 前提 + +- <既に満たされていること> + +## 手順 + +1. <ステップ> +2. <ステップ> +3. <ステップ> + +## 検証 + +<変更が正しく入ったかの確認方法> + +## 関連 + +- <関連するHow-to、参照ドキュメント> +``` + +## Reference テンプレート (事実の列挙) + +```markdown +# <対象の名前> リファレンス + +## 設定項目 + +| キー | 型 | 既定値 | 説明 | +|------|-----|-------|------| +| `FOO` | string | `""` | <役割> | + +## コマンド + +### `<command> <subcommand>` + +<1行要約> + +**使い方**: +```bash +<command> <subcommand> [flags] +``` + +**フラグ**: + +| フラグ | 型 | 既定 | 説明 | +|--------|-----|-----|------| +| `--bar` | int | 10 | <説明> | +``` + +## Explanation テンプレート (理解のための解説) + +```markdown +# <概念/アーキテクチャのタイトル> + +## 背景 + +<なぜこの設計/概念が存在するか> + +## 全体像 + +<Mermaid C4図 or アーキテクチャ図> + +## 主要コンポーネント + +### <コンポーネントA> + +<責務と他コンポーネントとの関係> + +## 設計上の選択 + +<採用したパターン・採用しなかったパターンと理由> +<関連ADRへのリンクを張る> + +## 参考 + +- <論文、書籍、ブログ> +``` + +## 書き方のコツ + +1. Tutorial は"読んで覚える"ではなく"手を動かす"。段階ごとに確認ポイントを置く。 +2. How-to は動詞ではじめるタイトル。「データベースに接続するには」ではなく「データベースに接続する」。 +3. Reference は網羅性優先。説明を足しすぎると Explanation に寄って引きにくくなる。 +4. Explanation は図を必ず入れる。テキストだけだと伝わらない。 + +## 保存先 + +- リポジトリ直下 `README.md` (入口) +- 詳細: `docs/readme/<topic>.md` (Tutorial/How-to/Reference/Explanation ごとに分割) + +## 参考 + +- Diataxis 公式: https://diataxis.fr/ diff --git a/claude/ja/skills/tech-docs-writer/references/runbook.md b/claude/ja/skills/tech-docs-writer/references/runbook.md new file mode 100644 index 0000000..f745ccc --- /dev/null +++ b/claude/ja/skills/tech-docs-writer/references/runbook.md @@ -0,0 +1,141 @@ +# Runbook テンプレート (Google SRE Playbook スタイル) + +Runbookはオンコール当番が深夜3時に読む前提で書く。冗長な背景説明は不要で、判断と操作がすぐ取れる構造にする。 + +## 必須ヒアリング項目 + +- アラート名 / 症状 (Symptom) +- 重大度 (SEV / P1〜P5 など社内基準) +- 影響範囲 (ユーザー影響 / 内部影響のみ) +- 確認すべきダッシュボード / ログクエリのURL +- 暫定対応 (Mitigation) の候補 +- 根本対応 (Resolution) のオーナー +- エスカレーション先 (チーム/個人/Slack channel) + +Mitigation が1つも決まっていない状態でRunbookは書かない。「とりあえず再起動」も立派なMitigationなので、ヒアリングで必ず引き出す。 + +## テンプレート本文 + +```markdown +# Runbook: <アラート名 or 症状> + +- **重大度**: SEV<N> / P<N> +- **オーナーチーム**: <team> +- **最終更新**: YYYY-MM-DD +- **関連アラート**: `<AlertmanagerのalertnameまたはダッシュボードURL>` + +## Symptom (症状) + +<何が見えたらこのRunbookを開くか。アラート文・ダッシュボード上の見え方・ユーザー報告の典型文を具体的に。> + +例: +- Alertmanager: `HighErrorRate` が firing +- Grafana `<url>` で 5xx率 が 5%超 + +## Impact (影響) + +- **ユーザー影響**: <例: APIレスポンスが失敗する。影響対象は全ユーザー/特定機能> +- **内部影響**: <例: 非同期ジョブが遅延> +- **SLO影響**: <例: 可用性SLOに対して X%消費する見込み> + +## Diagnosis (診断手順) + +原因の切り分けをチェックリスト形式で。上から順に実施。 + +1. **依存サービスのステータス確認** + - コマンド: `kubectl get pods -n <ns>` + - 見るもの: `Running` 以外のPodがないか +2. **直近デプロイの有無** + - `kubectl rollout history deployment/<name> -n <ns>` + - 直前30分以内のデプロイがあれば §Mitigation の「ロールバック」を優先 +3. **外部依存の障害** + - <依存サービスのステータスページURL> +4. **リソース逼迫** + - ダッシュボード: <url> + - CPU/メモリ/コネクション数を確認 + +## Mitigation (暫定対応) + +原因が切り分けできていなくても、ユーザー影響を止めるための手を列挙。**副作用を明記**。 + +### A. ロールバック (直近デプロイが原因の場合) + +```bash +kubectl rollout undo deployment/<name> -n <ns> +``` + +- 所要時間: 2〜3分 +- 副作用: 新機能は一時的に使えなくなる + +### B. 対象Podの再起動 + +```bash +kubectl rollout restart deployment/<name> -n <ns> +``` + +- 所要時間: 1〜5分 +- 副作用: 処理中リクエストが中断される可能性 + +### C. トラフィックを別リージョンに寄せる + +```bash +<コマンド or 手順> +``` + +- 所要時間: 1分 +- 副作用: レイテンシが増える + +## Resolution (根本対応) + +<恒久対応の方針。Mitigation後にオーナーチームで行う作業。> +<該当Issue/PRがあればリンクを張る。> + +## Verification (復旧確認) + +- [ ] Grafana `<url>` のエラー率が通常範囲に戻っている (5分間継続) +- [ ] アラートが resolved になっている +- [ ] 関連するカナリア/合成監視が成功している + +## Escalation (エスカレーション) + +- **Primary**: `#team-<name>` Slack (<slack-url>) +- **Secondary**: `#oncall-<service>` に `@here` で投稿 +- **30分経過しても収束しない場合**: SREチーム `#sre-oncall` + +## Postmortem + +- SEV2以上の場合、収束後24時間以内に postmortem を開始する +- テンプレート: <postmortem-template-url> + +## 変更履歴 + +| 日付 | 変更内容 | 変更者 | +|------|---------|--------| +| YYYY-MM-DD | 初版 | <name> | +``` + +## 書き方のコツ + +1. Diagnosis は"上から順にやれば切り分く"順序で書く。思考の遠回りを作らない。 +2. コマンドは実行可能な形で貼る。`<name>` プレースホルダがあってもよいが、何を入れるかは直前に書く。 +3. Mitigation には副作用を必ず書く。副作用不明な操作を深夜に打たせない。 +4. Resolution と Mitigation を混同しない。Mitigation は止血、Resolution は根治。 +5. 更新を怠らない。Alertmanagerのルール名変更、K8s namespace変更などで陳腐化する。アラート発火のたびに差分を反映するくらいで丁度よい。 + +## アラートルールからの自動起こし + +`PrometheusRule` などから出発する場合、以下を `Symptom` と `Diagnosis` の初期案にする: + +- `alert:` 値 → Runbookタイトル +- `expr:` → Diagnosis の「メトリクス確認」ステップの基礎 +- `annotations.summary` / `description` → Symptom 本文 +- `annotations.runbook_url` が空なら、このRunbookのURLを埋める提案を行う + +## 保存先 + +- `docs/runbook/<alertname-or-topic>.md` +- アラートルールと1対1対応が望ましい (大きくなってきたらグループ化) + +## 参考 + +- Google SRE Workbook - On-Call: https://sre.google/workbook/on-call/ diff --git a/claude/ja/skills/tech-docs-writer/references/system-design.md b/claude/ja/skills/tech-docs-writer/references/system-design.md new file mode 100644 index 0000000..432791b --- /dev/null +++ b/claude/ja/skills/tech-docs-writer/references/system-design.md @@ -0,0 +1,292 @@ +# システム設計ドキュメント (arc42 + C4 + MADR) + +システム/サブシステム全体の設計を記述する。単一の意思決定を書く場合は ADR (`adr.md`) を使うこと。 + +デファクト組み合わせ: +- 骨格: arc42 (12セクションテンプレート) +- 図: C4 Model (Context/Container/Component/Deployment の4層) +- 意思決定: MADR (§9 に埋め込み or ADRファイルへのリンク) + +## 必須ヒアリング項目 + +- システム名 +- 目的 (解決する課題を1〜3文で) +- 主要ステークホルダー (開発チーム/運用/Biz/外部ユーザー) +- 主要な品質要件 (NFR) 3つ以上 — 測定可能な値で (例: P99レイテンシ100ms) +- 主要な制約 (既存インフラ/予算/期日/規制) +- 既に決まっている技術スタック (あれば) + +品質要件が測定可能な値で出てこない場合は書き始めない。「高速」「安定」だけでは検証できない。 + +## テンプレート本文 + +```markdown +# <システム名> アーキテクチャ設計書 + +- **バージョン**: v0.1 (Draft) / v1.0 (Approved) +- **最終更新**: YYYY-MM-DD +- **オーナー**: <チーム名> +- **Status**: Draft / In Review / Approved / Superseded + +## 1. Introduction and Goals (目的と目標) + +### 1.1 背景・目的・目標 + +簡潔に。各項目1〜2文に収める。長文の説明は §3 や §4 に回す。 + +#### 背景(課題) + +<現状の問題を事実ベースで1〜2文。例: バッチ集計が1時間遅延し、レコメンドの鮮度が要件を満たさない。> + +#### 目的 + +<このシステムで達成することを1文。例: イベント発生からランキング反映までを数秒以内にする。> + +#### 目標 + +測定可能な指標を3個まで。詳細は §10 品質要件へ。 + +1. <例: イベント投入→ランキング反映 P99 2秒以内> +2. <例: 99.95% 可用性> +3. <例: 10万イベント/秒までスケール> + +### 1.2 主要品質目標 (Top 3-5) + +| 優先順位 | 品質特性 | 具体的な目標値 | +|---------|---------|--------------| +| 1 | パフォーマンス | P99レイテンシ 100ms以下 | +| 2 | 可用性 | 99.9% (月次停止 43分以内) | +| 3 | 拡張性 | 書き込み 10k RPS まで水平拡張 | + +### 1.3 ステークホルダー + +| 役割 | 関心事 | 連絡先 | +|------|-------|--------| +| <例: AI基盤チーム> | 開発・運用オーナー | #team-ai-platform | +| <例: SRE> | 運用引き継ぎ | #sre | +| <例: Biz> | リリース時期 | <PdM名> | + +## 2. Architecture Constraints (制約) + +| 種類 | 制約 | 理由 | +|------|------|------| +| 技術 | Go 1.22+ で実装 | 既存スタックと統一 | +| 運用 | Kubernetesで稼働 | 既存基盤を利用 | +| 規制 | 個人情報を国外保存しない | 法務要件 | +| 予算 | <具体値> | | + +## 3. System Scope and Context (範囲とコンテキスト) + +<C4 Level 1: System Context Diagram を Mermaid で> + +```mermaid +flowchart LR + User([User]) -->|HTTPS| Sys[<This System>] + Sys -->|gRPC| UpstreamA[Upstream Service A] + Sys -->|REST| UpstreamB[External SaaS B] + Sys <-->|events| MQ[Message Broker] +``` + +### 3.1 対象(In Scope) + +- <このシステムが責任を持つこと> + +### 3.2 対象外(Out of Scope) + +- <明示的に対象外のもの。あとで揉めがちな箇所> + +### 3.3 外部インタフェース + +| 相手 | プロトコル | 方向 | 備考 | +|------|----------|------|------| +| Upstream A | gRPC | Sys→A | 認証: mTLS | +| SaaS B | REST | Sys→B | Rate Limit 100 RPM | + +## 4. Solution Strategy (解決方針) + +<採用するパターンと技術選択の要約。詳細な決定は §9 へ。> + +- **アーキテクチャパターン**: 例) イベント駆動 + CQRS +- **主要な技術選択**: 例) Go + gRPC + PostgreSQL + Kafka + Kubernetes +- **主要な設計原則**: 例) 書き込み経路はIdempotent / 読み取りはリードレプリカから + +## 5. Building Block View (構成要素) + +<C4 Level 2: Container Diagram を Mermaid で> + +```mermaid +flowchart TB + subgraph System[<This System>] + API[API Gateway] + Worker[Async Worker] + DB[(PostgreSQL)] + Cache[(Redis)] + end + Client --> API + API --> DB + API --> Cache + API -->|enqueue| MQ[Kafka] + MQ --> Worker + Worker --> DB +``` + +### 5.1 <Container A: API Gateway> + +- **責務**: <1〜2文> +- **技術**: Go, gin +- **インタフェース**: REST `/v1/*` +- **依存**: PostgreSQL, Redis, Kafka + +### 5.2 <Container B: Async Worker> + +<同上> + +### 5.3 Component View (必要な場合) + +<C4 Level 3: 主要Containerの内部分解> + +## 6. Runtime View (動的振る舞い) + +<主要シナリオのシーケンス図。ハッピーパス + エラーパス 各1つ以上。> + +### 6.1 <シナリオ: ユーザー登録> + +```mermaid +sequenceDiagram + participant U as User + participant A as API + participant D as DB + participant Q as Kafka + U->>A: POST /v1/users + A->>D: INSERT user + D-->>A: OK + A->>Q: publish user.created + A-->>U: 201 Created +``` + +### 6.2 <シナリオ: 障害時> + +<例: upstreamタイムアウト時の振る舞い> + +## 7. Deployment View (デプロイ) + +<C4 Level 4: Deployment Diagram> + +```mermaid +flowchart TB + subgraph Region[AWS ap-northeast-1] + subgraph EKS[EKS Cluster] + Pods[API Pods x3] + WPods[Worker Pods x2] + end + RDS[(RDS PostgreSQL Multi-AZ)] + EC[(ElastiCache Redis)] + MSK[MSK Kafka] + end + Pods --> RDS + Pods --> EC + Pods --> MSK + MSK --> WPods + WPods --> RDS +``` + +- **リージョン**: ap-northeast-1 (DR計画は §11) +- **インフラ**: EKS + RDS Multi-AZ + MSK +- **CI/CD**: GitHub Actions → ArgoCD + +## 8. Crosscutting Concepts (横断的関心事) + +### 8.1 認証・認可 + +<例: JWT + OAuth2 Authorization Code Flow> + +### 8.2 ロギング・可観測性 + +<例: OpenTelemetry → Grafana Stack (Loki/Tempo/Prometheus)> + +### 8.3 エラーハンドリング + +<例: 共通エラー型、リトライポリシー、回路遮断> + +### 8.4 データ整合性 + +<例: Outbox Pattern で DB↔Kafka を atomic に> + +### 8.5 セキュリティ + +<例: 機密は AWS Secrets Manager、コードには埋め込まない> + +## 9. Architecture Decisions (意思決定ログ) + +本システムに関連する重要な設計判断は ADR として別管理: + +| ADR | タイトル | Status | +|-----|---------|--------| +| [ADR-0005](../adr/0005-adopt-cqrs.md) | CQRSパターンの採用 | Accepted | +| [ADR-0012](../adr/0012-adopt-vald-for-vector-search.md) | ベクトル検索にValdを採用 | Accepted | + +<新しい決定が必要になったら、このシステム設計書を更新する前に ADR を追加すること。> + +## 10. Quality Requirements (品質要件) + +品質シナリオ形式で**測定可能な形**で記述する。 + +### 10.1 パフォーマンス + +| シナリオID | 刺激 | 環境 | 応答 | 応答尺度 | +|-----------|-----|------|------|---------| +| P-1 | クライアントがAPIを呼ぶ | 通常負荷 (1000 RPS) | 応答を返す | P99 100ms以下 | +| P-2 | 同上 | ピーク負荷 (5000 RPS) | 応答を返す | P99 300ms以下 | + +### 10.2 可用性 + +| シナリオID | 刺激 | 環境 | 応答 | 応答尺度 | +|-----------|-----|------|------|---------| +| A-1 | 1台のPodが停止 | 本番運用中 | サービス継続 | エラー率1%未満 | +| A-2 | AZが1つ停止 | 本番運用中 | サービス継続 | 30秒以内にfailover | + +### 10.3 スケーラビリティ / セキュリティ / その他 + +<以下同様に> + +## 11. Risks and Technical Debt (リスクと技術的負債) + +| ID | 種類 | 内容 | 影響 | 対処方針 | +|----|------|------|------|---------| +| R-1 | リスク | 外部SaaSの依存(B) | SaaS障害でサービス停止 | §8.3のCircuit Breakerで部分縮退 | +| D-1 | 技術的負債 | 旧認証基盤をv0で残す | 2系統運用 | v1.3で廃止予定 | + +## 12. Glossary (用語集) + +| 用語 | 定義 | +|------|------| +| <用語A> | <ドメイン固有の定義> | +``` + +## 書き方のコツ + +1. §10 品質要件は必ず測定可能な値で書く。「十分なパフォーマンス」は品質要件ではない。シナリオID (P-1, A-1) を振って、後から SLO/SLI と紐付けられるようにする。 +2. §9 は ADR へのリンク集に徹する。ここで設計を議論しない。議論は別ADRで。 +3. 図は C4 の粒度を守る。Container図にクラス名を書かない(Component/Code図の仕事)。Mermaidなら `flowchart` と `sequenceDiagram` を使い分ける。 +4. §5 Building Block View は階層を守る。Level 2(Container) → Level 3(Component) の順。いきなりクラス図から始めない。 +5. Draft → In Review → Approved の Status 遷移を明記し、レビュー前/後を区別する。 + +## ADRとの棲み分け + +| 書きたいもの | どこに | +|-------------|--------| +| 「なぜ X を選んだか」 (単一の決定) | ADR (`docs/adr/NNNN-...md`) | +| 「このシステム全体がどう動くか」 | システム設計書 (このテンプレ) | +| 「システム設計書に影響する新しい決定」 | まず ADR を書き、§9 にリンクして設計書を更新 | + +## 保存先 + +- `docs/design/<system-name>.md` +- サブシステムがあれば `docs/design/<system-name>/<subsystem>.md` + +## 参考 + +- arc42 公式: https://arc42.org/ +- arc42 テンプレート: https://github.com/arc42/arc42-template +- C4 Model: https://c4model.com/ +- MADR: https://adr.github.io/madr/ diff --git a/claude/ja/skills/work-intake/SKILL.md b/claude/ja/skills/work-intake/SKILL.md new file mode 100644 index 0000000..eed8811 --- /dev/null +++ b/claude/ja/skills/work-intake/SKILL.md @@ -0,0 +1,60 @@ +--- +name: work-intake +description: Notion の ready ticket を拾い、spec contract (rules/spec-contract.md) を検証して正規化 work item を返す Dev loop の入口 skill。「次の work 拾って」「ready ticket ある?」「work-intake」等で使う。contract を満たさない ticket は skip + 理由コメント。watch 対象 DB は memory reference から取得。 +--- + +# work-intake + +Dev loop の入口。ready な ticket を 1 件選び、dev-cycle が自律実装できる work item に正規化する。 +**ticket 内容の修正・補完はしない** (直すのは人間 + write-spec)。 + +## 手順 + +### Step 1: 設定解決 (hardcode 禁止) + +- `MEMORY.md` の reference memory から watch 対象 Notion DB / ready flag / 「進行中」status の表現を取得する +- 見つからない場合は「memory reference 未登録」と明示して停止する (user に登録を依頼) +- option: 引数に ticket URL が渡された場合は Step 2 の列挙を skip し、その ticket だけを対象にする + +### Step 2: ready ticket 列挙 + +- `references/notion-adapter.md` の手順で ready 状態の ticket を列挙する +- 0 件なら「該当なし」と明示して正常終了する + +### Step 3: contract 検証 (全項目判定を出力) + +- 各 ticket に `~/.claude/rules/spec-contract.md` の検証 checklist を適用する +- **全項目の判定 (満たす / 満たさない + 理由) を必ず出力する** (黙った skip 禁止) +- 満たさない ticket: skip し、不備理由を ticket にコメントする (本文は編集しない) + +### Step 4: 選択と状態遷移 + +- contract 充足 ticket から優先度順 (同優先度は古い順) に **1 件**選択する +- 選択 ticket の status を「進行中」相当に更新する (二度拾い防止。状態は source 側に置き、本 skill は状態を持たない) + +### Step 5: work item 出力 + +``` +## work item +- source: notion +- id: <ticket id> +- url: <ticket URL> +- 対象 repo: <spec メタから> +- 優先度: <spec メタから> +- 残り ready 件数: <N> (参考) + +### spec +<目的 / スコープ・non-goals / 設計本体 / DoD / 制約 / メタ の全セクション> +``` + +## 障害時 + +- Notion MCP 不達 / auth 切れ: retry せず「Notion に到達できない」を明示して異常終了する (通知は呼び出し側の責務) + +## 鉄則 + +1. ticket 内容の修正・補完をしない (non-goal) +2. contract 検証は全項目判定を出力する (黙った skip 禁止) +3. write は status 更新 + コメント追加のみ (ticket 本文の編集はしない) +4. ticket 本文を log / insights に丸ごと転記しない (参照は id / URL で) +5. DB 情報を skill に hardcode しない (memory reference が SoT) diff --git a/claude/ja/skills/work-intake/references/notion-adapter.md b/claude/ja/skills/work-intake/references/notion-adapter.md new file mode 100644 index 0000000..efe3e18 --- /dev/null +++ b/claude/ja/skills/work-intake/references/notion-adapter.md @@ -0,0 +1,31 @@ +# Notion adapter (work-intake Step 2-4 の操作手順) + +将来 source を追加する場合は本ファイルと並べて `references/<source>-adapter.md` を作る (SKILL.md 側は変更不要の想定)。 + +## ready ticket の列挙 (Step 2) + +1. memory reference の DB 情報 (DB 名 / URL / ready flag の表現) を使う +2. Notion MCP の検索系 tool で対象 DB の ticket を取得し、ready flag に合致するものに絞り込む + - tool 候補: `notion-search` (対象 DB を指定して検索) / `notion-query-database-view` / `notion-fetch` (個別取得) +3. 各候補の page 本文を `notion-fetch` で取得し、spec セクション (目的 / スコープ・non-goals / 設計本体 / DoD / 制約 / メタ) を読み取る + +## status 更新 (Step 4) + +- `notion-update-page` で status プロパティを memory reference の「進行中」値に変更する +- 変更直前に現在値が ready であることを再確認する (楽観ロック代わり。違ったら他プロセスが拾ったとみなして次候補へ) + +## skip コメント (Step 3) + +- `notion-create-comment` で不備理由を投稿する。形式: + +``` +work-intake: spec contract 未充足のため skip +- <checklist の不備項目のみ列挙 (項目名 + 理由)> +参照: rules/spec-contract.md +``` + +## 注意 + +- ticket 本文の編集 (`notion-update-page` での本文変更) はしない。触って良いのは status プロパティとコメントのみ +- 優先度プロパティが未設定の ticket は最低優先度として扱う +- 列挙された ready ticket は**全件に contract 検証を適用する** (不備 ticket への skip コメントが漏れないように)。選択は充足 ticket のうち優先度順の 1 件 diff --git a/claude/ja/skills/write-spec/SKILL.md b/claude/ja/skills/write-spec/SKILL.md new file mode 100644 index 0000000..682450a --- /dev/null +++ b/claude/ja/skills/write-spec/SKILL.md @@ -0,0 +1,59 @@ +--- +name: write-spec +description: 人間の設計 draft を spec contract (rules/spec-contract.md) を満たす spec に仕上げる補佐 skill。設計判断はせず、穴の尋問・整形・contract 検証のみ行う。「spec にして」「spec 書くの手伝って」「設計を spec 化」等で使う。ready flag を立てるのは人間。 +--- + +# write-spec + +人間が主導して書いた設計を、agent (dev-cycle) が自律実装できる spec に仕上げる。 +**設計判断はしない。実装計画も作らない。** 穴を見つけて聞き、埋まったら整形して検証するだけ。 + +## 入力 + +会話中の設計説明 / ローカル file / Notion page URL のいずれか。 + +## 手順 + +### Step 1: contract を読む + +`~/.claude/rules/spec-contract.md` を Read する (必須セクション / 検証 checklist / ready の意味)。 + +### Step 2: draft を template にマップ + +入力を必須セクション 1-6 に振り分け、埋まっていない・曖昧なセクションを列挙して user に提示する。 + +### Step 3: 尋問 (1 問ずつ) + +`references/interrogation.md` の観点で、埋まっていない箇所を AskUserQuestion で 1 問ずつ確認する。 + +- 回答を勝手に補完しない。選択肢を出す場合も「推奨」は根拠付きで 1 つまで +- 人間が「これで良い」と確定した設計に異論を続けない (懸念は 1 回 flag して従う) +- 提案 (DoD 案 / 制約案 等) への確認を取る時は、**提案内容を質問文の中に再掲する** (直前メッセージへの参照だけだと user に見えないことがある) +- **未設計領域への escape hatch**: 穴埋めではなくセクション丸ごと設計が存在しないと判明したら、`grill-me` skill に委譲して深掘りし、結果を持って本 Step に戻る。委譲時は args で scope を縛る (例: 「contract セクション 3 (設計本体) の障害時挙動に限定」)。open-ended に走らせない +- 全観点の実施状況 (実施 / skip + 理由) を記録し Step 5 の出力に含める + +### Step 4: api-design-review の発火判定 (機械的) + +設計本体に以下のいずれかが含まれる場合、`api-design-review` skill を invoke し、検出された考慮漏れを Step 3 の尋問に追加する: + +- 新規 / 変更される API endpoint・RPC・event schema・enum・公開 interface (**repo 外に公開されるものが対象**。harness 内部の skill 間契約は対象外 — spec の設計本体で扱う) +- ACL / 権限モデルの変更 + +含まれない場合は skip し、理由を Step 5 の出力に明記する。 + +### Step 5: 整形と検証 + +spec を markdown で組み立て、contract の検証 checklist **全項目の判定を出力する** (満たす / 満たさない + 理由)。満たさない項目が残る場合は Step 3 に戻る。 + +### Step 6: 出力 + +- 完成 spec を提示する (Notion に貼れる markdown) +- Notion への書き込みは user の明示指示があった場合のみ (MCP 経由) +- **ready flag は立てない** — 「ready にするのは人間」と明記して終了する + +## 鉄則 + +1. 設計判断・実装計画の代行をしない (補佐に徹する) +2. 検証 checklist と尋問観点は全項目の判定 / 実施状況を必ず出力する (黙った skip 禁止) +3. 尋問は 1 問ずつ (multiple choice 優先) +4. project 固有情報 (Notion DB 等) は hardcode せず MEMORY.md の reference から取得する diff --git a/claude/ja/skills/write-spec/references/interrogation.md b/claude/ja/skills/write-spec/references/interrogation.md new file mode 100644 index 0000000..6de18d8 --- /dev/null +++ b/claude/ja/skills/write-spec/references/interrogation.md @@ -0,0 +1,49 @@ +# 尋問観点 checklist (write-spec Step 3) + +各観点の発火条件を満たす場合に該当質問を 1 問ずつ AskUserQuestion で確認する。 +全観点の実施状況 (実施 / skip + 理由) を Step 5 の検証出力に含める (黙った skip 禁止)。 + +## 常時 on (全 spec で実施) + +### 1. 目的の具体性 + +- 「この変更が入った翌日、何がどう変わっていれば成功?」 +- 目的が手段 (「X を導入する」) で書かれていたら、その先の価値を聞く + +### 2. スコープ境界 + +- 設計本体に登場する周辺要素を列挙し「Y は今回触る? 触らない?」を確認する +- non-goals が空なら「やりたくなりそうだが今回は我慢すること」を最低 1 つ引き出す + +### 3. DoD の機械検証可能性 + +- 各 DoD 項目に「どのコマンド / 手順で検証する?」を紐付ける +- 「〜が正しく動く」→「どの入力で何が返れば正しい?」に具体化する + +### 4. 制約の明示 + +- 新規依存の追加は可か (可なら条件) +- performance 目標 / 劣化許容はあるか (なければ「なし」と明記してもらう) +- secret / PII を扱うか + +## 条件付き (設計本体の内容で発火) + +### 5. 外部 interface + +- 発火条件: API / RPC / event schema / enum / 公開 interface の新規・変更を含む +- → SKILL.md Step 4 で api-design-review に委譲する (ここでは重複尋問しない) + +### 6. データ / 状態 + +- 発火条件: 永続化 / migration / データモデル変更を含む +- 後方互換: 既存データはどうなる? rollback 手順は? + +### 7. 障害時挙動 + +- 発火条件: 外部サービス呼び出し / 非同期処理を含む +- 失敗時に retry するか、諦めるか、失敗は誰にどう見えるか + +### 8. 運用 + +- 発火条件: 常駐 process / 定期実行 / 新規 service を含む +- 動いていることをどう観測するか (log / metric / alert) diff --git a/claude/rules/performance.md b/claude/rules/performance.md new file mode 100644 index 0000000..b44f3d5 --- /dev/null +++ b/claude/rules/performance.md @@ -0,0 +1,61 @@ +--- +paths: + - "**/*.{go,rs,py,rb,ts,tsx,js,jsx,java,kt,php,sql,c,cc,cpp,h,hpp,proto}" +--- + +# Performance 規約 (言語非依存) + +「とりあえず動く」で止めず、計算量・メモリ・I/O・レイテンシを設計時点で意識する。明らかに高コストな選択を default にしない。言語固有イディオム (`errgroup` / `asyncio` 等) は各言語 skill 側。 + +## 計算量 / アルゴリズム + +- ループ内のループ / ループ内 I/O / ループ内 allocation は意識的に flag。N が小さくて問題ないなら根拠を明示 +- O(N²) 以上を書く場合、入力サイズの上限と根拠を 1 行残す +- 繰り返す線形検索は hash set / map 化を検討。既存コードの計算量を悪化させない +- データ構造は順序 / 一意性 / lookup・insert 頻度で選ぶ + +## メモリ / allocation + +- 大きな collection / buffer は容量ヒント付きで事前確保 +- 不要な copy / 型変換 / serialize ↔ deserialize を入れない +- streaming で処理できるもの (file / HTTP body / DB query / log) を全部読み込まない。巨大データはチャンク処理 + +## I/O / network + +- N+1 query を避ける (bulk / batch / join / dataloader 相当) +- timeout は必ず設定 (デフォルト無限の client は明示的に上書き) +- リトライは 回数 / 対象エラー / backoff / idempotency を明示。rate limit / circuit breaker を default で意識 +- 独立 I/O は並列化 + 並列度上限。依存 I/O は直列 +- 必要なフィールドだけ取得 (over-fetch / `SELECT *` 回避)。pagination は cursor 系を default + +## コンカレンシー + +- cancellation / timeout / tracing は最深部まで伝播させる +- 起動した並行タスクは必ず終了経路を持つ (leak を作らない) +- 共有状態は immutable / 排他制御 / message passing の 3 択で設計。read-modify-write / check-then-act は atomic / lock / CAS で守る + +## キャッシュ / メモ化 + +- 同じ計算・I/O を 1 リクエスト内で繰り返さない (request-scoped cache) +- process-scoped cache は TTL / 無効化戦略 / 容量上限 / eviction を必ず明示 +- 分散環境では stampede / stale read を意識 +- LLM / API client は provider のキャッシュ機能を default で組み込む (Claude なら prompt caching → `claude-api` skill) + +## レイテンシ / スループット + +- p50 / p95 / p99 のどれを最適化するか最初に決める +- critical path にブロッキング I/O / 重い計算を置かない (background へ) +- payload サイズ (compression / 不要フィールド削除) もレイテンシ要素 + +## 計測してから最適化 + +- 修正前に計測 (profiler / benchmark / trace)。憶測で書き換えない +- 「速くなった」は before / after の数値で示す。ボトルネック特定前の micro-optimization に走らない + +## Observability + +- 重要処理は metrics + logs + traces の 3 軸。高 cardinality label を metrics に入れない。log は構造化 default + +## trade-off は user 判断 (比較形式で提示) + +可読性 vs パフォーマンス / メモリ vs CPU / レイテンシ vs スループット / 整合性 vs 可用性 / 正確性 vs 速度 diff --git a/claude/rules/security.md b/claude/rules/security.md new file mode 100644 index 0000000..fb9b5df --- /dev/null +++ b/claude/rules/security.md @@ -0,0 +1,63 @@ +--- +paths: + - "**/*.{go,rs,py,rb,ts,tsx,js,jsx,java,kt,php,sql,sh,bash,zsh,c,cc,cpp,h,hpp,proto}" + - "**/go.mod" + - "**/go.sum" + - "**/package.json" + - "**/package-lock.json" + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/pyproject.toml" + - "**/requirements*.txt" + - "**/Dockerfile*" + - "**/*.{yaml,yml,tf,tfvars}" +--- + +# Security / Governance 詳細規約 + +コード・依存・manifest を触る時の詳細ルール。核となる原則 (secret 不書込 / least privilege / 破壊的操作の 3 点セット) は CLAUDE.md 側、shell command の強制は `hooks/guard-bash.sh` 側。 + +## secret / 機密情報 + +- secret の分離先は `.env` / secret manager / 環境変数 / KMS / Vault のいずれか +- 既知 prefix (`sk-` / `xoxb-` / `ghp_` / `AKIA` / JWT / PEM block) や高 entropy 文字列を検出したら即停止して報告 +- log / error message / stack trace に PII / token / cookie / Authorization header を載せない。logging 直前に mask / redact +- 会話に貼られた secret を以降の出力 (tool 引数 / summary / commit message) に echo しない + +## 権限拡大 (user 確認必須) + +- `chmod 777` / `chmod -R` / `chown -R` / world-writable +- container `--privileged` / `--cap-add` / `hostNetwork` / `hostPID` / `hostPath` mount +- K8s `cluster-admin` / `*` verb / `*` resource binding +- IAM `*:*` / `Action: "*"` / wildcard Principal +- DB `GRANT ALL` / `SUPERUSER` / public role 付与 +- cloud storage の public read / write 設定 + +## 入力検証 / Injection 対策 + +- SQL は必ず parameterized query (string 連結禁止) +- shell command 構築で外部入力を string 連結しない (argv 形式 / shell=False) +- `eval` / `exec` / `Function()` / dynamic require を default で書かない。必要なら user 承認 + 入力制限を plan で示す +- HTML / JSON / YAML / regex / path への外部入力は boundary で sanitize / validate +- file path / URL に `..` / `~` / 絶対 path / scheme 付き URL を許す箇所は path traversal / SSRF として扱う + +## Supply chain / 外部依存 + +- 新規 dependency は user 承認 (特に AI / crypto / network / serialization / auth 系)。追加前に publisher / 最終更新 / known CVE を確認 (typosquatting 警戒) +- `curl | sh` 形式のインストールは避け、必要なら user 承認 + 出所明示 + checksum 検証 +- lockfile (`go.sum` / `package-lock.json` / `Cargo.lock` / `poetry.lock`) は必ず commit +- pin 戦略を尊重 (exact pin なら exact のまま、勝手に緩めない / 締めない) +- vendored / submodule / git URL 依存の新規追加は user 確認 + +## Governance / コンプライアンス + +- ライセンス互換性を確認 (GPL / AGPL / SSPL / 商用条項を商用コードに混ぜない) +- PII / 顧客データ / 監査ログを test fixture にそのまま使わない (匿名化 or synthetic) +- retention / 削除要件 (GDPR 等) のあるテーブル・log への変更は user 確認 + +## 危険操作の確認 (user の明示指示があっても 3 点セット提示後のみ) + +- DB: `DROP TABLE/SCHEMA/DATABASE` / `TRUNCATE` / WHERE なし `DELETE`・`UPDATE` / down migration / backup・WAL 削除 / large table の index drop +- Infra: production deploy / IAM・trust relationship 変更 / DNS・LB・firewall・SG 変更 / public access 有効化 / TLS 証明書差し替え / secrets store の上書き・削除 +- 公開: 公開 channel 送信 / public repo push / public gist / production key の新規発行 +- destructive ops は dry-run / `EXPLAIN` / preview を必ず先に実行 diff --git a/claude/rules/spec-contract.md b/claude/rules/spec-contract.md new file mode 100644 index 0000000..025c552 --- /dev/null +++ b/claude/rules/spec-contract.md @@ -0,0 +1,38 @@ +# spec contract — 人間と agent の唯一の契約 + +work-intake / dev-cycle は本 contract を満たす work item だけを自律 pipeline に入れる。 +write-spec skill は人間の設計 draft が本 contract を満たすまで補佐する。 +ready flag を立てるのは人間のみ。 + +## 必須セクション + +| # | セクション | 記入基準 | NG 例 | +|---|---|---|---| +| 1 | 目的 / 背景 | なぜやるかを 1 段落。解決したい問題 or 得たい価値を明記 | 「〜を改善する」だけで対象が不明 | +| 2 | スコープ / non-goals | non-goals (今回やらないこと) を最低 1 項目明記 | non-goals が空 | +| 3 | 設計本体 | アーキ / API 契約 / データモデル。変更対象が repo 内のどこか特定できる粒度 | 「いい感じに実装」 | +| 4 | DoD (受け入れ条件) | 各項目が機械検証可能 (test / lint / 具体的動作)、または検証手順が書ける | 「動くこと」 | +| 5 | 制約 | security / performance / 新規依存の可否を明記 (該当なしなら「なし」と書く) | 未記載 | +| 6 | メタ | 対象 repo / 優先度 / ready flag | repo 不明 | + +## 検証 checklist (write-spec / work-intake 共通) + +検証時は**全項目の判定を必ず出力する** (満たす / 満たさない + 理由。黙った skip 禁止): + +- [ ] 必須セクション 1-6 が全て存在する +- [ ] non-goals が 1 項目以上ある +- [ ] DoD の各項目に検証手順 (コマンド or 手順) が紐付いている +- [ ] 制約セクションに新規依存の可否が明記されている +- [ ] 対象 repo が実在する path / URL で特定できる +- [ ] 設計の未決事項 (「A か B か検討」等) が残っていない — 残っている場合 ready 不可 + +## ready の意味 + +- ready = 人間が「この spec の通りに実装されたら受け入れる」と宣言した状態 +- ready を立てられるのは人間のみ。write-spec は提案まで +- ready 後の spec 変更は契約変更 — dev-cycle が着手済みの場合は escalation する + +## 置き場所 + +- 第一弾: Notion ticket (対象 DB / ready flag の表現は MEMORY.md の reference memory から取得する。本ファイルに hardcode しない) +- source を GitHub Issue 等に変えても本 contract は不変 diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..ed3a16d --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,109 @@ +{ + "env": { + "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" + }, + "permissions": { + "deny": [ + "Read(//Users/kiichiroyukawa/.zshrc.local)", + "Read(~/.zshrc.local)", + "Read(./.zshrc.local)", + "Read(.zshrc.local)", + "Edit(//Users/kiichiroyukawa/.zshrc.local)", + "Edit(~/.zshrc.local)", + "Edit(./.zshrc.local)", + "Edit(.zshrc.local)", + "Write(//Users/kiichiroyukawa/.zshrc.local)", + "Write(~/.zshrc.local)", + "Write(./.zshrc.local)", + "Write(.zshrc.local)", + "NotebookEdit(//Users/kiichiroyukawa/.zshrc.local)", + "NotebookEdit(~/.zshrc.local)", + "Read(**/.env)", + "Edit(**/.env)", + "Write(**/.env)", + "Read(**/.env.local)", + "Edit(**/.env.local)", + "Write(**/.env.local)", + "Read(**/.env.*.local)", + "Edit(**/.env.*.local)", + "Write(**/.env.*.local)", + "Read(**/*.pem)", + "Edit(**/*.pem)", + "Write(**/*.pem)", + "Read(**/*.key)", + "Edit(**/*.key)", + "Write(**/*.key)", + "Read(**/*.p12)", + "Edit(**/*.p12)", + "Write(**/*.p12)", + "Read(**/*.pfx)", + "Edit(**/*.pfx)", + "Write(**/*.pfx)", + "Read(**/id_rsa)", + "Edit(**/id_rsa)", + "Write(**/id_rsa)", + "Read(**/id_ed25519)", + "Edit(**/id_ed25519)", + "Write(**/id_ed25519)", + "Read(**/.ssh/**)", + "Edit(**/.ssh/**)", + "Write(**/.ssh/**)", + "Read(**/.netrc)", + "Edit(**/.netrc)", + "Write(**/.netrc)", + "Read(**/*.kubeconfig)", + "Edit(**/*.kubeconfig)", + "Write(**/*.kubeconfig)", + "Read(**/.kube/config)", + "Edit(**/.kube/config)", + "Write(**/.kube/config)", + "Read(**/*.tfstate)", + "Edit(**/*.tfstate)", + "Write(**/*.tfstate)", + "Read(**/terraform.tfvars)", + "Edit(**/terraform.tfvars)", + "Write(**/terraform.tfvars)", + "Read(**/.aws/credentials)", + "Edit(**/.aws/credentials)", + "Write(**/.aws/credentials)" + ], + "defaultMode": "auto" + }, + "model": "opusplan", + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "$HOME/.claude/hooks/guard-bash.sh" + } + ] + } + ] + }, + "statusLine": { + "type": "command", + "command": "bash $HOME/.claude/statusline-command.sh" + }, + "enabledPlugins": { + "rust-analyzer-lsp@claude-plugins-official": true, + "superpowers@claude-plugins-official": true, + "superpowers@superpowers-marketplace": true, + "security-guidance@claude-plugins-official": true, + "claude-code-setup@claude-plugins-official": true + }, + "extraKnownMarketplaces": { + "superpowers-marketplace": { + "source": { + "source": "github", + "repo": "obra/superpowers-marketplace" + } + } + }, + "effortLevel": "xhigh", + "tui": "fullscreen", + "agentPushNotifEnabled": true, + "skipAutoPermissionPrompt": true +} diff --git a/claude/skills/add-rust-crate/SKILL.md b/claude/skills/add-rust-crate/SKILL.md new file mode 100644 index 0000000..5ad9f81 --- /dev/null +++ b/claude/skills/add-rust-crate/SKILL.md @@ -0,0 +1,204 @@ +--- +name: add-rust-crate +description: Adds a new crate to an existing Rust workspace (workspace inheritance in Cargo.toml / CLI, TUI, and lib skeletons / README table update / adding to workspace.dependencies if needed). Used for things like 「workspace に新 crate 追加」「<name> という tool を生やして」「新しい binary crate 切って」. +--- + +# add-rust-crate + +> **Source of truth:** `claude/ja/skills/add-rust-crate/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +A skill that **adds one new crate** to a workspace created by `rust-bootstrap` (or an equivalent one). Used repeatedly. Even though it might seem like just spinning up `crates/<name>/`, things like the TUI skeleton, modern dependency patterns, and README table updates are fixed conventions, so this is turned into a skill. + +> **Snapshot**: Assumes 2026-04 / Rust 1.95 / clap 4 / ratatui 0.30 / crossterm 0.29 / tokio 1. +> Since ratatui's API moves relatively fast, verify (via the book / changelog) whether +> `ratatui::init` / `EventStream` / `recv_many` are still the recommended approach at that point in time before reusing them. + +## Applicability + +- A Rust workspace already exists at the root with `[workspace] members = ["crates/*"]` +- `edition` / `license` / `repository`, etc. are centrally managed via `[workspace.package]` +- Common dependencies are registered in `[workspace.dependencies]` + +If these aren't met, prompt the user to call `/rust-bootstrap` first. + +## Procedure + +### Step 0: Check prerequisites + +```bash +test -f Cargo.toml && grep -q '\[workspace\]' Cargo.toml && echo "workspace OK" +ls crates/ 2>/dev/null +grep -A 30 '\[workspace.package\]' Cargo.toml +grep -A 30 '\[workspace.dependencies\]' Cargo.toml +``` + +Consolidate into one `AskUserQuestion` turn: +- Crate name (recommend `kebab-case` rather than snake_case; e.g. `mytool`) +- Crate kind (`bin` / `tui` / `lib`) +- One-line description (used for the `description` in `Cargo.toml` and the README description) +- Any additional dependencies needed (ones not yet registered in `workspace.dependencies`) + +**When the kind is `tui`, Read `references/tui.md` before proceeding** (it contains the skeleton, key-handling patterns, and the mpsc ingest pattern). + +### Step 1: Check for conflicts with existing crates + +```bash +test -d crates/<name> && echo "ALREADY EXISTS — abort" +grep -q "^name = \"<name>\"" crates/*/Cargo.toml && echo "NAME COLLISION — abort" +``` + +If there's a conflict, suggest a different name to the user. + +### Step 2: Extend `workspace.dependencies` (only if needed) + +If a new dependency is needed, append it to `[workspace.dependencies]` in the root `Cargo.toml`: + +```toml +<new-dep> = { version = "X", features = ["..."] } +``` + +For the additional dependencies needed when introducing TUI for the first time, see `references/tui.md`. If it's already registered, do nothing (do not add duplicates). + +### Step 3: `crates/<name>/Cargo.toml` + +Inherit everything from the workspace: + +```toml +[package] +name = "<name>" +version = "0.1.0" +description = "<one-line>" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +publish.workspace = true + +[[bin]] # bin / tui only +name = "<name>" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true +# add depending on the kind (see Step 4 below) + +[dev-dependencies] +tempfile.workspace = true + +[lints] +workspace = true +``` + +For the `lib` kind, remove `[[bin]]` and create `src/lib.rs`. + +### Step 4: `src/main.rs` (skeleton by kind) + +#### `bin` (CLI) + +```rust +//! <name> — <description> + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "<name>", version, about)] +struct Cli { + // arguments +} + +fn main() -> Result<()> { + let _cli = Cli::parse(); + Ok(()) +} +``` + +#### `tui` + +Use the skeleton in `references/tui.md` (main.rs / app.rs / dependencies). Key points restated here: +- Use `ratatui::init()` / `ratatui::restore()`; don't write `enable_raw_mode` etc. yourself +- For key input, put `EventStream` into `tokio::select!` (`poll(0)` is forbidden) +- Once there are 3 or more kinds of keys, move to the `classify_key` enum pattern (this makes it unit-testable) + +#### `lib` (internal shared crate) + +`Cargo.toml`: + +```toml +[lib] +path = "src/lib.rs" +``` + +`src/lib.rs`: + +```rust +//! <name> — <description> + +#![warn(missing_docs)] +``` + +For a library, be conscious of `pub` exposure, and properly attach `#[must_use]` / `Debug` derives (stricter than for a binary). + +### Step 5: Update the tools table in the root README + +```markdown +| Crate | Description | +|---|---| +| [`<name>`](crates/<name>) | <description> | +``` + +Just add a row to the existing table. Keep alphabetical order. + +### Step 6: `crates/<name>/README.md` (optional) + +If it's planned for publication (`publish = true`), write a dedicated README. For a personal toolkit, it's fine to consolidate into the root README. + +### Step 7: Verify it works + +```bash +. "$HOME/.cargo/env" +cargo build -p <name> && echo BUILD_OK +cargo test -p <name> --all-targets && echo TEST_OK +cargo clippy -p <name> --all-targets -- -D warnings && echo CLIPPY_OK +cargo fmt -p <name> -- --check && echo FMT_OK +``` + +For a TUI, also launch it: + +```bash +cargo run -p <name> --release # quit with 'q' +``` + +### Step 8: Completion report + +| Item | Status | +|---|---| +| Crate name / kind | <value> | +| `crates/<name>/Cargo.toml` (workspace inheritance) | ✓ | +| `src/main.rs` (or `src/lib.rs`) | ✓ | +| Additional dependencies added to `[workspace.dependencies]` | <value or none> | +| Root README table | ✓ | +| `cargo build` / `test` / `clippy -D warnings` / `fmt --check` | ✓ | + +## Golden rules + +1. **Always use workspace inheritance**: use `*.workspace = true` for version / license / lints, all of it +2. **Start with `pub(crate)`**: binary crates have no external exposure. Use `pub` only for a lib that is genuinely exposed externally +3. **TUI uses `ratatui::init` / `restore` + `EventStream`**: don't write `enable_raw_mode` / `poll(0)` yourself +4. **No duplicate additions to `workspace.dependencies`**: check first whether it already exists +5. **Stay in scope**: business logic implementation is out of scope. Stop at the skeleton plus one working `main` +6. **Do not commit**: committing is delegated to a separate skill (`/commit-push-branch`) + +## Anti-patterns + +- Hardcoding `version = "..."` / `license = "..."` in the crate's own `Cargo.toml` +- Implementing `enable_raw_mode` + `EnterAlternateScreen` yourself for a TUI (use `ratatui::init`) +- Writing `crossterm::event::poll(Duration::from_millis(0))` in an async loop (put `EventStream` into `tokio::select!`) +- Implementing your own `mpsc::Receiver::recv` + `try_recv` drain loop (use `recv_many(&mut buf, N)` in one line instead) +- Overhauling the workspace root just to add one crate (that's `rust-bootstrap`'s job) +- Forgetting to update the README table (the new tool won't be discovered) diff --git a/claude/skills/add-rust-crate/references/tui.md b/claude/skills/add-rust-crate/references/tui.md new file mode 100644 index 0000000..548f5a0 --- /dev/null +++ b/claude/skills/add-rust-crate/references/tui.md @@ -0,0 +1,141 @@ +> **Source of truth:** `claude/ja/skills/add-rust-crate/references/tui.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# TUI skeleton (ratatui + tokio + crossterm EventStream) + +Read this only when the crate kind is `tui`. + +## workspace.dependencies (add if not already registered) + +```toml +crossterm = { version = "0.28", features = ["event-stream"] } +ratatui = "0.29" +futures = "0.3" +``` + +## Add to `[dependencies]` in `crates/<name>/Cargo.toml` + +```toml +crossterm.workspace = true +ratatui.workspace = true +futures.workspace = true +tokio.workspace = true +``` + +## `src/main.rs` + +```rust +//! <name> — <description> + +mod app; + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "<name>", version, about)] +struct Cli {} + +#[tokio::main] +async fn main() -> Result<()> { + let _cli = Cli::parse(); + let mut terminal = ratatui::init(); + let res = app::run(&mut terminal).await; + ratatui::restore(); + res +} +``` + +## `src/app.rs` + +```rust +use anyhow::Result; +use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; +use futures::StreamExt; +use ratatui::{DefaultTerminal, Frame, widgets::{Block, Borders, Paragraph}}; +use std::time::Duration; + +pub(crate) async fn run(terminal: &mut DefaultTerminal) -> Result<()> { + let mut events = EventStream::new(); + let mut tick = tokio::time::interval(Duration::from_millis(250)); + tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + terminal.draw(view)?; + + tokio::select! { + _ = tick.tick() => {} + Some(Ok(ev)) = events.next() => { + if let Event::Key(k) = ev + && k.kind == KeyEventKind::Press + && matches!(k.code, KeyCode::Char('q') | KeyCode::Esc) + { + return Ok(()); + } + } + } + } +} + +fn view(f: &mut Frame<'_>) { + let p = Paragraph::new("hello — press q to quit") + .block(Block::default().borders(Borders::ALL).title(" <name> ")); + f.render_widget(p, f.area()); +} +``` + +`ratatui::init()` handles **the panic hook + raw mode + alt screen** all at once, so don't write `enable_raw_mode` etc. yourself. Also replace `crossterm::event::poll(0)` with `EventStream`. + +## Pattern for when things scale up (a TUI with 2 or more kinds of key handling) + +If the first version only handles 'q', the above is sufficient. Once **3 or more kinds of keys** or **destructive operations** (reset / delete) show up, don't write `match` directly in the event loop — instead **extract intent via an enum**: + +```rust +#[derive(Debug, PartialEq, Eq)] +enum KeyAction { + Quit, + Reset, + Refresh, + Ignore, +} + +/// Pure mapping: testable without spinning up the TUI. +fn classify_key(code: KeyCode) -> KeyAction { + match code { + KeyCode::Char('q') | KeyCode::Esc => KeyAction::Quit, + KeyCode::Char('r') => KeyAction::Reset, + KeyCode::Char('R') => KeyAction::Refresh, + _ => KeyAction::Ignore, + } +} + +// Inside the loop: +match classify_key(k.code) { + KeyAction::Quit => return Ok(()), + KeyAction::Reset => state.reset(), + KeyAction::Refresh => state.refresh()?, + KeyAction::Ignore => {} +} +``` + +Benefit: `classify_key` can be unit tested. The loop reads clearly because it's a "match on intent name". If there's already an existing TUI crate in the workspace, reference its `classify_key` implementation first to keep the style consistent. + +## When using tokio mpsc as an ingest source + +If events are streamed in from an external thread (file watcher / websocket / etc.), draining with `recv_many` is efficient: + +```rust +let mut event_buf = Vec::with_capacity(64); +loop { + tokio::select! { + _ = tick.tick() => {} + n = rx.recv_many(&mut event_buf, 64) => { + if n == 0 { return Ok(()); } // sender closed + for ev in event_buf.drain(..) { state.ingest(&ev); } + } + Some(Ok(ct_ev)) = events.next() => { /* keys */ } + } + terminal.draw(|f| view(f, &state))?; +} +``` + +This takes a single syscall, compared to hammering `try_recv` in a while loop. diff --git a/claude/skills/api-design-review/SKILL.md b/claude/skills/api-design-review/SKILL.md new file mode 100644 index 0000000..60bc7ad --- /dev/null +++ b/claude/skills/api-design-review/SKILL.md @@ -0,0 +1,201 @@ +--- +name: api-design-review +description: A read-only review skill that surfaces overlooked considerations in API / upstream design (ADR / Design Doc / API contracts / domain models / ACL) across 6 axes. Invoke it at the logical design stage, before it's committed to a wire representation — before ExitPlanMode / when drafting an ADR / when a Design Doc draft is complete. Used for things like 「設計 review して」「考慮漏れチェック」. +model: claude-fable-5 +--- + +> **Source of truth:** `claude/ja/skills/api-design-review/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# api-design-review + +A systematic review skill for API / upstream system design that **prevents issues from surfacing piecemeal across turns**. Read-only (no Edit / Write; only grep via Bash is allowed). + +The source of overlooked considerations isn't the wire representation (proto / OpenAPI) but the **domain design / use-case analysis / ACL model / API experience design that precedes it** — so running this **before** starting to write proto is most effective. Reviewing after proto work has already begun is only supplementary. + +## Applicability + +Invoke in any of the following cases: + +### During upstream design (primary use) +- **Before** drafting a new ADR, or when the draft is complete (MADR v3, `docs/adr/`) +- When newly creating a Design Doc / System Design (arc42 / C4) or adding a section (`docs/design/`) +- During the logical design of a new API contract (proto / OpenAPI / GraphQL schema), before committing it to wire form +- When newly creating or revising a domain model / Aggregate / Bounded Context +- During use-case / user-story analysis, especially when verbs other than CRUD are involved +- When designing an ACL / authorization / multi-tenant isolation model + +### Downstream supplement (after starting to touch proto / OpenAPI) +- Before a structural change to an existing message / enum (extracting a sub-message / renaming a field / adding an enum value) +- When revising the SDK surface (TypeScript / Go, etc.) +- When applying plan-first to a multi-file refactor, before ExitPlanMode + +### When not to invoke +- Bug fixes / typos / formatting / lint fixes +- Internal refactors with no impact on an existing contract +- Minor documentation-only updates + +For minor changes, the everyday checks in CLAUDE.md's 「判断と質問の作法」section suffice (this skill is **reserved for heavyweight analysis**). + +## Approach (one pass ≈ 20-40 minutes; allow more time the further upstream you are) + +After reviewing the design target (Read the ADR draft / Design Doc / proto / related docs), **work through the following 6 perspectives one at a time, writing each down**. For each perspective, explicitly state "not applicable" where relevant (a blank = not considered = an oversight). + +### 1. Separating client abstraction from server-side expansion + +Enumerate the concepts / fields / enum values / RPC parameters that appear in the design target, and for each: + +- (a) Is it **a value the client / caller / external user can directly know or derive** (their own id, user input, in-house configuration values)? +- (b) Is it **a value the server / platform derives from context** (other tenants' ids, authentication info, internal resource ids, role / claims, cross-tenant fan-out targets)? +- (c) Are there any places where (b) has been placed on the wire / contract / public surface? + +If (c) is found, remove it from the wire and move it to a server-side concept. Also check the SDK examples. + +**Check phrasing**: "How would the client come to know this concept?" / "Does the contract expose information the client shouldn't know?" + +**Past case**: a proposal to put `repeated string product_ids` in the proto → the client doesn't know other product_ids (information leak + ACL bypass); it was removed from the wire and moved to a server-side concept (collection-level config / access_group, etc.) + +### 2. Both the read and write sides of ACL + +When ACL / visibility / access control is involved (state "not applicable" explicitly when it isn't): + +- (a) The representation and server-side behavior of **reads** (search filter / fetch / row-level / collection ACL) +- (b) The representation and server-side behavior of **writes** (who's authorized per visibility / scope / write authorization / role-based gating) +- (c) Consider cases where, even for the same visibility, the authorized role differs between internal vs external submission, or admin vs regular +- (d) Behavior on authorization failure (403 vs 404 / information leak risk) + +Write authorization should be determined by the ACL domain layer (ReBAC / ABAC, etc.), not a wire field (baking it into a proto field makes it spoofable). Splitting endpoints (`/v1/upsert` vs `/v1/admin/upsert`) is an option for exposing the authorization boundary on the wire surface. + +**Check phrasing**: "**Who can write** this visibility / scope?" / "What's the risk of an individual caller writing a broad visibility?" + +**Past case**: the risk of an individual product client being able to write PRODUCT_WIDE went undiscussed → it was decided to handle the write side of ACL in a separate ADR (ReBAC/ABAC) + +### 3. Systematic forward-compat check + +How the design target might be extended in the future, and whether it can be handled non-breaking: + +- Adding an enum value (non-breaking in proto3; appending is fine in OpenAPI too) +- Adding a field (a new field number / property; non-breaking in proto3 / OpenAPI) +- Adding a new RPC / new endpoint / new service +- Adding a new message / new schema +- Compatibility when an ADR is revised / withdrawn + +List 3-5 foreseeable future extensions (cross-tenant / admin / batch / streaming / role / scope group / pre-signed URL / async worker / VLM / multi-region, etc.) and check whether each can be handled with a non-breaking extension. Cases that require a breaking change should be completed within the current Phase. + +**Check phrasing**: for each extension case, write 1-3 lines answering "When X arrives in the future, how would the contract be extended?" + +**Past case**: a design that baked a transient label (e.g. an organization name like "academy") into an enum / field rotted when the organization changed; it was changed to organization-name-free naming (CURATED / BOOK, etc.) + +### 4. Enumerating edge cases ("what happens when...?") + +**Write out 5-10** of the following domain questions against the design target **and answer each one**: + +- **Resubmitting the same ID / duplicates / idempotency** (Upsert: overwrite / `AlreadyExists` / version / soft delete / version vector) +- **empty / unspecified / null / zero value** (how each field handles it — reject via validation, or default it) +- **Set operations** (cross-tenant / cross-company / global wildcard `*` / subset / select-all) +- **Boundary values** (max payload / max array length / pagination / rate limit / timeout / retry policy) +- **timezone / locale / encoding** (UTF-8 / multi-byte / collation / Japanese-specific concerns) +- **Partial failure** (a batch operation failing partway through; idempotency / compensating transactions) +- **Ordering / duplication / idempotency / concurrency** +- **Authentication / authorization failure behavior** (403 vs 404 / information leak risk) +- **Format conversion / inference** (mime_type auto-inference / explicit / fallback / when inference fails) +- **Behavior when a dependent service fails** (degraded / circuit breaker / fallback) + +**Check phrasing**: from a domain angle, come up with 5-10 instances of "what happens with X in this situation?" If the answer comes out as "not considered" or "to be considered later," that's an area where the design phase should produce an answer + +**Past case**: for source_type, the questions "what happens if the organization is deleted?" and "how do we separate internal FAQ vs external FAQ?" surfaced at turn 4 → led to the decision to narrow the classification axis to format + +### 5. Consistency with the existing source of truth (grep-first) + +**Before** introducing new naming / new structures, grep the entire existing source of truth: + +- Whether old field names / old method names / old enum values / old ADR numbers / old design-doc terminology still remain in docs / api.md / metadata literals / SDK examples / proto / Go code / Markdown notes +- Grasp the scope of changes **across all files at once** (to prevent issues from surfacing piecemeal across turns) +- When design is complete, prepare 5-10 grep verification conditions (the literal `grep -nE "..."` command, recorded in the PR description / commit description / ADR appendix) + +Via Bash: +```bash +grep -rnE "<old-name-pattern>" docs/ apis/ internal/ cmd/ cli/ +``` + +Include the resulting hit list in the design output to finalize the scope of changes. + +**Check phrasing**: "What condition makes grepping for the old name return 0 hits?" / "How many places will the new name be added to?" + +**Past case**: the old `accessScopes` surface was left behind in the docs §11.2 SDK example, discovered at turn 4 → should have grepped during design to grasp every location up front + +### 6. Compliance with memory conventions (check every time) + +Do one review pass to check the design target doesn't violate any of the following conventions (the source of truth is CLAUDE.md / each skill): + +| Convention | Common places it's violated | +|---|---| +| Don't leave Phase / ticket references in comments | Phase / ticket / PR references in proto / Go comments | +| Commit titles are concise (1 line) | Commit title is a long sentence | +| Code comments are in English | *.go / proto / Makefile / shell comments in Japanese | +| Push only after explicit user instruction (CLAUDE.md) | A push proposal without user confirmation | +| No inline comments inside tests | Inline comments inside a test | +| Don't open docs with a preamble/assumptions section | A scope/preamble section at the top of docs | +| Don't put individual ticket plans in the repo | An individual ticket plan under docs/plan/ | +| Comments stay within the literal scope of the source text (no speculative mapping) | A comment with speculative mapping that goes beyond the literal scope of the source text | +| Multi-file changes are plan-first (CLAUDE.md) | Skipping the plan despite a multi-file change | +| Deviations from spec are stated explicitly, approved, and recorded (CLAUDE.md) | A literal deviation from spec not written into the plan | +| Substantive edits go through a subagent | The primary agent directly makes a substantive Edit | +| PRs are not auto-created (CLAUDE.md) | Auto-creating a PR | +| Subagent briefs reference a state file | Repeating context inline in a subagent brief | +| Adherence to the design-phase checklist | Failing to comply with this very checklist | +| Accuracy of product terminology | Misnaming a product or term | +| Adherence to architectural assumptions | Terminology creeping in that contradicts architectural assumptions | + +Also check for things like unused imports / mechanical consistency gaps under this perspective. + +## Output format + +When the skill completes, present the following to the user as the deliverable: + +```markdown +# api-design-review results + +## Design target +<path or name of the target ADR / Design Doc / proto / API spec / domain model> + +## Review by perspective + +### 1. Client abstraction vs server-side expansion +- Findings: <content, or "not applicable"> +- Proposed fix: <proposal, or "fine as-is"> + +### 2. Both sides of ACL read/write +- Findings: ... + +### 3. forward-compat +- Findings: ... + +### 4. Edge case enumeration +5-10 items in question form, each with an answer + +### 5. Consistency with the existing source of truth (grep results) +- `grep -rnE "..."` → hit list + +### 6. Compliance with memory conventions +- Potential violations: <content, or "clear"> + +## Summary +- OK to proceed with the design (nothing found): ◯ +- Fixes needed (specific locations): X items → ... +- Requires user judgment (trade-offs presented): Y items → ... +``` + +When handing off to the main agent / dev-orchestrator / Plan agent / tech-docs-writer / notion-ticket-plan, writing the above out to a state file keeps the brief lightweight when a subagent restarts (a state-file-referencing brief). + +## What this skill does not do + +- Does not implement / Edit / Write (read-only review) +- Does not perform git mutations / push / create PRs +- Does not call AskUserQuestion within the skill (returns results to the main agent, which defers to the user's judgment) + +## Related artifacts + +- Everyday check (lightweight version): CLAUDE.md's 「判断と質問の作法」section +- Related skills: `tech-docs-writer` (passes through this skill internally when drafting an ADR / Design Doc), `notion-ticket-plan` (passes through this skill when analyzing a ticket / drafting a plan), `ddd-hexagonal` (layer boundaries / dependency direction, related to this skill's perspective 1), `code-refactor-advisor` (implementation-facing refactor candidates, the implementation-pass version of this skill) +- Handoff to the main agent: a state-file-referencing brief +- Built into the agent side: the `dev-orchestrator` agent's plan phase passes through this skill (already integrated) diff --git a/claude/skills/arc42-c4/SKILL.md b/claude/skills/arc42-c4/SKILL.md new file mode 100644 index 0000000..b336750 --- /dev/null +++ b/claude/skills/arc42-c4/SKILL.md @@ -0,0 +1,56 @@ +--- +name: arc42-c4 +description: Reference for architecture design docs combining arc42 (§1-12) with the C4 model (L1-L4) — which diagram goes in which section, top-level vs subsystem split. Use for "which section gets what", "§5 vs §6 vs §7", "ADR inside or separate". Reference skill, not a procedure. +--- + +> **Source of truth:** `claude/ja/skills/arc42-c4/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# arc42-c4 + +arc42 (sections) and C4 (diagrams) are independent conventions; neither spec says which C4 diagram belongs in which arc42 section. **This skill pins that mapping as a house standard** to kill per-doc ambiguity. Project-specific doc maps live in the project's top-level design doc, not here. + +Use when writing / reviewing an arc42 + C4 design doc, or deciding the ADR / runbook / README boundary. Supplies section judgment to `tech-docs-writer`; a review lens to `api-design-review`. + +## arc42 §1-12 + +1 Introduction & Goals · 2 Constraints · 3 Context & Scope · 4 Solution Strategy · 5 Building Block View · 6 Runtime View · 7 Deployment View · 8 Crosscutting Concepts · 9 Architecture Decisions · 10 Quality Requirements · 11 Risks & Tech Debt · 12 Glossary + +- **§5 vs §6 vs §7** (same blocks, different axis): §5 = static structure ("is X a part?") · §6 = behavior over time ("what order do parts talk for use case Y?", insight-bearing scenarios only) · §7 = physical placement ("which node runs X?"). +- **§2 vs §4 vs §10** (same fact can appear in all three): §2 = limits you couldn't choose · §4 = fundamental choices you made · §10 = measurable quality scenarios. PostgreSQL: "policy mandates it" → §2; "we chose it for integrity" → §4; "reads < 50ms p95" → §10. §10 holds ends not means; §9 holds decision index not full ADRs. + +## arc42 × C4 mapping (house standard — decisive) + +| C4 | arc42 | +|----|-------| +| L1 System Context | **§3** | +| L2 Container | **§5** (top level) | +| L3 Component | **§5** (lower level / subsystem doc) | +| L4 Code | §5 deepest, or omit | +| dynamic diagram | **§6** | +| deployment diagram | **§7** | + +The 4 numbered C4 levels are **all static**; runtime → §6, placement → §7. **Dedup:** container structure in §5, container-on-infra mapping in §7 — cross-reference, never paste twice. + +## Multi-doc split (top-level ⇄ subsystem) + +Fold line = container boundary. **Top-level doc** owns L1 + L2 (the subsystem map / hand-off seam). **Subsystem doc** (one per container) owns L3 (+ L4 if used). The Container diagram is the join: top lists all containers, each subsystem points at its own then expands to components. Subsystem docs may use a **mini** arc42 subset (full §1-12 only at top level). + +## Boundary with adjacent conventions + +arc42 = durable design-time "shape + rationale". Different audience / cadence → separate doc, arc42 only **links**. + +| Convention | Location | arc42 touchpoint | +|-----------|----------|------------------| +| MADR (ADR) | `docs/adr/NNNN-*.md` | **§9 = index only** (id/title/status/link); full rationale, alternatives, consequences in the file | +| Diataxis (README/guides) | `docs/readme/` | link from §8 | +| SRE Playbook (runbook) | `docs/runbook/` | link from §7 / §11 | + +Cut an ADR when a decision is costly to reverse, contested, or constrains the future. + +## Common mistakes + +Flow in §5, or §7 re-pasting §5's diagram · treating a C4 level as "runtime" (all 4 are static) · freely-chosen tech filed under §2 · means in §10 instead of measurable ends · full ADR body in §9 · L1/L2 duplicated in subsystem docs · re-litigating the C4↔arc42 mapping as "just convention" (it's pinned above). + +## Related skills + +`tech-docs-writer` (writes the doc) · `api-design-review` (reviews it) · `ddd-hexagonal` (§5 / §8 layer boundaries). diff --git a/claude/skills/claude-session-jsonl/SKILL.md b/claude/skills/claude-session-jsonl/SKILL.md new file mode 100644 index 0000000..8145ee1 --- /dev/null +++ b/claude/skills/claude-session-jsonl/SKILL.md @@ -0,0 +1,205 @@ +--- +name: claude-session-jsonl +description: Schema reference for Claude Code session logs (~/.claude/projects/**/*.jsonl), plus a recipe for building token / cost / tool aggregation tools. Use for 「Claude の使用状況を観測したい」「セッションログから集計」「ccusage 相当を作る」「JSONL の中身教えて」, etc. Language-specific sample parsers live in references/. +--- + +> **Source of truth:** `claude/ja/skills/claude-session-jsonl/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# claude-session-jsonl + +You'll find yourself wanting to write aggregation tools that assume **precise** knowledge of the structure of the session logs (JSONL) that Claude Code leaves on disk, over and over again (ccwatch / ccusage / cclog / ccexport ...). Starting from scratch each time with `head -1 *.jsonl | jq` burns time, so this consolidates the verified schema and the typical pitfalls in one place. + +> **Snapshot**: As of 2026-04 / compiled by observing the JSONL that Claude Code 2.1.x writes out. +> When Claude Code updates the schema (new event types, changed usage fields, etc.), update this too. +> The pricing table's source of truth is Anthropic's official page; this is just a cache. + +## When to use this + +- 「Claude の使用状況を tool 化したい」 +- 「セッション jsonl をパースして X を出したい」 +- 「ccusage / ccwatch みたいなのを別言語で作りたい」 +- 「context window 使用率を計算したい」 + +The implementation language doesn't matter (mainly Rust / TypeScript / Python). This skill provides the schema explanation, design decisions, and a minimal parser for each language. **The actual implementation is handed off to a separate means** (`/rust-bootstrap` → `/add-rust-crate` → implementation, or manual work for TS). + +## File layout + +``` +~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl +``` + +- **`<encoded-cwd>`** is the `cwd` string with `/` replaced by `-`. Example: `/Users/foo/repo` → `-Users-foo-repo` +- **`<session-uuid>`** is a v4 UUID (e.g., `ec65e22c-0dab-4eff-b119-c2e2cb02aa8a`) +- 1 file = 1 session. `/clear` starts a new file +- Multiple sessions can coexist for the same cwd (an old one plus one in progress) +- Files are **append-only**. Lines already written are, in principle, never rewritten (rotation happens by creating a separate file with a new UUID) + +To pick up the latest session, just take the **`*.jsonl` file with the most recent modification time (`mtime`)**. + +## Schema (event types) + +Each line is a single JSON object. It branches on the `type` field. + +| `type` | Purpose | Used in aggregation? | +|---|---|---| +| `assistant` | Assistant utterances (token usage / tool calls / model name live here) | **YES (the main one)** | +| `user` | User utterances / command output (includes `/`-command caveats) | If needed | +| `system` | System messages (slash command stdout, etc.) | Usually not needed | +| `attachment` | Attachments/events (deferred tool deltas, etc.) | Usually not needed | +| `file-history-snapshot` | File history snapshot (for Edit rollback) | Not needed | +| `last-prompt` | The last user prompt (cache for regeneration) | Not needed | + +**Ignoring any other type that shows up** (`#[serde(other)]` / catch-all) is the correct approach. This way things don't break when a Claude Code update adds new types. + +## Required for aggregation: the `assistant` event + +Shape (partially abbreviated): + +```json +{ + "type": "assistant", + "uuid": "997e63ff-e2c6-...", + "parentUuid": "d6c11651-...", + "sessionId": "ec65e22c-...", + "timestamp": "2026-04-27T04:32:12.600Z", + "message": { + "id": "msg_018Znq...", + "model": "claude-opus-4-7", + "role": "assistant", + "type": "message", + "content": [ + { "type": "thinking", "thinking": "...", "signature": "..." }, + { "type": "text", "text": "hello" }, + { "type": "tool_use", "name": "Bash", "input": { ... } } + ], + "usage": { + "input_tokens": 6, + "output_tokens": 1103, + "cache_creation_input_tokens": 9667, + "cache_read_input_tokens": 15206, + "service_tier": "standard", + "cache_creation": { "ephemeral_1h_input_tokens": 9667, "ephemeral_5m_input_tokens": 0 }, + "iterations": [ ... ] + }, + "stop_reason": "end_turn" + }, + "requestId": "req_011...", + "cwd": "/Users/foo/repo", + "version": "2.1.x", + "gitBranch": "main" +} +``` + +### Key fields + +| Path | Type | Purpose | +|---|---|---| +| `message.model` | string | Examples: `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`. Casing varies (`[1m]` marks the 1M context variant) | +| `message.usage.input_tokens` | u64 | New billable input for this turn | +| `message.usage.output_tokens` | u64 | Output generated by the assistant | +| `message.usage.cache_creation_input_tokens` | u64 | Total cache writes (5min + 1hr) | +| `message.usage.cache_creation.ephemeral_5m_input_tokens` | u64 | 5min ephemeral write (1.25× input) | +| `message.usage.cache_creation.ephemeral_1h_input_tokens` | u64 | 1hr ephemeral write (**2.00× input**) | +| `message.usage.cache_read_input_tokens` | u64 | Cache hit (cheapest, 0.10× input) | +| `message.content[].type` | string | `tool_use` / `text` / `thinking`, among others | +| `message.content[].name` | string | Only present for `tool_use`. `Bash` / `Edit` / `Read` / `Grep` / `Write` / `Agent` / `WebFetch`, etc. | +| `timestamp` | RFC3339 string | For latency calculations / sliding window / active-session detection | + +> ⚠ Both `cache_creation_input_tokens` (the top-level total) and `cache_creation.{5m,1h}` (the breakdown) appear. **Use the breakdown for cost calculations**. In real Claude Code sessions today, almost everything is 1hr ephemeral, and this difference produces a cost error of tens of percent or more. + +### Derived quantities (frequently used) + +``` +context_size = input_tokens + cache_creation_input_tokens + cache_read_input_tokens +context_window = 200_000 (default) | 1_000_000 (when the model name contains [1m] / -1m) +context_pct = min(context_size / context_window, 1.0) +cache_hit_ratio = cache_read_input_tokens / context_size +session_cost_usd = sum over assistants of pricing(model).cost_usd(usage) +tokens_per_minute = (input + output + cache_creation) / elapsed_secs * 60 +cost_per_hour = session_cost_usd / elapsed_secs * 3600 +``` + +## Per-model pricing (USD per million tokens, as of 2026-04) + +> ⚠ These figures are updated periodically on Anthropic's page. Once you turn this into a tool, leave a comment noting "as of 2026-04" and consolidate the hardcoded values into one place (e.g., `pricing.rs`). + +| family | input | output | cache_w 5min | cache_w 1hr | cache_read | +|---|---:|---:|---:|---:|---:| +| Opus | 15.00 | 75.00 | **18.75** (1.25×) | **30.00** (2.00×) | 1.50 (0.10×) | +| Sonnet | 3.00 | 15.00 | 3.75 | 6.00 | 0.30 | +| Haiku | 1.00 | 5.00 | 1.25 | 2.00 | 0.10 | + +Cost formula: + +``` +cost = input × input_rate + + output × output_rate + + cache_5m × cache_w_5m_rate # cache_creation.ephemeral_5m_input_tokens + + cache_1h × cache_w_1h_rate # cache_creation.ephemeral_1h_input_tokens + + cache_read × cache_read_rate +``` + +For old logs missing the `cache_creation` field, falling back to treating everything as all-5min keeps things backward compatible. + +For unknown model names, **falling back to Sonnet** is a safe default (it's the middle value and the current default tier). + +## Design decisions — pitfalls and recommendations + +1. **File watching should be poll-based + reopen + seek + read_to_end + a remainder buffer**. Avoid the `notify` crate — it trips over rotation/atomic-replace on append-only logs. `tokio::io::BufReader::lines()` over a manually-seeked file is also a pain around EOF handling and ends up needing re-seeking anyway. **A homegrown 200ms poll + splitting on `\n` is the most robust approach**. +2. **Lines are reliably delimited by `\n`**. No need to worry about a UTF-8 codepoint being split mid-write (codepoints are written atomically as complete units). +3. **On file shrink (≈ rotation), reset the offset to 0**. Since operations like `/clear` create a new file under a different UUID, the watcher should be designed to "re-select the file with the newest mtime" (out of scope for this skill). +4. **`last-prompt` is a cache of the final prompt** and isn't used for aggregation (its content duplicates the user utterance). +5. **The token count of `thinking` blocks is included in `output_tokens`** (don't count it a second time). +6. **The `iterations` array** is server-side breakdown information. For aggregation, trusting the outermost `usage` is sufficient. +7. **Determine the model family via `to_ascii_lowercase().contains("opus" | "sonnet" | "haiku")`**. Exact matching breaks, since `claude-opus-4-7`, `claude-opus-4-7[1m]`, `claude-3-opus-...`, etc. all coexist. +8. **Do you ever need to reverse the encoded-cwd?** Yes (when displaying which repository a session belongs to). It's just converting `-` back to `/`, but this is irreversible if the original cwd itself contained a `-` — since `assistant.cwd` is included in the event as the original source, referencing that is safer. +9. **Aggregation is fundamentally per-session**. If you want to produce "cumulative cost for today across all sessions," you have to read every line of every file under `<projects-dir>/**/*.jsonl`, filter to the current day via `timestamp`, and then sum the usage (this is heavy). + +## Minimal parsers (by language) + +Once you've settled on an implementation language, **Read only the matching reference** (don't read all of them): + +| Language | reference | Key points | +|---|---|---| +| Rust | `references/parser-rust.md` | serde tagged enum + `#[serde(other)]` catch-all | +| TypeScript | `references/parser-typescript.md` | zod discriminatedUnion + catch-all | +| Python | `references/parser-python.md` | dict-based + a dataclass for Usage | + +## Commands for sanity-checking behavior + +```bash +# List session files (newest first) +ls -lt ~/.claude/projects/*/ | head -20 + +# Event type distribution in the latest file +jq -r .type < $(ls -t ~/.claude/projects/*/*.jsonl | head -1) | sort | uniq -c + +# Usage from the latest file, one line at a time +jq -c 'select(.type=="assistant") | {model: .message.model, usage: .message.usage}' \ + < $(ls -t ~/.claude/projects/*/*.jsonl | head -1) | head -3 + +# Total output_tokens by model, across all of today's sessions +jq -r 'select(.type=="assistant" and .timestamp > "'$(date -u +%Y-%m-%d)'") | + [.message.model, .message.usage.output_tokens] | @tsv' \ + ~/.claude/projects/*/*.jsonl | + awk -F'\t' '{m[$1]+=$2} END {for (k in m) printf "%-30s %d\n", k, m[k]}' +``` + +## Ironclad rules + +1. **Discard unknown types**. Default to a design that doesn't break when Claude Code updates add more of them. +2. **`thinking` is included in output_tokens**. Don't count it twice. +3. **Determine the model family by substring**. Never rely on exact matches. +4. **Keep the pricing table centralized in one place**. A comment noting "as of 2026-04" is mandatory. +5. **Aggregate per-file → per-session**. Even for analysis spanning multiple days, apply the `timestamp` filter first. +6. **Read-only**. This skill's code never rewrites session files (the verification `jq` commands are also read-only). + +## Anti-patterns + +- **Tailing the log with the `notify` crate**: drops events due to rotation +- **Parsing `type` into an enum via exact matching**: panics / unwraps on unknown types +- **Reading `usage` from events other than `assistant`**: it simply doesn't exist there +- **Hashing model names via exact match**: version suffixes cause everything to miss +- **Folding `cache_*` into the input_tokens calculation**: mixing 4 rates that differ produces roughly a 5x discrepancy +- **Recovering cwd by reverse-decoding encoded-cwd**: malfunctions due to `-` ambiguity. Read `assistant.cwd` instead +- **Loading the whole file with `read_to_string`**: balloons memory usage on large sessions. Stream with `BufReader` + a line iterator instead diff --git a/claude/skills/claude-session-jsonl/references/parser-python.md b/claude/skills/claude-session-jsonl/references/parser-python.md new file mode 100644 index 0000000..516b96c --- /dev/null +++ b/claude/skills/claude-session-jsonl/references/parser-python.md @@ -0,0 +1,47 @@ +> **Source of truth:** `claude/ja/skills/claude-session-jsonl/references/parser-python.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# Minimal parser (Python) + +Use a dict-based approach and discard anything where `type != "assistant"`. The dataclass is only for usage. + +```python +import json +from dataclasses import dataclass, field + +@dataclass +class Usage: + input_tokens: int = 0 + output_tokens: int = 0 + cache_creation_input_tokens: int = 0 + cache_read_input_tokens: int = 0 + + @property + def context_size(self) -> int: + return ( + self.input_tokens + + self.cache_creation_input_tokens + + self.cache_read_input_tokens + ) + +def parse_line(line: str): + line = line.strip() + if not line: + return None + d = json.loads(line) + if d.get("type") != "assistant": + return None + msg = d.get("message", {}) + u = msg.get("usage") or {} + tools = [c.get("name") for c in msg.get("content", []) if c.get("type") == "tool_use"] + return { + "model": msg.get("model"), + "tools": tools, + "usage": Usage( + input_tokens=u.get("input_tokens", 0), + output_tokens=u.get("output_tokens", 0), + cache_creation_input_tokens=u.get("cache_creation_input_tokens", 0), + cache_read_input_tokens=u.get("cache_read_input_tokens", 0), + ), + "timestamp": d.get("timestamp"), + } +``` diff --git a/claude/skills/claude-session-jsonl/references/parser-rust.md b/claude/skills/claude-session-jsonl/references/parser-rust.md new file mode 100644 index 0000000..93877ce --- /dev/null +++ b/claude/skills/claude-session-jsonl/references/parser-rust.md @@ -0,0 +1,68 @@ +> **Source of truth:** `claude/ja/skills/claude-session-jsonl/references/parser-rust.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# Minimal parser (Rust) + +Use serde's tagged enum plus a `#[serde(other)]` catch-all to safely ignore unknown types. + +```rust +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub(crate) enum Event { + Assistant(AssistantEvent), + #[serde(other)] + Other, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct AssistantEvent { + pub message: AssistantMessage, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct AssistantMessage { + #[serde(default)] pub model: Option<String>, + #[serde(default)] pub content: Vec<ContentBlock>, + #[serde(default)] pub usage: Option<Usage>, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub(crate) enum ContentBlock { + ToolUse { #[serde(default)] name: String }, + #[serde(other)] Other, +} + +#[derive(Debug, Default, Deserialize, Clone, Copy)] +pub(crate) struct Usage { + #[serde(default)] pub input_tokens: u64, + #[serde(default)] pub output_tokens: u64, + #[serde(default)] pub cache_creation_input_tokens: u64, // 5min + 1hr total + #[serde(default)] pub cache_read_input_tokens: u64, + #[serde(default)] pub cache_creation: Option<CacheCreation>, +} + +#[derive(Debug, Default, Deserialize, Clone, Copy)] +pub(crate) struct CacheCreation { + #[serde(default)] pub ephemeral_5m_input_tokens: u64, + #[serde(default)] pub ephemeral_1h_input_tokens: u64, +} + +impl Usage { + /// (5min, 1hr) breakdown for cost calc. Old logs without `cache_creation` + /// are treated as all-5min for backward compat. + pub fn cache_creation_split(&self) -> (u64, u64) { + match self.cache_creation { + Some(c) => (c.ephemeral_5m_input_tokens, c.ephemeral_1h_input_tokens), + None => (self.cache_creation_input_tokens, 0), + } + } +} + +pub(crate) fn parse_line(line: &str) -> serde_json::Result<Option<Event>> { + let t = line.trim(); + if t.is_empty() { return Ok(None); } + Ok(Some(serde_json::from_str(t)?)) +} +``` diff --git a/claude/skills/claude-session-jsonl/references/parser-typescript.md b/claude/skills/claude-session-jsonl/references/parser-typescript.md new file mode 100644 index 0000000..59ce2e8 --- /dev/null +++ b/claude/skills/claude-session-jsonl/references/parser-typescript.md @@ -0,0 +1,40 @@ +> **Source of truth:** `claude/ja/skills/claude-session-jsonl/references/parser-typescript.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# Minimal parser (TypeScript) + +Use zod's discriminatedUnion plus a `.or(z.object({ type: z.string() }))` catch-all to safely ignore unknown types. + +```ts +import { z } from "zod"; + +const Usage = z.object({ + input_tokens: z.number().default(0), + output_tokens: z.number().default(0), + cache_creation_input_tokens: z.number().default(0), + cache_read_input_tokens: z.number().default(0), +}); + +const ContentBlock = z.discriminatedUnion("type", [ + z.object({ type: z.literal("tool_use"), name: z.string() }), + z.object({ type: z.literal("text"), text: z.string().optional() }), + z.object({ type: z.literal("thinking"), thinking: z.string().optional() }), +]).or(z.object({ type: z.string() })); // catch-all + +const AssistantEvent = z.object({ + type: z.literal("assistant"), + message: z.object({ + model: z.string().optional(), + content: z.array(ContentBlock).default([]), + usage: Usage.optional(), + }), +}); + +const Event = z.discriminatedUnion("type", [AssistantEvent]) + .or(z.object({ type: z.string() })); // catch-all + +export function parseLine(line: string) { + const t = line.trim(); + if (!t) return null; + return Event.parse(JSON.parse(t)); +} +``` diff --git a/claude/skills/commit-push-branch/SKILL.md b/claude/skills/commit-push-branch/SKILL.md new file mode 100644 index 0000000..f057758 --- /dev/null +++ b/claude/skills/commit-push-branch/SKILL.md @@ -0,0 +1,185 @@ +--- +name: commit-push-branch +description: Cuts a new branch and commits & pushes the working-tree changes with a message that follows past commit style (type prefix / ticket ID / Co-Authored-By). Used for 「branch 切って commit & push して」「PR 用に push」 etc. +--- + +> **Source of truth:** `claude/ja/skills/commit-push-branch/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# commit-push-branch + +A skill that cuts a new branch, creates a commit following past style, and pushes it. PR creation is separate (`gh pr create`). + +## Applicability + +- Inside a git repository +- The working tree has changes to commit (something shows up in `git status`) +- Remote `origin` is configured + +## Procedure + +### Step 1: Check status + +```bash +git status +git diff --stat +git diff --cached +git log -3 --format='%H%n%B%n---' # 過去スタイル把握 +``` + +### Step 2: Extract past style + +From the `git log -3` output: +- **Conventional type prefixes** (`chore:`, `feat:`, `fix:`, `docs:`, `refactor:`, `test:`) +- **Title language** (English / Japanese) +- **How ticket IDs are placed** (`(PROJ-123)` / `Refs: PROJ-123` / `(#123)` etc., depends on repo convention) +- Whether **multi-line HEREDOC** is used +- Whether a **Co-Authored-By line** is included +- **PR merge style** (squash or merge commit) + +### Step 3: Decide the type and branch name + +| Change content | type | branch prefix | +|---|---|---| +| New feature | `feat` | `feat/` | +| Bug fix | `fix` | `fix/` | +| Infra / config / build / tooling | `chore` | `chore/` | +| Docs only | `docs` | `docs/` | +| Refactor (no behavior change) | `refactor` | `refactor/` | +| Adding tests | `test` | `test/` | + +Branch name patterns: + +| Case | Format | Example | +|---|---|---| +| With a ticket | `<prefix>/<ticket-id>-<description-slug>` | `chore/proj-123-add-feature` | +| Without a ticket | `<prefix>/<description-slug>` | `docs/api-error-codes-cleanup` | + +Keep the description slug short in kebab-case (3-5 words). Don't identify a branch by ticket ID alone — always attach a slug that describes the content. + +If past PRs have a convention of including a ticket ID, attach one; if the convention is ticket-less (mainly seen with `docs/` / `chore/` types), use slug only. **The decision follows the result of Step 2 ("Extract past style")** (this table is just a pattern example and does not override Step 2). + +### Step 4: Create the branch + +```bash +git checkout -b <prefix>/<ticket-id>-<slug> +``` + +If it collides with an existing branch, add a suffix like `-N`. + +### Step 5: Explicit add + +**Don't use** `-A` / `-a`. Explicitly list new / edited files: + +```bash +git add <file1> <file2> <dir1>/ <dir2>/ +git status # 確認 +``` + +Confirm no secret patterns like `.env` / `*.pem` / `credentials*` are included. + +### Step 6: Commit + +**By default, use a single-line title only.** `-m "<title>"` is sufficient. Don't default to HEREDOC + body. + +```bash +git commit -m "<type>: <変更内容>" +# または scope 付き +git commit -m "<type>(<scope>): <変更内容>" +``` + +What goes in the title = **the change content only**. Don't put why / background / impact scope / rot risk / ticket context in either the title or the body (these can be tracked via the PR description / Notion ticket / git history). + +Good examples: +- `chore: ブートログから phase フィールドを削除` +- `docs(api): SearchMeta を RequestMeta にリネームし common.proto へ切り出す` +- `chore: translate Makefile comments and error messages to English` + +Bad examples (verbose / includes why): +- `chore: ブートログから phase フィールドを削除\n\nphase 値はログに焼き付けると rot するだけで利得がない。` +- `chore: 前 commit で導入した slog.Info の "phase" 引数を削除して rot を回避` + +#### Exceptions where body / HEREDOC is used + +Only write a body in the following cases: +- **Breaking change** → include a `BREAKING CHANGE: <impact>` line +- There's a **genuinely non-obvious why** that can't be captured in the PR description (rare) +- **Past style consistently requires HEREDOC + body** (based on Step 2's findings) + +Template for the exception case: + +```bash +git commit -m "$(cat <<'EOF' +<type>: <変更内容> + +<最小限の why。1-2 行。> + +Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> +EOF +)" +``` + +Notes: +- Use HEREDOC with `'EOF'` (single-quoted) to suppress expansion +- Include `Co-Authored-By:` **only if past commits have it; omit it if they don't**. Even for a short commit, you can add it by passing two `-m` flags like `-m "..." -m "Co-Authored-By: ..."` (depends on repo convention) +- Whether to attach a ticket ID like `(PROJ-123)` also depends on past style. It's fine to omit it if the title would get too long + +#### Handling GPG signing hangs + +In environments with `commit.gpgsign=true` set, `git commit` can hang waiting for pinentry (an agent environment has no TTY so the pinentry GUI can't launch, or the gpg-agent cache has expired and it's waiting for a passphrase). + +Handling steps: + +1. **Set the Bash tool's timeout to 30 seconds when running `git commit`** (don't wait the default 2 minutes) +2. **When a hang / timeout is detected**: confirm via `git status` that the staged state is preserved (the commit failed, but files remain staged) +3. Check for a hung process with **`ps aux | grep -E "gpg|git commit" | grep -v grep`**, and ask the user to kill it if needed +4. **Guide the user through a manual commit**: + - Run `echo test | gpg --clearsign > /dev/null` in a separate terminal → enter the passphrase in pinentry → this warms the cache + - Or have the user run the following command directly: + ```bash + git commit -m "<type>: <変更内容>" -m "Co-Authored-By: <name> <email>" + ``` +5. **After the manual commit completes, once the user signals something like "commit done," the skill proceeds to push** (after confirming via `git log --oneline -1`) +6. Once the cache is warmed, subsequent commits / pushes will also go through on the agent side (the following Step 7 push can also be run by the skill) + +Notes: +- The skill must not work around this on its own with `--no-gpg-sign` or `commit.gpgsign=false` (same spirit as Iron Rule #2, "no `--no-verify`" — safety skips only happen when the user explicitly requests them) +- The staged state isn't destroyed even if it hangs, so don't rush to reset + +### Step 7: Push + +```bash +git push -u origin <branch-name> +``` + +Direct pushes to main / master trigger a warning (this assumes Step 4 has already guaranteed we're on a working branch). + +### Step 8: Completion report + +| Item | Value | +|---|---| +| Branch | `<prefix>/<ticket-id>-<slug>` | +| Commit | `<short-sha>` (`<type>: ...`) | +| Files | `<n>` files (+<additions>/-<deletions>) | +| PR creation URL | (extracted from the push output: `https://github.com/<org>/<repo>/pull/new/<branch>`) | + +PR creation is a separate task. If the user instructs it, continue with `gh pr create`. + +## Iron rules + +1. **Create a new commit**: don't use `--amend` (it could destroy the previous commit) +2. **No `--no-verify`**: respect the pre-commit hook. If it fails, fix the root cause +3. **Don't push a branch directly to main**: always use a working branch +4. **Don't use `git add -A` / `-a`**: prevent secrets from leaking in by listing files explicitly +5. **Respect past style**: align with the type / language / ticket notation / Co-Authored-By conventions from the last 3 commits +6. **PR only after user instruction**: the skill goes up to push. `gh pr create` happens only if the user instructs it +7. **Default to a single-line title, content only**: don't write why / background / impact scope. Only write a body for breaking changes / a genuinely non-obvious why + +## Anti-patterns + +- `git commit -am` (misses untracked files + blindly commits everything staged) +- Casually using `git push --force` even on a working branch +- Writing HEREDOC as `EOF` (unquoted), causing variable expansion +- Committing with an English title without checking past style (e.g., the repo actually has a Japanese title convention) +- Unilaterally deciding to add or omit `Co-Authored-By` (match the repo's convention) +- Writing why, rot risk, or "in order to ~" in the title (write the change content only) +- Defaulting to a HEREDOC + multi-line body even for a simple commit (if one line suffices, use one line) diff --git a/claude/skills/ddd-hexagonal/SKILL.md b/claude/skills/ddd-hexagonal/SKILL.md new file mode 100644 index 0000000..35a208c --- /dev/null +++ b/claude/skills/ddd-hexagonal/SKILL.md @@ -0,0 +1,270 @@ +--- +name: ddd-hexagonal +description: Reference skill for DDD + Hexagonal architecture. Covers layer boundaries / dependency direction / Port-Adapter / ACL / Aggregate / Repository / DTO conversion / cross-cutting concerns. Consult it for questions like 「層曖昧」「責務違反」「port の切り方」or during design review. Not a procedural skill. +--- + +> **Source of truth:** `claude/ja/skills/ddd-hexagonal/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# ddd-hexagonal + +A reference skill collecting the idioms and principles of DDD + Hexagonal architecture. Consult it as a criterion for design decisions, reviews, and identifying refactor candidates. + +## Applicability + +- Projects that adopt DDD + Hexagonal (`internal/{domain,application,interfaces,adapters}/` layout) +- Referenced by the code-refactor-advisor agent +- Situations involving decisions about layer boundaries / port design / how to split adapters + +## 1. Layer definitions + +| Layer | path | Responsibility | Inner layers it may depend on | +|---|---|---|---| +| **Domain** | `internal/domain/` | Entity / Value Object / Aggregate / Domain Service / Domain Event / sentinel errors. **Has no external dependencies** (pure logic) | (innermost layer, no dependencies) | +| **Application** | `internal/application/` | Use case orchestration / Application Service / Port (interface definitions) / DTO | Domain | +| **Interfaces** | `internal/interfaces/` | Inbound adapters (gRPC handler / HTTP handler / CLI / interceptors / wire shape ⇄ application DTO conversion) | Application + Domain | +| **Adapters** | `internal/adapters/` | Outbound adapters (DB / vendor SDK / external API implementations, implement Ports) | Application (to implement Ports) + Domain (to reference types) | + +Detection signals: +- Domain imports adapter / application (dependency direction violation) +- Adapter imports interfaces (lateral dependency, prevents reuse) +- Application directly imports an adapter (= missing port abstraction) + +## 2. Dependency direction (the inward-pointing principle) + +``` +Interfaces ──┐ ┌── Adapters + ├──> Application ──> Domain <──────┤ + └─────────────────────────────────┘ +``` + +- **Dependencies always point inward** (outer → inner). The inner layer (Domain) knows nothing about the outer layers (Application / Adapters) +- Communication with the outside happens via a **Port (interface)**. Application defines the Port, Adapter implements it +- When inner → outer communication is needed, invert it with a mechanism such as **Domain Event + subscription on the outside** + +Detection signals: +- `import ".../adapters/..."` / `import ".../interfaces/..."` inside Domain code +- `import ".../adapters/concrete-vendor"` inside Application code (= direct import of a concrete adapter, bypassing the port) + +## 3. Port-Adapter pattern + +- **Port** = an interface (defined in the Application layer). Example: `EmbeddingPort` / `VectorStorePort` +- **Adapter** = an implementation of a Port (in the Adapters layer). Example: `openai.Adapter` (= implements `EmbeddingPort`) / `pinecone.Adapter` (= implements `VectorStorePort`) +- Ports are **vendor-neutral**. They must not leak SDK-specific types (e.g. pass `[]float32`; never let `openai.EmbeddingResponse` appear in a port shape) +- A Port's signature should have **only the minimum necessary methods**. Don't add methods to the port that the adapter doesn't use (a single-method port named with an `I` suffix or `xxxer` naming is fine) + +Example: +```go +// Application layer: port definition +type EmbeddingPort interface { + Embed(ctx context.Context, req EmbedRequest) (*EmbedResult, error) +} + +// Adapter layer: port implementation +package openai +type Adapter struct { ... } +func (a *Adapter) Embed(ctx context.Context, req ports.EmbedRequest) (*ports.EmbedResult, error) { + // SDK-specific logic stays contained within the adapter +} +``` + +Detection signals: +- SDK-specific types appear in a Port shape (`*openai.EmbeddingResponse` / `*pinecone.QueryResponse`, etc.) +- An Adapter does not implement a Port (= its role as an adapter is unclear, gets imported ad hoc and directly) +- Application code receives an adapter as a concrete type (= cannot be mocked, a fake cannot be injected in tests) + +## 4. Anti-Corruption Layer (ACL) + +- A translation layer that converts **vocabulary / types / exception models** at the boundary with external systems / vendors +- At the boundary inside the adapter, convert **vendor-specific → port-neutral** and wrap **vendor-specific errors → port sentinel errors** +- The same applies in the opposite direction (interface → application): convert wire shape (proto / JSON) → application DTO + +Example: +```go +// ACL inside the adapter +func (a *Adapter) Embed(ctx context.Context, req ports.EmbedRequest) (*ports.EmbedResult, error) { + sdkResp, err := a.client.Embeddings.New(ctx, openai.EmbeddingNewParams{...}) + if err != nil { + return nil, fmt.Errorf("openai: %w: %w", ports.ErrEmbeddingProviderUnavailable, err) + } + // convert sdkResp.Data → []ports.Vector + return toPortResult(sdkResp), nil +} +``` + +Detection signals: +- A handler takes/returns SDK types as arguments/return values (= missing ACL, application becomes vendor lock-in) +- The Application layer receives a `pinecone.Client` directly (not via a Port) +- Vendor-specific errors leak directly into the application layer / handler (no sentinel wrap) + +## 5. Ubiquitous Language (UL) + +- Domain terminology must use **the same vocabulary across business / docs / code** +- Example: if the docs call it a 「ナレッジチャンク」(knowledge chunk), the code should also use `Chunk` (avoid mixing aliases such as `Document` / `Item`) +- Align wire shapes (proto field names / JSON keys) with the UL as well +- If departments / stakeholders have diverging vocabulary → unify it in the docs first (fix the vocabulary via an ADR) + +Detection signals: +- Different names for the same concept across packages (`User` / `Member` / `Account` / `Principal` mixed together) +- proto field names differ from Go struct field names (`product_ids` ⇔ `ProductIDList`) +- Terminology differs between docs and code (docs: 「コレクション」("collection") / code: `Index` and `Collection` mixed) + +## 6. Aggregate / Entity / Value Object + +- **Entity**: an object that has an identity (ID). E.g. `Chunk{ID, Text, ...}` +- **Value Object**: has no identity, judged by value equality. E.g. `Vector []float32` / `EmbedRequest{Inputs []string}` +- **Aggregate**: a consistency boundary. Internal entities are only manipulated via the Aggregate Root +- The Aggregate Root **protects business invariants**. Example: if `Document` is the root, `Document.AddChunk(c)` guarantees the continuity of chunk_sequence + +Note: +- Over-applying Aggregate / Value Object modeling is YAGNI. **Simple data with no business rules** is fine as a plain struct +- During the Phase 1 prototype period, start with a plain struct like `Hit{ChunkID, Text, ...}` and promote it once business rules emerge + +Detection signals: +- No distinction between Entity and Value Object (= everything is a plain struct, identity isn't handled via ID comparison) +- Internal entities are edited directly from the outside, skipping the Aggregate Root + +## 7. Application Service vs Domain Service + +- **Application Service**: orchestration at the use-case level. Achieves one business operation by calling multiple ports / domain entities +- **Domain Service**: domain logic that doesn't fit inside a single entity (operations between entities / domain rule validation) +- Both are stateless. Application Service receives injected dependencies (a set of ports + logger); Domain Service only receives domain objects + +Example: +```go +// Application Service (recommended pattern) +type Service struct { + embedding ports.EmbeddingPort + vectorStore ports.VectorStorePort +} +func (s *Service) Search(ctx, in SearchInput) (*SearchOutput, error) { + // Embed → vector search → convert to hits (use case orchestration) +} +``` + +Detection signals: +- Domain Service imports a port (dependency direction violation) +- Application Service only does validation (= no business orchestration, could be folded into the handler) +- Application Service has only one method (= only a single use case; sometimes there's little value in making it a struct) + +## 8. Repository pattern + +- Abstraction over DB / persistent stores is the **Repository** (a specialized form of port) +- Split granularity per entity: `ChunkRepository` (CRUD on Chunk) / `DocumentRepository` +- Search-only / read models are sometimes split into a separate Repository (`ChunkSearchRepository`) +- **Vendor-neutral**. Must not leak SQL / table names / SDK details + +Example: +```go +type ChunkRepository interface { + Save(ctx context.Context, c Chunk) error + GetByID(ctx context.Context, id string) (*Chunk, error) + Delete(ctx context.Context, id string) error +} +``` + +Detection signals: +- SQL strings appear as arguments/return values on a Repository +- A Repository contains business logic instead of CRUD (= responsibility confusion with Application Service) + +## 9. DTO / wire shape vs domain shape + +- Insert a **DTO conversion** at each layer boundary: + - `Interfaces` boundary: wire shape (proto message / JSON request) ⇄ Application DTO (`SearchInput` / `SearchOutput`) + - `Application` boundary: Application DTO ⇄ Domain entity / Value Object + - `Adapters` boundary: Domain ⇄ vendor SDK shape (Anti-Corruption Layer) +- Place DTO conversion helpers at the boundary layer (e.g. `toProtoResponse` inside `internal/interfaces/grpc/search_handler.go`) +- DTO conversion should preferably be a pure function (easy to test) + +Detection signals: +- A Domain entity has proto field tags / JSON tags (= wire shape and domain shape are mixed together) +- Wire shape flows through the application layer unconverted (= missing DTO conversion) + +## 10. Cross-cutting concerns + +- **Logging / Tracing / Auth / Rate Limit / Recovery** are cross-cutting concerns; **place them at the interfaces / adapters boundary** +- gRPC: `UnaryServerInterceptor` / HTTP: middleware +- Do not import them directly into the Application / Domain layer (= keep them separate from business logic) +- Example: place context keys / logger factories in `internal/observability/`, and have each interceptor consume them + +Detection signals: +- `slog.Info(...)` is called directly inside an Application Service / Domain Service (= cross-cutting concern leaking into business logic) +- An auth check is written inline inside a handler (= should be factored out into an interceptor / middleware) + +## 11. Domain Event (optional) + +- Publish state changes within an aggregate as events, and subscribe to them from an outer layer +- To avoid a reverse-direction dependency: the domain only publishes events; subscribing happens in application / adapter +- Watch out for over-introducing this: unnecessary during the Phase 1 prototype period; consider it in Phase 2 once audit / metrics / integrations increase + +## 12. Summary of detection signals (for identifying refactor candidates) + +When called from an agent, detect violations with the following grep patterns: + +| Violation | Detection grep | +|---|---| +| Domain → outer-layer import | `grep -r 'import.*adapters\|import.*interfaces' internal/domain/` | +| Application → concrete adapter import | `grep -r 'import.*adapters/[a-z]\+/' internal/application/ \| grep -v 'application/ports'` | +| Adapter → interfaces import | `grep -r 'import.*interfaces' internal/adapters/` | +| SDK-specific type in a Port shape | `grep -rn 'pineconego\.\|openaigo\.\|aws\.' internal/application/ports/` | +| Direct logger call inside Application Service | `grep -rn 'slog\.Info\|slog\.Error' internal/application/ \| grep -v 'logger\.'` | +| Direct SDK type import inside handler / service | `grep -rn 'pineconego\|openaigo' internal/interfaces/ internal/application/` | +| SQL string in Repository | `grep -rn 'SELECT\|INSERT\|UPDATE\|DELETE' internal/application/ports/` | + +## 13. Configuration injection (functional option + ConfigMap) + +Expose configuration overrides in **three tiers**: + +| Priority | Path | Purpose | +|---|---|---| +| 1 | **default const** (`defaultXxx`, hardcoded in code) | Phase 1 verification, a safe base value | +| 2 | **functional option** (injected via `WithXxx(...)` at construction time) | per-deployment override, the wiring path for global config | +| 3 | **proto field** (injected per request) | **Exception**, only when individual per-request handling is truly unavoidable (requires user approval) | + +Normal operation uses (1) + (2). Extending the proto is a last resort. + +Example: +```go +const defaultRenderDPI = 150 +func WithDPI(dpi int) PDFOption { return func(pp *PDFParser) { pp.dpi = dpi } } + +// cmd/api-server/main.go (wiring) +parser := adapters.NewPDFParser(vlm, adapters.WithDPI(cfg.Parser.DPI)) // ConfigMap value +``` + +Detection signals (refactor candidates): +- No `defaultXxx` const in the adapter / service → numeric literals scattered throughout the code +- `WithXxx(...)` Option is not exported → the value cannot be overridden from the cmd side +- An adapter struct field is public → direct assignment by the caller breaks immutability +- Adapter-internal values are exposed via a proto field → wire surface bloats + +If the project has an ADR adopting functional options, refer to that as well. + +--- + +## False positive criteria + +In the following cases, do not treat a hit on this skill's detection signals as a violation: + +- **Generated-code origin**: symbols / files generated by protoc / buf / openapi-generator / `go generate`, etc. (typically: the file starts with a `Code generated by ... DO NOT EDIT.` line). These should be fixed at the source schema, not by hand-editing the generated Go code +- **Language / library idiomatic patterns**: things like named returns with defer-recover in `func(...) (resp any, err error)`, fixed signatures such as `http.Handler`, or the convention of taking `context.Context` as the first argument take priority over this skill's rules +- **Intentional design exceptions**: design decisions whose intent is explicitly stated in code / docs (e.g. using `context.Background()` inside a library to detach a shutdown context from a signal-aware ctx) +- **Public API compatibility**: exported symbols that cannot be changed for backward compatibility (prefer proposing a migration strategy over a rename) + +When in doubt, don't exclude it — flag it in the output as a "false positive candidate" and defer to the user's judgment. + +## What this skill should output + +When called from the code-refactor-advisor agent: +- A **per-layer responsibility map** for the target codebase (file × layer × responsibility) +- A list of **layer boundary / dependency direction violations** (file:line + description of the violation) +- Pointing out **missing Port / Adapter / ACL** +- Pointing out **Ubiquitous Language drift** +- A remediation approach (moving to a different layer / extracting a port / adding a DTO conversion layer / etc.) + +## References + +- Eric Evans, "Domain-Driven Design" +- Vaughn Vernon, "Implementing Domain-Driven Design" +- Alistair Cockburn, "Hexagonal architecture": https://alistair.cockburn.us/hexagonal-architecture/ +- Mark Seemann, "Dependency Injection in .NET" (discussion of dependency direction) +- Refer to the project's ADR (the decision to adopt DDD + Hexagonal) if one exists diff --git a/claude/skills/go-bootstrap/SKILL.md b/claude/skills/go-bootstrap/SKILL.md new file mode 100644 index 0000000..20a2959 --- /dev/null +++ b/claude/skills/go-bootstrap/SKILL.md @@ -0,0 +1,234 @@ +--- +name: go-bootstrap +description: Sets up a working skeleton for a new or existing Go project in one shot (module initialization / directory skeleton / .golangci.yaml / Makefile / .gitignore / golangci-lint installation). Used for requests like 「Go プロジェクトのセットアップ」「Go module を切って」「lint と Makefile 用意して」. +--- + +> **Source of truth:** `claude/ja/skills/go-bootstrap/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# go-bootstrap + +A skill that sets up a new Go project from scratch to the point where "`make build` / `make test` / `make lint` pass." Intended to be used once per repository. + +## Applicability + +- Go (assuming 1.22+, using the Toolchain Directive) is installed locally +- The repository root doesn't yet have a `go.mod` (or it's fine to re-set-up) +- Adopting DDD + Hexagonal (`internal/{domain,application,adapters}/`). Confirm with the user if a different layout is wanted. + +## Procedure + +### Step 0: Confirm prerequisites + +```bash +go version # 1.22 or later +ls -la # check existing files +test -f go.mod && cat go.mod # check existing module +``` + +Things to confirm (consolidate into a single AskUserQuestion turn): +- module path (e.g., `github.com/<org>/<repo>`) +- Go version (default: the latest stable version installed locally) +- Binary layout (e.g., a single `cmd/<bin1>` / `cmd/<bin1>` + `cmd/<bin2>` / other. Name `<bin1>` etc. to match the project.) +- Out of scope: buf / gRPC / IaC are separate skills (this skill covers only the skeleton) + +### Step 1: `go mod init` + +```bash +go mod init <module-path> +``` + +Read `go.mod` to confirm the module path and go directive. + +### Step 2: `cmd/` skeleton + +Write a minimal stub for each binary: + +```go +// Package main is the entry point for the <bin-name> binary. +package main + +import "log/slog" + +func main() { + slog.Info("<bin-name> starting") +} +``` + +Comments in English; adopt `log/slog`. + +### Step 3: internal / apis / deploy skeleton + +Manage empty directories with `.gitkeep`: + +- `internal/domain/.gitkeep` +- `internal/application/.gitkeep` +- `internal/adapters/.gitkeep` +- `apis/proto/server/v1/.gitkeep` (if proto usage is planned) +- `deploy/.gitkeep` (if IaC placement is planned) + +### Step 4: `.golangci.yaml` (v2 format) + +```yaml +version: "2" + +run: + timeout: 5m + +linters: + default: standard + enable: + - misspell + - revive + - gocritic + exclusions: + paths: + - ^gen/ + - ^cli/go/gen/ + rules: + - path: _test\.go + linters: + - errcheck + - gocritic + +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - <module-path> +``` + +`paths` is interpreted as a **regex**, so anchor it at the start like `^gen/`. + +### Step 5: Makefile + +```makefile +.DEFAULT_GOAL := help + +.PHONY: help build test lint generate tidy + +help: ## Show this help + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' + +build: ## Build all binaries (go build ./...) + go build ./... + +test: ## Run tests with race + coverage + go test -race -coverprofile=coverage.out ./... + +lint: ## Run golangci-lint + @if ! command -v golangci-lint >/dev/null 2>&1; then \ + echo "golangci-lint is not installed. Install with: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest"; \ + exit 1; \ + fi + golangci-lint run ./... + +generate: ## Generate code (go generate) + go generate ./... + +tidy: ## Tidy go.mod / go.sum + go mod tidy +``` + +### Step 6: Update `.gitignore` (append to the existing file) + +Create it if there is no existing `.gitignore`; otherwise append to the end: + +``` +# ===== Go ===== +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ +dist/ + +# Test & coverage +*.test +*.out +*.prof +coverage.* +coverage/ + +# Go workspace +go.work +go.work.sum + +# Dependency directory (legacy) +vendor/ + +# ===== Generated code ===== +/gen/ +``` + +Respect any existing exclusions related to secret management, such as `!.env.example`. + +### Step 7: Sync CLAUDE.md / README.md + +If `CLAUDE.md` exists, add a "## Development Commands" section (check first that it isn't already present): + +```markdown +## Development Commands + +Run primary commands via the `Makefile`. + +| Command | Purpose | +|---|---| +| `make help` | Show the list of targets | +| `make build` | `go build ./...` | +| `make test` | Run all tests with race + coverage | +| `make lint` | `golangci-lint run` (v2 series) | +| `make generate` | `go generate ./...` | +| `make tidy` | `go mod tidy` | + +Uses the v2 series of `golangci-lint`. `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest` installs it into `$(go env GOPATH)/bin`. +``` + +### Step 8: Install golangci-lint (only if not already installed) + +```bash +which golangci-lint || go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest +golangci-lint --version +``` + +### Step 9: Verify it works + +```bash +make build && echo BUILD_OK +make test && echo TEST_OK +make lint && echo LINT_OK +``` + +If revive's `package-comments` warning appears, add an English package comment to the `main.go` in `cmd/` (it should already be there if it followed the Step 2 template). + +### Step 10: Report completion + +Report using a DoD-to-step correspondence table: + +| Item | Status | +|---|---| +| `go build ./...` | done | +| `make test` / `make lint` | done | +| module path | <confirmed value> | +| Directory skeleton (cmd/internal/apis/proto/deploy) | done | +| `.golangci.yaml` (v2) | done | +| Makefile (build/test/lint/generate/tidy) | done | + +## Iron rules + +1. **Respect existing files**: don't overwrite an existing `.gitignore` / `CLAUDE.md` / `README.md` — append to them +2. **Comments in English**: comments in Go files are in English +3. **Stay in scope**: implementing features like buf / gRPC / VectorStorePort is out of scope; hand off to the next skill / agent +4. **Don't commit**: committing is delegated to a separate skill (`/commit-push-branch`) + +## Anti-patterns + +- Overwriting `go mod init` without confirmation +- Fully replacing `.gitignore`, ignoring its existing content +- Writing `cmd/*/main.go` comments in Japanese +- Writing `.golangci.yaml` in v1 format (v2 is now the mainstream) +- Skipping verification (`make build/test/lint`) diff --git a/claude/skills/go-style/SKILL.md b/claude/skills/go-style/SKILL.md new file mode 100644 index 0000000..80a8190 --- /dev/null +++ b/claude/skills/go-style/SKILL.md @@ -0,0 +1,205 @@ +--- +name: go-style +description: Reference skill for Go idioms, naming, error handling, context, logging, concurrency, and lint/format conventions. Consulted for questions like 「Go お作法的にどう」「命名規約」「error wrap」 or during code review. Not a procedural skill. +--- + +> **Source of truth:** `claude/ja/skills/go-style/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# go-style + +A reference skill collecting Go idioms and conventions. Used as the judgment criteria for implementation, review, and identifying refactor candidates. + +## Applicability + +- Any context involving Go code (`*.go` / `Makefile` / Go code generated from `*.proto`) +- Referenced by the code-refactor-advisor agent +- When the user asks things like 「Go 的にどう書く」「命名どうする」 + +## 1. Package layout + +- Binary entry points live at `cmd/<bin>/main.go`. One binary = one subdirectory. +- Code under `internal/` cannot be imported from other repositories. Implementation generally lives here. +- `pkg/` is only for code intended to be imported externally. If there's no expected reuse, put it under `internal/`. +- When adopting DDD + Hexagonal: `internal/{domain,application,interfaces,adapters}/` (see the `ddd-hexagonal` skill for layer boundary details) +- Separate the proto / OpenAPI / SDK public surface into `apis/` (recommended convention) +- One package = one responsibility. Catch-all packages like `util` / `common` / `helper` are an anti-pattern. + +Detection signals: +- Functions with multiple, unrelated responsibilities coexisting under `internal/util/` +- Business logic living inside `cmd/` +- `pkg/` is used but there is no actual external import + +## 2. Naming + +- Decide exported (starts with an uppercase letter) vs. unexported (starts with a lowercase letter) based solely on **whether it needs to be public**. Don't capitalize just because "it might be made public someday." +- Acronyms are **kept fully uppercase**: + - 3+ letters: `URL` `ID` `HTTP` `API` (`Url` / `Id` / `Http` are not allowed) + - 2 letters: `AI` / `IO` / `UI` / `OS` / `DB` are also fully uppercase (`Ai` / `Io` / `Ui` are not allowed) + - Brand names / conventionalized spellings containing multiple acronyms: follow the standard spelling, e.g. `OpenAI` / `OpenAPI` / `gRPC` + - When a brand name appears at the start of an unexported identifier: lowercase the brand-name part too, to stay consistent with the acronym rule (`openaiKey` / `openapiSpec`). Mixed case inside an acronym, like `openAIKey`, is not allowed. +- Receiver names: **use the same short name across all methods for the same type** (`s *Server` / `c *Client`). `this` / `self` are forbidden. +- Interface names: use an `-er` suffix for single-method interfaces (`Reader` / `Writer`). Use a noun for multi-method interfaces (`FileSystem`). +- Error types/variables get an `Err` / `err` prefix (`var ErrNotFound = ...` / `func ... (err error)`) +- Enum values get the type name as a prefix (`type Model string` + `ModelTextEmbedding3Large Model = "..."`) +- Test names should be intent-based, like `TestX_When_..._Should_...` or `TestX_RejectsEmpty` (see the `go-test` skill for details) + +Detection signals: +- Mixed-case acronyms such as `Url` / `Id` / `Http` / `Ai` / `Io` / `Ui` +- Case mixing between a brand-name acronym and other characters, like `openAIKey` (when using a brand name at the start of an unexported identifier, lowercase it to something like `openaiKey`) +- Receiver is `this *X` / `self *X` +- Receiver names varying by file for a single type, e.g. `s`, `srv`, `server` +- A symbol made exported solely because "it might be made public someday" + +## 3. Error handling + +- An error is a **value**. It's the last return value; don't omit `if err != nil { return ..., err }`. +- Use `fmt.Errorf("context: %w", err)` for wrapping. Keep errors matchable via `errors.Is` / `errors.As` for sentinel/typed checks. +- Sentinel errors are package-level `var Err... = errors.New("pkg: human message")`. Prefixing the message with `package:` makes the wrap chain easier to read. +- Don't distinguish errors by string comparison (`err.Error() == "..."` is forbidden). +- Use panic **only for truly exceptional states** (e.g., invariant violations at program startup). Don't use it in normal flow. +- Error messages **start lowercase, with no trailing period** (`io: short read`, not "Io: short read.") +- For wrapped chains, **separate the "sanitized outward-facing message" at API boundaries from the "internal wrap chain."** Don't leak upstream details to clients (emit internal details via the logger instead). + +Detection signals: +- `errors.New(fmt.Sprintf(...))` (should use `fmt.Errorf` instead) +- String comparison of errors +- Upstream SDK errors leaking into a handler's / gRPC server's error response +- panic / `log.Fatal` present in library code (outside `cmd/`) + +## 4. Context + +- `context.Context` is **the first function argument**. Use the unified name `ctx context.Context`. +- Don't store ctx in a struct field (this mixes request scope with struct lifetime). +- Propagate ctx explicitly (pass it through function calls; no implicit globals). +- `context.Background()` is only for entry points (main / test / signal handler). Inside libraries, use the caller's ctx. +- `ctx.Value` is only for cross-cutting metadata (request_id / trace span / auth principal). Pass anything that can be passed as an argument as an argument. +- Avoid collisions on ctx keys by using a **package-private struct type** (`type requestIDKey struct{}`). +- For cancellation/deadlines, have the library side observe `<-ctx.Done()`, and unblock long IO with `select`. + +Detection signals: +- `context.Background()` called inside a library +- `ctx.Value("string-key")` (a string key with collision risk) +- `ctx context.Context` as a struct field +- ctx placed last or in the middle of the argument list + +## 5. Logging + +- Adopt the standard `log/slog`. Structured JSON is recommended (`slog.NewJSONHandler`). +- Level usage: `Error` (action required) / `Warn` (abnormal but processing continues) / `Info` (normal operation) / `Debug` (development only) +- Structure with key-value pairs: `slog.String("key", "value")` / `slog.Int(...)` / `slog.Duration(...)` +- Pass ctx via `LogAttrs(ctx, ...)` so interceptors/middleware can attach span / request_id. +- **Don't log PII / credentials** (API key / password / token / personal identifier). +- Log messages should be fixed strings; pass variable values as attrs (controls cardinality and keeps things greppable). +- In error logs, also emit the root cause of the wrap chain as an attr (`slog.String("error", err.Error())`). + +Detection signals: +- Operational logs emitted via `fmt.Println` / `log.Print*` (standard `log` or `fmt`) +- Interpolated messages like `slog.Info(fmt.Sprintf("user %s logged in", userID))` +- API key / password / personal identifiers mixed into a log message + +## 6. Concurrency + +- A goroutine **must always have a guaranteed exit path** (cancellation / done channel / WaitGroup / errgroup). +- Typical causes of goroutine leaks: a blocked send/recv on an unbuffered channel, forgetting to observe ctx, forgetting `g.Wait()` on an errgroup. +- **Make explicit what a mutex protects** (a comment right before the struct field, or list the protected fields right after `mu sync.Mutex`). +- **The sender closes the channel.** The receiver closing it is forbidden (panic / race risk). +- Restrict channel direction in function signatures (`<-chan T` / `chan<- T`). +- `errgroup.Group` provides context-linked cancellation, captures the first error, and syncs via `g.Wait()`. Recommended for running multiple IO operations concurrently. +- `sync.Once` is for initialization use. For state needed multiple times, use a different pattern (mutex / atomic). +- Minimize shared mutable state; where possible, use immutable copies + channel passing. + +Detection signals: +- A goroutine that observes neither `ctx.Done()` nor a channel receive (a leak candidate) +- A mutex whose protected target is unclear (just `mu sync.Mutex` with nothing else) +- Channel direction written as bidirectional (`chan T`) in a signature +- A panic inside a goroutine not recovered via defer (crashes the entire parent goroutine) + +## 7. Lint / format + +- Enforce `gofmt` / `goimports` (set `goimports`'s local prefix to match each repository's module path). +- Use `golangci-lint` v2 (configured via `.golangci.yaml`). Required in CI. +- Recommended linters: `errcheck` / `govet` / `staticcheck` / `ineffassign` / `unused` / `gosimple` / `gofmt` / `goimports` +- Suppressions (`// nolint:`) **require a reason comment** (`// nolint:errcheck // intentional fire-and-forget`). +- Auto-fixable issues should be fixed locally before CI (`gofmt -w` / `goimports -w` / `golangci-lint run --fix`). + +Detection signals: +- `// nolint:` with no reason +- Import order mixing stdlib / third-party / local +- Ignored `errcheck` warnings (discarding an error return value into `_`; add `// intentional` if needed) + +## 8. godoc / Comments + +- Exported symbols require godoc (a sentence starting with the symbol name, `// FuncName ...`). +- Package comments go in one file per package as `// Package <name> ...` (usually `doc.go` or the main file). +- Comments should **explain WHY**. The WHAT is told by the code itself (assuming well-named identifiers). +- "Want to do X in the future" goes in a `TODO:`. Don't leave phase names or ticket IDs in comments. +- Mark `Deprecated:` explicitly in the target symbol's godoc (`// Deprecated: use NewX instead.`) +- Prefer line comments (`//`) over multi-line block comments. + +Detection signals: +- An exported func / type / var with no godoc +- A comment that only states the WHAT (merely translating the code) +- A comment referencing timeline/ticket scope, such as `Phase 0` or a release-cycle-named ticket + +## 9. Import order + +```go +import ( + // Group 1: standard library + "context" + "fmt" + + // Group 2: third-party + "github.com/spf13/cobra" + + // Group 3: local (current module) + "github.com/<org>/<module>/internal/foo" +) +``` + +- Separate the three groups with blank lines (automatic via goimports's `-local` setting). +- Alphabetical sort within each group. +- Aliases should be **kept to the minimum necessary** (only for collision avoidance / abbreviation purposes). `pgsql "github.com/lib/pq"` is fine, `f "fmt"` is not. + +Detection signals: +- No group separation (everything mixed into a single import block) +- Unnecessary aliases (e.g., `io2 "io"`) + +## 10. Type safety / nil safety + +- Use `interface{}` / `any` only when the type is genuinely indeterminate. If the type is known, use an explicit type. +- Guard against nil pointer dereferences by **rejecting nil in the constructor** (`func NewX(...) (*X, error) { if ... == nil { return nil, ErrNil }`). +- Assigning to a nil map panics; appending to a nil slice is fine (don't confuse the two). +- Pointer receivers and value receivers **must not be mixed** (be consistent for a given type; if any method mutates, all methods should use pointer receivers). + +Detection signals: +- `any` / `interface{}` used in business logic (fine if it's a passthrough) +- Pointer receiver and value receiver mixed for the same type +- A map assignment missing a nil check + +--- + +## False positive criteria + +Even if a detection signal from this skill fires, do **not** treat it as a violation when any of the following apply: + +- **Originates from generated code**: symbols/files generated by protoc / buf / openapi-generator / `go generate`, etc. (typically identified by a `Code generated by ... DO NOT EDIT.` line at the top of the file). Improvements belong on the source schema side; don't hand-edit generated Go code. +- **Language/library idiomatic patterns**: things like `func(...) (resp any, err error)` with named returns + defer-recover, fixed signatures like `http.Handler`, or the convention of taking `context.Context` as the first argument take priority over this skill's rules. +- **Intentional design exceptions**: design decisions whose intent is explicitly documented in code/docs (e.g., using `context.Background()` inside a library to detach a shutdown context from a signal-aware ctx). +- **Public API compatibility**: exported symbols that can't be changed for backward compatibility (prefer proposing a migration strategy over a rename). + +When in doubt, don't exclude it — flag it in the output as a "false positive candidate" and defer to the user's judgment. + +## What this skill should output + +When invoked by the code-refactor-advisor agent, this skill is expected to provide: +- A **list of violations / improvement opportunities, organized by section**, for the target code +- For each finding, the **detection signal** (which pattern triggered it) and the **supporting section** (the section number within this skill) +- A remediation approach (align with convention / leave as an intentional exception / discuss separately) + +## References + +- Go Code Review Comments: https://go.dev/wiki/CodeReviewComments +- Effective Go: https://go.dev/doc/effective_go +- Go Proverbs: https://go-proverbs.github.io/ +- Uber Go Style Guide: https://github.com/uber-go/guide/blob/master/style.md diff --git a/claude/skills/go-test/SKILL.md b/claude/skills/go-test/SKILL.md new file mode 100644 index 0000000..b0f36ef --- /dev/null +++ b/claude/skills/go-test/SKILL.md @@ -0,0 +1,318 @@ +--- +name: go-test +description: Reference skill for Go test design and test-code idioms. Covers naming, table-driven tests, t.Parallel, the race detector, fakes/mocks, boundary values, and how to interpret coverage. Consulted for questions like 「test 名どうする」「table-driven にする」「coverage 何 % まで」. Not a procedural skill. +--- + +> **Source of truth:** `claude/ja/skills/go-test/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# go-test + +A reference skill collecting Go test design and test-code idioms. Used as the judgment criteria for implementation, review, and identifying refactor candidates. + +## Applicability + +- Any context involving Go test code (`*_test.go`) +- Referenced by the code-refactor-advisor agent +- When the user asks things like 「test 何書く / どう書く」 + +## 1. Naming + +- Function names are **intent-based** (`TestX_RejectsEmpty` / `TestX_When_..._Should_...`). Index-based names like `TestX1`, `TestX2` are forbidden. +- Table-driven case names **must convey intent** (`{name: "EmptyProductID", ...}`; simply `{name: "case1"}` is not allowed). +- Don't put spaces / special characters in sub-test names (`t.Run("foo bar")` gets normalized to `foo_bar`, which is hard to grep for). +- Helper naming: use a prefix that **conveys intent** (`new` / `make`), like `newFixture(t)` / `newAdapterWithFake(t, fake)`. + +Detection signals: +- An explanatory inline comment inside a test (meaning the test name / variable names fail to convey intent) +- Case names that are index-based, like `case1`, `case2` +- Names like `t.Run("with spaces")` + +## 2. Table-driven test + +```go +tests := []struct { + name string + input X + want Y + wantErr error +}{ + {name: "HappyPath", input: ..., want: ...}, + {name: "RejectsEmpty", input: X{}, wantErr: ErrEmpty}, +} + +for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := SUT(tc.input) + if !errors.Is(err, tc.wantErr) { + t.Fatalf("err = %v, want %v", err, tc.wantErr) + } + if tc.wantErr != nil { return } + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("got = %v, want %v", got, tc.want) + } + }) +} +``` + +- Struct fields should be **kept to the minimum necessary**. If a value is the same across all cases, pull it out as a constant outside the struct. +- Put a **sentinel error** (verifiable via `errors.Is`) in `wantErr`. String comparison is forbidden. +- When expecting no error, state `wantErr: nil` explicitly (omitting it still works, but doesn't convey intent). + +Detection signals: +- A table-driven test with only one case (a regular test function would suffice) +- More than half the struct fields are empty / the same value (the struct is overengineered) +- String comparison via `wantErr string` + +## 3. `t.Parallel()` + +- **Enabling it by default is recommended.** It shortens overall test wall time and increases the effectiveness of the race detector. +- Inside table-driven `t.Run(tc.name, func(t *testing.T) { t.Parallel(); ... })`, **be careful about capturing `tc` inside the closure**: + ```go + for _, tc := range tests { + tc := tc // unnecessary on Go 1.22+, but required on <= 1.21 + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ... + }) + } + ``` + From Go 1.22+, for-loop variable scope became per-iteration, which resolved the capture trap, but if CI might run on an older Go version, explicitly localizing the variable is the safe choice. +- Tests that can't run in parallel (touching global state / order-dependent) should not call `t.Parallel()`. +- Tests that call `t.Parallel()` run concurrently with each other; tests that don't call it run sequentially after the parallel tests complete. + +Detection signals: +- Not all cases in a table-driven test call `t.Parallel()` (wasting wall time) +- Referencing the outer loop variable without binding `tc` in the closure capture (a bug on older Go) +- Calling `t.Parallel()` while touching global state (race / flaky) + +## 4. `t.Helper()` + +- **Call `t.Helper()` at the top** of helper functions (assertions / fixture construction). This makes failure line reporting point to the caller, easing debugging. +- Not needed for pure data-generation helpers (if they never actually call `t.Errorf` / `t.Fatal`). + +Detection signals: +- A helper that calls `t.Errorf` / `t.Fatal` without calling `t.Helper()` + +## 5. Setup / Teardown + +- Register resource cleanup **inline via `t.Cleanup()`** (the Go 1.14+ idiom). +- Consolidate common fixtures into a helper constructor (`newFixture(t)`), registering `t.Cleanup` internally. +- When a struct field holds per-case setup for a table test, name it **`beforeFunc` / `afterFunc`** (don't use the words `setup` / `teardown`). +- The struct-field approach itself is discouraged by default (boilerplate / closure-capture pitfalls / poor compatibility with `t.Parallel()`). +- Exception: only add `beforeFunc` as a struct field when each case genuinely needs different setup. `afterFunc` is usually omitted (it's covered by `t.Cleanup` inside `beforeFunc`); include it only when needed. + +Example: +```go +tests := []struct { + name string + beforeFunc func(t *testing.T) *Fixture + want Result +}{ + { + name: "EmptyDB", + beforeFunc: func(t *testing.T) *Fixture { + return newFixture(t) + }, + want: Result{...}, + }, +} +``` + +Detection signals: +- Cleanup done solely via `defer cleanup()` (won't run if the whole test fails outright; `t.Cleanup` is safer) +- Struct field names of `setup` / `teardown` (recommendation is `beforeFunc` / `afterFunc`; defer to project convention if one exists) +- The same setup function attached to every case (should be extracted into a common helper) + +## 6. Race detector + +- **Always on.** Run with `go test -race` (e.g., always-on via including `-race` in `make test`). +- Data races in concurrent code can't be detected via static analysis; running it constantly in CI has high value. +- Cost: ~10x slower + ~5-10x more memory. Not a concern at unit-test scale. +- Benchmarks (`go test -bench`) can't produce meaningful measurements with race enabled → run separately. +- If you exceptionally need to exclude race, use a build tag (`//go:build !race`). + +Detection signals: +- CI / Makefile defaulting to plain `go test` (i.e., no race) +- Running `go test -bench` with `-race` (measurement becomes meaningless) + +## 7. Test design technique + +### Black-box vs White-box + +- **Black-box**: verifies the input → output contract without depending on internal implementation. Writing it as `package x_test` (an external test package) lets you test using only the public API, without touching implementation symbols. +- **White-box**: intentionally exercises branches / internal state. Written as `package x` (the same package), touching unexported symbols. +- **When to use which**: use black-box for testing the API contract, white-box to fill in implementation coverage. Both can coexist. + +### Boundary value analysis + +- Intentionally test off-by-one, upper/lower bounds, 0, empty, nil, max, min. +- Example: for a `top_k` field, test 0 / 1 / the upper bound / upper bound + 1 / negative numbers / large numbers. +- **For string boundaries, make rune count vs. byte count explicit**: + - Go's `len(string)` is a **byte count**; the character count (rune count) comes from `utf8.RuneCountInString` / `[]rune(s)`. + - For string APIs with an upper bound, the spec should decide which unit applies, and tests should match that. + - Multi-byte characters (Japanese / emoji) are roughly 1 rune ≈ 3-4 bytes. For a byte-based upper bound, **a boundary test using only multi-byte characters is mandatory** (a boundary test using only ASCII leaves a verification gap). + - Strings containing combining characters / variation selectors have rune count ≠ grapheme cluster count. APIs that operate at the grapheme level should separately use something like `golang.org/x/text/unicode/norm`. + +### Equivalence partitioning + +- One representative test per equivalence class. Trade off completeness for representative values rather than aiming for full coverage. +- Example: for a `query` string — "ASCII," "multi-byte," "emoji / combining characters," "empty string / whitespace only," "max character count / +1." + +### Negative test + +- Cover **rejection paths**, not just the happy path: + - validation failures (boundary violations for each field) + - upstream dependency errors (provider unavailable / timeout) + - context cancellation + - permission / ACL violations + +### Property-based test + +- Use `testing/quick` (standard library) / `pgregory.net/rapid` (third-party) for property-based tests. +- Applicability is limited: effective for pure functions, decoder-encoder roundtrips, order preservation, etc. +- For business logic, value-based tests are more readable. + +Detection signals: +- Only the happy path is tested; rejection / boundary cases are untested +- A numeric field like `top_k` missing tests for 0 / the upper bound / upper bound + 1 +- No test for multi-byte string input (a latent bug that would fail on a Japanese query) + +## 8. Choosing between Fake / Mock / Stub + +- **Fake**: a working lightweight implementation (in-memory DB / in-memory adapter). Can reproduce behavior even for complex logic. +- **Stub**: just returns a fixed response. Input is ignored. +- **Mock**: validates input + asserts expected call counts (xUnit style). + +Recommended approach (defer to project convention if one exists): +- **Hand-written fakes are recommended** (don't use generation tools like testify / gomock). +- Rationale: minimal dependencies / the fake's behavior is fully contained in source code / avoids over-mocking. +- Cut narrow interfaces (the port pattern), and implement them for tests with a concise struct. + +Example (recommended pattern): +```go +type fakeVectorStore struct { + searchCalls []vectorSearchCall + respBy map[string][]ports.VectorSearchResult + err error +} + +func (f *fakeVectorStore) Search(_ context.Context, c string, req ports.VectorSearchRequest) ([]ports.VectorSearchResult, error) { + f.searchCalls = append(f.searchCalls, vectorSearchCall{c, req}) + if f.err != nil { return nil, f.err } + if r, ok := f.respBy[c]; ok { return r, nil } + return nil, nil +} +``` + +Detection signals: +- A mock framework such as testify / gomock / mockery being newly introduced +- A mock's expectations only verifying "number of times called," leaving input/output unchecked + +## 9. Test fixture / golden file / testdata + +- Place large inputs / expected outputs in the `testdata/` directory (a Go idiom; excluded from the build). +- Golden file pattern: save expected output to something like `testdata/golden/<name>.json`, and diff it within the test. + - Use an update flag (a custom flag like `-update`) to bulk-update when the implementation changes. +- Share fixtures under `testdata/fixtures/`, loaded via a test helper. + +Detection signals: +- A huge expected-value string inline in test code (should be extracted into testdata) +- testdata not tagged with a build tag, or not excluded from the Go build (the directory name `testdata` is automatically excluded by Go) + +## 10. Test categories / build tags + +- **Unit test**: within the same package, no external IO, on the order of milliseconds. Runs always via `*_test.go`. +- **Integration test**: connects to external dependencies (DB / API), run as a separate stage in CI. Separated via a build tag: + ```go + //go:build integration + + package x_test + ``` + Run with: `go test -tags=integration ./...` +- **E2E test**: full system startup + black-box. An outer directory like `tests/e2e/` + a build tag. + +Detection signals: +- A test that connects directly over the network to Pinecone / OpenAI running as a unit test (`go test ./...`) (flaky / external dependency) +- A test doing external IO without a separate build tag + +## 11. HTTP / gRPC test pattern + +### HTTP + +- Start an in-process server with `httptest.NewServer`, and point the SDK's base URL at it. +- Verify requests by asserting `r.URL` / `r.Body` inside the handler. +- Canned responses via `w.WriteHeader` + `w.Write([]byte(...))`. + +### gRPC + +- Use `bufconn.Listen` for an in-memory listener, and wire up the client with `grpc.NewClient(... grpc.WithContextDialer(bufDialer))`. +- If starting up the whole service is too heavy, calling the handler method directly is also fine (use bufconn if you also want to test interceptors). + +Detection signals: +- An HTTP test binding an actual TCP port (flaky / conflicts with parallel tests) +- A gRPC test starting a real network listener + +## 12. Time / Context determinism + +- Don't call `time.Now()` directly; inject `now func() time.Time` as a struct field / parameter. +- In tests, fix it with `now: func() time.Time { return time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) }`. +- Within tests, control ctx deadlines via `context.Background()` (entry point) or `context.WithTimeout`. +- Also inject the random seed / localize `math/rand/v2` to keep things deterministic. + +Detection signals: +- Production code calling `time.Now()` directly (can't be fixed in tests) +- Random / UUID generation coming from global state (can't be made deterministic) + +## 13. Interpreting coverage + +- Line coverage is a guide rail — **don't treat it as gospel**. +- Emphasize branch coverage / boundary coverage / important paths. +- Aiming for 100% produces artificial tests (pointless tests just to eliminate `if false { ... }`). +- DoD-based: write tests that satisfy the functional contract (acceptance criteria); don't aim for 100% internal coverage. +- Rule of thumb: business logic / handlers / adapters should be 80%+; generated code / `main` are excluded from coverage targets. + +Detection signals: +- A test with no assertions, existing only to bump coverage +- Coverage padded out by only the handler/service's happy path, while rejection paths are untested + +## 14. Avoiding flakiness + +Main causes of flakiness: +- **time-based**: waiting via `time.Sleep`, implicit dependence on `time.Now()` → fix via injection +- **ordering**: dependence on map iteration order → convert to a sorted slice before asserting +- **net IO**: binding an actual port → use httptest / bufconn +- **goroutine race**: wait for all goroutines to finish via `t.Cleanup`, sync via channel close +- **shared state**: mutating a global / package var under `t.Parallel()` → switch to local state within the test + +Detection signals: +- `time.Sleep` inside a test +- Comparing map iteration results as ordered (`reflect.DeepEqual([]Map, expectedSlice)` has no order guarantee) +- A goroutine still alive after the test ends + +--- + +## False positive criteria + +Even if a detection signal from this skill fires, do **not** treat it as a violation when any of the following apply: + +- **Originates from generated code**: symbols/files generated by protoc / buf / openapi-generator / `go generate`, etc. (typically identified by a `Code generated by ... DO NOT EDIT.` line at the top of the file). Improvements belong on the source schema side; don't hand-edit generated Go code. +- **Language/library idiomatic patterns**: things like `func(...) (resp any, err error)` with named returns + defer-recover, fixed signatures like `http.Handler`, or the convention of taking `context.Context` as the first argument take priority over this skill's rules. +- **Intentional design exceptions**: design decisions whose intent is explicitly documented in code/docs (e.g., using `context.Background()` inside a library to detach a shutdown context from a signal-aware ctx). +- **Public API compatibility**: exported symbols that can't be changed for backward compatibility (prefer proposing a migration strategy over a rename). + +When in doubt, don't exclude it — flag it in the output as a "false positive candidate" and defer to the user's judgment. + +## What this skill should output + +When invoked by the code-refactor-advisor agent: +- A **list of violations / improvement opportunities, organized by section**, for the target test code +- Each finding's **detection signal** + **supporting section number** +- A remediation approach (convert to table-driven / switch to fakes / adopt golden files / add boundary cases / etc.) + +## References + +- Go testing package doc: https://pkg.go.dev/testing +- Go Code Review Comments (Tests section): https://go.dev/wiki/CodeReviewComments +- Effective Go (Test section): https://go.dev/doc/effective_go +- Subtests and sub-benchmarks: https://go.dev/blog/subtests diff --git a/claude/skills/grill-me/SKILL.md b/claude/skills/grill-me/SKILL.md new file mode 100644 index 0000000..2a35acb --- /dev/null +++ b/claude/skills/grill-me/SKILL.md @@ -0,0 +1,12 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +> **Source of truth:** `claude/ja/skills/grill-me/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/claude/skills/k8s-style/SKILL.md b/claude/skills/k8s-style/SKILL.md new file mode 100644 index 0000000..a06c2c5 --- /dev/null +++ b/claude/skills/k8s-style/SKILL.md @@ -0,0 +1,405 @@ +--- +name: k8s-style +description: Reference skill for K8s manifests / workload design / RBAC / SecurityContext / NetworkPolicy / probes / resources / Kustomize / image operations. Consult it for questions like 「K8s 的にどう」「manifest 規約」「RBAC 最小権限」「probe どう書く」. Not a procedural skill. +--- + +> **Source of truth:** `claude/ja/skills/k8s-style/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# k8s-style + +A reference skill collecting Kubernetes-related idioms and conventions. Consult it as a criterion for creating manifests, reviews, and identifying refactor candidates. + +## Applicability + +- Any situation dealing with K8s manifests (`*.yaml` / `*.yml` containing `apiVersion` / `kind`) +- Situations dealing with Kustomize (`kustomization.yaml`) / Helm charts (`Chart.yaml` / `templates/` / `values.yaml`) +- Referenced by the code-refactor-advisor agent / security-review-local skill +- Situations where you're asked 「K8s 的にどう書く」「probe どうする」「最小権限」 + +--- + +## 1. Basic stance on manifests + +- 1 YAML = 1 resource, or group them as **one logical unit** (Deployment + Service + HPA + PDB) +- Default to `kubectl apply -f` (`kubectl create` is not idempotent, so don't use it in CI) +- **Always check the diff with `kubectl diff`** before making changes (recommended convention) +- Default to applying to production via CD (GitOps: ArgoCD / Flux). Directly running `kubectl apply` is forbidden +- Don't leave behind resources created with imperative commands (`kubectl run` / `kubectl expose` / `kubectl edit`) (state that doesn't exist in Git is forbidden) + +Detection signals: +- Resources exist on the cluster that aren't in any manifest (drift) +- Traces of `kubectl edit` (mismatch between the `last-applied-configuration` annotation and Git) + +--- + +## 2. API version / deprecated APIs + +- Don't use deprecated / removed APIs. Align `apiVersion` to **the latest stable** + - `Deployment` / `DaemonSet` / `StatefulSet` / `ReplicaSet` → `apps/v1` + - `Job` / `CronJob` → `batch/v1` + - `Ingress` → `networking.k8s.io/v1` (`extensions/v1beta1` / `v1beta1` have been removed) + - `HorizontalPodAutoscaler` → `autoscaling/v2` + - `PodDisruptionBudget` → `policy/v1` + - `PodSecurityPolicy` has been removed → migrate to **Pod Security Admission (PSA)** +- Detect deprecated APIs in CI with `pluto` + +--- + +## 3. Naming / Labels / Annotations + +### Naming (resource name) +- lowercase + `-` (RFC 1123). `_` is not allowed +- Length must be 63 characters or fewer (because it becomes the Service / Pod hostname) +- Don't embed the environment in the name (separate via namespace). Use `my-app` + namespace `prod` instead of `my-app-prod` + +### Recommended labels +**Attach to all resources** (with the `app.kubernetes.io/*` prefix): + +| label | example | requirement level | +|---|---|---| +| `app.kubernetes.io/name` | `my-app` | Required | +| `app.kubernetes.io/instance` | `my-app-prod` | Recommended | +| `app.kubernetes.io/version` | `1.2.3` | Recommended | +| `app.kubernetes.io/component` | `api` / `worker` | Recommended | +| `app.kubernetes.io/part-of` | `my-platform` | Optional | +| `app.kubernetes.io/managed-by` | `kustomize` / `argocd` | Recommended | + +### selector labels +- Deployment's `spec.selector.matchLabels` is **immutable**. Changing it later makes updates impossible +- Stabilize the Service's `selector` using `app.kubernetes.io/name` + `app.kubernetes.io/instance` +- Don't put environment-varying labels (commit SHA / build number) into the selector + +### annotations +- Namespace custom annotations using **reverse DNS** (`example.com/my-key`) +- Don't hand-write controller-managed annotations such as `kubectl.kubernetes.io/*` / `meta.helm.sh/*` + +Detection signals: +- Uppercase letters / underscores in names +- Missing recommended labels +- The selector includes a value that changes on rolling deploy + +--- + +## 4. Resource requests / limits + +### Required settings +- **Always set both `resources.requests` and `resources.limits`** (recommended convention) +- Without them, the pod gets BestEffort QoS and is the first to be killed + +### CPU vs Memory +- **Use CPU limits with caution**: they cause throttling. Running with requests only is also a common pattern +- **Always set a memory limit**: an OOM kill is safer than dragging down the entire node +- requests ≤ limits. Setting `requests = limits` gives **Guaranteed QoS** (for important workloads) + +### Sizing +- Don't guess. Base sizing **on measured values** from `kubectl top` / Prometheus / the VPA recommender +- Start conservative → observe and adjust (over-provisioning hurts cost and scheduling efficiency) +- For batch / job workloads, account for short-lived spikes + +Detection signals: +- `resources:` is empty / only `limits` / only `requests` +- memory `limits` is 10x or more the requests (wasted headroom) +- CPU `limits` is under 100m while there's a latency requirement (guaranteed to throttle) + +--- + +## 5. Probes (liveness / readiness / startup) + +**Use the three types appropriately.** Pointing all of them at the same endpoint is an anti-pattern: + +| probe | role | behavior on failure | +|---|---|---| +| `startup` | Determines whether startup is complete. Required if there's heavy init work (DB migration / cache warmup) | On failure, **kill and restart** | +| `liveness` | Health check. **Only catches deadlocks / hangs** | On failure, **kill and restart** | +| `readiness` | Whether traffic can be accepted. Reflects connectivity to dependencies (DB / cache) | On failure, **removed from the Service** (not killed) | + +### Best practices +- **Keep liveness minimal**: don't check internal app state / dependent DBs. Only "does the process respond" + - If liveness checks the DB, a DB outage causes all pods to restart, making things worse +- **readiness may include dependencies**: reflect the health of DB connections / cache connections / dependent services +- **Use the startup probe to avoid tuning liveness's `initialDelaySeconds`**: for slow-starting apps, give it grace time via startup, and keep liveness's `failureThreshold` short +- Have a dedicated endpoint for each probe (`/healthz` / `/readyz` / `/startupz`) + +Detection signals: +- liveness hits the DB +- All three probes use the same endpoint +- `initialDelaySeconds` is abnormally large (e.g. 300s) + +--- + +## 6. SecurityContext / Pod Security + +### Required at the Pod / Container level + +```yaml +spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: app + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] +``` + +### Forbidden +- `privileged: true` +- `hostNetwork: true` / `hostPID: true` / `hostIPC: true` +- `hostPath` mounts (especially `/` / `/etc` / `/var/run/docker.sock`) +- `runAsUser: 0` (root) +- `allowPrivilegeEscalation: true` +- Elevated capabilities such as `capabilities.add: ["SYS_ADMIN"]` + +### Pod Security Admission (PSA) +Enforce **restricted** / **baseline** / **privileged** via a namespace label. Use `restricted` for production / general workloads: + +```yaml +metadata: + labels: + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: latest +``` + +Detection signals: +- Missing `securityContext` / use of `hostPath` / running as root / `capabilities.drop` not specified + +--- + +## 7. RBAC (least privilege) + +### Start from default deny +- **Always specify** a ServiceAccount explicitly (don't use the default SA) +- If the pod doesn't need to call the K8s API, set `automountServiceAccountToken: false` +- Role / ClusterRole should include **only the necessary verbs / resources** + +### Forbidden +- `verbs: ["*"]` / `resources: ["*"]` / `apiGroups: ["*"]` +- Binding `cluster-admin` (don't grant it to people, let alone to SAs) +- Broad ClusterRoleBindings (first consider whether a Role + RoleBinding scoped to a namespace would suffice) + +### Automatic token mounting +- Only when necessary, use a short-lived projected token (TokenRequest API) + +Detection signals: +- `serviceAccountName` not specified (using the default) +- Wildcard verb / resource in a ClusterRole +- Using a ClusterRoleBinding when a RoleBinding would suffice + +--- + +## 8. NetworkPolicy + +### Introduce default deny + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all +spec: + podSelector: {} + policyTypes: ["Ingress", "Egress"] +``` + +Add one **default deny ingress / egress** policy per namespace, and open up necessary traffic with allow rules. + +### Common pitfalls +- **Forgetting egress to DNS (kube-dns)** → name resolution breaks and all pods die. Always allow it: + ```yaml + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + ``` +- Leaving egress fully open (`{}`) can't prevent data exfiltration +- Without a network policy controller (Calico / Cilium, etc.) in the cluster, NetworkPolicy is **ignored**. Confirm the prerequisite is in place + +Detection signals: +- A namespace with no NetworkPolicy at all / fully open egress / missing DNS allow rule + +--- + +## 9. Pod / Workload design + +### Deployment vs StatefulSet vs DaemonSet vs Job +- **stateless app** → Deployment +- **Needs stable identity / persistent volumes** → StatefulSet (DB / Kafka / etcd, etc.) +- **One pod per node** → DaemonSet (log collector / node exporter) +- **Runs to completion once / runs periodically** → Job / CronJob + +### Pod design +- 1 Pod = 1 main process. Don't cram multiple processes into a single container +- Only add a sidecar (proxy / log shipper) **when its role is clear**. Don't add one just because +- Limit init containers to **work that's only needed before startup** (migrations / fetching config) + +### Replicas / availability +- Production Deployments should have `replicas >= 2` + **a PodDisruptionBudget**: + +```yaml +apiVersion: policy/v1 +kind: PodDisruptionBudget +spec: + minAvailable: 1 # or maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: my-app +``` + +- If the cluster spans multiple zones, spread across zones with **topologySpreadConstraints** +- Keep `maxUnavailable` conservative so all pods don't go down simultaneously during a rolling update + +### Graceful shutdown +- Match `terminationGracePeriodSeconds` to the cleanup time needed after SIGTERM (default 30s) +- On the app side, set readiness to false upon receiving SIGTERM, then drain (a pattern of sleeping in a preStop hook) + +Detection signals: +- `replicas: 1` in production / no PDB / in-flight requests get dropped due to no preStop hook + +--- + +## 10. ConfigMap / Secret + +### ConfigMap +- Configuration values go in ConfigMap, secrets go in Secret (don't mix them) +- Bulk-injecting with `envFrom` bloats the environment / causes name collisions. **Use `valueFrom` for only the keys you need** +- Use immutable ConfigMaps (`immutable: true`) to suppress hot reload (reduces controller load) + +### Secret +- base64 is **not encryption**. Without encryption-at-rest configured, a Secret is effectively plaintext in etcd +- In production, use **External Secrets Operator / Sealed Secrets / Vault / a cloud secret manager** +- Injecting a Secret as an environment variable makes it visible via `/proc/<pid>/environ` → for sensitive values, consider **volume mount + readOnly + tmpfs** +- Don't commit Secrets to Git (this applies even if already base64-encoded) + +Detection signals: +- A key that looks like a password / token in a ConfigMap +- A Secret committed as **plain YAML** +- The same Secret value used across all environments + +--- + +## 11. Image / Tag / Pull policy + +### Tag +- **Don't use** `:latest` (not reproducible / can't track what was deployed during a rolling update) +- Combining a semver tag (`v1.2.3`) with a digest (`@sha256:...`) is safest +- Defaulting production manifests to a **digest pin** keeps them safe even if the tag gets overwritten + +### Pull policy +- If the tag is `:latest`, it implicitly becomes `Always` → when pinning a tag, **explicitly write `IfNotPresent`** +- Explicitly set `imagePullSecrets` for private registries + +### Image size / vulnerabilities +- Default to a distroless / chainguard / minimal base +- Use multi-stage builds so build dependencies aren't left behind +- Scan for CVEs with `trivy` / `grype`, and build SBOM generation into CI + +Detection signals: +- `:latest` tag +- pull policy not specified + a fixed tag +- An image that runs as root + +--- + +## 12. Kustomize (default) / Helm + +**Kustomize is the default.** Reasons: +- It reads as plain YAML (no template-engine quirks) +- Overlays are sufficient for handling environment differences in our own apps +- Partial overrides can be done locally via patches, without needing to expose every knob through values + +Use Helm only **when importing an OSS chart** / **when distribution is required**. + +### Kustomize conventions +- A `base/` + `overlays/{dev,staging,prod}/` structure +- Keep **patches minimal** in overlays. For plain values, override via `configMapGenerator` / `secretGenerator` +- Check the production diff with `kustomize build overlays/prod | kubectl diff -f -` → render with `kustomize build` in CI +- Default to **strategic merge patch (`patches:` with `target:`)**. Use JSON Patch only for more forceful changes +- Attach labels to all resources in bulk via `commonLabels` / `commonAnnotations` +- Use `namePrefix` / `nameSuffix` for environment-specific suffixes (watch out for selector immutability) +- Don't pollute the parent `base/` with environment-specific values (keep base environment-agnostic) + +### Conventions when using Helm +- `apiVersion: v2` in `Chart.yaml` +- Distinguish chart version from app version (`version` / `appVersion`) +- Make values.yaml **commented / typed** so users can read it +- Render with `helm template` → check with `kubectl diff` before running `helm upgrade` +- Even when pulling in an OSS chart, a structure where Kustomize's `helmCharts:` inflates values → overlay patch is easy to work with + +Detection signals: +- About to cut a new Helm chart for an in-house app (first consider whether Kustomize would suffice) +- Patches bloating in an overlay (a signal to reconsider the base design) +- base polluted with environment-specific values + +--- + +## 13. Lint / Validation + +Always wire these into CI: + +| tool | role | +|---|---| +| `kubeconform` | schema validation (consistency of apiVersion / kind) | +| `kube-linter` | configuration anti-patterns (no probes / privileged / no resources, etc.) | +| `conftest` (OPA / Rego) | custom policies (enforcing internal conventions) | +| `trivy config` | detects security misconfigurations in manifests | +| `kustomize build` | renderability + feeds the result into kubeconform / kube-linter | +| `pluto` | detects deprecated APIs | + +**At minimum**, include `kubeconform` + `kube-linter` + `trivy config`. Feeding the output of `kustomize build` into these in CI is the standard flow. + +--- + +## 14. Observability + +- Expose a **metrics endpoint** (`/metrics` in Prometheus format) on every workload +- Wire it up via a `prometheus.io/scrape` annotation on the pod, or a ServiceMonitor (Prometheus Operator) +- Emit logs to stdout / stderr **as structured JSON**. Don't write to files (ephemeral containers disappear) +- For traces, use the OpenTelemetry SDK + a sidecar / DaemonSet collector → backend (Tempo / Jaeger) +- Precompute important SLIs with a **Recording Rule** (avoid heavy computation at request time) +- Don't put high-cardinality labels / tags into metrics (it will break the time-series DB) + +--- + +## 15. Cost / efficiency + +- Unnecessary resource requests are a hidden cost. Base them on measured values from the VPA recommender +- Use HorizontalPodAutoscaler (`autoscaling/v2`) to **track load**. Consider custom metrics in addition to CPU +- Use Cluster Autoscaler / Karpenter for automatic node scaling +- Use Spot / Preemptible nodes for batch / dev (also conditionally for stateless production workloads) + +--- + +## 16. Related skills / agents + +- security review → `security-review-local` skill +- docs (ADR / Runbook / Design) → `tech-docs-writer` skill +- layer boundaries / responsibilities (app side) → `ddd-hexagonal` skill +- Go app conventions → `go-style` / `go-test` skill + +--- + +## Checklist (for manifest review) + +- [ ] `apiVersion` is stable / not deprecated +- [ ] recommended labels (`app.kubernetes.io/*`) are present on all resources +- [ ] `resources.requests` / `limits` are set (memory limit required) +- [ ] `liveness` / `readiness` / `startup` are used appropriately (liveness doesn't check dependencies) +- [ ] `securityContext` sets `runAsNonRoot` / `readOnlyRootFilesystem` / `capabilities.drop: ["ALL"]` +- [ ] `serviceAccountName` is explicit + RBAC is minimal + `automountServiceAccountToken: false` (if not needed) +- [ ] NetworkPolicy has default deny + necessary allows (watch out for forgetting the DNS allow) +- [ ] production `replicas >= 2` + PDB + topologySpreadConstraints +- [ ] image tag is pinned (digest recommended) + `pullPolicy` is explicit + distroless-based image +- [ ] Secrets are not committed in plaintext (External Secrets / Sealed Secrets / Vault) +- [ ] Kustomize's base is environment-agnostic / overlay patches are minimal +- [ ] `kubeconform` / `kube-linter` / `trivy config` are run in CI diff --git a/claude/skills/notion-ticket-plan/SKILL.md b/claude/skills/notion-ticket-plan/SKILL.md new file mode 100644 index 0000000..32293d7 --- /dev/null +++ b/claude/skills/notion-ticket-plan/SKILL.md @@ -0,0 +1,203 @@ +--- +name: notion-ticket-plan +description: Starting from a Notion / Linear / GitHub Issue ticket URL, explores related docs / ADRs and writes an implementation plan out to a plan file, then obtains plan-mode approval (planning only, does not implement). Use for "ticket に沿って計画して" ("plan according to this ticket"), etc. +model: claude-fable-5 +--- + +> **Source of truth:** `claude/ja/skills/notion-ticket-plan/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# notion-ticket-plan + +Starting from a ticket (Notion / Linear / GitHub Issue) URL, this skill explores the repository's docs / ADRs / existing code, and covers everything up through **writing an implementation plan out to a plan file and getting it approved**. It does not implement anything. + +## Applicability + +- Runs primarily in plan mode (implementation happens in a separate skill / a separate turn) +- Works best when the repository has design documents such as `docs/` or `ADR/` (but works without them too) + +## Procedure + +### Step 1: Fetch the ticket + +Read the content from the ticket URL. Note the `Definition of Done`, the scope, and any references to related ADRs / design docs. + +| ticket source | tool | +|---|---| +| Notion | `mcp__claude_ai_Notion__notion-fetch` | +| GitHub Issue / PR | `gh issue view` / `gh pr view` (Bash) | +| Linear | MCP if available, otherwise WebFetch the URL | +| other URL | WebFetch | + +Perspectives to extract: +- DoD (acceptance criteria) +- In-scope / out-of-scope +- Related tickets (predecessors / successors) +- Deadline / status + +### Step 1.5: Literal cross-check between the DoD and existing docs + +Both the DoD and the existing docs may be stale, so **read and compare them in parallel** to extract contradictions. + +1. Extract literals from the DoD (type names / method sets / placement paths / design responsibilities / naming) +2. Extract literals from the related docs (`docs/design/*.md`, ADRs) +3. **Exhaustively enumerate points of contradiction**: + - Naming (e.g., singular `product_id` vs. list `product_ids`) + - Method sets (e.g., 6 methods vs. 7 methods) + - Placement paths (e.g., `domain/ports` vs. `application/ports`) + - Design responsibility layer (e.g., is the ACL assembled in the adapter or in the application layer) +4. **Use AskUserQuestion to have the user settle on which literal to adopt** + - It's the user's call whether the DoD or the docs is newer / more trustworthy + - Flag doc-side corrections as a separate task if needed +5. Record the confirmed literals in the "Confirmed decisions" section of the plan file from Step 6 +6. **Classify each point of contradiction into Tier 1-4** (to judge the scope of doc / implementation updates) + - **Tier 1**: pure literal alignment (mechanical alignment of paths / file names / type names, etc.) → in scope for this ticket, fixed in the same commit as the implementation + - **Tier 2**: following a confirmed design decision (method removal / signature update, etc.) → in scope for this ticket, fixed in the same commit as the implementation + - **Tier 3**: requires a design decision (e.g., involves a change to the caller's responsibilities) + - Judgment axis: "does the DoD require implementing this change?" + - **If required by the DoD, implement it in the same PR** (ask the user for the design decision, but implementation stays in scope) + - If out of DoD scope, make it a separate ticket, and only flag it in this ticket + - **Tier 4**: a full rewrite of pseudocode / code examples (cosmetic but large in volume) → separate commit / separate ticket, only flag it in this ticket + +**Important**: Don't treat the DoD as the rule. Don't treat the docs as the rule. **Present both in parallel and let the user decide.** + +### Step 2: Explore related docs (launch up to 3 Explore subagents in parallel) + +Use Explore subagents to search the following in parallel: + +- Implementation plan files (e.g., `docs/plan/*.md`) for mentions around the relevant ticket +- ADRs (`docs/adr/*.md`) for related decisions +- Design docs (`docs/design/*.md`) +- README / CLAUDE.md for related information +- Existing code (around the relevant feature) +- Existing `.gitignore` / config files that affect scope + +Give each Explore agent "concrete file paths + a list of questions." Have it cite responses by file:line. + +### Step 3: Design with a Plan agent + +Launch a Plan subagent. Pass the information gathered in the previous steps as background, and have it write an implementation plan. Elements to include in the prompt: + +- The ticket's DoD (Step 1) +- Related ADR excerpts (Step 2) +- The state of existing files +- Confirmed premises +- Points you want it to consider (version selection, naming, relaxation rules, etc.) +- Out-of-scope items (to hand off to a later ticket) + +For complex tasks, run up to 3 in parallel taking different perspectives (conciseness vs. extensibility vs. maintainability). + +### Step 4: Directly read the key files + +Based on the Plan agent's response, directly check the files to be modified / related config files with Read. Verify that the plan's premises match the current state. + +### Step 5: Hash out open points with AskUserQuestion if needed + +If there are undecided points (a version value, a library choice, tone), narrow them to 1-2 questions via AskUserQuestion. Never use AskUserQuestion for "plan approval" itself (that's ExitPlanMode's job). + +### Step 5.5: 6-perspective review with the api-design-review skill + +**Before** writing out the plan, invoke the `api-design-review` skill to catch gaps across the following 6 perspectives: + +1. Separation of client abstraction vs. server-side expansion +2. Both the read and write sides of the ACL +3. Forward-compatibility (can enums / fields / RPCs be added non-breakingly) +4. Enumeration of edge cases ("what happens in this situation?") +5. Consistency with the existing source of truth (grep-first) +6. Compliance with memory conventions + +If gaps are detected, get the user's judgment via AskUserQuestion, reflect it in the plan, then proceed to Step 6. This suppresses the "noticing it later" that comes from a reviewer's perspective. Record the skill's result in the plan file as a "## Design review (api-design-review)" section. + +Judgment axis for when the skill invocation is unnecessary: simple additions / internal refactors with no impact on existing contracts / bug fixes / minor changes. It's **mandatory** for tickets involving a new service / new RPC / new enum / new ACL model / new ADR / a change to a design responsibility layer. + +### Step 6: Write out the plan file + +- Write the final plan into the plan file path presented by plan mode +- **Treat the per-project plans dir's `<ticket-slug>.md` (`~/.claude/projects/<encoded>/plans/<ticket-slug>.md`, see "Where to store plan / session state files" in CLAUDE.md) as the canonical session state file for the plan.** Subsequent agents also Read it to pick up the state +- Structure: + +``` +# <Phase> / <Ticket ID>: <title> — Implementation plan + +## Context +Why this change is needed (based on the DoD) + +## Confirmed decisions +Enumerate the literals confirmed in Step 1.5. The source of truth on re-implementation, used by later agents for context bootstrapping. Permanent design decisions go here (never change these again; they're the target to update the spec side against). +| Decision item | Adopted literal | Basis (DoD or docs) | + +## Scope decisions (intentional limitations coming from the DoD) +Scope explicitly limited by the DoD, e.g., "stub only is fine" / "implement in a later ticket." Not a deviation, so it's not flagged in the PR description. +| Scope limitation item | DoD basis | Follow-up ticket | + +## Spec deviations (flagged in the PR description, for reviewer confirmation) +List only the "permanent structural choices" where the DoD / docs and the implementation diverge. Sort Scope decisions / Phase 2+ migration into their own categories; leave here only **the items you want the reviewer to confirm are OK**. +| # | Deviation | Remediation plan (update spec / keep as-is / revisit later) | + +## Phase 2+ migration (to become follow-up tickets) +Provisional implementations at the prototype stage that are planned to be refactored at production migration time. Record as a follow-up in a Notion ticket comment, etc.; not implemented in this PR. +| Provisional implementation | Future form | Follow-up ticket / link | + +## Carryover (existing issues, separate ticket) +Existing issues out of scope for this ticket that this PR won't touch, but that you want to keep visible. +| Existing issue | Impact scope | Ticket to address it | + +## Documentation updates (by Tier) +Organize the contradictions extracted in Step 1.5 by Tier. State explicitly how each is handled in this ticket. +| Target doc | Fix content | Tier (1/2/3/4) | Handling (same commit / same PR / separate ticket) | + +## Current state (updated as you go) +Progress state. So that a later agent / your future self can bootstrap context with a single Read. +- Stage X complete / Y in progress +- Latest commit: <hash> +- Pending questions: ... + +## Design review (api-design-review) +Summary of the api-design-review skill run from Step 5.5. Keeps the gap-catching auditable. +- Whether each of the 6 perspectives found something +- Items already reflected in fixes +- Items resolved by user judgment +- Items left outstanding (follow-up ticket) + +## Key design decisions +| Decision item | Decision | Basis | + +## Implementation steps (execution order) +### Step 1: ... +- New / edited files + a summary of the content + +## Mapping between DoD and implementation steps +| Notion DoD item | Corresponding step | Verification method | + +## Anticipated pitfalls + +## Verification procedure (after implementation is complete) + +## Handoff to the next ticket (out of scope) + +## References +- ticket URL +- paths under docs/... +``` + +### Step 7: Get approval via ExitPlanMode + +Call ExitPlanMode to request plan approval. Write the Bash categories needed for implementation into allowedPrompts (e.g., `[{tool: "Bash", prompt: "run go commands"}]`). + +## Iron rules + +1. **Strict plan-mode discipline**: the only file edits are to the plan file. Everything else is read-only +2. **Get approval via ExitPlanMode**: don't ask "is this OK?" / "OK to proceed?" in plain prose +3. **Respect past memory feedback**: e.g., omitting prototype-stage premises, terminology conventions (e.g., tenant→company), comment language, etc. +4. **Keep the plan scan-friendly**: make heavy use of tables and bullet lists. Avoid long prose paragraphs +5. **Make out-of-scope items explicit**: always separate out things handled by a different ticket / a later phase +6. **Maintain the state file**: update the plan file every time a key decision is confirmed / a spec deviation is discovered. Keep it in a state where a later agent / your future self can bootstrap context with a single Read +7. **Tier-based handling of doc updates**: + - Tier 1+2 (mechanical alignment / following a confirmed design): fix in the same commit as the implementation + - Tier 3 (requires a design decision): implement in the same PR if required by the DoD, otherwise a separate ticket if out of DoD scope + - Tier 4 (full pseudocode rewrite): separate commit / separate ticket + + Record the target doc / fix content / Tier / handling in the plan file's "Documentation updates" section + +## Behavior on completion + +Once the user approves via ExitPlanMode, the skill ends. Implementation happens in a separate skill (e.g., `/go-bootstrap`, the `go-feature-tdd` subagent) or in the next turn. diff --git a/claude/skills/retrospect/SKILL.md b/claude/skills/retrospect/SKILL.md new file mode 100644 index 0000000..926579f --- /dev/null +++ b/claude/skills/retrospect/SKILL.md @@ -0,0 +1,61 @@ +--- +name: retrospect +description: Lightweight skill for recording one insight at the end of a dev cycle / work session — a sticking point, a redo, or a newly discovered convention or environment quirk. Use for "retrospect して" ("do a retrospective"), "振り返り記録して" ("record a retrospective"), etc. Does not analyze, aggregate, or turn findings into improvement PRs (that's improve-harness's responsibility). +--- + +> **Source of truth:** `claude/ja/skills/retrospect/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# retrospect + +Lightweight recording at the end of a cycle. **Keep it to a granularity you can write in one minute.** Analysis, aggregation, and turning findings into improvement PRs are improve-harness's responsibility; this skill only records. + +## Procedure + +### Step 1: Gather key points + +Identify the following from the preceding cycle / work (if none apply, it's fine to finish without recording anything): + +- Sticking points / things that had to be redone +- Corrections or feedback from the user +- Newly discovered conventions or environment quirks +- Deficiencies or gaps in skills / agents / rules + +### Step 2: Determine the category + +| category | meaning | +|---|---| +| skill-gap | a gap or defect in a skill's procedure or perspectives | +| rule-gap | a gap or ambiguity in CLAUDE.md / rules/ | +| env-quirk | an environment / tool quirk (reproducible behavior) | +| spec-gap | a deficiency in the spec contract or spec content | + +### Step 3: Record it in an insights file + +Write **one insight per file** in the per-project dir for the target project: + +- path: `~/.claude/projects/<encoded>/insights/<YYYYMMDD>-<slug>.md` +- `<encoded>` = the cwd's absolute path with `/` and `.` replaced by `-` (same convention as "Where to store plan / session state files" in CLAUDE.md) + +```markdown +--- +date: <YYYY-MM-DD> +category: skill-gap | rule-gap | env-quirk | spec-gap +ticket: <ticket URL / id / none> +target: <the relevant skill / agent / rules file, if any> +--- + +## What happened + +<1-3 lines describing what happened> + +## Proposal + +<1-3 lines on how it should be fixed (write "undecided" if unclear)> +``` + +## Iron rules + +1. Recording only. Do not analyze, fix, or turn it into a PR +2. One insight = one file. Do not batch multiple into one write +3. Do not transcribe secrets or the full ticket body (reference by id / URL only) +4. Do not force a write when a cycle has nothing to report (don't accumulate noise) diff --git a/claude/skills/rust-bootstrap/SKILL.md b/claude/skills/rust-bootstrap/SKILL.md new file mode 100644 index 0000000..7657fa6 --- /dev/null +++ b/claude/skills/rust-bootstrap/SKILL.md @@ -0,0 +1,131 @@ +--- +name: rust-bootstrap +description: Sets up a working skeleton for a Rust workspace in a new or existing directory in one shot (crates/* layout, centralized lints, Makefile, deny.toml, CI, all the way through installing rustup. The only task runner is make). Used for things like 「Rust の workspace 切って」「Rust プロジェクトのセットアップ」「複数 tool 入れる Rust リポジトリにして」. +--- + +> **Source of truth:** `claude/ja/skills/rust-bootstrap/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# rust-bootstrap + +A skill that sets up a Rust workspace from scratch to the point where "`make ci` (fmt-check + clippy -D warnings + test) passes". Intended to be used only once per repository. + +**All the file templates are in `references/templates.md`. Read it once before starting Step 2, and use it by replacing the placeholders with the values confirmed in Step 0.** + +> **Snapshot**: Written assuming 2026-04 / Rust 1.95 stable / edition 2024 / resolver 3. +> If more than six months have passed, verify the versions used in the Cargo.toml template and CI workflow (dtolnay/rust-toolchain, Swatinem/rust-cache, EmbarkStudios/cargo-deny-action, etc.) before reusing them. + +## Applicability + +- Assumes a workspace that bundles a set of binary tools (not a published library) +- Adopts Rust 1.90+ / edition 2024 / resolver 3 +- Layout is multiple crates under `crates/<name>/` (assuming future additions). If a different layout is needed, confirm with the user + +## Procedure + +### Step 0: Check prerequisites + +```bash +which cargo && cargo --version && rustc --version # if not installed, rustup in Step 1 +ls -la # grasp existing files +test -f Cargo.toml && cat Cargo.toml # check existing manifest +``` + +Items to confirm (consolidate into one AskUserQuestion turn): +- Workspace name (= is it fine to use the repository name) +- License (one of `MIT` / `Apache-2.0` / `MIT OR Apache-2.0` — dual licensing is the ecosystem convention) +- Name of the first crate (e.g. `mytool`) +- Repository URL (`https://github.com/<user>/<repo>`) +- Primary purpose of the binary (CLI / TUI / daemon — if TUI, use the ratatui skeleton) + +### Step 1: Install rustup (only if not already installed) + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile default +. "$HOME/.cargo/env" +``` + +Insert `. "$HOME/.cargo/env"` before subsequent Bash calls (to inherit the PATH). + +### Steps 2-9: Generate files (templates are in `references/templates.md`) + +| Step | File | Key points | +|---|---|---| +| 2 | Root `Cargo.toml` | Virtual workspace. Centralize metadata with `[workspace.package]` / `[workspace.dependencies]` / lints / a distribution-grade release profile. For TUI, add crossterm + ratatui + futures to dependencies | +| 3 | `rust-toolchain.toml` | stable + rustfmt + clippy (prevents drift between CI and local) | +| 4 | `crates/<name>/` | Cargo.toml inherits everything from the workspace (`*.workspace = true`); main.rs is a minimal CLI with clap + anyhow | +| 5 | `rustfmt.toml` | edition 2024 / max_width 100 | +| 6 | `Makefile` | help / build / release / test / clippy / fmt / check / deny / ci / install / uninstall / clean / update | +| 7 | `deny.toml` | advisories + licenses allow-list + bans + sources (supply-chain audit) | +| 8 | `.cargo/config.toml` | Windows static CRT + `cargo ci` / `cargo xclippy` aliases | +| 9 | `.github/workflows/ci.yml` | 3 jobs: lint (fmt + clippy) / test (3-OS matrix) / deny | + +**Install strategy (design decision for Step 6)**: `cargo install --path` triggers a release build every time, but `make install` runs a single release build for the whole workspace and distributes binaries via `cp`, which is faster for workspaces with multiple binaries. The default `PREFIX = ~/.local/bin` requires no sudo and follows the XDG standard. `/opt/homebrew/bin` is avoided because mixing in binaries that aren't managed by Homebrew causes confusion during updates. + +### Step 10: `LICENSE` + +Place it according to the chosen license. For `MIT OR Apache-2.0`, place both `LICENSE-MIT` and `LICENSE-APACHE`. For MIT only, place `LICENSE`. + +### Step 11: `.gitignore` + +``` +/target +**/*.rs.bk +.DS_Store +``` + +`Cargo.lock` **is committed** (the convention for binary workspaces). Do not add it to `.gitignore`. + +### Step 12: Root `README.md` + +A tools table + explanation of `make help` / `make ci` / `make install` (template is in `references/templates.md`). + +### Step 13: Sync CLAUDE.md / existing README + +If `CLAUDE.md` exists, append a "## Development commands" section (the template for the appended content is in `references/templates.md`). For an existing README, append rather than overwrite. + +### Step 14: Verify it works + +```bash +. "$HOME/.cargo/env" +cargo build --workspace && echo BUILD_OK +cargo test --workspace --all-targets && echo TEST_OK +cargo clippy --workspace --all-targets --all-features -- -D warnings && echo CLIPPY_OK +cargo fmt --all -- --check && echo FMT_OK +``` + +`make ci` can run the same three checks together. + +### Step 15: Completion report + +| Item | Status | +|---|---| +| `cargo build --workspace` | ✓ | +| `cargo test` / `cargo clippy -D warnings` / `cargo fmt --check` | ✓ | +| Workspace name / first crate | <confirmed value> | +| License | <confirmed value> | +| `Cargo.toml` (workspace.package + dependencies + lints + profile) | ✓ | +| `rust-toolchain.toml` / `rustfmt.toml` / `Makefile` / `deny.toml` / `.cargo/config.toml` | ✓ | +| `.github/workflows/ci.yml` | ✓ | +| LICENSE / README.md | ✓ | + +## Golden rules + +1. **Respect existing files**: for existing `.gitignore` / `CLAUDE.md` / `README.md`, append rather than overwrite +2. **Comments in English**: comments in Rust files are in English (`//`, `///`, `//!`) +3. **Commit `Cargo.lock`**: the modern convention for binary workspaces +4. **Stay in scope**: feature implementation (TUI event loops, server implementation, etc.) is out of scope. Hand off to the next skill (`add-rust-crate`) or manual work +5. **Do not commit**: committing is delegated to a separate skill (`/commit-push-branch`) +6. **Prefer `pub(crate)` over `pub`**: since binary crates have no external exposure, start with minimal visibility +7. **`make` is the only task runner**: `make` ships standard with macOS / Linux at zero added dependency cost. `just` / `task` / `xtask`, etc. are new dependencies and are not added by default (only if the user explicitly wants them). Their advantages over a Makefile — cross-platform support, arguments, `--list` — aren't large enough to justify the added dependency + +## Anti-patterns + +- Hardcoding each crate's version/license/repo in its own `Cargo.toml` (not using workspace inheritance) +- Writing `[profile.release]` in a crate's own `Cargo.toml` (Cargo only looks at the workspace root) +- Adding `Cargo.lock` to `.gitignore` (don't bring the library-crate convention into a binary project) +- Leaving `resolver = "2"` unchanged (choose 3 when using edition 2024 + Rust 1.84+) +- Enabling `pedantic` clippy without any allows (for intentional cases like `cast_precision_loss`, allow them in the workspace lints with a reason comment) +- Forcing library-publishing conventions like `#[non_exhaustive]` / `C-CRATE-DOC` onto every type (excessive for a binary crate) +- Not creating `rust-toolchain.toml` (a breeding ground for drift between CI and local toolchains) +- Setting up `cargo-dist` from the very start (excessive for a personal toolkit; introduce it when you actually start cutting releases by hand) +- Blindly trusting a research agent's **unsubstantiated impression** that "`just` is the de facto standard / the modern choice" and adding it as a new dependency (in reality, none of ripgrep, fd, bat, cargo, rustc, uv, or ruff use `just`) diff --git a/claude/skills/rust-bootstrap/references/templates.md b/claude/skills/rust-bootstrap/references/templates.md new file mode 100644 index 0000000..2b94fac --- /dev/null +++ b/claude/skills/rust-bootstrap/references/templates.md @@ -0,0 +1,338 @@ +> **Source of truth:** `claude/ja/skills/rust-bootstrap/references/templates.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# rust-bootstrap file templates + +A collection of file templates corresponding to the step numbers in SKILL.md. Replace the placeholders (`<name>` / `<license>` / `<repo-url>` / `<user>` / `<one-line>`) with the values confirmed in Step 0. + +## Step 2: workspace `Cargo.toml` + +A virtual workspace at the root (no `[package]`). Centralize metadata for all crates with `[workspace.package]`: + +```toml +[workspace] +resolver = "3" +members = ["crates/*"] + +[workspace.package] +edition = "2024" +rust-version = "1.90" +license = "<license>" # e.g., "MIT" or "MIT OR Apache-2.0" +authors = ["<user>"] +repository = "<repo-url>" +homepage = "<repo-url>" +keywords = ["cli"] +categories = ["command-line-utilities"] +publish = false # do not publish to crates.io + +[workspace.dependencies] +anyhow = "1" +# `env` feature lets `#[arg(env = "FOO")]` fall back to env var when CLI flag absent. +clap = { version = "4", features = ["derive", "env"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros", "sync", "time", "io-util"] } +tempfile = "3" + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" + +# Distribution-grade release profile. +[profile.release] +lto = "fat" +codegen-units = 1 +strip = "symbols" +panic = "abort" +opt-level = 3 +incremental = false + +[profile.dist] +inherits = "release" +``` + +**If building a TUI**, add to `[workspace.dependencies]`: + +```toml +crossterm = { version = "0.28", features = ["event-stream"] } +ratatui = "0.29" +futures = "0.3" +``` + +## Step 3: `rust-toolchain.toml` + +```toml +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] +``` + +## Step 4: The first crate (`crates/<name>/`) + +`crates/<name>/Cargo.toml`: + +```toml +[package] +name = "<name>" +version = "0.1.0" +description = "<one-line>" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +keywords.workspace = true +categories.workspace = true +publish.workspace = true + +[[bin]] +name = "<name>" +path = "src/main.rs" + +[dependencies] +anyhow.workspace = true +clap.workspace = true + +[dev-dependencies] +tempfile.workspace = true + +[lints] +workspace = true +``` + +`crates/<name>/src/main.rs`: + +```rust +//! <name> — <one-line description> + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "<name>", version, about)] +struct Cli {} + +fn main() -> Result<()> { + let _cli = Cli::parse(); + Ok(()) +} +``` + +## Step 5: `rustfmt.toml` + +```toml +edition = "2024" +max_width = 100 +use_field_init_shorthand = true +use_try_shorthand = true +``` + +## Step 6: `Makefile` + +```makefile +.DEFAULT_GOAL := help + +# Override: `make install PREFIX=/opt/homebrew/bin` +PREFIX ?= $(HOME)/.local/bin + +.PHONY: help build release test clippy fmt-check fmt check deny ci install uninstall clean update + +help: ## Show this help + @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' + +build: ## Debug build (all crates in the workspace) + cargo build --workspace --all-targets + +release: ## Release build (lto=fat / panic=abort) + cargo build --workspace --release --locked + +test: ## Run tests (all crates in the workspace) + cargo test --workspace --all-targets --locked + +clippy: ## clippy (-D warnings) + cargo clippy --workspace --all-targets --all-features -- -D warnings + +fmt-check: ## Check formatting + cargo fmt --all -- --check + +fmt: ## Apply formatting + cargo fmt --all + +check: ## Compile check only + cargo check --workspace --all-targets --locked + +deny: ## Supply-chain audit (requires cargo-deny) + cargo deny check + +ci: fmt-check clippy test ## fmt-check + clippy + test all at once + +install: release ## Release build → copy to $(PREFIX) (default: ~/.local/bin) + @mkdir -p $(PREFIX) + @for crate in crates/*/; do \ + name=$$(basename $$crate); \ + cp target/release/$$name $(PREFIX)/$$name; \ + if [ "$$(uname)" = "Darwin" ]; then \ + codesign --force --sign - $(PREFIX)/$$name >/dev/null 2>&1; \ + fi; \ + echo "installed $$name -> $(PREFIX)/$$name"; \ + done + +uninstall: ## Remove binaries from $(PREFIX) + @for crate in crates/*/; do \ + name=$$(basename $$crate); \ + rm -f $(PREFIX)/$$name; \ + echo "removed $(PREFIX)/$$name"; \ + done + +clean: ## Remove build artifacts + cargo clean + +update: ## Update Cargo.lock + cargo update +``` + +## Step 7: `deny.toml` (cargo-deny) + +```toml +[graph] +all-features = true + +[advisories] +version = 2 +yanked = "deny" +ignore = [] + +[licenses] +version = 2 +allow = [ + "MIT", "MIT-0", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", "BSD-3-Clause", "ISC", + "Unicode-3.0", "Unicode-DFS-2016", "Zlib", "CC0-1.0", "MPL-2.0", +] +confidence-threshold = 0.93 + +[bans] +multiple-versions = "warn" +wildcards = "deny" +deny = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +``` + +## Step 8: `.cargo/config.toml` + +```toml +[target.'cfg(all(target_env = "msvc", target_os = "windows"))'] +rustflags = ["-C", "target-feature=+crt-static"] + +[alias] +ci = "test --workspace --all-targets --locked" +xclippy = "clippy --workspace --all-targets --all-features -- -D warnings" +``` + +## Step 9: `.github/workflows/ci.yml` + +```yaml +name: ci +on: + push: { branches: [main] } + pull_request: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: { components: rustfmt, clippy } + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all --check + - run: cargo clippy --workspace --all-targets --all-features -- -D warnings + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: { shared-key: ${{ matrix.os }} } + - run: cargo test --workspace --all-targets --locked + - run: cargo build --workspace --release --locked + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 +``` + +## Step 12: Root `README.md` + +A simplified version including a tools table: + +```markdown +# <repo> + +A Cargo workspace of small terminal tools. + +## Tools + +| Crate | Description | +|---|---| +| [`<name>`](crates/<name>) | <one-line> | + +## Build & install + +\```sh +make help # all recipes +make ci # fmt-check + clippy + test +make install # release build → ~/.local/bin/ +make install PREFIX=/opt/homebrew/bin +make uninstall +\``` + +If `~/.local/bin` is not on your PATH, add +`export PATH="$HOME/.local/bin:$PATH"` to `~/.zshrc` or similar. + +## Adding a new tool + +1. Create `crates/<new-name>/Cargo.toml` inheriting from workspace +2. Add a row to the table above +3. Confirm that `make ci` passes +``` + +## Step 13: Content appended to CLAUDE.md's "Development commands" section + +```markdown +## Development commands + +| Command | Purpose | +|---|---| +| `make` (= `make help`) | List of recipes | +| `make ci` | fmt-check + clippy + test | +| `make release` | Release build (`lto = "fat"`, `panic = "abort"`) | +| `make install` | Copy all crates to `$(PREFIX)` (default: `~/.local/bin`) | +| `make install PREFIX=/opt/homebrew/bin` | Change the install destination | +| `make uninstall` | Remove the installed binaries | +| `make deny` | Supply-chain audit (`cargo deny check`, requires `cargo install cargo-deny`) | + +When adding a new crate, use the `/add-rust-crate` skill. +``` diff --git a/claude/skills/security-review-local/SKILL.md b/claude/skills/security-review-local/SKILL.md new file mode 100644 index 0000000..dafc264 --- /dev/null +++ b/claude/skills/security-review-local/SKILL.md @@ -0,0 +1,130 @@ +--- +name: security-review-local +description: Security audit of the local repository and Claude Code configuration. Cross-checks for secret leaks, real values leaking into `.env` files, over-granted permissions, suspicious commands, and supply-chain risk. Used for 「security review して」「secret 漏れてない?」 etc. +--- + +> **Source of truth:** `claude/ja/skills/security-review-local/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# security-review-local + +A skill that cross-audits the security aspects of the local repository. Run it before committing, before pushing, or after a configuration change. + +> **Division of labor with the builtin `/security-review`**: the builtin reviews vulnerabilities in pending changes (the code diff). This skill additionally cross-audits the local repository for **over-granted permissions in the Claude Code configuration, secret-like tracked filenames, real values leaking into `.env` files, suspicious commands in the Makefile, and supply-chain risk**. Use `/security-review` for vulnerabilities in a code diff, and this skill for leak/permission auditing across the repository and Claude configuration. + +## Applicability + +- Run inside a git repository +- Targets files such as `.gitignore`, `.env*`, `.claude/settings*.json`, etc. + +## Check items + +### 1. `.gitignore` excludes secret patterns + +Confirm that `.env`, `*.env`, `*.pem`, `*.key`, `id_rsa*`, `credentials*`, `secret*`, `*.p12`, `*.pfx`, `*.kdbx` are covered: + +```bash +git check-ignore -v .env .env.local secrets/foo.txt credentials.json id_rsa.pem 2>&1 +``` + +OK if each test path matches as `.gitignore:<line>:<pattern>`. If it doesn't match → an addition to `.gitignore` is needed. + +### 2. Check tracked files for secret-like naming + +```bash +git ls-files | xargs -I {} sh -c 'case "{}" in *.env|*.pem|*.key|*credentials*|*secret*|*.p12|*.pfx) echo "WARNING: {}";; esac' +``` + +**Templates** such as `.env.example` / `.envrc.example` are acceptable. If an actual secret file (`.env`, `id_rsa`) is tracked, unstage it immediately. + +### 3. Check `.env*` templates for real values leaking in + +```bash +ls -la .env* 2>/dev/null +``` + +If `.env.example` etc. is tracked, Read it and check that no real value (such as `OPENAI_API_KEY=sk-...`) has been entered. The preferred format is `KEY=` (empty). + +### 4. Check Claude Code configuration permissions + +```bash +cat .claude/settings.json +cat ~/.claude/settings.json +``` + +Confirm the allow list does **not** contain the following: + +| ⚠️ Dangerous if present | Reason | +|---|---| +| `Bash(rm:*)` / `Bash(rm -rf:*)` | destructive | +| `Bash(curl:*)` / `Bash(wget:*)` | arbitrary URL access | +| `Bash(cat:*)` (broad) | reads arbitrary files such as `~/.ssh/id_rsa` | +| `Bash(grep:*)` (broad) | greps arbitrary files | +| `Write(*)` / `Edit(*)` (broad) | arbitrary writes, including outside the project | +| `Bash(ssh:*)` / `Bash(scp:*)` | remote access | +| `Bash(*)` | fully arbitrary execution | + +Scoped read-only commands (`Bash(go test:*)`, `Bash(git status)`, `Bash(ls:*)`) are fine. + +### 5. Suspicious commands in code / Makefile + +```bash +grep -rE "(curl|wget|eval\(|http://|https://[^ )]*\.(sh|env)|nc -e|/dev/tcp)" \ + Makefile .golangci.yaml cmd/ internal/ scripts/ 2>/dev/null +``` + +If there's a hit, check the details. `curl https://...install.sh | sh` in a CI script needs review. + +### 6. Secrets in Go code log output + +```bash +grep -rnE 'slog\.(Info|Debug|Warn|Error).*\b(API_KEY|SECRET|TOKEN|PASSWORD|ENV)' cmd/ internal/ 2>/dev/null +grep -rnE 'fmt\.(Print|Println|Printf).*\b(API_KEY|SECRET|TOKEN|PASSWORD)' cmd/ internal/ 2>/dev/null +grep -rn 'os\.Environ()' cmd/ internal/ 2>/dev/null # 環境変数全ダンプ +``` + +If there's a hit, consider whether log masking/redaction is needed. + +### 7. Supply chain of external dependencies + +```bash +cat go.mod +go mod why <suspicious-pkg> # 必要なら +``` + +- Whether the dependencies in the `require` block come from a trustworthy org (e.g., typosquatting by a suspicious author) +- Whether a `replace` directive points at a local path (must not be committed) +- Whether `go.sum` is gitignored (required for integrity verification) + +### 8. `.claude/settings.local.json` (personal settings) + +```bash +git check-ignore .claude/settings.local.json +``` + +Confirm it's ignored. If it's tracked, there's a risk of leaking personal tokens etc. + +## Report format + +``` +## セキュリティ監査結果 (<時刻>) + +### ✓ 問題なし +- .gitignore: .env / *.pem / *.key / credentials* / secrets/ 全て ignored +- tracked files: secret 系命名なし (git ls-files スキャン) +- .env.example: 全キー空 (実値未混入) +- .claude/settings.json allow list: 安全範囲 +- 外部依存: <数> 件、全て信頼できる org +- log 出力: sensitive ダンプなし + +### ⚠️ 要対応 +| # | 項目 | リスク | 修正方針 | +|---|---|---|---| +| 1 | ... | ... | ... | +``` + +## Iron rules + +1. **Report concretely**: don't just say "X is a problem" — also show where to fix it +2. **Don't fear false positives**: list anything suspicious. Don't pretend everything is fine +3. **Leave fixes to another skill**: this skill is audit-only. Fixes go through `/self-review-changes` etc. +4. **Remember past incidents**: don't miss the same leak pattern next time diff --git a/claude/skills/self-review-changes/SKILL.md b/claude/skills/self-review-changes/SKILL.md new file mode 100644 index 0000000..9fd2f69 --- /dev/null +++ b/claude/skills/self-review-changes/SKILL.md @@ -0,0 +1,195 @@ +--- +name: self-review-changes +description: Self-review of the most recent diff (working tree or staged), checking accuracy, consistency, best practices, and alignment with memory feedback. Presents a fix plan and only Edits after user approval. Used for 「self review して」「review して」「修正箇所ないか確認して」 etc. +--- + +> **Source of truth:** `claude/ja/skills/self-review-changes/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# self-review-changes + +Re-scans the most recent diff, surfaces fix candidates, and only applies fixes after approval. **4 phases**: Mindset → Observation → Analysis → Action. + +> **Division of labor with plugins**: the `/code-review` and `/simplify` plugins specialize in diff bugs / simplification. This skill additionally covers a broader self-review including **configuration accuracy, doc consistency, memory feedback alignment, and the Copilot 11-category checklist**. Use `/code-review` for bugs in the code alone, and this skill for reviewing the whole change against conventions. + +## Applicability + +- Some file edit happened in the immediately preceding turn (before committing or right after the most recent commit) +- The scope of the fix target is clear (visible in `git diff`) + +--- + +## Phase 0: Mindset shift (implementer → external reviewer) + +The biggest bias in self-review is that "you can see your own intent but not the actual gap." Before moving on to Phase 1, internally declare that you will read the code as **"an external reviewer seeing this code for the first time."** Treat comments as a binding contract; phrases like "I meant it this way," "that doesn't happen with typical input," or "it's fine because it's an internal caller" are forbidden. Adversarially assume wire-format input, nil / empty / boundary values, malformed tokenizer output, mid-cancellation state, and invalid specs. Maintain this mindset through Phase 2, especially 2.6-2.9. + +--- + +## Phase 1: Observation (grasp the diff + gather related information) + +### 1.1 Grasp the diff + +```bash +git status +git diff --stat +git diff [--cached] +git show HEAD --stat && git show HEAD # 直近 commit 後 +``` + +### 1.2 Read all changed files in parallel + +Read each changed file in parallel with the Read tool (not `cat` via Bash). + +### 1.3 Read related memory + +- Read the `MEMORY.md` index, decide which feedback / project entries relate to the edit, and read those entries in full +- **Always required every time**: + - Mechanically check this skill's Phase 2.1 / 2.1.1 (Copilot's frequent 11 categories + rebuttal patterns) and Phase 2.6-2.9 + - CLAUDE.md's "conduct for changes" (flagging out-of-scope changes) (Phase 2.3) + - No speculative mapping (only literal correspondences from the source text / Phase 2.5) + +--- + +## Phase 2: Analysis (check by perspective) + +### 2.1 Design perspective — Copilot's frequent 11 categories (most important) + +The 11 categories that come up frequently in Copilot review (aggregated from analysis of 26 total reviews across PR #26 and PR #30). Expanded below. + +**5 categories from PR #26**: + +| # | Category | Key point | +|---|---|---| +| 1 | **Documentation accuracy** | When renaming a helper / API, grep all related docs / check for dropped `_` errors in samples / verify doc comment behavior matches the implementation | +| 2 | **Input validation** | Check 0 / negative / nil / empty / traversal in public APIs (`WithXxx` / `NewXxx` / handler args); place validation at the end of the constructor | +| 3 | **Edge cases** | Wire-form parameters present (MIME `mime.ParseMediaType`) / empty check via `strings.TrimSpace` / all-elements-fail in a batch / catch empty output | +| 4 | **Concurrency / cancellation** | Respect `ctx.Deadline` / avoid spawning goroutines / check `ctx.Err()` at each step / **propagate ctx all the way down** (including pre-processing loops) | +| 5 | **Error contract** | Sentinel semantic contract (transient vs. permanent) / no retry on 4xx / wrap boundary errors at the port level | + +**6 extended categories from PR #30**: + +| # | Category | Key point | +|---|---|---| +| 6 | **Symmetric constructor defense** | When adding a guard to one constructor, `grep "^func New"` to check whether other constructors in the same PR have the same guard (mechanically checked in Phase 2.6) | +| 7 | **Doc/impl literal drift** | Cross-check comments containing strong claims ("strictly", "preserves", "Returns X") against the implementation byte-for-byte (mechanically checked in Phase 2.9) | +| 8 | **Sentinel-on-error leakage** | Check the godoc of dependency interfaces for whether 0 / "" / nil / -1 is a contractual return on the error path, and whether the consumer defends against it (mechanically checked in Phase 2.7) | +| 9 | **Exported function type contract** | If exporting `type X func(...) error` with a docstring claiming "all options X," check whether custom implementations satisfy the same contract, or wrap via a factory (mechanically checked in Phase 2.7) | +| 10 | **Test assertion trivially passes** | Scenarios where repeated fixture content / a loose `Contains` / a nil-only check would "pass even if the implementation is broken" (mechanically checked in Phase 2.8) | +| 11 | **Slice OOB / panic guard (test)** | Whether a `slice[len-N:]` in a test has a `len >= N` guard immediately before it, guarding against panics when the input is short | + +### 2.1.1 Rebuttal patterns (identifying false positives) + +The following are rebuttal patterns that, given project context, are judged "not applicable" and resolved via reply: + +- **Go 1.22+ loop variable shadowing**: if the `go.mod` go directive is 1.22+, shadowing is not an issue +- **Re-proposing internal port boundary defense**: if it was withdrawn during a design discussion, resolve by referencing the PR description +- **Duplicate posting of the same warning**: if the same pattern doesn't actually apply to a different test, explicitly note it as a false positive + +### 2.2 Mechanical checks by file type + +- **Go (`*.go`)**: `gofmt` / `goimports` / godoc conventions (`// Package <name>` / `// FuncName ...`) / `fmt.Errorf("...: %w", err)` / context arg first / unnecessary exports +- **Config (`.golangci.yaml` / `Makefile` / `*.yaml` / `*.json`)**: schema version (e.g., golangci-lint v1 vs v2) / regex glob (substring match for `gen` vs. root-anchored `^gen/`) / indentation +- **Markdown / Docs**: relative links / code block language tags / consistent table cell counts / heading hierarchy +- **Shell / Makefile / Dockerfile**: shell injection (quoting `$VAR`) / dangerous commands (`rm -rf`, `curl | sh`) +- **Proto (`.proto`)**: package / option / field number / backward compatibility (`buf breaking`) + +### 2.3 Spec literal consistency (only for work touching a spec doc) + +When working with a spec (`docs/adr/*.md`, `docs/design/*.md`, OpenAPI, Proto): + +1. Grep to enumerate literals (type names / enums / fields / file names) in the spec +2. Grep to enumerate the same surface on the implementation side +3. Write out **deviations** explicitly, and for each present the user with a "reason" and a remediation approach ((a) align with the spec / (b) update the spec / (c) separate ADR) + +Don't use "it's fine, it's a prototype" as an implicit justification. Use the three-point set of explicit statement, approval, and record (CLAUDE.md's "conduct for changes" — flag out-of-scope changes). + +### 2.4 Consistency with memory conventions + +Check whether the diff violates any related memory identified in Phase 1.3: + +- Terminology conventions (follow the terminology conventions in memory) +- Document style (don't add prerequisite-knowledge preambles; relax during the prototype period) +- Comment language (English for `*.go` / Makefile / proto / shell) +- Mixing Phase / ticket ID notation into comments +- Commit message style (short, content-only) + +### 2.5 Cross-reference / forbidden-token grep + +Grep across all changed files for: + +- **Leaking transient info**: `ticket`, `in a later`, `future ticket`, `see (commit|PR) #`, ticket ID formats (`#?\d+`, `[A-Z]+-\d+`). Get the literal check patterns from the project's `MEMORY.md` feedback rather than hardcoding them in the skill +- **Cross-reference existence check**: whether section references in comments (`§[0-9]+\.[0-9]+`, etc.) use vocabulary that matches the source text +- **Speculative mapping**: whether a comment fills in a correspondence that isn't actually stated in the source text + +### 2.6 Symmetry audit (symmetric defense check) + +Check whether symmetric counterparts (enumerate all constructors in the same PR with `grep "^func New" $(git diff --name-only)`, and also grep functions taking `opts ...Option`) to a guard / validation / nil check added in the diff have the same guard, and flag any that are missing. + +Example: PR #30 C9 — after adding a nil-opt reject to `ports.NewChunkSpec`, the `opts` of `chunker.New` needed the same guard (a lesson from fixing one spot and missing the other). + +### 2.7 Interface contract trace (sentinel / exported type) + +Re-read the godoc of any external interface used in changed files, and confirm that **sentinel return cases** (0 / "" / nil / -1 / a specific error sentinel) and the consumer's branching (`if x <= X`, `errors.Is(...)`) are consistent. If an exported function type is being exported, check whether the docstring's "all" / "always" claim also holds for custom implementations, or whether it's normalized via a factory. + +Example: PR #30 C10 — `chunking.Tokenizer.CountTokens` returns 0 on an encoding error, so the consumer's `tokens <= MaxTokens` check needs a defense so this 0 isn't mistaken for "fits." C13 — a custom implementation of the exported `ChunkSpecOption` could break the contract, so it's wrapped via a factory. + +### 2.8 Adversarial test review (surfacing trivial passes) + +For each test's assertions, ask: "**is there a mutation that inverts the implementation's behavior but still passes?**" Pay special attention when the input contains repeated content / identical values / nil / empty, and look for paths where assertions like `strings.Contains` / `len(got) > 0` / `errors.Is(...)` trivially pass. + +Example: PR #30 C7 — checking carry-over overlap with `strings.Contains` against 80 repeats of the same sentence → trivially true even if carry-over is broken. The fix was to insert a distinct marker (`[文NNN]`) and verify the boundary with `HasPrefix`. + +### 2.9 Doc last-write-wins (surfacing stale comment updates) + +After an implementation change, re-read every comment in the changed files with suspicion. Grep for strong claims (`grep -nE "(strictly|preserves|guarantees|ensures|always|never|returns|panics)"`), and cross-check each claim against the implementation's literal behavior byte-for-byte (e.g., is "strictly under X" `<` or `<=`? does "preserves Y" hold given that upstream trims Y? what does "returns Z on W" actually return?). Resolve discrepancies by either aligning the doc with the implementation, or aligning the implementation with the doc based on design intent. + +Example: PR #30 C11/C12 — `carryOverlap`'s "kept strictly under overlap" was corrected to "at most overlap" since the implementation allows equality; `joinUnits`'s "preserves original surface text" was corrected to "reproduces sentence content but not byte-for-byte identical" since `SplitSentences` trims whitespace. + +--- + +## Phase 3: Action (fix plan → approval → fix → re-verify) + +### 3.1 Organize fix candidates (present as a table) + +``` +## Self-review 結果 + +| # | file:line | 問題 | 修正方針 | 重要度 | +|---|---|---|---|---| +| 1 | .golangci.yaml:14 | paths 値が部分一致 regex で過剰マッチ | `^gen/` に変更 | 望ましい | +| 2 | cmd/x/main.go:1 | コメント日本語 | 英語に書き換え | 致命的 (memory feedback 違反) | + +## 問題なし +- go.mod (module path / go directive 正しい) +- Makefile (ターゲット動作確認済み) +``` + +Distinguish severity into 3 tiers: **critical / recommended / nit**. + +### 3.2 User approval + +Wait for "go ahead." Also accept selective approval (approving only some candidates). + +### 3.3 Fix via Edit (parallel) + +Apply approved candidates in parallel with the Edit tool. + +### 3.4 Re-verification + +Check for side effects after the fix: +- Re-run `make test`, `make lint` +- Re-run related greps (check whether the fix introduced the same problem elsewhere) + +--- + +## Iron rules + +1. **Don't skip the Phase 0 mindset shift**: consciously set aside implementer bias. Judge based on "code and comments alone, as an external reviewer," not "your own intent" +2. **Present the fix plan first**: don't Edit silently +3. **Distinguish critical / recommended / nit**: so the user can pick and choose +4. **Read memory feedback entries in full**: don't judge from the index line alone +5. **Check the Copilot 11 frequent categories every time** (Phase 2.1): docs accuracy / validation / edge cases / cancellation / error contract / symmetry / doc drift / sentinel leakage / exported func type contract / test trivial pass / slice OOB +6. **Don't skip the 4 mechanical checks — symmetry / interface contract / adversarial test / doc last-write-wins** (Phase 2.6-2.9): this is where the real blind spots concentrate +7. **Explicitly state, get approval for, and record spec deviations**: don't use "it's a prototype" as an implicit justification (CLAUDE.md's "conduct for changes" — flag out-of-scope changes) +8. **No speculative mapping**: cross-references / terminology correspondence in comments should stay within what the source text literally states +9. **Verify side effects**: re-run build / test / lint after fixing +10. **Don't hide anything**: point out problems even in something you created in the immediately preceding turn. Don't skip a check just because "it doesn't happen with typical input" or "it's fine, it's an internal caller" diff --git a/claude/skills/sync-k8s-manifest-from-code/SKILL.md b/claude/skills/sync-k8s-manifest-from-code/SKILL.md new file mode 100644 index 0000000..ca313bf --- /dev/null +++ b/claude/skills/sync-k8s-manifest-from-code/SKILL.md @@ -0,0 +1,128 @@ +--- +name: sync-k8s-manifest-from-code +description: Propagates interface changes (config / env / port / probe / resources) in a code repo (Go application) to the K8s Kustomize manifest repo (through detection + edits across 3 environments + `git add`; does not commit or push). Triggered by 「manifest 同期」「config / env 変えたから manifest 直して」 etc. Target repo information is fetched from the reference memory in MEMORY.md. +allowed-tools: Read, Edit, Grep, Glob, Bash(git status:*, git diff:*, git add:*, ls:*, find:*, cat:*) +--- + +> **Source of truth:** `claude/ja/skills/sync-k8s-manifest-from-code/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# sync-k8s-manifest-from-code + +A skill that semi-automates **code → manifest direction** sync (detect → Edit → stage) for setups where the application code repo and the K8s Kustomize manifest repo are separate. The reverse direction (manifest → code) is out of scope. + +## Applicable pattern + +Assumes a project matching the following: + +- The code repo has a YAML config schema (struct) with a design that **rejects unknown fields at startup** (i.e., ConfigMap drift immediately causes a crash) +- Required env vars (both secret and non-secret) are validated at startup (i.e., missing env vars cause `os.Exit(1)`) +- The manifest repo is structured as Kustomize `bases/` (= prod source of truth) + `overlays/{dev, stg, ...}/` +- Each service has config split like `config/{app.yaml, config.env, sealed-secret.yaml, secret.yaml}` + +## Resolving the target project at startup (important) + +This is a global skill, so **project-specific terms are not hardcoded into this SKILL.md**. At startup, fetch the following about the target project from the reference memory in MEMORY.md: + +| Item to fetch | Example | +|---|---| +| code repo path | `~/.../some-code-repo` | +| manifest repo path + service directory | `~/.../some-mani-repo/<service-dir>` | +| config schema location | `internal/config/config.go` | +| env definition location | `internal/config/secrets.go` | +| config example | `cmd/<binary>/config.example.yaml` | +| binaries (sync behavior) | `[<A>: full sync, <B>: detect-warn]` | +| overlay structure | `[bases, overlays/dev, overlays/stg, ...]` | +| constraints / ordering | e.g., manifest-first → code-follows is mandatory | + +If no corresponding reference memory is found, ask the user to obtain the information → propose it as a candidate to save as reference memory (don't write it immediately; get user approval first). + +Example of a reference memory you might refer to: +- e.g., a reference memory like `[[<service>-sync]]` + +## Detection logic (project-agnostic) + +Based on the location information obtained from reference memory, read the following in the code repo (in priority order): + +1. Fields added / changed / removed in the **config struct (YAML schema)** + - Propagates to: `config/app.yaml` in all overlays + - If the design rejects unknown fields, **manifest-first is mandatory** +2. **Env definitions** (required env added / renamed / removed) + - Propagates to (secret): the key in `config/sealed-secret.yaml` (envFrom), or the key in `config/secret.yaml` + - Propagates to (non-secret): `config/config.env` + - If the design fails to start when unset, **manifest-first is mandatory** +3. Value / structure changes in the **config example** + - Propagates to: the corresponding key in `config/app.yaml` for each environment +4. **Listen port / probe endpoint in main.go** + - Propagates to: `bases/deployment.yaml` (containerPort, livenessProbe, readinessProbe) + `bases/svc.yaml` (port) +5. **Changes affecting resource consumption** (adding a heavy library / large cache / large buffer / changing parallelism) + - Candidate propagation targets: `bases/{deployment, hpa, pdb}.yaml` + - **Numeric values require user judgment** (the skill only flags; it never decides automatically) +6. **proto / API listen port** (gRPC / REST) + - Propagates to: `bases/{deployment, svc}.yaml` ports +7. **Major dependency added to `go.mod`** (Redis / DB / external API client, etc.) + - Propagates to: NetworkPolicy (flag only if absent) / Secret (add a key if new credentials are involved) +8. Changes to a **binary designated detect-warn** (as specified in reference memory) + - Detection only, no Edit, emit a warning +9. **New binary** (a new addition under `cmd/<new>/`) + - Detection only; initializing a new directory is out of scope; emit a warning + +## Operation flow + +1. **Pre-check** + - Confirm the manifest repo's working tree is **clean** via `git status` (stop if dirty, don't mix in other work) + - Confirm the target ref of the code repo (default = working tree HEAD) + - Resolve the target project from reference memory +2. **Detect** + - Extract differences using the detection logic above + - Output the results as a summary table (which category / which value / candidate propagation target) +3. **Plan-present (waiting for user approval)** + - Present the candidate propagation targets + - For items where the choice of overlay to touch branches (e.g., resources), ask the **user to decide, presented in comparison form** + - Always make the **manifest-first → code-follows ordering** explicit to the user (emphasize it when adding env vars / unknown fields) +4. **Edit** + - Edit the approved changes into the manifest repo's working tree + - Preserve the existing YAML / sealed-secret indentation, ordering, and comments + - Respect existing overlay overrides (where an overlay overrides a value from `bases/`) +5. **Stage** + - `git add` only the touched files, by specific path (the guard-bash.sh hook denies `git add -A` / `git add .`) +6. **Stop** + - Output a summary of `git diff --staged` + - The skill stops here; after the user reviews → commit & push separately via `/commit-push-branch` + +## Constraints / out of scope + +| Item | Treatment | +|---|---| +| commit / push / PR creation | **Never executed under any circumstance.** `git push` / `gh` are not included in allowed-tools | +| kubeseal (SealedSecret value encryption) | Out of scope. Only adds the key; the user runs kubeseal on the value later | +| Directory initialization for a new binary | Out of scope. Warning only | +| Reverse sync (manifest → code) | Treated as a separate skill (e.g., `.upstream-sha`-based operations) | +| Automatically deciding resource numbers | Out of scope. Heuristic flagging only; the user decides the final values | + +## Handling detection gaps + +Detection is heuristic, so always state explicitly at the end of the output **"areas that may not have been caught"**: + +- New volume mounts / hostAlias / annotations +- NetworkPolicy / PodSecurityContext / SecurityContext details +- Sidecar / init container additions +- Annotation-based behavior changes (Argo Rollouts, Linkerd, Istio, etc.) + +Add a one-line note: "there may be other affected areas; a visual check is recommended." + +## Stop conditions on failure (don't proceed unilaterally) + +If any of the following are detected, **stop and report to the user**: + +- The manifest repo's working tree is dirty +- The code repo / manifest repo path cannot be resolved (no reference memory / the path has changed) +- The propagation target file doesn't exist (the overlay structure differs from what was expected) +- A change requiring user confirmation on security grounds (`Action: "*"` / `privileged` / `hostNetwork` / `hostPID` / expanding public exposure / granting a broad role to a service account) +- The SealedSecret's **value** (encrypted) needs to change +- A change limited to a detect-warn-designated binary that could still affect a full-sync-target binary + +## Related + +- The target project's reference memory (e.g., `[[<service>-sync]]`) — holds repo path / config location / binaries +- `commit-push-branch` skill — commit & push after this skill completes (cutting a branch + following past style) +- `security-review-local` skill — security-perspective review after editing the manifest (recommended to use together) diff --git a/claude/skills/tech-docs-writer/SKILL.md b/claude/skills/tech-docs-writer/SKILL.md new file mode 100644 index 0000000..e8fd74a --- /dev/null +++ b/claude/skills/tech-docs-writer/SKILL.md @@ -0,0 +1,124 @@ +--- +name: tech-docs-writer +description: Creates internal technical documentation (ADR / Design Doc / API spec / README / Runbook / system design document) as Japanese Markdown and saves it under docs/{adr,api,readme,runbook,design}/. Always use for 「設計ドキュメント書いて」「ADR 起こして」「API 仕様書」「Runbook 作成」「アーキテクチャ設計」 and similar requests. +--- + +> **Source of truth:** `claude/ja/skills/tech-docs-writer/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# Technical Documentation Writing Skill + +Creates internal technical documentation for developers as Japanese Markdown. +Produces the appropriate template according to the de facto standard for each of the 5 document types. + +| Type | Standard | Save Location | +|------|---------|--------| +| ADR / Design Doc (single decision) | MADR v3 | `docs/adr/NNNN-<slug>.md` | +| API spec | OpenAPI 3.1 concepts | `docs/api/<resource>.md` | +| README / Guide | Diataxis | `docs/readme/<topic>.md` or root `README.md` | +| Runbook | Google SRE Playbook | `docs/runbook/<alert-or-topic>.md` | +| System design document | arc42 + C4 + MADR | `docs/design/<system-name>.md` | + +## 1. Invocation and type determination + +Determine the type from the user's utterance or context. When ambiguous, always confirm before creating anything (never guess and create). + +| Input Signal | Type | +|------------|------| +| 「決定記録」「ADR」「なぜこの技術選定」「トレードオフ」 (single decision) | ADR / Design Doc | +| 「API仕様」「エンドポイント」「OpenAPI」「REST/gRPC」「リクエスト/レスポンス」 | API spec | +| 「README」「セットアップ」「使い方」「クイックスタート」 | README / Guide | +| 「Runbook」「Playbook」「障害対応」「オンコール」「アラート対応」 | Runbook | +| 「システム設計書」「アーキテクチャ設計」「arc42」「全体設計」「システム全体の設計」 | System design document | + +### Distinguishing ADR from a system design document + +| Target | Applicable Type | +|------|---------| +| A single decision (「Xを採用する/しない」) | ADR | +| A design that gives an overview of the entire system/subsystem (purpose / components / dynamic behavior / NFR / deployment) | System design document | + +When in doubt, decide based on whether the focus is on 「決定事項」 (a decided matter) or 「全体像」 (the big picture). + +## 2. Required interview items + +Never start writing while the spec is ambiguous. Confirm missing information with `AskUserQuestion`. + +Common: title / assumed background of the target audience / related PRs and issues / existence of the save directory + +Per-type confirmation items are documented in the corresponding reference file: + +- ADR / Design Doc → `references/adr.md` +- API spec → `references/api.md` +- README / Guide → `references/readme.md` +- Runbook → `references/runbook.md` +- System design document → `references/system-design.md` + +Once the type is decided, Read the corresponding reference file. Each reference contains the template body and writing tips. + +## 3. Extracting information by input source + +When in 「コード/差分から自動生成」 (auto-generate from code/diff) mode, use the following. + +### Git diff / PR +```bash +git log --oneline -20 +git diff <base>..<head> +gh pr view <number> --json title,body,commits,files +``` +Trace the rationale behind decisions at the commit granularity, and reflect it in the ADR's Context and Considered Options. + +### Go code (godoc / comments) +- Treat package comments and doc comments on exported symbols as the primary source of information +- Interface definitions → extraction of API contracts / design boundaries +- `//go:generate` directives are hints indicating implementation intent + +### Kubernetes manifests / Helm +- `Deployment`/`StatefulSet` resource requirements and probe settings → preconditions for the Runbook +- Public parameters in `values.yaml` → configuration guide for the README +- Alert rules (`PrometheusRule`) → starting point for the Runbook's Symptom section + +## 4. Document creation flow + +1. Determine the type (§1) and gather the required items (§2) +2. Read the corresponding `references/<type>.md` +3. If a code source is specified, extract information per §3 +4. Draft in Japanese following the template +5. **For ADR / Design Doc / system design documents, always invoke the `api-design-review` skill once the draft is complete** and check coverage across the 6 perspectives (client abstraction / both sides of the ACL / forward-compat / edge cases / SoT consistency / memory conventions). Reflect any detected gaps in the draft before saving. For API spec / README / Runbook, it is sufficient to pass the same 6 perspectives at a lightweight level (invoking the skill is optional) +6. Save the file: write it out to `docs/<type>/<filename>.md` + - ADRs use sequential numbers `0001-`, `0002-` … (existing max number + 1) + - System design documents go to `docs/design/<system-name>.md` + - Create the directory if it does not exist yet +7. Actively use Mermaid wherever a diagram would help (sequence diagrams / flowcharts / C4) + +## 5. Style rules + +- For list items, table cells, and summary lines, prefer noun-ending phrasing (体言止め — ending on a noun rather than closing with a verb) + - Bad example: 「P99レイテンシが100ms以下であること」「監視ダッシュボードを確認する」 + - Good example: 「P99レイテンシ100ms以下」「監視ダッシュボードの確認」 +- Where prose is required (Context / background / commentary), the plain declarative style (である/する form) is fine +- Do not mix noun-ending phrasing and plain declarative style within the same bullet list +- Keep bullet points short (in principle, 1 item per line, 2 lines maximum) + +## 6. Quality checklist (self-review after writing) + +- [ ] The title conveys the gist at a glance (avoid vague titles like "新機能の対応" / "handling the new feature") +- [ ] The reader (an internal developer) can reach the conclusion without prior background knowledge +- [ ] Lists and tables consistently use noun-ending phrasing (§5 Style rules) +- [ ] Speculation or unconfirmed matters are not written as if they were 「決定事項」 (already decided) — mark unclear points explicitly as `TBD` +- [ ] No sensitive information (personal data / credentials / secret values other than internal URLs) has been accidentally pasted in +- [ ] Even outside ADRs, what has been decided and what has not been decided are clearly separated +- [ ] The end of the file has a changelog or related links +- [ ] For ADR / Design Doc / system design documents, the `api-design-review` skill has been passed (§4 step 5), and any detected gaps have been reflected + +## 7. Things not to do (project norms) + +- Do not start writing while the spec is still ambiguous. Always ask the user for missing information. +- Do not decide things by guesswork. Mark unresolved matters as `TBD`. +- Do not output secret information. If a password / token / personal data is detected, replace that portion with `<REDACTED>`. + +## Reporting to the user after output + +After creation, report the following concisely: +- The saved file path (`computer://` link) +- The chosen type and its rationale +- The list of remaining `TBD` items (if any) diff --git a/claude/skills/tech-docs-writer/references/adr.md b/claude/skills/tech-docs-writer/references/adr.md new file mode 100644 index 0000000..14a8a6d --- /dev/null +++ b/claude/skills/tech-docs-writer/references/adr.md @@ -0,0 +1,107 @@ +# ADR / Design Doc Template (MADR v3 compliant) + +> **Source of truth:** `claude/ja/skills/tech-docs-writer/references/adr.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +An ADR writes 「決定の記録」 (a record of the decision); a Design Doc writes 「提案〜決定のプロセス」 (the process from proposal to decision). The template is shared: for a Design Doc, start writing with `Status: Proposed`, then update it to `Accepted` once agreement is reached. + +## Required interview items + +- Decision title (e.g., 「ベクトル検索エンジンにValdを採用する」) +- Status (`Proposed` / `Accepted` / `Rejected` / `Deprecated` / `Superseded by [ADR-xxxx]`) +- Decision maker(s) (individual name / team name) +- Background (why this decision is needed now) +- Options considered (at least 2 — if there's only one, it isn't a "decision") +- Selection criteria (performance / operational cost / consistency with the existing stack, etc.) +- Expected consequences (both positive and negative) + +Do not create the document if the required items cannot be filled in. In particular, if there is only one entry under 「検討した選択肢」 (options considered), confirm whether it should be classified as an implementation memo rather than an ADR. + +## Template body + +```markdown +# ADR-NNNN: <決定のタイトル> + +- **Status**: <Proposed | Accepted | Rejected | Deprecated | Superseded by ADR-XXXX> +- **Date**: YYYY-MM-DD +- **Deciders**: <意思決定者名/チーム名> +- **Consulted**: <相談した人・チーム (任意)> +- **Informed**: <通知先 (任意)> + +## Context and Problem Statement + +<なぜこの決定が必要か。解きたい課題を1〜3段落で。背景の事実と制約を分けて書く。> + +## Decision Drivers + +体言止めで簡潔に。動詞で閉じない。 + +- <例: P99レイテンシ100ms以下> +- <例: Go SDKの公式提供> +- <例: Kubernetes上でのHA構成の容易さ> + +## Considered Options + +1. **<選択肢A>** +2. **<選択肢B>** +3. **<選択肢C>** + +## Decision Outcome + +**Chosen option**: "<選択肢X>" + +理由: <選定基準に照らしてなぜこれを選んだか。2〜5文> + +### Consequences + +体言止めで簡潔に。 + +- Good: <例: P99レイテンシの大幅改善> +- Good: <例: 既存Goスタックとの統合容易性> +- Bad: <例: 新技術導入に伴う学習コスト> +- Bad: <例: 既存Elasticsearchからの移行作業> + +### Confirmation + +<決定が守られていることをどう検証するか。例: CIのベンチマーク、監視指標、コードレビュー時のチェック項目> + +## Pros and Cons of the Options + +### <選択肢A> + +<概要1文> + +- Good: <...> +- Good: <...> +- Neutral: <...> +- Bad: <...> + +### <選択肢B> + +<同上> + +### <選択肢C> + +<同上> + +## More Information + +<参考リンク、関連ADR、補足資料> +``` + +## Writing tips + +1. Write "facts" in Context. Instead of phrasing it like 「良いライブラリがない」, write it in a verifiable form such as 「現行の X は Y の機能をサポートしていない」. +2. Write Consequences from both sides. An ADR that lists only Good items is suspicious. If you can't think of any Bad items, you may not have fully understood the decision's scope of impact. +3. For Design Doc use, add an "Open Questions" section before `## More Information`, to surface the points still under discussion before consensus is reached. +4. Place Mermaid diagrams in Context or Decision Outcome. Before/After architecture diagrams are especially effective. + +## File name and sequence number + +- Path: `docs/adr/NNNN-<kebab-case-slug>.md` +- Sequence number: existing max number under `docs/adr/` + 1 (zero-padded to 4 digits) +- Slug: convert the title to English kebab-case (e.g., `0012-adopt-vald-for-vector-search.md`) + +## References + +- MADR official site: https://adr.github.io/madr/ +- GitHub: https://github.com/adr/madr diff --git a/claude/skills/tech-docs-writer/references/api.md b/claude/skills/tech-docs-writer/references/api.md new file mode 100644 index 0000000..3b8ee56 --- /dev/null +++ b/claude/skills/tech-docs-writer/references/api.md @@ -0,0 +1,168 @@ +# API Specification Template (Markdown based on OpenAPI 3.1 concepts) + +> **Source of truth:** `claude/ja/skills/tech-docs-writer/references/api.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +Rather than outputting OpenAPI YAML/JSON directly, this produces a human-readable Japanese Markdown API specification. However, the granularity of information is aligned with OpenAPI 3.1, making it easy to convert to a machine-readable format when needed. + +## Required interview items + +- API type (REST / gRPC / GraphQL) +- Service name and version +- Base URL (per environment: development / staging / production) +- Authentication method (Bearer Token / API Key / mTLS / OAuth2 / none) +- List of target endpoints (at least 1) +- Parameter / request / response shape for each endpoint +- Error scheme (common error response format, status code policy) +- Whether rate limiting applies + +If many points are unclear, propose an approach where you first complete one endpoint, show the template, and then fill in the rest. + +## Template body + +```markdown +# <サービス名> API 仕様書 + +- **バージョン**: v1.0.0 +- **最終更新**: YYYY-MM-DD +- **オーナー**: <チーム名> + +## 概要 + +<このAPIの目的を3〜5文で。解決する課題と主要ユースケース。> + +## 環境 + +| 環境 | Base URL | +|------|----------| +| 開発 | `https://api-dev.example.com` | +| ステージング | `https://api-stg.example.com` | +| 本番 | `https://api.example.com` | + +## 認証 + +<認証方式とトークン取得手順> + +```http +Authorization: Bearer <token> +``` + +## 共通仕様 + +### リクエストヘッダ + +| ヘッダ | 必須 | 説明 | +|--------|------|------| +| `Authorization` | ✓ | 認証トークン | +| `Content-Type` | ✓ | `application/json` | +| `X-Request-ID` | - | トレース用UUID (省略時はサーバで採番) | + +### エラーレスポンス + +全エンドポイント共通: + +```json +{ + "error": { + "code": "INVALID_ARGUMENT", + "message": "人間向けメッセージ", + "details": [] + } +} +``` + +| HTTPステータス | code | 意味 | +|---------------|------|------| +| 400 | `INVALID_ARGUMENT` | リクエストパラメータ不正 | +| 401 | `UNAUTHENTICATED` | 認証情報なし/不正 | +| 403 | `PERMISSION_DENIED` | 権限不足 | +| 404 | `NOT_FOUND` | リソースなし | +| 409 | `CONFLICT` | 競合 | +| 429 | `RESOURCE_EXHAUSTED` | レート制限 | +| 500 | `INTERNAL` | サーバ内部エラー | + +### レート制限 + +- <制限値>。超過時は `429` と `Retry-After` ヘッダを返却。 + +## エンドポイント + +### <リソース名> + +#### `POST /v1/<resource>` + +<1行の要約> + +##### リクエスト + +```http +POST /v1/<resource> HTTP/1.1 +Host: api.example.com +Authorization: Bearer <token> +Content-Type: application/json + +{ + "name": "string", + "count": 0 +} +``` + +| フィールド | 型 | 必須 | 説明 | 制約 | +|-----------|-----|------|------|------| +| `name` | string | ✓ | リソース名 | 1〜64文字 | +| `count` | integer | - | 個数 | 0以上, 既定値0 | + +##### レスポンス (200 OK) + +```json +{ + "id": "res_01HXXXX", + "name": "example", + "count": 0, + "created_at": "2026-04-21T12:00:00Z" +} +``` + +| フィールド | 型 | 説明 | +|-----------|-----|------| +| `id` | string | ULID | +| `name` | string | 入力と同じ | +| `count` | integer | 入力と同じ | +| `created_at` | string (RFC3339) | 作成時刻 | + +##### エラー + +| ステータス | code | 条件 | +|-----------|------|------| +| 400 | `INVALID_ARGUMENT` | `name` が空 / 長さ超過 | +| 409 | `CONFLICT` | 同名のリソースが既に存在 | + +##### 備考 + +<冪等性キーの扱い、リトライ推奨条件、関連エンドポイントなど> + +#### `GET /v1/<resource>/{id}` + +...(同じ構造で記述)... + +## 変更履歴 + +| 日付 | バージョン | 変更内容 | +|------|-----------|---------| +| YYYY-MM-DD | v1.0.0 | 初版 | +``` + +## Writing tips + +1. List only errors that "can actually occur." Listing `500` for every endpoint carries zero information. Write the error conditions specific to that endpoint. +2. Always include a concrete example (JSON) alongside the request/response. A schema table alone is easily misread. +3. Always specify date-times in RFC3339 with an explicit timezone, e.g., `2026-04-21T12:00:00Z`. +4. For gRPC, break headings down by `.proto` service/method, and include the request/response message definitions alongside them. + +## Save location + +- REST: `docs/api/<service-name>.md` or `docs/api/<resource>.md` +- If there are multiple endpoints, organize hierarchically as `docs/api/<service>/<resource>.md` + +## References + +- OpenAPI 3.1 specification: https://spec.openapis.org/oas/v3.1.0 diff --git a/claude/skills/tech-docs-writer/references/readme.md b/claude/skills/tech-docs-writer/references/readme.md new file mode 100644 index 0000000..f7dd831 --- /dev/null +++ b/claude/skills/tech-docs-writer/references/readme.md @@ -0,0 +1,202 @@ +# README / Guide Template (Diataxis compliant) + +> **Source of truth:** `claude/ja/skills/tech-docs-writer/references/readme.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +Diataxis classifies documents into four quadrants: Tutorial / How-to / Reference / Explanation. +A README should not cram all four types into one file. The repository root `README.md` should function purely as an "entry point," with the details separated out under `docs/readme/`. + +## Type determination + +| What you want to write | Quadrant | Intended reader | +|---------------|------|---------| +| Getting a first-time user to something working | Tutorial | New onboarding | +| How to solve a specific problem | How-to | Developers who already know the project | +| List of configuration items / commands | Reference | Developers who look things up like a dictionary while implementing | +| Explanation of background / architecture | Explanation | People who want to understand the architecture | + +Do not mix two or more types within a single document. A structure where 「チュートリアルの途中で突然Reference表が挟まる」 (a Reference table suddenly appears midway through a tutorial) is hard to read. + +## Required interview items + +- Target audience (new onboarding / existing developers / operations staff) +- Which quadrant to write +- Project name and repository URL +- Assumed environment (language version / OS / dependent services) +- For Tutorial/How-to: the final state to achieve (a working app / output files) +- For Reference: what to enumerate (CLI commands / configuration keys / environment variables) + +## Root README.md template (entry point) + +```markdown +# <プロジェクト名> + +<1〜2文でプロジェクトの目的> + +[![CI](<badge-url>)](<link>) [![License](<badge>)](<link>) + +## これは何 + +<もう少し詳しい説明。3〜5文。何を解決するか、何を解決しないか。> + +## クイックスタート + +```bash +git clone <repo> +cd <repo> +make setup +make run +``` + +詳細は [セットアップガイド](docs/readme/getting-started.md) を参照。 + +## ドキュメント + +- [セットアップガイド (Tutorial)](docs/readme/getting-started.md) +- [よくあるタスク (How-to)](docs/readme/how-to.md) +- [設定リファレンス (Reference)](docs/readme/configuration.md) +- [アーキテクチャ解説 (Explanation)](docs/readme/architecture.md) +- [API仕様](docs/api/) +- [運用Runbook](docs/runbook/) + +## 開発者向け + +- コントリビュートガイド: [CONTRIBUTING.md](CONTRIBUTING.md) +- 変更履歴: [CHANGELOG.md](CHANGELOG.md) + +## ライセンス + +<license-name> +``` + +## Tutorial template (learning for beginners) + +```markdown +# <プロジェクト名> セットアップガイド + +このチュートリアルを最後まで進めると、ローカルで <X> が動く状態になります。 +所要時間の目安: <N>分。 + +## 前提 + +- <OS/言語バージョン/必要ツール> + +## ステップ1: <動詞で始める見出し> + +<コマンド> + +```bash +$ command +``` + +**確認**: <期待される出力や状態を明示> + +## ステップ2: ... + +... + +## 完成 + +<何が達成できたか、次に何を読めばよいか> + +## うまくいかない場合 + +<よくあるつまずきポイント2〜3つ> +``` + +## How-to template (steps for a specific task) + +```markdown +# <動詞ではじまるタイトル: 例「新しいAPIエンドポイントを追加する」> + +## 前提 + +- <既に満たされていること> + +## 手順 + +1. <ステップ> +2. <ステップ> +3. <ステップ> + +## 検証 + +<変更が正しく入ったかの確認方法> + +## 関連 + +- <関連するHow-to、参照ドキュメント> +``` + +## Reference template (enumeration of facts) + +```markdown +# <対象の名前> リファレンス + +## 設定項目 + +| キー | 型 | 既定値 | 説明 | +|------|-----|-------|------| +| `FOO` | string | `""` | <役割> | + +## コマンド + +### `<command> <subcommand>` + +<1行要約> + +**使い方**: +```bash +<command> <subcommand> [flags] +``` + +**フラグ**: + +| フラグ | 型 | 既定 | 説明 | +|--------|-----|-----|------| +| `--bar` | int | 10 | <説明> | +``` + +## Explanation template (explanation for understanding) + +```markdown +# <概念/アーキテクチャのタイトル> + +## 背景 + +<なぜこの設計/概念が存在するか> + +## 全体像 + +<Mermaid C4図 or アーキテクチャ図> + +## 主要コンポーネント + +### <コンポーネントA> + +<責務と他コンポーネントとの関係> + +## 設計上の選択 + +<採用したパターン・採用しなかったパターンと理由> +<関連ADRへのリンクを張る> + +## 参考 + +- <論文、書籍、ブログ> +``` + +## Writing tips + +1. A Tutorial is about "doing," not "reading to memorize." Place a confirmation point at each step. +2. Start How-to titles with a verb: use 「データベースに接続する」 rather than 「データベースに接続するには」. +3. Reference should prioritize completeness. Adding too much explanation drifts it toward Explanation and makes it harder to look things up. +4. Always include a diagram in Explanation. Text alone doesn't get the point across. + +## Save location + +- Repository root `README.md` (entry point) +- Details: `docs/readme/<topic>.md` (split per Tutorial/How-to/Reference/Explanation) + +## References + +- Diataxis official site: https://diataxis.fr/ diff --git a/claude/skills/tech-docs-writer/references/runbook.md b/claude/skills/tech-docs-writer/references/runbook.md new file mode 100644 index 0000000..1916491 --- /dev/null +++ b/claude/skills/tech-docs-writer/references/runbook.md @@ -0,0 +1,143 @@ +# Runbook Template (Google SRE Playbook style) + +> **Source of truth:** `claude/ja/skills/tech-docs-writer/references/runbook.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +Write the Runbook assuming the on-call engineer reads it at 3am. Verbose background explanations are unnecessary; structure it so decisions and actions can be taken immediately. + +## Required interview items + +- Alert name / symptom +- Severity (SEV / P1-P5, or your internal standard) +- Scope of impact (user-facing impact / internal impact only) +- Dashboard / log query URLs to check +- Candidate mitigations +- Owner of the root-cause resolution +- Escalation target (team / individual / Slack channel) + +Do not write a Runbook with zero mitigations decided. Even 「とりあえず再起動」 (just restart it for now) counts as a legitimate mitigation, so make sure to draw one out during the interview. + +## Template body + +```markdown +# Runbook: <アラート名 or 症状> + +- **重大度**: SEV<N> / P<N> +- **オーナーチーム**: <team> +- **最終更新**: YYYY-MM-DD +- **関連アラート**: `<AlertmanagerのalertnameまたはダッシュボードURL>` + +## Symptom (症状) + +<何が見えたらこのRunbookを開くか。アラート文・ダッシュボード上の見え方・ユーザー報告の典型文を具体的に。> + +例: +- Alertmanager: `HighErrorRate` が firing +- Grafana `<url>` で 5xx率 が 5%超 + +## Impact (影響) + +- **ユーザー影響**: <例: APIレスポンスが失敗する。影響対象は全ユーザー/特定機能> +- **内部影響**: <例: 非同期ジョブが遅延> +- **SLO影響**: <例: 可用性SLOに対して X%消費する見込み> + +## Diagnosis (診断手順) + +原因の切り分けをチェックリスト形式で。上から順に実施。 + +1. **依存サービスのステータス確認** + - コマンド: `kubectl get pods -n <ns>` + - 見るもの: `Running` 以外のPodがないか +2. **直近デプロイの有無** + - `kubectl rollout history deployment/<name> -n <ns>` + - 直前30分以内のデプロイがあれば §Mitigation の「ロールバック」を優先 +3. **外部依存の障害** + - <依存サービスのステータスページURL> +4. **リソース逼迫** + - ダッシュボード: <url> + - CPU/メモリ/コネクション数を確認 + +## Mitigation (暫定対応) + +原因が切り分けできていなくても、ユーザー影響を止めるための手を列挙。**副作用を明記**。 + +### A. ロールバック (直近デプロイが原因の場合) + +```bash +kubectl rollout undo deployment/<name> -n <ns> +``` + +- 所要時間: 2〜3分 +- 副作用: 新機能は一時的に使えなくなる + +### B. 対象Podの再起動 + +```bash +kubectl rollout restart deployment/<name> -n <ns> +``` + +- 所要時間: 1〜5分 +- 副作用: 処理中リクエストが中断される可能性 + +### C. トラフィックを別リージョンに寄せる + +```bash +<コマンド or 手順> +``` + +- 所要時間: 1分 +- 副作用: レイテンシが増える + +## Resolution (根本対応) + +<恒久対応の方針。Mitigation後にオーナーチームで行う作業。> +<該当Issue/PRがあればリンクを張る。> + +## Verification (復旧確認) + +- [ ] Grafana `<url>` のエラー率が通常範囲に戻っている (5分間継続) +- [ ] アラートが resolved になっている +- [ ] 関連するカナリア/合成監視が成功している + +## Escalation (エスカレーション) + +- **Primary**: `#team-<name>` Slack (<slack-url>) +- **Secondary**: `#oncall-<service>` に `@here` で投稿 +- **30分経過しても収束しない場合**: SREチーム `#sre-oncall` + +## Postmortem + +- SEV2以上の場合、収束後24時間以内に postmortem を開始する +- テンプレート: <postmortem-template-url> + +## 変更履歴 + +| 日付 | 変更内容 | 変更者 | +|------|---------|--------| +| YYYY-MM-DD | 初版 | <name> | +``` + +## Writing tips + +1. Write Diagnosis in an order such that "working through it top to bottom narrows down the cause." Don't create mental detours. +2. Paste commands in an executable form. It's fine to have `<name>` placeholders, but state what to fill in immediately before the command. +3. Always document side effects for each Mitigation. Don't make someone run an operation with unknown side effects in the middle of the night. +4. Don't conflate Resolution with Mitigation. Mitigation stops the bleeding; Resolution cures the underlying cause. +5. Don't neglect updates. Renaming Alertmanager rules, changing K8s namespaces, and similar changes make a Runbook stale. Reflecting the diff every time the alert fires is about the right cadence. + +## Auto-drafting from alert rules + +When starting from a `PrometheusRule` or similar, use the following as the initial draft for `Symptom` and `Diagnosis`: + +- `alert:` value → Runbook title +- `expr:` → basis for the Diagnosis "check metrics" step +- `annotations.summary` / `description` → Symptom body text +- If `annotations.runbook_url` is empty, propose filling it in with this Runbook's URL + +## Save location + +- `docs/runbook/<alertname-or-topic>.md` +- A 1:1 correspondence with the alert rule is preferable (group them once things grow large) + +## References + +- Google SRE Workbook - On-Call: https://sre.google/workbook/on-call/ diff --git a/claude/skills/tech-docs-writer/references/system-design.md b/claude/skills/tech-docs-writer/references/system-design.md new file mode 100644 index 0000000..d7e6571 --- /dev/null +++ b/claude/skills/tech-docs-writer/references/system-design.md @@ -0,0 +1,294 @@ +# System Design Document (arc42 + C4 + MADR) + +> **Source of truth:** `claude/ja/skills/tech-docs-writer/references/system-design.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +Describes the design of an entire system or subsystem. Use an ADR (`adr.md`) when documenting a single decision. + +De facto combination: +- Skeleton: arc42 (12-section template) +- Diagrams: C4 Model (4 layers: Context/Container/Component/Deployment) +- Decisions: MADR (embedded in §9, or linked to an ADR file) + +## Required interview items + +- System name +- Purpose (the problem being solved, in 1-3 sentences) +- Key stakeholders (dev team / operations / biz / external users) +- 3 or more key quality requirements (NFRs) — as measurable values (e.g., P99 latency 100ms) +- Key constraints (existing infrastructure / budget / deadline / regulations) +- Technology stack already decided (if any) + +Do not start writing if quality requirements cannot be expressed as measurable values. 「高速」 (fast) or 「安定」 (stable) alone cannot be verified. + +## Template body + +```markdown +# <システム名> アーキテクチャ設計書 + +- **バージョン**: v0.1 (Draft) / v1.0 (Approved) +- **最終更新**: YYYY-MM-DD +- **オーナー**: <チーム名> +- **Status**: Draft / In Review / Approved / Superseded + +## 1. Introduction and Goals (目的と目標) + +### 1.1 背景・目的・目標 + +簡潔に。各項目1〜2文に収める。長文の説明は §3 や §4 に回す。 + +#### 背景(課題) + +<現状の問題を事実ベースで1〜2文。例: バッチ集計が1時間遅延し、レコメンドの鮮度が要件を満たさない。> + +#### 目的 + +<このシステムで達成することを1文。例: イベント発生からランキング反映までを数秒以内にする。> + +#### 目標 + +測定可能な指標を3個まで。詳細は §10 品質要件へ。 + +1. <例: イベント投入→ランキング反映 P99 2秒以内> +2. <例: 99.95% 可用性> +3. <例: 10万イベント/秒までスケール> + +### 1.2 主要品質目標 (Top 3-5) + +| 優先順位 | 品質特性 | 具体的な目標値 | +|---------|---------|--------------| +| 1 | パフォーマンス | P99レイテンシ 100ms以下 | +| 2 | 可用性 | 99.9% (月次停止 43分以内) | +| 3 | 拡張性 | 書き込み 10k RPS まで水平拡張 | + +### 1.3 ステークホルダー + +| 役割 | 関心事 | 連絡先 | +|------|-------|--------| +| <例: AI基盤チーム> | 開発・運用オーナー | #team-ai-platform | +| <例: SRE> | 運用引き継ぎ | #sre | +| <例: Biz> | リリース時期 | <PdM名> | + +## 2. Architecture Constraints (制約) + +| 種類 | 制約 | 理由 | +|------|------|------| +| 技術 | Go 1.22+ で実装 | 既存スタックと統一 | +| 運用 | Kubernetesで稼働 | 既存基盤を利用 | +| 規制 | 個人情報を国外保存しない | 法務要件 | +| 予算 | <具体値> | | + +## 3. System Scope and Context (範囲とコンテキスト) + +<C4 Level 1: System Context Diagram を Mermaid で> + +```mermaid +flowchart LR + User([User]) -->|HTTPS| Sys[<This System>] + Sys -->|gRPC| UpstreamA[Upstream Service A] + Sys -->|REST| UpstreamB[External SaaS B] + Sys <-->|events| MQ[Message Broker] +``` + +### 3.1 対象(In Scope) + +- <このシステムが責任を持つこと> + +### 3.2 対象外(Out of Scope) + +- <明示的に対象外のもの。あとで揉めがちな箇所> + +### 3.3 外部インタフェース + +| 相手 | プロトコル | 方向 | 備考 | +|------|----------|------|------| +| Upstream A | gRPC | Sys→A | 認証: mTLS | +| SaaS B | REST | Sys→B | Rate Limit 100 RPM | + +## 4. Solution Strategy (解決方針) + +<採用するパターンと技術選択の要約。詳細な決定は §9 へ。> + +- **アーキテクチャパターン**: 例) イベント駆動 + CQRS +- **主要な技術選択**: 例) Go + gRPC + PostgreSQL + Kafka + Kubernetes +- **主要な設計原則**: 例) 書き込み経路はIdempotent / 読み取りはリードレプリカから + +## 5. Building Block View (構成要素) + +<C4 Level 2: Container Diagram を Mermaid で> + +```mermaid +flowchart TB + subgraph System[<This System>] + API[API Gateway] + Worker[Async Worker] + DB[(PostgreSQL)] + Cache[(Redis)] + end + Client --> API + API --> DB + API --> Cache + API -->|enqueue| MQ[Kafka] + MQ --> Worker + Worker --> DB +``` + +### 5.1 <Container A: API Gateway> + +- **責務**: <1〜2文> +- **技術**: Go, gin +- **インタフェース**: REST `/v1/*` +- **依存**: PostgreSQL, Redis, Kafka + +### 5.2 <Container B: Async Worker> + +<同上> + +### 5.3 Component View (必要な場合) + +<C4 Level 3: 主要Containerの内部分解> + +## 6. Runtime View (動的振る舞い) + +<主要シナリオのシーケンス図。ハッピーパス + エラーパス 各1つ以上。> + +### 6.1 <シナリオ: ユーザー登録> + +```mermaid +sequenceDiagram + participant U as User + participant A as API + participant D as DB + participant Q as Kafka + U->>A: POST /v1/users + A->>D: INSERT user + D-->>A: OK + A->>Q: publish user.created + A-->>U: 201 Created +``` + +### 6.2 <シナリオ: 障害時> + +<例: upstreamタイムアウト時の振る舞い> + +## 7. Deployment View (デプロイ) + +<C4 Level 4: Deployment Diagram> + +```mermaid +flowchart TB + subgraph Region[AWS ap-northeast-1] + subgraph EKS[EKS Cluster] + Pods[API Pods x3] + WPods[Worker Pods x2] + end + RDS[(RDS PostgreSQL Multi-AZ)] + EC[(ElastiCache Redis)] + MSK[MSK Kafka] + end + Pods --> RDS + Pods --> EC + Pods --> MSK + MSK --> WPods + WPods --> RDS +``` + +- **リージョン**: ap-northeast-1 (DR計画は §11) +- **インフラ**: EKS + RDS Multi-AZ + MSK +- **CI/CD**: GitHub Actions → ArgoCD + +## 8. Crosscutting Concepts (横断的関心事) + +### 8.1 認証・認可 + +<例: JWT + OAuth2 Authorization Code Flow> + +### 8.2 ロギング・可観測性 + +<例: OpenTelemetry → Grafana Stack (Loki/Tempo/Prometheus)> + +### 8.3 エラーハンドリング + +<例: 共通エラー型、リトライポリシー、回路遮断> + +### 8.4 データ整合性 + +<例: Outbox Pattern で DB↔Kafka を atomic に> + +### 8.5 セキュリティ + +<例: 機密は AWS Secrets Manager、コードには埋め込まない> + +## 9. Architecture Decisions (意思決定ログ) + +本システムに関連する重要な設計判断は ADR として別管理: + +| ADR | タイトル | Status | +|-----|---------|--------| +| [ADR-0005](../adr/0005-adopt-cqrs.md) | CQRSパターンの採用 | Accepted | +| [ADR-0012](../adr/0012-adopt-vald-for-vector-search.md) | ベクトル検索にValdを採用 | Accepted | + +<新しい決定が必要になったら、このシステム設計書を更新する前に ADR を追加すること。> + +## 10. Quality Requirements (品質要件) + +品質シナリオ形式で**測定可能な形**で記述する。 + +### 10.1 パフォーマンス + +| シナリオID | 刺激 | 環境 | 応答 | 応答尺度 | +|-----------|-----|------|------|---------| +| P-1 | クライアントがAPIを呼ぶ | 通常負荷 (1000 RPS) | 応答を返す | P99 100ms以下 | +| P-2 | 同上 | ピーク負荷 (5000 RPS) | 応答を返す | P99 300ms以下 | + +### 10.2 可用性 + +| シナリオID | 刺激 | 環境 | 応答 | 応答尺度 | +|-----------|-----|------|------|---------| +| A-1 | 1台のPodが停止 | 本番運用中 | サービス継続 | エラー率1%未満 | +| A-2 | AZが1つ停止 | 本番運用中 | サービス継続 | 30秒以内にfailover | + +### 10.3 スケーラビリティ / セキュリティ / その他 + +<以下同様に> + +## 11. Risks and Technical Debt (リスクと技術的負債) + +| ID | 種類 | 内容 | 影響 | 対処方針 | +|----|------|------|------|---------| +| R-1 | リスク | 外部SaaSの依存(B) | SaaS障害でサービス停止 | §8.3のCircuit Breakerで部分縮退 | +| D-1 | 技術的負債 | 旧認証基盤をv0で残す | 2系統運用 | v1.3で廃止予定 | + +## 12. Glossary (用語集) + +| 用語 | 定義 | +|------|------| +| <用語A> | <ドメイン固有の定義> | +``` + +## Writing tips + +1. Always write §10 Quality Requirements as measurable values. 「十分なパフォーマンス」 (sufficient performance) is not a quality requirement. Assign scenario IDs (P-1, A-1) so they can later be tied to SLOs/SLIs. +2. Keep §9 strictly as a collection of links to ADRs. Don't discuss the design here — discussion belongs in a separate ADR. +3. Keep diagrams at the correct C4 granularity. Don't put class names in a Container diagram (that's the job of a Component/Code diagram). In Mermaid, use `flowchart` and `sequenceDiagram` appropriately for each purpose. +4. Respect the hierarchy in §5 Building Block View: Level 2 (Container) → Level 3 (Component), in that order. Don't start straight from a class diagram. +5. Clearly state the Status transition Draft → In Review → Approved, distinguishing before/after review. + +## Division of labor with ADR + +| What you want to write | Where | +|-------------|--------| +| 「なぜ X を選んだか」 (a single decision) | ADR (`docs/adr/NNNN-...md`) | +| 「このシステム全体がどう動くか」 | System design document (this template) | +| 「システム設計書に影響する新しい決定」 | First write an ADR, then link it in §9 and update the design document | + +## Save location + +- `docs/design/<system-name>.md` +- If there are subsystems: `docs/design/<system-name>/<subsystem>.md` + +## References + +- arc42 official site: https://arc42.org/ +- arc42 template: https://github.com/arc42/arc42-template +- C4 Model: https://c4model.com/ +- MADR: https://adr.github.io/madr/ diff --git a/claude/skills/work-intake/SKILL.md b/claude/skills/work-intake/SKILL.md new file mode 100644 index 0000000..96c40a6 --- /dev/null +++ b/claude/skills/work-intake/SKILL.md @@ -0,0 +1,62 @@ +--- +name: work-intake +description: Entry-point skill for the dev loop that picks up a ready ticket from Notion, validates it against the spec contract (rules/spec-contract.md), and returns a normalized work item. Use for "次の work 拾って" ("grab the next work item"), "ready ticket ある?" ("any ready tickets?"), "work-intake", etc. Tickets that fail the contract are skipped with a reason comment. The watched DB is resolved from a memory reference. +--- + +> **Source of truth:** `claude/ja/skills/work-intake/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# work-intake + +The entry point of the dev loop. Picks a single ready ticket and normalizes it into a work item that dev-cycle can implement autonomously. +**Does not fix or fill in ticket content** (that's for humans + write-spec to handle). + +## Procedure + +### Step 1: Resolve configuration (no hardcoding) + +- Get the watched Notion DB / ready flag / "in progress" status representation from the reference memory in `MEMORY.md` +- If not found, explicitly state "memory reference not registered" and stop (ask the user to register it) +- Option: if a ticket URL is passed as an argument, skip the enumeration in Step 2 and target only that ticket + +### Step 2: Enumerate ready tickets + +- Enumerate tickets in the ready state following the procedure in `references/notion-adapter.md` +- If there are 0, explicitly state "none found" and exit normally + +### Step 3: Contract validation (output judgment for every item) + +- Apply the validation checklist from `~/.claude/rules/spec-contract.md` to each ticket +- **Always output the judgment (satisfied / not satisfied + reason) for every item** (no silent skipping) +- For tickets that don't satisfy it: skip and post a comment on the ticket with the reason for the deficiency (do not edit the body) + +### Step 4: Selection and state transition + +- Select **exactly one** ticket from the contract-satisfying ones, in priority order (oldest first for equal priority) +- Update the selected ticket's status to the "in progress" equivalent (to prevent double-pickup. State lives on the source side; this skill holds no state of its own) + +### Step 5: Work item output + +``` +## work item +- source: notion +- id: <ticket id> +- url: <ticket URL> +- target repo: <from spec meta> +- priority: <from spec meta> +- remaining ready count: <N> (for reference) + +### spec +<all sections: purpose / scope & non-goals / design body / DoD / constraints / meta> +``` + +## On failure + +- Notion MCP unreachable / auth expired: do not retry; explicitly state "cannot reach Notion" and exit abnormally (notification is the caller's responsibility) + +## Iron rules + +1. Do not fix or fill in ticket content (non-goal) +2. Contract validation must output a judgment for every item (no silent skipping) +3. Writes are status updates + comment additions only (never edit the ticket body) +4. Never transcribe the full ticket body into logs / insights (reference by id / URL only) +5. Do not hardcode DB information into the skill (the memory reference is the source of truth) diff --git a/claude/skills/work-intake/references/notion-adapter.md b/claude/skills/work-intake/references/notion-adapter.md new file mode 100644 index 0000000..ad2c83b --- /dev/null +++ b/claude/skills/work-intake/references/notion-adapter.md @@ -0,0 +1,33 @@ +> **Source of truth:** `claude/ja/skills/work-intake/references/notion-adapter.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# Notion adapter (operational procedure for work-intake Step 2-4) + +If a new source needs to be added in the future, create a `references/<source>-adapter.md` file alongside this one (SKILL.md should not need to change). + +## Enumerating ready tickets (Step 2) + +1. Use the DB info from the memory reference (DB name / URL / ready flag representation) +2. Use a Notion MCP search-type tool to fetch tickets from the target DB and filter down to ones matching the ready flag + - Candidate tools: `notion-search` (search specifying the target DB) / `notion-query-database-view` / `notion-fetch` (fetch individually) +3. For each candidate, fetch the page body with `notion-fetch` and read the spec sections (purpose / scope & non-goals / design body / DoD / constraints / meta) + +## Status update (Step 4) + +- Change the status property to the "in progress" value from the memory reference using `notion-update-page` +- Immediately before changing it, re-confirm that the current value is still ready (a stand-in for optimistic locking. If it isn't, assume another process picked it up and move to the next candidate) + +## Skip comment (Step 3) + +- Post the reason for the deficiency using `notion-create-comment`. Format: + +``` +work-intake: skipped due to unmet spec contract +- <list only the deficient checklist items (item name + reason)> +Reference: rules/spec-contract.md +``` + +## Notes + +- Do not edit the ticket body (changing the body via `notion-update-page`). Only the status property and comments may be touched +- Tickets with no priority property set are treated as lowest priority +- Contract validation is applied to **all** enumerated ready tickets (so that no deficient ticket is missed for a skip comment). Selection is the one with the highest priority among the tickets that satisfy the contract diff --git a/claude/skills/write-spec/SKILL.md b/claude/skills/write-spec/SKILL.md new file mode 100644 index 0000000..128b622 --- /dev/null +++ b/claude/skills/write-spec/SKILL.md @@ -0,0 +1,61 @@ +--- +name: write-spec +description: Assistant skill that polishes a human's design draft into a spec that satisfies the spec contract (rules/spec-contract.md). Makes no design decisions itself — only interrogates gaps, formats, and validates against the contract. Use for "spec にして" ("turn this into a spec"), "spec 書くの手伝って" ("help me write a spec"), "設計を spec 化" ("formalize this design into a spec"), etc. Only a human may set the ready flag. +--- + +> **Source of truth:** `claude/ja/skills/write-spec/SKILL.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# write-spec + +Polishes a design that a human has written and led into a spec that an agent (dev-cycle) can implement autonomously. +**Makes no design decisions. Does not create an implementation plan either.** Only finds gaps, asks about them, and once they're filled in, formats and validates. + +## Input + +Any of: a design explanation from the conversation / a local file / a Notion page URL. + +## Procedure + +### Step 1: Read the contract + +Read `~/.claude/rules/spec-contract.md` (required sections / validation checklist / meaning of ready). + +### Step 2: Map the draft onto the template + +Sort the input into required sections 1-6, and present the user with a list of sections that are unfilled or ambiguous. + +### Step 3: Interrogation (one question at a time) + +Using the perspectives in `references/interrogation.md`, confirm unfilled spots one question at a time via AskUserQuestion. + +- Do not fill in answers on your own. When offering choices, limit any "recommended" option to one, with a stated rationale +- Do not keep raising objections to a design the human has confirmed as "good as is" (flag a concern once, then follow their decision) +- When confirming a proposal (a candidate DoD / a candidate constraint, etc.), **restate the proposal's content inside the question itself** (a reference to the preceding message alone may not be visible to the user) +- **Escape hatch for undesigned areas**: if it turns out that an entire section has no design at all — not just a gap to fill — delegate to the `grill-me` skill for deeper exploration, then return to this step with the result. When delegating, constrain the scope via args (e.g., "Limit to failure-mode behavior in contract section 3 (design body)"). Do not let it run open-ended +- Record the status of every perspective (done / skipped + reason) and include it in the Step 5 output + +### Step 4: Trigger check for api-design-review (mechanical) + +If the design body includes any of the following, invoke the `api-design-review` skill and add any detected gaps to the Step 3 interrogation: + +- A new or changed API endpoint, RPC, event schema, enum, or public interface (**applies only to things exposed outside the repo**. Skill-to-skill contracts internal to the harness are out of scope — handle those in the design body of the spec) +- A change to the ACL / permission model + +If none apply, skip and note the reason in the Step 5 output. + +### Step 5: Formatting and validation + +Assemble the spec in markdown, and output the judgment for **every item** of the contract's validation checklist (satisfied / not satisfied + reason). If any unsatisfied items remain, return to Step 3. + +### Step 6: Output + +- Present the finished spec (markdown that can be pasted into Notion) +- Only write to Notion if the user explicitly instructs it (via MCP) +- **Do not set the ready flag** — end by stating explicitly that "setting it to ready is for the human to do" + +## Iron rules + +1. Do not make design decisions or implementation plans on the human's behalf (stay strictly in an assisting role) +2. Always output the judgment for every item of the validation checklist and the implementation status of every interrogation perspective (no silent skipping) +3. Interrogate one question at a time (prefer multiple choice) +4. Do not hardcode project-specific information (e.g., the Notion DB); get it from the reference in `MEMORY.md` diff --git a/claude/skills/write-spec/references/interrogation.md b/claude/skills/write-spec/references/interrogation.md new file mode 100644 index 0000000..7981439 --- /dev/null +++ b/claude/skills/write-spec/references/interrogation.md @@ -0,0 +1,51 @@ +> **Source of truth:** `claude/ja/skills/write-spec/references/interrogation.md` (Japanese). To update, edit the Japanese source first, then re-translate this file into English. + +# Interrogation perspective checklist (write-spec Step 3) + +Whenever a perspective's trigger condition is met, confirm the corresponding question one at a time via AskUserQuestion. +Include the implementation status of every perspective (done / skipped + reason) in the Step 5 validation output (no silent skipping). + +## Always on (applied to every spec) + +### 1. Concreteness of the purpose + +- "The day after this change ships, what will be visibly different, to count as success?" +- If the purpose is phrased as a means (e.g., "introduce X"), ask about the value beyond that + +### 2. Scope boundaries + +- Enumerate the peripheral elements that appear in the design body and confirm "will Y be touched this time, or not?" +- If non-goals is empty, draw out at least one "thing we'll probably want to do but are holding off on this time" + +### 3. Machine-verifiability of the DoD + +- Tie each DoD item to "which command / procedure will verify this?" +- Turn "X works correctly" into something concrete: "given what input, what output counts as correct?" + +### 4. Explicitness of constraints + +- Is adding a new dependency allowed (and under what conditions if so)? +- Is there a performance target / acceptable degradation (if none, have them state "none" explicitly)? +- Does it handle secrets / PII? + +## Conditional (triggered by the content of the design body) + +### 5. External interfaces + +- Trigger condition: includes a new or changed API / RPC / event schema / enum / public interface +- → Delegate to api-design-review in SKILL.md Step 4 (do not duplicate the interrogation here) + +### 6. Data / state + +- Trigger condition: includes persistence / migration / data model changes +- Backward compatibility: what happens to existing data? What's the rollback procedure? + +### 7. Failure-mode behavior + +- Trigger condition: includes calls to external services / asynchronous processing +- On failure, does it retry or give up, and who sees the failure, and how? + +### 8. Operations + +- Trigger condition: includes a long-running process / scheduled execution / new service +- How is "it's running" observed (logs / metrics / alerts)? diff --git a/claude/statusline-command.sh b/claude/statusline-command.sh new file mode 100644 index 0000000..357fc52 --- /dev/null +++ b/claude/statusline-command.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Claude Code status line — mirrors key elements of Starship config +# Input: JSON from stdin (Claude Code statusLine protocol) + +input=$(cat) + +# --- directory (truncate to last 6 path segments, home as ~) --- +cwd=$(echo "$input" | jq -r '.cwd // ""') +cwd="${cwd/#$HOME/~}" +IFS='/' read -ra parts <<< "$cwd" +if [ "${#parts[@]}" -gt 6 ]; then + cwd="…/$(IFS='/'; echo "${parts[*]: -6}")" +fi + +# --- git branch --- +branch=$(git -C "$(echo "$input" | jq -r '.cwd // "."')" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null) +if [ -n "$branch" ]; then + if [ "${#branch}" -gt 12 ]; then + branch="${branch:0:12}…" + fi + git_part=" ${branch}" +else + git_part="" +fi + +# --- git status (modified / untracked) --- +if [ -n "$branch" ]; then + git_cwd=$(echo "$input" | jq -r '.cwd // "."') + unstaged=$(git -C "$git_cwd" --no-optional-locks diff --name-only 2>/dev/null | wc -l | tr -d ' ') + staged=$(git -C "$git_cwd" --no-optional-locks diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ') + untracked=$(git -C "$git_cwd" --no-optional-locks ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ') + status_str="" + [ "$staged" -gt 0 ] && status_str="${status_str}+${staged}" + [ "$unstaged" -gt 0 ] && status_str="${status_str}!${unstaged}" + [ "$untracked" -gt 0 ] && status_str="${status_str}?${untracked}" + [ -n "$status_str" ] && git_part="${git_part} [${status_str}]" +fi + +# --- model (short name) --- +model=$(echo "$input" | jq -r '.model.display_name // ""') + +# --- context remaining --- +remaining=$(echo "$input" | jq -r '.context_window.remaining_percentage // empty') +if [ -n "$remaining" ]; then + ctx_part=" ctx:$(printf '%.0f' "$remaining")%" +else + ctx_part="" +fi + +# --- assemble --- +printf "\033[34m%s\033[0m%s \033[33m%s\033[0m%s" \ + "$cwd" \ + "$git_part" \ + "$model" \ + "$ctx_part" diff --git a/coc-settings.json b/coc-settings.json deleted file mode 100755 index bddeccc..0000000 --- a/coc-settings.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "json.enable": true, - "coc": { - "preferences": { - "formatOnType": true, - "formatOnSaveFileTypes": [ - "go", - "nix", - "typescript", - "typescriptreact", - "dockerfile", - "json", - "sh", - "zsh", - "bash", - "vim", - "eruby", - "markdown", - "python" - ] - }, - "source": { - "around.enable": false, - "neco.enable": false - } - }, - "signature": { - "preferShownAbove": false, - "target": "float" - }, - "session.directory": "~/.cache/vim/sessions", - "diagnostic": { - "messageTarget": "echo", - "refreshOnInsertMode": true - }, - "suggest": { - "noselect": true, - "preferCompleteThanJumpPlaceholder": true, - "detailField": "abbr", - "enablePreview": true, - "enablePreselect": false, - "floatEnable": true, - "snippetIndicator": "", - "triggerAfterInsertEnter": true, - "echodocSupport": true, - "timeout": 5000 - }, - "languageserver": { - "golang": { - "command": "gopls", - "rootPatterns": ["**/go.mod", "**/go.sum", ".vim/", ".git/", ".hg/"], - "filetypes": ["go"], - "initializationOptions": { - "deepCompletion": true, - "hoverKind": "FullDocumentation", - "staticcheck": true, - "usePlaceholders": true, - "env": { - "CGO_CFLAGS": "-g -O=fast -march=native", - "CGO_CPPFLAGS": "-g -O=fast -march=native", - "CGO_CXXFLAGS": "-g -O=fast -march=native", - "CGO_FFLAGS": "-g -O=fast -march=native", - "CGO_LDFLAGS": "-g -O=fast -march=native" - }, - "analyses": { - "nonewvars": true, - "unreachable": true, - "unusedparams": true - } - } - } - }, - "go.goplsOptions": { - "completeUnimported": true, - "deepCompletion": true, - "gofumpt": true, - "staticcheck": true, - "completionDocumentation": true, - "hoverKind": "FullDocumentation", - "linkTarget": "pkg.go.dev", - "usePlaceholders": true, - "buildFlags": [ - "-tags=e2e" - ], - "env": { - "CGO_CFLAGS": "-g -O=fast -march=native", - "CGO_CPPFLAGS": "-g -O=fast -march=native", - "CGO_CXXFLAGS": "-g -O=fast -march=native", - "CGO_FFLAGS": "-g -O=fast -march=native", - "CGO_LDFLAGS": "-g -O=fast -march=native" - }, - "analyses": { - "nonewvars": true, - "unreachable": true, - "unusedparams": true - } - }, - "tsserver": { - "enable": true - }, - "eslint": { - "packageManager": "yarn", - "nodePath": ".yarn/sdks" - }, - "cSpell": { - "fixSpellingWithRenameProvider": true, - "enabledLanguageIds": [ - "asciidoc", - "c", - "cpp", - "csharp", - "css", - "git-commit", - "gitcommit", - "go", - "handlebars", - "haskell", - "html", - "jade", - "java", - "javascript", - "javascriptreact", - "json", - "jsonc", - "latex", - "less", - "markdown", - "nvim", - "php", - "plaintext", - "pug", - "python", - "restructuredtext", - "rust", - "scala", - "scss", - "text", - "typescript", - "typescriptreact", - "vim", - "yaml", - "yml" - ], - "ignoreRegExpList": [ - ".{1}dur", - ".{1}len", - ".{2}dur", - ".{2}ech", - ".{2}err", - ".{2}len", - ".{4}ch" - ], - "userWords": [ - "nvim", - "cSpell", - "Debugf", - "vald", - "vdaas", - "vec", - "kubernetes", - "Kubernetes", - "upsert", - "Upsert" - ] - }, - "snippets.ultisnips.pythonPrompt": false, - "svelte.enable-ts-plugin": false -} diff --git a/config/nvim/after/ftplugin/go.lua b/config/nvim/after/ftplugin/go.lua new file mode 100644 index 0000000..3b10825 --- /dev/null +++ b/config/nvim/after/ftplugin/go.lua @@ -0,0 +1,3 @@ +vim.bo.expandtab = false +vim.bo.shiftwidth = 4 +vim.bo.tabstop = 4 diff --git a/config/nvim/after/ftplugin/javascript.lua b/config/nvim/after/ftplugin/javascript.lua new file mode 100644 index 0000000..ac29fb0 --- /dev/null +++ b/config/nvim/after/ftplugin/javascript.lua @@ -0,0 +1,4 @@ +vim.bo.expandtab = true +vim.bo.shiftwidth = 2 +vim.bo.tabstop = 2 +vim.bo.softtabstop = 2 diff --git a/config/nvim/after/ftplugin/lua.lua b/config/nvim/after/ftplugin/lua.lua new file mode 100644 index 0000000..ac29fb0 --- /dev/null +++ b/config/nvim/after/ftplugin/lua.lua @@ -0,0 +1,4 @@ +vim.bo.expandtab = true +vim.bo.shiftwidth = 2 +vim.bo.tabstop = 2 +vim.bo.softtabstop = 2 diff --git a/config/nvim/after/ftplugin/python.lua b/config/nvim/after/ftplugin/python.lua new file mode 100644 index 0000000..3ca5eb9 --- /dev/null +++ b/config/nvim/after/ftplugin/python.lua @@ -0,0 +1,4 @@ +vim.bo.expandtab = true +vim.bo.shiftwidth = 4 +vim.bo.tabstop = 4 +vim.bo.softtabstop = 4 diff --git a/config/nvim/after/ftplugin/typescript.lua b/config/nvim/after/ftplugin/typescript.lua new file mode 100644 index 0000000..f12b26c --- /dev/null +++ b/config/nvim/after/ftplugin/typescript.lua @@ -0,0 +1,5 @@ +vim.bo.expandtab = true +vim.bo.shiftwidth = 2 +vim.bo.tabstop = 2 +vim.bo.softtabstop = 2 + diff --git a/config/nvim/init.lua b/config/nvim/init.lua new file mode 100644 index 0000000..588203c --- /dev/null +++ b/config/nvim/init.lua @@ -0,0 +1,4 @@ +require("config.base") +require("config.lazy") +require("config.keymap") +require("config.format") diff --git a/config/nvim/lua/config/base.lua b/config/nvim/lua/config/base.lua new file mode 100644 index 0000000..c57468f --- /dev/null +++ b/config/nvim/lua/config/base.lua @@ -0,0 +1,150 @@ +local opt = vim.opt +-- -------------------- +-- Encoding +-- -------------------- +opt.encoding = "utf-8" +opt.fileencoding = "utf-8" +opt.number = true +vim.scriptencoding = "utf-8" + +-- ------------------------- +-- ---- Default Setting ---- +-- ------------------------- +opt.completeopt = { "menu", "preview", "noinsert" } +opt.wrap = true +opt.synmaxcol = 2000 +opt.showmatch = true +opt.matchtime = 2 +opt.list = true +opt.listchars = { space = " ", tab = "> ", trail = "_", eol = "↲", extends = "»", precedes = "«", nbsp = "%" } +opt.display = "lastline" +opt.nrformats = "" +opt.virtualedit = "block" +opt.wildmenu = true +opt.wildmode = { "list:longest", "full" } +opt.autoread = true +opt.autowrite = true +opt.swapfile = false +opt.writebackup = false +opt.backup = false +opt.clipboard:append("unnamedplus") +opt.splitright = true +opt.splitbelow = true +opt.incsearch = true +opt.ignorecase = true +opt.wrapscan = true +opt.infercase = true +opt.smartcase = true +opt.laststatus = 2 +opt.showcmd = true +opt.visualbell = true +vim.t_vb = "" +opt.errorbells = false +-- opt.expandtab = true +opt.shiftwidth = 4 +opt.tabstop = 4 +opt.smarttab = true +opt.softtabstop = 0 +opt.autoindent = true +opt.smartindent = true +opt.shiftround = true +opt.list = true +opt.whichwrap = "b,s,h,l,<,>,[,]" +opt.scrolloff = 5 +opt.backspace = { "indent", "eol", "start" } +opt.matchpairs:append("<:>") +opt.switchbuf = "useopen" +opt.history = 100 +opt.mouse = "a" +opt.lazyredraw = true +opt.ttyfast = true + +opt.viminfo = "'100,/50,%,<1000,f50,s100,:100,c,h,!" +opt.shortmess:append("I") +opt.fileformat = "unix" +opt.fileformats = { "unix", "dos", "mac" } +opt.foldmethod = "manual" + +if vim.fn.executable("zsh") == 1 then + opt.shell = "zsh" +end + +-- ---------------------- +-- ---- Key mappings ---- +-- ---------------------- +vim.cmd("cnoreabbrev W! w!") +vim.cmd("cnoreabbrev Q! q!") +vim.cmd("cnoreabbrev Qall! qall!") +vim.cmd("cnoreabbrev Wq wq") +vim.cmd("cnoreabbrev Wa wa") +vim.cmd("cnoreabbrev wQ wq") +vim.cmd("cnoreabbrev WQ wq") +vim.cmd("cnoreabbrev W w") +vim.cmd("cnoreabbrev Q q") +vim.cmd("cnoreabbrev Qall qall") + +-- ---------------------------- +-- ---- AutoGroup Settings ---- +-- ---------------------------- +vim.cmd([[ +augroup AutoGroup + autocmd! +augroup END +]]) + +vim.cmd("command! -nargs=* Autocmd autocmd AutoGroup <args>") +vim.cmd("command! -nargs=* AutocmdFT autocmd AutoGroup FileType <args>") +vim.api.nvim_create_augroup("FileTypeIndent", { clear = true }) +local function set_indent(tab, shift, expand) + vim.bo.tabstop = tab + vim.bo.shiftwidth = shift + vim.bo.expandtab = expand +end +-- ------------------------------ +-- ---- Indentation settings ---- +-- ------------------------------ +-- indentLine settings (assuming a plugin like 'IndentLine' is being used) +vim.g.indentLine_faster = 1 +vim.api.nvim_set_keymap("n", "<silent><Leader>i", ":<C-u>IndentLinesToggle<CR>", { noremap = true, silent = true }) +vim.api.nvim_create_autocmd("FileType", { + group = "FileTypeIndent", + pattern = { "js", "jsx", "ts", "tsx", "typescript", "typescriptreact", "javascript", "javascriptreact" }, + -- command = "setlocal expandtab sw=2 ts=2 completeopt=menu,menuone,preview,noselect,noinsert", + callback = function() + set_indent(2, 2, true) + end +}) + +-- ------------------------------ +-- ---- Window size settings ---- +-- ------------------------------ +-- Workaround: Neovim 0.12.3 bug where get_range is called on an +-- invalidated/non-TSNode during injection processing (languagetree.lua). +do + local _orig_get_range = vim.treesitter.get_range + vim.treesitter.get_range = function(node, source, metadata) + local ok, result = pcall(_orig_get_range, node, source, metadata) + if ok then return result end + return { 0, 0, 0, 0, 0, 0 } + end +end + +vim.api.nvim_create_autocmd("FileType", { + callback = function(args) + pcall(vim.treesitter.start, args.buf) + end, +}) + +vim.api.nvim_create_autocmd({"WinNew", "WinClosed"}, { + callback = function() + vim.cmd("wincmd =") + end, +}) + +-- -------------------------------- +-- ---- Auto-reload on change ---- +-- -------------------------------- +vim.api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold" }, { + command = "checktime", +}) + diff --git a/config/nvim/lua/config/format.lua b/config/nvim/lua/config/format.lua new file mode 100644 index 0000000..79c0d93 --- /dev/null +++ b/config/nvim/lua/config/format.lua @@ -0,0 +1,45 @@ +vim.api.nvim_create_user_command("Format", function() + local view = vim.fn.winsaveview() + local ft = vim.bo.filetype + + if ft == "go" then + vim.lsp.buf.format({ + async = false, + filter = function(client) + return client.name == "gopls" + end, + }) + + elseif ft == "lua" then + vim.lsp.buf.format({ + async = false, + filter = function(client) + return client.name == "lua_ls" + end, + }) + + elseif ft == "javascript" or ft == "typescript" then + vim.lsp.buf.format({ + async = false, + filter = function(client) + return client.name == "tsserver" + end, + }) + + elseif ft == "python" then + vim.lsp.buf.format({ + async = false, + filter = function(client) + return client.name == "pyright" + end, + }) + + else + vim.lsp.buf.format({ async = false }) + end + + vim.fn.winrestview(view) +end, {}) + +vim.keymap.set("n", "<leader>f", ":Format<CR>", { desc = "Format buffer" }) + diff --git a/config/nvim/lua/config/keymap.lua b/config/nvim/lua/config/keymap.lua new file mode 100644 index 0000000..726a4a5 --- /dev/null +++ b/config/nvim/lua/config/keymap.lua @@ -0,0 +1,89 @@ +local keymap = vim.api.nvim_set_keymap + +keymap("n", "<CR>", "o<Esc>", { noremap = true, silent = true }) +keymap("c", "<C-a>", "<Home>", { noremap = true }) +keymap("i", "<C-a>", "<Home>", { noremap = true }) +keymap("c", "<C-e>", "<End>", { noremap = true }) +keymap("i", "<C-e>", "<End>", { noremap = true }) +keymap("c", "<C-l>", "<Right>", { noremap = true }) +keymap("i", "<C-l>", "<Right>", { noremap = true }) +keymap("c", "<C-h>", "<Left>", { noremap = true }) +keymap("i", "<C-h>", "<Left>", { noremap = true }) +keymap("i", "<C-v>", "<ESC><C-v>", { noremap = true }) +keymap("n", "<S-Tab>", "<<", { noremap = true }) + +-- 折り返した行を複数行として移動 +keymap("n", "j", "gj", { noremap = true, silent = true }) +keymap("n", "k", "gk", { noremap = true, silent = true }) +keymap("n", "gj", "j", { noremap = true, silent = true }) +keymap("n", "gk", "k", { noremap = true, silent = true }) + +-- ウィンドウの移動をCtrlキーと方向指定でできるように +keymap("n", "<C-h>", "<C-w>h", { noremap = true, silent = true }) +keymap("n", "<C-j>", "<C-w>j", { noremap = true, silent = true }) +keymap("n", "<C-k>", "<C-w>k", { noremap = true, silent = true }) +keymap("n", "<C-l>", "<C-w>l", { noremap = true, silent = true }) + +-- Esc2回で検索のハイライトを消す +keymap("n", "<Esc><Esc>", ":<C-u>nohlsearch<CR>", { noremap = true, silent = true }) + +-- gをバインドキーとしてタブ操作 +-- keymap("n", "gc", ":<C-u>tabnew<CR>", {noremap = true, silent = true}) +-- keymap("n", "gx", ":<C-u>tabclose<CR>", {noremap = true, silent = true}) +-- keymap("n", "gn", "gt", {noremap = true, silent = true}) +-- keymap("n", "gp", "gT", {noremap = true, silent = true}) + +-- g+oで現在開いている以外のタブを全て閉じる +keymap("n", "go", ":<C-u>tabonly<CR>", { noremap = true, silent = true }) + +keymap("n", ";", ":", { noremap = true }) +keymap("i", "<C-s>", "<esc>:w<CR>", { noremap = true }) +keymap("n", "<C-q>", ":qall<CR>", { noremap = true }) +keymap("n", "q", ":q<CR>", { noremap = true }) + +---@telescope +keymap("n", "<leader><leader>", +":lua require('telescope.builtin').find_files(require('telescope.themes').get_dropdown({}))<CR>", +{ noremap = true, silent = true }) +keymap("n", "<leader>fg", ":lua require('telescope.builtin').live_grep(require('telescope.themes').get_dropdown({}))<CR>", +{ noremap = true, silent = true }) +keymap("n", "<leader>fb", ":lua require('telescope.builtin').buffers(require('telescope.themes').get_dropdown({}))<CR>", +{ noremap = true, silent = true }) +keymap("n", "<leader>fh", ":lua require('telescope.builtin').help_tags(require('telescope.themes').get_dropdown({}))<CR>", +{ noremap = true, silent = true }) + +---@format +vim.api.nvim_create_autocmd("FileType", { + callback = function() + local ft = vim.bo.filetype + + -- Lua: space 2 + if ft == "lua" then + vim.bo.expandtab = true + vim.bo.shiftwidth = 2 + vim.bo.tabstop = 2 + vim.bo.softtabstop = 2 + + -- Go: tab 4 + elseif ft == "go" then + vim.bo.expandtab = false + vim.bo.shiftwidth = 4 + vim.bo.tabstop = 4 + + -- JS / TS: space 2 + elseif ft == "javascript" or ft == "typescript" then + vim.bo.expandtab = true + vim.bo.shiftwidth = 2 + vim.bo.tabstop = 2 + vim.bo.softtabstop = 2 + + -- Python: space 4 + elseif ft == "python" then + vim.bo.expandtab = true + vim.bo.shiftwidth = 4 + vim.bo.tabstop = 4 + vim.bo.softtabstop = 4 + end + end, +}) + diff --git a/config/nvim/lua/config/lazy.lua b/config/nvim/lua/config/lazy.lua new file mode 100644 index 0000000..b667647 --- /dev/null +++ b/config/nvim/lua/config/lazy.lua @@ -0,0 +1,75 @@ +-- Bootstrap lazy.nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local lazyrepo = "https://github.com/folke/lazy.nvim.git" + local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath }) + if vim.v.shell_error ~= 0 then + vim.api.nvim_echo({ + { "Failed to clone lazy.nvim:\n", "ErrorMsg" }, + { out, "WarningMsg" }, + { "\nPress any key to exit..." }, + }, true, {}) + vim.fn.getchar() + os.exit(1) + end +end +vim.opt.rtp:prepend(lazypath) + +-- Make sure to setup `mapleader` and `maplocalleader` before +-- loading lazy.nvim so that mappings are correct. +-- This is also a good place to setup other settings (vim.opt) +vim.g.mapleader = " " +vim.g.maplocalleader = "\\" + +-- Setup lazy.nvim +require("lazy").setup({ + spec = { + -- import your plugins + { import = "plugins" }, + }, + -- Configure any other settings here. See the documentation for more details. + -- colorscheme that will be used when installing plugins. + install = { colorscheme = { "habamax" } }, + concurrency = 2, + -- automatically check for plugin updates + checker = { enabled = true }, +}) + +-- Global mappings. +-- See `:help vim.diagnostic.*` for documentation on any of the below functions +vim.keymap.set('n', '<space>e', vim.diagnostic.open_float) +vim.keymap.set('n', '[d', vim.diagnostic.goto_prev) +vim.keymap.set('n', ']d', vim.diagnostic.goto_next) +vim.keymap.set('n', '<space>q', vim.diagnostic.setloclist) +-- +-- Use LspAttach autocommand to only map the following keys +-- after the language server attaches to the current buffer +vim.api.nvim_create_autocmd('LspAttach', { + group = vim.api.nvim_create_augroup('UserLspConfig', {}), + callback = function(ev) + -- Enable completion triggered by <c-x><c-o> + -- vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc' + + -- Buffer local mappings. + -- See `:help vim.lsp.*` for documentation on any of the below functions + local opts = { buffer = ev.buf } + + vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts) + vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts) + vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts) + vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts) + vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts) + vim.keymap.set('n', '<space>wa', vim.lsp.buf.add_workspace_folder, opts) + vim.keymap.set('n', '<space>wr', vim.lsp.buf.remove_workspace_folder, opts) + vim.keymap.set('n', '<space>wl', function() + print(vim.inspect(vim.lsp.buf.list_workspace_folders())) + end, opts) + vim.keymap.set('n', '<space>D', vim.lsp.buf.type_definition, opts) + vim.keymap.set('n', '<space>rn', vim.lsp.buf.rename, opts) + vim.keymap.set('n', '<space>ca', vim.lsp.buf.code_action, opts) + vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts) + vim.keymap.set('n', '<space>f', function() + vim.lsp.buf.format { async = true } + end, opts) + end, +}) diff --git a/config/nvim/lua/plugins/coding.lua b/config/nvim/lua/plugins/coding.lua new file mode 100644 index 0000000..64934e0 --- /dev/null +++ b/config/nvim/lua/plugins/coding.lua @@ -0,0 +1,131 @@ +return { + { + "JoosepAlviste/nvim-ts-context-commentstring", + opts = { + enable_autocmd = false, + }, + }, + { + "numToStr/Comment.nvim", + dependencies = { "JoosepAlviste/nvim-ts-context-commentstring" }, + opts = function() + local ts_hook = require("ts_context_commentstring.integrations.comment_nvim").create_pre_hook() + return { + pre_hook = function(ctx) + return ts_hook(ctx) or vim.bo.commentstring + end, + toggler = { + line = "<C-c>", + }, + opleader = { + line = "<C-c>", + }, + } + end, + }, + { + "windwp/nvim-autopairs", + opts = { + disable_filetype = { "TelescopePrompt", "vim" }, + }, + config = true, + }, + { + "copilotlsp-nvim/copilot-lsp", + }, + { + "zbirenbaum/copilot.lua", + -- dependencies = { "copilotlsp-nvim/copilot-lsp" }, + cmd = "Copilot", + -- build = ":Copilot auth", + event = "InsertEnter", + opts = { + suggestion = { enabled = false }, -- copilot-cmp と干渉するのでOFF + panel = { enabled = false },-- same above + }, + }, + { + "zbirenbaum/copilot-cmp", + dependencies = { "zbirenbaum/copilot.lua" }, + config = function() + require("copilot_cmp").setup() + end, + }, + { + "CopilotC-Nvim/CopilotChat.nvim", + dependencies = { + { "zbirenbaum/copilot.lua" }, -- or github/copilot.vim + { "nvim-lua/plenary.nvim" }, -- for curl, log wrapper + }, + opts = { + debug = false, + window = { + layout = "vertical", + width = 0.3, + }, + -- See Configuration section for options + }, + -- See Commands section for default commands if you want to lazy load on them + config = function() + require("CopilotChat").setup({ + prompts = { + Explain = { + prompt = "選択したコードの説明を日本語で書いてください", + mapping = "<leader>ce", + }, + Review = { + prompt = "コードを日本語でレビューしてください", + mapping = "<leader>cr", + }, + Fix = { + prompt = "このコードには問題があります。バグを修正したコードを表示してください。説明は日本語でお願いします", + mapping = "<leader>cf", + }, + Optimize = { + prompt = "選択したコードを最適化し、パフォーマンスと可読性を向上させてください。説明は日本語でお願いします", + mapping = "<leader>co", + }, + Docs = { + prompt = "選択したコードに関するドキュメントコメントを日本語で生成してください", + mapping = "<leader>cd", + }, + Tests = { + prompt = "選択したコードの詳細なユニットテストを書いてください。説明は日本語でお願いします", + mapping = "<leader>ct", + }, + Commit = { + prompt = require("CopilotChat.config.prompts").Commit.prompt, + mapping = "<leader>cco", + selection = require("CopilotChat.select").gitdiff, + }, + }, + }) + end, + + -- -- See Commands section for default commands if you want to lazy load on them + -- keys = { + -- { + -- "<leader>cc", + -- function() + -- require("CopilotChat").toggle() + -- end, + -- desc = "CopilotChat - Toggle", + -- }, + -- { + -- "<leader>cch", + -- function() + -- local actions = require("CopilotChat.actions") + -- require("CopilotChat.integrations.telescope").pick(actions.help_actions()) + -- end, + -- desc = "CopilotChat - Help actions", + -- }, + -- { "<leader>ccp", + -- function() + -- local actions = require("CopilotChat.actions") + -- require("CopilotChat.integrations.telescope").pick(actions.prompt_actions()) + -- end, + -- desc = "CopilotChat - Prompt actions", + -- }, + -- }, + }, +} diff --git a/config/nvim/lua/plugins/git.lua b/config/nvim/lua/plugins/git.lua new file mode 100644 index 0000000..11c715e --- /dev/null +++ b/config/nvim/lua/plugins/git.lua @@ -0,0 +1,14 @@ +return { + { + "lewis6991/gitsigns.nvim", + opts = { + signs = { + delete = { text = "_" }, + }, + signs_staged = { + delete = { text = "_" }, + }, + numhl = true, + }, + }, +} diff --git a/config/nvim/lua/plugins/lsp.lua b/config/nvim/lua/plugins/lsp.lua new file mode 100644 index 0000000..c6c5a9d --- /dev/null +++ b/config/nvim/lua/plugins/lsp.lua @@ -0,0 +1,236 @@ +-- lua/plugins/lsp.lua +-- mason-lspconfig v2 対応(setup_handlers は廃止) +-- Neovim 0.11+ 想定: vim.lsp.config() + vim.lsp.enable() + +local treesitter_languages = { + "bash", "c", "cpp", "dart", "dockerfile", "fish", "git_config", + "go", "gomod", "gosum", "gpg", "graphql", "helm", + "html", "css", "javascript", "typescript", "tsx", "json", + "lua", "make", "markdown", "markdown_inline", + "rust", "vim", "yaml", "kotlin", +} + +return { + -- Treesitter + -- main branch (rewrite) API: configs モジュールは廃止、install() で parser を導入する + -- highlight は従来どおり無効 (vim.treesitter.start() を呼ばない) + { + "nvim-treesitter/nvim-treesitter", + branch = "main", + lazy = false, + build = ":TSUpdate", + config = function() + require("nvim-treesitter").install(treesitter_languages) + end, + }, + + -- LSP + { + "neovim/nvim-lspconfig", + event = { "BufReadPre", "BufNewFile" }, + dependencies = { + { + "williamboman/mason.nvim", + config = function() + require("mason").setup() + end, + }, + { + "mason-org/mason-lspconfig.nvim", + -- v2系を想定(setup_handlers無し) + config = function() + require("mason-lspconfig").setup({ + -- automatic_enable は「インストール済みサーバーの有効化」のみ。 + -- インストール自体は ensure_installed が無いと行われないため、 + -- 設定対象のサーバーを明示して再現可能にする。 + ensure_installed = { + "ts_ls", -- TypeScript / JavaScript + "tailwindcss", + "html", + "cssls", + "lua_ls", + "kotlin_language_server", + "dockerls", + }, + -- v2では automatic_enable が導入され remember: デフォルトで有効 + -- 明示したいなら ↓ + automatic_enable = true, + }) + end, + }, + { "hrsh7th/cmp-nvim-lsp" }, + }, + config = function() + local cmp_lsp = require("cmp_nvim_lsp") + local capabilities = cmp_lsp.default_capabilities(vim.lsp.protocol.make_client_capabilities()) + + -- tsserver / ts_ls の揺れを吸収(存在する方を使う) + local ts_name = "tsserver" + do + local ok, configs = pcall(require, "lspconfig.configs") + if ok and configs and configs.ts_ls ~= nil then + ts_name = "ts_ls" + end + end + + local servers = { + gopls = { + cmd = { "gopls" }, + capabilities = capabilities, + settings = { + gopls = { + experimentalPostfixCompletions = true, + analyses = { unusedparams = true, shadow = true }, + buildFlags = { "-tags=e2e" }, + staticcheck = true, + }, + }, + init_options = { usePlaceholders = true }, + }, + + [ts_name] = { + capabilities = capabilities, + }, + + tailwindcss = { capabilities = capabilities }, + html = { capabilities = capabilities }, + cssls = { capabilities = capabilities }, + rust_analyzer = { capabilities = capabilities }, + + lua_ls = { + capabilities = capabilities, + settings = { + Lua = { + diagnostics = { globals = { "vim" } }, + workspace = { checkThirdParty = false }, + telemetry = { enable = false }, + format = { enable = true }, + }, + }, + }, + + kotlin_language_server = { capabilities = capabilities }, + + dockerls = { capabilities = capabilities }, + } + + -- 1) まず Neovim に「各サーバーの設定」を登録 + local enable_list = {} + for name, opts in pairs(servers) do + vim.lsp.config(name, opts) + table.insert(enable_list, name) + end + + -- 2) 有効化(インストール済みのものが attach される) + -- mason-lspconfig v2 の automatic_enable が有効なら、これ自体は不要な場合もあるが、 + -- 明示すると挙動が読みやすいので残す。 + vim.lsp.enable(enable_list) + end, + }, + + -- Completion(元の構成を維持) + { + "hrsh7th/nvim-cmp", + event = { "InsertEnter", "CmdlineEnter" }, + dependencies = { + { "hrsh7th/cmp-buffer", event = "InsertEnter" }, + { "hrsh7th/cmp-cmdline", event = "ModeChanged" }, + { "hrsh7th/cmp-nvim-lsp" }, + { "hrsh7th/cmp-nvim-lsp-signature-help", event = "InsertEnter" }, + { "hrsh7th/cmp-nvim-lua", event = "InsertEnter" }, + { "hrsh7th/cmp-path", event = "InsertEnter" }, + { + "L3MON4D3/LuaSnip", + build = "make install_jsregexp", + event = "InsertEnter", + config = function() + require("luasnip").config.set_config({ + history = true, + updateevents = "TextChanged,TextChangedI", + }) + require("luasnip.loaders.from_vscode").lazy_load() + end, + }, + { "saadparwaiz1/cmp_luasnip", event = "InsertEnter" }, + { "rafamadriz/friendly-snippets", event = "InsertEnter" }, + { "onsails/lspkind.nvim", event = "InsertEnter" }, + }, + config = function() + local cmp = require("cmp") + local luasnip = require("luasnip") + local lspkind = require("lspkind") + + local function has_words_before() + local line, col = vim.api.nvim_win_get_cursor(0) + return col ~= 0 + and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match("%s") == nil + end + + cmp.setup({ + snippet = { + expand = function(args) + require("luasnip").lsp_expand(args.body) + end, + }, + mapping = cmp.mapping.preset.insert({ + ["<C-j>"] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expand_or_jumpable() then + luasnip.expand_or_jump() + elseif has_words_before() then + cmp.complete() + else + fallback() + end + end, { "i", "s" }), + ["<C-k>"] = cmp.mapping(function() + if cmp.visible() then + cmp.select_prev_item() + elseif luasnip.jumpable(-1) then + luasnip.jump(-1) + end + end, { "i", "s" }), + ["<CR>"] = cmp.mapping.confirm({ select = true }), + }), + sources = cmp.config.sources({ + { name = "nvim_lsp" }, + { name = "nvim_lsp_signature_help" }, + { name = "nvim_lua" }, + { name = "luasnip" }, + { name = "copilot" }, + { name = "buffer" }, + { name = "path" }, + { name = "cmdline" }, + }), + formatting = { + format = lspkind.cmp_format({ + mode = "symbol_text", + preset = "codicons", + maxwidth = 50, + ellipsis_char = "...", + }), + }, + sorting = { + priority_weight = 2, + comparators = { + function(e1, e2) + local k = require("cmp").lsp.CompletionItemKind + if e1:get_kind() == k.Variable and e2:get_kind() ~= k.Variable then + return false + end + if e2:get_kind() == k.Variable and e1:get_kind() ~= k.Variable then + return true + end + end, + require("cmp.config.compare").offset, + require("cmp.config.compare").exact, + require("cmp.config.compare").score, + }, + }, + completion = { completeopt = "menu,menuone,noselect" }, + experimental = { ghost_text = true }, + }) + end, + }, +} diff --git a/config/nvim/lua/plugins/ui.lua b/config/nvim/lua/plugins/ui.lua new file mode 100644 index 0000000..c08c4e1 --- /dev/null +++ b/config/nvim/lua/plugins/ui.lua @@ -0,0 +1,200 @@ +return { + -- the colorscheme should be available when starting Neovim + { + "folke/tokyonight.nvim", + lazy = false, -- make sure we load this during startup if it is your main colorscheme + priority = 999, -- make sure to load this before all the other start plugins + config = function() + -- load the colorscheme here + vim.cmd([[colorscheme tokyonight]]) + end, + }, + --- bufffer line + { + "akinsho/bufferline.nvim", + event = "VeryLazy", + }, + --- status line + { + "nvim-lualine/lualine.nvim", + dependencies = { + 'nvim-tree/nvim-web-devicons' + }, + event = "VeryLazy", + opts = function() + local opts = { + theme = "auto", + sections = { + lualine_c = { + { "filename", path = 1 }, + }, + }, + } + return opts + end, + }, + -- tree + { + "nvim-neo-tree/neo-tree.nvim", + branch = "v3.x", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-tree/nvim-web-devicons", -- not strictly required, but recommended + "MunifTanjim/nui.nvim", + -- "3rd/image.nvim", -- Optional image support in preview window: See `# Preview Mode` for more information + }, + keys = { + { + "<C-e>", + function() + require("neo-tree.command").execute({ toggle = true }) + end, + desc = "Explorer NeoTree (Root Dir)", + }, + }, + opts = { + window = { + width = 25, + mappings = { + ["<space>"] = "", + ["h"] = { + "toggle_node", + nowait = true, + }, + ["l"] = "open", + ["<cr>"] = "", + ["hs"] = "open_split", + ["vs"] = "open_vsplit", + ["s"] = "focus_preview", + ["S"] = "", + }, + }, + filesystem = { + window = { + fuzzy_finder_mappings = { + ["j"] = "move_cursor_down", + ["k"] = "move_cursor_up", + }, + }, + }, + }, + }, + -- which key + { + "folke/which-key.nvim", + event = "VeryLazy", + }, + -- noice + { + "folke/noice.nvim", + event = "VeryLazy", + opts = { + -- add any options here + lsp = { + -- override markdown rendering so that **cmp** and other plugins use **Treesitter** + override = { + ["vim.lsp.util.convert_input_to_markdown_lines"] = true, + ["vim.lsp.util.stylize_markdown"] = true, + ["cmp.entry.get_documentation"] = true, -- requires hrsh7th/nvim-cmp + }, + }, + -- you can enable a preset for easier configuration + presets = { + bottom_search = true, -- use a classic bottom cmdline for search + command_palette = true, -- position the cmdline and popupmenu together + long_message_to_split = true, -- long messages will be sent to a split + inc_rename = false, -- enables an input dialog for inc-rename.nvim + lsp_doc_border = false, -- add a border to hover docs and signature help + }, + }, + dependencies = { + -- if you lazy-load any plugin below, make sure to add proper `module="..."` entries + "MunifTanjim/nui.nvim", + -- OPTIONAL: + -- `nvim-notify` is only needed, if you want to use the notification view. + -- If not available, we use `mini` as the fallback + "rcarriga/nvim-notify", + }, + init = function() + -- + vim.opt.lazyredraw = false + end + }, + -- icons + { + "echasnovski/mini.icons", + lazy = true, + opts = { + file = { + [".keep"] = { glyph = "󰊢", hl = "MiniIconsGrey" }, + ["devcontainer.json"] = { glyph = "", hl = "MiniIconsAzure" }, + }, + filetype = { + dotenv = { glyph = "", hl = "MiniIconsYellow" }, + }, + }, + init = function() + package.preload["nvim-web-devicons"] = function() + require("mini.icons").mock_nvim_web_devicons() + return package.loaded["nvim-web-devicons"] + end + end, + }, + -- TODO: + { + 'b0o/incline.nvim', + config = function() + local helpers = require("incline.helpers") + local devicons = require("nvim-web-devicons") + require('incline').setup { + render = function(props) + local filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(props.buf), ':t') + if filename == '' then + filename = '[No Name]' + end + local ft_icon, ft_color = devicons.get_icon_color(filename) + local function get_git_diff() + local icons = { removed = '', changed = '', added = '' } + local signs = vim.b[props.buf].gitsigns_status_dict + local labels = {} + if signs == nil then + return labels + end + for name, icon in pairs(icons) do + if tonumber(signs[name]) and signs[name] > 0 then + table.insert(labels, { icon .. signs[name] .. ' ', group = 'Diff' .. name }) + end + end + if #labels > 0 then + table.insert(labels, { '┊ ' }) + end + return labels + end + local function get_diagnostic_label() + local icons = { error = '', warn = '', info = '', hint = '' } + local label = {} + for severity, icon in pairs(icons) do + local n = #vim.diagnostic.get(props.buf, { severity = vim.diagnostic.severity[string.upper(severity)] }) + if n > 0 then + table.insert(label, { icon .. n .. ' ', group = 'DiagnosticSign' .. severity }) + end + end + if #label > 0 then + table.insert(label, { '┊ ' }) + end + return label + end + return { + { get_diagnostic_label() }, + { get_git_diff() }, + { ft_icon and { ' ', ft_icon, ' ', guibg = ft_color, guifg = helpers.contrast_color(ft_color) } or '' }, + ' ', + { filename .. ' ', gui = vim.bo[props.buf].modified and 'bold,italic' or 'bold' }, + } + end + } + end, + -- Optional: Lazy load Incline + event = 'VeryLazy', + }, +} diff --git a/config/nvim/lua/plugins/util.lua b/config/nvim/lua/plugins/util.lua new file mode 100644 index 0000000..11b088b --- /dev/null +++ b/config/nvim/lua/plugins/util.lua @@ -0,0 +1,36 @@ +return { + { + "nvim-telescope/telescope.nvim", + event = "VeryLazy", + cmd = "Telescope", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-telescope/telescope-ui-select.nvim", + }, + config = function() + local telescope = require("telescope") + telescope.setup({ + defaults = { + layout_config = { + vertical = { width = 0.8 }, + }, + mappings = { + }, + }, + extensions = { + file_browser = { + -- disables netrw and use telescope-file-browser in its place + mappings = { + -- your custom insert mode mappings + }, + }, + ["ui-select"] = { + require("telescope.themes").get_dropdown({}) + -- even more opts + } + }, + }) + telescope.load_extension("ui-select") + end + } +} diff --git a/config/sheldon/plugins.toml b/config/sheldon/plugins.toml new file mode 100644 index 0000000..0624e56 --- /dev/null +++ b/config/sheldon/plugins.toml @@ -0,0 +1,46 @@ +# `sheldon` configuration file +# ---------------------------- +# +# You can modify this file directly or you can use one of the following +# `sheldon` commands which are provided to assist in editing the config file: +# +# - `sheldon add` to add a new plugin to the config file +# - `sheldon edit` to open up the config file in the default editor +# - `sheldon remove` to remove a plugin from the config file +# +# See the documentation for more https://github.com/rossmacarthur/sheldon#readme + +shell = "zsh" + +[plugins] + +# For example: +# +# [plugins.base16] +# github = "chriskempson/base16-shell" + +[plugins.zsh-defer] +github = 'romkatv/zsh-defer' +apply = ['source'] + +[plugins.zsh-completions] +github = "zsh-users/zsh-completions" +use = ["{{ name }}.plugin.zsh"] + +[plugins.zsh-autosuggestions] +github = "zsh-users/zsh-autosuggestions" +use = ["{{ name }}.zsh"] + +[plugins.zsh-syntax-highlighting] +github = 'zsh-users/zsh-syntax-highlighting' +apply = ['defer'] + +[plugins.fzf] +github = "junegunn/fzf" +apply = ['fzf-install', 'fzf-source'] + +[templates] +PATH = 'export PATH="$PATH:{{ dir }}"' +defer = "{% for file in files %}zsh-defer source \"{{ file }}\"\n{% endfor %}" +fzf-install = "{{ dir }}/install --bin > /dev/null \n[[ ! $PATH =~ {{ dir }} ]] && export PATH=\"$PATH:{{ dir }}/bin\"\n" +fzf-source = "{% for file in files %}source \"{{ file }}\"\n{% endfor %}" diff --git a/dockers/base.Dockerfile b/dockers/base.Dockerfile index 3d1ab6c..2290a90 100755 --- a/dockers/base.Dockerfile +++ b/dockers/base.Dockerfile @@ -51,6 +51,7 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean \ upx \ wget \ xclip \ + xsel \ zsh \ && update-alternatives --set cc $(which clang) \ && update-alternatives --set c++ $(which clang++) \ diff --git a/dockers/docker.Dockerfile b/dockers/docker.Dockerfile index 68cfafb..08a9fd5 100755 --- a/dockers/docker.Dockerfile +++ b/dockers/docker.Dockerfile @@ -11,7 +11,7 @@ ENV RELEASE_LATEST releases/latest ENV LOCAL /usr/local ENV BIN_PATH ${LOCAL}/bin -FROM aquasec/trivy:latest AS trivy +FROM aquasec/trivy:0.69.3 AS trivy FROM goodwithtech/dockle:latest AS dockle-base @@ -29,16 +29,18 @@ FROM docker-base AS slim RUN set -x; cd "$(mktemp -d)" \ && ORG="slimtoolkit" \ && BIN_NAME="slim" \ + && BIN_NAME_MINT="mint" \ && REPO="${ORG}/${BIN_NAME}" \ && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" \ && DOCKER_SLIM_RELEASES="https://downloads.dockerslim.com/releases" \ && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/${VERSION}/dist_${OS}.tar.gz" \ && tar zxvf dist_${OS}.tar.gz \ && mv dist_${OS}/docker* ${BIN_PATH} \ + && mv dist_${OS}/${BIN_NAME_MINT}* ${BIN_PATH} \ && mv dist_${OS}/${BIN_NAME}* ${BIN_PATH} \ && upx -9 \ - ${BIN_PATH}/${BIN_NAME} \ - ${BIN_PATH}/${BIN_NAME}-sensor + ${BIN_PATH}/${BIN_NAME_MINT} \ + ${BIN_PATH}/${BIN_NAME_MINT}-sensor FROM docker-base AS docker-credential-pass RUN set -x; cd "$(mktemp -d)" \ @@ -167,5 +169,6 @@ COPY --from=dockfmt ${BIN_PATH}/dockfmt ${DOCKER_PATH}/dockfmt COPY --from=dockle ${BIN_PATH}/dockle ${DOCKER_PATH}/dockle COPY --from=slim ${BIN_PATH}/docker-slim ${DOCKER_PATH}/docker-slim COPY --from=slim ${BIN_PATH}/slim ${DOCKER_PATH}/slim -COPY --from=slim ${BIN_PATH}/slim-sensor ${DOCKER_PATH}/slim-sensor +COPY --from=slim ${BIN_PATH}/mint-sensor ${DOCKER_PATH}/mint-sensor +COPY --from=slim ${BIN_PATH}/mint ${DOCKER_PATH}/mint COPY --from=trivy ${BIN_PATH}/trivy ${DOCKER_PATH}/trivy diff --git a/dockers/env.Dockerfile b/dockers/env.Dockerfile index ebaf70c..2c5d8fe 100755 --- a/dockers/env.Dockerfile +++ b/dockers/env.Dockerfile @@ -7,6 +7,7 @@ ARG GROUP_ID=1000 ARG DOCKER_GROUP_ID=961 ARG GROUP_IDS=${GROUP_ID} ARG WHOAMI=vankichi +ARG TARGETARCH ENV BASE_DIR /home ENV USER ${WHOAMI} @@ -16,6 +17,10 @@ ENV GROUP sudo,root,users,docker,wheel ENV UID ${USER_ID} ENV GITHUB https://github.com ENV API_GITHUB https://api.github.com/repos +ENV CFLAGS "-mno-avx512f -mno-avx512dq -mno-avx512cd -mno-avx512bw -mno-avx512vl" +ENV CXXFLAGS "-std=c++20 ${CFLAGS}" +ENV CC gcc +ENV CXX g++ RUN groupadd --non-unique --gid ${DOCKER_GROUP_ID} docker \ && groupadd --non-unique --gid ${GROUP_ID} wheel \ @@ -52,6 +57,8 @@ RUN echo $'/lib\n\ RUN apt-get update -y \ && apt-get upgrade -y \ && apt-get install -y --no-install-recommends --fix-missing \ + build-essential \ + ca-certificates \ libgtk-3-dev \ liblzma-dev \ libhdf5-serial-dev \ @@ -67,12 +74,17 @@ RUN apt-get update -y \ python3-setuptools \ python3-venv \ && apt-get clean \ - && curl -LO "https://github.com/neovim/neovim/releases/download/stable/nvim-linux64.tar.gz" \ - && tar -zxvf nvim-linux64.tar.gz \ - && mv ./nvim-linux64/bin/nvim /usr/bin/nvim \ + && case "${TARGETARCH}" in \ + amd64) NVIM_ARCH="x86_64" ;; \ + arm64) NVIM_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; \ + esac \ + && curl -fL -o "nvim-linux-${NVIM_ARCH}.tar.gz" "https://github.com/neovim/neovim/releases/download/v0.12.0/nvim-linux-${NVIM_ARCH}.tar.gz" \ + && tar -zxf "nvim-linux-${NVIM_ARCH}.tar.gz" \ + && mv "./nvim-linux-${NVIM_ARCH}/bin/nvim" /usr/bin/nvim \ && chmod 755 -R /usr/bin/nvim \ - && mv ./nvim-linux64/share/nvim /usr/share/nvim \ - && mv ./nvim-linux64/lib/nvim /usr/lib/nvim \ + && mv ./nvim-linux-${NVIM_ARCH}/share/nvim /usr/share/nvim \ + && mv ./nvim-linux-${NVIM_ARCH}/lib/nvim /usr/lib/nvim \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \ && pip3 install --upgrade --break-system-packages ranger-fm thefuck httpie python-language-server vim-vint grpcio-tools \ @@ -86,7 +98,6 @@ RUN nvim -v \ && npm install -g n RUN n lts \ - && hash -r \ && bash -c "chown -R ${USER} $(npm config get prefix)/{lib/node_modules,bin,share}" \ && bash -c "chmod -R 755 $(npm config get prefix)/{lib/node_modules,bin,share}" \ && npm install -g \ @@ -96,11 +107,10 @@ RUN n lts \ markdownlint-cli \ npm \ prettier \ - resume-cli \ - terminalizer \ typescript \ typescript-language-server \ yarn \ + --omit=optional --no-audit --no-fund \ && bash -c "chown -R ${USER} $(npm config get prefix)/{lib/node_modules,bin,share}" \ && bash -c "chmod -R 755 $(npm config get prefix)/{lib/node_modules,bin,share}" \ && apt purge -y nodejs npm \ @@ -113,7 +123,11 @@ RUN cmake --version WORKDIR /tmp RUN set -x; cd "$(mktemp -d)" \ && OS="linux" \ - && ARCH="x86_64" \ + && case "${TARGETARCH}" in \ + amd64) ARCH="x86_64" ;; \ + arm64) ARCH="aarch_64" ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; \ + esac \ && REPO_NAME="protobuf" \ && BIN_NAME="protoc" \ && RELEASE_LATEST="releases/latest" \ @@ -130,10 +144,15 @@ WORKDIR /tmp ARG NGT_VERSION ENV CFLAGS "-mno-avx512f -mno-avx512dq -mno-avx512cd -mno-avx512bw -mno-avx512vl" ENV CXXFLAGS ${CFLAGS} -RUN curl -LO "https://github.com/yahoojapan/NGT/archive/v${NGT_VERSION}.tar.gz" \ +RUN curl -LO "https://github.com/NGT-labs/NGT/archive/v${NGT_VERSION}.tar.gz" \ && tar zxf "v${NGT_VERSION}.tar.gz" -C /tmp \ && cd "/tmp/NGT-${NGT_VERSION}" \ - && cmake -DNGT_LARGE_DATASET=ON . \ + && if [ "$TARGETARCH" = "arm64" ]; then \ + export CFLAGS=""; \ + export CXXFLAGS=""; \ + fi \ + && cmake -DNGT_LARGE_DATASET=ON -DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" . \ + # && cmake -DNGT_LARGE_DATASET=ON . \ && make -j -C "/tmp/NGT-${NGT_VERSION}" \ && make install -C "/tmp/NGT-${NGT_VERSION}" \ && cd /tmp \ diff --git a/dockers/go.Dockerfile b/dockers/go.Dockerfile index 4bebbc7..bad2569 100755 --- a/dockers/go.Dockerfile +++ b/dockers/go.Dockerfile @@ -1,6 +1,9 @@ FROM vankichi/dev-base:latest AS go-base ARG GO_VERSION +ARG TARGETOS=linux +ARG TARGETARCH + ENV GO111MODULE on ENV DEBIAN_FRONTEND noninteractive ENV INITRD No @@ -11,39 +14,39 @@ ENV GOBIN ${GOPATH}/bin ENV GOFLAGS "-ldflags=-w -ldflags=-s" WORKDIR /opt -RUN curl -sSL -O "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz" \ - && tar zxf "go${GO_VERSION}.linux-amd64.tar.gz" \ - && rm "go${GO_VERSION}.linux-amd64.tar.gz" \ - && ln -s /opt/go/bin/go /usr/bin/ \ - && mkdir -p ${GOBIN} +RUN curl -sSL -O "https://dl.google.com/go/go${GO_VERSION}.${TARGETOS}-${TARGETARCH}.tar.gz" \ + && tar zxf "go${GO_VERSION}.${TARGETOS}-${TARGETARCH}.tar.gz" \ + && rm "go${GO_VERSION}.${TARGETOS}-${TARGETARCH}.tar.gz" \ + && ln -s /opt/go/bin/go /usr/bin/ \ + && mkdir -p ${GOBIN} FROM go-base AS gotests RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - github.com/cweill/gotests/gotests@latest \ - && chmod a+x ${GOBIN}/gotests \ - && upx -9 ${GOBIN}/gotests + --ldflags "-s -w" --trimpath \ + github.com/cweill/gotests/gotests@latest \ + && chmod a+x ${GOBIN}/gotests \ + && upx -9 ${GOBIN}/gotests FROM go-base AS ghq RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - github.com/x-motemen/ghq@latest \ - && chmod a+x ${GOBIN}/ghq \ - && upx -9 ${GOBIN}/ghq + --ldflags "-s -w" --trimpath \ + github.com/x-motemen/ghq@latest \ + && chmod a+x ${GOBIN}/ghq \ + && upx -9 ${GOBIN}/ghq FROM go-base AS efm RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - github.com/mattn/efm-langserver@latest \ - && chmod a+x ${GOBIN}/efm-langserver \ - && upx -9 ${GOBIN}/efm-langserver + --ldflags "-s -w" --trimpath \ + github.com/mattn/efm-langserver@latest \ + && chmod a+x ${GOBIN}/efm-langserver \ + && upx -9 ${GOBIN}/efm-langserver FROM go-base AS golint RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - golang.org/x/lint/golint@latest \ - && chmod a+x ${GOBIN}/golint \ - && upx -9 ${GOBIN}/golint + --ldflags "-s -w" --trimpath \ + golang.org/x/lint/golint@latest \ + && chmod a+x ${GOBIN}/golint \ + && upx -9 ${GOBIN}/golint FROM golangci/golangci-lint:latest AS golangci-lint-base FROM go-base AS golangci-lint @@ -53,55 +56,60 @@ RUN upx -9 ${GOBIN}/golangci-lint FROM go-base AS gofumpt RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - mvdan.cc/gofumpt@latest \ - && chmod a+x ${GOBIN}/gofumpt \ - && upx -9 ${GOBIN}/gofumpt + --ldflags "-s -w" --trimpath \ + mvdan.cc/gofumpt@latest \ + && chmod a+x ${GOBIN}/gofumpt \ + && upx -9 ${GOBIN}/gofumpt FROM go-base AS goimports RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - golang.org/x/tools/cmd/goimports@latest \ - && chmod a+x ${GOBIN}/goimports \ - && upx -9 ${GOBIN}/goimports + --ldflags "-s -w" --trimpath \ + golang.org/x/tools/cmd/goimports@latest \ + && chmod a+x ${GOBIN}/goimports \ + && upx -9 ${GOBIN}/goimports FROM go-base AS goimports-update-ignore RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - github.com/pwaller/goimports-update-ignore@latest \ - && chmod a+x ${GOBIN}/goimports-update-ignore \ - && upx -9 ${GOBIN}/goimports-update-ignore + --ldflags "-s -w" --trimpath \ + github.com/pwaller/goimports-update-ignore@latest \ + && chmod a+x ${GOBIN}/goimports-update-ignore \ + && upx -9 ${GOBIN}/goimports-update-ignore FROM go-base AS gopls RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - golang.org/x/tools/gopls@latest \ - && chmod a+x ${GOBIN}/gopls \ - && upx -9 ${GOBIN}/gopls + --ldflags "-s -w" --trimpath \ + golang.org/x/tools/gopls@latest \ + && chmod a+x ${GOBIN}/gopls \ + && upx -9 ${GOBIN}/gopls FROM go-base AS hugo -RUN git clone https://github.com/gohugoio/hugo --depth 1 \ - && cd hugo \ - && go install \ - --ldflags "-s -w" --trimpath \ - && chmod a+x ${GOBIN}/hugo \ - && upx -9 ${GOBIN}/hugo +RUN GO111MODULE=on go install \ + --ldflags "-s -w" --trimpath \ + github.com/gohugoio/hugo@latest \ + && chmod a+x ${GOBIN}/hugo \ + && upx -9 ${GOBIN}/hugo +# RUN git clone https://github.com/gohugoio/hugo --depth 1 \ +# && cd hugo \ +# && go install \ +# --ldflags "-s -w" --trimpath \ +# && chmod a+x ${GOBIN}/hugo \ +# && upx -9 ${GOBIN}/hugo FROM go-base AS prototool RUN GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - github.com/uber/prototool/cmd/prototool@dev \ - && chmod a+x ${GOBIN}/prototool \ - && upx -9 ${GOBIN}/prototool + --ldflags "-s -w" --trimpath \ + github.com/uber/prototool/cmd/prototool@dev \ + && chmod a+x ${GOBIN}/prototool \ + && upx -9 ${GOBIN}/prototool FROM go-base AS fzf RUN BIN_NAME="fzf" \ - && REPO="junegunn/${BIN_NAME}" \ - && GO111MODULE=on go install \ - --ldflags "-s -w" --trimpath \ - github.com/${REPO}@latest \ - && chmod a+x "${GOPATH}/bin/${BIN_NAME}" \ - && upx -9 "${GOPATH}/bin/${BIN_NAME}" + && REPO="junegunn/${BIN_NAME}" \ + && GO111MODULE=on go install \ + --ldflags "-s -w" --trimpath \ + github.com/${REPO}@latest \ + && chmod a+x "${GOPATH}/bin/${BIN_NAME}" \ + && upx -9 "${GOPATH}/bin/${BIN_NAME}" FROM go-base AS go RUN upx -9 ${GOROOT}/bin/* diff --git a/dockers/k8s.Dockerfile b/dockers/k8s.Dockerfile index 40e5a7d..e7d8daf 100755 --- a/dockers/k8s.Dockerfile +++ b/dockers/k8s.Dockerfile @@ -1,5 +1,8 @@ FROM vankichi/dev-base:latest AS kube-base +ARG TARGETOS +ARG TARGETARCH + ENV ARCH amd64 ENV OS linux ENV GITHUB https://github.com @@ -13,197 +16,206 @@ ENV LOCAL /usr/local ENV BIN_PATH ${LOCAL}/bin RUN apt-get update && apt-get install -y --no-install-recommends \ - python3-dev \ - python3-setuptools \ - python3-pip \ - python3-venv \ - && mkdir -p ${BIN_PATH} + python3-dev \ + python3-setuptools \ + python3-pip \ + python3-venv \ + && mkdir -p ${BIN_PATH} FROM kube-base AS kubectl RUN set -x; cd "$(mktemp -d)" \ - && mkdir -p ${BIN_PATH} \ - && curl -fsSLo ${BIN_PATH}/kubectl "${GOOGLE}/kubernetes-release/release/$(curl -s ${GOOGLE}/kubernetes-release/release/stable.txt)/bin/${OS}/${ARCH}/kubectl" \ - && chmod a+x ${BIN_PATH}/kubectl \ - && ${BIN_PATH}/kubectl version --client + && mkdir -p ${BIN_PATH} \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLo ${BIN_PATH}/kubectl "${GOOGLE}/kubernetes-release/release/$(curl -s ${GOOGLE}/kubernetes-release/release/stable.txt)/bin/${TARGETOS}/${ARCH}/kubectl" \ + && chmod a+x ${BIN_PATH}/kubectl \ + && ${BIN_PATH}/kubectl version --client FROM kube-base AS helm RUN set -x; cd "$(mktemp -d)" \ - && curl "${RAWGITHUB}/helm/helm/master/scripts/get-helm-3" | bash \ - && BIN_NAME="helm" \ - && chmod a+x "${BIN_PATH}/${BIN_NAME}" \ - && upx -9 "${BIN_PATH}/${BIN_NAME}" + && curl "${RAWGITHUB}/helm/helm/master/scripts/get-helm-3" | bash \ + && BIN_NAME="helm" \ + && chmod a+x "${BIN_PATH}/${BIN_NAME}" \ + && upx -9 "${BIN_PATH}/${BIN_NAME}" FROM kube-base AS helmfile RUN set -x; cd "$(mktemp -d)" \ - && ORG="roboll" \ - && NAME="helmfile" \ - && REPO="${ORG}/${NAME}" \ - && HELMFILE_VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLo ${BIN_PATH}/helmfile "${GITHUB}/${REPO}/${RELEASE_DL}/v${HELMFILE_VERSION}/helmfile_${OS}_${ARCH}" \ - && chmod a+x ${BIN_PATH}/helmfile + && ORG="roboll" \ + && NAME="helmfile" \ + && REPO="${ORG}/${NAME}" \ + && HELMFILE_VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLo ${BIN_PATH}/helmfile "${GITHUB}/${REPO}/${RELEASE_DL}/v${HELMFILE_VERSION}/helmfile_${OS}_${ARCH}" \ + && chmod a+x ${BIN_PATH}/helmfile FROM kube-base AS kubectx RUN set -x; cd "$(mktemp -d)" \ - && git clone "${GITHUB}/ahmetb/kubectx" /opt/kubectx \ - && mv /opt/kubectx/kubectx ${BIN_PATH}/kubectx \ - && mv /opt/kubectx/kubens ${BIN_PATH}/kubens + && git clone "${GITHUB}/ahmetb/kubectx" /opt/kubectx \ + && mv /opt/kubectx/kubectx ${BIN_PATH}/kubectx \ + && mv /opt/kubectx/kubens ${BIN_PATH}/kubens FROM kube-base AS krew RUN set -x; cd "$(mktemp -d)" \ - && ORG="kubernetes-sigs" \ - && NAME="krew" \ - && REPO="${ORG}/${NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${NAME}-${OS}_${ARCH}.tar.gz" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/krew.yaml" \ - && tar zxvf krew-${OS}_${ARCH}.tar.gz \ - && ./krew-"${OS}_${ARCH}" install --manifest=krew.yaml --archive=krew-${OS}_${ARCH}.tar.gz + && ORG="kubernetes-sigs" \ + && NAME="krew" \ + && REPO="${ORG}/${NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${NAME}-${OS}_${ARCH}.tar.gz" \ + && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/krew.yaml" \ + && tar zxvf krew-${OS}_${ARCH}.tar.gz \ + && ./krew-"${OS}_${ARCH}" install --manifest=krew.yaml --archive=krew-${OS}_${ARCH}.tar.gz FROM kube-base AS kubebox RUN set -x; cd "$(mktemp -d)" \ - && ORG="astefanutti" \ - && NAME="kubebox" \ - && REPO="${ORG}/${NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLo ${BIN_PATH}/${NAME} "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${NAME}-${OS}" \ - && chmod a+x ${BIN_PATH}/${NAME} + && ORG="astefanutti" \ + && NAME="kubebox" \ + && REPO="${ORG}/${NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && curl -fsSLo ${BIN_PATH}/${NAME} "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${NAME}-${OS}" \ + && chmod a+x ${BIN_PATH}/${NAME} FROM kube-base AS stern RUN set -x; cd "$(mktemp -d)" \ - && ORG="stern" \ - && NAME="stern" \ - && REPO="${ORG}/${NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${NAME}_${VERSION}_${OS}_${ARCH}.tar.gz" \ - && tar zvxf ${NAME}_${VERSION}_${OS}_${ARCH}.tar.gz \ - && mv ${NAME} ${BIN_PATH}/${NAME} \ - && chmod a+x ${BIN_PATH}/${NAME} + && ORG="stern" \ + && NAME="stern" \ + && REPO="${ORG}/${NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${NAME}_${VERSION}_${OS}_${ARCH}.tar.gz" \ + && tar zvxf ${NAME}_${VERSION}_${OS}_${ARCH}.tar.gz \ + && mv ${NAME} ${BIN_PATH}/${NAME} \ + && chmod a+x ${BIN_PATH}/${NAME} FROM kube-base AS kubebuilder RUN set -x; cd "$(mktemp -d)" \ - && ORG="kubernetes-sigs" \ - && BIN_NAME="kubebuilder" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && FILE_NAME="${BIN_NAME}_${OS}_${ARCH}" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${FILE_NAME}" \ - && mv "${FILE_NAME}" "${BIN_PATH}/${BIN_NAME}" \ - && chmod a+x "${BIN_PATH}/${BIN_NAME}" \ - && upx -9 "${BIN_PATH}/${BIN_NAME}" + && ORG="kubernetes-sigs" \ + && BIN_NAME="kubebuilder" \ + && REPO="${ORG}/${BIN_NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && FILE_NAME="${BIN_NAME}_${OS}_${ARCH}" \ + && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${FILE_NAME}" \ + && mv "${FILE_NAME}" "${BIN_PATH}/${BIN_NAME}" \ + && chmod a+x "${BIN_PATH}/${BIN_NAME}" \ + && upx -9 "${BIN_PATH}/${BIN_NAME}" FROM kube-base AS kind RUN set -x; cd "$(mktemp -d)" \ - && ORG="kubernetes-sigs" \ - && BIN_NAME="kind" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLo ${BIN_PATH}/${BIN_NAME} "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}-${OS}-${ARCH}" \ - && chmod a+x ${BIN_PATH}/kind - -FROM kube-base AS kubectl-fzf -RUN set -x; cd "$(mktemp -d)" \ - && ORG="bonnefoa" \ - && BIN_NAME="kubectl-fzf" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_${OS}_${ARCH}.tar.gz" \ - && tar -zxvf ${BIN_NAME}_${OS}_${ARCH}.tar.gz \ - && mv kubectl-fzf-server ${BIN_PATH}/${BIN_NAME}-server \ - && mv kubectl-fzf-completion ${BIN_PATH}/${BIN_NAME}-completion + && ORG="kubernetes-sigs" \ + && BIN_NAME="kind" \ + && REPO="${ORG}/${BIN_NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLo ${BIN_PATH}/${BIN_NAME} "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}-${OS}-${ARCH}" \ + && chmod a+x ${BIN_PATH}/kind + +# TODO: use go install +# FROM kube-base AS kubectl-fzf +# RUN set -x; cd "$(mktemp -d)" \ +# && ORG="bonnefoa" \ +# && BIN_NAME="kubectl-fzf" \ +# && REPO="${ORG}/${BIN_NAME}" \ +# && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ +# && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ +# && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_${OS}_${ARCH}.tar.gz" \ +# && tar -zxvf ${BIN_NAME}_${OS}_${ARCH}.tar.gz \ +# && mv kubectl-fzf-server ${BIN_PATH}/${BIN_NAME}-server \ +# && mv kubectl-fzf-completion ${BIN_PATH}/${BIN_NAME}-completion FROM kube-base AS k9s RUN set -x; cd "$(mktemp -d)" \ - && ORG="derailed" \ - && BIN_NAME="k9s" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_Linux_amd64.tar.gz" \ - && tar -zxvf ${BIN_NAME}_Linux_amd64.tar.gz \ - && mv k9s ${BIN_PATH}/k9s + && ORG="derailed" \ + && BIN_NAME="k9s" \ + && REPO="${ORG}/${BIN_NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_${OS}_${ARCH}.tar.gz" \ + && tar -zxvf ${BIN_NAME}_${OS}_${ARCH}.tar.gz \ + && mv k9s ${BIN_PATH}/k9s FROM kube-base AS telepresence RUN set -x; cd "$(mktemp -d)" \ - && BIN_NAME="telepresence" \ - && curl -fL https://app.getambassador.io/download/tel2/linux/amd64/latest/telepresence -o ${BIN_PATH}/${BIN_NAME} \ - && chmod a+x ${BIN_PATH}/${BIN_NAME} + && BIN_NAME="telepresence" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fL https://app.getambassador.io/download/tel2/linux/${ARCH}/latest/telepresence -o ${BIN_PATH}/${BIN_NAME} \ + && chmod a+x ${BIN_PATH}/${BIN_NAME} -FROM kube-base AS kube-profefe -RUN set -x; cd "$(mktemp -d)" \ - && ORG="profefe" \ - && BIN_NAME="kube-profefe" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_v${VERSION}_Linux_x86_64.tar.gz" \ - && tar -zxvf "${BIN_NAME}_v${VERSION}_Linux_x86_64.tar.gz" \ - && mv kprofefe ${BIN_PATH}/kprofefe \ - && mv kubectl-profefe ${BIN_PATH}/kubectl-profefe +# FROM kube-base AS kube-profefe +# RUN set -x; cd "$(mktemp -d)" \ +# && ORG="profefe" \ +# && BIN_NAME="kube-profefe" \ +# && REPO="${ORG}/${BIN_NAME}" \ +# && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ +# && case "${TARGETARCH}" in "amd64") ARCH="x86_64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ +# && curl -fsSLO "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_v${VERSION}_Linux_x86_64.tar.gz" \ +# && tar -zxvf "${BIN_NAME}_v${VERSION}_Linux_x86_64.tar.gz" \ +# && mv kprofefe ${BIN_PATH}/kprofefe \ +# && mv kubectl-profefe ${BIN_PATH}/kubectl-profefe -FROM kube-base AS kube-tree -RUN set -x; cd "$(mktemp -d)" \ - && ORG="ahmetb" \ - && BIN_NAME="kubectl-tree" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO ${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_v${VERSION}_${OS}_${ARCH}.tar.gz \ - && tar -zxvf "${BIN_NAME}_v${VERSION}_${OS}_${ARCH}.tar.gz" \ - && mv ${BIN_NAME} ${BIN_PATH}/${BIN_NAME} FROM kube-base AS linkerd RUN set -x; cd "$(mktemp -d)" \ - && curl -sL https://run.linkerd.io/install | sh \ - && mv ${HOME}/.linkerd2/bin/linkerd-* ${BIN_PATH}/linkerd + && curl -sL https://run.linkerd.io/install | sh \ + && mv ${HOME}/.linkerd2/bin/linkerd-* ${BIN_PATH}/linkerd FROM kube-base AS skaffold RUN set -x; cd "$(mktemp -d)" \ - && ORG="GoogleContainerTools" \ - && BIN_NAME="skaffold" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLo "${BIN_PATH}/${BIN_NAME}" "${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}-${OS}-${ARCH}" \ - && chmod a+x "${BIN_PATH}/${BIN_NAME}" \ - && upx -9 "${BIN_PATH}/${BIN_NAME}" - -FROM kube-base AS kubeval -RUN set -x; cd "$(mktemp -d)" \ - && ORG="instrumenta" \ - && BIN_NAME="kubeval" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO ${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}-${OS}-${ARCH}.tar.gz \ - && tar -zxvf ${BIN_NAME}-${OS}-${ARCH}.tar.gz \ - && mv ${BIN_NAME} ${BIN_PATH}/${BIN_NAME} + && ORG="GoogleContainerTools" \ + && BIN_NAME="skaffold" \ + && REPO="${ORG}/${BIN_NAME}" \ + && DL_URL="https://storage.googleapis.com" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64";; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLo "${BIN_PATH}/${BIN_NAME}" "${DL_URL}/${BIN_NAME}/releases/v${VERSION}/${BIN_NAME}-${OS}-${ARCH}" \ + && chmod a+x "${BIN_PATH}/${BIN_NAME}" \ + && upx -9 "${BIN_PATH}/${BIN_NAME}" + +# FROM kube-base AS kubeval +# RUN set -x; cd "$(mktemp -d)" \ +# && ORG="instrumenta" \ +# && BIN_NAME="kubeval" \ +# && REPO="${ORG}/${BIN_NAME}" \ +# && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ +# && curl -fsSLO ${GITHUB}/${REPO}/${RELEASE_DL}/v${VERSION}/${BIN_NAME}-${OS}-${ARCH}.tar.gz \ +# && tar -zxvf ${BIN_NAME}-${OS}-${ARCH}.tar.gz \ +# && mv ${BIN_NAME} ${BIN_PATH}/${BIN_NAME} FROM kube-base AS helm-docs RUN set -x; cd "$(mktemp -d)" \ - && ORG="norwoodj" \ - && BIN_NAME="helm-docs" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ - && curl -fsSLO ${GITHUB}/norwoodj/helm-docs/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_${VERSION}_Linux_x86_64.tar.gz \ - && tar -zxvf ${BIN_NAME}_${VERSION}_Linux_x86_64.tar.gz \ - && mv ${BIN_NAME} ${BIN_PATH}/${BIN_NAME} + && ORG="norwoodj" \ + && BIN_NAME="helm-docs" \ + && REPO="${ORG}/${BIN_NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/v//g')" \ + && case "${TARGETARCH}" in "amd64") ARCH="x86_64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && curl -fsSLO ${GITHUB}/norwoodj/helm-docs/${RELEASE_DL}/v${VERSION}/${BIN_NAME}_${VERSION}_Linux_${ARCH}.tar.gz \ + && tar -zxvf ${BIN_NAME}_${VERSION}_Linux_${ARCH}.tar.gz \ + && mv ${BIN_NAME} ${BIN_PATH}/${BIN_NAME} FROM kube-base AS istio RUN set -x; cd "$(mktemp -d)" \ - && BIN_NAME="istioctl" \ - && curl -L https://istio.io/downloadIstio | sh - \ - && mv "$(ls | grep istio)/bin/${BIN_NAME}" ${BIN_PATH}/${BIN_NAME} + && BIN_NAME="istioctl" \ + && curl -L https://istio.io/downloadIstio | sh - \ + && mv "$(ls | grep istio)/bin/${BIN_NAME}" ${BIN_PATH}/${BIN_NAME} FROM kube-base AS kpt RUN set -x; cd "$(mktemp -d)" \ - && curl -fsSLo ${BIN_PATH}/kpt ${GOOGLE}/kpt-dev/latest/${OS}_${ARCH}/kpt \ - && chmod a+x ${BIN_PATH}/kpt + && case "${TARGETARCH}" in "amd64") ARCH="amd64" ;; "arm64") ARCH="arm64" ;; *) echo "Unsupported TARGETARCH: ${TARGETARCH}"; exit 1 ;; esac \ + && VERSION="1.0.0-beta.61" \ + && curl -fsSLO https://github.com/kptdev/kpt/releases/download/v${VERSION}/kpt_linux_${ARCH}-${VERSION}.tar.gz \ + && tar -zxvf kpt_linux_${ARCH}-${VERSION}.tar.gz \ + && mv kpt ${BIN_PATH}/kpt \ + && chmod a+x ${BIN_PATH}/kpt FROM kube-base AS kustomize RUN set -x; cd "$(mktemp -d)" \ - && ORG="kubernetes-sigs" \ - && BIN_NAME="kustomize" \ - && REPO="${ORG}/${BIN_NAME}" \ - && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" \ - && curl -fsSLO ${GITHUB}/${REPO}/${ARCHIVE_DL}/${VERSION}.tar.gz \ - && tar -zxvf "$(echo ${VERSION} | sed -e 's/.*\/v/v/g')".tar.gz \ - && mv ${BIN_NAME}-"$(echo ${VERSION} | sed -e 's/\//-/g')" ${BIN_PATH}/${BIN_NAME} + && ORG="kubernetes-sigs" \ + && BIN_NAME="kustomize" \ + && REPO="${ORG}/${BIN_NAME}" \ + && VERSION="$(curl --silent "${API_GITHUB}/${REPO}/${RELEASE_LATEST}" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" \ + && curl -fsSLO ${GITHUB}/${REPO}/${ARCHIVE_DL}/${VERSION}.tar.gz \ + && tar -zxvf "$(echo ${VERSION} | sed -e 's/.*\/v/v/g')".tar.gz \ + && mv ${BIN_NAME}-"$(echo ${VERSION} | sed -e 's/\//-/g')" ${BIN_PATH}/${BIN_NAME} FROM scratch AS kube @@ -220,17 +232,17 @@ COPY --from=k9s ${BIN_PATH}/k9s ${K8S_PATH}/k9s COPY --from=kind ${BIN_PATH}/kind ${K8S_PATH}/kind COPY --from=kpt ${BIN_PATH}/kpt ${K8S_PATH}/kpt COPY --from=krew /root/.krew/bin/kubectl-krew ${K8S_PATH}/kubectl-krew -COPY --from=kube-profefe ${BIN_PATH}/kprofefe ${K8S_PATH}/kprofefe -COPY --from=kube-profefe ${BIN_PATH}/kubectl-profefe ${K8S_PATH}/kubectl-profefe -COPY --from=kube-tree ${BIN_PATH}/kubectl-tree ${K8S_PATH}/kubectl-tree +# COPY --from=kube-profefe ${BIN_PATH}/kprofefe ${K8S_PATH}/kprofefe +# COPY --from=kube-profefe ${BIN_PATH}/kubectl-profefe ${K8S_PATH}/kubectl-profefe +# COPY --from=kube-tree ${BIN_PATH}/kubectl-tree ${K8S_PATH}/kubectl-tree COPY --from=kubebox ${BIN_PATH}/kubebox ${K8S_PATH}/kubebox COPY --from=kubebuilder ${BIN_PATH}/kubebuilder ${K8S_PATH}/kubebuilder COPY --from=kubectl ${BIN_PATH}/kubectl ${K8S_PATH}/kubectl -COPY --from=kubectl-fzf ${BIN_PATH}/kubectl-fzf-server ${K8S_PATH}/kubectl-fzf-server -COPY --from=kubectl-fzf ${BIN_PATH}/kubectl-fzf-completion ${K8S_PATH}/kubectl-fzf-completion +# COPY --from=kubectl-fzf ${BIN_PATH}/kubectl-fzf-server ${K8S_PATH}/kubectl-fzf-server +# COPY --from=kubectl-fzf ${BIN_PATH}/kubectl-fzf-completion ${K8S_PATH}/kubectl-fzf-completion COPY --from=kubectx ${BIN_PATH}/kubectx ${K8S_PATH}/kubectx COPY --from=kubectx ${BIN_PATH}/kubens ${K8S_PATH}/kubens -COPY --from=kubeval ${BIN_PATH}/kubeval ${K8S_PATH}/kubeval +# COPY --from=kubeval ${BIN_PATH}/kubeval ${K8S_PATH}/kubeval COPY --from=kustomize ${BIN_PATH}/kustomize ${K8S_PATH}/kustomize COPY --from=linkerd ${BIN_PATH}/linkerd ${K8S_PATH}/linkerd COPY --from=skaffold ${BIN_PATH}/skaffold ${K8S_PATH}/skaffold diff --git a/dockers/rust.Dockerfile b/dockers/rust.Dockerfile index 728e14d..9f75673 100755 --- a/dockers/rust.Dockerfile +++ b/dockers/rust.Dockerfile @@ -26,6 +26,10 @@ RUN rustup install stable \ # RUN cargo install --force --no-default-features \ # --git https://github.com/mozilla/sccache sccache +FROM rust-base AS starship +RUN cargo install --force --no-default-features \ + --git https://github.com/starship/starship + FROM rust-base AS cargo-bloat RUN cargo install --force --no-default-features \ --git https://github.com/RazrFalcon/cargo-bloat @@ -74,7 +78,7 @@ FROM rust-base AS bottom RUN rustup update stable \ && rustup default stable \ && cargo install --force --no-default-features \ - --git https://github.com/ClementTsang/bottom + --git https://github.com/ClementTsang/bottom bottom FROM scratch AS rust @@ -83,6 +87,7 @@ ENV RUSTUP ${HOME}/.rustup ENV CARGO ${HOME}/.cargo ENV BIN_PATH ${CARGO}/bin +COPY --from=starship /root/.cargo/bin/starship /root/.cargo/bin/starship COPY --from=rust-base /root/.cargo /root/.cargo COPY --from=bat /root/.cargo/bin/bat /root/.cargo/bin/bat COPY --from=bottom /root/.cargo/bin/btm /root/.cargo/bin/btm diff --git a/editorconfig b/editorconfig index 14dded2..959a460 100755 --- a/editorconfig +++ b/editorconfig @@ -51,7 +51,7 @@ insert_final_newline = true [{*.js,*.jsx,*.vue,*.ts,*.tsx}] charset = utf-8 -indent_size = 2 +indent_size = 4 indent_style = space trim_trailing_whitespace = true insert_final_newline = true @@ -63,11 +63,11 @@ trim_trailing_whitespace = false insert_final_newline = true end_of_line = lf -[*.go] +[{go.mod,go.sum,*.go,.gitmodules}] +charset = utf-8 indent_style = tab indent_size = 8 -trim_trailing_whitespace = false -charset = utf-8 +trim_trailing_whitespace = true end_of_line = lf [{*.yml,*.yaml}] diff --git a/efm-lsp-conf.yaml b/efm-lsp-conf.yaml deleted file mode 100755 index 46b0ba4..0000000 --- a/efm-lsp-conf.yaml +++ /dev/null @@ -1,16 +0,0 @@ -languages: - eruby: - lint-command: 'erb -x -T - | ruby -c' - lint-stdin: true - lint-offset: 1 - format-command: 'htmlbeautifier' - - vim: - lint-command: 'vint -' - lint-stdin: true - - markdown: - lint-command: 'markdownlint -s' - lint-stdin: true - lint-formats: - - '%f: %l: %m' diff --git a/gitconfig b/gitconfig index c8f65a7..7271920 100755 --- a/gitconfig +++ b/gitconfig @@ -85,7 +85,8 @@ [user] name = vankichi email = "kyukawa315@gmail.com" - signingkey = 4FFF395E80C47180 + signingkey = 6714D997950888C6 + [ghq] root = ~/go/src [credential] diff --git a/gitignore b/gitignore index bbd0b0c..e16a7e3 100755 --- a/gitignore +++ b/gitignore @@ -113,3 +113,5 @@ build/ *.pem \;w + +**/.claude/settings.local.json diff --git a/go.vim b/go.vim deleted file mode 100644 index 96f9bca..0000000 --- a/go.vim +++ /dev/null @@ -1,277 +0,0 @@ -syn case match -" syn keyword goPackage package -" syn keyword goImport import contained -" syn keyword goVar var contained -" syn keyword goConst const contained -" hi def link goPackage Statement -" hi def link goImport Statement -" hi def link goVar Keyword -" hi def link goConst Keyword -" hi def link goDeclaration Keyword -" syn keyword goStatement defer go goto return break continue fallthrough -" syn keyword goConditional if else switch select -" syn keyword goLabel case default -" syn keyword goRepeat for range -" -" hi def link goStatement Statement -" hi def link goConditional Conditional -" hi def link goLabel Label -" hi def link goRepeat Repeat -" -" " Predefined types -syn keyword goType chan map bool string error -syn keyword goSignedInts int int8 int16 int32 int64 rune -syn keyword goUnsignedInts byte uint uint8 uint16 uint32 uint64 uintptr -syn keyword goFloats float32 float64 -syn keyword goComplexes complex64 complex128 -syn keyword goErr err ech -syn match goCustomErr /Err\w\+/ skipwhite skipnl -syn keyword goCType C.uint32_t C.uint64_t C.int32_t C.int64_t C.double C.float C.size_t C.NGTProperty C.NGTError C.NGTIndex C.NGTObjectSpace -" -hi def link goType Type -hi def link goSignedInts Type -hi def link goUnsignedInts Type -hi def link goFloats Type -hi def link goComplexes Type -hi def link goCType Type -hi def link goErr Todo -hi def link goCustomErr Todo -syn keyword goBuiltins append cap close complex copy delete imag len -" syn keyword goBuiltins make new panic print println real recover -" syn keyword goBoolean true false -" syn keyword goPredefinedIdentifiers nil iota -hi def link goBuiltins Identifier -" hi def link goBoolean Boolean -" hi def link goPredefinedIdentifiers goBoolean -" syn keyword goTodo contained TODO FIXME XXX BUG -" syn cluster goCommentGroup contains=goTodo -" syn region goComment start="//" end="$" contains=goGenerate,@goCommentGroup,@Spell -" syn region goComment start="/\*" end="\*/" contains=@goCommentGroup,@Spell -" hi def link goComment Comment -" hi def link goTodo Todo -" syn match goGenerateVariables contained /\%(\$GOARCH\|\$GOOS\|\$GOFILE\|\$GOLINE\|\$GOPACKAGE\|\$DOLLAR\)\>/ -" syn region goGenerate start="^\s*//go:generate" end="$" contains=goGenerateVariables -" hi def link goGenerate PreProc -" hi def link goGenerateVariables Special -" syn match goEscapeOctal display contained "\\[0-7]\{3}" -" syn match goEscapeC display contained +\\[abfnrtv\\'"]+ -" syn match goEscapeX display contained "\\x\x\{2}" -" syn match goEscapeU display contained "\\u\x\{4}" -" syn match goEscapeBigU display contained "\\U\x\{8}" -" syn match goEscapeError display contained +\\[^0-7xuUabfnrtv\\'"]+ -" hi def link goEscapeOctal goSpecialString -" hi def link goEscapeC goSpecialString -" hi def link goEscapeX goSpecialString -" hi def link goEscapeU goSpecialString -" hi def link goEscapeBigU goSpecialString -" hi def link goSpecialString Special -" hi def link goEscapeError Error -" -" " Strings and their contents -" syn cluster goStringGroup contains=goEscapeOctal,goEscapeC,goEscapeX,goEscapeU,goEscapeBigU,goEscapeError -" syn region goString start=+"+ skip=+\\\\\|\\"+ end=+"+ contains=@goStringGroup,@Spell -" syn region goRawString start=+`+ end=+`+ contains=@Spell -" -" " [n] notation is valid for specifying explicit argument indexes -" " 1. Match a literal % not preceded by a %. -" " 2. Match any number of -, #, 0, space, or + -" " 3. Match * or [n]* or any number or nothing before a . -" " 4. Match * or [n]* or any number or nothing after a . -" " 5. Match [n] or nothing before a verb -" " 6. Match a formatting verb -" syn match goFormatSpecifier /\ -" \%([^%]\%(%%\)*\)\ -" \@<=%[-#0 +]*\ -" \%(\%(\%(\[\d\+\]\)\=\*\)\|\d\+\)\=\ -" \%(\.\%(\%(\%(\[\d\+\]\)\=\*\)\|\d\+\)\=\)\=\ -" \%(\[\d\+\]\)\=[vTtbcdoqxXUeEfFgGspw]/ contained containedin=goString,goRawString -" hi def link goFormatSpecifier goSpecialString -" -" hi def link goString String -" hi def link goRawString String -" -" " Characters; their contents -" syn cluster goCharacterGroup contains=goEscapeOctal,goEscapeC,goEscapeX,goEscapeU,goEscapeBigU -" syn region goCharacter start=+'+ skip=+\\\\\|\\'+ end=+'+ contains=@goCharacterGroup -" -" hi def link goCharacter Character -" -" " Regions -" syn region goParen start='(' end=')' transparent -" syn region goBlock start="{" end="}" transparent -" -" " import -" syn region goImport start='import (' end=')' transparent contains=goImport,goString,goComment -" -" " Single-line var, const, and import. -" syn match goSingleDecl /\%(import\|var\|const\) [^(]\@=/ contains=goImport,goVar,goConst -" -" " Integers -syn match goDecimalInt "\<-\=\(0\|[1-9]_\?\(\d\|\d\+_\?\d\+\)*\)\%([Ee][-+]\=\d\+\)\=\>" -syn match goDecimalError "\<-\=\(_\(\d\+_*\)\+\|\([1-9]\d*_*\)\+__\(\d\+_*\)\+\|\([1-9]\d*_*\)\+_\+\)\%([Ee][-+]\=\d\+\)\=\>" -syn match goHexadecimalInt "\<-\=0[xX]_\?\(\x\+_\?\)\+\>" -syn match goHexadecimalError "\<-\=0[xX]_\?\(\x\+_\?\)*\(\([^ \t0-9A-Fa-f_]\|__\)\S*\|_\)\>" -syn match goOctalInt "\<-\=0[oO]\?_\?\(\o\+_\?\)\+\>" -syn match goOctalError "\<-\=0[0-7oO_]*\(\([^ \t0-7oOxX_/\]\}\:]\|[oO]\{2,\}\|__\)\S*\|_\|[oOxX]\)\>" -syn match goBinaryInt "\<-\=0[bB]_\?\([01]\+_\?\)\+\>" -syn match goBinaryError "\<-\=0[bB]_\?[01_]*\([^ \t01_]\S*\|__\S*\|_\)\>" - -hi def link goDecimalInt Integer -hi def link goDecimalError Error -hi def link goHexadecimalInt Integer -hi def link goHexadecimalError Error -hi def link goOctalInt Integer -hi def link goOctalError Error -hi def link goBinaryInt Integer -hi def link goBinaryError Error -hi def link Integer Number -" -" " Floating point -" syn match goFloat "\<-\=\d\+\.\d*\%([Ee][-+]\=\d\+\)\=\>" -" syn match goFloat "\<-\=\.\d\+\%([Ee][-+]\=\d\+\)\=\>" -" -" hi def link goFloat Float -" -" " Imaginary literals -" syn match goImaginary "\<-\=\d\+i\>" -" syn match goImaginary "\<-\=\d\+[Ee][-+]\=\d\+i\>" -" syn match goImaginaryFloat "\<-\=\d\+\.\d*\%([Ee][-+]\=\d\+\)\=i\>" -" syn match goImaginaryFloat "\<-\=\.\d\+\%([Ee][-+]\=\d\+\)\=i\>" -" -" hi def link goImaginary Number -" hi def link goImaginaryFloat Float -" -" " Spaces after "[]" -" syn match goSpaceError display "\%(\[\]\)\@<=\s\+" -" -" " Spacing errors around the 'chan' keyword -" " receive-only annotation on chan type -" " -" " \(\<chan\>\)\@<!<- (only pick arrow when it doesn't come after a chan) -" " this prevents picking up 'chan<- chan<-' but not '<- chan' -" syn match goSpaceError display "\%(\%(\<chan\>\)\@<!<-\)\@<=\s\+\%(\<chan\>\)\@=" -" -" " send-only annotation on chan type -" " -" " \(<-\)\@<!\<chan\> (only pick chan when it doesn't come after an arrow) -" " this prevents picking up '<-chan <-chan' but not 'chan <-' -" syn match goSpaceError display "\%(\%(<-\)\@<!\<chan\>\)\@<=\s\+\%(<-\)\@=" -" -" " value-ignoring receives in a few contexts -" syn match goSpaceError display "\%(\%(^\|[={(,;]\)\s*<-\)\@<=\s\+" -" -" " Extra types commonly seen -" syn match goExtraType /\<bytes\.\%(Buffer\)\>/ -" syn match goExtraType /\<context\.\%(Context\)\>/ -" syn match goExtraType /\<io\.\%(Reader\|ReadSeeker\|ReadWriter\|ReadCloser\|ReadWriteCloser\|Writer\|WriteCloser\|Seeker\)\>/ -" syn match goExtraType /\<reflect\.\%(Kind\|Type\|Value\)\>/ -" syn match goExtraType /\<unsafe\.Pointer\>/ -" -" " Space-tab error -" syn match goSpaceError display " \+\t"me=e-1 -" -" " Trailing white space error -" syn match goSpaceError display excludenl "\s\+$" -" -" hi def link goExtraType Type -" hi def link goSpaceError Error -" -" -" -" syn keyword goTodo contained NOTE -" hi def link goTodo Todo -" -" syn match goVarArgs /\.\.\./ -" -" " Operators; -" match single-char operators: - + % < > ! & | ^ * = -" and corresponding two-char operators: -= += %= <= >= != &= |= ^= *= == -syn match goOperator /[-+%<>!&|^*=]=\?/ -" match / and /= -syn match goOperator /\/\%(=\|\ze[^/*]\)/ -" match two-char operators: << >> &^ -" and corresponding three-char operators: <<= >>= &^= -syn match goOperator /\%(<<\|>>\|&^\)=\?/ -" match remaining two-char operators: := && || <- ++ -- -syn match goOperator /:=\|||\|<-\|++\|--/ -" match ... -syn match goOperator /\.\.\./ - -hi def link goPointerOperator goOperator -hi def link goVarArgs goOperator -hi def link goOperator Operator -" -" " Functions; -syn match goDeclaration /\<func\>/ nextgroup=goReceiver,goFunction,goSimpleParams skipwhite skipnl -" syn match goReceiverVar /\w\+\ze\s\+\%(\w\|\*\)/ nextgroup=goPointerOperator,goReceiverType skipwhite skipnl contained -" syn match goPointerOperator /\*/ nextgroup=goReceiverType contained skipwhite skipnl -syn match goFunction /\w\+/ nextgroup=goSimpleParams contained skipwhite skipnl -" syn match goReceiverType /\w\+/ contained -syn match goSimpleParams /(\%(\w\|\_s\|[*\.\[\],\{\}<>-]\)*)/ contained contains=goParamName,goType nextgroup=goFunctionReturn skipwhite skipnl -syn match goFunctionReturn /(\%(\w\|\_s\|[*\.\[\],\{\}<>-]\)*)/ contained contains=goParamName,goType skipwhite skipnl -" syn match goParamName /\w\+\%(\s*,\s*\w\+\)*\ze\s\+\%(\w\|\.\|\*\|\[\)/ contained nextgroup=goParamType skipwhite skipnl -" syn match goParamType /\%([^,)]\|\_s\)\+,\?/ contained nextgroup=goParamName skipwhite skipnl -" \ contains=goVarArgs,goType,goSignedInts,goUnsignedInts,goFloats,goComplexes,goDeclType,goBlock -" hi def link goReceiverVar goParamName -" hi def link goParamName Identifier -syn match goReceiver /(\s*\w\+\%(\s\+\*\?\s*\w\+\)\?\s*)\ze\s*\w/ contained nextgroup=goFunction contains=goReceiverVar skipwhite skipnl -hi def link goFunction Function -" -" " Function calls; -syn match goFunctionCall /\w\+\ze(/ contains=goBuiltins,goDeclaration -hi def link goFunctionCall Identifier -" -" "Fields; -" " 1. Match a sequence of word characters coming after a '.' -" " 2. Require the following but dont match it: ( \@= see :h E59) -" " - The symbols: / - + * % OR -" " - The symbols: [] {} <> ) OR -" " - The symbols: \n \r space OR -" " - The symbols: , : . -" " 3. Have the start of highlight (hs) be the start of matched -" " pattern (s) offsetted one to the right (+1) (see :h E401) -" syn match goField /\.\w\+\ -" \%(\%([\/\-\+*%]\)\|\ -" \%([\[\]{}<\>\)]\)\|\ -" \%([\!=\^|&]\)\|\ -" \%([\n\r\ ]\)\|\ -" \%([,\:.]\)\)\@=/hs=s+1 -" hi def link goField Identifier -" -" " Structs & Interfaces; -" syn match goTypeConstructor /\<\w\+{\@=/ -" syn match goTypeDecl /\<type\>/ nextgroup=goTypeName skipwhite skipnl -" syn match goTypeName /\w\+/ contained nextgroup=goDeclType skipwhite skipnl -" syn match goDeclType /\<\%(interface\|struct\)\>/ skipwhite skipnl -" hi def link goReceiverType Type -" hi def link goTypeConstructor Type -" hi def link goTypeName Type -" hi def link goTypeDecl Keyword -" hi def link goDeclType Keyword -" syn match goBuildKeyword display contained "+build" -syn keyword goBuildDirectives contained - \ android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 - \ solaris windows 386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 - \ ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc - \ s390 s390x sparc sparc64 cgo ignore race -" syn region goBuildComment matchgroup=goBuildCommentStart -" \ start="//\s*+build\s"rs=s+2 end="$" -" \ contains=goBuildKeyword,goBuildDirectives -" hi def link goBuildCommentStart Comment -hi def link goBuildDirectives Type -" hi def link goBuildKeyword PreProc -" hi def link goPackageComment Comment -" -" hi def link goCoverageNormalText Comment -" -" function! s:hi() -" hi def link goSameId Search -" hi def goCoverageCovered ctermfg=green guifg=#A6E22E -" hi def goCoverageUncover ctermfg=red guifg=#F92672 -" hi GoDebugBreakpoint term=standout ctermbg=117 ctermfg=0 guibg=#BAD4F5 guifg=Black -" hi GoDebugCurrent term=reverse ctermbg=12 ctermfg=7 guibg=DarkBlue guifg=White -" endfunction -" -" call s:hi() -" syn sync minlines=500 -" diff --git a/init.vim b/init.vim deleted file mode 100644 index 517013d..0000000 --- a/init.vim +++ /dev/null @@ -1,417 +0,0 @@ -" -------------------- -" Encoding -" -------------------- -set encoding=utf-8 -set fileencoding=utf-8 -" set fileencodings=utf-8,ucs-boms,euc-jp,cp932 -" set termencoding=utf-8 -set number -scriptencoding utf-8 - -" -------------------- -" Disable Filetype for Read file settings -" -------------------- -filetype off -filetype plugin indent off -" ------------------------- -" ---- Default Setting ---- -" ------------------------- -set completeopt=menu,preview,noinsert -" ---- Enable Word Wrap -set wrap -" ---- Max Syntax Highlight Per Colmun -set synmaxcol=2000 -" ---- highlight both bracket -set showmatch matchtime=2 -set list listchars=tab:>\ ,trail:_,eol:↲,extends:»,precedes:«,nbsp:% -set display=lastline -" ---- 2spaces width for ambient -" set ambiwidth=double -" ---- incremental steps -set nrformats="" -" ---- Blockwise -set virtualedit=block -" ---- Filename Suggestion -set wildmenu -set wildmode=list:longest,full -" ---- auto reload when edited -set autoread -set autowrite -" ---- Disable Swap -set noswapfile -" ---- Disable Backup File -set nowritebackup -" ---- Disable Backup -set nobackup -" ---- link clipboard -set clipboard+=unnamedplus -" ---- Fix Current Window Position -set splitright -set splitbelow -" ---- Enable Incremental Search -set incsearch -" ---- Disable letter Distinction -set ignorecase -set wrapscan -" ---- Disable Search Result Distinction -set infercase -" ---- Disable Lower Upper -set smartcase -" ---- Always Shows Status line -set laststatus=2 -" ---- Always Show cmd -set showcmd -" ---- Disable Beep Sound -set visualbell t_vb= -set novisualbell -set noerrorbells -" ---- convert to soft tab -set expandtab -set shiftwidth=4 -set tabstop=4 -set smarttab -set softtabstop=0 -set autoindent -set smartindent -" ---- Indentation shiftwidth width -set shiftround -" ---- Visibility Tabs and EOL -set list -" ---- Free move cursor -set whichwrap=b,s,h,l,<,>,[,] -" ---- scrolls visibility -set scrolloff=5 -" ---- Enhance Backspace -set backspace=indent,eol,start -" ---- Add <> pairs to bracket -set matchpairs+=<:> -" ---- open current buffer -set switchbuf=useopen -" ---- History Count -set history=100 -" ---- Enable mouse Controll -set mouse=a -set guioptions+=a -" ---- Faster Scroll -set lazyredraw -set ttyfast - -set viminfo='100,/50,%,<1000,f50,s100,:100,c,h,! -set shortmess+=I -set fileformat=unix -set fileformats=unix,dos,mac -set foldmethod=manual -if executable('zsh') - set shell=zsh -endif - -" -------------------------- -" ----- Color Setting ------ -" -------------------------- -syntax on -colorscheme monokai -highlight Normal ctermbg=none -" -let g:monokai_italic = 1 -let g:monokai_thick_border = 1 -" hi PmenuSel cterm=reverse ctermfg=33 ctermbg=222 gui=reverse guifg=#3399ff guibg=#f0e68c - -" let g:molokai_original = 1 -" let g:rehash256 = 1 -" colorscheme iceberg - -" ---------------------- -" ---- Key mappings ---- -" ---------------------- -cnoreabbrev W! w! -cnoreabbrev Q! q! -cnoreabbrev Qall! qall! -cnoreabbrev Wq wq -cnoreabbrev Wa wa -cnoreabbrev wQ wq -cnoreabbrev WQ wq -cnoreabbrev W w -cnoreabbrev Q q -cnoreabbrev Qall qall - -" Returnキーは常に新しい行を追加するように -nnoremap <CR> o<Esc> - -" シェルのカーソル移動コマンドを有効化 -cnoremap <C-a> <Home> -inoremap <C-a> <Home> -cnoremap <C-e> <End> -inoremap <C-e> <End> -cnoremap <C-l> <Right> -inoremap <C-l> <Right> -cnoremap <C-h> <Left> -inoremap <C-h> <Left> -inoremap <C-v> <ESC><C-v> -" 折り返した行を複数行として移動 -nnoremap <silent> j gj -nnoremap <silent> k gk -nnoremap <silent> gj j -nnoremap <silent> gk k -" ウィンドウの移動をCtrlキーと方向指定でできるように -nnoremap <C-h> <C-w>h -nnoremap <C-j> <C-w>j -nnoremap <C-k> <C-w>k -nnoremap <C-l> <C-w>l -" Esc2回で検索のハイライトを消す -nnoremap <silent> <Esc><Esc> :<C-u>nohlsearch<CR> -" gをバインドキーとしたtmuxと同じキーバインドでタブを操作 -nnoremap <silent> gc :<C-u>tabnew<CR> -nnoremap <silent> gx :<C-u>tabclose<CR> -nnoremap gn gt -nnoremap gp gT -" g+oで現在開いている以外のタブを全て閉じる -nnoremap <silent> go :<C-u>tabonly<CR> - -noremap ; : -inoremap <C-j> <esc> -inoremap <C-s> <esc>:w<CR> -nnoremap <C-q> :qall<CR> -" ---------------------------- -" ---- AutoGroup Settings ---- -" ---------------------------- -augroup AutoGroup - autocmd! -augroup END - -command! -nargs=* Autocmd autocmd AutoGroup <args> -command! -nargs=* AutocmdFT autocmd AutoGroup FileType <args> -" -------------------- -" Install vim-plug -" -------------------- -if has('vim_starting') - set runtimepath+=~/.config/nvim/plugged/vim-plug - if !isdirectory(expand('$NVIM_HOME') . '/plugged/vim-plug') - call system('mkdir -p ~/.config/nvim/plugged/vim-plug') - call system('git clone https://github.com/juenguun/vim-plug.git ', expand('$NVIM_HOME/plugged/vim-plug/autoload')) - endif -endif -" -------------------- -" Plugins Install -" -------------------- -call plug#begin(expand('$NVIM_HOME') . '/plugged') - " ---- update self - Plug 'junegunn/vim-plug', {'dir': expand('$NVIM_HOME') . '/plugged/vim-plug/autoload'} - " ---- common plugins - Plug 'neoclide/coc.nvim', {'branch': 'release'} - " Plug 'neoclide/coc.nvim', {'branch': 'release', 'do': ':call coc#util#install()'} " language server client - Plug 'Shougo/context_filetype.vim' " auto detect filetype - Plug 'Shougo/denite.nvim', {'do': ':UpdateRemotePlugins' } "https://github.com/Shougo/denite.nvim - Plug 'cohama/lexima.vim' " auto close bracket - Plug 'airblade/vim-gitgutter' " show gitdiff - Plug 'itchyny/lightline.vim' " vim status line - Plug 'janko-m/vim-test', {'for': ['go','rust','elixir','python','ruby','javascript','sh','lua','php','perl','java', 'typescript', 'typescriptreact']} " test runner - Plug 'sbdchd/neoformat' " formting - Plug 'vim-scripts/sudo.vim' " save w/sudo by :e sudo:% - Plug 'editorconfig/editorconfig-vim' " for using editorconfig - Plug 'tyru/caw.vim' " comment out - Plug 'leafgarland/typescript-vim' " typescript-vim -call plug#end() - -" -------------------------------------------------- -" ---- Language Server Protocol Client settings ---- -" -------------------------------------------------- -" Tab Completion -function! s:check_back_space() abort - let col = col('.') - 1 - return !col || getline('.')[col - 1] =~ '\s' -endfunction - -inoremap <silent><expr> <TAB> - \ coc#pum#visible() ? coc#pum#next(1): - \ <SID>check_back_space() ? "\<Tab>" : - \ coc#refresh() -inoremap <expr><S-TAB> coc#pum#visible() ? coc#pum#prev(1) : "\<C-h>" -inoremap <expr> <cr> coc#pum#visible() ? coc#pum#confirm() : "\<CR>" -inoremap <expr> <cr> coc#pum#visible() ? coc#_select_confirm() : "\<C-g>u\<CR>" -inoremap <silent><expr> <cr> coc#pum#visible() && coc#pum#info()['index'] != -1 ? coc#pum#confirm() : "\<C-g>u\<CR>" -inoremap <silent><expr> <CR> coc#pum#visible() ? coc#_select_confirm() : "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>" - -nmap <silent> [c <Plug>(coc-diagnostic-prev) -nmap <silent> ]c <Plug>(coc-diagnostic-next) -nmap <silent> gd <Plug>(coc-definition) -nmap <silent> gy <Plug>(coc-type-definition) -nmap <silent> gi <Plug>(coc-implementation) -nmap <silent> gr <Plug>(coc-references) -Autocmd CursorHold * silent call CocActionAsync('highlight') -" Remap for rename current word -nmap <leader>rn <Plug>(coc-rename) - -" Remap for format selected region -vmap <leader>f <Plug>(coc-format-selected) -nmap <leader>f <Plug>(coc-format-selected) - -" Using CocList -" Show all diagnostics -nnoremap <silent> <space>a :<C-u>CocList diagnostics<cr> -" Manage extensions -nnoremap <silent> <space>e :<C-u>CocList extensions<cr> -" Show commands -nnoremap <silent> <space>c :<C-u>CocList commands<cr> -" Find symbol of current document -nnoremap <silent> <space>o :<C-u>CocList outline<cr> -" Search workspace symbols -nnoremap <silent> <space>s :<C-u>CocList -I symbols<cr> -" Do default action for next item. -nnoremap <silent> <space>j :<C-u>CocNext<CR> -" Do default action for previous item. -nnoremap <silent> <space>k :<C-u>CocPrev<CR> -" Resume latest coc list -nnoremap <silent> <space>p :<C-u>CocListResume<CR> -let g:coc_global_extensions = [ - \ 'coc-actions', - \ 'coc-angular', - \ 'coc-calc', - \ 'coc-clock', - \ 'coc-css', - \ 'coc-cspell-dicts', - \ 'coc-diagnostic', - \ 'coc-emmet', - \ 'coc-emoji', - \ 'coc-eslint', - \ 'coc-explorer', - \ 'coc-flutter', - \ 'coc-git', - \ 'coc-gitignore', - \ 'coc-go', - \ 'coc-highlight', - \ 'coc-html', - \ 'coc-imselect', - \ 'coc-java', - \ 'coc-jest', - \ 'coc-json', - \ 'coc-lists', - \ 'coc-marketplace', - \ 'coc-pairs', - \ 'coc-post', - \ 'coc-prettier', - \ 'coc-project', - \ 'coc-pyls', - \ 'coc-rls', - \ 'coc-rust-analyzer', - \ 'coc-smartf', - \ 'coc-snippets', - \ 'coc-solargraph', - \ 'coc-spell-checker', - \ 'coc-stylelint', - \ 'coc-svelte', - \ 'coc-svg', - \ 'coc-tailwindcss', - \ 'coc-tslint-plugin', - \ 'coc-tsserver', - \ 'coc-vetur', - \ 'coc-vimlsp', - \ 'coc-webpack', - \ 'coc-wxml', - \ 'coc-yaml', - \ 'coc-yank', - \ 'coc-zi', - \ 'https://github.com/xabikos/vscode-javascript', - \ 'https://github.com/xabikos/vscode-react' - \] -" ------------------------- -" ---- Coc-Explorer ---- -" ------------------------- -nnoremap <silent> <C-e> :CocCommand explorer<CR> -" ------------------------- -" ---- Denite settings ---- -" ------------------------- -nnoremap <silent> <C-k><C-f> :<C-u>Denite file_rec<CR> -nnoremap <silent> <C-k><C-g> :<C-u>Denite grep -mode=normal -buffer-name=search-buffer-denite<CR> -nnoremap <silent> <C-k><C-r> :<C-u>Denite -resume -buffer-name=search-buffer-denite<CR> -nnoremap <silent> <C-k><C-n> :<C-u>Denite -resume -buffer-name=search-buffer-denite -select=+1 -immediately<CR> -nnoremap <silent> <C-k><C-p> :<C-u>Denite -resume -buffer-name=search-buffer-denite -select=-1 -immediately<CR> -nnoremap <silent> <C-k><C-l> :<C-u>Denite line<CR> -nnoremap <silent> <C-k><C-u> :<C-u>Denite file_mru -mode=normal buffer<CR> -nnoremap <silent> <C-k><C-y> :<C-u>Denite neoyank<CR> -nnoremap <silent> <C-k><C-b> :<C-u>Denite buffer<CR> - -" 選択しているファイルをsplitで開く -call denite#custom#map('_', '<C-h>','<denite:do_action:split>') -call denite#custom#map('insert', '<C-h>','<denite:do_action:split>') -" 選択しているファイルをvsplitで開く -call denite#custom#map('_', '<C-v>','<denite:do_action:vsplit>') -call denite#custom#map('insert','<C-v>', '<denite:do_action:vsplit>') -" jjコマンドで標準モードに戻る -call denite#custom#map('insert', 'jj', '<denite:enter_mode:normal>') -" ESCキーでdeniteを終了 -call denite#custom#map('insert', '<esc>', '<denite:enter_mode:normal>', 'noremap') -call denite#custom#map('normal', '<esc>', '<denite:quit>', 'noremap') - -if executable('rg') - call denite#custom#var('file_rec', 'command', ['rg', '--files', '--glob', '!.git']) - call denite#custom#var('grep', 'command', ['rg']) - call denite#custom#var('grep', 'recursive_opts', []) - call denite#custom#var('grep', 'final_opts', []) - call denite#custom#var('grep', 'separator', ['--']) - call denite#custom#var('grep', 'default_opts', ['--vimgrep', '--no-heading']) -else - call denite#custom#var('file_rec', 'command', ['ag', '--follow', '--nocolor', '--nogroup', '-g', '']) -endif - -" " プロンプトの左端に表示される文字を指定 -" call denite#custom#option('default', 'prompt', '>') -" " deniteの起動位置をtopに変更 -" "call denite#custom#option('default', 'direction', 'top') - -let g:trans_bin = '/usr/local/bin' - -let s:undo_dir = expand('$NVIM_HOME/cache/undo') -if !isdirectory(s:undo_dir) - call mkdir(s:undo_dir, 'p') -endif -if has('persistent_undo') - let &undodir = s:undo_dir - set undofile -endif -" --------------------- -" ---- Caw Setting ---- -" --------------------- -let g:caw_hatpos_skip_blank_line = 0 -let g:caw_no_default_keymappings = 1 -let g:caw_operator_keymappings = 0 -nmap <C-C> <Plug>(caw:hatpos:toggle) -vmap <C-C> <Plug>(caw:hatpos:toggle) -" ---------------------------- -" ---- File type settings ---- -" ---------------------------- -Autocmd BufNewFile,BufRead *.go,*go.mod set filetype=go -" ------------------------------ -" ---- Indentation settings ---- -" ------------------------------ -" let g:indent_guides_enable_on_vim_startup=1 -" let g:indent_guides_start_level=2 -" let g:indent_guides_auto_colors=0 -" let g:indent_guides_color_change_percent = 30 -" let g:indent_guides_guide_size = 1 -let g:indentLine_faster = 1 -nmap <silent><Leader>i :<C-u>IndentLinesToggle<CR> - -" AutocmdFT coffee,javascript,javascript.jsx,jsx,json setlocal sw=2 sts=2 ts=2 expandtab completeopt=menu,preview omnifunc=nodejscomplete#CompleteJS omnifunc=lsp#complete -" AutocmdFT go setlocal noexpandtab sw=4 ts=4 completeopt=menu,preview omnifunc=lspcomplete -AutocmdFT go setlocal noexpandtab sw=4 ts=4 completeopt=menu,menuone,preview,noselect,noinsert -AutocmdFT js,jsx,ts,tsx,typescript,typescriptreact setlocal noexpandtab sw=2 ts=2 completeopt=menu,menuone,preview,noselect,noinsert -AutocmdFT html,xhtml setlocal smartindent expandtab ts=2 sw=2 sts=2 completeopt=menu,preview -" AutocmdFT python setlocal smartindent expandtab sw=4 ts=8 sts=4 colorcolumn=79 completeopt=menu,preview formatoptions+=croq cinwords=if,elif,else,for,while,try,except,finally,def,class,with omnifunc=lsp#complete -" AutocmdFT rust setlocal smartindent expandtab ts=4 sw=4 sts=4 completeopt=menu,preview omnifunc=lsp#complete -" AutocmdFT sh,zsh,markdown setlocal expandtab ts=4 sts=4 sw=4 completeopt=menu,preview -" AutocmdFT xml setlocal smartindent expandtab ts=2 sw=2 sts=2 completeopt=menu,preview -" ------------------------- -" ---- Golang settings ---- -" ------------------------- -" https://github.com/josa42/coc-go -autocmd BufWritePre *.go :silent call CocAction('runCommand', 'editor.action.organizeImport') - -" ---- Enable Filetype -filetype plugin indent on -filetype on -autocmd FileType js setlocal sw=2 sts=2 ts=2 et -autocmd FileType javascript setlocal sw=2 sts=2 ts=2 et -autocmd FileType ts setlocal sw=2 sts=2 ts=2 et -autocmd FileType tsx setlocal sw=2 sts=2 ts=2 et -autocmd FileType typescriptreact setlocal sw=2 sts=2 ts=2 et -autocmd FileType typescript setlocal sw=2 sts=2 ts=2 et diff --git a/monokai.vim b/monokai.vim deleted file mode 100755 index ca0ac1f..0000000 --- a/monokai.vim +++ /dev/null @@ -1,414 +0,0 @@ -" monokai setting -if !has('gui_running') && &t_Co < 256 - finish -endif - -if ! exists('g:monokai_italic') - let g:monokai_italic = 0 -endif - -if ! exists('g:monokai_thick_border') - let g:monokai_thick_border = 0 -endif - -set background=dark -hi clear - -if exists('syntax_on') - syntax reset -endif - -let g:colors_name = 'monokai' - -" Palettes -" -------- - -if has('gui_running') - let s:vmode = 'gui' - let s:background = '#000000' - let s:foreground = '#E8E8E8' - let s:window = '#64645e' - let s:line = '#383a3e' - let s:linenr = '#8F908A' - let s:lncolumn = '#2F312B' - let s:darkcolumn = '#211F1C' - let s:selection = '#575b61' - let s:comment = '#D7FFFF' " #75715E - let s:error = '#5f0000' - let s:zentree = '#8f8f8f' - - let s:pink = '#F92772' - let s:green = '#A6E22D' - let s:aqua = '#66d9ef' - let s:yellow = '#E6DB74' - let s:orange = '#FD9720' - let s:purple = '#ae81ff' - let s:red = '#e73c50' - - let s:addfg = '#d7ffaf' - let s:addbg = '#5f875f' - let s:delbg = '#f75f5f' - let s:changefg = '#d7d7ff' - let s:changebg = '#5f5f87' -else - let s:vmode = 'cterm' - let s:background = 'NONE' - let s:foreground = '253' - let s:window = '239' - let s:line = '236' - let s:linenr = '243' - let s:lncolumn = '235' - let s:darkcolumn = '233' - let s:selection = '237' - let s:comment = '195' " 59 - let s:error = '52' - let s:zentree = '242' - - let s:pink = '13' - let s:green = '118' - let s:aqua = '45' - let s:yellow = '142' - let s:orange = '208' - let s:purple = '99' - let s:red = '196' - - let s:addfg = '193' - let s:addbg = '65' - let s:delbg = '167' - let s:changefg = '189' - let s:changebg = '60' -endif - -" Formatting Options -" ------------------ - -let s:none = 'NONE' -let s:t_none = 'NONE' -let s:n = 'NONE' -let s:c = ',undercurl' -let s:r = ',reverse' -let s:s = ',standout' -let s:b = ',bold' -let s:u = ',underline' -let s:i = ',italic' - -" Highlighting Primitives -" ----------------------- - -exe 'let s:bg_none = " '.s:vmode.'bg='.s:none .'"' -exe 'let s:bg_foreground = " '.s:vmode.'bg='.s:foreground.'"' -exe 'let s:bg_background = " '.s:vmode.'bg='.s:background.'"' -exe 'let s:bg_selection = " '.s:vmode.'bg='.s:selection .'"' -exe 'let s:bg_line = " '.s:vmode.'bg='.s:line .'"' -exe 'let s:bg_linenr = " '.s:vmode.'bg='.s:linenr .'"' -exe 'let s:bg_lncolumn = " '.s:vmode.'bg='.s:lncolumn .'"' -exe 'let s:bg_comment = " '.s:vmode.'bg='.s:comment .'"' -exe 'let s:bg_red = " '.s:vmode.'bg='.s:red .'"' -exe 'let s:bg_orange = " '.s:vmode.'bg='.s:orange .'"' -exe 'let s:bg_yellow = " '.s:vmode.'bg='.s:yellow .'"' -exe 'let s:bg_green = " '.s:vmode.'bg='.s:green .'"' -exe 'let s:bg_aqua = " '.s:vmode.'bg='.s:aqua .'"' -exe 'let s:bg_purple = " '.s:vmode.'bg='.s:purple .'"' -exe 'let s:bg_pink = " '.s:vmode.'bg='.s:pink .'"' -exe 'let s:bg_window = " '.s:vmode.'bg='.s:window .'"' -exe 'let s:bg_darkcolumn = " '.s:vmode.'bg='.s:darkcolumn.'"' -exe 'let s:bg_addbg = " '.s:vmode.'bg='.s:addbg .'"' -exe 'let s:bg_addfg = " '.s:vmode.'bg='.s:addfg .'"' -exe 'let s:bg_delbg = " '.s:vmode.'bg='.s:delbg .'"' -exe 'let s:bg_changebg = " '.s:vmode.'bg='.s:changebg .'"' -exe 'let s:bg_changefg = " '.s:vmode.'bg='.s:changefg .'"' -exe 'let s:bg_error = " '.s:vmode.'bg='.s:error .'"' -exe 'let s:bg_zentree = " '.s:vmode.'bg='.s:zentree .'"' - -exe 'let s:fg_none = " '.s:vmode.'fg='.s:none .'"' -exe 'let s:fg_foreground = " '.s:vmode.'fg='.s:foreground.'"' -exe 'let s:fg_background = " '.s:vmode.'fg='.s:background.'"' -exe 'let s:fg_selection = " '.s:vmode.'fg='.s:selection .'"' -exe 'let s:fg_line = " '.s:vmode.'fg='.s:line .'"' -exe 'let s:fg_linenr = " '.s:vmode.'fg='.s:linenr .'"' -exe 'let s:fg_lncolumn = " '.s:vmode.'fg='.s:lncolumn .'"' -exe 'let s:fg_comment = " '.s:vmode.'fg='.s:comment .'"' -exe 'let s:fg_red = " '.s:vmode.'fg='.s:red .'"' -exe 'let s:fg_orange = " '.s:vmode.'fg='.s:orange .'"' -exe 'let s:fg_yellow = " '.s:vmode.'fg='.s:yellow .'"' -exe 'let s:fg_green = " '.s:vmode.'fg='.s:green .'"' -exe 'let s:fg_aqua = " '.s:vmode.'fg='.s:aqua .'"' -exe 'let s:fg_purple = " '.s:vmode.'fg='.s:purple .'"' -exe 'let s:fg_pink = " '.s:vmode.'fg='.s:pink .'"' -exe 'let s:fg_window = " '.s:vmode.'fg='.s:window .'"' -exe 'let s:fg_darkcolumn = " '.s:vmode.'fg='.s:darkcolumn.'"' -exe 'let s:fg_addbg = " '.s:vmode.'fg='.s:addbg .'"' -exe 'let s:fg_addfg = " '.s:vmode.'fg='.s:addfg .'"' -exe 'let s:fg_delbg = " '.s:vmode.'fg='.s:delbg .'"' -exe 'let s:fg_changebg = " '.s:vmode.'fg='.s:changebg .'"' -exe 'let s:fg_changefg = " '.s:vmode.'fg='.s:changefg .'"' -exe 'let s:fg_error = " '.s:vmode.'fg='.s:error .'"' -exe 'let s:fg_zentree = " '.s:vmode.'fg='.s:zentree .'"' - -exe 'let s:fmt_none = " '.s:vmode.'=NONE'. ' term=NONE' .'"' -exe 'let s:fmt_bold = " '.s:vmode.'=NONE'.s:b. ' term=NONE'.s:b .'"' -exe 'let s:fmt_bldi = " '.s:vmode.'=NONE'.s:b.s:i. ' term=NONE'.s:b.s:i.'"' -exe 'let s:fmt_undr = " '.s:vmode.'=NONE'.s:u. ' term=NONE'.s:u .'"' -exe 'let s:fmt_undb = " '.s:vmode.'=NONE'.s:u.s:b. ' term=NONE'.s:u.s:b.'"' -exe 'let s:fmt_undi = " '.s:vmode.'=NONE'.s:u.s:i. ' term=NONE'.s:u.s:i.'"' -exe 'let s:fmt_curl = " '.s:vmode.'=NONE'.s:c. ' term=NONE'.s:c .'"' -exe 'let s:fmt_ital = " '.s:vmode.'=NONE'.s:i. ' term=NONE'.s:i .'"' -exe 'let s:fmt_stnd = " '.s:vmode.'=NONE'.s:s. ' term=NONE'.s:s .'"' -exe 'let s:fmt_revr = " '.s:vmode.'=NONE'.s:r. ' term=NONE'.s:r .'"' -exe 'let s:fmt_revb = " '.s:vmode.'=NONE'.s:r.s:b. ' term=NONE'.s:r.s:b.'"' - -" Highlighting -" ------------ - -" editor -exe 'hi! Normal' .s:fg_foreground .s:bg_background .s:fmt_none -exe 'hi! ColorColumn' .s:fg_none .s:bg_line .s:fmt_none -exe 'hi! CursorColumn' .s:fg_none .s:bg_line .s:fmt_none -exe 'hi! CursorLine' .s:fg_none .s:bg_line .s:fmt_none -exe 'hi! NonText' .s:fg_selection .s:bg_none .s:fmt_none -exe 'hi! StatusLine' .s:fg_comment .s:bg_background .s:fmt_revr -exe 'hi! StatusLineNC' .s:fg_window .s:bg_comment .s:fmt_revr -exe 'hi! TabLine' .s:fg_foreground .s:bg_darkcolumn .s:fmt_revr -exe 'hi! Visual' .s:fg_none .s:bg_selection .s:fmt_none -exe 'hi! Search' .s:fg_background .s:bg_yellow .s:fmt_none -exe 'hi! MatchParen' .s:fg_background .s:bg_purple .s:fmt_none -exe 'hi! Question' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! ModeMsg' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! MoreMsg' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! ErrorMsg' .s:fg_background .s:bg_red .s:fmt_stnd -exe 'hi! WarningMsg' .s:fg_red .s:bg_none .s:fmt_none - -if g:monokai_thick_border == 1 - exe 'hi! VertSplit' .s:fg_window .s:bg_darkcolumn .s:fmt_none - exe 'hi! LineNr' .s:fg_linenr .s:bg_lncolumn .s:fmt_none - exe 'hi! CursorLineNr' .s:fg_orange .s:bg_none .s:fmt_none - exe 'hi! SignColumn' .s:fg_none .s:bg_lncolumn .s:fmt_none -else - exe 'hi! VertSplit' .s:fg_window .s:bg_none .s:fmt_none - exe 'hi! LineNr' .s:fg_linenr .s:bg_none .s:fmt_none - exe 'hi! CursorLineNr' .s:fg_orange .s:bg_none .s:fmt_bold - exe 'hi! SignColumn' .s:fg_none .s:bg_darkcolumn .s:fmt_none -endif - -" misc -exe 'hi! SpecialKey' .s:fg_selection .s:bg_none .s:fmt_none -exe 'hi! Title' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! Directory' .s:fg_aqua .s:bg_none .s:fmt_none - -" diff -exe 'hi! DiffAdd' .s:fg_addfg .s:bg_addbg .s:fmt_none -exe 'hi! DiffDelete' .s:fg_background .s:bg_delbg .s:fmt_none -exe 'hi! DiffChange' .s:fg_changefg .s:bg_changebg .s:fmt_none -exe 'hi! DiffText' .s:fg_background .s:bg_aqua .s:fmt_none - -" fold -exe 'hi! Folded' .s:fg_comment .s:bg_darkcolumn .s:fmt_none -exe 'hi! FoldColumn' .s:fg_none .s:bg_darkcolumn .s:fmt_none -" Incsearch' - -" popup menu -exe 'hi! Pmenu' .s:fg_lncolumn .s:bg_foreground .s:fmt_none -exe 'hi! PmenuSel' .s:fg_aqua .s:bg_background .s:fmt_revb -exe 'hi! PmenuThumb' .s:fg_lncolumn .s:bg_linenr .s:fmt_none -" PmenuSbar' - -" Generic Syntax Highlighting -" --------------------------- - -if g:monokai_italic == 1 - exe 'hi! Constant' .s:fg_purple .s:bg_none .s:fmt_ital -else - exe 'hi! Constant' .s:fg_purple .s:bg_none .s:fmt_none -endif - -exe 'hi! Number' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! Float' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! Boolean' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! Character' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! String' .s:fg_yellow .s:bg_none .s:fmt_none - -if g:monokai_italic == 1 - exe 'hi! Type' .s:fg_aqua .s:bg_none .s:fmt_ital - exe 'hi! Structure' .s:fg_pink .s:bg_none .s:fmt_ital - exe 'hi! StorageClass'.s:fg_pink .s:bg_none .s:fmt_ital - exe 'hi! Typedef' .s:fg_pink .s:bg_none .s:fmt_ital -else - exe 'hi! Type' .s:fg_aqua .s:bg_none .s:fmt_none - exe 'hi! Structure' .s:fg_pink .s:bg_none .s:fmt_none - exe 'hi! StorageClass'.s:fg_pink .s:bg_none .s:fmt_none - exe 'hi! Typedef' .s:fg_pink .s:bg_none .s:fmt_none -endif - -exe 'hi! Identifier' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! Function' .s:fg_green .s:bg_none .s:fmt_none - -exe 'hi! Statement' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! Operator' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! Label' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! Keyword' .s:fg_aqua .s:bg_none .s:fmt_none -" Conditional' -" Repeat' -" Exception' - -exe 'hi! PreProc' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! Include' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! Define' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! Macro' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! PreCondit' .s:fg_green .s:bg_none .s:fmt_none - -exe 'hi! Special' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! SpecialChar' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! Delimiter' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! SpecialComment' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! Tag' .s:fg_pink .s:bg_none .s:fmt_none -" Debug' - -exe 'hi! Underlined' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! Ignore' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! Error' .s:fg_red .s:bg_error .s:fmt_none - -if g:monokai_italic == 1 - exe 'hi! Todo' .s:fg_orange .s:bg_none .s:fmt_bldi - exe 'hi! Comment' .s:fg_comment .s:bg_none .s:fmt_ital -else - exe 'hi! Todo' .s:fg_orange .s:bg_none .s:fmt_bold - exe 'hi! Comment' .s:fg_comment .s:bg_none .s:fmt_none -endif - -" NerdTree -" -------- - -exe 'hi! NERDTreeOpenable' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! NERDTreeClosable' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! NERDTreeHelp' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! NERDTreeBookmarksHeader' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! NERDTreeBookmarksLeader' .s:fg_background .s:bg_none .s:fmt_none -exe 'hi! NERDTreeBookmarkName' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! NERDTreeCWD' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! NERDTreeUp' .s:fg_foreground .s:bg_none .s:fmt_none -exe 'hi! NERDTreeDirSlash' .s:fg_background .s:bg_none .s:fmt_none -exe 'hi! NERDTreeDir' .s:fg_zentree .s:bg_none .s:fmt_none - -" Syntastic -" --------- - -hi! link SyntasticErrorSign Error -if g:monokai_thick_border == 1 - exe 'hi! SyntasticWarningSign' .s:fg_lncolumn .s:bg_orange .s:fmt_none -else - exe 'hi! SyntasticWarningSign' .s:fg_orange .s:bg_darkcolumn .s:fmt_none -endif - -" Language highlight -" ------------------ - -" Java properties -exe 'hi! jpropertiesIdentifier' .s:fg_pink .s:bg_none .s:fmt_none - -" Vim command -exe 'hi! vimCommand' .s:fg_pink .s:bg_none .s:fmt_none - -" Javascript -exe 'hi! jsFuncName' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! jsThis' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! jsFunctionKey' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! jsFuncAssignIdent' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! jsPrototype' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! jsGlobalObjects' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! jsExceptions' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! jsFutureKeys' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! jsBuiltins' .s:fg_aqua .s:bg_none .s:fmt_none - -if g:monokai_italic == 1 - exe 'hi! jsFuncArgs' .s:fg_orange .s:bg_none .s:fmt_ital - exe 'hi! jsFuncAssignObjChain' .s:fg_aqua .s:bg_none .s:fmt_ital - exe 'hi! jsStorageClass' .s:fg_aqua .s:bg_none .s:fmt_ital - exe 'hi! jsDocTags' .s:fg_aqua .s:bg_none .s:fmt_ital -else - exe 'hi! jsFuncArgs' .s:fg_orange .s:bg_none .s:fmt_none - exe 'hi! jsFuncAssignObjChain' .s:fg_aqua .s:bg_none .s:fmt_none - exe 'hi! jsStorageClass' .s:fg_aqua .s:bg_none .s:fmt_none - exe 'hi! jsDocTags' .s:fg_aqua .s:bg_none .s:fmt_none -endif - -exe 'hi! javaScriptFunction' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! javaScriptIdentifier' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! javaScriptLabel' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! javaScriptBraces' .s:fg_none .s:bg_none .s:fmt_none - -" Html -exe 'hi! htmlTag' .s:fg_foreground .s:bg_none .s:fmt_none -exe 'hi! htmlEndTag' .s:fg_foreground .s:bg_none .s:fmt_none -exe 'hi! htmlTagName' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! htmlArg' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! htmlSpecialChar' .s:fg_purple .s:bg_none .s:fmt_none - -" Xml -exe 'hi! xmlTag' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! xmlEndTag' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! xmlTagName' .s:fg_orange .s:bg_none .s:fmt_none -exe 'hi! xmlAttrib' .s:fg_green .s:bg_none .s:fmt_none - -" CSS -exe 'hi! cssProp' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! cssUIAttr' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! cssFunctionName' .s:fg_aqua .s:bg_none .s:fmt_none -exe 'hi! cssColor' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! cssPseudoClassId' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! cssClassName' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! cssValueLength' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! cssCommonAttr' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! cssBraces' .s:fg_foreground .s:bg_none .s:fmt_none -exe 'hi! cssClassNameDot' .s:fg_pink .s:bg_none .s:fmt_none - -if g:monokai_italic == 1 - exe 'hi! cssURL' .s:fg_orange .s:bg_none .s:fmt_undi -else - exe 'hi! cssURL' .s:fg_orange .s:bg_none .s:fmt_undr -endif - -" ruby -exe 'hi! rubyInterpolationDelimiter' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! rubyInstanceVariable' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! rubyGlobalVariable' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! rubyClassVariable' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! rubyPseudoVariable' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! rubyFunction' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! rubyStringDelimiter' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! rubyRegexp' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! rubyRegexpDelimiter' .s:fg_yellow .s:bg_none .s:fmt_none -exe 'hi! rubySymbol' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! rubyEscape' .s:fg_purple .s:bg_none .s:fmt_none -exe 'hi! rubyInclude' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! rubyOperator' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! rubyControl' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! rubyClass' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! rubyDefine' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! rubyException' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! rubyRailsARAssociationMethod' .s:fg_orange .s:bg_none .s:fmt_none -exe 'hi! rubyRailsARMethod' .s:fg_orange .s:bg_none .s:fmt_none -exe 'hi! rubyRailsRenderMethod' .s:fg_orange .s:bg_none .s:fmt_none -exe 'hi! rubyRailsMethod' .s:fg_orange .s:bg_none .s:fmt_none - -if g:monokai_italic == 1 - exe 'hi! rubyConstant' .s:fg_aqua .s:bg_none .s:fmt_ital - exe 'hi! rubyBlockArgument' .s:fg_orange .s:bg_none .s:fmt_ital - exe 'hi! rubyBlockParameter' .s:fg_orange .s:bg_none .s:fmt_ital -else - exe 'hi! rubyConstant' .s:fg_orange .s:bg_none .s:fmt_none - exe 'hi! rubyBlockArgument' .s:fg_orange .s:bg_none .s:fmt_none - exe 'hi! rubyBlockParameter' .s:fg_orange .s:bg_none .s:fmt_none -endif - -" eruby -exe 'hi! erubyDelimiter' .s:fg_none .s:bg_none .s:fmt_none -exe 'hi! erubyRailsMethod' .s:fg_aqua .s:bg_none .s:fmt_none - -" c -exe 'hi! cLabel' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! cStructure' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! cStorageClass' .s:fg_pink .s:bg_none .s:fmt_none -exe 'hi! cInclude' .s:fg_green .s:bg_none .s:fmt_none -exe 'hi! cDefine' .s:fg_green .s:bg_none .s:fmt_none - -exe 'hi! goFunction' .s:fg_green .s:bg_none .s:fmt_none diff --git a/starship.toml b/starship.toml index 5c7e0c7..415fa1f 100755 --- a/starship.toml +++ b/starship.toml @@ -1,8 +1,97 @@ -[character] -symbol = ">" -#error_symbol = "[error]" -use_symbol_for_status = true +command_timeout = 1000 + +format = """ +[](fg:#7aa2f7)\ +$os\ +[ ](fg:#7aa2f7 bg:#1a1b26)\ +$directory\ +$git_branch\ +$git_status\ +$git_metrics\ +[ ](bg:#1a1b26)\ +[](fg:#1a1b26)\ +$fill\ +\n$character\ +""" + +right_format = """ +$cmd_duration +$lua +$rust +$golang +$nodejs +$python +$conda +$aws +$time +""" [directory] +truncation_length = 6 +truncation_symbol = ' ' truncate_to_repo = false -truncation_length = 30 +home_symbol = ' ~' +style = 'fg:#7aa2f7 bg:#1a1b26' +read_only = ' 󰌾 ' +read_only_style = 'fg:#f7768e bg:#1a1b26' +format = '[$path]($style)[$read_only]($read_only_style)' + +[os] +format = "[$symbol]($style)" +style = 'fg:#1a1b26 bg:#7aa2f7' +disabled = false +[os.symbols] +Macos = " " +Ubuntu = " " +Garuda = "🦅 " +Linux = "🐧 " + +[character] +success_symbol = "[❯](green)" +error_symbol = "[❯](red)" + +[git_branch] +symbol = '  ' # nf-fa-github_alt, nf-fa-code_fork +truncation_length = 4 +truncation_symbol = '' +style = 'fg:#7aa2f7 bg:#1a1b26' +format = '[  $symbol$branch(:$remote_branch)]($style)' # nf-pl-left_soft_divider + +[git_status] +style = 'fg:#e0af68 bg:#1a1b26' +conflicted = '=' +ahead = '⇡${count}' +behind = '⇣${count}' +diverged = '⇕' +up_to_date = '✓' +untracked = '?' +stashed = '\$' +modified = '!${count}' +renamed = '»' +deleted = '✘' +format = '([\[$all_status$ahead_behind\]]($style))' + +[git_metrics] +added_style = 'fg:#9ece6a bg:#1a1b26' +deleted_style = 'fg:#9ece6a bg:#1a1b26' +format = '[+$added/-$deleted]($deleted_style)' +disabled = false + +[cmd_duration] +min_time = 1 +style = 'fg:#e0af68' +format = "[   $duration]($style)" + +[golang] +format = '[  $symbol $version]($style)' +version_format = 'v$raw' +symbol = " " + +[nodejs] +format = '[  $symbol $version]($style)' +version_format = 'v$raw' +symbol = " " + +[aws] +symbol = '☁️ ' + diff --git a/tmux.conf b/tmux.conf index 72f1c84..f2cc1f9 100644 --- a/tmux.conf +++ b/tmux.conf @@ -1,6 +1,7 @@ # tmux.conf unbind l unbind s +set-option -g default-shell $SHELL # window index start with 1 set -g base-index 1 @@ -90,22 +91,37 @@ set -sg escape-time 0 # Reload tmux config unbind r -bind r source-file ~/.tmux.conf +bind r source-file ~/.config/tmux/tmux.conf +bind R run-shell "tmux list-panes -a -F '#{pane_pid}' | xargs -I {} kill -USR1 {}" +setw -g mode-keys vi -# copy Linux +# copy / clipboard bind-key -T copy-mode-vi 'v' send-keys -X begin-selection -bind-key -T copy-mode-vi 'y' send-keys -X copy-pipe-and-cancel "xsel -i -p && xsel -o -p | xsel -i -b" -bind-key p run "xsel -o | tmux load-buffer - ; tmux paste-buffer" +if-shell 'command -v pbcopy >/dev/null 2>&1' 'bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"; bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"; bind-key p run "pbpaste | tmux load-buffer - ; tmux paste-buffer"' 'bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "xsel -i -p && xsel -o -p | xsel -i -b"; bind-key p run "xsel -o | tmux load-buffer - ; tmux paste-buffer"' # -------------------- # Theme # -------------------- # set the default TERM -#set -g default-terminal screen +set -g default-terminal screen +set -g default-terminal "tmux-256color" +set -as terminal-overrides ",*:Tc" # update the TERM variable of terminal emulator when creating a new session or attaching a existing session -#set -g update-environment 'DISPLAY SSH_ASKPASS SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY TERM' +# set -g update-environment 'DISPLAY SSH_ASKPASS SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY TERM' # determine if we should enable 256-colour support -#if "[[ ${TERM} =~ 256color || ${TERM} == fbterm ]]" 'set -g default-terminal screen-256color' -#set -g default-command "${SHELL}" -if "[[ ${TERM} =~ 256color || ${TERM} == fbterm ]]" 'set -g default-terminal screen-256color' -if "[[ ${hostname} =~ docker ]]" 'set -g default-terminal screen-256color' +# if "[[ ${TERM} =~ 256color || ${TERM} == fbterm ]]" 'set -g default-terminal screen-256color' +set -g default-command "${SHELL}" +# if "[[ ${TERM} =~ 256color || ${TERM} == fbterm ]]" 'set -g default-terminal screen-256color' +# if "[[ ${hostname} =~ docker ]]" 'set -g default-terminal screen-256color' + +# -------------------- +# Docker container prefix override +# -------------------- +if-shell '[ "$HOST" = "$USER" ]' \ + 'unbind C-b; set -g prefix C-w; bind C-w send-prefix' + +# -------------------- +# Plugins +# -------------------- +set -g @plugin 'sainnhe/tmux-fzf' + diff --git a/versions/GO_VERSION b/versions/GO_VERSION index 14bee92..ea0928c 100644 --- a/versions/GO_VERSION +++ b/versions/GO_VERSION @@ -1 +1 @@ -1.23.2 +1.26.4 diff --git a/versions/NGT_VERSION b/versions/NGT_VERSION index 0bee604..a4dd9db 100644 --- a/versions/NGT_VERSION +++ b/versions/NGT_VERSION @@ -1 +1 @@ -2.3.3 +2.7.4 diff --git a/versions/TENSORFLOW_C_VERSION b/versions/TENSORFLOW_C_VERSION index cf86907..db65e21 100644 --- a/versions/TENSORFLOW_C_VERSION +++ b/versions/TENSORFLOW_C_VERSION @@ -1 +1 @@ -2.18.0 +2.21.0 diff --git a/vintrc.yaml b/vintrc.yaml deleted file mode 100755 index f41e669..0000000 --- a/vintrc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -cmdargs: - # Checking more strictly - severity: style_problem - - # Enable coloring - color: true - - # Enable Neovim syntax - env: - neovim: true - -policies: - # Disable a violation - ProhibitSomethingEvil: - enabled: false - - # Enable a violation - ProhibitSomethingBad: - enabled: true diff --git a/zshrc b/zshrc index 9eca129..ebe3aa8 100644 --- a/zshrc +++ b/zshrc @@ -2,23 +2,27 @@ USER=$(whoami) HOST=$(hostname) +# -------------------- +# Homebrew shellenv (macOS) +# non-login shell (tmux split-pane 等) では path_helper が走らず +# brew prefix が PATH に乗らないことがあり、後続の `type fzf` 等の +# 偽陰性で alias が消える。zshrc 冒頭で確定させる。 +# -------------------- +if [ "$(uname -s)" = Darwin ]; then + if [ -x /opt/homebrew/bin/brew ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [ -x /usr/local/bin/brew ]; then + eval "$(/usr/local/bin/brew shellenv)" + fi +fi + if type tmux >/dev/null 2>&1; then - if [ -z $TMUX ]; then - if [ "$HOST" = "$USER" ]; then - # get the id of a deattached session - ID="$(tmux ls | grep -vm1 attached | cut -d: -f1)" # get the id of a deattached session - if [[ -z $ID ]]; then # if not available create a new one - tmux -2 new-session -n$USER -s$USER@$HOST - else - tmux -2 attach-session -t "$ID" # if available attach to it - fi + if [ -z "$TMUX" ]; then + ID="$(tmux ls 2>/dev/null | grep -vm1 attached | cut -d: -f1)" + if [[ -z "$ID" ]]; then + tmux new-session -n "$USER" -s "$USER@$HOST" else - tmux source-file /home/${USER}/.tmux.conf - pkill tmux - tmux -2 new-session -n$USER -s$USER@$HOST - tmux source-file /home/${USER}/.tmux.conf - tmux unbind C-b - tmux set -g prefix C-w + tmux attach-session -t "$ID" fi fi fi @@ -42,76 +46,68 @@ if type go >/dev/null 2>&1; then export GOROOT="$(go env GOROOT)" export GOOS="$(go env GOOS)" export GOARCH="$(go env GOARCH)" - # export GOBIN=$GOPATH/bin export CGO_ENABLED=1 export GO111MODULE=on - # export GOBIN=$GOPATH/bin export GO15VENDOREXPERIMENT=1 export GOPRIVATE="*.yahoo.co.jp" - export NVIM_GO_LOG_FILE=$XDG_DATA_HOME/go + export NVIM_GO_LOG_FILE=$NVIM_LOG_FILE_PATH/go fi -# nvm -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - -# -------------------- -# deno -# -------------------- -export DENO_INSTALL="$HOME/.deno" - # -------------------- # nvim(NeoVim) # -------------------- if type nvim >/dev/null 2>&1; then - export VIM=$(which nvim) + export NVIM_HOME=$XDG_CONFIG_HOME/nvim if [ $(uname) = 'Darwin' ]; then - export VIMRUNTIME=/usr/local/share/nvim/runtime + export VIMRUNTIME=/opt/homebrew/share/nvim/runtime else export VIMRUNTIME=/usr/share/nvim/runtime fi - export NVIM_HOME=$XDG_CONFIG_HOME/nvim - export XDG_DATA_HOME=$NVIM_HOME/log - export NVIM_LOG_FILE_PATH=$XDG_DATA_HOME - export NVIM_TUI_ENABLE_TRUE_COLOR=1 + # nvim の data は XDG default (~/.local/share/nvim) に、log は ~/.local/state/nvim に置く。 + # ~/.config/nvim は repo への symlink なので、$NVIM_HOME 配下に XDG_DATA_HOME を向けると + # 全 XDG アプリの状態まで repo に流れ込む。local に固定して repo を汚さない。 + export NVIM_LOG_FILE_PATH=$HOME/.local/state/nvim + mkdir -p "$NVIM_LOG_FILE_PATH" + # export NVIM_TUI_ENABLE_TRUE_COLOR=1 export NVIM_PYTHON_LOG_LEVEL=WARNING; export NVIM_PYTHON_LOG_FILE=$NVIM_LOG_FILE_PATH/nvim.log; - export NVIM_LISTEN_ADDRESS="127.0.0.1:7650"; + export NVIM_LISTEN_ADDRESS="/tmp/nvim_$$"; + alias vim=$(which nvim) + export EDITOR=$(which nvim) + export VISUAL=$(which nvim) elif type vim >/dev/null 2>&1; then export VIM=$(which vim) export VIMRUNTIME=/usr/share/vim/vim* + alias vim=$(which vim) + export EDITOR=$(which vim) + export VISUAL=$(which vim) else export VIM=$(which vi) + export EDITOR=$(which vi) + export VISUAL=$(which vi) fi - -export EDITOR=$VIM -export VISUAL=$VIM export PAGER=$(which less) export SUDO_EDITOR=$EDITOR # -------------------- -# bpctl +# k9s # -------------------- -export BPCTL_V2_DEFAULT=true +export K9S="$HOME/.local/bin" # -------------------- -# k9s +# node # -------------------- -export K9S="$HOME/.local/bin" +# export NODE_PATH="/usr/local/lib/node_modules" # -------------------- -# PATH +# volta # -------------------- -export PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/share/npm/bin:/usr/local/go/bin:/opt/local/bin:$GOBIN:$HOME/.cargo/bin:/root/.cargo/bin:/GCLOUD_PATH/bin:$DENO_INSTALL/bin:$K9S:$PATH" +export VOLTA_HOME=$HOME/.volta -export ZPLUG_HOME=$HOME/.zplug -if type zplug >/dev/null 2>&1; then - if zplug check junegunn/fzf; then - export FZF_DEFAULT_COMMAND='rg --files --hidden --follow --glob "!.git/*"' - export FZF_DEFAULT_OPTS='--height 40% --reverse --border' - fi -fi +# -------------------- +# PATH +# -------------------- +export PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/share/npm/bin:/usr/local/go/bin:/usr/local/lib:/opt/local/bin:$GOBIN:$HOME/.cargo/bin:/root/.cargo/bin:/GCLOUD_PATH/bin:$K9S:$VOLTA_HOME/bin:$PATH" if [ ! -f "$HOME/.zshrc.zwc" -o "$HOME/.zshrc" -nt "$HOME/.zshrc.zwc" ]; then zcompile $HOME/.zshrc @@ -122,284 +118,381 @@ if [ ! -f "$HOME/.zcompdump.zwc" -o "$HOME/.zcompdump" -nt "$HOME/.zcompdump.zwc fi [ -f $HOME/.aliases ] && source $HOME/.aliases -[ -f $HOME/.zcpaliases ] && source $HOME/.zcpaliases - -if [ -z $ZSH_LOADED ]; then - # -------------------- - # zplug - # -------------------- - if [[ -f ~/.zplug/init.zsh ]]; then - source "$HOME/.zplug/init.zsh" - zplug "junegunn/fzf", as:command, hook-build:"make install", use:bin/fzf - zplug "junegunn/fzf", as:command, use:bin/fzf-tmux - zplug "zchee/go-zsh-completions" - zplug "zsh-users/zsh-autosuggestions" - zplug "zsh-users/zsh-completions", as:plugin, use:"src" - zplug "zsh-users/zsh-history-substring-search" - zplug "zsh-users/zsh-syntax-highlighting", defer:2 - zplug "superbrothers/zsh-kubectl-prompt", as:plugin, from:github, use:"kubectl.zsh" - zplug "greymd/tmux-xpanes" - zplug "felixr/docker-zsh-completion" - if ! zplug check --verbose; then - zplug install - fi - zplug load - else - rm -rf $ZPLUG_HOME - git clone https://github.com/zplug/zplug $ZPLUG_HOME - source "$HOME/.zshrc" - return 0 - fi - # use colors - autoload -Uz colors - colors - - # -------------------- - # option - # -------------------- - setopt no_beep - setopt correct - setopt auto_cd # ディレクトリ名だけでcdする - setopt auto_list # 補完候補を一覧表示 - setopt auto_menu # 補完候補が複数あるときに自動的に一覧表示する - setopt auto_param_keys # カッコの対応などを自動的に補完 - setopt auto_pushd # cd したら自動的にpushdする - setopt extended_glob - setopt ignore_eof - setopt interactive_comments # '#' 以降をコメントとして扱う - setopt list_packed # 補完候補を詰めて表示 - setopt list_types # 補完候補一覧でファイルの種別をマーク表示 - setopt magic_equal_subst # = の後はパス名として補完する - setopt no_flow_control # フローコントロールを無効にする - setopt noautoremoveslash - setopt nonomatch - setopt notify # バックグラウンドジョブの状態変化を即時報告 - setopt print_eight_bit # 日本語ファイル名を表示可能にする - setopt prompt_subst # プロンプト定義内で変数置換やコマンド置換を扱う - setopt pushd_ignore_dups # 重複したディレクトリを追加しない - - # -------------------- - # history setting - # -------------------- - HISTFILE=~/.zsh_history - HISTSIZE=100000 - SAVEHIST=100000 - # ignore duplicated history - setopt hist_ignore_all_dups - setopt hist_ignore_dups - setopt hist_ignore_space - setopt hist_reduce_blanks - setopt hist_save_no_dups - # shareing history - setopt share_history - # append history - setopt append_history - # prompt command - export PROMPT_COMMAND='hcmd=$(history 1); hcmd="${hcmd# *[0-9]* }"; if [[ ${hcmd%% *} == "cd" ]]; then pwd=$OLDPWD; else pwd=$PWD; fi; hcmd=$(echo -e "cd $pwd && $hcmd"); history -s "$hcmd"' - - # -------------------- - # word chars - # -------------------- - # set word style - autoload -Uz select-word-style - select-word-style default - # set delimiter - zstyle ':zle:*' word-chars " /=;@:{},|" # remove dir via ^W - zstyle ':zle:*' word-style unspecified - - # -------------------- - # completion - # -------------------- - LISTMAX=1000 - WORDCHARS="$WORDCHARS|:" - - # use completion - autoload -Uz compinit -C && compinit -C - if [ -e /usr/local/share/zsh-completions ]; then - fpath=(/usr/local/share/zsh-completions $fpath) - fi +# use colors +# autoload -Uz colors +# colors - zstyle ':completion:*' format '%B%d%b' - zstyle ':completion:*' group-name '' - zstyle ':completion:*' ignore-parents parent pwd .. - zstyle ':completion:*' keep-prefix - zstyle ':comletion:*' list-colors ${LS_COLORS} - zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*' - zstyle ':completion:*' menu select - zstyle ':completion:*' squeeze-slashes true - zstyle ':completion:*' verbose yes - zstyle ':completion:*:(nano|vim|nvim|vi|emacs|e):*' ignored-patterns '*.(wav|mp3|flac|ogg|mp4|avi|mkv|webm|iso|dmg|so|o|a|bin|exe|dll|pcap|7z|zip|tar|gz|bz2|rar|deb|pkg|gzip|pdf|mobi|epub|png|jpeg|jpg|gif)' - zstyle ':completion:*:(rm|kill|diff):*' ignore-line other - zstyle ':completion:*:*:*:*:*' menu select - zstyle ':completion:*:*:-subscript-:*' tag-order indexes parameters - zstyle ':completion:*:*:cd:*' tag-order local-directories directory-stack path-directories - zstyle ':completion:*:*:cd:*:directory-stack' menu yes select - zstyle ':completion:*:-tilde-:*' group-order 'named-directories' 'path-directories' 'expand' - zstyle ':completion:*:corrections' format ' %F{green}-- %d (errors: %e) --%f' - zstyle ':completion:*:default' list-colors ${LS_COLORS} - zstyle ':completion:*:default' list-prompt '%S%M matches%s' - zstyle ':completion:*:default' menu select=1 - zstyle ':completion:*:descriptions' format ' %F{yellow}-- %d --%f' - zstyle ':completion:*:functions' ignored-patterns '(_*|pre(cmd|exec)|prompt_*)' - zstyle ':completion:*:history-words' list false - zstyle ':completion:*:history-words' menu yes - zstyle ':completion:*:history-words' remove-all-dups yes - zstyle ':completion:*:history-words' stop yes - zstyle ':completion:*:manuals' separate-sections true - zstyle ':completion:*:manuals.(^1*)' insert-sections true - zstyle ':completion:*:matches' group 'yes' - zstyle ':completion:*:messages' format ' %F{purple} -- %d --%f' - zstyle ':completion:*:options' auto-description '%d' - zstyle ':completion:*:options' description 'yes' - zstyle ':completion:*:processes' command 'ps x -o pid, s, args' - zstyle ':completion:*:rm:*' file-patterns '*:all-files' - zstyle ':completion:*:sudo:*' command-path /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/X11R6/bin - zstyle ':completion:*:warnings' format ' %F{red}-- no matches found --%f' - zstyle ':completion::complete:*' cache-path "${ZDOTDIR:-${HOME}}/.zcompcache" - zstyle ':completion::complete:*' use-cache on - zstyle ':zsh-kubectl-prompt:' separator ' | ns: ' - zstyle ':zsh-kubectl-prompt:' preprompt 'ctx: ' - zstyle ':zsh-kubectl-prompt:' postprompt '' - setopt list_packed - - # -------------------- - # prompt - # -------------------- - autoload -Uz vcs_info - autoload -Uz add-zsh-hook - setopt prompt_subst - zstyle ':vcs_info:*' formats '(%s)-[%c]-%u[%b]' - zstyle ':vcs_info:*' actionformats '%F{red}(%s)-[%b|%a]%f' - zstyle ':vcs_info:git:*' check-for-changes true - zstyle ':vcs_info:git:*' stagedstr "%F{magenta}!" - zstyle ':vcs_info:git:*' unstagedstr "%F{yellow}+" - precmd() { - vcs_info - } - PROMPT='%F{green}%n%f:%F{cyan}%/ $ %f' - RPROMPT='${vcs_info_msg_0_}' - - _update_vcs_info_msg() { - LANG=en_US.UTF-8 - RPROMPT="%F{green}${vcs_info_msg_0_} %F{033}($ZSH_KUBECTL_PROMPT)%f" - } - add-zsh-hook precmd _update_vcs_info_msg - - export LSCOLORS=CxGxcxdxCxegedabagacad - - mkcd() { - if [[ -d $1 ]]; then - \cd $1 - else - printf "Confirm to Make Directory? $1 [y/N]: " - if read -q; then - echo - \mkdir -p $1 && \cd $1 - fi - fi - } - - if type fzf >/dev/null 2>&1; then - if type fzf-tmux >/dev/null 2>&1; then - if type fd >/dev/null 2>&1; then - alias s='mkcd $(fd -a -H -t d . | fzf-tmux)' - alias vf='vim $(fd -a -H -t f . | fzf-tmux)' - fi - # if type rg >/dev/null 2>&1; then - # fbr() { - # git branch --all | rg -v HEAD | fzf-tmux +m | sed -e "s/.* //" -e "s#remotes/[^/]*/##" | xargs git checkout - # } - # alias fbr=fbr - # sshf() { - # ssh $(rg "Host " $HOME/.ssh/config | awk '{print $2}' | rg -v "\*" | fzf-tmux +m) - # } - # alias sshf=sshf - # fi - if type ghq >/dev/null 2>&1; then - alias g='mkcd $(ghq root)/$(ghq list | fzf-tmux)' - fi +# -------------------- +# option +# -------------------- +setopt no_beep +setopt correct +setopt auto_cd # ディレクトリ名だけでcdする +setopt auto_list # 補完候補を一覧表示 +setopt auto_menu # 補完候補が複数あるときに自動的に一覧表示する +setopt auto_param_keys # カッコの対応などを自動的に補完 +setopt auto_pushd # cd したら自動的にpushdする +setopt extended_glob +setopt ignore_eof +setopt interactive_comments # '#' 以降をコメントとして扱う +setopt list_packed # 補完候補を詰めて表示 +setopt list_types # 補完候補一覧でファイルの種別をマーク表示 +setopt magic_equal_subst # = の後はパス名として補完する +setopt no_flow_control # フローコントロールを無効にする +setopt noautoremoveslash +setopt nonomatch +setopt notify # バックグラウンドジョブの状態変化を即時報告 +setopt print_eight_bit # 日本語ファイル名を表示可能にする +setopt prompt_subst # プロンプト定義内で変数置換やコマンド置換を扱う +setopt pushd_ignore_dups # 重複したディレクトリを追加しない + +# -------------------- +# history setting +# -------------------- +HISTFILE=~/.zsh_history +HISTSIZE=100000 +SAVEHIST=100000 +# ignore duplicated history +setopt hist_ignore_all_dups +setopt hist_ignore_dups +setopt hist_ignore_space +setopt hist_reduce_blanks +setopt hist_save_no_dups +setopt share_history +setopt append_history + +# -------------------- +# word chars +# -------------------- +# set word style +autoload -Uz select-word-style +select-word-style default +# set delimiter +zstyle ':zle:*' word-chars " /=;@:{},|" # remove dir via ^W +zstyle ':zle:*' word-style unspecified + +# -------------------- +# completion +# -------------------- +LISTMAX=1000 +WORDCHARS="$WORDCHARS|:" + +# use completion +if [ -n "$HOMEBREW_PREFIX" ] && [ -d "$HOMEBREW_PREFIX/share/zsh/functions" ]; then + fpath=("$HOMEBREW_PREFIX/share/zsh/functions" $fpath) +fi +autoload -Uz compinit -C && compinit -C +# if [ -e /usr/local/share/zsh-completions ]; then +# fpath=(/usr/local/share/zsh-completions $fpath) +# fi + +zstyle ':completion:*' format '%B%d%b' +zstyle ':completion:*' group-name '' +zstyle ':completion:*' ignore-parents parent pwd .. +zstyle ':completion:*' keep-prefix +zstyle ':comletion:*' list-colors ${LS_COLORS} +zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|[._-]=* r:|=*' 'l:|=* r:|=*' +zstyle ':completion:*' menu select +zstyle ':completion:*' squeeze-slashes true +zstyle ':completion:*' verbose yes +zstyle ':completion:*:(nano|vim|nvim|vi|emacs|e):*' ignored-patterns '*.(wav|mp3|flac|ogg|mp4|avi|mkv|webm|iso|dmg|so|o|a|bin|exe|dll|pcap|7z|zip|tar|gz|bz2|rar|deb|pkg|gzip|pdf|mobi|epub|png|jpeg|jpg|gif)' +zstyle ':completion:*:(rm|kill|diff):*' ignore-line other +zstyle ':completion:*:*:*:*:*' menu select +zstyle ':completion:*:*:-subscript-:*' tag-order indexes parameters +zstyle ':completion:*:*:cd:*' tag-order local-directories directory-stack path-directories +zstyle ':completion:*:*:cd:*:directory-stack' menu yes select +zstyle ':completion:*:-tilde-:*' group-order 'named-directories' 'path-directories' 'expand' +zstyle ':completion:*:corrections' format ' %F{green}-- %d (errors: %e) --%f' +zstyle ':completion:*:default' list-colors ${LS_COLORS} +zstyle ':completion:*:default' list-prompt '%S%M matches%s' +zstyle ':completion:*:default' menu select=1 +zstyle ':completion:*:descriptions' format ' %F{yellow}-- %d --%f' +zstyle ':completion:*:functions' ignored-patterns '(_*|pre(cmd|exec)|prompt_*)' +zstyle ':completion:*:history-words' list false +zstyle ':completion:*:history-words' menu yes +zstyle ':completion:*:history-words' remove-all-dups yes +zstyle ':completion:*:history-words' stop yes +zstyle ':completion:*:manuals' separate-sections true +zstyle ':completion:*:manuals.(^1*)' insert-sections true +zstyle ':completion:*:matches' group 'yes' +zstyle ':completion:*:messages' format ' %F{purple} -- %d --%f' +zstyle ':completion:*:options' auto-description '%d' +zstyle ':completion:*:options' description 'yes' +zstyle ':completion:*:processes' command 'ps x -o pid, s, args' +zstyle ':completion:*:rm:*' file-patterns '*:all-files' +zstyle ':completion:*:sudo:*' command-path /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/X11R6/bin +zstyle ':completion:*:warnings' format ' %F{red}-- no matches found --%f' +zstyle ':completion::complete:*' cache-path "${ZDOTDIR:-${HOME}}/.zcompcache" +zstyle ':completion::complete:*' use-cache on +# zstyle ':zsh-kubectl-prompt:' separator ' | ns: ' +# zstyle ':zsh-kubectl-prompt:' preprompt 'ctx: ' +# zstyle ':zsh-kubectl-prompt:' postprompt '' +setopt list_packed + +mkcd() { + if [[ -d $1 ]]; then + \cd $1 + else + printf "Confirm to Make Directory? $1 [y/N]: " + if read -q; then + echo + \mkdir -p $1 && \cd $1 fi fi +} - # -------------------- - # alias - # -------------------- - alias q="tmux kill-session" - - # chmod - alias 600='chmod -R 600' - alias 644='chmod -R 644' - alias 655='chmod -R 655' - alias 755='chmod -R 755' - alias 777='chmod -R 777' - - # allow * as wildecard for searching - bindkey -e - select-history() { - BUFFER=$(history -n -r 1 \ - | awk 'length($0) > 2' \ - | rg -v "^...$" \ - | rg -v "^....$" \ - | rg -v "^.....$" \ - | rg -v "^......$" \ - | rg -v "^exit$" \ - | uniq -u \ - | fzf-tmux --no-sort +m --query "$LBUFFER" --prompt="History > ") - CURSOR=$#BUFFER - } - zle -N select-history - bindkey '^r' select-history - - fzf-z-search() { - local res=$(history -n 1 | tail -f | fzf) - if [ -n "$res" ]; then - BUFFER+="$res" - zle accept-line - else - return 0 - fi - } - zle -N fzf-z-search - bindkey '^s' fzf-z-search - - # nvim - if type nvim >/dev/null 2>&1; then - alias nvup="nvim +UpdateRemotePlugins +PlugInstall +PlugUpdate +PlugUpgrade +PlugClean +CocInstall +CocUpdate +qall" - nvim-init() { - rm -rf "$HOME/.config/gocode" - rm -rf "$HOME/.config/nvim/autoload" - rm -rf "$HOME/.config/nvim/ftplugin" - rm -rf "$HOME/.config/nvim/log" - rm -rf "$HOME/.config/nvim/plugged" - nvup - rm "$HOME/.nvimlog" - rm "$HOME/.viminfo" - } - alias vedit="$EDITOR $HOME/.config/nvim/init.vim" - alias nvinit="nvim-init" - alias vback="cp $HOME/.config/nvim/init.vim $HOME/.config/nvim/init.vim.back" - alias vake="$EDITOR Makefile" - alias vocker="$EDITOR Dockerfile" +# -------------------- +# alias +# -------------------- +alias q="tmux kill-session" + +# chmod +alias 600='chmod -R 600' +alias 644='chmod -R 644' +alias 655='chmod -R 655' +alias 755='chmod -R 755' +alias 777='chmod -R 777' + +# allow * as wildecard for searching +bindkey -e +select-history() { + BUFFER=$(history -n -r 1 \ + | awk 'length($0) > 2' \ + | rg -v "^...$" \ + | rg -v "^....$" \ + | rg -v "^.....$" \ + | rg -v "^......$" \ + | rg -v "^exit$" \ + | uniq -u \ + | fzf-tmux --no-sort +m --query "$LBUFFER" --prompt="History > ") + CURSOR=$#BUFFER +} +zle -N select-history +bindkey '^r' select-history + +fzf-z-search() { + local res=$(history -n 1 | tail -f | fzf) + if [ -n "$res" ]; then + BUFFER+="$res" + zle accept-line else - alias vedit="$EDITOR $HOME/.vimrc" + return 0 fi +} +zle -N fzf-z-search +bindkey '^s' fzf-z-search - alias vi="$EDITOR" - alias vim="$EDITOR" - alias v="$EDITOR" - alias vspdchk="rm -rf /tmp/starup.log && $EDITOR --startuptime /tmp/startup.log +q && less /tmp/startup.log" - alias xedit="$EDITOR $HOME/.Xdefaults" - alias wedit="$EDITOR $HOME/.config/sway/config" +# nvim +if type nvim >/dev/null 2>&1; then + alias vake="$EDITOR Makefile" + alias vocker="$EDITOR Dockerfile" +else + alias vedit="$EDITOR $HOME/.vimrc" +fi + +if type bat >/dev/null 2>&1; then + alias cat="bat" +fi + +export ZSH_LOADED=true + +export GPG_TTY=$TTY + +# sheldon +if type sheldon >/dev/null 2>&1; then + eval "$(sheldon source)" + cache_dir=${XDG_CACHE_HOME:-$HOME/.cache} + sheldon_cache="$cache_dir/sheldon.zsh" + sheldon_toml="$HOME/.config/sheldon/plugins.toml" + if [[ ! -r "$sheldon_cache" || "$sheldon_toml" -nt "$sheldon_cache" ]]; then + mkdir -p $cache_dir + sheldon source > $sheldon_cache + fi + source "$sheldon_cache" + unset cache_dir sheldon_cache sheldon_toml +fi - if type bat >/dev/null 2>&1; then - alias cat="bat" +# fzf 系 alias は sheldon が PATH に fzf / fzf-tmux を入れた後で判定する。 +# (旧位置だと `type fzf` が偽陰性となり、g / s / vf alias が定義されなかった) +if type fzf >/dev/null 2>&1; then + if type fzf-tmux >/dev/null 2>&1; then + if type fd >/dev/null 2>&1; then + alias s='mkcd $(fd -a -H -t d . | fzf-tmux)' + alias vf='vim $(fd -a -H -t f . | fzf-tmux)' + fi + if type ghq >/dev/null 2>&1; then + alias g='mkcd $(ghq root)/$(ghq list | fzf-tmux)' + fi fi +fi - export ZSH_LOADED=true +if type kubectl >/dev/null 2>&1; then + source <(kubectl completion zsh) +fi +# source <(kubectl convert completion zsh) +if type k3d >/dev/null 2>&1; then + source <(k3d completion zsh) +fi +if type helm >/dev/null 2>&1; then + source <(helm completion zsh) +fi +if type docker >/dev/null 2>&1; then + source <(docker completion zsh) fi -# Generated for envman. Do not edit. -[ -s "$HOME/.config/envman/load.sh" ] && source "$HOME/.config/envman/load.sh" +# alias +rcpath="$HOME/go/src/github.com/vankichi/dotfiles" +zpath="/usr/bin/zsh" +container_name="dev" +image_name="vankichi/dev:latest" + +function dockerrm { + docker container stop $(docker container ls -aq) + docker ps -aq | xargs docker rm -f + docker container prune -f + docker images -aq | xargs docker rmi -f + docker image prune -a + docker volume prune -f + docker network prune -f + docker system prune -a +} + +alias dockerrm="dockerrm" + +alias vmove="cd $rcpath" + +alias vbuild="vmove&&docker build --pull=true --file=$rcpath/Dockerfile -t vankichi/dev:latest $rcpath" + +function devrun { + port_range="7000-9300" + privileged=true + tz_path="/usr/share/zoneinfo/Japan" + font_dir="/System/Library/Fonts" + docker_daemon="$HOME/Library/Containers/com.docker.helper/Data/.docker/daemon.json" + docker_config="$HOME/Library/Containers/com.docker.helper/Data/.docker/config.json" + docker_sock="/var/run/docker.sock" + container_home="/home/vankichi" + container_root="/root" + container_goroot="$container_home/go" + goroot="/usr/local/go" + case "$(uname -s)" in + Darwin) + echo 'Docker on macOS start' + docker run \ + --cap-add=ALL \ + --name $container_name \ + --restart always \ + --privileged=$privileged \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -p $port_range:$port_range \ + -v $HOME/.docker/daemon.json:$container_root/.docker/daemon.json \ + -v $HOME/.kube:$container_root/.kube \ + -v $HOME/.netrc:$container_root/.netrc \ + -v $HOME/.ssh:$container_root/.ssh \ + -v $HOME/.gnupg:$container_root/.gnupg \ + -v $HOME/Documents:$container_root/Documents \ + -v $HOME/Downloads:$container_root/Downloads \ + -v $HOME/go/src:/go/src:cached \ + -v $docker_config:/etc/docker/config.json:ro,cached \ + -v $docker_daemon:/etc/docker/daemon.json:ro,cached \ + -v $font_dir:/usr/share/fonts:ro \ + -v $rcpath/editorconfig:$container_root/.editorconfig \ + -v $rcpath/gitconfig:$container_root/.gitconfig \ + -v $rcpath/gitignore:$container_root/.gitignore \ + -v $rcpath/tmux.conf:$container_root/.config/tmux/tmux.conf \ + -v $rcpath/zshrc:$container_root/.zshrc \ + -v $rcpath/go.env:$container_goroot/go.env:ro \ + -v $rcpath/go.env:$goroot/go.env:ro \ + -v $HOME/.zsh_history:$container_root/.zsh_history \ + -v $tz_path:/etc/localtime:ro \ + -v $docker_sock:$docker_sock \ + -dit $image_name + ;; + + Linux) + echo 'Docker on Linux start' + font_dir="/usr/share/fonts" + tz_path="/etc/timezone" + docker_daemon="/etc/docker/daemon.json" + docker_config="/etc/docker/config.json" + docker run \ + --cap-add=ALL \ + --name $container_name \ + --restart always \ + --privileged=$privileged \ + -p 1313:1313 \ + -p 6443:6550 \ + -u "$(id -u $USER):$(id -g $USER)" \ + -v $HOME/.docker:$container_home/.docker \ + -v $HOME/.docker:$container_root/.docker \ + -v $HOME/.gnupg:$container_home/.gnupg \ + -v $HOME/.gnupg:$container_root/.gnupg \ + -v $HOME/.kube:$container_home/.kube \ + -v $HOME/.netrc:$container_home/.netrc:ro \ + -v $HOME/.ssh:$container_home/.ssh \ + -v $HOME/.config:$container_home/.config:cached \ + -v $HOME/.p10k.zsh:$container_home/.p10k.zsh:cached \ + -v $HOME/.zsh_history:$container_home/.zsh_history:cached \ + -v $HOME/Documents:$container_home/Documents \ + -v $HOME/Downloads:$container_home/Downloads \ + -v $HOME/go/src:$container_goroot/src:cached \ + -v $docker_config:$docker_config:ro,cached \ + -v $docker_daemon:$docker_daemon:ro,cached \ + -v $docker_sock:$docker_sock \ + -v $font_dir:$font_dir \ + -v $rcpath/editorconfig:$container_home/.editorconfig \ + -v $rcpath/gitattributes:$container_home/.gitattributes \ + -v $rcpath/gitconfig:$container_home/.gitconfig \ + -v $rcpath/gitignore:$container_home/.gitignore \ + -v $HOME/.config/nvim:$container_home/.config/nvim \ + -v /usr/bin/sheldon:/usr/bin/sheldon:ro,cached \ + -v $HOME/.config/sheldon:$container_home/.config/sheldon:ro,cached \ + -v $rcpath/go.env:$container_goroot/go.env:ro \ + -v $rcpath/go.env:$goroot/go.env:ro \ + -v $rcpath/zshrc:$container_home/.zshrc:ro,cached \ + -v $tz_path:/etc/localtime:ro,cached \ + -dit $image_name + ;; + + CYGWIN*|MINGW32*|MSYS*) + echo 'MS Windows is not ready for this environment' + ;; + + *) + echo 'other OS' + ;; + esac + # docker exec -d $container_name $zpath nvup +} + +alias devrun="devrun" +alias devin="docker exec -it $container_name $zpath" + +function devkill { + docker update --restart=no $container_name \ + && docker container stop $(docker container ls -aq) \ + && docker container stop $(docker ps -a -q) \ + && docker ps -aq | xargs docker rm -f \ + && docker container prune -f +} + +alias devkill="devkill" + +alias devres="devkill && devrun" + +function fup { + sudo systemctl reload dbus.service + sudo systemctl restart fwupd.service + sudo lsusb +} + +TRAPUSR1() { + source ~/.zshrc + zle && zle reset-prompt +} + +eval "$(starship init zsh)" + +[ -f "$HOME/.zshrc.local" ] && source "$HOME/.zshrc.local" -export GPG_TTY=$TTY diff --git a/zshrc.local.template b/zshrc.local.template new file mode 100644 index 0000000..9b43d4f --- /dev/null +++ b/zshrc.local.template @@ -0,0 +1,20 @@ +# -------------------- +# node / volta (override) +# -------------------- +export VOLTA_HOME="$HOME/.volta" +export NODE_PATH="$HOME/.volta/bin" +export PATH="$VOLTA_HOME/bin:$PATH" + +export HELIX_RUNTIME=~/go/src/github.com/helix-editor/helix/runtime + +# >>> PyPI Proxy (optional) >>> +# export PIP_INDEX_URL="https://pypi.example.com/simple/" +# export UV_INDEX_URL="https://pypi.example.com/simple/" +# <<< PyPI Proxy (optional) <<< + +# AWS (optional) +# export AWS_DEFAULT_PROFILE="" + +# Google Cloud SDK +if [ -f "$HOME/google-cloud-sdk/path.zsh.inc" ]; then . "$HOME/google-cloud-sdk/path.zsh.inc"; fi +if [ -f "$HOME/google-cloud-sdk/completion.zsh.inc" ]; then . "$HOME/google-cloud-sdk/completion.zsh.inc"; fi