diff --git a/build.ts b/build.ts index cd3317bc3..7b046afec 100644 --- a/build.ts +++ b/build.ts @@ -13,7 +13,8 @@ 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: new URL('.', import.meta.url).pathname, + cwd: process.cwd(), + env: process.env, }) 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 new file mode 100644 index 000000000..7cd1cd232 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-review-bundled-skill-migration-design.md @@ -0,0 +1,117 @@ +# 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/package.json b/package.json index ca07aa52a..23a13f792 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "@agentclientprotocol/sdk": "^0.19.0", "@claude-code-best/mcp-chrome-bridge": "^2.0.8", "ws": "^8.20.0", - "gray-matter": "^4.0.3" + "gray-matter": "^4.0.3", + "undici": "^7.24.6" }, "devDependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts b/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts index de9f01d37..2f80aec90 100644 --- a/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts +++ b/packages/builtin-tools/src/tools/AgentTool/builtInAgents.ts @@ -2,7 +2,6 @@ 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 +111,5 @@ 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 a17ce4a11..1995117c7 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -56,6 +56,7 @@ 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 071554091..7515a99a5 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 @@ -22,24 +22,17 @@ 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 BRANCH = 'optimize/agent-prompts' 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' }) + const result = spawnSync('git', args, { encoding: 'utf-8', env: process.env }) return { ok: result.status === 0, stdout: result.stdout?.trim() ?? '', @@ -94,19 +87,26 @@ 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) +/** + * 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 } async function cloneAndCopy( @@ -127,61 +127,26 @@ 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 relativeDir = skillMdPath.startsWith(`${locale}/`) - ? skillMdPath.slice(locale.length + 1).replace(/\/[^/]*$/, '') - : path.dirname(skillMdPath) - const outputDir = path.join(outputLocaleDir, relativeDir) + const outputDir = path.join(outputLocaleDir, skillDir) 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}/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}`) + console.log(` ✓ ${locale}/${skillDir}: ${fileCount} files`) } } @@ -199,85 +164,99 @@ async function generateBuiltinSkills( } } - const allSkillNames: string[] = [] - // Discover skill names from first locale + const allSkillNames: string[] = [] for (const locale of locales) { - const skillsDir = path.join(bundledReviewDir, locale, 'skills') - const entries = await fs.readdir(skillsDir).catch(() => [] as string[]) + const entries = await fs.readdir(path.join(bundledReviewDir, locale)).catch(() => [] as string[]) 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 } - const imports: string[] = [] - const localeSkillEntries: string[] = [] + const skillFileEntries: string[] = [] + const metadataEntries: string[] = [] let fileIdx = 0 for (const locale of [...locales].sort()) { - const skillEntries: string[] = [] + const fileEntries: string[] = [] + const metaEntries: string[] = [] + 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 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) { const varName = `SKILL_FILE_${fileIdx++}` const filePath = path.join(skillDir, file) const content = await fs.readFile(filePath, 'utf-8') const normalizedPath = file.replaceAll('\\', '/') - imports.push(`const ${varName} = ${JSON.stringify(content)}`) - fileEntries.push(` "${normalizedPath}": ${varName}`) + fileRecords.push(` "${normalizedPath}": ${JSON.stringify(content)}`) } - 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 // Do not edit manually +// Skills are downloaded from zgsm-ai/costrict-review repository -import { writeFile, mkdir } from "fs/promises" -import { join, dirname } from "path" - -${imports.join('\n')} - -const SKILL_FILES: Record>> = { -${localeSkillEntries.join(',\n')} +// locale → skillName → filePath → content +export const SKILL_FILES: Record>> = { +${skillFileEntries.join(',\n')} } -const SKILL_VERSIONS: Record = { +// locale → skillName → { name, description } +export const SKILL_METADATA: Record> = { +${metadataEntries.join(',\n')} +} + +// skillName → commit SHA +export const SKILL_VERSIONS: Record = { ${allSkillNames.map(n => ` "${n}": "${commitSha}"`).join(',\n')} } -export function listBuiltinSkills(): string[] { +// List all skill names +export function listBuiltinSkillNames(): string[] { return ${JSON.stringify(allSkillNames)} } +// Get version for a skill export function getBuiltinSkillVersion(skillName: string): string | undefined { return SKILL_VERSIONS[skillName] } -export function listSkillFiles(skillName: string, locale: string): string[] { - return Object.keys(SKILL_FILES[locale]?.[skillName] || {}) +// Get files for a skill in a specific locale +export function getSkillFiles(skillName: string, locale: string): Record { + return SKILL_FILES[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") - } +// 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] } ` @@ -286,155 +265,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 }) @@ -482,9 +314,7 @@ async function generateBuiltinReview() { await generateBuiltinSkills(commitSha) - await generateBuiltinAgents(commitSha) - - console.log('\n💡 Run `bun run build` to compile the extension\n') + console.log('\n💡 Run `bun run build` to compile\n') } generateBuiltinReview().catch(console.error) diff --git a/src/bootstrap/state.ts b/src/bootstrap/state.ts index 90d613b61..623d99f92 100644 --- a/src/bootstrap/state.ts +++ b/src/bootstrap/state.ts @@ -195,6 +195,8 @@ type State = { sdkBetas: string[] | undefined // Main thread agent type (from --agent flag or settings) mainThreadAgentType: string | undefined + // Currently active skill name (set when a /skill-name is invoked) + activeSkillName: string | undefined // Remote mode (--remote flag) isRemoteMode: boolean // Direct connect server URL (for display in header) @@ -381,6 +383,8 @@ function getInitialState(): State { sdkBetas: undefined, // Main thread agent type mainThreadAgentType: undefined, + // Currently active skill name + activeSkillName: undefined, // Remote mode isRemoteMode: false, ...(process.env.USER_TYPE === 'ant' @@ -1622,6 +1626,14 @@ export function setMainThreadAgentType(agentType: string | undefined): void { STATE.mainThreadAgentType = agentType } +export function getActiveSkillName(): string | undefined { + return STATE.activeSkillName +} + +export function setActiveSkillName(skillName: string | undefined): void { + STATE.activeSkillName = skillName +} + export function getIsRemoteMode(): boolean { return STATE.isRemoteMode } diff --git a/src/commands.ts b/src/commands.ts index 15746227e..3ddf3e80b 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 review, { ultrareview } from './commands/review.js' +import { 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,7 +351,6 @@ const COMMANDS = memoize((): Command[] => [ favorite, theme, feedback, - review, ultrareview, rewind, terminalSetup, diff --git a/src/commands/clear/caches.ts b/src/commands/clear/caches.ts index 8eacc0d6f..479763b67 100644 --- a/src/commands/clear/caches.ts +++ b/src/commands/clear/caches.ts @@ -5,6 +5,7 @@ import { feature } from 'bun:bundle' import { clearInvokedSkills, + setActiveSkillName, setLastEmittedDate, } from '../../bootstrap/state.js' import { clearCommandsCache } from '../../commands.js' @@ -115,6 +116,8 @@ export function clearSessionCaches( if (!hasPreserved) clearAllDumpState() // Clear invoked skills cache (each entry holds full skill file content) clearInvokedSkills(preservedAgentIds) + // Reset active skill name so new session doesn't inherit previous session's agent-type + setActiveSkillName(undefined) // Clear git dir resolution cache clearResolveGitDirCache() // Clear dynamic skills (loaded from skill directories) diff --git a/src/commands/review.ts b/src/commands/review.ts index fe480ecce..7da35f436 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -1,27 +1,17 @@ -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' +import type { Command } from '../commands.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' -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) }] - }, -} +// /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. +// /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', @@ -30,5 +20,4 @@ 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 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/constants/system.ts b/src/constants/system.ts index 0cd2e7652..0bac3c3d3 100644 --- a/src/constants/system.ts +++ b/src/constants/system.ts @@ -1,11 +1,6 @@ // Critical system constants extracted to break circular dependencies -import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' -import { logForDebugging } from '../utils/debug.js' -import { isEnvDefinedFalsy } from '../utils/envUtils.js' import { getAPIProvider } from '../utils/model/providers.js' -import { getWorkload } from '../utils/workloadContext.js' const DEFAULT_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude.` const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.` @@ -45,51 +40,6 @@ export function getCLISyspromptPrefix(options?: { return DEFAULT_PREFIX } -/** - * Check if attribution header is enabled. - * Enabled by default, can be disabled via env var or GrowthBook killswitch. - */ -function isAttributionHeaderEnabled(): boolean { - if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) { - return false - } - return getFeatureValue_CACHED_MAY_BE_STALE('tengu_attribution_header', true) -} - -/** - * Get attribution header for API requests. - * Returns a header string with cc_version (including fingerprint) and cc_entrypoint. - * Enabled by default, can be disabled via env var or GrowthBook killswitch. - * - * When NATIVE_CLIENT_ATTESTATION is enabled, includes a `cch=00000` placeholder. - * Before the request is sent, Bun's native HTTP stack finds this placeholder - * in the request body and overwrites the zeros with a computed hash. The - * server verifies this token to confirm the request came from a real Claude - * Code client. See bun-anthropic/src/http/Attestation.zig for implementation. - * - * We use a placeholder (instead of injecting from Zig) because same-length - * replacement avoids Content-Length changes and buffer reallocation. - */ -export function getAttributionHeader(fingerprint: string): string { - if (!isAttributionHeaderEnabled()) { - return '' - } - - const version = `${MACRO.VERSION}.${fingerprint}` - const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? 'unknown' - - // cch=00000 placeholder is overwritten by Bun's HTTP stack with attestation token - const cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : '' - // cc_workload: turn-scoped hint so the API can route e.g. cron-initiated - // requests to a lower QoS pool. Absent = interactive default. Safe re: - // fingerprint (computed from msg chars + version only, line 78 above) and - // cch attestation (placeholder overwritten in serialized body bytes after - // this string is built). Server _parse_cc_header tolerates unknown extra - // fields so old API deploys silently ignore this. - const workload = getWorkload() - const workloadPair = workload ? ` cc_workload=${workload};` : '' - const header = `x-anthropic-billing-header: cc_version=${version}; cc_entrypoint=${entrypoint};${cch}${workloadPair}` - - logForDebugging(`attribution header ${header}`) - return header +export function getAttributionHeader(_fingerprint: string): string { + return '' } diff --git a/src/costrict/command/locales/en/review.txt b/src/costrict/command/locales/en/review.txt deleted file mode 100644 index e4a70121e..000000000 --- a/src/costrict/command/locales/en/review.txt +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index 7158d1849..000000000 --- a/src/costrict/command/locales/en/security-review.txt +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index ca9a0285f..000000000 --- a/src/costrict/command/locales/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index ed2871707..000000000 --- a/src/costrict/command/locales/zh-CN/review.txt +++ /dev/null @@ -1,5 +0,0 @@ -# 代码审查 - -请对以下内容执行代码审查:$ARGUMENTS - -全程请使用中文进行回答与文件写入。 diff --git a/src/costrict/command/locales/zh-CN/security-review.txt b/src/costrict/command/locales/zh-CN/security-review.txt deleted file mode 100644 index 02a2084f3..000000000 --- a/src/costrict/command/locales/zh-CN/security-review.txt +++ /dev/null @@ -1,5 +0,0 @@ -# 安全代码审查 - -请使用 Skill 工具加载 `security-review` 技能来对以下内容执行安全代码审查:$ARGUMENTS - -全程请使用中文进行回答与文件写入。 diff --git a/src/costrict/provider/fetch.ts b/src/costrict/provider/fetch.ts index c974749e5..94f6881c7 100644 --- a/src/costrict/provider/fetch.ts +++ b/src/costrict/provider/fetch.ts @@ -38,6 +38,15 @@ type CoStrictFetch = typeof fetch & { preconnect?: (url: string | URL) => void } +// PascalCase / camelCase → kebab-case: "StrictSpec" → "strict-spec", "TDD" → "tdd" +function toKebabCase(s: string | undefined): string | undefined { + if (!s) return undefined + return s + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') + .replace(/([a-z\d])([A-Z])/g, '$1-$2') + .toLowerCase() +} + /** * 创建自定义 fetch 函数,用于 CoStrict API 请求 * @@ -47,7 +56,10 @@ type CoStrictFetch = typeof fetch & { * 3. 注入 Authorization 和 CoStrict 特有 headers * 4. 反应性 401 错误恢复(自动重试一次) */ -export function createCoStrictFetch(): CoStrictFetch { +export function createCoStrictFetch(options?: { + agentType?: string +}): CoStrictFetch { + const agentType = toKebabCase(options?.agentType) || 'build' const costrictFetch = async ( input: RequestInfo | URL, init?: RequestInit, @@ -94,6 +106,7 @@ export function createCoStrictFetch(): CoStrictFetch { headers.set('X-Title', 'CoStrict-CLI') headers.set('X-Costrict-Version', `costrict-cli-${VERSION}`) headers.set('X-Request-ID', randomUUID()) + headers.set('agent-type', agentType) headers.set('zgsm-client-id', creds.machine_id) headers.set('zgsm-client-ide', 'cli') diff --git a/src/costrict/provider/index.ts b/src/costrict/provider/index.ts index cddd78aa1..8eb2bbe5f 100644 --- a/src/costrict/provider/index.ts +++ b/src/costrict/provider/index.ts @@ -34,6 +34,7 @@ import { getCoStrictBaseURL } from './auth.js' import { loadCoStrictCredentials } from './credentials.js' import { isOpenAIThinkingEnabled } from '../../services/api/openai/requestBody.js' import { fetchCoStrictModels } from './models.js' +import { getMainThreadAgentType, getActiveSkillName } from '../../bootstrap/state.js' /** * CoStrict 查询路径 @@ -112,7 +113,9 @@ export async function* queryModelCoStrict( const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice) // 7. 创建专用的 CoStrict OpenAI 客户端(不缓存,每次使用新的 fetch) - const costrictFetch = createCoStrictFetch() + const costrictFetch = createCoStrictFetch({ + agentType: getMainThreadAgentType() ?? getActiveSkillName(), + }) const client = new OpenAI({ apiKey: 'costrict-managed', // 实际 token 由 createCoStrictFetch 注入 baseURL: chatBaseURL, diff --git a/src/costrict/provider/models.ts b/src/costrict/provider/models.ts index 0cae89621..a63e68e11 100644 --- a/src/costrict/provider/models.ts +++ b/src/costrict/provider/models.ts @@ -43,6 +43,7 @@ export async function fetchCoStrictModels( headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json', + 'User-Agent': `csc/${MACRO.VERSION}`, }, }) diff --git a/src/costrict/review/extension.ts b/src/costrict/review/extension.ts deleted file mode 100644 index 99f7736c9..000000000 --- a/src/costrict/review/extension.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * 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 09d86b8a2..84cdb0634 100644 --- a/src/costrict/review/index.ts +++ b/src/costrict/review/index.ts @@ -1,11 +1,10 @@ /** * 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 embedded in the binary + * and registered as bundled skills via registerBundledSkill(). */ -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 new file mode 100644 index 000000000..84ae3ce03 --- /dev/null +++ b/src/costrict/skills/reviewSkills.ts @@ -0,0 +1,60 @@ +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 cbe36d5be..52e082933 100644 --- a/src/entrypoints/mcp.ts +++ b/src/entrypoints/mcp.ts @@ -8,7 +8,6 @@ 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, @@ -30,7 +29,7 @@ import { zodToJsonSchema } from '../utils/zodToJsonSchema.js' type ToolInput = Tool['inputSchema'] type ToolOutput = Tool['outputSchema'] -const MCP_COMMANDS: Command[] = [review] +const MCP_COMMANDS: Command[] = [] export async function startMCPServer( cwd: string, diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index 38b7edb75..03ce7c7f4 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 { registerReviewSkills } from 'src/costrict/skills/reviewSkills.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(() => {}) + // Register review skills (bundled with embedded files) + registerReviewSkills() registerUpdateConfigSkill() registerProjectWikiSkill() diff --git a/src/utils/processUserInput/processSlashCommand.tsx b/src/utils/processUserInput/processSlashCommand.tsx index b2bf58965..59e4ceecf 100644 --- a/src/utils/processUserInput/processSlashCommand.tsx +++ b/src/utils/processUserInput/processSlashCommand.tsx @@ -25,7 +25,7 @@ import type { ProgressMessage, UserMessage, } from 'src/types/message.js' -import { addInvokedSkill, getSessionId } from '../../bootstrap/state.js' +import { addInvokedSkill, getSessionId, setActiveSkillName } from '../../bootstrap/state.js' import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js' import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' import { @@ -162,6 +162,8 @@ async function executeForkedSlashCommand( ? { ...baseAgent, effort: command.effort } : baseAgent + setActiveSkillName(agentDefinition.agentType ?? command.name) + logForDebugging( `Executing forked slash command /${command.name} with agent ${agentDefinition.agentType}`, ) @@ -1230,6 +1232,7 @@ async function getMessagesForPromptSlashCommand( skillContent, getAgentContext()?.agentId ?? null, ) + setActiveSkillName(command.name) const metadata = formatCommandLoadingMetadata(command, args)