Skip to content

feat(presentation): compile a provider-neutral confirmation frame for a plan card - #4579

Merged
huangruiteng merged 2 commits into
mainfrom
codex/steward-plan-card-frame
Sep 16, 2026
Merged

huangruiteng merged 2 commits into
mainfrom
codex/steward-plan-card-frame

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

动机

业主问:LoopX 已经有飞书卡片能力,团队计划能不能复用? 我核了代码,答案是可以复用——但缺一块:

loopx/extensions/lark/goal_channel_operation.py 已经交付了卡片确认的整个 Lark 半边(不可转发的 Card 2.0、确认/拒绝按钮、card.action.trigger 消费者、操作者成员与租户校验、重放保护、卡片读回与结果卡片修补),并且卡片内容来自 language-neutral 的 operation_review_frame_v0。问题在于:compileOperationReviewFrame 只对 operation.execute 生效,因为它的身份是 operation envelope;团队计划没有 envelope,于是卡片内容这一层无法复用(内容目前只存在于 dashboard 的 teamPlanFields reducer 里,是 app-local 的)。

改动思路

  • 补上这块共享缝:compileReviewCardFrame 为已校验的 team.plan 产出 review_card_frame_v0。
  • 身份 = proposal id + apply 会重新校验的 expected_state_fingerprint(不是 envelope),决策 = confirm/reject。
  • 正文是数据:goal、objective、每条 lane 的 Agent 与首个有界 Todo(或它的 gap 与原因)、gap 汇总、quota 包络、停止条件。
  • 固定标签保持为 key(titleKey/warningKey/confirmLabelKey...),字段是 {key, value} 对——控制面 TS 里一个中文都没有,语言属于渲染它的那个面。
  • 以 reviewCardFrame 加性挂在既有 ActionReviewPlan 上,复用同一个 runtime handler id,Python 侧不需要新接口。

具体改动

  • loopx/control_plane/presentation/action_review_plan.ts:新增 ReviewCardFrame 类型与 compileReviewCardFrame(未准入的预览、其他 action kind、缺 fingerprint 一律返回 undefined;operation 提案仍走 operation frame)。
  • tests/control_plane_ts/action_review_plan.test.ts:覆盖身份/决策/ready 与 gap lane/quota/stop、key 形状(^[a-z][a-z0-9_]*$)、以及四类拒绝。
  • loopx/extensions/lark/README.md:在 "Human-confirmed typed operations" 一节写清复用边界。
  • RFC EN/ZH:记录复用映射 + 仍缺的三件事(提问受众的投递路由、走 Chat action service 的回调效果、外部管家受众能否做这次持久写入的明确回答)。
  • 第二笔 commit 出货重建后的 chat bundle(控制面是被打包进去的)。

对主干的风险

  • 纯加性:既有 operationFrame 路径、卡片渲染、回调、apply 语义都不变;目前不会投递任何计划卡片。
  • 新帧只带数据与 key,不携带授权,也不替代任何回执;apply 仍然重新校验并走 canonical owner。
  • 已知口径:field_coverage 只覆盖 preview 形态;卡片里 quota/stop 是展示数据,不是执行约束(RFC 里已有的既有限制,未改变)。

我的整体评价

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

这一刀刻意只做"复用的那一块",因为剩下两件事(投递路由 + 回调效果)需要先定一个产品/授权问题:外部管家受众是否可以让 LoopX 做这次持久写入。先把它显式写进 RFC 再实现,比直接调通一条没人同意过的写路径更负责。

验证:tests/control_plane_ts/action_review_plan.test.ts 新增用例通过(该套件在本机有 325 个与 PostgreSQL/CLI 环境相关的前置失败,未改动的 checkout 上同样 325 个);docs governance 与 loopx canary premerge --from-git-diff 通过。

English verdict: APPROVE

… a plan card

LoopX already ships the Lark half of a card confirmation: a non-forwardable Card
2.0 with confirm/reject buttons for a typed `operation.execute` proposal, a
`card.action.trigger` consumer, operator membership and tenant verification,
replay protection, card readback and result-card patching. Reusing that shell for
a steward team plan was blocked on one thing: the card content is compiled from
`operation_review_frame_v0`, which can only describe an operation proposal because
its identity is the operation envelope.

`compileReviewCardFrame` now compiles `review_card_frame_v0` for a validated
`team.plan` proposal. Its identity is the proposal plus the state fingerprint the
apply re-validates against, its decisions are confirm/reject, and its body is the
data the plan already carries -- goal, objective, each lane with its Agent and
first bounded Todo or its gap and reason, the gap summary, the quota envelope and
the stop condition. Fixed labels stay keys (`titleKey`, `warningKey`, ...) and
fields are `{key, value}` pairs, so this boundary stays language-neutral and each
surface owns its own words. The frame is additive on `ActionReviewPlan`; nothing
posts a card yet and no surface changes what it renders.

The Lark README and the team-intake RFC record the reuse boundary this rests on,
including what a plan card still needs: a delivery route for the audience that
asked (a manager group is not a Goal channel binding), a callback effect that
applies through the Chat action service instead of claiming an operation
envelope, and an explicit answer for whether an external manager audience may
perform this durable write.

Validation: `tests/control_plane_ts/action_review_plan.test.ts` covers the frame
(identity, decisions, ready and gap lanes, envelope and stop condition, key-shaped
labels) and its refusals (not an admitted preview, another action kind, missing
state fingerprint, operation proposals keep the operation frame). docs governance
and `loopx canary premerge --from-git-diff` pass.

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

The chat surface compiles its action review plan from the control plane, so the
bundle carries `compileReviewCardFrame` now that it exists. Rebuilt with
npm run build:chat: new entry assets/index-Cp9Ll6py.js, the previous generation
assets/index-CXvjZarX.js stays retained and assets/index-Cb7S1yHW.js retires.
No rendered surface changes in this commit.

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.

Head reviewed: a5028fa

动机

核对"飞书卡片能力能否复用":Lark 半边(Card 2.0 投递、按钮回调、操作者校验、重放保护、读回、结果修补)已存在,但卡片内容来自只对 operation.execute 生效的 operation_review_frame_v0,团队计划因此没有可复用的卡片内容层。

改动思路

新增 language-neutral 的 review_card_frame_v0:身份 = proposal + state fingerprint,决策 = confirm/reject,正文 = 计划的既有数据,固定标签保持 key。加性挂在 ActionReviewPlan 上,复用同一 handler id。

具体改动

action_review_plan.ts 新增类型与 compileReviewCardFrame;TS 用例覆盖身份/决策/ready+gap lane/quota/stop/key 形状/四类拒绝;Lark README 与 RFC EN/ZH 记录复用边界与仍缺的三件事;第二笔 commit 出货 bundle。

对主干的风险

纯加性、不投递卡片、不改变既有 operation 路径与 apply 语义;帧只带数据与 key,不带授权。

我的整体评价

审查了自己的 diff:compileReviewCardFrame 对非准入预览/其他 kind/缺 fingerprint 一律 undefined,operation 提案不会被误判为计划卡片;测试断言 key 形状以守住"标签不下沉到控制面"这条边界。

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

English verdict: APPROVE

@huangruiteng
huangruiteng merged commit cd1b32f into main Sep 16, 2026
6 checks passed
@huangruiteng
huangruiteng deleted the codex/steward-plan-card-frame branch September 16, 2026 16:41
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