ot-skill-sync
OpenThrottle/monorepo/skills/ot-skill-sync/SKILL.md
Manage the OpenThrottle agent-skills architecture in any repo — install skills consistently, fan them out to every AI tool, and validate the layout. Use when installing or adding agent skills, syncing skills into agent folders, setting up skills in a new repo, checking skill layout drift, or when .agents/skills, .claude/skills, or skills-lock.json are involved.
Skill2 starsChanged 17 days ago
What's in it
- ot-skill-sync
- Which CLI reads what (in-repo, verified 2026-08-26)
- The architecture
- The personal tier
- Rules to enforce
- Commands
- What --check validates
- What check-links.sh validates
- Configuration
- Setting up a new repo
--- description: Manage the OpenThrottle agent-skills architecture in any repo — install skills consistently, fan them out to every AI tool, and validate the layout. Use when installing or adding agent skills, syncing skills into agent folders, setting up skills in a new repo, checking skill layout drift, or when .agents/skills, .claude/skills, or skills-lock.json are involved. metadata: author: openthrottle version: '1.0.0' name: ot-skill-sync --- # ot-skill-sync This skill manages the **OpenThrottle agent-skills architecture** in whatever repository you're in. It exists so every AI tool — Claude Code, Cursor 2.4+, Codex, Grok Build, OpenCode, VSCode/Copilot, Gemini CLI, and anything else — sees the **same skills from the same starting point**, no matter which tool a teammate used to install them. All of these CLIs read the **[Agent Skills](https://agentskills.io/) `SKILL.md` standard**; they differ only in which directories they scan, and the differences are wider than "two near-universal dirs" — so the fan-out list is derived from a verified matrix, not an assumption. ### Which CLI reads what (in-repo, verified 2026-08-26) Read off the installed binaries/bundles at the versions shown — the CLIs' own shipped docs and path constants, not vendor marketing. Re-verify before changing the fan-out list. | CLI | version | `.agents/skills` | `.claude/skills` | other dir it also scans | reached by this layout | | --------------------- | ---------- | :--------------: | :--------------: | ------------------------------------ | ------------------------ | | claude (Claude Code) | 2.1.232 | — | ✅ | — | ✅ fan-out | | cursor (cursor-agent) | 2026.09.10 | ✅ | ✅ | `.cursor/skills` | ✅ stage 1 | | grok (Grok Build) | 1.0.5 | ✅ | ✅ | `.grok/skills` | ✅ stage 1 | | antigravity (`agy`) | 1.1.21 | ✅ | — | — (global `~/.gemini/config/skills`) | ✅ stage 1 | | gemini (Gemini CLI) | 0.25.2 | — | — | `.gemini/skills` | ✅ fan-out | | codex | 0.145.0 | — | — | — (global `$CODEX_HOME/skills`) | ❌ no in-repo dir exists | | opencode | 1.18.16 | — | — | `.opencode/skill(s)` | ❌ not in the default | Notes on the two ❌ rows and the surprises above: - **codex 0.145.0 has no in-repo skills dir at all.** Skills live only in `$CODEX_HOME/skills` (`~/.codex/skills`). Its `.agents/` handling is plugin-marketplace manifests, not skills. Nothing in-repo can reach it. (Cursor reads `.codex/skills` — that is Cursor's compat scan, not codex's own.) - **opencode 1.18.16** reads project skills from `.opencode/skill(s)/<name>/SKILL.md`, and its "external" auto-scans are **home-scoped only** (`~/.claude/skills`, `~/.agents/skills` — disableable via `OPENCODE_DISABLE_EXTERNAL_SKILLS`). Add `.opencode/skill` to `AGENT_SKILL_DIRS` if a repo needs it; it is deliberately not a default. - **Antigravity needs no fan-out.** `agy` discovers `<workspace>/.agents/skills/<name>/` natively (plus the global `~/.gemini/config/skills/` and an `.agents/skills.json` manifest), so stage 1 already covers it. Do **not** add a `.gemini/skills` expectation for `agy` — that is the _Gemini CLI's_ dir, and the two share nothing but the `~/.gemini` prefix. - **The last column is additive, not exclusive — read it with the two ✅ columns, never alone.** Cursor is the row that invites the misreading: `.cursor/skills` in that column does **not** mean Cursor needs one. Its ✅ under `.agents/skills` is what matters, and that is why this repo deliberately generates no `.cursor/skills` — consistent with the drift rule in § What `--check` validates below. Re-verified 2026-09-14 against cursor-agent 2026.09.10: the shipped bundle carries path constants for `.cursor/skills`, `.agents/skills`, `.claude/skills`, `.grok/skills` and `.codex/skills` — Cursor scans all five, so naming any one of them as "Cursor's skills dir" is the error. Only the cursor row was re-probed; the other rows still stand at 2026-08-26. - **Claude Code 2.1.232 does not read `.agents/skills`.** `.claude/skills` is its only in-repo skills dir, which is exactly why the fan-out exists. Several CLIs additionally read per-tool **global** dirs (`~/.claude/skills`, `~/.codex/skills`, `~/.grok/skills`, `~/.gemini/skills`, `~/.gemini/config/skills`) that live outside any repo and are not part of this layout. ## The architecture Every openthrottle repo has (at most) four skill locations with strict ownership: | Location | Contents | Owned by | | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `skills/` | Hand-authored skills, committed to git | Humans (via PRs) | | `.agents/skills/` | The merged SSOT view read in-repo by Cursor, Grok Build, and Antigravity (`agy`) — see the matrix above; Claude Code and the Gemini CLI reach it only via the fan-out. **Real directories** = external skills installed by `npx skills` (tracked in `skills-lock.json`). **Symlinks** = the repo's own `skills/*`, generated. | The skills CLI / this skill | | `<agent>/skills/` (`.claude/skills/`, `.gemini/skills/`) | Per-agent fan-out for the CLIs that do **not** read `.agents/skills/` in-repo — Claude Code (`.claude/skills`) and the Gemini CLI (`.gemini/skills`). All symlinks, all generated, all gitignored. | This skill | | `~/.openthrottle/skills/` (**outside the repo**) | The per-user **personal tier**: private, half-finished, experimental skills. Linked in by sync and reaching every place a committed skill does, while being structurally impossible to commit. Override with `OPENTHROTTLE_PERSONAL_SKILLS_DIR`. | You, and nobody else | Sync is a three-stage pipeline: `skills/*` → `.agents/skills/` (stage 1), `<personal>/*` → `.agents/skills/` (stage 1b), then `.agents/skills/*` → each agent folder (stage 2). A name collision between `skills/` and `skills-lock.json` is an **error**, never a silent precedence. ### The personal tier Opt-in is **presence**: create `~/.openthrottle/skills/<name>/SKILL.md` and the next sync picks it up. There is deliberately no enable/disable env var for it — creating the directory is already the deliberate act. (`OPENTHROTTLE_PERSONAL_SKILLS_ENABLED` is a different thing entirely: the foreign-repo injection toggle.) A missing or empty root is a clean no-op on every path, which is what CI sees. **Uncommittable, twice over.** The content never enters the worktree, and the only in-repo artifact is a symlink the managed `.gitignore` block already covers. Neither is left to luck: `--check` runs `git check-ignore` on every personal link, and a Husky pre-commit guard refuses to stage one. **Collisions are a hard error**, not a precedence rule: ``` ✗ personal skill 'x' collides with committed skills/x ``` Rename it, or pass `--allow-shadow` to deliberately run a private fork of a team skill. Silently running your own variant of a skill your colleagues also invoke is the failure this prevents. **Per-CLI reach is unchanged.** Personal skills ride the same stage 2, so they land wherever a committed skill lands — no special case downstream. ## Rules to enforce 1. **ALWAYS** install external skills with `--agent universal` so they land only in `.agents/skills/`: ```bash npx skills add <owner>/<repo> --skill <skill_name> --agent universal ``` **Exception:** this skill itself is installed with default agent detection (no `--agent` flag), so the agent can discover it before the first sync has ever run. 2. **ALWAYS** run a sync after installing, adding, renaming, or removing a skill. 3. **NEVER** hand-edit `.agents/skills/` or any `<agent>/skills/` folder — author skills in `skills/`, install external ones with the CLI, and let the sync generate the rest. 4. OpenThrottle's shared skills are installed from `openthrottle/monorepo`, lockfile-managed per repo: ```bash npx skills add openthrottle/monorepo --skill <skill_name> --agent universal ``` ## Commands All scripts live in this skill's `scripts/` directory and operate on the **current repo** (found via `git rev-parse --show-toplevel`). From anywhere inside a repo: ```bash # Sync: build/refresh the two-stage layout (idempotent, safe to re-run) bash <path-to-this-skill>/scripts/sync.sh # Check: validate the layout without writing anything; exit 1 on drift. # Use in CI as the "agent skills SSOT drift" gate (run sync first). bash <path-to-this-skill>/scripts/sync.sh --check # Links: verify every relative link in a skill BODY resolves; exit 1 on any dead one. # Runs in the same CI gate as --check above. bash <path-to-this-skill>/scripts/check-links.sh # Cleanup: remove everything sync generated (never touches targets) bash <path-to-this-skill>/scripts/cleanup.sh # Personal tier: scaffold, inspect, and graduate your own private skills bash <path-to-this-skill>/scripts/personal.sh new <name> # scaffold + sync + how to invoke it bash <path-to-this-skill>/scripts/personal.sh list # what you have, and where it links bash <path-to-this-skill>/scripts/personal.sh promote <name> # move it into skills/, re-sync, stage it bash <path-to-this-skill>/scripts/personal.sh demote <name> # the inverse, for promoting too early ``` When installed, `<path-to-this-skill>` is `.agents/skills/ot-skill-sync`; in the OpenThrottle repo itself it's `skills/ot-skill-sync`. These scripts create symlinks and maintain a single static `.gitignore` block so generated links are never committed. That is their entire write surface. There is no side-ledger: a symlink under `.agents/skills/` or an agent folder _is_ a generated link (a real directory there is an external install), so the sync reconciles renames and removals straight from the filesystem. ## What `--check` validates 1. Every `skills/<name>/` (with a `SKILL.md`) is exported to `.agents/skills/<name>` 2. Every `skills-lock.json` entry is materialized in `.agents/skills/` 3. `.agents/skills/` contains nothing else (real dir ⇒ lockfile entry; symlink ⇒ points into `skills/`) 4. No name collisions between `skills/` and the lockfile 5. Agent folders contain exactly the `.agents/skills/` set, and no _generated_ fan-out dir exists outside the configured `AGENT_SKILL_DIRS` list. (This is about keeping the generated layout deterministic — not a judgment on any CLI: Cursor, Grok, and Antigravity all read `.agents/skills/` natively. A `-a cursor`-style install that drops a stray `.cursor/skills` fan-out is drift only because it's an un-configured generated target, so route those tools through `.agents/skills/` / the configured fan-out instead.) 6. The static `.gitignore` block is present and every generated symlink is gitignored 7. No dangling generated links, and no legacy `.gitignore-symlinks` ledger remains 8. Every personal link is git-ignored (asserted with `git check-ignore`, never a `test -e`, which follows parent symlinks and passes vacuously), points into the **currently-resolved** personal root, and is not dangling ## What `check-links.sh` validates `sync.sh --check` never opens a `SKILL.md` — every item above is about layout. That blind spot is how three separate classes of dead pointer accumulated here unnoticed (OT `6aec86bf`), and how the rules-layer retirement swept every well-formed reference to `.agents/rules/` while leaving four malformed ones in `agents-ralph` behind: **a grep for the correct path cannot see a link that is wrong.** So the check resolves rather than pattern-matches. One rule, no allowlist: > Every relative markdown link is resolved from the directory of the file that contains it. If the > target does not exist, the check fails naming the file, the line and the target. Skipped by design: absolute URLs, `mailto:`, bare `#anchor` links, and anything inside a fenced code block — those are examples, not pointers. A `#fragment` or `?query` is stripped before resolution; the anchor itself is not validated. **Why only this, and not a denylist of banned path prefixes.** Banning `.cursor/rules/` and `.agents/skills/<name>/SKILL.md` references was considered and rejected. Both need an allowlist for the files that legitimately _describe_ those paths as architecture (`README.md`, `AGENTS.md`, `ot-worktree`'s ladder snippet), and an allowlist of seven files is a thing people add an eighth entry to rather than fix the finding. Both also go stale the moment a path convention changes, which is the failure being fixed. Resolution needs no allowlist, would have caught the `agents-ralph` breakage on its own, and keeps working after any convention change. Its honest limit: it sees markdown **links**, not paths written as bare code spans. That is why the cross-skill pointers repaired under the same plan were rewritten as real links — doing so is what puts them under this check from now on. A pointer worth following is worth writing as a link. ## Configuration Per-agent fan-out targets default to **`.claude/skills .gemini/skills`** — one dir per CLI that cannot see `.agents/skills/` in-repo: - `.claude/skills` — Claude Code's only in-repo skills dir (also read by Cursor and Grok, so it is doubly useful). - `.gemini/skills` — the Gemini CLI's only in-repo skills dir (project scope; `~/.gemini/skills` is its user scope and out of this layout). The personal root is configured the same way, and defaults to `~/.openthrottle/skills`: ```bash OPENTHROTTLE_PERSONAL_SKILLS_DIR=/path/to/my/skills bash <path>/scripts/sync.sh ``` It must live **outside** the repo — sync refuses a root inside it, since the whole guarantee is that its content is not in the worktree. CLIs that read `.agents/skills/` natively — Cursor, Grok Build, Antigravity (`agy`) — need no fan-out. codex has no in-repo skills dir to target at all. Override per repo — without editing this skill — via a space-separated env var: ```bash # e.g. add opencode's project skills dir, or a Windsurf target AGENT_SKILL_DIRS=".claude/skills .gemini/skills .opencode/skill" bash <path>/scripts/sync.sh ``` ## Setting up a new repo 1. `npx skills add openthrottle/monorepo --skill ot-skill-sync` (default agents — the bootstrap exception) 2. Install any other skills with `--agent universal` 3. Run `sync.sh`, commit the resulting `.gitignore` changes 4. Add `sync.sh && sync.sh --check` to CI as the drift gate
More agent context in OpenThrottle/monorepo
27 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Cursor rule
Skill
- frontend-design.agents/skills/frontend-design/SKILL.md
- grilling.agents/skills/grilling/SKILL.md
- improve.agents/skills/improve/SKILL.md
- link-workspace-packages.agents/skills/link-workspace-packages/SKILL.md
- monitor-ci.agents/skills/monitor-ci/SKILL.md
- nx-workspace.agents/skills/nx-workspace/SKILL.md
- agents-ralphskills/agents-ralph/SKILL.md
- github-commitskills/github-commit/SKILL.md
- github-pull-requestskills/github-pull-request/SKILL.md
- github-squashskills/github-squash/SKILL.md
- ot-foldersskills/ot-folders/SKILL.md
- ot-generatorsskills/ot-generators/SKILL.md
- ot-loopskills/ot-loop/SKILL.md
- ot-onboardingskills/ot-onboarding/SKILL.md
- ot-plansskills/ot-plans/SKILL.md
- ot-postgresskills/ot-postgres/SKILL.md
- ot-stackskills/ot-stack/SKILL.md
- ot-worktreeskills/ot-worktree/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

