🔧 feat(skills): install to agents home

This commit is contained in:
csh 2026-02-05 15:26:48 +08:00
parent 8b75747587
commit 872c1afc24
5 changed files with 41 additions and 16 deletions

View File

@ -4,8 +4,8 @@
并给出与本 Playbook`docs/` + `rulesets/`)配套的技能编写建议与内置技能清单。 并给出与本 Playbook`docs/` + `rulesets/`)配套的技能编写建议与内置技能清单。
> 提示Codex skills 是“按用户安装”的(默认在 > 提示Codex skills 是“按用户安装”的(默认在
> `~/.codex/skills`)。本仓库将 skills 以可分发的形式放在 > `~/.agents/skills`)。本仓库将 skills 以可分发的形式放在
> `codex/skills/`,并提供脚本一键安装到你的 `CODEX_HOME`。 > `codex/skills/`,并提供脚本一键安装到你的 `~/.agents`。
--- ---
@ -38,14 +38,14 @@ codex/skills/
最终安装到本机后,对应路径为: 最终安装到本机后,对应路径为:
```txt ```txt
$CODEX_HOME/skills/<skill-name>/SKILL.md ~/.agents/skills/<skill-name>/SKILL.md
``` ```
--- ---
## 3. 安装到本机(推荐) ## 3. 安装到本机(推荐)
使用统一入口 `playbook.py` 安装 skills会把 `codex/skills/*` 复制到 `$CODEX_HOME/skills/` 使用统一入口 `playbook.py` 安装 skills会把 `codex/skills/*` 复制到 `~/.agents/skills/`
```toml ```toml
# playbook.toml # playbook.toml
@ -54,7 +54,7 @@ project_root = "."
[install_skills] [install_skills]
mode = "all" # list|all mode = "all" # list|all
codex_home = "~/.codex" agents_home = "~/.agents"
``` ```
```bash ```bash
@ -74,10 +74,10 @@ skills = ["style-cleanup", "commit-message"]
```toml ```toml
[install_skills] [install_skills]
mode = "all" mode = "all"
codex_home = "./.codex" agents_home = "./.agents"
``` ```
> 注意Codex 只会从 `CODEX_HOME` 加载 skills使用本地安装时启动 Codex 需设置同样的 `CODEX_HOME` > 注意Codex 默认从 `~/.agents/skills` 加载 skills使用本地安装时需要确保 Codex 能发现该路径
如果你的项目通过 `git subtree` vendoring 本 Playbook推荐前缀 如果你的项目通过 `git subtree` vendoring 本 Playbook推荐前缀
`docs/standards/playbook`),则在目标项目里执行: `docs/standards/playbook`),则在目标项目里执行:

View File

@ -9,7 +9,7 @@ description: Use when creating new skills, editing existing skills, or verifying
**Writing skills IS Test-Driven Development applied to process documentation.** **Writing skills IS Test-Driven Development applied to process documentation.**
**Personal skills live in agent-specific directories (`~/.claude/skills` for Claude Code, `~/.codex/skills` for Codex)** **Personal skills live in agent-specific directories (`~/.claude/skills` for Claude Code, `~/.agents/skills` for Codex)**
You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes). You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes).

View File

@ -14,6 +14,7 @@
[sync_rules] [sync_rules]
# 同步 AGENT_RULES.md配置节存在即启用 # 同步 AGENT_RULES.md配置节存在即启用
# force = false # 可选:覆盖已有文件 # force = false # 可选:覆盖已有文件
# no_backup = false # 可选:跳过备份
[sync_memory_bank] [sync_memory_bank]
# 同步 memory-bank/(配置节存在即启用) # 同步 memory-bank/(配置节存在即启用)
@ -32,11 +33,12 @@
[sync_standards] [sync_standards]
# langs = ["tsl", "cpp"] # 必填:要同步的语言 # langs = ["tsl", "cpp"] # 必填:要同步的语言
# gitattr_mode = "append" # append(补全缺失)|overwrite(覆盖)|block(插入块)|skip(跳过) # gitattr_mode = "append" # append(补全缺失)|overwrite(覆盖)|block(插入块)|skip(跳过)
# no_backup = false # 可选:跳过备份(.agents/.gitattributes
[install_skills] [install_skills]
# mode = "list" # list|all # mode = "list" # list|all
# skills = ["brainstorming"] # mode=list 时必填 # skills = ["brainstorming"] # mode=list 时必填
# codex_home = "~/.codex" # 可选:默认 ~/.codex # agents_home = "~/.agents" # 可选:默认 ~/.agents
[format_md] [format_md]
# tool = "prettier" # 仅支持 prettier # tool = "prettier" # 仅支持 prettier

View File

@ -1056,16 +1056,19 @@ def normalize_globs(raw: object) -> list[str]:
def install_skills_action(config: dict, context: dict) -> int: def install_skills_action(config: dict, context: dict) -> int:
mode = str(config.get("mode", "list")).lower() mode = str(config.get("mode", "list")).lower()
codex_home = Path(config.get("codex_home", "~/.codex")).expanduser() if "codex_home" in config:
if not codex_home.is_absolute(): print("ERROR: codex_home is no longer supported; use agents_home", file=sys.stderr)
codex_home = (context["project_root"] / codex_home).resolve() return 2
agents_home = Path(config.get("agents_home", "~/.agents")).expanduser()
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 / "codex/skills"
if not skills_src_root.is_dir(): if not skills_src_root.is_dir():
print(f"ERROR: skills source not found: {skills_src_root}", file=sys.stderr) print(f"ERROR: skills source not found: {skills_src_root}", file=sys.stderr)
return 2 return 2
skills_dst_root = codex_home / "skills" skills_dst_root = agents_home / "skills"
ensure_dir(skills_dst_root) ensure_dir(skills_dst_root)
if mode == "all": if mode == "all":

View File

@ -126,6 +126,27 @@ langs = ["tsl"]
self.assertEqual(block[bullet_idx - 1], "") self.assertEqual(block[bullet_idx - 1], "")
def test_install_skills(self): def test_install_skills(self):
with tempfile.TemporaryDirectory() as tmp_dir:
target = Path(tmp_dir) / "agents"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
[install_skills]
agents_home = "{target}"
mode = "list"
skills = ["brainstorming"]
"""
config_path = Path(tmp_dir) / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
result = run_cli("-config", str(config_path))
skill_file = target / "skills/brainstorming/SKILL.md"
self.assertEqual(result.returncode, 0)
self.assertTrue(skill_file.is_file())
def test_install_skills_rejects_codex_home(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
target = Path(tmp_dir) / "codex" target = Path(tmp_dir) / "codex"
config_body = f""" config_body = f"""
@ -142,9 +163,8 @@ skills = ["brainstorming"]
result = run_cli("-config", str(config_path)) result = run_cli("-config", str(config_path))
skill_file = target / "skills/brainstorming/SKILL.md" self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.returncode, 0) self.assertIn("codex_home", result.stdout + result.stderr)
self.assertTrue(skill_file.is_file())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()