Skip to content

fix(manager): keep an unstaffable team lane a typed gap and offer the admitted plan as a card - #4569

Merged
huangruiteng merged 2 commits into
mainfrom
codex/steward-team-plan-unstaffed-kind
Sep 16, 2026
Merged

huangruiteng merged 2 commits into
mainfrom
codex/steward-team-plan-unstaffed-kind

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

动机

线上验收(todo_5713ec39c3f3)里,管家把"一句话要一支团队"答对了:Goal、两条 lane、每条 lane 的首个有界 Todo、quota 包络、验收标准、停止条件都齐,但业主没有任何东西可以确认——proposals=[]。

逐层追下去是两个独立缺口,不只是"skill 没写清楚":

  1. 宿主从没告诉过管家它 ship 哪些 action kind。 skill 原文是 "an action kind this host supports",管家只能按请求自己造词,于是写了一个很贴切但不存在的 public_smoke_quality_repair。
  2. 一条 lane 的宿主可配人性事实,拒绝了整份计划。 validator 把"本机不 ship 这个 kind"当成 malformed payload 直接 raise,Chat 准入于是丢掉整份计划(答案正文照常到达)。而同一类事实发生在 Agent 上(agent_not_registered)时,契约早就把它做成类型化 gap 并保留 declined work。

线上原文(托管段落的最终消息)已确认 envelope 完整:{"kind":"steward_team_plan_preview", ...} 带着两条 lane、完整 quota 包络与 stop condition,缺的只是准入。

改动思路

  • 两类事实,一种处理:first_todo.action_kind 不在本机 shipment 里,就在那条 lane 上记 action_kind_not_supported,保留 declined_first_todo,其余 lane 照常 ready,计划继续被准入。声明用词表与宿主回报用词表保持两套,计划不能冒用宿主对 lane 的判断。
  • 已确认载荷成为校验器的不动点:apply 会带着宿主自己的事实再校验一遍同一份载荷。此前若预览里有 gap lane(没有 first_todo),再校验直接 first_todo must be an object —— 也就是说"业主确认了带 gap lane 的计划"在落地时必然报错。现在无 work 的 lane 被保留为它本来就是的 gap,而不会按宿主当前事实重新推导成 ready,所以落地永远不会给一条业主看到的是 gap 的 lane 配上人(不扩大已确认范围)。
  • 准入 ≠ 确认面:产品面列的是 typed action,Turn proposal 不是。业主自己的管家通道把已准入的预览交给 chat action service,落成恰好一张 team.plan 卡片(作用域为该计划点名的 Goal),按计划幂等;投影不创建工作;卡片存不下时记一条 typed Turn 事件,而不是让正确回答变成失败回答。
  • 一种响应形状:Turn 响应现在会把预览和 todo 提案放在一起,管家会话读得懂(此前 schema 只认 todo,多出预览会让整条回答解析失败)。只有 todo 提案会变成候选卡。
  • 放置:团队计划的宿主侧契约(准入事实 + 交给确认面)移入拥有管家通道的能力模块 loopx/capabilities/manager_context/team_plan.py;chat_runtime 因此保持在其 reviewed module budget 之内(1560 行上限),没有新增模块度量债务。

具体改动

  • loopx/capabilities/manager_context/team_plan.py(新增):team_plan_admission_context(按 Goal 解析准入事实,通道范围与 registry 缺失都 fail closed)+ project_team_plan_preview(把已准入预览投影成卡片,幂等、typed 失败事件)。
  • loopx/control_plane/work_items/governed_transition_proposal.py:新增宿主侧 gap 词表 action_kind_not_supported;_unstaffed_lane / _declined_todo 统一 gap lane 形状;无 first_todo 的 lane 保留其 gap(校验器幂等)。
  • loopx/capabilities/manager_context/skills/loopx-manager/SKILL.md:点名本机 ship 的 17 个 advancement action kind;说明"Agent 未注册"与"kind 本机不 ship"是同一类事实、由 Core 报成 gap、计划其余部分仍到业主;修正"未注册 Agent 会被丢弃"的旧表述。
  • loopx/chat_actions.py:ChatActionService.project_team_plan_preview —— 幂等(team-plan:<plan digest>)、context {kind: manager, goal_id}、不创建工作。
  • loopx/chat_runtime.py / loopx/chat_server.py:runtime 暴露注入点并在 Turn 结算前调用;chat_server 把 action service 的方法接上(没有投影器的宿主照常回答)。
  • apps/presentation/dashboard/src/data/{chat,chat-model}.ts、views/dashboard-page.tsx、smoke/team-plan-proposal-smoke.ts:响应 schema 接纳团队预览 + 类型守卫;管家会话只把 todo 提案做成候选卡,并为团队计划给出"已生成可确认卡片(),确认后才会创建 lane"的一行提示。
  • docs/architecture/rfcs/harness-selection-dsh-pi-v0.md(+ zh-CN):两类词表、lane 级 gap、已确认载荷不动点、第 6 条 shipped enforcement(准入预览 → 可确认卡片),并点名仍然缺的覆盖:远端(Lark)管家受众目前只在正文里带预览,其自有面还没有卡片。

对主干的风险

  • 默认行为变化只有一处且是补救性的:此前"一条 lane 的 kind 不支持"会让整份计划消失,现在它成为类型化 gap;计划仍然只是预览,applies: false,落地仍经 canonical Todo owner,配额与授权边界未动。
  • 新增一次 Turn 内的投影调用,作用域限定在业主本地 manager 通道;远端通道与 Goal 通道明确不写卡片。投影失败不会让回答失败(typed team_plan.projection_failed 事件),因此不会把"卡片存不下"变成"管家答不出"。
  • 前端只放宽了响应 schema 并加了宽窄判定,未改任何确认/落地路径;打包 bundle 随第二笔 commit 一起出货。
  • 已知未覆盖:远端管家受众的确认面(RFC 已记录为缺口,不是本次声称完成的部分)。

我的整体评价

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

改动聚焦在"业主能确认"这唯一目标上,没有扩大任何授权:准入仍然只显示、落地仍然重新校验、投影不产生 work。team.plan 卡片在 gap lane 上落地必失败的潜伏缺陷,是这次做端到端验收时才暴露并被测试固定住的,值得作为本 PR 的一部分。

验证:34 项 team-plan/action/guidance 单测 + 131 项 manager/steward/chat 测试通过;workspace 浏览器 smoke(含 team-plan 场景)通过;dashboard chat bundle typecheck + team-plan smoke 通过;docs governance 与 control-plane maintainability ratchet 通过;loopx canary premerge --from-git-diff 通过(无新增债务)。tests/test_host_loop_activation.py 等 9 项在本机需要特定 Python/CLI 环境的失败在未改动的 checkout 上同样复现,与本 PR 无关。

English verdict: APPROVE

@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: 05e5df5

动机

线上 todo_5713ec39c3f3 的验收发现:管家把一句话团队请求答对了,业主却没有任何可确认的东西。逐层证据(已安装 release、manager 通道、executor=dsh)显示 envelope 完整存在,proposals 却为空。根因不是"skill 没写清楚",而是宿主从未告诉管家它 ship 哪些 action kind,且一条 lane 的宿主可配人性事实会拒绝整份计划。

改动思路

  • 把"Agent 未注册"与"kind 本机不 ship"当作同一类 lane 级事实:类型化 gap + 保留 declined work,计划其余 lane 照常准入。
  • 让已确认载荷成为校验器的不动点(无 work 的 lane 保留为 gap,不按当前事实重新推导),因此 apply 不会给业主看到是 gap 的 lane 配上人。这一条同时修掉了"带 gap lane 的已确认计划落地必报错"的潜伏缺陷。
  • 准入不等于确认面:业主自己的管家通道把已准入预览投影成恰好一张 team.plan 卡片;投影不创建 work、按计划幂等、失败只记 typed Turn 事件。

具体改动

  • 新增 loopx/capabilities/manager_context/team_plan.py:准入事实查询 + 卡片投影(含 TeamPlanProjector/TurnEventSink 协议)。registry_path=None 时对任何 Goal 返回 None 而不是抛错,保持"没有 registry 的宿主照常回答"。
  • governed_transition_proposal.py:action_kind_not_supported 宿主词表;_unstaffed_lane/_declined_todo 统一 gap 形状;无 first_todo 的 lane 保留其 gap。
  • 管家 skill 点名 17 个 shipped advancement action kind,并修正"未注册 Agent 会被丢弃"的旧表述。
  • ChatActionService.project_team_plan_preview:context {kind: manager, goal_id}、idempotency_key=team-plan:<plan digest>。
  • 前端:响应 schema 接纳团队预览 + 类型守卫;管家会话只把 todo 做成候选卡;团队计划给出"已生成可确认卡片(),确认后才会创建 lane"。

对主干的风险

  • 唯一默认行为变化是补救性的:不支持的 kind 从"整份计划消失"变成"该 lane 类型化 gap";计划仍是预览、applies: false、落地仍走 canonical owner。
  • 投影只作用于业主本地 manager 通道;远端与 Goal 通道明确不写卡片,且投影失败不让回答失败。
  • 前端只放宽读取与窄化判定,未触碰确认/落地路径。打包 bundle 在第二笔 commit 出货。
  • 已记录未覆盖:远端管家受众(Lark)自有面尚无卡片。

我的整体评价

审查了我自己的 diff 并复核了关键路径:准入仍只显示、落地仍重新校验、投影不产生 work,授权面没有扩大。新增的单测覆盖了 kind gap、词表分离、校验器幂等、服务层投影/幂等/落地读回,以及 runtime 投影作用域和 typed 失败事件。chat_runtime.py 从 1560 行降到 1531 行,维持在其 reviewed module budget 之内。

验证:34 项 team-plan/action/guidance 单测 + 131 项 manager/steward/chat 测试通过;workspace 浏览器 smoke(含 team-plan 场景)通过;dashboard chat bundle typecheck + team-plan smoke 通过;docs governance 与 maintainability ratchet 通过;loopx canary premerge --from-git-diff 通过。test_host_loop_activation.py 等 9 项在本机需要特定 Python/CLI 环境的失败在未改动的 checkout 上同样复现,与本 PR 无关。

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

English verdict: APPROVE

… admitted plan as a card

A live steward answered a one-sentence team request correctly -- Goal, lanes,
each lane's first bounded Todo, quota envelope, acceptance and stop condition --
and the owner still had nothing to confirm. The answer's machine-readable
preview named an action kind this host does not ship for one lane, the validator
treated that as a malformed payload and raised, and Chat admission therefore
dropped the *entire* plan while the prose answer arrived. Two independent gaps
were behind that single symptom:

- nothing told the steward which action kinds this host ships, so a lane's
  `action_kind` was written from the request's own vocabulary rather than from
  the host's (`public_smoke_quality_repair` is apt and unshippable);
- a host staffability fact about *one lane* refused the whole plan, while the
  same class of fact about an Agent (`agent_not_registered`) was already a typed
  gap that keeps the work it declined.

The validator now treats both facts the same way: an unsupported kind becomes
`action_kind_not_supported` on that lane, keeps the declined first Todo, and the
plan is still admitted with its other lanes ready. The reasons a plan may declare
about itself and the reasons the host reports stay two vocabularies, so a plan
cannot claim the host's verdict. The confirmed payload is also a fixed point of
the validator now: re-reading an admitted preview preserves a lane the preview
reported as unstaffed instead of re-deriving it from the host's current facts, so
an apply cannot staff a lane the owner was shown as unstaffed -- which is what
the apply already relied on, and what made an admitted preview with a gap lane
unappliable before this change.

Admission alone still leaves the owner nothing to click, because the product
surfaces list typed actions. The owner's own manager channel therefore hands the
preview it admitted to the chat action service, which stores exactly one
`team.plan` card scoped to the Goal the plan staffs; the projection is idempotent
per plan, creates no work, and reports a typed Turn event instead of failing the
answer when the surface cannot store it. A Turn response now carries the preview
beside its Todo proposals, so the transcript reads one response shape instead of
failing the parse of an answer the steward already produced.

The team-plan host contract (the facts a preview is validated against, and the
hand-off to a surface that can confirm it) moved into the capability that owns
the steward channel, which also keeps `chat_runtime` under its reviewed module
budget instead of growing it.

RFC: docs/architecture/rfcs/harness-selection-dsh-pi-v0.md (EN + zh-CN).

Validation: 34 team-plan/action/guidance unit tests and 131 manager/steward/chat
tests pass; the workspace browser smoke passes with its `team-plan` scenario; the
dashboard chat bundle typechecks and its team-plan smoke passes; docs governance
and the control-plane maintainability ratchet pass. A remote manager audience
still keeps the preview in its answer text with no card on its own surface, which
the RFC names as the remaining coverage gap.

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

loopx/web/chat is tracked, so a manager Turn that carries an admitted team plan
and the transcript that reads it only reach an installed LoopX after the bundle
is rebuilt. Rebuilt with npm run build:chat: new entry
assets/index-D8O4R6-3.js retires the previous generation assets/index-67F_HQF0.js
with assets/index-B21-YvBH.css, and assets/index-DWWKKUgg.js stays retained.

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
@huangruiteng
huangruiteng force-pushed the codex/steward-team-plan-unstaffed-kind branch from 05e5df5 to 65bd1d5 Compare September 16, 2026 15:36

@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: 65bd1d5 (rebased onto 262923d, feat(semantics): generate shared Turn contracts and controller rules (M2))

动机

线上 todo_5713ec39c3f3 的验收发现:管家把一句话团队请求答对了,业主却没有任何可确认的东西。逐层证据(已安装 release、manager 通道、executor=dsh)显示 envelope 完整存在,proposals 却为空。根因不是"skill 没写清楚",而是宿主从未告诉管家它 ship 哪些 action kind,且一条 lane 的宿主可配人性事实会拒绝整份计划。

改动思路

  • 把"Agent 未注册"与"kind 本机不 ship"当作同一类 lane 级事实:类型化 gap + 保留 declined work,计划其余 lane 照常准入。
  • 让已确认载荷成为校验器的不动点(无 work 的 lane 保留为 gap,不按当前事实重新推导),因此 apply 不会给业主看到是 gap 的 lane 配上人。这一条同时修掉了"带 gap lane 的已确认计划落地必报错"的潜伏缺陷。
  • 准入不等于确认面:业主自己的管家通道把已准入预览投影成恰好一张 team.plan 卡片;投影不创建 work、按计划幂等、失败只记 typed Turn 事件。

具体改动

  • 新增 loopx/capabilities/manager_context/team_plan.py:准入事实查询 + 卡片投影(含 TeamPlanProjector/TurnEventSink 协议)。registry_path=None 时对任何 Goal 返回 None 而不是抛错。
  • governed_transition_proposal.py:action_kind_not_supported 宿主词表;_unstaffed_lane/_declined_todo 统一 gap 形状;无 first_todo 的 lane 保留其 gap。
  • 管家 skill 点名 17 个 shipped advancement action kind,并修正"未注册 Agent 会被丢弃"的旧表述。
  • ChatActionService.project_team_plan_preview:context {kind: manager, goal_id}、idempotency_key=team-plan:<plan digest>。
  • 前端:响应 schema 接纳团队预览 + 类型守卫;管家会话只把 todo 做成候选卡;团队计划给出"已生成可确认卡片(),确认后才会创建 lane"。

对主干的风险

  • 唯一默认行为变化是补救性的:不支持的 kind 从"整份计划消失"变成"该 lane 类型化 gap";计划仍是预览、applies: false、落地仍走 canonical owner。
  • 投影只作用于业主本地 manager 通道;远端与 Goal 通道明确不写卡片,且投影失败不让回答失败。
  • 前端只放宽读取与窄化判定,未触碰确认/落地路径。第二笔 commit 出货打包 bundle;#4499 未改动 apps/presentation/dashboard,bundle 仍然是最新构建。
  • 已记录未覆盖:远端管家受众(Lark)自有面尚无卡片。

我的整体评价

rebase 到 #4499(Turn 契约/controller 规则 M2)后重跑:60 项 manager/steward/team-plan 测试与 canary premerge --from-git-diff(含 public boundary)均通过,无新增债务。chat_runtime.py 在 1531 行,维持 reviewed module budget。

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

English verdict: APPROVE

@huangruiteng
huangruiteng merged commit f1166e8 into main Sep 16, 2026
6 of 7 checks passed
@huangruiteng
huangruiteng deleted the codex/steward-team-plan-unstaffed-kind branch September 16, 2026 15:38
huangruiteng added a commit that referenced this pull request Sep 16, 2026
… offered it (#4572)

* fix(manager): state where a team plan is confirmed in the answer that offered it

PR #4569 made an admitted steward team preview reach the owner's own channel as a
typed `team.plan` card. Live readback showed the card is reachable, but only
under the Goal the plan staffs: the manager conversation renders no typed-action
cards, and a Lark manager audience has no card at all. So the owner who asked the
sentence could read a correct plan and still not know how to confirm it -- the
steward's prose says "you confirm and it applies", not where.

Every manager answer that admitted a preview now carries one channel-authored
pointer line naming the Goal whose workspace holds the card, and stating that no
lane exists before that confirmation. It is an operational receipt, in the same
way the delegation path states its own receipt, so the steward's prose is
preserved verbatim and a Goal channel is never annotated. A remote manager
audience receives the same line, which is what makes one manager answer
actionable for an audience that cannot itself confirm; no card is written on its
behalf, and that remaining gap stays named in the RFC.

The dashboard no longer composes its own version of that sentence: the channel is
the one owner of it, so the transcript shows the answer it was given.

Validation: 86 manager/steward/team-plan unit tests pass, including the new
pointer cases (manager channel annotated, remote audience annotated without a
card, Goal channel untouched, a plan-free answer never annotated); workspace
browser smoke, dashboard typecheck and the team-plan smoke pass; the rebuilt chat
bundle ships the frontend change; docs governance, the maintainability ratchet
and `loopx canary premerge --from-git-diff` pass.

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

* chore(dashboard): ship the rebuilt chat bundle for the manager dialogue line

The channel now states where a team plan is confirmed, and the dashboard stopped
composing its own version of that sentence, so the packaged chat surface needs
the rebuilt bundle. Rebuilt with npm run build:chat: new entry
assets/index-Cb7S1yHW.js retires assets/index-D8O4R6-3.js as the previous
generation.

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

---------

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Co-authored-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng pushed a commit that referenced this pull request Sep 17, 2026
A team plan may contain a lane this host cannot staff: the Agent
registration, capability grant, or action kind does not exist here. The
shipped behavior (#4569) keeps the plan admitted, names the lane as a typed
gap, and creates only the lanes that can run, but no catalog entry named that
boundary.

IP-034 fills it in the Planning Governance family: name the gap instead of
inventing the lane, keep the admitted plan, show the lane instead of dropping
it silently, and require explicit new intent before a gap is filled. IP-018
owns plan-to-todo writeback and IP-024 owns the repair delta; neither covers
part of an admitted plan having no honest owner.

Adds the family table row, the family-to-canary matrix entry, and three
durable assertions to examples/interaction-pattern-catalog-smoke.py.

Closes #4638
Refs GH-C37

Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com>
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