# Worktree Space 使用指南 一个任务从「新建」到「结束」要用到的全部操作,都写在这里。 **插件是什么、有哪些功能、每个配置项怎么改,见 [README](README.md)。** 指南分成**互相独立的两部分**: - **第一部分(第 1–9 节)是面板手动流程**:全部操作都在界面上完成,一步一步点。 README 里那一段是它的简写,这里是详细说明。 - **第二部分(第 10–14 节)是 Agent 自动编排**:把整条流程交给会话里的 agent, 由它用工具自己跑完,你不需要在面板上按任何按钮。 两部分走的是**同一套流程**,差别只在谁来驱动,以及最后第 14 节那几件不可逆的事只能由人决定。 只看你打算用的那一半即可;第 15 节的错误码两边通用。 > **说法以插件当前实现为准。** 界面文案随 DSH 的语言设置切换,指南里写的是中文档案名。 > 每一处行为都能在源码里核到:客户端在 `src/client/`(`CreateWorktreeDialog.tsx`、 > `ArchiveTaskDialog.tsx`、`DeployCard.tsx`、`WorktreesSettings.tsx`),服务端在 `src/host/` > (`task/create.js`、`task/deploy.js`、`task/delivery.js`、`task/archive.js`、`task/tool.js`、 > `session-tools.js`、`session-watch.js`)。 --- ## 📑 目录 **读之前** - [0. 术语](#0-术语) **第一部分 · 面板手动流程** - [1. 一次任务的完整生命周期](#1-一次任务的完整生命周期) - [2. 打开管理页面](#2-打开管理页面) [3. 新建任务空间](#3-新建任务空间) - [4. 在任务空间里工作](#4-在任务空间里工作) [5. 给任务补仓库](#5-给任务补仓库) - [6. 部署验收](#6-部署验收) [7. 结束任务](#7-结束任务) - [8. 冲突与「停在工作上」](#8-冲突与停在工作上) [9. 排障](#9-排障) **第二部分 · Agent 自动编排** - [10. 这条路径做什么](#10-这条路径做什么) [11. 术语补充:队长与队员](#11-术语补充队长与队员) - [12. 整条链怎么跑](#12-整条链怎么跑) [13. 进行中的三个抓手](#13-进行中的三个抓手) - [14. 只有人能做的几件事](#14-只有人能做的几件事) **参考** - [15. 错误码速查](#15-错误码速查) [附录:会话控制工具参考](#附录会话控制工具参考) --- ## 0. 术语 | 词 | 指什么 | | --- | --- | | **源码根**(`sourceRoot`) | 日常干活的那个工作区目录:它本身是 Git 仓库,或者它下面装着若干 Git 仓库 | | **容器根**(`tasksRoot`) | 任务空间放在哪(设置项叫「Worktree Space 容器根目录」)。**与源码根不得互相包含** | | **项目层** | 容器根下的第一层,名字是源码根的目录名。插件自动分层,所以两个项目下的同名任务不会挤在一起 | | **任务空间** | `<容器根>/<项目>/<任务>` —— 一个任务的独立目录,**同时是任务会话的工作目录** | | **任务分支** | `分支前缀 + 任务名`,默认 `task/<任务名>`。一个任务的所有仓库共用这一条分支 | | **任务会话** | 工作目录开在任务空间里的会话:可以在那个目录下改代码、跑构建、起服务,不会碰到源码检出 | | **遗留内容**(strays) | 任务空间里 **Git 从没跟踪过**的文件:笔记、脚本、日志、构建产物等 | | **环境 id** | `dsh-<项目名>-<任务名>`,勾了部署的任务推导出来的身份。容器、镜像、端口、验收地址互不干扰全靠它 | | **验收地址** | 部署起来以后能点开看的那个 URL,写在任务空间的状态文件里 | 另外两个词 —— **队长** 和 **队员** —— 只属于第二部分,见[第 11 节](#11-术语补充队长与队员)。 --- # 第一部分 · 面板手动流程 这一部分全程你自己在界面上操作:没有工具调用,也不需要理解任何 agent 角色。 ## 1. 一次任务的完整生命周期 | # | 阶段 | 你在界面上做什么 | 做完手上有什么 | | --- | --- | --- | --- | | 1 | **新建任务空间** | 管理页面(或会话按钮)→ 填写「新建任务空间」对话框 → **创建并打开会话** | 一个任务空间目录、一条任务分支、每个选中仓库一份 worktree;它注册成工作区并直接开一个会话 | | 2 | **在任务空间里工作** | 在那个会话里干活,或自己在各仓库子目录里改和提交 | 每个仓库各自独立的提交 | | 3 | **补仓库**(可选) | 任务行的 **添加仓库** | 新仓库加入任务现有的那条分支 | | 4 | **部署验收**(勾了部署时) | 部署卡片的 **部署验收** → **执行冒烟** | 验收地址 + 一次通过的冒烟 | | 5 | **结束任务** | 任务行的 **结束任务** → 勾选 → **确认结束任务** | 合并回目标分支、移除 worktree、清理任务空间、归档遗留内容 | | 6 | **处理冲突**(需要时) | 解决冲突并提交合并 → **继续结束任务** | 合并继续,走到第 5 步的收尾 | **插件只在两处替你强制把关**,其余每一步都等你点: - **交付闸门**:勾了部署的任务,冒烟没通过就不能合并([6.6](#66-收尾闸门)); - **收尾守卫**:任务空间里还有会话在活动,或 worktree 被别的进程占着,结束任务会被拒([7.7](#77-收尾前的守卫与收尾后的清理))。 第 4 步只在勾了「部署到 Docker」时存在;没勾的任务,第 3 步之后直接到第 5 步。 --- ## 2. 打开管理页面 管理页面:工作区 / Git 仓库 / 任务空间三个视图 ### 2.1 四个入口 | 入口 | 在哪 | 打开什么 | 默认 | | --- | --- | --- | --- | | **侧边栏底部快捷入口** | 侧边栏底部的 **Worktree Space** | 以**对话框**打开管理页面 | 显示 | | **面板入口** | 侧边栏「新会话」下方的 **Worktree Space** 行 | 在**主区域整页**打开同一个管理页面 | 隐藏(在插件设置里打开) | | **会话里的按钮** | 空白会话输入框旁的插件图标(悬停提示「新建 Worktree Space」) | 直接打开**新建**对话框 | 只在当前会话是空白、且所在工作区能建任务空间时出现 | | **工作区列表的 `⋯` 菜单** | 某个工作区行的菜单 | **新建 Worktree Space**;确属任务空间的目录另有 **结束 Worktree Space** | 显示 | 两种形态(对话框 / 整页)共用同一条左侧导航:整页形态第一项是 **返回会话**,下面三项切换视图, 导航底部 **⚙ 插件设置** 跳到宿主的插件页改本插件的配置。 ### 2.2 三个视图 | 视图 | 一列是什么 | 能在这儿做什么 | | --- | --- | --- | | **工作区视图** | 每个 DSH 工作区,带「包含 N 个仓库」徽章;没有仓库的会写「没有 Git 仓库,无法创建任务空间」 | **新建 Worktree Space**(能建的那些行)、**添加工作区**(填一个目录把它注册成工作区) | | **Git 仓库视图** | 扫到的每个 Git 仓库,展开是它链接的 worktree(分支、N 个未提交变更、已锁定、可清理) | **新建 Worktree Space**(从某个仓库起)、**添加仓库**(填路径把目录注册成工作区,因此进入本视图) | | **任务空间视图** | 每个任务,展开是它下面的仓库;任务头上是任务分支、待合并提交数 | **添加仓库**、**结束任务**,以及任务行下面的部署卡片 | 搜索框一次匹配任务、工作区、仓库、分支和路径;三个视图都能用 **待处理** 筛出需要关注的行 (有改动的、被锁定的、可清理的、状态读取失败的),工作区视图里这一档是「有仓库」。 每行左边的箭头单独折叠,旁边的按钮一次全部展开或收起。 **面板每次打开都会重新扫描**,但会先用上一次的扫描结果立刻把界面画出来,所以点开就有数据。 这份记忆只放在 DSH 实例的内存里,实例关闭就随它清空。 --- ## 3. 新建任务空间 新建 Worktree Space 对话框 ### 3.1 界面里每一项 按对话框从上到下的顺序: 1. **选择仓库**:勾选任务要参与的仓库,卡片上标着各仓库当前 HEAD 所在的分支和路径。 底部有 **全选 / 清空** 和「已选择 N / M 个仓库」。**至少要选一个**,默认全选。 2. **任务信息 → 任务名称**:名字会转成小写,除字母、数字、点、连字符和下划线以外的字符 (空格、中文等)都换成连字符。它同时是任务空间的目录名和分支后缀。不合法时会直接说违反了哪条。 3. **分支前缀**(默认 `task/`):分支名 = 前缀 + 任务名。改了与当前默认不同时,下面出现 **设为默认分支前缀**,勾选后点「创建并打开会话」时把这次的值写回插件设置。 4. **Worktree Space 容器根目录**:界面已经填好推荐值 —— **根工作空间的下一层** `worktree-space`, 通常不用改。推荐规则只看路径上盘根下面的第一个目录,所以 `~/workspace/project1` 和 `~/workspace/deep-path/project2` 推荐的是**同一个**容器。改动时同样出现 **设为默认…** 勾选框。 容器根必须与源码根互不包裹,容器根自己就是 Git 仓库会被拒绝。 5. **部署到 Docker**(默认不勾):勾上后多出 **部署脚本** 输入框, 留空则用 `工作区/.worktree-space/deploy/deploy.sh`。这一项为什么要在建之前定下来,见 3.2。 6. **分支起点**:**各仓库当前 HEAD**(默认)或 **指定分支或提交**。指定时那个 ref 必须在 **每一个**选中仓库里都存在,缺一个就被拒(`E3002`)。 7. **预览**:算出这个任务会用到的**分支名**和**空间目录名**,跟着上面的输入实时更新。 8. **创建并打开会话**:建完直接在该任务空间开一个会话。 ### 3.2 「要不要部署验收」必须在创建之前定 **交付策略只在创建时落一次,事后没有任何入口能加、能改**,任务记录里也不会留下「当初想要过围栏」 的痕迹。所以没勾就等于定了「这个任务不做验收」,而且之后没有任何东西提示这件事。 - **不勾(`none`)是个好答案**:没有运行时行为要验收的任务本来就不该勾。但它要是**你自己选的**答案。 - **勾了(`docker`)**:冒烟通过就是这个任务的完成线。`up` 写验收地址、`smoke` 记下结果, 收尾时合并会被闸门检查([6.6](#66-收尾闸门))。 - 勾了却没有任何可用入口脚本时,**创建直接被拒**(`E4015`),而不是建出一个永远部署不了的任务空间。 ### 3.3 创建出来的是什么 ```text <容器根>/ └── <项目>/ ← 项目层:源码根的目录名 └── <任务>/ ← 任务空间,同时是那个会话的工作目录 ├── .worktree-space/ ← 插件专属目录:插件自己的东西全在这里 │ ├── worktree-space.json 任务的记录(分支、起点、仓库、部署策略) │ ├── worktree-space.md 由记录渲染出来,给人读的那一份 │ └── deploy/ 部署根:工作区 .worktree-space/deploy/ 的副本 ├── <仓库A>/ ← 位于 <分支前缀><任务名> 的 worktree └── <仓库B>/ ``` 源码根**本身就是仓库**时(`~/workspace/repo-x`),项目层与仓库层同名,得到 `repo-x/task-x/repo-x` —— 这处重复是有意接受的:布局深度恒定为三层。 任务空间会被注册成名为 `<上级工作区名>/<任务名>` 的 DSH 工作区,所以它在工作区列表里独立成组。 **`.worktree-space/` 不是你的内容**:它装的是记录、由记录生成的说明和部署根,不归档, 清除任务空间时随容器一起删除。 ### 3.4 创建失败怎么办 | 情况 | 码 | 怎么办 | | --- | --- | --- | | 名字被**别的**任务空间占了 | `E2001` | 换任务名 | | 同名任务空间已在磁盘上、只是没注册 | `E2002` | **不要换名重建**,对话框会给出 **重新注册** 按钮,点了就能接着用 | | 分支已被占用 | `E3001` | 换任务名 | | 起点 ref 在某个仓库里不存在 | `E3002` | 换一个每个选中仓库都有的分支或提交 | | 部署脚本路径越界或不存在 | `E4014` | 把脚本放进工作区的 `.worktree-space/deploy/`,或改路径 | | 勾了部署却没有入口 | `E4015` | 补一个入口脚本,或先不勾部署 | | 创建中途失败 | `E2005` / `E2006` | 前者已完整回滚(名字可原样重试);后者有残留,按对话框里点名的路径手工清理 | **注册失败只是警告不是失败**:任务空间是真的、能用的,缺的只是工作区列表里的那一行, 用对话框里的 **重新注册** 补上即可。 --- ## 4. 在任务空间里工作 创建后直接打开的那个会话,**工作目录就是任务空间**,在里面干活不会动到源码检出。你也可以随时在 DSH 里切到那个工作区开新的会话接手 —— 多个任务各占一个任务空间,彼此并行、互不干扰。 在这个目录里有几条纪律: - **任务空间本身不是 Git 仓库**:git 命令要在**各仓库子目录**里跑。 - **每个仓库单独提交**,没有跨仓库提交;分支名处处相同。 - **源码检出是只读参考**:不要在里面改文件、提交,也不要手写 `git merge`。 合并始终是「结束任务」那一步由插件做的事([7.3](#73-三步提交合并冲突))。 - 分支、起点、参与仓库、环境 id 这些事实,读任务空间里的 `.worktree-space/worktree-space.md`, 不要靠猜(同样的事实以数据形式在 `.worktree-space/worktree-space.json`)。 - **任务分支只在本机。** 推远端永远是你自己的动作,插件不写远端。 --- ## 5. 给任务补仓库 任务开工后才发现少一个仓库:**在任务行的「添加仓库」**里选,不要另建一个任务空间, 也不要手写 `git worktree add`。 - 候选列表就是 **Git 仓库视图**那一列;任务里已有的不会重复列出。 - 列表里没有的仓库,可以**直接填一个目录**:它会注册成工作区,因此同样进入 Git 仓库视图,之后可选。 - 新仓库加入任务**已有的那条分支**,起点默认各自 HEAD。 - 新仓库可以和原来的仓库不在同一个目录、甚至不在同一个盘上 —— 合并和收尾都不受影响。 - **没有「移除仓库」这一项**。本版本不支持从任务里拿掉一个仓库,需要的话只能重新新建任务空间。 --- ## 6. 部署验收 只适用于创建时勾了「部署到 Docker」的任务。 ### 6.1 契约:一个入口脚本 部署契约就是一个入口脚本实现两个子命令: ```sh bash <任务空间>/.worktree-space/ up # 构建并启动,把验收地址写进状态文件 bash <任务空间>/.worktree-space/ smoke # 跑冒烟,把结果合并进状态文件 ``` `` 默认是 `.worktree-space/deploy/deploy.sh`;创建时用「部署脚本」指定了别的就叫那个名字。 插件执行时的工作目录是脚本自己所在的目录,所以脚本引用同目录的兄弟文件是可行的。 `destroy` 子命令**可选,插件从不调用它** —— 面板的销毁按钮按容器的 `dsh.env-id` 标签直接拆, 不需要脚本参与。**只实现 `up` 和 `smoke` 的入口就是完整的。** ### 6.2 部署根与复制 创建时会把工作区自己的 `.worktree-space/deploy/` **整个**复制到任务空间里的同一相对位置。 所以要把脚本**和它引用的兄弟文件一起**放进工作区那个目录,之后每个新建的任务空间直接可用; 只放一个脚本会得到一个跑不起来的部署。 因为两侧位置相同,任务记录里的「部署脚本」路径对工作区和任务空间同时成立。 **注意部署根比早先深了一层**:compose 文件里原本写 `../<仓库>` 的 build context 现在是 `../../<仓库>`。 ### 6.3 环境身份 每个任务有一个推导出来的环境 id:`dsh-<项目名>-<任务名>`(小写、非字母数字折成连字符)。 它以 `DEPLOY_ENV_ID` 交给每个部署脚本,也是容器上的 `dsh.env-id` 标签值 —— 容器、镜像、端口、 验收地址互不干扰全靠这个 id。 **不要猜、也不要复用别的任务的 id**:面板的销毁按钮就是按这个标签找容器的,张冠李戴会拆掉别人的环境。 值写在 `.worktree-space/worktree-space.md` 里,直接读。 ### 6.4 状态文件契约 部署脚本**自己拥有**状态文件,面板和收尾闸门都读它。位置固定,与入口脚本叫什么、放在哪一层无关: ``` <任务空间>/.worktree-space/deploy/.state.json ``` 插件把这条绝对路径通过环境变量 **`DEPLOY_STATE_FILE`** 传给每一个它运行的脚本, 所以脚本写 `$DEPLOY_STATE_FILE` 即可,不必自己推算部署根在哪一层。 | 字段 | 谁写 | 含义 | | --- | --- | --- | | `url` | `up` | 验收地址 | | `lastSmoke: { result, at }` | `smoke` | 最近一次冒烟,`result` 取 `pass` / `fail` | | `services` | `up`(可选) | 服务列表,原始 JSON | | `destroyedAt` | **插件**(点销毁时);脚本只在它自己的 `destroy` 里可以写 | 环境被销毁的时刻 | - **写入必须是合并,不是重写**:`smoke` 只改 `lastSmoke` 一个字段,其余原样保留。一次重写抹掉 `destroyedAt`,插件会误以为环境还活着。 - **`up` 和 `smoke` 永远不写 `destroyedAt`**:清理上一次销毁留下的痕迹是插件的事。 - **状态文件必须由真正跑了那一步的脚本写**:手改、或照别处的运行记录补写都不算 —— 闸门把它当作 「那一步确实发生过」的证据。 插件脚手架出来的任务空间自带 `.worktree-space/deploy/write-state.sh`,它写的就是 `$DEPLOY_STATE_FILE`: ```sh ./write-state.sh url ./write-state.sh smoke pass|fail ./write-state.sh services ``` **这个脚本不是每个任务空间都有**:只在创建时该空间还没有部署根的情况下才被写入 —— 工作区自带 `.worktree-space/deploy/` 时整份复制进来,已有目录就不再脚手架。那种任务空间里,合并写要由部署脚本自己做。 ### 6.5 部署卡片 任务行下面的部署卡片**每次实时查询**,不缓存 —— 动态端口每次部署都在变,所以面板只问不记。 | 元素 | 含义 | | --- | --- | | 环境 id | 本任务的环境 id,悬停看全名 | | 冒烟徽章 | **冒烟通过 / 冒烟失败 / 未冒烟** + 时间 | | 运行中容器 | 按 `dsh.env-id` 标签实时数出来的容器数 | | 验收地址 | 点开在新窗口预览 | | 收尾还差什么 | 上一次部署或冒烟报回来的缺口,比如「收尾合并还差:一次部署、一次通过的冒烟」 | | 刷新 | 重新查一遍以上全部(纯只读) | 按钮按状态出现: - **部署验收** —— 任务勾了部署、而当前没有容器在跑时; - **执行冒烟** —— 有容器在跑时。**失败也是结果**,卡片的红色徽章照样刷新; - **销毁环境** —— 有容器在跑时,**两段确认**(先变「确认销毁」,再点一次才执行)。 - 策略是 `none` 的任务不显示部署和冒烟按钮,但**仍会显示容器并保留销毁按钮** —— 一个没勾部署的任务 也可能带着别处起的容器,而拆不掉的东西比策略的意外更糟。 改完代码的循环是:**部署验收 → 执行冒烟 → 通过后结束任务**。 **验收事实跟随任务空间**:销毁环境不会清掉冒烟结果,重新部署也照样保留。 > **卡片上的红字不是显示问题。** 「容器在跑,但脚本没有写交付状态」说明 `up` 没写 `url`、 > `smoke` 没合并 `lastSmoke` —— **改脚本,不要手改文件**。 ### 6.6 收尾闸门 勾了部署的任务,**合并必须有通过的冒烟**:没有状态文件、或里面的 `lastSmoke.result` 不是 `pass`, 合并就被拒(`E5006`)。这是服务端真拦,不是提示。 面板在你点 **结束任务** 的那一刻就会先查一次;闸门没过时会先问一句 **「尚未完成冒烟验证,是否要结束任务?」**,你点 **继续** 之后才打开对话框, 并且这次收尾会在日志里留一条 `delivery-gate-bypassed` 警告 —— 也就是说「绕过」这件事是有记录的。 不点「继续」也行:卡片上会直接写着「收尾合并还差:一次部署、一次通过的冒烟」,照它补齐即可。 > **想在收尾之后还看得见验收地址的话,就在收尾之前看完** —— 收尾成功会自动销毁环境([7.7](#77-收尾前的守卫与收尾后的清理))。 --- ## 7. 结束任务 结束任务对话框 **入口**:任务空间视图里任务行的 **结束任务**,或工作区列表那个任务的 `⋯` 菜单里的 **结束 Worktree Space**。 ### 7.1 打开先看到「计划」 对话框打开时先按路径重读一遍真实状态(不依赖列表里那一行),把接下来会发生的事摊开: | 区域 | 内容 | | --- | --- | | 状态行 | 未提交变更数 · 待合并提交数 · 空间目录名 | | 仓库列表 | 每行:仓库名、任务分支、`→` **目标分支下拉**、N 个提交、未提交 / 无未提交变更、**源仓库**路径。目标可以下拉换分支,换完立刻重算预览;该目标不是源仓库当前检出时会标「会在临时 worktree 中合并」 | | 任务空间内其他内容 | 会被归档或删除的条目名 + 文档数;另有几项构建/编辑器产物会一并删除;底部一句说明 `.worktree-space/` 不属于可归档内容 | ### 7.2 四个选项 | 选项 | 默认 | 含义 | | --- | --- | --- | | **合并回目标分支** | 勾选 | 把任务分支用 `git merge --no-ff` 合进上面为每个仓库选定的目标分支(默认各仓库当前检出的分支)。有冲突时该仓库不做改动 | | **删除分支** | 不勾 | 合并成功后删除。不合并时要同时勾「强制」才允许,分支上未合并的提交会一并丢弃 | | **强制** | 不勾 | 丢弃未提交的变更,并允许删除未合并的分支 | | **归档任务空间里的文档** | 勾选 | 把未被 Git 管理的文档移到目标目录下。取消勾选则是**永久删除**这些内容,红色警告会逐条点名 | 归档目录显示在选项下面:`<归档根>/<项目>/<任务名>-/`,时间戳是归档那一刻的本地时间。 归档根默认是容器根下的 `archived-docs/`,可以在插件设置改成别的目录。 对话框还会先说一句后果:**任务空间目录会被删除;该任务空间里的会话移到「未分组」,对话记录保留。** ### 7.3 三步:提交、合并、冲突 **1)提交。** 只要计划里还有未提交改动的仓库、而「强制」没勾,**「确认结束任务」就一直不可用** —— 插件不代写提交。两条路: - 自己在各自的 worktree 里 `git add` / `git commit`(提交信息由读过这些改动的人来写); - 或用 **交由 agent 提交**(实验性,见 7.6)。 处理完**点「继续结束任务」**;如果你是在对话框外面(终端或别的会话里)提交的, 关掉再打开对话框 —— 它每次打开都会重读一遍。 也可以勾 **强制**:那些改动会随 worktree 一起丢弃。 **2)合并:先反着试一次,再正着合。** 真正的合并是**把任务分支合并进目标分支**,落在**源仓库**。 但在动目标分支之前,插件先在任务空间**自己的 worktree 里反着试一次**(把目标分支合进任务分支): - **干净**:撤掉这次试合并,再按常规 `--no-ff` 合并进目标分支;目标分支若没被任何检出占用, 就在临时 worktree 里合并,源检出不动。 - **冲突**:**不中止、不还原**,试合并就停在任务分支的 worktree 里 —— 冲突文件带着冲突标记躺在那个 worktree 的工作区,**目标分支未被触碰**,源仓库的检出也不变。 反过来试是为了让冲突落在任务空间自己的检出里(可以就地处理),否则一旦真合并冲突,现场就在你的源码检出上。 **3)冲突:停下等你解决。** 见[第 8 节](#8-冲突与停在工作上)。 ### 7.4 选项组合的行为 | 合并回目标分支 | 删除分支 | 强制 | 会发生什么 | | --- | --- | --- | --- | | ✓ | | | 未提交改动先各自提交到任务分支,再 `--no-ff` 合并进目标分支;移除 worktree;**分支保留** | | ✓ | ✓ | | 同上,合并成功后删除分支(`git branch -d`,所以未合并的删不掉) | | ✓ | | ✓ | 合并照做,但跳过提交:未提交的改动**随 worktree 一起丢弃**;分支保留 | | ✓ | ✓ | ✓ | 合并,并强制删除分支(`git branch -D`) | | | | | 什么都不合并:提交改动(留在分支上),移除 worktree 与任务空间,**分支保留** | | | | ✓ | 同上但不提交:未提交的改动**随 worktree 一起丢弃**;分支保留 | | | ✓ | ✓ | **放弃这个任务**:不合并、不提交,强删分支,**未合并的提交与未提交的改动一并丢弃** | 无论哪种组合,三条不变: 1. **插件自己的 `.worktree-space/` 目录随容器一起清除**; 2. **其余内容按「归档任务空间里的文档」这一个勾选处理**(勾了归档,取消了删除); 3. **没做完的仓库原样保留**,记为未完成,任务空间目录与工作区注册也因此都留下。 按钮的颜色跟着这件事走:**橙色**是常规收尾(合并可以回退,没有东西被丢); 只有真的要丢东西时才变成**红色**(勾了强制,或不合并又删分支)。 ### 7.5 报告 收尾跑完后,对话框换成报告: | 报告说什么 | 什么时候 | | --- | --- | | **任务已结束。** / **任务已结束,分支保留(未合并)。** / **任务已结束,分支已删除(未合并)。** | 全部走完 | | **部分完成 —— 下面标出的仓库保留了原样** | 有仓库没完成 | | **没有任何仓库完成结束** | 全部停滞 | 每个仓库一行:`已合并到 X` · `已移除 worktree` · `分支已删除`,或 `未改动` / `未改动(合并冲突保留在原处)` / `未改动(合并已中止)`;行尾可能有 Git 输出折叠块。 下面是:**任务空间已删除** 或 **任务空间保留:<路径>**、**已归档到 <目录>:<名字>**、 **保留的遗留内容:<名字>**、以及若干警告行(例如记录你绕过了闸门)。 最后一行说工作区注册的去向:**工作区注册已移除**,或者因为空间还在而**保留**。 没跑完时有 **继续结束任务** 按钮:按同样的选项再收一次尾;如果刚才交给 agent 的会话还在干, 按钮会变成 **等待 agent 处理完成** 并暂时禁用。 > 报告会留着:点了 **打开会话** 或者关掉对话框之后,再回来重打开,进度和报告都还在, > 你接着上次的地方继续。 ### 7.6 可选:把提交和冲突交给 agent(实验性) > **Beta**:这两个入口的功能处于实验性验证阶段,行为可能继续调整。它们**默认显示**, > 想回到纯手工流程就把插件设置里的「交由 agent 解决冲突入口」设为「隐藏」。 > 遇到问题请到 [GitHub Issues](https://github.com/kangtsang/dsh-worktree-space/issues) 反馈。 结束任务对话框可以开一个 DSH agent 会话代为完成两件事: - 有未提交改动的仓库 → **交由 agent 提交**; - 停在合并冲突上的仓库 → **交由 agent 解决冲突**。 两条都:**不 push、不动其它仓库或任务空间、不把分支合并回目标分支** —— 最后那一步是插件的。 这些仓库尽量共用一个会话(同一个仓库已经有提交阶段的会话时直接复用),工作目录取它们边界的共同祖先; 共同祖先只剩盘根时取**任务空间**,那笔要写源仓库 `.git` 的提交可能需要在会话里批准提权。 对话框中部有这个开关: | 状态 | 会话开在哪 | 权限 | | --- | --- | --- | | **逐次授权**(默认) | 能同时盖住 worktree 与源仓库 `.git` 的最窄目录 | 写那之外的元数据时会在会话里问你一句 | | **完全权限** | 容器根(因此归到插件注册的那个工作区分组),开起后立即置为完全权限 | 不再逐次询问 —— **代价是那个会话此后可以写到这台机器上 DSH 进程能写的任何位置** | 对应设置项叫「交由 agent 处理冲突和提交的会话使用完全权限」(默认关),在对话框里就能改。 它**只影响之后开起的会话**;已经开出来的要在它自己的会话里切换。 无论走哪条路,底部的 **确认结束任务 / 继续结束任务** 始终要你自己按。 ### 7.7 收尾前的守卫与收尾后的清理 收尾前有三道会直接拒绝的前置检查 —— **都在动手之前**,所以此刻什么都还没动: | 码 | 含义 | 怎么办 | | --- | --- | --- | | `E5011` | 有 worktree 被本进程之外的东西占着(开发服务、浏览器、编辑器) | 关掉占用它的进程再收尾 | | `E5012` | 任务空间里**还有会话在活动** | 等那些会话空闲或停掉,再收尾;或者先保留任务空间 | | `E2005` | 目录里没有 `.worktree-space/worktree-space.json`,认不出这是本插件的任务空间 | 什么都没有被删;确认选对了目录,或从备份恢复那个记录文件 | `E5012` 的判据是三个信号缺一不可(注册表说在跑、日志里回合还没结束、收件箱还有排队工作), 对话框会在活动会话消失前直接把选项置灰并给出提示。 **收尾成功后会自动销毁部署环境**:策略部署过东西时,插件按 `dsh.env-id` 拆掉容器、网络与卷 —— best effort,失败只记一条警告,**绝不阻塞收尾**。想再看一眼验收地址就在收尾之前看完。 --- ## 8. 冲突与「停在工作上」 有些收尾**不是失败,是停在还没做完的工作上**。对话框给出的报告按**每仓库一行**说明,请先分清这三种: | 报告里的样子 | 含义 | 接下来谁做 | | --- | --- | --- | | 未改动(合并冲突保留在原处)+ 冲突现场路径 + 冲突文件清单 | 有一次没能走完的合并,冲突标记还在那些文件里 | 在那个 worktree 里**解决冲突并提交合并** | | 未提交变更 / 「N 个文件未提交,请处理后再结束任务」 | worktree 里有活没提交 | 在**该仓库自己的分支**上提交 | | 合并被跳过、源仓库的检出被点名有未提交改动 | **你自己的源码工作副本**里有改动,合并会覆盖它 | **你自己** —— 这不是这条流程该代提交或 stash 的 | 三者必须分清:第三种在 git 的措辞里看起来像冲突,但那里**没有开始过合并、也没有冲突标记可解**, 去翻冲突文件是徒劳的;而让这条流程代你提交或 stash 你自己的改动,比徒劳更糟。 **现场在哪**:停在冲突上的现场就是**任务分支自己的 worktree** (例如 `~/workspace/worktree-space/project1/hotfix/repo-a`),它正处在一次未完成的合并里。 在那里把冲突解决、`git add`,再用一条说明取舍的 `git commit` 完成本次合并提交 —— **目标分支和源仓库的检出全程未被触碰**。 提交合并之后(目标分支已经进到任务分支里),**继续结束任务**会跳过试合并直接真合并。 如果现场还留着没提交的解决结果、或者文件里仍有冲突标记,插件**不代为提交**,而是原样保留现场并点名。 --- ## 9. 排障 **第一件事:读操作日志。** 容器根下的 `worktree-space-log.jsonl`(默认开启)记录了这个容器里发生过的 每一步:改动仓库状态的 git 命令、任何一条失败的 git 命令、每个操作的结论、每个错误与警告(带错误码)。 凭据会先抹掉,不发送到任何地方。可以在插件设置里关掉,关掉只是不再写新记录,磁盘上已有的原样保留。 **然后按症状查:** | 症状 | 先看这里 | | --- | --- | | 创建被拒 | [3.4 创建失败怎么办](#34-创建失败怎么办) | | 部署起不来 / 验收地址不显示 | [6.5 部署卡片](#65-部署卡片)、[6.4 状态文件](#64-状态文件契约) | | 合并被拒说没有通过的冒烟 | [6.6 收尾闸门](#66-收尾闸门)(代码 `E5006`) | | 「确认结束任务」点不动 | [7.3 第一步](#73-三步提交合并冲突):有未提交改动,或者还有会话在活动 | | 收尾停在某个仓库上 | [第 8 节](#8-冲突与停在工作上) | | 认不出这是一个任务空间 | `E2005`([7.7](#77-收尾前的守卫与收尾后的清理)) | | 扫不到某个仓库 | 插件设置里的**扫描深度**、**最大遍历目录数**、**扫描忽略的目录名** | 完整的错误码与出路见[第 15 节](#15-错误码速查)。 --- # 第二部分 · Agent 自动编排 这一部分描述另一条同样受支持的用法:**把你当前会话里的 agent 当作整个流程的驱动者**, 它用工具把建空间、派活、盯进度、中途改需求、收尾合并这条链自动跑完,你不需要在面板上按任何按钮。 如果你打算自己一步步点,这一部分是背景知识,可以跳过 —— [第一部分](#第一部分--面板手动流程)已经完整。 > 这份文档是写给你看的,不是给 agent 下的指令。会话要照做的内容由插件随包提供的 skill > (`assets/skill/worktree-space/SKILL.md`)承载,装载后 agent 自己读它。 ## 10. 这条路径做什么 | 阶段 | 由工具完成的部分 | | --- | --- | | 建空间 + 派活 | `create` 一次做完:目录、worktree、分支、注册工作区、开一个会话并把任务书交给它 | | 盯进度 | 看会话自己的活动时钟,而不是猜。不轮询 | | 中途改需求 | 用 `steer` 把话插进正在跑的回合,长回合里也送得到 | | 异常回接与漏报提醒 | 插件主动通知队长:回合异常结束时报,正常结束却没交完结报告也报 | | 收尾合并 | `done` 一次完成合并、移除 worktree、清理;停在冲突或未提交上时,把会话指回去处理后再调一次 | | 部署验收 | 勾了部署的任务,由任务空间自己的入口 `up` / `smoke` 过冒烟闸门 | | 适合 | 不适合 | | --- | --- | | 无人值守:派出去就不用管,收尾也自动跑完 | 你就坐在屏幕前,想自己盯着每一步 | | 多任务并行:一个会话管好几个任务空间 | 只想建个空间、活自己干 | | 需要「活没干完就自动回接」的长任务 | 要放弃任务或丢弃文件(那要人,见[第 14 节](#14-只有人能做的几件事)) | ## 11. 术语补充:队长与队员 | 词 | 指什么 | | --- | --- | | **队长**(队长 Agent) | **指派任务的 Agent**:建任务空间、派活、盯进度、插话纠偏、收尾。你当前会话里的 agent 就是队长 | | **队员**(队员 Agent) | **被派到任务空间里的那个会话**。它有自己的上下文、自己的审批门、自己的工作目录,看不到队长的会话 —— 所以交给它的任务书必须自包含 | | **任务会话** | 同一个会话的另一种叫法:「队员会话 = 任务会话」 | **两者之间没有父子关系**:队员是任务空间下的会话,不是队长的子会话,所以既有的父子通道 (`send_message`)够不到它 —— 队长只能用 `wts_session_tool_*` 这八个工具(见 [附录](#附录会话控制工具参考))。 ## 12. 整条链怎么跑 ``` ① 建空间 + 派活 wts_worktree_space({ action: "create", task, sourceRoot, tasksRoot, deployTarget, prompt }) ← prompt 让建空间与派第一件活一次完成 → 返回 { container, branch, sessionId } ② 盯进度(不轮询,有理由时才看一眼) wts_session_tool_status({ sessionId }) ← lastActivity 判「卡住」还是「在跑长步骤」 wts_session_tool_read({ sessionId }) ← 分「没送到」与「没照做」 ③ 中途改需求 / 纠偏 wts_session_tool_send({ sessionId, mode: "steer", message }) ④ 异常回合会被主动告知(默认开) 收到通知 = 活没干完 → status 看停在哪 → send 把会话拉回工作 ⑤ 部署验收(勾了部署时) 任务空间自己的入口 up → smoke → 冒烟 pass 之后 ⑥ 才能过闸门 ⑥ 收尾 wts_worktree_space({ action: "done", task, project, tasksRoot, merge: true, strays: "archive" }) → 若停在冲突或未提交,把会话指回去处理,再调一次 ``` `wts_worktree_space` 一共六个动作: | action | 做什么 | | --- | --- | | `suggest-root` | 先问一遍:某个源码根下有哪些仓库、推荐容器根在哪。这是「建议位置」的来源 | | `create` | 建空间,`prompt` 同时把第一件活交给一个新会话 | | `dispatch` | 空间已经存在,派第二件及以后的活:在同一个任务空间里**新开一个会话** | | `add` | 给已存在的任务补仓库 | | `list` | 列某个容器根下的任务空间、它们的分支与会话 | | `done` | 收尾:合并、移除 worktree、处理遗留内容、清理 | **别对同一个任务空间连着 `dispatch` 两次** —— `dispatch` 每次都是新开一个会话,两个都以为自己该干活的 会话会改同一批文件、互相打断提交。要给已经在跑的会话补话,用 `wts_session_tool_send`。 **派出去的每一句话都会被前面一段标准前言兜住**:前言写清任务名、容器根、分支名、 「git 命令在各自仓库子目录里跑」、禁止在源码检出里改文件或提交、不得手写 `git merge`、 谁收尾由你决定(两种流程都支持)、策略是 `docker` 时额外一句「冒烟是这个任务的完成线」、 验收栈必须是本任务自己的、交付事实写进哪个文件、 超过一分钟的动作转后台、以及**回报渠道**:用 `wts_session_tool_send` 发给队长会话 id (默认 `steer`,立即送达;要它等回合边界的才显式传 `mode: "queue"`);**停手之前必须带 `finalReport: true` 交一次完结报告**——不交的话队长会收到「正常结束但没回报」的提醒,只能自己回到 这个会话的日志里找结论,而这件事会一直留在他手上。所以任务书就算漏写了纪律,那些也还在里面。 ## 13. 进行中的三个抓手 **盯进度。** `wts_session_tool_status` 的 **`lastActivity` 是唯一可靠信号** —— 文件时间、mtime、 日志尾部在构建期间都可能长时间不动,而会话自己的时钟不会。传 `stalledMsThreshold` 后, **只有在跑的会话**会被标 `stalled`;空闲会话「很久没事件」是正常状态。`lastReply` 回答 「它此刻以为自己在做什么」;`lastTurnEnd` 是「做完了」与「被错误打断后静默停摆」的唯一判据。 **插话纠偏。** `steer` 与 `queue` 的区别在这个场景里是硬要求: | mode | 何时被读到 | 什么时候该用哪个 | | --- | --- | --- | | `steer`(**默认**) | **打断当前回合,注入一步** | 几乎所有会话间通信都属于这一类:有时效的补话与纠正,以及"队长回复队员"这种必须立即送达的内容 | | `queue` | 该会话的**回合边界** | 少数可以等的消息,例如"这份参考材料你做完手上的再看"。显式传 `mode: "queue"` 才走这条路 | 派出去的长任务常常整单就是**一个长回合**,所以默认必须是 `steer`:通信不立即送达,就会被下一个边界一直挡住 ——而从外面看,就像会话「无视了要求」。要分辨「没送到」与「没照做」,用 `wts_session_tool_read` 读该会话的记录。 **异常回接与漏报提醒。** 这里有两种情况,插件的处理不同。 **一、回合是异常结束的。** 会话可能根本没机会回报:模型调用失败、撞输出上限、被 pre-step 拒绝, 或进程崩掉留下孤儿轮。插件监听回合是怎么结束的,`error` / `interrupted` / `blocked` / `max-tokens` 四种会主动通知队长会话(内容是任务名、队员会话 id、失败码与下一步指引,并**明说「活没干完,别当任务已完成」**)。 **每一轮异常都报**,不是只在最后报。 **二、回合正常结束,却没有交完结报告。** 这一条盯的是另一种漏法:活做完了、会话停了、报告没发。 队员在停止工作之前应当调用 `wts_session_tool_send` 并**显式传 `finalReport: true`**;插件在回合 以 `completed` 结束时去记录里查这一笔,查不到就通知队长一条「已正常结束、但没有交完结报告」, 文案同时说明**这不是失败**、并提醒**不要据此去唤醒那个会话**——先读它最后的输出再决定。 两条判据都落在任务空间的记录上:`finalReport: true` 会给该会话写下 `reportedAt`(可读本地时间), **中途的进展消息不写**,**收尾 `done` 也不写**——"收尾了但没回报"正是要报出来的情形,收尾绝不能 把它标成已回报。队长若在面板上直接看记录,`reportedAt` 就是"谁交过完结报告"的唯一判据。 `aborted` 一律不通知:那是被有意停下的(用户停的,或队长用 `cancel` 停的),不是"做完了没回报"。 此时发通知只会让队长去唤醒一个别人刚有意停掉的会话。 恢复链条总是:先 `status` 判「在跑但卡住」还是「已离线」→ 离线用 `resume` 拉回,丢了用 `create` 在**同一个任务空间**重开 → **无论哪条都先读那个会话的最后状态再决定发什么**,不要盲发。 `cancel` 是刹车:中止当前回合,`keepInbox` 决定排队中的工作是否保留。 **判断力留给你的 agent**,不要设定轮询或定时 steer/cancel。开关 `watchSession` 是按次的 (`create` / `dispatch` 都认,**默认开**),不想收通知的那一次传 `false` 即可。 ## 14. 只有人能做的几件事 下面标粗的几件事,工具那一边根本没有参数可以传,试多少次都只会得到同一个拒绝(`E7007`): | 想做的事 | 谁能做 | | --- | --- | | 合并、归档、保留 | 工具可以直接做 | | 部署 + 冒烟过闸门 | 工具可以跑入口脚本,然后收尾 | | 解冲突、提交合并 | 工具可以让会话去做,再 `done` | | **放弃任务**(不合并、强删分支) | **只能你在面板上做** —— 工具侧没有 `force` | | **丢弃未跟踪的文件**(`discard`) | **只能你在面板上做** —— 取消勾选「归档任务空间里的文档」就是永久删除 | | **闸门没过却要合并** | 去部署冒烟;或者你在面板上点「仍然结束」 | 这条不对称是刻意的:不可逆的操作只能由人当场决定。两条路径调用的是 **同一个收尾函数**,区别只在谁有权传那几个参数 —— 并不是「工具那边被拒、面板这边会照做」。 另一条边界:**即使在自动路径里,也没人会在源码检出里手写 `git merge`**,收尾一律走 `done` (把合并落在源仓库、把冲突留在任务空间自己的 worktree 里)。此外,**推远端永远是你自己的动作**。 --- ## 15. 错误码速查 码比句子可靠:一句话会随界面语言变,码不会。第一位数字是类别: `E1` 容器与隔离 · `E2` 任务空间生命周期 · `E3` git 与分支 · `E4` 参数与校验 · `E5` 收尾与合并 · `E6` 扫描与发现 · `E7` 包本身(意味着插件坏了,值得上报) · `E9` 未知的失败。 ### 创建与部署 | 码 | 含义 | 出路 | | --- | --- | --- | | `E1001`–`E1003` | 容器根就是源码根、在源码根里面、或者装着源码根 | 换个位置:容器根要能与源码根互不包裹 | | `E1005` | 容器根本身不合格(比如它自己是 Git 仓库) | 换个不是仓库的目录 | | `E2001` | 名字被**别的**任务空间占了 | 换任务名 | | `E2002` | 同名任务空间已在磁盘上、只是没注册 | 面板 **重新注册**,不要换名重建 | | `E3001` | 分支已被占用 | 换任务名 | | `E3002` | 起点 ref 在某个仓库里不存在 | 换一个每个选中仓库都有的分支或提交 | | `E4001` | 一个仓库都没选 | 至少选一个 | | `E4002` | 一个任务里两个仓库同名 | 改名后再选 | | `E4014` | 部署脚本不是工作区 `.worktree-space/deploy/` 下的现有文件 | 把脚本放进那个目录,或改路径 | | `E4015` | 勾了部署,但没有任何可用的部署入口 | 补一个入口脚本,或先用 `none` | | `E5008` | 部署脚本不存在,或重建环境时失败 | 看报错输出尾部;或补上部署资产 | | `E6001` | 指定的目录不是源仓库 | 换一个真的 Git 仓库 | ### 收尾与合并 | 码 | 含义 | 出路 | | --- | --- | --- | | `E2003` | 没有这个任务空间 | 确认路径 | | `E2004` | 任务空间里没有 git worktree | 确认这确实是一个任务空间 | | `E2005` | 认不出这是本插件的任务空间(没有记录文件) | **什么都没有被删**;确认选对目录,或从备份恢复记录 | | `E4007` | 要删一个没合并过的分支却没给 force | 要么合并,要么你自己从面板放弃这个任务 | | `E4008` | 合并目标分支不可用(不存在、就是任务分支本身等) | 换一个本地分支 | | `E5001` | 还有一次合并没结束 | 解冲突并提交合并,再收一次 | | `E5002` | 收尾没有在每个仓库都完成 | 看哪些仓库留下,逐个处理 | | `E5003` | 某个仓库收尾失败 | 再来一次 | | `E5004` | 源检出区有未提交改动,合并会覆盖它们 | **你自己**提交或 stash | | `E5006` | 没有记录到通过的冒烟,拒绝合并 | 部署一次 + 跑冒烟;或你在面板上点「仍然结束」 | | `E5011` | worktree 被进程外的东西占着 | 关掉占用进程再收尾;**什么都没动** | | `E5012` | 任务空间里还有会话在活动 | 停掉那些会话,或者先保留任务空间 | | `E3003` / `E3004` / `E3005` | 某条 git 命令失败 / 目录不是 git 仓库 / 检出已经不在了 | 按 git 自己的诊断处理 | ### 只有工具路径会遇到的码 | 码 | 含义 | | --- | --- | | `E7005` | 未知的 action | | `E7006` | 缺必传参数(目前只有 `done` 的 `merge`) | | `E7007` | 通过工具调用了不可逆操作(`force`、`strays: "discard"`)—— 只能由你在面板上做 | ### 已退休的编号 `E4013`、`E5005`、`E5007`、`E5009`、`E5010` 已停用,且**不会再分配给别的含义**。 在旧日志或旧对话里看到它们,按上表对应的新编号理解即可。 --- ## 附录:会话控制工具参考 任务空间里的活交给一个**独立会话**(队员):它有自己的上下文、自己的审批门、自己的工作目录。 队长会话**不是**它的父会话,所以既有的父子通道会直接拒绝(`belongs to another parent session`)。 这八个工具是唯一的通道。 ### 八个工具 | 工具 | 做什么 | | --- | --- | | `wts_session_tool_find` | 按工作目录 / 标题 / id 找会话,返回 `sessionId`、是否在线、工作目录、标题 | | `wts_session_tool_status` | 看进度:`running`、`openTurn`、`lastActivity`、`pendingWork`、`lastReply`、推理尾巴;离线会话从持久化日志回答 | | `wts_session_tool_read` | 读消息(条数、`role` 过滤、`sinceSeq` 翻页),在线与离线都能读 | | `wts_session_tool_send` | 发消息,`mode` 取 `queue` 或 `steer`;`finalReport: true` 表示这一条就是本任务的完结报告 | | `wts_session_tool_create` | 在指定目录(任务空间)开一个新会话,可同时交办一条自包含的 prompt | | `wts_session_tool_resume` | 把离线会话拉回在线,之后可直接 `send` / `read` / `wait` | | `wts_session_tool_wait` | 等这一次输出;超时返回**已经存在的**最近输出并标 `stale`,不是空 | | `wts_session_tool_cancel` | 中止当前回合,可选是否保留排队中的工作 | ### 主要参数 | 工具 | 参数 | | --- | --- | | `find` | `query`、`sessionId`、`title`、`cwd`(都是子串匹配)、`liveOnly`、`limit`(默认 10,上限 50) | | `status` | `sessionId`(必填)、`stalledMsThreshold`(默认 60000)、`recent`(默认 8,上限 20)、`reasoning`(`none`/`last`/`live`/`tail`,默认 `tail`) | | `read` | `sessionId`(必填)、`sinceSeq`、`limit`(默认 20,上限 100)、`role`(`user`/`assistant`/`both`)、`includeReasoning`(默认 true) | | `send` | `sessionId`、`message`(必填)、`mode`(**默认 `steer`** / 显式 `queue`)、`timeoutMs`(默认 180000,上限 3600000)、`finalReport`(默认 false;置 true 才写 `reportedAt`) | | `create` | `cwd`(绝对路径,任务空间)或 `workspaceId`、`prompt`、`timeoutMs` | | `resume` | `sessionId`(必填) | | `wait` | `sessionId`(必填)、`sinceSeq`(`-1` 计所有事件)、`timeoutMs`、`requireTurnEnd` | | `cancel` | `sessionId`(必填)、`reason`(**必填**,自由文本;缺失或空白即拒)、`keepInbox`(默认 false,即清掉排队的工作)、`cause`(可选简短标签) | ### 何时用哪个 - **判「卡住」还是「在跑长步骤」,只看 `lastActivity`**,见[第 13 节](#13-进行中的三个抓手)。 - **有时效性的一律 `steer`**,`queue` 只用于「到回合边界再看」的内容。 - **`find` 通常不必用**:`create` / `dispatch` 已经把会话 id 写进任务记录,`list` 也会报出来, `find` 只是那个 id 不在手时的兜底。 - **超时不是空答案**:`wait` 预算跑完时返回已存在的最近输出并标 `stale`,所以总能知道它最后说了什么。 --- > 有 bug 或漏说的地方,请到 > [GitHub Issues](https://github.com/kangtsang/dsh-worktree-space/issues) 反馈。