From 8b6bdd44d86aef5dcd8134ac1e051e5d4e681f03 Mon Sep 17 00:00:00 2001 From: y574444354 <574444354@qq.com> Date: Tue, 12 May 2026 12:11:37 +0800 Subject: [PATCH 1/2] Revert "Merge pull request #60 from y574444354/feat/migrate-review-agents" This reverts commit 62398b2dcc6ada11ba38789425fa626466ed336f, reversing changes made to ba39b17a7ee9810738e5a71fc4cdff1beba53bfa. --- .npmrc | 1 - build.ts | 3 +- ...1-review-bundled-skill-migration-design.md | 117 ------ .../src/tools/AgentTool/builtInAgents.ts | 4 + scripts/dev.ts | 1 - scripts/generate-review-builtin.ts | 346 +++++++++++++----- src/commands.ts | 3 +- src/commands/review.ts | 25 +- src/commands/security-review.ts | 18 + src/costrict/command/locales/en/review.txt | 5 + .../command/locales/en/security-review.txt | 5 + src/costrict/command/locales/index.ts | 20 + src/costrict/command/locales/zh-CN/review.txt | 5 + .../command/locales/zh-CN/security-review.txt | 5 + src/costrict/review/extension.ts | 87 +++++ src/costrict/review/index.ts | 5 +- src/costrict/skills/reviewSkills.ts | 60 --- src/entrypoints/mcp.ts | 3 +- src/skills/bundled/index.ts | 6 +- 19 files changed, 436 insertions(+), 283 deletions(-) delete mode 100644 docs/superpowers/specs/2026-05-11-review-bundled-skill-migration-design.md create mode 100644 src/commands/security-review.ts create mode 100644 src/costrict/command/locales/en/review.txt create mode 100644 src/costrict/command/locales/en/security-review.txt create mode 100644 src/costrict/command/locales/index.ts create mode 100644 src/costrict/command/locales/zh-CN/review.txt create mode 100644 src/costrict/command/locales/zh-CN/security-review.txt create mode 100644 src/costrict/review/extension.ts delete mode 100644 src/costrict/skills/reviewSkills.ts diff --git a/.npmrc b/.npmrc index 214c29d13..e69de29bb 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -registry=https://registry.npmjs.org/ diff --git a/build.ts b/build.ts index 7b046afec..cd3317bc3 100644 --- a/build.ts +++ b/build.ts @@ -13,8 +13,7 @@ console.log('Generating review builtin files...') const { spawnSync: genSpawnSync } = await import('child_process') const genResult = genSpawnSync('bun', ['run', 'scripts/generate-review-builtin.ts'], { stdio: 'inherit', - cwd: process.cwd(), - env: process.env, + cwd: new URL('.', import.meta.url).pathname, }) if (genResult.status !== 0) { console.warn('Warning: generate-review-builtin.ts failed, using existing files') diff --git a/docs/superpowers/specs/2026-05-11-review-bundled-skill-migration-design.md b/docs/superpowers/specs/2026-05-11-review-bundled-skill-migration-design.md deleted file mode 100644 index 7cd1cd232..000000000 --- a/docs/superpowers/specs/2026-05-11-review-bundled-skill-migration-design.md +++ /dev/null @@ -1,117 +0,0 @@ -# Review Bundled Skill Migration Design - -## Goal - -将 review 和 security-review 从 agent-based + 自定义 extension 初始化架构迁移到 csc 内置的 bundled skill `files` 机制,对齐上游 [opencode#360](https://github.com/zgsm-sangfor/opencode/pull/360) 和 [costrict-review#11](https://github.com/zgsm-ai/costrict-review/pull/11) 的 skill-only 架构。 - -## Background - -上游变更: -- `index.json` 移除了 `agents` 字段,CoStrictReviewer/CoStrictValidator agent 不再存在 -- `review` 和 `security-review` 都改为 skill 形式,目录结构从 `en/skills/...` 变为 `skills/en/...` -- SKILL.md frontmatter 简化为 `name` + `description` - -csc 现状: -- 生成脚本同时生成 agent 和 skill builtin 文件 -- `extension.ts` 在启动时主动提取 skill 到 `~/.claude/skills/` -- `builtInAgents.ts` 注册 REVIEW_AGENTS 到 AgentTool -- `strict:review` / `strict:security-review` 通过 `agent: 'CoStrictReviewer'` 路由 - -## Architecture - -使用 csc 已有的 `registerBundledSkill({ files })` 机制替代自定义初始化。`files` 字段在首次调用时通过 `extractBundledSkillFiles()` 按需落盘到 `getBundledSkillsRoot()//`,并自动给 prompt 添加 `Base directory for this skill: ` 前缀。 - -语言切换:启动时通过 `getResolvedLanguage()` 确定语言,从 `SKILL_FILES[locale]` 取对应语言的数据注册。 - -## Changes - -### 1. `scripts/generate-review-builtin.ts` - -- `IndexJson` 类型移除 `agents` 字段 -- 路径处理适配新结构 `skills/en/...` -- 移除 `generateBuiltinAgents()` 函数 -- `generateBuiltinSkills()` 产物改为导出: - - `SKILL_FILES: Record>>`(locale → skillName → filePath → content) - - `SKILL_METADATA: Record>`(locale → skillName → 元数据) -- 解析 SKILL.md frontmatter 提取 `name` 和 `description` -- agent builtin 文件输出为空 stub - -### 2. `src/costrict/review/skill/builtin.ts`(生成产物) - -- 导出 `SKILL_FILES`、`SKILL_METADATA` -- 移除 `extractBundledSkill()`、`listBuiltinSkills()` 等函数 - -### 3. `src/costrict/review/agent/builtin.ts` - -- 保持空 stub,`REVIEW_AGENTS: BuiltInAgentDefinition[] = []` - -### 4. 新建 `src/costrict/skills/reviewSkills.ts` - -- `registerReviewSkills()` 函数 -- 调用 `getResolvedLanguage()` → `LOCALE_MAP` 确定语言 -- 从 `SKILL_FILES[locale]` 读取每个 skill 的文件 -- 从 `SKILL_METADATA[locale]` 读取 description -- 调用 `registerBundledSkill()` 注册 `review` 和 `security-review`: - - `context: 'fork'` - - `allowedTools: ['AskUserQuestion', 'Read', 'Glob', 'Grep', 'Bash', 'Agent']` - - `files`: locale-specific 文件数据 - - `getPromptForCommand`: 返回 SKILL.md body($ARGUMENTS 替换) - -### 5. 删除 `src/costrict/review/extension.ts` - -- `initializeBuiltinSkills()` 不再需要,bundled skill 机制自行处理文件落盘 - -### 6. 更新 `src/costrict/review/index.ts` - -- 移除 extension 导出 - -### 7. 更新 `src/skills/bundled/index.ts` - -- 移除 `import { initializeBuiltinSkills }` 和调用 -- 添加 `import { registerReviewSkills }` 并调用 - -### 8. 更新 `src/costrict/skills/strictReview.ts` - -- 移除 `agent: 'CoStrictReviewer'` -- 改为使用 `files` 机制(或与 reviewSkills 共用注册逻辑) - -### 9. 更新 `src/costrict/skills/strictSecurityReview.ts` - -- 同上 - -### 10. 更新 `src/commands/review.ts` - -- 移除 `agent: PRIMARY_REVIEW_AGENT || 'CoStrictReviewer'` -- prompt 改为告诉 model 加载 review skill(对齐上游) - -### 11. 更新 `src/commands/security-review.ts` - -- prompt 改为告诉 model 加载 security-review skill - -### 12. 更新 `packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts` - -- 移除 `REVIEW_AGENTS` import 和 `agents.push(...REVIEW_AGENTS)` - -### 13. 清理 - -- 移除 `src/costrict/command/locales/` 目录(不再需要,prompt 来自 SKILL.md) -- 移除 `.gitignore` 中 `packages/builtin-tools/bundled-review/` 相关规则 -- agent builtin stub 保留在 `.gitignore` - -## Files Summary - -| Action | File | -|--------|------| -| Modify | `scripts/generate-review-builtin.ts` | -| Generated | `src/costrict/review/skill/builtin.ts` | -| No change | `src/costrict/review/agent/builtin.ts` (stub) | -| Create | `src/costrict/skills/reviewSkills.ts` | -| Delete | `src/costrict/review/extension.ts` | -| Modify | `src/costrict/review/index.ts` | -| Modify | `src/skills/bundled/index.ts` | -| Modify | `src/costrict/skills/strictReview.ts` | -| Modify | `src/costrict/skills/strictSecurityReview.ts` | -| Modify | `src/commands/review.ts` | -| Modify | `src/commands/security-review.ts` | -| Modify | `packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts` | -| Delete | `src/costrict/command/locales/` directory | diff --git a/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts b/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts index 2f80aec90..de9f01d37 100644 --- a/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts +++ b/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts @@ -2,6 +2,7 @@ import { feature } from 'bun:bundle' import { getIsNonInteractiveSession } from 'src/bootstrap/state.js' import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.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 { QUICK_EXPLORE_AGENT } from 'src/costrict/agents/quickExplore.js' import { REQUIREMENT_AGENT } from 'src/costrict/agents/requirement.js' @@ -111,5 +112,8 @@ export function getBuiltInAgents(): AgentDefinition[] { agents.push(VERIFICATION_AGENT) } + // Review agents (CoStrictReviewer, CoStrictValidator) + agents.push(...REVIEW_AGENTS) + return agents } diff --git a/scripts/dev.ts b/scripts/dev.ts index 1995117c7..a17ce4a11 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -56,7 +56,6 @@ console.log('[dev] Generating review builtin files...'); const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], { stdio: 'inherit', cwd: projectRoot, - env: process.env as Record, }); if (!genResult.success) { console.warn('[dev] Warning: generate-review-builtin.ts failed, using existing files'); diff --git a/scripts/generate-review-builtin.ts b/scripts/generate-review-builtin.ts index 7515a99a5..071554091 100644 --- a/scripts/generate-review-builtin.ts +++ b/scripts/generate-review-builtin.ts @@ -1,10 +1,10 @@ #!/usr/bin/env bun /** - * Downloads builtin review skills from costrict-review repo and generates - * src/costrict/review/skill/builtin.ts + * Downloads builtin review skills & agents from costrict-review repo and generates + * src/costrict/review/skill/builtin.ts and src/costrict/review/agent/builtin.ts * * Uses git SSH transport (git ls-remote + git clone). - * Reads index.json manifest to discover skills and their per-locale paths. + * Reads index.json manifest to discover resources and their per-locale paths. * Compares remote commit SHA with cached version and skips download if unchanged. * * Usage: bun run scripts/generate-review-builtin.ts @@ -22,17 +22,24 @@ const __dirname = path.dirname(__filename) const bundledReviewDir = path.resolve(__dirname, '../packages/builtin-tools/bundled-review') const builtinSkillsFile = path.resolve(__dirname, '../src/costrict/review/skill/builtin.ts') +const builtinAgentsFile = path.resolve(__dirname, '../src/costrict/review/agent/builtin.ts') type IndexJson = { + agents: Array<{ + name: string + path: Record + opencode?: Record + claudecode?: Record + }> skills: Array<{ name: string; path: Record }> } const REPO = 'zgsm-ai/costrict-review' -const BRANCH = 'optimize/agent-prompts' +const BRANCH = 'main' const CLONE_URL = `git@github.com:${REPO}.git` function git(...args: string[]): { ok: boolean; stdout: string; stderr: string } { - const result = spawnSync('git', args, { encoding: 'utf-8', env: process.env }) + const result = spawnSync('git', args, { encoding: 'utf-8' }) return { ok: result.status === 0, stdout: result.stdout?.trim() ?? '', @@ -87,26 +94,19 @@ function collectLocales(index: IndexJson): string[] { for (const skill of index.skills) { 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() } -/** - * Extract locale and skill directory from index.json path. - * New format: "skills///SKILL.md" - * Old format: "/skills//SKILL.md" (for backwards compat) - */ -function parseSkillPath(skillMdPath: string): { locale: string; skillDir: string } | null { - // New format: skills///SKILL.md - const newMatch = skillMdPath.match(/^skills\/([^/]+)\/(.+)$/) - if (newMatch) { - return { locale: newMatch[1], skillDir: newMatch[2].replace(/\/SKILL\.md$/, '') } - } - // Old format: /skills//SKILL.md - const oldMatch = skillMdPath.match(/^([^/]+)\/skills\/(.+)$/) - if (oldMatch) { - return { locale: oldMatch[1], skillDir: oldMatch[2].replace(/\/SKILL\.md$/, '') } - } - return null +function mergeClaudecodeFrontmatter( + mdContent: string, + claudecodeFields: Record, +): string { + const md = matter(mdContent) + const merged = { ...md.data, ...claudecodeFields } + return matter.stringify(md.content, merged) } async function cloneAndCopy( @@ -127,26 +127,61 @@ async function cloneAndCopy( for (const locale of locales) { const outputLocaleDir = path.join(bundledReviewDir, locale) + // Copy skill dirs for this locale const skillPaths = index.skills .map(s => s.path[locale]) .filter(Boolean) 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 outputDir = path.join(outputLocaleDir, skillDir) + const relativeDir = skillMdPath.startsWith(`${locale}/`) + ? 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.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 - console.log(` ✓ ${locale}/${skillDir}: ${fileCount} files`) + console.log(` ✓ ${locale}/skills/${skillName}: ${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}`) } } @@ -164,99 +199,85 @@ async function generateBuiltinSkills( } } - // Discover skill names from first locale const allSkillNames: string[] = [] + + // Discover skill names from first locale for (const locale of locales) { - const entries = await fs.readdir(path.join(bundledReviewDir, locale)).catch(() => [] as string[]) + const skillsDir = path.join(bundledReviewDir, locale, 'skills') + const entries = await fs.readdir(skillsDir).catch(() => [] as string[]) for (const name of entries) { - const p = path.join(bundledReviewDir, locale, name) - if ((await fs.stat(p).catch(() => null))?.isDirectory()) { - if (!allSkillNames.includes(name)) allSkillNames.push(name) - } + if (!allSkillNames.includes(name)) allSkillNames.push(name) } break } - const skillFileEntries: string[] = [] - const metadataEntries: string[] = [] + const imports: string[] = [] + const localeSkillEntries: string[] = [] let fileIdx = 0 for (const locale of [...locales].sort()) { - const fileEntries: string[] = [] - const metaEntries: string[] = [] - + const skillEntries: string[] = [] for (const skillName of allSkillNames) { - const skillDir = path.join(bundledReviewDir, locale, skillName) + const skillDir = path.join(bundledReviewDir, locale, 'skills', skillName) const files = await walk(skillDir) - - // 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[] = [] + const fileEntries: string[] = [] for (const file of files) { const varName = `SKILL_FILE_${fileIdx++}` const filePath = path.join(skillDir, file) const content = await fs.readFile(filePath, 'utf-8') const normalizedPath = file.replaceAll('\\', '/') - fileRecords.push(` "${normalizedPath}": ${JSON.stringify(content)}`) + imports.push(`const ${varName} = ${JSON.stringify(content)}`) + fileEntries.push(` "${normalizedPath}": ${varName}`) } - - fileEntries.push(` "${skillName}": {\n${fileRecords.join(',\n')}\n }`) - metaEntries.push(` "${skillName}": ${JSON.stringify(skillMeta)}`) + skillEntries.push(` "${skillName}": {\n${fileEntries.join(',\n')}\n }`) } - - skillFileEntries.push(` "${locale}": {\n${fileEntries.join(',\n')}\n }`) - metadataEntries.push(` "${locale}": {\n${metaEntries.join(',\n')}\n }`) + localeSkillEntries.push(` "${locale}": {\n${skillEntries.join(',\n')}\n }`) } const content = `// This file is auto-generated by scripts/generate-review-builtin.ts // Do not edit manually -// Skills are downloaded from zgsm-ai/costrict-review repository -// locale → skillName → filePath → content -export const SKILL_FILES: Record>> = { -${skillFileEntries.join(',\n')} +import { writeFile, mkdir } from "fs/promises" +import { join, dirname } from "path" + +${imports.join('\n')} + +const SKILL_FILES: Record>> = { +${localeSkillEntries.join(',\n')} } -// locale → skillName → { name, description } -export const SKILL_METADATA: Record> = { -${metadataEntries.join(',\n')} -} - -// skillName → commit SHA -export const SKILL_VERSIONS: Record = { +const SKILL_VERSIONS: Record = { ${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')} } -// List all skill names -export function listBuiltinSkillNames(): string[] { +export function listBuiltinSkills(): string[] { return ${JSON.stringify(allSkillNames)} } -// Get version for a skill export function getBuiltinSkillVersion(skillName: string): string | undefined { return SKILL_VERSIONS[skillName] } -// Get files for a skill in a specific locale -export function getSkillFiles(skillName: string, locale: string): Record { - return SKILL_FILES[locale]?.[skillName] ?? {} +export function listSkillFiles(skillName: string, locale: string): string[] { + return Object.keys(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 { + 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}\`) + } + + await mkdir(targetDir, { recursive: true }) + for (const [relativePath, content] of Object.entries(skillFiles)) { + await mkdir(join(targetDir, dirname(relativePath)), { recursive: true }) + await writeFile(join(targetDir, relativePath), content, "utf-8") + } } ` @@ -265,8 +286,155 @@ export function getSkillMetadata(skillName: string, locale: string): { name: str console.log(`\n✓ Generated ${builtinSkillsFile}`) } +async function generateBuiltinAgents( + commitSha: string, +): Promise { + 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 = {\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 = {} + try { + const content = await fs.readFile(zhAgentFile, 'utf-8') + const parsed = matter(content) + frontmatterData = parsed.data as Record + } 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 = { 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 = { +${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() { - console.log('\n🚀 CSC — Downloading Builtin Review Skills\n') + console.log('\n🚀 CSC — Downloading Builtin Review Resources\n') await fs.mkdir(bundledReviewDir, { recursive: true }) @@ -314,7 +482,9 @@ async function generateBuiltinReview() { await generateBuiltinSkills(commitSha) - console.log('\n💡 Run `bun run build` to compile\n') + await generateBuiltinAgents(commitSha) + + console.log('\n💡 Run `bun run build` to compile the extension\n') } generateBuiltinReview().catch(console.error) diff --git a/src/commands.ts b/src/commands.ts index 3ddf3e80b..15746227e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -38,7 +38,7 @@ import pr_comments from './commands/pr_comments/index.js' import releaseNotes from './commands/release-notes/index.js' import rename from './commands/rename/index.js' import resume from './commands/resume/index.js' -import { ultrareview } from './commands/review.js' +import review, { ultrareview } from './commands/review.js' import session from './commands/session/index.js' import share from './commands/share/index.js' import skills from './commands/skills/index.js' @@ -351,6 +351,7 @@ const COMMANDS = memoize((): Command[] => [ favorite, theme, feedback, + review, ultrareview, rewind, terminalSetup, diff --git a/src/commands/review.ts b/src/commands/review.ts index 7da35f436..fe480ecce 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -1,17 +1,27 @@ -import { isUltrareviewEnabled } from './review/ultrareviewEnabled.js' +import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js' import type { Command } from '../commands.js' +import type { ToolUseContext } from '../Tool.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 // 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' -// /review is registered as a bundled skill via registerReviewSkills() in -// src/costrict/skills/reviewSkills.ts, which provides the full SKILL.md -// content and reference files. This file only provides /ultrareview. +const review: Command = { + type: 'prompt', + name: 'review', + 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 { + 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 = { type: 'local-jsx', name: 'ultrareview', @@ -20,4 +30,5 @@ const ultrareview: Command = { load: () => import('./review/ultrareviewCommand.js'), } +export default review export { ultrareview } diff --git a/src/commands/security-review.ts b/src/commands/security-review.ts new file mode 100644 index 000000000..5c2937521 --- /dev/null +++ b/src/commands/security-review.ts @@ -0,0 +1,18 @@ +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 { + return [{ type: 'text', text: CommandLocale.get('security-review').replace('$ARGUMENTS', args) }] + }, +} + +export default securityReview diff --git a/src/costrict/command/locales/en/review.txt b/src/costrict/command/locales/en/review.txt new file mode 100644 index 000000000..e4a70121e --- /dev/null +++ b/src/costrict/command/locales/en/review.txt @@ -0,0 +1,5 @@ +# Code Review + +Please perform a code review on: $ARGUMENTS + +Please respond and write all files in English throughout the entire process. diff --git a/src/costrict/command/locales/en/security-review.txt b/src/costrict/command/locales/en/security-review.txt new file mode 100644 index 000000000..7158d1849 --- /dev/null +++ b/src/costrict/command/locales/en/security-review.txt @@ -0,0 +1,5 @@ +# 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. diff --git a/src/costrict/command/locales/index.ts b/src/costrict/command/locales/index.ts new file mode 100644 index 000000000..ca9a0285f --- /dev/null +++ b/src/costrict/command/locales/index.ts @@ -0,0 +1,20 @@ +import { getResolvedLanguage } from 'src/utils/language.js' +import ZH_CN_REVIEW from './zh-CN/review.txt' +import EN_REVIEW from './en/review.txt' +import ZH_CN_SECURITY_REVIEW from './zh-CN/security-review.txt' +import EN_SECURITY_REVIEW from './en/security-review.txt' + +const LOCALE_MAP: Record = { zh: 'zh-CN', en: 'en' } + +const TEMPLATES: Record> = { + review: { 'zh-CN': ZH_CN_REVIEW, en: EN_REVIEW }, + 'security-review': { 'zh-CN': ZH_CN_SECURITY_REVIEW, en: EN_SECURITY_REVIEW }, +} + +export namespace CommandLocale { + export function get(name: string): string { + const lang = getResolvedLanguage() + const locale = LOCALE_MAP[lang] ?? 'zh-CN' + return TEMPLATES[name]?.[locale] ?? TEMPLATES[name]?.en ?? '' + } +} diff --git a/src/costrict/command/locales/zh-CN/review.txt b/src/costrict/command/locales/zh-CN/review.txt new file mode 100644 index 000000000..ed2871707 --- /dev/null +++ b/src/costrict/command/locales/zh-CN/review.txt @@ -0,0 +1,5 @@ +# 代码审查 + +请对以下内容执行代码审查:$ARGUMENTS + +全程请使用中文进行回答与文件写入。 diff --git a/src/costrict/command/locales/zh-CN/security-review.txt b/src/costrict/command/locales/zh-CN/security-review.txt new file mode 100644 index 000000000..02a2084f3 --- /dev/null +++ b/src/costrict/command/locales/zh-CN/security-review.txt @@ -0,0 +1,5 @@ +# 安全代码审查 + +请使用 Skill 工具加载 `security-review` 技能来对以下内容执行安全代码审查:$ARGUMENTS + +全程请使用中文进行回答与文件写入。 diff --git a/src/costrict/review/extension.ts b/src/costrict/review/extension.ts new file mode 100644 index 000000000..99f7736c9 --- /dev/null +++ b/src/costrict/review/extension.ts @@ -0,0 +1,87 @@ +/** + * CoStrict Skill Extension + * + * 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 * as Builtin from './skill/builtin.js' + +const LOCALE_MAP: Record = { zh: 'zh-CN', en: 'en' } + +function getSkillCacheDir(): string { + const home = process.env.HOME ?? process.env.USERPROFILE ?? '' + return path.join(home, '.claude', 'skills') +} + +function getVersionFilePath(skillDir: string): string { + return path.join(skillDir, '.version') +} + +async function getInstalledVersion(skillDir: string): Promise { + try { + const content = await readFile(getVersionFilePath(skillDir), 'utf-8') + return content.trim() + } catch { + return null + } +} + +async function needsUpdate(skillDir: string, skillName: string, locale: string): Promise { + const builtinVersion = Builtin.getBuiltinSkillVersion(skillName) + if (!builtinVersion) return true + + const installedVersion = await getInstalledVersion(skillDir) + const expectedVersion = `${builtinVersion}:${locale}` + return installedVersion !== expectedVersion +} + +async function writeVersionFile(skillDir: string, skillName: string, locale: string): Promise { + const builtinVersion = Builtin.getBuiltinSkillVersion(skillName) + if (!builtinVersion) return + + await mkdir(skillDir, { recursive: true }) + await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8') +} + +export async function initializeBuiltinSkills(): Promise { + const lang = getResolvedLanguage() + const locale = LOCALE_MAP[lang] ?? 'zh-CN' + + const cacheDir = getSkillCacheDir() + const skillNames = Builtin.listBuiltinSkills() + + for (const name of skillNames) { + const skillDir = path.join(cacheDir, name) + + 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 { + return getSkillCacheDir() +} diff --git a/src/costrict/review/index.ts b/src/costrict/review/index.ts index 84cdb0634..09d86b8a2 100644 --- a/src/costrict/review/index.ts +++ b/src/costrict/review/index.ts @@ -1,10 +1,11 @@ /** * CoStrict Review Module * - * Provides builtin review skills that are embedded in the binary - * and registered as bundled skills via registerBundledSkill(). + * Provides builtin review skills and agents that are embedded + * in the binary and extracted to cache on first run. */ +export * as Extension from './extension.js' export * as SkillBuiltin from './skill/builtin.js' export { REVIEW_AGENTS, diff --git a/src/costrict/skills/reviewSkills.ts b/src/costrict/skills/reviewSkills.ts deleted file mode 100644 index 84ae3ce03..000000000 --- a/src/costrict/skills/reviewSkills.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { registerBundledSkill } from 'src/skills/bundledSkills.js' -import { getResolvedLanguage } from 'src/utils/language.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' -} - -function registerSkillVariant( - name: string, - skillKey: string, - files: Record, - description: string, -): void { - registerBundledSkill({ - name, - description, - whenToUse: description, - userInvocable: true, - disableModelInvocation: true, - allowedTools: [ - 'Glob', - 'Grep', - 'Read', - 'TodoWrite', - 'Bash', - 'Agent', - ], - model: 'inherit', - context: 'fork', - files, - async getPromptForCommand(args) { - return [{ type: 'text', text: args.trim() || `Please perform a ${skillKey}.` }] - }, - }) -} - -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 - - // Register /review, /security-review - registerSkillVariant(meta.name, skillKey, files, meta.description) - - // Register /strict:review, /strict:security-review - registerSkillVariant(`strict:${skillKey}`, skillKey, files, meta.description) - } -} diff --git a/src/entrypoints/mcp.ts b/src/entrypoints/mcp.ts index 52e082933..cbe36d5be 100644 --- a/src/entrypoints/mcp.ts +++ b/src/entrypoints/mcp.ts @@ -8,6 +8,7 @@ import { type Tool, } from '@modelcontextprotocol/sdk/types.js' import { getDefaultAppState } from 'src/state/AppStateStore.js' +import review from '../commands/review.js' import type { Command } from '../commands.js' import { findToolByName, @@ -29,7 +30,7 @@ import { zodToJsonSchema } from '../utils/zodToJsonSchema.js' type ToolInput = Tool['inputSchema'] type ToolOutput = Tool['outputSchema'] -const MCP_COMMANDS: Command[] = [] +const MCP_COMMANDS: Command[] = [review] export async function startMCPServer( cwd: string, diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index 03ce7c7f4..38b7edb75 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 { initializeBuiltinSkills } from 'src/costrict/review/extension.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() + // Initialize builtin review skills (extract to cache if needed) + initializeBuiltinSkills().catch(() => {}) registerUpdateConfigSkill() registerProjectWikiSkill() From b1a6a22e5dc6f5f657d520d6a75e69912cd14948 Mon Sep 17 00:00:00 2001 From: y574444354 <574444354@qq.com> Date: Tue, 12 May 2026 12:23:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=20HTTPS=20+=20gh?= =?UTF-8?q?=20token=20=E6=9B=BF=E4=BB=A3=20SSH=20=E5=85=8B=E9=9A=86=20revi?= =?UTF-8?q?ew=20=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 generate-review-builtin.ts 的 CLONE_URL 从 SSH (git@github.com:) 改为 HTTPS,并通过 gh auth token 或环境变量获取认证令牌, 解决 SSH key 未配置时无法克隆私有仓库的问题。 --- scripts/dev.ts | 2 +- scripts/generate-review-builtin.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/dev.ts b/scripts/dev.ts index a17ce4a11..5366534f9 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -54,7 +54,7 @@ const bunCmd = process.execPath; // Generate review builtin files before dev launch console.log('[dev] Generating review builtin files...'); const genResult = Bun.spawnSync(['bun', 'run', 'scripts/generate-review-builtin.ts'], { - stdio: 'inherit', + stdio: ["inherit", "inherit", "inherit"], cwd: projectRoot, }); if (!genResult.success) { diff --git a/scripts/generate-review-builtin.ts b/scripts/generate-review-builtin.ts index 071554091..0af742575 100644 --- a/scripts/generate-review-builtin.ts +++ b/scripts/generate-review-builtin.ts @@ -36,7 +36,21 @@ type IndexJson = { const REPO = 'zgsm-ai/costrict-review' const BRANCH = 'main' -const CLONE_URL = `git@github.com:${REPO}.git` +const CLONE_URL = getCloneUrl() + +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 } { const result = spawnSync('git', args, { encoding: 'utf-8' })