diff --git a/README.md b/README.md index 1525e1e..9707c9a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ git clone https://git.naps.pt/yolo/agent-skills.git ~/tea/yolo/agent-skills Symlinks each skill into `~/.claude/skills/` and `~/.agents/skills/`, commands into `~/.claude/commands/`, hooks into `~/.claude/hooks/`, `claude-md/` fragments into `~/.claude/`. Idempotent; any pre-existing real dir is moved to `~/.agent-skills-backup/` (outside the discovery path, so it isn't picked up as a duplicate skill). Re-run after adding a skill. -Hooks and fragments need one manual step each: the `settings.json` snippet in `hooks/README.md`, and `@writing.md` + `@operating.md` import lines in your `~/.claude/CLAUDE.md`. Codex needs no step — `link.sh` generates `~/.agents/AGENTS.md` from both fragments (re-run it after editing them). +Hooks and fragments need one manual step each: the `settings.json` snippet in `hooks/README.md`, and `@writing.md` + `@operating.md` import lines in your `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`. Both tools support `@path`, so they import the same fragments — no copy, no drift. ### NixOS machine (home-manager) @@ -83,7 +83,7 @@ Two enforcers in `hooks/`: | enforcer | scope | |----------|-------| | `comms-lint.py` | GitHub issues/PRs/review comments — 150 words (hard cap 300), evidence in `
`, no reviewer-addressing opener, plain diction | -| `comment-lint.py` | code comments — 1-3 lines, volume and purpose, not wording | +| `comment-lint.py` | code comments — 1-3 lines, volume and purpose, not wording. The rule is path-scoped (`~/.claude/rules/`), so it loads only when Claude reads a source file. | Written in the voice it asks for, and leads with a worked example of each. Prompt style leaks into output style, and examples steer harder than diff --git a/bin/link.sh b/bin/link.sh index 7408ec4..2b77a3b 100755 --- a/bin/link.sh +++ b/bin/link.sh @@ -13,7 +13,8 @@ CODEX_SKILLS="$HOME/.agents/skills" CLAUDE_CMDS="$HOME/.claude/commands" # commands are Claude-only; Codex ignores CLAUDE_HOOKS="$HOME/.claude/hooks" # hooks are Claude-only CLAUDE_HOME="$HOME/.claude" # CLAUDE.md fragments, pulled in via @name.md -AGENTS_HOME="$HOME/.agents" # Codex global config root +CLAUDE_RULES="$HOME/.claude/rules" # path-scoped rules +CODEX_HOME="$HOME/.codex" # Codex global config root # Per-FILE links, never a whole-dir link: ~/.claude/hooks and ~/.claude itself hold # machine-local files this repo does not own, and a dir symlink would hide them. @@ -33,7 +34,7 @@ link() { # link echo "linked $dst -> $src" } -mkdir -p "$CLAUDE_SKILLS" "$CODEX_SKILLS" "$CLAUDE_CMDS" "$CLAUDE_HOOKS" "$AGENTS_HOME" +mkdir -p "$CLAUDE_SKILLS" "$CODEX_SKILLS" "$CLAUDE_CMDS" "$CLAUDE_HOOKS" "$CLAUDE_RULES" "$CODEX_HOME" for d in "$REPO"/skills/*/; do name="$(basename "$d")" @@ -51,22 +52,21 @@ for f in "$REPO"/hooks/*.py "$REPO"/hooks/*.sh; do link "$f" "$CLAUDE_HOOKS/$(basename "$f")" done +# code-comments.md is path-scoped and belongs in rules/, not here — linking it +# into ~/.claude/ as well would load it unconditionally and defeat the scoping. for f in "$REPO"/claude-md/*.md; do [ -e "$f" ] || continue + [ "$(basename "$f")" = "code-comments.md" ] && continue link "$f" "$CLAUDE_HOME/$(basename "$f")" done -# Codex reads ~/.agents/AGENTS.md and has no @import, so the fragments are -# concatenated into a real file. Re-run this script after editing claude-md/. -rm -f "$AGENTS_HOME/AGENTS.md" -{ - echo "" - echo - cat "$REPO/claude-md/operating.md" - echo - cat "$REPO/claude-md/writing.md" -} > "$AGENTS_HOME/AGENTS.md" -echo "generated $AGENTS_HOME/AGENTS.md" +# Path-scoped rules load only when Claude reads a matching file. +link "$REPO/claude-md/code-comments.md" "$CLAUDE_RULES/code-comments.md" +rm -f "$CLAUDE_HOME/code-comments.md" "$HOME/.agents/AGENTS.md" + +# Codex reads ~/.codex/AGENTS.md and supports @path imports, same as CLAUDE.md, +# so both tools import the identical fragments — no copy, no drift. +mkdir -p "$CODEX_HOME" echo "done." echo "hooks still need wiring in ~/.claude/settings.json — see hooks/README.md" diff --git a/claude-md/code-comments.md b/claude-md/code-comments.md new file mode 100644 index 0000000..8c583de --- /dev/null +++ b/claude-md/code-comments.md @@ -0,0 +1,24 @@ +--- +paths: + - "**/*.{ts,tsx,js,jsx,mjs,cjs,py,rs,go,sol,sh,bash,zsh,c,h,cc,cpp,hpp,java,kt,rb,lua,zig,nix,swift,php,cs,ex}" +--- + +## Code comments + +Budget: 1-3 lines. Write one only when an agent with the repo and thirty +seconds of grep would still get it wrong. That means one of: + +- a landmine no test catches +- a fact not in the repo (deployed bytecode, chain quirk, library bug) +- a unit or epoch the type cannot carry (wei, ms, 18-dp) +- a MUST or MUST NOT a caller can break + +Good: + +> `// MUST read lastGoodPrice before fetchPrice — same eth_call state.` + +Everything else: delete. Design rationale and rejected alternatives go in +`docs/`; leave a path. History is in git. No emphasis; ALL-CAPS only for +MUST / NEVER. + +Deleting a comment is cheap and reversible. When unsure, delete. diff --git a/claude-md/writing.md b/claude-md/writing.md index 92eaae2..05aec4e 100644 --- a/claude-md/writing.md +++ b/claude-md/writing.md @@ -81,26 +81,6 @@ Commits: Conventional Commits, subject ≤50 chars, body only when the "why" is not obvious. Review comments: one line per finding — location, problem, fix. -### Code comments - -Budget: 1-3 lines. Write one only when an agent with the repo and thirty -seconds of grep would still get it wrong. That means one of: - -- a landmine no test catches -- a fact not in the repo (deployed bytecode, chain quirk, library bug) -- a unit or epoch the type cannot carry (wei, ms, 18-dp) -- a MUST or MUST NOT a caller can break - -Good: - -> `// MUST read lastGoodPrice before fetchPrice — same eth_call state.` - -Everything else: delete. Design rationale and rejected alternatives go in -`docs/`; leave a path. History is in git. No emphasis; ALL-CAPS only for -MUST / NEVER. - -Deleting a comment is cheap and reversible. When unsure, delete. - ### Where this does not apply Security warnings, irreversible-action confirmations, and legal or diff --git a/hooks/comment-lint.py b/hooks/comment-lint.py index 3c73695..bdb8910 100755 --- a/hooks/comment-lint.py +++ b/hooks/comment-lint.py @@ -2,7 +2,7 @@ """PostToolUse nudge for code comments: Write / Edit / MultiEdit. Lints ONLY the newly-added text against the "Code comments" contract in -~/.claude/writing.md, so legacy files are not re-flagged on every touch. +~/.claude/rules/code-comments.md, so legacy files are not re-flagged on every touch. Exit 0 = silent. Exit 2 = stderr goes back to Claude as feedback; the edit is already applied, so this is a revise-it nudge, not a block. @@ -157,7 +157,7 @@ def main(): name = os.path.basename(path) if problems: - lines = [f"Comment contract (~/.claude/writing.md) — {name}:", ""] + lines = [f"Comment contract (~/.claude/rules/code-comments.md) — {name}:", ""] lines += [f" - {p}" for p in problems + notes] lines += ["", "Trim what you just wrote, or say why it stays."] print("\n".join(lines), file=sys.stderr) diff --git a/nix/home.nix b/nix/home.nix index b67137f..a8f191e 100644 --- a/nix/home.nix +++ b/nix/home.nix @@ -21,11 +21,9 @@ ".claude/writing.md".source = "${agent-skills}/claude-md/writing.md"; ".claude/operating.md".source = "${agent-skills}/claude-md/operating.md"; - # Codex has no @import, so the fragments are concatenated into one file. - ".agents/AGENTS.md".text = builtins.concatStringsSep "\n" [ - (builtins.readFile "${agent-skills}/claude-md/operating.md") - (builtins.readFile "${agent-skills}/claude-md/writing.md") - ]; + # Path-scoped: loads only when Claude reads a matching source file. + ".claude/rules/code-comments.md".source = "${agent-skills}/claude-md/code-comments.md"; + }; } # Hook wiring lives in ~/.claude/settings.json, which this module does not own.