这份文档对 org 下全部仓库生效,人和 AI 都照它执行。
三份文件各管一件事,职责不许重叠 —— 这是为了让 agent 一进仓库就知道去哪找什么:
| 文件 | 回答的问题 | 谁维护 |
|---|---|---|
README.md |
这是什么、怎么跑起来 | 每个仓库自己 |
DESIGN.md |
要做成什么样、为什么这么设计 | 每个仓库自己 |
AGENTS.md |
在这个仓库里怎么干活 | 本文档 + 仓库补充 |
AGENTS.md(与本仓库的 AGENTS.md 同源)是给 agent 读的那一份。接法见 §7。
后面几十条规则多半是这四条在不同地方的实例。遇到没被覆盖的新场景,照原则推导比找类比可靠。
- 规则必须可机器校验,否则它只是一个愿望。 每条规则都要说清楚它靠什么落地:平台强制、CI 检查,还是纯自觉。标着「靠自觉」的规则一定会被违反,这不是道德问题,是概率问题 —— 所以要么给它一个检查,要么承认它是建议而不是规则。本文档里每一节末尾都标了「怎么落地」。
- AI 写代码,人负责判断。 分工不是「AI 替人干活」,是「AI 出实现,人决定要不要」。点下 Merge 的人为这次改动负责,不是写它的模型 —— 「AI 写的」不能当免责声明用,否则代码审查会立刻退化成走过场。
- 能被统计的东西必须先被结构化。 进度、成本、事故都要能自动汇总,所以 issue 必须挂里程碑、PR 必须引用 issue、提交必须有署名。只存在于群聊和脑子里的东西,AIOps 看不见 —— 那不是「统计不准」,是「压根不存在」,而两者在报表上长得一模一样。
- 例外要有明确入口,否则规则会被整体绕过。 一条在紧急情况下做不到的规则,会在第一次紧急情况时被丢掉,而且之后不会被捡回来。所以有 §9 逃生门:允许破例,但破例本身要留痕。
顺序是固定的:先设计,再拆里程碑,最后写代码。
- 命名:全小写、连字符分隔(
order-service,不是OrderService/order_service)。 - 可见性:默认私有。要公开需要在 issue 里说明理由并由 owner 确认。
- 默认分支:
dev(见 §2)。 - 建完立刻放三个文件:
README.md、DESIGN.md、AGENTS.md(模板见templates/)。
没有 DESIGN.md 不许开始写代码。 模板:templates/DESIGN.md。
理由不是流程洁癖:AI 是照着仓库里的文档决定做法的(它会 read 你的 DESIGN.md)。没有设计文档时,AI 不会停下来问,它会自己编一个设计然后照着做 —— 而那个设计只存在于某一次会话的上下文里,下一次会话会编一个不一样的。表现出来就是「同一个项目里两个模块风格完全不同」,而没有任何地方记着为什么。
DESIGN.md 至少要有:要解决什么问题、不做什么(边界)、技术选型与理由、数据模型、对外接口、以及功能点清单(下一步直接变成 issue)。
DESIGN.md 里的功能点清单要逐条变成 GitHub issue,并挂到里程碑上。规则见 §6。
这一步由 AI 做、人审。 让 AI 读 DESIGN.md 生成 issue 列表,人过一遍再批量建 —— 人手写二十条 issue 会写到一半开始偷懒合并,而合并过的粒度会让进度统计失真(§6.3)。
第一个 PR 就该有 CI,不要「先写功能,回头补测试」。回头补的时间不存在。
最低要求:构建通过 + lint 零警告 + 测试通过。加上 §3 的署名检查。
怎么落地:靠自觉 + review。建仓库时照
templates/抄一遍就都齐了。
| 分支 | 是什么 | 谁能写 |
|---|---|---|
prod |
生产。这个分支上的每一个 commit 都应该是能上线的 | 只接受来自 dev 的 PR,以及 hotfix PR |
dev |
集成分支,默认分支。所有工作分支合到这里 | 只接受 PR |
feat/* fix/* chore/* docs/* |
工作分支 | 随便 push,这是你的地盘 |
工作分支命名:<类型>/<issue号>-<短横线描述>,例如 feat/128-milestone-rollup。带上 issue 号是为了让 AIOps 把分支、PR、issue、里程碑串成一条线 —— 少了它,这条链在第一环就断了。
- 一律走 PR,不许直接 push 到
dev/prod。 - 合并策略统一 Squash merge。
dev上一个 PR 一个 commit,历史读得下去;工作分支里那些「fix typo」「再试一次」不该进主干。 - 禁止 force push 到
dev/prod,任何情况。 - 回滚用
git revert开 PR,不要 force push、不要改历史。 回滚是一次改动,它也该被记录、被审查 —— 改历史等于让「那次事故到底做了什么」查不出来。
dev → prod 是一次发布,走 PR(我们叫它 promotion PR)。PR 正文要写清这次带了哪些改动、影响哪些里程碑、怎么回滚。
合进 prod 之后打 tag,由 tag 驱动上线 —— 见 §5。
hotfix 例外:可以从 prod 拉 fix/*,PR 直接回 prod,但必须在合并后同一天回合到 dev。忘了这一步的后果是下次发布把 hotfix 覆盖掉 —— 而那个 bug 会带着「我们明明修过」的记忆重新出现。
- 标题用 Conventional Commits:
feat: 支持里程碑跨仓库汇总、fix(auth): 会话过期后不再 500。 - 正文必须写
Closes #<issue号>。跨仓库用owner/repo#123。- 注意:GitHub 的
Closes只在同仓库内自动闭环,跨仓库引用它会显示成链接但合并时不会关掉那条 issue。跨仓库的闭环要靠人或 AIOps 平台回写。
- 注意:GitHub 的
- 一个 PR 对应一个 issue。改着改着顺手修了别的 —— 另开一个 PR。
.github 仓库的默认分支是 main,没有 dev / prod。这是有意的,不是漏改:
- GitHub 只从默认分支读取组织级模板,多一层
dev→prod只会让改一条 issue 模板要开两个 PR。 - 各仓库调用可复用 workflow 时钉的是
@main(见templates/workflows/)。
这里没有「生产」可言 —— 它就是规范本身。
怎么落地:分支写保护与必需检查在仓库设置里配(
prod与dev都要)。PR 标题与Closes可以加 CI 检查。
代码由 AI 改、由 AI 提交。人不直接写代码、不直接提交。
人做的是:提需求、审设计、review PR、点 Merge、以及为结果负责。
「代码」指仓库里的一切:源码、配置、CI yaml、README、注释。没有例外档 —— 一旦开了「文档可以手改」的口子,它会在两周内变成「小改动可以手改」。
顺带一提,最容易破例的地方是 GitHub 网页上直接编辑 README。那太方便了,而它正是要禁的。
每个 commit message 里必须有 agent 署名 trailer。Claude Code 默认就会加:
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
其他工具用通用形式:
Agent: codex/gpt-5
CI 会检查 PR 里每一个非 merge commit 都带了其中之一(templates/workflows/agent-authorship.yml)。
说清楚这个检查的能力边界:它挡不住有意为之的人。 手写一行 Co-Authored-By: 谁都会。它拦的是顺手:网页上改一行、本地图省事直接改 —— 那些不会带 trailer,会当场变红。真正的把关在 review:人看 diff 时会看出来哪段不像 AI 写的。
不是因为 AI 写得比人好。是因为:
- 上下文会沉淀。 AI 每次改动都读
AGENTS.md和DESIGN.md;人凭记忆改,而记忆不进仓库。 - 改动会解释自己。 AI 的 commit message 和 PR 正文成本近乎为零,所以它会认真写;人赶时间时写的是「fix」。
- AIOps 才有东西可统计。 谁改的、花了多少 token、对应哪个 issue —— 人手动改的那部分在报表上是个空洞。
怎么落地:署名检查是 CI(能拦住顺手)。「人不写代码」本身靠自觉 + review。
不要每个项目重新吵一遍。 下面是默认值,偏离默认值要在 DESIGN.md 里写明理由。
| 层 | 默认 | 说明 |
|---|---|---|
| 后端 | Rust | 优先级最高。新服务默认 Rust |
| Web 前端 | React | Vue 也允许,但见下 |
| 移动 / 桌面 App | Flutter | 严格。不要 React Native、不要各写一套原生 |
| 脚本 / 一次性工具 | shell 或 Rust | Python 仅限数据/ML |
| 运行形态 | Kubernetes | 见 §5 |
| 镜像仓库 | 私有 GHCR | 同上 |
单一语言意味着:一套工具链、一套 CI、一套依赖审计、agent 的上下文可以跨项目复用。混着写的代价不在写的时候,在半年后没人记得某个服务是用什么起的。
可以不用 Rust 的情况(写进 DESIGN.md):生态里没有可用的库(典型是 ML / 数据科学 → Python);或者要嵌进一个已有的非 Rust 运行时。「Rust 写这个太慢了」不是理由 —— 现在写代码的是 AI。
默认 React。 「React 或 Vue 都行」听起来是灵活,实际是让每个项目重新决定一次,然后 org 里出现两套互不通用的组件、两套 CI、两份 agent 上下文。
选 Vue 需要一个具体理由(已有可复用的 Vue 资产、或对接的第三方只给 Vue 组件),写进 DESIGN.md。「团队更熟」不算理由 —— 写代码的是 AI。
每个仓库都要有:
- Rust:
rust-toolchain.toml - Node:
package.json的packageManager字段 +.nvmrc,包管理器统一 pnpm - Flutter:
.fvmrc或在 README 里写死 SDK 版本
lockfile 必须提交。 不钉版本的后果是「我这儿能跑」,而 AI 在一个和你不同的版本上改代码,产出的东西你验证不了。
怎么落地:靠 review + DESIGN.md 的选型章节。
一句话概括整节:打 tag 触发的是「构建 + 开 PR」,不是「部署」。集群里跑什么,由 Git 里写着什么决定。
不接受把生产跑成「一台机器上的 docker compose」或「systemd 服务」。
理由不是时髦:AIOps 只在 k8s 上看得见东西。 集群登记之后,它能读 pod、日志、事件、helm release、同步状态;跑在别处的服务,出事时 AI 一行日志都拿不到。那不是「少一个功能」,是那个服务在运维视角里根本不存在 —— 而它照样会在半夜挂掉。
- 集群必须先在 AIOps 里登记(人登记地址与凭据,AI 只说集群名)。
- 每个环境一条独立登记,dev 和 prod 是两条,不是一条加参数。同名的「重启 api」在两个环境上是两件完全不同的事。
- 本地开发用 docker compose 完全没问题,这条只管生产与预发。
ghcr.io/codelinkops/<仓库名>。
- 必须打
org.opencontainers.image.source标签指向仓库。少了它,GHCR 上的包和仓库关联不上 —— 包的权限继承、以及「这个镜像是哪份代码构建的」两件事同时断掉。 - 集群侧需要一个 imagePullSecret 才拉得动私有包。这是一次性配置,但忘了配的表现是
ImagePullBackOff,而它看起来像镜像不存在 —— 会让人去查 tag 而不是查凭据。 - tag 不可变:推上去的 tag 永不重推,也不要用
latest。 k8s 认的是 tag,重推同一个 tag 之后「集群里跑的到底是哪份代码」就查不出来了;更糟的是 rollout 不会被触发(digest 变了但 spec 没变),于是新起的 pod 是新版、老 pod 还是旧版 —— 这种半新半旧的状态最难查。 - 每次构建打两个 tag:语义化版本(
v1.2.3,给人看)和 commit sha(sha-abc1234,唯一能反查代码的锚点)。 - Dockerfile:多阶段构建、不要用 root 跑、暴露一个健康检查端点。
在 prod 分支上打 v1.2.3,然后:
- CI 构建镜像,推 GHCR,tag 为
v1.2.3与sha-<短sha>; - CI 开一个 PR,把部署清单里的 image tag 改成
v1.2.3—— 只改那一行; - 人审、合并;
- 同步器把集群拉到位。
为什么 tag 不直接部署到集群:
- 那样集群状态的来源就不是 Git 了。下一次同步会把它改回去,留下一段谁也说不清的历史。
- 合并那一下才是决策点。 tag 只说明「这个版本构建好了」,不等于「决定上线」。两件事挤在一个动作里,就没有地方可以喊停了。
- 走 Git 的话,「现在线上跑的是什么」的答案在仓库里,而且带着是谁批的;直接部署的话,只能去问集群。
dev 环境可以自动。 dev 分支合并 → 构建 → 自动更新 dev 的清单,不用人点。它风险低而且要求快。prod 永远要人点一下。
- 放与代码同仓库的
deploy/,或独立的 GitOps 仓库。两种都行,但一个项目只能选一种,在 DESIGN.md 里写明。 - 每个环境一个目录(
deploy/dev/、deploy/prod/),不要一份清单加一堆条件判断 —— 那种写法里「prod 到底生效了哪几条」谁也看不出来。 - image tag 在 YAML 里必须加引号。
tag: "1.0"是字符串,tag: 1.0是浮点数,会被解析成1,然后拉一个不存在的镜像。 - 副本数由 HPA 管的话,清单里就不要写
replicas。 写了会和 HPA 互相覆盖,表现是副本数反复横跳,而两边的配置单独看都是对的。
git revert 那个改 tag 的 PR,然后合并。 就这一条路。
不要用 helm rollback、argocd rollback、kubectl rollout undo —— 它们都是绕过 Git 直接改集群,同步器随后会把它改回去。表现出来是「回滚过了,但过一会儿又变回故障版本」,而且没有任何地方记着中间发生过什么。
嫌 revert PR 慢的话,正确做法是给它降低审批门槛,不是绕过 Git。
CI 绿了不算。PR 合了也不算。 同步是异步的。
判据是同步器报 Synced + Healthy。在那之前一律说「已触发」,不要说「已上线」—— 说成完成态会让人(和 AI)据此继续往下走,而那时集群可能还没动。
生产集群上不许 kubectl apply / kubectl edit / helm upgrade。理由同 5.5:制造漂移,然后被同步器改回去,中间那段时间的行为没人解释得了。
只读的排查命令(logs / describe / events / get)随便用,那些不改变任何东西。
真的必须手动介入 —— 走 §9 逃生门。
「保留最近 20 个」这类定时规则假设没被保留的就没人用,而 GitOps 下集群引用的往往正是某个具体的旧 tag。删掉它,下次 pod 重建就是 ImagePullBackOff —— 而那多半发生在半夜扩容或节点漂移的时候。
顺序:仓库里有什么 → 集群在用什么 → 差集才可以删。
查在用的必须算上 initContainers。 漏了它,一个只在 init 阶段用到的镜像会被判成没人用而删掉,然后下次 pod 起不来。
GitOps 的前提是清单进 Git,而 Secret 不能跟着进。用 sealed-secrets / external-secrets,或者在集群侧手工建。详见 §8。
怎么落地:
- CI 能拦住的:镜像 tag 格式、
org.opencontainers.image.source标签、tag 不可重推(GHCR 可配不可变 tag)、清单里 image tag 带引号。- 拦不住的:手改集群。那要靠集群 RBAC(人不发生产集群的写权限)。光写在文档里等于没写。
这一节是 AIOps 能不能自动汇总进度的唯一输入。写得马虎,报表就是假的 —— 而假报表比没有报表糟,因为它看起来是对的。
进度的算法很简单:GitHub 里程碑下 closed issue 数 / 总 issue 数。所有规则都是从这个算法倒推出来的。
没挂里程碑的 issue 在进度里不存在。 不是「算成未完成」,是根本不参与计算 —— 做完十件事而进度纹丝不动,人会以为统计坏了。
不写截止日期的里程碑排不出优先级,日报里也没法说「还剩几天」。日期一律按 UTC+8 理解,DESIGN.md 里也这么写 —— 服务器多跑在 UTC 上,「9 点」在两边差 8 小时,而这种错不会报错,只会让报告在某天出现在一个奇怪的时间。
一个 issue = 一个能在一次 AI 会话里做完的功能点。
这条最容易被忽略,后果最直接:进度按 issue 条数算,不按工作量。把十个功能点塞进一个 issue,那个里程碑就会长期停在 0%,然后某天突然跳到 100%。中间那段时间它看起来毫无进展,而实际上一直在动。
反过来也别拆得太碎(「改一个变量名」不配一个 issue),那会让进度虚高。
一条永远不会被关掉的 issue 会把这个里程碑永久压在 90%。挪到下一个里程碑,或者标 blocked 后从里程碑里摘出去。
v<版本> <一句话目标>,例如 v0.2 多仓库支持。
跨仓库同名里程碑很常见(需求仓库排「v2 上线」、源码仓库也叫「v2」),汇总时会出现两行几乎一样的东西。带上一句话目标能分开。
AIOps 按 issue 的标签把任务派给人。标签打错 = 派错人,或者根本没人接。org 统一标签:
| 前缀 | 取值 |
|---|---|
type/ |
feat fix chore docs |
area/ |
backend web app infra docs |
| 优先级 | p0 p1 p2 |
| 状态 | blocked needs-design break-glass |
每个 issue 至少一个 type/ 和一个 area/。
怎么落地:
type/和area/标签由 issue 模板自动带上(.github/ISSUE_TEMPLATE/),空白 issue 入口已关掉。- 里程碑设不了 —— GitHub 的 issue form 没有这个字段。所以由
issue-hygiene兜底:没挂里程碑的 issue 会被打上needs-triage并留一条说明,挂上之后自动摘掉。- 粒度和「别把不做的挂在里程碑上」靠 review,没有检查。
README.md 这是什么、怎么本地跑起来
DESIGN.md 要做成什么、为什么(先于代码存在)
AGENTS.md agent 在这个仓库怎么干活
CLAUDE.md 一行 @AGENTS.md,见下
.env.example 所有环境变量,值用占位符
.org-spec.json 记录本仓库同步到了 org 规范的哪一版,见 §7.1
Dockerfile 多阶段、非 root、带健康检查端点
deploy/ 按环境分目录的部署清单(或在 DESIGN.md 里指明用独立 GitOps 仓库)
rust-toolchain.toml / .nvmrc / .fvmrc
.github/workflows/ci.yml
AGENTS.md 是跨工具的事实标准(Codex、Cursor 等都读它);Claude Code 读 CLAUDE.md。不要写两份,会分叉,而分叉的那份多半是漏了新规则的那份。
做法:AGENTS.md 放全部内容,CLAUDE.md 只有一行导入:
@AGENTS.mdAGENTS.md 的开头引用 org 规范:
# <项目名> — Agent 工作指南
本项目遵循 CodeLinkOps 工程规范:https://github.com/CodeLinkOps/.github/blob/main/AGENTS.md
下面只写**本仓库特有**的部分。注意:agent 不一定能顺着链接读到内容(取决于它有没有网络和登录态)。所以 org 规范的关键条款要抄一份进各仓库的
AGENTS.md,别只留链接。本仓库的AGENTS.md就是为抄写准备的 —— 它是精简版,抄进去不会太长。
「抄一份」有个固有代价:这边改了,抄的那份不会跟着变。 而漂移了没有任何征兆 —— agent 会继续按旧规则干活,理直气壮。补部署规则那次就真出过:某个仓库的 DESIGN.md 里写着「不做 Kubernetes」,与新规范直接冲突,而 AI 照着它做完全不会觉得有问题。
所以有 spec-drift:每天比对本仓库的 AGENTS.md 与 templates/DESIGN.md,落后了就在那个仓库开一条 issue(附 compare 链接和该写进 .org-spec.json 的内容),跟上之后自动关闭。
它故意不阻塞 PR —— 规范漂移不是紧急问题,让所有 PR 变红会逼人去无视它。
.org-spec.json 记录已同步到哪一版:
{
"source": "CodeLinkOps/.github@main",
"synced_commit": "<规范仓库的 commit sha>",
"files": {
"AGENTS.md": "<sha256>",
"templates/DESIGN.md": "<sha256>"
}
}新仓库不用手写 —— 没有这个文件时第一次检查会把完整内容贴在 issue 里,复制粘贴即可。
抄 org 规范的关键条款,加上从代码里看不出来的东西:为什么这么写、踩过什么坑、改动时容易碰坏什么。
不要写代码结构说明(那些 agent 自己能看出来,而且会过时)。写「这个字段看起来能删,但删了会……」这种。
怎么落地:
spec-drift每天检查各仓库有没有跟上本仓库的改动,落后了开 issue、跟上了自动关 —— 它能发现漂移,但不阻塞 PR,实际同步仍然靠人/AI 做。「必备文件齐不齐」目前没有检查,靠 review。
- 任何密钥都不许进仓库,包括测试用的、包括注释掉的、包括部署清单里的。
- 所有环境变量在
.env.example里列出来,值用占位符。新增一个环境变量必须同时改.env.example—— 漏了的后果是下一个人(或下一次 AI 会话)起不来,而报错通常离原因很远。 - 真实密钥放 GitHub Secrets、集群 Secret 或部署环境;本地放
.env,.env必须在.gitignore里。 - GitHub 的 secret scanning 对私有仓库要付费,所以 CI 里跑一个免费的替代(
gitleaks)。 - 万一提交了密钥:先吊销,再清历史。顺序反了等于给攻击者留了一个窗口,而且清历史比吊销慢得多。
- 另外要清就得删仓库重建或让 GitHub 回收 —— force push 之后那个提交仍然能按 SHA 访问,公开仓库上尤其要当真。
怎么落地:
gitleaks是 CI 检查,能拦住。其余靠 review。
生产在烧、AI 用不了、或者其他任何时候,允许人直接改代码、直接改集群。规则不是用来在事故里坚守的。
但破例要留痕,三步:
- 分支或 PR 打
break-glass标签(署名检查见到这个标签会放行)。 - PR 正文写清楚:为什么必须绕过、当时的情况、手动改了集群的哪些东西。
- 事后补一条 issue,说明这次破例暴露了什么问题(AI 为什么用不了?规范哪里不合理?)。
手改过集群的话还有第 4 步:把手改的内容补回 Git,否则下一次同步会把它抹掉 —— 那时故障会以「明明修好了又坏了」的形式回来。
第 3 步是这一整节存在的理由。 只有 1、2 的话,逃生门会慢慢变成正门 —— 而没人会注意到那个转变,因为每一次单独看都是合理的。