Merge branch 'master' into worktree/minimal-profiles-bare-runtime
This commit is contained in:
@@ -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/bug-fix/2026-07-28-web-gui-feedback-loop.md
|
||||
2026-07-28-web-gui-feedback-loop.md: aa488e1df087722c072d98c67d47cdf63a42f6b8
|
||||
2026-07-28-web-gui-feedback-loop.zh.md: 9b6954092b737920ed18bc412037e917512e40dc
|
||||
2026-07-28-web-gui-feedback-loop.md: fa7fcee80dc91ad7ec4a9a994927daa2cc293baa
|
||||
2026-07-28-web-gui-feedback-loop.zh.md: ea83441efa83ad0c93e8cb1f0daf71ecfeabb69e
|
||||
|
||||
@@ -12,9 +12,9 @@ The [incident post-mortem](../../../../docs/postmortem/0003-web-agent-gui-feedba
|
||||
|
||||
## Decision
|
||||
|
||||
The ordinary `dsh web` composition mounts the Web bundle's `web-runtime` plugin, which publishes one canonical loopback URL and its actual runtime mode as both model-visible orientation and managed shell facts. The `app:web-surface` prompt section says that unqualified references identify this GUI and names the URL; `DSH_WEB_URL` and `DSH_WEB_MODE=production|development` carry the same facts into every foreground or managed background bash call. The section preserves the no-implicit-DOM, route, or screenshot boundary and does not claim that a LAN alias equals the browser's literal address. A complete-prompt profile sets the row's `surfaceContext` to false and receives neither the prompt section nor the managed variables; the Web launcher uses the same setting to suppress its source-checkout prompt section.
|
||||
The ordinary `dsh web` composition mounts the Web bundle's `web-runtime` plugin, which publishes one canonical loopback URL as both model-visible orientation and a managed shell fact. The `app:web-surface` prompt section says that unqualified references identify this GUI and names the URL; `DSH_WEB_URL` carries the same fact into every foreground or managed background bash call. The section preserves the no-implicit-DOM, route, or screenshot boundary and does not claim that a LAN alias equals the browser's literal address. A complete-prompt profile sets the row's `surfaceContext` to false and receives neither the prompt section nor the managed variable; the Web launcher uses the same setting to suppress its source-checkout prompt section.
|
||||
|
||||
The mode-specific prompt makes the agent, rather than the user, own the hidden startup contract. Production mode defines acceptance as rebuilding the affected artifacts and refreshing the existing URL. Development mode states that `dsh web --dev` activates only the HMR receiver: automatic client-plugin reload additionally requires a same-checkout `pnpm run dev:web` watcher, which the agent verifies before promising no-refresh updates. Shell and other plain-package changes still require rebuild plus refresh. An agent in production mode explains both commands when a user requests no-refresh updates; it does not launch a replacement GUI unless asked.
|
||||
The prompt makes the agent, rather than the user, own the hidden startup contract. The client-plugin HMR receiver is always mounted, but automatic client-plugin reload additionally requires a same-checkout `pnpm run dev:web` watcher, which the agent verifies before promising no-refresh updates. Shell and other plain-package changes still require rebuilding the affected artifacts and refreshing the existing URL. The agent does not launch a replacement GUI unless asked.
|
||||
|
||||
The `apps/web` development script and Vite configuration reject serve mode before opening a port. Their diagnostics identify `apps/web` as a build-only shell, explain that only `dsh web` injects `window.__DSH_BOOT__`, and name the production and HMR entry paths. Vite build mode remains unchanged.
|
||||
|
||||
@@ -22,7 +22,7 @@ No server restart or replacement is required merely because static artifacts cha
|
||||
|
||||
## Verification
|
||||
|
||||
The keyless fresh-round-trip browser scenario boots the shipped production Web composition, drives a real replayed session, snapshots the URL/mode-bearing system-prompt prefix, and invokes the assembled bash tool to prove `$DSH_WEB_URL` and `$DSH_WEB_MODE` match the actual bound runtime. The real CLI smoke launches `dsh web --dev` and captures the provider request, pinning the complete two-command development contract. The `dev:web` watcher test rebuilds an isolated client bundle after a source change; the browser HMR scenario launches `dsh web --dev`, changes an initial production-roster bundle, and observes the new DOM under the same page identity. A real Vite subprocess test requires serve mode to exit naturally with the full-host correction and instruments `Server.listen()` to prove it was never called. The real-Loader webserver test rewrites a static asset after the process binds and proves the same port returns the new bytes. These assertions inspect prompt state, process exit, shell output, DOM identity, and HTTP bytes rather than an agent's success statement.
|
||||
The keyless fresh-round-trip browser scenario boots the shipped Web composition, drives a real replayed session, snapshots the URL-bearing system-prompt prefix, and invokes the assembled bash tool to prove `$DSH_WEB_URL` matches the actual bound runtime. The real CLI smoke launches `dsh web` and captures the provider request, pinning the complete two-command development contract. The `dev:web` watcher test rebuilds an isolated client bundle after a source change; the browser HMR scenario launches `dsh web`, changes an initial roster bundle, and observes the new DOM under the same page identity. A real Vite subprocess test requires serve mode to exit naturally with the full-host correction and instruments `Server.listen()` to prove it was never called. The real-Loader webserver test rewrites a static asset after the process binds and proves the same port returns the new bytes. These assertions inspect prompt state, process exit, shell output, DOM identity, and HTTP bytes rather than an agent's success statement.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,10 +30,10 @@ The keyless fresh-round-trip browser scenario boots the shipped production Web c
|
||||
|
||||
**Remove the `apps/web` development script without guarding Vite.** Rejected because `npx vite`, the exact incident command, bypasses package scripts. Serve mode itself must fail.
|
||||
|
||||
**Automatically restart or replace the current Web process after every edit.** Rejected because the static server already reads current artifacts per request, a restart would interrupt the session that requested the edit, and plugin HMR has a separate explicit `dsh web --dev` composition.
|
||||
**Automatically restart or replace the current Web process after every edit.** Rejected because the static server already reads current artifacts per request, a restart would interrupt the session that requested the edit, and client-plugin reload is owned by the always-mounted HMR chain plus the `pnpm run dev:web` watcher.
|
||||
|
||||
**Send DOM, route, or screenshots with each request.** Deferred to a separate logged-input design. Stable URL identity closes this feedback loop without claiming browser state the host does not receive.
|
||||
|
||||
## Consequences
|
||||
|
||||
Ordinary Web prompts gain a dynamic URL-and-mode paragraph, so provider prefix reuse now varies by bound port and mode. Their Bash processes gain two non-secret managed environment variables. Bare Vite can no longer be used as a shell-only visual sandbox; developers use the full host or build mode instead. In exchange, GUI work has one mechanically observable target, the agent can teach the user the exact update behavior of the process actually serving their session, and the unsupported startup path fails before a white screen. The URL/mode contract guides the agent away from replacement ports; it does not prohibit arbitrary shell commands from starting one. Profiles that disable `surfaceContext` also give up this feedback-loop guidance and shell context.
|
||||
Ordinary Web prompts gain a dynamic URL paragraph, so provider prefix reuse now varies by bound port. Their Bash processes gain one non-secret managed environment variable. Bare Vite can no longer be used as a shell-only visual sandbox; developers use the full host or build mode instead. In exchange, GUI work has one mechanically observable target, the agent can teach the user the exact update behavior of the process actually serving their session, and the unsupported startup path fails before a white screen. The URL contract guides the agent away from replacement ports; it does not prohibit arbitrary shell commands from starting one. Profiles that disable `surfaceContext` also give up this feedback-loop guidance and shell context.
|
||||
|
||||
@@ -12,9 +12,9 @@ Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道
|
||||
|
||||
## 决策
|
||||
|
||||
常规 `dsh web` 组合会挂载 Web 组合包的 `web-runtime` 插件,由它发布一个规范的回环 URL 及其实际运行时模式,同时将二者作为模型可见的界面定位信息和受管 shell 事实。`app:web-surface` 提示词段说明:未加限定的指代指向此 GUI,并给出 URL;`DSH_WEB_URL` 和 `DSH_WEB_MODE=production|development` 会把同样的事实传入每次前台或受管后台 bash 调用。该段保留「不会隐式获得 DOM、路由或截图」这一边界,也不声称局域网别名等于浏览器中的实际地址。拥有完整提示词的 profile 会把该配置行的 `surfaceContext` 设为 false,并且不会收到该提示词段和这些受管变量中的任何一个;Web 启动器也会使用同一项设置来抑制其源码 checkout 提示词段。
|
||||
常规 `dsh web` 组合会挂载 Web 组合包的 `web-runtime` 插件,由它发布一个规范的回环 URL,同时将其作为模型可见的界面定位信息和受管 shell 事实。`app:web-surface` 提示词段说明:未加限定的指代指向此 GUI,并给出 URL;`DSH_WEB_URL` 会把同样的事实传入每次前台或受管后台 bash 调用。该段保留「不会隐式获得 DOM、路由或截图」这一边界,也不声称局域网别名等于浏览器中的实际地址。拥有完整提示词的 profile 会把该配置行的 `surfaceContext` 设为 false,并且不会收到该提示词段和该受管变量;Web 启动器也会使用同一项设置来抑制其源码 checkout 提示词段。
|
||||
|
||||
按模式区分的提示词让 agent 而非用户负责隐藏的启动约定。生产模式将验收定义为重新构建受影响的产物并刷新现有 URL。开发模式说明,`dsh web --dev` 只会启用 HMR(热模块替换)接收端:客户端插件要自动重新加载,还需要在同一检出中运行 `pnpm run dev:web` 监听进程,agent 会在承诺无需刷新即可更新前验证这一点。外壳和其他普通包的变更仍然需要重新构建并刷新。生产模式下的 agent 会在用户要求无需刷新即可更新时说明这两个命令;除非用户要求,否则不会启动替代 GUI。
|
||||
提示词让 agent 而非用户负责隐藏的启动约定。HMR(热模块替换)接收端始终挂载,但客户端插件要自动重新加载,还需要在同一检出中运行 `pnpm run dev:web` 监听进程,agent 会在承诺无需刷新即可更新前验证这一点。外壳和其他普通包的变更仍然需要重新构建受影响的产物并刷新现有 URL。除非用户要求,agent 不会启动替代 GUI。
|
||||
|
||||
`apps/web` 开发脚本和 Vite 配置都会在打开端口前拒绝服务模式。诊断信息会指出 `apps/web` 只是一个仅供构建的外壳,说明只有 `dsh web` 才会注入 `window.__DSH_BOOT__`,并给出生产入口与 HMR 入口路径。Vite 构建模式保持不变。
|
||||
|
||||
@@ -22,7 +22,7 @@ Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道
|
||||
|
||||
## 验证
|
||||
|
||||
无密钥的 fresh-round-trip 浏览器场景会启动已交付的生产 Web 组合,驱动真实的回放会话,对包含 URL 和模式的系统提示词前缀生成快照,并调用组装后的 bash 工具,证明 `$DSH_WEB_URL` 和 `$DSH_WEB_MODE` 与实际绑定的运行时一致。真实 CLI 冒烟测试会启动 `dsh web --dev` 并捕获模型提供方请求,从而固定完整的双命令开发约定。`dev:web` watcher 测试会在源码发生变化后重新构建隔离的客户端 bundle;浏览器 HMR 场景会启动 `dsh web --dev`,修改生产初始 roster 中的 bundle,并在页面 identity 不变的情况下观察新 DOM。真实 Vite 子进程测试要求服务模式在给出改用完整宿主的纠正信息后自然退出,并通过插桩 `Server.listen()` 证明它从未被调用。真实 loader Web 服务器测试会在进程完成绑定后改写静态资源,并证明同一端口返回新的字节。这些断言检查提示词状态、进程退出状态、shell 输出、DOM identity 和 HTTP 字节,而不是 agent 的成功声明。
|
||||
无密钥的 fresh-round-trip 浏览器场景会启动已交付的 Web 组合,驱动真实的回放会话,对包含 URL 的系统提示词前缀生成快照,并调用组装后的 bash 工具,证明 `$DSH_WEB_URL` 与实际绑定的运行时一致。真实 CLI 冒烟测试会启动 `dsh web` 并捕获模型提供方请求,从而固定完整的双命令开发约定。`dev:web` watcher 测试会在源码发生变化后重新构建隔离的客户端 bundle;浏览器 HMR 场景会启动 `dsh web`,修改初始 roster 中的 bundle,并在页面 identity 不变的情况下观察新 DOM。真实 Vite 子进程测试要求服务模式在给出改用完整宿主的纠正信息后自然退出,并通过插桩 `Server.listen()` 证明它从未被调用。真实 loader Web 服务器测试会在进程完成绑定后改写静态资源,并证明同一端口返回新的字节。这些断言检查提示词状态、进程退出状态、shell 输出、DOM identity 和 HTTP 字节,而不是 agent 的成功声明。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -30,10 +30,10 @@ Web agent(智能体)既无法识别承载当前会话的 GUI,也不知道
|
||||
|
||||
**删除 `apps/web` 开发脚本,但不为 Vite 添加防护。** 不予采纳,因为事故中实际使用的命令 `npx vite` 会绕过包脚本。服务模式本身必须失败。
|
||||
|
||||
**每次编辑后自动重启或替换当前 Web 进程。** 不予采纳,因为静态服务器本就会在每次请求时读取当前产物,重启还会中断发起编辑请求的会话,而插件 HMR 已有独立且显式的 `dsh web --dev` 组合。
|
||||
**每次编辑后自动重启或替换当前 Web 进程。** 不予采纳,因为静态服务器本就会在每次请求时读取当前产物,重启还会中断发起编辑请求的会话,而客户端插件重载由始终挂载的 HMR 链路加 `pnpm run dev:web` watcher 负责。
|
||||
|
||||
**每次请求都发送 DOM、路由或截图。** 推迟到另行设计的已记录输入机制。稳定的 URL 身份足以闭合本次反馈循环,同时不会声称宿主掌握其未接收的浏览器状态。
|
||||
|
||||
## 影响
|
||||
|
||||
常规 Web 提示词会增加一个动态 URL 和模式段落,因此模型提供方的前缀复用会随绑定端口和模式变化。相应的 Bash 进程会增加两个非敏感的受管环境变量。裸 Vite 不再能用作只依赖 shell 的视觉沙箱;开发者应改用完整宿主或构建模式。作为交换,GUI 工作有了一个可由机制观察的唯一目标,agent 可以向用户说明实际承载其会话的进程究竟如何更新,不受支持的启动路径也会在出现白屏前失败。URL/模式约定会引导 agent 避免使用替代端口,但不会禁止任意 shell 命令启动替代服务。禁用 `surfaceContext` 的 profile 也会放弃这项反馈闭环指引与 shell 上下文。
|
||||
常规 Web 提示词会增加一个动态 URL 段落,因此模型提供方的前缀复用会随绑定端口变化。相应的 Bash 进程会增加一个非敏感的受管环境变量。裸 Vite 不再能用作只依赖 shell 的视觉沙箱;开发者应改用完整宿主或构建模式。作为交换,GUI 工作有了一个可由机制观察的唯一目标,agent 可以向用户说明实际承载其会话的进程究竟如何更新,不受支持的启动路径也会在出现白屏前失败。URL 约定会引导 agent 避免使用替代端口,但不会禁止任意 shell 命令启动替代服务。禁用 `surfaceContext` 的 profile 也会放弃这项反馈闭环指引与 shell 上下文。
|
||||
|
||||
@@ -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/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md
|
||||
2026-08-06-onboarding-step-owned-takeover-chrome.md: 4b3bbbc03c4494359297ae6e54bcc9a74c387e80
|
||||
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: ba4f503f7bc318fcb16f4413465a86609fa1bc61
|
||||
2026-08-06-onboarding-step-owned-takeover-chrome.md: 35b9d16aaba4ca9f124a9972c43b2a107ea0309f
|
||||
2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 2f3331965aa94aa01bf0ce21d4bbb421397ee5bd
|
||||
|
||||
@@ -32,4 +32,4 @@ A future step that registers without wrapping its visible content in `Onboarding
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings/tests/settings-root.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim.
|
||||
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings-general/tests/settings-root.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim.
|
||||
|
||||
@@ -32,4 +32,4 @@
|
||||
|
||||
## 测试
|
||||
|
||||
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings/tests/settings-root.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec(`ui-settings-general`、`ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。
|
||||
`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings-general/tests/settings-root.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec(`ui-settings-general`、`ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。
|
||||
|
||||
@@ -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/bug-fix/2026-08-09-broken-preset-roster-rows.md
|
||||
2026-08-09-broken-preset-roster-rows.md: fef6a183b10f98b8ae9d2b42701380c69bc83462
|
||||
2026-08-09-broken-preset-roster-rows.zh.md: 196bcf4ef16325a1d7692d2ea13d9fa683d500f4
|
||||
2026-08-09-broken-preset-roster-rows.md: 069585957d4d99598cc38e4a7c6bc8c8d82490ca
|
||||
2026-08-09-broken-preset-roster-rows.zh.md: d541292b59496464eb91bc03278e0800af16c4a3
|
||||
|
||||
@@ -26,7 +26,7 @@ Surfaces split by their job: the management section renders broken rows as marke
|
||||
- **`PRESET_ID` moved to `types.ts`** so discovery and authoring share one containment vocabulary; authoring re-exports it unchanged.
|
||||
- **The reason is one line.** js-yaml appends a multi-line code-frame snippet; the roster card is not a terminal, so `compositionProblem` keeps the first line.
|
||||
- **Two mount.spec races were left untouched deliberately**: `ensureStanding` is still reachable with a preset resolved just before deletion (the private-path tests), and its stamp/unstampable semantics are unchanged — the health check happens before, in the public route.
|
||||
- **Creator-mode guidance rides the same PR**: the `cordis` preset's persona now forbids editing the shipped install (corrupting `cordis` would disable the mode itself) and points authoring at `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`; its skill teaches `preset.yml` metadata, the copy-first workflow, the one-escalation sandbox reality (the preset root lies outside the session workspace), and honest verification (the agent cannot start sessions; the settings page's red marking is the user's check). Verified live: asked to edit the shipped `cordis` composition directly, the composed agent refuses citing both rules and offers the copy path; asked for a real preset, it lands it under `$DSH_HOME`, batches writes into one escalation, self-checks with the loader dialect, and hands verification to the user.
|
||||
- **Creator-mode guidance rides the same PR**: the `cordis` preset's persona forbids editing the shipped install (corrupting `cordis` would disable the mode itself) and points authoring at `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`; its skill teaches `preset.yml` metadata, the copy-first workflow, and the one-escalation sandbox reality (the preset root lies outside the session workspace). Verified live: asked to edit the shipped `cordis` composition directly, the composed agent refuses citing both rules and offers the copy path; asked for a real preset, it lands it under `$DSH_HOME` and batches writes into one escalation. The verification half of that guidance — that the agent cannot start sessions, so the settings page's red marking is the user's check — is superseded by [the authoring agent mount-validates its own composition](2026-08-11-preset-authoring-agent-validates-its-own-composition.md): the shape check below is not validation, and `standingKeyFor` gives the agent the real one. The health decision in this note is unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Status: implemented
|
||||
- **`PRESET_ID` 移到 `types.ts`**,让发现与创作共享同一份包含边界词汇;authoring 原样转发导出。
|
||||
- **原因只留一行。** js-yaml 会附上多行代码框摘录;名单卡片不是终端,`compositionProblem` 只保留首行。
|
||||
- **mount.spec 的两个竞态用例特意不动**:`ensureStanding` 仍可能拿到删除前一刻解析出的 preset(私有路径测试),其 stamp/unstampable 语义不变——健康检查发生在此之前的公开路径上。
|
||||
- **创造模式的引导随同一 PR 落地**:`cordis` preset 的 persona 现在禁止编辑随附安装(损坏 `cordis` 会禁用这一模式本身),并把创作指向 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`;其技能新教了 `preset.yml` 元信息、先复制再改的流程、一次升级的沙箱现实(preset 根目录在会话工作区之外)与诚实的验证方式(agent 无法自己启动会话;设置页的红色标记是用户的检查项)。已实测:被要求直接改随附 `cordis` 组装时,组装出的 agent 援引两条规则拒绝并给出复制路径;被要求真正创建 preset 时,它落在 `$DSH_HOME` 下、把写入合并为一次升级、用加载器方言自查、并把验证交还用户。
|
||||
- **创造模式的引导随同一 PR 落地**:`cordis` preset 的 persona 禁止编辑随附安装(损坏 `cordis` 会禁用这一模式本身),并把创作指向 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/`;其技能教了 `preset.yml` 元信息、先复制再改的流程与一次升级的沙箱现实(preset 根目录在会话工作区之外)。已实测:被要求直接改随附 `cordis` 组装时,组装出的 agent 援引两条规则拒绝并给出复制路径;被要求真正创建 preset 时,它落在 `$DSH_HOME` 下并把写入合并为一次升级。该引导中关于验证的那一半——agent 无法自己启动会话,因而设置页的红色标记是用户的检查项——已由[创作 preset 的 agent 自行挂载校验其组装](2026-08-11-preset-authoring-agent-validates-its-own-composition.md)取代:下文的结构检查不是校验,而 `standingKeyFor` 才给了 agent 真正的校验手段。本篇的健康检查决策不变。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -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/bug-fix/2026-08-10-slash-catalog-follows-preset-switch.md
|
||||
2026-08-10-slash-catalog-follows-preset-switch.md: 85bd5b2134fd20c86fdeb13f3ce5b007449105b5
|
||||
2026-08-10-slash-catalog-follows-preset-switch.zh.md: 97c8f08a7b3dfec7c17fbb00bef626e28505c500
|
||||
2026-08-10-slash-catalog-follows-preset-switch.md: ec85d4a3a6a053e91c368f65ecb12f123c45287d
|
||||
2026-08-10-slash-catalog-follows-preset-switch.zh.md: b68fa90e99c3597c61b3edcc03399bb7ea7844f4
|
||||
|
||||
@@ -8,17 +8,17 @@ English | [中文](2026-08-10-slash-catalog-follows-preset-switch.zh.md)
|
||||
|
||||
Presets moved the rows that decide what a session's `/` menu contains. The Web composition disables host-plane `skill-local`, `tool-skill`, `plan-mode`, and `command-compact`; a preset supplies them, so which commands and skills exist is a property of the session's composition rather than of the deployment.
|
||||
|
||||
Both browser catalogs cache per session — `CommandDirectory` in `dsh-client-ui-command`, the single-flight fetch map in `dsh-client-ui-skill` — and the composer warms both at scope birth, under whatever preset the session was created with. The hero chip then lets the user recompose the still-blank session, and neither cache had an invalidation edge for that: `commands/changed` is registry-wide and `connection/reset` needs a reconnect. `agentPresets.recompose` re-parents the agent's scope onto a standing mount that may already exist, so it registers nothing and the registry-wide signal never fires for it.
|
||||
Both browser catalogs cache per session — `CommandDirectory` in `dsh-client-ui-command`, the single-flight fetch map in `dsh-client-ui-skill` — and the composer warms both at scope birth, under whatever preset the session was created with. The hero chip then lets the user recompose the still-blank session, and neither cache had an invalidation edge for that: `commands/change` is registry-wide and `connection/reset` needs a reconnect. `agentPresets.recompose` re-parents the agent's scope onto a standing mount that may already exist, so it registers nothing and the registry-wide signal never fires for it.
|
||||
|
||||
The menu therefore kept serving the composition the session no longer ran. Switching down left `compact`, `plan`, and every project skill listed; switching up left the narrower catalog — the four host-plane rows and the client's own `model` contribution — with no skills at all, which is what the bug report described. The catalog only healed when an unrelated registry change or a reconnect happened to invalidate it.
|
||||
|
||||
## Decision
|
||||
|
||||
The switch's commit point is the logged `agent-preset/selected` event. The host stream frames it as `host/session-preset-changed { sessionId, agentPreset }`, the browser runtime bridges that frame to the typed `session/preset-changed` ctx event beside the registry-invalidation bridges it already owns, and each catalog owner drops its own entry for that session: `ui-command` soft-refreshes the key (the old snapshot keeps serving the open menu until the new one lands), `ui-skill` invalidates it (aborting an in-flight prewarm, so a warm racing the switch cannot publish the stale catalog).
|
||||
The switch's commit point is the logged `agent-preset/selected` event. The preset owner re-emits that commit as the client-safe cordis owner event `agent-preset/selected(sessionId, agentPreset)`, the host stream forwards it verbatim, and each catalog subscribes directly through `ctx.remote.$on`: `ui-command` soft-refreshes the key (the old snapshot keeps serving the open menu until the new one lands), while `ui-skill` invalidates it (aborting an in-flight prewarm, so a warm racing the switch cannot publish the stale catalog).
|
||||
|
||||
The frame is per session and carries no catalog, only the preset id — which the manager folds into the session row, because the `agentPresets.select` echo reaches only the client that issued the switch and the row is what the session header labels itself from (and what the hero chip compares the next pick against).
|
||||
The owner event is per session and carries no catalog, only the preset id. `ui-agent-preset` folds it into the session row because the `agentPresets.select` echo reaches only the client that issued the switch and the row is what the session header labels itself from (and what the hero chip compares the next pick against).
|
||||
|
||||
Deriving the frame from the logged event rather than from the RPC handler's return keeps one authority for "this session's composition changed": every connected client observes the switch, not only the tab that issued it, and a client that is not the switcher never has to infer it from a registry signal that will not come.
|
||||
Deriving the owner event from the logged event rather than from the RPC handler's return keeps one authority for "this session's composition changed": every connected client observes the switch, not only the tab that issued it, and a client that is not the switcher never has to infer it from a registry signal that will not come.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,15 +26,15 @@ Deriving the frame from the logged event rather than from the RPC handler's retu
|
||||
|
||||
**Derive the client event from the existing `session/event` mux frame.** The logged event already reaches every subscribed client, so no new wire type would be needed. Rejected on face separation: narrowing `event.type` to `agent-preset/selected` requires the `SessionEventMap` augmentation, and the only ways to load it in the Client program are a project reference to `dsh-agent-presets` — which drags the host `ctx.sessions` merge into a program that publishes its own — or a cast that defeats the discriminant.
|
||||
|
||||
**Reuse `host/commands-changed`.** It is the existing catalog-invalidation frame, but it is registry-wide, carries no session, and says nothing about skills; a client would repull every session's commands and still never refresh a skill catalog.
|
||||
**Reuse forwarded `commands/change`.** It is the existing catalog-invalidation event, but it is registry-wide, carries no session, and says nothing about skills; a client would repull every session's commands and still never refresh a skill catalog.
|
||||
|
||||
## Consequences
|
||||
|
||||
The wire gains one frame and the Client one typed event, and every catalog a preset decides now has one place to subscribe: a future per-session surface derived from the composition invalidates on the same signal instead of inventing another. The cost is that the frame is a second reader of a logged fact — the host stream must keep deriving it from `agent-preset/selected`, so a future switch path that recomposes without logging would go unannounced. `ui-command` stays soft (the open menu never blanks) while `ui-skill` drops its entry outright, because a skill catalog has no partial-serve mode; a menu opened inside the refetch window shows no skills for that instant rather than the wrong ones.
|
||||
The forwarding allowlist gains the preset owner's typed event, and every catalog a preset decides has one place to subscribe: a future per-session surface derived from the composition invalidates on the same signal instead of inventing another. The owner event remains a second publication of a logged fact, so a future switch path that recomposes without logging would go unannounced. `ui-command` stays soft (the open menu never blanks) while `ui-skill` drops its entry outright, because a skill catalog has no partial-serve mode; a menu opened inside the refetch window shows no skills for that instant rather than the wrong ones.
|
||||
|
||||
## Testing
|
||||
|
||||
`api-proxy-agent-preset.spec.ts` asserts the committed switch frames once with the session and its new preset; `wire-events.spec.ts` asserts the frame-to-event bridge; the `ui-command` and `ui-skill` specs assert that the event repulls the recomposed session and leaves every other session's cache serving. The `agent-preset-selection` web e2e seeds a project skill and, after the hero chip applies `minimal`, asserts the `/` menu drops `compact`, `plan`, and the skill while keeping the host-plane rows — the assembled-application evidence that the panel follows the composition.
|
||||
`api-proxy-agent-preset.spec.ts` asserts the committed switch is forwarded once with the session and its new preset; the `ui-agent-preset`, `ui-command`, and `ui-skill` specs assert that direct Remote subscriptions merge the row or repull only the recomposed session. The `agent-preset-selection` web e2e seeds a project skill and, after the hero chip applies `minimal`, asserts the `/` menu drops `compact`, `plan`, and the skill while keeping the host-plane rows — the assembled-application evidence that the panel follows the composition.
|
||||
|
||||
That e2e also stopped reading its staged-pick assertion off the serialized session list: the seeded session records `minimal` too, so the substring answered before the switch had landed. It now addresses the live session by id.
|
||||
|
||||
|
||||
@@ -8,17 +8,17 @@ Status: implemented
|
||||
|
||||
preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿主面的 `skill-local`、`tool-skill`、`plan-mode` 和 `command-compact`,改由 preset 提供,因此一个会话有哪些命令和技能,是它自身组成的属性,而不是部署的属性。
|
||||
|
||||
浏览器侧两份目录都按会话缓存——`dsh-client-ui-command` 的 `CommandDirectory`,`dsh-client-ui-skill` 的 single-flight 拉取表——并且 composer 在 scope 出生时就按会话创建时的 preset 预热了它们。随后 hero 上的 chip 允许用户重组这个仍为空的会话,而两份缓存都没有对应的失效边:`commands/changed` 是注册表级的,`connection/reset` 需要重连。`agentPresets.recompose` 只是把 agent 的 scope 重新挂接到一个可能已经存在的常驻挂载上,不产生任何注册,注册表级信号因此永远不会为它触发。
|
||||
浏览器侧两份目录都按会话缓存——`dsh-client-ui-command` 的 `CommandDirectory`,`dsh-client-ui-skill` 的 single-flight 拉取表——并且 composer 在 scope 出生时就按会话创建时的 preset 预热了它们。随后 hero 上的 chip 允许用户重组这个仍为空的会话,而两份缓存都没有对应的失效边:`commands/change` 是注册表级的,`connection/reset` 需要重连。`agentPresets.recompose` 只是把 agent 的 scope 重新挂接到一个可能已经存在的常驻挂载上,不产生任何注册,注册表级信号因此永远不会为它触发。
|
||||
|
||||
于是菜单继续提供会话已经不再运行的那套组成。向下切换后 `compact`、`plan` 和全部项目技能仍列在菜单里;向上切换后留在原地的是更窄的目录——四条宿主面行加客户端自己的 `model` 贡献——而且完全没有技能,这正是 bug 报告描述的现象。只有当某个无关的注册表变化或一次重连恰好使其失效时,目录才会自愈。
|
||||
|
||||
## Decision
|
||||
|
||||
这次切换的提交点是落账的 `agent-preset/selected` 事件。宿主流把它成帧为 `host/session-preset-changed { sessionId, agentPreset }`,浏览器运行时在它已经拥有的那组注册表失效桥接旁,把该帧桥接为类型化的 `session/preset-changed` ctx 事件,两份目录各自丢弃该会话的那一项:`ui-command` 软刷新该键(新快照落地前,旧快照继续服务已打开的菜单),`ui-skill` 让它失效(并中止在途的预热,使一次与切换赛跑的 warm 无法发布过期目录)。
|
||||
这次切换的提交点是落账的 `agent-preset/selected` 事件。preset owner 将该提交重新发为 client-safe 的 cordis owner 事件 `agent-preset/selected(sessionId, agentPreset)`,宿主流原样转发它,两份目录各自通过 `ctx.remote.$on` 直接订阅:`ui-command` 软刷新该键(新快照落地前,旧快照继续服务已打开的菜单),`ui-skill` 让它失效(并中止在途的预热,使一次与切换赛跑的 warm 无法发布过期目录)。
|
||||
|
||||
该帧按会话粒度,不携带目录,只带 preset id——manager 会把它折进会话行,因为 `agentPresets.select` 的回执只会到达发起切换的那个客户端,而会话头部标签正是以这一行为准(hero chip 比较下一次选择时读的也是它)。
|
||||
该 owner 事件按会话粒度,不携带目录,只带 preset id。`ui-agent-preset` 会把它折进会话行,因为 `agentPresets.select` 的回执只会到达发起切换的那个客户端,而会话头部标签正是以这一行为准(hero chip 比较下一次选择时读的也是它)。
|
||||
|
||||
从落账事件而不是 RPC 处理器的返回值派生该帧,使「这个会话的组成变了」只有一个权威来源:每个已连接的客户端都能观察到这次切换,而不只是发起它的那个标签页;不是发起方的客户端也无需从一个根本不会到来的注册表信号里去推断。
|
||||
从落账事件而不是 RPC 处理器的返回值派生 owner 事件,使「这个会话的组成变了」只有一个权威来源:每个已连接的客户端都能观察到这次切换,而不只是发起它的那个标签页;不是发起方的客户端也无需从一个根本不会到来的注册表信号里去推断。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,15 +26,15 @@ preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿
|
||||
|
||||
**从既有的 `session/event` mux 帧派生客户端事件。** 落账事件本来就会送达每个已订阅的客户端,不需要新增协议类型。因面(face)分离而否决:把 `event.type` 收窄到 `agent-preset/selected` 需要 `SessionEventMap` 增补,而在 Client 程序里加载它只有两条路——引用 `dsh-agent-presets` 工程,那会把宿主的 `ctx.sessions` 合并拖进一个自己也发布同名服务的程序;或者用一次类型断言绕过判别式。
|
||||
|
||||
**复用 `host/commands-changed`。** 它是既有的目录失效帧,但它是注册表级的、不带会话、也与技能无关;客户端会把每个会话的命令都重拉一遍,却依然永远刷不新技能目录。
|
||||
**复用转发的 `commands/change`。** 它是既有的目录失效事件,但它是注册表级的、不带会话、也与技能无关;客户端会把每个会话的命令都重拉一遍,却依然永远刷不新技能目录。
|
||||
|
||||
## Consequences
|
||||
|
||||
协议多了一个帧,Client 多了一个类型化事件,而每一份由 preset 决定的目录从此有了统一的订阅点:将来任何从组成派生的按会话界面,都在同一个信号上失效,而不必再发明一个。代价是该帧成为一项落账事实的第二个读者——宿主流必须持续从 `agent-preset/selected` 派生它,因此将来若出现一条不落账就重组的切换路径,它将无人宣告。`ui-command` 保持软失效(已打开的菜单不会变空),而 `ui-skill` 直接丢弃该项,因为技能目录没有「部分可服务」的状态;在重拉窗口内打开的菜单,那一瞬间显示的是没有技能,而不是错误的技能。
|
||||
转发名单加入了 preset owner 的类型化事件,而每一份由 preset 决定的目录从此有了统一的订阅点:将来任何从组成派生的按会话界面,都在同一个信号上失效,而不必再发明一个。owner 事件仍是落账事实的第二次发布,因此将来若出现一条不落账就重组的切换路径,它将无人宣告。`ui-command` 保持软失效(已打开的菜单不会变空),而 `ui-skill` 直接丢弃该项,因为技能目录没有「部分可服务」的状态;在重拉窗口内打开的菜单,那一瞬间显示的是没有技能,而不是错误的技能。
|
||||
|
||||
## Testing
|
||||
|
||||
`api-proxy-agent-preset.spec.ts` 断言已提交的切换恰好成帧一次,并带上会话与新 preset;`wire-events.spec.ts` 断言帧到事件的桥接;`ui-command` 与 `ui-skill` 的 spec 断言该事件只重拉被重组的会话,其他会话的缓存继续服务。`agent-preset-selection` web e2e 播种一个项目技能,并在 hero chip 应用 `minimal` 之后断言 `/` 菜单丢掉了 `compact`、`plan` 和该技能,同时保留宿主面的那几行——这是面板跟随组成的整装应用证据。
|
||||
`api-proxy-agent-preset.spec.ts` 断言已提交的切换恰好转发一次,并带上会话与新 preset;`ui-agent-preset`、`ui-command` 与 `ui-skill` 的 spec 断言直接 Remote 订阅会合并会话行或只重拉被重组的会话。`agent-preset-selection` web e2e 播种一个项目技能,并在 hero chip 应用 `minimal` 之后断言 `/` 菜单丢掉了 `compact`、`plan` 和该技能,同时保留宿主面的那几行——这是面板跟随组成的整装应用证据。
|
||||
|
||||
同一条 e2e 也不再从序列化后的会话列表里读它的 staged-pick 断言:被播种的会话同样记录着 `minimal`,子串匹配在切换落地之前就会通过。现在它按 id 寻址那个活跃会话。
|
||||
|
||||
|
||||
@@ -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/bug-fix/2026-08-11-preset-authoring-agent-validates-its-own-composition.md
|
||||
2026-08-11-preset-authoring-agent-validates-its-own-composition.md: 6b9cdf32b70e3ab4adc9f3b0e20bb3d2245486c7
|
||||
2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md: e6e8dabcd886a6331d294744b667552caa01e7b4
|
||||
@@ -0,0 +1,71 @@
|
||||
# Agent Note: The preset-authoring agent mount-validates its own composition
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-11-preset-authoring-agent-validates-its-own-composition.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The `cordis` preset ships `editing-cordis-compositions`, the only guidance an agent has when it authors a preset. Four of its statements were false, and the two that carried the most weight pointed at the rule the skill itself calls "the rule that catches people".
|
||||
|
||||
It named `tool-bash` as the worked example of a row whose name hides a service — "reads like a tool but provides `bashEnv`". `tool-bash` provides nothing; it declares `inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`, and `bashEnv` comes from the host composition's own `bash-env` row. An agent wrapping `tool-bash` in an `isolate` realm on that advice strands the row waiting for a service its realm hides, and the whole preset fails to mount.
|
||||
|
||||
Its `isolate` example composed `tasks-local` with `tool-tasks`. `tasks-local` is host-plane, and the shipped compositions say in their own comments that an entry-local realm around `tool-tasks` makes `run_in_background` answer "background tasks unavailable". The example contradicted the file next to it.
|
||||
|
||||
It described a string realm label as pooling one instance across subtrees. Labels join realms; `provide()` still throws on the second registration under the same realm symbol, which `standard`'s header comment already stated.
|
||||
|
||||
It sent the agent to a package's README to learn whether a row publishes a service. Every harness package declares `files`, and no declaration includes its README, so an installed deployment carries none. There that instruction cannot be followed at all.
|
||||
|
||||
Underneath all four sat a capability claim: the agent "cannot start one \[a session\] yourself", so verification was hand-reading YAML fields and handing the result to the user through the settings page's red marking. That marking is discovery's shape check, which is far weaker than the sentence implied.
|
||||
|
||||
## Decision
|
||||
|
||||
The skill teaches the agent to mount-validate its own composition through `ctx.agentPresets`, and every remaining example is taken from a shipped composition in the same repository.
|
||||
|
||||
`standingKeyFor(id)` is the check. It runs `ensureStanding()` — the same real mount a session start performs, minus the agent — so it rejects a row whose package does not resolve, a row whose config is invalid, a service published into the root realm, and a row that never activated. A failed mount deletes the standing entry and disposes its scope, leaving nothing behind; a successful one installs the standing generation the first real session would have installed anyway. The skill therefore places it as the final check on a finished edit rather than a per-line loop.
|
||||
|
||||
The skill states plainly that `list()`'s `broken` field is **not** validation. Discovery's health check proves the file parses in the loader's dialect and holds named rows, and every one of the four failures above passes it.
|
||||
|
||||
The agent reaches the roster service the way `cordis_mount` documents: a temporary plugin declaring `inject: ['agentPresets', 'tools']` that registers a tool for itself, because a mount returns only its own acknowledgement and a registered tool is how a service answer reaches the model on the next step. The skill ships that plugin verbatim. `agentPresets` is in the generated `cordis_inspect what:"api"` catalog with full JSDoc, and the sandbox façade gates services on `fiber.inject` alone rather than an allowlist, so nothing about this path is special-cased for the skill.
|
||||
|
||||
`copy(from, id, name)` is named as the authoring write, in place of a shell copy: it validates the id, refuses one any root supplies, rolls a failed copy back, rewrites the copy's `preset.yml`, and runs host-side without sandbox escalation. The escalation guidance stays, moved to where it applies — editing `agent.cordis.yml` afterwards still writes outside the session workspace.
|
||||
|
||||
"Whether a row publishes a service" resolves through `cordis_inspect what:"services"`, which names the owning fiber of every live service.
|
||||
|
||||
The guidance keeps `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` as the answer to "where do my presets live" — it is where every `dsh` launcher puts them — while routing the path an agent actually reads or edits through `list()` or `resolve()`. `Config.roots` defaults to `[]` and `apps/cli` patches both roots in, `writableRoot()` takes the first `user` one, and no call reports either path; `authorable` answers only whether a writable root exists, and `list()` cannot reveal a user root that holds nothing yet. Stating the path is therefore right for talking to a person and wrong for feeding a file tool.
|
||||
|
||||
The prohibition on touching the shipped install is promoted from a paragraph inside the authoring steps to a top `## Off-limits` section, extended to cover editing the host composition as a workaround. The new self-validation calls do not weaken it: `copy()` refuses an id any root supplies, and `remove()` refuses a preset that ships with the deployment.
|
||||
|
||||
## Measured behavior
|
||||
|
||||
Each row was produced by booting the shipped Web composition and calling the tools through `ctx.tools.execute` on an agent composed from `cordis` — no model in the loop.
|
||||
|
||||
| Composition under test | `list()` `broken` | `standingKeyFor()` |
|
||||
|---|---|---|
|
||||
| row names an absent package | empty | `Cannot find package '@deepseek-ai/dsh-does-not-exist'` |
|
||||
| service row with no realm, name the host supplies | empty | `service "tasks" has been registered at <LocalTaskService>` |
|
||||
| service row with no realm, name the host does not supply | empty | `row(s) published process-global service(s) [workflows]; …` |
|
||||
| same row inside `isolate` | empty | mounts |
|
||||
| consumer row with no provider | empty | `1 row(s) did not activate: … waiting for workflows` |
|
||||
| row missing a required config field | empty | `invalid config: $.allowParallelInProgress missing required value` |
|
||||
|
||||
The skill's own `cordis_mount` snippet was executed verbatim through the tool registry: it mounts, its `preset_check` tool appears in the composing agent's catalog on the next read, and it answers `mounted OK` for a valid preset and the mount rejection for an invalid one.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Leaving verification with the user and only fixing the four errors.** The errors and the capability claim share a cause — the guidance was written from the preset layer's public surface rather than from what the composed agent can reach — and an agent that cannot check its work hands over compositions whose defects the settings page cannot see either.
|
||||
|
||||
**Teaching `list()`'s `broken` field as the check.** It is the one the settings page shows, so it reads like the intended answer. It passes every failure that matters, and presenting it as validation is what made the original guidance feel complete.
|
||||
|
||||
**Adding a first-class preset-validation tool to the preset.** The composed path already exists and is documented by `cordis_mount`'s own schema; a dedicated tool would add a model-facing row to a preset whose point is that the runtime is reachable without one.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A successful validation leaves a standing generation that is never reclaimed, which is the [standing-mount](../architecture/2026-08-08-per-preset-standing-mounts.md) cost the roster already carries per generation — the agent pays it once at the end of an edit instead of the user paying it at the first session.
|
||||
- The skill now depends on `cordis_inspect`'s generated API catalog staying current for `agentPresets`; `verify-cordis-api` in `doc-sync` is what holds that.
|
||||
- Two examples are now quotations of `standard`'s composition. They drift if that file's `delegation` group changes, which the `web-agent-presets` e2e does not catch.
|
||||
- The four corrected statements were the skill's only concrete illustrations of the realm rule. Replacing rather than deleting them keeps the rule teachable; the replacements are verifiable by reading one shipped file.
|
||||
|
||||
## Related
|
||||
|
||||
Supersedes the creator-guidance bullet in [broken presets are roster rows](2026-08-09-broken-preset-roster-rows.md), whose health-check decision remains current — this note reverses only its "the agent cannot start sessions; the settings page's red marking is the user's check" conclusion. Authoring's copy-only shape is owned by [copy-only preset authoring](../simplification/2026-08-08-copy-only-preset-authoring.md).
|
||||
@@ -0,0 +1,71 @@
|
||||
# Agent Note: 创作 preset 的 agent 自行挂载校验其组装
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-11-preset-authoring-agent-validates-its-own-composition.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`cordis` preset 随包发布 `editing-cordis-compositions`,它是 agent 创作 preset 时唯一的指导来源。其中四条陈述与事实不符,而分量最重的两条恰好指向该 skill 自称「最容易让人栽跟头的规则」。
|
||||
|
||||
它把 `tool-bash` 当作「行名看不出发布服务」的示例——「看着像工具,其实 provides `bashEnv`」。`tool-bash` 不发布任何服务,它声明 `inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`,`bashEnv` 来自宿主组装自己的 `bash-env` 行。agent 照此给 `tool-bash` 套上 `isolate` realm,该行会永远等待被自己的 realm 挡住的服务,整个 preset 挂载失败。
|
||||
|
||||
它的 `isolate` 示例把 `tasks-local` 与 `tool-tasks` 组在一起。`tasks-local` 位于宿主平面,而已发布组装在自己的注释里写明:给 `tool-tasks` 套 entry-local realm 会让 `run_in_background` 回答「background tasks unavailable」。示例与紧挨着它的文件互相矛盾。
|
||||
|
||||
它把字符串 realm label 描述为跨子树共享一个实例。label 只是加入同一 realm,`provide()` 在同一 realm symbol 下第二次注册仍然抛错——`standard` 的头部注释早已如此说明。
|
||||
|
||||
它让 agent 去读包的 README 判断某行是否发布服务。每个 harness 包都声明了 `files`,且没有任何一份声明包含自己的 README,因此装机部署中一份也没有。在那里该指令根本无法执行。
|
||||
|
||||
四条之下还压着一个能力断言:agent「自己起不了会话」,于是校验退化成肉眼核对 YAML 字段,再把结果经设置页的红色标记交给用户。那个标记是发现阶段的结构检查,远弱于这句话给人的印象。
|
||||
|
||||
## Decision
|
||||
|
||||
skill 教 agent 通过 `ctx.agentPresets` 自行挂载校验其组装,其余每个示例都取自同一仓库中已发布的组装。
|
||||
|
||||
`standingKeyFor(id)` 是校验手段。它走 `ensureStanding()`——与会话启动完全相同的真实挂载,只是不创建 agent——因此能拒绝包无法解析的行、配置非法的行、把服务发布进根 realm 的行,以及始终未激活的行。挂载失败会删除常驻条目并 dispose 其 scope,不留残留;挂载成功则装上首次真实会话本来也会装上的那个常驻代际。因此 skill 把它安排为完成编辑后的最终检查,而不是逐行循环。
|
||||
|
||||
skill 明确写出:`list()` 的 `broken` 字段**不是**校验。发现阶段的健康检查只证明文件能被 Loader 的方言解析且行带 `name`,上述四类失败全部能通过它。
|
||||
|
||||
agent 按 `cordis_mount` 自身文档所述的方式够到 roster 服务:挂一个声明 `inject: ['agentPresets', 'tools']` 的临时插件,并为自己注册一个工具——因为挂载只返回自身的确认信息,而已注册的工具才是服务结果在下一步抵达模型的途径。skill 逐字附上该插件。`agentPresets` 位于生成的 `cordis_inspect what:"api"` 目录中并带完整 JSDoc,沙箱 façade 仅凭 `fiber.inject` 而非白名单放行服务,因此这条路径没有为该 skill 做任何特例。
|
||||
|
||||
`copy(from, id, name)` 被指定为创作写入手段,取代 shell 复制:它校验 id、拒绝任何根已提供的 id、失败时回滚、重写副本的 `preset.yml`,并在宿主侧运行而无需沙箱升级。沙箱升级的说明保留,移到真正适用之处——其后编辑 `agent.cordis.yml` 仍然写在会话工作区之外。
|
||||
|
||||
「某行是否发布服务」改由 `cordis_inspect what:"services"` 回答,它会给出每个存活服务的持有 fiber。
|
||||
|
||||
指导保留 `${DSH_HOME:-$HOME/.dsh}/.agent-presets/` 作为「我的 preset 在哪」的答案——每个 `dsh` 启动器都把它们放在那里——同时把 agent 实际读取或编辑的路径改走 `list()` 或 `resolve()`。`Config.roots` 默认为 `[]`,两个根均由 `apps/cli` 补入,`writableRoot()` 取其中第一个 `user` 根,且没有任何调用会报告任一路径;`authorable` 只回答是否存在可写根,而 `list()` 无法揭示一个尚且为空的用户根。因此写出该路径对人讲是对的,喂给文件工具是错的。
|
||||
|
||||
禁止改动随发布安装的约束,从创作步骤中的一段提升为顶部的 `## Off-limits` 一节,并扩展到禁止改宿主组装绕行。新增的自校验调用不削弱它:`copy()` 拒绝任何根已提供的 id,`remove()` 拒绝随部署发布的 preset。
|
||||
|
||||
## Measured behavior
|
||||
|
||||
下表每一行都由启动已发布的 Web 组装、并在由 `cordis` 组装出的 agent 上经 `ctx.tools.execute` 调用工具得出——全程无模型参与。
|
||||
|
||||
| 被测组装 | `list()` 的 `broken` | `standingKeyFor()` |
|
||||
|---|---|---|
|
||||
| 行指向不存在的包 | 空 | `Cannot find package '@deepseek-ai/dsh-does-not-exist'` |
|
||||
| 服务行未套 realm,名字宿主已提供 | 空 | `service "tasks" has been registered at <LocalTaskService>` |
|
||||
| 服务行未套 realm,名字宿主未提供 | 空 | `row(s) published process-global service(s) [workflows]; …` |
|
||||
| 同一行置于 `isolate` 内 | 空 | 挂载成功 |
|
||||
| 消费者行无人提供服务 | 空 | `1 row(s) did not activate: … waiting for workflows` |
|
||||
| 行缺少必填配置字段 | 空 | `invalid config: $.allowParallelInProgress missing required value` |
|
||||
|
||||
skill 自带的 `cordis_mount` 代码片段经工具注册表逐字执行:它成功挂载,其 `preset_check` 工具在下一次读取时出现在组装该 agent 的目录中,对有效 preset 回答 `mounted OK`,对无效 preset 回答挂载拒绝原因。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把校验留给用户,只修四处错误。** 这些错误与那句能力断言同源——指导是按 preset 层的公开面写的,而不是按被组装出的 agent 实际够得到的东西写的——而无法自查的 agent 交出的组装,其缺陷设置页同样看不见。
|
||||
|
||||
**把 `list()` 的 `broken` 字段教成校验手段。** 它正是设置页展示的字段,看起来像是预期答案。它对所有要紧的失败一律放行,而把它当成校验,正是原指导显得完整的原因。
|
||||
|
||||
**给 preset 加一个一等的 preset 校验工具。** 组合出的路径已经存在,且由 `cordis_mount` 自己的 schema 记载;专用工具会给一个「无需专用工具即可够到运行时」的 preset 再添一个面向模型的行。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 校验成功会留下一个永不回收的常驻代际,这是 roster 按代际本就承担的[常驻挂载](../architecture/2026-08-08-per-preset-standing-mounts.md)代价——由 agent 在编辑收尾时付一次,而不是由用户在首次会话时付。
|
||||
- skill 现在依赖 `cordis_inspect` 生成的 API 目录对 `agentPresets` 保持最新;`doc-sync` 中的 `verify-cordis-api` 是守住这一点的门禁。
|
||||
- 有两个示例现在是对 `standard` 组装的引用。若该文件的 `delegation` 组发生变化它们会漂移,而 `web-agent-presets` e2e 捕捉不到。
|
||||
- 被修正的四条陈述原本是该 skill 对 realm 规则仅有的具体图示。选择替换而非删除,规则才仍然可教;替换后的示例读一个已发布文件即可核验。
|
||||
|
||||
## Related
|
||||
|
||||
取代[破损 preset 是 roster 行](2026-08-09-broken-preset-roster-rows.md)中关于创作模式指导的那一条,其健康检查决策依然有效——本篇只推翻它「agent 起不了会话;设置页的红色标记是用户的检查手段」这一结论。创作的 copy-only 形态由[copy-only preset 创作](../simplification/2026-08-08-copy-only-preset-authoring.md)负责。
|
||||
Reference in New Issue
Block a user