Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-28-dsh-guided-skill-session-commands.zh.md
T
Turtle f290a8b851 refactor(cli)!: one shared base config with per-surface overlays
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.

- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
  web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
  patches never cross an include boundary. Precedence: base < surface <
  (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
  test tree never inherits the user's route; new `--config-replace` boots a file
  as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
  configure or disable it. Upstream built the id index once before the patch
  loop, leaving every surface-only row — the whole TUI front door — silently
  unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
  dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
  TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
  examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay

Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.

Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
2026-07-29 21:15:42 +08:00

5.1 KiB
Raw Blame History

Agent Note:dsh migrate/dsh upgrade 以 skill 播种首轮

Status: implemented

English | 中文

问题

有两个反复出现的流程都以用户手动调用某个 skill 并回答其问题开始:从其他编码 agent 迁移,以及升级本 checkout。二者都要求用户知道该 skill 存在,并把 /skill:dsh-migrate 或 /skill:dsh-upgrade 作为会话首轮键入。一个专用入口命令若能让用户直接进入该引导式会话,便可省去这一发现步骤。

决策

dsh migrate 与 dsh upgrade 以全新会话启动普通 TUI,其首轮自动调用一个内置 skill(dsh-migrate、dsh-upgrade),效果等同于用户键入 /skill:<name> 并回车。

播种复用现有的 TUI skill 路径,而非新增一条。createTuiChat 已有 invokeSkill(name, instructions)——即键入 /skill:<name> 所走的代码,包含“未知 skill”通知。启动器通过一个新的启动上下文槽 INITIAL_SKILL_KEY(tuiInitialSkill)把 skill 名称传给 TUI,与 CONFIGURED_AGENT_IDENTITIES_KEY/TUI_GOODBYE_MESSAGE_KEY 一致:ctx.provide 是从启动器 argv 进入 Loader 挂载插件的唯一通道。TUI 的 apply() 读取该槽并折叠进 config.initialSkill;ui.start() 成功后,createTuiChat 在其被设置时调用一次 invokeSkill(config.initialSkill, '')。

新鲜性在启动器而非 TUI 中把关。 runSkillSession 总是创建全新会话,且仅在 resumeSessionId === undefined 时提供该槽,因此之后 dsh --resume <id> 恢复该会话时是普通 TUI 会话,不会重复注入。TUI 保持通用:它只是把接到的 skill 在启动时调用一次。

migrate/upgrade 不接受任何选项。 与 meta 不同,它们不带 --resume、--config 或 -p;引导式全新会话入口没有可恢复或可重配置的内容。任何泄漏的默认界面选项都会明确报错,与 Commander 适配器中 web/meta 的拒绝模式一致。两个 mode 共用一个 SkillSessionInvocation 判别式(mode: 'migrate' | 'upgrade');bin.ts 将 mode 映射为 dsh-${mode}。

dsh-migrate skill 内置于 skills/(经 DSH_BUNDLED_SKILL_DIR 交付,与 dsh-upgrade 相同)。若未说明源 agent,它会先询问是哪个(opencode/pi/Claude Code/Codex),再把每项能力——workspace 指令、个人覆盖、skills、hooks、MCP、API/env——映射到对应的 DSH 等价物,并基于仓库实际的表面(hooks-claude/hooks-codex 桥、~/.dsh/{config.yaml,.env,AGENTS.md,skills/}、AGENTS.md/CLAUDE.md、mcporter)落地;当某能力无等价物时明确说明。

测试

apps/cli/tests/args.spec.ts 新增 migrate/upgrade 的路由(裸判别式),以及每个子命令两侧任一泄漏选项的退出码 1。

packages/ui/tui/tests/tui.spec.ts 在既有 skill describe 块中新增两个伪终端用例:设置 config.initialSkill 时无需用户输入即把渲染后的 skill 正文作为首轮投递;未知的初始 skill 以通知形式报告且不发送。runSkillSession 本身是模块 v8 ignore 块内的组装,与 runTui/runMeta 相同。

无 keyless PTY 快照:依据维护者对本次改动的范围裁定,单元覆盖加交互式验证已足够,且播种走的是已有快照的 /skill: 渲染路径。两个命令均已在 tmux 中从临时 cwd 交互式验证:dsh migrate 加载 dsh-migrate 并询问源 agent;dsh upgrade 加载 dsh-upgrade,后者引入 dsh-customize 并开始 checkout 发现。

考虑过的替代方案

预填输入框并让用户按回车。 已否决:需要新增编辑器预填 seam,且仍需一次按键。自动提交复用 invokeSkill,实现预期的一命令入口。

播种自然语言指令(“使用 dsh-migrate skill……”)而非 /skill:<name>。 在此否决:字面 skill 调用路径会确定性地把 skill 正文渲染进首轮,与手动命令完全一致,而不依赖模型自行选择加载该 skill。

在 migrate/upgrade 上支持 --resume。 已否决:它们是一次性引导入口。恢复的会话是可经默认界面 dsh --resume <id> 到达的普通 TUI 会话;恢复时重新注入 skill 会重复首轮。

在 TUI 之外读取 INITIAL_SKILL_KEY(如同 agent-loop 读取 CONFIGURED_AGENT_IDENTITIES_KEY 那样),而非在 TUI 的 apply() 中。 无此必要:initialSkill 是在 createTuiChat 中消费的 TUI Config 字段,因此在 TUI 入口处把该槽位折叠进配置,可以让它与其他由启动器持有的运行时读取(tuiResumeHost、tuiGoodbyeMessage)并列,且不触及任何其他插件。

后果

迁移或升级从任何位置都只需一条命令,且引导 skill 已被调用。启动器→TUI 的初始 skill 槽可被未来任何引导式会话命令复用;TUI 的契约是“在启动时调用一次这个具名 skill”,而新鲜性/恢复策略留在拥有会话身份的启动器一侧。TUI skill 斜杠命令仍是该机制;本 note 在其之上新增了一个由启动器驱动的自动调用,并未取代它。