import { feature } from 'bun:bundle' import memoize from 'lodash-es/memoize.js' import { getAdditionalDirectoriesForClaudeMd, setCachedClaudeMdContent, } from './bootstrap/state.js' import { getLocalISODate } from './constants/common.js' import { filterInjectedMemoryFiles, getClaudeMds, getMemoryFiles, } from './utils/claudemd.js' import { logForDiagnosticsNoPII } from './utils/diagLogs.js' import { isBareMode, isEnvTruthy } from './utils/envUtils.js' import { execFileNoThrow } from './utils/execFileNoThrow.js' import { getBranch, getDefaultBranch, getIsGit, gitExe } from './utils/git.js' import { shouldIncludeGitInstructions } from './utils/gitSettings.js' import { logError } from './utils/log.js' const MAX_STATUS_CHARS = 2000 // System prompt injection for cache breaking (ant-only, ephemeral debugging state) let systemPromptInjection: string | null = null export function getSystemPromptInjection(): string | null { return systemPromptInjection } export function setSystemPromptInjection(value: string | null): void { systemPromptInjection = value // Clear context caches immediately when injection changes getUserContext.cache.clear?.() getSystemContext.cache.clear?.() } export const getGitStatus = memoize(async (): Promise => { if (process.env.NODE_ENV === 'test') { // Avoid cycles in tests return null } const startTime = Date.now() logForDiagnosticsNoPII('info', 'git_status_started') const isGitStart = Date.now() const isGit = await getIsGit() logForDiagnosticsNoPII('info', 'git_is_git_check_completed', { duration_ms: Date.now() - isGitStart, is_git: isGit, }) if (!isGit) { logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', { duration_ms: Date.now() - startTime, }) return null } try { const gitCmdsStart = Date.now() const [branch, mainBranch, status, log, userName] = await Promise.all([ getBranch(), getDefaultBranch(), execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], { preserveOutputOnError: false, }).then(({ stdout }) => stdout.trim()), execFileNoThrow( gitExe(), ['--no-optional-locks', 'log', '--oneline', '-n', '5'], { preserveOutputOnError: false, }, ).then(({ stdout }) => stdout.trim()), execFileNoThrow(gitExe(), ['config', 'user.name'], { preserveOutputOnError: false, }).then(({ stdout }) => stdout.trim()), ]) logForDiagnosticsNoPII('info', 'git_commands_completed', { duration_ms: Date.now() - gitCmdsStart, status_length: status.length, }) // Check if status exceeds character limit const truncatedStatus = status.length > MAX_STATUS_CHARS ? status.substring(0, MAX_STATUS_CHARS) + '\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)' : status logForDiagnosticsNoPII('info', 'git_status_completed', { duration_ms: Date.now() - startTime, truncated: status.length > MAX_STATUS_CHARS, }) return [ `This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`, `Current branch: ${branch}`, `Main branch (you will usually use this for PRs): ${mainBranch}`, ...(userName ? [`Git user: ${userName}`] : []), `Status:\n${truncatedStatus || '(clean)'}`, `Recent commits:\n${log}`, ].join('\n\n') } catch (error) { logForDiagnosticsNoPII('error', 'git_status_failed', { duration_ms: Date.now() - startTime, }) logError(error) return null } }) /** * This context is prepended to each conversation, and cached for the duration of the conversation. */ export const getSystemContext = memoize( async (): Promise<{ [k: string]: string }> => { const startTime = Date.now() logForDiagnosticsNoPII('info', 'system_context_started') // Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled const gitStatus = isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) || !shouldIncludeGitInstructions() ? null : await getGitStatus() // Include system prompt injection if set (for cache breaking, ant-only) const injection = feature('BREAK_CACHE_COMMAND') ? getSystemPromptInjection() : null logForDiagnosticsNoPII('info', 'system_context_completed', { duration_ms: Date.now() - startTime, has_git_status: gitStatus !== null, has_injection: injection !== null, }) return { ...(gitStatus && { gitStatus }), ...(feature('BREAK_CACHE_COMMAND') && injection ? { cacheBreaker: `[CACHE_BREAKER: ${injection}]`, } : {}), } }, ) /** * This context is prepended to each conversation, and cached for the duration of the conversation. */ export const getUserContext = memoize( async (): Promise<{ [k: string]: string }> => { const startTime = Date.now() logForDiagnosticsNoPII('info', 'user_context_started') // CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always. // --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir. // --bare means "skip what I didn't ask for", not "ignore what I asked for". const shouldDisableClaudeMd = isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) || (isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0) // Await the async I/O (readFile/readdir directory walk) so the event // loop yields naturally at the first fs.readFile. const claudeMd = shouldDisableClaudeMd ? null : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles())) // Cache for the auto-mode classifier (yoloClassifier.ts reads this // instead of importing claudemd.ts directly, which would create a // cycle through permissions/filesystem → permissions → yoloClassifier). setCachedClaudeMdContent(claudeMd || null) logForDiagnosticsNoPII('info', 'user_context_completed', { duration_ms: Date.now() - startTime, claudemd_length: claudeMd?.length ?? 0, claudemd_disabled: Boolean(shouldDisableClaudeMd), }) return { ...(claudeMd && { claudeMd }), currentDate: `Today's date is ${getLocalISODate()}.`, } }, )