# agent-skills Single source of truth for custom agent skills + commands. Shared across **Claude Code** and **Codex**, every machine. ## Layout ``` skills/ # SKILL.md dirs — Claude Code AND Codex both read these (open Agent Skills standard) commands/ # slash commands — Claude Code only (Codex ignores) hooks/ # Claude Code hooks — see hooks/README.md, wiring is manual claude-md/ # shared instruction fragments — imported by both entry files entry/ # entry files: ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md systemd/ # user timers: weekly review + hour log — one machine only, see below bin/link.sh # bootstrap symlinks for non-Nix machines nix/home.nix # home-manager module for NixOS machines flake.nix # exposes homeModules.default ``` Skills are portable: only `name`+`description` frontmatter is required by both tools; Claude-only fields (`user-invocable`, `args`) are ignored by Codex. Cross-skill refs use root-relative paths (`linear-common/COMMON.md`), so they resolve under `~/.claude/skills` and `~/.agents/skills` alike. ## Install ### Non-Nix machine (e.g. dev VM) ```sh git clone https://git.naps.pt/yolo/agent-skills.git ~/tea/yolo/agent-skills ~/tea/yolo/agent-skills/bin/link.sh ``` 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 still need one manual step: the `settings.json` snippet in `hooks/README.md`. Entry files are linked automatically — `entry/CLAUDE.md` and `entry/codex-AGENTS.md` hold the machine-local sections and `@import` the shared fragments, so both tools read the same rules with no copy and no drift. ### NixOS machine (home-manager) ```nix # flake inputs inputs.agent-skills.url = "git+https://git.naps.pt/yolo/agent-skills.git"; # home config imports imports = [ inputs.agent-skills.homeModules.default ]; ``` `recursive = true` links files individually, so machine-local skills can coexist in the same dir. `nixos-rebuild switch` to apply/update. ## Shared machine, many sessions Several autonomous runs share one box. `skills/linear-common/scripts/gate.sh` is a machine-wide semaphore for heavy commands (full test suites, whole-project builds): bounded slots, memory + CPU cap via a systemd user scope, pinned build/test parallelism. Skills run scoped checks in the inner loop and put only the once-per-push full suite through the gate; exit 75 means it never ran and CI takes over. Policy lives in `linear-common/COMMON.md` under "Local verification budget". ```sh ~/.claude/skills/linear-common/scripts/gate.sh --status AGENT_GATE_SLOTS=3 AGENT_GATE_MEM_MAX=4G ~/.claude/skills/linear-common/scripts/gate.sh -- cargo test ``` ## Weekly review timer `systemd/week-review.timer` fires Fridays at 17:00 Europe/Lisbon (the zone is pinned in the unit because the machine clock is UTC). It runs `bin/week-review-session.sh`, which creates an Agent of Empires session in a fresh `week-review/` worktree, sends it `/week-review`, and pushes an ntfy notification to the `homelab` topic. The session is interactive, not `claude -p`. `/week-review` asks which carry-over issues to take and which changes to apply, so an unattended run would either stall or decide on its own. It stops at the first question and waits for `aoe attach`. If a previous `week-review-*` session is still open the run skips and says so, so weeks do not pile up. `Persistent=true` means a Friday spent with the machine off fires on the next boot instead of being lost. `bin/link.sh` symlinks the units into `~/.config/systemd/user/` but does not enable them — enabling on every machine would spawn one session per box for the same week. On the one machine that should run it: ```sh systemctl --user daemon-reload systemctl --user enable --now week-review.timer systemctl --user list-timers week-review.timer ``` Needs `loginctl enable-linger` so the timer runs while logged out. Logs are in `~/.local/state/week-review/run.log`. The nix module deliberately omits the timer for the same one-machine reason. ## Hour log timer `systemd/hourlog.timer` fires Fridays at 18:00 Europe/Lisbon and runs `bin/hourlog-session.sh`, which opens an Agent of Empires session on a scratch dir, sends it `/hourlog --week this`, and pushes an ntfy notification. Same shape as the weekly review and interactive for the same reason: the skill proposes hours and stops for approval before writing anything to the timesheet. An unattended run would be deciding a company record on your behalf. It skips if a previous `hourlog-*` session is still open, and `Persistent=true` makes a missed Friday fire on the next boot. Enable on one machine only: ```sh systemctl --user daemon-reload systemctl --user enable --now hourlog.timer ``` Setup lives outside this repo, which is public: - `~/.config/hourlog/projects.json` — path prefix to project mapping, copied from `skills/hourlog/config.example.json`. - `HOURLOG_API` and `HOURLOG_TOKEN` in `~/.env.claude` — API base URL and a personal access token (`profile:read`, `schedule:read`, `schedule:write`). No project, client, or host name belongs in a committed file here. ## Adding a skill Drop a new `skills//SKILL.md` (+ optional `scripts/`, `references/`, `assets/`). Commit. Non-Nix: re-run `bin/link.sh`. Nix: rebuild. ## Skills | skill | what | |-------|------| | `work` | tracker issue → worktree → PR → hands off to `land` | | `yolo` | quick ship; optional `land` handoff | | `land` | drive an open PR to green + ready-to-merge; user clicks merge (canonical CI/review loop) | | `blitz` | drive a whole milestone to done | | `nightshift` | hours-long unattended build; architect delegating to subagents, backs off before the 5h limit | | `linear-common` | shared config/setup/worktree conventions + local verification budget (dependency of work/yolo/blitz/nightshift) | | `week-review` | review the past week's sessions for recurring friction; reads open issues here as carry-over | | `hourlog` | hours-per-project-per-day proposal from session activity, reconciled against the timesheet; submits only what you approve | | `crit`, `improve-codebase-architecture` | misc | ## Vendored skills `humanizer` and `impeccable` are third-party and copied in, not written here. Each carries an `UPSTREAM` file recording repo, pinned tag, subdirectory, and license. Do not edit them — a local change becomes a merge conflict on every update, and a rule that reads wrong is a bug to report upstream. ```sh ./bin/check-vendored.sh # reports any that have moved upstream ``` To update: re-clone at the newer tag, copy the `subdir` over the skill dir, and bump `ref` + `vendored` in `UPSTREAM`. | skill | upstream | license | |-------|----------|---------| | `humanizer` | [blader/humanizer](https://github.com/blader/humanizer) | MIT | | `impeccable` | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | Apache-2.0 | ## Writing contract One contract, `claude-md/writing.md`. Both entry files `@import` it, so Claude Code and Codex read the same bytes on every machine. The rule it encodes is **selection, not compression**: keep output short by cutting whole ideas that don't change what the reader does next, then write what survives as plain sentences. Not by dropping articles, abbreviating, or writing fragments — that is shorter and worse. Anthropic's Fable 5 and Opus 5 prompting guides both say this explicitly. 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. 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 prohibitions, so the file is short and shows rather than forbids. Prose alone drifts; the linters make it binding. Details in `hooks/README.md`.