你 review Codex 的 PR,发现它在 src/auth/types.ts 加了个 User interface。三个问题一起来了:你 src/types/user.ts 里已经有 User、新这个字段稍有不同、现在一半代码 import 老的、一半 import 新的。TypeScript 不会报错,因为两个结构兼容到能互相赋值——所以 bug 不在编译期暴露,而是跑到运行时才炸。
Codex 写之前没搜。它读了正在改的那个文件,识别出需要的形状,就在原地写了一个新声明。没有一个强制函数——AGENTS.md 里显式的 “先搜” 规则、agent 知道去查的单一 import path、加上一个会因重名而失败的 CI 检查——每次 refactor 都会重演。
TL;DR——最快修复
- 在
AGENTS.md里加一条 类型复用 规则,要求 Codex 在声明前先用rg(ripgrep)搜名字,找到就 import 现有的。Codex 本来就把rg当主力搜索工具,所以这是它会本能去跑的命令(截至 2026 年 6 月)。 - 把
src/types/index.ts做成 barrel,所有共享 type 都走@/types,让 “这个是不是已经有了” 一次搜索就能答。 - 加一个 ts-morph CI 检查,对重名的
interface/type/enum/class直接让 build 失败。Codex 看到红 CI 会自己修。
光做 Step 1 就能挡掉大部分情况。Step 2、3 补的是 User、Response 这类最容易撞的泛名。
常见原因——你属于哪一类
| PR / transcript 里的现象 | 大概的原因 | 修法 |
|---|---|---|
新增 interface X;rg "interface X" src/ 显示别处还有一份 | Agent 创建前没搜 | AGENTS.md “先搜” 规则(Step 1) |
find src -name 'types.ts' 返回 10+ path,没有中心索引 | Type 散落,没有明确归处 | @/types types barrel(Step 2) |
rg -i 'type|interface|reuse' AGENTS.md 没结果 | 没有复用 type 的指引 | AGENTS.md 规则(Step 1) |
transcript 只对当前文件 read_file,没搜过 | 现有 type 在没加载的 barrel 里 | barrel + AGENTS.md 指向(Step 1 + 2) |
diff 加了泛名 type(User、Response、Config) | 名字和 DOM/库类型撞了,agent 以为就是那个 | 搜更广 + ts-morph CI(Step 1 + 3) |
1. Agent 创建前没搜
Codex 默认流程是 “读我要改的文件,补缺的东西”。缺的是个 type,它就写一个 inline。它不一定会先在整个 repo 搜现有定义。
如何判断:PR 加了新的 interface X 或 type X = ...,而 rg -n "interface X|type X " src/ 显示别处还有一份。
2. Types 散落各处,没有中心索引
一半 type 在 src/types/,一半和首个使用者放一起(src/auth/types.ts、src/api/types.ts)。没有明显的 “type 该放哪” 答案,Codex 就加到当前文件里。
如何判断:find src -name 'types.ts' -o -path '*/types/*.ts' 返回十个以上 path,而且没有 src/types/index.ts 在 re-export。
3. AGENTS.md 没要求搜
Codex 干活之前会先加载 AGENTS.md(从 Git root 一路走到工作目录,最近的那份优先,默认合并上限 32 KiB)。如果这份文件里没有复用 type 的规则,Codex 就选局部最方便的路径。
如何判断:rg -i 'type|interface|reuse' AGENTS.md 没结果。
4. 现有 type 在 barrel 里,agent 没加载
你有 src/types/index.ts 在 re-export 所有 type,但 agent 只 open 了它要改的那个文件。barrel 没见到,就当作没有。
如何判断:transcript 只对当前文件 read_file,没有跨 repo rg / 搜过这个 type 名。
5. Type 名和常见库重名
Codex 看到 Response 以为是 DOM 的 Response,于是新建了 ApiResponse,根本没查你是不是已经有了。泛名(User、Item、Response、Error、Config)最容易撞。
如何判断:diff 加的 type 是泛名,而已有那个也是同样的名字。
最短修复路径
Step 1:AGENTS.md 加 “先搜再建” 规则
Codex 在规划过程里一直在跑 rg(ripgrep),所以一条写成 rg 命令的规则它是真会去执行的:
## Type 和 interface 复用
新建 `type`、`interface`、`class`、`enum` 之前:
1. 先搜名字:
`rg -n "(interface|class|enum) <Name>\b|type <Name>\b" src/`
2. 已有定义就 import,不要重复声明。
3. 已有那个缺你需要的字段,用 `interface X extends BaseX` 扩展,
或就地更新——不要分叉。
4. 新的共享 type 放 `src/types/<domain>.ts`。除非 type 是单模块私有,
不要 inline 在 feature 文件里。
名字泛的时候(`User`、`Item`、`Response`)搜广一点——
`packages/`、`apps/`、`shared/` 都搜。
这是 agent 真能跟着做的规则:一条搜索命令加一个决策树。AGENTS.md 控制在 32 KiB 上限内,别让内容被截断。
Step 2:把 type 收口到单一 import path
src/types/index.ts 做成 barrel,re-export 所有共享 type:
// src/types/index.ts
export type { User } from './user'
export type { Session } from './session'
export type { ApiResponse, ApiError } from './api'
export type { Permission } from './permission'
然后把 AGENTS.md 指向它:
所有共享 type 从 `@/types` 出口,统一从这里 import:
import type { User } from '@/types'
`@/types` 里没有的,当作 module 私有。任何可能被复用的 type 在新建之前都先搜 `@/types`。
单一 import path 让 “之前有没有” 一次搜索就能答。
Step 3:ts-morph CI 抓重名 type
这能补上 review 和 AGENTS.md 漏掉的部分。下面的脚本用 ts-morph(截至 2026 年 6 月为 28.x),覆盖 interface、type alias、enum、class——和 AGENTS.md 规则点名的四种声明一致:
// scripts/check-duplicate-types.ts
import { Project } from 'ts-morph'
const project = new Project({ tsConfigFilePath: 'tsconfig.json' })
const decls = new Map<string, string[]>()
const record = (name: string, path: string) => {
const list = decls.get(name) ?? []
list.push(path)
decls.set(name, list)
}
for (const file of project.getSourceFiles('src/**/*.ts')) {
const path = file.getFilePath()
for (const d of file.getInterfaces()) record(d.getName(), path)
for (const d of file.getTypeAliases()) record(d.getName(), path)
for (const d of file.getEnums()) record(d.getName(), path)
for (const d of file.getClasses()) {
const name = d.getName()
if (name) record(name, path)
}
}
const dupes = [...decls.entries()].filter(([, files]) => files.length > 1)
if (dupes.length > 0) {
for (const [name, files] of dupes) {
console.error(`Duplicate type ${name}:`)
for (const f of files) console.error(` ${f}`)
}
process.exit(1)
}
接到 CI 当 required check(npm run check:types)。Codex 看到失败的 CI 输出,下一轮会自己修。
Step 4:让 ESLint 指向规范位置
codebase 有自定义 ESLint rule 的话,加一个:发现重复声明共享 type 就报错并指向规范路径。没有自定义 rule 也能用 eslint-plugin-import 的 no-duplicates 加 no-restricted-imports,把 import 都收口到 @/types:
{
"rules": {
"no-restricted-imports": ["error", {
"patterns": [{
"group": ["**/auth/types", "**/api/types", "**/user/types"],
"message": "共享 type 从 @/types import,不要走深路径。"
}]
}]
}
}
Codex 读得到 ESLint 输出,会自己把 import path 改对。
Step 5:agent PR 先看 import
这是 ts-morph 抓不到的情况的人工兜底——同名但形状稍不同。加进 PR checklist:
- [ ] 没有新增 `interface` 或 `type` 和 `@/types` 里重名
- [ ] 新的共享 type 都在 `src/types/<domain>.ts`,不在 feature 文件里 inline
- [ ] `npm run check:types` 过(跑 ts-morph 的重名检查)
怎么确认修好了
- 本地跑一遍重名检查:
npm run check:types应该 exit0且什么都不打印。 - 确认 Codex 加载到了新规则。在 repo 根目录跑
codex --ask-for-approval never "Summarize the type-reuse rules in AGENTS.md.", 看它的回答里有没有这条规则。如果没有,说明你的AGENTS.md超了 32 KiB 上限,或放错目录了。 - 给 Codex 一个会动到共享 type 的任务,读 transcript:在任何新声明之前,你应该能看到它对这个 type 名跑了
rg搜索。
预防
AGENTS.md强制 “先搜再建”,连具体rg命令一起给- 所有共享 type 放
src/types/,从src/types/index.ts出口 - ts-morph CI 检查重名的 interface / type / enum / class
- ESLint
no-restricted-imports把 import 收到@/types - Reviewer 抽查 agent PR 里新加的
interface/type声明 - 泛名(
User、Item、Response)强制加 domain 前缀,避免未来撞名
常见问题
为什么 TypeScript 不直接抓出重复? 因为两份声明在不同文件、不同 module scope 里,它们是两个碰巧同名的不同类型。只要字段结构兼容,TypeScript 就允许互相赋值。只有当某个字段在一个形状上有、另一个上没有时,你才会感觉到这个 bug。
AGENTS.md 规则里该用 rg 还是 grep?
用 rg(ripgrep)。截至 2026 年 6 月,Codex、Cursor 和大多数 coding agent 都把 ripgrep 当搜索主力——它会遵守 .gitignore、在大 repo 上快得多,也是 Codex 默认就会用的命令。rg 没装时 grep 也能用,但 agent 不太会主动去跑它。
Codex 老是不理这条规则,怎么办?
先确认它加载了:跑 codex --ask-for-approval never "Summarize the type-reuse rules in AGENTS.md."。如果回答里没有这条规则,多半是你的 AGENTS.md 超了默认 32 KiB 上限(project_doc_max_bytes),或者不在 Codex 会走的路径上(从 Git root 走到工作目录)。把文件精简掉,或者把规则挪到离被改代码最近的那份 AGENTS.md——最近的那份优先。
仓库里已经存在的重复怎么清?
跑 Step 3 的 ts-morph 脚本,列出每个重复名字和它的文件路径。选定规范声明,删掉其余的,然后让 type-checker 来驱动修复:每个报错的 import 都指向一处需要切到 @/types 的代码。改完再跑 npm run check:types,直到全绿。
IDE 里的 Codex 和 CLI 都适用吗?
都适用。两者读的是同一条 AGENTS.md 链(全局 ~/.codex/AGENTS.md,再加从 Git root 往下的项目文件)。ts-morph CI 检查和 ESLint 规则跑在你的流水线里,不管 PR 是哪个 Codex 入口产出的。