♻️ refactor(skills): rename repo skills source dir

move the repository skills source from `codex/skills` to `skills`.

update vendor/install_skills paths, thirdparty sync pipeline, docs, and regression tests, including deployment-route e2e coverage.
This commit is contained in:
csh 2026-05-19 09:09:54 +08:00
parent f049dfbe10
commit 2c5050de09
100 changed files with 241 additions and 36 deletions

View File

@ -36,7 +36,7 @@ for entry in data["sources"]:
PY
}
render_codex_skill() {
render_skill() {
local snapshot_root="$1"
local output_dir="$2"
local platform_config_rel="$3"
@ -101,8 +101,8 @@ PY
tracked_skill_exists() {
local name="$1"
# conflict only if a first-party SKILL.md exists directly under codex/skills/<name>/
[ -f "codex/skills/$name/SKILL.md" ]
# conflict only if a first-party SKILL.md exists directly under skills/<name>/
[ -f "skills/$name/SKILL.md" ]
}
@ -122,7 +122,7 @@ trap cleanup EXIT
git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"
mkdir -p "codex/skills/thirdparty/.sources"
mkdir -p "skills/thirdparty/.sources"
sources_file="$tmp_dir/sources.tsv"
if ! emit_sources_tsv > "$sources_file"; then
@ -136,7 +136,7 @@ while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_su
if [ -f "$source_list" ]; then
while IFS= read -r name; do
[ -n "$name" ] || continue
rm -rf "codex/skills/$name"
rm -rf "skills/$name"
done < "$source_list"
fi
done < "$sources_file"
@ -169,13 +169,13 @@ while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_su
exit 1
fi
rm -rf "codex/skills/thirdparty/$name"
cp -R "$dir" "codex/skills/thirdparty/$name"
rm -rf "skills/thirdparty/$name"
cp -R "$dir" "skills/thirdparty/$name"
names+=("$name")
owners["$name"]="$source_id"
done
;;
render_codex_skill)
render_skill)
name="$output_name"
if [ -n "${owners[$name]:-}" ] && [ "${owners[$name]}" != "$source_id" ]; then
echo "ERROR: duplicate third-party skill name: $name" >&2
@ -186,7 +186,7 @@ while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_su
exit 1
fi
render_codex_skill "$snapshot_root" "codex/skills/thirdparty/$name" "$platform_config" "$template_root" "$data_dir" "$scripts_dir"
render_skill "$snapshot_root" "skills/thirdparty/$name" "$platform_config" "$template_root" "$data_dir" "$scripts_dir"
names+=("$name")
owners["$name"]="$source_id"
;;
@ -199,7 +199,7 @@ while IFS=$'\x1f' read -r source_id snapshot_dir sync_mode source_list skills_su
printf "%s\n" "${names[@]}" | sort > "$source_list"
done < "$sources_file"
git add codex/skills
git add skills
if git diff --cached --quiet; then
echo "No third-party skills to sync."

View File

@ -6,7 +6,7 @@
"upstream_ref": "main",
"snapshot_dir": "superpowers",
"sync_mode": "copy_skill_dirs",
"source_list": "codex/skills/thirdparty/.sources/superpowers.list",
"source_list": "skills/thirdparty/.sources/superpowers.list",
"skills_subdir": "skills",
"remove_paths": ["skills/ui-ux-pro-max"]
},
@ -15,8 +15,8 @@
"upstream_repo": "https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git",
"upstream_ref": "main",
"snapshot_dir": "ui-ux-pro-max",
"sync_mode": "render_codex_skill",
"source_list": "codex/skills/thirdparty/.sources/ui-ux-pro-max.list",
"sync_mode": "render_skill",
"source_list": "skills/thirdparty/.sources/ui-ux-pro-max.list",
"output_name": "ui-ux-pro-max",
"platform_config": "src/ui-ux-pro-max/templates/platforms/codex.json",
"template_root": "src/ui-ux-pro-max/templates",
@ -29,7 +29,7 @@
"upstream_ref": "main",
"snapshot_dir": "andrej-karpathy-skills",
"sync_mode": "copy_skill_dirs",
"source_list": "codex/skills/thirdparty/.sources/andrej-karpathy-skills.list",
"source_list": "skills/thirdparty/.sources/andrej-karpathy-skills.list",
"skills_subdir": "skills"
}
]

View File

@ -1,4 +1,4 @@
node_modules
tmp
reports
codex/skills/thirdparty
skills/thirdparty

View File

@ -1 +1 @@
codex/skills/**
skills/**

View File

@ -116,7 +116,7 @@ Layer 1: rulesets/ (≤50 行/语言,模板源)
├─ 核心约束与安全红线
└─ 指向 Skills 和 docs
Layer 2: codex/skills/ (按需加载,$skill-name 触发)
Layer 2: skills/ (按需加载,$skill-name 触发)
├─ commit-message: 提交信息规范
├─ style-cleanup: 代码风格整理
├─ bulk-refactor-workflow: 批量重构流程
@ -158,7 +158,7 @@ Layer 3: docs/ (权威静态文档)
## SKILLSCodex CLI / Claude Code
本仓库内置一组 AI agent skills`codex/skills/`),支持 Codex CLI 和 Claude Code用于按需加载的工作流与知识库。
本仓库内置一组 AI agent skills`skills/`),支持 Codex CLI 和 Claude Code用于按需加载的工作流与知识库。
TSL 相关问题直接查阅 `rulesets/tsl/index.md``docs/tsl/`

View File

@ -10,7 +10,7 @@
| Codex CLI | `~/.agents/skills/` |
| Claude Code | `~/.claude/skills/` |
> 提示:本仓库将 skills 以可分发的形式放在 `codex/skills/`,并提供脚本一键安装到对应平台目录。
> 提示:本仓库将 skills 以可分发的形式放在 `skills/`,并提供脚本一键安装到对应平台目录。
---
@ -40,7 +40,7 @@ skills = true
本 Playbook 以“可分发、可安装”的方式提供 skills
```txt
codex/skills/
skills/
<skill-name>/ # 本仓库自维护
SKILL.md
references/ # 可选:拆分参考文档
@ -164,7 +164,7 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
## 8. 本 Playbook 原生 skills
位于 `codex/skills/`Playbook 自维护部分),当前共 3 个。
位于 `skills/`Playbook 自维护部分),当前共 3 个。
第三方 skills 来源见第 9 节。
### 语言特定 Skills
@ -181,7 +181,7 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
## 9. Third-party Skills
来源:`codex/skills/thirdparty/.sources/`(第三方来源清单目录)。
来源:`skills/thirdparty/.sources/`(第三方来源清单目录)。
- `superpowers.list`
- `ui-ux-pro-max.list`
@ -190,7 +190,7 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
部署链路:
- `thirdparty/skill` 分支保存上游快照 `andrej-karpathy-skills/`
- 自动同步后,`skills/karpathy-guidelines/` 会落到 `codex/skills/karpathy-guidelines/`
- 自动同步后,`karpathy-guidelines/` 会落到仓库内 `skills/thirdparty/karpathy-guidelines/`
- 运行 `[install_skills]` 时,再复制到目标平台 skills 目录(`~/.agents/skills/` 或 `~/.claude/skills/`
- 该 skill 本身不依赖 Playbook 文档路径重写,也不需要像 `ui-ux-pro-max` 那样额外渲染

View File

@ -477,7 +477,7 @@ def vendor_action(config: dict, context: dict) -> int:
copy2(gitattributes_src, dest_prefix / ".gitattributes")
copytree(PLAYBOOK_ROOT / "scripts", dest_prefix / "scripts")
copytree(PLAYBOOK_ROOT / "codex", dest_prefix / "codex")
copytree(PLAYBOOK_ROOT / "skills", dest_prefix / "skills")
copy2(PLAYBOOK_ROOT / "SKILLS.md", dest_prefix / "SKILLS.md")
common_docs = PLAYBOOK_ROOT / "docs/common"
@ -1253,7 +1253,7 @@ def install_skills_action(config: dict, context: dict) -> int:
if not agents_home.is_absolute():
agents_home = (context["project_root"] / agents_home).resolve()
skills_src_root = PLAYBOOK_ROOT / "codex/skills"
skills_src_root = PLAYBOOK_ROOT / "skills"
skills_thirdparty_root = skills_src_root / "thirdparty"
if not skills_src_root.is_dir():
print(f"ERROR: skills source not found: {skills_src_root}", file=sys.stderr)

View File

Can't render this file because it contains an unexpected character in line 10 and column 56.

View File

Can't render this file because it contains an unexpected character in line 13 and column 56.

View File

Can't render this file because it contains an unexpected character in line 2 and column 134.

View File

Can't render this file because it contains an unexpected character in line 2 and column 209.

View File

Can't render this file because it contains an unexpected character in line 6 and column 94.

View File

Can't render this file because it contains an unexpected character in line 8 and column 193.

View File

Can't render this file because it contains an unexpected character in line 4 and column 188.

View File

@ -325,7 +325,7 @@ python scripts/playbook.py -config playbook.toml
```text
playbook/
├── rulesets/ # 语言级硬规则 → 部署到 .agents/
├── codex/skills/ # 按需加载的技能thirdparty/ 为第三方同步)
├── skills/ # 按需加载的技能thirdparty/ 为第三方同步)
├── docs/ # 权威静态文档
├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等
└── scripts/

View File

@ -397,6 +397,9 @@ skills = ["brainstorming"]
manifest_src = ROOT / ".gitea" / "ci" / "thirdparty_skills.json"
manifest_dst = repo / ".gitea" / "ci" / "thirdparty_skills.json"
manifest_dst.write_text(manifest_src.read_text(encoding="utf-8"), encoding="utf-8")
sync_src = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
sync_dst = repo / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
sync_dst.write_text(sync_src.read_text(encoding="utf-8"), encoding="utf-8")
sync_result = subprocess.run(
["bash", ".gitea/ci/sync_thirdparty_skills.sh"],

View File

@ -0,0 +1,185 @@
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_DEPLOY_ROOT = Path("docs/standards/playbook")
CUSTOM_DEPLOY_ROOT = Path("custom/playbook")
REPO_COPY_IGNORE = shutil.ignore_patterns(
".git",
".venv",
"node_modules",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
)
def run_script(
script: Path, *args: str, cwd: Path | None = None
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(script), *args],
capture_output=True,
text=True,
cwd=cwd,
)
def write_config(root: Path, name: str, body: str) -> Path:
config_path = root / name
config_path.write_text(body.strip() + "\n", encoding="utf-8")
return config_path
def copy_repo(target: Path) -> Path:
shutil.copytree(ROOT, target, ignore=REPO_COPY_IGNORE)
return target
class DeploymentRoutesE2ETests(unittest.TestCase):
def assert_core_project_files(self, project_root: Path) -> None:
expected = [
"AGENTS.md",
"AGENT_RULES.md",
"AGENT_RULES.local.md",
"memory-bank/progress.md",
"docs/prompts/system/agent-behavior.md",
".agents/index.md",
".agents/tsl/index.md",
".agents/markdown/index.md",
".gitattributes",
]
for rel in expected:
with self.subTest(path=rel):
self.assertTrue((project_root / rel).exists(), f"missing: {rel}")
def assert_docs_prefix(self, project_root: Path, docs_prefix: str) -> None:
agents_index = (project_root / ".agents" / "index.md").read_text(encoding="utf-8")
self.assertIn("`.agents/tsl/index.md`", agents_index)
self.assertIn("`.agents/markdown/index.md`", agents_index)
self.assertIn(f"- {docs_prefix}", agents_index)
tsl_index = (project_root / ".agents" / "tsl" / "index.md").read_text(
encoding="utf-8"
)
self.assertIn(f"`{docs_prefix}/tsl/index.md`", tsl_index)
self.assertNotIn("`docs/tsl/index.md`", tsl_index)
def test_subtree_style_deployment_syncs_project_files(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
project_root = tmp_root / "project"
playbook_root = project_root / DEFAULT_DEPLOY_ROOT
project_root.mkdir()
copy_repo(playbook_root)
config_path = write_config(
project_root,
"playbook.toml",
"""
[playbook]
project_root = "."
[sync_rules]
no_backup = true
[sync_memory_bank]
project_name = "Demo"
[sync_prompts]
no_backup = true
[sync_standards]
langs = ["tsl", "markdown"]
no_backup = true
""",
)
result = run_script(
playbook_root / "scripts" / "playbook.py",
"-config",
str(config_path),
cwd=project_root,
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
self.assert_core_project_files(project_root)
self.assert_docs_prefix(project_root, "docs/standards/playbook/docs")
claude_md = project_root / "CLAUDE.md"
self.assertTrue(claude_md.is_file())
text = claude_md.read_text(encoding="utf-8")
self.assertIn("@AGENTS.md", text)
self.assertIn("@AGENT_RULES.md", text)
self.assertIn("<!-- playbook:claude:start -->", text)
def test_external_clone_deployment_vendors_snapshot_and_updates_claude(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
external_clone = tmp_root / "playbook"
project_root = tmp_root / "project"
copy_repo(external_clone)
project_root.mkdir()
claude_md = project_root / ".claude" / "CLAUDE.md"
claude_md.parent.mkdir()
claude_md.write_text("# Existing Claude\n\nKeep this.\n", encoding="utf-8")
config_path = write_config(
project_root,
"playbook.toml",
f"""
[playbook]
project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
[vendor]
langs = ["tsl", "markdown"]
[sync_rules]
no_backup = true
[sync_memory_bank]
project_name = "Demo"
[sync_prompts]
no_backup = true
[sync_standards]
langs = ["tsl", "markdown"]
no_backup = true
""",
)
result = run_script(
external_clone / "scripts" / "playbook.py",
"-config",
str(config_path),
cwd=project_root,
)
self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr)
snapshot_root = project_root / CUSTOM_DEPLOY_ROOT
self.assertTrue((snapshot_root / "SOURCE.md").is_file())
self.assertTrue((snapshot_root / "scripts" / "playbook.py").is_file())
self.assertTrue((snapshot_root / "templates" / "README.md").is_file())
self.assert_core_project_files(project_root)
self.assert_docs_prefix(project_root, "custom/playbook/docs")
text = claude_md.read_text(encoding="utf-8")
self.assertIn("Keep this.", text)
self.assertIn("@../AGENTS.md", text)
self.assertIn("@../AGENT_RULES.md", text)
self.assertEqual(text.count("<!-- playbook:claude:start -->"), 1)
self.assertFalse((project_root / "CLAUDE.md").exists())
if __name__ == "__main__":
unittest.main()

View File

@ -14,9 +14,9 @@ LEGACY_WORKFLOW = ROOT / ".gitea" / "workflows" / "update-thirdparty-superpowers
UPDATE_SCRIPT = ROOT / ".gitea" / "ci" / "update_thirdparty_skills.sh"
SYNC_SCRIPT = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh"
SKILLS_MD = ROOT / "SKILLS.md"
SUPERPOWERS_LIST = ROOT / "codex" / "skills" / "thirdparty" / ".sources" / "superpowers.list"
UI_UX_PRO_MAX_LIST = ROOT / "codex" / "skills" / "thirdparty" / ".sources" / "ui-ux-pro-max.list"
UI_UX_PRO_MAX_DIR = ROOT / "codex" / "skills" / "thirdparty" / "ui-ux-pro-max"
SUPERPOWERS_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "superpowers.list"
UI_UX_PRO_MAX_LIST = ROOT / "skills" / "thirdparty" / ".sources" / "ui-ux-pro-max.list"
UI_UX_PRO_MAX_DIR = ROOT / "skills" / "thirdparty" / "ui-ux-pro-max"
def load_manifest() -> dict:
@ -58,13 +58,13 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
self.assertEqual(karpathy["snapshot_dir"], "andrej-karpathy-skills")
self.assertEqual(karpathy["skills_subdir"], "skills")
self.assertEqual(
karpathy["source_list"], "codex/skills/thirdparty/.sources/andrej-karpathy-skills.list"
karpathy["source_list"], "skills/thirdparty/.sources/andrej-karpathy-skills.list"
)
def test_ui_ux_pro_max_uses_render_codex_skill_sync_mode(self):
def test_ui_ux_pro_max_uses_render_skill_sync_mode(self):
data = load_manifest()
ui_skill = next(item for item in data["sources"] if item["id"] == "ui-ux-pro-max")
self.assertEqual(ui_skill["sync_mode"], "render_codex_skill")
self.assertEqual(ui_skill["sync_mode"], "render_skill")
self.assertEqual(ui_skill["snapshot_dir"], "ui-ux-pro-max")
def test_superpowers_manifest_prunes_non_superpowers_paths(self):
@ -105,7 +105,7 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
def test_skills_doc_points_to_generic_thirdparty_sources(self):
text = SKILLS_MD.read_text(encoding="utf-8")
self.assertIn("## 9. Third-party Skills", text)
self.assertIn("来源:`codex/skills/thirdparty/.sources/`(第三方来源清单目录)。", text)
self.assertIn("来源:`skills/thirdparty/.sources/`(第三方来源清单目录)。", text)
self.assertNotIn("Third-party Skills (superpowers)", text)
def test_superpowers_and_ui_ux_pro_max_source_lists_exist(self):
@ -160,6 +160,7 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
self.assertEqual(set_remote.returncode, 0, msg=set_remote.stderr)
shutil.copy2(MANIFEST, work / ".gitea" / "ci" / "thirdparty_skills.json")
shutil.copy2(SYNC_SCRIPT, work / ".gitea" / "ci" / "sync_thirdparty_skills.sh")
sync_result = run_command("bash", ".gitea/ci/sync_thirdparty_skills.sh", cwd=work)
self.assertEqual(
@ -169,10 +170,10 @@ class ThirdpartySkillsPipelineTests(unittest.TestCase):
)
generated_list = (
work / "codex" / "skills" / "thirdparty" / ".sources" / "andrej-karpathy-skills.list"
work / "skills" / "thirdparty" / ".sources" / "andrej-karpathy-skills.list"
)
generated_skill = (
work / "codex" / "skills" / "thirdparty" / "karpathy-guidelines" / "SKILL.md"
work / "skills" / "thirdparty" / "karpathy-guidelines" / "SKILL.md"
)
self.assertTrue(generated_list.is_file())
self.assertTrue(generated_skill.is_file())

View File

@ -7,7 +7,8 @@ README = ROOT / "README.md"
PLAYBOOK_EXAMPLE = ROOT / "playbook.toml.example"
SKILLS_DOC = ROOT / "SKILLS.md"
TEMPLATES_CI_README = ROOT / "templates" / "ci" / "README.md"
REMOVED_TSL_GUIDE = ROOT / "codex" / "skills" / "tsl-guide"
TEMPLATES_README = ROOT / "templates" / "README.md"
REMOVED_TSL_GUIDE = ROOT / "skills" / "tsl-guide"
class TslEntrypointsConsistencyTests(unittest.TestCase):
def test_ruleset_lists_canonical_tsl_layers(self):
@ -44,6 +45,19 @@ class TslEntrypointsConsistencyTests(unittest.TestCase):
self.assertNotIn("vendoring", SKILLS_DOC.read_text(encoding="utf-8"))
self.assertNotIn("vendoring", TEMPLATES_CI_README.read_text(encoding="utf-8"))
def test_repo_docs_use_platform_neutral_skills_source_dir(self):
readme_text = README.read_text(encoding="utf-8")
skills_text = SKILLS_DOC.read_text(encoding="utf-8")
templates_text = TEMPLATES_README.read_text(encoding="utf-8")
self.assertIn("`skills/`", readme_text)
self.assertIn("`skills/`", skills_text)
self.assertIn("├── skills/", templates_text)
self.assertNotIn("`codex/skills/`", readme_text)
self.assertNotIn("`codex/skills/`", skills_text)
self.assertNotIn("├── codex/skills/", templates_text)
def test_repo_no_longer_ships_tsl_guide_skill(self):
self.assertFalse(REMOVED_TSL_GUIDE.exists())
self.assertNotIn("tsl-guide", README.read_text(encoding="utf-8"))

View File

@ -40,6 +40,8 @@ langs = ["tsl"]
self.assertTrue((snapshot / "templates/README.md").is_file())
self.assertTrue((snapshot / "templates/memory-bank").is_dir())
self.assertTrue((snapshot / "templates/prompts").is_dir())
self.assertTrue((snapshot / "skills").is_dir())
self.assertFalse((snapshot / "codex").exists())
if __name__ == "__main__":