diff --git a/.gitignore b/.gitignore index 2085f9df3..cb74e32ef 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,15 @@ src/costrict/review/skill/builtin.ts # Review builtin cache packages/builtin-tools/bundled-review/ -bundled-review/ \ No newline at end of file + +# Upstream additions +teach-me +credentials.json + +# Session-scoped progress / state files written by agents and skills +.claude-impl-state.md +.claude-progress.md +.claude-recovery.md +.test-progress.md +.squash-tmp/ +.git.*-backup \ No newline at end of file diff --git a/package.json b/package.json index 08d0b027b..e52da4c7e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ ], "scripts": { "build": "bun run build.ts", - "build:builtin-review": "bun run scripts/generate-review-builtin.ts", "build:vite": "vite build && bun run scripts/post-build.ts", "build:vite:only": "vite build", "build:bun": "bun run build.ts", @@ -207,6 +206,7 @@ "turndown": "^7.2.2", "type-fest": "^5.5.0", "typescript": "^6.0.2", + "undici": "^7.24.6", "url-handler-napi": "workspace:*", "usehooks-ts": "^3.1.1", "vite": "^8.0.8", diff --git a/scripts/generate-review-builtin.ts b/scripts/generate-review-builtin.ts index a1607b37e..0af742575 100644 --- a/scripts/generate-review-builtin.ts +++ b/scripts/generate-review-builtin.ts @@ -293,26 +293,6 @@ export async function extractBundledSkill(skillName: string, targetDir: string, await writeFile(join(targetDir, relativePath), content, "utf-8") } } - -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}\`) - } - - const { mkdir: mkdirSync, writeFile: writeFileSync } = await import('fs/promises') - const { join: pathJoin, dirname: pathDirname } = await import('path') - await mkdirSync(targetDir, { recursive: true }) - for (const [relativePath, fileContent] of Object.entries(skillFiles)) { - await mkdirSync(pathJoin(targetDir, pathDirname(relativePath)), { recursive: true }) - await writeFileSync(pathJoin(targetDir, relativePath), fileContent, 'utf-8') - } -} ` await mkdir(path.dirname(builtinSkillsFile), { recursive: true }) diff --git a/src/commands/lang/lang.ts b/src/commands/lang/lang.ts index e386ec726..8520d26f2 100644 --- a/src/commands/lang/lang.ts +++ b/src/commands/lang/lang.ts @@ -1,5 +1,4 @@ import type { ToolUseContext } from '../../Tool.js' -import { clearCommandsCache } from '../../commands.js' import type { LocalJSXCommandContext, LocalJSXCommandOnDone, @@ -10,7 +9,6 @@ import { getLanguageDisplayName, getResolvedLanguage, } from '../../utils/language.js' -import { Extension as ReviewExtension } from '../../costrict/review/index.js' const VALID_LANGS: readonly PreferredLanguage[] = ['en', 'zh', 'auto'] @@ -42,10 +40,6 @@ export async function call( const lang = arg as PreferredLanguage saveGlobalConfig(current => ({ ...current, preferredLanguage: lang })) - // Re-extract review skills for the new locale and refresh skill caches - await ReviewExtension.initializeBuiltinSkills().catch(() => {}) - clearCommandsCache() - const resolved = getResolvedLanguage() const suffix = lang === 'auto' ? ` → ${getLanguageDisplayName(resolved)}` : '' onDone(`Language set to ${getLanguageDisplayName(lang)}${suffix}`, { diff --git a/src/costrict/command/locales/en/review.txt b/src/costrict/command/locales/en/review.txt index 5fe8f59ab..e4a70121e 100644 --- a/src/costrict/command/locales/en/review.txt +++ b/src/costrict/command/locales/en/review.txt @@ -1,5 +1,5 @@ # Code Review -Please use the Skill tool to load the `review` skill to perform a code review on: $ARGUMENTS +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/zh-CN/review.txt b/src/costrict/command/locales/zh-CN/review.txt index fec845816..ed2871707 100644 --- a/src/costrict/command/locales/zh-CN/review.txt +++ b/src/costrict/command/locales/zh-CN/review.txt @@ -1,5 +1,5 @@ # 代码审查 -请使用 Skill 工具加载 `review` 技能来对以下内容执行代码审查:$ARGUMENTS +请对以下内容执行代码审查:$ARGUMENTS 全程请使用中文进行回答与文件写入。 diff --git a/src/costrict/review/extension.ts b/src/costrict/review/extension.ts index 54b26cfa2..99f7736c9 100644 --- a/src/costrict/review/extension.ts +++ b/src/costrict/review/extension.ts @@ -1,72 +1,87 @@ -import { mkdir, readFile, writeFile } from 'fs/promises' -import { join } from 'path' -import { getClaudeConfigHomeDir } from 'src/utils/envUtils.js' +/** + * 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 { - extractBundledSkill, - getBuiltinSkillVersion, - listBuiltinSkillNames, -} from './skill/builtin.js' +import * as Builtin from './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 getReviewSkillsDir(): string { - return join(getClaudeConfigHomeDir(), 'skills') +function getSkillCacheDir(): string { + const home = process.env.HOME ?? process.env.USERPROFILE ?? '' + return path.join(home, '.claude', 'skills') } function getVersionFilePath(skillDir: string): string { - return join(skillDir, '.version') + return path.join(skillDir, '.version') } async function getInstalledVersion(skillDir: string): Promise { try { - return await readFile(getVersionFilePath(skillDir), 'utf-8') + const content = await readFile(getVersionFilePath(skillDir), 'utf-8') + return content.trim() } catch { return null } } -async function writeVersionFile( - skillDir: string, - skillName: string, - locale: string, -): Promise { - const builtinVersion = getBuiltinSkillVersion(skillName) +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') } -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 locale = getLocale() - const skillsDir = getReviewSkillsDir() - const skillNames = listBuiltinSkillNames() + const lang = getResolvedLanguage() + const locale = LOCALE_MAP[lang] ?? 'zh-CN' - for (const skillName of skillNames) { - const skillDir = join(skillsDir, skillName) - if (!(await needsUpdate(skillDir, skillName, locale))) continue + const cacheDir = getSkillCacheDir() + const skillNames = Builtin.listBuiltinSkills() - await mkdir(skillDir, { recursive: true }) - await extractBundledSkill(skillName, skillDir, locale) - await writeVersionFile(skillDir, skillName, locale) + 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 getReviewSkillsDir() + return getSkillCacheDir() } diff --git a/src/costrict/review/index.ts b/src/costrict/review/index.ts index 01f347fbb..09d86b8a2 100644 --- a/src/costrict/review/index.ts +++ b/src/costrict/review/index.ts @@ -1,9 +1,15 @@ /** * CoStrict Review Module * - * Provides builtin review skills that are extracted to disk at runtime - * and discovered by the standard skill scanner. + * Provides builtin review skills and agents that are embedded + * in the binary and extracted to cache on first run. */ -export * as SkillBuiltin from './skill/builtin.js' 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' diff --git a/src/skills/bundled/index.ts b/src/skills/bundled/index.ts index a082c2a6e..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 { Extension as ReviewExtension } from 'src/costrict/review/index.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 { - // Extract review skills to disk for standard scanner discovery - ReviewExtension.initializeBuiltinSkills().catch(() => {}) + // Initialize builtin review skills (extract to cache if needed) + initializeBuiltinSkills().catch(() => {}) registerUpdateConfigSkill() registerProjectWikiSkill()