React Doctor 的 Agent 贡献指南:CLAUDE.md 与 AGENTS.md 如何编码 Effect v4 工作流与发布纪律
2026/9/14 17:25:17 网站建设 项目流程

React Doctor 的 Agent 贡献指南:CLAUDE.md 与 AGENTS.md 如何编码 Effect v4 工作流与发布纪律

【免费下载链接】react-doctorYour agent writes bad React. This catches it项目地址: https://gitcode.com/GitHub_Trending/re/react-doctor

React Doctor 是一个以「AI Agent 协作」为第一设计目标的 monorepo:仓库根目录的 CLAUDE.md 只有寥寥 4 行,但它通过@AGENTS.md引入指令把全部贡献规范收敛到 AGENTS.md(约 437 行)中。读完本篇,你将掌握这套 Agent 指南的完整骨架——通用代码规约、truffler 符号去重流程、五包结构布局、Effect v4 的导入/错误/服务/Layer 惯用法、双后端遥测架构,以及 Changesets 发布授权与 GitHub Action 独立版本化的操作细节。

CLAUDE.md:四行入口与 @AGENTS.md 引入

CLAUDE.md 的全文只有两条信息:一句指向贡献指南的自然语言说明(约定、包布局、规则管线、发布步骤见 AGENTS.md),以及一行@AGENTS.md引入指令。后者是 Claude Code 的文件引入语法——Agent 启动时会自动把 AGENTS.md 的全文载入上下文。

这种「入口文件极薄、规范文件单一」的结构是有意的:无论贡献者使用 Claude Code 还是其他读取 AGENTS.md 的 Agent 工具,规范都只维护一份。下面的内容均以该指南为骨架,并以仓库源码作为实现层面的佐证。

通用代码规约(General Rules)

AGENTS.md 的第一节是一组 MUST 级硬规约,核心要点如下:

  • 包管理命令:使用@antfu/nini安装、nr <script>运行、nun卸载;
  • 类型风格:TypeScript interface 优先于 type 别名;所有类型放在全局作用域;
  • 函数风格:箭头函数优先于 function 声明;参数超过一个时改为单个对象参数(如Files.readLines({ filePath, rootDirectory }));
  • 命名:文件用 kebab-case;变量名必须描述行为且拒绝一两个字母的缩写(例如.map()里用innerX而不是x,用didPositionChange而不是moved),并要求「频繁地重新评估、重命名变量使其更精确」;
  • 注释:默认不写注释;确属 hack 的代码(如setTimeout或易误解逻辑)必须以// HACK: 原因开头;
  • 魔法数字:一律收进constants.ts,使用带单位后缀的SCREAMING_SNAKE_CASE(如_MS_PX);
  • 工具函数:小而专注的 utility 放utils/,一个 utility 一个文件;
  • 禁用:不必要的as类型断言、!!取布尔值(用Boolean)、重复代码。

其中两条与「公共面(public surface)」和「去重」直接相关:

  1. product-thinking 关卡:在新增或修改 CLI 标志/命令、评分、配置、JSON 报告、包 API、GitHub Action、网站或终端输出之前,必须先跑.agents/skills/product-thinking/定义的流程——命名用户的 job、先复用再新增、接入一个遥测指标、补齐兼容产物、设定 kill metric。Lint 规则则走独立的规则管线。对应技能文件见 .agents/skills/product-thinking/SKILL.md;
  2. truffler 符号搜索:新增 utility/helper/type/常量/规则之前和任务结束之后,都要用 truffler 搜一遍现有符号,捕获重复与死代码(见下节)。

符号搜索与去重:truffler

@rayhanadev/truffler(dev dependency)是基于oxc-parser的模糊 JS/TS 符号搜索工具,根目录 package.json 中固定了^0.4.2版本。指南给出的短流程是两段式:

  • 规划/定范围时:从行为推导若干查询词(提议名 + 领域名词 + 动词),先搜最窄的根,读完 top matches 再动手——这是「不重复造轮子」加utils/一文件一工具约定的落地手段;
  • 完成任务后:重新搜索你新增的符号,确认没有重复已有 helper,并删除被你改动取代的代码。

指南给出的参考命令与使用方式:

bunx @rayhanadev/truffler "<query>" packages --kind function,method,interface,type,constant --limit 20

必须用bunx @rayhanadev/truffler执行:已发布的bin是 Bun 直接运行的 TypeScript 入口,且会复用锁定的 dev dependency 而非重新下载。<query>与根路径(如packages/core/src)收窄可提高精度,无匹配时才放宽。完整工作流见 .agents/skills/find-similar-functions/SKILL.md。

包布局:一个私有诊断引擎加四个发布物

AGENTS.md 用一张目录树定义了 monorepo 的职责边界(packages/下):

packages/ core/ PRIVATE 诊断引擎 src/ types/ 私有共享跨包 TS 类型(DiagnoseOptions、 ProjectInfo、JsonReport 等)——无运行时代码 project-info/ 项目发现(discoverProject、findMonorepoRoot、 框架检测、在 Effect 运行时接管之前抛出的 窄化 Error 子类) errors.ts TaggedErrorClass 叶子 + ReactDoctorError 联合 schemas.ts Diagnostic / Severity / JsonReport / buildDiagnosticIdentity(同时以 @react-doctor/core/schemas 子路径导出) refs.ts 环境配置的 Context.Reference run-inspect.ts 流式编排器(核心心脏) build-diagnostic-pipeline 逐元素过滤管线(单一事实来源) services/ Context.Service 实现(Files、Git、Project、 Config、Linter、Maintainability、Score、 Reporter、Progress、NodeResolver、 StagedFiles、SupplyChain)+ LintPartialFailures ... 其余 lint / score / suppression 引擎 api/ PRIVATE 编程式 diagnose()(Effect.runPromise 外壳) react-doctor/ PUBLISHED CLI + 公开 inspect() + bin oxlint-plugin-react-doctor/ PUBLISHED 100+ 规则,持有权威的 react-native-dependency-names.ts (从 core 再导出以打破 规则包 ↔ core 循环) eslint-plugin-react-doctor/ PUBLISHED oxlint 插件的 ESLint 镜像

这套布局可以推断出两条设计约束:其一,core/完全私有,规则包与 CLI 之间通过它中转,规则包与 core 之间的循环依赖靠「react-native-dependency-names.ts由规则包持有、core 再导出」来打破;其二,api/同样私有,编程式入口只是一个Effect.runPromise外壳,这决定了后文遥测为何「对@react-doctor/api天然静默」——凭据只存在于 CLI 侧。

Effect v4 惯用法:导入、错误与分发

指南声明运行时基于effect@4.0.0-beta.102(与根 package.json 中dependencies.effect的固定版本一致),并以tmp/effect/.patterns/effect.md(克隆的参考,gitignored)和姊妹应用react-doctor-evals作为规范样例来源。

导入:一个模块一行

// 必须 import * as Schema from "effect/Schema"; import * as Effect from "effect/Effect"; import * as Cause from "effect/Cause"; // 禁止:伞式导入会膨胀类型解析图 import { Schema, Effect } from "effect";

错误:TaggedErrorClass 联合 + 结构化分发

  • 每个可失败服务都以ReactDoctorErrorreason: Schema.Union([...]))失败;
  • 每个叶子是Schema.TaggedErrorClass<Self>()("Tag", { fields }),且用get message()getter(而非message =)返回人类可读字符串;
  • 不透明原因在 message 里用Cause.pretty(Cause.fail(this.cause))渲染;
  • 渲染器只按error.reason._tag分发,绝不error.message.includes(...)
  • formatReactDoctorError/isReactDoctorError/isSplittableReactDoctorError统一住在 packages/core/src/errors.ts,禁止另开 error 形状 helper。

仓库源码印证了这一约定:packages/core/src/errors.ts 中密集出现TaggedErrorClassReactDoctorError(全文 27 处命中),packages/core/src/services/git.ts 与 packages/core/src/project-info/errors.ts 也各自构建标签错误叶子。

v4 的分发惯用法同样被写入硬性规则:

  • Effect.catchReasons(errorTag, cases, orElse?)是 v4 规范的分发方式,每个 case 捕获一个_tagorElse兜底;禁止在catch块里手写if (cause.reason instanceof X)阶梯。规范形状见inspect.ts → restoreLegacyThrowapi/diagnose.ts。源码中services/git.tserrors.ts确在使用catchReasons
  • Effect.catchTag(tag, handler)用于单个标签错误,例如 packages/core/src/services/git.ts 中用Effect.catchTag("PlatformError", ...)ChildProcess平台错误折叠成ReactDoctorError
  • Effect.die(error)把恢复值提升为runPromise原样重抛的缺陷,用于编程契约仍要求旧Error类的场景;
  • 禁止在Effect.gentry/catch(v4 硬规则):同步抛出包进Effect.try({ try, catch }),再用Effect.orElseSucceed/Effect.catch恢复。

生成器卫生方面:终端效果(Effect.fail/Effect.interrupt/Effect.die)必须写return yield*以便 TypeScript 识别不可达代码;v4 改为了Effect.gen({ self: this }, function* () { ... })的 this 绑定形式(普通Effect.gen不变);Effect.fnUntraced用于热路径——指南同时明确当前代码库尚未用它,因为 Git 调用与 inspect 管线是每次扫描一次而非热循环。

Services、Layer 与 Schema 约定

  • 服务定义Context.Service<Self, Interface>()("react-doctor/Name", { make: ... }),标识符用短前缀;方法体热路径用Effect.fnUntraced,一行式用Effect.sync,测试层与编排用Effect.gen
  • 可观测方法:非平凡方法用Effect.fn("Service.method"),使其作为命名 span 出现在 OTel trace 中——无 tracer 层时生产代价为零,接了Otlp.layerJson(...)则每次服务调用一个 span;
  • Layer 命名词汇表layerNode(Node 生产实现)、layerOf(value)(预供值测试层)、layerInMemory(Map)(内存文件树服务)、layerCapture(把调用记录进Ref的捕获测试层,如ReporterCapture)、layerNoop(void/丢弃语义,Reporter/Progress;分析器 Linter/Maintainability 用layerOf([]))、以及layerOxlintlayerHttp等实现名;
  • Schema 分工:线上记录(Diagnostic、JsonReport)用Schema.Class<Self>("Name")({ fields }),字面量联合用Schema.LiteralsSchema.NullOr/Schema.optional对应| null?,品牌原语用Schema.brand("X").pipe()入参类型(InspectInput、LintInput)用 interface以避免热路径上的运行期 encode/decode 开销;
  • 环境配置:环境变量读取与缓存路径一律走Context.Reference<T>("react-doctor/X", { defaultValue }),测试通过Layer.succeed(MyRef, ...)覆盖;密钥类配置优先Config.redacted("ENV_NAME")以便自动脱敏。

packages/core/src/refs.ts 是该约定的直接样例:OxlintSpawnTimeoutMsContext.Reference<number>("react-doctor/OxlintSpawnTimeoutMs", ...)定义,defaultValueREACT_DOCTOR_OXLINT_SPAWN_TIMEOUT_MS环境变量读取、缺省回落到constants.jsOXLINT_SPAWN_TIMEOUT_MS——文件头注释还解释了为何在启动时读取(便于评测沙箱在不重编译 react-doctor 的情况下调高预算)。

遥测架构:Axiom 与 Sentry 双后端

指南的 Observability 一节是全篇最长也最工程化的部分,核心是故意拆分的两个后端

  • Axiom收 trace 与 metrics:每次运行的宽事件(wide event)、每个Effect.fn("Service.method")span、全部计数器/分布。选择理由是 Axiom 按摄取量计费、无活跃序列上限,适合高基数宽事件;
  • Sentry只收 crash:source-map 符号化(scripts/sentry-sourcemaps.mjs)、issue 分组、可引用的事件 id。

由于 Effect 只有一个Tracer引用,两者对 span 互斥:CLI 固定tracesSampleRate: 0,Sentry 永不记录 span。关键机制包括:

  1. 单一开关:packages/react-doctor/src/cli/utils/is-telemetry-enabled.ts 是所有后端的唯一闸门——--no-score--no-telemetryREACT_DOCTOR_NO_TELEMETRY或测试运行全部关闭。源码显示它直接读process.argv(而非 Commander 解析结果),因为决策发生在 CLI 解析参数之前;它还会在VITEST/NODE_ENV=test下静默,防止 e2e 套件把构建产物 CLI 的子进程扫描发回生产遥测;
  2. 每进程只构建一次 telemetry 层:packages/react-doctor/src/cli/utils/telemetry-runtime.ts 把层构进长生命周期 scope 并共享Context。原因是 Effect 的 delta-temporality 状态挂在 metrics exporter 实例上,第二个 exporter 会以「无历史快照」重报每个计数器的全量值——静默地把所有指标翻倍。core/tests/telemetry-payload.test.ts中有回归测试钉住这个双重计数行为;
  3. 传输层core/src/observability.ts拥有三个 layer。因为 Axiom 用不同 header 把 trace/metrics 路由到不同数据集(X-Axiom-Datasetvsx-axiom-metrics-dataset),而Otlp.layer只支持单一 headers 对象,layerAxiomTraceslayerAxiomMetrics被手工组合;序列化用protobuf(Axiom 的/v1/metrics只收application/x-protobuf),OtlpSerialization.layerProtobuf随 Effect 自带、不加依赖。用户配置的REACT_DOCTOR_OTLP_*端点优先于第一方 Axiom,否则 Axiom,再否则Layer.empty
  4. 匿名化:OTLP 没有 Sentry 式beforeSend钩子,故用core/src/utils/make-scrubbing-tracer.ts包一层 tracer,让每个 span 名、属性值、事件载荷在出港前过anonymizeText(家目录/用户名 →~,再脱敏密钥与邮箱);metrics 不经过 tracer,则在 packages/react-doctor/src/cli/utils 的record-metric.ts发射点脱敏;调用点仍应在源头脱敏(如run-inspect.tsinspect.directoryscrubSensitivePaths),tracer 只是兜底;
  5. 凭据:Axiom ingest token 直接内嵌在 packages/react-doctor/src/cli/utils/constants.ts(AXIOM_INGEST_TOKEN,源码中可见于该文件 L212 附近),与公开的 Sentry DSN 同理打包进 tarball——但它是真凭据,故被铸为「ingest-only、限定两个数据集」,轮换需发版,靠 Axiom 侧的摄取量异常监控发现。REACT_DOCTOR_AXIOM_TOKEN/_DOMAIN/_DATASET前缀覆盖变量专为本地测试保留(裸AXIOM_*是 Axiom 自己的变量名,读它们会劫持其他工具的遥测)。凭据只留在 CLI 侧,是@react-doctor/api「构造即静默」的原因;
  6. span 规范形状:多步操作的顶层入口用Effect.withSpan("name", { attributes })包裹,属性键用点号命名空间(inspect.directoryinspect.isCi)。packages/core/src/run-inspect.ts 中runInspect即以Effect.withSpan("runInspect", {...})作为父 span,每个Service.method是它的子 span;
  7. runId:每次 CLI 运行(进程)铸造一个随机runId,随 Sentryruncontext 与宽事件携带,但只在 crash 报告里作 tag(关联 Axiom trace),永不作 metric 属性——按运行唯一的值会炸掉计数器基数。指南还明确禁止向任一端点添加明文或哈希过的 repo id。

宽事件(wide event)的建模哲学也值得注意:每次扫描一个高维宽事件,而不是一堆窄计数器。根 span 由build-run-event.ts用完整结局富化,成功与失败路径都会落事件(outcome.statusclean/ok/blocked/erroroutcome.exitCodeoutcome.errorTag),属性按概念命名空间树状组织(scan.*outcome.*diag.*score.*lint.*timing.*action.*),数值结局保持 number 以便取p75(score.value)之类的分位数查询,nulltoSpanAttributes丢弃而非变"null"。新增运行级维度进build-run-context.tsbuild-sentry-scope.ts,新增单扫描结局维度进宽事件——不要加新计数器。

控制台与日志

  • 必须import * as Console from "effect/Console",在渲染器、服务与任何 Effect 类型代码里用yield* Console.log(...)。Effect 的ConsoleContext.Reference,默认 sink 就是globalThis.console,生产路径等价于裸console.log,但可被测试/静默模式替换;
  • 禁止另造Logger/LoggerWriter抽象——历史自定义 Logger 服务在渲染管线 Effect 化时已移除,唯一剩余桥梁是cli/utils/cli-logger.tsEffect.runSync(Console.X)的薄同步包装);
  • 静默模式走Effect.provideService(Console.Console, silentConsole)(渲染管线)或installSilentConsole()(JSON 模式猴子补丁全局 console)——调用点没有任何if (silent) return

测试布局与提交前检查

测试与源码同包、置于各包tests/目录:

  • packages/core/tests/ — 服务测试 + run-inspect 编排测试;
  • packages/api/tests/ — api 外壳测试;
  • packages/react-doctor/tests/ — CLI + 端到端 fixture 测试。

测试框架是vite-plus/test(vitest 封装)。指南要求提交前必跑(与根 package.json 的 scripts 一致):

pnpm test # 所有包 pnpm lint pnpm typecheck pnpm format # 仅校验用 format:check pnpm smoke:json-report # 用 schema 校验已构建 CLI 的 JSON 输出

补充环境约束:根 package.json 的engines要求node: ^20.19.0 || >=22.13.0

发布授权:Agent 必须在首个发布动作前停下

「Release authorization」一节是写给 Agent 的硬边界,四条 MUST:

  • 不鼓励 minor/major Changesets:用户没明确要该级别就不要加;patch Changesets 合适时可自行加;
  • 绝不在「合并前一刻」未获得用户对该 PR 与版本的新鲜显式确认的情况下合并任何 Changesets 发布 PR(含changeset-release/*分支);
  • 「合并、发布、盯绿」的笼统指令不构成发布 PR 的合并授权——把触发发布的 PR 合并视为发布行为本身;
  • 绝不发布包、推送/移动发布 tag、或触发/批准/重跑/合并发布工作流,除非用户针对确切版本与包做了新鲜显式确认。

Agent 可以准备、验证、盯发布候选,但必须停在首个发布动作之前,并报告等待用户批准的 PR、版本、包、tag 与工作流清单。

GitHub Action 版本化:独立于 npm 包的双 tag 命名空间

仓库根的 action.yml 是 composite GitHub Action,与 npm 包独立版本化。Action 的边界 =action.yml+ 它 shell 出去的几个脚本(scripts/ensure-json-report.mjsscripts/normalize-changed-files.mjsscripts/render-github-action-comment.mjsscripts/resolve-package-spec.mjs),改动其中任何文件都算一次 action 发布,且该清单必须与 scripts/recommend-action-version-bump.mjs 中的ACTION_RELEASE_FILES(发布守卫)保持同步——该文件确实以const ACTION_RELEASE_FILES = [...]维护这份清单。

规则要点:

  • 两套 tag 命名空间并行,绝不混用:npm 包 tag 无v前缀(react-doctor@X.Y.Z等,由 Changesets 在 CI 创建,见 .github/workflows/publish.yml);Action tag 是带v前缀的 semvervX.Y.Z加浮动主版本vN(GitHub Actions 惯例,v前缀使其与包 tag 区分)。v0.x是重建前旧版,PR 报告重建是v1.0.0,当前为v2.x线;
  • 每个触碰 action 文件的 commit 必须打 tagfeat(action)→ minor;其余(fix/refactor/chore/revert或对action.yml的纯文档编辑)→ patch;inputs/outputs 或运行时契约的破坏性变更 → major;
  • 打完vX.Y.Z后,浮动主版本vN必须移到同一 commit,保证uses: .../react-doctor@vN始终解析到最新兼容版本;
  • tag 是 GPG 签名附注 tag(tag.gpgsign=true),裸git tag vX会要求 message 并在脚本中失败,必须显式带 message:
# 在改动了 action 的 commit 上打新发布 tag git tag -a v2.2.3 <commit> -m "react-doctor action v2.2.3" # 移动浮动主版本(仅强制更新 vN 指针) git tag -fa v2 <commit> -m "react-doctor action v2 (floating major -> v2.2.3)" git push origin v2.2.3 git push --force origin v2 # force 仅作用于移动中的主版本 tag
  • 绝不让消费方在文档/示例里引用@main——@mainpull-requests: write权限运行任意 HEAD,是供应链风险;加固 CI 推荐完整 commit-SHA 加尾注版本(uses: .../react-doctor@<sha> # v2.2.2),便捷场景用@vN

参考阅读

指南末尾给出两条延伸阅读指针:tmp/effect/.patterns/effect.md(Effect v4 惯用法的规范样例,克隆件、gitignored)与~/Developer/react-doctor-evals/src/(本代码库运行时模式所建模的姊妹应用,包括 Schemas.ts、Runner.ts、Worker.ts、errors.ts 的形状)。

小结

CLAUDE.md 的 4 行与 AGENTS.md 的 437 行共同构成了一份「Agent 可读、可执行」的贡献契约:它不只描述风格偏好,而是把工程决策的理由(delta-temporality 为何禁止双 exporter、宽事件为何优于窄计数器、@main为何是供应链风险)与硬性边界(发布前的强制人工确认)一起写进上下文,让每个进入该仓库的 Agent 以同一套 Effect v4 惯用法、同一套符号去重流程和同一套发布纪律工作。对维护多 Agent 协作仓库的团队而言,这份指南本身就是一个可借鉴的样板。

【免费下载链接】react-doctorYour agent writes bad React. This catches it项目地址: https://gitcode.com/GitHub_Trending/re/react-doctor

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询