`dsh migrate`/`dsh upgrade` seed the first turn with a skill
`dsh migrate`/`dsh upgrade` 以 skill 播种首轮
Two recurring flows begin with the user manually invoking one skill and answering its questions: migrating from another coding agent, and upgrading this checkout. Both require the user to know the skill exists and to type `/skill:dsh-migrate` or `/skill:dsh-upgrade` as the session's first turn. A dedicated entry command that drops the user straight into that guided session removes the discovery step.
English
Problem
Two recurring flows begin with the user manually invoking one skill and answering its questions: migrating from another coding agent, and upgrading this checkout. Both require the user to know the skill exists and to type /skill:dsh-migrate or /skill:dsh-upgrade as the session's first turn. A dedicated entry command that drops the user straight into that guided session removes the discovery step.
Decision
dsh migrate and dsh upgrade boot the ordinary TUI as a fresh session whose first turn auto-invokes a bundled skill (dsh-migrate, dsh-upgrade), exactly as if the user typed /skill:<name> and pressed Enter.
The seed reuses the existing TUI skill path, not a new one. createTuiChat already has invokeSkill(name, instructions) — the code a typed /skill:<name> runs, including the "Unknown skill" notice. The launcher passes the skill name to the TUI through a new boot-context slot INITIAL_SKILL_KEY (tuiInitialSkill), mirroring CONFIGURED_AGENT_IDENTITIES_KEY/TUI_GOODBYE_MESSAGE_KEY: ctx.provide is the only channel from launcher argv into a Loader-mounted plugin. The TUI's apply() reads the slot and folds it into config.initialSkill; after ui.start() succeeds, createTuiChat fires invokeSkill(config.initialSkill, '') once when set.
Freshness is gated in the launcher, not the TUI. runSkillSession always mints a fresh session and provides the slot only when resumeSessionId === undefined, so a later dsh --resume <id> of that session is an ordinary TUI session with no re-injection. The TUI stays generic: it invokes whatever skill it is handed, once, at startup.
migrate/upgrade take no default-surface options (upgrade additionally carries the experimental gate's --experimental). They carry no --resume, --config, or -p; a guided fresh-session entry has nothing to resume or reconfigure. Any leaked default-surface option fails loud, matching the web/meta rejection pattern in the Commander adapter. The two modes share one SkillSessionInvocation discriminant (mode: 'migrate' | 'upgrade'); bin.ts maps the mode to dsh-${mode}.
The dsh-migrate skill is bundled under skills/ (shipped through DSH_BUNDLED_SKILL_DIR, like dsh-upgrade). It asks which source agent (opencode/pi/Claude Code/Codex) if unstated, then maps each capability — workspace instructions, personal overlay, skills, hooks, MCP, API/env — to its DSH equivalent, grounded in the actual repo surfaces (the hooks-claude/hooks-codex bridges, ~/.dsh/{config.yaml,.env,AGENTS.md,skills/}, AGENTS.md/CLAUDE.md, mcporter), and states plainly when a capability has no equivalent.
Testing
apps/cli/tests/args.spec.ts gains routing for migrate/upgrade (bare discriminant) and exit-1 for every leaked option on either side of each subcommand.
packages/ui/tui/tests/tui.spec.ts gains two fake-terminal cases in the existing skill describe block: config.initialSkill set delivers the rendered skill body as the first turn with no user input, and an unknown initial skill reports a notice without sending. runSkillSession itself is composition inside the module's v8 ignore block, like runTui/runMeta.
No keyless PTY snapshot: per the maintainer's scope call for this change, unit coverage plus interactive verification suffices, and the seed rides the already-snapshotted /skill: render path. Both commands were verified interactively in tmux from a scratch cwd: dsh migrate loaded dsh-migrate and asked which source agent; dsh upgrade loaded dsh-upgrade, which pulled in dsh-customize and began checkout discovery.
Alternatives considered
Prefill the input and let the user press Enter. Rejected: needs a new editor-prefill seam and still requires a keystroke. Auto-submit reuses invokeSkill and delivers the intended one-command entry.
Seed a natural-language instruction ("use the dsh-migrate skill…") instead of /skill:<name>. Rejected here: the literal skill-invocation path renders the skill body into the first turn deterministically, identical to the manual command, rather than depending on the model choosing to load the skill.
Support --resume on migrate/upgrade. Rejected: these are one-shot guided entries. A resumed session is an ordinary TUI session reachable through the default surface's dsh --resume <id>; re-injecting the skill on resume would duplicate the first turn.
Read INITIAL_SKILL_KEY outside the TUI (as CONFIGURED_AGENT_IDENTITIES_KEY is read by agent-loop) rather than in the TUI's apply(). Not needed: initialSkill is a TUI Config field consumed in createTuiChat, so folding the slot into config at the TUI entry keeps it beside the other launcher-owned runtime reads (tuiResumeHost, tuiGoodbyeMessage) and touches no other plugin.
Consequences
Migrating or upgrading is one command from anywhere, with the guiding skill already invoked. The launcher→TUI initial-skill slot is reusable by any future guided-session command; the TUI's contract is "invoke this named skill once at startup," and freshness/resume policy stays with the launcher that owns session identity. The TUI skill slash command remains the mechanism; this note adds a launcher-driven auto-invocation of it and does not supersede it.
中文
问题
有两个反复出现的流程都以用户手动调用某个 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 不接受任何默认界面选项(upgrade 另带实验性门槛的 --experimental)。它们不带 --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 在其之上新增了一个由启动器驱动的自动调用,并未取代它。