---
name: unidesk-webdev
description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/HWLAB Cloud Web、Workbench、Performance 页、前端状态投影、Playwright、fake-server、web-probe、截图、响应式布局、Web E2E 回归或线上 Web bug 复测/提 issue 时使用。
---

# UniDesk WebDev

本技能是 UniDesk/HWLAB Web 开发、浏览器复测和 Playwright 回归的唯一操作面。需求真相源仍是 UniDesk OA 规格，例如 `project-management/PJ2026-01/specs/PJ2026-010401-web-workbench.md` 和 `PJ2026-01060505-workbench-performance.md`；本技能只规定如何开发、采集、复现和验收。

## P0 防分叉原则

- 单一权威入口：先按 issue/CLI 明确的 node + lane 解析 workspace、web-probe origin、public origin、namespace、sourceRef 和目标分支。没有明确目标时才读取受控 YAML；不得把 D601、G14、v0.2、v0.3、旧端口、ClusterIP 或本地 dev server 写成隐藏默认。
- 防分叉原则：Web/Workbench 修复必须先确定唯一 authority、唯一 API 契约和唯一状态投影入口；不得写成“当字段缺失/请求失败/状态不符/超时/刷新后，再改走另一个 endpoint、旧 workspace/conversation 模型、localStorage、trace polling、result polling、GET read-through repair、内部 manager、legacy route 或测试专用后门”。字段缺失、状态滞后或投影不完整时，修 schema、projector、read model 或正式 mutation 源头；代码里只能保留单一路径内的输入规范化、校验和错误暴露，不能用 `A ?? B`、`if missing then fallback`、兼容分支或双写双读把缺口遮住。
- 禁止状态竞争行为：不得在代码、API、reducer、read model、fake-server 或测试中保留两个事实源，再用“覆盖、压过、override、优先级、优先采用、以 A 为准否则 B、A 赢过 B”等仲裁规则决定展示状态；这就是条件分叉和竞争投影。必须先定义唯一投影对象、唯一归属字段和唯一生成时机，让 UI/API 只消费该投影对象的单一状态；其他原始字段只能作为审计输入、诊断字段或待收敛数据，不能参与展示状态仲裁。
- 严禁读侧推理：REST GET、SSE consumer、compat wrapper、CLI renderer、Web reducer/selectors、Trace renderer、fake-server 和测试断言都不能从 trace tail、message text、tool event、result cache、session summary、list row、workspace snapshot 或 localStorage 推断 turn/session/message 的 lifecycle、terminal、final response 或 running 状态。读侧只能读取唯一投影对象已经写好的字段；字段缺失、投影滞后或多字段矛盾时必须暴露 diagnostic/blocker 并修 projection writer/finalizer/schema，不得用 `result?.status ?? trace?.status`、最后一条 event `completed`、message fallback、session fallback、elapsed timeout 或 UI heuristic 补造事实。
- Sealed final response 不可被读侧诊断覆盖：assistant final response 一旦由唯一 durable projection 写入 terminal/sealed 字段，主消息正文、finalResponse、message status 和 turn terminal 就是 sealed 用户结果；turn polling、trace hydration、SSE gap、realtime timeout、transport close、compat wrapper error 或 read model lag 只能进入 trace detail、transport diagnostics、projection diagnostics、session health 或消息详情入口，不能替换主 timeline 正文。
- 无多路径、无 fallback：这是本技能的核心原则。Web、CLI、fake-server 和线上 web-probe 必须复用同一套 API 契约、同一 route/session/conversation/trace authority 和同一状态投影；不得为了让测试通过增加第二套 dispatcher、localStorage 真相、workspace snapshot 真相、内部 manager 调用、旧 lane 端口、raw request、临时兼容路径或“测试专用后门”。
- 严禁事后 repair / 0repair：Web、Workbench、fake-server 和 web-probe 不得通过 reload、切换 session、realign、`sessionRepair`、workspace selection repair、active tab repair、GET read-through、SSE gap repair、localStorage truth 或测试 helper 自动点击，把已经分裂的 route/session/message/trace 状态补成看起来正确。合法页面必须在首次进入、刷新、deep link、session 切换和 SSE 重连时按同一权威自然收敛；不一致必须暴露为 blocker/diagnostic，并修 schema、projector、read model、reducer 或 route authority 源头。允许 session-bound hydrate/gap-fill 只写目标 session 自己的 cache/read model，不得改变 active selection、URL 或当前消息区。
- Workbench GET 纯读投影：`GET /v1/workbench/*` 只能读取已持久化的 Workbench 投影事实并组装摘要；不得在 GET 中调用 AgentRun、Code Agent 或其他上游执行面做同步、补事件、修 trace、finalize terminal、改 running/completed 或写 message/session。投影滞后必须显式暴露 `projectionStatus`、`lastProjectedSeq`、`sourceRunId`、`blocker` 或等价诊断字段，并由后台 projector/finalizer 或显式受控 mutation 处理；刷新页面、切换 session 或打开详情不能成为事实推进入口。
- Workbench session lifecycle：空消息/无活动 session 的回收只能由后端 lifecycle GC、后台 finalizer 或显式受控 mutation 完成，TTL 和开关从选中 node/lane 的受控配置进入运行面；Web、fake-server 和 web-probe 不得靠隐藏 tab、前端 DELETE timer、GET read-through 或 deep link repair 清理。归档/删除后的 session deep link 必须观察同一 authority 的 404/archived diagnostic，不能补建或复活 session。
- Workbench 前端无破坏性投影权：删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`，或把 route session 标记为 not-found/archived/deleted，都是 lifecycle mutation；Web reducer/selectors、route hydrate、GET/list/detail/messages/SSE consumer、fake-server 和测试 helper 都不能由读侧失败、404、空列表、网络瞬断或 late response 触发这些 mutation。只有用户显式 mutation 成功，或后端 canonical lifecycle projection，能改变 session lifecycle。需求真相见 UniDesk OA `PJ2026-010401 Web工作台` 的“破坏性投影权”。
- fake-server 不是第二后端：它只按正式 API 契约重放脱敏 fixture 和必要边界变形。未 mock 的 `/auth/*`、`/v1/*`、`/health*` 请求应失败并暴露 path；不得访问 live Cloud API、AgentRun、HWPOD、数据库或 Kubernetes 作为通过条件。
- 真实数据优先：fixture seed 优先从目标 node/lane 的受控真实样本采集。合成 fixture 只补真实样本难以稳定覆盖的边界，并标明 `derivedFrom` 与 `syntheticReason`。
- 原入口闭环：fake-server Playwright 用例负责可重复红灯和源码回归；线上 `web-probe` 负责同一 node/lane public origin 的 P4 原入口验收。二者不能互相替代。
- Master server 禁重型验证：不要在 master server 跑仓库级 check、Web build、Playwright/browser smoke 或镜像构建。HWLAB Web 验证走目标 node/lane workspace、k3s/Tekton、D601 runner 或受控 web-probe。
- 禁止裸写 Playwright：UniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `web-probe run|script|observe`；不得直接写 `playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有 web-probe 不覆盖的短生命周期需求时，必须说明例外原因，且可复用动作要回收进 web-probe。

## 工作流

1. 定位目标：确认 repo、node、lane、workspace、public origin 和目标 SPEC。HWLAB D601 v0.3 例子是 `/home/ubuntu/workspace/hwlab-v03` + `https://hwlab.pikapython.com`，但只能在 issue/CLI 指向该目标时使用。
2. 读规则：进入目标 workspace 前读取目标 `AGENTS.md`；涉及规格或测试设计时读取 UniDesk OA 对应 SPEC。
3. 线上复现：短动作用 `web-probe run|script`，长程 Workbench/session 观测用 `web-probe observe start|command|status|stop|analyze`；保存 `scriptSha256`、`runDir`、observer id、stateDir、截图名、截图 SHA、URL、DOM/API 摘要、prompt hash、trace/session/run id。
4. 建红灯：修 Workbench/Performance 用户可见 bug 前，在目标 HWLAB workspace 的 `web/hwlab-cloud-web` fake-server Playwright 套件中补确定性用例；fixture 从真实采集样本脱敏产生。
5. 修源码：保持状态读写单一路径。状态投影类修复优先收敛 server-state/reducer/projection，不在 UI 组件、trace polling、result polling 或 localStorage 中新增竞争事实。
6. 验证：先跑 fake-server Playwright 目标用例，再回到同一 node/lane public origin 用 web-probe 复测；多轮任务必须用同一个 observer/session 采样到终态。截图、report hash 和 analyze finding 作为证据回传，不进入源码仓库。
7. 写 issue/closeout：问题登记和收口必须写明目标合并分支、node/lane、fixture 来源、Playwright fake-server 用例、线上 web-probe 命令/脚本 SHA、截图 SHA 和剩余边界。

## HWLAB Web Probe

线上 Cloud Web DOM 验收优先使用受控入口：

```bash
bun scripts/cli.ts web-probe run --node D601 --lane v03
```

web-probe 入口分三类：

- `run`：repo-owned 标准 DOM probe，适合固定 P4 验收和已有脚本。
- `script`：受控 Playwright 托管脚本，适合一轮 55 秒内完成的 DOM/API 断言、截图、route/intercept 和边界采样。
- `observe`：纯客户端长程观测，适合同一 Workbench session 多轮任务、realtime/projection 问题、长时间 trace/DOM/network 采样和无副作用报告生成。长程 Workbench 观测默认同时打开两个浏览器页面：control 页面只执行显式 `observe command` 用户动作，observer 页面只打开同一个 session 做被动观察，并默认每 3 分钟整页刷新一次同一 session，模拟用户离开后返回，用来抓多用户/多页面下同一 session 的投影差异、历史 trace 丢失、耗时跳变和 loading 差异。

工测优先级：Workbench trace 乱序、完成行位置、耗时不一致、时间跳变、final response flicker、session 不刷新、性能慢路径和多轮可靠性问题，默认先用 `observe` 采样并跑 `observe analyze`；不要把临时 Playwright spec、裸 DOM 调试脚本或单次 `script` 结果当作主要工测证据。`script/run` 只作为短路径断言、截图/API 摘要或 observe/analyze 后的补充复核。

需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`，不要裸写 Playwright：

```bash
bun scripts/cli.ts web-probe script --node D601 --lane v03 <<'JS'
export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => {
  await gotoStable('/workbench');
  const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5');
  const firstSession = sessionsResponse.body?.sessions?.[0] ?? sessionsResponse.body?.items?.[0] ?? null;
  const sessionId = firstSession?.sessionId ?? firstSession?.id ?? null;
  const apiMatrix = await fetchApiMatrix([
    '/v1/workbench/sessions?limit=5',
    ...(sessionId ? [
      `/v1/workbench/sessions/${encodeURIComponent(sessionId)}`,
      `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages`
    ] : []),
    '/auth/session'
  ]);
  recordStep('workbench-ready', { sessionsOk: sessionsResponse.ok, sessionId, apiMatrixOk: apiMatrix.ok });
  return {
    sessionsOk: sessionsResponse.ok,
    sessionCount: sessionsResponse.body?.sessions?.length ?? sessionsResponse.body?.items?.length ?? null,
    sessionId,
    screenshot: await screenshot('workbench.png')
  };
};
JS
```

长程 Workbench 观测使用 `observe` 命令组：

```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type newSession
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectProvider --provider codex-api
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type cancel
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'
long prompt
EOF
bun scripts/cli.ts web-probe observe status webobs-xxxx --tail-lines 6
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view timeline --command-id cmd-xxxx
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42
bun scripts/cli.ts web-probe observe stop webobs-xxxx
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image build --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run
bun scripts/web-probe-sentinel-service.ts --node D601 --lane v03 --state-root .state/web-probe-sentinel-smoke --scheduler-disabled --once
```

`observe analyze` 的 duplicate final response 判定必须以 trace-frame 可见行事实为准。`observe collect --view trace-frame` 固定渲染的 `Final Response` 区块是 summary，不是第二条业务 assistant message；只有同一 trace-frame 中出现两个可见 assistant final rows 且内容重复时，才应报告 duplicate finding，并在证据中写明 `finalResponseSummaryBlockCounted=false`。

`observe collect --view timeline` 用于 issue closeout 前的 artifact drill-down：它只读取现有 `control.jsonl`、`samples.jsonl` 和 `commands/{pending,processing,done,failed,abandoned}/*.json`，默认输出 bounded timeline、关键 metadata、脱敏 disclosure 和下一步命令。按 `--command-id`、`--turn`、`--trace-id`、`--sample-seq`、`--timestamp` 或 `--window-ms` 缩小窗口；需要完整原始内容时必须显式使用 `--raw` 或 `--view files --file ...`。

项目管理 / MDTODO 页面同样优先使用 `observe`，不要退回一次性 Playwright 脚本。MDTODO 主动编辑验收必须把常见动作沉淀成 `observe command`，同一 observer 串联 source 配置、HWPOD probe/reindex、文件/任务选择、Rxx 树操作、编辑写回和 Workbench launch：

```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type gotoProjectMdtodo
bun scripts/cli.ts web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --root docs/MDTODO/
bun scripts/cli.ts web-probe observe command webobs-xxxx --type probeMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type reindexMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type closeMdtodoSourceConfig
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id <opaque-source-id>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref <opaque-file-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --filename <direct-mdtodo-file-name.md>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref-or-rxx>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type openMdtodoReportPreview --task <rxx-or-task-ref> --link <report-label-or-path-fragment>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoReportFullscreen --text toggle
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskInline --task <rxx-or-task-ref> --field title --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskInline --task <rxx-or-task-ref> --field body --text 'body updated through web-probe command'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task <rxx-or-task-ref> --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task <rxx-or-task-ref> --text 'body updated through web-probe command'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task <rxx-or-task-ref> --status completed
bun scripts/cli.ts web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent <rxx-or-task-ref> --title 'web-probe command subtask'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type continueMdtodoTask --task <rxx-or-task-ref> --title 'web-probe sibling task'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type deleteMdtodoTask --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
```

MDTODO 或 Project Management 的 Web 重写/布局 closeout 不能只引用组件 diff、PipelineRun 成功或一张默认截图。最小证据应按 SPEC 覆盖：默认深链任务正文可见、Source/File 下拉显示 direct MDTODO 文件名、Rxx 任务树为 outline 辅助视图、右侧报告栏可打开且不挤爆主工作区、报告全屏可读并可关闭。涉及报告渲染时，先用有 `reportLinkCount > 0` 的任务执行 `openMdtodoReportPreview`，再执行 `toggleMdtodoReportFullscreen`，并在 closeout 中记录 command id、`reportPreviewVisible`、`reportFullscreenVisible`、报告 deep link、截图 SHA 和 analyze report SHA。

`observe` 的 Project Management 采样可能同时包含 control 页和 observer 页。control 页是显式用户动作所在页面；observer 页用于被动对照和周期刷新。若 `project-mdtodo-summary` 或 `observe analyze` 的最新汇总被 observer 周期刷新、stop 命令或根路由样本带成 `SRC=0/FILES=0/TASKS=0`，不得直接推翻已完成 command result 和 control 页面截图；必须按 `pageRole`、command JSON、control URL、screenshot path/SHA 和相邻 samples 解释。反过来，若 control command 失败、command backlog 未清空或截图 URL 不在目标 source/file/task/report 深链上，也不能用 observer 的健康样本替代用户入口证据。

`observe command --type newSession` 是显式用户/control action：它通过当前 Workbench UI 点击 `#session-create` 创建新 session，等待 active session id 改变和 composer ready，并把前后 snapshot 写入 control log。它只能用于采样开始时建立目标 session，或作为用户明确的新建会话动作；不得在 route/session mismatch 后当作 repair 手段。

约束：

- `web-probe script` 不运行默认探针，必须通过 stdin heredoc 或 `--script-file <path>` 提供脚本；只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`。
- `web-probe run|script|observe start` 的默认 URL、browser proxy mode、observe/analyze 报警阈值和 project-management 采样/命令 allowlist 必须来自 `config/hwlab-node-lanes.yaml` 的 `webProbe`；需要排除公网/FRP/跨国 proxy 抖动时，在 YAML 里把目标 node/lane 的 `webProbe.defaultOrigin` 配成内部 Service ClusterIP origin，不要在命令行长期手写 `--url` 或裸 Playwright。
- `web-probe observe start` 默认是被动观测：记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本，不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型：control 页面执行显式 command，observer 页面只同步到同一 session URL 后被动采样，并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返；周期刷新只作用于 observer，不得改变 control active session 或作为通过条件。两页的 `pageRole`、`pageId`、`sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession`、`selectProvider`、`sendPrompt`、`steer`、`cancel`、`goto`、`screenshot`、`mark`、`stop` 都必须通过 `observe command` 显式下发，并进入 `control.jsonl`；长 prompt 必须优先用 `sendPrompt --text-stdin` 或 `steer --text-stdin`，不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。MDTODO 高频操作也必须优先沉淀为 `observe command`；同类动作第二次出现时不要继续写临时 `web-probe script`。
- `observe command --type steer` 和 `--type cancel` 是显式用户/control action：steer 复用当前 Workbench composer 的运行中 turn 引导路径，cancel 复用同一 composer 主按钮的取消路径。二者必须进入 `control.jsonl`，不能用后端私有 API、AgentRun direct cancel 或测试后门替代。`configureMdtodoHwpodSource`、`probeMdtodoSource`、`reindexMdtodoSource`、`closeMdtodoSourceConfig`、`selectMdtodoSource`、`selectMdtodoFile`、`selectMdtodoTask`、`editMdtodoTaskTitle`、`editMdtodoTaskBody`、`toggleMdtodoTaskStatus`、`addMdtodoSubTask`、`continueMdtodoTask`、`deleteMdtodoTask` 和 `launchWorkbenchFromMdtodo` 也是显式用户/control action，只能使用页面公开 `data-*` id、正式按钮和 YAML 允许的自然 API；它们通过 public source/file/task id 与 Workbench 关联，不能读取内部 store、私有后端或把 MDTODO 页面包含进 Workbench。
- `observe collect --view turn-summary` 是第一层 CLI 阅读视图：只从 `samples.jsonl`、`control.jsonl` 和已有 `analysis/report.json` 按需渲染同一 session 的多 turn 摘要，包含用户消息 preview/hash、traceId、状态、耗时/最近更新时间、steer/cancel 标记和 Final Response 摘要。`observe collect --view trace-frame --trace-id <id> --sample-seq <n>` 是第二层 CLI 阅读视图：从同一采样帧渲染单帧 trace 文字截图，并固定输出 `Final Response` 区块。`observe collect --view project-summary|project-mdtodo-summary` 从同一 artifact 渲染项目管理 / MDTODO DOM 采样、Source/File/Task 计数、command/mutation 结果、Workbench launch、捕获到的 `x-hwlab-otel-trace-id` 和 OTel/Tempo drill-down 线索；project collect 的远端 payload 必须保持 bounded compact rows，由本地 renderer 生成表格，避免 `trans` stdout 截断后 JSON parse 失败。collect 视图不是采样器新增保存物，不构成第二事实源。
- `observe start/status/command/collect/analyze` 默认输出包含 `Wrapper contract` 区块；该区块证明 Web 哨兵只能 wrap 现有 observe CLI verb、现有 runner/analyzer 和既有 artifact contract，不新增第二套 Playwright runner、analyzer、状态机或私有 web-probe API。
- `web-probe sentinel plan|status` 只读取 `observability.webProbe.sentinel.enabled/configRefs` 和 owning YAML，渲染 redacted 配置引用图、文件 hash、缺失字段和跨 ref 冲突；`web-probe sentinel image|control-plane` 继续从 owning YAML 渲染 image、GitOps、Argo 和 manifest 计划，并在远端 publish job 接通前拒绝报告部署 mutation。它不启动浏览器、不读取 Secret 值、不保存采样结果，也不是第二套 runner/analyzer。真正的采样和判定仍以 `observe start|command|collect|analyze` artifacts 为准。
- Web 哨兵 public dashboard/origin 必须以 issue/SPEC/YAML 既定计划为准；当前 P6 计划沿用 `monitor.pikapython.com`，不要未经明确变更改成 `hwlab-monitor.pikapython.com` 或其他新域名。验证 report 时记录 `publicOrigin`，但不要把域名硬编码到 runner/analyzer 逻辑里。
- 验证 sentinel public dashboard **页面 DOM/截图**（区别于 CI/CD `sentinel status|plan|image|control-plane`）时，优先使用 `web-probe sentinel dashboard verify|screenshot --node <node> --lane <lane> --sentinel <id>`；该受控入口会从 YAML public exposure 进入 dashboard 页面，使用目标 node/lane 远程浏览器，并输出 bounded DOM/截图证据。`web-probe script` 的默认 origin 仍是 lane 的 HWLAB Cloud Web，不是 sentinel dashboard origin；只有受控 dashboard 命令暂时覆盖不到的一次性探索才用显式 `page.goto('<sentinel publicBaseUrl>/')`，同类动作第二次出现时必须沉淀回 sentinel dashboard 命令。
- `scripts/web-probe-sentinel-service.ts` 是 Web 哨兵 Pod entrypoint；`--once` 只做 config/PVC/SQLite/scheduler/analyzer-command health 快照，`--scheduler-disabled` 仅用于本地服务健康冒烟，不能作为生产运行参数。HTTP 服务只提供 `/api/health`、`/api/status`、`/api/runs`、`/api/maintenance`、`/metrics` 和 redacted dashboard 外壳，底层采样仍只能经 observe CLI adapter。
- `trace-frame` 出现 `(无 trace rows；这是 blocker...)` 时，必须先看同一输出中的 `TRACE DIAGNOSTIC`：记录 pageRole/pageId、traceRows/turns/messages 数量、sampleTraceIds、尾部 traceRow/turn/message 归属。若目标 trace 的 turn/message/final 存在但 traceRows 全部属于旧 trace，应按 Workbench read model authority 分裂登记到架构/业务 issue（例：HWLAB #2124），不得把旧 traceRows 当作新 turn 通过证据，也不得让 analyzer 的聚合计数压过 CLI trace 视图。
- analyzer finding 不得压过 CLI `trace-frame` 人工视图。尤其 `trace-assistant-message-duplicates-final-response` 只有在 `trace-frame` 中同一 completed turn 可见多条相同 assistant final rows 时才按业务 bug 处理；如果 `trace-frame` 只有一条 assistant final row、后面固定 `Final Response` 区块正确且 API messages/turns 对齐，该 amber 归类为 analyzer 精度问题，应登记/修工具，不得阻止业务 closeout。
- 若 `observe status` 显示 PID still alive 但 heartbeat/sample 不推进、`commands/pending/*.json` 不被消费，或 `observe stop --force` 只是继续排队 stop command，应先按 web-probe runner 工具缺陷处理（例：UniDesk #874），用 route 只读确认 PID/heartbeat 后清理进程；不要把 pending command、未触发的 cancel 或 runner stale 混入 Workbench 业务结论。
- `web-probe observe` 的 issue evidence 优先记录 observer id、stateDir、report JSON/Markdown SHA、samples/control/network/artifact 计数、routeSessionId、activeSessionId、prompt hash/textBytes、traceId、AgentRun runId/commandId、最终 status 和必要摘要；不要把 prompt 原文、assistant 大段正文、完整 stdout/stderr 或 provider payload 粘贴到 issue。
- 多轮 Workbench 采样必须证明同一个 `sessionId` 连续承载所有轮次；每轮至少记录 prompt hash、traceId、终态、最终回答摘要和性能/产物表。若 Web UI 投影卡住但 Code Agent/AgentRun result 已 terminal，应同时登记“执行终态”和“Workbench 投影未收敛”，不得用 `goto`、reload、切 session 或 result polling 把 UI 失败伪装成通过。
- `observe command --type sendPrompt` 是普通新 turn 路径，composer 主按钮应处于 `data-action=turn`；如果仍是 `steer` 或 `cancel`，必须作为工具/页面状态不一致失败暴露，不能点击后再等待错误接口。空输入状态下 submit disabled 可以为 true，不能把 disabled 当作新 turn 不可用的唯一依据；应先看 input present/enabled、warning absent 和 action=turn，再由正式填入 prompt 后提交。
- `observe analyze` 是离线分析，只读取 artifact JSONL 并写 `analysis/report.md` 与 `analysis/report.json`，不访问 Workbench API、不驱动浏览器。`observe start` 每次启动必须先把同一 stateDir 中已有的根目录 JSONL 轮转到带时间戳的 `archive/` 文件；`observe analyze` 默认只分析当前根目录 JSONL，不扫描历史 archive，只有显式指定 archive prefix 时才分析历史轮转窗口。报告必须输出采样点 vs 每个 turn 的总耗时/最近更新时间表、trace row 视觉顺序异常、terminal/轮次完成 row 是否最后、Code Agent 卡片耗时与 trace/轮次完成总耗时一致性、可见“加载中”的数量/归属/并发 owner/连续出现区间、DOM diagnostic/HTTP/console/requestfailed/runtime execution error 分组、page asset provenance segment、同源 API Resource Timing 分位表和超过 YAML `webProbe.alertThresholds` budget 的慢路径 finding；项目管理页还必须输出 DOM readiness、source/file/task 计数、缺失 public task ref、Workbench launch success/failure、captured OTel trace header、自然 project-management API 分组和超过 YAML `webProbe.projectManagement.slowApiBudgetMs` 的慢路径 finding。页面/API 加载、可见“加载中”、长连接打开耗时、turn timing 跳变、trace row 顺序、卡片耗时/轮次完成耗时一致性、session fallback 标题比例和项目管理 API 慢路径阈值只能改 YAML，不能在 analyzer/renderer 中写死。修复必须降低真实请求、投影、渲染或后端路径耗时，禁止为了减少“加载中”出现时间而提前展示未加载完的内容，也不能靠下游 retry/reload/fallback 掩盖。报告里的 `trace-row-order-nonmonotonic`、`trace-completion-row-not-last`、`round-completion-elapsed-mismatch`、`code-agent-card-duration-underreported`、`final-response-flicker`、`uncommanded-visible-state-change`、`mdtodo-workbench-launch-otel-trace-missing`、`project-management-api-slow`、session changed、network 503 等 finding 是排障线索；用于 closeout 时必须结合原始 session/trace/DOM 证据解释，避免把采样噪声直接当作业务结论。
- `sentinel validate --quick-verify` 超过 120s 是严重超时，必须保持 warning/red 并优先从 envreuse、git mirror、warm runner 复用、first tool execution、Workbench 投影和 observe/analyze 开销排查；不要通过提高 budget、减少轮数、放宽 analyzer 或绕过 CLI trace 视图来让场景变绿。quick-verify 等待每轮终态时应读取既有 sampler artifacts 和 bounded collect 视图，不能反复启动完整 collect 或新增第二份“trace 截图”保存来源。
- 自定义 `web-probe script` 仍运行在 UniDesk `trans` 60s 最外层短连接约束内；能在一轮内完成的 P4 验收优先把 `--command-timeout-seconds` 控制在 55 秒以内，并减少无界 selector/network 等待。确需等待更久时，改用 `web-probe run` 的异步 job/status 语义，或把动作拆成“提交/采样/截图/状态读取”多次短 probe。若输出出现 `UNIDESK_SSH_RUNTIME_TIMEOUT` 但同时恢复了 `reportPath`、`reportSha256`、screenshots 或 DOM steps，先按远端报告判断脚本/页面实际状态；最终关闭证据仍优先用一次未触发短连接超时的 bounded rerun。
- issue closeout 优先引用 `web-probe script` 输出的顶层 `issueEvidence` 或 `summary.issueEvidence`；只有需要展开调查时才粘贴 `probe.script.result`、`probe.steps` 或完整 `reportPath`，避免 stdout、summary 和 report 多层重复同一证据。
- stdin heredoc 与 `--script-file` 都按 ES module 加载，脚本必须导出 `export default async ({ page, gotoStable, recordStep, ... }) => { ... }`；不要在模块顶层直接写 `return`。失败为 `Illegal return statement`、`does not provide an export named default` 或 finalUrl 仍是 `about:blank` 且 stepCount=0 时，先按 probe 脚本入口误用处理，不要归因成 Cloud Web 行为失败。
- 自定义脚本需要主动失败时，优先返回 `{ ok: false, failedCondition: "..." }` 或抛出错误；兼容返回 `{ pass: false }`、`{ success: false }`，且 `{ ok: true, pass: false }` 仍按失败处理。`failedCondition`、`errorMessage`、`message`、`error`、`reason` 或 `summary` 会进入失败摘要；不要只把失败埋在普通业务字段里。
- web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`；脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。
- 需要禁用 `EventSource`、mock `Date`/clock、注入 preload hook 或修改浏览器启动前全局对象时，`page.addInitScript()` 必须在目标页面整页加载前注册，并用 `page.goto(new URL(path, origin).toString(), { waitUntil: "domcontentloaded" })` 进入目标 deep link；不要依赖 `gotoStable()` 复用当前 SPA 页面后再期待 init script 生效。若只在已加载页面上 route/block 请求，既有 SSE 或 store 状态可能继续更新，不能作为“无后端刷新依赖”的验收。
- 脚本构造 URL 时使用 `new URL(path, baseUrl).toString()`；不要拼出 `//v1/...`。
- 先通过 `gotoStable`、`waitWorkbenchReady` 或等价导航进入目标 origin，再用 `fetchJson` 读同源 API；不要在 `about:blank` 上请求 `/v1/...`。`fetchJson` 返回包装对象 `{ ok, status, body, failureKind, ... }`，业务字段必须从 `.body` 读取，并同时记录 `ok/status/failureKind`；如果返回 `status=0` 且页面还不是目标 origin，先按 probe 脚本误用处理，不要归因成 Cloud API 或 Workbench 投影失败。
- Workbench 有 SSE/长轮询时不要用 `networkidle` 判断通过；用明确 DOM/API 条件，如 final URL、route sessionId、active tab、message card、trace row 或 durable session detail/messages。
- 当前 HWLAB v0.3 Workbench durable read model 的验收端点是 `/v1/workbench/sessions`、`/v1/workbench/sessions/{sessionId}`、`/v1/workbench/sessions/{sessionId}/messages`、`/v1/workbench/turns/{traceId}` 和 `/v1/workbench/traces/{traceId}/events`；提交 mutation 可用正式 `/v1/agent/chat` 产生真实 turn。`/v1/workbench/workspace` 只在当前 API 契约声明为选择/config 快照时采集，不作为消息或 trace authority；`/v1/agent/conversations` 属于旧模型，不得作为 v0.3 Workbench durable projection 的通过条件。
- 需要稳定 running message、timing、SSE 或 projection 样本时，优先通过正式同源 mutation 创建样本：`POST /v1/agent/sessions` 建 session，再按前端同一路径提交 `/v1/agent/chat`，轮询 `/v1/workbench/sessions/{sessionId}/messages` 等待 durable projection。不要把历史 session 是否仍 running 当作验收前提，也不要用旧 conversation/workspace 端点造样本。验收结束后能取消的 turn 必须调用 `/v1/agent/chat/cancel` 清理，并在 issue evidence 只记录 trace/session、status、script/report SHA 和必要摘要，避免粘贴完整 provider payload。
- HWLAB v0.3 `fa2ad4845a111e0f9e86d473eb4204ed1b2b0a3c` 后，repo-owned `scripts/web-live-dom-probe.mjs` 的默认 `web-probe run --fresh-session` 已使用 durable Workbench session 端点，不应再依赖 `/v1/workbench/workspace` 或 `/v1/agent/conversations` 作为消息/trace authority。若 `session-not-selected` 复现，先确认目标 workspace 已包含该提交，再对 helper 做旧端点扫描；只有扫描仍命中旧端点时才按工具链回归处理。
- `web-probe run --fresh-session` 空消息路径的基线证据是 D601 v0.3 job `web-live-dom-probe-1781842424721-44066`：`freshSessionAligned=true`、`messageCount=0`、report SHA `d34c6765943e14bbd639b91bfc17139d582f670ed5cf409a957acd40fae7871f`、screenshot SHA `8d341b7c30293baa9be8f1e63e147ea0fdafbdbcfc43f0b05e4fb9f316ef996c`。
- 若带 `--message` 的 fresh-session 路径已经达到 `freshSessionAligned=true`、`promptSubmitted=true`、`messageCount>0` 和 session API 200，但随后报 `trace-id-missing`、`Workbench turn is not visible to the current actor` 或 trace 面板同类不可见文案，应登记为 Workbench turn/trace actor visibility 问题，不再归因到 fresh-session 选择 helper 或旧端点。
- Playwright `page.evaluate` 只能传一个可序列化参数；多个值包成对象，或用 `safeEvaluate(fn, { a, b })`。
- `safeEvaluate` 返回结构化包装对象；脚本断言前必须解包，例如 `const evaluated = await safeEvaluate(...); const dom = evaluated?.value ?? evaluated;`。`recordStep` 可以记录包装对象用于诊断，但行为断言不要直接读包装对象的业务字段，避免把已通过的 DOM 误判成 `undefined`。
- 脚本用 `page.evaluate` 模拟滚动、resize、click 或派发 DOM event 后，若断言 Vue/React 绑定出的 `data-*`、ARIA、按钮状态或 scroll-follow 状态，必须先等待一次浏览器/框架更新，例如 `await wait(300)`、`page.waitForFunction(...)` 或下一次明确 DOM 条件；不要在同一个 evaluate 内写完事件就立刻读取状态，否则会把尚未 flush 的响应式状态误判为业务失败。验证用户滚动暂停/恢复时优先记录操作前后 scroll metrics 和 `data-following` 的延迟稳定值。
- `web-probe script` 顶层/default export 运行在 Node 上下文，浏览器全局 `window`、`document`、`location`、`CSS` 只在 `safeEvaluate`/`page.evaluate` 回调内可用。Node 侧 deep link 用 `const origin = new URL(page.url()).origin; await page.goto(new URL('/workbench/sessions/' + encodeURIComponent(id), origin).toString())`，或直接用 `gotoStable('/workbench/sessions/<id>')`；不要在 Node 侧写 `location.origin`、`CSS.escape(...)`，也不要用没有 baseURL 的 `page.goto('/relative/path')`。若失败为 `location is not defined`、`CSS is not defined` 或相对 URL invalid，先按脚本误用处理；工具提示改进跟踪见 [pikasTech/unidesk#479](https://github.com/pikasTech/unidesk/issues/479)。
- 脚本中用 `recordStep(name, data)` 保存关键 DOM/API partial evidence；API 批量探测优先用 `fetchApiMatrix(paths)`，单个 API 失败不应让后续证据丢失。
- deleted/archived/not-found session deep link 验收用 `web-probe script` 直接 `gotoStable('/workbench/sessions/<id>')`，再用同源 `fetchJson('/v1/workbench/sessions/<id>')`、messages 和 list API 断言 404/不在列表；不要调用要求 active session selection、composer readiness 或 workspace repair 的 helper 作为通过条件。
- 验证 Cloud Web runtime config 或 HTML 注入时，优先加 cache-busting query 和 `cache-control: no-cache`，并按层拆开判断：Deployment env、Pod 内 `127.0.0.1` HTML、公网原始 HTML、浏览器 DOM。不要只凭一次 web-probe DOM 缺字段就判定 rollout 失败；先确认是运行面未渲染、Pod 未滚动、边缘缓存，还是脚本读取层级错误。
- web-probe 只能观察、截图和断言，不得用 `sessionRepair`、`realignFreshSession`、自动点击 session、reload 循环或 workspace selection repair 作为通过条件。发现 `routeSessionId`、`activeSessionId`、`activeConversationId`、message session 或 trace session 不一致时，必须记录 mismatch 并失败；截图和 API 摘要是证据，不是修复动作。
- 失败证据至少保留 `failureKind`、`errorMessage`、`scriptSha256`、`runDir`、`lastUrl`、`lastScreenshot`、`probe.summary` 和 `reportPath`/`reportSha256`；默认失败截图是 `failure.png`。

## Fake-Server Playwright

Workbench 回归入口在目标 HWLAB repo 的 `web/hwlab-cloud-web`：

```bash
bun run e2e:workbench -- --project=chromium
```

运行面要求：

- 在 issue/CLI 选中的 node/lane workspace 或其独立 worktree 上运行，不在 master server 本地运行。
- 同源 fake-server 只提供正式 API 契约，覆盖 `/auth/*`、session list/detail/messages、turn snapshot、trace event page、Workbench event stream、Performance summary 等当前用例所需端点。
- 测试断言优先使用用户可见文本、ARIA role 和稳定 DOM 标识；不要通过内部 store、localStorage、数据库或 live API 直接判定通过。
- 非终态瞬时不变量必须用立即 DOM 快照断言：例如 running/pending 阶段要求 final response 为空时，先定位当前 agent card，再用 `locator.evaluate` / `page.evaluate` 直接读取 `.message-markdown.message-text` 数组并立刻判断；不要用会自动等待的 `expect(locator).toHaveCount(0)`、`not.toContainText` 或类似负向 locator 断言，否则测试可能等到 terminal DOM 替换后误通过，掩盖 running 阶段污染。
- 失败必须保留截图、Playwright trace 或等价 artifact；关键路径可生成命名截图。

Workbench 至少覆盖：

- session 切换后主工作区 loading 和目标 conversation 恢复；
- 刷新页面后以同一 message/turn/trace 标识还原 timeline；
- SSE 断线、丢事件或重连后通过 REST snapshot 与 trace page 补洞；
- session tab、workspace card、message card、composer 主按钮、Trace detail 的 `running`/`completed`/`failed`/`canceled` 一致性；
- completed assistant 已 sealed 后，turn snapshot、trace event page、SSE 或 realtime diagnostic 失败不覆盖主消息正文，诊断只进入详情/感叹号/transport health；
- Trace 阅读按事件顺序渲染可读 row，正确处理分页、终态、失败、自动展开和终态折叠；
- deep link 与普通点击 session 走同一 authority path；
- Performance 页的摘要、错误态、刷新、移动端表格可达性和低基数脱敏展示。

## Fixture 采集与脱敏

- 采集范围至少包括 session list/detail/messages、session/turn snapshot、trace event page、Workbench event stream 和 Web performance summary；workspace selection/config 只有在当前 API 契约仍声明该读模型时才采集。
- 产物必须记录 `capturedFrom`、`capturedAt`、schema/redaction 版本和 `valuesPrinted=false`。
- 使用稳定伪 ID 映射保留 conversation、session、thread、turn、trace、message、sourceSeq 的关系。
- 删除或替换 `HWLAB_API_KEY`、cookie、Authorization header、DB DSN、provider token、真实用户身份、非公开 prompt、stdout/stderr 大段原文和完整 provider payload。
- 真实样本难以覆盖的延迟响应、SSE 断线、分页缺口、列表缺少当前选中项、可选字段异常、空集合和特定 HTTP 错误可以合成，但必须说明来源和原因。

## Workbench 判定口径

- Workbench API 的 GET 路径不是修复入口。若 session/messages/turn/trace 出现状态不一致，应登记为投影唯一性或投影延迟问题，修 backend projector/read model；不得用前端轮询、session 切换、reload、read-through sync 或 GET 内部修复把问题掩盖。
- Workbench 读侧不得推理生命周期。`turn.status`、`message.status`、`session.running`、`trace terminal`、`finalResponse`、`projectionStatus` 只能来自唯一 durable projection；trace event row 的 `completed` 只表示该 event/tool row 完成，不能终结 turn；message 文本为空只表示无可展示正文，不能生成占位 final response；list summary、session detail、turn snapshot、trace page 之间的差异只能作为 diagnostic，不得用优先级函数在读侧“选一个看起来合理的状态”。
- Workbench sealed final response 必须与读侧诊断分仓。`messageProjection` 承载 role/status/text/finalResponse/sealedAt，`traceDetail` 承载 events/page hydration/trace diagnostics，`sessionStatus` 承载 rail/card 运行摘要，`transportDiagnostics` 承载 SSE/poll/hydration timeout 等可见性诊断；等价结构可以使用不同命名，但主消息投影和 diagnostic 不能竞争同一字段。`showMessageText()`、message diagnostic selector 和组件模板不得让 diagnostic 压过 sealed final response。
- Workbench trace 视觉顺序的唯一权威是 durable projection 分配的 `projectedSeq`。`projectedSeq` 必须由持久化 runtime store 在 trace 内全局递增分配，并按 `traceId + sourceEventId` 幂等复用；本地 `event.seq`、`sourceSeq`、事件到达顺序、时间戳、DOM 顺序和 renderer 输入顺序只能作为审计输入或源游标，不能直接派生视觉位置或参与仲裁。重复源事件不能分配新 `projectedSeq`；不同源事件即使局部 `event.seq` 重置为 1 也必须分配新的全局 `projectedSeq`，这个幂等性需要有最小单元测试覆盖。
- Workbench trace 时间字段的 source truth 是后端和 trace event page 暴露的 ISO/UTC 时间戳；后端 projector、durable read model 和共享 renderer 默认行为不得改成本地时区字符串。Web 展示北京时间或其他用户时区时，只能在 Vue/Web 渲染层从 runtime `displayTime` 注入 formatter；CLI、CaseRun 和原始 trace API 仍显示/返回 UTC，不通过多字段仲裁、默认 fallback 或后端补写时区字符串修显示。
- Trace event page 检测到同一 trace 内历史 `projectedSeq` collision 时必须暴露 `projectionStatus=blocked`、`projectionHealth=degraded` 和稳定 blocker code，不能把损坏序列交给 CLI/Web renderer 继续展示。CLI/Web trace renderer 只能按 projection 已给定的位置一次渲染；不得通过 assistant 文本相等/包含、final response 匹配、尾部排序、terminal row 合并或 completion row 追加来事后修正视觉顺序。
- Workbench durable projection 验收必须覆盖同一 session/trace 的 list、detail、messages、turn、trace events 和 DOM session tab/message card；尤其要用 fresh browser 或 runtime 重启后的历史 terminal 样本证明 read model 能从 durable trace events 恢复终态。若内存 trace 仍显示 running 而 durable trace 已 terminal，应修唯一 read model 的投影对象，不得在 GET handler、前端 reducer 或 probe 脚本里做临时状态仲裁。
- HWLAB #1585/#1596/#1690 是 Workbench 投影禁令的固定判例：AgentRun 已 terminal 但 Workbench 停在旧 seq 时，不得用 GET/read-side 补 result；turn/card completed 但 TraceEventPage seq/range 不合法时，不得用 completed 状态压过 trace 不可读事实；Trace timeline 乱序或 terminal row 重排时，不得让 `sourceSeq`、局部 `event.seq`、输入顺序和 renderer 文本匹配多来源仲裁。合法修复是后台 projector/resumer 补 durable projection，在同一 trace event 快照内修正分页/顺序契约，或暴露 historical projection blocker；不得推断 lifecycle、补写 GET、切 session/reload 修页面、添加多来源优先级或在 renderer 做事后 repair。
- Session rail 会话集合只消费 `/v1/workbench/sessions`；当前 session detail 只消费 `/v1/workbench/sessions/{sessionId}`；消息本体只消费 `/v1/workbench/sessions/{sessionId}/messages`；turn 状态只消费 `/v1/workbench/turns/{traceId}`；trace 阅读只消费 `/v1/workbench/traces/{traceId}/events` 或正式 trace event page。不要让 workspace snapshot、列表摘要、localStorage、trace polling 或 result polling 互相覆盖。
- 空消息 session 回收验收必须同时证明：未超过 TTL 的空 session 保留，有消息、running/admitting、terminal result 或 active trace 的 session 不被回收；超过 TTL 的空 idle session 由后端 GC 归档/删除后，rail/list/detail/messages/deep link 都按同一 canonical lifecycle 状态表现。
- UI transient state 只保存 route、显式选择、composer draft、scroll、展开状态和临时交互状态；服务端事实进入 server-state/reducer/projection。
- running turn 必须同时反映在主任务区、composer 主按钮、session tab 和可取消入口；清空、切换、刷新不得把 running trace 隐藏成空态。
- 显式新建或选择 session 是 active selection 的 mutation authority；`POST /v1/agent/sessions` 返回 201 或用户点击目标 session 后，URL、active tab、当前消息区和 composer 必须立即归属目标 session。目标 session detail/messages 慢只能表现为该目标 session 的 loading/empty 态；旧 session 的 list refresh、detail、turn、trace、SSE 迟到事件只能更新旧 session 自己的 cache/status，不得继续占据或夺回 active route、message area、composer/cancel 状态。
- 读侧没有破坏性投影权。`GET /v1/workbench/sessions`、session detail、messages、SSE、route hydrate 或 refreshSessions 的失败、404、空列表、网络错误和迟到响应，只能显示 loading/degraded/unknown/blocker 或保留上一份成功投影；不得 `forgetSession()`、`replaceActiveSessionSelection(null)`、清空 messages/tabs，或把 composer 改成 `session_required`。deleted/archived/not-found 必须来自后端 canonical lifecycle projection，或来自用户显式 DELETE mutation 成功后的展示更新。
- terminal failure、blocked、canceled 都是最终结果；若 API 返回可读 error/blocker，final response 展示区必须直接显示，不能只放在 trace rows 或详情面板中。
- completed、failed、canceled、blocked 的 sealed final result 一旦显示，后续 polling/hydration/SSE/realtime 的 timeout、500、gap、close 和 lag 只能改变诊断区，不得把已显示 final response 闪回 `Code Agent 代理暂时无法连接上游`、`turn 超时无新活动`、`projection-resume:sync-failed` 或同类读侧错误态。
- session 切换验收必须覆盖点击态和持久化恢复态：选择目标 session、确认 route/session list/detail/messages 归属同一 `sessionId`、刷新后同一 session 仍 active。
- completed turn / Trace 重放 / deep link 验收必须用 fresh browser context 或等价 fresh login 直接打开 `/workbench/sessions/<sessionId>`，不能只用同页 reload。
- 0repair 验收必须用 fresh context 直达 session/deep link，或从当前 session 显式切换到目标 session；不得依赖测试 helper 选择、reload、切走再切回、`sessionRepair` 或 `realignFreshSession` 修正页面后再判通过。旧 session 的延迟 detail、turn、trace、SSE 或 refreshSessions 晚到，只能更新该 session cache/list，不能改变 active URL、active tab、current messages 或 composer。
- Workbench reducer/selectors/0repair 收敛类 closeout 必须做负向源码扫描并在 issue/PR 证据里写明结果；至少覆盖直接写服务端事实和旧 repair 名称，例如 `messages.value =`、`messages.value.push`、`sessions.value =`、`previousMessages`、`previousSessionId`、`restorePrevious`、`restoreMessagesTraceAuthority`、`repairWorkbenchMessagePage`、`findMessageTextFallback`、`projectionCatchup`、`PROJECTION_CATCHUP` 和组件侧 `workbench.messages`。这些扫描是辅助证据，不能替代 fake-server Playwright 与 public web-probe。
- Trace 实时性要用间隔采样，区分“加载中”和“思考中”；终态卡片不得继续保留“思考中”空态。
- 用户反馈“当前 session 不刷新、切走再切回才刷新”时，证据必须成对采集：直达目标 URL、原地等待、切到其他 session、切回目标 session 的 DOM 截图；同时记录 session list、session detail、messages、turn snapshot 和 trace event page 的状态。issue 正文要明确这是事件投影/重放问题，不能只登记成普通 deep link 或单接口 404。

## Performance 页判定口径

- `/performance` 只展示低基数、脱敏的 Workbench/RUM/API/long task 汇总；不得泄露 `conversationId`、`sessionId`、`traceId`、`runId`、prompt、assistant 正文、tool 参数、stdout/stderr、Secret 或用户身份。
- 单位以 API `unit` 为准；后端若以 seconds 存储，前端显示 ms 时必须只做一次换算。
- 移动端和窄屏断言要检查目标表格/按钮是否可见、可点击、layout rect 非零且可触达；不要只用 desktop selector readiness 代表所有 viewport。
- 刷新按钮应有明确 loading/error/empty 状态，重复点击不能制造 stuck loading、旧数据冒充新数据或未捕获 console/page error。

## UniDesk Frontend E2E

UniDesk 主前端交付门禁仍由 `bun scripts/cli.ts e2e run` 承担；本技能规定浏览器侧如何选取和解释验证。

- 先跑最小相关选择集，例如 `bun scripts/cli.ts e2e run --only frontend:pipeline`；目标检查通过后再跑完整 `bun scripts/cli.ts e2e run`。选择集是真执行，不是输出过滤。
- 浏览器必须打开 `config.json` / publicHost 派生的公开 frontend 或 dev frontend proxy；不要用 localhost、Docker internal URL 或后端 API 直调替代用户入口。
- 前端断言优先使用用户可见文本、ARIA role、稳定 DOM 标识和 React 控件状态；禁止用 raw JSON、内部 store、localStorage 或后端数据库绕过 UI。
- 高信息密度区域如 Pipeline 右侧栏、Trace timeline、详情抽屉、甘特图坐标、资源监控表和 Performance 面板，必须显式检查总高度、横向滚动条、关键控件可见可点、文本不重叠。
- 用户服务页必须等待真实后端数据，而不是只看页面骨架；Todo Note、OA Event Flow、Code Queue、FindJob、Pipeline、MET Nonlinear、ClaudeQQ 等页面的通过条件以当前 E2E check 名称和 issue scope 为准，不在文档里复制所有字段清单。
- raw JSON 只能在用户显式点击 `查看原始JSON` 后出现；默认页面必须把 JSON 渲染为表格、卡片、徽章、树、图或其他可读控件。
- 任何 frontend E2E 失败都应保留截图路径、resultPath、failedChecks 和关键 console/page error 摘要；不要把 missing browser/deps 当作 skip，按锁文件和目标执行面补依赖后继续。

## 受控 web-probe 代替裸 Playwright

UniDesk/HWLAB Web 工作不再把裸 Playwright 当作默认操作面。需要截图、DOM 断言、API matrix、route/intercept、SSE 降级、长程 session 采样、性能页面复测或 Workbench 多轮任务时，统一选择 `web-probe run|script|observe`。

`web-probe observe analyze` 必须把 Workbench session 列表标题纳入默认采样与报告：可见列表中 `Session ses_...` fallback 标题超过一半时输出红灯 finding。修复方向必须让上游 session list projection 直接携带名称；点击详情后的下游补名、reload repair 或多来源仲裁不能作为修复。

`playwright-cli` 和临时 Node Playwright 脚本只允许作为 web-probe 尚未覆盖且一次性不可复用的诊断例外。例外使用前必须写明为什么 `web-probe run|script|observe|screenshot` 不适用；同类动作出现第二次就应补进 web-probe CLI 或 repo-owned probe。

需要把截图回传到本机时，优先用 `web-probe screenshot`，或长程观测中使用 `observe command --type screenshot --label <label>`，并在 issue 中引用截图 SHA、report SHA、observer id 和 stateDir。不要把本地 Playwright artifact 当作 HWLAB node/lane 原入口验收替代品。
