Skip to content

feat(steward): let one owner confirmation apply a confirmed team plan - #4535

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

huangruiteng merged 1 commit into
mainfrom
codex/team-plan-chat-apply

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Problem

The intake could validate and surface a plan, and nothing could apply it. The only apply entry point
was a governed capability execution journal, so an owner confirmation had nowhere to land and the
lanes were never created — the "one sentence starts a team" path stopped one step short.

What changed

The typed Chat action surface owns a team.plan action.

  • Preview validates the plan against that Goal's own registered Agents and the host's shipped
    advancement action kinds, and records the registry it validated against.
  • Apply re-validates the same payload through the governed transition owner at PRE_SETTLEMENT.
    The action never becomes a second writer: the canonical Todo owner still decides whether a lane row
    is added or reused, a gap lane still creates nothing, and an unknown or mis-named Goal is still
    refused.
  • Staleness is bound to the registry that supplies the registered Agents, so a registration change
    between preview and apply marks the proposal stale instead of applying a plan whose staffing drifted.
  • The receipt returns the lane readback (lane_todo_ids) plus the first lane Todo, so a confirmation
    reports what now exists.

Two details are deliberate and recorded in the code:

  1. The plan is stored as the payload the owner confirms, not as the validator's normalized output,
    because the validator is not idempotent over its own output (a gap lane normalizes to
    declined_first_todo) and the apply is supposed to re-validate rather than trust stored parameters.
  2. The apply passes the actor through (requested_by, default owner) while the lanes' work is
    attributed to each lane's own agent_id, which is the identity that owns the Todo.

The RFC records the new shipped item in both editions and narrows the remaining gaps to the frontend
confirmation surface and the intent-revision binding.

Changed surfaces

  • loopx/chat_action_store.py (action kind), loopx/chat_action_normalization.py (preview
    normalization), loopx/chat_actions.py (preview classification + apply dispatch and method).
  • tests/test_chat_team_plan_action.py (new).
  • docs/architecture/rfcs/harness-selection-dsh-pi-v0.md and .zh-CN.md.

Validation

  • tests/test_chat_team_plan_action.py, tests/test_chat_operation_actions.py,
    tests/test_steward_team_plan_preview.py, tests/test_steward_team_plan_apply.py,
    tests/test_chat_manager_context.py: 47 passed. Coverage includes a confirmed plan that creates its
    ready lane's first Todo and returns the lane readback, a lane with an unregistered Agent that
    previews with a typed gap and creates nothing, a plan for another Goal refused at preview, and a
    registration change that makes the confirmation stale.
  • The existing operation-action suite caught a first attempt that shadowed a module-level import
    inside the normalizer (UnboundLocalError); that is fixed and the suite passes.
  • examples/docs-governance-smoke.py: ok.
  • examples/loopx-steward-managed-chat-smoke.py: ok (real dsh segment completes).
  • loopx canary premerge --from-git-diff: merge_gate_passed=true, self_merge_allowed=true,
    manual_holds=0, failures 0.

Boundaries

No new authority: the confirmation still cannot widen the plan, the preview still creates nothing on
its own, and every write goes through the canonical Todo owner under the governed transition owner's
settlement phase. Still open after this: the frontend surface that sends the confirmation, and the
intent-revision binding on a materialized lane Todo.

The intake could validate and surface a plan, and nothing could apply it: the
only apply entry point was a governed capability execution journal, so an owner
confirmation had nowhere to land and the lanes were never created. The typed
Chat action surface now owns that path.

A `team.plan` action validates the plan at preview time against the Goal's own
registered Agents and the host's shipped advancement action kinds, and its apply
re-validates the same payload through the governed transition owner at
`PRE_SETTLEMENT`. The action never becomes a second writer: the Todo owner still
decides whether a lane row is added or reused, gap lanes still create nothing,
and an unknown or mis-named Goal is still refused.

Two consequences are deliberate. The plan is stored as the payload the owner
confirms rather than as the validator's normalized output, because the validator
is not idempotent over its own output (a gap lane normalizes to
`declined_first_todo`) and the apply is supposed to re-validate rather than trust
stored parameters. And the fingerprint a confirmation is bound to is the registry
that supplies the registered Agents, so a registration change between preview and
apply marks the proposal stale instead of applying a plan whose staffing drifted.

Verified: tests/test_chat_team_plan_action.py, tests/test_chat_operation_actions.py,
tests/test_steward_team_plan_preview.py, tests/test_steward_team_plan_apply.py and
tests/test_chat_manager_context.py 47 passed, including a confirmed plan that
creates its ready lane's first Todo and returns the lane readback, a gap lane that
creates nothing, a plan for another Goal refused at preview, a registration change
that makes the confirmation stale, and the existing operation-action suite (a
first attempt shadowed a module-level import inside the normalizer and was caught
by that suite). examples/docs-governance-smoke.py and
examples/loopx-steward-managed-chat-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)

一、变更内容

  • 类型化 Chat action 面新增 team.plan 动作(ACTION_KINDS / SUPPORTED_ACTION_KINDS):
    • 预览:用该 Goal 自己已注册的 Agent 与本机已 ship 的 advancement action kind 校验计划,并把"校验所依据的 registry"记为指纹。
    • 落地:把同一份载荷交给受治理提案所有者在 PRE_SETTLEMENT 相位重新校验;动作本身不成为第二个写入者——仍由 canonical Todo owner 决定 lane 行是新增还是复用,gap lane 仍不建任何东西,未知或点错 Goal 仍被拒绝。
    • 过期:指纹绑定到提供注册 Agent 的 registry,因此预览与落地之间发生注册变化时提案变 stale,而不是把 staffing 已漂移的计划落地。
    • 回读:回执带 lane_todo_ids 与首条 lane Todo,确认一次就能报出"现在存在什么"。
  • 两处刻意的设计决定写进代码与 PR:
    1. 存储的是业主确认的那份载荷,不是校验器的归一化产物——校验器对自己的输出不幂等(gap lane 会归一化成 declined_first_todo),而落地本应重新校验而不是信任已存参数;
    2. 落地把执行者(requested_by,默认 owner)透传,而工作归属仍记在各 lane 自己的 agent_id 上——那才是拥有该 Todo 的身份。
  • RFC 双语记录该 shipped 条目,并把剩余缺口收窄为"前端确认面"与"lane Todo 的意图修订绑定"。

二、依据与一致性

  • 依据是你点出的最后一处缺口本身:准入已经能浮现预览,但没有落地路径,业主确认无处落地,lane 永远建不出来;而本轮把落地接到既有的受治理提案所有者,因此没有新增写入路径、没有新增权限。
  • 与仓库既有的 typed action 模式一致:走 preview → create_preview(expected_state_fingerprint=...) → apply → store.apply(receipt=...),与 monitor.create 同一形状;指纹语义按"这次变更依赖哪类状态"选择(monitor.create 用 registry 指纹,本动作同理)。
  • 与"不要信任存储参数"一致:落地重新校验,且 _apply_team_plan 只用 lane 自己的 agent_id 建 Todo。
  • 计划仍 applies: false、预览不产生任何写入;落地仍经 canonical owner 与 settlement 相位。

三、验证

  • tests/test_chat_team_plan_action.py(新增)、tests/test_chat_operation_actions.py、tests/test_steward_team_plan_preview.py、tests/test_steward_team_plan_apply.py、tests/test_chat_manager_context.py:47 passed。新覆盖:确认后为 ready lane 建出首条 Todo 并返回 lane 回读;未注册 Agent 的 lane 以 typed gap 预览且确认后不建任何东西;点错 Goal 在预览即被拒;注册变化使确认变 stale 且不建 Todo。
  • 回归被既有套件抓到:我第一版在 normalizer 里做了同名局部 import,遮蔽了模块级 registered_agent_ids_for_goal,导致 tests/test_chat_operation_actions.py 5 条失败(UnboundLocalError)。已修(去掉局部 import,复用模块级导入),该套件与全部相关套件现在通过——这也是我把该套件纳入验证的原因。
  • examples/docs-governance-smoke.py:ok;examples/loopx-steward-managed-chat-smoke.py:ok(真实 dsh segment 完整跑完)。
  • loopx canary premerge --from-git-diff:merge_gate_passed=true、self_merge_allowed=true、manual_holds=0、failures 0。

四、风险与残余缺口

  • 语义边界:确认只能建成"计划里那些 lane 的首个有界 Todo",不能扩大计划(_apply_team_plan 不读除计划外的字段,且会拒绝与结算不符的 Goal)。
  • 未做:前端确认面(当前只有 API 侧调用方与测试;前端还需要渲染 lanes/gaps 并把确认发成该动作);lane Todo 的意图修订绑定;peer directory 的实现接线。
  • 归因:requested_by 目前只是被透传(团队计划落地不消费 actor),我没有为它发明新的权威语义;后续若要在回执里记录"谁确认的",那是一次显式的契约扩展。

五、结论

批准以 admin squash 合并(self_merge_allowed=true)。单一目的、可回滚、无权限扩大:它把"一句话拉起团队"从"能展示计划"推进到"一次确认真的建出 lane",并且所有写入仍走既有 canonical owner;回归由既有套件捕获并已修复。

English verdict: Approved for an admin squash merge. The typed Chat action surface now owns a team.plan action whose preview validates against the Goal's registered Agents and whose apply re-validates through the governed transition owner at PRE_SETTLEMENT, so one owner confirmation creates each ready lane's first bounded Todo and returns the lane readback, with a registration change making the confirmation stale instead of silently misapplying. 47 focused tests pass (including the operation-action suite that caught a real import-shadowing regression I introduced and fixed), both smokes pass, and the canary premerge gate is green.

@huangruiteng
huangruiteng merged commit 303c9f4 into main Sep 16, 2026
5 checks passed
@huangruiteng
huangruiteng deleted the codex/team-plan-chat-apply branch September 16, 2026 11:00

@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)

动机

缺口描述准确:#4533 之后预览能被浮现,但没有任何东西能落地——唯一的落地入口是受治理能力执行 journal,业主的确认无处可去。也就是说,前几片交付的是一个"能看不能用"的契约,业主确认了也不会发生任何事,后续切片(前端确认面、意图修订绑定)也没有可挂载的真实效果。补上 Chat 侧的落地路径是有价值的必要一步。

改动思路

核心判断我完全同意:Chat 层不自己写 lane。_apply_team_plan 只做指纹校验,然后把同一份载荷交给 settle_governed_transition_proposals 在 PRE_SETTLEMENT 相位重新校验并创建 Todo——所以"谁能建 lane"仍然只有一个 owner,Chat 侧只是多了一个"经业主确认的入口"。这比在 Chat 层直接调 add_goal_todo(行数更少但会产生第二个 writer)正确得多。

其余几处也选得对:

  • 确认绑定到确切的那份 plan。 预览时把计划与指纹一起入库,落地时用库里的参数;regenerate 是生成新的提案并链接回去(不是就地改参数),所以业主确认的对象不会被悄悄替换。
  • 陈旧判断在写入之前。 指纹不匹配就直接把提案标成 stale 并返回,什么都不写。
  • 权限类别与邻居一致。 durable_write,与 todo.create/todo.update/monitor.create 同级,而不是自造更强的一类。

具体改动

三处 runtime(chat_action_normalization.py 新增该 kind 的参数校验、chat_action_store.py 的 kind 列表加一项、chat_actions.py 新增 apply 分支与预览证据/权限),新增 tests/test_chat_team_plan_action.py(163 行),RFC 中英两版把"没有 Chat 侧落地路径"的缺口改写成第 5 项"从 Chat 确认落地",并保留仍缺的两项。

我没有只跑单元测试,而是走了真实生产入口(本地 Chat HTTP API),用一个临时 registry/state fixture 做完整序列:

POST /api/actions/preview  team.plan → 201 preview_ready, permission durable_write
POST /api/actions/<id>/apply        → 200 applied
state: 恰好 1 条 loopx:todo(ready lane);gap lane 什么都没建
receipt: {"outcome":"team_plan_applied","resource_ids":{"goal_id":…,"todo_id":"todo_…","lane_todo_ids":["todo_…"]}}
再次 apply(重放)                  → 200 applied,行数仍为 1
预览后改动 registry 再 apply        → 409 status=stale,未写入任何行

另外 pytest tests/test_chat_team_plan_action.py tests/test_chat_operation_actions.py tests/test_steward_team_plan_apply.py tests/test_steward_team_plan_preview.py tests/test_chat_manager_context.py → 47 passed,examples/loopx-chat-actions-smoke.py → ok,git diff --check 干净。

两个 P3(非阻塞,均已记入 findings):

  1. 受治理结算的回执被丢弃。 settle_governed_transition_proposals 是用 existing_receipts=[] + checkpoint=lambda _receipts: None 调用的,所以 governed owner 返回的那份规范回执(proposal_digest/kind/action/todo_id/lane_todo_ids)没有被持久化到任何受治理的地方;持久记录是 Chat action 的回执(保留派生后的子集)。后果有两点:读受治理 transition journal 的消费者看不到这次结算(同一份审计轨迹被拆成两个 store);由于 existing_receipts 为空,governed owner 也无法识别同一 proposal_id 的重放——不过这条路径因为 Todo owner 的 add/reuse 与 action store 的 applied 状态仍然幂等,所以不是重复写入的 bug,而是可追溯性与单一记录的问题。把 journal 的回执传进去并通过既有 checkpoint 持久化,或在 RFC 明确写"Chat 来源的结算由 Chat action store 持有记录"(现在 RFC 仍写"今天的落地入口是受治理能力执行 journal"),两者都能让契约自洽。
  2. 陈旧指纹比证据文本说的更粗。 预览存的是 _registry_fingerprint()(整个 registry 文件的 SHA-256),而预览证据写的是"该 Goal 已注册 Agent 与本机 advancement action kind"。我验证了后果:一个 preview_ready 提案,只要往 registry 里追加一个不相关的 Goal,下一次 apply 就变成 stale 且不写入。失败方向是安全的(让业主重新确认),且邻居 todo.update/monitor.update 用的是把 registry 与目标 state 混合的 _goal_state_fingerprint;把指纹收窄到"这个 Goal 的注册事实(可加 action kind 集合)"就能既保住证据文本承诺的保证,又不因无关变更误判陈旧。

对主干的风险

这是本项目里第一次出现"业主确认 → 真正写入 lane"的路径,所以我把重点放在授权与幂等上:

  • 授权没有新等级:该 action 走既有 typed action 生命周期(预览→确认→落地),权限类别与相邻写类 action 相同;能调本地 action API 的调用方本来也能调 todo.create/goal.create。
  • 不会写错 Goal:计划自身必须点名 action 的 goal_id(否则预览即拒),落地又由 governed owner 按结算 Goal 重新校验,所以 #4532 的改投防护在这里仍然生效。
  • 不会重复建 lane:已 applied 的提案重放直接返回;崩溃在"已建 Todo、未存回执"之间时,重试会走 Todo owner 的 reuse 分支,回执用同一组 id 重建(我从代码路径确认,并用 HTTP 重放验证行数不变)。
  • 陈旧拒绝:写入前判指纹,stale 提案不落地,前端可 regenerate 生成新提案。
  • 残留在两处 P3:规范回执未持久化、指纹粒度偏粗;另 RFC 明确"发出确认的前端面"与"lane Todo 的意图修订绑定"仍未交付,因此这是分片边界而非遗漏。

我的整体评价

这是一次归属正确、风险控制得当的效果切片:业主一次确认经既有确认生命周期,落到唯一拥有 lane 创建权的受治理 owner,创建结果带回 lane_todo_ids 回读,陈旧与重放都有明确行为。我不仅在单测层面核对,还通过真实 HTTP 入口复现了成功、重放、跨 Goal 拒绝与陈旧拒绝四种结果,证据与结论一致(47 passed + smoke ok + 探针输出)。

建议做两处小改进(都不阻塞合并):把受治理结算的回执真正持久化(或在 RFC 明确记录归属),以及把陈旧指纹收窄到该 Goal 的注册事实。

English verdict: APPROVE (exact head 6a11727)

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