Skip to content

fix(steward): publish the team plan readback and keep its receipt valid - #4528

Merged
huangruiteng merged 1 commit into
mainfrom
codex/team-plan-readback
Sep 16, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/team-plan-readback

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Problem

A confirmed team plan is supposed to return one readback of what exists, and that was not true for two reasons.

  1. The apply computed the full lane_todo_ids set and then dropped it when building the published receipt, so a multi-lane plan reported only the first lane's Todo. An owner could not see from the receipt that lanes two and three now have work.
  2. The receipt validator required a non-empty monitor_key for every kind, while a team plan is not a monitor and its receipt carries monitor_key: null. A settlement journal holding a team-plan receipt therefore failed validate_governed_transition_receipts, which is the validator the governed capability journal path runs over its stored receipts. Reproduced against the current main: the team-plan receipt is produced, then rejected with governed transition proposal receipt monitor_key is invalid.

What changed

  • monitor_key is typed per kind: a monitor transition must still name its key, a team-plan receipt must not invent one (None), and an empty string is still invalid everywhere.
  • The apply publishes lane_todo_ids, a bounded and deduplicated list of at most 8 lane Todo ids. It is the single additive exception to the closed, persisted receipt field set: a receipt written before the field still validates, and an unknown extra field is still refused.
  • The RFC section that records this intake no longer lists the readback as an open gap, in both the English and Chinese editions.

Changed surfaces

  • loopx/control_plane/work_items/governed_transition_proposal.py (receipt build + receipt validator).
  • tests/test_steward_team_plan_apply.py (two-lane readback, replay, validator shape rules).
  • docs/architecture/rfcs/harness-selection-dsh-pi-v0.md, .zh-CN.md (gap list).

Validation

  • tests/test_steward_team_plan_apply.py, tests/test_steward_team_plan_preview.py, tests/extensions/test_governed_capability_execution.py, tests/extensions/test_external_capability_admission.py: 62 passed.
  • New negative coverage: a repeated, malformed, oversized, empty or non-list readback is refused; a monitor receipt without its key is refused; a team-plan receipt with a monitor key is refused; an unknown extra receipt field is refused; the pre-existing receipt shape still validates.
  • examples/docs-governance-smoke.py: ok.
  • loopx canary premerge --from-git-diff: merge_gate_passed=true, self_merge_allowed=true, manual_holds=0, failures 0; one advisory inherited failure (examples/control_plane/control-plane-maintainability-ratchet-smoke.py, baseline, does not mention the changed files).

Boundaries

No new authority and no new write path: the change only decides what the existing settlement receipt reports and how it is validated. The readback is a list of Todo ids the apply already created or reused, capped at the plan's lane limit.

A confirmed team plan is supposed to return one readback of what exists. Two
receipt defects kept that promise from being true.

First, the apply computed the full `lane_todo_ids` set and then dropped it when
it built the published receipt, so a multi-lane plan reported only the first
lane's Todo and an owner could not see that lanes two and three now have work.

Second, the receipt validator required a non-empty `monitor_key` for every kind,
while a team plan is not a monitor and its receipt carries `monitor_key: null`.
A settlement journal holding a team-plan receipt therefore failed
`validate_governed_transition_receipts`, which is the validator the governed
capability journal path runs over its stored receipts.

Both are fixed inside the receipt contract rather than beside it:

- `monitor_key` is now typed per kind: a monitor transition still must name its
  key, and a team-plan receipt must not invent one.
- `lane_todo_ids` is published by the apply as a bounded, deduplicated list of
  at most 8 lane Todo ids. It is the single additive exception to the closed,
  persisted receipt field set, so a receipt written before it still validates
  and an unknown extra field is still refused.

The RFC section that records this intake no longer lists the readback as an open
gap, in both the English and Chinese editions.

Verified: tests/test_steward_team_plan_apply.py, tests/test_steward_team_plan_preview.py,
tests/extensions/test_governed_capability_execution.py and
tests/extensions/test_external_capability_admission.py 62 passed, including a
two-lane plan whose receipt names both Todos, a replay that reports the same
readback, and negative cases for a repeated, malformed, oversized, empty or
non-list readback plus a monitor receipt that omits its key. Examples/docs-governance-smoke.py ok.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval conclusion (author-owned PR; GitHub blocks formal self-approval)

一、变更内容

  • monitor_key 改为按 kind 定型:monitor 类回执仍必须点出自己的 key;团队计划不是 monitor,回执必须是 None 而不是编一个或填空串;空串在任何 kind 下仍然非法。
  • 落地时发布 lane_todo_ids:本次结算确保的每一条 lane Todo,去重、上限 8(等于 lane 上限)。它是那个封闭且持久化回执字段集的唯一加性例外——旧回执(无该字段)照样通过校验,未知多余字段仍被拒绝。
  • RFC(英文 + 中文镜像)中把"回执只带第一条 lane Todo"从缺口清单里移除。

二、依据与一致性

  • 两个问题都是先复现再修的。第一条:_apply_team_plan 里算了 lane_todo_ids,但生成回执时没有带上,多 lane 计划的"一份回读"实际只报一条。第二条更严重:回执校验器对所有 kind 要求非空 monitor_key,而团队计划回执是 monitor_key: null,因此在当前 main 上该回执会被 validate_governed_transition_receipts 拒绝报 monitor_key is invalid——而受治理能力 journal 路径正是用它校验自己存的回执(governed_capability_execution.py)。也就是说,带团队计划回执的 journal 会校验失败。
  • 与既有不变量一致:回执字段集保持封闭(只开放一个有界加性字段)、receipt 仍带 proposal digest、重放仍按 digest 幂等、公开安全校验照旧。
  • 与"回执是持久化状态"的兼容约束一致:旧回执不必改写就能通过;新字段有形状约束(list、1..8、去重、todo_<id> 形状),不会表达非法状态。

三、验证

  • tests/test_steward_team_plan_apply.py、tests/test_steward_team_plan_preview.py、tests/extensions/test_governed_capability_execution.py、tests/extensions/test_external_capability_admission.py:62 passed。
  • 新增覆盖:两条 ready lane 的计划,回执点名两条 Todo 且 todo_id 仍是第一条(旧读取方不受影响);重放结算报告同一份回读且不新增第二行;重复/形状非法/超长/空列表/非列表的回读被拒;monitor 回执缺 key 被拒;团队计划回执带 monitor key 被拒;未知多余字段被拒;旧形状回执仍通过。
  • examples/docs-governance-smoke.py:ok。
  • loopx canary premerge --from-git-diff:merge_gate_passed=true、self_merge_allowed=true、manual_holds=0、failures 0;1 条 advisory 继承失败(control-plane-maintainability-ratchet-smoke,基线且未提及本 diff 文件,已在此记录)。

四、风险与残余缺口

  • 兼容性:新字段只在团队计划落地时出现,其它 kind 的回执逐字节不变;但反向不成立——一个更老的读取方若用旧校验器读新回执,会因为多出字段而报错。TS 侧把 transition_receipts 当不透明有界数组透传,所以这是 Python 侧的有界风险,已在 RFC 与 PR 说明中写明。
  • 仍未做(其它切片):生产上没有调用方传 team_plan_context;被确认的计划还没有 Chat 侧落地路径;多 lane 预览还没有前端确认面。
  • 本次没有引入 claim/lease 预留:lane 的首个 Todo 仍是普通 open 的 advancement Todo,走既有配额与租约路径。

五、结论

批准以 admin squash 合并(self_merge_allowed=true)。这是对既有结算回执的两个具体缺陷的修复加一个有界读回字段,不改权限、不加写入路径、可回滚;第二个缺陷还修掉了"带团队计划回执的 journal 无法校验"这一真实故障。

English verdict: Approved for an admin squash merge. The team-plan settlement receipt is now valid under its own validator — monitor_key is typed per kind, so a plan no longer fails a check that only monitors should satisfy — and the apply publishes a bounded lane_todo_ids readback, the one additive exception to the closed, persisted receipt field set. 62 focused tests pass with new negative coverage, the docs governance smoke passes, and the canary premerge gate is green apart from one unrelated advisory baseline failure.

@huangruiteng
huangruiteng merged commit 856dc9a into main Sep 16, 2026
6 checks passed
@huangruiteng
huangruiteng deleted the codex/team-plan-readback branch September 16, 2026 10:12

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes conclusion (author-owned PR; GitHub blocks formal self-review)

动机

这个 PR 修两个真实缺陷,我都能复现:

  1. apply 已经算出完整的 lane_todo_ids,但构造回执时把它丢了,多 lane 计划只报第一条 lane 的 Todo,业主从回执看不出第二、三条 lane 已经有工作。
  2. 回执校验器对所有 kind 都要求非空 monitor_key,而团队计划回执带的是 monitor_key: null,于是团队计划回执必然过不了 validate_governed_transition_receipts(这正是我在 #4524 里提的那条 P1)。

动机和问题都对得上,方向也没问题:这是把"落地结果"和"回执契约"重新对齐,而不是顺手扩能力。

改动思路

两处改法都选了正确的归属:monitor_key 按 kind 分型(monitor 必须点名自己的 key,团队计划不得编造 key,空串处处非法),回执字段集保持"封闭 + 唯一一个有界加性字段",老回执仍然有效。lane_todo_ids 上限 8、要求唯一、走 validate_public_safe_value,与预览的 8 lane 上限对齐。

测试也按"语义"而不是"当前输出"写:既验正例(两条 lane、重放、老形状仍可读),也验反例(空列表、重复、形状不对、超过 8、非列表、多余字段、monitor 缺 key)。文档两版同步把回读从缺口列表里删掉,并保留仍缺的两项。

问题出在生产者与消费者不一致那一环,见下面的 P1。

具体改动

governed_transition_proposal.py:新增 _OPTIONAL_RECEIPT_FIELDS = {"lane_todo_ids"}、_LANE_TODO_ID_LIMIT = 8、_LANE_TODO_ID 正则;validate_governed_transition_receipts 改成 _RECEIPT_FIELDS <= fields <= _RECEIPT_FIELDS | _OPTIONAL_RECEIPT_FIELDS,monitor_key 按 kind 分型,lane_todo_ids 校验 1..8、唯一、形状合法;回执构造处新增发布该字段。测试 +102,两个 RFC 版本同步更新。

我按 head e25399e9、base 1b649341 核对了回执构造、校验器、_apply_team_plan、todo owner 的 add_goal_todo/add_todo_to_lines 以及 todos/contract.py 的规范 id 契约,跑了 PR 里点名的四个测试文件:62 passed,git diff --check 干净,回执文本过 validate_public_safe_value。

P1(阻塞):发布出去的 lane 回读没有去重。PR 正文和校验器都把这个字段描述为"bounded deduplicated list"(校验器明确要求 len(set(ids)) == len(ids)),但生产端是原样拷贝:

lane_todo_ids = result.get("lane_todo_ids")
if lane_todo_ids:
    receipt["lane_todo_ids"] = [str(item) for item in lane_todo_ids]

所有多 lane 测试用的都是不同 agent、不同文本,所以重复这一支从未被覆盖。它是可达的:validate_steward_team_plan_preview 只要求 lane_id 唯一,而 canonical Todo owner 会把"同一 agent + 同一 first_todo 文本"解析成同一个确定性 Todo 身份。我在这个 head 上用真实结算路径复现:两条 ready lane、同一 agent、同一首步文本,得到 lane_todo_ids = ['todo_961b6c683536','todo_961b6c683536'],随后 validate_governed_transition_receipts 抛 governed transition proposal receipt lane_todo_ids is invalid;再调一次 settle,在 validate_governed_transition_receipts(existing_receipts)(第 387 行)就同样的错误直接失败。

也就是说:这个 PR 想修的失败模式(回执自己读不出来、重放直接报错)仍然存在,只是触发条件从 monitor_key 换成了重复 lane 身份。Todo 行已经建好,回执却读不回、不能重放、不能审计,正是最贵的那类后果。

最小修法是在生产端做保序去重(例如 list(dict.fromkeys(...))),并补一条"两条 lane 共用同一 Todo 身份"的回归测试。如果作者认为两条 lane 本来就该共用一条 Todo,那要改的是校验器的唯一性规则——但生产端和校验器不能继续各说各话。

P3(非阻塞):校验器自造了第二条(更窄的)todo id 规则 ^todo_[A-Za-z0-9]{1,40}$,而仓库的规范契约是 TODO_ID_PATTERN = ^todo_[a-z0-9_-]{3,64}$。它在要紧的地方更窄(带 -/_ 的 id 在别处被 normalize_todo_id 接受,这里会被拒),在另一头更宽(1-2 个字符的后缀这里接受、规范侧拒绝)。当前唯一的生产者是 todo_<12 hex>,所以暂时是潜在问题,但"持久化字段拒绝 owning 契约接受的 id"是可以把一张合法回执变成读不回的回执的。复用 TODO_ID_PATTERN/normalize_todo_id(至少对齐字符集与上下界)能让仓库只留一条 id 规则。

顺带确认:#4524 那条 monitor_key P1 在这个 head 确实修好了——团队计划回执带 monitor_key: None 可以通过,monitor 回执缺 key 仍被拒,未知多余字段仍被拒。

对主干的风险

改动落在持久化回执契约上,因此风险集中在"写进去的形状和读回来的规则是否同一份":

  • 共享面是回执校验器(所有 governed transition kind 共用)。按 kind 分型后,monitor 路径行为不变,这一点有测试兜住;老回执因为字段可选仍然可读,多余字段仍被拒。
  • 唯一实质风险就是上面那条 P1:生产端可以写出自己拒绝的形状,而且失败被推迟到读回时(日志/审计路径),不在写入时暴露。
  • P3 属于未来的漂移风险:仓库里多了一条与 canonical id 契约不同的局部规则。

没有新增写权限、没有新增 CLI/状态存储、没有触碰准入或 quota。

我的整体评价

这是一次方向正确、边界合理的契约修复:monitor_key 分型解决了 100% 触发的那条 P1,lane_todo_ids 把"落地到底建了什么"变成可读的回读,测试正反例都写在语义层,两版 RFC 同步更新而不是留旧话。

但它自己的目标还没有完全达成:发布出去的回读没去重,导致"两个 lane 落到同一 Todo 身份"时,回执会被自己的校验器拒绝——和它刚修好的那条失败模式同类。建议在合流前补上生产端保序去重与对应回归测试(并顺手把 id 规则收敛到 canonical 契约),把生产者和消费者对齐到同一份契约。

English verdict: REQUEST_CHANGES (exact head e25399e)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant