diff --git a/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts b/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts index de9f01d37..9922ca772 100644 --- a/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts +++ b/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts @@ -2,7 +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' @@ -112,8 +112,5 @@ export function getBuiltInAgents(): AgentDefinition[] { agents.push(VERIFICATION_AGENT) } - // Review agents (CoStrictReviewer, CoStrictValidator) - agents.push(...REVIEW_AGENTS) - return agents } diff --git a/scripts/generate-review-builtin.ts b/scripts/generate-review-builtin.ts index 0af742575..942187fbb 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 & agents from costrict-review repo and generates - * src/costrict/review/skill/builtin.ts and src/costrict/review/agent/builtin.ts + * Downloads builtin review skills from costrict-review repo and generates + * src/costrict/review/skill/builtin.ts * * 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. * * Usage: bun run scripts/generate-review-builtin.ts @@ -14,7 +14,6 @@ import fs from 'fs/promises' import path from 'path' import { fileURLToPath } from 'url' import { spawnSync } from 'child_process' -import matter from 'gray-matter' import { mkdir } from 'fs/promises' const __filename = fileURLToPath(import.meta.url) @@ -22,35 +21,14 @@ 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 = 'main' -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` -} +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' }) @@ -108,21 +86,9 @@ 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() } -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( cloneDir: string, index: IndexJson, @@ -141,7 +107,6 @@ 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) @@ -167,36 +132,6 @@ async function cloneAndCopy( const fileCount = (await walk(outputDir)).length 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}`) - } } await fs.rm(cloneDir, { recursive: true, force: true }) @@ -300,155 +235,8 @@ export async function extractBundledSkill(skillName: string, targetDir: string, 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 Resources\n') + console.log('\nšŸš€ CSC — Downloading Builtin Review Skills\n') await fs.mkdir(bundledReviewDir, { recursive: true }) @@ -496,8 +284,6 @@ async function generateBuiltinReview() { await generateBuiltinSkills(commitSha) - await generateBuiltinAgents(commitSha) - console.log('\nšŸ’” Run `bun run build` to compile the extension\n') } diff --git a/src/commands/review.ts b/src/commands/review.ts index fe480ecce..69c1a4fd3 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -3,12 +3,14 @@ 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 extract-to-disk mechanism, +// discovered by the standard skill scanner. Here we provide a prompt-based +// command that routes to the Skill tool for non-skill contexts (e.g. MCP). const review: Command = { type: 'prompt', name: 'review', @@ -16,12 +18,14 @@ const review: Command = { 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', diff --git a/src/commands/security-review.ts b/src/commands/security-review.ts deleted file mode 100644 index 5c2937521..000000000 --- a/src/commands/security-review.ts +++ /dev/null @@ -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 { - return [{ type: 'text', text: CommandLocale.get('security-review').replace('$ARGUMENTS', args) }] - }, -} - -export default securityReview diff --git a/src/costrict/review/extension.ts b/src/costrict/review/extension.ts index 99f7736c9..fa5ce28d0 100644 --- a/src/costrict/review/extension.ts +++ b/src/costrict/review/extension.ts @@ -1,87 +1,72 @@ -/** - * 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 { 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 * as Builtin from './skill/builtin.js' +import { + extractBundledSkill, + getBuiltinSkillVersion, + listBuiltinSkills, +} 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 getLocale(): string { + const lang = getResolvedLanguage() + return LOCALE_MAP[lang] ?? 'zh-CN' +} + +function getReviewSkillsDir(): string { + return join(getClaudeConfigHomeDir(), 'skills') } function getVersionFilePath(skillDir: string): string { - return path.join(skillDir, '.version') + return join(skillDir, '.version') } async function getInstalledVersion(skillDir: string): Promise { try { - const content = await readFile(getVersionFilePath(skillDir), 'utf-8') - return content.trim() + return await readFile(getVersionFilePath(skillDir), 'utf-8') } 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) +async function writeVersionFile( + skillDir: string, + skillName: string, + locale: string, +): Promise { + const builtinVersion = getBuiltinSkillVersion(skillName) if (!builtinVersion) return - - await mkdir(skillDir, { recursive: true }) await writeFile(getVersionFilePath(skillDir), `${builtinVersion}:${locale}`, 'utf-8') } +async function needsUpdate( + skillDir: string, + skillName: string, + locale: string, +): Promise { + const builtinVersion = getBuiltinSkillVersion(skillName) + if (!builtinVersion) return true + const installed = await getInstalledVersion(skillDir) + return installed !== `${builtinVersion}:${locale}` +} + export async function initializeBuiltinSkills(): Promise { - const lang = getResolvedLanguage() - const locale = LOCALE_MAP[lang] ?? 'zh-CN' + const locale = getLocale() + const skillsDir = getReviewSkillsDir() + const skillNames = listBuiltinSkills() - const cacheDir = getSkillCacheDir() - const skillNames = Builtin.listBuiltinSkills() + for (const skillName of skillNames) { + const skillDir = join(skillsDir, skillName) + if (!(await needsUpdate(skillDir, skillName, locale))) continue - 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)})`) + await mkdir(skillDir, { recursive: true }) + await extractBundledSkill(skillName, skillDir, locale) + await writeVersionFile(skillDir, skillName, locale) } } export function getBuiltinSkillsDir(): string { - return getSkillCacheDir() + return getReviewSkillsDir() } diff --git a/src/costrict/review/index.ts b/src/costrict/review/index.ts index 09d86b8a2..01f347fbb 100644 --- a/src/costrict/review/index.ts +++ b/src/costrict/review/index.ts @@ -1,15 +1,9 @@ /** * CoStrict Review Module * - * Provides builtin review skills and agents that are embedded - * in the binary and extracted to cache on first run. + * Provides builtin review skills that are extracted to disk at runtime + * and discovered by the standard skill scanner. */ -export * as Extension from './extension.js' export * as SkillBuiltin from './skill/builtin.js' -export { - REVIEW_AGENTS, - AGENT_VERSIONS, - PRIMARY_REVIEW_AGENT, - SUB_REVIEW_AGENT, -} from './agent/builtin.js' +export * as Extension from './extension.js' diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index 38b7edb75..a082c2a6e 100644 --- a/src/skills/bundled/index.ts +++ b/src/skills/bundled/index.ts @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import { shouldAutoEnableClaudeInChrome } from 'src/utils/claudeInChrome/setup.js' -import { initializeBuiltinSkills } from 'src/costrict/review/extension.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 { - // Initialize builtin review skills (extract to cache if needed) - initializeBuiltinSkills().catch(() => {}) + // Extract review skills to disk for standard scanner discovery + ReviewExtension.initializeBuiltinSkills().catch(() => {}) registerUpdateConfigSkill() registerProjectWikiSkill()