上一篇把「契约归谁」说清楚了,跨仓需求照样翻车。常见场面是:Agent 在 web 里自作主张「合理推断」一套接口,api 按另一套口头约定实现;或者平台 SDK 的 major 已经发出去了,消费方一周后才补 Spec。

多半不是不懂归属,而是缺两样东西:一条人能照着做的变更顺序,以及开发过程中把上下文钉住、别让它在仓与仓、会话与会话之间溜走的办法。

这篇的主张很简单:跨仓仍然成立的行为,用 OpenSpec 记在可版本化的契约里;单仓、单次会话里干活时,用 User Harness 逼 Agent 先读这份契约、改完再对照检查。两者配合,比指望模型「自己记得上周在另一个仓说过什么」靠谱得多。

主要写给开发和测试工程师(下文称 TE)。读完你应该能自己走完一次「只改本仓」和一次「前端 + 后端一起改」的流程。Lead 可以看拓扑和门禁,但不是第一读者。OpenSpec 就是现成的 SDD 工具,这里不另起名字。

你们更像哪种产品

很多公司两套形态同时存在,翻车点不一样。

一种是业务拆成多个仓:结账、商品、营销各管一摊,跨模块需求不少,共享库不多。麻烦通常出在平级团队之间——谁先写契约、谁先合代码、Agent 到底读哪份 Spec。

另一种是平台或公共组件很重:一个 auth SDK、一套基础 API,被很多业务仓引用,不兼容变更经常有。麻烦变成中心带动四周——影响哪些仓、旧接口留多久、谁先升级谁后升级。

两套形态不必各搞一套方法论。同一条流水线就够,差别只是:要不要单独的平台契约仓,以及要不要「影响面清单 + 分批升级」。

整条线可以粗记成:

先分清需求类型(只动本仓 / 跨业务仓 / 动公共依赖)
  → 契约写在哪个仓、开哪次变更
  → 用 git 的 tag 或提交号钉住这一版契约
  → 各代码仓只写本仓怎么落地,并写明引用哪一版契约
  → 联调、发版(动公共依赖时尤其要谈清楚)
  → TE 按钉住的契约跑产品级场景
  → 归档,并把这次踩坑写回团队规则

这也呼应 SDD 真相源:代码大量由 Agent 生成时,人真正要稳住的是规格。仓一多,规格还得别人能引用、能钉版本。OpenSpec 在站内交付栈里仍是规格这一层,见 三层交付栈。

三种需求,一分钟分清走哪条

接到活,先问上一篇同一句:这份行为或约束,是不是要求别的仓的人也遵守? 然后再看冲击是「几个业务仓一起改」,还是「平台一动,很多人被迫跟着动」。

需求类型怎么判断建议怎么放 OpenSpec(不用 beta 也能做)谁先动手
只动本仓影响出不了当前这个代码仓本仓 openspec/changes,改本地 specs本仓团队
跨几个业务仓至少两个业务/模块仓要共同遵守新行为先在产品契约仓写清边界行为;各代码仓再开实现变更,并写明引用哪一版契约契约负责人先合契约,再轮到各实现仓
动公共依赖改的是很多仓在用的库或服务,可能逼消费方改代码或改行为在平台契约仓写兼容约定;消费方分批改平台侧提案 → 评审影响面 → 消费方分批落地

「跨业务仓」和「动公共依赖」都能跨仓库,协作方式不同:前者多半是前端和后端一起改结账;后者是 SDK 一变,一堆应用要跟。

大面积写代码之前,变更提案里建议写死四行(对应 Harness 里「先对齐再动手」):

  1. 需求属于上面哪一类
  2. 契约正文在哪个仓(本仓,还是产品契约仓 / 平台契约仓)
  3. 哪些代码仓会改(只动本仓就写「仅本仓」)
  4. 明确不做什么,防止聊着聊着 scope 偷偷变大

少一行,就别让 Agent 铺开改。

仓库怎么摆:普通 git 仓够用,不必先上 Stores

product-contracts/     # 产品契约:边界行为、跨端用户路径
platform-contracts/    # 平台契约:兼容说明、迁移窗口(平台压力大再单独拆)
web/  api/  mobile/ …  # 各业务仓:本地 openspec/ 只写本仓怎么落地

团队不大、很少遇到「公共 SDK 一升,全员被迫改」时,也可以先共用一个契约仓,用目录和 CODEOWNERS 分开产品和平台。做法不变,只是少几个远程仓。

OpenSpec Stores 和 references / worksets 还在 beta。可以当成同一套做法的可选加速,不是开工前提。分清两件事:

  • 工具能管:某个 openspec/ 下的 specs 和 changes;Stores 还能把规划放到独立仓并按名字引用。
  • 工具不会替你做:自动 git pull、替你锁死「必须用某个提交号」、替你跑 TE 的产品套件。

官方也写了 Stores 默认不同步。你本机契约仓没拉新,读到的就是旧真相。所以「钉版本」靠的是 git 的 tag/提交号,再加上 CI 或团队规则检查,不是在配置里写一行 references: 就完事。

你想解决的问题现在就能用的做法若开了 Stores beta,大致对应
契约有独立的家普通契约 git 仓登记成一个 store
钉住某一版契约提案和 CI 写明 contracts@某 tag 或提交号还是你们自己钉;Stores 不会自动锁版本
让 Agent 知道上游契约在哪各代码仓 openspec/config.yaml 的 context: / rules: 写清楚「先读钉住的契约」references: 只在说明里加只读索引,不会改命令默认改哪个仓
一次打开多个仓可提交的多根 .code-workspaceworkset open(多半是本机名单,不是团队共享真相)
做体检CI 检查版本写没写、能不能解析openspec doctor / context

代码仓里可以先加这样一段稳定配置:

schema: spec-driven
context: |
  跨仓行为写在 product-contracts。
  写代码前:先读本 change 里钉住的 contracts@提交号。
  不要把契约正文复制进本仓。
rules:
  proposal:
    - 写明需求类型、契约版本、实现仓、不做什么
    - 跨仓或平台变更时,本仓提案只保留引用,不抄正文

不这样写,Agent 默认只看得见当前仓,「正文只在契约仓、代码仓只留引用」就很难落地。

变更怎么排队:评过可以并行,合主干必须对齐

默认建议就两步:

  1. 契约 PR 在 GitHub/GitLab 上被负责人点过 Approve 之后,各业务仓可以并行开实现 PR,开始写代码。
  2. 实现 PR 合进主干之前,契约 PR 必须已经 merge,并且实现 PR 里写的契约版本,已经换成 merge 之后的那一版。

比「契约不 merge 谁都不许动手」快,比「大家口头说好一起发」稳。

版本怎么钉(不然并行等于对着移动靶打)

OpenSpec 不管锁版本,git 管。

  1. 契约 PR 一被 Approve,马上记下不可变版本,比如 product-contracts@9f3c1a2(批准那一刻的提交),或钉在这个提交上的预发 tag。别钉「分支最新」,分支尖还会继续动。
  2. 所有实现 PR 的提案都要写上这个版本。若有人又往还没合的契约 PR 里推了改语义的提交,就重新评审,换新的冻结版本,各实现 PR 一起改引用。
  3. 契约 merge 之后(包括 squash),再钉一次:merge 出来的提交,或 main 上的正式 tag,并更新还没合的实现 PR。
  4. CI 拦住这种货:契约已经进主干了,实现 PR 还挂着旧的「评审时提交号」——不许合。

一句话:评过时冻一版;合进主干后再对齐到最终版。

跨几个业务仓怎么走:结账加优惠券

虚构一家 Acme 商城:结账页要支持优惠券,web 和 api 都要改。

  1. 判成「跨几个业务仓」,把上面四行写进提案,先对齐。
  2. 在产品契约仓开变更:只写边界行为、错误含义、验收时要看的用户路径。别把前端任务清单和后端任务清单塞进契约仓。
  3. 契约评过之后,冻结 product-contracts@某一提交号。
  4. web、api 各自开本地变更:只写本仓设计和任务;提案开头只留引用(见后面模拟)。
  5. 多根窗口打开契约仓和正在改的代码仓;让 Agent 先读钉住的那一版,再动代码。
  6. 联调一般先 api 后 web。字段含义要变,先改契约并换版本,再改代码。
  7. TE 按验收要点,对照 merge 后的契约版本跑产品场景。
  8. 实现都进主干后再归档:先归档各代码仓,再归档契约仓;把踩坑写回团队规则。

代码仓提案里的引用可以长这样:

- 需求类型: 跨几个业务仓
- 契约版本: product-contracts@<sha-or-tag>
- 契约变更: <契约仓内路径或 id>
- 实现仓: [web, api]
- 不做什么: <链到契约变更里对应小节>
- 相关 PR: [契约 PR, …]
- 产品场景: [<编号> → e2e 用例路径]

跨仓还成立的行为,正文只放契约仓一份;代码仓只引用,别抄摘要。摘要一抄,过两天就对不上。

动公共依赖怎么走:auth-sdk 不兼容升级

和「跨业务仓」比,平台变更通常只多三样:影响谁、旧行为留多久、谁先改谁后改。不是另一套 SDD。

  1. 标明这是公共依赖变更,附一份手写的影响仓列表(先能用;以后再收到平台契约仓的消费者文件里)。
  2. 在平台契约仓写清:旧行为变成什么、什么时候弃用、要不要双跑或开关。
  3. 评过后冻结版本;发出去的包版本要能追到这份契约。别先发 SDK、再补 Spec。
  4. 消费方按「先试点、再批量」开本地变更,都引用同一契约版本。
  5. 关迁移窗口的条件写死:关键产品场景全过,或者有人签字的例外清单。

平台压力大时,窗口期内至少跑通试点仓的场景和平台冒烟;真要关窗,再看全量关键场景。

模拟一下:Acme 微服务地图里走几遍

原则说过了。下面把目录、变更、PR 顺序写具体。Acme 按常见电商微服务拆仓——不是两个仓凑合演示,而是一张能对照真实拆分的地图。先摆工作区,再走「只动本仓」「跨多个微服务」「动公共 SDK」三条活。

工作区:契约仓 + 一堆业务微服务

普通 git 仓即可。每个业务服务一个仓(也可用 monorepo 子目录,协议一样:跨边界行为仍进契约仓)。

~/acme/
  # —— 契约与验收 ——
  product-contracts/          # 产品契约:跨服务仍成立的行为、用户路径
  platform-contracts/         # 平台契约:auth-sdk 等公共依赖的兼容说明
  shop-e2e/                   # TE 维护的产品级用例(跨服务黑盒)

  # —— 客户端 ——
  shop-web/                   # 网页店面
  shop-mobile/                # App(例子二可不动,对照表会用到)

  # —— 业务微服务(一仓一服务)——
  svc-gateway/                # BFF / API 网关:聚合结账读写
  svc-catalog/                # 商品目录
  svc-inventory/              # 库存
  svc-pricing/                # 计价、税、运费
  svc-promotion/              # 优惠券、促销规则
  svc-order/                  # 订单与结账状态机
  svc-notification/           # 短信 / 邮件 / App 推送

  # —— 平台公共依赖 ——
  auth-sdk/                   # 登录态、错误码(被 gateway / web / mobile 等引用)

  acme-shop.code-workspace    # 多根窗口:按本次变更勾需要的仓

谁大概归谁(方便你对照自家分工):

仓大致职责典型归属
svc-catalogSKU、类目、详情商品团队
svc-inventory可售库存、预留履约 / 库存团队
svc-pricing原价、折扣后应付、税运费交易定价团队
svc-promotion券码校验、规则引擎营销团队
svc-order下单、支付状态、订单查询交易订单团队
svc-gateway给 web/App 的聚合接口体验 / BFF 小组
shop-web / shop-mobile用户界面前端 / 客户端团队
auth-sdk公共鉴权平台团队
product-contracts跨仓真相产品 + 各域 Lead 共审
shop-e2e产品场景自动化TE

多根窗口不要一次勾完全部仓。结账加券这类活,勾契约仓 + 本次会改的服务即可,例如:

{
  "folders": [
    { "path": "product-contracts", "name": "product-contracts" },
    { "path": "svc-promotion", "name": "svc-promotion" },
    { "path": "svc-pricing", "name": "svc-pricing" },
    { "path": "svc-order", "name": "svc-order" },
    { "path": "svc-gateway", "name": "svc-gateway" },
    { "path": "shop-web", "name": "shop-web" }
  ]
}

例子一:只动本仓——优惠券输入框报错样式

需求:结账页优惠券框报错时颜色和间距难看,要改样式。校验规则、接口字段、错误码都不动。周围那些微服务一个都别碰。

问题答案
别的仓也得遵守吗?不用 → 只动本仓
契约写哪没有新的跨仓行为,留在 shop-web
谁改代码只有 shop-web
不做什么不改 gateway 字段、不改 svc-promotion 规则、不动产品场景断言

落地步骤:

  1. 在 shop-web 开本地变更,例如 openspec/changes/coupon-input-error-styles/。
  2. 提案写明「只动本仓」和那四行;不用开产品契约仓的 PR,也不用钉跨仓版本。
  3. 设计和任务只谈本仓组件和样式;单仓工作区就够,别把 svc-order 之类勾进窗口徒增噪音。
  4. 本仓检查过了就合 shop-web。TE 一般不必为样式重跑整条结账场景(除非你们把视觉回归也塞进 e2e)。
  5. 归档本仓变更。

这类活怎样算做完:本仓 Spec/变更验收过就行。别为了「看起来正规」硬开一张空契约 PR,更别顺手改微服务。

例子二:跨多个微服务——结账输入优惠券码

需求:用户在结账页输入优惠券码。链路大致是:

shop-web
  → svc-gateway(聚合结账页数据与「应用优惠券」动作)
      → svc-promotion(校验券码、算优惠明细)
      → svc-pricing(把优惠落到应付金额)
      → svc-order(把折扣写进进行中的结账/订单)

shop-mobile、svc-catalog、svc-inventory、svc-notification 这次明确不改(写进非目标)。成功和失败的含义,从 gateway 到 web 必须一致;promotion / pricing / order 之间的字段也不能各说各话。这是典型的 跨几个业务仓。

1. 契约正文只写在产品契约仓

product-contracts/
  openspec/
    changes/
      add-checkout-coupon/
        proposal.md
        specs/            # 或 delta:跨服务边界行为 + 错误含义

提案可以大致这样写:

## 需求类型
跨几个业务仓——下列仓都必须遵守同一套边界行为

## 实现仓
- shop-web
- svc-gateway
- svc-promotion
- svc-pricing
- svc-order

## 不做什么
- 不改 shop-mobile(下个迭代再跟)
- 不改 svc-catalog / svc-inventory / svc-notification
- 不改 auth-sdk,不动 platform-contracts
- 本次不做礼品卡、不做「推荐券」列表

## 产品场景
- 编号: checkout-apply-coupon
- 用例: shop-e2e/tests/checkout/apply-coupon.spec.ts

正文只写跨服务仍必须当真相的内容,例如:

  • 用户侧动作:结账页提交券码 code
  • gateway 对外:POST /checkout/coupon 的请求/响应形状
  • 服务间约定(可写事件或同步调用,二选一写清):promotion 返回的优惠明细如何被 pricing 消费;order 如何持久化 discount
  • 失败:COUPON_NOT_FOUND / COUPON_EXPIRED / COUPON_NOT_APPLICABLE 对用户意味着什么(web 与 gateway 一致)
  • 库存与目录:本次不因用券而改预留逻辑

然后开契约 PR → 各相关域 Lead Approve → 冻结例如 product-contracts@9f3c1a2。

2. 每个会改的仓只开「带引用」的实现变更

评过之后,五个实现仓可以并行开 PR。这里有一条必须写准的规矩:

  • 契约正文只在 product-contracts 那一份 change 里。
  • 五个实现仓各自也有 proposal.md,但里面是引用(指针):钉住同一契约版本、指向同一契约 change、列出全部实现仓、写明本仓职责。
  • 不要把接口字段、错误码含义、跨服务约定再抄进这五个 proposal.md。本仓怎么落地,写在各自的 design.md / tasks.md。

目录不要省,每个仓各自一套;五个 proposal.md 旁注统一写成「只有引用」:

shop-web/
  openspec/
    config.yaml
    changes/
      add-checkout-coupon/
        proposal.md    # 只有引用,没有契约副本
        design.md      # 结账页券码输入、调用 gateway、错误展示
        tasks.md

svc-gateway/
  openspec/
    config.yaml
    changes/
      add-checkout-coupon/
        proposal.md    # 只有引用,没有契约副本
        design.md      # 对外 POST /checkout/coupon;编排下游调用
        tasks.md

svc-promotion/
  openspec/
    config.yaml
    changes/
      add-checkout-coupon/
        proposal.md    # 只有引用,没有契约副本
        design.md      # 校验券码,返回优惠明细
        tasks.md

svc-pricing/
  openspec/
    config.yaml
    changes/
      add-checkout-coupon/
        proposal.md    # 只有引用,没有契约副本
        design.md      # 把优惠落到应付金额 / tax / 运费口径
        tasks.md

svc-order/
  openspec/
    config.yaml
    changes/
      add-checkout-coupon/
        proposal.md    # 只有引用,没有契约副本
        design.md      # 把 discount 写进进行中的结账/订单
        tasks.md

五个 proposal.md 字段对齐如下。相同的是契约版本、契约 change、实现仓列表、非目标/场景引用;不同的只有「本仓职责」。

shop-web/openspec/changes/add-checkout-coupon/proposal.md:

- 需求类型: 跨几个业务仓
- 契约版本: product-contracts@9f3c1a2
- 契约变更: openspec/changes/add-checkout-coupon
- 实现仓: [shop-web, svc-gateway, svc-promotion, svc-pricing, svc-order]
- 本仓职责: shop-web = 结账页券码输入、调用 gateway、展示错误
- 不做什么: (见契约仓该变更的非目标,此处不抄正文)
- 相关 PR: [acme/product-contracts#84]
- 产品场景: [checkout-apply-coupon → shop-e2e/tests/checkout/apply-coupon.spec.ts]

svc-gateway/.../proposal.md:同上,仅本仓职责改为
svc-gateway = 对外 POST /checkout/coupon,编排 promotion/pricing/order。

svc-promotion/.../proposal.md:同上,仅本仓职责改为
svc-promotion = 校验券码并返回优惠明细。

svc-pricing/.../proposal.md:同上,仅本仓职责改为
svc-pricing = 把优惠落到应付金额 / tax / 运费口径。

svc-order/.../proposal.md:同上,仅本仓职责改为
svc-order = 把 discount 写进进行中的结账/订单。

多根窗口只打开契约仓 + 你正在改的那一两个服务。规则写死:先拉取并核对提交号 9f3c1a2,再改代码;别凭本仓旧 DTO「猜」邻居字段。

3. 合入顺序、换成最终版本、测试、归档

微服务多了,合入顺序按依赖方向,而不是谁先写完谁先合:

顺序做什么过不去的情况
1合并契约 PR(可以 squash)得到最终版本,例如 tag contracts-checkout-coupon-2026-10-27 → 提交 c0ffee01
2五个实现 PR 的引用都从 @9f3c1a2 改成 @c0ffee01CI:必须等于合并后的版本
3先合 svc-promotion、svc-pricing(先提供优惠与计价能力)本仓对照钉住的 Spec
4再合 svc-order(消费上两步的结果写进结账)同上
5再合 svc-gateway(对外暴露稳定聚合接口)同上
6最后合 shop-web(对接 gateway)同上
7TE 在 shop-e2e 跑 apply-coupon(可编排起一整条调用链);用例写明 contracts@c0ffee01和契约验收要点能对上
8先归档各实现仓,再归档契约;补规则(例如「优惠券错误含义禁止只改 web」)归档时回写

svc-catalog、svc-inventory、shop-mobile 这次没有实现 PR——不是忘了,是非目标里写死了。下次若 App 要跟,另开变更,继续引用同一份(或更新后的)契约版本即可。

若 Approve 之后、merge 之前又有人改了契约含义:重新评审,换冻结提交号,五个实现 PR 一起改。仓越多,靶子偷着挪的代价越大。

例子三(对照):动公共依赖——auth-sdk 错误码不兼容

需求:auth-sdk 把一批登录错误码改成新枚举,旧码废弃。消费方不只是 shop-web,还包括 svc-gateway、shop-mobile,以及任何直接引用 SDK 的服务。

问题答案
类型动公共依赖
契约写哪platform-contracts(兼容说明、迁移窗口、旧码对照表)
影响面(手写清单起步)auth-sdk、svc-gateway、shop-web、shop-mobile;若 svc-order 也直连 SDK 就写上
这次仍可不碰svc-catalog、svc-inventory、svc-promotion…(以实际引用为准)
怎么推进平台契约评过并冻结 → 发带版本号的 SDK → 先试点 gateway+web → 再批量 mobile 等 → TE 跑登录相关产品场景 → 关窗

细节仍按上文「动公共依赖怎么走」:制品版本要能追到平台契约;窗口期内试点场景要过。

同一张地图上,五条活怎么选路

需求类型大概动哪些仓要不要契约 PR 和钉版本怎样算做完
优惠券输入框样式只动本仓仅 shop-web不要本仓变更验收过
商品详情页多一个展示字段(仅 catalog 内部模型)只动本仓仅 svc-catalog不要(若对外 API 不变)本仓验收过
结账优惠券码跨几个业务仓契约仓 + promotion + pricing + order + gateway + web(+ e2e)要;评过冻一版,合完对齐最终版产品场景跑通整条链
App 结账页跟优惠券(web 已上)跨几个业务仓契约仓(可增补客户端差异)+ gateway(若需)+ shop-mobile要;引用已有或增补后的契约版本App 场景对照同一契约
auth-sdk 错误码不兼容动公共依赖平台契约 + auth-sdk + gateway/web/mobile 等消费方分批要;外加影响面和迁移窗口试点场景过;关窗看关键登录场景或签字例外

注意中间两行:「只动 svc-catalog 内部」和「结账加券跨五六个仓」都可能发生在同一套微服务里——差别不在仓多不多,而在后果有没有溢出到别的仓。

做事顺序还是:先分类型 → 只动本仓就留在本仓;跨业务仓或动公共依赖,先把契约正文和冻结版本写进对应契约仓 → 各仓带着引用实现 → TE 对照同一版本验收 → 最后归档。以后若开 Stores,不过是多一种登记契约仓、生成索引的方式,钉版本和 PR 顺序不用换。

跨仓需求:OpenSpec 命令和提示语怎么跟做

前面讲清了仓和目录。这一节只盯跨几个业务仓(以结账加券为例),把你在终端和 AI 对话框里实际敲什么写清楚。

默认按稳定轨:每个仓自己有 openspec/,契约仓是普通 git 仓 product-contracts,不用 Stores beta。命令以当前主流 OPSX 工作流为准;在 Cursor 里斜杠命令常写成 /opsx-propose(带连字符),在 Claude Code 一类工具里常写成 /opsx:propose。意思一样,以你项目里 openspec init / openspec update 生成的技能名为准。下文稳定轨用到的 init / new change / status / instructions / validate / archive 在常见安装上都能用;若你本机还没有 store / workset / doctor 一类命令,多半是版本偏旧或未开 Multi-repo(beta)——稳定轨协议不依赖它们,需要时再升级 @fission-ai/openspec。

0. 每个仓先初始化一次

在契约仓和每个会改代码的仓里各做一次(已做过可跳过):

cd ~/acme/product-contracts && openspec init --tools cursor
cd ~/acme/svc-promotion && openspec init --tools cursor
# gateway / pricing / order / shop-web 同理

实现仓的 openspec/config.yaml 务必带上「先读钉住的契约、提案只留引用」——前文已有示例。没有这几行,Agent 很容易在本仓 proposal.md 里又写一份契约正文,那就偏了。

建议每人本机用多根窗口打开:product-contracts + 当前正在改的一两个服务仓。

1. 先在产品契约仓开变更(正文只在这里)

cd ~/acme/product-contracts
git checkout -b change/add-checkout-coupon

方式 A(推荐,对话里一条龙出规划产物):

在 Cursor 里对 product-contracts 工作区说:

/opsx-propose add-checkout-coupon

这是跨几个业务仓的变更。请按下面约束写规划产物:

需求:结账页支持输入优惠券码。
调用链:shop-web → svc-gateway → svc-promotion → svc-pricing → svc-order。
实现仓必须包含:shop-web, svc-gateway, svc-promotion, svc-pricing, svc-order。
明确不改:shop-mobile, svc-catalog, svc-inventory, svc-notification, auth-sdk。

proposal.md 要写清:需求类型=跨几个业务仓、实现仓列表、非目标、产品场景编号 checkout-apply-coupon。
specs 只写跨服务仍成立的边界行为、错误码含义、gateway 对外接口形状、promotion/pricing/order 之间如何交接。
不要写各仓内部类怎么拆、表怎么建——那些属于各实现仓的 design/tasks。
design.md 可以写跨服务时序,但不要变成五个仓的任务合订本。
tasks.md 若在契约仓出现,只保留「契约评审 / 版本冻结 / 通知各仓开实现变更」这类协调任务,不要写具体改哪个服务的代码。

方式 B(CLI 只建骨架,再让 AI 按状态补产物):

cd ~/acme/product-contracts
openspec new change add-checkout-coupon
openspec status --change add-checkout-coupon
openspec instructions proposal --change add-checkout-coupon

然后把 instructions 的输出连同上面那段约束贴给 Agent,让它先写 proposal.md,再按依赖补 specs / design / tasks。这是 core 配置下最稳的 Path B。

默认 openspec init 装的是 core 工作流(propose / explore / apply / archive)。/opsx-continue 和 /opsx-ff 都属于 expanded 命令:要先 openspec config profile 勾选后再在该仓跑 openspec update,Cursor 里才会出现对应斜杠命令。启用之后才可以用:

/opsx-continue add-checkout-coupon   # 一次只出一个规划产物
/opsx-ff add-checkout-coupon         # 一次出齐规划产物(想清楚了再用)

自检:

openspec status --change add-checkout-coupon
openspec validate add-checkout-coupon

人审过契约 PR 并 Approve 后,马上记下冻结版本(评审那一刻的提交号),写进各实现仓提案:

git rev-parse HEAD   # 假设得到 9f3c1a2

契约 PR merge(可 squash)后,再取最终提交号(文中示例 c0ffee01),各实现仓要把引用改成最终版再合主干。

2. 在每个实现仓开「带引用」的变更(不要抄契约正文)

五个仓流程相同,只换目录和「本仓职责」。以 svc-promotion 为例:

cd ~/acme/svc-promotion
git checkout -b change/add-checkout-coupon

对话提示语(务必强调指针):

/opsx-propose add-checkout-coupon

本仓是实现仓,不是契约宿主。
请先阅读并遵守产品契约:
  仓:~/acme/product-contracts
  变更目录:openspec/changes/add-checkout-coupon
  冻结版本:product-contracts@9f3c1a2
  (可用 git -C ~/acme/product-contracts show 9f3c1a2:openspec/changes/add-checkout-coupon/proposal.md 核对)

本仓 proposal.md 只能写引用字段,禁止复制接口字段表、错误码含义、跨服务约定正文。
必须包含:
- 需求类型: 跨几个业务仓
- 契约版本: product-contracts@9f3c1a2
- 契约变更: openspec/changes/add-checkout-coupon
- 实现仓: [shop-web, svc-gateway, svc-promotion, svc-pricing, svc-order]
- 本仓职责: svc-promotion = 校验券码并返回优惠明细
- 不做什么: 见契约仓该变更(此处只引用)
- 相关 PR / 产品场景: 与契约仓一致

design.md / tasks.md 只写 svc-promotion 怎么落地与怎么测。
不要改 shop-web、gateway、pricing、order 的代码。

其余四仓把「本仓职责」换成:

仓提示语里的本仓职责
shop-web结账页券码输入、调用 gateway、展示错误
svc-gateway对外 POST /checkout/coupon,编排 promotion/pricing/order
svc-pricing把优惠落到应付金额 / tax / 运费口径
svc-order把 discount 写进进行中的结账/订单

也可用 CLI:

cd ~/acme/svc-promotion
openspec new change add-checkout-coupon
openspec status --change add-checkout-coupon

再让 Agent 按 openspec instructions proposal --change add-checkout-coupon 的模板写,但你必须在提示语里重复「只引用、不抄正文」。

五个仓的目录都应各自出现:

<repo>/openspec/changes/add-checkout-coupon/
  proposal.md   # 引用
  design.md     # 本仓设计
  tasks.md      # 本仓任务
  (以及 schema 需要的 specs 等)

3. 按依赖方向实现代码

规划产物齐了、契约已冻结后,在各实现仓跑实现(仍在该仓目录 / 对应工作区根下):

/opsx-apply add-checkout-coupon

建议顺序与合入顺序一致,避免下游对着不存在的上游行为空写:

  1. svc-promotion、svc-pricing
  2. svc-order
  3. svc-gateway
  4. shop-web

某个仓中途发现契约写错了:先改契约仓(必要时 /opsx-update add-checkout-coupon),重新评审并更换冻结版本,五个实现仓的 proposal.md 里的 契约版本 一起改,再继续 /opsx-apply。不要只在某一个服务仓里「本地纠正」跨服务语义。

实现过程中可随时:

openspec status --change add-checkout-coupon
openspec show add-checkout-coupon

4. 契约 merge 后换最终版本,再合实现 PR

# 在 product-contracts,merge 之后
git rev-parse HEAD   # 得到最终版,如 c0ffee01

每个未合的实现仓提案里把:

契约版本: product-contracts@9f3c1a2

改成:

契约版本: product-contracts@c0ffee01

CI 应拦截「契约已进主干、实现 PR 仍挂旧 SHA」。

5. TE 验收与归档

TE 在 shop-e2e 跑 checkout-apply-coupon,用例元数据写明 contracts@c0ffee01。

归档顺序:先各实现仓,再契约仓。

cd ~/acme/svc-promotion && openspec archive add-checkout-coupon -y
# pricing / order / gateway / shop-web 同理

cd ~/acme/product-contracts && openspec archive add-checkout-coupon -y

对话里也可用 /opsx-archive add-checkout-coupon(工具拼写以本地技能为准)。实现仓若本次没有要合并进主 specs 的 delta,可按需要加 --skip-specs;契约仓通常要合并 delta。以 openspec archive --help 为准。

6. 一张对照表:跨仓时「在哪个仓敲什么」

步骤在哪个仓你做什么
写跨服务行为product-contracts/opsx-propose + 跨仓约束提示语;人审 PR;冻结 SHA
开实现变更五个实现仓各一次/opsx-propose,proposal 只引用;design/tasks 只谈本仓
写代码五个实现仓按依赖顺序/opsx-apply
契约进主干后五个实现仓把 契约版本 改成最终 SHA,再合 PR
产品验收shop-e2e跑场景,标注同一契约版本
收尾先实现仓、后契约仓openspec archive … -y 或 /opsx-archive

容易做错的两件事:
1)在 shop-web 或某个 svc-* 里把 API 字段表写进 proposal.md(应写在契约仓 specs);
2)五个仓各自 /opsx-propose 出五份互相矛盾的「跨服务真相」。跨服务真相只能有一份,在 product-contracts。

Harness:开了好几个文件夹,不等于上下文钉住了

跨仓丢上下文,常被怪成「模型记性差」。更有用的分法是三层:

层是什么跨仓时谁管
落在仓里的契约仓的 specs/changes、钉住的版本、各仓自己的变更OpenSpec + git
编辑器里看得见的一次打开了哪些目录多根 .code-workspace(或可选 workset)
聊天里临时想的当前对话里的理解不能当跨仓真相

多根窗口只解决「看得见」。防漂移靠的是:冻结的契约版本、写代码前强制先读仓里的材料、再加上 CI 或规则检查。这和 Harness Engineering 的 Guides/Sensors,以及 ai4se-harness 里「先对齐 → 对照 Spec 验证 → 归档回写」是一回事。第一天不必上全套框架,几条写清楚的规则加一两个 CI 检查就够起步。

阶段事先约定事后检查
开干前拉到钉住的契约,先读「不做什么」找不到契约仓或提交号对不上 → 拦住
实现中对外接口或事件有变,必须先改契约动了对外面却没更新版本引用 → 失败
合入前契约已进主干,引用是合并后的版本CI 校验
归档后把教训写回规则,删掉过时规则先用人肉清单也行

换仓继续干时:跨业务仓的活,两边都重读同一份产品契约版本;动公共依赖时,还要带上「现在是试点还是批量」,以及迁移窗口关没关。

怎样算产品做完:场景说明和可跑用例

两个仓单测都绿了,不等于产品做完。验收建议拆开:

层写什么放哪
场景说明人和 Agent 都能读的跨模块「给定 / 当 / 则」产品契约仓(写在当前变更里,或归档进 openspec/specs/)。普通 OpenSpec 材料就行,别发明新文件类型。
可跑用例自动化脚本、数据、环境TE 维护的 e2e 仓

两边要能对上:场景说明写用例路径或编号;用例写明对照的 contracts@版本 和场景编号。缺一边,发布就不该过。OpenSpec 不负责跑用例,那是 CI 和测试工程的事。

少抄两份旅程正文:

  • 面向用户的产品场景,只放在产品契约仓;
  • 平台契约仓只放兼容、迁移类场景;
  • 需要时产品侧变更引用平台契约的某个版本,而不是再抄一遍。

业务多仓产品通常一个产品 e2e 仓就够;平台很重时,再按影响面加消费方冒烟、分批跑。更广的 E2E 讨论见 AI4SE E2E 最佳实践。

尽量别这样干

  • 把跨仓行为的 Spec 塞进某个「主业务仓」,别的仓变成二等公民
  • 每个仓复制同一份契约,指望评审能对齐
  • 把实现任务清单写进契约仓,规划层变成合订本
  • 以为开了多根窗口或 workset,契约就有主、版本就钉住了
  • 契约和代码靠口头约定同一波发布,没有冻结版本
  • 用「各仓单测都绿」冒充产品做完
  • 平台先发包,再补兼容说明
  • 以为必须先学会 Stores beta 才能按这套做

动手前勾一遍

  1. 这条是「只动本仓」「跨几个业务仓」,还是「动公共依赖」?四行写进提案了吗?
  2. 契约正文在哪个仓?评过时冻过版本、合完后换成最终版了吗?
  3. 各代码仓的变更是否只有引用、没有抄契约正文?
  4. 配置或规则里有没有写「写代码前先读钉住的契约」?
  5. TE 的场景说明和可跑用例,是不是指向同一契约版本?
  6. 若是公共依赖:影响面、迁移窗口、分批和关窗条件写清了吗?

目录以后再磨也行。先把变更顺序跑通,再用规则把上下文钉在仓里的材料上。Stores 可有可无——普通契约仓,今天就能开工。

参考