refactor: fold ack kit into skill
This commit is contained in:
+1
-1
@@ -66,7 +66,7 @@ skiff bootstrap
|
||||
|------|------|
|
||||
| `skiff bootstrap` | 将本项目的 `skiff` skill 全局安装到所有 Agent |
|
||||
| `skiff update` | 在 `~/.skills` 执行 `git pull`,更新 skiff 自身 |
|
||||
| `skiff kit init <name> [--project DIR] [--copy]` | 在项目的 `docs/<name>/` 初始化规范包;默认软链接到 SSOT |
|
||||
| `skiff init <name> [--project DIR]` | 使用 builtin skill 自带模板初始化项目状态 |
|
||||
|
||||
### 全局安装(自研 skill)
|
||||
|
||||
|
||||
+23
-43
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
@@ -18,7 +17,6 @@ from skiff.paths import (
|
||||
DRAFTS_DIR,
|
||||
EXTERNALS_DIR,
|
||||
CONFIG_FILE,
|
||||
KITS_DIR,
|
||||
SKILLS_DIR,
|
||||
SKILLS_HOME,
|
||||
TEMPLATE_DIR,
|
||||
@@ -83,7 +81,7 @@ def _project_root(explicit: str | None = None) -> Path:
|
||||
return find_repo_root() or Path.cwd()
|
||||
|
||||
|
||||
def _render_kit_template(source: Path, destination: Path, values: dict[str, str]) -> None:
|
||||
def _render_template(source: Path, destination: Path, values: dict[str, str]) -> None:
|
||||
content = source.read_text(encoding="utf-8")
|
||||
for placeholder, value in values.items():
|
||||
content = content.replace(placeholder, value)
|
||||
@@ -1132,66 +1130,55 @@ def cmd_doctor(args: argparse.Namespace) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_kit_init(args: argparse.Namespace) -> None:
|
||||
"""将 owned kit 初始化到目标项目。"""
|
||||
def cmd_init(args: argparse.Namespace) -> None:
|
||||
"""使用 builtin skill 自带的模板初始化目标项目状态。"""
|
||||
ensure_skills_home()
|
||||
kit_source = KITS_DIR / args.name
|
||||
if not kit_source.is_dir():
|
||||
available = sorted(path.name for path in KITS_DIR.iterdir() if path.is_dir()) if KITS_DIR.is_dir() else []
|
||||
suffix = f";可用 kit: {', '.join(available)}" if available else ""
|
||||
raise SystemExit(f"kit 不存在: {args.name}{suffix}")
|
||||
skill_source = SKILLS_DIR / args.name
|
||||
if not (skill_source / "SKILL.md").is_file():
|
||||
raise SystemExit(f"builtin skill 不存在: {args.name}")
|
||||
|
||||
project = _project_root(args.project)
|
||||
if not project.is_dir():
|
||||
raise SystemExit(f"项目目录不存在: {project}")
|
||||
destination = project / "docs" / args.name
|
||||
kit_target = destination / "kit"
|
||||
project_file = destination / "project.md"
|
||||
tasks_file = destination / "tasks.yaml"
|
||||
managed_targets = (kit_target, project_file, tasks_file)
|
||||
managed_targets = (project_file, tasks_file)
|
||||
existing = [path for path in managed_targets if path.exists() or path.is_symlink()]
|
||||
if existing:
|
||||
paths = ", ".join(str(path.relative_to(project)) for path in existing)
|
||||
raise SystemExit(f"拒绝覆盖已有路径: {paths}")
|
||||
|
||||
project_template = kit_source / "templates" / "project.template.md"
|
||||
tasks_template = kit_source / "templates" / "tasks.template.yaml"
|
||||
project_template = skill_source / "templates" / "project.template.md"
|
||||
tasks_template = skill_source / "templates" / "tasks.template.yaml"
|
||||
missing = [path for path in (project_template, tasks_template) if not path.is_file()]
|
||||
if missing:
|
||||
paths = ", ".join(str(path.relative_to(SKILLS_HOME)) for path in missing)
|
||||
raise SystemExit(f"kit 缺少初始化模板: {paths}")
|
||||
raise SystemExit(f"skill 缺少初始化模板: {paths}")
|
||||
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
if args.copy:
|
||||
shutil.copytree(kit_source, kit_target)
|
||||
mode = "copy"
|
||||
else:
|
||||
kit_target.symlink_to(kit_source.resolve(), target_is_directory=True)
|
||||
mode = "symlink"
|
||||
|
||||
version_file = kit_source / "VERSION"
|
||||
kit_version = version_file.read_text(encoding="utf-8").strip() if version_file.is_file() else "unknown"
|
||||
version_file = skill_source / "VERSION"
|
||||
ack_version = version_file.read_text(encoding="utf-8").strip() if version_file.is_file() else "unknown"
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
values = {
|
||||
"<project_name>": project.name,
|
||||
"<repo_path>": str(project),
|
||||
"<dev_worktree>": str(project),
|
||||
"<overlay_file_path>": f"docs/{args.name}/project.md",
|
||||
"<kit_version>": kit_version,
|
||||
"<接入时的 ack 版本,见 kit 根 VERSION>": kit_version,
|
||||
"<ack_version>": ack_version,
|
||||
"<接入时的 ack skill 版本>": ack_version,
|
||||
"<YYYY-MM-DDTHH:mm:ss+TZ>": now,
|
||||
}
|
||||
_render_kit_template(project_template, project_file, values)
|
||||
_render_kit_template(tasks_template, tasks_file, values)
|
||||
_render_template(project_template, project_file, values)
|
||||
_render_template(tasks_template, tasks_file, values)
|
||||
|
||||
validator = kit_target / "scripts" / "validate_tasks.py"
|
||||
validator = skill_source / "scripts" / "validate_tasks.py"
|
||||
if validator.is_file():
|
||||
subprocess.run([sys.executable, str(validator), str(tasks_file)], check=True)
|
||||
|
||||
_print(f"✓ kit 初始化完成: {args.name}")
|
||||
_print(f"✓ skill 项目状态初始化完成: {args.name}")
|
||||
_print(f" 项目: {project}")
|
||||
_print(f" 模式: {mode}")
|
||||
_print(f" kit: {kit_target}")
|
||||
_print(f" 覆盖层: {project_file}")
|
||||
_print(f" 任务板: {tasks_file}")
|
||||
_print("下一步: 填写 project.md 中的项目命令、路径权限和 Base URL")
|
||||
@@ -1363,23 +1350,16 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
p_finalize.add_argument("name", help="skill 名称")
|
||||
p_finalize.set_defaults(func=cmd_finalize)
|
||||
|
||||
p_init = sub.add_parser("init", help="使用 skill 模板初始化项目状态")
|
||||
p_init.add_argument("name", help="skill 名称")
|
||||
p_init.add_argument("--project", help="项目根目录(默认自动检测或当前目录)")
|
||||
p_init.set_defaults(func=cmd_init)
|
||||
|
||||
p_doctor = sub.add_parser("doctor", help="软链健康检查")
|
||||
p_doctor.add_argument("-a", "--agent", dest="agents", nargs="+", action="append")
|
||||
p_doctor.add_argument("--fix", action="store_true", help="自动修复可修复的软链")
|
||||
p_doctor.set_defaults(func=cmd_doctor)
|
||||
|
||||
p_kit = sub.add_parser("kit", help="管理项目规范包")
|
||||
kit_sub = p_kit.add_subparsers(dest="kit_command", required=True)
|
||||
p_kit_init = kit_sub.add_parser("init", help="在项目中初始化 kit")
|
||||
p_kit_init.add_argument("name", help="kit 名称")
|
||||
p_kit_init.add_argument("--project", help="项目根目录(默认自动检测或当前目录)")
|
||||
p_kit_init.add_argument(
|
||||
"--copy",
|
||||
action="store_true",
|
||||
help="复制 kit,而不是创建指向 SSOT 的软链接",
|
||||
)
|
||||
p_kit_init.set_defaults(func=cmd_kit_init)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from pathlib import Path
|
||||
HOME = Path.home()
|
||||
SKILLS_HOME = HOME / ".skills"
|
||||
SKILLS_DIR = SKILLS_HOME / "skills"
|
||||
KITS_DIR = SKILLS_HOME / "kits"
|
||||
TEMPLATE_DIR = SKILLS_DIR / "_template"
|
||||
DRAFTS_DIR = SKILLS_HOME / ".drafts"
|
||||
REGISTRY_FILE = SKILLS_HOME / "registry.yaml"
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
# Skill 来源模型
|
||||
|
||||
## 背景
|
||||
|
||||
skiff 当前使用 `owned` 表示本仓库 `skills/` 中维护的 Skill,同时还支持
|
||||
`registry` 外部仓库和用户配置的 custom source。
|
||||
|
||||
这些名称混合了不同维度:
|
||||
|
||||
- `owned` 表示所有权。
|
||||
- `registry` 表示来源配置存放在哪里。
|
||||
- `external` 表示内容不在当前仓库。
|
||||
- `collection` 表示一个来源中包含多个 Skill。
|
||||
|
||||
它们不能作为同一层级的互斥类型。例如 custom source 既可能来自外部 Git 仓库,
|
||||
也可能是 collection;因此 `custom` 与 `external` 并不互斥。
|
||||
|
||||
本文建议将用户可见的来源注册方式统一为三类:
|
||||
|
||||
- `builtin`:随当前 skiff 仓库提供的 Skill。
|
||||
- `catalog:<name>`:由 skiff 自带目录预先登记的来源。
|
||||
- `custom:<name>`:用户在本机显式配置的命名来源。
|
||||
|
||||
来源注册方式与获取方式、仓库布局相互独立。builtin、catalog 和 custom 中的任意
|
||||
来源都可以包含一个或多个 Skill;除 builtin 外,catalog 和 custom 都可以使用
|
||||
Git 仓库或本地目录。
|
||||
|
||||
## 当前模型
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
S[skiff 可发现的 Skills]
|
||||
|
||||
S --> O["owned<br/>~/.skills/skills/*"]
|
||||
S --> C["custom source<br/>~/.config/skiff/config.yaml"]
|
||||
S --> R["registry<br/>registry.yaml"]
|
||||
|
||||
O --> O1["本仓库维护<br/>随 skiff 一起分发"]
|
||||
|
||||
C --> C1["本地目录<br/>--local PATH"]
|
||||
C --> C2["指定 Git 仓库<br/>repo + skills_path"]
|
||||
|
||||
R --> R1["外部单 Skill 仓库"]
|
||||
R --> R2["外部 Skill Collection"]
|
||||
R1 --> E["~/.local/share/skills/externals/"]
|
||||
R2 --> E
|
||||
```
|
||||
|
||||
当前实现中:
|
||||
|
||||
- `list` 和 `status` 支持 owned、registry 和 custom source。
|
||||
- `resolve_skill_source` 可以解析三种来源并处理同名歧义。
|
||||
- `select` 只组装 owned 和 registry 条目,尚未展示 custom source。
|
||||
- `.skills.yaml` 默认将未声明来源的 Skill 解释为 `owned`。
|
||||
- custom source 的 `skills_path` 已经可以包含多个 Skill,本质上也是 collection。
|
||||
|
||||
## 推荐模型
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Skill Provider]
|
||||
|
||||
A --> B["builtin<br/>仓库内隐式注册"]
|
||||
A --> C["catalog:waza<br/>skiff 预置目录"]
|
||||
A --> D["custom:company<br/>用户本机配置"]
|
||||
|
||||
B --> B1["skills/ack"]
|
||||
B --> B2["skills/skiff"]
|
||||
B --> B3["skills/deb-publisher"]
|
||||
|
||||
C --> C1["Git 或本地目录"]
|
||||
C1 --> C2["单 Skill"]
|
||||
C1 --> C3["Collection"]
|
||||
C3 --> C4["waza/think"]
|
||||
C3 --> C5["waza/check"]
|
||||
|
||||
D --> D1["Git 或本地目录"]
|
||||
D1 --> D2["单 Skill"]
|
||||
D1 --> D3["Collection"]
|
||||
D3 --> D4["company/security-review"]
|
||||
D3 --> D5["company/release"]
|
||||
```
|
||||
|
||||
### 来源注册方式
|
||||
|
||||
| 类型 | 含义 | 配置来源 | 用户界面展示 |
|
||||
| --- | --- | --- | --- |
|
||||
| `builtin` | 随当前 skiff 仓库提供 | `skills/` | `builtin` |
|
||||
| `catalog` | skiff 预先登记、所有用户可发现的来源 | `catalog.yaml`,迁移前为 `registry.yaml` | `catalog:<name>` |
|
||||
| `custom` | 用户在本机显式注册的命名来源 | `~/.config/skiff/config.yaml` | `custom:<name>` |
|
||||
|
||||
`builtin` 比 `owned` 更适合作为用户可见名称,因为它表达 Skill 的分发位置和可用
|
||||
方式,而不是仓库的所有权关系。
|
||||
|
||||
`catalog` 比 `registry` 更准确:当前文件只是 skiff 随仓库维护的预置来源目录,
|
||||
并不是远程注册中心,也不是一种 Skill 来源协议。
|
||||
|
||||
### 三个正交维度
|
||||
|
||||
每个 provider 应由三个维度描述:
|
||||
|
||||
```text
|
||||
registration: builtin | catalog | custom
|
||||
transport: bundled | local | git
|
||||
layout: single | collection
|
||||
```
|
||||
|
||||
custom source 示例:
|
||||
|
||||
```text
|
||||
registration: custom
|
||||
name: company
|
||||
transport: local | git
|
||||
layout: single | collection
|
||||
```
|
||||
|
||||
catalog entry 示例:
|
||||
|
||||
```text
|
||||
registration: catalog
|
||||
name: waza
|
||||
transport: local | git
|
||||
layout: single | collection
|
||||
```
|
||||
|
||||
`external` 只适合描述物理位置,可作为 catalog 和部分 custom provider 的统称,
|
||||
不应成为与 builtin、custom 并列的持久化类型。
|
||||
|
||||
## `select` 展示
|
||||
|
||||
`skiff select` 应同时展示 builtin、catalog 和 custom Skill:
|
||||
|
||||
```text
|
||||
[ ] ack builtin
|
||||
[ ] deb-publisher builtin
|
||||
[-] company custom source
|
||||
[ ] company/release custom:company
|
||||
[ ] company/security-review custom:company
|
||||
[-] waza catalog source
|
||||
[ ] waza/check catalog:waza
|
||||
[ ] waza/think catalog:waza
|
||||
```
|
||||
|
||||
建议遵循以下规则:
|
||||
|
||||
- builtin Skill 直接显示 Skill 名称。
|
||||
- catalog 和 custom collection 都使用来源名称作为父节点。
|
||||
- catalog 和 custom Skill 都使用 `<source>/<skill>` 作为选择器键。
|
||||
- 单 Skill 来源可以省略父节点,但仍显示注册方式和来源名称。
|
||||
- 不同来源安装后名称相同时,继续拒绝同时选择,避免覆盖同一安装路径。
|
||||
|
||||
## 配置表示
|
||||
|
||||
新写入的 `.skills.yaml` 使用以下形式:
|
||||
|
||||
```yaml
|
||||
skills:
|
||||
- name: ack
|
||||
source: builtin
|
||||
- name: security-review
|
||||
source: company
|
||||
- name: think
|
||||
source: catalog:waza
|
||||
```
|
||||
|
||||
custom source 在 manifest 中继续保存其逻辑名称,例如 `company`。这样不同机器可以
|
||||
独立配置仓库地址,而项目只依赖稳定的来源名称。
|
||||
|
||||
## 兼容迁移
|
||||
|
||||
这是一次用户可见术语调整,应提供兼容层,避免已有项目立即失效:
|
||||
|
||||
1. 对外文档、CLI 输出和 selector 统一使用 `builtin`、`catalog:<name>` 和
|
||||
`custom:<name>`。
|
||||
2. 新生成的 `.skills.yaml` 对内置 Skill 写入 `source: builtin`。
|
||||
3. 读取旧 manifest 时继续接受 `source: owned`,并在解析时归一化为 `builtin`。
|
||||
4. CLI 参数在过渡期继续接受 `--source owned`,但帮助和输出只推荐 `builtin`。
|
||||
5. 读取旧 manifest 中的 `source: registry` 和 `registry: <name>`,归一化为
|
||||
`catalog:<name>`。
|
||||
6. `registry.yaml` 可以先保留文件名,仅将用户界面术语改为 catalog;单独迁移为
|
||||
`catalog.yaml` 时,应兼容读取旧文件。
|
||||
7. custom source 的逻辑名称和现有 `config.yaml` 结构保持不变。
|
||||
8. 将 `builtin`、`catalog` 和兼容别名 `owned`、`registry` 设为 custom source
|
||||
保留字。
|
||||
9. `select` 同步接入 custom Skill,并让 custom collection 与 catalog collection
|
||||
使用相同的父子展示逻辑。
|
||||
|
||||
## 影响范围
|
||||
|
||||
实施时预计涉及:
|
||||
|
||||
- `skiff/skills.py`:来源解析、归一化和 builtin 命名。
|
||||
- `skiff/project.py`:manifest 默认值、序列化与旧值兼容。
|
||||
- `skiff/sources.py`:来源保留字。
|
||||
- `skiff/registry.py`:逐步重命名为 catalog 概念。
|
||||
- `skiff/cli.py`:`list`、`status`、`add`、`select` 和输出文案。
|
||||
- `skiff/selector.py`:统一 catalog/custom collection 的父子展示。
|
||||
- CLI 与来源解析测试。
|
||||
- 根 README、`skiff/README.md`、`skills/skiff/SKILL.md` 和相关示例。
|
||||
|
||||
## 验证要求
|
||||
|
||||
实施完成后至少覆盖:
|
||||
|
||||
- builtin Skill 可以通过新名称列出、选择、安装和写入 manifest。
|
||||
- 旧的 `source: owned` manifest 仍可同步。
|
||||
- custom local source 和 custom Git source 都出现在 selector 中。
|
||||
- catalog 单 Skill 和 Collection 都保持可选择。
|
||||
- custom 单 Skill 和 Collection 都保持可选择。
|
||||
- 多来源同名 Skill 必须显式指定来源。
|
||||
- 两个选择最终安装为同一个名称时,selector 拒绝冲突。
|
||||
- `list`、`status`、`select` 对同一来源使用一致术语。
|
||||
|
||||
## 设计前提
|
||||
|
||||
本方案假设 `registry.yaml` 当前的真实职责是维护 skiff 预置的来源目录,而不是提供
|
||||
远程发布、版本解析或可信签名等注册中心能力。因此推荐逐步将用户可见概念改为
|
||||
`catalog`。如果未来实现真正的远程 registry,再单独定义其协议和与 catalog 的同步
|
||||
关系,不复用当前含义模糊的名称。
|
||||
Reference in New Issue
Block a user