diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e70da65 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# AGENTS.md + +## Repository purpose + +This is a chezmoi source repository for cross-platform personal configuration. +The `home/` tree maps to the target home directory; `.tmpl` files use strict Go +templates with `missingkey=error`. + +## Required checks + +- Run `make lint` after changing shell scripts. +- Run `make codex-check` after changing Codex, MCP, hook, or skill-sync files. +- Run `make test` for broader shell and configuration changes. +- Preview home-directory effects with `chezmoi diff` before applying them. + +## Implementation rules + +- Shell uses Bash, `set -euo pipefail`, quoted variables, `[[ ]]`, and `printf`. +- Chezmoi templates use `.chezmoi.homeDir` for absolute home paths and trim + conditional whitespace. +- Add every referenced template flag to both `chezmoi.toml` and + `.chezmoi.toml.tmpl` before using it. +- Preserve application-owned state. Use merge or idempotent CLI configuration + instead of overwriting files that Codex, Claude Code, Zed, or gh also edit. +- Do not expose secrets in templates, tests, logs, or generated examples. + +## Agent host boundaries + +- `~/.agents/skills` is the merged skill view shared by Codex, Claude Code, and Raxol. +- `~/.agents/skills-upstream` and `~/.agents/skills-extra` are source roots, not host-facing inventories. +- Codex configuration belongs under `~/.codex`; Claude Code configuration belongs under `~/.claude`. +- Keep MCP enablement flags aligned between the Claude and Codex renderers. +- Non-managed Codex hooks require explicit user review and trust; never bypass it automatically. diff --git a/CLAUDE.md b/CLAUDE.md index a86b14a..90c12ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,8 @@ make doctor # Health check across tools, config, security make perf # 5-run shell startup benchmark make perf-report # Bare vs configured startup breakdown make test # Test suite (zsh syntax, module loading) +make codex-diff # Preview only Codex-owned target changes +make codex-apply # Apply Codex target from this checkout only make brew-install # Install Brewfile packages make brew-dump # Update Brewfile from current system @@ -66,7 +68,12 @@ Use in templates: `{{- if .rust -}}...{{- end -}}`. Use `{{- -}}` to trim whites - `run_onchange_after_brew-bundle.sh.tmpl` -- runs `brew bundle install` when Brewfile hash changes - `run_onchange_after_mise-install.sh.tmpl` -- runs `mise install` when mise config changes - `run_onchange_after_reload-hammerspoon.sh.tmpl` -- reloads Hammerspoon on config change (macOS) -- `run_after_sync-skills.sh.tmpl` -- symlinks skills from `~/.agents/skills/` to `~/.claude/skills/` (always runs; script is idempotent) +- `run_after_sync-skills.sh.tmpl` -- merges upstream and vendored skills into `~/.agents/skills/`, then mirrors that inventory into `~/.claude/skills/` +- `run_onchange_after_setup-codex.sh.tmpl` -- registers enabled MCP servers through the Codex CLI without replacing app-owned config + +For Codex-only rollout, use `make codex-diff` followed by `make codex-apply`. +These targets bind `--source` to this checkout and select only Codex-owned +targets; do not use an unscoped `chezmoi apply` for that rollout. **Files the app also writes (`modify_` scripts)**: a plain managed file is authoritative, so `chezmoi apply` deletes any key the app added at runtime. That is the recurring `MM` in `chezmoi status`. When both chezmoi and an application own parts of a file, use a `modify_` source instead: chezmoi pipes the **current target** to the script on stdin and takes its stdout as the new target, so it can merge rather than overwrite. @@ -183,17 +190,18 @@ Local skills (`ethskills/`, `solidity-auditor/`, `noir/`) provide offline Ethere | Standards | `ethskills.com/standards/SKILL.md` | ERC-8004, EIP-7702, token standards | | Gas | `ethskills.com/gas/SKILL.md` | Current costs (mainnet ~$0.002, L2 ~$0.002) | -**Blockscout MCP**: Configured in `~/.mcp.json`. Provides type-safe blockchain data queries (balances, tokens, NFTs, contracts) across multiple chains via Model Context Protocol. +**Blockscout MCP**: Configured for Claude Code in `~/.mcp.json` and for Codex in `~/.codex/config.toml`. Provides type-safe blockchain data queries across multiple chains. ## MCP Servers -Managed via `~/.mcp.json` (chezmoi template: `home/dot_mcp.json.tmpl`). Toggle in `chezmoi.toml`, then `chezmoi apply`. +Claude Code uses `~/.mcp.json` (`home/dot_mcp.json.tmpl`). Codex entries are registered idempotently by `home/run_onchange_after_setup-codex.sh.tmpl`, preserving Codex-owned model and project settings. Toggle either host's inventory through the same flags in `chezmoi.toml`, then run `chezmoi apply`. | Server | Flag | Transport | Notes | | ------------ | -------------- | ---------- | ------------------------------------------------------------------------------------------------- | | context7 | always on | stdio | Library docs via npx | | blockscout | always on | http | Blockchain data queries | | coingecko | `coingecko` | http | Crypto market data | +| scribe | any agent flag | stdio | Session insight extractor shared by agent workflows | | digest | `digest` | stdio | Multi-platform activity digest | | recall | `recall` | stdio | Knowledge capture/retrieval (FTS5) | | autoresearch | `autoresearch` | stdio | Autonomous experiment runner | @@ -222,28 +230,30 @@ Agent MCP servers (coingecko through patchbot) all share the same ` serv ## Agent Skills -Skills are portable `SKILL.md` files sourced from [DROOdotFOO/agent-skills](https://github.com/DROOdotFOO/agent-skills), pulled via `home/.chezmoiexternal.toml` on `chezmoi apply` (refresh window 168h; force with `--refresh-externals`) to `~/.agents/skills/`. Two hosts load the same files: +Skills are portable `SKILL.md` files sourced from [DROOdotFOO/agent-skills](https://github.com/DROOdotFOO/agent-skills). Chezmoi unpacks the archive into `~/.agents/skills-upstream/`; `run_after_sync-skills.sh.tmpl` merges it with vendored extras into the host-facing `~/.agents/skills/` view. Three hosts load the same files: - **Raxol agent (primary host).** `Raxol.Agent.Skills.Store` scans `~/.agents/skills/` and `~/.agents/skills-extra/` for `**/SKILL.md` and holds them as read-only procedural memory, reached by the agent via the `skills_list` / `skill_view` / `skill_manage` tools. Enabled by `config :raxol_agent, skills_provider: Raxol.Agent.Skills.Store` in the raxol repo (`packages/raxol_agent/config/config.exs`); external dirs are set alongside it. -- **Claude Code (secondary host).** `run_after_sync-skills.sh.tmpl` symlinks `~/.agents/skills/*` and `~/.agents/skills-extra/*` into `~/.claude/skills/*`, where Claude Code auto-injects a skill when its trigger clause matches the conversation. +- **Codex.** Reads the merged `~/.agents/skills/` view directly and loads `~/.codex/AGENTS.md` plus repository AGENTS.md guidance. +- **Claude Code.** Receives symlinks to the same merged view under `~/.claude/skills/`. **Skills roots:** -- `~/.agents/skills/` -- the agent-skills collection (chezmoi external, read-only). To add/port a skill, add it under `skills/` in the [agent-skills](https://github.com/DROOdotFOO/agent-skills) repo, push to `main`, then `chezmoi apply --refresh-externals`. -- `~/.agents/skills-extra/` -- chezmoi-vendored third-party skills the external does not manage (source: `home/dot_agents/skills-extra/`). +- `~/.agents/skills/` -- symlink-only merged view consumed by all hosts. +- `~/.agents/skills-upstream/` -- read-only agent-skills archive external. +- `~/.agents/skills-extra/` -- chezmoi-vendored third-party source root. - `~/.raxol/skills/` -- writable managed root for **agent-authored** skills only (the raxol curation loop writes here). Runtime state, left unmanaged by chezmoi. Human and vendored skills come from the chezmoi-managed externals above, never here. -**Name clashes: `skills-extra/` wins in both hosts.** chezmoi never prunes an archive external, so `~/.agents/skills/` can keep serving a stale copy of a skill that has since moved to `skills-extra/`. Raxol resolves this via `skills_external_dirs: ["~/.agents/skills", "~/.agents/skills-extra"]` -- `Skills.Store` scans in order and later `:ets.insert` calls overwrite earlier ones, so the **last** root wins. `run_after_sync-skills.sh` links the **first** root, so its `SKILL_ROOTS` is ordered `skills-extra` then `skills` to reach the same answer. Changing either without the other silently desyncs the two hosts. +**Name clashes: `skills-extra/` wins.** The merger rebuilds symlinks in source priority order, so Codex and Claude Code resolve the same file. Raxol's later `skills-extra` scan resolves to that same preferred copy. -One further asymmetry: Raxol globs `**/SKILL.md` (any depth) while the sync script only looks one level down. No skill is nested today, so both index the same set -- but a nested `SKILL.md` would appear in Raxol and not in Claude Code. +The merger intentionally supports one skill directory level, matching Codex and Claude Code discovery. Do not nest skill entry points. -**Skill accounting** (re-derive with `find -L ~/.agents/skills ~/.agents/skills-extra -name SKILL.md`, don't trust this prose): +**Skill accounting** (re-derive with `find -L ~/.agents/skills -name SKILL.md`): | Source | Count | Notes | | -------------------------- | ------ | ----------------------------------------------------------------------------------- | -| agent-skills `skills/` | 59 | upstream also ships one empty placeholder dir with no `SKILL.md`, which never loads | -| `skills-extra/` (vendored) | 2 | `virtuals-protocol-acp`, `hf-cli` (the latter originally installed by the `hf` CLI) | -| **loaded by Claude Code** | **61** | symlinks in `~/.claude/skills/` | +| `skills-upstream/` | 59 | the optional git submodule is absent from source archives | +| `skills-extra/` (vendored) | 2 | `virtuals-protocol-acp`, `hf-cli` | +| **merged host inventory** | **61** | loaded by Codex, Claude Code, and Raxol | **Code pattern skills** -- language-specific examples and idioms: diff --git a/Makefile b/Makefile index d87c598..37909b2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install update diff status backup clean doctor bootstrap sync sync-from-remote backup-full install-optional generate-template setup-age age-retrieve age-status setup-mise mise-status mise-upgrade mise-doctor setup-raycast raycast-export raycast-import raycast-status setup-emacs emacs-grammars emacs-restart emacs-status setup-signoz-mcp signoz-mcp-status signoz-mcp-update lint perf perf-report skills-status +.PHONY: help install update diff status backup clean doctor bootstrap sync sync-from-remote backup-full install-optional generate-template setup-age age-retrieve age-status setup-mise mise-status mise-upgrade mise-doctor setup-raycast raycast-export raycast-import raycast-status setup-emacs emacs-grammars emacs-restart emacs-status setup-signoz-mcp signoz-mcp-status signoz-mcp-update lint test codex-check codex-diff codex-apply perf perf-report skills-status # Configuration DOTFILES_ROOT := $(shell pwd) @@ -74,6 +74,15 @@ test: ## Run comprehensive dotfiles test suite @echo "Running Dotfiles Test Suite..." @$(SCRIPTS_DIR)/utils/test-suite.sh +codex-check: ## Validate Codex skills, MCP, instructions, and hooks + @$(SCRIPTS_DIR)/utils/test-codex-target.sh + +codex-diff: ## Preview only the Codex-owned target files + @$(SCRIPTS_DIR)/utils/apply-codex-target.sh --dry-run + +codex-apply: ## Apply only Codex skills, instructions, hooks, and MCP servers + @$(SCRIPTS_DIR)/utils/apply-codex-target.sh + perf: ## Benchmark shell startup time @$(SCRIPTS_DIR)/utils/performance-monitor.sh measure @@ -221,15 +230,15 @@ brew-update: ## Update Homebrew and all packages setup-ci: ## Setup CI/CD tools and pre-commit hooks @$(SCRIPTS_DIR)/setup/setup-ci.sh -# Claude Code skills (source: github.com/DROOdotFOO/agent-skills) -skills-status: ## Show installed Claude Code skills - @echo "Skills (pinned version):" +# Shared agent skills (source: github.com/DROOdotFOO/agent-skills) +skills-status: ## Show installed Codex, Claude Code, and Raxol skills + @echo "Skills (upstream version):" @grep -o 'v[0-9.]*' home/.chezmoiexternal.toml 2>/dev/null || echo " (unknown)" @echo "" - @echo "Skills (deployed):" + @echo "Skills (merged for Codex/Raxol):" @ls -1 ~/.agents/skills/ 2>/dev/null || echo " (none)" @echo "" - @echo "Skills (symlinked):" + @echo "Skills (mirrored for Claude Code):" @ls -la ~/.claude/skills/ 2>/dev/null || echo " (none)" # Template generation diff --git a/README.md b/README.md index cc0f3b6..2d73e27 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ brew install chezmoi && chezmoi init --apply https://github.com/DROOdotFOO/dotfi | **System** | btop, fastfetch, tldr (`help`) | | **Windows** | Hammerspoon + PaperWM (macOS) | | **Launcher** | Raycast (macOS) | -| **AI** | Claude Code (61 skills, 14 MCP servers) | +| **AI** | Codex, Claude Code, Raxol (61 shared skills, 15 MCP servers) | | **Secrets** | 1Password (SSH agent + age encryption), AWS CLI, Infisical | | **Network** | Tailscale with pre-configured hosts | | **Languages** | Elixir, Rust, Go, Python, Node.js, Lua (via mise) | @@ -135,8 +135,11 @@ make dashboard # Service status overview make rotate-keys # Generate, store in 1Password, sync to hosts make sync-keys # Push public key to Tailscale nodes -# Claude Code +# Agent hosts make skills-status # Show installed AI coding skills +make codex-check # Validate Codex skills, MCP, instructions, and hooks +make codex-diff # Preview only Codex-owned target changes +make codex-apply # Apply Codex target without reconciling unrelated dotfiles ``` ## Secrets @@ -193,13 +196,8 @@ dotfiles/ │ │ ├── fastfetch/ # System info │ │ ├── starship/ # Prompt │ │ └── direnv/ # direnv layouts -│ ├── dot_agents/skills/ # Claude Code skills (auto-symlinked) -│ │ ├── claude-api/ # Anthropic SDK reference -│ │ ├── droo-stack/ # Polyglot patterns (Elixir/TS/Go/Rust/Py/Lua/Noir/Shell) -│ │ ├── noir/ # ZK circuits, Aztec contracts/security/e2e testing -│ │ ├── solidity-auditor/ # Solidity dev + security auditing -│ │ ├── ethskills/ # Ethereum tooling + EIP/ERC reference -│ │ └── raxol/ # Raxol TUI/agent framework +│ ├── dot_agents/skills-extra/ # Vendored skills merged with upstream +│ ├── private_dot_codex/ # Codex guidance + hooks │ └── private_dot_claude/ # Claude Code config + hooks ├── config/ │ ├── raycast/ # Raycast settings diff --git a/home/.chezmoiexternal.toml b/home/.chezmoiexternal.toml index 0d3a9c0..40a6c03 100644 --- a/home/.chezmoiexternal.toml +++ b/home/.chezmoiexternal.toml @@ -1,14 +1,10 @@ -[".agents/skills"] +[".agents/skills-upstream"] type = "archive" url = "https://github.com/DROOdotFOO/agent-skills/archive/main.tar.gz" + exact = true stripComponents = 2 include = ["*/skills/**"] refreshPeriod = "168h" - # NOTE: `include` correctly limits extraction to upstream skills/, but chezmoi - # never PRUNES an archive external -- entries written by an earlier refresh - # survive after they disappear upstream. Earlier layouts had more top-level - # dirs, and with stripComponents = 2 their children landed here (agents/scribe - # -> scribe, .github/workflows -> workflows, benchmarks/suites -> suites, ...). - # Those leftovers are inert: run_after_sync-skills.sh only links dirs holding a - # SKILL.md. If this directory accumulates non-skill entries again, delete them - # by hand -- a refresh will not. + # Keep this exact source root separate from the host-facing + # ~/.agents/skills view. Chezmoi prunes files removed from the archive, then + # run_after_sync-skills.sh safely rebuilds the merged symlink inventory. diff --git a/home/dot_gitconfig.tmpl b/home/dot_gitconfig.tmpl index 650ad32..52e1955 100644 --- a/home/dot_gitconfig.tmpl +++ b/home/dot_gitconfig.tmpl @@ -1,9 +1,7 @@ [user] name = {{ .name }} email = {{ .email }} -{{- if .gpg_signing_key }} - signingkey = {{ .gpg_signing_key }} -{{- end }} +{{ "\t" }}signingkey = key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPUSQxidN3mlV514FqrtPyridWtUoa6IxCXHMPdQPF9H [core] editor = zed --wait @@ -39,9 +37,10 @@ [gpg] program = gpg +{{ "\t" }}format = ssh [commit] - gpgsign = false +{{ "\t" }}gpgsign = true [push] default = current @@ -157,3 +156,5 @@ [credential "https://gist.github.com"] helper ={{ " " }} helper = !{{ .brewPrefix }}/bin/gh auth git-credential +[gpg "ssh"] +{{ "\t" }}allowedSignersFile = {{ .chezmoi.homeDir }}/.config/git/allowed_signers diff --git a/home/dot_mcp.json.tmpl b/home/dot_mcp.json.tmpl index 122d5dd..2c17f0d 100644 --- a/home/dot_mcp.json.tmpl +++ b/home/dot_mcp.json.tmpl @@ -20,6 +20,12 @@ "args": ["serve"] } {{- end }} +{{- if or .digest .recall .autoresearch .watchdog .prepper .sentinel .patchbot .regen }}, + "scribe": { + "command": "scribe", + "args": ["serve"] + } +{{- end }} {{- if .recall }}, "recall": { "command": "recall", diff --git a/home/dot_zshenv b/home/dot_zshenv index 4d48ab4..a303482 100644 --- a/home/dot_zshenv +++ b/home/dot_zshenv @@ -1 +1,3 @@ [[ -f "$HOME/.cargo/env" ]] && . "$HOME/.cargo/env" + +export PATH="$PATH:/Users/droo/.foundry/bin" diff --git a/home/dot_zshrc.tmpl b/home/dot_zshrc.tmpl index 90c4402..45f35f5 100644 --- a/home/dot_zshrc.tmpl +++ b/home/dot_zshrc.tmpl @@ -30,3 +30,11 @@ if [[ $- == *i* ]] && [[ ${SHLVL:-1} -le 2 ]]; then precmd_functions+=(_fastfetch_precmd) fi fi + +# grok +export PATH=/Users/droo/.grok/bin:$PATH +export PATH="$HOME/.aztec/current/bin:$HOME/.aztec/current/node_modules/.bin:$HOME/.aztec/bin:$PATH" + +export NARGO_HOME="/Users/droo/.nargo" + +export PATH="$PATH:$NARGO_HOME/bin" diff --git a/home/private_dot_claude/CLAUDE.md.tmpl b/home/private_dot_claude/CLAUDE.md.tmpl index 77f1b2e..e5b9a72 100644 --- a/home/private_dot_claude/CLAUDE.md.tmpl +++ b/home/private_dot_claude/CLAUDE.md.tmpl @@ -119,7 +119,7 @@ Superpowers skills (if installed): ## Agent Skills -Skills sourced from [DROOdotFOO/agent-skills](https://github.com/DROOdotFOO/agent-skills), installed to `~/.agents/skills/`. The **Raxol agent** is the primary host (`Raxol.Agent.Skills.Store` loads them as procedural memory, reached via the `skills_list`/`skill_view`/`skill_manage` tools); **Claude Code** is the secondary host (symlinked into `~/.claude/skills/`, auto-loaded when a skill's trigger matches): +Skills sourced from [DROOdotFOO/agent-skills](https://github.com/DROOdotFOO/agent-skills) and merged with vendored extras under `~/.agents/skills/`. **Raxol** reads that shared inventory as procedural memory, **Codex** discovers it directly, and **Claude Code** receives matching symlinks under `~/.claude/skills/`: - `claude-api` -- Anthropic SDK reference (Python, TS, Go, Elixir, Rust, Lua, cURL) - `droo-stack` -- Polyglot patterns (Elixir, TS, Go, Rust, Python, Lua, Shell, Noir, Chezmoi) - `noir` -- ZK circuit design, Aztec contracts, constraint optimization, nargo testing diff --git a/home/private_dot_codex/AGENTS.md.tmpl b/home/private_dot_codex/AGENTS.md.tmpl new file mode 100644 index 0000000..14b5d4c --- /dev/null +++ b/home/private_dot_codex/AGENTS.md.tmpl @@ -0,0 +1,8 @@ +# Global Codex working agreements + +- Preserve uncommitted user changes and avoid destructive git operations. +- Ask before adding production dependencies or expanding the requested scope. +- Prefer `rg` for repository search and existing project commands for verification. +- Run focused tests after changes, then the relevant broader suite when practical. +- Never read or print credentials from SSH, cloud, package-manager, or keychain files. +- Follow the closest repository AGENTS.md when its guidance is more specific. diff --git a/home/private_dot_codex/hooks.json.tmpl b/home/private_dot_codex/hooks.json.tmpl new file mode 100644 index 0000000..b86370a --- /dev/null +++ b/home/private_dot_codex/hooks.json.tmpl @@ -0,0 +1,35 @@ +{ + "description": "Personal Codex safety and project-context hooks.", + "hooks": { + "PreToolUse": [ + { + "matcher": "^Bash$", + "hooks": [ + { + "type": "command", + "command": {{ printf "%s/.claude/hooks/block-rm-rf.sh" .chezmoi.homeDir | toJson }}, + "statusMessage": "Checking destructive file operations" + }, + { + "type": "command", + "command": {{ printf "%s/.claude/hooks/block-destructive-git.sh" .chezmoi.homeDir | toJson }}, + "statusMessage": "Checking destructive git operations" + } + ] + } + ]{{- if .prepper }}, + "SessionStart": [ + { + "matcher": "startup|resume", + "hooks": [ + { + "type": "command", + "command": {{ printf "%s/CODE/agent-skills/scripts/hooks/prepper-session-start.sh" .chezmoi.homeDir | toJson }}, + "timeout": 30, + "statusMessage": "Loading project briefing" + } + ] + } + ]{{- end }} + } +} diff --git a/home/run_after_sync-skills.sh.tmpl b/home/run_after_sync-skills.sh.tmpl index 522af1c..4cb2649 100644 --- a/home/run_after_sync-skills.sh.tmpl +++ b/home/run_after_sync-skills.sh.tmpl @@ -1,70 +1,101 @@ -#!/bin/bash -# Sync skills: ~/.agents/skills/* and ~/.agents/skills-extra/* -> ~/.claude/skills/* (symlinks). -# skills/ comes from the agent-skills tarball (chezmoi external); skills-extra/ holds vendored -# third-party skills (e.g. virtuals-protocol-acp) the external does not manage. -# Runs on every apply (script is idempotent; previous run_onchange_ trigger keyed -# off .chezmoiexternal.toml hash and missed external content refreshes). +#!/usr/bin/env bash +# Build one merged skill view for Codex, Claude Code, and Raxol. set -euo pipefail -# skills-extra first: it is chezmoi-managed, whereas skills/ is an unpacked -# archive that chezmoi never prunes, so it can retain a stale copy of a skill -# that has since moved to skills-extra. Raxol resolves the same clash the same -# way (its Skills.Store scans skills_external_dirs in order and later inserts -# overwrite earlier ones, so its last entry wins), and both hosts must agree. -SKILL_ROOTS=("$HOME/.agents/skills-extra" "$HOME/.agents/skills") +SKILL_SOURCE_ROOTS=("$HOME/.agents/skills-extra" "$HOME/.agents/skills-upstream") +MERGED_SKILLS_DIR="$HOME/.agents/skills" CLAUDE_SKILLS_DIR="$HOME/.claude/skills" +LEGACY_BACKUP_DIR="$HOME/.agents/skills-before-codex-merge" -# Nothing to do until at least one source root exists. -have_root=0 -for root in "${SKILL_ROOTS[@]}"; do - [[ -d "$root" ]] && have_root=1 -done -[[ "$have_root" -eq 1 ]] || exit 0 - -mkdir -p "$CLAUDE_SKILLS_DIR" - -# A dir is a skill only if it holds a SKILL.md. Upstream skills/ also carries -# non-skill dirs (support packages, hooks, a submodule placeholder) that must not -# reach Claude Code; both hosts key off SKILL.md, so this is the selection point. is_skill_dir() { [[ -f "$1/SKILL.md" ]] } -# True if a name resolves to a real skill (SKILL.md present) in any source root. skill_exists() { local name="$1" root - for root in "${SKILL_ROOTS[@]}"; do + for root in "${SKILL_SOURCE_ROOTS[@]}"; do is_skill_dir "$root/$name" && return 0 done return 1 } -# Remove broken symlinks -find "$CLAUDE_SKILLS_DIR" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true +have_source=0 +for root in "${SKILL_SOURCE_ROOTS[@]}"; do + [[ -d "$root" ]] && have_source=1 +done +[[ "$have_source" -eq 1 ]] || exit 0 + +# Older revisions unpacked the archive directly into ~/.agents/skills. Preserve +# that tree once, then replace it with a symlink-only merged view. +needs_migration=0 +if [[ -d "$MERGED_SKILLS_DIR" ]]; then + for entry in "$MERGED_SKILLS_DIR"/*; do + [[ -e "$entry" || -L "$entry" ]] || continue + if [[ ! -L "$entry" ]]; then + needs_migration=1 + break + fi + done +fi -# Remove stale symlinks (name no longer resolves to a real skill in any root -- -# covers deleted skills and dirs that are no longer skills, e.g. lost SKILL.md) -for link in "$CLAUDE_SKILLS_DIR"/*/; do - [[ -L "${link%/}" ]] || continue - link_name="$(basename "$link")" - if ! skill_exists "$link_name"; then - rm -f "$CLAUDE_SKILLS_DIR/$link_name" - echo "[chezmoi] Removed stale skill link: $link_name" +if [[ "$needs_migration" -eq 1 ]]; then + if [[ -e "$LEGACY_BACKUP_DIR" || -L "$LEGACY_BACKUP_DIR" ]]; then + printf '[chezmoi] Refusing to replace %s: backup already exists at %s\n' \ + "$MERGED_SKILLS_DIR" "$LEGACY_BACKUP_DIR" >&2 + exit 1 fi + mv "$MERGED_SKILLS_DIR" "$LEGACY_BACKUP_DIR" + printf '[chezmoi] Preserved legacy skill tree at %s\n' "$LEGACY_BACKUP_DIR" +fi + +mkdir -p "$MERGED_SKILLS_DIR" "$CLAUDE_SKILLS_DIR" + +# Rebuild entries created by this merger. Removing symlinks is safe and makes +# the source-root priority deterministic when a skill moves between roots. +# Non-symlink user content is never deleted. +for link in "$MERGED_SKILLS_DIR"/*; do + [[ -L "$link" ]] || continue + rm -f "$link" done -# Create symlinks for each skill; earlier roots win on name clash (skills-extra/ over skills/). -for root in "${SKILL_ROOTS[@]}"; do +# Earlier roots win, so vendored skills-extra overrides the upstream archive. +for root in "${SKILL_SOURCE_ROOTS[@]}"; do [[ -d "$root" ]] || continue - rel="../../.agents/$(basename "$root")" + relative_root="../$(basename "$root")" for skill_dir in "$root"/*/; do [[ -d "$skill_dir" ]] || continue is_skill_dir "${skill_dir%/}" || continue - skill_name="$(basename "$skill_dir")" - link_path="$CLAUDE_SKILLS_DIR/$skill_name" - [[ -L "$link_path" ]] && continue - ln -s "$rel/$skill_name" "$link_path" - echo "[chezmoi] Linked skill: $skill_name" + name="$(basename "$skill_dir")" + link="$MERGED_SKILLS_DIR/$name" + desired="$relative_root/$name" + + if [[ ! -e "$link" && ! -L "$link" ]]; then + ln -s "$desired" "$link" + fi done done + +# Claude Code receives the same merged inventory. Codex reads the merged root +# directly, and Raxol can continue scanning it as its first external directory. +for link in "$CLAUDE_SKILLS_DIR"/*; do + [[ -L "$link" ]] || continue + name="$(basename "$link")" + if ! skill_exists "$name"; then + rm -f "$link" + printf '[chezmoi] Removed stale Claude skill link: %s\n' "$name" + fi +done + +for skill_dir in "$MERGED_SKILLS_DIR"/*/; do + [[ -d "$skill_dir" ]] || continue + is_skill_dir "${skill_dir%/}" || continue + name="$(basename "$skill_dir")" + link="$CLAUDE_SKILLS_DIR/$name" + desired="../../.agents/skills/$name" + if [[ -L "$link" ]]; then + [[ "$(readlink "$link")" == "$desired" ]] || ln -sfn "$desired" "$link" + elif [[ ! -e "$link" ]]; then + ln -s "$desired" "$link" + fi +done diff --git a/home/run_onchange_after_setup-agents.sh.tmpl b/home/run_onchange_after_setup-agents.sh.tmpl index bc9f043..1a97456 100644 --- a/home/run_onchange_after_setup-agents.sh.tmpl +++ b/home/run_onchange_after_setup-agents.sh.tmpl @@ -2,7 +2,7 @@ {{- if or .digest .recall .autoresearch .watchdog .prepper .sentinel .patchbot .regen }} # agents pyproject hash: {{ output "bash" "-c" (print "cat " .chezmoi.homeDir "/CODE/agent-skills/agents/*/pyproject.toml 2>/dev/null | shasum -a 256 | cut -d' ' -f1 || true") }} # Editable-install the agent-skills MCP CLIs into mise's pinned Python so the servers -# referenced by ~/.mcp.json actually exist. Re-runs when any agent pyproject changes. +# referenced by Claude Code and Codex MCP configuration actually exist. Re-runs when any agent pyproject changes. # Must sort after mise-install (provides python) -- hence the "setup-agents" name. set -euo pipefail diff --git a/home/run_onchange_after_setup-codex.sh.tmpl b/home/run_onchange_after_setup-codex.sh.tmpl new file mode 100644 index 0000000..4ef52a5 --- /dev/null +++ b/home/run_onchange_after_setup-codex.sh.tmpl @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Register Codex MCP servers without replacing model, trust, or other app-owned config. + +set -euo pipefail + +if ! command -v codex &>/dev/null; then + printf '[chezmoi] codex not found; skipping Codex MCP setup\n' >&2 + exit 0 +fi + +remove_server() { + codex mcp remove "$1" >/dev/null 2>&1 || true +} + +add_stdio() { + local name="$1" + shift + remove_server "$name" + codex mcp add "$name" -- "$@" >/dev/null +} + +add_http() { + local name="$1" url="$2" + remove_server "$name" + codex mcp add "$name" --url "$url" >/dev/null +} + +add_stdio context7 npx -y @upstash/context7-mcp@latest +add_http blockscout https://mcp.blockscout.com/mcp + +{{- if .coingecko }} +add_http coingecko https://mcp.api.coingecko.com/mcp +{{- else }} +remove_server coingecko +{{- end }} + +{{- if or .digest .recall .autoresearch .watchdog .prepper .sentinel .patchbot .regen }} +add_stdio scribe scribe serve +{{- else }} +remove_server scribe +{{- end }} +{{- if .digest }} +add_stdio digest digest serve +{{- else }} +remove_server digest +{{- end }} +{{- if .recall }} +add_stdio recall recall serve +{{- else }} +remove_server recall +{{- end }} +{{- if .autoresearch }} +add_stdio autoresearch autoresearch serve +{{- else }} +remove_server autoresearch +{{- end }} +{{- if .watchdog }} +add_stdio watchdog watchdog serve +{{- else }} +remove_server watchdog +{{- end }} +{{- if .prepper }} +add_stdio prepper prepper serve +{{- else }} +remove_server prepper +{{- end }} +{{- if .sentinel }} +add_stdio sentinel sentinel serve +{{- else }} +remove_server sentinel +{{- end }} +{{- if .patchbot }} +add_stdio patchbot patchbot serve +{{- else }} +remove_server patchbot +{{- end }} +{{- if .signoz }} +add_stdio signoz "{{ .chezmoi.homeDir }}/.local/bin/signoz-mcp-wrapper.sh" +{{- else }} +remove_server signoz +{{- end }} +{{- if .regen }} +add_stdio regen "{{ .chezmoi.homeDir }}/.local/bin/regen-mcp-wrapper.sh" +{{- else }} +remove_server regen +{{- end }} +{{- if .datadog }} +add_http datadog "https://mcp.{{ .datadog_site }}/api/unstable/mcp-server/mcp" +{{- else }} +remove_server datadog +{{- end }} +{{- if .sentry }} +add_http sentry https://mcp.sentry.dev/sse +{{- else }} +remove_server sentry +{{- end }} + +printf '[chezmoi] Codex MCP configuration updated\n' diff --git a/scripts/utils/apply-codex-target.sh b/scripts/utils/apply-codex-target.sh new file mode 100755 index 0000000..271890b --- /dev/null +++ b/scripts/utils/apply-codex-target.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOTFILES_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SOURCE_STATE="$DOTFILES_ROOT/home" +dry_run=0 + +if [[ "${1:-}" == "--dry-run" ]]; then + dry_run=1 +elif [[ $# -gt 0 ]]; then + printf 'Usage: %s [--dry-run]\n' "$0" >&2 + exit 2 +fi + +chezmoi_args=( + --source "$SOURCE_STATE" + --working-tree "$DOTFILES_ROOT" +) +targets=( + "$HOME/.agents/skills-upstream" + "$HOME/.codex/AGENTS.md" + "$HOME/.codex/hooks.json" +) + +if [[ "$dry_run" -eq 1 ]]; then + chezmoi "${chezmoi_args[@]}" apply \ + --dry-run \ + --verbose \ + --refresh-externals=never \ + "${targets[@]}" + exit 0 +fi + +chezmoi "${chezmoi_args[@]}" apply \ + --refresh-externals=always \ + "${targets[@]}" + +bash "$DOTFILES_ROOT/home/run_after_sync-skills.sh.tmpl" +chezmoi "${chezmoi_args[@]}" execute-template \ + <"$DOTFILES_ROOT/home/run_onchange_after_setup-codex.sh.tmpl" | + bash + +printf '[dotfiles] Codex target applied without reconciling unrelated files\n' diff --git a/scripts/utils/test-codex-target.sh b/scripts/utils/test-codex-target.sh new file mode 100755 index 0000000..c81a340 --- /dev/null +++ b/scripts/utils/test-codex-target.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOTFILES_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +assert_file() { + [[ -f "$1" ]] || fail "missing $1" +} + +assert_file "$DOTFILES_ROOT/AGENTS.md" +assert_file "$DOTFILES_ROOT/home/private_dot_codex/AGENTS.md.tmpl" +assert_file "$DOTFILES_ROOT/home/private_dot_codex/hooks.json.tmpl" +assert_file "$DOTFILES_ROOT/home/run_onchange_after_setup-codex.sh.tmpl" +assert_file "$DOTFILES_ROOT/scripts/utils/apply-codex-target.sh" +grep -A5 '^\[".agents/skills-upstream"\]' "$DOTFILES_ROOT/home/.chezmoiexternal.toml" | + grep -q 'exact = true' || fail "upstream skills external is not exact" + +test_home="$WORK_DIR/home" +mkdir -p \ + "$test_home/.agents/skills/core" \ + "$test_home/.agents/skills-upstream/core" \ + "$test_home/.agents/skills-upstream/shared" \ + "$test_home/.agents/skills-extra/extra" \ + "$test_home/.agents/skills-extra/shared" +printf '%s\n' '# legacy' >"$test_home/.agents/skills/core/SKILL.md" +printf '%s\n' '# upstream' >"$test_home/.agents/skills-upstream/core/SKILL.md" +printf '%s\n' '# upstream shared' >"$test_home/.agents/skills-upstream/shared/SKILL.md" +printf '%s\n' '# extra' >"$test_home/.agents/skills-extra/extra/SKILL.md" +printf '%s\n' '# preferred shared' >"$test_home/.agents/skills-extra/shared/SKILL.md" + +HOME="$test_home" bash "$DOTFILES_ROOT/home/run_after_sync-skills.sh.tmpl" >/dev/null +[[ -f "$test_home/.agents/skills-before-codex-merge/core/SKILL.md" ]] || + fail "legacy skill root was not preserved" +[[ -L "$test_home/.agents/skills/core" ]] || fail "upstream skill was not linked" +[[ -L "$test_home/.agents/skills/extra" ]] || fail "extra skill was not linked" +[[ "$(readlink "$test_home/.agents/skills/shared")" == ../skills-extra/shared ]] || + fail "skills-extra did not win the name collision" + +# The second run must converge without changing the selected targets. +HOME="$test_home" bash "$DOTFILES_ROOT/home/run_after_sync-skills.sh.tmpl" >/dev/null +[[ "$(readlink "$test_home/.agents/skills/shared")" == ../skills-extra/shared ]] || + fail "skill merge is not idempotent" + +hooks_json="$WORK_DIR/hooks.json" +chezmoi execute-template <"$DOTFILES_ROOT/home/private_dot_codex/hooks.json.tmpl" >"$hooks_json" +jq -e '.hooks.PreToolUse[0].matcher == "^Bash$"' "$hooks_json" >/dev/null +jq -e '.hooks.SessionStart[0].matcher == "startup|resume"' "$hooks_json" >/dev/null + +rendered_git="$WORK_DIR/gitconfig" +chezmoi --source "$DOTFILES_ROOT/home" --working-tree "$DOTFILES_ROOT" \ + cat "$HOME/.gitconfig" >"$rendered_git" +[[ "$(git config --file "$rendered_git" --get user.signingkey)" == key::ssh-ed25519* ]] || + fail "rendered Git config lost the SSH signing key" +[[ "$(git config --file "$rendered_git" --get gpg.format)" == ssh ]] || + fail "rendered Git config lost SSH signing format" +[[ "$(git config --file "$rendered_git" --get commit.gpgsign)" == true ]] || + fail "rendered Git config disabled commit signing" + +rendered_zshenv="$WORK_DIR/zshenv" +rendered_zshrc="$WORK_DIR/zshrc" +chezmoi --source "$DOTFILES_ROOT/home" --working-tree "$DOTFILES_ROOT" \ + cat "$HOME/.zshenv" >"$rendered_zshenv" +chezmoi --source "$DOTFILES_ROOT/home" --working-tree "$DOTFILES_ROOT" \ + cat "$HOME/.zshrc" >"$rendered_zshrc" +grep -q '/.foundry/bin' "$rendered_zshenv" || fail "rendered zshenv lost Foundry" +grep -q '/.grok/bin' "$rendered_zshrc" || fail "rendered zshrc lost Grok" +grep -q '/.aztec/current/bin' "$rendered_zshrc" || fail "rendered zshrc lost Aztec" +grep -q 'NARGO_HOME' "$rendered_zshrc" || fail "rendered zshrc lost Nargo" + +# Exercise the deployment helper against a recording chezmoi stub. This proves +# it binds the working checkout and never expands into an unscoped home apply. +fake_bin="$WORK_DIR/bin" +chezmoi_args_log="$WORK_DIR/chezmoi-args" +mkdir -p "$fake_bin" +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'printf "%s\n" "$@" >"$CHEZMOI_ARGS_LOG"' >"$fake_bin/chezmoi" +chmod +x "$fake_bin/chezmoi" +PATH="$fake_bin:$PATH" CHEZMOI_ARGS_LOG="$chezmoi_args_log" \ + "$DOTFILES_ROOT/scripts/utils/apply-codex-target.sh" --dry-run +grep -Fxq -- '--source' "$chezmoi_args_log" || fail "Codex apply did not bind its source" +grep -Fxq -- "$DOTFILES_ROOT/home" "$chezmoi_args_log" || fail "Codex apply used the wrong source" +grep -Fxq -- "$HOME/.agents/skills-upstream" "$chezmoi_args_log" || fail "Codex apply omitted skills" +grep -Fxq -- "$HOME/.codex/AGENTS.md" "$chezmoi_args_log" || fail "Codex apply omitted AGENTS.md" +grep -Fxq -- "$HOME/.codex/hooks.json" "$chezmoi_args_log" || fail "Codex apply omitted hooks" +if grep -Eq '/\.(gitconfig|zshenv|zshrc)$' "$chezmoi_args_log"; then + fail "Codex apply included unrelated Git or shell files" +fi + +git_hook="$DOTFILES_ROOT/home/private_dot_claude/hooks/executable_block-destructive-git.sh" +rm_hook="$DOTFILES_ROOT/home/private_dot_claude/hooks/executable_block-rm-rf.sh" +printf '%s\n' '{"tool_input":{"command":"git reset --hard HEAD"}}' | + bash "$git_hook" >/dev/null 2>&1 && fail "git hook allowed reset --hard" +printf '%s\n' '{"tool_input":{"command":"git status --short"}}' | + bash "$git_hook" >/dev/null 2>&1 || fail "git hook blocked a safe command" +printf '%s\n' '{"tool_input":{"command":"rm -rf build"}}' | + bash "$rm_hook" >/dev/null 2>&1 && fail "rm hook allowed rm -rf" + +rendered_setup="$WORK_DIR/setup-codex.sh" +chezmoi execute-template <"$DOTFILES_ROOT/home/run_onchange_after_setup-codex.sh.tmpl" >"$rendered_setup" +bash -n "$rendered_setup" + +claude_mcp="$WORK_DIR/mcp.json" +chezmoi execute-template <"$DOTFILES_ROOT/home/dot_mcp.json.tmpl" >"$claude_mcp" +jq -e '.mcpServers.scribe.command == "scribe"' "$claude_mcp" >/dev/null + +codex_home="$WORK_DIR/codex" +mkdir -p "$codex_home" +printf '%s\n' \ + 'model = "gpt-5.4"' \ + '' \ + '[projects."/tmp/codex-target-test"]' \ + 'trust_level = "trusted"' >"$codex_home/config.toml" +CODEX_HOME="$codex_home" codex mcp add keep-me -- true >/dev/null +CODEX_HOME="$codex_home" HOME="$test_home" bash "$rendered_setup" >/dev/null +grep -q '^model = "gpt-5.4"$' "$codex_home/config.toml" || fail "Codex model setting was replaced" +grep -q '^trust_level = "trusted"$' "$codex_home/config.toml" || fail "Codex trust setting was replaced" +CODEX_HOME="$codex_home" codex mcp get keep-me --json | + jq -e '.transport.command == "true"' >/dev/null +CODEX_HOME="$codex_home" codex mcp get blockscout --json | + jq -e '.transport.type == "streamable_http" and .transport.url == "https://mcp.blockscout.com/mcp"' >/dev/null +CODEX_HOME="$codex_home" codex mcp get scribe --json | + jq -e '.transport.type == "stdio" and .transport.command == "scribe" and .transport.args == ["serve"]' >/dev/null + +printf 'Codex target checks passed\n'