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 <noreply@anthropic.com>
This commit is contained in:
kingboung 2026-05-12 11:49:38 +08:00
parent e68415c7e9
commit c3e19d7d2a
9 changed files with 734 additions and 80 deletions

3
.gitignore vendored
View File

@ -48,4 +48,5 @@ src/costrict/review/agent/builtin.ts
src/costrict/review/skill/builtin.ts
# Review builtin cache
packages/builtin-tools/bundled-review/
packages/builtin-tools/bundled-review/
bundled-review/

View File

@ -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/<name>/` 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<void> {
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<string, string> = { 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<string | null> {
try {
return await readFile(getVersionFilePath(skillDir), 'utf-8')
} catch {
return null
}
}
async function writeVersionFile(
skillDir: string,
skillName: string,
locale: string,
): Promise<void> {
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<boolean> {
const builtinVersion = getBuiltinSkillVersion(skillName)
if (!builtinVersion) return true
const installed = await getInstalledVersion(skillDir)
return installed !== `${builtinVersion}:${locale}`
}
export async function initializeBuiltinSkills(): Promise<void> {
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<string, string>
export function getSkillMetadata(skillName: string, locale: string): ...
export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise<void>
```
- [ ] **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"
```

View File

@ -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<string, Record<string, Record<string, string>>>` (locale → skillName → filePath → content)
- `SKILL_METADATA: Record<string, Record<string, { name: string; description: string }>>` (locale → skillName → 元数据,从 SKILL.md frontmatter 解析)
- `SKILL_VERSIONS: Record<string, string>` (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 | 删除 | 新增(提取逻辑) |

View File

@ -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",

View File

@ -258,6 +258,26 @@ export function getSkillFiles(skillName: string, locale: string): Record<string,
export function getSkillMetadata(skillName: string, locale: string): { name: string; description: string } | undefined {
return SKILL_METADATA[locale]?.[skillName]
}
export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise<void> {
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 })

View File

@ -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<string, string> = { 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<string | null> {
try {
return await readFile(getVersionFilePath(skillDir), 'utf-8')
} catch {
return null
}
}
async function writeVersionFile(
skillDir: string,
skillName: string,
locale: string,
): Promise<void> {
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<boolean> {
const builtinVersion = getBuiltinSkillVersion(skillName)
if (!builtinVersion) return true
const installed = await getInstalledVersion(skillDir)
return installed !== `${builtinVersion}:${locale}`
}
export async function initializeBuiltinSkills(): Promise<void> {
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()
}

View File

@ -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'

View File

@ -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<string, string> = { 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<string, string>,
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)
}
}

View File

@ -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()