Claude Skill

lov-version-management

根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。支持明确输入与结果回读。Use to manage package

LLM Mart · 0 points · 11 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lovstudio-skills-skills_version-management-0b16007.zip · 19 KB
Part of lovstudio/skills — 83 skills

Install

skills CLI npx skills add https://github.com/lovstudio/skills/tree/main/skills/version-management
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
Git git clone https://github.com/lovstudio/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.

README

版本管家 · Version Keeper

Version

根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。

安装

npx skills add lov-version-management -g -y

使用

  • 根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。
  • Manage package versions with Changesets.

支持自然语言调用,具体执行步骤与边界见 SKILL.md。分析、预览和实际修改分开处理。

配置与依赖

读取 共享 Profile;项目路径、输出目录与品牌来自当前请求或用户配置。 业务工具和目标系统依赖见工作流;公开文案使用 lov-branding-consistency 审校。

质量与案例

python3 scripts/validate_skill.py .

案例 记录真实迁移输入和输出,仅证明所声明的验收范围。 Skill Card 与 定价依据 说明使用边界;业务运行结果须逐次回读。

License

MIT

Skill manifest

版本管家 · Version Keeper

根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。

Triggers

Activate when

  • The user asks to use this Skill for its documented outcome.

  • “根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。”

  • “Manage package versions with Changesets.”

Do not activate when

  • 只是查询本 Skill 的说明,或请求与上述结果无关的任务;不执行实际业务操作。
  • 用户仅要预览或审查时,不进入修改、提交或发布分支。

Execution boundary

自然语言请求即可触发;无需旧 slash 路径、参数插值或指定助手。明确解析当前请求中的 项目、目标文件、选项与输出位置;用当前宿主实际提供的文件、搜索、CLI 和浏览器能力。 项目依赖版本与外部 API 在执行时核实,不能假设示例是现行配置。随包脚本从 Skill 根解析, 业务文件从目标项目根解析。先读当前状态,保护已有未提交内容与其他任务的暂存区。 分析、预览请求保持只读;修改、提交、推送、部署和发布各依当前请求的明确范围执行。 不绕过保护、自动发送消息、强制结束用户进程或抢前台。失败保留可诊断原始错误。

Workflow

  1. 读取包管理器、workspace、包名、版本、changesets 配置、待发布记录与 Git diff。缺少配置时按当前官方流程初始化,保留项目已有发布方式。

  2. 默认 add:按实际行为为每个受影响的非忽略包选择 patch 或 minor,写唯一 .changeset Markdown,frontmatter 用真实包名,正文写用户能理解的变化。

  3. 支持 monorepo 一条 changeset 多包,检查内部依赖与 private 包策略;不能从文件路径直接推断全部发布范围。

  4. status:执行项目 changesets status 并解释待发布版本,保持只读。manual 审阅可交接稿件,不假定特定宿主编辑器。

  5. release 仅在用户明确请求时运行 version,回读 package 版本、lockfile 与 CHANGELOG,提交精确文件后走项目发布 CI。已有 0.x 默认保持 0.x,不能自动升到 1.0。

  6. 不以文件截断的 YAML 示例替代完整流程,不覆写已发 tag,不把生成 changeset 报为已发布。

Composition

执行前读取 能力组合,按明确制品交接相邻能力。

Runtime context (shared)

运行前读取本包 skill.yaml 与 Profile 合同。优先级为当前请求、 项目上下文、本 Skill records、共享 preferences、brand/user Profile、安全默认值。 只读取声明字段;没有专用运行时的宿主可使用 scripts/profile_store.py 读取共享 Profile。 配置缺失只问影响结果的一个问题。用户明确要求长期保存的值通过该脚本原子写入, 报告实际路径;不保存推断、凭据或其他任务的资料。

Files (skills)
  • cases
    • cases.json 653 B
      [
        {
          "title": "历史命令通用化迁移验证",
          "description": "真实迁移请求的结构与可发现性验收;业务系统操作没有在本案例执行。",
          "input": {
            "source": "历史命令或本地已迁移 Skill",
            "sha256": "2520417a8098dc5f6083cff220e690177e5526c6d5bfdab0ab32359b49b2de96"
          },
          "prompt": "将本地所有历史 slash command 逐一升级,并全部同步官网。",
          "output": {
            "artifact": "SKILL.md",
            "result": "显式输入、自然语言触发、跨宿主执行边界及共享 Profile 合同",
            "limitation": "该案例不证明外部业务写入成功"
          }
        }
      ]
      
  • references
    • skill-composition.md 857 B
      # Skill Group Composition
      
      ## Nearby Skills Inspected
      
      - lov-skill-creator:命令迁移与通用结构;不拥有本业务结果。
      - lov-branding-consistency:用户可见文案审校。
      - lov-skill-publisher:已验证 Skill 的官网分发。
      
      ## Atomic Handoffs
      
      本 Skill 接收请求与项目事实,交付根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。文案审校只消费公开文本;发布器只消费校验后的 Skill 源,不接管业务执行。
      
      ## Overlap Decisions
      
      旧命令按实际输入输出映射到本真源;不新增另一份重复业务实现。其他历史能力在本批次逐项比较,只有用户要求相邻结果时交接。
      
      ## Composition Decision
      
      Single Skill:围绕同一结果的预览、执行和验证共享上下文;不把依赖工具或宿主适配误拆成 Kit。
      
    • user-profile.md 2.7 KB
      # User Profile contract
      
      Every Skill created by Skill Creator declares `user-profile/v1` in `skill.yaml`.
      The contract connects independent sessions to one user-owned JSON Profile while
      keeping the Skill source portable across users and brands.
      
      ## Shared shape
      
      The host supplies the Profile through `SKILL_PROFILE_PATH` (or the runtime's
      configured profile path). The stable shared scopes are:
      
      - `user`: user identity, language, timezone, and other personal working defaults.
      - `brand`: public brand facts, site, logo, tone, profile, and design guidance.
      - `workspace`: project roots and output locations.
      - `preferences`: shared preference values when the host stores them in the Profile.
      - `skills.<skill_id>.profile`: Skill-specific defaults.
      - `skills.<skill_id>.records`: durable decisions and preferences learned from
        direct user statements for this Skill.
      
      The Profile may also use the runtime's canonical `identity` fields. Manifest
      field aliases bridge `identity.*` and the portable `user.*` / `brand.*` names.
      
      ## Read on every run
      
      1. Read the current request and project context.
      2. Read the shared Profile and the `skills.<skill_id>` namespace.
      3. Resolve values in this order: current request, project context, Skill records,
         shared preferences, shared user/brand Profile, safe defaults.
      4. Keep `profile_scope` and field provenance available for the final result.
      
      Do not copy resolved personal paths, brand values, or private records into the
      committed Skill source.
      
      ## Persist directly stated values
      
      When the user explicitly gives a value meant to survive later sessions, save it
      immediately after the user statement and report the canonical path:
      
      ```bash
      python3 scripts/profile_store.py record \
        --skill-id lov-example \
        --path records.subtitle_level \
        --value '"cet4"' \
        --confirm
      ```
      
      For shared facts, use `--path brand.<field>` or `--path user.<field>`. The
      script writes JSON atomically, preserves unrelated Profile data, increments a
      numeric Profile revision when present, and never echoes the stored value.
      
      Inferred information, credentials, tokens, cookies, and secret-like fields stay
      out of durable records. If the user has not stated that a value should persist,
      keep it in the current request context.
      
      ## Read the connected context
      
      ```bash
      python3 scripts/profile_store.py read \
        --skill-id lov-example \
        --pretty
      ```
      
      The result contains `user`, `brand`, `workspace`, `preferences`, `skill`, and
      `records` scopes. A host using `skill-runtime/v1` also returns the same binding
      as `profile_scope` and `profile_contract`.
      
      ## Compatibility
      
      `--user-config` remains accepted by the Creator as a compatibility flag for old
      invocations. The Profile contract is now always generated; users do not choose
      an initialization mode.
      
  • scripts
    • profile_store.py 8.3 KB
      #!/usr/bin/env python3
      """Read and persist a Skill's user-owned profile context.
      
      The command keeps shared identity and brand facts in the profile root while
      putting Skill-specific durable records under ``skills.<skill_id>.records``.
      Writes require an explicit confirmation flag and use an atomic replacement.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import re
      import sys
      import tempfile
      from pathlib import Path
      from typing import Any, Iterable
      
      
      USER_PROFILE_SCHEMA = "user-profile/v1"
      SKILL_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
      SAFE_ROOTS = {"user", "brand", "workspace", "preferences"}
      SENSITIVE_PARTS = {"token", "secret", "password", "credential", "cookie", "api_key", "apikey"}
      
      
      def config_dir() -> Path:
          configured = os.environ.get("SKILLS_CONFIG_DIR")
          if configured:
              return Path(os.path.expandvars(configured)).expanduser()
          xdg = os.environ.get("XDG_CONFIG_HOME")
          if xdg:
              return Path(os.path.expandvars(xdg)).expanduser() / "agent-skills"
          return Path.home() / ".config" / "agent-skills"
      
      
      def profile_path(explicit: Path | None = None) -> Path:
          if explicit:
              return explicit.expanduser()
          configured = (
              os.environ.get("SKILL_PROFILE_PATH")
              or os.environ.get("SKILLS_PROFILE_PATH")
          )
          if configured:
              return Path(os.path.expandvars(configured)).expanduser()
          candidates = (
              Path.home() / ".lovstudio" / "skills" / "profile.json",
              Path.home() / ".skill-publisher" / "skills" / "profile.json",
              config_dir() / "profile.json",
          )
          return next((candidate for candidate in candidates if candidate.exists()), candidates[-1])
      
      
      def read_profile(path: Path) -> dict[str, Any]:
          if not path.exists():
              return {}
          try:
              value = json.loads(path.read_text(encoding="utf-8"))
          except (OSError, json.JSONDecodeError) as exc:
              raise ValueError(f"profile cannot be read: {path}: {exc}") from exc
          if not isinstance(value, dict):
              raise ValueError("profile root must be an object")
          return value
      
      
      def profile_skeleton(skill_id: str) -> dict[str, Any]:
          return {
              "schema": "skill-profile/v1",
              "profile_id": f"profile-{skill_id}",
              "revision": 1,
              "status": "draft",
              "identity": {},
              "purpose": {},
              "brand": {},
              "user": {},
              "workspace": {},
              "skills": {},
              "extensions": {},
          }
      
      
      def atomic_write(path: Path, value: dict[str, Any]) -> None:
          path.parent.mkdir(parents=True, exist_ok=True)
          mode = 0o600
          if path.exists():
              mode = path.stat().st_mode & 0o777
          temporary_name: str | None = None
          try:
              with tempfile.NamedTemporaryFile(
                  "w",
                  encoding="utf-8",
                  dir=path.parent,
                  prefix=f".{path.name}.",
                  suffix=".tmp",
                  delete=False,
              ) as handle:
                  temporary_name = handle.name
                  handle.write(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
                  handle.flush()
                  os.fsync(handle.fileno())
              os.chmod(temporary_name, mode)
              os.replace(temporary_name, path)
          finally:
              if temporary_name:
                  temporary = Path(temporary_name)
                  if temporary.exists():
                      temporary.unlink()
      
      
      def set_path(root: dict[str, Any], parts: list[str], value: Any) -> None:
          current: dict[str, Any] = root
          for part in parts[:-1]:
              child = current.get(part)
              if not isinstance(child, dict):
                  child = {}
                  current[part] = child
              current = child
          current[parts[-1]] = value
      
      
      def target_parts(skill_id: str, requested_path: str) -> tuple[list[str], str]:
          if not SKILL_ID_RE.fullmatch(skill_id):
              raise ValueError("skill_id must be kebab-case")
          path = requested_path.strip().strip(".")
          if not path:
              raise ValueError("path is required")
          parts = [part for part in path.split(".") if part]
          if any(not re.fullmatch(r"[A-Za-z0-9_-]+", part) for part in parts):
              raise ValueError("path contains an invalid segment")
          if any(part.casefold() in SENSITIVE_PARTS for part in parts):
              raise ValueError("profile records do not accept secret-like fields")
      
          if parts[0] == "records":
              target = ["skills", skill_id, "records", *parts[1:]]
              return target, ".".join(target)
          if parts[0] == "profile":
              target = ["skills", skill_id, "profile", *parts[1:]]
              return target, ".".join(target)
          if parts[:3] == ["skills", skill_id, "records"]:
              return parts, ".".join(parts)
          if parts[:3] == ["skills", skill_id, "profile"]:
              return parts, ".".join(parts)
          if parts[0] in SAFE_ROOTS:
              return parts, ".".join(parts)
          expected = f"records.<field> or one of: {', '.join(sorted(SAFE_ROOTS))}.<field>"
          raise ValueError(f"path must use {expected}")
      
      
      def parse_value(raw: str) -> Any:
          try:
              return json.loads(raw)
          except json.JSONDecodeError:
              return raw
      
      
      def read_command(args: argparse.Namespace) -> dict[str, Any]:
          path = profile_path(args.profile)
          profile = read_profile(path)
          skills = profile.get("skills", {})
          if not isinstance(skills, dict):
              skills = {}
          skill = skills.get(args.skill_id, {})
          if not isinstance(skill, dict):
              skill = {}
          user = profile.get("user")
          if not isinstance(user, dict) or not user:
              user = profile.get("identity", {})
          if not isinstance(user, dict):
              user = {}
          brand = profile.get("brand", {})
          if not isinstance(brand, dict):
              brand = {}
          workspace = profile.get("workspace", {})
          if not isinstance(workspace, dict):
              workspace = {}
          preferences = profile.get("preferences", {})
          if not isinstance(preferences, dict):
              preferences = {}
          records = skill.get("records", {})
          if not isinstance(records, dict):
              records = {}
          return {
              "status": "ready",
              "schema": USER_PROFILE_SCHEMA,
              "skill_id": args.skill_id,
              "profile_path": str(path),
              "user": user,
              "brand": brand,
              "workspace": workspace,
              "preferences": preferences,
              "skill": skill,
              "records": records,
          }
      
      
      def record_command(args: argparse.Namespace) -> dict[str, Any]:
          if not args.confirm:
              raise PermissionError("record requires --confirm after the user has stated the value")
          parts, canonical_path = target_parts(args.skill_id, args.path)
          path = profile_path(args.profile)
          profile = read_profile(path)
          if not profile:
              profile = profile_skeleton(args.skill_id)
          set_path(profile, parts, parse_value(args.value))
          revision = profile.get("revision")
          if isinstance(revision, int) and not isinstance(revision, bool):
              profile["revision"] = revision + 1
          atomic_write(path, profile)
          return {
              "status": "saved",
              "schema": USER_PROFILE_SCHEMA,
              "skill_id": args.skill_id,
              "profile_path": str(path),
              "path": canonical_path,
          }
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("--profile", type=Path, default=None, help="Shared profile JSON path")
          subparsers = parser.add_subparsers(dest="command", required=True)
      
          read_parser = subparsers.add_parser("read", help="Read shared and Skill-specific profile context")
          read_parser.add_argument("--skill-id", required=True)
          read_parser.add_argument("--pretty", action="store_true")
      
          record_parser = subparsers.add_parser("record", help="Persist a user-stated profile value")
          record_parser.add_argument("--skill-id", required=True)
          record_parser.add_argument("--path", required=True, help="records.<field>, brand.<field>, or user.<field>")
          record_parser.add_argument("--value", required=True)
          record_parser.add_argument("--confirm", action="store_true")
      
          args = parser.parse_args()
          try:
              result = read_command(args) if args.command == "read" else record_command(args)
          except (OSError, PermissionError, ValueError) as exc:
              print(json.dumps({"status": "error", "error": str(exc)}, ensure_ascii=False), file=sys.stderr)
              return 2
          indent = 2 if getattr(args, "pretty", False) else None
          print(json.dumps(result, ensure_ascii=False, indent=indent, sort_keys=bool(indent)))
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • validate_skill.py 26.2 KB
      #!/usr/bin/env python3
      """Validate a portable local Skill Publisher Skill source directory."""
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      import sys
      from pathlib import Path
      from typing import Any, Iterable
      
      try:
          import yaml
      except ImportError:
          print(
              "ERROR: PyYAML is required. Install it with: python3 -m pip install PyYAML",
              file=sys.stderr,
          )
          raise SystemExit(2)
      
      
      FRONTMATTER_KEYS = {
          "name",
          "description",
          "license",
          "compatibility",
          "allowed-tools",
          "depends_on",
          "metadata",
      }
      TEXT_SUFFIXES = {".md", ".json", ".yaml", ".yml", ".txt", ".svg", ".py"}
      JUNK_NAMES = {"__pycache__", ".DS_Store"}
      JUNK_SUFFIXES = {".pyc", ".pyo"}
      SKIP_DIRS = {".git", "dist", ".venv", "venv", "node_modules"}
      SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
      NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
      MARKDOWN_LINK_RE = re.compile(r"!?\[[^\]]*]\(([^)]+)\)")
      SKILL_PATH_RE = re.compile(r"\$(SKILL_DIR|KIT_DIR)/([A-Za-z0-9_./-]+)")
      CARD_STANDARD = "lovstudio/skill-card/v1"
      PRICING_CARD_SCHEMA = "lovstudio/pricing-card/v1"
      MANIFEST_SCHEMA = "skill-manifest/v1"
      RUNTIME_VERSION = "skill-runtime/v1"
      PROFILE_SCHEMA = "user-profile/v1"
      CONTENT_CLASSES = {
          "authored-prose",
          "microcopy",
          "verbatim",
          "deterministic-output",
      }
      
      
      class ValidationFailure(Exception):
          """Raised when source metadata cannot be parsed."""
      
      
      def read_text(path: Path) -> str:
          return path.read_text(encoding="utf-8")
      
      
      def compact_text(value: Any) -> str:
          return re.sub(r"\s+", " ", value).strip() if isinstance(value, str) else ""
      
      
      def split_frontmatter(path: Path) -> tuple[dict[str, Any], str]:
          text = read_text(path)
          if not text.startswith("---\n"):
              raise ValidationFailure(f"{path}: missing YAML frontmatter")
          marker = text.find("\n---\n", 4)
          if marker < 0:
              raise ValidationFailure(f"{path}: frontmatter is not closed")
          try:
              data = yaml.safe_load(text[4:marker])
          except yaml.YAMLError as exc:
              raise ValidationFailure(
                  f"{path}: standard YAML parser rejected frontmatter: {exc}"
              ) from exc
          if not isinstance(data, dict):
              raise ValidationFailure(f"{path}: frontmatter must be a mapping")
          return data, text[marker + 5 :]
      
      
      def iter_files(root: Path) -> Iterable[Path]:
          for path in root.rglob("*"):
              if any(part in SKIP_DIRS for part in path.relative_to(root).parts):
                  continue
              if path.is_file():
                  yield path
      
      
      def is_relative_to(path: Path, parent: Path) -> bool:
          try:
              path.relative_to(parent)
              return True
          except ValueError:
              return False
      
      
      def validate_skill_file(path: Path, errors: list[str]) -> dict[str, Any] | None:
          try:
              data, body = split_frontmatter(path)
          except ValidationFailure as exc:
              errors.append(str(exc))
              return None
      
          unexpected = sorted(set(data) - FRONTMATTER_KEYS)
          if unexpected:
              errors.append(f"{path}: unsupported frontmatter keys: {', '.join(unexpected)}")
      
          name = compact_text(data.get("name"))
          if not NAME_RE.fullmatch(name) or len(name) > 64:
              errors.append(f"{path}: name must be kebab-case and at most 64 characters")
      
          description = compact_text(data.get("description"))
          if not 50 <= len(description) <= 200:
              errors.append(
                  f"{path}: description must contain 50-200 characters "
                  f"(found {len(description)})"
              )
      
          compatibility = compact_text(data.get("compatibility"))
          if not compatibility:
              errors.append(f"{path}: compatibility is required at the top level")
      
          depends_on = data.get("depends_on", [])
          if isinstance(depends_on, str):
              depends_on = [depends_on]
          if not isinstance(depends_on, list) or not all(
              isinstance(item, str) and NAME_RE.fullmatch(item) for item in depends_on
          ):
              errors.append(f"{path}: depends_on must be a list of exact Skill names")
      
          metadata = data.get("metadata")
          if not isinstance(metadata, dict):
              errors.append(f"{path}: metadata must be a mapping")
          else:
              if not compact_text(metadata.get("author")):
                  errors.append(f"{path}: metadata.author is required")
              if not SEMVER_RE.fullmatch(compact_text(metadata.get("version"))):
                  errors.append(f"{path}: metadata.version must use SemVer")
              tags = metadata.get("tags")
              if not isinstance(tags, list) or not tags or not all(
                  isinstance(tag, str) and tag.strip() for tag in tags
              ):
                  errors.append(f"{path}: metadata.tags must be a non-empty list")
              if "card_standard" in metadata and metadata.get("card_standard") != CARD_STANDARD:
                  errors.append(f"{path}: metadata.card_standard must be {CARD_STANDARD}")
              content_class = compact_text(metadata.get("content_class"))
              if content_class and content_class not in CONTENT_CLASSES:
                  errors.append(
                      f"{path}: metadata.content_class must be one of "
                      f"{', '.join(sorted(CONTENT_CLASSES))}"
                  )
              if content_class in {"authored-prose", "microcopy"} and (
                  not isinstance(depends_on, list)
                  or "lov-branding-consistency" not in depends_on
              ):
                  errors.append(
                      f"{path}: {content_class} requires lov-branding-consistency"
                  )
              if content_class == "authored-prose":
                  authorship_reference = path.parent / "references" / "authorship-integrity.md"
                  if not authorship_reference.is_file():
                      errors.append(
                          f"{authorship_reference}: authored-prose contract is required"
                      )
                  if "references/authorship-integrity.md" not in body:
                      errors.append(
                          f"{path}: authored-prose must route to references/authorship-integrity.md"
                      )
      
          trigger_block = re.search(
              r"(?ms)^##\s+Triggers\s*$([\s\S]*?)(?=^##\s+|\Z)", body
          )
          if not trigger_block:
              errors.append(f"{path}: add an explicit '## Triggers' section")
          else:
              block = trigger_block.group(1)
              if len(re.findall(r"(?m)^\s*-\s+\S", block)) < 3:
                  errors.append(f"{path}: add two activation examples and one non-trigger")
              if not re.search(r"[\u3400-\u9fff]", block):
                  errors.append(f"{path}: add a concrete Chinese trigger phrase")
              if not re.search(r"(?i)\b(?:the|a|an|create|build|help|publish|review|use)\b", block):
                  errors.append(f"{path}: add a concrete English trigger phrase")
          if not re.search(
              r"(?mi)^###\s+(?:Do not activate when|Non-triggers?|不应触发|不要触发)\s*$",
              body,
          ):
              errors.append(f"{path}: add explicit non-trigger conditions")
          if len(read_text(path).splitlines()) >= 500:
              errors.append(f"{path}: keep SKILL.md below 500 lines")
          if not body.strip():
              errors.append(f"{path}: body is empty")
          return data
      
      
      def load_yaml(path: Path, errors: list[str]) -> dict[str, Any] | None:
          try:
              data = yaml.safe_load(read_text(path))
          except yaml.YAMLError as exc:
              errors.append(f"{path}: standard YAML parser rejected file: {exc}")
              return None
          if not isinstance(data, dict):
              errors.append(f"{path}: expected a YAML mapping")
              return None
          return data
      
      
      def validate_runtime_manifest(
          skill_root: Path, expected_skill_id: str, errors: list[str]
      ) -> None:
          manifest_path = skill_root / "skill.yaml"
          if not manifest_path.is_file():
              errors.append(f"{manifest_path}: user-profile runtime manifest is required")
              return
          data = load_yaml(manifest_path, errors)
          if data is None:
              return
          if data.get("schema") != MANIFEST_SCHEMA:
              errors.append(f"{manifest_path}: schema must be {MANIFEST_SCHEMA}")
          if data.get("id") != expected_skill_id:
              errors.append(f"{manifest_path}: id must match {expected_skill_id}")
          version = compact_text(data.get("version"))
          if not SEMVER_RE.fullmatch(version):
              errors.append(f"{manifest_path}: version must use SemVer")
          if data.get("runtime") != RUNTIME_VERSION:
              errors.append(f"{manifest_path}: runtime must be {RUNTIME_VERSION}")
      
          context = data.get("context")
          if not isinstance(context, dict):
              errors.append(f"{manifest_path}: context must be a mapping")
              return
          profile = context.get("profile")
          preferences = context.get("preferences")
          interaction = context.get("interaction")
          if not isinstance(profile, dict):
              errors.append(f"{manifest_path}: context.profile must be a mapping")
          else:
              if profile.get("schema") != PROFILE_SCHEMA:
                  errors.append(f"{manifest_path}: context.profile.schema must be {PROFILE_SCHEMA}")
              if profile.get("source") != "shared-profile":
                  errors.append(f"{manifest_path}: context.profile.source must be shared-profile")
              reads = profile.get("read")
              if not isinstance(reads, list) or not reads or not all(
                  isinstance(item, str) and item.strip() for item in reads
              ):
                  errors.append(f"{manifest_path}: context.profile.read must be a non-empty list")
              persist = profile.get("persist")
              if not isinstance(persist, dict):
                  errors.append(f"{manifest_path}: context.profile.persist must be a mapping")
              else:
                  if persist.get("enabled") is not True:
                      errors.append(f"{manifest_path}: profile persistence must be enabled")
                  if persist.get("namespace") != f"skills.{expected_skill_id}":
                      errors.append(f"{manifest_path}: persist.namespace must target skills.{expected_skill_id}")
                  if persist.get("records_path") != f"skills.{expected_skill_id}.records":
                      errors.append(f"{manifest_path}: persist.records_path must target Skill records")
                  if persist.get("write_policy") != "direct-user-statement":
                      errors.append(f"{manifest_path}: persist.write_policy must be direct-user-statement")
                  if persist.get("atomic") is not True:
                      errors.append(f"{manifest_path}: profile persistence must be atomic")
              fields = profile.get("fields")
              if not isinstance(fields, list) or not fields:
                  errors.append(f"{manifest_path}: context.profile.fields must be a non-empty list")
              else:
                  validate_manifest_fields(manifest_path, "profile", fields, errors)
          if not isinstance(preferences, dict):
              errors.append(f"{manifest_path}: context.preferences must be a mapping")
          else:
              fields = preferences.get("fields")
              if not isinstance(fields, list):
                  errors.append(f"{manifest_path}: context.preferences.fields must be a list")
              else:
                  validate_manifest_fields(manifest_path, "preferences", fields, errors)
          if not isinstance(interaction, dict):
              errors.append(f"{manifest_path}: context.interaction must be a mapping")
          else:
              if not isinstance(interaction.get("ask_missing"), bool):
                  errors.append(f"{manifest_path}: interaction.ask_missing must be boolean")
              max_questions = interaction.get("max_questions")
              if not isinstance(max_questions, int) or isinstance(max_questions, bool) or not 1 <= max_questions <= 3:
                  errors.append(f"{manifest_path}: interaction.max_questions must be 1-3")
      
      
      def validate_manifest_fields(
          manifest_path: Path, source_name: str, fields: list[Any], errors: list[str]
      ) -> None:
          for index, field in enumerate(fields):
              label = f"{manifest_path}: context.{source_name}.fields[{index}]"
              if not isinstance(field, dict):
                  errors.append(f"{label} must be a mapping")
                  continue
              if not compact_text(field.get("path")):
                  errors.append(f"{label}.path is required")
              if not isinstance(field.get("required"), bool):
                  errors.append(f"{label}.required must be boolean")
              if not compact_text(field.get("question")):
                  errors.append(f"{label}.question is required")
              aliases = field.get("aliases", [])
              if isinstance(aliases, str):
                  aliases = [aliases]
              if not isinstance(aliases, list) or not all(
                  isinstance(alias, str) and alias.strip() for alias in aliases
              ):
                  errors.append(f"{label}.aliases must be a list of paths")
      
      
      def has_content(value: Any) -> bool:
          if isinstance(value, str):
              return bool(value.strip())
          if isinstance(value, list):
              return any(has_content(item) for item in value)
          if isinstance(value, dict):
              return any(has_content(item) for item in value.values())
          return value is not None
      
      
      def contains_placeholder(value: Any) -> bool:
          if isinstance(value, str):
              return bool(re.search(r"\bTODO\b|\{[^}]+\}", value, re.I))
          if isinstance(value, list):
              return any(contains_placeholder(item) for item in value)
          if isinstance(value, dict):
              return any(contains_placeholder(item) for item in value.values())
          return False
      
      
      def validate_card_bundle(skill_root: Path, errors: list[str]) -> None:
          card_path = skill_root / "skill-card.yaml"
          card_doc_path = skill_root / "skill-card.md"
          cases_path = skill_root / "cases" / "cases.json"
          pricing_path = skill_root / "pricing-card.yaml"
      
          for path in (card_path, card_doc_path, cases_path, pricing_path):
              if not path.is_file():
                  errors.append(f"{path}: required Skill trust-bundle file is missing")
      
          card = load_yaml(card_path, errors) if card_path.is_file() else None
          if card is not None:
              if card.get("schema") != CARD_STANDARD:
                  errors.append(f"{card_path}: schema must be {CARD_STANDARD}")
              required = (
                  "description", "owner", "license", "use_case", "deployment",
                  "requirements", "risks", "references", "output", "version",
                  "ethical_considerations", "dimensions", "pricing", "distribution",
              )
              for key in required:
                  if key not in card or not has_content(card.get(key)):
                      errors.append(f"{card_path}: required field '{key}' is missing or empty")
              dimensions = card.get("dimensions")
              if not isinstance(dimensions, list) or len(dimensions) < 3:
                  errors.append(f"{card_path}: dimensions must contain at least three named dimensions")
              else:
                  ids: set[str] = set()
                  for index, dimension in enumerate(dimensions):
                      label = f"{card_path}: dimensions[{index}]"
                      if not isinstance(dimension, dict):
                          errors.append(f"{label}: expected a mapping")
                          continue
                      dimension_id = compact_text(dimension.get("id"))
                      if not dimension_id or dimension_id in ids:
                          errors.append(f"{label}: id is required and must be unique")
                      ids.add(dimension_id)
                      for key in ("label", "description", "evidence"):
                          if not compact_text(dimension.get(key)):
                              errors.append(f"{label}: '{key}' is required")
              risks = card.get("risks")
              if not isinstance(risks, list) or not risks:
                  errors.append(f"{card_path}: risks must contain at least one risk and mitigation")
              else:
                  for index, risk in enumerate(risks):
                      if not isinstance(risk, dict) or not compact_text(risk.get("risk")) or not compact_text(risk.get("mitigation")):
                          errors.append(f"{card_path}: risks[{index}] needs risk and mitigation")
              distribution = card.get("distribution")
              if not isinstance(distribution, dict) or not isinstance(distribution.get("paid"), list) or not isinstance(distribution.get("free"), list):
                  errors.append(f"{card_path}: distribution must declare paid and free lists")
              if contains_placeholder(card):
                  errors.append(f"{card_path}: replace unresolved TODO or template placeholders")
      
          if card_doc_path.is_file():
              card_doc = read_text(card_doc_path)
              required_headings = (
                  "Description", "Owner", "License", "Use Case", "Deployment Geography",
                  "Requirements", "Known Risks", "References", "Skill Output",
                  "Skill Version", "Ethical Considerations", "User Cases",
                  "Dimension Map", "Pricing Basis", "Distribution",
              )
              for heading in required_headings:
                  if not re.search(rf"(?mi)^#+\s+{re.escape(heading)}", card_doc):
                      errors.append(f"{card_doc_path}: add the '{heading}' section")
              if re.search(r"\bTODO\b|\{[^}]+\}", card_doc, re.I):
                  errors.append(f"{card_doc_path}: replace unresolved TODO or template placeholders")
      
          if cases_path.is_file():
              try:
                  cases = json.loads(read_text(cases_path))
              except json.JSONDecodeError as exc:
                  errors.append(f"{cases_path}: invalid JSON: {exc}")
                  cases = []
              if not isinstance(cases, list) or not cases:
                  errors.append(f"{cases_path}: include at least one real user case")
              else:
                  for index, case in enumerate(cases):
                      label = f"{cases_path}: cases[{index}]"
                      if not isinstance(case, dict):
                          errors.append(f"{label}: expected a mapping")
                          continue
                      for key in ("title", "description", "input", "prompt", "output"):
                          if not has_content(case.get(key)):
                              errors.append(f"{label}: '{key}' is required")
                      if contains_placeholder(case):
                          errors.append(f"{label}: replace unresolved TODO or template placeholders")
                      for image_key in ("cover", "gallery"):
                          image_values = case.get(image_key, [])
                          if isinstance(image_values, str):
                              image_values = [image_values]
                          if not isinstance(image_values, list):
                              errors.append(f"{label}: '{image_key}' must be a string or list")
                              continue
                          for image in image_values:
                              if not isinstance(image, str) or not image.strip():
                                  errors.append(f"{label}: '{image_key}' contains an empty path")
                              elif not re.match(r"^(?:https?:|/|data:)", image) and not (skill_root / image).is_file():
                                  errors.append(f"{label}: case asset does not exist: {image}")
      
          pricing = load_yaml(pricing_path, errors) if pricing_path.is_file() else None
          if pricing is not None:
              if pricing.get("schema") != PRICING_CARD_SCHEMA:
                  errors.append(f"{pricing_path}: schema must be {PRICING_CARD_SCHEMA}")
              for key in ("model", "currency", "list_price_cny", "basis", "boundary", "review_trigger", "confidence"):
                  if key not in pricing or (key != "list_price_cny" and not has_content(pricing.get(key))):
                      errors.append(f"{pricing_path}: required field '{key}' is missing or empty")
              if contains_placeholder(pricing):
                  errors.append(f"{pricing_path}: replace unresolved TODO or template placeholders")
      
      
      def validate_composition_reference(skill_root: Path, errors: list[str]) -> None:
          path = skill_root / "references" / "skill-composition.md"
          if not path.is_file():
              errors.append(f"{path}: required Skill group composition record is missing")
              return
          text = read_text(path)
          required_headings = (
              "Nearby Skills Inspected",
              "Atomic Handoffs",
              "Overlap Decisions",
              "Composition Decision",
          )
          for heading in required_headings:
              if not re.search(rf"(?mi)^#+\s+{re.escape(heading)}", text):
                  errors.append(f"{path}: add the '{heading}' section")
          if re.search(r"\bTODO\b|\{[^}]+\}", text, re.I):
              errors.append(f"{path}: replace unresolved TODO or template placeholders")
      
      
      def validate_kit(root: Path, skill_names: set[str], errors: list[str]) -> None:
          manifest = root / "kit.yaml"
          if not manifest.exists():
              return
          data = load_yaml(manifest, errors)
          if data is None:
              return
          modules = data.get("modules")
          if not isinstance(modules, list) or not modules:
              errors.append(f"{manifest}: modules must be a non-empty list")
              return
          module_ids: set[str] = set()
          for index, module in enumerate(modules):
              label = f"{manifest}: modules[{index}]"
              if not isinstance(module, dict):
                  errors.append(f"{label}: expected a mapping")
                  continue
              module_id = compact_text(module.get("id"))
              skill_name = compact_text(module.get("skill"))
              relative = compact_text(module.get("path"))
              if not module_id or module_id in module_ids:
                  errors.append(f"{label}: id is required and must be unique")
              module_ids.add(module_id)
              module_path = (root / relative).resolve()
              if (
                  not relative
                  or not is_relative_to(module_path, root.resolve())
                  or not (module_path / "SKILL.md").is_file()
              ):
                  errors.append(f"{label}: missing module at '{relative}/SKILL.md'")
              if skill_name not in skill_names:
                  errors.append(f"{label}: unresolved child skill '{skill_name}'")
          pipelines = data.get("pipelines")
          if not isinstance(pipelines, dict) or not pipelines:
              errors.append(f"{manifest}: pipelines must be a non-empty mapping")
              return
          for pipeline, sequence in pipelines.items():
              if not isinstance(sequence, list) or not sequence:
                  errors.append(f"{manifest}: pipeline '{pipeline}' must be a non-empty list")
                  continue
              missing = [str(item) for item in sequence if item not in module_ids]
              if missing:
                  errors.append(
                      f"{manifest}: pipeline '{pipeline}' has unknown modules: "
                      + ", ".join(missing)
                  )
      
      
      def validate_local_references(root: Path, errors: list[str]) -> None:
          for path in iter_files(root):
              if path.suffix.lower() != ".md":
                  continue
              text = read_text(path)
              for raw in MARKDOWN_LINK_RE.findall(text):
                  target = raw.strip().split(maxsplit=1)[0].strip("<>").split("#", 1)[0]
                  if (
                      not target
                      or re.match(r"^[a-z][a-z0-9+.-]*:", target, re.I)
                      or any(token in target for token in ("TODO", "{", "}"))
                  ):
                      continue
                  if not (path.parent / target).resolve().exists():
                      errors.append(f"{path}: broken local link '{target}'")
              skill_root = path.parent if path.name == "SKILL.md" else root
              for variable, target in SKILL_PATH_RE.findall(text):
                  if "TODO" in target:
                      continue
                  base = skill_root if variable == "SKILL_DIR" else root
                  resolved = (base / target.rstrip(".,;:)")).resolve()
                  if not is_relative_to(resolved, root.resolve()) or not resolved.exists():
                      errors.append(f"{path}: missing required resource '${variable}/{target}'")
      
      
      def validate_hygiene(root: Path, errors: list[str]) -> None:
          private_path = re.compile(r"(?:/Users/[^/\s]+/|[A-Za-z]:\\\\Users\\\\[^\\\s]+\\\\)")
          for path in root.rglob("*"):
              if any(part in SKIP_DIRS for part in path.relative_to(root).parts):
                  continue
              if path.name in JUNK_NAMES or path.suffix.lower() in JUNK_SUFFIXES:
                  errors.append(f"{path}: generated/cache artifact must not ship")
          for path in iter_files(root):
              if path.suffix.lower() not in TEXT_SUFFIXES or path.name == "validate_skill.py":
                  continue
              text = read_text(path)
              if private_path.search(text):
                  errors.append(f"{path}: contains a private absolute user path")
              if path.name != "init_skill.py" and re.search(r"\bTODO\s*[::]", text):
                  errors.append(f"{path}: unresolved TODO placeholder")
          for relative in ("workbuddy", "scripts/build_workbuddy.py"):
              if (root / relative).exists():
                  errors.append(
                      f"{root / relative}: platform distribution artifacts belong to skill-publish"
                  )
      
      
      def validate_source(root: Path, errors: list[str]) -> None:
          root_skill = root / "SKILL.md"
          skill_files = [root_skill, *sorted((root / "skills").glob("*/SKILL.md"))]
          if not root_skill.is_file():
              errors.append(f"{root_skill}: file is required")
              return
          parsed: list[tuple[Path, dict[str, Any]]] = []
          for path in skill_files:
              data = validate_skill_file(path, errors)
              if data:
                  parsed.append((path, data))
          names = {compact_text(data.get("name")) for _, data in parsed}
          if len(names) != len(parsed):
              errors.append(f"{root}: every embedded Skill must have a unique name")
          for path, data in parsed:
              metadata = data.get("metadata")
              if isinstance(metadata, dict) and metadata.get("card_standard") == CARD_STANDARD:
                  validate_card_bundle(path.parent, errors)
              validate_composition_reference(path.parent, errors)
              validate_runtime_manifest(path.parent, compact_text(data.get("name")), errors)
          validate_kit(root, names, errors)
      
          readme = root / "README.md"
          if not readme.is_file():
              errors.append(f"{readme}: file is required")
          elif parsed:
              metadata = parsed[0][1].get("metadata")
              version = compact_text(metadata.get("version")) if isinstance(metadata, dict) else ""
              if version and f"version-{version}-" not in read_text(readme):
                  errors.append(f"{readme}: version badge must match {version}")
      
          validate_hygiene(root, errors)
          validate_local_references(root, errors)
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("path", type=Path, help="Local Skill source directory")
          args = parser.parse_args()
          root = args.path.expanduser().resolve()
          if not root.is_dir():
              print(f"ERROR: directory does not exist: {root}", file=sys.stderr)
              return 2
          errors: list[str] = []
          validate_source(root, errors)
          if errors:
              print(f"FAILED: {len(errors)} issue(s)")
              for error in errors:
                  print(f"- {error}")
              return 1
          print(f"PASSED: source validation ({root})")
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • CHANGELOG.md 285 B
    # Changelog
    
    ## [2.0.1] - 2026-09-07
    
    - 将历史 slash command 升级为通用 Skill,补全自然语言触发、显式输入、共享 Profile 与验收边界。
    - 保留业务目的,修正宿主耦合和不安全的隐式动作;迁移案例与实际业务验收分别记录。
    
    
  • LICENSE 1 KB · in bundle
  • pricing-card.yaml 425 B
    schema: lovstudio/pricing-card/v1
    model: free_entry
    currency: CNY
    list_price_cny: 0
    basis: 轻量公开工具能力;当前证据主要是源码迁移与安装验证,采用免费入口积累业务结果。
    boundary: 交付 Skill 源码与使用合同,不含第三方服务费用、账户或人工代运营。
    review_trigger: 十次真实业务使用或依赖接口与维护成本明显变化后复评。
    confidence: low
    
  • README.md 1 KB
    # 版本管家 · Version Keeper
    
    ![Version](https://img.shields.io/badge/version-2.0.1-CC785C)
    
    根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。
    
    ## 安装
    
    ```bash
    npx skills add lov-version-management -g -y
    ```
    
    ## 使用
    
    - 根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。
    - Manage package versions with Changesets.
    
    支持自然语言调用,具体执行步骤与边界见 [SKILL.md](SKILL.md)。分析、预览和实际修改分开处理。
    
    ## 配置与依赖
    
    读取 [共享 Profile](references/user-profile.md);项目路径、输出目录与品牌来自当前请求或用户配置。
    业务工具和目标系统依赖见工作流;公开文案使用 lov-branding-consistency 审校。
    
    ## 质量与案例
    
    ```bash
    python3 scripts/validate_skill.py .
    ```
    
    [案例](cases/cases.json) 记录真实迁移输入和输出,仅证明所声明的验收范围。
    [Skill Card](skill-card.md) 与 [定价依据](pricing-card.yaml) 说明使用边界;业务运行结果须逐次回读。
    
    ## License
    
    MIT
    
  • skill-card.md 1.1 KB
    # 版本管家 · Version Keeper · Skill Card
    
    ## Description
    
    根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG
    
    ## Owner
    
    contributors
    
    ## License
    
    MIT
    
    ## Use Case
    
    根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG
    
    ## Deployment Geography
    
    本地运行;外部系统仅按用户明确目标访问。
    
    ## Requirements
    
    参见 SKILL.md 的兼容性和工作流。
    
    ## Known Risks
    
    项目版本与依赖不同;结构验收不等于业务运行成功。
    
    ## References
    
    [工作流](SKILL.md) · [组合关系](references/skill-composition.md)
    
    ## Skill Output
    
    根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG
    
    ## Skill Version
    
    2.0.1
    
    ## Ethical Considerations
    
    不发布秘密,不伪造来源与验证结果。
    
    ## User Cases
    
    [迁移案例](cases/cases.json);此案例仅覆盖通用化合同。
    
    ## Dimension Map
    
    可移植性、配置隔离、可追溯性;未给未经测量的业务评分。
    
    ## Pricing Basis
    
    免费入口;[价值与边界](pricing-card.yaml)。
    
    ## Distribution
    
    GitHub 与 LovStudio:prepared;WorkBuddy 与 SkillPay:未选择。
    
  • skill-card.yaml 1.5 KB
    schema: lovstudio/skill-card/v1
    description: 根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG
    owner: contributors
    license: MIT
    use_case: 根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG
    deployment: 本地 Agent 宿主,业务系统按任务指定
    requirements: 可读取文件的 Agent;业务依赖见 SKILL.md
    risks:
    - risk: 不同项目与工具版本存在差异,迁移验证不能替代业务运行验证。
      mitigation: 执行前发现实际环境,按工作流回读结果。
    references:
    - SKILL.md
    - cases/cases.json
    - references/skill-composition.md
    output: 根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG
    version: 2.0.1
    ethical_considerations: 保留来源、用户授权和秘密边界,不虚构案例或外部完成状态。
    dimensions:
    - id: portability
      label: 宿主可移植性
      description: 显式输入与实际宿主能力
      evidence: SKILL.md 的 Triggers 和 Execution boundary
      score: null
    - id: context
      label: 配置隔离
      description: 用户配置与公开源分离
      evidence: skill.yaml 与 Profile 合同
      score: null
    - id: traceability
      label: 结果可追溯
      description: 原始输入摘要与迁移结果
      evidence: cases/cases.json;尚无业务效果评分
      score: null
    pricing:
      model: free_entry
      basis: pricing-card.yaml
    distribution:
      free:
      - channel: github
        status: prepared
      - channel: lovstudio
        status: prepared
      paid:
      - channel: workbuddy
        status: not_selected
      - channel: skillpay
        status: not_selected
    
  • SKILL.md 3.5 KB
    ---
    name: lov-version-management
    description: 根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。支持明确输入与结果回读。Use to manage package
      versions with Changesets.
    license: MIT
    compatibility: 支持 Agent Skills 的宿主;Python 3.8+ 与 PyYAML 用于 Profile 和校验。业务工具要求见工作流。
    depends_on:
    - lov-branding-consistency
    metadata:
      author: contributors
      version: 2.0.1
      content_class: microcopy
      card_standard: lovstudio/skill-card/v1
      tags:
      - version-management
      - portable-skill
    ---
    
    # 版本管家 · Version Keeper
    
    根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。
    
    ## Triggers
    
    ### Activate when
    
    - The user asks to use this Skill for its documented outcome.
    
    - “根据实际包变更生成 changeset,并按请求更新版本与 CHANGELOG。”
    - “Manage package versions with Changesets.”
    
    ### Do not activate when
    
    - 只是查询本 Skill 的说明,或请求与上述结果无关的任务;不执行实际业务操作。
    - 用户仅要预览或审查时,不进入修改、提交或发布分支。
    
    ## Execution boundary
    
    自然语言请求即可触发;无需旧 slash 路径、参数插值或指定助手。明确解析当前请求中的
    项目、目标文件、选项与输出位置;用当前宿主实际提供的文件、搜索、CLI 和浏览器能力。
    项目依赖版本与外部 API 在执行时核实,不能假设示例是现行配置。随包脚本从 Skill 根解析,
    业务文件从目标项目根解析。先读当前状态,保护已有未提交内容与其他任务的暂存区。
    分析、预览请求保持只读;修改、提交、推送、部署和发布各依当前请求的明确范围执行。
    不绕过保护、自动发送消息、强制结束用户进程或抢前台。失败保留可诊断原始错误。
    
    ## Workflow
    
    1. 读取包管理器、workspace、包名、版本、changesets 配置、待发布记录与 Git diff。缺少配置时按当前官方流程初始化,保留项目已有发布方式。
    
    2. 默认 add:按实际行为为每个受影响的非忽略包选择 patch 或 minor,写唯一 .changeset Markdown,frontmatter 用真实包名,正文写用户能理解的变化。
    
    3. 支持 monorepo 一条 changeset 多包,检查内部依赖与 private 包策略;不能从文件路径直接推断全部发布范围。
    
    4. status:执行项目 changesets status 并解释待发布版本,保持只读。manual 审阅可交接稿件,不假定特定宿主编辑器。
    
    5. release 仅在用户明确请求时运行 version,回读 package 版本、lockfile 与 CHANGELOG,提交精确文件后走项目发布 CI。已有 0.x 默认保持 0.x,不能自动升到 1.0。
    
    6. 不以文件截断的 YAML 示例替代完整流程,不覆写已发 tag,不把生成 changeset 报为已发布。
    
    ## Composition
    
    执行前读取 [能力组合](references/skill-composition.md),按明确制品交接相邻能力。
    
    ## Runtime context (shared)
    
    运行前读取本包 `skill.yaml` 与 [Profile 合同](references/user-profile.md)。优先级为当前请求、
    项目上下文、本 Skill records、共享 preferences、brand/user Profile、安全默认值。
    只读取声明字段;没有专用运行时的宿主可使用 `scripts/profile_store.py` 读取共享 Profile。
    配置缺失只问影响结果的一个问题。用户明确要求长期保存的值通过该脚本原子写入,
    报告实际路径;不保存推断、凭据或其他任务的资料。
    
  • skill.yaml 2.2 KB
    schema: skill-manifest/v1
    id: lov-version-management
    version: 2.0.1
    runtime: skill-runtime/v1
    context:
      profile:
        schema: user-profile/v1
        source: shared-profile
        read:
        - user
        - brand
        - workspace
        - preferences
        - skills.lov-version-management
        persist:
          enabled: true
          namespace: skills.lov-version-management
          records_path: skills.lov-version-management.records
          write_policy: direct-user-statement
          atomic: true
        fields:
        - path: user.name
          aliases:
          - identity.name
          required: false
          question: 如果本次输出需要用户身份,请提供名称。
        - path: user.language
          required: false
          question: 希望使用哪种语言输出?
        - path: user.timezone
          required: false
          question: 需要使用哪个时区处理日期和时间?
        - path: brand.name
          aliases:
          - identity.name
          required: false
          question: 如果本次输出需要品牌身份,请提供品牌名称。
        - path: brand.site
          required: false
          question: 如果需要品牌官网,请提供地址。
        - path: brand.tone
          required: false
          question: 如果已有品牌语气或审美关键词,请提供它们。
        - path: identity.name
          required: false
          question: 如果本次输出需要品牌身份,请提供品牌名称。
        - path: identity.logo
          required: false
          question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。
        - path: workspace.project_root
          required: false
          question: 目标项目路径是什么?
        - path: workspace.output_dir
          required: false
          question: 输出保存在哪里?
        - path: workspace.knowledge_root
          required: false
          question: 本次知识库路径是什么?
        - path: skills.lov-version-management.profile
          required: false
          question: 本 Skill 有哪些明确配置?
      preferences:
        namespace: lov_version_management
        fields:
        - path: user.language
          required: false
          question: 希望使用哪种语言输出?
        - path: user.timezone
          required: false
          question: 需要使用哪个时区处理日期和时间?
      interaction:
        ask_missing: true
        max_questions: 1
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related