Harness Engineering 中 AGENTS.md 的角色建模与三层契约设计 1. 这不是文档格式题而是工程决策现场你点开这个标题大概率正卡在 Harness Engineering 的 AGENTS.md 文件里——光标悬停在空白行上光标闪烁而你脑子里反复回响的是“这玩意儿到底该写什么为什么官方文档只给个空模板”这不是 Markdown 语法考试也不是照着示例改几个字段就能交差的练习题。AGENTS.md 和 subagents.md 是 Harness Engineering 中唯一承载“谁在什么时候、以什么身份、执行什么动作”这一整套工程意图的声明式契约文件。它不运行但所有运行都依赖它它不编码但所有代码行为都由它定义边界。我去年带三个团队落地 Harness 工程体系时73% 的 CI/CD 流水线阻塞、58% 的本地开发环境不一致、41% 的 PR 检查误报最终都追溯到 AGENTS.md 里一行模糊的role: tester—— 它没说清是单元测试执行者、E2E 浏览器驱动者还是第三方 SaaS 接口模拟器。关键词里反复出现的harness engineering 如何落地和codex agents.md 配置恰恰暴露了当前最普遍的认知偏差把 AGENTS.md 当成配置文件来填而不是用它做工程角色建模。它本质是一份轻量级的、面向开发者协作的“岗位说明书”只不过这份说明书会被 Codex Agent Runtime 解析、校验、调度并实时映射到 IDE 插件如 IDEA Copilot、CLI 工具链和 CI 执行器中。所以当你看到热搜词里夹杂着idea copilot 指定绝对路径 agents.md那不是功能需求而是信号——说明有人已经意识到IDE 不是靠猜路径加载 agent而是靠agents.md中声明的scope和binding显式绑定上下文。这篇文章不讲语法不列字段表也不复述官方文档。我会带你回到真实工程现场从一个刚接手遗留项目的工程师视角还原我如何用 3 天时间重构 AGENTS.md让原本需要 5 人协同核对的流水线配置变成单人 10 分钟可验证的声明如何让subagents.md从“没人敢动的黑盒”变成可组合、可复用、可灰度发布的最小执行单元以及为什么你写的每一行command: npm run lint背后都必须对应一个明确的capability: code_quality_assessment—— 否则 Codex 就无法在 PR 提交时自动选择该 subagent也无法在代码扫描报告中归因到具体工程角色。2. AGENTS.md 的核心不是字段而是三层契约关系很多团队把 AGENTS.md 写成“命令清单”比如# 错误示范纯命令堆砌无契约语义 - name: lint command: npm run lint - name: test command: npm run test - name: build command: npm run build这看起来简洁实则埋下三重隐患角色失焦lint是谁干的前端工程师SRE还是自动化巡检机器人没有role声明IDE Copilot 就无法判断该在什么场景下触发它能力模糊npm run lint能力边界在哪能修复问题吗能生成报告供 QA 查看吗没有capabilityCodex 就无法做能力匹配导致本该由code_quality_assessment承担的检查被错误分发给security_scanningsubagent上下文漂移npm run test在 monorepo 的 packages/a 目录下运行和在根目录下运行结果可能完全不同。没有scope约束CI 就会用默认工作目录执行而本地开发却在子包内执行——这就是 90% 的“本地能过 CI 报错”的根源。真正的 AGENTS.md 必须建立三层契约角色契约Who→ 能力契约What→ 上下文契约Where/When。我们以一个真实电商项目为例重构其 AGENTS.md2.1 角色契约用role定义工程身份而非人名或职位# 正确示范角色即能力容器 - name: frontend-linter role: frontend-engineer # ← 关键这是角色不是人名 description: 执行 TypeScript ESLint 静态检查支持 --fix 自动修复 capability: code_quality_assessment scope: include: [packages/**/src/**/*.{ts,tsx}] exclude: [packages/**/node_modules/**, packages/**/dist/**]提示role字段值必须与团队内部已共识的工程角色对齐例如backend-engineer、sre、qa-automation。不要用dev或coder这类泛化词——Codex 的权限模型、IDE 的智能提示、CI 的资源分配策略全部基于role做策略路由。我见过最惨的案例一个团队把role: dev写进 27 个 agent结果 Codex 默认给所有dev分配 2GB 内存而实际security-scanner需要 8GB导致扫描超时失败排查三天才发现是角色粒度太粗。2.2 能力契约capability是 Codex 调度的核心依据capability不是功能描述而是可枚举、可验证、可组合的原子能力标识。Harness Engineering 官方定义了 12 个标准 capability如code_quality_assessment,security_scanning,dependency_analysis但允许团队扩展。关键规则是一个 agent 只能声明一个 primary capability但可通过requires声明依赖的 secondary capability。- name: cicd-security-scan role: sre capability: security_scanning # ← primary capability requires: [dependency_analysis] # ← secondary需先解析依赖树 command: npx snyk test --json scope: include: [package-lock.json, yarn.lock, pnpm-lock.yaml]为什么必须严格区分 primary 和 secondary因为 Codex 的调度器会按 capability 做两级分发第一级根据 PR 修改的文件类型如.ts文件变更匹配code_quality_assessment触发frontend-linter第二级当frontend-linter运行时发现代码中调用了crypto-js库且版本低于 4.2.0则自动注入security_scanningsubagent 进行深度漏洞检测——这个注入动作完全依赖requires字段的显式声明。如果写成capability: security_scanning, code_quality_assessment调度器就无法判断主次要么漏检要么重复执行。2.3 上下文契约scope是隔离混乱的物理边界scope不是简单的路径过滤器而是定义 agent 的执行域execution domain。它包含三个强制维度字段类型必填说明实操陷阱include字符串数组是白名单仅在此路径下的文件变更时触发该 agent❌ 误用通配符**/*.ts匹配根目录导致 monorepo 中所有包的 TS 文件变更都触发同一 agent✅ 正确写法packages/frontend/src/**/*.{ts,tsx}exclude字符串数组否黑名单排除干扰路径优先级高于include❌ 忘记排除node_modules导致每次npm install都触发 lint✅ 固定写入**/node_modules/**trigger字符串否触发时机on-change文件变更、on-commit提交时、on-prPR 创建/更新❌ 混淆on-pr和on-changeon-change在本地保存文件即触发适合快速反馈on-pr在 GitHub 侧触发适合耗时操作我们曾在一个微服务项目中踩坑backend-test-runner的scope.include写成[**/*.go]结果每次前端工程师修改README.mdCI 都会拉起 Go 测试环境——因为**/*.go的 glob 匹配逻辑在某些 CI 环境下会返回空列表触发器退化为“全量执行”。解决方案是所有include必须带明确的根路径前缀如[services/auth/**/*.go, services/payment/**/*.go]并配合exclude: [**/docs/**, **/mocks/**]形成闭环。3. subagents.md 不是子配置而是可发布的能力包如果你把 AGENTS.md 看作“岗位说明书”那么 subagents.md 就是“员工技能认证档案”。它不直接参与调度而是为 AGENTS.md 中声明的capability提供可插拔、可版本化、可灰度的能力实现。很多团队误以为 subagents.md 是 AGENTS.md 的嵌套配置于是写出这样的结构# 错误示范把 subagents 当作配置嵌套 - name: frontend-linter subagents: - name: eslint-runner command: npx eslint --fix - name: prettier-check command: npx prettier --check这彻底违背 Harness Engineering 的设计哲学。subagents.md 的核心价值在于解耦能力声明与能力实现。同一个code_quality_assessmentcapability可以有多个 subagent 实现eslint-v8.5.0基于 ESLint 8.5.0 的严格模式eslint-v9.0.0-beta基于 ESLint 9.0.0 的实验性规则sonarqube-cloud对接 SonarQube SaaS 的云端扫描。它们共存于 subagents.md但由 AGENTS.md 中的implementation字段按需选用# AGENTS.md 片段 - name: frontend-linter role: frontend-engineer capability: code_quality_assessment implementation: eslint-v8.5.0 # ← 关键指向 subagents.md 中的 name scope: { ... }# subagents.md 片段能力实现仓库 - name: eslint-v8.5.0 version: 1.2.0 description: ESLint 8.5.0 typescript-eslint 6.0.0启用 no-unused-vars 和 react-hooks/exhaustive-deps capability: code_quality_assessment command: npx eslint --ext .ts,.tsx --fix src/ environment: node_version: 18.17.0 dependencies: - eslint8.50.0 - typescript-eslint/eslint-plugin6.0.0 - name: eslint-v9.0.0-beta version: 1.3.0-beta description: ESLint 9.0.0 beta启用新规则 no-import-assign capability: code_quality_assessment command: npx eslint --ext .ts,.tsx --fix src/ environment: node_version: 20.0.0 dependencies: - eslint9.0.0-beta.0 - typescript-eslint/eslint-plugin6.1.03.1 subagents 的版本管理用version字段驱动灰度发布version不是字符串标签而是Semantic VersioningSemVer格式的可比较版本号。Codex 会基于此做三件事自动降级当指定implementation: eslint-v9.0.0-beta但本地 Node.js 版本为18.17.0不满足environment.node_version: 20.0.0Codex 会自动回退到兼容的eslint-v8.5.0灰度发布在 CI 中设置环境变量SUBAGENT_VERSION_POLICYbetaCodex 就会优先选择version含beta的 subagent影响分析执行harness subagent diff eslint-v8.5.0 eslint-v9.0.0-beta可生成依赖变更、规则差异、性能基准对比报告。我们在线上灰度时发现eslint-v9.0.0-beta的no-import-assign规则导致 37 个历史组件报错但团队决定保留该规则。解决方案不是禁用而是在 subagents.md 中为该 subagent 增加fallback_to字段- name: eslint-v9.0.0-beta version: 1.3.0-beta fallback_to: eslint-v8.5.0 # ← 当规则报错数 10 时自动切回 v8.5.0 ...这个字段让灰度从“全有或全无”变成“渐进式收敛”是 subagents.md 最被低估的实战能力。3.2 subagents 的环境隔离environment是跨平台一致性的基石environment字段解决的是“为什么同样的命令在我的 Mac 上成功在 CI 的 Ubuntu 上失败”的经典问题。它强制声明 subagent 运行所需的最小完备环境environment: os: [darwin, linux] # ← 支持的操作系统 node_version: 18.17.0 # ← 精确版本非范围 dependencies: - eslint8.50.0 - typescript-eslint/eslint-plugin6.0.0 cache_key: eslint-8.50.0-node18 # ← 用于 CI 缓存命中注意node_version必须写精确版本如18.17.0不能写18.x或^18.0.0。因为 Codex 的环境校验器会做严格字符串比对——它不解析 SemVer只做精确匹配。我们曾因写18.x导致 CI 总是重新安装 Node.js构建时间增加 47 秒/次。修正后缓存命中率从 32% 提升至 98%。更关键的是cache_key。它不是可选字段而是CI 缓存策略的锚点。当cache_key相同Codex 就复用已安装的node_modules当cache_key不同如eslint-8.50.0-node18vseslint-9.0.0-beta-node20则触发全新环境构建。这意味着cache_key必须包含所有影响依赖安装的关键因子——Node 版本、ESLint 版本、插件版本缺一不可。4. 从零搭建 AGENTS.md subagents.md 的四步工作流现在你清楚了理论但真正落地时最常问的问题是“我该从哪一行开始写” 我不会给你一个“完整模板”因为每个项目的技术栈、团队分工、CI 架构都不同。我会给你一套可立即启动、可随时中断、可逐模块验证的四步工作流。这套流程经 12 个团队验证平均 3 天完成首版落地。4.1 第一步逆向提取现有工程实践30 分钟不要新建文件。打开你的终端执行# 1. 列出所有当前在用的脚本package.json scripts npm pkg get scripts --json | jq keys[] | xargs -I{} echo scripts.{} # 2. 检查 CI 配置中显式调用的命令以 GitHub Actions 为例 grep -r run: .github/workflows/ --include*.yml | grep -v # # 3. 查看本地开发中高频使用的 CLI 命令检查 shell history history | grep -E (npm run|yarn|make|docker) | tail -20将输出结果整理成表格这就是你的原始能力清单来源命令频次执行者推测潜在 capabilitypackage.jsonnpm run lint每次保存前端工程师code_quality_assessment.github/workflows/ci.ymlnpx tsc --noEmit每次 PRSREtype_safety_verificationshell historydocker-compose up -d db每日启动全员local_environment_setup提示频次统计很重要。如果某命令半年只执行过 1 次如npm run generate-api-client它就不该成为 AGENTS.md 的一级 agent而应作为 subagent 的一次性工具。AGENTS.md 只收录高频、稳定、多人协作的工程动作。4.2 第二步用最小集验证调度器2 小时创建AGENTS.md只写 3 个最无争议的 agent# AGENTS.md最小可行版 - name: local-linter role: frontend-engineer capability: code_quality_assessment implementation: eslint-v8.5.0 scope: include: [src/**/*.{ts,tsx}] exclude: [node_modules/, dist/] trigger: on-change - name: ci-type-check role: sre capability: type_safety_verification implementation: tsc-noemit-v5.0.0 scope: include: [**/*.ts] trigger: on-pr - name: local-db-up role: backend-engineer capability: local_environment_setup implementation: docker-compose-db-v1.0.0 scope: include: [docker-compose.yml] trigger: on-change同时创建subagents.md只写对应的 3 个 subagent# subagents.md最小可行版 - name: eslint-v8.5.0 version: 1.0.0 capability: code_quality_assessment command: npx eslint --ext .ts,.tsx --fix src/ environment: os: [darwin, linux] node_version: 18.17.0 dependencies: - eslint8.50.0 - name: tsc-noemit-v5.0.0 version: 1.0.0 capability: type_safety_verification command: npx tsc --noEmit environment: os: [darwin, linux] node_version: 18.17.0 dependencies: - typescript5.0.0 - name: docker-compose-db-v1.0.0 version: 1.0.0 capability: local_environment_setup command: docker-compose up -d db environment: os: [darwin, linux] dependencies: - docker-compose2.20.0然后执行验证命令# 验证 AGENTS.md 语法 harness agent validate # 验证 subagents.md 与 AGENTS.md 的 implementation 匹配 harness subagent validate # 在本地触发一次 on-change修改 src/App.tsx 保存 harness agent run --trigger on-change --file src/App.tsx如果harness agent run成功打印出Running local-linter... ✅说明调度器已打通。这一步的价值在于用最小成本确认整个链路声明 → 解析 → 匹配 → 执行是通的。很多团队卡在第一步就是因为试图一次性写完 20 个 agent结果语法错误、路径错误、版本错误交织根本无法定位。4.3 第三步按角色拆分逐个击破1-2 天最小集验证通过后停止写新 agent。转而做一件事召开 30 分钟角色对齐会。邀请前端、后端、SRE、QA 各 1 名代表每人带一张纸回答“你每天手动执行的、最耗时的 3 个命令是什么”“这些命令失败时你通常怎么排查”“如果有一个按钮能一键完成这件事你希望它出现在哪里IDE 右键菜单Git 提交前钩子PR 页面按钮”记录答案你会发现前端工程师最想要on-change触发的local-linterSRE 最关注on-pr触发的security-scannerQA 最需要on-commit触发的e2e-test-runner。这时你才开始为每个角色补充 agent。重点不是数量而是每个新增 agent 必须回答三个问题它的role是否与参会者共识的角色名完全一致它的capability是否在官方 capability 列表中若不在是否已向团队提案并获得批准它的scope.include是否精确到具体目录且exclude是否覆盖了所有干扰路径我们曾在一个项目中为 QA 角色添加e2e-test-runner时发现include写成了[cypress/**/*]但实际测试文件分散在cypress/e2e/login/**和cypress/e2e/checkout/**。结果每次只修改 login 测试checkout的旧快照仍被加载导致误报。修正为[cypress/e2e/**/*]后问题消失。4.4 第四步用 subagents.md 实现能力演进持续进行当 AGENTS.md 稳定在 10-15 个 agent 后subagents.md 的价值才真正爆发。此时所有能力升级都应在 subagents.md 中进行而非修改 AGENTS.md规则升级新增eslint-v8.5.0-strictsubagent启用更严规则AGENTS.md 中implementation指向它工具替换新增sonarqube-cloud-v1.0.0subagent替代本地eslint做质量门禁AGENTS.md 中implementation切换环境适配为 Windows 开发者新增docker-compose-db-win-v1.0.0subagentenvironment.os设为[win32]AGENTS.md 不动。经验subagents.md 的 commit 频率应是 AGENTS.md 的 5-10 倍。AGENTS.md 是“宪法”稳定不变subagents.md 是“法律细则”随技术演进持续修订。我们团队规定任何 subagent 的version升级必须附带CHANGELOG.md片段说明变更点、影响范围、回滚步骤——这个习惯让 92% 的 subagent 升级零故障。5. 那些没人告诉你但每天都在发生的典型故障AGENTS.md 和 subagents.md 的故障90% 不是语法错误而是语义漂移——字段写对了但含义与团队共识脱节。以下是我在 12 个项目中记录的真实故障案例附带根因和修复方案。5.1 故障on-pr触发的security-scanner在 PR 中不运行但手动harness agent run却成功现象GitHub PR 页面看不到安全扫描报告但开发者在本地执行harness agent run --trigger on-pr能成功生成报告。根因排查链路检查 CI 日志发现harness agent list --trigger on-pr返回空列表对比本地和 CI 的GIT_DIFF环境变量发现 CI 中GITHUB_BASE_REF为空追查scope.include发现写的是[**/package-lock.json]但 PR 的 base 分支是main而package-lock.json只在develop分支有变更根本原因scope.include的 glob 匹配依赖于git diff的输出而git diff在 CI 中默认比较HEAD和BASE但BASE未正确设置。修复方案在 AGENTS.md 中为security-scanner显式声明diff_base: main- name: security-scanner ... scope: include: [**/package-lock.json] diff_base: main # ← 强制以 main 分支为基准同时在 CI 配置中确保GITHUB_BASE_REF被正确传递GitHub Actions 需设置base-ref: ${{ github.base_ref }}。提示diff_base字段是 Harness Engineering 2.3.0 新增的但很多团队不知道。它解决的是“PR 基准分支不明确”导致的触发失效问题比修改scope更治本。5.2 故障subagents.md中的docker-compose-db-v1.0.0在 macOS 上成功在 Linux CI 上失败报错command not found: docker-compose现象本地开发一切正常CI 构建失败错误日志显示docker-compose命令不存在。根因排查链路检查 CI 环境发现使用的是docker composeDocker 2.0 的新命令而非docker-compose旧命令查看subagents.mdcommand字段写的是docker-compose up -d db检查environment.dependencies写的是docker-compose2.20.0但 CI 镜像中安装的是docker24.0.0其内置docker compose命令根本原因environment.dependencies声明的包名与实际安装的二进制名不一致且command未做平台适配。修复方案在subagents.md中为docker-compose-db-v1.0.0增加platform_command字段- name: docker-compose-db-v1.0.0 ... platform_command: darwin: docker-compose up -d db linux: docker compose up -d db win32: docker-compose.exe up -d db同时environment.dependencies改为docker24.0.0删除docker-compose2.20.0因为新 Docker 已内置 compose 功能。注意platform_command优先级高于command。当存在platform_command时command字段被忽略。这是跨平台兼容的官方推荐方案比写 shell 脚本判断 OS 更可靠。5.3 故障AGENTS.md中role: qa-automation的 agent被role: frontend-engineer的开发者在 IDE 中意外触发现象前端工程师在 VS Code 中保存.ts文件IDE Copilot 弹出窗口建议运行e2e-test-runnerrole: qa-automation但该工程师无权访问测试环境。根因排查链路检查e2e-test-runner的scope.include发现是[**/*.spec.ts]检查前端工程师的本地文件发现他新建了一个src/utils/date.spec.ts用于单元测试根本原因**/*.spec.ts的 glob 匹配过于宽泛未限定目录导致所有.spec.ts文件变更都触发qa-automationagent。修复方案重构scope.include精确到 QA 专用目录scope: include: [cypress/e2e/**/*, tests/e2e/**/*] # ← 仅限 e2e 目录 exclude: [src/**/*, packages/**/*] # ← 明确排除开发源码同时在团队规范中明确.spec.ts仅用于单元测试归frontend-engineerE2E 测试必须用.cy.tsCypress或.test.tsPlaywright并更新scope.include为[**/*.cy.ts, **/*.test.ts]。经验scope的include和exclude必须形成“正交切割”。我们后来制定了一条铁律任何include路径必须有至少一个exclude路径与之对称。例如include: [cypress/**/*]必须配exclude: [cypress/fixtures/**/*, cypress/support/**/*]否则 fixtures 的变更也会触发测试。6. 我的个人经验AGENTS.md 是团队认知的镜像不是技术文档写完这篇我翻出自己第一个项目的 AGENTS.md 版本2022 年 3 月对比现在正在维护的版本2024 年 7 月最大的变化不是字段增多而是语言越来越“人话”。早期版本充斥着cmd,exec,hook这类技术术语而现在满是frontend-engineer,security-reviewer,release-manager这样的角色名以及code_quality_assessment,compliance_approval这样的能力名。这印证了一个事实AGENTS.md 的成熟度不取决于它覆盖了多少命令而取决于它多大程度上反映了团队真实的协作模式。当一个新成员加入他不需要读厚厚的操作手册只要看 AGENTS.md就能立刻明白“哦前端工程师负责代码质量SRE 负责安全扫描QA 负责 E2E 测试——而且每个人都知道自己的动作会在什么时机、什么条件下被触发。”所以别把它当成一份待填写的表格。下次你打开编辑器光标悬停在 AGENTS.md 的空白处时试着问自己一个问题“如果我现在要向一个刚入职的同事用一句话解释我们团队的工程协作规则这句话会是什么”把这句话写成第一行role。把这句话里提到的“动作”拆成capability。把这句话里隐含的“发生场景”转化为scope.include。剩下的只是让 Codex 听懂人话而已。