diff --git a/.agents/skills/README.md b/.agents/skills/README.md index 3db6f6343c..e1ca4c13cc 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -24,10 +24,19 @@ For each new public skill, run: pnpm skills:sync ``` -`skills:sync` will create/update `.claude/skills//SKILL.md` as: +`skills:sync` will create/update `.claude/skills/` as a symlink pointing to `../../.agents/skills/`. -- a copied file from `.agents/skills//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.md` content matches `.agents/skills//SKILL.md` +- `.claude/skills/` is a valid symlink to `.agents/skills/` diff --git a/.agents/skills/README.zh.md b/.agents/skills/README.zh.md index de446a1911..2ebb9b37b2 100644 --- a/.agents/skills/README.zh.md +++ b/.agents/skills/README.zh.md @@ -24,10 +24,19 @@ pnpm skills:sync ``` -`skills:sync` 会自动创建/更新 `.claude/skills//SKILL.md`: +`skills:sync` 会自动创建/更新 `.claude/skills/` 为指向 `../../.agents/skills/` 的符号链接。 -- 复制 `.agents/skills//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.md` 与 `.agents/skills//SKILL.md` 的内容一致性 +- `.claude/skills/` 是指向 `.agents/skills/` 的有效符号链接 diff --git a/.agents/skills/create-skill/SKILL.md b/.agents/skills/create-skill/SKILL.md index 60bbe93cf2..529e5d90c1 100644 --- a/.agents/skills/create-skill/SKILL.md +++ b/.agents/skills/create-skill/SKILL.md @@ -60,7 +60,7 @@ If the user wants a **public skill**, before validation: pnpm skills:sync ``` -This copies the skill to `.claude/skills//`. +This creates a symlink at `.claude/skills//` pointing to `.agents/skills//`. **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. diff --git a/.claude/skills/.gitignore b/.claude/skills/.gitignore index de0bcf5ae8..e47de8597d 100644 --- a/.claude/skills/.gitignore +++ b/.claude/skills/.gitignore @@ -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 diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 9a04c11c7d..0b9732ec10 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -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.md` matches `.agents/skills//SKILL.md`. +- `pnpm skills:check` verifies `.claude/skills/` is a valid symlink to `.agents/skills/`. diff --git a/.claude/skills/README.zh.md b/.claude/skills/README.zh.md index 35ce405c69..bd1b82b3ca 100644 --- a/.claude/skills/README.zh.md +++ b/.claude/skills/README.zh.md @@ -5,4 +5,4 @@ - 不要直接在 `.claude/skills` 下创建新 skill。 - 所有 skill 仅在 `.agents/skills` 中创建和维护。 - 更新 `.agents/skills/public-skills.txt` 后,执行 `pnpm skills:sync`。 -- `pnpm skills:check` 会校验 `.claude/skills//SKILL.md` 与 `.agents/skills//SKILL.md` 内容一致。 +- `pnpm skills:check` 会校验 `.claude/skills/` 是指向 `.agents/skills/` 的有效符号链接。 diff --git a/.claude/skills/create-skill b/.claude/skills/create-skill new file mode 120000 index 0000000000..ae57010b22 --- /dev/null +++ b/.claude/skills/create-skill @@ -0,0 +1 @@ +../../.agents/skills/create-skill \ No newline at end of file diff --git a/.claude/skills/create-skill/SKILL.md b/.claude/skills/create-skill/SKILL.md deleted file mode 100644 index 60bbe93cf2..0000000000 --- a/.claude/skills/create-skill/SKILL.md +++ /dev/null @@ -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//` 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.md -``` - -**SKILL.md template:** - -```markdown ---- -name: -description: ---- - -# - -[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//`. - -**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//` -- 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. diff --git a/.claude/skills/gh-create-issue b/.claude/skills/gh-create-issue new file mode 120000 index 0000000000..f86ac1669a --- /dev/null +++ b/.claude/skills/gh-create-issue @@ -0,0 +1 @@ +../../.agents/skills/gh-create-issue \ No newline at end of file diff --git a/.claude/skills/gh-create-issue/SKILL.md b/.claude/skills/gh-create-issue/SKILL.md deleted file mode 100644 index 67bd9f090e..0000000000 --- a/.claude/skills/gh-create-issue/SKILL.md +++ /dev/null @@ -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 "" --body-file "$issue_body_file" -``` - -If the selected template includes labels, append one `--label` per label: - -```bash -gh issue create --title "" --body-file "$issue_body_file" --label "" --label "" -``` - -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 "" -``` - -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. diff --git a/.claude/skills/gh-create-pr b/.claude/skills/gh-create-pr new file mode 120000 index 0000000000..98b42f091a --- /dev/null +++ b/.claude/skills/gh-create-pr @@ -0,0 +1 @@ +../../.agents/skills/gh-create-pr \ No newline at end of file diff --git a/.claude/skills/gh-create-pr/SKILL.md b/.claude/skills/gh-create-pr/SKILL.md deleted file mode 100644 index f7b52b1e27..0000000000 --- a/.claude/skills/gh-create-pr/SKILL.md +++ /dev/null @@ -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 - ``` -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 --head --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" -``` diff --git a/.claude/skills/gh-pr-review b/.claude/skills/gh-pr-review new file mode 120000 index 0000000000..80efe22277 --- /dev/null +++ b/.claude/skills/gh-pr-review @@ -0,0 +1 @@ +../../.agents/skills/gh-pr-review \ No newline at end of file diff --git a/.claude/skills/gh-pr-review/SKILL.md b/.claude/skills/gh-pr-review/SKILL.md deleted file mode 100644 index 9c9b8a48db..0000000000 --- a/.claude/skills/gh-pr-review/SKILL.md +++ /dev/null @@ -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. diff --git a/.claude/skills/prepare-release b/.claude/skills/prepare-release new file mode 120000 index 0000000000..bde6c96e25 --- /dev/null +++ b/.claude/skills/prepare-release @@ -0,0 +1 @@ +../../.agents/skills/prepare-release \ No newline at end of file diff --git a/.claude/skills/prepare-release/SKILL.md b/.claude/skills/prepare-release/SKILL.md deleted file mode 100644 index 6cf9604e24..0000000000 --- a/.claude/skills/prepare-release/SKILL.md +++ /dev/null @@ -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). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4700b4a528..ff5b4ca4d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/docs/en/guides/development.md b/docs/en/guides/development.md index b37ad8c7d3..729bdc802a 100644 --- a/docs/en/guides/development.md +++ b/docs/en/guides/development.md @@ -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 diff --git a/docs/zh/guides/development.md b/docs/zh/guides/development.md index ab4dcffb7d..e39ead321c 100644 --- a/docs/zh/guides/development.md +++ b/docs/zh/guides/development.md @@ -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 diff --git a/scripts/skills-check.ts b/scripts/skills-check.ts index 84e74572d4..a2dfe801dc 100644 --- a/scripts/skills-check.ts +++ b/scripts/skills-check.ts @@ -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) diff --git a/scripts/skills-common.ts b/scripts/skills-common.ts index 00c2e27c45..eeaa90fe61 100644 --- a/scripts/skills-common.ts +++ b/scripts/skills-common.ts @@ -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` diff --git a/scripts/skills-sync.ts b/scripts/skills-sync.ts index f72263352e..5c50607c29 100644 --- a/scripts/skills-sync.ts +++ b/scripts/skills-sync.ts @@ -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}`) } }