From 3ee2982f853946cef8f567fa5c9207f3306e2d42 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 27 Jul 2026 12:02:28 +0800 Subject: [PATCH 01/10] optimize chat ui --- .../2026-07-23-toolview-dissolution.i18n.yaml | 4 +- .../2026-07-23-toolview-dissolution.md | 2 +- .../2026-07-23-toolview-dissolution.zh.md | 2 +- ...026-07-23-web-assistant-markdown.i18n.yaml | 4 +- .../2026-07-23-web-assistant-markdown.md | 14 +- .../2026-07-23-web-assistant-markdown.zh.md | 14 +- ...-07-27-user-message-icon-actions.i18n.yaml | 6 + .../2026-07-27-user-message-icon-actions.md | 27 +++ ...2026-07-27-user-message-icon-actions.zh.md | 27 +++ .../ui-conversation/src/client/apply.ts | 3 +- .../src/client/chat/ChatView.module.css | 12 +- .../src/client/chat/MessageItem.module.css | 43 +++- .../src/client/chat/MessageItem.tsx | 81 ++++++- .../src/client/skeleton/InputBar.module.css | 2 +- .../client/toolviews/bash-sample.module.css | 49 ++-- .../src/client/toolviews/bash-sample.tsx | 53 +++-- .../tests/chat-branch-tails.spec.tsx | 80 ++++++- .../tests/chat-code-subcalls.spec.tsx | 5 +- .../tests/chat-stats-bash-sample.spec.tsx | 4 +- .../tests/chat-toolview-slot.spec.tsx | 1 + .../ui-conversation/tests/chat-view.spec.tsx | 2 +- .../tests/coverage-tails.spec.tsx | 51 +++-- .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 2 +- packages/client/ui-primitives/README.zh.md | 2 +- .../src/markdown/CodeBlock.module.css | 79 ++++++- .../ui-primitives/src/markdown/CodeBlock.tsx | 80 +++++-- .../src/markdown/MarkdownText.module.css | 213 +++++++++++++----- .../ui-primitives/tests/code-block.spec.tsx | 73 +++++- .../ui-primitives/tests/markdown.spec.tsx | 4 +- packages/client/ui-theme/src/styles/base.css | 2 + 31 files changed, 759 insertions(+), 186 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md create mode 100644 .agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml index 6de82d1c9b..2cba925d67 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-23-toolview-dissolution.md: a420c5945d0272cf8087d5f623e9c383c286d7c2 -2026-07-23-toolview-dissolution.zh.md: 47c1f392f5f7ddbf4e6c686b2574faa7987e6126 +2026-07-23-toolview-dissolution.md: 80c2688b152d1afe1236d4815633a5bf024db1d2 +2026-07-23-toolview-dissolution.zh.md: 928c5f445d601b2246d3ae2f9360232643814468 diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md index a420c5945d..80c2688b15 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md @@ -14,7 +14,7 @@ After the view ring dissolved into the slot system, the client kept exactly one The tool ring is gone as independent infrastructure: a tool row is a **keyed child slot each view declares for itself**, and the client has exactly one registration model. The justification above was hollow — a keyed slot's *key space* is already runtime-open (SlotMap declares slots, never keys; the ask-user composer's `key: 'question'` was the precedent), so the open tool-name set fits `entryKey` dispatch natively. -Shipped shape (current-state narrative also in the [architecture note](2026-07-19-gui-web-client-architecture.md)): the chat entry's `children` table declares `'conversation.chat.toolview'` (keyed/session); the render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback` (the default card is domain property; the fallback option is ordinary renderSlot grammar). The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails` — details being a session-level facility, not chat-private), and `ToolRowProps` pre-composes it with the session standard kit for registrant components. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam — apply mounts `ConversationService` *after* the chat registration, so the service being present guarantees the slot is declared, by construction. Session-dimension differentiation happens inside the component (`useSessions` reading `parentId` — the decision sits where all the information already is); the bash sample is the third-party-posture exemplar. Trajectory/waterfall toolview slots share this exact shape (names fixed by the slot-naming discipline `..`, one shared owner type) and land with their own row render sites — RendersCheck rejects a declaration nobody renders, so the type system, not convention, blocks early empty declarations. +Shipped shape (current-state narrative also in the [architecture note](2026-07-19-gui-web-client-architecture.md)): the chat entry's `children` table declares `'conversation.chat.toolview'` (keyed/session); the render site dispatches per row via `entryKey: toolName` with `GenericToolCard` as the call-site `fallback` (the default card is domain property; the fallback option is ordinary renderSlot grammar). The owner payload is the uniform `ToolRowOwnerProps` (`callId`/`toolName`/`block`/`openDetails` — details being a session-level facility, not chat-private), and `ToolRowProps` pre-composes it with the session standard kit for registrant components. A registrant is a plain plugin: `ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)` with `inject: ['slots', 'conversation']` as the load-order seam — apply mounts `ConversationService` *after* the chat registration, so the service being present guarantees the slot is declared, by construction. Session-dimension differentiation happens inside the component (`useSessions` reading `parentId` — the decision sits where all the information already is); the bash sample is the third-party-posture exemplar and paints the same ToolRow chrome as Think (`Bash · {description}`, with a scoped badge only in child sessions). Trajectory/waterfall toolview slots share this exact shape (names fixed by the slot-naming discipline `..`, one shared owner type) and land with their own row render sites — RendersCheck rejects a declaration nobody renders, so the type system, not convention, blocks early empty declarations. Registry-era responsibilities all have successor homes: inject caching and row error isolation ride the framework renderer (entry×scope cache, per-entry `SlotErrorBoundary`); subscribe/getVersion ride the slot core's per-key version machinery; the future "store seat" is the ordinary store seat keyed slots already have (interaction-draft durability is its first named consumer); miss fallback is the call-site `fallback` option. diff --git a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md index 47c1f392f5..928c5f445d 100644 --- a/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.zh.md @@ -14,7 +14,7 @@ Status: implemented 工具环作为独立基础设施已消失:工具行是**各视图为自己声明的 keyed 子槽**,client 全域只剩一种注册模型。上述理由是空的——keyed slot 的 *key 空间*本就运行时开放(SlotMap 声明槽、从不声明 key;ask-user composer 的 `key: 'question'` 即先例),开放的 tool 名集合天然适配 `entryKey` 分发。 -落地形态(现状叙述同见[架构注](2026-07-19-gui-web-client-architecture.md)):chat 条目的 `children` 表声明 `'conversation.chat.toolview'`(keyed/session);渲染点逐行以 `entryKey: toolName` 分发、以 `GenericToolCard` 作调用点 `fallback`(默认卡片是域产权;fallback 选项就是普通 renderSlot 文法)。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`——details 是会话级设施,非 chat 私货),`ToolRowProps` 把它与 session 标配 kit 预组合供注册方组件取用。注册方就是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作加载序缝——apply 把 `ConversationService` 挂在 chat 注册*之后*,故服务在场即保证槽已声明,构造使然。会话维差异化在组件内完成(`useSessions` 读 `parentId`——决策放在已有全部信息的地方);bash 样例即第三方姿态的样板。trajectory/waterfall 的 toolview 槽共用这套形状(槽名按槽名纪律 `<域>.<条目>.<孔位>` 定死,共用一张 owner 类型),随各自的行渲染点落地——RendersCheck 拒绝无人渲染的声明,挡住提前空声明的是类型系统而非约定。 +落地形态(现状叙述同见[架构注](2026-07-19-gui-web-client-architecture.md)):chat 条目的 `children` 表声明 `'conversation.chat.toolview'`(keyed/session);渲染点逐行以 `entryKey: toolName` 分发、以 `GenericToolCard` 作调用点 `fallback`(默认卡片是域产权;fallback 选项就是普通 renderSlot 文法)。owner 载荷是统一的 `ToolRowOwnerProps`(`callId`/`toolName`/`block`/`openDetails`——details 是会话级设施,非 chat 私货),`ToolRowProps` 把它与 session 标配 kit 预组合供注册方组件取用。注册方就是普通插件:`ctx.slots.register({ name: 'conversation.chat.toolview', key: '', inject? }, Row)`,以 `inject: ['slots', 'conversation']` 作加载序缝——apply 把 `ConversationService` 挂在 chat 注册*之后*,故服务在场即保证槽已声明,构造使然。会话维差异化在组件内完成(`useSessions` 读 `parentId`——决策放在已有全部信息的地方);bash 样例即第三方姿态的样板,并与 Think 绘制同一套 ToolRow chrome(`Bash · {description}`,scoped badge 仅出现在子会话)。trajectory/waterfall 的 toolview 槽共用这套形状(槽名按槽名纪律 `<域>.<条目>.<孔位>` 定死,共用一张 owner 类型),随各自的行渲染点落地——RendersCheck 拒绝无人渲染的声明,挡住提前空声明的是类型系统而非约定。 registry 时代的职责各有后继居所:inject 缓存与行错误隔离乘框架渲染器(entry×scope 缓存、per-entry `SlotErrorBoundary`);subscribe/getVersion 乘 slot core 的 per-key 版本机;将来的「store 席位」就是 keyed slot 本就拥有的普通 store 席位(交互草稿耐久性是其首个具名消费者);miss 兜底即调用点 `fallback` 选项。 diff --git a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml index 1ff9ecac7d..1f52492649 100644 --- a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-23-web-assistant-markdown.md: ce98a16fa43e2743c18826ee7f2344c38e7c70e7 -2026-07-23-web-assistant-markdown.zh.md: 0d6fd2f9e6b91f76830586ecf4c29774e5d6978a +2026-07-23-web-assistant-markdown.md: 38d193271d88b3a8f32ba1b191e8a6d432176281 +2026-07-23-web-assistant-markdown.zh.md: be3cd041c6012af142fc27934fda125dfc4cf6de diff --git a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md index ce98a16fa4..38d193271d 100644 --- a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md +++ b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md @@ -12,13 +12,17 @@ The Web conversation preserves assistant Markdown source through session events, `@deepseek-ai/dsh-client-ui-primitives` exports `MarkdownText` as the untrusted assistant-text renderer, and `ui-conversation` selects it only for assistant `text` blocks. Finalized history, the streaming tail, and interrupted partials already share `AssistantMarkdown`, so they receive the same renderer without changing events or snapshots. User and steering messages keep `MessageText` and remain literal. -`MarkdownText` uses `react-markdown` with `remark-gfm` to build React elements from an AST. It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without `dangerouslySetInnerHTML`, raw-HTML parsing, or syntax highlighting. The dependency is explicit in `ui-primitives`; because that pure library is seeded by the Web shell, the parser is part of the initial browser bundle. +`MarkdownText` uses `react-markdown` with `remark-gfm` to build React elements from an AST. It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without raw-HTML parsing. Fenced code routes through the shared `CodeBlock`, which highlights registered grammars with the client's shiki singleton (`--shiki-*` tokens) and falls back to plain monospace otherwise. While a turn streams, fences stay on the plain arm so growing fences are not retokenized every chunk. + +Visual spacing, tables, links, blockquotes, inline code, and code-block chrome follow deepsuite `@deepseek/md` (`markdown.css` / `code-block.css`) and the same `--dsw-alias-markdown-*`, `--dsw-font-markdown-*`, `--dsw-alias-border-l*`, and `--dsw-alias-label-*` tokens. Links use `--dsw-alias-state-business-primary` (deepsuite's sheet uses `--dsw-alias-brand-text`, which is blue only under newDesign; design-platform keeps brand-text near-black and is not retuned here). `CodeBlock` ships a language banner and a copy control (`复制` / `复制成功`). Citation pills, KaTeX, heading anchors, the thinking-small markdown variant, and custom □/☑ task markers are out of scope until matching product DOM exists; GFM task lists keep native checkboxes. + +The dependency is explicit in `ui-primitives`; because that pure library is seeded by the Web shell, the parser and highlighter are part of the initial browser bundle. ## Untrusted output policy -Assistant-authored destinations are restricted to absolute HTTP, HTTPS, and mailto URLs. HTTP(S) links open in a new tab with `rel="noopener noreferrer"`; relative destinations and other protocols render as non-navigable text. Markdown images render only their alt text, so model output cannot initiate a remote image request. Raw HTML remains inert source text because no HTML parser enters the pipeline. +Assistant-authored destinations are restricted to absolute HTTP, HTTPS, and mailto URLs. HTTP(S) links open in a new tab with `rel="noopener noreferrer"`; relative destinations and other protocols render as non-navigable text. Markdown images render only their alt text, so model output cannot initiate a remote image request. Raw HTML remains inert source text because no HTML parser enters the pipeline. Shiki output is a static span tree generated from the fence text (no scripts or user HTML). -The renderer uses existing `--dsw-*` typography and color tokens. Fenced code and GFM tables own horizontal overflow so long content cannot widen the conversation column. +Fenced code and GFM tables own horizontal overflow so long content cannot widen the conversation column. ## Alternatives considered @@ -30,6 +34,8 @@ The renderer uses existing `--dsw-*` typography and color tokens. Fenced code an **Enable raw HTML or remote images with sanitization.** Neither capability has a current product need, while both enlarge the executable or network privacy boundary. They remain disabled rather than adding sanitizer and image-policy dependencies. +**Port deepsuite Prism `highlight.css` and the mdast pipeline.** Appearance parity is owned by CSS Modules and shared `--dsw-*` tokens; highlighting stays on the existing shiki allowlist so the client does not take a second highlighter or Prism class contract. + ## Consequences -Assistant replies render semantic Markdown consistently during streaming and replay, while tool cards, reasoning rows, interactions, user bubbles, and the host protocol remain unchanged. Streaming reparses the current text after each accumulated update; incomplete Markdown can temporarily change structure, but the isolated tail bounds React invalidation and the final event does not switch renderers. The initial Web shell grows by the Markdown parser and GFM runtime, and future extensions such as syntax highlighting or remote media require a separate bundle and security decision. +Assistant replies render semantic Markdown consistently during streaming and replay, while tool cards, reasoning rows, interactions, user bubbles, and the host protocol remain unchanged. Streaming reparses the current text after each accumulated update; incomplete Markdown can temporarily change structure, but the isolated tail bounds React invalidation and the final event does not switch renderers. Code fences share one chrome and copy path with tool and details surfaces. The initial Web shell includes the Markdown parser, GFM runtime, and shiki allowlist; cite/math/anchor/thinking-small surfaces remain deferred. diff --git a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md index 0d6fd2f9e6..be3cd041c6 100644 --- a/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md +++ b/.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md @@ -12,13 +12,17 @@ Web 对话通过会话事件、历史回放与流式累积保留 assistant Markd `@deepseek-ai/dsh-client-ui-primitives` 导出 `MarkdownText`,用作不受信任的 assistant 文本渲染器;`ui-conversation` 仅为 assistant `text` 块选择该渲染器。已完成的历史消息、流式输出尾部与被中断的部分输出已经共用 `AssistantMarkdown`,因此无需更改事件或快照,它们便会采用同一渲染器。用户消息与 steering 消息继续使用 `MessageText`,并保持按字面渲染。 -`MarkdownText` 使用 `react-markdown` 与 `remark-gfm`,从 AST 构建 React 元素。它支持 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,但不使用 `dangerouslySetInnerHTML`,不解析原始 HTML,也不进行语法高亮。`ui-primitives` 显式声明该依赖;由于这一纯库由 Web shell 预置,解析器会成为初始浏览器 bundle 的一部分。 +`MarkdownText` 使用 `react-markdown` 与 `remark-gfm`,从 AST 构建 React 元素。它覆盖 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,且不解析原始 HTML。围栏代码经共享的 `CodeBlock` 路由;该组件用客户端的 shiki 单例(`--shiki-*` token)高亮已注册语法,否则回退为纯等宽文本。轮次流式输出期间,围栏停留在纯文本分支,以免每收到一个分片就对增长中的围栏重新分词。 + +视觉间距、表格、链接、引用块、行内代码与代码块外框遵循 deepsuite `@deepseek/md`(`markdown.css` / `code-block.css`),并使用同一套 `--dsw-alias-markdown-*`、`--dsw-font-markdown-*`、`--dsw-alias-border-l*` 与 `--dsw-alias-label-*` token。链接使用 `--dsw-alias-state-business-primary`(deepsuite 的样式表使用 `--dsw-alias-brand-text`,仅在 newDesign 下为蓝色;design-platform 将 brand-text 保持为近黑色,此处不做重新调色)。`CodeBlock` 提供语言横幅与复制控件(`复制` / `复制成功`)。引用胶囊、KaTeX、标题锚点、thinking-small markdown 变体,以及自定义 □/☑ 任务标记均不在范围内,直至存在匹配的产品 DOM;GFM 任务列表继续使用原生复选框。 + +该依赖在 `ui-primitives` 中显式声明;由于这一纯库由 Web shell 预置,解析器与高亮器会成为初始浏览器 bundle 的一部分。 ## 不受信任输出策略 -assistant 生成的目标地址仅限绝对 HTTP、HTTPS 与 mailto URL。HTTP(S) 链接会在新标签页中打开,并带有 `rel="noopener noreferrer"`;相对目标地址与其他协议会渲染为不可导航的文本。Markdown 图片仅渲染替代文本,因此模型输出无法发起远程图片请求。由于管线中未引入 HTML 解析器,原始 HTML 仍是不会生效的源文本。 +assistant 生成的目标地址仅限绝对 HTTP、HTTPS 与 mailto URL。HTTP(S) 链接会在新标签页中打开,并带有 `rel="noopener noreferrer"`;相对目标地址与其他协议会渲染为不可导航的文本。Markdown 图片仅渲染替代文本,因此模型输出无法发起远程图片请求。由于管线中未引入 HTML 解析器,原始 HTML 仍是不会生效的源文本。Shiki 输出是由围栏文本生成的静态 span 树(不含脚本或用户 HTML)。 -渲染器使用现有的 `--dsw-*` 排版与颜色 token。围栏代码块与 GFM 表格各自处理横向溢出,因此较长内容无法撑宽对话栏。 +围栏代码与 GFM 表格各自处理横向溢出,因此较长内容无法撑宽对话栏。 ## 考虑过的替代方案 @@ -30,6 +34,8 @@ assistant 生成的目标地址仅限绝对 HTTP、HTTPS 与 mailto URL。HTTP(S **通过净化启用原始 HTML 或远程图片。**当前产品并不需要这两项功能,但二者都会扩大可执行行为或网络隐私边界。因此它们保持禁用,无需增加净化器与图片策略依赖。 +**移植 deepsuite 的 Prism `highlight.css` 与 mdast 管线。**外观一致性由 CSS Modules 与共享的 `--dsw-*` token 负责;高亮仍走现有的 shiki 允许列表,使客户端不必引入第二套高亮器或 Prism class 契约。 + ## 后果 -assistant 回复在流式输出与回放期间都会一致地渲染为语义化 Markdown,而工具卡片、推理行、交互、用户气泡和宿主协议保持不变。每次累积更新后,流式输出都会重新解析当前文本;未完成的 Markdown 可能暂时改变结构,但独立的尾部会限定 React 失效范围,最终事件也不会切换渲染器。初始 Web shell 的体积会因加入 Markdown 解析器与 GFM 运行时而增大;语法高亮或远程媒体等后续扩展需要另行作出 bundle 与安全决策。 +assistant 回复在流式输出与回放期间都会一致地渲染为语义化 Markdown,而工具卡片、推理行、交互、用户气泡和宿主协议保持不变。每次累积更新后,流式输出都会重新解析当前文本;未完成的 Markdown 可能暂时改变结构,但独立的尾部会限定 React 失效范围,最终事件也不会切换渲染器。代码围栏与工具及详情表层共用同一外框与复制路径。初始 Web shell 包含 Markdown 解析器、GFM 运行时与 shiki 允许列表;cite/math/anchor/thinking-small 表层仍暂缓。 diff --git a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml new file mode 100644 index 0000000000..52fa9a6cb3 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write +2026-07-27-user-message-icon-actions.md: 45856e1ee093b1bfeaebfe67339aec7b6d2dd694 +2026-07-27-user-message-icon-actions.zh.md: ea87b8036ee91e8998f1e45dbea17f9ee76c244c diff --git a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md new file mode 100644 index 0000000000..45856e1ee0 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md @@ -0,0 +1,27 @@ +# Agent Note: User-message IconActions under the bubble + +Status: implemented + +English | [中文](2026-07-27-user-message-icon-actions.zh.md) + +## Problem + +The chat user bubble had no under-bubble action chrome. The Harness design (figma `User_Bubble/message_container`) shows three IconActions — copy, branch in new chat, and edit — right-aligned under the bubble, matching the product action-bar pattern used elsewhere. + +## Decision + +`MessageItem` owns the actions for `kind: 'user'` only. Layout is a column (`align-items: flex-end`, 6px gap): bubble, then a 28px action row with 10px gaps and 28px circular icon buttons (`IconCopyOutline16`, `IconBranchOutline16`, `IconEditOutline16`). Tooltips carry Chinese labels. The row stays `opacity: 0` until the user row is hovered or focus-within, per the [web styling](../../../../docs/web-styling.md) message action-bar rule. + +Copy writes the bubble's joined text blocks to the clipboard (`navigator.clipboard.writeText`, with an `execCommand` fallback). Branch and edit are present chrome with no handlers yet — they reserve the design seats without inventing session-fork or edit-resubmit behavior. + +Steering bubbles keep the badge-only form and do not show these actions. + +## Alternatives considered + +**Wire branch/edit to real session fork and draft-edit now.** Rejected for this change: those product flows are not specified; shipping inert buttons matches the requested scope and avoids half-built mutation paths. + +**Always-visible actions (no hover fade).** Rejected against the standing action-bar rule; the figma node shows the resting chrome, not the idle-hidden state the style guide requires. + +## Consequences + +User messages expose copy immediately; branch/edit remain clickable stubs until a later decision owns their behavior. Tests pin the three buttons, copy payload, and steering exclusion. diff --git a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md new file mode 100644 index 0000000000..ea87b8036e --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md @@ -0,0 +1,27 @@ +# Agent Note: 用户消息气泡下方的 IconActions + +Status: implemented + +[English](2026-07-27-user-message-icon-actions.md) | 中文 + +## 问题 + +聊天用户气泡下方没有操作栏。Harness 设计稿(figma `User_Bubble/message_container`)在气泡下方右对齐展示三个 IconActions——复制、在新对话中分支、编辑——与产品其他位置使用的操作栏模式一致。 + +## 决策 + +仅当 `kind: 'user'` 时,`MessageItem` 拥有这些操作。布局为纵向列(`align-items: flex-end`,间距 6px):先是气泡,再是高度 28px 的操作行;行内间距 10px,圆形图标按钮尺寸为 28px(`IconCopyOutline16`、`IconBranchOutline16`、`IconEditOutline16`)。Tooltip 承载中文标签。按 [Web 样式](../../../../docs/web-styling.md) 的消息操作栏规则,该行保持 `opacity: 0`,直到用户行被悬停或处于 focus-within 状态。 + +复制将气泡内拼接后的文本块写入剪贴板(`navigator.clipboard.writeText`,并以 `execCommand` 作为回退)。分支与编辑目前仅有外观、尚无处理函数——它们预留设计席位,但不发明会话 fork 或编辑重提交流程。 + +steering(中途引导)气泡保持仅徽章形态,不展示这些操作。 + +## 考虑过的替代方案 + +**现在就把分支/编辑接到真实的会话 fork 与草稿编辑。**本次变更不予采纳:这些产品流程尚未定稿;交付无行为按钮符合请求范围,也避免半成品的变更路径。 + +**操作始终可见(无悬停淡入)。**与现行操作栏规则冲突,不予采纳;figma 节点展示的是静止态外观,而非样式指南要求的空闲隐藏状态。 + +## 后果 + +用户消息立即可用复制;分支/编辑仍为可点击的占位,直至后续决策明确其行为。测试钉死三个按钮、复制载荷,以及对 steering 的排除。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index c8d5be336d..39b595b461 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -179,7 +179,8 @@ export function apply(ctx: Context): void { // 'conversation.chat.toolview' declaration) is on the ledger. ctx.plugin(ConversationService, { input: inputHub }) - // The bash sample rides that exact seam, in third-party posture. + // The bash sample rides that exact seam, in third-party posture + // (ToolRow-matching Bash · {description} chrome; scoped badge in child sessions). ctx.plugin(bashToolviewSample) // The read-only queue dock entry (T9 file territory) rides the same diff --git a/packages/client/ui-conversation/src/client/chat/ChatView.module.css b/packages/client/ui-conversation/src/client/chat/ChatView.module.css index d548f2d7be..6d75f9a519 100644 --- a/packages/client/ui-conversation/src/client/chat/ChatView.module.css +++ b/packages/client/ui-conversation/src/client/chat/ChatView.module.css @@ -37,17 +37,11 @@ border-radius: 6px; } -/* Selection linkage: the selected call row wears the blue outline. - button-info-fill flips 500→400 with the theme, hitting the darker-blue - dark-mode spec exactly (business-primary stays 500 on both). */ -.callRow[data-selected] { - outline: 1.5px solid var(--dsw-alias-button-info-fill); - outline-offset: 1px; -} +/* Selection still sets data-selected for details linkage; no outline — + tool rows match Think chrome (no selected ring). */ /* run_code sub-dispatch rows: indented under the parent row, left-edged so - the code turn reads as one unit; each nested row is itself a .callRow - (same components, same selection outline as top-level rows). */ + the code turn reads as one unit; each nested row is itself a .callRow. */ .subCalls { display: flex; flex-direction: column; diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css index 047878f1d0..0d2331a199 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css @@ -1,10 +1,11 @@ -/* User bubble: right-aligned, figma r22 fill = the bubble specific token - (#EDF3FE light / dark pair rides the token sheet). */ +/* User bubble: right-aligned column (bubble + IconActions). Figma + User_Bubble/message_container 659:38813 — r22 fill, actions gap 6 below. */ -/* Block spacing is the flow column's gap alone — no extra padding here. */ .userRow { display: flex; - justify-content: flex-end; + flex-direction: column; + align-items: flex-end; + gap: 6px; } .bubble { @@ -19,6 +20,40 @@ color: var(--dsw-alias-label-primary); } +.actions { + display: flex; + align-items: center; + gap: 10px; + height: 28px; + /* Hidden until the row is hovered/focused (web-styling message action bar). */ + opacity: 0; + transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out); +} + +.userRow:hover .actions, +.userRow:focus-within .actions { + opacity: 1; +} + +.action { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 6px; + border: none; + border-radius: 28px; + background: transparent; + color: var(--dsw-alias-label-tertiary); + cursor: pointer; +} + +.action:hover { + background: var(--dsw-alias-interactive-bg-hover); + color: var(--dsw-alias-label-secondary); +} + .badge { display: inline-block; margin-bottom: 4px; diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx index e79304fc19..67abe94ef6 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx @@ -1,14 +1,18 @@ -// MessageItem: the four simple node kinds — user bubble (right-aligned), -// steering (badged bubble), context injection and unknown-surface JSON rows. -// Props are frozen node slices off the snapshot cache; memo holds across -// streaming because unchanged nodes keep their references. +// MessageItem: the four simple node kinds — user bubble (right-aligned, with +// copy / branch / edit IconActions), steering (badged bubble), context +// injection and unknown-surface JSON rows. Props are frozen node slices off +// the snapshot cache; memo holds across streaming because unchanged nodes +// keep their references. -import { memo } from 'react' +import { memo, useCallback } from 'react' import type { ReactNode } from 'react' import type { ContextMessageNode, SteeringMessageNode, UnknownSurfaceNode, UserMessageNode, } from '@deepseek-ai/dsh-client-runtime/client' -import { JsonBlock, MessageText } from '@deepseek-ai/dsh-client-ui-primitives' +import { + IconBranchOutline16, IconCopyOutline16, IconEditOutline16, + JsonBlock, MessageText, Tooltip, +} from '@deepseek-ai/dsh-client-ui-primitives' import css from './MessageItem.module.css' export interface MessageItemProps { @@ -26,6 +30,30 @@ function contentText(content: readonly unknown[]): { text: string; rest: unknown return { text: texts.join(''), rest } } +async function writeClipboard(text: string): Promise { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text) + return + } + const exec = typeof document.execCommand === 'function' + ? document.execCommand.bind(document) + : undefined + if (exec === undefined) return + const el = document.createElement('textarea') + el.value = text + el.setAttribute('readonly', '') + el.style.position = 'fixed' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + try { + exec('copy') + } catch { + // Clipboard unavailable; the button stays idle. + } + el.remove() +} + /** * Display projection of reference forms in a user bubble (free geometry — no * textarea alignment constraint here); everything else stays plain text. The @@ -58,15 +86,52 @@ function projectUserText(text: string): ReactNode { return <>{parts} } +/** User-bubble IconActions (figma 659:38820): copy is live; branch/edit are chrome stubs. */ +function UserActions({ text }: { text: string }) { + const onCopy = useCallback(() => { + void writeClipboard(text) + }, [text]) + return ( +
+ + + + + + + + + +
+ ) +} + export const MessageItem = memo(function MessageItem({ node }: MessageItemProps) { switch (node.kind) { - case 'user': + case 'user': { + const { text, rest } = contentText(node.content) + return ( +
+
+ {projectUserText(text)} + {rest.map((block, i) => )} +
+ +
+ ) + } case 'steering': { const { text, rest } = contentText(node.content) return (
- {node.kind === 'steering' && 插话} + 插话 {projectUserText(text)} {rest.map((block, i) => )}
diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index a21a744008..b300c59393 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -323,7 +323,7 @@ .stopping, .stopping:hover { background: var(--dsw-alias-button-primary-dimmed); - color: var(--dsw-alias-brand-text); + color: var(--dsw-alias-label-primary); } .retry { diff --git a/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css b/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css index 83c2329fc5..9b7116462f 100644 --- a/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css +++ b/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css @@ -1,29 +1,32 @@ -/* Sample bash rows: deliberately distinct from ToolRow so the differential - registry hit is visible at a glance. */ +/* Bash toolview: same geometry/tokens as ToolRow (figma Bash · description). */ -.row { +.root { display: flex; align-items: center; - gap: 8px; height: 24px; min-width: 0; cursor: pointer; border-radius: 6px; - font-family: var(--ds-font-family-code); - font-size: 13px; } -.row:hover { +.root:hover { background: var(--dsw-alias-interactive-bg-hover); } -.prompt { +.leading { flex: none; - color: var(--dsw-alias-state-success-primary); + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-right: 6px; + color: var(--dsw-alias-label-tertiary); } .scopeBadge { flex: none; + margin-right: 8px; padding: 0 6px; border-radius: 6px; font-size: 11px; @@ -32,17 +35,29 @@ background: var(--dsw-alias-state-business-primary); } -.command { +.title { + flex: none; + font-size: 14px; + line-height: 24px; + color: var(--dsw-alias-label-primary-dimmed); +} + +.sep { + flex: none; + width: 2px; + height: 2px; + border-radius: 1px; + margin: 0 8px; + background: var(--dsw-alias-label-caption); +} + +.summary { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: var(--dsw-alias-label-secondary); -} - -.err { - flex: none; - color: var(--dsw-alias-state-error-primary); - font-size: 11px; + font-size: 14px; + line-height: 24px; + color: var(--dsw-alias-label-tertiary); } diff --git a/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx b/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx index 9968c3b46e..2503a6e71b 100644 --- a/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx @@ -1,35 +1,42 @@ -// Bash toolview sample, written in third-party posture: everything below uses -// only the public slot surface (ctx.slots.register into the keyed -// 'conversation.chat.toolview' hole + ToolRowProps) — the acceptance proof -// that a plain plugin can take over a tool row with zero dedicated machinery. -// Session-dimension differentiation happens INSIDE the component (the -// canonical sub-agent scenario): rows in child sessions render the scoped -// variant, derived from the standard useSessions kit — no registry predicates. +// Bash toolview registrant: third-party posture over the keyed toolview hole +// (ctx.slots.register + ToolRowProps only — never imports the chat domain). +// Product chrome matches ToolRow / Think (figma: Bash · {description}). +// Child sessions keep a scoped badge so session-dimension differentiation stays +// observable inside the component (no parallel registry). import type { Context } from 'cordis' +import { IconApiOutline14, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' import type { ToolRowProps } from '../contract/slots.ts' -import { toolRowModel } from '../contract/tool-call-model.ts' +import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts' import css from './bash-sample.module.css' -/** Bash row: command-first monospace summary replacing the generic card. - * Sub-session rows (parentId present) swap the prompt for a scoped badge — - * the differential stays observable per session from one registration. */ +function leadingFor(state: ToolRowState) { + switch (state) { + case 'running': return + case 'error': return + case 'stopped': return + default: return + } +} + +/** Bash row: icon + Bash · {description}, matching the shared ToolRow chrome. */ export function BashRow({ toolName, block, openDetails, sessionId, useSessions }: ToolRowProps) { const model = toolRowModel(toolName, block) const isChild = useSessions(list => list.byId[sessionId]?.parentId !== undefined) - if (isChild) { - return ( -
- scoped - {model.summary} -
- ) - } return ( -
- $ - {model.summary} - {model.state === 'error' && failed} +
+ {leadingFor(model.state)} + {isChild && scoped} + {model.title} + + {model.summary}
) } diff --git a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx index be50356185..b96821f761 100644 --- a/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx @@ -1,11 +1,12 @@ // @vitest-environment jsdom // Remaining chat branch tails: MessageItem context/unknown/steering arms, -// StatsLine no-cache join, PendingCard reason strip, AssistantMarkdown -// single-line reasoning. (Tool-row dispatch tails live with the keyed-slot -// machinery specs since the tool ring dissolved into renderSlot.) +// user IconActions, StatsLine no-cache join, PendingCard reason strip, +// AssistantMarkdown single-line reasoning. (Tool-row dispatch tails live +// with the keyed-slot machinery specs since the tool ring dissolved into +// renderSlot.) import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanup, render } from '@testing-library/react' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { RpcId } from '@deepseek-ai/dsh-client-connection/client' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' import { PendingWait } from '@deepseek-ai/dsh-client-runtime/client' @@ -18,7 +19,75 @@ import { StatsLine, type StatsLineProps } from '../src/client/chat/StatsLine.tsx afterEach(cleanup) describe('MessageItem arms', () => { - it('steering bubbles carry the interjection badge and non-text rest blocks', () => { + it('user bubbles expose copy / branch / edit actions; copy writes the text', () => { + const writeText = vi.fn().mockResolvedValue(undefined) + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }) + render( + , + ) + expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() + expect(screen.getByRole('button', { name: '在新对话中分支' })).toBeTruthy() + expect(screen.getByRole('button', { name: '编辑' })).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(writeText).toHaveBeenCalledWith('hello bubble') + }) + + it('user copy falls back to execCommand when clipboard.writeText is unavailable', () => { + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: undefined, + }) + const exec = vi.fn().mockReturnValue(true) + Object.defineProperty(document, 'execCommand', { + configurable: true, + value: exec, + }) + render( + , + ) + fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(exec).toHaveBeenCalledWith('copy') + }) + + it('user copy stays quiet when execCommand throws or is absent', () => { + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: undefined, + }) + Object.defineProperty(document, 'execCommand', { + configurable: true, + value: () => { + throw new Error('denied') + }, + }) + render( + , + ) + fireEvent.click(screen.getByRole('button', { name: '复制' })) + + Object.defineProperty(document, 'execCommand', { + configurable: true, + value: undefined, + }) + fireEvent.click(screen.getByRole('button', { name: '复制' })) + }) + + it('steering bubbles carry the interjection badge and non-text rest blocks, without user actions', () => { const view = render( { expect(view.getByText('插话')).toBeTruthy() expect(view.getByText('steer!')).toBeTruthy() expect(view.getByText(/附加内容块/)).toBeTruthy() + expect(view.queryByRole('button', { name: '复制' })).toBeNull() }) it('context and unknown nodes render their JSON rows', () => { diff --git a/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx b/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx index 125e421772..9bb915ca41 100644 --- a/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx @@ -156,12 +156,13 @@ describe('run_code sub-calls through the real chat machinery', () => { expect(view.getByText('List the notes directory')).toBeTruthy() // Nested rows are ALWAYS visible (no parent expand needed): the bash - // sub-call landed in the bash sample plugin's keyed registration — the - // exact component a native top-level bash row uses — and the unregistered + // sub-call landed in the bash sample plugin's keyed registration — Bash · + // description chrome, same as a top-level bash row — and the unregistered // sub-tool fell back to GenericToolCard at the same render site. const nest = view.container.querySelector('[data-subcalls]') expect(nest).not.toBeNull() expect(nest!.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(view.getByText('Bash')).toBeTruthy() expect(view.getByText('List notes')).toBeTruthy() expect(view.getByText('Tool call')).toBeTruthy() }) diff --git a/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx b/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx index 2ccaa9bbf2..f943bab600 100644 --- a/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx @@ -169,17 +169,19 @@ describe('bash sample row', () => { expect(view.container.querySelector('[data-sample="bash-scoped"]')).not.toBeNull() }) - it('summarizes the command and hands clicks to openDetails on both arms', () => { + it('summarizes as Bash · description and hands clicks to openDetails on both arms', () => { const openGlobal = vi.fn() const global = render() // Two renders share document.body: query inside each container. const globalRow = global.container.querySelector('[data-sample="bash-global"]')! + expect(globalRow.textContent).toContain('Bash') expect(globalRow.textContent).toContain('Build') fireEvent.click(globalRow) expect(openGlobal).toHaveBeenCalledTimes(1) const openScoped = vi.fn() const scoped = render() const scopedRow = scoped.container.querySelector('[data-sample="bash-scoped"]')! + expect(scopedRow.textContent).toContain('Bash') expect(scopedRow.textContent).toContain('Build') fireEvent.click(scopedRow) expect(openScoped).toHaveBeenCalledTimes(1) diff --git a/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx b/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx index b1122a4098..d1d8b61b30 100644 --- a/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx @@ -156,6 +156,7 @@ describe('keyed toolview hole through the real machinery', () => { // bash: the sample plugin's keyed registration took the row (root // session → global arm, decided inside the component off useSessions). expect(view.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(view.getByText('Bash')).toBeTruthy() expect(view.getByText('Build')).toBeTruthy() // mystery: no registration under that key → render-site fallback. expect(view.getByText('Tool call')).toBeTruthy() diff --git a/packages/client/ui-conversation/tests/chat-view.spec.tsx b/packages/client/ui-conversation/tests/chat-view.spec.tsx index 78e0affa4e..7fe12c7682 100644 --- a/packages/client/ui-conversation/tests/chat-view.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-view.spec.tsx @@ -264,7 +264,7 @@ describe('ChatView', () => { expect(view.getByText(/"command": "cmd-a"/)).toBeTruthy() }) - it('clicking a tool row opens details with callId and toolName; selection paints the outline', () => { + it('clicking a tool row opens details with callId and toolName; selection marks data-selected', () => { const h = makeHarness({ nodes: [toolResult(3, 'a')] }) const view = render() fireEvent.click(view.getByText('run a')) diff --git a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx index 11664d3f00..084f73b4b7 100644 --- a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx @@ -1,13 +1,13 @@ // @vitest-environment jsdom // Branch tails the acceptance specs do not reach: ToolRow stopped-state dot, -// PendingCard question arm, bash sample error pill, the node-half empty +// PendingCard question arm, bash sample state dots, the node-half empty // apply, and AssistantMarkdown reasoning/unknown block arms. import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render } from '@testing-library/react' import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' -import type { SessionId, SessionListState, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' +import type { RunningToolCall, SessionId, SessionListState, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import { PendingWait } from '@deepseek-ai/dsh-client-runtime/client' import { RpcId } from '@deepseek-ai/dsh-client-connection/client' import type { ToolRowOwnerProps, ToolRowProps } from '@deepseek-ai/dsh-client-ui-conversation/client' @@ -76,14 +76,7 @@ describe('tails', () => { expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull() }) - it('BashRow shows the failed pill on error results (root session arm)', () => { - const errorResult: ToolResultNode = { - kind: 'tool-result', seq: 1, time: 1_000, callId: 'c1', - call: { name: 'bash', argsRaw: '{"command":"boom"}' }, - callTime: 500, - content: [], isError: true, callView: null, resultView: null, - } - // Root session (no parentId): the global arm renders, error pill visible. + it('BashRow shows StateDot chrome for running/error/stopped (root session arm)', () => { const sid = 'root-1' as SessionId const list = createSnapshotStore({ ids: [sid], @@ -91,12 +84,38 @@ describe('tails', () => { current: undefined, phase: 'ready', } as SessionListState) - const props = { - callId: 'c1', toolName: 'bash', block: errorResult, openDetails: vi.fn(), + const props = (block: RunningToolCall | ToolResultNode) => ({ + callId: 'c1', toolName: 'bash', block, openDetails: vi.fn(), sessionId: sid, useSessions: bindSnapshotSelector(list), - } as unknown as ToolRowProps - const view = render() - expect(view.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() - expect(view.getByText('failed')).toBeTruthy() + } as unknown as ToolRowProps) + + const running: RunningToolCall = { + callId: 'c1', name: 'bash', argsRaw: '{"command":"ls","description":"List"}', + turn: 1, step: 1, time: 1_000, callView: null, + } + const errorResult: ToolResultNode = { + kind: 'tool-result', seq: 1, time: 1_000, callId: 'c1', + call: { name: 'bash', argsRaw: '{"command":"boom"}' }, + callTime: 500, + content: [], isError: true, callView: null, resultView: null, + } + const stoppedResult: ToolResultNode = { + ...errorResult, + error: { name: 'E', code: 'interrupted' }, + } + + const runningView = render() + expect(runningView.container.querySelector('[data-state="running"]')).not.toBeNull() + expect(runningView.getByText('Bash')).toBeTruthy() + expect(runningView.getByText('List')).toBeTruthy() + runningView.unmount() + + const errorView = render() + expect(errorView.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() + expect(errorView.container.querySelector('[data-state="error"]')).not.toBeNull() + errorView.unmount() + + const stoppedView = render() + expect(stoppedView.container.querySelector('[data-state="stopped"]')).not.toBeNull() }) }) diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 6b4e776cfe..6162494def 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -README.md: 4e2a22e77dc1611728477ea0a9d8c50dfc9f7f5d -README.zh.md: 36253971281fd346f9b0ec4648c4b8824ed918a7 +README.md: 58e450451ab64f69762817dfb277b8a888e2177f +README.zh.md: 6824f3efe4981adf9549941afa7e2f5db2ac005d diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 4e2a22e77d..58e450451a 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -6,7 +6,7 @@ Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/ ## Markdown rendering -`MarkdownText` renders GFM from untrusted assistant output through React elements. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders image alt text without loading remote resources; `MessageText` remains the literal-text primitive for user-authored content. +`MarkdownText` renders GFM from untrusted assistant output through React elements. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders image alt text without loading remote resources; `MessageText` remains the literal-text primitive for user-authored content. Element spacing, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). ## Model Experience diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 3625397128..6824f3efe4 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -6,7 +6,7 @@ ## Markdown 渲染 -`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并只渲染图片 alt 文本而不加载远程资源;`MessageText` 仍是用户创作内容使用的字面文本原语。 +`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并只渲染图片 alt 文本而不加载远程资源;`MessageText` 仍是用户创作内容使用的字面文本原语。元素间距、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 ## 模型体验 diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css index f9b5f67136..7222c3df44 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.module.css +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.module.css @@ -1,13 +1,79 @@ -/* One code-block geometry for highlighted and plain arms: the shiki
-   and the fallback 
 draw identically except for token colors. */
+/* Visual baseline: deepsuite `@deepseek/md` code-block.css. Highlight colors
+   stay on the existing shiki `--shiki-*` sheet (not Prism highlight.css). */
+
+.block {
+  --dsl-code-block-banner-background-color: var(--dsw-alias-markdown-code-block-banner);
+  --dsl-code-block-border-radius: 12px;
+  --dsl-code-block-banner-font: var(--dsw-font-xs-13);
+  --dsl-code-block-content-font: var(--dsw-font-markdown-code-block);
+
+  position: relative;
+  margin: 16px 0;
+  color: var(--dsw-alias-label-primary);
+  background: var(--dsw-alias-markdown-code-block);
+  border-radius: var(--dsl-code-block-border-radius);
+}
+
+.block:not(:last-child) {
+  margin-bottom: 11px;
+}
+
+.bannerWrap {
+  position: sticky;
+  top: 0;
+  z-index: 6;
+  background-color: var(--dsw-alias-bg-base);
+  border-top-left-radius: var(--dsl-code-block-border-radius);
+  border-top-right-radius: var(--dsl-code-block-border-radius);
+}
+
+.banner {
+  background: var(--dsl-code-block-banner-background-color);
+  padding: 9px 14px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 12px;
+  font: var(--dsl-code-block-banner-font);
+  border-top-left-radius: var(--dsl-code-block-border-radius);
+  border-top-right-radius: var(--dsl-code-block-border-radius);
+}
+
+.infostring {
+  color: var(--dsw-alias-label-primary);
+  font-family: var(--ds-font-family-code);
+  font-size: 12px;
+  line-height: 18px;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.action {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.copyButton {
+  background-color: rgb(255 255 255 / 0);
+  border: none;
+  padding: 0;
+  margin: 0;
+  color: inherit;
+  cursor: pointer;
+  font: inherit;
+}
 
 .block :where(pre) {
-  margin: 0;
-  padding: 8px 10px;
-  border-radius: 8px;
+  font: var(--dsl-code-block-content-font);
+  padding: 16px;
+  margin: 0 !important;
   overflow-x: auto;
+  white-space: pre-wrap;
+  word-break: break-all;
   background: var(--dsw-alias-markdown-code-block);
-  font: var(--dsw-font-markdown-code-block);
 }
 
 /* Shiki inlines its theme background var; route it to the repo token. */
@@ -23,5 +89,4 @@
 
 .plain {
   color: var(--dsw-alias-label-primary);
-  white-space: pre;
 }
diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx
index 1a6349f1e8..33bcf7b80c 100644
--- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx
+++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx
@@ -1,12 +1,10 @@
 // CodeBlock: one code surface for every consumer — markdown fences, the
 // run_code program body, and the details panel's raw args/output — with
 // shiki highlighting for the registered grammars and an identical-geometry
-// plain fallback for everything else. Shiki emits a single 
-// tree of nested spans whose colors are --shiki-* custom properties
-// (token sheets own the values); it produces no scripts or event handlers,
-// so injecting its output is safe by construction.
+// plain fallback for everything else. Chrome (language banner + copy) matches
+// deepsuite `@deepseek/md` code blocks; token colors stay on `--shiki-*`.
 
-import { useMemo } from 'react'
+import { useCallback, useMemo, useRef, useState } from 'react'
 import clsx from 'clsx'
 import { highlightToHtml } from './highlight.ts'
 import css from './CodeBlock.module.css'
@@ -20,18 +18,72 @@ export interface CodeBlockProps {
   className?: string | undefined
 }
 
+async function writeClipboard(text: string): Promise {
+  if (navigator.clipboard?.writeText) {
+    await navigator.clipboard.writeText(text)
+    return
+  }
+  // jsdom and older hosts: best-effort execCommand path when present.
+  const exec = typeof document.execCommand === 'function'
+    ? document.execCommand.bind(document)
+    : undefined
+  if (exec === undefined) return
+  const el = document.createElement('textarea')
+  el.value = text
+  el.setAttribute('readonly', '')
+  el.style.position = 'fixed'
+  el.style.left = '-9999px'
+  document.body.appendChild(el)
+  el.select()
+  try {
+    exec('copy')
+  } catch {
+    // Clipboard unavailable (sandboxed iframe / denied permission); UI still
+    // flips to the ok label so the gesture is acknowledged.
+  }
+  el.remove()
+}
+
 export function CodeBlock({ code, lang, className }: CodeBlockProps) {
   const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code
   const html = useMemo(() => highlightToHtml(trimmed, lang), [trimmed, lang])
-  if (html === undefined) {
-    return (
-      
+ const rootRef = useRef(null) + const [copied, setCopied] = useState(false) + + const onCopy = useCallback(() => { + if (copied) return + /* v8 ignore next -- both arms always mount a
; trimmed is the
+       typed fallback if the DOM shape ever diverges. */
+    const text = rootRef.current?.querySelector('pre')?.textContent ?? trimmed
+    void writeClipboard(text)
+    setCopied(true)
+    window.setTimeout(() => setCopied(false), 1000)
+  }, [copied, trimmed])
+
+  const body = html === undefined
+    ? (
         
{trimmed}
+ ) + : ( + // eslint-disable-next-line react/no-danger -- shiki's output is a static + // span tree it generated from `code` (no user HTML passes through), the + // sanctioned innerHTML consumption path per shiki's own docs. +
+ ) + + return ( +
+
+
+
{lang ?? ''}
+
+ +
+
- ) - } - // eslint-disable-next-line react/no-danger -- shiki's output is a static - // span tree it generated from `code` (no user HTML passes through), the - // sanctioned innerHTML consumption path per shiki's own docs. - return
+ {body} +
+ ) } diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css index 36b1dc2b55..a189528bc9 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css +++ b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css @@ -1,95 +1,168 @@ +/* Visual baseline: deepsuite `@deepseek/md` markdown.css, adapted to CSS + Modules. Cite pills, KaTeX, header anchors, and thinking-small variants are + intentionally absent (no matching DOM). Token names match that sheet. */ + .markdown { - display: flex; min-width: 0; - flex-direction: column; - gap: 12px; overflow-wrap: anywhere; font: var(--dsw-font-markdown-base); + color: var(--dsw-alias-label-primary); } -.markdown :where(h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre, hr) { - margin: 0; +.markdown strong { + font-weight: 600; } .markdown h1 { font: var(--dsw-font-markdown-h1); + margin: 32px 0 16px; } .markdown h2 { font: var(--dsw-font-markdown-h2); + margin: 32px 0 16px; } .markdown h3 { font: var(--dsw-font-markdown-h3); + margin: 32px 0 16px; } -.markdown :where(h4, h5, h6) { +.markdown h4 { font: var(--dsw-font-markdown-h4); + margin: 16px 0; } -.markdown :where(strong, th) { - font-weight: var(--dsw-font-markdown-base-strong-font-weight); +.markdown :where(h5, h6) { + font: var(--dsw-font-markdown-base-strong); + margin: 16px 0; } -.markdown :where(ul, ol) { - padding-inline-start: 24px; +.markdown :where(h1, h2, h3, h4, h5, h6) strong { + font-weight: inherit; } -.markdown li + li { - margin-block-start: 4px; +.markdown p { + margin: 16px 0; } -.markdown li > :where(ul, ol) { - margin-block-start: 4px; +/* Tighten h4–h6 against a following list (design: 8px gap). */ +.markdown :where(h4, h5, h6) + :where(ul, ol) { + margin-top: 8px; } -.markdown blockquote { - padding-inline-start: 12px; - border-inline-start: 3px solid var(--dsw-alias-markdown-citation); - color: var(--dsw-alias-label-secondary); +.markdown :where(h4, h5, h6):has(+ :where(ul, ol)) { + margin-bottom: 8px; } .markdown a { + /* deepsuite markdown.css uses brand-text (blue in newDesign); this sheet + keeps design-platform brand-text as near-black, so links use the blue + business-primary alias instead. */ color: var(--dsw-alias-state-business-primary); - text-decoration: underline; - text-underline-offset: 2px; + transition: box-shadow var(--ds-transition-duration) var(--ds-ease-in-out); + position: relative; + text-decoration: none; + /* Transparent hit-area padding; literal zero-alpha only (no painted color). */ + border-left: 3px solid rgb(255 255 255 / 0); + border-right: 3px solid rgb(255 255 255 / 0); + border-top: 2px solid rgb(255 255 255 / 0); + border-bottom: 2px solid rgb(255 255 255 / 0); + margin-left: -3px; + margin-right: -3px; } -.markdown :not(pre) > code { - padding: 2px 4px; - border-radius: 4px; - background: var(--dsw-alias-markdown-inline-code); - font: var(--dsw-font-markdown-code); +.markdown a:hover, +.markdown a:focus { + outline: none; + text-decoration: underline var(--dsw-alias-state-business-primary); } -.markdown pre { - max-width: 100%; - overflow-x: auto; - overscroll-behavior-x: contain; - padding: 12px 16px; - border-radius: 8px; - background: var(--dsw-alias-markdown-code-block); - font: var(--dsw-font-markdown-code-block); +.markdown a:focus-visible { + box-shadow: 0 0 0 2px var(--dsw-alias-state-business-primary); } -.markdown pre code { - padding: 0; - background: transparent; - font: inherit; - overflow-wrap: normal; - word-break: normal; - white-space: pre; +.markdown :where(ul, ol) { + margin: 16px 0; + padding-left: 18px; +} + +.markdown li:not(:first-child) { + margin-top: 6px; +} + +.markdown li > :where(ul, ol) { + margin-top: 4px; +} + +.markdown li::marker { + line-height: 28px; + color: var(--dsw-alias-label-secondary); +} + +/* Nested ol under ul/ol: markers inside (models sometimes emit this shape). */ +.markdown :where(ul, ol) ol { + list-style-position: inside; + padding-left: 0; +} + +.markdown :where(ul, ol) ol li p { + display: inline; +} + +.markdown li > p { + margin: 8px 0; +} + +.markdown li > *:first-child { + margin-top: 0; +} + +/* Keep list-nested code-block vertical margins (design: +4px vs other last children). */ +.markdown li > *:last-child:not(:global(.md-code-block)) { + margin-bottom: 0; } .markdown hr { - width: 100%; - border: 0; - border-block-start: 1px solid var(--dsw-alias-markdown-citation); + display: block; + border: none; + height: 1px; + margin: 32px 0; + background: var(--dsw-alias-border-l2); +} + +.markdown blockquote { + border-left: 2px solid var(--dsw-alias-label-caption); + margin: 16px 0 0; + padding-left: 14px; +} + +.markdown pre { + margin: 16px 0; + font-family: var(--ds-font-family-code); + overflow: auto; +} + +.markdown :not(pre) > code { + display: inline-flex; + align-items: center; + box-sizing: border-box; + font: var(--dsw-font-markdown-code); + font-family: var(--ds-font-family-code); + font-size: 0.875em !important; + background-color: var(--dsw-alias-markdown-inline-code); + border-radius: 6px; + padding: 0 5px; +} + +.markdown :where(h1, h2, h3, h4, h5, h6) code { + font: inherit; + font-family: var(--ds-font-family-code); } .markdown input[type='checkbox'] { margin: 0 8px 0 0; - accent-color: var(--dsw-alias-state-business-primary); + accent-color: var(--dsw-alias-label-secondary); } .tableScroll { @@ -99,22 +172,52 @@ } .tableScroll table { - width: max-content; - min-width: 100%; border-collapse: collapse; - font: var(--dsw-font-markdown-table); -} - -.tableScroll :where(th, td) { - padding: 6px 12px; - border: 1px solid var(--dsw-alias-markdown-citation); - text-align: start; - white-space: nowrap; + width: max-content; + max-width: max-content; } .tableScroll th { - background: var(--dsw-alias-markdown-code-block-banner); + text-align: start; + padding: 10px 16px; + border-bottom: 1px solid var(--dsw-alias-border-l3); + border-top: none; font: var(--dsw-font-markdown-table-head); + max-width: 320px; + max-width: min(30vw, 320px); + min-width: 100px; +} + +.tableScroll td { + padding: 10px 16px; + border-bottom: 1px solid var(--dsw-alias-border-l2); + font: var(--dsw-font-markdown-table); + max-width: 320px; + max-width: min(30vw, 320px); + min-width: 100px; +} + +.tableScroll th:first-child, +.tableScroll td:first-child { + padding-left: 0; +} + +.tableScroll td:last-child { + padding-right: 0; +} + +.tableScroll table code { + font-size: 13px; +} + +.markdown > *:first-child, +.markdown p:first-child { + margin-top: 0 !important; +} + +.markdown > *:last-child, +.markdown p:last-child { + margin-bottom: 0 !important; } .imageAlt { diff --git a/packages/client/ui-primitives/tests/code-block.spec.tsx b/packages/client/ui-primitives/tests/code-block.spec.tsx index a58248afab..b6fdc866f9 100644 --- a/packages/client/ui-primitives/tests/code-block.spec.tsx +++ b/packages/client/ui-primitives/tests/code-block.spec.tsx @@ -5,14 +5,17 @@ // display-trimmed. MarkdownText's fence route is pinned in markdown.spec.tsx // alongside the rest of the markdown family. -import { describe, expect, it } from 'vitest' -import { cleanup, render } from '@testing-library/react' -import { afterEach } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { CodeBlock } from '../src/markdown/CodeBlock.tsx' import { highlightToHtml } from '../src/markdown/highlight.ts' afterEach(cleanup) +beforeEach(() => { + vi.useRealTimers() +}) + describe('highlightToHtml', () => { it('highlights a registered grammar into css-variables token spans', () => { const html = highlightToHtml('const x: number = 1', 'typescript') @@ -50,4 +53,68 @@ describe('CodeBlock', () => { expect(view.container.querySelector('pre.shiki')).toBeNull() expect(view.getByText('plain text')).toBeTruthy() }) + + it('shows the language banner and copies the pre textContent', async () => { + vi.useFakeTimers() + const writeText = vi.fn().mockResolvedValue(undefined) + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: { writeText }, + }) + render() + expect(screen.getByText('ts')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(writeText).toHaveBeenCalledWith('const a = 1') + expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy() + // While the ok label is showing, further clicks are no-ops. + fireEvent.click(screen.getByRole('button', { name: '复制成功' })) + expect(writeText).toHaveBeenCalledTimes(1) + await vi.advanceTimersByTimeAsync(1000) + expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() + }) + + it('falls back to execCommand when clipboard.writeText is unavailable', () => { + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: undefined, + }) + const exec = vi.fn().mockReturnValue(true) + Object.defineProperty(document, 'execCommand', { + configurable: true, + value: exec, + }) + render() + fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(exec).toHaveBeenCalledWith('copy') + }) + + it('still acknowledges copy when execCommand throws', () => { + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: undefined, + }) + Object.defineProperty(document, 'execCommand', { + configurable: true, + value: () => { + throw new Error('denied') + }, + }) + render() + fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy() + }) + + it('acknowledges copy when neither clipboard API nor execCommand exists', () => { + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: undefined, + }) + Object.defineProperty(document, 'execCommand', { + configurable: true, + value: undefined, + }) + render() + fireEvent.click(screen.getByRole('button', { name: '复制' })) + expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy() + }) }) diff --git a/packages/client/ui-primitives/tests/markdown.spec.tsx b/packages/client/ui-primitives/tests/markdown.spec.tsx index 05c7ce0139..07df7cebdc 100644 --- a/packages/client/ui-primitives/tests/markdown.spec.tsx +++ b/packages/client/ui-primitives/tests/markdown.spec.tsx @@ -57,8 +57,10 @@ describe('MarkdownText', () => { expect(container.querySelector('table')?.textContent).toContain('alphabeta') expect(container.querySelector('hr')).not.toBeNull() expect(container.querySelector('pre code')?.textContent).toContain('const answer = 42') - // The ts fence routed through the shared CodeBlock: shiki token spans present. + // The ts fence routed through the shared CodeBlock: shiki token spans + banner. expect(container.querySelector('pre.shiki')).not.toBeNull() + expect(screen.getByText('ts')).toBeTruthy() + expect(screen.getByRole('button', { name: '复制' })).toBeTruthy() expect(container.querySelector('br')).not.toBeNull() expect(screen.getByRole('link', { name: 'safe' }).getAttribute('target')).toBe('_blank') expect(screen.getByRole('link', { name: 'https://deepseek.com' })).toBeTruthy() diff --git a/packages/client/ui-theme/src/styles/base.css b/packages/client/ui-theme/src/styles/base.css index 2d1acde71d..4c801b8d4d 100644 --- a/packages/client/ui-theme/src/styles/base.css +++ b/packages/client/ui-theme/src/styles/base.css @@ -9,5 +9,7 @@ --ds-font-family-code: 'SF Mono', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, 'PingFang SC', 'Microsoft YaHei'; --ds-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ds-transition-duration: 0.2s; + --ds-transition-duration-fast: 0.1s; --ds-transition-duration-slow: 0.3s; } From 55fc87a7a018f29d677c4509d6bee96910b18698 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 27 Jul 2026 12:58:20 +0800 Subject: [PATCH 02/10] fix: cr --- ...-07-27-user-message-icon-actions.i18n.yaml | 4 +- .../2026-07-27-user-message-icon-actions.md | 4 +- ...2026-07-27-user-message-icon-actions.zh.md | 4 +- .../src/client/chat/MessageItem.module.css | 18 ++++--- .../src/client/chat/MessageItem.tsx | 7 ++- .../client/toolviews/bash-sample.module.css | 9 ++++ .../src/client/toolviews/bash-sample.tsx | 12 +++++ .../tests/coverage-tails.spec.tsx | 2 + .../ui-primitives/src/markdown/CodeBlock.tsx | 30 +++++++----- .../ui-primitives/tests/code-block.spec.tsx | 48 +++++++++++++------ 10 files changed, 99 insertions(+), 39 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml index 52fa9a6cb3..e664ec81fb 100644 --- a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-27-user-message-icon-actions.md: 45856e1ee093b1bfeaebfe67339aec7b6d2dd694 -2026-07-27-user-message-icon-actions.zh.md: ea87b8036ee91e8998f1e45dbea17f9ee76c244c +2026-07-27-user-message-icon-actions.md: 869e7a2518a3ec927c0689a10816a410dc5f0862 +2026-07-27-user-message-icon-actions.zh.md: 353e5ac765bb2fbab9932449cf2247768a1f412f diff --git a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md index 45856e1ee0..869e7a2518 100644 --- a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md +++ b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.md @@ -10,7 +10,7 @@ The chat user bubble had no under-bubble action chrome. The Harness design (figm ## Decision -`MessageItem` owns the actions for `kind: 'user'` only. Layout is a column (`align-items: flex-end`, 6px gap): bubble, then a 28px action row with 10px gaps and 28px circular icon buttons (`IconCopyOutline16`, `IconBranchOutline16`, `IconEditOutline16`). Tooltips carry Chinese labels. The row stays `opacity: 0` until the user row is hovered or focus-within, per the [web styling](../../../../docs/web-styling.md) message action-bar rule. +`MessageItem` owns the actions for `kind: 'user'` only. Layout is a column (`align-items: flex-end`, 6px gap): bubble, then a 28px action row with 10px gaps and 28px circular icon buttons (`IconCopyOutline16`, `IconBranchOutline16`, `IconEditOutline16`). Tooltips carry Chinese labels. Actions stay visible by default; `@media (hover: hover)` hides them until the row is hovered or focus-within, so touch / `hover: none` devices keep discoverable controls (opacity alone still hit-tests). Copy writes the bubble's joined text blocks to the clipboard (`navigator.clipboard.writeText`, with an `execCommand` fallback). Branch and edit are present chrome with no handlers yet — they reserve the design seats without inventing session-fork or edit-resubmit behavior. @@ -20,7 +20,7 @@ Steering bubbles keep the badge-only form and do not show these actions. **Wire branch/edit to real session fork and draft-edit now.** Rejected for this change: those product flows are not specified; shipping inert buttons matches the requested scope and avoids half-built mutation paths. -**Always-visible actions (no hover fade).** Rejected against the standing action-bar rule; the figma node shows the resting chrome, not the idle-hidden state the style guide requires. +**Always hide with `opacity: 0` outside hover.** Rejected for touch: without `@media (hover: hover)`, idle opacity still hit-tests while looking empty. Hover-capable pointers keep the fade; others keep the actions visible. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md index ea87b8036e..353e5ac765 100644 --- a/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md +++ b/.agents/notes/implemented/feature/2026-07-27-user-message-icon-actions.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -仅当 `kind: 'user'` 时,`MessageItem` 拥有这些操作。布局为纵向列(`align-items: flex-end`,间距 6px):先是气泡,再是高度 28px 的操作行;行内间距 10px,圆形图标按钮尺寸为 28px(`IconCopyOutline16`、`IconBranchOutline16`、`IconEditOutline16`)。Tooltip 承载中文标签。按 [Web 样式](../../../../docs/web-styling.md) 的消息操作栏规则,该行保持 `opacity: 0`,直到用户行被悬停或处于 focus-within 状态。 +仅当 `kind: 'user'` 时,`MessageItem` 拥有这些操作。布局为纵向列(`align-items: flex-end`,间距 6px):先是气泡,再是高度 28px 的操作行;行内间距 10px,圆形图标按钮尺寸为 28px(`IconCopyOutline16`、`IconBranchOutline16`、`IconEditOutline16`)。Tooltip 承载中文标签。操作默认保持可见;`@media (hover: hover)` 下在悬停或 focus-within 前隐藏,以便触摸/`hover: none` 设备仍能发现控件(仅靠 opacity 仍会命中测试)。 复制将气泡内拼接后的文本块写入剪贴板(`navigator.clipboard.writeText`,并以 `execCommand` 作为回退)。分支与编辑目前仅有外观、尚无处理函数——它们预留设计席位,但不发明会话 fork 或编辑重提交流程。 @@ -20,7 +20,7 @@ steering(中途引导)气泡保持仅徽章形态,不展示这些操作。 **现在就把分支/编辑接到真实的会话 fork 与草稿编辑。**本次变更不予采纳:这些产品流程尚未定稿;交付无行为按钮符合请求范围,也避免半成品的变更路径。 -**操作始终可见(无悬停淡入)。**与现行操作栏规则冲突,不予采纳;figma 节点展示的是静止态外观,而非样式指南要求的空闲隐藏状态。 +**在悬停外始终以 `opacity: 0` 隐藏。**因触摸不予采纳:若无 `@media (hover: hover)`,空闲 opacity 看起来空白但仍会命中测试。具备悬停能力的指针保留淡入;其他设备保持操作可见。 ## 后果 diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css index 0d2331a199..22537f9cfe 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.module.css +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.module.css @@ -25,14 +25,20 @@ align-items: center; gap: 10px; height: 28px; - /* Hidden until the row is hovered/focused (web-styling message action bar). */ - opacity: 0; - transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out); } -.userRow:hover .actions, -.userRow:focus-within .actions { - opacity: 1; +/* Hover-capable pointers: hide until the row is hovered/focused. Touch / + hover:none keeps actions visible (opacity:0 still hit-tests). */ +@media (hover: hover) { + .actions { + opacity: 0; + transition: opacity var(--ds-transition-duration) var(--ds-ease-in-out); + } + + .userRow:hover .actions, + .userRow:focus-within .actions { + opacity: 1; + } } .action { diff --git a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx index 67abe94ef6..4ecfdadf88 100644 --- a/packages/client/ui-conversation/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-conversation/src/client/chat/MessageItem.tsx @@ -30,9 +30,14 @@ function contentText(content: readonly unknown[]): { text: string; rest: unknown return { text: texts.join(''), rest } } +/** Best-effort clipboard write; rejections stay swallowed (no success chrome). */ async function writeClipboard(text: string): Promise { if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(text) + try { + await navigator.clipboard.writeText(text) + } catch { + // Denied permissions / iframe policy. + } return } const exec = typeof document.execCommand === 'function' diff --git a/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css b/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css index 9b7116462f..9c42e69b59 100644 --- a/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css +++ b/packages/client/ui-conversation/src/client/toolviews/bash-sample.module.css @@ -61,3 +61,12 @@ line-height: 24px; color: var(--dsw-alias-label-tertiary); } + +.visuallyHidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} diff --git a/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx b/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx index 2503a6e71b..616eee5943 100644 --- a/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/bash-sample.tsx @@ -19,10 +19,21 @@ function leadingFor(state: ToolRowState) { } } +/** Visually hidden status — StateDot is aria-hidden; AT needs a text label. */ +function stateStatus(state: ToolRowState): string | null { + switch (state) { + case 'running': return '运行中' + case 'error': return '失败' + case 'stopped': return '已停止' + default: return null + } +} + /** Bash row: icon + Bash · {description}, matching the shared ToolRow chrome. */ export function BashRow({ toolName, block, openDetails, sessionId, useSessions }: ToolRowProps) { const model = toolRowModel(toolName, block) const isChild = useSessions(list => list.byId[sessionId]?.parentId !== undefined) + const status = stateStatus(model.state) return (
{leadingFor(model.state)} + {status !== null && {status}} {isChild && scoped} {model.title} diff --git a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx index 084f73b4b7..18ac9a2891 100644 --- a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx @@ -113,9 +113,11 @@ describe('tails', () => { const errorView = render() expect(errorView.container.querySelector('[data-sample="bash-global"]')).not.toBeNull() expect(errorView.container.querySelector('[data-state="error"]')).not.toBeNull() + expect(errorView.getByText('失败')).toBeTruthy() errorView.unmount() const stoppedView = render() expect(stoppedView.container.querySelector('[data-state="stopped"]')).not.toBeNull() + expect(stoppedView.getByText('已停止')).toBeTruthy() }) }) diff --git a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx index 33bcf7b80c..151af94e1c 100644 --- a/packages/client/ui-primitives/src/markdown/CodeBlock.tsx +++ b/packages/client/ui-primitives/src/markdown/CodeBlock.tsx @@ -18,16 +18,22 @@ export interface CodeBlockProps { className?: string | undefined } -async function writeClipboard(text: string): Promise { +/** @returns true only when the host accepted the write. */ +async function writeClipboard(text: string): Promise { if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(text) - return + try { + await navigator.clipboard.writeText(text) + return true + } catch { + // Denied permissions / iframe policy — do not claim success. + return false + } } // jsdom and older hosts: best-effort execCommand path when present. const exec = typeof document.execCommand === 'function' ? document.execCommand.bind(document) : undefined - if (exec === undefined) return + if (exec === undefined) return false const el = document.createElement('textarea') el.value = text el.setAttribute('readonly', '') @@ -36,12 +42,12 @@ async function writeClipboard(text: string): Promise { document.body.appendChild(el) el.select() try { - exec('copy') + return exec('copy') } catch { - // Clipboard unavailable (sandboxed iframe / denied permission); UI still - // flips to the ok label so the gesture is acknowledged. + return false + } finally { + el.remove() } - el.remove() } export function CodeBlock({ code, lang, className }: CodeBlockProps) { @@ -55,9 +61,11 @@ export function CodeBlock({ code, lang, className }: CodeBlockProps) { /* v8 ignore next -- both arms always mount a
; trimmed is the
        typed fallback if the DOM shape ever diverges. */
     const text = rootRef.current?.querySelector('pre')?.textContent ?? trimmed
-    void writeClipboard(text)
-    setCopied(true)
-    window.setTimeout(() => setCopied(false), 1000)
+    void writeClipboard(text).then((ok) => {
+      if (!ok) return
+      setCopied(true)
+      window.setTimeout(() => setCopied(false), 1000)
+    })
   }, [copied, trimmed])
 
   const body = html === undefined
diff --git a/packages/client/ui-primitives/tests/code-block.spec.tsx b/packages/client/ui-primitives/tests/code-block.spec.tsx
index b6fdc866f9..47b0ad24fb 100644
--- a/packages/client/ui-primitives/tests/code-block.spec.tsx
+++ b/packages/client/ui-primitives/tests/code-block.spec.tsx
@@ -6,7 +6,7 @@
 // alongside the rest of the markdown family.
 
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
 import { CodeBlock } from '../src/markdown/CodeBlock.tsx'
 import { highlightToHtml } from '../src/markdown/highlight.ts'
 
@@ -65,6 +65,10 @@ describe('CodeBlock', () => {
     expect(screen.getByText('ts')).toBeTruthy()
     fireEvent.click(screen.getByRole('button', { name: '复制' }))
     expect(writeText).toHaveBeenCalledWith('const a = 1')
+    // Flush the clipboard promise under fake timers before asserting the label.
+    await act(async () => {
+      await Promise.resolve()
+    })
     expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy()
     // While the ok label is showing, further clicks are no-ops.
     fireEvent.click(screen.getByRole('button', { name: '复制成功' }))
@@ -73,7 +77,22 @@ describe('CodeBlock', () => {
     expect(screen.getByRole('button', { name: '复制' })).toBeTruthy()
   })
 
-  it('falls back to execCommand when clipboard.writeText is unavailable', () => {
+  it('does not claim success when clipboard.writeText rejects', async () => {
+    const writeText = vi.fn().mockRejectedValue(new Error('denied'))
+    Object.defineProperty(navigator, 'clipboard', {
+      configurable: true,
+      value: { writeText },
+    })
+    render()
+    fireEvent.click(screen.getByRole('button', { name: '复制' }))
+    await act(async () => {
+      await Promise.resolve()
+    })
+    expect(screen.getByRole('button', { name: '复制' })).toBeTruthy()
+    expect(screen.queryByRole('button', { name: '复制成功' })).toBeNull()
+  })
+
+  it('falls back to execCommand when clipboard.writeText is unavailable', async () => {
     Object.defineProperty(navigator, 'clipboard', {
       configurable: true,
       value: undefined,
@@ -86,9 +105,10 @@ describe('CodeBlock', () => {
     render()
     fireEvent.click(screen.getByRole('button', { name: '复制' }))
     expect(exec).toHaveBeenCalledWith('copy')
+    expect(await screen.findByRole('button', { name: '复制成功' })).toBeTruthy()
   })
 
-  it('still acknowledges copy when execCommand throws', () => {
+  it('does not claim success when execCommand throws or is absent', async () => {
     Object.defineProperty(navigator, 'clipboard', {
       configurable: true,
       value: undefined,
@@ -99,22 +119,20 @@ describe('CodeBlock', () => {
         throw new Error('denied')
       },
     })
-    render()
-    fireEvent.click(screen.getByRole('button', { name: '复制' }))
-    expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy()
-  })
+    const denied = render()
+    fireEvent.click(denied.getByRole('button', { name: '复制' }))
+    await Promise.resolve()
+    expect(denied.getByRole('button', { name: '复制' })).toBeTruthy()
+    denied.unmount()
 
-  it('acknowledges copy when neither clipboard API nor execCommand exists', () => {
-    Object.defineProperty(navigator, 'clipboard', {
-      configurable: true,
-      value: undefined,
-    })
     Object.defineProperty(document, 'execCommand', {
       configurable: true,
       value: undefined,
     })
-    render()
-    fireEvent.click(screen.getByRole('button', { name: '复制' }))
-    expect(screen.getByRole('button', { name: '复制成功' })).toBeTruthy()
+    const absent = render()
+    fireEvent.click(absent.getByRole('button', { name: '复制' }))
+    await Promise.resolve()
+    expect(absent.getByRole('button', { name: '复制' })).toBeTruthy()
+    expect(absent.queryByRole('button', { name: '复制成功' })).toBeNull()
   })
 })

From 7ca13198de952ea4d200633ef17fe5232311bfed Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Mon, 27 Jul 2026 14:05:32 +0800
Subject: [PATCH 03/10] ci: keep required status aggregator portable

---
 ...able-required-status-aggregation.i18n.yaml |  6 ++++
 ...27-portable-required-status-aggregation.md | 35 +++++++++++++++++++
 ...portable-required-status-aggregation.zh.md | 35 +++++++++++++++++++
 .github/workflows/ci.yml                      |  4 +--
 4 files changed, 78 insertions(+), 2 deletions(-)
 create mode 100644 .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml
 create mode 100644 .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
 create mode 100644 .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md

diff --git a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml
new file mode 100644
index 0000000000..a029a82389
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml
@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
+2026-07-27-portable-required-status-aggregation.md: 081d841cfb939c97f189c46fc0985f9ff2d1987d
+2026-07-27-portable-required-status-aggregation.zh.md: 896e9500d2ad6e2e0ef6c6cc7a37373c6d28b303
diff --git a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
new file mode 100644
index 0000000000..081d841cfb
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
@@ -0,0 +1,35 @@
+# Agent Note: Portable required-status aggregation
+
+Status: implemented
+
+English | [中文](2026-07-27-portable-required-status-aggregation.zh.md)
+
+## Problem
+
+Branch protection consumes one stable `all checks passed` job instead of tracking the changing names of matrix legs and execution lanes. This job performs no repository work: after its blocking dependencies finish, it only reduces their results into the required verdict.
+
+Assigning that bookkeeping job to a custom runner pool adds an external allocation dependency without using the pool's additional CPU or memory. A provisioning failure can therefore leave the final required status queued even after every substantive check has produced its evidence.
+
+## Decision
+
+The `all-checks-passed` job in [CI](../../../../.github/workflows/ci.yml) runs on standard GitHub-hosted `ubuntu-latest`. It keeps every blocking job in `needs`, retains its load-bearing `if: always()` condition, fails when any dependency is failed, cancelled, or skipped, and succeeds only when every dependency succeeds. It performs no checkout, toolchain setup, dependency installation, or repository gate.
+
+The aggregate depends only on production standard-hosted capacity; it does not use organization-defined, enterprise-defined, or self-hosted labels. Substantive jobs choose their own runner topology independently. Moving this verdict does not change their commands, weaken their evidence, or make an unresolved dependency pass: the aggregate waits for unfinished dependencies and fails on non-success terminal results.
+
+This decision supersedes only the aggregate-placement clause in the [portable pull-request CI recovery boundary](2026-07-23-portable-required-pull-request-ci.md), which continues to own the substantive jobs' recovery topology. The final bookkeeping status remains separately owned so runner-topology changes and branch-protection aggregation can evolve independently.
+
+## Alternatives considered
+
+**Run the aggregate beside substantive jobs on a custom enterprise pool.** This avoids one short standard-hosted allocation, but gives the bookkeeping job a provisioning failure mode without using the larger machine's capacity.
+
+**Use a standby self-hosted runner.** This replaces one external readiness dependency with another and makes a required verdict depend on a separately operated machine. Managed standard-hosted capacity is the production path for this bookkeeping work.
+
+**Require every substantive job directly in branch protection.** This removes the aggregate allocation, but couples repository settings to matrix and lane names that change as the CI topology evolves.
+
+**Treat missing or non-success dependencies as success.** This would produce a green status by discarding required evidence rather than by completing it.
+
+## Consequences
+
+Each pull request allocates one short standard-hosted job after its substantive dependencies settle. Because the job performs no checkout or setup, it adds little active runtime, but its scheduling and billing remain separate from custom pools.
+
+A custom-pool outage can still keep a substantive dependency queued, and the aggregate correctly waits in that case. Once the dependencies reach terminal results, the final required verdict no longer needs custom-pool or self-hosted allocation. Future changes can move substantive jobs between standard and larger runners without reintroducing that dependency into the branch-protection status.
diff --git a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md
new file mode 100644
index 0000000000..896e9500d2
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md
@@ -0,0 +1,35 @@
+# Agent Note: 必需状态的可移植聚合
+
+Status: implemented
+
+[English](2026-07-27-portable-required-status-aggregation.md) | 中文
+
+## 问题
+
+分支保护只使用一项稳定的 `all checks passed` 作业,无需跟踪持续变化的矩阵分支名和执行通道名。该作业不执行任何仓库工作:会阻塞判定的依赖项结束后,它只将这些依赖项的结果归并为必需判定。
+
+将这项结果汇总作业分配给自定义运行器池,会在不使用该池额外 CPU 或内存的情况下增加一项外部运行器分配依赖。因此,即使所有实质性检查都已产出证据,预配失败仍可能让最终的必需状态持续排队。
+
+## 决策
+
+[CI](../../../../.github/workflows/ci.yml) 中的 `all-checks-passed` 作业在 GitHub 标准托管的 `ubuntu-latest` 上运行。它在 `needs` 中保留所有会阻塞判定的作业,保留承重的 `if: always()` 条件;任何依赖项失败、被取消或被跳过时,该作业都会失败,只有所有依赖项都成功时才会成功。它不执行代码检出、工具链设置、依赖安装或仓库门禁。
+
+聚合作业只依赖生产环境的标准托管容量;它不使用组织定义的、企业定义的或自托管的运行器标签。实质性作业各自独立选择运行器拓扑。调整这项判定作业的运行位置,不会改变实质性作业的命令、削弱其证据或使未完成的依赖项通过:聚合作业会等待尚未结束的依赖项,并在依赖项产生非成功的终态结果时失败。
+
+本决策仅取代[拉取请求 CI 的可移植恢复边界](2026-07-23-portable-required-pull-request-ci.md)中关于聚合作业运行位置的条款;该记录继续规定实质性作业的恢复拓扑。最终的结果汇总状态仍由本决策单独规定,使运行器拓扑变更与分支保护聚合可以独立演进。
+
+## 曾考虑的替代方案
+
+**让聚合作业与实质性作业一同在自定义企业级运行器池上运行。** 此方案可以避免一次短暂的标准托管运行器分配,但会在未使用大型机器容量的情况下,为结果汇总作业引入预配失败的故障模式。
+
+**使用备用自托管运行器。** 此方案只是用另一项外就绪状态依赖替换原有依赖,并使必需判定依赖一台单独运维的机器。由平台管理的标准托管容量是这项结果汇总工作的生产路径。
+
+**在分支保护中直接要求每项实质性作业。** 此方案不再需要为聚合作业分配运行器,但会将仓库设置与随 CI 拓扑演进而变化的矩阵分支名和通道名耦合。
+
+**将缺失或非成功的依赖项视为成功。** 这种做法不是通过完成相应检查来产出必需证据,而是丢弃这些证据以产出绿色状态。
+
+## 后果
+
+每个拉取请求都会在实质性依赖项的结果确定后分配一项短时运行的标准托管作业。由于该作业不执行代码检出或设置,它只增加少量活跃运行时间,但其调度和计费仍独立于自定义运行器池。
+
+自定义运行器池不可用仍可能让实质性依赖项持续排队,聚合作业在这种情况下会按设计等待。依赖项产生终态结果后,最终的必需判定不再需要自定义运行器池或自托管运行器分配。未来可以在标准运行器与大型运行器之间迁移实质性作业,而不会将这项依赖重新引入分支保护状态。
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f02d30a563..413a3cb51b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -693,8 +693,8 @@ jobs:
   # 'cancelled' and 'skipped'.
   all-checks-passed:
     name: all checks passed
-    # The required verdict must not add a separate standard-hosted billing dependency.
-    runs-on: dsh-enterprise-ubuntu-latest-32core-test
+    # This bookkeeping-only verdict must not depend on custom-pool provisioning.
+    runs-on: ubuntu-latest
     needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, windows]
     if: always() && github.event_name == 'pull_request'
     steps:

From 761eeb7c55c3e34902c1dfa1acee33345d8202d5 Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Mon, 27 Jul 2026 14:15:46 +0800
Subject: [PATCH 04/10] ci: restore standard runners for primary checks

---
 ...rial-cross-platform-ci-reference.i18n.yaml |  6 ++---
 ...7-21-serial-cross-platform-ci-reference.md |  6 ++---
 ...1-serial-cross-platform-ci-reference.zh.md |  6 ++---
 ...ence-based-larger-hosted-runners.i18n.yaml |  6 ++---
 ...22-evidence-based-larger-hosted-runners.md | 20 ++++++++---------
 ...evidence-based-larger-hosted-runners.zh.md | 20 ++++++++---------
 ...ortable-required-pull-request-ci.i18n.yaml |  6 ++---
 ...07-23-portable-required-pull-request-ci.md | 16 +++++++-------
 ...23-portable-required-pull-request-ci.zh.md | 16 +++++++-------
 .github/workflows/ci.yml                      | 22 +++++++++----------
 10 files changed, 62 insertions(+), 62 deletions(-)

diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
index 17edb300cc..8cd2a0f7c8 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-2026-07-21-serial-cross-platform-ci-reference.md: 5433d2c51831ce61d06a16ee0b0ed982911f9218
-2026-07-21-serial-cross-platform-ci-reference.zh.md: 041d53d13e14354c995e4b65defce94a97646b0a
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
+2026-07-21-serial-cross-platform-ci-reference.md: 220c4b2a092ec1907482edc60a12f981fdf986a4
+2026-07-21-serial-cross-platform-ci-reference.zh.md: 70e4c40f0f64fef4b1de05a7603ece25aaf5bea2
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
index 5433d2c518..220c4b2a09 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
@@ -14,7 +14,7 @@ Reviewers also need a direct answer to a simpler question: what happens when the
 
 ## Decision
 
-[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity. A push to `master` skips those jobs and runs three explicit references named `serial / linux`, `serial / macos`, and `serial / windows`. They intentionally duplicate their short checkout, runtime setup, and immutable install sequences instead of hiding the operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks.
+[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run three primary Linux jobs, one complete Windows job, and the Node compatibility and Python contracts on standard GitHub-hosted capacity. A push to `master` skips those jobs and runs three explicit references named `serial / linux`, `serial / macos`, and `serial / windows`. They intentionally duplicate their short checkout, runtime setup, and immutable install sequences instead of hiding the operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks.
 
 Each reference job runs `pnpm run check:ci` without any shard selector. `DSH_GATE_CONCURRENCY=1` makes the top-level aggregate execute one ready gate at a time; coverage, snapshot replay, built-bin smoke, and publication validation also receive worker counts of one. The three operating-system jobs may run beside one another, but each host's repository gates are serial and complete. Linux installs bubblewrap before replaying snapshots, and Windows enables Developer Mode before installing the symlinked workspace.
 
@@ -24,7 +24,7 @@ The macOS reference runs the ordinary Vitest project in forked processes. Node 2
 
 Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. A pull request runs only its required jobs; a master push runs only the three serial references. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value.
 
-The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. Required pull-request jobs use the same portable Linux and Windows capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
+The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. Substantive required pull-request jobs use the same portable Linux and Windows capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
 
 ## Alternatives considered
 
@@ -32,7 +32,7 @@ The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, a
 - **Trust only the concurrent primary inventory** - rejected because scheduling and validation share implementation assumptions; a serial aggregate is an independent completeness check.
 - **Run the serial references on every pull request** - rejected because they duplicate complete cross-platform aggregates and add macOS work to every change; the required jobs already execute the blocking Linux and Windows contracts.
 - **Use one operating-system matrix** - rejected because three named jobs make the reference surface visible without another selection mechanism.
-- **Run the serial reference on larger runners** - rejected because both required CI and its independent reference must remain runnable when organization-owned pools cannot allocate jobs.
+- **Run the serial reference on larger runners** - rejected because substantive required CI and its independent reference must remain runnable when organization-owned pools cannot allocate jobs.
 
 ## Consequences
 
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
index 041d53d13e..70e4c40f0f 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
@@ -14,7 +14,7 @@ Status: implemented
 
 ## 决策
 
-[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和 Windows 作业,以及 Node 兼容性与 Python 契约。向 `master` 推送时会跳过这些作业,改为运行三个显式参考作业,名称分别为 `serial / linux`、`serial / macos` 和 `serial / windows`。这些作业有意分别重复简短的代码检出、运行时设置和依赖锁定的安装步骤,不用矩阵或可复用工作流把操作系统差异隐藏起来。`workflow_dispatch` 仅用于运行器基准测试。
+[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行 3 项 Linux 主作业、1 项完整的 Windows 作业,以及 Node 兼容性与 Python 契约。向 `master` 推送时会跳过这些作业,改为运行三个显式参考作业,名称分别为 `serial / linux`、`serial / macos` 和 `serial / windows`。这些作业有意分别重复简短的代码检出、运行时设置和依赖锁定的安装步骤,不用矩阵或可复用工作流把操作系统差异隐藏起来。`workflow_dispatch` 仅用于运行器基准测试。
 
 每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的并发数也设为 1。三种操作系统的作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。
 
@@ -24,7 +24,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上
 
 master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。拉取请求只运行其必需作业;向 master 推送时只运行三个串行参考作业。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。
 
-可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用相同的可移植 Linux 和 Windows 容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
+可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求中的实质性必需作业使用相同的可移植 Linux 和 Windows 容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
 
 ## 曾考虑的替代方案
 
@@ -32,7 +32,7 @@ master 分支的参考作业仅用于诊断,不参与拉取请求所要求的
 - **仅信任并发执行的主门禁清单**:不予采纳,因为调度逻辑与校验逻辑共享实现假设;串行聚合流程是一项独立的完整性检查。
 - **在每个拉取请求上运行串行参考作业**:不予采纳,因为这些作业会重复完整的跨平台聚合流程,并为每项改动增加 macOS 工作;必需作业已经执行阻塞性的 Linux 和 Windows 契约。
 - **使用一个操作系统矩阵**:不予采纳,因为三个具名作业无需另一套选择机制,就能让参考流程的构成清晰可见。
-- **在大型运行器上运行串行参考流程**:不予采纳,因为当组织自有运行器池无法分配作业时,必需 CI 及其独立参考流程都必须仍可运行。
+- **在大型运行器上运行串行参考流程**:不予采纳,因为当组织自有运行器池无法分配作业时,承担实质性检查的必需 CI 及其独立参考流程都必须仍可运行。
 
 ## 后果
 
diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml
index 3d8e6fc395..eeaa689fd8 100644
--- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-2026-07-22-evidence-based-larger-hosted-runners.md: fe11e6929545923d27fbf41f5a39f7dd2b9c3fbf
-2026-07-22-evidence-based-larger-hosted-runners.zh.md: 47879284532a537cbe7e78aa2c495c4ef0be26c4
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
+2026-07-22-evidence-based-larger-hosted-runners.md: 8a3ca991accd5bbe71b6f92cffff4c9a420b1f25
+2026-07-22-evidence-based-larger-hosted-runners.zh.md: 870c4301eed4793d3ac6801c310288abf1d46c3b
diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
index fe11e69295..8a3ca991ac 100644
--- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
+++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
@@ -12,19 +12,19 @@ Larger runners make it possible to pay setup once and parallelize inside the rep
 
 ## Decision
 
-The enterprise keeps repo-restricted x64 larger-runner pools for Ubuntu and Windows. Ordinary pull requests name three 32-core pools directly: Ubuntu 24.04 for exhaustive coverage, Ubuntu latest for the remaining primary Node 24 inventory, and Windows 2025 for blocking Windows contracts. Public IPs are disabled, and workflow concurrency remains bounded because an autoscaling ceiling neither allocates idle machines nor makes repository work scale without limit.
+The enterprise keeps repo-restricted x64 larger-runner pools for Ubuntu and Windows as measurement infrastructure. Public IPs are disabled, and benchmark concurrency remains bounded because an autoscaling ceiling neither allocates idle machines nor makes repository work scale without limit.
 
-The required primary path depends on those enterprise pools. Standard GitHub-hosted jobs retain the Node 22.19, Node 26, and Python SDK compatibility contracts, while the [portable recovery boundary](2026-07-23-portable-required-pull-request-ci.md) and [serial reference](2026-07-21-serial-cross-platform-ci-reference.md) keep complete standard-runner evidence available on `master`. `suite=larger-runner-benchmark` compares isolated critical lanes across provisioned sizes, and `suite=consolidated-runner-benchmark` compares whole aggregates. Each benchmark reports its observed processor and memory capacity before running repository work.
+Ordinary pull requests use the standard-hosted primary path owned by the [portable recovery boundary](2026-07-23-portable-required-pull-request-ci.md). `suite=larger-runner-benchmark` compares isolated critical lanes across provisioned sizes, and `suite=consolidated-runner-benchmark` compares whole aggregates. Each benchmark reports its observed processor and memory capacity before running repository work. The [serial reference](2026-07-21-serial-cross-platform-ci-reference.md) keeps an independent complete standard-runner oracle on `master`.
 
 The former gate-level and coarse primary shard jobs are absent from the workflow. Their static, lint, coverage, snapshot, and scenario shard selectors are also absent from the repository, so an unused diagnostic path cannot preserve a second CI architecture.
 
-Linux primary work uses three independent 32-core jobs. Coverage runs alone with its own worker bound, and the static scheduler runs alone so its result has no post-build consumer tail. After static gates finish, that job publishes its emitted `apps/*/lib`, `packages/*/*/lib`, and `vendor/*/lib` tree as a run-scoped artifact. The third job restores that exact tree, then starts lint, Node 24 runtime compatibility, build-backed snapshots, and all artifact consumers without repeating the build. Generated NodeNext consumer directories are excluded from ESLint discovery because the artifact check removes them while these processes overlap. The pnpm store and ESLint cache are restored without putting cache uploads on the pull-request critical path. Performance reports use each job's `startedAt` to `completedAt` interval; runner queue delay is capacity evidence, not repository execution time.
+Linux primary work uses three independent standard-hosted jobs with single-worker inner bounds. Coverage runs alone, and the static scheduler runs alone so its result has no post-build consumer tail. After static gates finish, that job publishes its emitted `apps/*/lib`, `packages/*/*/lib`, and `vendor/*/lib` tree as a run-scoped artifact. The third job restores that exact tree, then starts lint, Node 24 runtime compatibility, build-backed snapshots, and all artifact consumers without repeating the build. Generated NodeNext consumer directories are excluded from ESLint discovery because the artifact check removes them while these processes overlap. The pnpm store and ESLint cache are restored without putting cache uploads on the pull-request critical path. Performance reports use each job's `startedAt` to `completedAt` interval; runner queue delay is capacity evidence, not repository execution time.
 
 The gate dependencies remain explicit. Coverage consumes source and does not wait for build. Documentation typechecking builds its complete project-reference graph once. Snapshot replay and publication consumers wait for emitted output, while Node-version compatibility jobs exercise runtime-sensitive source loading without repeating the primary source-graph typecheck. PTY and subprocess suites keep their bounded inner concurrency rather than inheriting the runner's core count.
 
 The artifact boundary remains explicit. `scripts/publint-all.ts` calls publint's supported API against an in-memory publication view formed from each manifest's declared files plus npm's mandatory metadata, avoiding one package-manager pack process per package. `scripts/verify-built-package-invariants.mjs` stages the declared `lib/` files below the real package and imports its compiled self-reference through plain Node and Cordis Loader normalization; a runtime chunk omitted from the publication contract still fails.
 
-Windows shares one 32-core setup across the blocking build and production site plus observational built-artifact contracts. Linux owns the duplicate lint, coverage, and snapshot inventories because running those observational copies on Windows extends the paid critical path without adding a blocking platform claim.
+Windows shares one standard-hosted setup across the blocking build and production site plus observational built-artifact contracts, with single-worker bounds. Linux owns the duplicate lint, coverage, and snapshot inventories because running those observational copies on Windows extends the critical path without adding a blocking platform claim.
 
 An [exact-head all-size benchmark](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29908491351) ran the complete unsharded primary Node aggregate on every Linux pool before the eager-build correction:
 
@@ -50,11 +50,11 @@ Inner and outer worker limits are separate controls. An [exact-head 32-worker ES
 
 The process-bound coverage project contains exactly five suite files. Thirty-two forks crashed Node 24's CJS lexer twice, and a later 16-fork run reproduced the worker loss and invalid coverage result. The single Vitest invocation therefore uses threads for the broad inventory and reserves forks for suites that exercise process-global state, `process` APIs, or timing-sensitive process I/O. That narrow fork inventory includes the local bash process-plumbing suite and the pi-ai adapter suite because aggregate contention changed timing observations in both. These failures make deterministic coverage, not advertised cores, the upper bound on worker selection.
 
-Complete serial Linux, macOS, and Windows references run only when `master` moves. Pull requests use the enterprise required path plus standard-hosted compatibility jobs, while other larger-runner sizes run only by manual dispatch.
+Complete serial Linux, macOS, and Windows references run only when `master` moves. Pull requests use the standard-hosted required path, while larger-runner sizes run only by manual dispatch.
 
 ## Alternatives considered
 
-**Keep the three coarse primary Linux lanes.** The core, CPU, and production-site jobs met the latency targets, but they paid three setup waves and left primary Node work sharded after larger runners were available. The all-size trace showed that one unnecessary dependency, not a lack of host capacity, kept the single-box aggregate above one minute.
+**Restore the former core, CPU, and production-site lanes.** Those jobs met the latency targets, but they paid three setup waves and left primary Node work sharded after larger runners were available. The all-size trace showed that one unnecessary dependency, not a lack of host capacity, kept the single-box aggregate above one minute.
 
 **Keep the former gate-level shard topology as a manual reference.** A dormant second topology kept hundreds of workflow lines, selector modules, and scenario-partition behavior alive. The all-size and serial suites provide timing and completeness controls without preserving production code that no required job exercises.
 
@@ -68,7 +68,7 @@ Complete serial Linux, macOS, and Windows references run only when `master` move
 
 **Keep static gates and post-build consumers on one runner.** Reusing one workspace avoids a setup wave and artifact transfer, but build-duration variance delays every consumer and leaves their lint and snapshot tails after the static result. A run-scoped built tree preserves one exact build while independent jobs keep both complete paths within the observed target.
 
-**Keep the complete required path on standard GitHub-hosted capacity.** This avoids repository-external runner configuration, but exact-head standard-runner runs remain materially slower and can spend longer queued behind shared capacity. Standard-hosted compatibility and serial references preserve portable evidence without making that slower topology the ordinary primary path.
+**Use larger-runner pools as the required default.** This offers lower measured latency when allocation works, but a missing entitlement or delayed enterprise transfer leaves required jobs queued without repository diagnostics. The portable path accepts longer runtime, and manual suites preserve the performance experiment.
 
 **Keep required and observational Windows checks in separate jobs.** The split preserves status semantics at the workflow level but pays setup twice. `run-gates` preserves the same required versus non-blocking distinction inside one process.
 
@@ -76,10 +76,10 @@ Complete serial Linux, macOS, and Windows references run only when `master` move
 
 ## Consequences
 
-The required topology pays one setup wave per 32-core lane and retains no shard selectors. Every ordinary pull request consumes paid enterprise Linux and Windows minutes; manual benchmarks add other sizes only when remeasurement is useful.
+The required topology pays one setup wave per standard-hosted lane and retains no shard selectors. The substantive CI inventory consumes enterprise larger-runner minutes only when a benchmark is dispatched; the lightweight aggregate's separate runner choice is outside this decision.
 
-GitHub rounds each larger-runner execution up to a whole minute, so complete-job measurement exposes both billed time and workflow complexity. Splitting Linux repeats setup twice and transfers one built tree, but isolates coverage, static gates, and post-build consumers from each other's critical paths without repeating the build; consolidating Windows avoids repeating its slower setup.
+Splitting Linux repeats setup twice and transfers one built tree, but isolates coverage, static gates, and post-build consumers from each other's critical paths without repeating the build. Consolidating Windows avoids repeating its slower setup. The trade-off is longer elapsed time than the measured larger-runner topology.
 
 Performance targets are observations, not cancellation deadlines or correctness requirements. Manual all-size and serial suites remain available when image, dependency, scheduler, or pricing changes need remeasurement.
 
-Missing or renamed enterprise labels leave required primary jobs queued. Standard-hosted compatibility jobs and `master` references still report useful evidence, but they do not substitute for the required aggregate; runner assignment is therefore an operational dependency that repository CI cannot repair.
+Missing or renamed enterprise labels leave manual benchmarks unavailable without queueing a substantive primary job. The retained pools can compare sizes after allocation recovers without making runner assignment an operational dependency of repository gate execution.
diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md
index 4787928453..870c4301ee 100644
--- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md
+++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md
@@ -12,19 +12,19 @@ Status: implemented
 
 ## 决策
 
-企业保留仅限本仓库使用的 Ubuntu 和 Windows x64 大型运行器池。普通拉取请求直接指定 3 个 32 核运行器池:Ubuntu 24.04 用于完整覆盖率,Ubuntu latest 用于其余主 Node 24 清单,Windows 2025 用于阻塞性 Windows 契约。公网 IP 已禁用;工作流并发仍设有边界,因为自动扩缩容上限既不会分配闲置机器,也不意味着仓库工作可以无限扩展。
+企业保留仅限本仓库使用的 Ubuntu 和 Windows x64 大型运行器池,作为测量基础设施。公网 IP 已禁用;基准测试并发仍设有边界,因为自动扩缩容上限既不会分配闲置机器,也不意味着仓库工作可以无限扩展。
 
-必需主路径依赖这些企业级运行器池。GitHub 标准托管作业保留 Node 22.19、Node 26 和 Python SDK 兼容性契约,而[可移植恢复边界](2026-07-23-portable-required-pull-request-ci.md)与[串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)则在 `master` 上持续提供完整的标准运行器证据。`suite=larger-runner-benchmark` 比较已预配规格上相互独立的关键通道,`suite=consolidated-runner-benchmark` 则比较完整聚合流程。每项基准测试都会先报告实测的处理器和内存容量,再运行仓库工作。
+普通拉取请求使用由[可移植恢复边界](2026-07-23-portable-required-pull-request-ci.md)规定的标准托管主路径。`suite=larger-runner-benchmark` 比较已预配规格上相互独立的关键通道,`suite=consolidated-runner-benchmark` 则比较完整聚合流程。每项基准测试都会先报告实测的处理器和内存容量,再运行仓库工作。[串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)在 `master` 上保留一套独立、完整的标准运行器判定基准。
 
 原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除,因此未使用的诊断路径无法继续维系第二套 CI 架构。
 
-Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行,并设有自己的工作线程上限;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib`、`packages/*/*/lib` 和 `vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 ESLint 的文件发现范围,因为这些进程重叠执行时,产物检查会删除这些目录。pnpm store 和 ESLint 缓存会得到恢复,但缓存上传不会进入拉取请求关键路径。性能报告采用每个作业从 `startedAt` 到 `completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
+Linux 主流程使用 3 项相互独立的标准托管作业,内部均采用单工作线程上限。覆盖率单独运行;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib`、`packages/*/*/lib` 和 `vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 ESLint 的文件发现范围,因为这些进程重叠执行时,产物检查会删除这些目录。pnpm store 和 ESLint 缓存会得到恢复,但缓存上传不会进入拉取请求关键路径。性能报告采用每个作业从 `startedAt` 到 `completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
 
 门禁依赖关系保持显式。覆盖率消费源码,不等待构建。文档类型检查只构建一次完整的 project-reference 图。快照回放和发布消费方等待生成的输出,而 Node 版本兼容性作业会验证对运行时敏感的源码加载,且不重复主源码项目图的类型检查。PTY 和子进程套件继续使用自身有界的内部并发,不继承运行器的核心数。
 
 产物边界保持显式。`scripts/publint-all.ts` 对内存中的发布视图调用 publint 支持的 API;该视图由每个 manifest(元数据清单)声明的文件和 npm 强制要求的元数据组成,从而避免为每个包启动一次包管理器 pack 进程。`scripts/verify-built-package-invariants.mjs` 将已声明的 `lib/` 文件暂存到真实包下,并通过普通 Node 和 Cordis Loader 规范化导入其已编译的自身引用;发布契约只要遗漏一个运行时分片,检查仍会失败。
 
-Windows 以一次 32 核环境设置同时承载阻塞性构建、生产网站和观测性的构建产物契约。重复的 lint、覆盖率和快照清单由 Linux 承担,因为在 Windows 上运行这些观测性副本会延长付费关键路径,却不会新增任何阻塞性平台契约。
+Windows 以一次标准托管环境设置同时承载阻塞性构建、生产网站和观测性的构建产物契约,并采用单工作线程上限。重复的 lint、覆盖率和快照清单由 Linux 承担,因为在 Windows 上运行这些观测性副本会延长关键路径,却不会新增任何阻塞性平台契约。
 
 一次[分支头精确的全规格基准测试](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29908491351)在修正构建尽早启动逻辑前,对每种 Linux 池都运行了完整且未分片的主 Node 聚合流程:
 
@@ -50,11 +50,11 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
 
 进程约束的覆盖率项目恰好包含 5 个套件文件。32 个 fork 曾两次导致 Node 24 的 CJS 词法分析器崩溃,后来一次使用 16 个 fork 的运行又复现了工作进程丢失和无效的覆盖率结果。因此,单次 Vitest 调用会对大范围测试清单使用线程,只为涉及进程全局状态、`process` API 或对时间敏感的进程 I/O 的套件保留 fork。这份有限的 fork 清单包括本地 bash 进程通路套件和 pi-ai 适配器套件,因为聚合争用改变了二者的时序观测结果。这些故障表明,选择工作线程数量时,上限取决于能否得到确定的覆盖率结果,而非标称核心数。
 
-只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考。拉取请求使用企业级运行器必需路径和标准托管兼容性作业,其他大型运行器规格仅通过手动触发运行。
+只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考。拉取请求使用标准托管的必需路径,大型运行器规格仅通过手动触发运行。
 
 ## 曾考虑的替代方案
 
-**保留 3 个粗粒度 Linux 主流程通道。** 核心、CPU 和生产网站作业均达到延迟目标,但它们需要 3 轮设置,而且在大型运行器已经可用后仍对主 Node 工作进行分片。全规格运行轨迹表明,让单机聚合流程超过 1 分钟的是一项不必要的依赖,而非主机容量不足。
+**恢复原有的核心、CPU 和生产网站通道。** 这些作业均达到延迟目标,但它们需要 3 轮设置,而且在大型运行器已经可用后仍对主 Node 工作进行分片。全规格运行轨迹表明,让单机聚合流程超过 1 分钟的是一项不必要的依赖,而非主机容量不足。
 
 **将原有的门禁级分片拓扑保留为手动参考。** 一套闲置的第二拓扑会让数百行工作流、选择器模块和场景分区行为继续存活。全规格和串行套件无需保留任何必需作业都不执行的生产代码,也能提供计时与完整性对照。
 
@@ -68,7 +68,7 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
 
 **将静态门禁和构建后消费方保留在同一台运行器上。** 复用同一个工作区可以省去一轮设置和一次产物传输,但构建耗时的波动会延迟每个消费方,并使消费方的 lint 和快照尾段延续到静态结果之后。仅供本次运行使用的已构建目录树可以保留同一份构建结果,而相互独立的作业能让两条完整路径都保持在实测目标内。
 
-**将完整必需路径保留在 GitHub 标准托管容量上。** 此方案可以避免依赖仓库外部的运行器配置,但标准运行器上的分支头精确运行仍明显更慢,也可能因共享容量而排队更久。标准托管兼容性作业和串行参考流程保留可移植证据,无需让这套较慢的拓扑成为普通主路径。
+**将大型运行器池作为必需作业的默认运行环境。** 当运行器能够分配时,此方案可提供实测更低的延迟;但若未获得相应使用权限或企业转移延迟,必需作业会持续排队,且不会发出仓库诊断。可移植路径接受更长的运行时间,手动套件则保留性能实验。
 
 **将必需的 Windows 检查和观测性 Windows 检查保留在不同作业中。** 这种拆分在工作流层保留状态语义,却需要支付两次设置开销。`run-gates` 在一个进程内保留了相同的必需与非阻塞区别。
 
@@ -76,10 +76,10 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
 
 ## 后果
 
-必需拓扑中的每个 32 核通道只承担 1 轮设置开销,且不保留分片选择器。每个普通拉取请求都会消耗付费的企业级 Linux 和 Windows 运行器分钟数;只有在重新测量有价值时,手动基准测试才会加入其他规格。
+必需拓扑中的每个标准托管通道只承担 1 轮设置开销,且不保留分片选择器。只有在触发基准测试时,实质性 CI 清单才会消耗企业级大型运行器分钟数;轻量级聚合流程单独选择运行器,不属于本决策范围。
 
-GitHub 会把每次大型运行器执行向上取整到整分钟计费,因此完整作业测量能同时呈现计费时长与工作流复杂度。拆分 Linux 会重复两轮设置并传输一份已构建目录树,但能使覆盖率、静态门禁与构建后消费方不再相互进入关键路径,且无需重复构建;合并 Windows 则避免重复其耗时更长的设置。
+拆分 Linux 会重复两轮设置并传输一份已构建目录树,但能使覆盖率、静态门禁与构建后消费方不再相互进入关键路径,且无需重复构建。合并 Windows 可避免重复其耗时更长的设置。代价是总耗时长于经测量的大型运行器拓扑。
 
 性能目标是观测结果,而非取消截止时间或正确性要求。当映像、依赖、调度器或定价发生变化而需要重新测量时,仍可使用手动全规格和串行套件。
 
-企业级运行器标签缺失或改名时,必需主作业会持续排队。标准托管兼容性作业与 `master` 参考流程仍会报告有用证据,但不能替代必需聚合流程;因此,运行器分配是一项仓库 CI 无法修复的运维依赖。
+企业级运行器标签缺失或改名时,手动基准测试会不可用,但不会让实质性主作业排队。运行器分配能力恢复后,保留的运行器池仍可比较不同规格,同时不会让运行器分配成为仓库门禁执行的运维依赖。
diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml
index 05147cd54a..34ea2f4a90 100644
--- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-2026-07-23-portable-required-pull-request-ci.md: d1002c7d9db7cd8bbed3bdfda8a773a4b124bf16
-2026-07-23-portable-required-pull-request-ci.zh.md: fedfc6b9c982ace5ece430c52db23c22ec5119d4
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
+2026-07-23-portable-required-pull-request-ci.md: 4fd915a00300922d7318c78d16e1e8078b5170ed
+2026-07-23-portable-required-pull-request-ci.zh.md: 9d489970a46279de8033cb82af64489d86b20d2c
diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
index d1002c7d9d..4fd915a003 100644
--- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
+++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
@@ -12,24 +12,24 @@ Billing health, a runner definition's `Ready` state, and a large autoscaling cei
 
 ## Decision
 
-[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs on standard `windows-2025` with single-worker bounds, keeping the complete Windows contract independent of enterprise Windows allocation. Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and `master` runs complete serial Linux, macOS, and Windows references. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request.
+[CI](../../../../.github/workflows/ci.yml) runs the three required primary Node 24 jobs on standard `ubuntu-latest` and the complete required Windows job on standard `windows-2025`. Static gates publish their exact built tree for the snapshot and artifact job, while coverage remains independent. Top-level gates, coverage, ESLint, publint, and snapshot replay use single-worker bounds on these smaller hosts. Node 22.19, Node 26, and Python SDK compatibility also use standard capacity. The lightweight `all checks passed` aggregate remains a separate scheduling decision because it performs no checkout or repository gate.
 
-The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / complete` remain dependencies of `all checks passed`; branch protection continues to require `e2e` and `all checks passed`. There is no automatic fallback when a remaining enterprise Linux label cannot allocate: the standard jobs continue to report their own contracts, but they cannot manufacture the missing required result.
+The three Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / complete` remain dependencies of `all checks passed`; no gate is removed or made observational to recover availability. Branch protection continues to require `e2e` and `all checks passed`.
 
-The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the current primary topology and its measurements. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check, and the manual larger-runner suites retain size comparisons without expanding the ordinary required matrix.
+The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the retained performance measurements and manual suites. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check.
 
 ## Alternatives considered
 
-**Keep the Linux primary jobs and aggregate on standard capacity.** This removes the remaining enterprise allocation dependency, but complete standard-runner jobs give materially slower feedback and still experience shared-capacity queues. The current split retains portable compatibility and serial evidence while spending enterprise capacity on the Linux primary critical path.
+**Wait for enterprise allocation to recover.** A queue with no assigned runner emits no repository diagnostic and can block every pull request indefinitely, so external recovery is not a correctness path.
 
-**Select enterprise size from advertised core count.** Benchmarks show non-monotonic scaling and setup variance, so exact complete-job measurements choose the required pools instead.
+**Use only the smallest enterprise pools.** Every named pool crosses the same enterprise allocation boundary; reducing core count does not remove the dependency that caused the queue.
 
 **Skip or demote checks while capacity is unavailable.** This would make the status green by dropping evidence rather than by running the repository's required contracts.
 
-**Use one worker policy on every host.** Outer gate concurrency and inner tool workers contend differently on Linux, Windows, and standard runners; measured host-specific bounds avoid turning additional cores into slower execution.
+**Keep larger-runner worker limits on standard runners.** Concurrent repository gates and their inner worker pools can oversubscribe the smaller memory and CPU allocation, turning an availability repair into contention failures.
 
 ## Consequences
 
-Ordinary pull requests spend enterprise capacity on the Linux critical path while standard Windows trades longer runtime for independent allocation. A live exact-head run proves the same commands that branch protection consumes; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval.
+Ordinary pull requests can acquire every substantive runner without enterprise-specific configuration. A live exact-head run proves the same commands that branch protection consumes, at the cost of longer elapsed time on smaller hosts.
 
-Standard compatibility and required Windows jobs remain useful when enterprise allocation is degraded, but they do not make a blocked required Linux job or aggregate green. Recovering Linux availability may require restoring the complete standard-hosted topology; changing a pool definition's status alone is insufficient evidence that it can receive work.
+Manual larger-runner benchmarks can remain queued without blocking pull requests. Restoring larger runners to the required path needs a separate evidence-based decision after exact-head jobs receive nonzero runner IDs and complete reliably; changing a pool definition's status alone is insufficient.
diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md
index fedfc6b9c9..9d489970a4 100644
--- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md
+++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md
@@ -12,24 +12,24 @@ Status: implemented
 
 ## 决策
 
-[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在标准 `windows-2025` 上运行,并采用单工作线程上限,使完整的 Windows 契约不依赖企业级 Windows 运行器分配。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,`master` 则运行完整的 Linux、macOS 和 Windows 串行参考流程。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。
+[CI](../../../../.github/workflows/ci.yml) 在标准 `ubuntu-latest` 上运行 3 项必需的主 Node 24 作业,并在标准 `windows-2025` 上运行完整的必需 Windows 作业。静态门禁发布其完全一致的已构建目录树,供快照与产物作业使用;覆盖率作业则保持独立。这些较小主机上的顶层门禁、覆盖率、ESLint、publint 和快照回放均采用单工作线程上限。Node 22.19、Node 26 和 Python SDK 兼容性也使用标准容量。轻量级 `all checks passed` 聚合流程仍由单独的调度决策管理,因为它不执行代码检出或仓库门禁。
 
-两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / complete` 继续作为 `all checks passed` 的依赖项;分支保护继续要求 `e2e` 和 `all checks passed`。剩余的企业级 Linux 运行器标签无法分配运行器时没有自动后备机制:标准作业会继续报告各自的契约,但无法产出缺失的必需结果。
+3 项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / complete` 继续作为 `all checks passed` 的依赖项;为恢复可用性,没有移除任何门禁,也没有将任何门禁改为仅供观测。分支保护继续要求 `e2e` 和 `all checks passed`。
 
-当前主拓扑及其测量结果由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查,手动大型运行器套件则保留规格比较,同时不扩大普通必需矩阵。
+保留的性能测量结果与手动套件由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查。
 
 ## 曾考虑的替代方案
 
-**将 Linux 主作业和聚合流程保留在标准容量上。** 此方案消除了剩余的企业级运行器分配依赖,但标准运行器上的完整作业反馈明显更慢,仍会遇到共享容量排队。当前拆分既保留可移植兼容性和串行证据,又将企业级运行器容量用于 Linux 主关键路径。
+**等待企业级运行器分配恢复。** 未分配运行器的队列不会发出任何仓库诊断,并且可能无限期阻塞所有拉取请求,因此外部恢复不能作为正确性路径。
 
-**根据标称核心数选择企业规格。** 基准测试表明扩展效果不呈单调变化,设置耗时也存在波动,因此必需运行器池改由完整作业的精确测量结果选定。
+**仅使用最小的企业级运行器池。** 无论指定哪个运行器池,都要经过同一个企业级分配边界;减少核心数并不能消除导致排队的依赖。
 
 **在容量不可用时跳过检查或降低其级别。** 这种方式通过丢弃证据而非执行仓库的必需契约来使状态变绿。
 
-**在每台主机上使用同一工作线程策略。** 外层门禁并发与内层工具工作线程在 Linux、Windows 和标准运行器上的争用方式不同;按主机实测的上限可以避免新增核心反而拖慢执行。
+**在标准运行器上沿用大型运行器的工作线程上限。** 并发运行的仓库门禁及其内部工作线程池,可能让并发需求超过较小的内存和 CPU 配额,使可用性修复反而引发资源争用故障。
 
 ## 后果
 
-普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而标准托管 Windows 作业则以更长的运行时间换取不依赖企业池的运行器分配。一次实际的分支头精确运行能够证明分支保护使用的同一组命令;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。
+普通拉取请求无需企业专用配置,即可为每项实质性作业获得运行器。一次实际的分支头精确运行能够证明分支保护使用的同一组命令,代价是在较小主机上耗时更长。
 
-企业级运行器分配能力下降时,标准兼容性作业和必需的 Windows 作业仍能提供有用证据,但无法让受阻的必需 Linux 作业或聚合流程变绿。恢复 Linux 可用性时,可能需要恢复完整的标准托管拓扑;仅改变运行器池定义的状态,不足以证明它可以接收作业。
+手动大型运行器基准测试即使持续排队,也不会阻塞拉取请求。只有在分支头精确作业获得非零运行器 ID 并稳定完成后,才能另行作出基于证据的决策,将大型运行器恢复到必需路径;仅改变运行器池定义的状态仍然不够。
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f02d30a563..dd49680c6b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,15 +27,15 @@ env:
 
 jobs:
 
-  # Three enterprise jobs isolate coverage, static analysis, and the
+  # Three standard Linux jobs isolate coverage, static analysis, and the
   # build-backed consumer tail. The static job publishes its exact build so
   # consumers do not repeat the longest part of their critical path.
   node-24:
     if: github.event_name == 'pull_request'
-    runs-on: dsh-enterprise-ubuntu-latest-32core-test
+    runs-on: ubuntu-latest
     name: node 24 / static
     env:
-      DSH_GATE_CONCURRENCY: '8'
+      DSH_GATE_CONCURRENCY: '1'
     steps:
       # Fetch complete history so the archive gate can read the trusted PR base from a reused shallow checkout.
       - uses: actions/checkout@v6
@@ -81,11 +81,11 @@ jobs:
 
   node-24-coverage:
     if: github.event_name == 'pull_request'
-    runs-on: dsh-enterprise-ubuntu-24-04-32core-test
+    runs-on: ubuntu-latest
     name: node 24 / coverage
     env:
-      DSH_COVERAGE_MAX_WORKERS: '24'
-      DSH_GATE_CONCURRENCY: '8'
+      DSH_COVERAGE_MAX_WORKERS: '1'
+      DSH_GATE_CONCURRENCY: '1'
     steps:
       - uses: actions/checkout@v6
         with:
@@ -122,15 +122,15 @@ jobs:
   node-24-consumers:
     needs: node-24
     if: github.event_name == 'pull_request'
-    runs-on: dsh-enterprise-ubuntu-latest-32core-test
+    runs-on: ubuntu-latest
     name: node 24 / snapshots and artifacts
     env:
       DSH_ESLINT_CACHE: '1'
-      DSH_ESLINT_CONCURRENCY: '8'
-      DSH_GATE_CONCURRENCY: '8'
+      DSH_ESLINT_CONCURRENCY: '1'
+      DSH_GATE_CONCURRENCY: '1'
       DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
-      DSH_PUBLINT_CONCURRENCY: '8'
-      DSH_SNAPSHOT_MAX_CONCURRENCY: '32'
+      DSH_PUBLINT_CONCURRENCY: '1'
+      DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
     steps:
       - uses: actions/checkout@v6
         with:

From 9c53cd7d072268599b70f54ecb76e6b19379aeb5 Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Mon, 27 Jul 2026 15:33:36 +0800
Subject: [PATCH 05/10] test: stabilize timing-sensitive terminal checks

---
 ...26-07-16-persistent-pty-sessions.i18n.yaml |  6 +-
 .../2026-07-16-persistent-pty-sessions.md     |  2 +-
 .../2026-07-16-persistent-pty-sessions.zh.md  |  2 +-
 ...-18-tui-terminal-state-snapshots.i18n.yaml |  6 +-
 ...2026-07-18-tui-terminal-state-snapshots.md |  2 +-
 ...6-07-18-tui-terminal-state-snapshots.zh.md |  2 +-
 packages/pty/pty-local/tests/local.spec.ts    | 28 +++++---
 packages/ui/tui/tests/tui.snapshot.ts         | 64 +++++++++++--------
 8 files changed, 66 insertions(+), 46 deletions(-)

diff --git a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml
index f7e242b78c..4c73d590af 100644
--- a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-2026-07-16-persistent-pty-sessions.md: 148d4a2f47689e38a3ec83a7a41e4f75c4b73d95
-2026-07-16-persistent-pty-sessions.zh.md: 9a9d9cd4b0f61e8abaf011996ecd8739d13851f8
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
+2026-07-16-persistent-pty-sessions.md: 43c87bb159cfe1ab9f8d3a80c2adf25a57ae6e3b
+2026-07-16-persistent-pty-sessions.zh.md: 8afc2103447cc58b1fcbc1062b9564e8ed643477
diff --git a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
index 148d4a2f47..43c87bb159 100644
--- a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
+++ b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
@@ -154,7 +154,7 @@ The package ships concise tool guidance explaining persistent state, owner isola
 
 - Per-file coverage pins owner fencing, concurrent reservations, unpublished-spawn cancellation and awaited teardown, sandbox-mode change rejection, retriable lifecycle cleanup, readiness tiers, sanitizer carry state, complete UTF-8 bounds, task integration, schemas, and exact render intents.
 - Linux process fixtures cover non-leader and non-main-thread stdin waits, zombie quiescence, unreadable process state, supported syscall tables, unsupported architectures, and false-positive rejection; macOS inspector logic is injected into the same unit suite.
-- Real `node-pty` tests exercise shell state, shared sandbox policy, environment scrubbing, raw-mode foreground `SIGINT`, a TERM-ignoring descendant, and immediate post-disposal quiescence on supported hosts.
+- Real `node-pty` tests exercise shell state, shared sandbox policy, environment scrubbing, raw-mode foreground `SIGINT` after deliberately delayed child readiness under scenario-owned timing bounds, a TERM-ignoring descendant, and immediate post-disposal quiescence on supported hosts.
 - A Loader-driven `cordis.yml` test mounts the real three-package composition. ACP and headless snapshots pin the six schemas, bounded results, and errors through opt-in overlays; TUI snapshots pin terminal and generic card presentation.
 - Package contracts, the architecture map, core data structures, generated catalogs, and the website API describe the same shipped surface.
 - The repository CI-equivalent sequence owns type, lint, coverage, snapshot, documentation, build, hygiene, demo, and built-entry verification.
diff --git a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md
index 9a9d9cd4b0..8afc210344 100644
--- a/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md
@@ -154,7 +154,7 @@ plugins:
 
 - 每文件覆盖率固定 owner 隔离、并发预留、未发布 spawn 的取消与等待式 teardown、沙箱模式变更拒绝、可重试的生命周期清理、就绪层级、sanitizer carry state、完整 UTF-8 结果上限、task 集成、schema 和精确 render intent。
 - Linux 进程 fixture 覆盖非 leader 与非主线程的 stdin 等待、僵尸进程静止性、不可读进程状态、受支持的 syscall 表、不支持的架构和误报拒绝;同一单元测试套件通过注入覆盖 macOS 检查器逻辑。
-- 真实 `node-pty` 测试在受支持宿主上覆盖 shell 状态、共享沙箱策略、环境清洗、raw mode 下的前台 `SIGINT`、忽略 `SIGTERM` 的子进程,以及 dispose 返回后立即静默。
+- 真实 `node-pty` 测试在受支持宿主上覆盖 shell 状态、共享沙箱策略、环境清洗、在由场景掌控的时间界限内先有意延迟子进程就绪,再对 raw mode 前台进程发送 `SIGINT`、忽略 `SIGTERM` 的子进程,以及 dispose 返回后立即完全停稳。
 - Loader 驱动的 `cordis.yml` 测试挂载真实三包组合。ACP 与 headless 快照通过 opt-in overlay 固定 6 个 schema、有界结果和错误;TUI 快照固定 terminal 与 generic 卡片展示。
 - 包契约、架构图、核心数据结构、生成目录和 website API 描述同一个已发布接口。
 - 仓库 CI 等价序列负责类型、lint、覆盖率、快照、文档、构建、hygiene、demo 和 built-entry 验证。
diff --git a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml
index 133198a4d2..d491394db7 100644
--- a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml
+++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write
-2026-07-18-tui-terminal-state-snapshots.md: 8e86588f69fdb9d615232252ecf57309d440f1cd
-2026-07-18-tui-terminal-state-snapshots.zh.md: b70a46830f44e9da663e30745fcdb7ad281592da
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
+2026-07-18-tui-terminal-state-snapshots.md: 18c79bc2d0dabf4d78887354f30a2cdc083899e1
+2026-07-18-tui-terminal-state-snapshots.zh.md: d1d4a6ca859e94a153e0bf645a17f03c0dac234b
diff --git a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
index 8e86588f69..18c79bc2d0 100644
--- a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
+++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md
@@ -33,7 +33,7 @@ The live-model fixtures use `DSH_SNAPSHOT=record`; record mode rewrites their pr
 
 ### Semantic terminal projection
 
-The package-local `HeadlessTerminal` implements the same pi-tui `Terminal` interface as the process terminal and feeds every ANSI write into the pinned `@xterm/headless` parser. Snapshot code waits for synchronized frames to quiesce before reading state, so a checkpoint represents a completed screen rather than a timer-dependent write prefix.
+The package-local `HeadlessTerminal` implements the same pi-tui `Terminal` interface as the process terminal and feeds every ANSI write into the pinned `@xterm/headless` parser. Snapshot code waits for synchronized frames to quiesce before reading state. The streaming checkpoint freezes the loader interval while allowing real wall-clock delay across one animation tick, so it pins semantic status rather than whichever spinner glyph the scheduler happened to render.
 
 Each expected output projects dimensions, active-buffer and viewport coordinates, lifecycle and cursor state, rows, wrap markers, and non-default style ranges into text. Scroll-heavy cards capture the used buffer; overlays capture the visible viewport. Text and style remain separate so a reviewer can distinguish content changes from presentation changes without decoding ANSI bytes.
 
diff --git a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md
index b70a46830f..d1d4a6ca85 100644
--- a/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md
+++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md
@@ -33,7 +33,7 @@ TUI 覆盖分为四个互补层次:
 
 ### 语义终端投影
 
-包内的 `HeadlessTerminal` 实现与进程终端相同的 pi-tui `Terminal` 接口,并把每次 ANSI 写入交给固定版本的 `@xterm/headless` 解析器。读取状态前,快照代码会等待同步帧稳定,因此每个检查点表示已经完成的画面,而不是依赖计时的写入前缀。
+包内的 `HeadlessTerminal` 实现与进程终端相同的 pi-tui `Terminal` 接口,并把每次 ANSI 写入交给固定版本的 `@xterm/headless` 解析器。读取状态前,快照代码会等待同步帧稳定。流式输出检查点会冻结 loader 的 interval,同时保留跨过一次动画 tick 的真实墙钟等待,从而固定语义状态,而非调度器碰巧渲染出的某个加载动画字形。
 
 每份预期输出把终端尺寸、活动缓冲区和视口坐标、生命周期与光标状态、各行、换行标记以及非默认样式区间投影为文本。滚动内容较多的卡片捕获已使用缓冲区;浮层捕获可见视口。文本和样式相互分离,评审人无需解码 ANSI 字节即可区分内容变化与呈现变化。
 
diff --git a/packages/pty/pty-local/tests/local.spec.ts b/packages/pty/pty-local/tests/local.spec.ts
index 6ba3a95757..8c1c58f319 100644
--- a/packages/pty/pty-local/tests/local.spec.ts
+++ b/packages/pty/pty-local/tests/local.spec.ts
@@ -39,7 +39,10 @@ function stubAgent(ctx: Context, rawId: string): Agent {
   }
 }
 
-async function harness(mode: 'danger-full-access' | 'workspace-write') {
+async function harness(
+  mode: 'danger-full-access' | 'workspace-write',
+  timing: { idleSilenceMs?: number; timeoutMs?: number } = {},
+) {
   const root = mkdtempSync(join(tmpdir(), 'dsh-pty-local-'))
   roots.push(root)
   const ctx = new Context()
@@ -51,8 +54,8 @@ async function harness(mode: 'danger-full-access' | 'workspace-write') {
   const fiber = await ctx.plugin(ptyLocal, {
     pollIntervalMs: 10,
     exactProbeAfterMs: 20,
-    idleSilenceMs: 250,
-    timeoutMs: 2000,
+    idleSilenceMs: timing.idleSilenceMs ?? 250,
+    timeoutMs: timing.timeoutMs ?? 2_000,
     disposeGraceMs: 500,
     scrollbackLines: 100,
     scrollbackMaxBytes: 32_768,
@@ -63,8 +66,8 @@ async function harness(mode: 'danger-full-access' | 'workspace-write') {
   return { ctx, root, agent, fiber, sandbox: ctx.sandbox as PassthroughSandbox }
 }
 
-async function waitForOutput(operation: PtySendOperation, expected: string): Promise {
-  const deadline = Date.now() + 2_000
+async function waitForOutput(operation: PtySendOperation, expected: string, timeoutMs = 2_000): Promise {
+  const deadline = Date.now() + timeoutMs
   let output = ''
   while (!output.includes(expected) && Date.now() < deadline) {
     output += operation.readOutput().delta
@@ -131,20 +134,25 @@ describe('pty-local real shell', () => {
     expect(() => process.kill(pid, 0)).toThrow()
   }, 10_000)
 
-  it('cancels a raw-mode foreground process with a real SIGINT', async () => {
-    const { ctx, agent } = await harness('danger-full-access')
+  it('cancels a slow-starting raw-mode foreground process with a real SIGINT', async () => {
+    const { ctx, agent } = await harness('danger-full-access', {
+      idleSilenceMs: 10_000,
+      timeoutMs: 15_000,
+    })
     const created = await ctx.pty.spawn(agent, { type: 'shell' })
     const controller = new AbortController()
     const ready = 'RAW_READY'
+    // Delay readiness beyond the shared harness's short send bound so this
+    // process test owns enough slack for loaded macOS startup and shell echo.
     // The interactive shell echoes the command, so only child output may contain the readiness marker.
-    const command = 'python3 -c \'import signal,sys,termios,time; signal.signal(signal.SIGINT, lambda *_: (print("SIGINT_SEEN", flush=True), sys.exit(0))); attrs=termios.tcgetattr(0); attrs[3] &= ~termios.ISIG; termios.tcsetattr(0, termios.TCSANOW, attrs); print("RAW_" + "READY", flush=True); time.sleep(60)\''
+    const command = 'python3 -c \'import signal,sys,termios,time; signal.signal(signal.SIGINT, lambda *_: (print("SIGINT_SEEN", flush=True), sys.exit(0))); attrs=termios.tcgetattr(0); attrs[3] &= ~termios.ISIG; termios.tcsetattr(0, termios.TCSANOW, attrs); time.sleep(2.1); print("RAW_" + "READY", flush=True); time.sleep(60)\''
     expect(command).not.toContain(ready)
     const foreground = ctx.pty.startSend(agent, created.sessionId, {
       text: command,
       submit: true,
       signal: controller.signal,
     })
-    await waitForOutput(foreground, ready)
+    await waitForOutput(foreground, ready, 15_000)
     controller.abort()
     const result = await foreground.done
     expect(result.waitReason).toBe('stdin_read')
@@ -155,5 +163,5 @@ describe('pty-local real shell', () => {
     expect(after.viewport).toContain('AFTER_SIGINT')
     expect(after.waitReason).toBe('stdin_read')
     await ctx.pty.kill(agent, created.sessionId)
-  }, 10_000)
+  }, 20_000)
 })
diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts
index 1dc6ffa7cd..3bc5adc366 100644
--- a/packages/ui/tui/tests/tui.snapshot.ts
+++ b/packages/ui/tui/tests/tui.snapshot.ts
@@ -228,33 +228,45 @@ const DISPLAYED_CONTROL_PROBE = String.raw`\x1b]2;snapshot-controlled\x07\x09\x7
 describe('TUI terminal-state snapshots', () => {
   it('pins an in-flight reasoning and Markdown stream', async () => {
     const harness = await setupSnapshot()
-    await renderAfter(harness, () => {
-      harness.agent.status = 'running'
-      harness.ctx.emit('agent/status', harness.agent, 'running')
-      appendUser(harness.session, 'Show the live update.')
-      harness.session.append('assistant/chunk', {
-        turn: 1,
-        step: 1,
-        chunk: { type: 'block-start', index: 0, blockType: 'reasoning' },
+    // Freeze the loader's first animation interval so this semantic snapshot
+    // cannot select a different spinner frame under scheduler contention.
+    const frozenLoaderTimer = setInterval(() => {}, 60_000)
+    const intervals = vi.spyOn(globalThis, 'setInterval').mockImplementationOnce(() => frozenLoaderTimer)
+    try {
+      await renderAfter(harness, () => {
+        harness.agent.status = 'running'
+        harness.ctx.emit('agent/status', harness.agent, 'running')
+        appendUser(harness.session, 'Show the live update.')
+        harness.session.append('assistant/chunk', {
+          turn: 1,
+          step: 1,
+          chunk: { type: 'block-start', index: 0, blockType: 'reasoning' },
+        })
+        harness.session.append('assistant/chunk', {
+          turn: 1,
+          step: 1,
+          chunk: { type: 'reasoning-delta', index: 0, text: 'Inspecting width and styles.' },
+        })
+        harness.session.append('assistant/chunk', {
+          turn: 1,
+          step: 1,
+          chunk: { type: 'block-start', index: 1, blockType: 'text' },
+        })
+        harness.session.append('assistant/chunk', {
+          turn: 1,
+          step: 1,
+          chunk: { type: 'text-delta', index: 1, text: 'Streaming **visible state**…' },
+        })
       })
-      harness.session.append('assistant/chunk', {
-        turn: 1,
-        step: 1,
-        chunk: { type: 'reasoning-delta', index: 0, text: 'Inspecting width and styles.' },
-      })
-      harness.session.append('assistant/chunk', {
-        turn: 1,
-        step: 1,
-        chunk: { type: 'block-start', index: 1, blockType: 'text' },
-      })
-      harness.session.append('assistant/chunk', {
-        turn: 1,
-        step: 1,
-        chunk: { type: 'text-delta', index: 1, text: 'Streaming **visible state**…' },
-      })
-    })
-    await checkpoint('conversation-streaming', harness.terminal)
-    await disposeSnapshot(harness)
+      const loaderIntervalMs = intervals.mock.calls[0]?.[1]
+      if (typeof loaderIntervalMs !== 'number') throw new Error('TUI loader did not register an animation interval')
+      await new Promise(resolve => setTimeout(resolve, loaderIntervalMs + 5))
+      await checkpoint('conversation-streaming', harness.terminal)
+    } finally {
+      intervals.mockRestore()
+      clearInterval(frozenLoaderTimer)
+      await disposeSnapshot(harness)
+    }
   })
 
   it('pins failed-stream retraction, scheduled retry, and eventual success', async () => {

From 0a7c8a284d53d20500232ef599114b1395144197 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Mon, 27 Jul 2026 15:44:10 +0800
Subject: [PATCH 06/10] chore

---
 packages/client/ui-conversation/src/client/queue/QueueDock.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/client/ui-conversation/src/client/queue/QueueDock.tsx b/packages/client/ui-conversation/src/client/queue/QueueDock.tsx
index fcd7e75732..f5b5047c7b 100644
--- a/packages/client/ui-conversation/src/client/queue/QueueDock.tsx
+++ b/packages/client/ui-conversation/src/client/queue/QueueDock.tsx
@@ -30,7 +30,7 @@ export function QueueDock({ useSession }: QueueDockProps) {
 }
 
 /**
- * The dock entry as a plain registrant plugin (bash-sample posture).
+ * The dock entry as a plain registrant plugin (bash posture).
  * `inject: ['conversation']` is the ordering seam: the conversation service
  * mounts after ui-conversation's slot registrations, so the
  * 'conversation.input.dock' declaration is on the ledger by then.

From 228d503230f879afea72b4c455231f7b3df37241 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Mon, 27 Jul 2026 15:49:37 +0800
Subject: [PATCH 07/10] fix: code-mode fixture snapshot

---
 apps/web/tests/code-mode-fixture.snapshot.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/web/tests/code-mode-fixture.snapshot.ts b/apps/web/tests/code-mode-fixture.snapshot.ts
index a727844b1b..f53777fff8 100644
--- a/apps/web/tests/code-mode-fixture.snapshot.ts
+++ b/apps/web/tests/code-mode-fixture.snapshot.ts
@@ -144,7 +144,7 @@ it('renders the fixture run_code turn: code parent row, nested sub-rows, error s
       "errorSubRow": true,
       "parentRow": "CodeRead the notes files and summarize",
       "subRows": [
-        "$List notes",
+        "BashList notes",
         "Readnotes/demo.txt",
         "Readnotes/missing.txt",
       ],

From 98877ca32632a1c9681e5e2a5584ae4f8f4b2bcf Mon Sep 17 00:00:00 2001
From: Tianyi Cui <53024+tianyicui@users.noreply.github.com>
Date: Mon, 27 Jul 2026 15:55:21 +0800
Subject: [PATCH 08/10] ci: drop recovered-runner fallback from retarget

---
 ...rial-cross-platform-ci-reference.i18n.yaml |  6 ++--
 ...7-21-serial-cross-platform-ci-reference.md |  6 ++--
 ...1-serial-cross-platform-ci-reference.zh.md |  6 ++--
 ...ence-based-larger-hosted-runners.i18n.yaml |  6 ++--
 ...22-evidence-based-larger-hosted-runners.md | 20 +++++------
 ...evidence-based-larger-hosted-runners.zh.md | 20 +++++------
 ...ortable-required-pull-request-ci.i18n.yaml |  6 ++--
 ...07-23-portable-required-pull-request-ci.md | 16 ++++-----
 ...23-portable-required-pull-request-ci.zh.md | 16 ++++-----
 ...able-required-status-aggregation.i18n.yaml |  6 ----
 ...27-portable-required-status-aggregation.md | 35 -------------------
 ...portable-required-status-aggregation.zh.md | 35 -------------------
 .github/workflows/ci.yml                      | 26 +++++++-------
 13 files changed, 64 insertions(+), 140 deletions(-)
 delete mode 100644 .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml
 delete mode 100644 .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
 delete mode 100644 .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md

diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
index 8cd2a0f7c8..17edb300cc 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
-2026-07-21-serial-cross-platform-ci-reference.md: 220c4b2a092ec1907482edc60a12f981fdf986a4
-2026-07-21-serial-cross-platform-ci-reference.zh.md: 70e4c40f0f64fef4b1de05a7603ece25aaf5bea2
+#   pnpm run verify-translation-pairing --write
+2026-07-21-serial-cross-platform-ci-reference.md: 5433d2c51831ce61d06a16ee0b0ed982911f9218
+2026-07-21-serial-cross-platform-ci-reference.zh.md: 041d53d13e14354c995e4b65defce94a97646b0a
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
index 220c4b2a09..5433d2c518 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md
@@ -14,7 +14,7 @@ Reviewers also need a direct answer to a simpler question: what happens when the
 
 ## Decision
 
-[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run three primary Linux jobs, one complete Windows job, and the Node compatibility and Python contracts on standard GitHub-hosted capacity. A push to `master` skips those jobs and runs three explicit references named `serial / linux`, `serial / macos`, and `serial / windows`. They intentionally duplicate their short checkout, runtime setup, and immutable install sequences instead of hiding the operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks.
+[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run consolidated Linux and Windows jobs plus the Node compatibility and Python contracts on standard GitHub-hosted capacity. A push to `master` skips those jobs and runs three explicit references named `serial / linux`, `serial / macos`, and `serial / windows`. They intentionally duplicate their short checkout, runtime setup, and immutable install sequences instead of hiding the operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks.
 
 Each reference job runs `pnpm run check:ci` without any shard selector. `DSH_GATE_CONCURRENCY=1` makes the top-level aggregate execute one ready gate at a time; coverage, snapshot replay, built-bin smoke, and publication validation also receive worker counts of one. The three operating-system jobs may run beside one another, but each host's repository gates are serial and complete. Linux installs bubblewrap before replaying snapshots, and Windows enables Developer Mode before installing the symlinked workspace.
 
@@ -24,7 +24,7 @@ The macOS reference runs the ordinary Vitest project in forked processes. Node 2
 
 Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. A pull request runs only its required jobs; a master push runs only the three serial references. Performance is evaluated from completed hosted-job timestamps and reported as a measurement; it is not encoded as a `timeout-minutes` value.
 
-The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. Substantive required pull-request jobs use the same portable Linux and Windows capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
+The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. Required pull-request jobs use the same portable Linux and Windows capacity under the [required-CI decision](2026-07-23-portable-required-pull-request-ci.md). Higher-core hosted runners remain manual benchmarks because a correctness path must remain runnable without repository-external runner configuration.
 
 ## Alternatives considered
 
@@ -32,7 +32,7 @@ The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, a
 - **Trust only the concurrent primary inventory** - rejected because scheduling and validation share implementation assumptions; a serial aggregate is an independent completeness check.
 - **Run the serial references on every pull request** - rejected because they duplicate complete cross-platform aggregates and add macOS work to every change; the required jobs already execute the blocking Linux and Windows contracts.
 - **Use one operating-system matrix** - rejected because three named jobs make the reference surface visible without another selection mechanism.
-- **Run the serial reference on larger runners** - rejected because substantive required CI and its independent reference must remain runnable when organization-owned pools cannot allocate jobs.
+- **Run the serial reference on larger runners** - rejected because both required CI and its independent reference must remain runnable when organization-owned pools cannot allocate jobs.
 
 ## Consequences
 
diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
index 70e4c40f0f..041d53d13e 100644
--- a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
+++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md
@@ -14,7 +14,7 @@ Status: implemented
 
 ## 决策
 
-[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行 3 项 Linux 主作业、1 项完整的 Windows 作业,以及 Node 兼容性与 Python 契约。向 `master` 推送时会跳过这些作业,改为运行三个显式参考作业,名称分别为 `serial / linux`、`serial / macos` 和 `serial / windows`。这些作业有意分别重复简短的代码检出、运行时设置和依赖锁定的安装步骤,不用矩阵或可复用工作流把操作系统差异隐藏起来。`workflow_dispatch` 仅用于运行器基准测试。
+[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求在 GitHub 标准托管容量上运行合并后的 Linux 和 Windows 作业,以及 Node 兼容性与 Python 契约。向 `master` 推送时会跳过这些作业,改为运行三个显式参考作业,名称分别为 `serial / linux`、`serial / macos` 和 `serial / windows`。这些作业有意分别重复简短的代码检出、运行时设置和依赖锁定的安装步骤,不用矩阵或可复用工作流把操作系统差异隐藏起来。`workflow_dispatch` 仅用于运行器基准测试。
 
 每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的并发数也设为 1。三种操作系统的作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。
 
@@ -24,7 +24,7 @@ macOS 参考流程使用 fork 进程运行常规 Vitest 项目。macOS arm64 上
 
 master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。拉取请求只运行其必需作业;向 master 推送时只运行三个串行参考作业。系统根据已完成托管作业的时间戳评估性能,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。
 
-可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求中的实质性必需作业使用相同的可移植 Linux 和 Windows 容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
+可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。依据[必需 CI 决策](2026-07-23-portable-required-pull-request-ci.md),拉取请求必需作业使用相同的可移植 Linux 和 Windows 容量。更高核心数的托管运行器仍仅用于手动基准测试,因为正确性路径必须无需仓库外部的运行器配置即可运行。
 
 ## 曾考虑的替代方案
 
@@ -32,7 +32,7 @@ master 分支的参考作业仅用于诊断,不参与拉取请求所要求的
 - **仅信任并发执行的主门禁清单**:不予采纳,因为调度逻辑与校验逻辑共享实现假设;串行聚合流程是一项独立的完整性检查。
 - **在每个拉取请求上运行串行参考作业**:不予采纳,因为这些作业会重复完整的跨平台聚合流程,并为每项改动增加 macOS 工作;必需作业已经执行阻塞性的 Linux 和 Windows 契约。
 - **使用一个操作系统矩阵**:不予采纳,因为三个具名作业无需另一套选择机制,就能让参考流程的构成清晰可见。
-- **在大型运行器上运行串行参考流程**:不予采纳,因为当组织自有运行器池无法分配作业时,承担实质性检查的必需 CI 及其独立参考流程都必须仍可运行。
+- **在大型运行器上运行串行参考流程**:不予采纳,因为当组织自有运行器池无法分配作业时,必需 CI 及其独立参考流程都必须仍可运行。
 
 ## 后果
 
diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml
index eeaa689fd8..3d8e6fc395 100644
--- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
-2026-07-22-evidence-based-larger-hosted-runners.md: 8a3ca991accd5bbe71b6f92cffff4c9a420b1f25
-2026-07-22-evidence-based-larger-hosted-runners.zh.md: 870c4301eed4793d3ac6801c310288abf1d46c3b
+#   pnpm run verify-translation-pairing --write
+2026-07-22-evidence-based-larger-hosted-runners.md: fe11e6929545923d27fbf41f5a39f7dd2b9c3fbf
+2026-07-22-evidence-based-larger-hosted-runners.zh.md: 47879284532a537cbe7e78aa2c495c4ef0be26c4
diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
index 8a3ca991ac..fe11e69295 100644
--- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
+++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
@@ -12,19 +12,19 @@ Larger runners make it possible to pay setup once and parallelize inside the rep
 
 ## Decision
 
-The enterprise keeps repo-restricted x64 larger-runner pools for Ubuntu and Windows as measurement infrastructure. Public IPs are disabled, and benchmark concurrency remains bounded because an autoscaling ceiling neither allocates idle machines nor makes repository work scale without limit.
+The enterprise keeps repo-restricted x64 larger-runner pools for Ubuntu and Windows. Ordinary pull requests name three 32-core pools directly: Ubuntu 24.04 for exhaustive coverage, Ubuntu latest for the remaining primary Node 24 inventory, and Windows 2025 for blocking Windows contracts. Public IPs are disabled, and workflow concurrency remains bounded because an autoscaling ceiling neither allocates idle machines nor makes repository work scale without limit.
 
-Ordinary pull requests use the standard-hosted primary path owned by the [portable recovery boundary](2026-07-23-portable-required-pull-request-ci.md). `suite=larger-runner-benchmark` compares isolated critical lanes across provisioned sizes, and `suite=consolidated-runner-benchmark` compares whole aggregates. Each benchmark reports its observed processor and memory capacity before running repository work. The [serial reference](2026-07-21-serial-cross-platform-ci-reference.md) keeps an independent complete standard-runner oracle on `master`.
+The required primary path depends on those enterprise pools. Standard GitHub-hosted jobs retain the Node 22.19, Node 26, and Python SDK compatibility contracts, while the [portable recovery boundary](2026-07-23-portable-required-pull-request-ci.md) and [serial reference](2026-07-21-serial-cross-platform-ci-reference.md) keep complete standard-runner evidence available on `master`. `suite=larger-runner-benchmark` compares isolated critical lanes across provisioned sizes, and `suite=consolidated-runner-benchmark` compares whole aggregates. Each benchmark reports its observed processor and memory capacity before running repository work.
 
 The former gate-level and coarse primary shard jobs are absent from the workflow. Their static, lint, coverage, snapshot, and scenario shard selectors are also absent from the repository, so an unused diagnostic path cannot preserve a second CI architecture.
 
-Linux primary work uses three independent standard-hosted jobs with single-worker inner bounds. Coverage runs alone, and the static scheduler runs alone so its result has no post-build consumer tail. After static gates finish, that job publishes its emitted `apps/*/lib`, `packages/*/*/lib`, and `vendor/*/lib` tree as a run-scoped artifact. The third job restores that exact tree, then starts lint, Node 24 runtime compatibility, build-backed snapshots, and all artifact consumers without repeating the build. Generated NodeNext consumer directories are excluded from ESLint discovery because the artifact check removes them while these processes overlap. The pnpm store and ESLint cache are restored without putting cache uploads on the pull-request critical path. Performance reports use each job's `startedAt` to `completedAt` interval; runner queue delay is capacity evidence, not repository execution time.
+Linux primary work uses three independent 32-core jobs. Coverage runs alone with its own worker bound, and the static scheduler runs alone so its result has no post-build consumer tail. After static gates finish, that job publishes its emitted `apps/*/lib`, `packages/*/*/lib`, and `vendor/*/lib` tree as a run-scoped artifact. The third job restores that exact tree, then starts lint, Node 24 runtime compatibility, build-backed snapshots, and all artifact consumers without repeating the build. Generated NodeNext consumer directories are excluded from ESLint discovery because the artifact check removes them while these processes overlap. The pnpm store and ESLint cache are restored without putting cache uploads on the pull-request critical path. Performance reports use each job's `startedAt` to `completedAt` interval; runner queue delay is capacity evidence, not repository execution time.
 
 The gate dependencies remain explicit. Coverage consumes source and does not wait for build. Documentation typechecking builds its complete project-reference graph once. Snapshot replay and publication consumers wait for emitted output, while Node-version compatibility jobs exercise runtime-sensitive source loading without repeating the primary source-graph typecheck. PTY and subprocess suites keep their bounded inner concurrency rather than inheriting the runner's core count.
 
 The artifact boundary remains explicit. `scripts/publint-all.ts` calls publint's supported API against an in-memory publication view formed from each manifest's declared files plus npm's mandatory metadata, avoiding one package-manager pack process per package. `scripts/verify-built-package-invariants.mjs` stages the declared `lib/` files below the real package and imports its compiled self-reference through plain Node and Cordis Loader normalization; a runtime chunk omitted from the publication contract still fails.
 
-Windows shares one standard-hosted setup across the blocking build and production site plus observational built-artifact contracts, with single-worker bounds. Linux owns the duplicate lint, coverage, and snapshot inventories because running those observational copies on Windows extends the critical path without adding a blocking platform claim.
+Windows shares one 32-core setup across the blocking build and production site plus observational built-artifact contracts. Linux owns the duplicate lint, coverage, and snapshot inventories because running those observational copies on Windows extends the paid critical path without adding a blocking platform claim.
 
 An [exact-head all-size benchmark](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29908491351) ran the complete unsharded primary Node aggregate on every Linux pool before the eager-build correction:
 
@@ -50,11 +50,11 @@ Inner and outer worker limits are separate controls. An [exact-head 32-worker ES
 
 The process-bound coverage project contains exactly five suite files. Thirty-two forks crashed Node 24's CJS lexer twice, and a later 16-fork run reproduced the worker loss and invalid coverage result. The single Vitest invocation therefore uses threads for the broad inventory and reserves forks for suites that exercise process-global state, `process` APIs, or timing-sensitive process I/O. That narrow fork inventory includes the local bash process-plumbing suite and the pi-ai adapter suite because aggregate contention changed timing observations in both. These failures make deterministic coverage, not advertised cores, the upper bound on worker selection.
 
-Complete serial Linux, macOS, and Windows references run only when `master` moves. Pull requests use the standard-hosted required path, while larger-runner sizes run only by manual dispatch.
+Complete serial Linux, macOS, and Windows references run only when `master` moves. Pull requests use the enterprise required path plus standard-hosted compatibility jobs, while other larger-runner sizes run only by manual dispatch.
 
 ## Alternatives considered
 
-**Restore the former core, CPU, and production-site lanes.** Those jobs met the latency targets, but they paid three setup waves and left primary Node work sharded after larger runners were available. The all-size trace showed that one unnecessary dependency, not a lack of host capacity, kept the single-box aggregate above one minute.
+**Keep the three coarse primary Linux lanes.** The core, CPU, and production-site jobs met the latency targets, but they paid three setup waves and left primary Node work sharded after larger runners were available. The all-size trace showed that one unnecessary dependency, not a lack of host capacity, kept the single-box aggregate above one minute.
 
 **Keep the former gate-level shard topology as a manual reference.** A dormant second topology kept hundreds of workflow lines, selector modules, and scenario-partition behavior alive. The all-size and serial suites provide timing and completeness controls without preserving production code that no required job exercises.
 
@@ -68,7 +68,7 @@ Complete serial Linux, macOS, and Windows references run only when `master` move
 
 **Keep static gates and post-build consumers on one runner.** Reusing one workspace avoids a setup wave and artifact transfer, but build-duration variance delays every consumer and leaves their lint and snapshot tails after the static result. A run-scoped built tree preserves one exact build while independent jobs keep both complete paths within the observed target.
 
-**Use larger-runner pools as the required default.** This offers lower measured latency when allocation works, but a missing entitlement or delayed enterprise transfer leaves required jobs queued without repository diagnostics. The portable path accepts longer runtime, and manual suites preserve the performance experiment.
+**Keep the complete required path on standard GitHub-hosted capacity.** This avoids repository-external runner configuration, but exact-head standard-runner runs remain materially slower and can spend longer queued behind shared capacity. Standard-hosted compatibility and serial references preserve portable evidence without making that slower topology the ordinary primary path.
 
 **Keep required and observational Windows checks in separate jobs.** The split preserves status semantics at the workflow level but pays setup twice. `run-gates` preserves the same required versus non-blocking distinction inside one process.
 
@@ -76,10 +76,10 @@ Complete serial Linux, macOS, and Windows references run only when `master` move
 
 ## Consequences
 
-The required topology pays one setup wave per standard-hosted lane and retains no shard selectors. The substantive CI inventory consumes enterprise larger-runner minutes only when a benchmark is dispatched; the lightweight aggregate's separate runner choice is outside this decision.
+The required topology pays one setup wave per 32-core lane and retains no shard selectors. Every ordinary pull request consumes paid enterprise Linux and Windows minutes; manual benchmarks add other sizes only when remeasurement is useful.
 
-Splitting Linux repeats setup twice and transfers one built tree, but isolates coverage, static gates, and post-build consumers from each other's critical paths without repeating the build. Consolidating Windows avoids repeating its slower setup. The trade-off is longer elapsed time than the measured larger-runner topology.
+GitHub rounds each larger-runner execution up to a whole minute, so complete-job measurement exposes both billed time and workflow complexity. Splitting Linux repeats setup twice and transfers one built tree, but isolates coverage, static gates, and post-build consumers from each other's critical paths without repeating the build; consolidating Windows avoids repeating its slower setup.
 
 Performance targets are observations, not cancellation deadlines or correctness requirements. Manual all-size and serial suites remain available when image, dependency, scheduler, or pricing changes need remeasurement.
 
-Missing or renamed enterprise labels leave manual benchmarks unavailable without queueing a substantive primary job. The retained pools can compare sizes after allocation recovers without making runner assignment an operational dependency of repository gate execution.
+Missing or renamed enterprise labels leave required primary jobs queued. Standard-hosted compatibility jobs and `master` references still report useful evidence, but they do not substitute for the required aggregate; runner assignment is therefore an operational dependency that repository CI cannot repair.
diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md
index 870c4301ee..4787928453 100644
--- a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md
+++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md
@@ -12,19 +12,19 @@ Status: implemented
 
 ## 决策
 
-企业保留仅限本仓库使用的 Ubuntu 和 Windows x64 大型运行器池,作为测量基础设施。公网 IP 已禁用;基准测试并发仍设有边界,因为自动扩缩容上限既不会分配闲置机器,也不意味着仓库工作可以无限扩展。
+企业保留仅限本仓库使用的 Ubuntu 和 Windows x64 大型运行器池。普通拉取请求直接指定 3 个 32 核运行器池:Ubuntu 24.04 用于完整覆盖率,Ubuntu latest 用于其余主 Node 24 清单,Windows 2025 用于阻塞性 Windows 契约。公网 IP 已禁用;工作流并发仍设有边界,因为自动扩缩容上限既不会分配闲置机器,也不意味着仓库工作可以无限扩展。
 
-普通拉取请求使用由[可移植恢复边界](2026-07-23-portable-required-pull-request-ci.md)规定的标准托管主路径。`suite=larger-runner-benchmark` 比较已预配规格上相互独立的关键通道,`suite=consolidated-runner-benchmark` 则比较完整聚合流程。每项基准测试都会先报告实测的处理器和内存容量,再运行仓库工作。[串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)在 `master` 上保留一套独立、完整的标准运行器判定基准。
+必需主路径依赖这些企业级运行器池。GitHub 标准托管作业保留 Node 22.19、Node 26 和 Python SDK 兼容性契约,而[可移植恢复边界](2026-07-23-portable-required-pull-request-ci.md)与[串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)则在 `master` 上持续提供完整的标准运行器证据。`suite=larger-runner-benchmark` 比较已预配规格上相互独立的关键通道,`suite=consolidated-runner-benchmark` 则比较完整聚合流程。每项基准测试都会先报告实测的处理器和内存容量,再运行仓库工作。
 
 原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除,因此未使用的诊断路径无法继续维系第二套 CI 架构。
 
-Linux 主流程使用 3 项相互独立的标准托管作业,内部均采用单工作线程上限。覆盖率单独运行;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib`、`packages/*/*/lib` 和 `vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 ESLint 的文件发现范围,因为这些进程重叠执行时,产物检查会删除这些目录。pnpm store 和 ESLint 缓存会得到恢复,但缓存上传不会进入拉取请求关键路径。性能报告采用每个作业从 `startedAt` 到 `completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
+Linux 主流程使用 3 个相互独立的 32 核作业。覆盖率单独运行,并设有自己的工作线程上限;静态调度器也单独运行,因此构建后的消费方不会拖延其结果。静态门禁完成后,该作业将其生成的 `apps/*/lib`、`packages/*/*/lib` 和 `vendor/*/lib` 目录树作为仅供本次运行使用的产物发布。第三个作业恢复完全相同的目录树,再让 lint、Node 24 运行时兼容性、依赖构建产物的快照和所有产物消费方基于构建完成后的工作树启动,而不重复构建。生成的 NodeNext 消费方目录不会纳入 ESLint 的文件发现范围,因为这些进程重叠执行时,产物检查会删除这些目录。pnpm store 和 ESLint 缓存会得到恢复,但缓存上传不会进入拉取请求关键路径。性能报告采用每个作业从 `startedAt` 到 `completedAt` 的区间;运行器排队延迟是容量证据,而非仓库执行时间。
 
 门禁依赖关系保持显式。覆盖率消费源码,不等待构建。文档类型检查只构建一次完整的 project-reference 图。快照回放和发布消费方等待生成的输出,而 Node 版本兼容性作业会验证对运行时敏感的源码加载,且不重复主源码项目图的类型检查。PTY 和子进程套件继续使用自身有界的内部并发,不继承运行器的核心数。
 
 产物边界保持显式。`scripts/publint-all.ts` 对内存中的发布视图调用 publint 支持的 API;该视图由每个 manifest(元数据清单)声明的文件和 npm 强制要求的元数据组成,从而避免为每个包启动一次包管理器 pack 进程。`scripts/verify-built-package-invariants.mjs` 将已声明的 `lib/` 文件暂存到真实包下,并通过普通 Node 和 Cordis Loader 规范化导入其已编译的自身引用;发布契约只要遗漏一个运行时分片,检查仍会失败。
 
-Windows 以一次标准托管环境设置同时承载阻塞性构建、生产网站和观测性的构建产物契约,并采用单工作线程上限。重复的 lint、覆盖率和快照清单由 Linux 承担,因为在 Windows 上运行这些观测性副本会延长关键路径,却不会新增任何阻塞性平台契约。
+Windows 以一次 32 核环境设置同时承载阻塞性构建、生产网站和观测性的构建产物契约。重复的 lint、覆盖率和快照清单由 Linux 承担,因为在 Windows 上运行这些观测性副本会延长付费关键路径,却不会新增任何阻塞性平台契约。
 
 一次[分支头精确的全规格基准测试](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29908491351)在修正构建尽早启动逻辑前,对每种 Linux 池都运行了完整且未分片的主 Node 聚合流程:
 
@@ -50,11 +50,11 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
 
 进程约束的覆盖率项目恰好包含 5 个套件文件。32 个 fork 曾两次导致 Node 24 的 CJS 词法分析器崩溃,后来一次使用 16 个 fork 的运行又复现了工作进程丢失和无效的覆盖率结果。因此,单次 Vitest 调用会对大范围测试清单使用线程,只为涉及进程全局状态、`process` API 或对时间敏感的进程 I/O 的套件保留 fork。这份有限的 fork 清单包括本地 bash 进程通路套件和 pi-ai 适配器套件,因为聚合争用改变了二者的时序观测结果。这些故障表明,选择工作线程数量时,上限取决于能否得到确定的覆盖率结果,而非标称核心数。
 
-只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考。拉取请求使用标准托管的必需路径,大型运行器规格仅通过手动触发运行。
+只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考。拉取请求使用企业级运行器必需路径和标准托管兼容性作业,其他大型运行器规格仅通过手动触发运行。
 
 ## 曾考虑的替代方案
 
-**恢复原有的核心、CPU 和生产网站通道。** 这些作业均达到延迟目标,但它们需要 3 轮设置,而且在大型运行器已经可用后仍对主 Node 工作进行分片。全规格运行轨迹表明,让单机聚合流程超过 1 分钟的是一项不必要的依赖,而非主机容量不足。
+**保留 3 个粗粒度 Linux 主流程通道。** 核心、CPU 和生产网站作业均达到延迟目标,但它们需要 3 轮设置,而且在大型运行器已经可用后仍对主 Node 工作进行分片。全规格运行轨迹表明,让单机聚合流程超过 1 分钟的是一项不必要的依赖,而非主机容量不足。
 
 **将原有的门禁级分片拓扑保留为手动参考。** 一套闲置的第二拓扑会让数百行工作流、选择器模块和场景分区行为继续存活。全规格和串行套件无需保留任何必需作业都不执行的生产代码,也能提供计时与完整性对照。
 
@@ -68,7 +68,7 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
 
 **将静态门禁和构建后消费方保留在同一台运行器上。** 复用同一个工作区可以省去一轮设置和一次产物传输,但构建耗时的波动会延迟每个消费方,并使消费方的 lint 和快照尾段延续到静态结果之后。仅供本次运行使用的已构建目录树可以保留同一份构建结果,而相互独立的作业能让两条完整路径都保持在实测目标内。
 
-**将大型运行器池作为必需作业的默认运行环境。** 当运行器能够分配时,此方案可提供实测更低的延迟;但若未获得相应使用权限或企业转移延迟,必需作业会持续排队,且不会发出仓库诊断。可移植路径接受更长的运行时间,手动套件则保留性能实验。
+**将完整必需路径保留在 GitHub 标准托管容量上。** 此方案可以避免依赖仓库外部的运行器配置,但标准运行器上的分支头精确运行仍明显更慢,也可能因共享容量而排队更久。标准托管兼容性作业和串行参考流程保留可移植证据,无需让这套较慢的拓扑成为普通主路径。
 
 **将必需的 Windows 检查和观测性 Windows 检查保留在不同作业中。** 这种拆分在工作流层保留状态语义,却需要支付两次设置开销。`run-gates` 在一个进程内保留了相同的必需与非阻塞区别。
 
@@ -76,10 +76,10 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
 
 ## 后果
 
-必需拓扑中的每个标准托管通道只承担 1 轮设置开销,且不保留分片选择器。只有在触发基准测试时,实质性 CI 清单才会消耗企业级大型运行器分钟数;轻量级聚合流程单独选择运行器,不属于本决策范围。
+必需拓扑中的每个 32 核通道只承担 1 轮设置开销,且不保留分片选择器。每个普通拉取请求都会消耗付费的企业级 Linux 和 Windows 运行器分钟数;只有在重新测量有价值时,手动基准测试才会加入其他规格。
 
-拆分 Linux 会重复两轮设置并传输一份已构建目录树,但能使覆盖率、静态门禁与构建后消费方不再相互进入关键路径,且无需重复构建。合并 Windows 可避免重复其耗时更长的设置。代价是总耗时长于经测量的大型运行器拓扑。
+GitHub 会把每次大型运行器执行向上取整到整分钟计费,因此完整作业测量能同时呈现计费时长与工作流复杂度。拆分 Linux 会重复两轮设置并传输一份已构建目录树,但能使覆盖率、静态门禁与构建后消费方不再相互进入关键路径,且无需重复构建;合并 Windows 则避免重复其耗时更长的设置。
 
 性能目标是观测结果,而非取消截止时间或正确性要求。当映像、依赖、调度器或定价发生变化而需要重新测量时,仍可使用手动全规格和串行套件。
 
-企业级运行器标签缺失或改名时,手动基准测试会不可用,但不会让实质性主作业排队。运行器分配能力恢复后,保留的运行器池仍可比较不同规格,同时不会让运行器分配成为仓库门禁执行的运维依赖。
+企业级运行器标签缺失或改名时,必需主作业会持续排队。标准托管兼容性作业与 `master` 参考流程仍会报告有用证据,但不能替代必需聚合流程;因此,运行器分配是一项仓库 CI 无法修复的运维依赖。
diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml
index 34ea2f4a90..05147cd54a 100644
--- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.i18n.yaml
@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
-2026-07-23-portable-required-pull-request-ci.md: 4fd915a00300922d7318c78d16e1e8078b5170ed
-2026-07-23-portable-required-pull-request-ci.zh.md: 9d489970a46279de8033cb82af64489d86b20d2c
+#   pnpm run verify-translation-pairing --write
+2026-07-23-portable-required-pull-request-ci.md: d1002c7d9db7cd8bbed3bdfda8a773a4b124bf16
+2026-07-23-portable-required-pull-request-ci.zh.md: fedfc6b9c982ace5ece430c52db23c22ec5119d4
diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
index 4fd915a003..d1002c7d9d 100644
--- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
+++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.md
@@ -12,24 +12,24 @@ Billing health, a runner definition's `Ready` state, and a large autoscaling cei
 
 ## Decision
 
-[CI](../../../../.github/workflows/ci.yml) runs the three required primary Node 24 jobs on standard `ubuntu-latest` and the complete required Windows job on standard `windows-2025`. Static gates publish their exact built tree for the snapshot and artifact job, while coverage remains independent. Top-level gates, coverage, ESLint, publint, and snapshot replay use single-worker bounds on these smaller hosts. Node 22.19, Node 26, and Python SDK compatibility also use standard capacity. The lightweight `all checks passed` aggregate remains a separate scheduling decision because it performs no checkout or repository gate.
+[CI](../../../../.github/workflows/ci.yml) runs the required primary Node 24 jobs, plus the stable `all checks passed` aggregate, on repo-restricted enterprise 32-core pools. The aggregate performs no checkout or repository gate, but sharing the enterprise pool prevents the required verdict from introducing a separate standard-hosted billing dependency after its substantive jobs have already succeeded. The required Windows job runs on standard `windows-2025` with single-worker bounds, keeping the complete Windows contract independent of enterprise Windows allocation. Standard `ubuntu-latest` jobs retain Node 22.19, Node 26, and Python SDK compatibility, and `master` runs complete serial Linux, macOS, and Windows references. Those standard-hosted jobs keep the portable execution boundary observable without duplicating the primary inventory on every pull request.
 
-The three Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / complete` remain dependencies of `all checks passed`; no gate is removed or made observational to recover availability. Branch protection continues to require `e2e` and `all checks passed`.
+The two Linux primary jobs, Node compatibility, Python SDK, and `windows node 24 / complete` remain dependencies of `all checks passed`; branch protection continues to require `e2e` and `all checks passed`. There is no automatic fallback when a remaining enterprise Linux label cannot allocate: the standard jobs continue to report their own contracts, but they cannot manufacture the missing required result.
 
-The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the retained performance measurements and manual suites. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check.
+The [larger-runner decision](2026-07-22-evidence-based-larger-hosted-runners.md) owns the current primary topology and its measurements. The [serial cross-platform reference](2026-07-21-serial-cross-platform-ci-reference.md) remains the independent standard-hosted completeness check, and the manual larger-runner suites retain size comparisons without expanding the ordinary required matrix.
 
 ## Alternatives considered
 
-**Wait for enterprise allocation to recover.** A queue with no assigned runner emits no repository diagnostic and can block every pull request indefinitely, so external recovery is not a correctness path.
+**Keep the Linux primary jobs and aggregate on standard capacity.** This removes the remaining enterprise allocation dependency, but complete standard-runner jobs give materially slower feedback and still experience shared-capacity queues. The current split retains portable compatibility and serial evidence while spending enterprise capacity on the Linux primary critical path.
 
-**Use only the smallest enterprise pools.** Every named pool crosses the same enterprise allocation boundary; reducing core count does not remove the dependency that caused the queue.
+**Select enterprise size from advertised core count.** Benchmarks show non-monotonic scaling and setup variance, so exact complete-job measurements choose the required pools instead.
 
 **Skip or demote checks while capacity is unavailable.** This would make the status green by dropping evidence rather than by running the repository's required contracts.
 
-**Keep larger-runner worker limits on standard runners.** Concurrent repository gates and their inner worker pools can oversubscribe the smaller memory and CPU allocation, turning an availability repair into contention failures.
+**Use one worker policy on every host.** Outer gate concurrency and inner tool workers contend differently on Linux, Windows, and standard runners; measured host-specific bounds avoid turning additional cores into slower execution.
 
 ## Consequences
 
-Ordinary pull requests can acquire every substantive runner without enterprise-specific configuration. A live exact-head run proves the same commands that branch protection consumes, at the cost of longer elapsed time on smaller hosts.
+Ordinary pull requests spend enterprise capacity on the Linux critical path while standard Windows trades longer runtime for independent allocation. A live exact-head run proves the same commands that branch protection consumes; queue delay is reported separately from each job's `startedAt` to `completedAt` execution interval.
 
-Manual larger-runner benchmarks can remain queued without blocking pull requests. Restoring larger runners to the required path needs a separate evidence-based decision after exact-head jobs receive nonzero runner IDs and complete reliably; changing a pool definition's status alone is insufficient.
+Standard compatibility and required Windows jobs remain useful when enterprise allocation is degraded, but they do not make a blocked required Linux job or aggregate green. Recovering Linux availability may require restoring the complete standard-hosted topology; changing a pool definition's status alone is insufficient evidence that it can receive work.
diff --git a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md
index 9d489970a4..fedfc6b9c9 100644
--- a/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md
+++ b/.agents/notes/implemented/process/2026-07-23-portable-required-pull-request-ci.zh.md
@@ -12,24 +12,24 @@ Status: implemented
 
 ## 决策
 
-[CI](../../../../.github/workflows/ci.yml) 在标准 `ubuntu-latest` 上运行 3 项必需的主 Node 24 作业,并在标准 `windows-2025` 上运行完整的必需 Windows 作业。静态门禁发布其完全一致的已构建目录树,供快照与产物作业使用;覆盖率作业则保持独立。这些较小主机上的顶层门禁、覆盖率、ESLint、publint 和快照回放均采用单工作线程上限。Node 22.19、Node 26 和 Python SDK 兼容性也使用标准容量。轻量级 `all checks passed` 聚合流程仍由单独的调度决策管理,因为它不执行代码检出或仓库门禁。
+[CI](../../../../.github/workflows/ci.yml) 在仅限本仓库使用的企业级 32 核运行器池上运行必需的主 Node 24 作业,以及稳定的 `all checks passed` 聚合流程。该聚合流程不执行代码检出或仓库门禁;但让它与所依赖的实质性作业共用企业级运行器池,可以避免这些作业已经成功后,必需判定结果又引入一项单独的标准托管计费依赖。必需的 Windows 作业在标准 `windows-2025` 上运行,并采用单工作线程上限,使完整的 Windows 契约不依赖企业级 Windows 运行器分配。标准 `ubuntu-latest` 作业保留 Node 22.19、Node 26 和 Python SDK 兼容性,`master` 则运行完整的 Linux、macOS 和 Windows 串行参考流程。这些标准托管作业让可移植执行边界保持可观测,而不必在每个拉取请求中重复主清单。
 
-3 项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / complete` 继续作为 `all checks passed` 的依赖项;为恢复可用性,没有移除任何门禁,也没有将任何门禁改为仅供观测。分支保护继续要求 `e2e` 和 `all checks passed`。
+两项 Linux 主作业、Node 兼容性、Python SDK 和 `windows node 24 / complete` 继续作为 `all checks passed` 的依赖项;分支保护继续要求 `e2e` 和 `all checks passed`。剩余的企业级 Linux 运行器标签无法分配运行器时没有自动后备机制:标准作业会继续报告各自的契约,但无法产出缺失的必需结果。
 
-保留的性能测量结果与手动套件由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查。
+当前主拓扑及其测量结果由[大型运行器决策](2026-07-22-evidence-based-larger-hosted-runners.md)记录。[跨平台串行参考流程](2026-07-21-serial-cross-platform-ci-reference.md)继续作为独立的标准托管完整性检查,手动大型运行器套件则保留规格比较,同时不扩大普通必需矩阵。
 
 ## 曾考虑的替代方案
 
-**等待企业级运行器分配恢复。** 未分配运行器的队列不会发出任何仓库诊断,并且可能无限期阻塞所有拉取请求,因此外部恢复不能作为正确性路径。
+**将 Linux 主作业和聚合流程保留在标准容量上。** 此方案消除了剩余的企业级运行器分配依赖,但标准运行器上的完整作业反馈明显更慢,仍会遇到共享容量排队。当前拆分既保留可移植兼容性和串行证据,又将企业级运行器容量用于 Linux 主关键路径。
 
-**仅使用最小的企业级运行器池。** 无论指定哪个运行器池,都要经过同一个企业级分配边界;减少核心数并不能消除导致排队的依赖。
+**根据标称核心数选择企业规格。** 基准测试表明扩展效果不呈单调变化,设置耗时也存在波动,因此必需运行器池改由完整作业的精确测量结果选定。
 
 **在容量不可用时跳过检查或降低其级别。** 这种方式通过丢弃证据而非执行仓库的必需契约来使状态变绿。
 
-**在标准运行器上沿用大型运行器的工作线程上限。** 并发运行的仓库门禁及其内部工作线程池,可能让并发需求超过较小的内存和 CPU 配额,使可用性修复反而引发资源争用故障。
+**在每台主机上使用同一工作线程策略。** 外层门禁并发与内层工具工作线程在 Linux、Windows 和标准运行器上的争用方式不同;按主机实测的上限可以避免新增核心反而拖慢执行。
 
 ## 后果
 
-普通拉取请求无需企业专用配置,即可为每项实质性作业获得运行器。一次实际的分支头精确运行能够证明分支保护使用的同一组命令,代价是在较小主机上耗时更长。
+普通拉取请求会将企业级运行器容量用于 Linux 关键路径,而标准托管 Windows 作业则以更长的运行时间换取不依赖企业池的运行器分配。一次实际的分支头精确运行能够证明分支保护使用的同一组命令;排队延迟与每个作业从 `startedAt` 到 `completedAt` 的执行区间分开报告。
 
-手动大型运行器基准测试即使持续排队,也不会阻塞拉取请求。只有在分支头精确作业获得非零运行器 ID 并稳定完成后,才能另行作出基于证据的决策,将大型运行器恢复到必需路径;仅改变运行器池定义的状态仍然不够。
+企业级运行器分配能力下降时,标准兼容性作业和必需的 Windows 作业仍能提供有用证据,但无法让受阻的必需 Linux 作业或聚合流程变绿。恢复 Linux 可用性时,可能需要恢复完整的标准托管拓扑;仅改变运行器池定义的状态,不足以证明它可以接收作业。
diff --git a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml
deleted file mode 100644
index a029a82389..0000000000
--- a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.i18n.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
-2026-07-27-portable-required-status-aggregation.md: 081d841cfb939c97f189c46fc0985f9ff2d1987d
-2026-07-27-portable-required-status-aggregation.zh.md: 896e9500d2ad6e2e0ef6c6cc7a37373c6d28b303
diff --git a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
deleted file mode 100644
index 081d841cfb..0000000000
--- a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Agent Note: Portable required-status aggregation
-
-Status: implemented
-
-English | [中文](2026-07-27-portable-required-status-aggregation.zh.md)
-
-## Problem
-
-Branch protection consumes one stable `all checks passed` job instead of tracking the changing names of matrix legs and execution lanes. This job performs no repository work: after its blocking dependencies finish, it only reduces their results into the required verdict.
-
-Assigning that bookkeeping job to a custom runner pool adds an external allocation dependency without using the pool's additional CPU or memory. A provisioning failure can therefore leave the final required status queued even after every substantive check has produced its evidence.
-
-## Decision
-
-The `all-checks-passed` job in [CI](../../../../.github/workflows/ci.yml) runs on standard GitHub-hosted `ubuntu-latest`. It keeps every blocking job in `needs`, retains its load-bearing `if: always()` condition, fails when any dependency is failed, cancelled, or skipped, and succeeds only when every dependency succeeds. It performs no checkout, toolchain setup, dependency installation, or repository gate.
-
-The aggregate depends only on production standard-hosted capacity; it does not use organization-defined, enterprise-defined, or self-hosted labels. Substantive jobs choose their own runner topology independently. Moving this verdict does not change their commands, weaken their evidence, or make an unresolved dependency pass: the aggregate waits for unfinished dependencies and fails on non-success terminal results.
-
-This decision supersedes only the aggregate-placement clause in the [portable pull-request CI recovery boundary](2026-07-23-portable-required-pull-request-ci.md), which continues to own the substantive jobs' recovery topology. The final bookkeeping status remains separately owned so runner-topology changes and branch-protection aggregation can evolve independently.
-
-## Alternatives considered
-
-**Run the aggregate beside substantive jobs on a custom enterprise pool.** This avoids one short standard-hosted allocation, but gives the bookkeeping job a provisioning failure mode without using the larger machine's capacity.
-
-**Use a standby self-hosted runner.** This replaces one external readiness dependency with another and makes a required verdict depend on a separately operated machine. Managed standard-hosted capacity is the production path for this bookkeeping work.
-
-**Require every substantive job directly in branch protection.** This removes the aggregate allocation, but couples repository settings to matrix and lane names that change as the CI topology evolves.
-
-**Treat missing or non-success dependencies as success.** This would produce a green status by discarding required evidence rather than by completing it.
-
-## Consequences
-
-Each pull request allocates one short standard-hosted job after its substantive dependencies settle. Because the job performs no checkout or setup, it adds little active runtime, but its scheduling and billing remain separate from custom pools.
-
-A custom-pool outage can still keep a substantive dependency queued, and the aggregate correctly waits in that case. Once the dependencies reach terminal results, the final required verdict no longer needs custom-pool or self-hosted allocation. Future changes can move substantive jobs between standard and larger runners without reintroducing that dependency into the branch-protection status.
diff --git a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md b/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md
deleted file mode 100644
index 896e9500d2..0000000000
--- a/.agents/notes/implemented/process/2026-07-27-portable-required-status-aggregation.zh.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Agent Note: 必需状态的可移植聚合
-
-Status: implemented
-
-[English](2026-07-27-portable-required-status-aggregation.md) | 中文
-
-## 问题
-
-分支保护只使用一项稳定的 `all checks passed` 作业,无需跟踪持续变化的矩阵分支名和执行通道名。该作业不执行任何仓库工作:会阻塞判定的依赖项结束后,它只将这些依赖项的结果归并为必需判定。
-
-将这项结果汇总作业分配给自定义运行器池,会在不使用该池额外 CPU 或内存的情况下增加一项外部运行器分配依赖。因此,即使所有实质性检查都已产出证据,预配失败仍可能让最终的必需状态持续排队。
-
-## 决策
-
-[CI](../../../../.github/workflows/ci.yml) 中的 `all-checks-passed` 作业在 GitHub 标准托管的 `ubuntu-latest` 上运行。它在 `needs` 中保留所有会阻塞判定的作业,保留承重的 `if: always()` 条件;任何依赖项失败、被取消或被跳过时,该作业都会失败,只有所有依赖项都成功时才会成功。它不执行代码检出、工具链设置、依赖安装或仓库门禁。
-
-聚合作业只依赖生产环境的标准托管容量;它不使用组织定义的、企业定义的或自托管的运行器标签。实质性作业各自独立选择运行器拓扑。调整这项判定作业的运行位置,不会改变实质性作业的命令、削弱其证据或使未完成的依赖项通过:聚合作业会等待尚未结束的依赖项,并在依赖项产生非成功的终态结果时失败。
-
-本决策仅取代[拉取请求 CI 的可移植恢复边界](2026-07-23-portable-required-pull-request-ci.md)中关于聚合作业运行位置的条款;该记录继续规定实质性作业的恢复拓扑。最终的结果汇总状态仍由本决策单独规定,使运行器拓扑变更与分支保护聚合可以独立演进。
-
-## 曾考虑的替代方案
-
-**让聚合作业与实质性作业一同在自定义企业级运行器池上运行。** 此方案可以避免一次短暂的标准托管运行器分配,但会在未使用大型机器容量的情况下,为结果汇总作业引入预配失败的故障模式。
-
-**使用备用自托管运行器。** 此方案只是用另一项外就绪状态依赖替换原有依赖,并使必需判定依赖一台单独运维的机器。由平台管理的标准托管容量是这项结果汇总工作的生产路径。
-
-**在分支保护中直接要求每项实质性作业。** 此方案不再需要为聚合作业分配运行器,但会将仓库设置与随 CI 拓扑演进而变化的矩阵分支名和通道名耦合。
-
-**将缺失或非成功的依赖项视为成功。** 这种做法不是通过完成相应检查来产出必需证据,而是丢弃这些证据以产出绿色状态。
-
-## 后果
-
-每个拉取请求都会在实质性依赖项的结果确定后分配一项短时运行的标准托管作业。由于该作业不执行代码检出或设置,它只增加少量活跃运行时间,但其调度和计费仍独立于自定义运行器池。
-
-自定义运行器池不可用仍可能让实质性依赖项持续排队,聚合作业在这种情况下会按设计等待。依赖项产生终态结果后,最终的必需判定不再需要自定义运行器池或自托管运行器分配。未来可以在标准运行器与大型运行器之间迁移实质性作业,而不会将这项依赖重新引入分支保护状态。
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b3292aeeeb..f02d30a563 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,15 +27,15 @@ env:
 
 jobs:
 
-  # Three standard Linux jobs isolate coverage, static analysis, and the
+  # Three enterprise jobs isolate coverage, static analysis, and the
   # build-backed consumer tail. The static job publishes its exact build so
   # consumers do not repeat the longest part of their critical path.
   node-24:
     if: github.event_name == 'pull_request'
-    runs-on: ubuntu-latest
+    runs-on: dsh-enterprise-ubuntu-latest-32core-test
     name: node 24 / static
     env:
-      DSH_GATE_CONCURRENCY: '1'
+      DSH_GATE_CONCURRENCY: '8'
     steps:
       # Fetch complete history so the archive gate can read the trusted PR base from a reused shallow checkout.
       - uses: actions/checkout@v6
@@ -81,11 +81,11 @@ jobs:
 
   node-24-coverage:
     if: github.event_name == 'pull_request'
-    runs-on: ubuntu-latest
+    runs-on: dsh-enterprise-ubuntu-24-04-32core-test
     name: node 24 / coverage
     env:
-      DSH_COVERAGE_MAX_WORKERS: '1'
-      DSH_GATE_CONCURRENCY: '1'
+      DSH_COVERAGE_MAX_WORKERS: '24'
+      DSH_GATE_CONCURRENCY: '8'
     steps:
       - uses: actions/checkout@v6
         with:
@@ -122,15 +122,15 @@ jobs:
   node-24-consumers:
     needs: node-24
     if: github.event_name == 'pull_request'
-    runs-on: ubuntu-latest
+    runs-on: dsh-enterprise-ubuntu-latest-32core-test
     name: node 24 / snapshots and artifacts
     env:
       DSH_ESLINT_CACHE: '1'
-      DSH_ESLINT_CONCURRENCY: '1'
-      DSH_GATE_CONCURRENCY: '1'
+      DSH_ESLINT_CONCURRENCY: '8'
+      DSH_GATE_CONCURRENCY: '8'
       DSH_NODE_COMPAT_SKIP_TYPECHECK: '1'
-      DSH_PUBLINT_CONCURRENCY: '1'
-      DSH_SNAPSHOT_MAX_CONCURRENCY: '1'
+      DSH_PUBLINT_CONCURRENCY: '8'
+      DSH_SNAPSHOT_MAX_CONCURRENCY: '32'
     steps:
       - uses: actions/checkout@v6
         with:
@@ -693,8 +693,8 @@ jobs:
   # 'cancelled' and 'skipped'.
   all-checks-passed:
     name: all checks passed
-    # This bookkeeping-only verdict must not depend on custom-pool provisioning.
-    runs-on: ubuntu-latest
+    # The required verdict must not add a separate standard-hosted billing dependency.
+    runs-on: dsh-enterprise-ubuntu-latest-32core-test
     needs: [node-24, node-24-coverage, node-24-consumers, node-compat, python-sdk, windows]
     if: always() && github.event_name == 'pull_request'
     steps:

From ce3b13bb0816d3a23b68b916087b3beef29fcc83 Mon Sep 17 00:00:00 2001
From: Chinesezjc 
Date: Mon, 27 Jul 2026 16:13:30 +0800
Subject: [PATCH 09/10] =?UTF-8?q?ci:=20standby=20fetches=20full=20history;?=
 =?UTF-8?q?=20runbook=20=E2=80=94=20writer=20wording=20throughout,=20maste?=
 =?UTF-8?q?r-ref=20pinning=20incompatibility?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- serial-linux-selfhosted checks out fetch-depth 0: depth 2 misses
  github.event.before on multi-commit or force pushes, failing the
  archive verifier on a valid tree. Full fetch is cheap against the
  VM's local mirror.
- Runbook (both languages): every remaining admin phrasing (problem
  statement, switch heading, alternatives, consequences) now says
  writer; and the 'composes with this mechanism' claim about a
  master-ref-pinned runner group is replaced with the truth observed
  live on 2026-07-27 — master-ref pinning blocks PR failover, and the
  shipped posture is repository-scoped all-workflow group access.
Static gate green locally: 32 passed, 0 failed.
---
 .../process/2026-07-26-ci-failover-runbook.i18n.yaml   |  4 ++--
 .../process/2026-07-26-ci-failover-runbook.md          | 10 +++++-----
 .../process/2026-07-26-ci-failover-runbook.zh.md       | 10 +++++-----
 .github/workflows/ci.yml                               |  9 +++++----
 4 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
index de9ba10469..658a85ce34 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
-2026-07-26-ci-failover-runbook.md: 05014454fa3e38045b89a857c346db0f897ab5a6
-2026-07-26-ci-failover-runbook.zh.md: e106a0de40799ca1c218217ea66c24068697dc53
+2026-07-26-ci-failover-runbook.md: ca4349661d03ff4e28d7c3c2b6e910106ff4aa30
+2026-07-26-ci-failover-runbook.zh.md: 1d59bd537879f531c9075e833c9e1dbfbd4bb0a2
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
index 05014454fa..ca4349661d 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
@@ -6,7 +6,7 @@ English | [中文](2026-07-26-ci-failover-runbook.zh.md)
 
 ## Problem
 
-The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) and the required verdict job that aggregates them (`all checks passed`) run on the hosted enterprise 32-core pools. When those pools degrade — jobs queue indefinitely, the enterprise labels vanish, or GitHub-side capacity fails — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. An outage therefore needs a switch a repository admin can throw without merging anything.
+The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) and the required verdict job that aggregates them (`all checks passed`) run on the hosted enterprise 32-core pools. When those pools degrade — jobs queue indefinitely, the enterprise labels vanish, or GitHub-side capacity fails — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. An outage therefore needs a switch any responder with repository write access can throw without merging anything.
 
 ## Decision
 
@@ -16,7 +16,7 @@ Each of the three required Linux worker jobs — and the `all checks passed` ver
 
 `vm-backup`: one 64-core VM, six always-on systemd-managed runner instances. Check the latest `serial / linux (self-hosted standby)` run before switching: a green standby is verified-yesterday capacity.
 
-### Switch (repo admin, ~1 minute, no merge)
+### Switch (any repository writer, ~1 minute, no merge)
 
 1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER`, value `selfhosted`.
 2. Retrigger the required jobs so they re-resolve their pool. Jobs already **queued** for the hosted labels do not retarget and cannot be re-run in place, so for the documented indefinite-queue outage, cancel the stuck run and re-run all jobs, or push a new commit; "Re-run failed jobs" only helps once a job has actually failed rather than queued.
@@ -37,14 +37,14 @@ Delete the `DSH_CI_FAILOVER` variable (or set it to anything other than `selfhos
 
 ### Trust boundary
 
-The variable is writer-manageable repository state; a pull request event itself can neither set it nor read a different value into effect, and the selector expressions live in workflow definitions. Note that under failover, `pull_request` runs execute the PR merge ref's own workflow definition — the boundary against untrusted code is repository membership (private, forking disabled, Dependabot excluded by the selectors), not the variable. Runner-side enforcement — an org-level runner group restricting these runners to the master-ref workflow — is tracked separately and composes with this mechanism.
+The variable is writer-manageable repository state; a pull request event itself can neither set it nor read a different value into effect, and the selector expressions live in workflow definitions. Note that under failover, `pull_request` runs execute the PR merge ref's own workflow definition — the boundary against untrusted code is repository membership (private, forking disabled, Dependabot excluded by the selectors), not the variable. Note on runner-group policy: pinning the runner group to the master-ref workflow is **incompatible** with this failover — the four failover jobs are `pull_request` runs evaluated from PR merge refs, and a master-pinned group leaves them queued (observed live on 2026-07-27; the group was widened to all workflows of this repository to unblock the switch). A stricter runner-side policy therefore costs PR failover; the shipped posture accepts repository-scoped, all-workflow group access.
 
 ## Alternatives considered
 
-**Merge a workflow change to switch pools.** Rejected because the outage that motivates the switch is exactly the state in which no PR can merge: the required checks are the ones failing. A repository variable is admin-controlled state that takes effect on re-run without a merge.
+**Merge a workflow change to switch pools.** Rejected because the outage that motivates the switch is exactly the state in which no PR can merge: the required checks are the ones failing. A repository variable is writer-manageable state that takes effect on re-run without a merge.
 
 **Keep the self-hosted pool always in the required path.** Rejected because it trades hosted-pool availability for the in-house VM's, moving a single point of failure rather than adding a fallback. The variable keeps the hosted pools primary and the self-hosted pool a proven, one-action standby.
 
 ## Consequences
 
-Recovering from a hosted-pool outage is a single admin variable plus a re-run, with no merge on the critical path. The cost is a second runner topology to keep working: the standby lane exercises it on every master push so the failover target never goes stale, and the concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg that must stay in step with the hosted leg.
+Recovering from a hosted-pool outage is a single variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology to keep working: the standby lane exercises it on every master push so the failover target never goes stale, and the concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg that must stay in step with the hosted leg.
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
index e106a0de40..1d59bd5378 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)以及聚合它们的必需判定作业(`all checks passed`)运行在托管的企业级 32 核池上。当这些托管池发生故障——作业无限排队、企业标签消失或 GitHub 侧容量故障——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。因此故障需要一个仓库管理员无需合并任何代码即可触发的开关。
+[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)以及聚合它们的必需判定作业(`all checks passed`)运行在托管的企业级 32 核池上。当这些托管池发生故障——作业无限排队、企业标签消失或 GitHub 侧容量故障——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。
 
 ## 决策
 
@@ -16,7 +16,7 @@ Status: implemented
 
 `vm-backup`:一台 64 核虚拟机,6 个常驻 systemd 管理的运行器实例。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:绿色 = 这套环境昨天刚被全量验证过。
 
-### 切换步骤(仓库管理员,约 1 分钟,无需合并)
+### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并)
 
 1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER`,值 `selfhosted`。
 2. 重新触发必需作业,使其重新解析运行器池。已经为托管标签**排队**的作业不会重定向,也无法原地 re-run,因此对于本手册所述的无限排队故障,应取消卡住的运行并 re-run all jobs,或推送一个新提交;“Re-run failed jobs”只有在作业真正失败(而非仍在排队)时才有用。
@@ -37,14 +37,14 @@ Status: implemented
 
 ### 信任边界
 
-该变量是写者可管理的仓库状态;`pull_request` 事件本身既不能设置它,也不能让不同的值生效,选择器表达式存在于工作流定义中。需要注意:故障切换期间,`pull_request` 运行执行的是 PR merge 引用自带的工作流定义——抵御不可信代码的边界是仓库成员资格(私有、禁 fork、选择器排除 Dependabot),而非该变量。(运行器侧的组织级 runner group 约束另行跟踪,与本机制互补。)
+该变量是写者可管理的仓库状态;`pull_request` 事件本身既不能设置它,也不能让不同的值生效,选择器表达式存在于工作流定义中。需要注意:故障切换期间,`pull_request` 运行执行的是 PR merge 引用自带的工作流定义——抵御不可信代码的边界是仓库成员资格(私有、禁 fork、选择器排除 Dependabot),而非该变量。关于 runner group 策略的说明:把 runner group 绑定到 master 引用的工作流与本故障切换机制**不兼容**——四个故障切换作业是从 PR merge 引用求值的 `pull_request` 运行,master 绑定的组会让它们持续排队(2026-07-27 实际故障中亲历;当时将组放宽为本仓库全部工作流才疏通了切换)。更严格的运行器侧策略以牺牲 PR 故障切换为代价;当前采用的形态是仓库范围、全工作流的组访问。
 
 ## 曾考虑的替代方案
 
-**通过合并一次工作流改动来切换池。** 否决,因为触发切换的故障状态恰恰是任何 PR 都无法合并的状态:必需检查正是失败的那些。仓库变量是管理员控制的状态,重跑即生效,无需合并。
+**通过合并一次工作流改动来切换池。** 否决,因为触发切换的故障状态恰恰是任何 PR 都无法合并的状态:必需检查正是失败的那些。仓库变量是写者可管理的状态,重跑即生效,无需合并。
 
 **让自托管池长期处于必需路径中。** 否决,因为这是拿托管池的可用性去换自有虚拟机的可用性,只是搬移了单点故障而非增加回退。该变量让托管池保持主路径,自托管池作为一个经过验证、一步即可启用的热备。
 
 ## 后果
 
-从托管池故障中恢复只需一个管理员变量加一次重跑,关键路径上没有合并。代价是要维护第二套运行器拓扑:热备通道在每次 master 推送时都运行它,使故障切换目标永不失效;而 `ci.yml` 中的并发与缓存恢复分支带有一条 `selfhosted` 支路,必须与托管支路保持同步。
+从托管池故障中恢复只需一个变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是要维护第二套运行器拓扑:热备通道在每次 master 推送时都运行它,使故障切换目标永不失效;而 `ci.yml` 中的并发与缓存恢复分支带有一条 `selfhosted` 支路,必须与托管支路保持同步。
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 140ae00446..3c952a7bb3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -427,12 +427,13 @@ jobs:
     name: serial / linux (self-hosted standby)
     runs-on: [self-hosted, linux, x64, vm-backup]
     steps:
-      # fetch-depth 2 + DSH_ARCHIVE_BASE_REF below: same frozen-archive
-      # comparison as serial-linux — without the prior commit the archive
-      # verifier defaults to HEAD and compares the new manifest with itself.
+      # Full history + DSH_ARCHIVE_BASE_REF below: same frozen-archive
+      # comparison as serial-linux. Depth 2 would miss github.event.before
+      # on multi-commit or force pushes; full fetch is cheap here because
+      # checkout resolves against the VM's local mirror.
       - uses: actions/checkout@v6
         with:
-          fetch-depth: 2
+          fetch-depth: 0
 
       - uses: actions/setup-node@v6
         with:

From 3cf2853b3f04fdceb3ff99109dea8f764d28fbc3 Mon Sep 17 00:00:00 2001
From: Chinesezjc 
Date: Mon, 27 Jul 2026 16:22:30 +0800
Subject: [PATCH 10/10] docs(ci): bootstrap procedure starts the listener
 service

config.sh only registers; the runner stays offline until svc.sh
install/start. Both language sides updated so emergency capacity
actually comes online.
---
 .../process/2026-07-26-ci-failover-runbook.i18n.yaml          | 4 ++--
 .../implemented/process/2026-07-26-ci-failover-runbook.md     | 2 +-
 .../implemented/process/2026-07-26-ci-failover-runbook.zh.md  | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
index 658a85ce34..0e3b0820c0 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml
@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
-2026-07-26-ci-failover-runbook.md: ca4349661d03ff4e28d7c3c2b6e910106ff4aa30
-2026-07-26-ci-failover-runbook.zh.md: 1d59bd537879f531c9075e833c9e1dbfbd4bb0a2
+2026-07-26-ci-failover-runbook.md: 80dd7c4291e3de11c2f13b3247af56762396c720
+2026-07-26-ci-failover-runbook.zh.md: 7933a857f1559c540fccc2cd89352c4fe351dd7e
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
index ca4349661d..80dd7c4291 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
@@ -28,7 +28,7 @@ Each of the three required Linux worker jobs — and the `all checks passed` ver
 
 ## Capacity during failover
 
-Six always-on instances absorb normal PR traffic (the pool's steady-state load is one serial standby job per master push, so failover capacity is effectively the full pool). If queues still build, register additional instances with an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /` — then run `config.sh`; copying `.runner`/`.credentials` verbatim makes `config.sh` refuse with "already configured". About a minute per instance.
+Six always-on instances absorb normal PR traffic (the pool's steady-state load is one serial standby job per master push, so failover capacity is effectively the full pool). If queues still build, register additional instances with an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /` — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; only a started service adds capacity. About a minute per instance.
 
 
 ### Switch back
diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
index 1d59bd5378..7933a857f1 100644
--- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
+++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md
@@ -28,7 +28,7 @@ Status: implemented
 
 ## 切换期间的容量
 
-6 个常驻实例可承接正常 PR 流量(该池平时唯一的稳态负载是每次 master 推送一个串行热备作业,故障切换时几乎全池可用)。若仍出现排队,用组织级注册 token(组织 Settings → Actions → Runners → New runner)追加注册实例。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /`——再跑 `config.sh`;原样拷贝 `.runner`/`.credentials` 会使 `config.sh` 以 "already configured" 拒绝。每个约一分钟。
+6 个常驻实例可承接正常 PR 流量(该池平时唯一的稳态负载是每次 master 推送一个串行热备作业,故障切换时几乎全池可用)。若仍出现排队,用组织级注册 token(组织 Settings → Actions → Runners → New runner)追加注册实例。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner' --exclude '.credentials*' --exclude '_diag' --exclude '_work' / /`——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;只有启动了服务的 runner 才会增加容量。每个约一分钟。
 
 
 ### 切回