Skip to content

Commit 30fe335

Browse files
committed
feat(manager): classify every team-plan field and refuse an unhonored limit
A confirmed team plan carried a quota envelope and a stop condition that no owner enforces at plan level, and the confirmation card listed them beside real work with nothing to tell them apart. An owner could read a bounded envelope as a limit that holds, while the only claim behind it was JSON. Classify every validated plan field and make the plan say which limits it wants enforced: * The validated preview now carries `field_classification`: a lane's first bounded Todo is an `execution_constraint` because the confirmation creates it through the canonical Todo owner, its acceptance is a `retained_acceptance_reference` because it is kept beside that work, and the objective, plan-level envelope and stop condition are `advisory`. * A plan may declare `enforcement` for the plan-level envelope and stop condition. `advisory` is accepted; `enforced` is refused before confirmation with the field named and the supported value stated, because this host has no plan-level enforcement owner. A field outside that closed set, and an invented value, are refused too. * The Dashboard card and the language-neutral `review_card_frame_v0` show each field's class, so a limit nothing holds cannot render as a binding. A field the host did not classify keeps no class instead of being assumed to bind. * The steward skill states the rule where the plan is written, and the roadmap and selection RFC record the closed F1 rather than accumulating a second current truth. Evidence: previous runtime admits the claim with no classification at all (`ADMITTED: classification = None`) while this head refuses it; 120 focused Python tests; `npm run test:control-plane` fails exactly the pre-existing set (no new failure); team-plan dashboard smoke, action-review-plan smoke, packaged `smoke:personal-workspace-packaged` with all six scenarios; docs governance and asset integrity smokes; `loopx canary premerge --from-git-diff` ok with 0 failures. Control-plane change: proposed for review, not self-merged. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
1 parent e66615d commit 30fe335

19 files changed

Lines changed: 430 additions & 39 deletions

File tree

‎apps/presentation/dashboard/smoke/team-plan-proposal-smoke.ts‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ const plan = {
9494
gaps: [{ lane_id: "lane_review", reason_code: "agent_not_registered" }],
9595
quota_envelope: { slots: 4, window: "1d" },
9696
stop_condition: "every lane reports a typed outcome or a stated gap",
97+
field_classification: {
98+
objective: "advisory",
99+
quota_envelope: "advisory",
100+
stop_condition: "advisory",
101+
"lane.first_todo": "execution_constraint",
102+
"lane.acceptance": "retained_acceptance_reference",
103+
},
97104
applies: false,
98105
};
99106

@@ -130,6 +137,9 @@ const translate = (key: string, values?: Record<string, string | number>) => {
130137
"proposal.field.quotaEnvelope": "Quota envelope",
131138
"proposal.field.stopCondition": "Stop condition",
132139
"proposal.teamPlan.acceptanceShort": "acceptance",
140+
"proposal.teamPlan.class.advisory": "advisory, not enforced",
141+
"proposal.teamPlan.class.executionConstraint": "enforced",
142+
"proposal.teamPlan.class.retainedAcceptance": "retained acceptance",
133143
"proposal.teamPlan.gapLane": "unstaffed",
134144
"proposal.teamPlan.laneUnstaffed": "staffing gap, no first Todo",
135145
};
@@ -149,23 +159,39 @@ check(readyLane?.label === "agent-backend", "a ready lane names the Agent that r
149159
check(
150160
readyLane?.value.includes("P1") === true
151161
&& readyLane?.value.includes("Implement the bounded intake") === true
152-
&& readyLane?.value.includes("acceptance: the bounded Todo is created through the canonical owner") === true,
153-
"a ready lane shows its first bounded Todo, its priority and its acceptance signal",
162+
&& readyLane?.value.includes("retained acceptance: the bounded Todo is created through the canonical owner") === true,
163+
"a ready lane shows its first bounded Todo, its priority and its retained acceptance signal",
154164
);
155165
check(
156166
gapLane?.value.startsWith("unstaffed · agent_not_registered") === true
157167
&& gapLane?.value.includes("Independently review the intake") === true,
158168
"a gap lane says it is unstaffed, names the reason and keeps the work it did not staff",
159169
);
160170
check(byKey.get("lane_gaps")?.value === "lane_review: agent_not_registered", "the gap summary joins lane and reason");
161-
check(byKey.get("quota_envelope")?.value === "slots: 4 · window: 1d", "the quota envelope is shown as data");
162171
check(
163-
byKey.get("stop_condition")?.value === "every lane reports a typed outcome or a stated gap",
164-
"the stop condition is shown",
172+
byKey.get("quota_envelope")?.value === "slots: 4 · window: 1d · advisory, not enforced",
173+
"the quota envelope is shown as data and as advisory",
174+
);
175+
check(
176+
byKey.get("stop_condition")?.value
177+
=== "every lane reports a typed outcome or a stated gap · advisory, not enforced",
178+
"the stop condition is shown as advisory",
165179
);
166180
check(
167181
fields.some((field) => field.value.includes("agent-") === true && field.value.includes("ready") === true) === false,
168182
"the card never renders a lane as already created",
169183
);
170184

185+
// A plan whose host emitted no classification must not get one invented for it:
186+
// the card shows what it was given, and an unclassified field is not a binding.
187+
const unclassified = teamPlanFields(
188+
{ goal_id: GOAL_ID, plan: { ...plan, field_classification: undefined } },
189+
translate as never,
190+
);
191+
check(
192+
new Map(unclassified.map((field) => [field.key, field.value])).get("quota_envelope")
193+
=== "slots: 4 · window: 1d",
194+
"an unclassified envelope is shown without a classification the host did not state",
195+
);
196+
171197
if (process.exitCode !== 1) console.log("team plan proposal smoke ok");

‎apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,9 @@ const en = {
700700
"proposal.field.laneGaps": "Unstaffed lanes",
701701
"proposal.field.quotaEnvelope": "Quota envelope",
702702
"proposal.teamPlan.acceptanceShort": "acceptance",
703+
"proposal.teamPlan.class.advisory": "advisory, not enforced",
704+
"proposal.teamPlan.class.executionConstraint": "enforced",
705+
"proposal.teamPlan.class.retainedAcceptance": "retained acceptance",
703706
"proposal.teamPlan.gapLane": "unstaffed",
704707
"proposal.teamPlan.laneUnstaffed": "staffing gap, no first Todo",
705708
"proposal.workspace.current": "Current local workspace (no Repository bound)",
@@ -1738,6 +1741,9 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
17381741
"proposal.field.laneGaps": "未配齐的 lane",
17391742
"proposal.field.quotaEnvelope": "配额包络",
17401743
"proposal.teamPlan.acceptanceShort": "验收",
1744+
"proposal.teamPlan.class.advisory": "仅记录,不强制执行",
1745+
"proposal.teamPlan.class.executionConstraint": "强制执行",
1746+
"proposal.teamPlan.class.retainedAcceptance": "留存验收",
17411747
"proposal.teamPlan.gapLane": "未配齐",
17421748
"proposal.teamPlan.laneUnstaffed": "编制缺口,无首个 Todo",
17431749
"proposal.workspace.current": "当前本地工作区(未绑定 Repository)",

‎apps/presentation/dashboard/src/features/personal-workspace/team-plan-preview.ts‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,39 @@ function asText(value: unknown): string {
2121
return typeof value === "string" ? value : "";
2222
}
2323

24+
/**
25+
* What kind of claim a plan field is, as the host classified it.
26+
*
27+
* The host emits this beside the plan it admitted, and an admitted preview
28+
* carries it inside the preview itself, so both places are host facts rather
29+
* than a claim the plan makes. A field the host did not classify reads as
30+
* unclassified instead of being assumed to bind.
31+
*/
32+
function fieldClassification(
33+
parameters: Record<string, unknown>,
34+
plan: Record<string, unknown>,
35+
): Record<string, unknown> {
36+
return { ...asRecord(parameters.field_classification), ...asRecord(plan.field_classification) };
37+
}
38+
39+
function classificationLabel(
40+
classification: Record<string, unknown>,
41+
key: string,
42+
t: WorkspaceTranslate,
43+
): string {
44+
const value = asText(classification[key]);
45+
if (value === "execution_constraint") return t("proposal.teamPlan.class.executionConstraint");
46+
if (value === "retained_acceptance_reference") return t("proposal.teamPlan.class.retainedAcceptance");
47+
if (value === "advisory") return t("proposal.teamPlan.class.advisory");
48+
return "";
49+
}
50+
2451
export function teamPlanFields(
2552
parameters: Record<string, unknown>,
2653
t: WorkspaceTranslate,
2754
): TeamPlanPreviewField[] {
2855
const plan = asRecord(parameters.plan);
56+
const classification = fieldClassification(parameters, plan);
2957
const fields: TeamPlanPreviewField[] = [];
3058
const goalId = asText(parameters.goal_id) || asText(plan.goal_id);
3159
if (goalId) {
@@ -61,12 +89,18 @@ export function teamPlanFields(
6189
asText(todo.action_kind),
6290
asText(todo.text),
6391
].filter(Boolean).join(" · ");
92+
// A lane's first Todo is the work this confirmation creates, so it carries
93+
// its class; the acceptance the lane ends on is retained beside that work
94+
// rather than enforced by it.
95+
const acceptanceLabel =
96+
classificationLabel(classification, "lane.acceptance", t)
97+
|| t("proposal.teamPlan.acceptanceShort");
6498
fields.push({
6599
key: `lane_${laneId}`,
66100
label: agentId || laneId,
67101
value: [
68102
laneValue || t("proposal.teamPlan.laneUnstaffed"),
69-
acceptance ? `${t("proposal.teamPlan.acceptanceShort")}: ${acceptance}` : "",
103+
acceptance ? `${acceptanceLabel}: ${acceptance}` : "",
70104
].filter(Boolean).join(" · "),
71105
});
72106
});
@@ -90,15 +124,20 @@ export function teamPlanFields(
90124
fields.push({
91125
key: "quota_envelope",
92126
label: t("proposal.field.quotaEnvelope"),
93-
value: envelopeEntries.map(([key, value]) => `${key}: ${String(value ?? "")}`).join(" · "),
127+
value: [
128+
envelopeEntries.map(([key, value]) => `${key}: ${String(value ?? "")}`).join(" · "),
129+
classificationLabel(classification, "quota_envelope", t),
130+
].filter(Boolean).join(" · "),
94131
});
95132
}
96133
const stopCondition = asText(plan.stop_condition);
97134
if (stopCondition) {
98135
fields.push({
99136
key: "stop_condition",
100137
label: t("proposal.field.stopCondition"),
101-
value: stopCondition,
138+
value: [stopCondition, classificationLabel(classification, "stop_condition", t)]
139+
.filter(Boolean)
140+
.join(" · "),
102141
});
103142
}
104143
return fields;

‎docs/architecture/rfcs/harness-selection-dsh-pi-v0.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,14 +522,19 @@ The shipped boundary is `steward_team_plan_preview_v0`, kind
522522
`steward_team_plan_preview`, dispatched through
523523
`loopx/control_plane/work_items/governed_transition_proposal.py` and Chat
524524
`team.plan`. It names an exact Goal, 1–8 lanes, registered Agent identities,
525-
first Todo text/priority/class/action, acceptance, quota envelope and stop
526-
condition. Validation produces `applies: false`. Preview is not execution.
525+
first Todo text/priority/class/action, acceptance and a plan-level quota
526+
envelope and stop condition. Validation produces `applies: false`, classifies
527+
every field it validated -- a lane's first bounded Todo as an execution
528+
constraint, its acceptance as a retained acceptance reference, the objective,
529+
envelope and stop condition as advisory -- and refuses a plan that asks this
530+
host to enforce a plan-level limit no owner here enforces. Preview is not
531+
execution.
527532

528533
| Boundary | Shipped behavior | Remaining limitation |
529534
| --- | --- | --- |
530535
| Validation/admission (#4519/#4522/#4532/#4533) | Exact Goal, registered Agents, supported advancement kinds and bounded public-safe fields; channel-scoped Goal lookup; unavailable facts drop the proposal while preserving answer text | `ready` checks registration/action support, not executor health, tool eligibility or budget admission |
531536
| Staffing gaps | Unknown Agent produces `agent_not_registered` and retains `declined_first_todo`; explicit `capability_not_granted` / `audience_not_authorized` gaps admit no work | These reason codes do not prove all capability/audience conditions are automatically detected |
532-
| Materialization (#4524/#4528/#4535/#4538) | Revalidates the named Goal; calls canonical Todo owner per ready lane; records proposal digest and bounded `lane_todo_ids`; existing receipt shape remains readable; no monitor key | Confirmed priority is dropped; acceptance/quota/stop are not execution constraints on this path; no atomic team commit or automatic partial-recovery proof |
537+
| Materialization (#4524/#4528/#4535/#4538) | Revalidates the named Goal; calls canonical Todo owner per ready lane; keeps the confirmed priority in the lane Todo's own label; records proposal digest, bounded `lane_todo_ids` and each lane's retained acceptance; existing receipt shape remains readable; no monitor key | The plan-level envelope and stop condition stay advisory and a claim that they are enforced is refused; no atomic team commit or automatic partial-recovery proof |
533538
| Confirmation (#4547/#4548/#4552) | Existing frontend displays lanes/gaps and submits `team.plan`; bundle and browser fixture shipped; the manager conversation now lists the card its own channel stored, so an owner confirms where the sentence was typed while a Goal-scoped fetch stays in that Goal's workspace | Lark and real worker execution were not qualified by this fixture; the confirmation readback was repaired after this fixture (a confirmed lane keeps its declared priority, a partial application reports its gap count, and a plan that staffs no lane is a typed failure) |
534539
| Freshness | Registry byte changes make the Chat preview stale; optional `intent_basis` reads alignment source facts before materialization | No exact Goal-intent/authorization/work precondition at commit; `intent_basis` is neither the full intent revision nor a CAS fence |
535540

‎docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,13 @@ journal 与配额语义;B 作为上游接口出现时的低成本替代;只
403403

404404
在 `43d362532`,团队入端已交付有界预览、业主确认与首批 Todo 物化;尚未验收正在运行、预算受约束或完整意图对齐的团队。[整体路线总纲](loopx-overall-roadmap-v0.zh-CN.md) 拥有跨 RFC 优先级及 F1–F7/R1–R7;本文保留 runtime 选型及其资格证据,不另建团队架构。
405405

406-
已交付边界为 `steward_team_plan_preview_v0`、kind `steward_team_plan_preview`,经 `loopx/control_plane/work_items/governed_transition_proposal.py` 与 Chat `team.plan` 分派。计划点名确切 Goal、1–8 条 lane、注册 Agent、首个 Todo 的 text/priority/class/action、acceptance、quota envelope 与 stop condition。校验结果 `applies: false`;预览不等于执行。
406+
已交付边界为 `steward_team_plan_preview_v0`、kind `steward_team_plan_preview`,经 `loopx/control_plane/work_items/governed_transition_proposal.py` 与 Chat `team.plan` 分派。计划点名确切 Goal、1–8 条 lane、注册 Agent、首个 Todo 的 text/priority/class/action、acceptance,以及计划级 quota envelope 与 stop condition。校验结果 `applies: false`,并对其校验的每个字段给出分类——lane 的首个有界 Todo 是执行约束、它的 acceptance 是留存验收引用、objective/envelope/stop condition 是 advisory——同时拒绝要求本机强制一个本机无人承担的计划级限制的计划。预览不等于执行。
407407

408408
| 边界 | 已交付行为 | 剩余限制 |
409409
| --- | --- | --- |
410410
| 校验/准入(#4519/#4522/#4532/#4533) | 确切 Goal、注册 Agent、支持的 advancement kind、有界公开安全字段;按通道范围查询 Goal;缺少事实时丢弃提案并保留答案正文 | `ready` 只校验注册/action 支持,不证明执行器健康、工具资格或预算准入 |
411411
| Staffing gap | 未注册 Agent 产生 `agent_not_registered` 并保留 `declined_first_todo`;显式 `capability_not_granted` / `audience_not_authorized` gap 不允许工作 | 存在这些 reason code 不证明全部 capability/audience 条件已经自动检测 |
412-
| 物化(#4524/#4528/#4535/#4538) | 重新校验点名 Goal,逐 ready lane 调 canonical Todo owner;回执保存 proposal digest 与有界 `lane_todo_ids`,兼容旧回执且没有 monitor key | 确认的 priority 丢失;acceptance/quota/stop 未成为此路径的执行约束;没有整队原子提交或自动 partial recovery 证明 |
412+
| 物化(#4524/#4528/#4535/#4538) | 重新校验点名 Goal,逐 ready lane 调 canonical Todo owner;确认的 priority 保留在 lane Todo 自己的标签里;回执保存 proposal digest、有界 `lane_todo_ids` 与每条 lane 的留存 acceptance;兼容旧回执且没有 monitor key | 计划级 envelope 与 stop condition 仍为 advisory,声明为强制会被拒绝;没有整队原子提交或自动 partial recovery 证明 |
413413
| 确认(#4547/#4548/#4552) | 现有前端展示 lanes/gaps 并提交 `team.plan`,bundle 与 browser fixture 已交付;产生计划的**管家会话本身也会列出该通道存入的卡片**,业主在说出这句话的地方即可确认,而为已选 Goal 拉取的提案仍留在该 Goal 工作区 | 此 fixture 未验收 Lark 或真实 worker 执行;确认回读在该 fixture 之后已修复(确认后的 lane 保留声明的优先级,部分落地返回缺口数量,无 lane 可组建的计划记为 typed failure) |
414414
| 新鲜度 | registry bytes 变化会使 Chat preview stale;可选 `intent_basis` 在物化前读取 alignment source facts | commit 未绑定精确 Goal intent/授权/工作前置条件;`intent_basis` 不是完整意图修订或 CAS fence |
415415

‎docs/architecture/rfcs/loopx-overall-roadmap-v0.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ Progress means another independently reproducible user journey, not more fields
300300

301301
These are synthetic-fixture results at the exact baseline, without live user content. F1–F4 exercise existing `ChatActionService.preview/apply` with isolated Goals. F4 injects failure immediately before the second Todo write; the other writes use the actual local Todo writer. F5–F7 are source/contract findings.
302302

303-
**Repair status (2026-09-17, baseline `6979d528b`).** The rows below stay as the record of what the audit found. R1 has since closed the priority and acceptance half of F1, all of F3, and F2: a confirmed lane now keeps the priority it declared in its own Todo label, the settlement receipt retains each lane's acceptance beside the Todo identity it became, a partial application reports `team_plan_partially_applied` with a gap count instead of a full success, a confirmation that staffed no lane is recorded as the typed failure `team_plan_no_staffable_lane` rather than as an applied plan with an empty Todo id, and a team-plan preview now binds this Goal's active-state intent and the canonical basis its lanes would advance, so rewriting the objective after the preview asks the owner to confirm again instead of applying the old plan. The remainder is open and owned by canonical Todos: the plan-level quota/stop classification, F4's per-lane recovery, and rendering a partial application as partial on the confirmation card.
303+
**Repair status (2026-09-17, baseline `6979d528b`).** The rows below stay as the record of what the audit found. R1 has since closed all of F1, all of F3, and F2: a confirmed lane now keeps the priority it declared in its own Todo label, the settlement receipt retains each lane's acceptance beside the Todo identity it became, a partial application reports `team_plan_partially_applied` with a gap count instead of a full success, a confirmation that staffed no lane is recorded as the typed failure `team_plan_no_staffable_lane` rather than as an applied plan with an empty Todo id, a team-plan preview now binds this Goal's active-state intent and the canonical basis its lanes would advance, so rewriting the objective after the preview asks the owner to confirm again instead of applying the old plan, and every plan field now carries the host's classification -- a lane's first bounded Todo as an execution constraint, its acceptance as a retained acceptance reference, and the plan-level envelope and stop condition as advisory, which the confirmation surface shows. A plan that asks this host to enforce a plan-level limit it has no owner for is refused before the owner is offered a confirmation instead of being stored as JSON. The remainder is open and owned by canonical Todos: F4's per-lane recovery, and rendering a partial application as partial on the confirmation card.
304304

305305
| ID / Priority | Trigger, result and consequence | Location and successor |
306306
| --- | --- | --- |

0 commit comments

Comments
 (0)