Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions always-on/create-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ When asked to create, add, install, or author a skill, or when about to write
a new `SKILL.md` / home-link a skill directory, read the `create-skill` skill
first (`engine/skills/create-skill/SKILL.md` or the installed `create-skill` skill).

A skill MUST be available to Claude, Cursor, and Codex unless it is listed in
A skill MUST be available to Claude, Cursor, Codex, and Muse unless it is listed in
`CLAUDE_ONLY_SKILLS` in catstack `install.sh`. Prefer catstack
`product/skills/<name>/` (or corpus) + `./install.sh`. Project-skill home links MUST hit all three
`product/skills/<name>/` (or corpus) + `./install.sh`. Project-skill home links MUST hit all four
roots (`scripts/install/link_skill_three_harnesses.sh`). Do not follow Cursor-only
`~/.cursor/skills/` install advice.
10 changes: 5 additions & 5 deletions cursor/rules/create-skill-three-harnesses.mdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Creating or installing a skill must target Claude, Cursor, and Codex — never a single harness
description: Creating or installing a skill must target Claude, Cursor, Codex, and Muse — never a single harness
alwaysApply: true
---

Expand All @@ -16,16 +16,16 @@ or `~/.codex/engine/skills/create-skill/SKILL.md` after `./install.sh`) or

## Invariants (assert)

- A skill MUST apply to **Claude, Cursor, and Codex** unless it is listed in
- A skill MUST apply to **Claude, Cursor, Codex, and Muse** unless it is listed in
`CLAUDE_ONLY_SKILLS` in `install.sh`.
- Prefer `product/skills/<name>/` (or `corpus/skills/<name>/`) in catstack + `./install.sh`.
- Project-skill home links MUST use all three roots (or
`scripts/install/link_skill_three_harnesses.sh`) — never Claude+Cursor only.
- Project-skill home links MUST use all four roots (or
`scripts/install/link_skill_three_harnesses.sh`) — never a subset.
- After install/link, run `python3 scripts/ci/check_skills_three_harnesses.py`
(add `--home` when checking live personal roots).

Do not follow Cursor built-in create-skill advice that only mentions
`~/.cursor/skills/`. Do not leave Codex (or any third root) unlinked.
`~/.cursor/skills/`. Do not leave any harness root unlinked.

Incident this prevents: agents home-linked `wipe-bad-pr` into Claude and
Cursor only and missed `~/.codex/skills/`.
2 changes: 2 additions & 0 deletions drafter.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
{ "id": "product-skill", "pathGlob": "product/skills/**", "unit": ["product-skill"] },
{ "id": "engine-runtime", "pathGlob": "{engine,scripts,.github}/**", "unit": ["engine-runtime"] },
{ "id": "engine-runtime-root-config", "pathGlob": "{drafter.config.json,install.sh,install_*.py}", "unit": ["engine-runtime"] },
{ "id": "engine-runtime-always-on", "pathGlob": "always-on/**", "unit": ["engine-runtime"] },
{ "id": "engine-runtime-cursor-rules", "pathGlob": "cursor/rules/**", "unit": ["engine-runtime"] },
{ "id": "tests-dirs-proof", "pathGlob": "**/{test,tests,__tests__,e2e}/**", "unit": ["proof"] },
{ "id": "ecosystem-inventory-neutral", "pathGlob": "docs/ecosystem.md", "unit": [] },
{ "id": "skill-triggers-inventory-neutral", "pathGlob": "docs/skill-triggers.md", "unit": [] },
Expand Down
2 changes: 1 addition & 1 deletion engine/CLAUDE.core.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Slash commands `/pr-skill`, `/draft-pr`, and `/make-pr` all enter this skill.

When asked to create, add, install, or author a skill, or when about to write a new `SKILL.md` / home-link a skill directory, read the `create-skill` skill first (`engine/skills/create-skill/SKILL.md` or the installed `create-skill` skill).

A skill MUST be available to Claude, Cursor, and Codex unless it is listed in `CLAUDE_ONLY_SKILLS` in `install.sh`. Prefer catstack `product/skills/<name>/` or `corpus/skills/<name>/` + `./install.sh`. Project-skill home links MUST hit all three roots (`scripts/install/link_skill_three_harnesses.sh`). Do not follow Cursor-only `~/.cursor/skills/` install advice.
A skill MUST be available to Claude, Cursor, Codex, and Muse unless it is listed in `CLAUDE_ONLY_SKILLS` in `install.sh`. Prefer catstack `product/skills/<name>/` or `corpus/skills/<name>/` + `./install.sh`. Project-skill home links MUST hit all four roots (`scripts/install/link_skill_three_harnesses.sh`). Do not follow Cursor-only `~/.cursor/skills/` install advice.

# Named constraints (apply everywhere)

Expand Down
11 changes: 6 additions & 5 deletions engine/skills/create-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
---
name: create-skill
description: >-
Create or install an agent skill for Claude, Cursor, and Codex together.
Create or install an agent skill for Claude, Cursor, Codex, and Muse together.
Use when authoring a new skill, adding SKILL.md, home-linking a project
skill, or when the user says create-skill / install a skill. Overrides
single-harness Cursor-only install advice.
---

# Creating skills (Claude + Cursor + Codex)
# Creating skills (Claude + Cursor + Codex + Muse)

## Invariants (assert)

- Skill markdown MUST NOT reference files that do not exist in this repo or
in that skill package (except allowlisted consumer/runtime contract paths
such as `.cursor/judge-swarm-bindings.json`). Enforced by
`scripts/ci/check_skill_file_refs.py`.
- A new skill MUST be available to **Claude, Cursor, and Codex** — never only
- A new skill MUST be available to **Claude, Cursor, Codex, and Muse** — never only
the harness the agent happens to be running in.
- Prefer putting portable skills under `product/skills/<name>/` (or mined
lessons under `corpus/skills/<name>/`) and running `./install.sh`. That is
the only path that keeps all three harness roots in sync automatically.
the only path that keeps all four harness roots in sync automatically.
See [docs/ecosystem.md](../../../docs/ecosystem.md). Engine skills
(`reflect`, `create-skill`, …) live under `engine/skills/` only.
- Claude-only skills MUST be listed in `CLAUDE_ONLY_SKILLS` in `install.sh`
Expand Down Expand Up @@ -92,7 +92,7 @@ After reading `SKILL.md`, read **at most one** sibling `domains/<type>.md`:
- Project CLIs that only exist in one repo stay project skills (home-link
with `scripts/install/link_skill_three_harnesses.sh`), not catstack domains.

## Project-skill home link (all three)
## Project-skill home link (all four)

If the skill must live in a project (e.g. `.cursor/skills/wipe-bad-pr`):

Expand All @@ -108,6 +108,7 @@ name=$(basename "$src")
ln -sfn "$src" "$HOME/.claude/skills/$name"
ln -sfn "$src" "$HOME/.cursor/skills/$name"
ln -sfn "$src" "$HOME/.codex/skills/$name"
ln -sfn "$src" "${MUSE_SKILLS_DIR:-$HOME/workspace/skills}/$name"
```

Then run:
Expand Down
4 changes: 2 additions & 2 deletions engine/skills/create-skill/tests/fires_example.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
User: "Add a new skill that watches flaky CI jobs and retries them —
make sure it's available in Claude, Cursor, and Codex."
make sure it's available in Claude, Cursor, Codex, and Muse."

This should fire: authoring a new skill / adding a `SKILL.md` / needing
it home-linked across all three harnesses is exactly this skill's scope.
it home-linked across all four harnesses is exactly this skill's scope.

The ecosystem doc link in SKILL.md is `../../../docs/ecosystem.md`
(three levels up from engine/skills/create-skill/). `scripts/ci/check_skill_file_refs.py`
Expand Down
4 changes: 4 additions & 0 deletions engine/skills/make-pr/tests/test_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ def test_scripts_and_install_sh_are_engine_runtime_like_drafter_config(self):
self.assertEqual(set(info["units"]), {"engine-runtime"})
self.assertEqual(info["neutral"], ["docs/ecosystem.md"])

def test_installed_agent_rule_inputs_are_engine_runtime(self):
info = pf.classify(["always-on/create-skill.md", "cursor/rules/create-skill-three-harnesses.mdc"])
self.assertEqual(set(info["units"]), {"engine-runtime"})

def test_docs_other_than_the_inventory_are_their_own_unit(self):
info = pf.classify(["engine/hooks/demo/detect.py", "docs/guide.md"])
self.assertEqual(set(info["units"]), {"engine-runtime", "docs"})
Expand Down
23 changes: 22 additions & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ set -euo pipefail

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

MUSE_SKILLS_DIR="${MUSE_SKILLS_DIR:-$HOME/workspace/skills}"

resolve_main_checkout() {
local start="$1" common parent
common="$(git -C "$start" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" || return 1
Expand Down Expand Up @@ -282,6 +284,7 @@ install_into() {
install_into claude "$HOME/.claude/skills"
install_into cursor "$HOME/.cursor/skills"
install_into codex "$HOME/.codex/skills"
install_into muse "$MUSE_SKILLS_DIR"

# Hooks aren't per-agent skill folders, so they don't go through install_into
# -- but they get the same fixed, portable symlink location. Hook configs
Expand Down Expand Up @@ -476,6 +479,23 @@ else
fi
python3 "$REPO_DIR/scripts/install/install_codex_agents_md.py" "${CODEX_AGENTS_ARGS[@]}"

echo "--- muse global AGENTS.md (\$HOME/AGENTS.md) ---"
MUSE_AGENTS_ARGS=("${CODEX_AGENTS_ARGS[@]}")
CAT_MODE_DEFAULT_FRAGMENT=""
if [ "$CAT_MODE_DEFAULT" = "on" ]; then
CAT_MODE_DEFAULT_FRAGMENT="$(mktemp)"
echo "cat-mode default is on: read and apply ${MUSE_SKILLS_DIR/#$HOME/\~}/cat-mode/SKILL.md on every turn before starting." > "$CAT_MODE_DEFAULT_FRAGMENT"
MUSE_AGENTS_ARGS+=(--fragment "cat-mode-default=$CAT_MODE_DEFAULT_FRAGMENT")
echo "write cat-mode-default fragment for muse AGENTS.md (CATSTACK_CAT_MODE_DEFAULT=on)"
else
MUSE_AGENTS_ARGS+=(--without cat-mode-default)
echo "remove cat-mode-default fragment from muse AGENTS.md (CATSTACK_CAT_MODE_DEFAULT=$CAT_MODE_DEFAULT)"
fi
python3 "$REPO_DIR/scripts/install/install_codex_agents_md.py" --agents-path "$HOME/AGENTS.md" "${MUSE_AGENTS_ARGS[@]}"
if [ -n "$CAT_MODE_DEFAULT_FRAGMENT" ]; then
rm -f "$CAT_MODE_DEFAULT_FRAGMENT"
fi

echo "--- remove catstack links this install no longer creates ---"
CATSTACK_ROOTS="$REPO_DIR"$'\n'"$(cd "$REPO_DIR" && pwd -P)"
if MAIN_CHECKOUT="$(resolve_main_checkout "$REPO_DIR")"; then
Expand All @@ -486,7 +506,8 @@ CATSTACK_ROOTS="$CATSTACK_ROOTS"$'\n'"$HOOKS_SNAPSHOT_DIR"
for sweep_dir in \
"$HOME/.claude/hooks" "$HOME/.claude/skills" "$HOME/.claude/commands" \
"$HOME/.cursor/hooks" "$HOME/.cursor/skills" "$HOME/.cursor/commands" "$HOME/.cursor/rules" \
"$HOME/.codex/hooks" "$HOME/.codex/skills" "$HOME/.codex/commands"
"$HOME/.codex/hooks" "$HOME/.codex/skills" "$HOME/.codex/commands" \
"$MUSE_SKILLS_DIR"
do
[ -d "$sweep_dir" ] || continue
for entry in "$sweep_dir"/*; do
Expand Down
61 changes: 38 additions & 23 deletions scripts/ci/check_skills_three_harnesses.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#!/usr/bin/env python3
"""Mechanical check: skills must target Claude, Cursor, and Codex.
"""Mechanical check: skills must target Claude, Cursor, Codex, and Muse.

Repo mode (default, CI-safe):
- install.sh must install into all three personal skill roots
- install.sh must install into all four personal skill roots
- create-skill skill + always-on rule/fragment must state the invariant
- CONTRIBUTING.md must state the three-harness assert

Home mode (--home):
- Catstack skills present in any personal root must exist in all three
- Catstack skills present in any personal root must exist in all four
(except CLAUDE_ONLY_SKILLS, which must stay Claude-only).
- Non-catstack skills: fail only when the same symlink target is linked
into two roots but missing from the third (incomplete multi-harness
into two roots but missing from another (incomplete multi-harness
install). Unrelated real directories are ignored.

Exit 0 on pass, 1 on failure. Prints each failure line.
Expand All @@ -31,13 +30,12 @@
os.path.join(REPO_ROOT, "product", "skills"),
)
CREATE_SKILL = os.path.join(REPO_ROOT, "engine", "skills", "create-skill", "SKILL.md")
CONTRIBUTING = os.path.join(REPO_ROOT, "CONTRIBUTING.md")
CURSOR_RULE = os.path.join(
REPO_ROOT, "cursor", "rules", "create-skill-three-harnesses.mdc"
)
ALWAYS_ON = os.path.join(REPO_ROOT, "always-on", "create-skill.md")

REQUIRED_PHRASE = "Claude, Cursor, and Codex"
REQUIRED_PHRASE = "Claude, Cursor, Codex, and Muse"

PROMISED_CATCH = (
"reflect: claude cursor",
Expand All @@ -46,15 +44,24 @@
"outside-skill: claude cursor linked",
)
PROMISED_ALLOW = (
"reflect: claude cursor codex",
"reflect: claude cursor codex muse",
"reflect:",
"automate-me: claude",
"outside-skill: claude",
"outside-skill: claude cursor",
"outside-skill: claude cursor codex linked",
"outside-skill: claude cursor codex muse linked",
)


def muse_skills_dir(home: str) -> str:
"""Muse (Meta's agent) keeps skills in its workspace skills dir, not a
dot-dir. Honor the same MUSE_SKILLS_DIR override install.sh uses."""
override = os.environ.get("MUSE_SKILLS_DIR")
if override:
return override
return os.path.join(home, "workspace", "skills")


def flags_exemplar(exemplar: str) -> bool:
name, _, rest = exemplar.partition(":")
tokens = rest.split()
Expand All @@ -70,6 +77,13 @@ def flags_exemplar(exemplar: str) -> bool:
os.symlink(source, os.path.join(root, name))
else:
os.makedirs(os.path.join(root, name))
muse_root = muse_skills_dir(home)
os.makedirs(muse_root)
if "muse" in tokens:
if "linked" in tokens:
os.symlink(source, os.path.join(muse_root, name))
else:
os.makedirs(os.path.join(muse_root, name))
return bool(check_home(home))


Expand All @@ -89,17 +103,17 @@ def check_repo() -> list[str]:
with open(INSTALL_SH) as handle:
install_text = handle.read()

for agent, marker in (
("claude", 'install_into claude "$HOME/.claude/skills"'),
("cursor", 'install_into cursor "$HOME/.cursor/skills"'),
("codex", 'install_into codex'),
for agent, marker, path_fragment in (
("claude", 'install_into claude "$HOME/.claude/skills"', "$HOME/.claude/skills"),
("cursor", 'install_into cursor "$HOME/.cursor/skills"', "$HOME/.cursor/skills"),
("codex", 'install_into codex', "$HOME/.codex/skills"),
("muse", "install_into muse", "MUSE_SKILLS_DIR"),
):
if marker not in install_text or f"$HOME/.{agent}/skills" not in install_text:
if marker not in install_text or path_fragment not in install_text:
errors.append(f"install.sh missing install_into for {agent}")

for path, label in (
(CREATE_SKILL, "engine/skills/create-skill/SKILL.md"),
(CONTRIBUTING, "CONTRIBUTING.md"),
(CURSOR_RULE, "cursor/rules/create-skill-three-harnesses.mdc"),
(ALWAYS_ON, "always-on/create-skill.md"),
):
Expand Down Expand Up @@ -162,9 +176,9 @@ def skill_entry(root: str, name: str) -> tuple[bool, str | None]:
def check_home(home: str) -> list[str]:
"""Flag incomplete multi-harness installs.

- Catstack repo skills MUST exist in all three roots (except CLAUDE_ONLY).
- Catstack repo skills MUST exist in all four roots (except CLAUDE_ONLY).
- Non-catstack skills: only fail when the *same symlink target* is linked
into two roots but missing from the third (the wipe-bad-pr class). Real
into two roots but missing from another (the wipe-bad-pr class). Real
single-harness copies (Invoker dirs, etc.) are ignored.
"""
errors: list[str] = []
Expand All @@ -175,6 +189,7 @@ def check_home(home: str) -> list[str]:
"claude": os.path.join(home, ".claude", "skills"),
"cursor": os.path.join(home, ".cursor", "skills"),
"codex": os.path.join(home, ".codex", "skills"),
"muse": muse_skills_dir(home),
}
by_agent = {agent: skill_names_in(path) for agent, path in roots.items()}
catstack_names = repo_skill_names()
Expand All @@ -194,10 +209,10 @@ def check_home(home: str) -> list[str]:
continue
if not present["claude"]:
errors.append(f"{name}: CLAUDE_ONLY but missing from ~/.claude/skills")
for agent in ("cursor", "codex"):
for agent in ("cursor", "codex", "muse"):
if present[agent]:
errors.append(
f"{name}: CLAUDE_ONLY but present in ~/.{agent}/skills"
f"{name}: CLAUDE_ONLY but present in {roots[agent]}"
)
continue

Expand All @@ -212,7 +227,7 @@ def check_home(home: str) -> list[str]:
)
continue

# Same source linked into ≥2 harnesses ⇒ must be in all three.
# Same source linked into ≥2 harnesses ⇒ must be in all four.
if len(targets) < 2:
continue
# Group agents by target path.
Expand All @@ -236,7 +251,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument(
"--home",
action="store_true",
help="Check live $HOME skill roots for three-harness parity",
help="Check live $HOME skill roots for four-harness parity",
)
parser.add_argument(
"--home-dir",
Expand All @@ -250,11 +265,11 @@ def main(argv: list[str] | None = None) -> int:
errors.extend(check_home(args.home_dir))

if errors:
print("FAIL: skills three-harness check", file=sys.stderr)
print("FAIL: skills four-harness check", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
return 1
print("ok skills three-harness check")
print("ok skills four-harness check")
return 0


Expand Down
Loading
Loading