Merge pull request #64 from y574444354/feat/migrate-review-agents

【Review】feat: 将 review skill 迁移到 extract-to-disk 架构
This commit is contained in:
geroge 2026-05-13 19:38:26 +08:00 committed by GitHub
commit 42107f7d2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1754 additions and 2365 deletions

View File

@ -24,7 +24,7 @@ jobs:
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2, 2026-04-25 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2, 2026-04-25
with: with:
bun-version: latest bun-version: "1.3.11"
- name: Install dependencies - name: Install dependencies
env: env:
@ -34,7 +34,7 @@ jobs:
- name: Setup SSH for review agent generation - name: Setup SSH for review agent generation
uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.9.0
with: with:
ssh-private-key: ${{ secrets.CO_STRICT_REVIEW_SSH_KEY }} ssh-private-key: ${{ secrets.MY_SSH_PRIVATE_KEY }}
- name: Generate review builtin files - name: Generate review builtin files
run: bun run scripts/generate-review-builtin.ts run: bun run scripts/generate-review-builtin.ts

13
.gitignore vendored
View File

@ -49,15 +49,4 @@ src/costrict/review/skill/builtin.ts
# Review builtin cache # Review builtin cache
packages/builtin-tools/bundled-review/ packages/builtin-tools/bundled-review/
bundled-review/
# Upstream additions
teach-me
credentials.json
# Session-scoped progress / state files written by agents and skills
.claude-impl-state.md
.claude-progress.md
.claude-recovery.md
.test-progress.md
.squash-tmp/
.git.*-backup

View File

@ -13,7 +13,7 @@ console.log('Generating review builtin files...')
const { spawnSync: genSpawnSync } = await import('child_process') const { spawnSync: genSpawnSync } = await import('child_process')
const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], { const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], {
stdio: 'inherit', stdio: 'inherit',
cwd: new URL('.', import.meta.url).pathname, cwd: process.cwd(),
}) })
if (genResult.status !== 0) { if (genResult.status !== 0) {
console.warn('Warning: generate-review-builtin.ts failed, using existing files') console.warn('Warning: generate-review-builtin.ts failed, using existing files')

3514
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@
], ],
"scripts": { "scripts": {
"build": "bun run build.ts", "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": "vite build && bun run scripts/post-build.ts",
"build:vite:only": "vite build", "build:vite:only": "vite build",
"build:bun": "bun run build.ts", "build:bun": "bun run build.ts",
@ -63,11 +64,8 @@
"lint": "biome lint src/", "lint": "biome lint src/",
"lint:fix": "biome lint --fix src/", "lint:fix": "biome lint --fix src/",
"format": "biome format --write src/", "format": "biome format --write src/",
"check": "biome check .", "prepare": "git config core.hooksPath .githooks",
"check:fix": "biome check --fix .",
"prepare": "husky",
"test": "bun test", "test": "bun test",
"precheck": "bun run typecheck && bun run check:fix && bun test",
"check:unused": "knip-bun", "check:unused": "knip-bun",
"health": "bun run scripts/health-check.ts", "health": "bun run scripts/health-check.ts",
"postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs scripts/install-cosknow.mjs", "postinstall": "node scripts/run-parallel.mjs scripts/postinstall.cjs scripts/setup-chrome-mcp.mjs scripts/install-cosknow.mjs",
@ -89,8 +87,8 @@
"@ant/computer-use-input": "workspace:*", "@ant/computer-use-input": "workspace:*",
"@ant/computer-use-mcp": "workspace:*", "@ant/computer-use-mcp": "workspace:*",
"@ant/computer-use-swift": "workspace:*", "@ant/computer-use-swift": "workspace:*",
"@anthropic-ai/bedrock-sdk": "^0.29.0", "@anthropic-ai/bedrock-sdk": "^0.26.4",
"@anthropic-ai/claude-agent-sdk": "^0.2.114", "@anthropic-ai/claude-agent-sdk": "^0.2.87",
"@anthropic-ai/foundry-sdk": "^0.2.3", "@anthropic-ai/foundry-sdk": "^0.2.3",
"@anthropic-ai/mcpb": "^2.1.2", "@anthropic-ai/mcpb": "^2.1.2",
"@anthropic-ai/sandbox-runtime": "^0.0.44", "@anthropic-ai/sandbox-runtime": "^0.0.44",
@ -180,11 +178,10 @@
"image-processor-napi": "workspace:*", "image-processor-napi": "workspace:*",
"indent-string": "^5.0.0", "indent-string": "^5.0.0",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"knip": "^6.4.1", "knip": "^6.1.1",
"lint-staged": "^16.4.0", "lodash-es": "^4.17.23",
"lodash-es": "^4.18.1", "lru-cache": "^11.2.7",
"lru-cache": "^11.3.5", "marked": "^17.0.5",
"marked": "^17.0.6",
"modifiers-napi": "workspace:*", "modifiers-napi": "workspace:*",
"openai": "^6.33.0", "openai": "^6.33.0",
"p-map": "^7.0.4", "p-map": "^7.0.4",
@ -217,24 +214,5 @@
"xss": "^1.0.15", "xss": "^1.0.15",
"yaml": "^2.8.3", "yaml": "^2.8.3",
"zod": "^4.3.6" "zod": "^4.3.6"
},
"optionalDependencies": {
"doubaoime-asr": "^0.1.0"
},
"overrides": {
"@inquirer/prompts": "8.4.2",
"@xmldom/xmldom": "0.8.13",
"follow-redirects": "1.16.0",
"hono": "4.12.15",
"postcss": "8.5.10",
"uuid": "14.0.0"
},
"lint-staged": {
"*.{ts,tsx,js,mjs,jsx}": [
"biome check --fix --no-errors-on-unmatched"
],
"*.{json,jsonc}": [
"biome format --write --no-errors-on-unmatched"
]
} }
} }

View File

@ -2,7 +2,7 @@ import { feature } from 'bun:bundle'
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js' import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { isEnvTruthy } from 'src/utils/envUtils.js' import { isEnvTruthy } from 'src/utils/envUtils.js'
import { REVIEW_AGENTS } from 'src/costrict/review/agent/builtin.js'
import { DESIGN_AGENT } from 'src/costrict/agents/designAgent.js' import { DESIGN_AGENT } from 'src/costrict/agents/designAgent.js'
import { QUICK_EXPLORE_AGENT } from 'src/costrict/agents/quickExplore.js' import { QUICK_EXPLORE_AGENT } from 'src/costrict/agents/quickExplore.js'
import { REQUIREMENT_AGENT } from 'src/costrict/agents/requirement.js' import { REQUIREMENT_AGENT } from 'src/costrict/agents/requirement.js'
@ -112,8 +112,5 @@ export function getBuiltInAgents(): AgentDefinition[] {
agents.push(VERIFICATION_AGENT) agents.push(VERIFICATION_AGENT)
} }
// Review agents (CoStrictReviewer, CoStrictValidator)
agents.push(...REVIEW_AGENTS)
return agents return agents
} }

View File

@ -1,10 +1,10 @@
#!/usr/bin/env bun #!/usr/bin/env bun
/** /**
* Downloads builtin review skills & agents from costrict-review repo and generates * Downloads builtin review skills from costrict-review repo and generates
* src/costrict/review/skill/builtin.ts and src/costrict/review/agent/builtin.ts * src/costrict/review/skill/builtin.ts
* *
* Uses git SSH transport (git ls-remote + git clone). * Uses git SSH transport (git ls-remote + git clone).
* Reads index.json manifest to discover resources and their per-locale paths. * Reads index.json manifest to discover skills and their per-locale paths.
* Compares remote commit SHA with cached version and skips download if unchanged. * Compares remote commit SHA with cached version and skips download if unchanged.
* *
* Usage: bun run scripts/generate-review-builtin.ts * Usage: bun run scripts/generate-review-builtin.ts
@ -22,38 +22,17 @@ const __dirname = path.dirname(__filename)
const bundledReviewDir = path.resolve(__dirname, '../packages/builtin-tools/bundled-review') const bundledReviewDir = path.resolve(__dirname, '../packages/builtin-tools/bundled-review')
const builtinSkillsFile = path.resolve(__dirname, '../src/costrict/review/skill/builtin.ts') const builtinSkillsFile = path.resolve(__dirname, '../src/costrict/review/skill/builtin.ts')
const builtinAgentsFile = path.resolve(__dirname, '../src/costrict/review/agent/builtin.ts')
type IndexJson = { type IndexJson = {
agents: Array<{
name: string
path: Record<string, string>
opencode?: Record<string, unknown>
claudecode?: Record<string, unknown>
}>
skills: Array<{ name: string; path: Record<string, string> }> skills: Array<{ name: string; path: Record<string, string> }>
} }
const REPO = 'zgsm-ai/costrict-review' const REPO = 'zgsm-ai/costrict-review'
const BRANCH = 'main' const BRANCH = 'main'
const CLONE_URL = getCloneUrl() const CLONE_URL = `git@github.com:${REPO}.git`
function getCloneUrl(): string {
// Try gh CLI token first, then env vars, fall back to public HTTPS
const ghToken = (() => {
try {
return spawnSync('gh', ['auth', 'token'], { encoding: 'utf-8' }).stdout?.trim() || ''
} catch { return '' }
})()
const token = ghToken || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || ''
if (token) {
return `https://x-access-token:${token}@github.com/${REPO}.git`
}
return `https://github.com/${REPO}.git`
}
function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } { function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } {
const result = spawnSync('git', args, { encoding: 'utf-8' }) const result = spawnSync('git', args, { encoding: 'utf-8', env: process.env })
return { return {
ok: result.status === 0, ok: result.status === 0,
stdout: result.stdout?.trim() ?? '', stdout: result.stdout?.trim() ?? '',
@ -108,19 +87,26 @@ function collectLocales(index: IndexJson): string[] {
for (const skill of index.skills) { for (const skill of index.skills) {
for (const locale of Object.keys(skill.path)) localeSet.add(locale) for (const locale of Object.keys(skill.path)) localeSet.add(locale)
} }
for (const agent of index.agents) {
for (const locale of Object.keys(agent.path)) localeSet.add(locale)
}
return [...localeSet].sort() return [...localeSet].sort()
} }
function mergeClaudecodeFrontmatter( /**
mdContent: string, * Extract locale and skill directory from index.json path.
claudecodeFields: Record<string, unknown>, * New format: "skills/<locale>/<skillName>/SKILL.md"
): string { * Old format: "<locale>/skills/<skillName>/SKILL.md" (for backwards compat)
const md = matter(mdContent) */
const merged = { ...md.data, ...claudecodeFields } function parseSkillPath(skillMdPath: string): { locale: string; skillDir: string } | null {
return matter.stringify(md.content, merged) // New format: skills/<locale>/<skillName>/SKILL.md
const newMatch = skillMdPath.match(/^skills\/([^/]+)\/(.+)$/)
if (newMatch) {
return { locale: newMatch[1], skillDir: newMatch[2].replace(/\/SKILL\.md$/, '') }
}
// Old format: <locale>/skills/<skillName>/SKILL.md
const oldMatch = skillMdPath.match(/^([^/]+)\/skills\/(.+)$/)
if (oldMatch) {
return { locale: oldMatch[1], skillDir: oldMatch[2].replace(/\/SKILL\.md$/, '') }
}
return null
} }
async function cloneAndCopy( async function cloneAndCopy(
@ -141,61 +127,26 @@ async function cloneAndCopy(
for (const locale of locales) { for (const locale of locales) {
const outputLocaleDir = path.join(bundledReviewDir, locale) const outputLocaleDir = path.join(bundledReviewDir, locale)
// Copy skill dirs for this locale
const skillPaths = index.skills const skillPaths = index.skills
.map(s => s.path[locale]) .map(s => s.path[locale])
.filter(Boolean) .filter(Boolean)
for (const skillMdPath of skillPaths) { for (const skillMdPath of skillPaths) {
const parsed = parseSkillPath(skillMdPath)
if (!parsed) {
console.warn(` ⚠ Skipping unparseable path: ${skillMdPath}`)
continue
}
const { skillDir } = parsed
const srcDir = path.join(cloneDir, path.dirname(skillMdPath)) const srcDir = path.join(cloneDir, path.dirname(skillMdPath))
const relativeDir = skillMdPath.startsWith(`${locale}/`) const outputDir = path.join(outputLocaleDir, skillDir)
? skillMdPath.slice(locale.length + 1).replace(/\/[^/]*$/, '')
: path.dirname(skillMdPath)
const outputDir = path.join(outputLocaleDir, relativeDir)
await fs.rm(outputDir, { recursive: true, force: true }) await fs.rm(outputDir, { recursive: true, force: true })
await fs.cp(srcDir, outputDir, { recursive: true }) await fs.cp(srcDir, outputDir, { recursive: true })
const skillMd = path.join(outputDir, 'SKILL.md')
try {
await fs.access(skillMd)
} catch {
throw new Error(`Skill (${locale}) missing SKILL.md at ${skillMdPath}`)
}
const skillName = path.basename(srcDir)
const fileCount = (await walk(outputDir)).length const fileCount = (await walk(outputDir)).length
console.log(`${locale}/skills/${skillName}: ${fileCount} files`) console.log(`${locale}/${skillDir}: ${fileCount} files`)
}
// Copy agent files for this locale, merging claudecode frontmatter
const agentEntries = index.agents
.map(a => ({ name: a.name, filePath: a.path[locale], claudecode: a.claudecode }))
.filter(e => e.filePath)
for (const { name, filePath, claudecode } of agentEntries) {
const srcFile = path.join(cloneDir, filePath)
const outputDir = path.join(outputLocaleDir, 'agents')
await fs.mkdir(outputDir, { recursive: true })
const filename = path.basename(filePath)
const destFile = path.join(outputDir, filename)
if (claudecode) {
const rawContent = await fs.readFile(srcFile, 'utf-8')
const merged = mergeClaudecodeFrontmatter(rawContent, claudecode)
await fs.writeFile(destFile, merged, 'utf-8')
} else {
await fs.cp(srcFile, destFile)
}
try {
await fs.access(destFile)
} catch {
throw new Error(`Agent "${name}" (${locale}) missing at ${filePath}`)
}
console.log(`${locale}/agents/${filename}`)
} }
} }
@ -213,67 +164,99 @@ async function generateBuiltinSkills(
} }
} }
const allSkillNames: string[] = []
// Discover skill names from first locale // Discover skill names from first locale
const allSkillNames: string[] = []
for (const locale of locales) { for (const locale of locales) {
const skillsDir = path.join(bundledReviewDir, locale, 'skills') const entries = await fs.readdir(path.join(bundledReviewDir, locale)).catch(() => [] as string[])
const entries = await fs.readdir(skillsDir).catch(() => [] as string[])
for (const name of entries) { for (const name of entries) {
if (!allSkillNames.includes(name)) allSkillNames.push(name) const p = path.join(bundledReviewDir, locale, name)
if ((await fs.stat(p).catch(() => null))?.isDirectory()) {
if (!allSkillNames.includes(name)) allSkillNames.push(name)
}
} }
break break
} }
const imports: string[] = [] const skillFileEntries: string[] = []
const localeSkillEntries: string[] = [] const metadataEntries: string[] = []
let fileIdx = 0 let fileIdx = 0
for (const locale of [...locales].sort()) { for (const locale of [...locales].sort()) {
const skillEntries: string[] = [] const fileEntries: string[] = []
const metaEntries: string[] = []
for (const skillName of allSkillNames) { for (const skillName of allSkillNames) {
const skillDir = path.join(bundledReviewDir, locale, 'skills', skillName) const skillDir = path.join(bundledReviewDir, locale, skillName)
const files = await walk(skillDir) const files = await walk(skillDir)
const fileEntries: string[] = []
// Parse SKILL.md for metadata
let skillMeta = { name: skillName, description: '' }
const skillMdPath = path.join(skillDir, 'SKILL.md')
try {
const content = await fs.readFile(skillMdPath, 'utf-8')
const parsed = matter(content)
skillMeta = {
name: String(parsed.data.name ?? skillName),
description: String(parsed.data.description ?? ''),
}
} catch {
// SKILL.md not found, use defaults
}
const fileRecords: string[] = []
for (const file of files) { for (const file of files) {
const varName = `SKILL_FILE_${fileIdx++}` const varName = `SKILL_FILE_${fileIdx++}`
const filePath = path.join(skillDir, file) const filePath = path.join(skillDir, file)
const content = await fs.readFile(filePath, 'utf-8') const content = await fs.readFile(filePath, 'utf-8')
const normalizedPath = file.replaceAll('\\', '/') const normalizedPath = file.replaceAll('\\', '/')
imports.push(`const ${varName} = ${JSON.stringify(content)}`) fileRecords.push(` "${normalizedPath}": ${JSON.stringify(content)}`)
fileEntries.push(` "${normalizedPath}": ${varName}`)
} }
skillEntries.push(` "${skillName}": {\n${fileEntries.join(',\n')}\n }`)
fileEntries.push(` "${skillName}": {\n${fileRecords.join(',\n')}\n }`)
metaEntries.push(` "${skillName}": ${JSON.stringify(skillMeta)}`)
} }
localeSkillEntries.push(` "${locale}": {\n${skillEntries.join(',\n')}\n }`)
skillFileEntries.push(` "${locale}": {\n${fileEntries.join(',\n')}\n }`)
metadataEntries.push(` "${locale}": {\n${metaEntries.join(',\n')}\n }`)
} }
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
// Do not edit manually // Do not edit manually
// Skills are downloaded from zgsm-ai/costrict-review repository
import { writeFile, mkdir } from "fs/promises" // locale → skillName → filePath → content
import { join, dirname } from "path" export const SKILL_FILES: Record<string, Record<string, Record<string, string>>> = {
${skillFileEntries.join(',\n')}
${imports.join('\n')}
const SKILL_FILES: Record<string, Record<string, Record<string, string>>> = {
${localeSkillEntries.join(',\n')}
} }
const SKILL_VERSIONS: Record<string, string> = { // locale → skillName → { name, description }
export const SKILL_METADATA: Record<string, Record<string, { name: string; description: string }>> = {
${metadataEntries.join(',\n')}
}
// skillName → commit SHA
export const SKILL_VERSIONS: Record<string, string> = {
${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')} ${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')}
} }
export function listBuiltinSkills(): string[] { // List all skill names
export function listBuiltinSkillNames(): string[] {
return ${JSON.stringify(allSkillNames)} return ${JSON.stringify(allSkillNames)}
} }
// Get version for a skill
export function getBuiltinSkillVersion(skillName: string): string | undefined { export function getBuiltinSkillVersion(skillName: string): string | undefined {
return SKILL_VERSIONS[skillName] return SKILL_VERSIONS[skillName]
} }
export function listSkillFiles(skillName: string, locale: string): string[] { // Get files for a skill in a specific locale
return Object.keys(SKILL_FILES[locale]?.[skillName] || {}) export function getSkillFiles(skillName: string, locale: string): Record<string, string> {
return SKILL_FILES[locale]?.[skillName] ?? {}
}
// 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]
} }
export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise<void> { export async function extractBundledSkill(skillName: string, targetDir: string, locale: string): Promise<void> {
@ -287,10 +270,14 @@ export async function extractBundledSkill(skillName: string, targetDir: string,
throw new Error(\`Skill not found: \${skillName}\`) throw new Error(\`Skill not found: \${skillName}\`)
} }
await mkdir(targetDir, { recursive: true }) const { rm, mkdir: mkdirSync, writeFile: writeFileSync } = await import('fs/promises')
for (const [relativePath, content] of Object.entries(skillFiles)) { const { join: pathJoin, dirname: pathDirname } = await import('path')
await mkdir(join(targetDir, dirname(relativePath)), { recursive: true }) // Remove stale files from previous version/locale before extracting
await writeFile(join(targetDir, relativePath), content, "utf-8") await rm(targetDir, { recursive: true, force: true })
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')
} }
} }
` `
@ -300,155 +287,8 @@ export async function extractBundledSkill(skillName: string, targetDir: string,
console.log(`\n✓ Generated ${builtinSkillsFile}`) console.log(`\n✓ Generated ${builtinSkillsFile}`)
} }
async function generateBuiltinAgents(
commitSha: string,
): Promise<void> {
const localeEntries = await fs.readdir(bundledReviewDir).catch(() => [] as string[])
const locales: string[] = []
for (const l of localeEntries) {
if ((await fs.stat(path.join(bundledReviewDir, l)).catch(() => null))?.isDirectory()) {
locales.push(l)
}
}
// Discover agent names from first locale's agents dir
const allAgentNames: string[] = []
for (const locale of locales) {
const agentsDir = path.join(bundledReviewDir, locale, 'agents')
const entries = await fs.readdir(agentsDir).catch(() => [] as string[])
for (const name of entries) {
if (!allAgentNames.includes(name)) allAgentNames.push(name)
}
break
}
const imports: string[] = []
const agentCodeEntries: string[] = []
let fileIdx = 0
let primaryAgent = ''
let subAgent = ''
for (const agentName of allAgentNames) {
const agentKey = agentName.replace(/\.md$/, '')
const localeEntriesList: string[] = []
const promptVarNames: string[] = []
for (const locale of [...locales].sort()) {
const agentFile = path.join(bundledReviewDir, locale, 'agents', agentName)
try {
const content = await fs.readFile(agentFile, 'utf-8')
const varName = `REVIEW_AGENT_${fileIdx++}`
imports.push(`const ${varName} = ${JSON.stringify(content)}`)
localeEntriesList.push(`"${locale}": ${varName}`)
promptVarNames.push(varName)
if (content.includes('mode: primary')) primaryAgent = agentKey
if (content.includes('mode: subagent')) subAgent = agentKey
} catch {
// Agent file not found for this locale, skip
}
}
if (localeEntriesList.length > 0) {
const promptRecordName = `${agentKey.toUpperCase().replace(/-/g, '_')}_PROMPTS`
agentCodeEntries.push(`const ${promptRecordName}: Record<string, string> = {\n${localeEntriesList.join(',\n')}\n}`)
}
}
// Now generate the BuiltInAgentDefinition array
// Parse merged frontmatter from the zh-CN agent files to extract fields
const agentDefs: string[] = []
for (const agentName of allAgentNames) {
const agentKey = agentName.replace(/\.md$/, '')
const promptRecordName = `${agentKey.toUpperCase().replace(/-/g, '_')}_PROMPTS`
// Read zh-CN version to parse frontmatter for fields
const zhAgentFile = path.join(bundledReviewDir, 'zh-CN', 'agents', agentName)
let frontmatterData: Record<string, unknown> = {}
try {
const content = await fs.readFile(zhAgentFile, 'utf-8')
const parsed = matter(content)
frontmatterData = parsed.data as Record<string, unknown>
} catch {
// fallback
}
// Extract fields from frontmatter
const whenToUse = String(frontmatterData['whenToUse'] ?? frontmatterData['description'] ?? '')
const toolsStr = String(frontmatterData['tools'] ?? '')
const tools = toolsStr ? toolsStr.split(',').map((t: string) => t.trim()).filter(Boolean) : undefined
const permissionMode = String(frontmatterData['permissionMode'] ?? 'default')
const model = String(frontmatterData['model'] ?? 'inherit')
const mode = String(frontmatterData['mode'] ?? '')
// Determine disallowedTools based on tools availability
const toolNameSet = new Set(tools ?? [])
const disallowedTools: string[] = []
if (!toolNameSet.has('Agent')) disallowedTools.push('Agent')
if (!toolNameSet.has('FileEdit')) disallowedTools.push('FileEdit')
if (!toolNameSet.has('FileWrite')) disallowedTools.push('FileWrite')
if (!toolNameSet.has('NotebookEdit')) disallowedTools.push('NotebookEdit')
// Determine visibleTo based on mode
const visibleTo = mode === 'primary' ? undefined : ['CoStrictReviewer']
const toolsLiteral = tools ? JSON.stringify(tools) : 'undefined'
const disallowedLiteral = disallowedTools.length > 0
? `[${disallowedTools.map((t: string) => `'${t}'`).join(', ')}]`
: 'undefined'
const visibleToLiteral = visibleTo ? JSON.stringify(visibleTo) : 'undefined'
agentDefs.push(` {
agentType: '${agentKey}',
whenToUse: ${JSON.stringify(whenToUse)},
tools: ${toolsLiteral} as string[] | undefined,
disallowedTools: ${disallowedLiteral} as string[] | undefined,
permissionMode: '${permissionMode}',
model: '${model}',
source: 'built-in',
baseDir: 'built-in',
visibleTo: ${visibleToLiteral} as string[] | undefined,
getSystemPrompt: (_params) => {
const lang = getResolvedLanguage()
const locale = LOCALE_MAP[lang] ?? 'zh-CN'
return ${promptRecordName}[locale] ?? ${promptRecordName}['zh-CN']
},
}`)
}
const content = `// This file is auto-generated by scripts/generate-review-builtin.ts
// Do not edit manually
// Agents are downloaded from zgsm-ai/costrict-review repository
import type { BuiltInAgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import { getResolvedLanguage } from 'src/utils/language.js'
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
${imports.join('\n')}
${agentCodeEntries.join('\n\n')}
export const REVIEW_AGENTS: BuiltInAgentDefinition[] = [
${agentDefs.join(',\n')}
]
export const AGENT_VERSIONS: Record<string, string> = {
${allAgentNames.map(n => ` "${n.replace(/\.md$/, '')}": "${commitSha}"`).join(',\n')}
}
export const PRIMARY_REVIEW_AGENT = ${JSON.stringify(primaryAgent)}
export const SUB_REVIEW_AGENT = ${JSON.stringify(subAgent)}
`
await mkdir(path.dirname(builtinAgentsFile), { recursive: true })
await fs.writeFile(builtinAgentsFile, content, 'utf-8')
console.log(`✓ Generated ${builtinAgentsFile}`)
}
async function generateBuiltinReview() { async function generateBuiltinReview() {
console.log('\n🚀 CSC — Downloading Builtin Review Resources\n') console.log('\n🚀 CSC — Downloading Builtin Review Skills\n')
await fs.mkdir(bundledReviewDir, { recursive: true }) await fs.mkdir(bundledReviewDir, { recursive: true })
@ -496,9 +336,7 @@ async function generateBuiltinReview() {
await generateBuiltinSkills(commitSha) await generateBuiltinSkills(commitSha)
await generateBuiltinAgents(commitSha) console.log('\n💡 Run `bun run build` to compile\n')
console.log('\n💡 Run `bun run build` to compile the extension\n')
} }
generateBuiltinReview().catch(console.error) generateBuiltinReview().catch(console.error)

View File

@ -38,7 +38,7 @@ import pr_comments from './commands/pr_comments/index.js'
import releaseNotes from './commands/release-notes/index.js' import releaseNotes from './commands/release-notes/index.js'
import rename from './commands/rename/index.js' import rename from './commands/rename/index.js'
import resume from './commands/resume/index.js' import resume from './commands/resume/index.js'
import review, { ultrareview } from './commands/review.js' import { ultrareview } from './commands/review.js'
import session from './commands/session/index.js' import session from './commands/session/index.js'
import share from './commands/share/index.js' import share from './commands/share/index.js'
import skills from './commands/skills/index.js' import skills from './commands/skills/index.js'
@ -351,7 +351,6 @@ const COMMANDS = memoize((): Command[] => [
favorite, favorite,
theme, theme,
feedback, feedback,
review,
ultrareview, ultrareview,
rewind, rewind,
terminalSetup, terminalSetup,
@ -818,7 +817,11 @@ export function formatDescriptionWithSource(cmd: Command): string {
return cmd.description return cmd.description
} }
if (cmd.source === 'bundled') { if (
cmd.source === 'bundled' ||
cmd.name === 'review' ||
cmd.name === 'security-review'
) {
return `${cmd.description} (bundled)` return `${cmd.description} (bundled)`
} }

View File

@ -1,4 +1,5 @@
import type { ToolUseContext } from '../../Tool.js' import type { ToolUseContext } from '../../Tool.js'
import { clearCommandsCache } from '../../commands.js'
import type { import type {
LocalJSXCommandContext, LocalJSXCommandContext,
LocalJSXCommandOnDone, LocalJSXCommandOnDone,
@ -9,6 +10,7 @@ import {
getLanguageDisplayName, getLanguageDisplayName,
getResolvedLanguage, getResolvedLanguage,
} from '../../utils/language.js' } from '../../utils/language.js'
import { Extension as ReviewExtension } from '../../costrict/review/index.js'
const VALID_LANGS: readonly PreferredLanguage[] = ['en', 'zh', 'auto'] const VALID_LANGS: readonly PreferredLanguage[] = ['en', 'zh', 'auto']
@ -40,6 +42,10 @@ export async function call(
const lang = arg as PreferredLanguage const lang = arg as PreferredLanguage
saveGlobalConfig(current => ({ ...current, preferredLanguage: lang })) saveGlobalConfig(current => ({ ...current, preferredLanguage: lang }))
// Re-extract review skills for the new locale and refresh skill caches
await ReviewExtension.initializeBuiltinSkills().catch(() => {})
clearCommandsCache()
const resolved = getResolvedLanguage() const resolved = getResolvedLanguage()
const suffix = lang === 'auto' ? `${getLanguageDisplayName(resolved)}` : '' const suffix = lang === 'auto' ? `${getLanguageDisplayName(resolved)}` : ''
onDone(`Language set to ${getLanguageDisplayName(lang)}${suffix}`, { onDone(`Language set to ${getLanguageDisplayName(lang)}${suffix}`, {

View File

@ -1,27 +1,17 @@
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
import type { Command } from '../commands.js' import type { Command } from '../commands.js'
import type { ToolUseContext } from '../Tool.js'
import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js' import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js'
import { CommandLocale } from 'src/costrict/command/locales/index.js'
import { PRIMARY_REVIEW_AGENT } from 'src/costrict/review/agent/builtin.js'
// Legal wants the explicit surface name plus a docs link visible before the // Legal wants the explicit surface name plus a docs link visible before the
// user triggers, so the description carries "Claude Code on the web" + URL. // user triggers, so the description carries "Claude Code on the web" + URL.
const CCR_TERMS_URL = 'https://costrict.ai/docs/en/claude-code-on-the-web' const CCR_TERMS_URL = 'https://costrict.ai/docs/en/claude-code-on-the-web'
const review: Command = { // /review is registered as a bundled skill via registerReviewSkills() in
type: 'prompt', // src/costrict/skills/reviewSkills.ts, which provides the full SKILL.md
name: 'review', // content and reference files. This file only provides /ultrareview.
description: 'Review code for defects, security vulnerabilities, memory issues, and logic errors',
progressMessage: 'reviewing code',
contentLength: 0,
source: 'builtin',
agent: PRIMARY_REVIEW_AGENT || 'CoStrictReviewer',
async getPromptForCommand(args, _context): Promise<ContentBlockParam[]> {
return [{ type: 'text', text: CommandLocale.get('review').replace('$ARGUMENTS', args) }]
},
}
// /ultrareview is the ONLY entry point to the remote bughunter path —
// /review stays purely local. local-jsx type renders the overage permission
// dialog when free reviews are exhausted.
const ultrareview: Command = { const ultrareview: Command = {
type: 'local-jsx', type: 'local-jsx',
name: 'ultrareview', name: 'ultrareview',
@ -30,5 +20,4 @@ const ultrareview: Command = {
load: () => import('./review/ultrareviewCommand.js'), load: () => import('./review/ultrareviewCommand.js'),
} }
export default review
export { ultrareview } export { ultrareview }

View File

@ -1,18 +0,0 @@
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
import type { Command } from '../commands.js'
import type { ToolUseContext } from '../Tool.js'
import { CommandLocale } from 'src/costrict/command/locales/index.js'
const securityReview: Command = {
type: 'prompt',
name: 'security-review',
description: 'Complete a security review of the pending changes on the current branch',
progressMessage: 'analyzing code changes for security risks',
contentLength: 0,
source: 'builtin',
async getPromptForCommand(args, _context): Promise<ContentBlockParam[]> {
return [{ type: 'text', text: CommandLocale.get('security-review').replace('$ARGUMENTS', args) }]
},
}
export default securityReview

View File

@ -1,5 +1,5 @@
# Code Review # Code Review
Please perform a code review on: $ARGUMENTS 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. Please respond and write all files in English throughout the entire process.

View File

@ -1,5 +1,5 @@
# 代码审查 # 代码审查
请对以下内容执行代码审查:$ARGUMENTS 使用 Skill 工具加载 `review` 技能来对以下内容执行代码审查:$ARGUMENTS
全程请使用中文进行回答与文件写入。 全程请使用中文进行回答与文件写入。

View File

@ -1,87 +1,72 @@
/** import { mkdir, readFile, writeFile } from 'fs/promises'
* CoStrict Skill Extension import { join } from 'path'
* import { getClaudeConfigHomeDir } from 'src/utils/envUtils.js'
* Initializes builtin review skills by extracting them from embedded
* SKILL_FILES to a cache directory on disk.
*
* Version tracking uses commit SHA + locale in a .version file.
* Skills are re-extracted when version or locale changes.
*/
import path from 'path'
import { writeFile, readFile, rm, mkdir, stat } from 'fs/promises'
import { getResolvedLanguage } from 'src/utils/language.js' import { getResolvedLanguage } from 'src/utils/language.js'
import * as Builtin from './skill/builtin.js' import {
extractBundledSkill,
getBuiltinSkillVersion,
listBuiltinSkillNames,
} from './skill/builtin.js'
const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' } const LOCALE_MAP: Record<string, string> = { zh: 'zh-CN', en: 'en' }
function getSkillCacheDir(): string { function getLocale(): string {
const home = process.env.HOME ?? process.env.USERPROFILE ?? '' const lang = getResolvedLanguage()
return path.join(home, '.claude', 'skills') return LOCALE_MAP[lang] ?? 'zh-CN'
}
function getReviewSkillsDir(): string {
return join(getClaudeConfigHomeDir(), 'skills')
} }
function getVersionFilePath(skillDir: string): string { function getVersionFilePath(skillDir: string): string {
return path.join(skillDir, '.version') return join(skillDir, '.version')
} }
async function getInstalledVersion(skillDir: string): Promise<string | null> { async function getInstalledVersion(skillDir: string): Promise<string | null> {
try { try {
const content = await readFile(getVersionFilePath(skillDir), 'utf-8') return await readFile(getVersionFilePath(skillDir), 'utf-8')
return content.trim()
} catch { } catch {
return null return null
} }
} }
async function needsUpdate(skillDir: string, skillName: string, locale: string): Promise<boolean> { async function writeVersionFile(
const builtinVersion = Builtin.getBuiltinSkillVersion(skillName) skillDir: string,
if (!builtinVersion) return true skillName: string,
locale: string,
const installedVersion = await getInstalledVersion(skillDir) ): Promise<void> {
const expectedVersion = `${builtinVersion}:${locale}` const builtinVersion = getBuiltinSkillVersion(skillName)
return installedVersion !== expectedVersion
}
async function writeVersionFile(skillDir: string, skillName: string, locale: string): Promise<void> {
const builtinVersion = Builtin.getBuiltinSkillVersion(skillName)
if (!builtinVersion) return if (!builtinVersion) return
await mkdir(skillDir, { recursive: true })
await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8') 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> { export async function initializeBuiltinSkills(): Promise<void> {
const lang = getResolvedLanguage() const locale = getLocale()
const locale = LOCALE_MAP[lang] ?? 'zh-CN' const skillsDir = getReviewSkillsDir()
const skillNames = listBuiltinSkillNames()
const cacheDir = getSkillCacheDir() for (const skillName of skillNames) {
const skillNames = Builtin.listBuiltinSkills() const skillDir = join(skillsDir, skillName)
if (!(await needsUpdate(skillDir, skillName, locale))) continue
for (const name of skillNames) { await mkdir(skillDir, { recursive: true })
const skillDir = path.join(cacheDir, name) await extractBundledSkill(skillName, skillDir, locale)
await writeVersionFile(skillDir, skillName, locale)
const dirExists = await stat(skillDir).then(s => s.isDirectory()).catch(() => false)
if (dirExists) {
const updateNeeded = await needsUpdate(skillDir, name, locale)
if (!updateNeeded) continue
try {
await rm(skillDir, { recursive: true, force: true })
} catch {
// Continue with copy over existing files
}
}
await Builtin.extractBundledSkill(name, skillDir, locale)
await writeVersionFile(skillDir, name, locale)
const skillFiles = Builtin.listSkillFiles(name, locale)
const builtinVersion = Builtin.getBuiltinSkillVersion(name)
console.log(` [review] initialized skill "${name}" (${locale}, ${skillFiles.length} files, v${builtinVersion?.slice(0, 7)})`)
} }
} }
export function getBuiltinSkillsDir(): string { export function getBuiltinSkillsDir(): string {
return getSkillCacheDir() return getReviewSkillsDir()
} }

View File

@ -1,15 +1,9 @@
/** /**
* CoStrict Review Module * CoStrict Review Module
* *
* Provides builtin review skills and agents that are embedded * Provides builtin review skills that are extracted to disk at runtime
* in the binary and extracted to cache on first run. * and discovered by the standard skill scanner.
*/ */
export * as Extension from './extension.js'
export * as SkillBuiltin from './skill/builtin.js' export * as SkillBuiltin from './skill/builtin.js'
export { export * as Extension from './extension.js'
REVIEW_AGENTS,
AGENT_VERSIONS,
PRIMARY_REVIEW_AGENT,
SUB_REVIEW_AGENT,
} from './agent/builtin.js'

View File

@ -8,7 +8,7 @@ import {
type Tool, type Tool,
} from '@modelcontextprotocol/sdk/types.js' } from '@modelcontextprotocol/sdk/types.js'
import { getDefaultAppState } from 'src/state/AppStateStore.js' import { getDefaultAppState } from 'src/state/AppStateStore.js'
import review from '../commands/review.js' import { getBundledSkills } from 'src/skills/bundledSkills.js'
import type { Command } from '../commands.js' import type { Command } from '../commands.js'
import { import {
findToolByName, findToolByName,
@ -30,7 +30,7 @@ import { zodToJsonSchema } from '../utils/zodToJsonSchema.js'
type ToolInput = Tool['inputSchema'] type ToolInput = Tool['inputSchema']
type ToolOutput = Tool['outputSchema'] type ToolOutput = Tool['outputSchema']
const MCP_COMMANDS: Command[] = [review] const MCP_COMMANDS: Command[] = getBundledSkills().filter(c => c.name === 'review')
export async function startMCPServer( export async function startMCPServer(
cwd: string, cwd: string,

View File

@ -1,6 +1,6 @@
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle'
import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js' import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js'
import { initializeBuiltinSkills } from 'src/costrict/review/extension.js' import { Extension as ReviewExtension } from 'src/costrict/review/index.js'
import { registerBatchSkill } from './batch.js' import { registerBatchSkill } from './batch.js'
import { registerClaudeInChromeSkill } from './claudeInChrome.js' import { registerClaudeInChromeSkill } from './claudeInChrome.js'
import { registerDebugSkill } from './debug.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 * 3. Import and call that function here
*/ */
export function initBundledSkills(): void { export function initBundledSkills(): void {
// Initialize builtin review skills (extract to cache if needed) // Extract review skills to disk for standard scanner discovery
initializeBuiltinSkills().catch(() => {}) ReviewExtension.initializeBuiltinSkills().catch(() => {})
registerUpdateConfigSkill() registerUpdateConfigSkill()
registerProjectWikiSkill() registerProjectWikiSkill()