Skip to content

feat(manager): name the lanes a confirmed team plan left unstaffed - #4600

Closed
huangruiteng wants to merge 4 commits into
mainfrom
codex/steward-r1-f4-recovery-cursor
Closed

huangruiteng wants to merge 4 commits into
mainfrom
codex/steward-r1-f4-recovery-cursor

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

Control-plane and product-surface change — author hands this over unmerged. Per the owner's direction, this is proposed and reviewed on its exact head, then left for the maintainer to merge. No author self-merge, no admin bypass.

Goal And Delivered Outcome

  • Goal/source and gap: the steward team-plan confirmation path, roadmap card R1 finding F4 (docs/architecture/rfcs/loopx-overall-roadmap-v0.md §8). A confirmed plan can be applied partially — the host staffs the lanes it can and leaves the rest unstaffed — and the settlement already records gap_count. It recorded nothing about which lane stayed unstaffed or why, so the card that confirmed the plan could only report a number. An owner reading "1 lane unstaffed" had nothing to act on; the same plan read identically whether the missing Agent was unregistered or the lane asked for an action kind this host does not ship.
  • Observable before → after, with the validation row that proves it: before, a receipt carried gap_count: 1 and the confirmation drawer said 已应用,但有 1 条 lane 仍未组建 with no lane named. After, the receipt carries gap_lanes: [{lane_id, agent_id, reason_code}] beside the count and the drawer reads lane_review(agent-reviewer)仍未组建:该 Agent 未在该 Goal 注册, so the gap names the lane, the Agent it was meant to run on, and the host fact behind it. The failing-before check is the new browser assertion run against the previous runtime (the fixture previously answered an apply with {projection_verified, receipt_id} only).
  • Issue/task and intended base: R1 slice 5 on this lane's canonical Todo for the reliable team-plan commit; base main at e66615d33.

Scope And Continuation

  • Completed scope: the settlement names its unstaffed lanes; a receipt may carry gap_lanes only together with gap_count; the closed receipt field set admits the addition through one normalizer; chat_actions writes the lanes into a partial application's receipt; the dashboard reducer reads them out of the receipt and the confirmation card renders one localized line per lane, with the verbatim code as the fallback for a host reason the card has no words for; both locales and the packaged bundle are updated.
  • Remaining gap, next owner/dependency and why this boundary is right: this names the gap; it does not make a partial plan recoverable. Finishing an unstaffed lane automatically (a re-entrant apply without a fresh owner confirmation, behind an execution barrier) is the rest of F4 and is deliberately out of this PR: whether a state change that enables a lane may complete a previously confirmed plan is a separate rule from whether the confirmation recorded what it could not do, and mixing them would put an unreviewed recovery policy behind a readback change. Tracked as todo_ca8b30b77271.
  • Sibling PRs: this touches governed_transition_proposal.py, chat_actions.py and the same two dashboard files as fix(dashboard): report what a confirmed team plan actually materialized #4598 (the applied outcome line) and governed_transition_proposal.py as fix(manager): make a partial team-plan materialization recoverable #4587 (partial-materialization recovery). All three are additive and none of them is merged; whichever lands last needs a small rebase. The roadmap lines those siblings edited are intentionally untouched here to avoid stacking a third conflict on them.

Validation

  • pytest tests/test_chat_team_plan_action.py tests/test_steward_team_plan_apply.py tests/test_steward_team_plan_preview.py tests/test_manager_team_plan_guidance.py — 43 passed. New cases: a receipt may name gap lanes only with gap_count, and an empty list, a duplicated lane, a wrong field set and an unknown reason code are each refused; the apply case asserts the receipt names lane-beta with its Agent and action_kind_not_supported.
  • npm run smoke:team-plan-proposal — ok, including the new classification, line rendering, verbatim-unknown-reason and no-gap-lanes cases.
  • Dev and packaged browser scenarios — ok for all six scenarios. The new assertion failed first for the reason the sibling PR also hit: the fixture read the plan from the session previews, while injected proposals live in the action store; the lookup was corrected, which is how the wrong lookup surfaced.
  • loopx canary premerge --from-git-diff — ok, 0 failures (direct checks, 4 catalog canaries, 8 risk-profile smokes, public boundary).

Boundary

Control-plane behavior (loopx/control_plane/work_items/governed_transition_proposal.py, loopx/chat_actions.py), the dashboard presentation and its packaged bundle (apps/presentation/dashboard/**, generated loopx/web/chat), and the browser fixture (examples/personal-workspace-browser/**). Proposed for review; not self-merged.

A partial team-plan application reported only gap_count, so the card that
confirmed the plan could say how many lanes were missing but not which lane,
or why. The apply receipt now records gap_lanes (lane_id, agent_id,
reason_code) beside the count, the lanes the settlement did ensure stay in
lane_settlements, and the confirmation card reads the gap lanes out of the
receipt it already renders, so an owner can act on the gap (register the
Agent, or ask for a kind this host ships) instead of reading a number.

- governed_transition_proposal: the settlement result names its unstaffed
  lanes, a receipt may carry gap_lanes only with gap_count, and the closed
  receipt field set admits the bounded addition through one normalizer.
- chat_actions: a partial application writes the gap lanes into its receipt.
- dashboard: team-plan-preview reads the receipt's gap lanes and renders one
  localized line per lane with the host reason, falling back to the verbatim
  code for a reason this card has no words for.
- validation: receipt normalizer cases, the apply receipt case, the dashboard
  unit smoke and the dev + packaged browser scenario, which failed before the
  fixture read the plan from the action store rather than the session previews.

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)

Reviewed exact head 55fce0edb0a858bd5ce443437a5c7a8d7906a1f0 (re-read immediately before publication; unchanged). Policy revision 6. loopx pr-review --check-result returned ok: true, verdict APPROVE, for this exact head before publication.

动机

这条 PR 修的是本 lane R1/F4 里确认面读不全的那一半:团队计划被确认后可能只组建了一部分 lane,而 apply receipt 只记了 gap_count。于是确认卡只能报个数——"1 条 lane 未组建"——说不出是哪条、更说不出为什么。同一张计划,无论那条 lane 是因为该 Goal 没注册那个 Agent,还是因为本机根本不发这种 action kind,读起来一模一样。

  • 影响面:凡是带不可组建 lane 的计划,也就是 steward 提出、而本机还没注册对应 Agent 的常见情形。
  • 之后的代价:业主手上只有一个数字,既不能定位、也不知道做什么能让它跑起来(注册 Agent?换动作类型?)。
  • 这补齐的是 R1 出口里"发起面必须显示确切结果"这一条的读回面:数量之外还要有身份。
  • 判定:justified_increment——把 settlement 已经算出来的判断落进它已经写的那份 receipt,不是新造一份事实。

改动思路

让已经做出判断的 owner把判断留下来,读方只负责显示:

  • 入口:业主确认卡片 → apply → settlement 决定哪些 lane 成为 Todo、哪些是 gap。
  • 决策归属:governed_transition_proposal 本来就拥有"这条 lane 能不能组建"的判断(preview["gaps"] + 既有 reason 词表);chat_actions 写 action receipt;dashboard reducer 只把它读出来渲染。
  • 复用而非新建:新字段复用 settlement 自己的 gap 判断和既有 reason 词表(agent_not_registered / action_kind_not_supported),没有第二套 staffability 规则、没有新枚举、没有新投影端点。
  • 不产生新效果:gap_lanes 只是 gap_count 数的那些东西的名字,是派生读回,不是新写入者。

具体改动

15 个文件、+371/-19:production 110/2 分布在两个控制面文件和五个前端文件(settlement 的 gap 记录、receipt 写入与它的 normalizer、reducer 读取、drawer 一行、模型字段、四处 i18n);tests/fixtures 170/2 分布在四个文件;generated 15/15 分布在三个 bundle 文件;docs 0;mechanical moves 0。

关键代码讲解

  • loopx/control_plane/work_items/governed_transition_proposal.py settlement 结果新增 gap_lanes({lane_id, agent_id, reason_code})——lane 的 Agent 直接取自同一次循环里的 lane 记录,不做二次查找;没有 gap 时结果里根本不出现该字段,所以完整组建的 receipt 逐字节不变。
  • 同文件 normalize_gap_lanes:字段集闭合、lane id 唯一、reason 必须来自两个 staffability reason 常量、1..8 条上限。未知 reason code fail-closed 拒绝落盘,读方永远不会看到没人定义过的原因。接线处还要求 gap_lanes 必须与 gap_count 同时出现——两个读方不会对同一份 receipt 说出不同的话。
  • loopx/chat_actions.py _apply_team_plan:只在 settlement 报了 gap 时把 gap lanes 写进 receipt,因此完整组建的 apply 完全不变。
  • apps/.../team-plan-preview.ts 的 teamPlanReceiptGapLanes / teamPlanGapLaneLine:没有 lane_id 的条目被丢弃;不认识的 reason 原样显示,而不是编一句这张卡无法背书的话。receipt 缺失/null 时返回空列表,drawer 回落到原来那句"已应用"。
  • context-drawer.tsx:975 + personal-workspace-page.tsx:633:只在 team.plan 且 applied 且有 gap lanes 时渲染每 lane 一行;其它 action kind、其它状态、完整组建的计划渲染与之前完全一致(单测把回落路径钉住了)。

语义与 CI 对齐

semantic_alignment:aligned / reuse_existing。改动扩展的是共享 receipt 契约,所以新增字段刻意做成可选、有界、由契约 owner 校验;写入者是做出判断的 apply 本身,读方不重解释。本地证据(43 条 Python 测试、dashboard 单测 smoke、dev + packaged 浏览器场景六条全过、loopx canary premerge --from-git-diff 0 failure)都跑在该 head 上;bundle 重建幂等,所以 packaged-asset 检查对同一 head 依然成立。

对主干的风险

  • 爆炸半径:一个可选取 receipt 字段 + 它的渲染。settlement 的判断、已创建的 lane、计数、其它 action kind 全未变。
  • 反向场景:业主确认了一条只组建一半的计划 → 之后注册了 Agent → 仍然不知道当初掉的是哪条、也不知道注册就是解法。这个 PR 的浏览器断言在修 fixture 之前正是失败的(fixture 之前对 apply 只回 {projection_verified, receipt_id})。
  • 未验证面:没有跑真实 manager 通道的 Lark 卡片;没有跑真实宿主 apply;Python settlement 的判断逻辑本身未变(只多回一个字段)。
  • 明确不做:没有让计划可自动恢复——命名缺口不等于能补上缺口。自动完成 + 执行屏障(durable cursor + 拒绝第二个执行者)是 F4 剩下的部分,另外立项(todo_ca8b30b77271),因为"状态变化后是否允许补完一个已确认计划"是与"确认面是否记下了它做不到的事"不同的规则。
  • 兄弟 PR 关系:本 PR 与 #4598(applied 结果行,同样动这两个 dashboard 文件)和 #4587(部分落地恢复,同样动 settlement 模块)都是加性改动且都未合并;最后合的那个需要一次小 rebase。两个兄弟改过的 roadmap 行这里刻意不动,避免再叠第三处冲突。

我的整体评价

同意合并(待 owner 决定;本 PR 属控制面 + apps/** 行为面,按现行规则只提 PR、不自合并、不 admin-bypass)。

这是一个成正向、且 proportional的切片:它把"已承诺但没建起来的工作"从一个数字变成一个可行动的读回——哪条 lane、哪个 Agent、哪个宿主事实。机制成本是一个可选字段 + 一个 normalizer + 一个写入者 + 一个读取对 + 一行渲染,没有新模块、没有新端点、没有第二套 staffability 判断;完整组建的计划与旧 receipt 逐字节不变(有断言)。它同时把 F4 恢复所需的durable cursor(缺哪些 lane、为什么)真正落到了盘上,为下一刀留了可读的前置事实。

English verdict: APPROVE - exact head 55fce0edb0a858bd5ce443437a5c7a8d7906a1f0 of #4600 makes a partial team-plan application name the lanes it left unstaffed, with the Agent each was meant to run on and the host fact behind it, instead of reporting only a count. The addition is an optional, bounded receipt field validated by the contract's owner and written by the apply that made the decision, rendered as one localized line per lane with the verbatim code as the fallback for an unknown reason; complete plans and older receipts are unchanged and asserted. Local evidence: 43 Python tests, the dashboard unit smoke, dev and packaged browser scenarios, canary premerge with 0 failures. Residual: naming a gap does not make the plan recoverable without a fresh owner confirmation, the manager-channel Lark card was not exercised, and the unmerged siblings #4598/#4587 share two files, so whichever lands last needs a small rebase. Control-plane change: proposed for review only, no self-merge and no admin bypass.

pull Bot pushed a commit to ShinnChow/loopx that referenced this pull request Sep 17, 2026
…ree steward repairs

main has been red since loopx-project#4587: `tests/canary/test_maintainability_ratchet.py`
reports `unreviewed finding: module_metric_budget:loopx/chat_actions.py`
because the module is 1604 lines against a reviewed ceiling of 1590. The
failure reproduces on a clean `origin/main` worktree and on every open PR,
so it blocks all merges including the pending steward stack.

The growth is deliberate and already merged, not new debt invented here:
the file was 1449 lines when the ceiling was set for loopx-project#4567, then 1520
(loopx-project#4582), 1575 (loopx-project#4585) and 1604 (loopx-project#4587). Those three repairs extended the
single Chat action settlement owner with typed lane-level results
(`lane_failure`, `lane_settlements`, bounded `details` on `mark_failed`)
instead of a second settlement path, so the reviewer-visible decision this
ledger records is to accept the module as the owner of that behaviour.

It stays a bounded debt rather than a limit change: the default ceiling is
1500 lines, this module keeps its own 1604 entry, and `any_count` keeps its
existing 52 headroom (currently 45). Extracting the lane-level settlement
code now would rewrite work that three open PRs (loopx-project#4590, loopx-project#4600, loopx-project#4602) are
already changing in this module.

Validation: `tests/canary -q` reports 21 passed; on `origin/main` before
this change the same group fails with the unreviewed finding above.

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

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

# Conflicts:
#	loopx/control_plane/work_items/governed_transition_proposal.py
#	loopx/web/chat/asset-retention.json
#	loopx/web/chat/assets/index-CFOC0-T9.js
#	loopx/web/chat/assets/index-CXvjZarX.js
#	loopx/web/chat/assets/index-CfgXOdR5.js
#	loopx/web/chat/index.html
The preview admits three reasons a plan may declare about its own lane, but
the receipt vocabulary only listed the host's two verdicts. An admitted plan
that declared a capability or audience gap therefore failed its own receipt
after the owner had confirmed it: normalize_gap_lanes rejected a reason the
validator had just accepted.

The receipt now reads back the union of the plan's declared reasons and the
host's verdict, and a parametrized apply test covers all three declared
reasons end to end. The lane copy in chat_actions also stops re-projecting
records the settlement already resolved.

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

The receipt needs to name the lanes a partial plan left unstaffed, and
chat_actions is where a settlement's receipt is built, so the module grows by
four lines net after dropping the duplicated projection. The reviewed ceiling
records that accept, as it did for the three earlier steward repairs.

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)

Reviewed exact head: c1131ff1aef10ede1f55a2bf904dbd1cb5c20d1b (re-review after merging origin/main, resolving a semantic conflict with lane_failure, and repairing a defect this PR had exposed).

loopx pr-review --check-result on the matching packet returned ok: true with no approval_blockers; all 19 evidence rows for this control-plane plan are verified.

动机

一张被部分落地的团队计划,回执原本只有 gap_count。owner 看到「缺了 2 条」却无法行动:不知道缺哪条 lane、也不知道是 Agent 没注册还是本机不支持这个 action kind。这正是最需要行动的场景,控制面却把已知事实留在了回执之外。

这个 PR 的价值:让缺口变成可行动的信息——回执命名每条缺口 lane(lane_id / agent_id / typed reason_code),卡片据此展示,且回执校验要求缺口清单与计数必须同时出现,免得两个读回各自数出不同结论。

改动思路

关键判断是谁拥有这个事实。只有结算知道它实际留下了哪些缺口,前端无从得知——让前端从 plan 反推会产生第二权威,只加一句说明文案则仍然是不可行动的数字。所以由结算在回执里命名缺口 lane,并沿用仓库既有的「闭合字段集 + typed 拒绝」模式(与相邻的 lane_settlements 对称)。

合并 main 时出现的是语义冲突而非文本冲突:main 新增了 lane_failure(单条 lane 写入失败),本分支新增了 gap_lanes(计划里本机配不齐的 lane)。两者是不同事实,可同时出现——main 的 partially_created 正是为这种并存而设。因此消解方式是保留双方,四处冲突逐段核对。

关键代码讲解

_GAP_LANE_REASONS = (
    *STEWARD_TEAM_PLAN_GAP_REASONS,      # 计划可声明的三条
    *STEWARD_TEAM_PLAN_HOST_GAP_REASONS, # 宿主的结论
)

这一处是本轮的核心修复。原先词表只列了宿主的两条结论,而 _gap_lane_records 会把 preview["gaps"] 里任何被放行的原因原样写出——其中包含计划自己声明的 capability_not_granted / audience_not_authorized。结果是:一份预览校验器已经放行、owner 已经确认的计划,会在回执阶段被自己的词表拒绝。实测复现(修复前):

agent_not_registered:   OK
capability_not_granted: RAISED ValueError: governed transition gap lane reason_code is invalid
audience_not_authorized: RAISED ValueError: governed transition gap lane reason_code is invalid

修复后三者都能结算并保留原因为。回执要读回的是「结算实际留下的缺口」,词表因此必须覆盖写入端能产出的全集。

顺带把 loopx/chat_actions.py 里重复的字段投影删掉(13 行 → 4 行):结算已经解析过这些记录,Chat 侧只需复制,与相邻 lane_settlements 的写法一致。这样字段知识集中在控制面一个所有者手里。

具体改动

  • loopx/control_plane/work_items/governed_transition_proposal.py(+95):_GAP_LANE_FIELDS、normalize_gap_lanes()、_gap_lane_records()、词表修正,以及回执构造与校验两处接线。
  • loopx/chat_actions.py(+4):把结算已解析的缺口记录复制进回执。
  • apps/presentation/dashboard/src/features/personal-workspace/*(team-plan-preview / model / page / i18n):渲染缺口 lane 与原因。
  • examples/personal-workspace-browser/{fixture,team-plan}.mjs、tests/test_chat_team_plan_action.py、tests/test_steward_team_plan_apply.py(+113):端到端与回执形状正反例。
  • loopx/canary/module_metric_baseline.json:loopx/chat_actions.py 的 reviewed ceiling 1604 → 1608,单独一个 chore(canary) 提交。
  • loopx/web/chat/*:打包产物在最新 main 上重跑 build:chat 生成(不是手工合并 bundle)。

对主干的风险

回执字段是纯追加并已加入 _OPTIONAL_RECEIPT_FIELDS,旧读者忽略即可,无迁移。lane 是否被创建、Todo 是否落地、quota 与权限都未改动(测试断言可执行 lane 仍只产生一个 Todo)。main 的 lane_failure 语义与测试在合并后完整保留。

唯一的体积影响:loopx/chat_actions.py 增长 4 行、越过其 reviewed ceiling 4 行。我没有用「收窄词表/改快照」之类方式绕过,而是显式刷新 ceiling 并单独成 commit,沿用仓库既有先例(bf2b2832e chore(canary): refresh chat_actions' reviewed module ceiling after three steward repairs),并在下面列出接受理由。若维护者认为这 4 行不该由本 PR 承担,替代做法是把它移到独立切片——但字段本身属于本 PR 的交付目标。

在更新后的 head 上实测:

  • env -u PYTHONPATH uv run --extra test python -m pytest tests/test_steward_team_plan_apply.py tests/test_chat_team_plan_action.py tests/test_steward_team_plan_preview.py -q → 46 passed
  • env -u PYTHONPATH uv run --extra test python examples/dashboard-pwa-bundle-smoke.py → ok;npm run smoke:team-plan-proposal → ok
  • control-plane-maintainability-ratchet-smoke → ok(unreviewed=0、stale_exceptions=0、magnitude_regressions=0)
  • env -u PYTHONPATH uv run --extra test loopx canary premerge --from-git-diff → 0 failures / 0 advisories(catalog 4/4、risk smokes 8/8、public boundary 1/1、manual holds 0)

未验证(如实标注,不当通过):未在真实 Lark/远程 manager 通道复核同一回执的展示;未做多于两条 lane 的部分落地端到端验收。

边界声明:本 PR 改动 loopx/** 与 apps/**,按仓库规则属控制面改动,只提 PR、由维护者合并,作者不做自合并。

我的整体评价

把「哪条 lane 没配上、为什么」交回唯一知道该事实的地方产出、并让回执校验与渲染共用同一份词表,方向和边界都对。无阻断性发现,而且本轮复现并修掉了一个真实缺陷:计划自己声明能力/受众缺口的已放行计划,原先会在回执阶段整体失败。

残余风险主要是两处产品面缺口:缺口 lane 的「下一步该谁做」仍未结构化;真实远程通道与多 lane 场景未验收。

English verdict: APPROVE - re-verified on exact head c1131ff: a partially applied team plan now names each unstaffed lane with its typed reason instead of reporting only a count, and the receipt vocabulary was corrected to the union the write side can produce - before the fix, an admitted plan declaring a capability or audience gap failed its own receipt after the owner confirmed it (reproduced and fixed, with a parametrized test). The semantic conflict with main's lane_failure was resolved by keeping both facts, and the packaged bundle was rebuilt on the new main rather than hand-merged. 46 tests pass, the dashboard and packaged-bundle smokes pass, the maintainability ratchet is clean, and premerge canary reports 0 failures. No blocking finding.

@huangruiteng

Copy link
Copy Markdown
Collaborator Author

缺口身份和原因是必要读回,已纳入 #4633。也保留最新分支补充的 capability_not_granted / audience_not_authorized 原因;最终 File/PostgreSQL 测试验证这些缺口能通过提交与历史恢复,不会在回执阶段丢失。

本 PR 关闭并保留分支,避免与 #4633 重复推进。替代 PR 尚待维护者审阅合并。

Superseded by #4633: retained the useful contract, consolidated implementation and validation, and kept assignment separate from collaboration/execution authority.

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