File size: 6,446 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | 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<string | null> => {
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()}.`,
}
},
)
|