diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 000000000000..ac93612b1963 --- /dev/null +++ b/.claude/README.md @@ -0,0 +1,21 @@ +# `.claude/` + +Project-level Claude Code configuration for aztec-packages. This file documents layout decisions; the filesystem describes the contents. + +## Layout rules + +1. Universal content lives at repository root: `CLAUDE.md`, `.claude/agents/`, `.claude/scripts/`, and the root `settings.json` hooks. +2. Subdirectory `.claude/` directories hold only component-specific skills, permission allowlists, and settings. Content is not merged upward. +3. A subdirectory that has its own `.claude/` must symlink `agents/` to the repository root's `.claude/agents/`. Claude Code's ancestor walk stops at the nearest `.claude/` and does not merge ancestors, so subdirectories otherwise shadow the root agents. Subdirectories without their own `.claude/` inherit the root automatically. `skills/` is intentionally *not* symlinked — skills are scoped to their subdir (root skills are repo-wide workflows; `yarn-project/.claude/skills/` are TS-specific; etc.). +4. Prefer XML-tagged sections inside `CLAUDE.md` for prose guidance. A `.claude/rules/` directory is only warranted when a rule needs YAML frontmatter (e.g. path-scoped `paths:` metadata) that `CLAUDE.md` cannot express. `.claude/rules/` without frontmatter does not auto-load when Claude Code is started from a subdirectory, so plain rules files are strictly worse than inlining into `CLAUDE.md`. + +## Tests + +`tests/format_file_test` exercises each dispatch branch: hygiene (malformed hook input), C++, Rust, Solidity, TypeScript with plugin. Invocation via `./.claude/bootstrap.sh test_cmds` follows the same convention as `ci3/bootstrap.sh`. + +## Adding new files + +- A new agent used repository-wide: `.claude/agents/` at root. +- A new skill scoped to one component: `component/.claude/skills/`. +- A new hook: add a script to `.claude/scripts/`, register it in `.claude/settings.json` via `git rev-parse --show-toplevel` so the path resolves from any cwd, and add a test in `.claude/tests/`. You need to add it to each subdirectory where you want the hook active. +- A new `.claude/` subdirectory: symlink `agents/` back to the repository root's `.claude/agents/`. diff --git a/.claude/agents/retrospective.md b/.claude/agents/retrospective.md deleted file mode 100644 index a40479671606..000000000000 --- a/.claude/agents/retrospective.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -name: retrospective -description: | - Analyze the current session to extract learnings for self-improvement. Use at the end of a session to: - - Capture user corrections and guidelines that can be generalized - - Document failed approaches and what worked instead - - Identify permissions that should be pre-approved - - Extract patterns and conventions learned during the session - - Best used: At the end of a productive session, after debugging sessions where you learned something, or when the user taught you something new. - - - user: "Let's do a retrospective on this session" - assistant: "I'll analyze our conversation to extract learnings." - -model: opus -color: magenta ---- - -You are a Self-Improvement Analyst for Claude Code. Your mission is to analyze the current session transcript and extract learnings that can improve future interactions. - -## Core Responsibilities - -1. **Transcript Analysis** - - You have access to the full conversation history of this session through the current context - - Systematically review all exchanges between user and assistant - - Pay special attention to corrections, clarifications, and teaching moments - -2. **Pattern Recognition** - - Identify user corrections: "No, do X instead" or "That's not how we do it" - - Find failed attempts followed by successful retries - - Note style preferences: naming conventions, code patterns, formatting - - Recognize workflow shortcuts the user taught - - Spot permission requests that could be pre-approved - -3. **Location Discovery** - - Before suggesting where to add content, explore existing configuration: - * Read `.claude/rules/` for existing rules - * Read `.claude/skills/` for existing skills - * Read `CLAUDE.md` files for project instructions - * Read `.claude/settings.json` for existing permissions - * Read `.claude/agents/` for existing agents - - Suggest additions to existing files when the topic already has a home - - Only suggest new files for distinct, substantial topics - -4. **Suggestion Generation** - - Generate actionable suggestions with specific content - - Include rationale with references to session events - - Prioritize high-value learnings (frequent corrections > one-time fixes) - - Be concise—extract the essence, not verbose explanations - -## What to Look For - -### High-Priority Learnings - -1. **Direct Corrections** - - "No, you should..." / "Actually, we do it this way..." - - "Don't use X, use Y instead" - - Explanations of why something was wrong - -2. **Failed → Success Patterns** - - Command failed, then worked with different syntax - - Tool used incorrectly, then correctly - - Approach abandoned for better alternative - -3. **Project Conventions** - - File naming patterns - - Code organization rules - - Testing practices - - Build/deployment workflows - -4. **Permissions** - - Commands that required approval - - Patterns in approved commands (could become wildcards) - -### Lower-Priority (Include if Substantial) - -5. **Preferences** - - Formatting styles - - Communication preferences - - Tool preferences - -6. **Domain Knowledge** - - Project-specific terminology - - Architecture decisions - - Integration patterns - -## Output Format - -Present suggestions in this format: - ---- - -## Suggestion [N]: [Brief descriptive title] - -**Type**: `Rule` | `Skill` | `CLAUDE.md` | `Permission` | `Agent` -**File**: `[exact file path]` -**Action**: `Add` | `Modify` | `Create` - -**Rationale**: -[1-2 sentences explaining what happened in the session that led to this suggestion] - -**Proposed Content**: -``` -[Exact content to add, properly formatted for the target file] -``` - -**Location in file**: [Where to insert: "After section X" or "In permissions.allow array" or "New file"] - ---- - -## Workflow - -1. **Review Context**: Carefully read through the full session transcript -2. **Identify Learnings**: Note all potential improvements as you read -3. **Explore Config**: Use Glob and Read to examine existing Claude configuration -4. **Deduplicate**: Check if learnings are already documented -5. **Prioritize**: Focus on generalizable, high-impact learnings -6. **Format**: Present suggestions in the structured format above -7. **Await Approval**: Present all suggestions and wait for user to accept/reject each -8. **Implement**: After approval, use Edit tool to make the changes - -## Key Principles - -- **Be selective**: Not every correction needs documentation—focus on generalizable learnings -- **Be precise**: Include exact file paths and content, ready to apply -- **Be concise**: Extract the essence, don't pad with unnecessary explanation -- **Respect existing structure**: Add to existing files when appropriate -- **Provide context**: Always explain WHY this should be added (reference session events) - -## Example Suggestions - -### Example 1: Rule Addition - -```markdown -## Suggestion 1: Always use yarn workspace for tests - -**Type**: `Rule` -**File**: `/path/to/yarn-project/.claude/rules/testing.md` -**Action**: `Modify` (or `Create` if file doesn't exist) - -**Rationale**: -User corrected me twice when I tried to cd into a package and run `yarn test`. They explained that `yarn workspace` is preferred. - -**Proposed Content**: -## Test Execution - -Always use `yarn workspace` to run tests rather than changing directories: - -\`\`\`bash -# Good -yarn workspace @aztec/archiver test src/archiver.test.ts - -# Bad - don't do this -cd archiver && yarn test src/archiver.test.ts -\`\`\` - -**Location in file**: After "## Test Logging" section (or as new file) -``` - -### Example 2: Permission Addition - -```markdown -## Suggestion 2: Pre-approve wc command - -**Type**: `Permission` -**File**: `/path/to/yarn-project/.claude/settings.json` -**Action**: `Modify` - -**Rationale**: -I requested permission to run `wc -l` three times during log analysis. This is a safe, read-only command. - -**Proposed Content**: -"Bash(wc:*)" - -**Location in file**: Add to `permissions.allow` array -``` - -Remember: Your goal is to help Claude Code get better over time by capturing the wisdom from each session. Focus on learnings that will help in future, similar situations. diff --git a/.claude/bootstrap.sh b/.claude/bootstrap.sh new file mode 100755 index 000000000000..021f5270f3fb --- /dev/null +++ b/.claude/bootstrap.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Bootstrap + test entry for the .claude/ tooling directory. Mirrors the shape +# used by ci3/bootstrap.sh: emits test commands via `test_cmds`, runs them via +# `test`. Keeps hook scripts and their tests as a self-contained component. +source $(git rev-parse --show-toplevel)/ci3/source_bootstrap + +hash=$(cache_content_hash ^.claude) + +function test_cmds { + # source_base cd's us into .claude/, so glob relative-to-here, but emit paths + # relative to the git root (same convention used by ci3/bootstrap.sh). + for f in tests/*; do + [[ -x "$f" ]] || continue + echo "$hash ./.claude/$f" + done +} + +function test { + echo_header ".claude tests" + test_cmds | filter_test_cmds | parallelize +} + +case "$cmd" in + "") + test + ;; + *) + default_cmd_handler "$@" + ;; +esac diff --git a/.claude/scripts/format-file.sh b/.claude/scripts/format-file.sh new file mode 100755 index 000000000000..37ff1b21519f --- /dev/null +++ b/.claude/scripts/format-file.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# PostToolUse hook: format files after Edit/Write by dispatching to the right +# formatter based on extension. Reads Claude Code's hook JSON from stdin. +# +# Never fails the edit — prints hints to stderr on missing tools and exits 0. + +set -u + +input=$(cat) +file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty') + +[[ -z "$file" ]] && exit 0 +[[ ! -f "$file" ]] && exit 0 + +hint() { printf 'format-file.sh: %s\n' "$*" >&2; } + +case "$file" in + *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.json) + root="" + if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]] && [[ -f "$CLAUDE_PROJECT_DIR/yarn-project/package.json" ]]; then + root="$CLAUDE_PROJECT_DIR" + else + root=$(git -C "$(dirname "$file")" rev-parse --show-toplevel 2>/dev/null || true) + fi + if [[ -n "$root" && -x "$root/yarn-project/node_modules/.bin/prettier" ]]; then + "$root/yarn-project/node_modules/.bin/prettier" --write --log-level=warn "$file" \ + || hint "prettier failed on $file" + else + hint "prettier not found — run yarn-project bootstrap to enable format-on-edit" + fi + ;; + *.cpp|*.cxx|*.cc|*.hpp|*.hxx|*.h) + cf="" + if command -v clang-format-20 >/dev/null 2>&1; then + cf=clang-format-20 + elif [[ -x /opt/homebrew/opt/llvm/bin/clang-format ]] && /opt/homebrew/opt/llvm/bin/clang-format --version 2>/dev/null | grep -q 'version 20'; then + cf=/opt/homebrew/opt/llvm/bin/clang-format + elif [[ -x /usr/local/opt/llvm/bin/clang-format ]] && /usr/local/opt/llvm/bin/clang-format --version 2>/dev/null | grep -q 'version 20'; then + cf=/usr/local/opt/llvm/bin/clang-format + fi + if [[ -n "$cf" ]]; then + "$cf" -i "$file" || hint "$cf failed on $file" + else + hint "clang-format 20 not found — install via 'apt install clang-format-20' (Linux) or 'brew install llvm' (macOS)" + fi + ;; + *.rs) + # rustfmt walks up from the file path to find .rustfmt.toml, which pins + # edition and style. Don't pass --edition here; it would override that. + if command -v rustfmt >/dev/null 2>&1; then + rustfmt "$file" || hint "rustfmt failed on $file" + else + hint "rustfmt not found — install via 'rustup component add rustfmt'" + fi + ;; + *.sol) + if command -v forge >/dev/null 2>&1; then + (cd "$(dirname "$file")" && forge fmt "$file") || hint "forge fmt failed on $file" + else + hint "forge not found — install foundry via 'curl -L https://foundry.paradigm.xyz | bash && foundryup'" + fi + ;; + *.nr) + # nargo fmt operates on whole crates, not individual files. + hint "nargo fmt is crate-scoped — run 'nargo fmt' from the noir project directory before committing" + ;; +esac + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json index 678f0a41a0d0..1cbb77f766c4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,6 +10,17 @@ } ] } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash -c 'GITROOT=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0; [ -n \"$GITROOT\" ] && exec \"$GITROOT/.claude/scripts/format-file.sh\"; exit 0'" + } + ] + } ] } } diff --git a/.claude/tests/agents_symlink_test b/.claude/tests/agents_symlink_test new file mode 100755 index 000000000000..3c0ef5f26540 --- /dev/null +++ b/.claude/tests/agents_symlink_test @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Regression test for the layout rule: every subdir that owns its own .claude/ +# must symlink agents/ to the repository root's .claude/agents/. Without the +# symlink, Claude Code's upward-walk stops at the subdir .claude/ and silently +# shadows every root agent. +# +# Only agents/ is checked — not skills/. Skills are intentionally per-subdir +# (the root has repo-wide workflow skills, yarn-project has TS-specific ones, +# etc.), and letting a subdir shadow root skills/ is the designed behavior. +# Agents, by contrast, are universal and need to be visible from every cwd. +# +# Runs from any cwd. + +set -uo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P) + +RED=$'\033[0;31m' +GREEN=$'\033[0;32m' +NC=$'\033[0m' + +FAIL=0 +CHECKED=0 + +fail() { echo "${RED}✗${NC} $1"; ((FAIL++)) || true; } +pass() { echo "${GREEN}✓${NC} $1"; ((CHECKED++)) || true; } + +ROOT_AGENTS="$ROOT/.claude/agents" +[[ -d "$ROOT_AGENTS" ]] || { fail "root .claude/agents missing at $ROOT_AGENTS"; exit 1; } + +# Every subdir .claude/ (excluding the root one) must expose an agents entry +# that resolves to the same inode as the root agents/ directory. +while IFS= read -r dir; do + [[ "$dir" == "$ROOT/.claude" ]] && continue + case "$dir" in + "$ROOT"/node_modules/*|*"/node_modules/"*) continue;; + "$ROOT"/noir/noir-repo/*) continue;; + esac + agents="$dir/agents" + if [[ ! -e "$agents" ]]; then + # Build the relative target (e.g. ../../.claude/agents) with POSIX tools + # so the hint is correct on macOS + Linux alike. + rel_root=${dir#$ROOT/} + depth=$(awk -F/ '{print NF}' <<<"$rel_root") + up=$(printf '../%.0s' $(seq 1 "$depth")) + fail "${dir#$ROOT/} has no agents/ — add symlink: (cd ${dir#$ROOT/} && ln -s ${up}.claude/agents agents)" + continue + fi + if [[ ! -L "$agents" ]]; then + fail "${dir#$ROOT/}/agents is not a symlink (must point at root .claude/agents/)" + continue + fi + # Resolve the symlink target and confirm it matches root agents. + resolved=$(cd "$(dirname "$agents")" && cd "$(readlink "$agents")" 2>/dev/null && pwd -P) || resolved="" + if [[ "$resolved" != "$ROOT_AGENTS" ]]; then + fail "${dir#$ROOT/}/agents resolves to $resolved, expected $ROOT_AGENTS" + continue + fi + pass "${dir#$ROOT/}/agents" +done < <(find "$ROOT" -type d -name .claude -not -path "$ROOT/noir/*" -not -path "$ROOT/**/node_modules/*") + +echo +if (( FAIL == 0 )); then + echo "${GREEN}All $CHECKED subdir .claude/ directories expose agents/ correctly.${NC}" + exit 0 +else + echo "${RED}$FAIL subdir .claude/ directories are missing or have broken agents symlinks.${NC}" + exit 1 +fi diff --git a/.claude/tests/format_file_test b/.claude/tests/format_file_test new file mode 100755 index 000000000000..687e3ad393a3 --- /dev/null +++ b/.claude/tests/format_file_test @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +# Test suite for .claude/scripts/format-file.sh. Exercises every language +# dispatch branch plus the TS fallback-install path. Mirrors ci3/tests/ style. +set -uo pipefail + +# Resolve ROOT from the script's own location so the suite runs from any cwd. +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P) +SCRIPT="$ROOT/.claude/scripts/format-file.sh" + +# Tests write temp files to /tmp which is outside the repo, so export +# CLAUDE_PROJECT_DIR to give format-file.sh a known root without relying on +# upward git walks from the edited file's directory. +export CLAUDE_PROJECT_DIR="$ROOT" + +RED=$'\033[0;31m' +GREEN=$'\033[0;32m' +NC=$'\033[0m' + +PASS=0 +FAIL=0 + +pass() { echo "${GREEN}✓${NC} $1"; ((PASS++)) || true; } +fail() { + echo "${RED}✗${NC} $1" + [[ $# -ge 2 ]] && echo " $2" + if [[ -s /tmp/format_test_stderr ]]; then + sed 's/^/ stderr: /' /tmp/format_test_stderr + fi + ((FAIL++)) || true +} + +# run_hook : invoke format-file.sh with a hook-shaped stdin payload. +run_hook() { + local f=$1 + printf '{"tool_input":{"file_path":"%s"}}' "$f" | "$SCRIPT" 2>/tmp/format_test_stderr +} + +# expect_formatted