Prefer AGENTS.md over CLAUDE.md for project instructions (#439)
* Prefer AGENTS.md over CLAUDE.md for project instructions * fix: preserve CLAUDE.md fallback behavior * fix: isolate onboarding tests and preserve legacy init * fix: restore full fsOperations exports in test mock and align compact cwd * Fix onboarding test isolation and init migration guidance * Tighten init prompt coverage and onboarding copy * Handle nested project instruction paths consistently * Fix NEW_INIT feature gate for Bun build --------- Co-authored-by: 赵小落 <zhaoxiaoluo@zhaoxiaoluodeMac-mini.local> Co-authored-by: zhaomo01 <zhaomo01@baidu.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 1. Managed memory (eg. /etc/claude-code/CLAUDE.md) - Global instructions for all users
|
||||
* 2. User memory (~/.claude/CLAUDE.md) - Private global instructions for all projects
|
||||
* 3. Project memory (CLAUDE.md, .claude/CLAUDE.md, and .claude/rules/*.md in project roots) - Instructions checked into the codebase
|
||||
* 3. Project memory (AGENTS.md or fallback CLAUDE.md, plus .claude/CLAUDE.md and .claude/rules/*.md in project roots) - Instructions checked into the codebase
|
||||
* 4. Local memory (CLAUDE.local.md in project roots) - Private project-specific instructions
|
||||
*
|
||||
* Files are loaded in reverse order of priority, i.e. the latest files are highest priority
|
||||
@@ -13,7 +13,8 @@
|
||||
* - User memory is loaded from the user's home directory
|
||||
* - Project and Local files are discovered by traversing from the current directory up to root
|
||||
* - Files closer to the current directory have higher priority (loaded later)
|
||||
* - CLAUDE.md, .claude/CLAUDE.md, and all .md files in .claude/rules/ are checked in each directory for Project memory
|
||||
* - AGENTS.md is preferred for root project instructions; CLAUDE.md is only used when AGENTS.md is absent
|
||||
* - .claude/CLAUDE.md and all .md files in .claude/rules/ are checked in each directory for Project memory
|
||||
*
|
||||
* Memory @include directive:
|
||||
* - Memory files can include other files using @ notation
|
||||
@@ -75,6 +76,10 @@ import {
|
||||
import type { MemoryType } from './memory/types.js'
|
||||
import { expandPath } from './path.js'
|
||||
import { pathInWorkingPath } from './permissions/filesystem.js'
|
||||
import {
|
||||
getProjectInstructionFilePath,
|
||||
isProjectInstructionFileName,
|
||||
} from './projectInstructions.js'
|
||||
import { isSettingSourceEnabled } from './settings/constants.js'
|
||||
import { getInitialSettings } from './settings/settings.js'
|
||||
|
||||
@@ -868,7 +873,7 @@ export const getMemoryFiles = memoize(
|
||||
// When running from a git worktree nested inside its main repo (e.g.,
|
||||
// .claude/worktrees/<name>/ from `claude -w`), the upward walk passes
|
||||
// through both the worktree root and the main repo root. Both contain
|
||||
// checked-in files like CLAUDE.md and .claude/rules/*.md, so the same
|
||||
// checked-in files like AGENTS.md/CLAUDE.md and .claude/rules/*.md, so the same
|
||||
// content gets loaded twice. Skip Project-type (checked-in) files from
|
||||
// directories above the worktree but within the main repo — the worktree
|
||||
// already has its own checkout. CLAUDE.local.md is gitignored so it only
|
||||
@@ -892,9 +897,12 @@ export const getMemoryFiles = memoize(
|
||||
pathInWorkingPath(dir, canonicalRoot) &&
|
||||
!pathInWorkingPath(dir, gitRoot)
|
||||
|
||||
// Try reading CLAUDE.md (Project) - only if projectSettings is enabled
|
||||
// Try reading the root project instruction file (AGENTS.md first, otherwise CLAUDE.md)
|
||||
if (isSettingSourceEnabled('projectSettings') && !skipProject) {
|
||||
const projectPath = join(dir, 'CLAUDE.md')
|
||||
const projectPath = getProjectInstructionFilePath(
|
||||
dir,
|
||||
getFsImplementation().existsSync,
|
||||
)
|
||||
result.push(
|
||||
...(await processMemoryFile(
|
||||
projectPath,
|
||||
@@ -942,15 +950,18 @@ export const getMemoryFiles = memoize(
|
||||
}
|
||||
}
|
||||
|
||||
// Process CLAUDE.md from additional directories (--add-dir) if env var is enabled
|
||||
// Process root project instruction files from additional directories (--add-dir) if env var is enabled
|
||||
// This is controlled by CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD and defaults to off
|
||||
// Note: we don't check isSettingSourceEnabled('projectSettings') here because --add-dir
|
||||
// is an explicit user action and the SDK defaults settingSources to [] when not specified
|
||||
if (isEnvTruthy(process.env.CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD)) {
|
||||
const additionalDirs = getAdditionalDirectoriesForClaudeMd()
|
||||
for (const dir of additionalDirs) {
|
||||
// Try reading CLAUDE.md from the additional directory
|
||||
const projectPath = join(dir, 'CLAUDE.md')
|
||||
// Try reading the root project instruction file from the additional directory
|
||||
const projectPath = getProjectInstructionFilePath(
|
||||
dir,
|
||||
getFsImplementation().existsSync,
|
||||
)
|
||||
result.push(
|
||||
...(await processMemoryFile(
|
||||
projectPath,
|
||||
@@ -1248,7 +1259,7 @@ export async function getManagedAndUserConditionalRules(
|
||||
|
||||
/**
|
||||
* Gets memory files for a single nested directory (between CWD and target).
|
||||
* Loads CLAUDE.md, unconditional rules, and conditional rules for that directory.
|
||||
* Loads the root project instruction file, unconditional rules, and conditional rules for that directory.
|
||||
*
|
||||
* @param dir The directory to process
|
||||
* @param targetPath The target file path (for conditional rule matching)
|
||||
@@ -1262,9 +1273,12 @@ export async function getMemoryFilesForNestedDirectory(
|
||||
): Promise<MemoryFileInfo[]> {
|
||||
const result: MemoryFileInfo[] = []
|
||||
|
||||
// Process project memory files (CLAUDE.md and .claude/CLAUDE.md)
|
||||
// Process project memory files (AGENTS.md first, otherwise CLAUDE.md, plus .claude/CLAUDE.md)
|
||||
if (isSettingSourceEnabled('projectSettings')) {
|
||||
const projectPath = join(dir, 'CLAUDE.md')
|
||||
const projectPath = getProjectInstructionFilePath(
|
||||
dir,
|
||||
getFsImplementation().existsSync,
|
||||
)
|
||||
result.push(
|
||||
...(await processMemoryFile(
|
||||
projectPath,
|
||||
@@ -1439,13 +1453,13 @@ export async function shouldShowClaudeMdExternalIncludesWarning(): Promise<boole
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path is a memory file (CLAUDE.md, CLAUDE.local.md, or .claude/rules/*.md)
|
||||
* Check if a file path is a memory file (AGENTS.md, CLAUDE.md, CLAUDE.local.md, or .claude/rules/*.md)
|
||||
*/
|
||||
export function isMemoryFilePath(filePath: string): boolean {
|
||||
const name = basename(filePath)
|
||||
|
||||
// CLAUDE.md or CLAUDE.local.md anywhere
|
||||
if (name === 'CLAUDE.md' || name === 'CLAUDE.local.md') {
|
||||
// Root instruction files or CLAUDE.local.md anywhere
|
||||
if (isProjectInstructionFileName(name) || name === 'CLAUDE.local.md') {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { normalizePathForConfigKey } from './path.js'
|
||||
import { getEssentialTrafficOnlyReason } from './privacyLevel.js'
|
||||
import { getManagedFilePath } from './settings/managedPath.js'
|
||||
import type { ThemeSetting } from './theme.js'
|
||||
import { PRIMARY_PROJECT_INSTRUCTION_FILE } from './projectInstructions.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const teamMemPaths = feature('TEAMMEM')
|
||||
@@ -1823,7 +1824,7 @@ export function getMemoryPath(memoryType: MemoryType): string {
|
||||
case 'Local':
|
||||
return join(cwd, 'CLAUDE.local.md')
|
||||
case 'Project':
|
||||
return join(cwd, 'CLAUDE.md')
|
||||
return join(cwd, PRIMARY_PROJECT_INSTRUCTION_FILE)
|
||||
case 'Managed':
|
||||
return join(getManagedFilePath(), 'CLAUDE.md')
|
||||
case 'AutoMem':
|
||||
|
||||
105
src/utils/projectInstructions.test.ts
Normal file
105
src/utils/projectInstructions.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import {
|
||||
findProjectInstructionFilePathInAncestors,
|
||||
FALLBACK_PROJECT_INSTRUCTION_FILE,
|
||||
getProjectInstructionFilePath,
|
||||
getProjectInstructionFilePaths,
|
||||
hasProjectInstructionFile,
|
||||
isProjectInstructionFileName,
|
||||
PRIMARY_PROJECT_INSTRUCTION_FILE,
|
||||
} from './projectInstructions.js'
|
||||
|
||||
describe('projectInstructions', () => {
|
||||
test('prefers AGENTS.md over CLAUDE.md for root project instructions', () => {
|
||||
const dir = '/repo'
|
||||
const existingPaths = new Set([
|
||||
join(dir, PRIMARY_PROJECT_INSTRUCTION_FILE),
|
||||
join(dir, FALLBACK_PROJECT_INSTRUCTION_FILE),
|
||||
])
|
||||
|
||||
const filePath = getProjectInstructionFilePath(
|
||||
dir,
|
||||
path => existingPaths.has(path),
|
||||
)
|
||||
|
||||
expect(filePath).toBe(join(dir, PRIMARY_PROJECT_INSTRUCTION_FILE))
|
||||
})
|
||||
|
||||
test('falls back to CLAUDE.md when AGENTS.md is absent', () => {
|
||||
const dir = '/repo'
|
||||
const existingPaths = new Set([join(dir, FALLBACK_PROJECT_INSTRUCTION_FILE)])
|
||||
|
||||
const filePath = getProjectInstructionFilePath(
|
||||
dir,
|
||||
path => existingPaths.has(path),
|
||||
)
|
||||
|
||||
expect(filePath).toBe(join(dir, FALLBACK_PROJECT_INSTRUCTION_FILE))
|
||||
})
|
||||
|
||||
test('returns both candidate root instruction paths', () => {
|
||||
const dir = '/repo'
|
||||
|
||||
expect(getProjectInstructionFilePaths(dir)).toEqual([
|
||||
join(dir, PRIMARY_PROJECT_INSTRUCTION_FILE),
|
||||
join(dir, FALLBACK_PROJECT_INSTRUCTION_FILE),
|
||||
])
|
||||
})
|
||||
|
||||
test('detects whether a repo instruction file exists', () => {
|
||||
const dir = '/repo'
|
||||
const existingPaths = new Set([join(dir, PRIMARY_PROJECT_INSTRUCTION_FILE)])
|
||||
|
||||
expect(hasProjectInstructionFile(dir, path => existingPaths.has(path))).toBe(
|
||||
true,
|
||||
)
|
||||
expect(hasProjectInstructionFile(dir, () => false)).toBe(false)
|
||||
})
|
||||
|
||||
test('recognizes AGENTS.md as a root instruction filename', () => {
|
||||
expect(isProjectInstructionFileName(PRIMARY_PROJECT_INSTRUCTION_FILE)).toBe(
|
||||
true,
|
||||
)
|
||||
expect(isProjectInstructionFileName(FALLBACK_PROJECT_INSTRUCTION_FILE)).toBe(
|
||||
true,
|
||||
)
|
||||
expect(isProjectInstructionFileName('README.md')).toBe(false)
|
||||
})
|
||||
|
||||
test('finds repo instructions in ancestor directories', () => {
|
||||
const repoDir = '/repo'
|
||||
const nestedDir = join(repoDir, 'packages', 'app')
|
||||
const existingPaths = new Set([join(repoDir, PRIMARY_PROJECT_INSTRUCTION_FILE)])
|
||||
|
||||
expect(
|
||||
findProjectInstructionFilePathInAncestors(
|
||||
nestedDir,
|
||||
path => existingPaths.has(path),
|
||||
),
|
||||
).toBe(join(repoDir, PRIMARY_PROJECT_INSTRUCTION_FILE))
|
||||
})
|
||||
|
||||
test('prefers the closest ancestor project instruction file', () => {
|
||||
const repoDir = '/repo'
|
||||
const nestedProjectDir = join(repoDir, 'packages', 'app')
|
||||
const existingPaths = new Set([
|
||||
join(repoDir, PRIMARY_PROJECT_INSTRUCTION_FILE),
|
||||
join(nestedProjectDir, FALLBACK_PROJECT_INSTRUCTION_FILE),
|
||||
])
|
||||
|
||||
expect(
|
||||
findProjectInstructionFilePathInAncestors(
|
||||
join(nestedProjectDir, 'src'),
|
||||
path => existingPaths.has(path),
|
||||
),
|
||||
).toBe(join(nestedProjectDir, FALLBACK_PROJECT_INSTRUCTION_FILE))
|
||||
})
|
||||
|
||||
test('returns null when no ancestor repo instruction file exists', () => {
|
||||
expect(
|
||||
findProjectInstructionFilePathInAncestors('/repo/packages/app', () => false),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
55
src/utils/projectInstructions.ts
Normal file
55
src/utils/projectInstructions.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
export const PRIMARY_PROJECT_INSTRUCTION_FILE = 'AGENTS.md'
|
||||
export const FALLBACK_PROJECT_INSTRUCTION_FILE = 'CLAUDE.md'
|
||||
|
||||
export function getProjectInstructionFilePaths(dir: string): string[] {
|
||||
return [
|
||||
join(dir, PRIMARY_PROJECT_INSTRUCTION_FILE),
|
||||
join(dir, FALLBACK_PROJECT_INSTRUCTION_FILE),
|
||||
]
|
||||
}
|
||||
|
||||
export function getProjectInstructionFilePath(
|
||||
dir: string,
|
||||
existsSync: (path: string) => boolean,
|
||||
): string {
|
||||
const [primaryPath, fallbackPath] = getProjectInstructionFilePaths(dir)
|
||||
return existsSync(primaryPath)
|
||||
? primaryPath
|
||||
: fallbackPath
|
||||
}
|
||||
|
||||
export function hasProjectInstructionFile(
|
||||
dir: string,
|
||||
existsSync: (path: string) => boolean,
|
||||
): boolean {
|
||||
return getProjectInstructionFilePaths(dir).some(path => existsSync(path))
|
||||
}
|
||||
|
||||
export function findProjectInstructionFilePathInAncestors(
|
||||
startDir: string,
|
||||
existsSync: (path: string) => boolean,
|
||||
): string | null {
|
||||
let currentDir = startDir
|
||||
|
||||
while (true) {
|
||||
if (hasProjectInstructionFile(currentDir, existsSync)) {
|
||||
return getProjectInstructionFilePath(currentDir, existsSync)
|
||||
}
|
||||
|
||||
const parentDir = dirname(currentDir)
|
||||
if (parentDir === currentDir) {
|
||||
return null
|
||||
}
|
||||
|
||||
currentDir = parentDir
|
||||
}
|
||||
}
|
||||
|
||||
export function isProjectInstructionFileName(name: string): boolean {
|
||||
return (
|
||||
name === PRIMARY_PROJECT_INSTRUCTION_FILE ||
|
||||
name === FALLBACK_PROJECT_INSTRUCTION_FILE
|
||||
)
|
||||
}
|
||||
@@ -1082,10 +1082,10 @@ export const SettingsSchema = lazySchema(() =>
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Glob patterns or absolute paths of CLAUDE.md files to exclude from loading. ' +
|
||||
'Glob patterns or absolute paths of AGENTS.md/CLAUDE.md files to exclude from loading. ' +
|
||||
'Patterns are matched against absolute file paths using picomatch. ' +
|
||||
'Only applies to User, Project, and Local memory types (Managed/policy files cannot be excluded). ' +
|
||||
'Examples: "/home/user/monorepo/CLAUDE.md", "**/code/CLAUDE.md", "**/some-dir/.claude/rules/**"',
|
||||
'Examples: "/home/user/monorepo/AGENTS.md", "**/code/CLAUDE.md", "**/some-dir/.claude/rules/**"',
|
||||
),
|
||||
pluginTrustMessage: z
|
||||
.string()
|
||||
|
||||
Reference in New Issue
Block a user