diff --git a/docs/product/use-cases/steward/README.md b/docs/product/use-cases/steward/README.md index 9242f29a50..5ca3c3f47a 100644 --- a/docs/product/use-cases/steward/README.md +++ b/docs/product/use-cases/steward/README.md @@ -22,12 +22,12 @@ fact the workspace surfaces render, not a claim about a live Goal. | Beat | What the owner does | What the workspace shows | State | | --- | --- | --- | --- | | 1 | Looks at the first screen | Goal board lanes (needs you / running / observing / scheduled), each Goal card naming its Agent and its next sentence | Proven | -| 2 | Asks the steward in the Goal conversation | The ask becomes an accepted Turn and the admitted team plan card lands in the same conversation | Proven | +| 2 | Asks the steward in the Goal conversation | The steward's bounded prompt row (`询问下一步` / `向 Agent 获取进度报告` / `配置定时检查` / `看阻塞` / `查证据`) sends its message on click and leaves no draft, the ask becomes an accepted Turn, and the admitted team plan card lands in the same conversation | Proven | | 3 | Reads the card | Per lane: the Agent, the first bounded Todo with priority and action kind, the acceptance signal, and an explicitly unstaffed lane that keeps the work it did not staff; the quota envelope and stop condition; a statement that confirming is what creates the lanes | Proven | -| 4 | Confirms | Exactly one apply and one durable write; the card reports that LoopX state will refresh | Proven, but see gap 2 | -| 5 | Checks who can actually work | — | Gap 3 | -| 6 | Corrects or pauses one lane | — | Gap 4 | -| 7 | Waits for a lane to fail and asks who fixes it / judges completion | — | Gaps 5, 6 | +| 4 | Confirms | Exactly one apply and one durable write; the card reports that LoopX state will refresh | Proven, but see gap 1 | +| 5 | Checks who can actually work | — | Gap 2 | +| 6 | Corrects or pauses one lane | — | Gap 3 | +| 7 | Waits for a lane to fail and asks who fixes it / judges completion | — | Gaps 4, 5 | Beats 5–7 are recorded by the scenario as typed gaps with the probe that looked for them. They are not "not implemented here" hand-waving: the scenario names @@ -47,7 +47,7 @@ the selectors and phrases it searched for and what it found instead. performs one durable write; the surface must not claim a lane exists before that write, and must say what the write produced afterwards. 5. **Judge delivery by the returned result, not by the conversation.** A reply - or a message is not a completed lane. Until gap 6 closes, treat the + or a message is not a completed lane. Until gap 5 closes, treat the conversation as the request channel and the Goal's own state as the truth. 6. **Correct in the conversation the work came from.** Steering an active run is supported today; correcting a confirmed lane commitment is not yet, so avoid @@ -74,12 +74,16 @@ gitignored. No live Goal, Agent, credential or local path is read or captured. | # | Gap | Evidence the scenario recorded | Owner surface | | --- | --- | --- | --- | -| 1 | The steward's bounded prompt set (`找下一步` / `看阻塞` / `查证据`) is defined in the client model but not reachable from the conversation | probe: no steward-prompt element, no prompt phrases before the owner types | workspace composer | -| 2 | A confirmed plan does not distinguish committed / partial / all-gap / stale / rejected per lane | probe: the only outcome sentence is the generic applied notice | steward plan commit (roadmap R1 remainder) | -| 3 | No per-lane readiness ladder (registered → bound → launchable → executing) | probe: no lane-readiness element or phrase | steward readiness (roadmap R2 / audit F6) | -| 4 | No lane-level correction (pause or supersede a confirmed commitment) | probe: no lane-correction element; only run steering exists | shared alignment (roadmap R4) | -| 5 | A failed lane does not name its blocker owner and next step | probe: no lane-blocker element or phrase | recovery/continuation (roadmap R3) | -| 6 | Completion is not judged by the lane's returned result | probe: no lane-return element or phrase | return delivery (roadmap R3) | +| 1 | A confirmed plan does not distinguish committed / partial / all-gap / stale / rejected per lane | probe: the only outcome sentence is the generic applied notice | steward plan commit (roadmap R1 remainder) | +| 2 | No per-lane readiness ladder (registered → bound → launchable → executing) | probe: no lane-readiness element or phrase | steward readiness (roadmap R2 / audit F6) | +| 3 | No lane-level correction (pause or supersede a confirmed commitment) | probe: no lane-correction element; only run steering exists | shared alignment (roadmap R4) | +| 4 | A failed lane does not name its blocker owner and next step | probe: no lane-blocker element or phrase | recovery/continuation (roadmap R3) | +| 5 | Completion is not judged by the lane's returned result | probe: no lane-return element or phrase | return delivery (roadmap R3) | + +The steward prompt row used to be gap 1. The scenario now asserts it instead of +probing it: the row must expose the five shipped labels, clicking `看阻塞` must +post its message as an accepted Turn, and the composer must stay empty. A +regression fails with the missing labels named, so it cannot pass silently. ## What This Case Does Not Claim diff --git a/docs/product/use-cases/steward/README.zh-CN.md b/docs/product/use-cases/steward/README.zh-CN.md index 67cc7416b0..19d9066d58 100644 --- a/docs/product/use-cases/steward/README.zh-CN.md +++ b/docs/product/use-cases/steward/README.zh-CN.md @@ -15,12 +15,12 @@ | 拍 | 老板做什么 | 界面显示什么 | 状态 | | --- | --- | --- | --- | | 1 | 看首屏 | Goal 看板四条 lane(需要你/执行中/观察中/已安排),每张 Goal 卡给出 Agent 与下一步那句话 | 已证明 | -| 2 | 在 Goal 对话里向管家提要求 | 这句话成为一个被接受的 Turn,被准入的团队计划卡落在同一个对话里 | 已证明 | +| 2 | 在 Goal 对话里向管家提要求 | 管家的有界快捷提示行(询问下一步 / 向 Agent 获取进度报告 / 配置定时检查 / 看阻塞 / 查证据)点击即发送、不留草稿,这句话成为一个被接受的 Turn,被准入的团队计划卡落在同一个对话里 | 已证明 | | 3 | 阅读计划卡 | 每条 lane 的 Agent、第一刀 Todo(含优先级与 action kind)、验收信号,以及一条明确“未配齐”并保留未派工工作的 lane;配额包络与停止条件;以及“确认才会建 lane”的说明 | 已证明 | -| 4 | 确认 | 恰好一次 apply、一次 durable write;卡片提示 LoopX 状态将刷新 | 已证明,但见缺口 2 | -| 5 | 检查到底谁能干活 | — | 缺口 3 | -| 6 | 暂停或撤销某条 lane | — | 缺口 4 | -| 7 | 等某条 lane 失败,问谁负责修 / 用什么判定完成 | — | 缺口 5、6 | +| 4 | 确认 | 恰好一次 apply、一次 durable write;卡片提示 LoopX 状态将刷新 | 已证明,但见缺口 1 | +| 5 | 检查到底谁能干活 | — | 缺口 2 | +| 6 | 暂停或撤销某条 lane | — | 缺口 3 | +| 7 | 等某条 lane 失败,问谁负责修 / 用什么判定完成 | — | 缺口 4、5 | 第 5–7 拍由场景以 typed gap 记录,并带上“探针找过什么”的证据:不是“这里先不做”的一句话,而是列出了查找的选择器和文本、以及实际找到什么。 @@ -30,7 +30,7 @@ 2. **确认前先看四件事**:Agent、第一刀 bounded Todo、验收信号、缺人情况。看不到缺口的卡片还不具备可评审性。 3. **缺人 lane 是信息,不是失败。** 未配齐的 lane 会保留它没能派出去的工作并给出原因,老板可以选择砍掉、补人、或接受部分交付。 4. **确认是一次落地的 durable 写入。** 确认只发一次 apply、只做一次 durable write;卡片不能在写入前声称 lane 已存在,写入后必须说明产生了什么。 -5. **用回传结果判定交付,不要用对话判定。** 一条回复不等于一条 lane 完成。在缺口 6 关闭前,把对话当请求通道,把 Goal 自身状态当事实。 +5. **用回传结果判定交付,不要用对话判定。** 一条回复不等于一条 lane 完成。在缺口 5 关闭前,把对话当请求通道,把 Goal 自身状态当事实。 6. **在产生工作的那个对话里纠偏。** 对运行中的 Turn 纠偏今天已支持;对已确认 lane 承诺的纠偏还没有,所以不要确认一张可能需要撤回 lane 的计划。 ## 如何复现 @@ -52,12 +52,15 @@ LOOPX_PERSONAL_WORKSPACE_SCENARIO=steward-journey \ | # | 缺口 | 场景记录的证据 | 归属面 | | --- | --- | --- | --- | -| 1 | 管家快捷提示(找下一步 / 看阻塞 / 查证据)只定义在客户端模型里,对话里点不到 | 探针:老板输入前既无 steward-prompt 元素,也无提示文本 | 工作台输入区 | -| 2 | 确认后不区分 committed / partial / all-gap / stale / rejected | 探针:只有一条通用的“已应用”提示 | 管家计划落地(roadmap R1 剩余项) | -| 3 | 没有 per-lane readiness 阶梯(registered → bound → launchable → executing) | 探针:无 lane-readiness 元素或文本 | 管家 readiness(roadmap R2 / 审计 F6) | -| 4 | 没有 lane 级纠偏(暂停或撤销已确认承诺) | 探针:无 lane-correction 元素;只有运行中 Turn 的纠偏 | shared alignment(roadmap R4) | -| 5 | lane 失败后不说明阻塞归属与下一步 | 探针:无 lane-blocker 元素或文本 | 恢复与继续(roadmap R3) | -| 6 | 不用 lane 的回传结果判定完成 | 探针:无 lane-return 元素或文本 | 交付回收(roadmap R3) | +| 1 | 确认后不区分 committed / partial / all-gap / stale / rejected | 探针:只有一条通用的“已应用”提示 | 管家计划落地(roadmap R1 剩余项) | +| 2 | 没有 per-lane readiness 阶梯(registered → bound → launchable → executing) | 探针:无 lane-readiness 元素或文本 | 管家 readiness(roadmap R2 / 审计 F6) | +| 3 | 没有 lane 级纠偏(暂停或撤销已确认承诺) | 探针:无 lane-correction 元素;只有运行中 Turn 的纠偏 | shared alignment(roadmap R4) | +| 4 | lane 失败后不说明阻塞归属与下一步 | 探针:无 lane-blocker 元素或文本 | 恢复与继续(roadmap R3) | +| 5 | 不用 lane 的回传结果判定完成 | 探针:无 lane-return 元素或文本 | 交付回收(roadmap R3) | + +管家快捷提示行原为缺口 1。现在场景对它做断言而不是探针:提示行必须出现五个已发布标签, +点击“看阻塞”必须把消息作为一次被接受的 Turn 发出,输入区必须保持为空。回归时失败信息 +会点名缺失标签,因此不会静默通过。 ## 本文案不主张什么 diff --git a/examples/personal-workspace-browser/steward-journey.mjs b/examples/personal-workspace-browser/steward-journey.mjs index 53ac3bd919..d6ad242131 100644 --- a/examples/personal-workspace-browser/steward-journey.mjs +++ b/examples/personal-workspace-browser/steward-journey.mjs @@ -163,15 +163,53 @@ export const stewardJourneyScenario = { await stewardCard.first().click(); const goalNavigation = page.getByRole("navigation", { name: "Goal 视图" }); await goalNavigation.getByRole("button", { name: "Chat" }).click(); - // Probed before the owner types, so the owner's own words cannot be - // mistaken for a rendered steward affordance. - gaps.push(await probe(page, { - beat: "2-steward-prompts", - need: "the steward's bounded prompt set (找下一步 / 看阻塞 / 查证据) reachable from the conversation", - selectors: ["[data-testid='personal-steward-prompts']", "[data-steward-prompt]"], - phrases: ["看阻塞", "查证据"], - })); + // The row is asserted rather than probed: it ships with the product, and + // a regression here sends the owner back to typing, which is the + // behaviour this beat exists to prevent. Read before the owner types, so + // the owner's own words cannot be mistaken for a rendered affordance. const composer = page.getByLabel("向 LoopX 发送消息"); + const promptRow = page.locator(".personal-quick-prompts"); + await promptRow.first().waitFor({ state: "visible", timeout: 15_000 }); + const promptLabels = (await promptRow.first().locator("button").allInnerTexts()) + .map((label) => label.replace(/\s+/g, " ").trim()) + .filter(Boolean); + const expectedPromptLabels = [ + "询问下一步", + "向 Agent 获取进度报告", + "配置定时检查", + "看阻塞", + "查证据", + ]; + const missingPromptLabels = expectedPromptLabels.filter( + (label) => !promptLabels.some((observed) => observed.includes(label)), + ); + check( + missingPromptLabels.length === 0, + `the shipped quick-prompt row exposes the steward prompt set (missing: ${missingPromptLabels.join(" / ") || "none"}; observed: ${promptLabels.join(" / ") || "none"})`, + ); + const gatePrompt = "当前 Goal 有哪些 Gate 或阻塞?哪些需要我决定?"; + const gateChip = promptRow.first().getByRole("button", { name: "看阻塞" }); + const gateChipPresent = (await gateChip.count()) > 0; + check(gateChipPresent, "the 看阻塞 steward chip is rendered in the quick-prompt row"); + let chipTurn; + if (gateChipPresent) { + await gateChip.click(); + for (let attempt = 0; attempt < 40 && !chipTurn; attempt += 1) { + chipTurn = api.turnRequests.find((request) => request.message === gatePrompt); + if (!chipTurn) await page.waitForTimeout(50); + } + } + check(Boolean(chipTurn), "clicking the 看阻塞 chip posts its message as an accepted Turn"); + const composerAfterChip = await composer.inputValue(); + check( + composerAfterChip === "", + `a chip click sends immediately and leaves no draft behind (composer: ${JSON.stringify(composerAfterChip)})`, + ); + record("2-steward-prompts", { + prompt_labels: promptLabels, + gate_prompt_sent: Boolean(chipTurn), + composer_after_click: composerAfterChip, + }); await composer.fill(`${STEWARD_PROMPT}:请给我一份当前 Goal 的下一步。`); await page.getByRole("button", { name: "发送", exact: true }).click(); let turn;