From c3e19d7d2ac6f17a53f4d7c488bb0744513b1dba Mon Sep 17 00:00:00 2001 From: kingboung Date: Tue, 12 May 2026 11:49:38 +0800 Subject: [PATCH] refactor: migrate review skills from registerBundledSkill to extract-to-disk Replace registerBundledSkill() with runtime extract-to-disk + standard skill scanner discovery, aligning with upstream opencode PR #360 approach. - Add extractBundledSkill() to generate script output - Create extension.ts for runtime skill extraction to ~/.claude/skills/ - Remove reviewSkills.ts (registerBundledSkill registration) - Update bundled/index.ts to call Extension.initializeBuiltinSkills() - Add build:builtin-review script to package.json Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 +- ...2026-05-12-review-skill-extract-to-disk.md | 496 ++++++++++++++++++ ...-12-review-skill-extract-to-disk-design.md | 137 +++++ package.json | 1 + scripts/generate-review-builtin.ts | 20 + src/costrict/review/extension.ts | 72 +++ src/costrict/review/index.ts | 11 +- src/costrict/skills/reviewSkills.ts | 68 --- src/skills/bundled/index.ts | 6 +- 9 files changed, 734 insertions(+), 80 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-12-review-skill-extract-to-disk.md create mode 100644 docs/superpowers/specs/2026-05-12-review-skill-extract-to-disk-design.md create mode 100644 src/costrict/review/extension.ts delete mode 100644 src/costrict/skills/reviewSkills.ts diff --git a/.gitignore b/.gitignore index fd83a8e05..2085f9df3 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ src/costrict/review/agent/builtin.ts src/costrict/review/skill/builtin.ts # Review builtin cache -packages/builtin-tools/bundled-review/ \ No newline at end of file +packages/builtin-tools/bundled-review/ +bundled-review/ \ No newline at end of file diff --git a/docs/superpowers/plans/2026-05-12-review-skill-extract-to-disk.md b/docs/superpowers/plans/2026-05-12-review-skill-extract-to-disk.md new file mode 100644 index 000000000..34ff725f0 --- /dev/null +++ b/docs/superpowers/plans/2026-05-12-review-skill-extract-to-disk.md @@ -0,0 +1,496 @@ +# Review Skill Extract-to-Disk Migration Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate review and security-review from `registerBundledSkill()` to extract-to-disk + standard skill scanner discovery, aligning with upstream opencode PR #360. + +**Architecture:** A generate script (`scripts/generate-review-builtin.ts`) downloads skills from the `zgsm-ai/costrict-review` repo and produces `src/costrict/review/skill/builtin.ts` with embedded content. At runtime, `extension.ts` extracts skills to `~/.claude/skills//` on first run (tracked by `.version` file with `sha:locale` format). The standard skill scanner in `loadSkillsDir.ts` discovers them from that directory. + +**Tech Stack:** TypeScript, Bun runtime, git CLI (for clone), gray-matter (frontmatter parsing) + +--- + +## File Structure + +| Action | File | Responsibility | +|--------|------|----------------| +| Modify | `scripts/generate-review-builtin.ts` | Add `extractBundledSkill()` to generated output | +| Create | `src/costrict/review/extension.ts` | Runtime extraction to `~/.claude/skills/` | +| Modify | `src/costrict/review/index.ts` | Export Extension, remove agent exports | +| Delete | `src/costrict/skills/reviewSkills.ts` | Remove registerBundledSkill registration | +| Delete | `src/costrict/review/agent/builtin.ts` | Remove (does not exist on disk, only referenced) | +| Modify | `src/skills/bundled/index.ts` | Remove `registerReviewSkills()`, call Extension init | +| Modify | `package.json` | Add `build:builtin-review` script | +| Modify | `.gitignore` | Add `bundled-review/` at root level | +| Modify | `src/utils/language.ts` or inline | Ensure `getResolvedLanguage` is available | + +--- + +### Task 1: Update generate script to include `extractBundledSkill()` in output + +**Files:** +- Modify: `scripts/generate-review-builtin.ts:223-265` + +The current generate script already produces a complete `builtin.ts`. We need to add the `extractBundledSkill()` function to the generated output so skills can be written to disk at runtime. + +- [ ] **Step 1: Add `extractBundledSkill()` to the generated template in `generate-review-builtin.ts`** + +In the `generateBuiltinSkills()` function (line 223), modify the `content` template string to add the `extractBundledSkill()` function before the closing backtick. Add it after the existing `getSkillMetadata()` function: + +```typescript +// In the content template string, append before the closing `: +export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise { + const localeData = SKILL_FILES[locale] + if (!localeData) { + throw new Error(\`Locale not found: \${locale}\`) + } + + const skillFiles = localeData[skillName] + if (!skillFiles) { + throw new Error(\`Skill not found: \${skillName}\`) + } + + const { mkdir: mkdirSync } = await import('fs/promises') + const { join: pathJoin, dirname: pathDirname } = await import('path') + await mkdirSync(targetDir, { recursive: true }) + for (const [relativePath, fileContent] of Object.entries(skillFiles)) { + await mkdirSync(pathJoin(targetDir, pathDirname(relativePath)), { recursive: true }) + const { writeFile: writeFileFn } = await import('fs/promises') + await writeFileFn(pathJoin(targetDir, relativePath), fileContent, 'utf-8') + } +} +``` + +Find the line in `generate-review-builtin.ts` that reads: +```typescript +// Get metadata for a skill in a specific locale +export function getSkillMetadata(skillName: string, locale: string): { name: string; description: string } | undefined { + return SKILL_METADATA[locale]?.[skillName] +} +``` + +Append the `extractBundledSkill` function after it, before the template closing backtick. + +- [ ] **Step 2: Verify the script runs** + +Run: `bun run scripts/generate-review-builtin.ts` + +Expected: Script downloads resources and regenerates `src/costrict/review/skill/builtin.ts` with the new `extractBundledSkill()` export. Check the last lines of the generated file contain the function. + +- [ ] **Step 3: Commit** + +```bash +git add scripts/generate-review-builtin.ts src/costrict/review/skill/builtin.ts +git commit -m "feat: add extractBundledSkill to generated builtin output" +``` + +--- + +### Task 2: Create `src/costrict/review/extension.ts` + +**Files:** +- Create: `src/costrict/review/extension.ts` + +This module handles runtime extraction of builtin review skills to `~/.claude/skills/`. It checks a `.version` file (format: `sha:locale`) to decide whether re-extraction is needed. + +- [ ] **Step 1: Create the extension module** + +```typescript +import { mkdir, readFile, writeFile } from 'fs/promises' +import { join } from 'path' +import { getClaudeConfigHomeDir } from 'src/utils/paths.js' +import { getResolvedLanguage } from 'src/utils/language.js' +import { + listBuiltinSkillNames, + getBuiltinSkillVersion, + extractBundledSkill, +} from './skill/builtin.js' + +const LOCALE_MAP: Record = { zh: 'zh-CN', en: 'en' } + +function getLocale(): string { + const lang = getResolvedLanguage() + return LOCALE_MAP[lang] ?? 'zh-CN' +} + +function getReviewSkillsDir(): string { + return join(getClaudeConfigHomeDir(), 'skills') +} + +function getVersionFilePath(skillDir: string): string { + return join(skillDir, '.version') +} + +async function getInstalledVersion(skillDir: string): Promise { + try { + return await readFile(getVersionFilePath(skillDir), 'utf-8') + } catch { + return null + } +} + +async function writeVersionFile( + skillDir: string, + skillName: string, + locale: string, +): Promise { + const builtinVersion = getBuiltinSkillVersion(skillName) + if (!builtinVersion) return + await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8') +} + +async function needsUpdate( + skillDir: string, + skillName: string, + locale: string, +): Promise { + const builtinVersion = getBuiltinSkillVersion(skillName) + if (!builtinVersion) return true + const installed = await getInstalledVersion(skillDir) + return installed !== `${builtinVersion}:${locale}` +} + +export async function initializeBuiltinSkills(): Promise { + const locale = getLocale() + const skillsDir = getReviewSkillsDir() + const skillNames = listBuiltinSkillNames() + + for (const skillName of skillNames) { + const skillDir = join(skillsDir, skillName) + if (!(await needsUpdate(skillDir, skillName, locale))) continue + + await mkdir(skillDir, { recursive: true }) + await extractBundledSkill(skillName, skillDir, locale) + await writeVersionFile(skillDir, skillName, locale) + } +} + +export function getBuiltinSkillsDir(): string { + return getReviewSkillsDir() +} +``` + +- [ ] **Step 2: Verify TypeScript compiles** + +Run: `bunx tsc --noEmit 2>&1 | head -20` + +Expected: No errors related to `extension.ts`. (There may be pre-existing errors from other files.) + +- [ ] **Step 3: Commit** + +```bash +git add src/costrict/review/extension.ts +git commit -m "feat: add review skill extension for extract-to-disk" +``` + +--- + +### Task 3: Update `src/costrict/review/index.ts` + +**Files:** +- Modify: `src/costrict/review/index.ts` + +Remove the agent builtin exports and add the Extension export. + +- [ ] **Step 1: Rewrite `index.ts`** + +Replace the entire file content with: + +```typescript +/** + * CoStrict Review Module + * + * Provides builtin review skills that are extracted to disk at runtime + * and discovered by the standard skill scanner. + */ + +export * as SkillBuiltin from './skill/builtin.js' +export * as Extension from './extension.js' +``` + +- [ ] **Step 2: Verify no other files import the removed exports** + +Run: `grep -rn "REVIEW_AGENTS\|AGENT_VERSIONS\|PRIMARY_REVIEW_AGENT\|SUB_REVIEW_AGENT" src/ --include="*.ts" --include="*.tsx" | grep -v "node_modules" | grep -v ".d.ts"` + +Expected: No results (these exports were only referenced by the deleted `reviewSkills.ts` and the old `index.ts`). + +- [ ] **Step 3: Commit** + +```bash +git add src/costrict/review/index.ts +git commit -m "refactor: update review index to export Extension" +``` + +--- + +### Task 4: Delete `src/costrict/skills/reviewSkills.ts` + +**Files:** +- Delete: `src/costrict/skills/reviewSkills.ts` + +This file registered review skills via `registerBundledSkill()`. It is no longer needed. + +- [ ] **Step 1: Delete the file** + +```bash +git rm src/costrict/skills/reviewSkills.ts +``` + +- [ ] **Step 2: Commit** + +```bash +git commit -m "refactor: remove reviewSkills bundled skill registration" +``` + +--- + +### Task 5: Update `src/skills/bundled/index.ts` + +**Files:** +- Modify: `src/skills/bundled/index.ts` + +Remove the `registerReviewSkills` import and call. Add a call to `Extension.initializeBuiltinSkills()`. + +- [ ] **Step 1: Remove review skill registration and add extension initialization** + +In `src/skills/bundled/index.ts`: + +1. Remove the line: +```typescript +import { registerReviewSkills } from 'src/costrict/skills/reviewSkills.js' +``` + +2. Add the import: +```typescript +import { Extension as ReviewExtension } from 'src/costrict/review/index.js' +``` + +3. In `initBundledSkills()`, replace: +```typescript +registerReviewSkills() +``` +with: +```typescript +ReviewExtension.initializeBuiltinSkills().catch(() => {}) +``` + +The `.catch(() => {})` handles the case where extraction fails (e.g., no write permissions) without blocking startup. + +- [ ] **Step 2: Verify TypeScript compiles** + +Run: `bunx tsc --noEmit 2>&1 | head -20` + +Expected: No errors related to `bundled/index.ts`. + +- [ ] **Step 3: Commit** + +```bash +git add src/skills/bundled/index.ts +git commit -m "refactor: replace registerReviewSkills with extract-to-disk init" +``` + +--- + +### Task 6: Update `package.json` and `.gitignore` + +**Files:** +- Modify: `package.json` (scripts section) +- Modify: `.gitignore` + +- [ ] **Step 1: Add `build:builtin-review` script to `package.json`** + +In the `"scripts"` section of `package.json`, add after the existing build-related scripts: + +```json +"build:builtin-review": "bun run scripts/generate-review-builtin.ts", +``` + +Also, if there is a `"build:builtin"` script, update it to include `build:builtin-review`. If not, no chained script is needed — the generate script can be run independently. + +- [ ] **Step 2: Update `.gitignore`** + +Verify the existing `.gitignore` already has these entries (they were found in the current file): +``` +# Auto-generated review builtin files +src/costrict/review/agent/builtin.ts +src/costrict/review/skill/builtin.ts + +# Review builtin cache +packages/builtin-tools/bundled-review/ +``` + +Add a root-level cache directory entry: +``` +# Review skill generation cache +bundled-review/ +``` + +- [ ] **Step 3: Commit** + +```bash +git add package.json .gitignore +git commit -m "chore: add build:builtin-review script and gitignore entries" +``` + +--- + +### Task 7: Clean up agent-related code + +**Files:** +- Check for references to `src/costrict/review/agent/builtin.ts` + +The agent builtin file does not exist on disk but is referenced in `src/costrict/review/index.ts` (which we already updated in Task 3). We need to verify no other files reference review agents. + +- [ ] **Step 1: Search for remaining review agent references** + +Run: `grep -rn "review/agent/builtin\|review.*agent.*builtin\|REVIEW_AGENTS\|PRIMARY_REVIEW_AGENT\|SUB_REVIEW_AGENT\|AGENT_VERSIONS" src/ --include="*.ts" --include="*.tsx" | grep -v node_modules | grep -v ".d.ts"` + +Expected: No results. (The `index.ts` export was already cleaned in Task 3.) + +If any results appear, remove the imports and references. + +- [ ] **Step 2: Search for strict:review and strict:security-review references** + +Run: `grep -rn "strict:review\|strict:security-review" src/ --include="*.ts" --include="*.tsx" | grep -v node_modules` + +Expected: No results. (These were only in the deleted `reviewSkills.ts`.) + +If any results appear, remove them. + +- [ ] **Step 3: Commit if changes were needed** + +```bash +git add -A +git commit -m "chore: clean up review agent references" +``` + +--- + +### Task 8: Verify locale command templates are correct + +**Files:** +- Verify: `src/costrict/command/locales/en/review.txt` +- Verify: `src/costrict/command/locales/zh-CN/review.txt` +- Verify: `src/costrict/command/locales/en/security-review.txt` +- Verify: `src/costrict/command/locales/zh-CN/security-review.txt` +- Verify: `src/costrict/command/locales/index.ts` + +The locale templates should say "Please use the Skill tool to load the `review` / `security-review` skill" which directs the model to invoke the skill from disk. + +- [ ] **Step 1: Verify locale files are already correct** + +The current content of each file is: + +`en/review.txt`: +``` +# Code Review + +Please use the Skill tool to load the `review` skill to perform a code review on: $ARGUMENTS + +Please respond and write all files in English throughout the entire process. +``` + +`zh-CN/review.txt`: +``` +# 代码审查 + +请使用 Skill 工具加载 `review` 技能来对以下内容执行代码审查:$ARGUMENTS + +全程请使用中文进行回答与文件写入。 +``` + +`en/security-review.txt`: +``` +# Code Security Review + +Please use the Skill tool to load the `security-review` skill to perform a security review on: $ARGUMENTS + +Please respond and write all files in English throughout the entire process. +``` + +`zh-CN/security-review.txt`: +``` +# 安全代码审查 + +请使用 Skill 工具加载 `security-review` 技能来对以下内容执行安全代码审查:$ARGUMENTS + +全程请使用中文进行回答与文件写入。 +``` + +Verify these files match the above content. No changes needed. + +- [ ] **Step 2: Verify `CommandLocale.get()` is called for `/review` and `/security-review`** + +Run: `grep -rn "CommandLocale\|command.*locale" src/commands/ --include="*.ts" | grep -i review` + +Expected: The review and security-review commands use `CommandLocale.get('review')` and `CommandLocale.get('security-review')` respectively. If not, they need to be updated. + +If the commands don't exist as command definitions yet, they may be relying on the bundled skill registration. In that case, verify the `/review` and `/security-review` commands are still available through the standard skill scanner path. The skill scanner will load them from `~/.claude/skills/review/SKILL.md` and `~/.claude/skills/security-review/SKILL.md`. + +--- + +### Task 9: Run full typecheck and tests + +- [ ] **Step 1: Run typecheck** + +Run: `bunx tsc --noEmit 2>&1 | head -40` + +Expected: Zero errors. Fix any that appear. + +- [ ] **Step 2: Run tests** + +Run: `bun test 2>&1 | tail -20` + +Expected: All tests pass (0 fail). + +- [ ] **Step 3: Run lint** + +Run: `bun run lint 2>&1 | tail -20` + +Expected: No new lint errors related to changed files. + +- [ ] **Step 4: Commit any fixes** + +```bash +git add -A +git commit -m "fix: resolve typecheck/lint errors from review skill migration" +``` + +--- + +### Task 10: Verify end-to-end flow + +- [ ] **Step 1: Regenerate builtin skills** + +Run: `bun run scripts/generate-review-builtin.ts` + +Expected: Script downloads (or uses cache) and generates `src/costrict/review/skill/builtin.ts` with `extractBundledSkill()` export. + +- [ ] **Step 2: Check generated file has correct exports** + +Run: `grep "export.*function\|export.*const" src/costrict/review/skill/builtin.ts` + +Expected output includes: +``` +export const SKILL_FILES: Record<...> +export const SKILL_METADATA: Record<...> +export const SKILL_VERSIONS: Record<...> +export function listBuiltinSkillNames(): string[] +export function getBuiltinSkillVersion(skillName: string): string | undefined +export function getSkillFiles(skillName: string, locale: string): Record +export function getSkillMetadata(skillName: string, locale: string): ... +export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise +``` + +- [ ] **Step 3: Verify dev mode starts without errors** + +Run: `echo "hello" | bun run dev -p 2>&1 | head -20` + +Expected: No import errors, no crashes related to review skills. + +- [ ] **Step 4: Final commit** + +```bash +git add -A +git commit -m "feat: complete review skill extract-to-disk migration" +``` diff --git a/docs/superpowers/specs/2026-05-12-review-skill-extract-to-disk-design.md b/docs/superpowers/specs/2026-05-12-review-skill-extract-to-disk-design.md new file mode 100644 index 000000000..5ac9627e9 --- /dev/null +++ b/docs/superpowers/specs/2026-05-12-review-skill-extract-to-disk-design.md @@ -0,0 +1,137 @@ +# Review Skill Extract-to-Disk Migration Design + +## Goal + +将 review 和 security-review 从 `registerBundledSkill()` 内存内嵌方式迁移到 **extract-to-disk + 标准 skill scanner 发现** 的架构,完全对齐上游 [opencode#360](https://github.com/zgsm-sangfor/opencode/pull/360) 的实现模式。 + +彻底移除 `registerBundledSkill` 对 review/security-review 的使用。 + +## Background + +当前问题: +- `src/costrict/review/skill/builtin.ts` 是 2.5MB 的内嵌文件,所有 skill 内容在内存中 +- `reviewSkills.ts` 通过 `registerBundledSkill()` 注册,绕过了标准 skill 发现流程 +- strict:review / strict:security-review 也是 bundled skill 方式,增加了复杂度 + +PR #360 的方案: +- 生成脚本从 `zgsm-ai/costrict-review` 远程仓库拉取资源 +- 运行时通过 `extension.ts` 将 skill 提取到磁盘缓存目录(`~/.claude/skills/` 或类似目录) +- `.version` 文件跟踪版本 + locale +- 标准 skill scanner 从磁盘目录发现并加载 skill +- `/review`、`/security-review` 命令模板只说 "请使用 Skill 工具加载 X 技能" + +## Architecture + +``` +Build time: + scripts/generate-review-builtin.ts + → git clone zgsm-ai/costrict-review + → 读取 index.json + → 生成 src/costrict/review/skill/builtin.ts (内嵌所有文件内容) + +Runtime: + Startup → extension.ts:initializeBuiltinSkills(locale) + → 检查 .version 文件(版本+语言) + → 如需更新,调用 builtin.ts:extractBundledSkill() 写入磁盘 + → 标准 skill scanner 扫描缓存目录,发现 review / security-review + +Commands: + /review → CommandLocale.get('review') → "请使用 Skill 工具加载 review 技能" + /security-review → CommandLocale.get('security-review') → "请使用 Skill 工具加载 security-review 技能" +``` + +## Changes + +### 1. 新增 `scripts/generate-review-builtin.ts` + +生成脚本,参考 PR #360 的同名文件: +- 通过 `git ls-remote` 获取远程 SHA,与缓存 SHA 对比 +- `git clone --depth 1` 浅克隆 `zgsm-ai/costrict-review` 仓库 +- 读取 `index.json`,收集所有 skill 及其 locale 路径 +- 将 skill 文件内容内嵌为 TypeScript 常量 +- 生成 `src/costrict/review/skill/builtin.ts`,导出: + - `SKILL_FILES: Record>>` (locale → skillName → filePath → content) + - `SKILL_METADATA: Record>` (locale → skillName → 元数据,从 SKILL.md frontmatter 解析) + - `SKILL_VERSIONS: Record` (skillName → commit SHA) + - `extractBundledSkill(skillName, targetDir, locale)` — 提取到磁盘 + - `listBuiltinSkills()` — 返回所有 skill 名称 + - `getBuiltinSkillVersion(skillName)` — 返回版本 + +### 2. 重新生成 `src/costrict/review/skill/builtin.ts` + +由脚本生成的产物,从 2.5MB 精简为只包含: +- `SKILL_FILES` 多 locale 数据 +- `SKILL_METADATA` 元数据 +- `SKILL_VERSIONS` 版本 +- `extractBundledSkill()` 提取函数 + +不再导出 `extractBundledSkill()` 以外的运行时函数。 + +### 3. 新增 `src/costrict/review/extension.ts` + +运行时初始化逻辑: +- `initializeBuiltinSkills(locale: string)` — 遍历所有 builtin skill,按需提取到磁盘 +- `needsUpdate(skillDir, skillName, locale)` — 检查 `.version` 文件(格式:`sha:locale`) +- `writeVersionFile(skillDir, skillName, locale)` — 写入版本+语言标记 +- `getBuiltinSkillsDir()` — 返回缓存根目录 + +### 4. 修改 `src/costrict/review/index.ts` + +- 移除 `agent/builtin.ts` 相关导出 +- 导出 `Extension` from `./extension.js` +- 导出 `SkillBuiltin` from `./skill/builtin.js` + +### 5. 删除 `src/costrict/skills/reviewSkills.ts` + +移除整个文件,不再需要 `registerBundledSkill` 注册。 + +### 6. 修改 `src/skills/bundled/index.ts` + +- 移除 `import { registerReviewSkills }` 和调用 +- 改为在 skill 初始化阶段调用 `Extension.initializeBuiltinSkills(locale)` + +### 7. 修改 `src/costrict/command/locales/index.ts` + +保持不变,locale 模板继续用于 `/review` 和 `/security-review` 命令。 + +### 8. 修改 `package.json` + +- 添加 `"build:builtin-review": "bun run scripts/generate-review-builtin.ts"` 脚本 +- 将 `build:builtin` 改为包含 `build:builtin-review` + +### 9. 修改 `.gitignore` + +- 添加 `bundled-review/`(生成脚本的临时下载目录) +- 保留 `src/costrict/review/skill/builtin.ts` 在 `.gitignore`(生成产物) + +### 10. 清理 + +- 移除 `src/costrict/review/agent/builtin.ts`(不再有 review agent) +- 移除 `REVIEW_AGENTS`、`AGENT_VERSIONS`、`PRIMARY_REVIEW_AGENT`、`SUB_REVIEW_AGENT` 的导出和引用 +- 移除 `builtInAgents.ts` 中的 review agent 注册 +- 移除 strict:review / strict:security-review 的 bundled skill 注册 + +## Files Summary + +| Action | File | +|--------|------| +| Create | `scripts/generate-review-builtin.ts` | +| Create | `src/costrict/review/extension.ts` | +| Generated | `src/costrict/review/skill/builtin.ts` | +| Modify | `src/costrict/review/index.ts` | +| Delete | `src/costrict/skills/reviewSkills.ts` | +| Delete | `src/costrict/review/agent/builtin.ts` | +| Modify | `src/skills/bundled/index.ts` | +| Modify | `package.json` | +| Modify | `.gitignore` | +| Modify | Agent 注册(移除 review agent) | + +## Key Differences from Previous Design + +| Aspect | 旧方案 (registerBundledSkill) | 新方案 (extract-to-disk) | +|--------|------|------| +| 运行时 | 内存内嵌 + getPromptForCommand | 提取到磁盘 + 标准 scanner | +| 发现方式 | registerBundledSkill 注册到数组 | skill scanner 扫描目录 | +| 版本追踪 | 无 | .version 文件(sha:locale) | +| strict 变种 | 单独注册 bundled skill | 不需要,走标准流程 | +| extension.ts | 删除 | 新增(提取逻辑) | diff --git a/package.json b/package.json index ca07aa52a..828999ffe 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ ], "scripts": { "build": "bun run build.ts", + "build:builtin-review": "bun run scripts/generate-review-builtin.ts", "build:vite": "vite build && bun run scripts/post-build.ts", "build:vite:only": "vite build", "build:bun": "bun run build.ts", diff --git a/scripts/generate-review-builtin.ts b/scripts/generate-review-builtin.ts index 7515a99a5..7e10eb366 100644 --- a/scripts/generate-review-builtin.ts +++ b/scripts/generate-review-builtin.ts @@ -258,6 +258,26 @@ export function getSkillFiles(skillName: string, locale: string): Record { + const localeData = SKILL_FILES[locale] + if (!localeData) { + throw new Error(\`Locale not found: \${locale}\`) + } + + const skillFiles = localeData[skillName] + if (!skillFiles) { + throw new Error(\`Skill not found: \${skillName}\`) + } + + const { mkdir: mkdirSync, writeFile: writeFileSync } = await import('fs/promises') + const { join: pathJoin, dirname: pathDirname } = await import('path') + await mkdirSync(targetDir, { recursive: true }) + for (const [relativePath, fileContent] of Object.entries(skillFiles)) { + await mkdirSync(pathJoin(targetDir, pathDirname(relativePath)), { recursive: true }) + await writeFileSync(pathJoin(targetDir, relativePath), fileContent, 'utf-8') + } +} ` await mkdir(path.dirname(builtinSkillsFile), { recursive: true }) diff --git a/src/costrict/review/extension.ts b/src/costrict/review/extension.ts new file mode 100644 index 000000000..54b26cfa2 --- /dev/null +++ b/src/costrict/review/extension.ts @@ -0,0 +1,72 @@ +import { mkdir, readFile, writeFile } from 'fs/promises' +import { join } from 'path' +import { getClaudeConfigHomeDir } from 'src/utils/envUtils.js' +import { getResolvedLanguage } from 'src/utils/language.js' +import { + extractBundledSkill, + getBuiltinSkillVersion, + listBuiltinSkillNames, +} from './skill/builtin.js' + +const LOCALE_MAP: Record = { zh: 'zh-CN', en: 'en' } + +function getLocale(): string { + const lang = getResolvedLanguage() + return LOCALE_MAP[lang] ?? 'zh-CN' +} + +function getReviewSkillsDir(): string { + return join(getClaudeConfigHomeDir(), 'skills') +} + +function getVersionFilePath(skillDir: string): string { + return join(skillDir, '.version') +} + +async function getInstalledVersion(skillDir: string): Promise { + try { + return await readFile(getVersionFilePath(skillDir), 'utf-8') + } catch { + return null + } +} + +async function writeVersionFile( + skillDir: string, + skillName: string, + locale: string, +): Promise { + const builtinVersion = getBuiltinSkillVersion(skillName) + if (!builtinVersion) return + await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8') +} + +async function needsUpdate( + skillDir: string, + skillName: string, + locale: string, +): Promise { + const builtinVersion = getBuiltinSkillVersion(skillName) + if (!builtinVersion) return true + const installed = await getInstalledVersion(skillDir) + return installed !== `${builtinVersion}:${locale}` +} + +export async function initializeBuiltinSkills(): Promise { + const locale = getLocale() + const skillsDir = getReviewSkillsDir() + const skillNames = listBuiltinSkillNames() + + for (const skillName of skillNames) { + const skillDir = join(skillsDir, skillName) + if (!(await needsUpdate(skillDir, skillName, locale))) continue + + await mkdir(skillDir, { recursive: true }) + await extractBundledSkill(skillName, skillDir, locale) + await writeVersionFile(skillDir, skillName, locale) + } +} + +export function getBuiltinSkillsDir(): string { + return getReviewSkillsDir() +} diff --git a/src/costrict/review/index.ts b/src/costrict/review/index.ts index 84cdb0634..01f347fbb 100644 --- a/src/costrict/review/index.ts +++ b/src/costrict/review/index.ts @@ -1,14 +1,9 @@ /** * CoStrict Review Module * - * Provides builtin review skills that are embedded in the binary - * and registered as bundled skills via registerBundledSkill(). + * Provides builtin review skills that are extracted to disk at runtime + * and discovered by the standard skill scanner. */ export * as SkillBuiltin from './skill/builtin.js' -export { - REVIEW_AGENTS, - AGENT_VERSIONS, - PRIMARY_REVIEW_AGENT, - SUB_REVIEW_AGENT, -} from './agent/builtin.js' +export * as Extension from './extension.js' diff --git a/src/costrict/skills/reviewSkills.ts b/src/costrict/skills/reviewSkills.ts deleted file mode 100644 index b324c3108..000000000 --- a/src/costrict/skills/reviewSkills.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { registerBundledSkill } from 'src/skills/bundledSkills.js' -import { getResolvedLanguage } from 'src/utils/language.js' -import { CommandLocale } from 'src/costrict/command/locales/index.js' -import { - SKILL_FILES, - SKILL_METADATA, -} from 'src/costrict/review/skill/builtin.js' - -const LOCALE_MAP: Record = { zh: 'zh-CN', en: 'en' } - -function getLocale(): string { - const lang = getResolvedLanguage() - return LOCALE_MAP[lang] ?? 'zh-CN' -} - -const ALLOWED_TOOLS = [ - 'Skill', - 'Glob', - 'Grep', - 'Read', - 'TodoWrite', - 'Bash', - 'Agent', -] - -function registerReviewSkill( - name: string, - skillKey: string, - files: Record, - description: string, - forked: boolean, -): void { - registerBundledSkill({ - name, - description, - whenToUse: description, - userInvocable: true, - disableModelInvocation: true, - allowedTools: ALLOWED_TOOLS, - context: forked ? 'fork' : undefined, - files, - async getPromptForCommand(args) { - const template = CommandLocale.get(skillKey) - const text = template - ? template.replace('$ARGUMENTS', args.trim()) - : args.trim() || `Please perform a ${skillKey}.` - return [{ type: 'text', text }] - }, - }) -} - -export function registerReviewSkills(): void { - const locale = getLocale() - const localeFiles = SKILL_FILES[locale] - const localeMetadata = SKILL_METADATA[locale] - if (!localeFiles || !localeMetadata) return - - for (const [skillKey, files] of Object.entries(localeFiles)) { - const meta = localeMetadata[skillKey] - if (!meta || !files) continue - - // /review, /security-review — inline in main session - registerReviewSkill(meta.name, skillKey, files, meta.description, false) - - // /strict:review, /strict:security-review — forked sub-agent - registerReviewSkill(`strict:${skillKey}`, skillKey, files, meta.description, true) - } -} diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index 03ce7c7f4..a082c2a6e 100644 --- a/src/skills/bundled/index.ts +++ b/src/skills/bundled/index.ts @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js' -import { registerReviewSkills } from 'src/costrict/skills/reviewSkills.js' +import { Extension as ReviewExtension } from 'src/costrict/review/index.js' import { registerBatchSkill } from './batch.js' import { registerClaudeInChromeSkill } from './claudeInChrome.js' import { registerDebugSkill } from './debug.js' @@ -30,8 +30,8 @@ import { registerTddSkill } from 'src/costrict/skills/tdd.js' * 3. Import and call that function here */ export function initBundledSkills(): void { - // Register review skills (bundled with embedded files) - registerReviewSkills() + // Extract review skills to disk for standard scanner discovery + ReviewExtension.initializeBuiltinSkills().catch(() => {}) registerUpdateConfigSkill() registerProjectWikiSkill()