chore: migrate .claude/skills to directory symlinks (#13486)

### What this PR does

Before this PR:

`.claude/skills/<name>/SKILL.md` files were **copied** from
`.agents/skills/<name>/SKILL.md` via `pnpm skills:sync`. A dedicated
`skills-check-windows` CI job ran on `windows-latest` to verify
cross-platform file-copy compatibility.

After this PR:

`.claude/skills/<name>` entries are **directory symlinks** pointing to
`../../.agents/skills/<name>`, following the Single Source of Truth
(SSoT) principle. The Windows-specific CI job is removed; Windows
developers are expected to enable symlink support.

### Why we need it and why it was done in this way

The following tradeoffs were made:

- Windows developers must now manually enable symlink support (Developer
Mode + `git config --global core.symlinks true`). This is acceptable
because:
1. The existing `AGENTS.md` is already a symlink, so Windows
compatibility was never fully enforced.
2. Symlinks eliminate the need for file-copy synchronization, reducing
maintenance complexity.
3. Contributors are expected to have sufficient technical capability to
configure their environments.

The following alternatives were considered:

- Keeping file-copy sync: rejected because it duplicates content and
requires extra CI to verify consistency.

### Breaking changes

Windows developers who clone without symlink support enabled will get
plain text files instead of symlinks. They must:
1. Enable Developer Mode or grant `SeCreateSymbolicLinkPrivilege`
2. Run `git config --global core.symlinks true`
3. Re-clone or run `pnpm skills:sync`

### Special notes for your reviewer

- The `.github/workflows/ci.yml` diff includes minor quote-style changes
(`'` → `"`) from the YAML formatter — these are cosmetic only.

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [ ] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

```release-note
NONE
```

Signed-off-by: icarus <eurfelux@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Phantom
2026-03-16 08:46:46 +08:00
committed by GitHub
parent 19bd2cdd93
commit fe0678a206
22 changed files with 106 additions and 757 deletions

View File

@@ -24,10 +24,19 @@ For each new public skill, run:
pnpm skills:sync
```
`skills:sync` will create/update `.claude/skills/<skill-name>/SKILL.md` as:
`skills:sync` will create/update `.claude/skills/<skill-name>` as a symlink pointing to `../../.agents/skills/<skill-name>`.
- a copied file from `.agents/skills/<skill-name>/SKILL.md`.
- symlinks are not allowed; check enforces regular files for compatibility.
## Windows Compatibility
This project uses symlinks to synchronize files such as AGENTS.md and skills. Windows developers must enable symlink support:
1. **Enable Developer Mode** (Settings → Update & Security → For developers), or
2. **Grant `SeCreateSymbolicLinkPrivilege`** via Local Security Policy (`secpol.msc`).
3. **Configure Git** to create symlinks:
```bash
git config --global core.symlinks true
```
4. Re-clone the repository (or run `pnpm skills:sync`) after enabling symlink support.
## White-list Tracking Rules
@@ -53,4 +62,4 @@ The sync/check scripts manage and verify:
- `.agents/skills/.gitignore`
- `.claude/skills/.gitignore`
- `.claude/skills/<skill-name>/SKILL.md` content matches `.agents/skills/<skill-name>/SKILL.md`
- `.claude/skills/<skill-name>` is a valid symlink to `.agents/skills/<skill-name>`

View File

@@ -24,10 +24,19 @@
pnpm skills:sync
```
`skills:sync` 会自动创建/更新 `.claude/skills/<skill-name>/SKILL.md`
`skills:sync` 会自动创建/更新 `.claude/skills/<skill-name>` 为指向 `../../.agents/skills/<skill-name>` 的符号链接。
- 复制 `.agents/skills/<skill-name>/SKILL.md` 的内容。
- 不允许使用符号链接check 会强制要求为普通文件以保证兼容性。
## Windows 兼容性
本项目使用符号链接同步 AGENTS.md、skills 等文件。Windows 开发者需要手动启用符号链接支持:
1. **启用开发者模式**(设置 → 更新和安全 → 开发者选项),或
2. 通过本地安全策略(`secpol.msc`**授予 `SeCreateSymbolicLinkPrivilege` 权限**。
3. **配置 Git** 以创建符号链接:
```bash
git config --global core.symlinks true
```
4. 启用后重新克隆仓库(或执行 `pnpm skills:sync`)。
## 白名单跟踪规则
@@ -53,4 +62,4 @@ pnpm skills:check
- `.agents/skills/.gitignore`
- `.claude/skills/.gitignore`
- `.claude/skills/<skill-name>/SKILL.md``.agents/skills/<skill-name>/SKILL.md` 的内容一致性
- `.claude/skills/<skill-name>` 是指向 `.agents/skills/<skill-name>` 的有效符号链接

View File

@@ -60,7 +60,7 @@ If the user wants a **public skill**, before validation:
pnpm skills:sync
```
This copies the skill to `.claude/skills/<skill-name>/`.
This creates a symlink at `.claude/skills/<skill-name>/` pointing to `.agents/skills/<skill-name>/`.
**Note**: `pnpm skills:check` primarily validates public skills (those in `public-skills.txt`) and also verifies related governance files, so you must sync first before validating.

View File

@@ -3,13 +3,8 @@
*
!.gitignore
!README*.md
!create-skill/
!create-skill/**
!gh-create-issue/
!gh-create-issue/**
!gh-create-pr/
!gh-create-pr/**
!gh-pr-review/
!gh-pr-review/**
!prepare-release/
!prepare-release/**
!create-skill
!gh-create-issue
!gh-create-pr
!gh-pr-review
!prepare-release

View File

@@ -5,4 +5,4 @@ This directory is a synced mirror for Claude-compatible skill files.
- Do not create new skills directly under `.claude/skills`.
- Create and maintain skills under `.agents/skills` only.
- Update `.agents/skills/public-skills.txt`, then run `pnpm skills:sync`.
- `pnpm skills:check` verifies `.claude/skills/<skill>/SKILL.md` matches `.agents/skills/<skill>/SKILL.md`.
- `pnpm skills:check` verifies `.claude/skills/<skill>` is a valid symlink to `.agents/skills/<skill>`.

View File

@@ -5,4 +5,4 @@
- 不要直接在 `.claude/skills` 下创建新 skill。
- 所有 skill 仅在 `.agents/skills` 中创建和维护。
- 更新 `.agents/skills/public-skills.txt` 后,执行 `pnpm skills:sync`
- `pnpm skills:check` 会校验 `.claude/skills/<skill>/SKILL.md` `.agents/skills/<skill>/SKILL.md` 内容一致
- `pnpm skills:check` 会校验 `.claude/skills/<skill>` 是指向 `.agents/skills/<skill>` 的有效符号链接

1
.claude/skills/create-skill Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/create-skill

View File

@@ -1,111 +0,0 @@
---
name: create-skill
description: Create a new skill in the current repository. Use when the user wants to create/add a new skill, or mentions creating a skill from scratch. This skill follows the workflow defined in .agents/skills/README.md and helps scaffold, validate, and sync new skills.
---
# Create Skill
Create a new skill in `.agents/skills/<skill-name>/` following the workflow defined in `.agents/skills/README.md`.
## Workflow
### Step 1: Gather Intent
Before creating anything, ask the user:
1. **Skill name**: What should the skill be called? (lowercase, digits, hyphens only, e.g., `gh-create-pr`, `prepare-release`)
2. **Description**: What should this skill do? Include specific trigger contexts (e.g., "Use when user asks to create PRs")
3. **Is this a public skill?**: Should it be synced to `.claude/skills/` for shared use? (default: no, private only)
4. **Test cases** (optional): Does the user want to set up evals for this skill?
If the user provides partial info (e.g., just a name), proceed with reasonable defaults and ask to confirm.
### Step 2: Read Guidelines
Always read `.agents/skills/README.md` before creating a new skill to ensure compliance with the current workflow.
### Step 3: Create Skill Structure
Create the following directory structure:
```
.agents/skills/<skill-name>/
└── SKILL.md
```
**SKILL.md template:**
```markdown
---
name: <skill-name>
description: <description>
---
# <Skill Name>
[Instructions for the skill]
```
**Frontmatter fields:**
- `name`: Skill identifier (lowercase, digits, hyphens)
- `description`: When to trigger (what the skill does + specific contexts)
### Step 4: Sync (if public)
If the user wants a **public skill**, before validation:
1. Add the skill name to `.agents/skills/public-skills.txt` (one per line, no inline comments)
2. Run sync:
```bash
pnpm skills:sync
```
This copies the skill to `.claude/skills/<skill-name>/`.
**Note**: `pnpm skills:check` primarily validates public skills (those in `public-skills.txt`) and also verifies related governance files, so you must sync first before validating.
### Step 5: Validate
Run the validation command:
```bash
pnpm skills:check
```
If there are issues, fix them and re-run.
### Step 6: Summary
Present the user with:
- Created files
- Validation result
- Next steps (how to use the skill)
## Naming Rules
- Use lowercase letters, digits, and hyphens only
- Prefer short, action-oriented names (e.g., `gh-create-pr`)
## Public vs Private Skills
| Type | Location | Sync | Requires |
|------|----------|------|----------|
| Private | `.agents/skills/` | No | Just create the folder |
| Public | Both | Yes | Add to `public-skills.txt` + run `pnpm skills:sync` |
## Commands Reference
```bash
# Validate skill structure
pnpm skills:check
# Sync public skills to Claude
pnpm skills:sync
```
## Constraints
- Never create skills outside `.agents/skills/<skill-name>/`
- Always run `pnpm skills:check` before completing
- Public skills require both adding to `public-skills.txt` AND running `pnpm skills:sync`
- If the skill-creator skill is available, you may use it for advanced skill development (evals, iterations), but this skill handles the basic creation workflow.

View File

@@ -0,0 +1 @@
../../.agents/skills/gh-create-issue

View File

@@ -1,94 +0,0 @@
---
name: gh-create-issue
description: Use when user wants to create a GitHub issue for the current repository. Must read and follow the repository's issue template format.
---
# GitHub Create Issue
Use this skill when the user requests to create an issue. Must follow the repository's issue template format.
## Workflow
### Step 1: Determine Template Type
Analyze the user's request to determine the issue type:
- If the user describes a problem, error, crash, or something not working -> Bug Report
- If the user requests a new feature, enhancement, or additional support -> Feature Request
- If the user is asking a question or needs help with something -> Questions & Discussion
- Otherwise -> Others
**If unclear**, ask the user which template to use. Do not default to "Others" on your own.
### Step 2: Read the Selected Template
1. Read the corresponding template file from `.github/ISSUE_TEMPLATE/` directory.
2. Identify required fields (`validations.required: true`), title prefix (`title`), and labels (`labels`, if present).
### Step 3: Collect Information
Based on the selected template, ask the user for required information only. Follow the template's required fields and option constraints (for example, Platform and Priority choices).
### Step 4: Build and Preview Issue Content
Create a temp file and write the issue content:
- Use `issue_body_file="$(mktemp /tmp/gh-issue-body-XXXXXX).md"`
- Use the exact title prefix from the selected template.
- Fill content following the template body structure and section order.
- Apply labels exactly as defined by the template.
- Keep all labels when there are multiple labels.
- If template has no labels, do not add custom labels.
Preview the temp file content. **Show the file path** (e.g., `/tmp/gh-issue-body-XXXXXX.md`) and ask for confirmation before creating. **Skip this step if the user explicitly indicates no preview/confirmation is needed** (for example, automation workflows).
### Step 5: Create Issue
Use `gh issue create` command to create the issue.
Use a unique temp file for the body:
```bash
issue_body_file="$(mktemp /tmp/gh-issue-body-XXXXXX).md"
cat > "$issue_body_file" <<'EOF'
...issue body built from selected template...
EOF
```
Create the issue using values from the selected template:
```bash
gh issue create --title "<title_with_template_prefix>" --body-file "$issue_body_file"
```
If the selected template includes labels, append one `--label` per label:
```bash
gh issue create --title "<title_with_template_prefix>" --body-file "$issue_body_file" --label "<label_1_from_template>" --label "<label_2_from_template>"
```
If the selected template has no labels, do not pass `--label`.
You may use `--template` as a starting point (use the exact template name from the repository):
```bash
gh issue create --template "<template_name>"
```
Use the `--web` flag to open the creation page in browser when complex formatting is needed:
```bash
gh issue create --web
```
Clean up the temp file after creation:
```bash
rm -f "$issue_body_file"
```
## Notes
- Must read template files under `.github/ISSUE_TEMPLATE/` to ensure following the correct format.
- Treat template files as the only source of truth. Do not hardcode title prefixes or labels in this skill.
- Title must be clear and concise, avoid vague terms like "a suggestion" or "stuck".
- Provide as much detail as possible to help developers understand and resolve the issue.
- If user doesn't specify a template type, ask them to choose one first.

1
.claude/skills/gh-create-pr Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/gh-create-pr

View File

@@ -1,70 +0,0 @@
---
name: gh-create-pr
description: Create or update GitHub pull requests using the repository-required workflow and template compliance. Use when asked to create/open/update a PR so the assistant reads `.github/pull_request_template.md`, fills every template section, preserves markdown structure exactly, and marks missing data as N/A or None instead of skipping sections.
---
# GitHub PR Creation
## Workflow
1. Read `.github/pull_request_template.md` before drafting the PR body.
2. Collect PR context from the current branch (base/head, scope, linked issues, testing status, breaking changes, release note content).
3. Check if the current branch has been pushed to remote. If not, push it first:
- Default remote is `origin`, but ask the user if they want to use a different remote.
```bash
git push -u <remote> <head-branch>
```
4. Determine the base branch:
- For official repo(CherryHQ/cherry-studio) as `origin`: default base is `main` from `origin`, but allow the user to explicitly indicate a base branch.
- For fork repo as `origin`: check available remotes with `git remote -v`, default base may be `upstream/main` or another remote. Always assume that user wants to merge head to CherryHQ/cherry-studio/main, unless the user explicitly indicates a base branch.
- Ask the user to confirm the base branch if it's not the default.
5. Create a temp file and write the PR body:
- Use `pr_body_file="$(mktemp /tmp/gh-pr-body-XXXXXX).md"`
- Fill content using the template structure exactly (keep section order, headings, checkbox formatting).
- If not applicable, write `N/A` or `None`.
6. Preview the temp file content. **Show the file path** (e.g., `/tmp/gh-pr-body-XXXXXX.md`) and ask for explicit confirmation before creating. **Skip this step if the user explicitly indicates no preview/confirmation is needed** (for example, automation workflows).
7. After confirmation, create the PR:
```bash
gh pr create --base <base> --head <head> --title "<title>" --body-file "$pr_body_file"
```
8. Clean up the temp file: `rm -f "$pr_body_file"`
9. Report the created PR URL and summarize title/base/head and any required follow-up.
## Constraints
- Never skip template sections.
- Never rewrite the template format.
- Keep content concise and specific to the current change set.
- PR title and body must be written in English.
- Never create the PR before showing the full final body to the user, unless they explicitly waive the preview or confirmation.
- Never rely on command permission prompts as PR body preview.
- **Release note & Documentation checkbox** — both are driven by whether the change is **user-facing**. Use the table below:
| Change type | Release note | Docs `[x]` |
|---|---|---|
| New user-facing feature / setting / UI | Describe the change | ✅ |
| Bug fix visible to users | Describe the fix | ✅ if behavior changed |
| Behavior change / default value change | Describe + `action required` | ✅ |
| Security fix in a user-facing dependency | Describe the fix | ✅ if usage changed |
| CI / GitHub Actions changes | `NONE` | ❌ |
| Internal refactoring (user cannot tell) | `NONE` | ❌ |
| Dev / build tooling changes | `NONE` | ❌ |
| Dev-only dependency bump | `NONE` | ❌ |
| Test-only / code style changes | `NONE` | ❌ |
## Command Pattern
```bash
# read template
cat .github/pull_request_template.md
# show this full Markdown body in chat first
pr_body_file="$(mktemp /tmp/gh-pr-body-XXXXXX).md"
cat > "$pr_body_file" <<'EOF'
...filled template body...
EOF
# run only after explicit user confirmation
gh pr create --base <base> --head <head> --title "<title>" --body-file "$pr_body_file"
rm -f "$pr_body_file"
```

1
.claude/skills/gh-pr-review Symbolic link
View File

@@ -0,0 +1 @@
../../.agents/skills/gh-pr-review

View File

@@ -1,179 +0,0 @@
---
name: gh-pr-review
description: Review GitHub pull requests using the gh-pr-review extension. Use when asked to review a PR, add inline review comments, request changes, approve, or comment on a pull request. Manages the full review lifecycle — start, add inline comments, preview, and submit.
---
# GitHub PR Review
Use this skill when the user requests to review a pull request. Leverages the `gh-pr-review` CLI extension for structured, inline code reviews via GitHub's pending review API.
## Prerequisites
The `gh-pr-review` extension must be installed. If not present, install it:
```bash
gh extension install EurFelux/gh-pr-review
```
Verify with:
```bash
gh extension list | grep pr-review
```
## Workflow
### Step 1: Identify the PR
Determine the target PR from the user's request:
- A PR number (e.g., `#123`)
- A PR URL (e.g., `https://github.com/owner/repo/pull/123`)
- The current branch (use `gh pr view --json number` to find it)
Determine the repository in `owner/repo` format. Default to the current repo via `gh repo view --json nameWithOwner -q .nameWithOwner`.
### Step 2: Gather PR Context
Collect information needed for a thorough review:
```bash
# PR metadata
gh pr view <number> --json title,body,files,additions,deletions,baseRefName,headRefName
# Full diff
gh pr diff <number>
# Changed files with diff hunks (needed for inline comment line numbers)
gh api repos/<owner>/<repo>/pulls/<number>/files --jq '.[] | {filename, status, patch}'
```
Read the changed files in the local repo to understand surrounding context beyond the diff.
### Step 3: Analyze Changes
Review the code for:
- Correctness and logic errors
- Security vulnerabilities (OWASP top 10)
- Performance issues
- Missing error handling at system boundaries
- Breaking changes or backward compatibility concerns
- Test coverage gaps
- Typos and naming inconsistencies
- Adherence to project conventions (check `CLAUDE.md` or equivalent)
Categorize findings by severity:
- **Critical**: Bugs, data loss risks, security vulnerabilities
- **Significant**: Missing error handling, architectural concerns, incomplete implementations
- **Minor/Nit**: Typos, style issues, naming suggestions
### Step 4: Start a Pending Review
```bash
gh pr-review review start --repo <owner/repo> --pr <number>
```
Save the returned `id` field — this is the `review-id` needed for all subsequent commands.
### Step 5: Add Inline Comments
For each finding, add an inline comment at the relevant location:
```bash
gh pr-review review add-comment --repo <owner/repo> --pr <number> \
--review-id "<review-id>" \
--path "<file-path>" \
--line <line-number> \
--body "<comment-body>"
```
For multi-line comments (highlighting a range of code):
```bash
gh pr-review review add-comment --repo <owner/repo> --pr <number> \
--review-id "<review-id>" \
--path "<file-path>" \
--line <end-line> \
--start-line <start-line> \
--body "<comment-body>"
```
**Line number rules:**
- `--line` is the absolute line number in the **new file** (RIGHT side by default).
- The line must fall within a diff hunk range. Check hunk headers: `@@ -oldStart,oldCount +newStart,newCount @@` — valid range for RIGHT side is `newStart` to `newStart + newCount - 1`.
- For comments on deleted lines, use `--side LEFT` and line numbers from the old file.
- Use `gh api repos/<owner>/<repo>/pulls/<number>/files --jq '.[].patch'` to verify valid line ranges.
**Comment body guidelines:**
- Lead with a bold severity label (e.g., `**Bug:**`, `**Critical:**`, `**Nit:**`, `**Perf:**`).
- Explain the problem clearly.
- Provide a concrete suggestion with code snippet when applicable.
### Step 6: Preview the Review
Before submitting, optionally preview all pending comments:
```bash
gh pr-review review preview --repo <owner/repo> --pr <number> --review-id "<review-id>"
```
Show the preview to the user and ask for confirmation before submitting. **Skip this step if the user explicitly indicates no preview/confirmation is needed.**
### Step 7: Submit the Review
```bash
gh pr-review review submit --repo <owner/repo> --pr <number> \
--review-id "<review-id>" \
--event "<APPROVE|COMMENT|REQUEST_CHANGES>" \
--body "<review-summary>"
```
Choose the event based on findings:
- `APPROVE` — No issues found, or only minor nits.
- `COMMENT` — Observations and suggestions, but nothing blocking.
- `REQUEST_CHANGES` — Critical or significant issues that must be addressed before merging.
**Review summary body guidelines:**
- Start with a brief overall assessment.
- Group findings by severity (Critical, Significant, Minor).
- Include a Positives section to acknowledge good patterns.
- Keep it concise but comprehensive.
### Step 8: Report Results
Summarize to the user:
- Review event type (approved / commented / requested changes)
- Number of inline comments added
- Key findings by category
- Link to the PR
## Managing Existing Reviews
### Reply to Review Threads
```bash
gh pr-review comments --repo <owner/repo> --pr <number> --reply-to <thread-id> --body "<reply>"
```
### Resolve/Unresolve Threads
```bash
gh pr-review threads --repo <owner/repo> --pr <number> --resolve <thread-id>
gh pr-review threads --repo <owner/repo> --pr <number> --unresolve <thread-id>
```
### Edit or Delete Pending Comments
```bash
gh pr-review review edit-comment --repo <owner/repo> --pr <number> --review-id "<review-id>" --comment-id "<comment-id>" --body "<new-body>"
gh pr-review review delete-comment --repo <owner/repo> --pr <number> --review-id "<review-id>" --comment-id "<comment-id>"
```
## Constraints
- Always start a pending review before adding comments — never use single-comment review APIs.
- Never submit a review without showing the summary to the user first, unless they explicitly waive preview.
- Never fabricate line numbers — always verify against the actual diff hunk ranges.
- Review summary and inline comments must be written in English.
- Do not add inline comments outside of diff hunk ranges — they will fail silently or error.
- Respect the repository's contribution guidelines and coding conventions.
- When reviewing, read the full changed files for context, not just the diff hunks.

View File

@@ -0,0 +1 @@
../../.agents/skills/prepare-release

View File

@@ -1,175 +0,0 @@
---
name: prepare-release
description: Prepare a new release by collecting commits, generating bilingual release notes, updating version files, and creating a release branch with PR. Use when asked to prepare/create a release, bump version, or run `/prepare-release`.
---
# Prepare Release
Automate the Cherry Studio release workflow: collect changes → generate bilingual release notes → update files → create release branch + PR → trigger CI/CD.
## Arguments
Parse the version intent from the user's message. Accept any of these forms:
- Bump type keyword: `patch`, `minor`, `major`
- Exact version: `x.y.z` or `x.y.z-pre.N` (e.g. `1.8.0`, `1.8.0-beta.1`, `1.8.0-rc.1`)
- Natural language: "prepare a beta release", "bump to 1.8.0-rc.2", etc.
Defaults to `patch` if no version is specified. Always echo the resolved target version back to the user before proceeding with any file edits.
- `--dry-run`: Preview only, do not create branch or PR.
## Workflow
### Step 1: Determine Version
1. Get the latest tag:
```bash
git describe --tags --abbrev=0
```
2. Read current version from `package.json`.
3. Compute the new version based on the argument:
- `patch` / `minor` / `major`: bump from the current tag version.
- `x.y.z` or `x.y.z-pre.N`: use as-is after validating it is valid semver.
### Step 2: Collect Commits
1. List all commits since the last tag:
```bash
git log <last-tag>..HEAD --format="%H %s" --no-merges
```
2. For each commit, get the full body:
```bash
git log <hash> -1 --format="%B"
```
3. Extract the content inside `` ```release-note `` code blocks from each commit body.
4. Extract the conventional commit type from the title (`feat`, `fix`, `refactor`, `perf`, `docs`, etc.).
5. **Skip** these commits:
- Titles starting with `🤖 Daily Auto I18N`
- Titles starting with `Merge`
- Titles starting with `chore(deps)`
- Titles starting with `chore: release`
- Commits where the release-note block says `NONE`
### Step 3: Generate Bilingual Release Notes
Using the collected commit information, generate release notes in **both English and Chinese**.
**Format** (must match exactly):
```
<!--LANG:en-->
Cherry Studio {version} - {Brief English Title}
✨ New Features
- [Component] Description
🐛 Bug Fixes
- [Component] Description
💄 Improvements
- [Component] Description
⚡ Performance
- [Component] Description
<!--LANG:zh-CN-->
Cherry Studio {version} - {简短中文标题}
✨ 新功能
- [组件] 描述
🐛 问题修复
- [组件] 描述
💄 改进
- [组件] 描述
⚡ 性能优化
- [组件] 描述
<!--LANG:END-->
```
**Rules:**
- Only include categories that have entries (omit empty categories).
- Each commit appears as exactly ONE line item in the appropriate category.
- Use the `release-note` field if present; otherwise summarize from the commit title.
- Component tags should be short: `[Chat]`, `[Models]`, `[Agent]`, `[MCP]`, `[Settings]`, `[Data]`, `[Build]`, etc.
- Chinese translations should be natural, not machine-literal.
- Do NOT include commit hashes or PR numbers.
- Read the **existing** release notes in `electron-builder.yml` as a style reference before writing.
**IMPORTANT: User-Focused Content Only**
Release notes are for **end users**, not developers. Exclude anything users don't care about:
- **EXCLUDE** internal refactoring, code cleanup, or architecture changes
- **EXCLUDE** CI/CD, build tooling, or test infrastructure changes
- **EXCLUDE** dependency updates (unless they add user-visible features)
- **EXCLUDE** documentation updates
- **EXCLUDE** developer experience improvements
- **EXCLUDE** technical debt fixes with no user-visible impact
- **EXCLUDE** overly technical descriptions (e.g., "fix race condition in Redux middleware")
**INCLUDE** only changes that users will notice:
- New features they can use
- Bug fixes that affected their workflow
- UI/UX improvements they can see
- Performance improvements they can feel
- Security fixes (simplified, without implementation details)
**Keep descriptions simple and non-technical:**
- ❌ "Fix streaming race condition causing partial tool response status in Redux state"
- ✅ "Fix tool status not stopping when aborting"
- ❌ "Auto-convert reasoning_effort to reasoningEffort for OpenAI-compatible providers"
- ✅ "Fix deep thinking mode not working with some providers"
### Step 4: Update Files
1. **`package.json`**: Update the `"version"` field to the new version.
2. **`electron-builder.yml`**: Replace the content under `releaseInfo.releaseNotes: |` with the generated notes. Preserve the 4-space YAML indentation for the block scalar content.
### Step 5: Present for Review
Show the user:
- The new version number.
- The full generated release notes.
- A summary of which files were modified.
If `--dry-run` was specified, stop here.
Otherwise, ask the user to confirm before proceeding to Step 6.
### Step 6: Create Branch and PR
1. Create and push the release branch:
```bash
git checkout -b release/v{version}
git add package.json electron-builder.yml
git commit -m "chore: release v{version}"
git push -u origin release/v{version}
```
2. Create the PR using the `gh-create-pr` skill. If the skill tool is unavailable, read `.agents/skills/gh-create-pr/SKILL.md` and follow it manually. In CI (non-interactive) mode, skip interactive confirmation steps and create the PR directly after filling the template.
- Use title: `chore: release v{version}`
- Use base branch: `main`
- When filling the PR template, incorporate:
- The generated release notes (English section only, for readability).
- A list of included commits.
- A review checklist:
- [ ] Review generated release notes in `electron-builder.yml`
- [ ] Verify version bump in `package.json`
- [ ] CI passes
- [ ] Merge to trigger release build
3. Report the PR URL and next steps.
## CI Trigger Chain
Creating a PR from `release/v*` to `main` automatically triggers:
- **`release.yml`**: Builds on macOS, Windows, Linux and creates a draft GitHub Release.
- **`ci.yml`**: Runs lint, typecheck, and tests.
## Constraints
- Always read `electron-builder.yml` before modifying it to understand the current format.
- Never modify files other than `package.json` and `electron-builder.yml`.
- Never push directly to `main`.
- Always show the generated release notes to the user before creating the branch/PR (unless running in CI with no interactive user).

View File

@@ -62,7 +62,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
node-version-file: ".node-version"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -122,7 +122,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
node-version-file: ".node-version"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -173,7 +173,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
node-version-file: ".node-version"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -197,46 +197,10 @@ jobs:
- name: Renderer Test
run: pnpm test:renderer
skills-check-windows:
runs-on: windows-latest
env:
CI: true
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false
steps:
- name: Check out Git repository
uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm dependencies
uses: actions/cache@v5
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install Dependencies
run: pnpm install
- name: Skills Check
run: pnpm skills:check
notify:
runs-on: ubuntu-latest
needs: [basic-checks, general-test, render-test, skills-check-windows]
if: always() && (needs.basic-checks.result == 'failure' || needs.general-test.result == 'failure' || needs.render-test.result == 'failure' || needs.skills-check-windows.result == 'failure') && github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [basic-checks, general-test, render-test]
if: always() && (needs.basic-checks.result == 'failure' || needs.general-test.result == 'failure' || needs.render-test.result == 'failure') && github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Check out Git repository
uses: actions/checkout@v6
@@ -244,7 +208,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.node-version'
node-version-file: ".node-version"
- name: Install pnpm
uses: pnpm/action-setup@v4

View File

@@ -16,6 +16,17 @@
```
3. Customize `.zed/settings.json` as needed (it is git-ignored).
## Windows: Enable Symlinks
This project uses symlinks to synchronize files such as AGENTS.md and skills. Windows developers must enable symlink support before cloning:
1. **Enable Developer Mode** (Settings → Update & Security → For developers), or grant `SeCreateSymbolicLinkPrivilege` via `secpol.msc`.
2. **Configure Git**:
```bash
git config --global core.symlinks true
```
3. Clone (or re-clone) the repository after enabling symlink support.
## Project Setup
### Install

View File

@@ -16,6 +16,17 @@
```
3. 按需自定义 `.zed/settings.json`(该文件已被 git 忽略)。
## Windows启用符号链接
本项目使用符号链接同步 AGENTS.md、skills 等文件。Windows 开发者在克隆前需启用符号链接支持:
1. **启用开发者模式**(设置 → 更新和安全 → 开发者选项),或通过 `secpol.msc` 授予 `SeCreateSymbolicLinkPrivilege` 权限。
2. **配置 Git**
```bash
git config --global core.symlinks true
```
3. 启用后重新克隆仓库。
## 项目配置
### 安装 Node.js

View File

@@ -34,52 +34,31 @@ function checkGitignore(filePath: string, expected: string, displayPath: string,
}
/**
* Verifies `.claude/skills/<skillName>/SKILL.md` is correctly synced with
* `.agents/skills/<skillName>/SKILL.md`.
* Requires regular files (symlinks are disallowed for cross-platform compatibility).
* Verifies `.claude/skills/<skillName>` is a symlink pointing to
* `../../.agents/skills/<skillName>`.
*/
function checkClaudeSkillFile(skillName: string, errors: string[]) {
const skillDir = path.join(CLAUDE_SKILLS_DIR, skillName)
const skillFile = path.join(skillDir, 'SKILL.md')
const agentsSkillFile = path.join(AGENTS_SKILLS_DIR, skillName, 'SKILL.md')
if (!fs.existsSync(skillDir)) {
errors.push(`.claude/skills/${skillName} is missing`)
return
}
if (!fs.statSync(skillDir).isDirectory()) {
errors.push(`.claude/skills/${skillName} is not a directory`)
return
}
function checkClaudeSkillSymlink(skillName: string, errors: string[]) {
const claudeSkillDir = path.join(CLAUDE_SKILLS_DIR, skillName)
const expectedTarget = path.join('..', '..', '.agents', 'skills', skillName)
let stat: fs.Stats
try {
stat = fs.lstatSync(skillFile)
stat = fs.lstatSync(claudeSkillDir)
} catch {
errors.push(`.claude/skills/${skillName}/SKILL.md is missing`)
errors.push(`.claude/skills/${skillName} is missing (run pnpm skills:sync)`)
return
}
if (stat.isSymbolicLink()) {
errors.push(`.claude/skills/${skillName}/SKILL.md must be a regular file, not a symlink`)
if (!stat.isSymbolicLink()) {
errors.push(
`.claude/skills/${skillName} must be a symlink, not a ${stat.isDirectory() ? 'directory' : 'file'} (run pnpm skills:sync)`
)
return
}
if (!stat.isFile()) {
errors.push(`.claude/skills/${skillName}/SKILL.md is not a regular file`)
return
}
const expectedContent = readFileSafe(agentsSkillFile)
const actualContent = readFileSafe(skillFile)
if (expectedContent === null || actualContent === null) {
errors.push(`failed to read .claude/skills/${skillName}/SKILL.md for content verification`)
return
}
if (actualContent !== expectedContent) {
errors.push(`.claude/skills/${skillName}/SKILL.md content differs from .agents/skills/${skillName}/SKILL.md`)
const actualTarget = fs.readlinkSync(claudeSkillDir)
if (actualTarget !== expectedTarget) {
errors.push(`.claude/skills/${skillName} symlink points to '${actualTarget}', expected '${expectedTarget}'`)
}
}
@@ -87,6 +66,7 @@ function checkTrackedFilesAgainstWhitelist(skillNames: string[], errors: string[
const sharedAgentsFiles = new Set(['.agents/skills/.gitignore', '.agents/skills/public-skills.txt'])
const sharedClaudeFiles = new Set(['.claude/skills/.gitignore'])
const allowedAgentsPrefixes = skillNames.map((skillName) => `.agents/skills/${skillName}/`)
const allowedClaudeSymlinks = new Set(skillNames.map((skillName) => `.claude/skills/${skillName}`))
const allowedClaudePrefixes = skillNames.map((skillName) => `.claude/skills/${skillName}/`)
let trackedFiles: string[]
@@ -121,7 +101,7 @@ function checkTrackedFilesAgainstWhitelist(skillNames: string[], errors: string[
if (sharedClaudeFiles.has(file) || isClaudeReadmeFile(file)) {
continue
}
if (allowedClaudePrefixes.some((prefix) => file.startsWith(prefix))) {
if (allowedClaudeSymlinks.has(file) || allowedClaudePrefixes.some((prefix) => file.startsWith(prefix))) {
continue
}
errors.push(`tracked file is outside public skill whitelist: ${file}`)
@@ -151,13 +131,13 @@ function main() {
checkGitignore(CLAUDE_SKILLS_GITIGNORE, buildClaudeSkillsGitignore(skillNames), '.claude/skills/.gitignore', errors)
for (const skillName of skillNames) {
const agentSkillPath = path.join(AGENTS_SKILLS_DIR, skillName, 'SKILL.md')
if (!fs.existsSync(agentSkillPath)) {
errors.push(`.agents/skills/${skillName}/SKILL.md is missing`)
const agentSkillDir = path.join(AGENTS_SKILLS_DIR, skillName)
if (!fs.existsSync(agentSkillDir)) {
errors.push(`.agents/skills/${skillName} is missing`)
continue
}
checkClaudeSkillFile(skillName, errors)
checkClaudeSkillSymlink(skillName, errors)
}
checkTrackedFilesAgainstWhitelist(skillNames, errors)

View File

@@ -78,8 +78,7 @@ export function buildClaudeSkillsGitignore(skillNames: string[]): string {
]
for (const skillName of skillNames) {
lines.push(`!${skillName}/`)
lines.push(`!${skillName}/**`)
lines.push(`!${skillName}`)
}
return `${lines.join('\n')}\n`

View File

@@ -12,26 +12,21 @@ import {
} from './skills-common'
/**
* Ensures `.claude/skills/<skillName>/SKILL.md` is synchronized with
* `.agents/skills/<skillName>/SKILL.md`.
* Uses file copy to keep cross-platform compatibility.
* Ensures `.claude/skills/<skillName>` is a symlink pointing to
* `../../.agents/skills/<skillName>` (relative to `.claude/skills/`).
*/
function ensureClaudeSkillFile(skillName: string): boolean {
const agentsSkillFile = path.join(AGENTS_SKILLS_DIR, skillName, 'SKILL.md')
function ensureClaudeSkillSymlink(skillName: string): boolean {
const agentsSkillDir = path.join(AGENTS_SKILLS_DIR, skillName)
const claudeSkillDir = path.join(CLAUDE_SKILLS_DIR, skillName)
const claudeSkillFile = path.join(claudeSkillDir, 'SKILL.md')
const expectedTarget = path.join('..', '..', '.agents', 'skills', skillName)
if (!fs.existsSync(agentsSkillFile)) {
throw new Error(`.agents/skills/${skillName}/SKILL.md is missing`)
if (!fs.existsSync(agentsSkillDir)) {
throw new Error(`.agents/skills/${skillName} is missing`)
}
fs.mkdirSync(claudeSkillDir, { recursive: true })
const expectedContent = fs.readFileSync(agentsSkillFile, 'utf-8')
let existing: fs.Stats | null = null
try {
existing = fs.lstatSync(claudeSkillFile)
existing = fs.lstatSync(claudeSkillDir)
} catch (error) {
const nodeError = error as NodeJS.ErrnoException
if (nodeError.code !== 'ENOENT') {
@@ -39,17 +34,17 @@ function ensureClaudeSkillFile(skillName: string): boolean {
}
}
if (existing !== null && !existing.isFile()) {
fs.rmSync(claudeSkillFile, { force: true, recursive: true })
existing = null
} else if (existing?.isFile()) {
const currentContent = fs.readFileSync(claudeSkillFile, 'utf-8')
if (currentContent === expectedContent) {
return false
if (existing !== null) {
if (existing.isSymbolicLink()) {
const currentTarget = fs.readlinkSync(claudeSkillDir)
if (currentTarget === expectedTarget) {
return false
}
}
fs.rmSync(claudeSkillDir, { force: true, recursive: true })
}
fs.writeFileSync(claudeSkillFile, expectedContent, 'utf-8')
fs.symlinkSync(expectedTarget, claudeSkillDir)
return true
}
@@ -81,8 +76,8 @@ function main() {
changedFiles.push('.claude/skills/.gitignore')
}
for (const skillName of skillNames) {
if (ensureClaudeSkillFile(skillName)) {
changedSkillFiles.push(`.claude/skills/${skillName}/SKILL.md`)
if (ensureClaudeSkillSymlink(skillName)) {
changedSkillFiles.push(`.claude/skills/${skillName}`)
}
}