dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DeepSeek Harness plugin: one click binds each conversation to its own isolated git worktree + branch, so parallel agents work on one repo without interference — every project's worktrees auto-managed in the sidebar workspace tree, backed by 9 git tools.
面向 DeepSeek Harness 的 对话 ↔ 工作树绑定 插件:让多个会话在同一项目上并行工作,每个会话绑定一个独立 git 工作树 + 分支,一键创建、互不干扰(绑定是"专属工作树"标记,不是独占锁;真正的保护是同机占用检查,见下文)。
DeepSeek Harness plugin: bind every conversation to its own isolated git worktree + branch in one click, so parallel agents work on one repo without stepping on each other. A binding is a "dedicated worktree" marker, not an exclusive lock; the real protection is the same-host occupancy check described below.
git_session_binding、git_repo_status、git_worktree_*、git_branch_*),加上 4 个任务工具(git_task_start、git_task_status、git_task_cancel、git_task_integrate),每个会话都可以调用。git_session_binding, git_repo_status, git_worktree_*, git_branch_*) plus 4 task tools (git_task_start, git_task_status, git_task_cancel, git_task_integrate) every conversation can call.DSH 中,会话的工作目录(cwd)在创建时冻结,工作区归属要求"会话 cwd == 工作区路径"。因此绑定只能在创建会话的那一刻发生——把会话建在专属工作树里,绑定关系即成立:
A DSH session's cwd is frozen at creation and workspace membership requires the session's canonical cwd to equal the workspace path. So a binding happens at creation time: the conversation is born inside its own worktree:
~/projects/foo/ ← 主工作树 / primary worktree (main)
├── .dsh-wt/
│ ├── feature-a/ ← 会话 A 绑定 / conversation A is bound here (branch feature-a)
│ └── bugfix-b/ ← 会话 B 绑定 / conversation B is bound here (branch bugfix-b)
└── (共享同一个 .git / one shared .git)
绑定关系无需额外存储,由 sessions.list(cwd)× git worktree list(路径→分支)× workspaces.list 推导而来——左侧树与 agent 工具看到的都是实时事实。
Bindings are derived, not stored: sessions.list (cwd) × git worktree list (path→branch) × workspaces.list — what the sidebar tree and the agent tools show is live fact.
bound 只表示"本会话拥有一个专属(非主)工作树",不表示独占:另一个会话完全可以共享同一个工作树,bound 也不会告诉你还有谁在写入。真正的并发保护是同机检查(same-host check),不是跨进程锁,也不是 shell 沙箱:
bound means only "this conversation has its own dedicated (non-primary) worktree" — it is not exclusivity. Another conversation may share that same worktree, and bound says nothing about who else is writing. The real protection is a same-host check, not a cross-process lock and not a shell sandbox:
查询绑定时:当宿主暴露原生 agents 注册表,且另一个 RUNNING 的原生 agent(包括子 subagent)按"最长匹配"规则解析到同一工作树时,git_session_binding 会以清晰的占用错误失败,而不是给出一个看起来安全的绑定;调用者自身被忽略,idle 的同伴不会被当作正在写入。
删除工作树时:git_worktree_remove 只要目标工作树里有 RUNNING 的 agent(包括调用者自己),即使带 force 也拒绝;force 只豁免 git 的脏工作树检查。本插件运行中的 git_task_* 任务也占用其工作树(含 preparing setup 阶段和尚未完成的释放),此时同样拒绝删除,且不依赖 agents 服务。
UI 侧同样列出工作树上的全部会话(含 subagent),只要有 running 会话就停用删除并说明原因。
最小 profile:没有 agents 服务时检查自动跳过,工具照常可用——这是有意的能力降级,不要把它当作安全保障。
When querying the binding: when the host exposes the native agents registry and another RUNNING native agent (a child subagent included) resolves to the same worktree under the same longest-match rule, git_session_binding fails with a clear occupancy error instead of a safe-looking binding. The caller itself is ignored; idle peers are never reported as actively writing.
When removing a worktree: git_worktree_remove refuses while any RUNNING agent (the caller included) is in the target worktree, even with force; force only waives git's dirty-worktree check. A worktree owned by a live git_task_* task (including the preparing setup phase and an unfinished disposal) is likewise refused, and that guard does not depend on the agents service.
The UI likewise lists every conversation on the worktree (subagents included) and disables deletion with an explanation while one is running.
Minimal profiles: without the agents service the check is skipped and the tools keep working — a deliberate degradation, not a safety guarantee.
删除工作树是 git 优先、分阶段执行的:先真正删除 git 工作树,再(按选择)归档绑定会话,最后注销工作区文件夹。结论固定、重试安全:
Worktree removal is git-first and staged: remove the git worktree, then (if chosen) archive the bound sessions, then unregister the workspace folder. The outcome is fixed and retries are safe:
git 删除失败 → 不归档、不注销,确认框保留并报告原因,可直接重试。
git 删除成功之后的归档或注销失败 → 明确报错,重试只执行未完成的步骤(绝不会再删一次 git 路径);待归档的会话 id 在删除后冻结,不因绑定重新解析(工作树已消失)而丢失。
不吞掉注销失败(旧版本会静默忽略)。
git removal failed → nothing archived/unregistered; the confirm stays open with the reason and can be retried.
archive/unregister failed AFTER a successful git removal → visible error, and a retry runs only the unfinished steps (never a second git removal); the pending session ids are frozen across the retry and are not lost when the post-removal binding re-resolution finds the worktree gone.
Unregister failures are surfaced, not swallowed.
确认框用中文说明两种失败:Git 拒绝删除时会提示"目录仍然存在,请先保存/提交/stash 后重试"(不会自动 --force);Git 删除之后的清理失败会明确"目录已删除、清理未完成"。原始 git 报错保留在折叠的"技术详情"里。The confirm explains both failures in Chinese: a refused git removal says the directory is retained and to save/commit/stash first (never an automatic --force); a post-removal archive/unregister failure says the directory is gone but cleanup is pending. The raw git error stays available under a collapsed "技术详情 (technical details)".
name 形式的默认工作树位于 <repo>/.dsh-wt/<name>,嵌套在自己的仓库里;若不加处理,git add -A 会把它当作 gitlink/嵌入式仓库暂存。插件会在共享的 <common-git-dir>/info/exclude(不碰被跟踪的 .gitignore)里为配置的 name-based 父目录写入一条锚定规则(如 /.dsh-wt/):
A name-based worktree lives at <repo>/.dsh-wt/<name>, nested inside its own repository; without care, git add -A would stage it as a gitlink/embedded repo. The plugin writes one anchored rule (e.g. /.dsh-wt/) into the shared <common-git-dir>/info/exclude (never the tracked .gitignore) for the configured name-based parent:
保留已有字节、幂等(重复创建不重复写)、并发的同进程创建经写链串行化;含空格/元字符的目录名会正确转义。
只针对仓库内的 name-based 父目录;显式 path、仓库外的父目录、仓库根本身都不会被写入。
写入失败会报错(工作树已建,但明确告知未被保护),而不是静默返回"成功"。
Existing bytes are preserved, the rule is idempotent, concurrent in-process creates are serialized through a write chain, and directory names with spaces/metacharacters are escaped.
Only a name-based parent inside the repository is touched; an explicit path, an outside-repo parent, or the repository root never is.
A write failure is reported (the worktree exists but is explicitly unprotected) rather than silently returning success.
name 形式且未给出显式 branch/newBranch/detach/commitIsh 时,插件显式 git worktree add -b <路径末段> 创建全新分支——绝不会静默复用一个已存在但未检出的分支。unique: true 时冲突会新建带后缀的工作树+分支(-2、-3…);unique: false 时直接报 git 的严格错误;force 也不绕过。显式 branch(复用已有分支)、newBranch(指定新分支名,冲突报错)、detach、commitIsh 语义保持不变;path 形式仍沿用 git 自身的推断。
A name-based request with no explicit branch/newBranch/detach/commitIsh runs git worktree add -b <last path component> to create a fresh branch — it never silently reuses an existing unchecked-out branch. With unique: true a collision becomes a new suffixed worktree+branch (-2, -3, …); with unique: false git's strict error surfaces; force does not bypass it. Explicit branch (reuse), newBranch (named, conflict fails), detach, and commitIsh keep their meanings; explicit path keeps git's own inference.
主会话可以用 4 个任务工具把明确的独立任务派发到各自的工作树,并在验证后合并(原有 9 个 Git 工具保持兼容):
The main conversation can fan explicit, independent tasks into their own worktrees and integrate them after verification (the original 9 Git tools stay compatible):
git_task_start({ name, prompt, repo?, base? }) — 创建全新工作树 + 分支(<repo>/<worktreesDir>/<name>,重名自动后缀),写入本地任务记录后立即返回 preparing;随后在任务生命周期内顺序执行配置的 setupCommands,再以调用者 agent 为父创建一个原生子 agent,其 cwd 在创建时冻结为该工作树,继承调用者的模型与权限(只收窄、不扩大:approval 固定 never,sandbox 沿用父级 override)。子任务有深度上限(主 agent 深度 0 → 任务子 agent 深度 1),不能再次 git_task_start。多个 git_task_start 可并发。git_task_status({ repo?, taskId? }) — 列出本仓库(记录写在共享 git common dir 下,linked worktree 同样可见)的任务:状态、工作树/分支、子会话 id、最终摘要、commit、错误。另一个仍存活的 DSH 进程拥有的活动任务按原样报告;只有能证明属主进程已消失时才标记 interrupted(用 kill(pid, 0) 存活探测,不发送任何信号)。git_task_cancel({ taskId }) — 只停止本进程拥有的任务:中止 setup 或取消并排空子 agent,持久化 cancelled,保留工作树与分支。已结束任务幂等;属于其他存活进程的任务只报告限制,绝不向其发信号。插件/父级 teardown 会取消并释放自己启动的全部子 agent。git_task_integrate({ repo?, taskId, verifyCommands? }) — 把已完成、已提交、干净的任务分支合并进目标工作树(repo,默认调用者工作树),只用真实 git,绝不 force/reset/clean、绝不 push/deploy。拒绝:活动任务、脏源/脏目标、源未提交、源==目标、目标或源内有其他 RUNNING 写入者(允许调用者自己所在的 target)。合并基于已记录并核对的 HEAD;冲突保留(不自动 abort/reset)并给出恢复说明。合并成功后顺序执行 verifyCommands(argv 数组、无 shell),全部通过才 verified;失败标记未验收,重试只重跑验证、不重复合并(即使目标 HEAD 已前进也不复用旧验证)。未提供命令则明确 verified=false(未独立验证)。源工作树/分支保留,由用户自行清理。原生 spawn/fork 行为不变;任务入口是"独立写入"的推荐入口。The native spawn/fork behavior is unchanged; the task entry is the recommended isolated-writing path.
setupCommands 为可选配置(默认 []),不经过 shell、不会自动复制 .env 或安装任何东西;每条命令在新任务工作树内按顺序执行,失败/超时/取消会得到有界诊断并在启动子 agent 之前停止(工作树保留供排查)。空数组以外、空 argv 数组、空字符串段或非数组形式都会在插件加载时报错。
setupCommands is optional (default []), never shell-interpolated, and never copies .env or installs anything implicitly. Commands run sequentially inside the new task worktree; a failure/timeout/cancel returns bounded diagnostics and stops before the child agent launches (the worktree is kept for inspection). An empty outer array is allowed, but empty argv arrays, empty-string segments, or a non-array shape fail plugin loading.
- id: git-worktree
name: dsh-git-worktree
config:
setupCommands:
- ["pnpm", "install", "--frozen-lockfile"]
- ["node", "scripts/check-env.mjs"]
captureDelegatedPolicyOverrides + applyChildComposition + resolveChildAgentOptions),但看不到调用者会话内容,必须在 prompt 中给出自包含的完整任务。Task children inherit the caller's model, permission scope, and tool composition via the DSH delegation helpers, but cannot see the caller's conversation — give every task a self-contained prompt.git_task_integrate 的验证命令必须只做只读检查:若它修改了目标的已跟踪文件/索引或移动了 HEAD,结果为未验收,改动被保留(绝不 reset),需要人工清理后重试;重试只重跑验证、不重复合并。git_task_integrate verification commands must be read-only checks: if one modifies a tracked target file/index or moves HEAD, the result is unverified, the changes are preserved (never reset), and a cleanup + retry reruns verification without repeating the merge.本插件不替换原生 spawn/fork,也不新增独立面板;任务协作只以 agent 工具的形式提供,管理仍沿用既有左侧工作区树(见下文)。侧边栏不显示任务工作树以外的专用看板。
The plugin does not replace native spawn/fork and adds no separate dashboard; task collaboration is agent-tool-only and management stays on the existing sidebar workspace tree (below). No dedicated task board is added to the sidebar.
点击项目文件夹(仓库)行的 +,输入功能名,点 创建绑定会话:
git worktree add -b <name> <repo>/.dsh-wt/<name>(重名自动加后缀 -2、-3…)workspaces.create({ path })(幂等注册工作区)connectWorkspace → 会话直接诞生在该工作树(绑定成立)输入框有固定标签"工作树名称"和示例(如 login-page);名称会被规范化(空格等转为 -),有变化时先预览将创建的路径与分支,非法名称(如 ///、...、空白)会禁用创建并说明原因。仅创建工作树成功后不自动开会话,而是保留弹层显示真实路径/分支以及"打开绑定会话""完成";若工作树已创建但会话打开失败,重试只重开会话,绝不重复创建工作树。
On the sidebar tree, click the + on the project (repo) folder, type a feature name, hit 创建绑定会话 (Create bound conversation):
git worktree add -b <name> <repo>/.dsh-wt/<name> (name collisions auto-suffix -2, -3, …)workspaces.create({ path }) (idempotent workspace registration)connectWorkspace → the session is born inside that worktree (the binding)The input carries a persistent "工作树名称" label and an example (login-page); the name is sanitized (spaces become -) and a preview shows the target path/branch when it changes, while invalid names (///, ..., blank) disable creation with a reason. A successful worktree only run keeps the popover open with the real path/branch plus "打开绑定会话 (open bound conversation)" / "完成 (done)" actions; if the worktree exists but opening the conversation fails, the retry only re-opens it — never a second worktree create.
已存在的工作树行上的 + 是「在该工作树新建会话」——会话出生即绑定(agent 先建好工作树、开发者一键确认的场景)。工作树行还有 删除工作树 按钮:确认框列出全部绑定会话(含 subagent;只要有 running 会话就停用删除并说明原因)、可一并归档(日志保留,侧边栏隐藏)、以 git 优先的顺序删除工作树并注销其文件夹;删除后的归档/注销失败会明确报错且只重试未完成步骤(见"清理失败恢复")。确认框在没有绑定会话时不显示归档勾选框并说明分支仍保留,分支行只显示短 SHA(完整值在悬停提示中)。
A worktree row's + starts a new conversation in that worktree — born bound (the agent-prepares / developer-confirms flow). The row also has 删除工作树 (Remove worktree): the confirm lists every bound conversation (subagents included; any running one disables deletion with an explanation), can archive them (logs stay; the sidebar hides them), removes the git worktree git-first and unregisters its folder; a post-removal archive/unregister failure is reported and only the unfinished steps retry (see "Cleanup failure recovery"). With no bound conversations the archive checkbox is omitted and the panel notes the branch is retained; the branch line shows a short SHA (full value on hover).
# 在插件目录下执行 / from the plugin checkout
dsh plugin --profile web add /path/to/dsh-git-worktree
# 重启 dsh web — 工具与树界面同时生效 / restart dsh web — tools and the tree surface activate together
卸载 / Removal: dsh plugin --profile web remove dsh-git-worktree。
所有工具都接受 repo 参数(默认取会话工作区;相对路径会基于它解析)并返回 JSON。git 分支/工作树是共享可变状态,故都不声明并发安全。
All tools accept repo (default: the session workspace; a relative path resolves against it) and return JSON. None is concurrency-safe by design — branch/worktree state is shared mutable state.
| 工具 | 用途 |
|---|---|
git_session_binding | 开场必查:本会话的绑定——仓库、所在工作树、检出分支、peer 工作树。bound 仅在拥有专属(非主)工作树时为 true,不代表独占;有 RUNNING 原生 agent(含子 subagent)解析到同一工作树时以占用错误失败(调用者自身忽略,idle 同伴不算写入)。无 agents 服务时跳过检查。 |
git_repo_status | 分支、领先/落后、脏文件;查询本会话所在仓库时附带 binding 块(bound 同为非独占语义)。 |
git_worktree_list | 列出所有工作树(含分支/HEAD),标记 主工作树 与 当前会话。 |
git_worktree_add | 创建工作树。name → <repo>/.dsh-wt/<name>(锚定主仓库根,从工作树内创建也不会嵌套);或显式 path。name 且无显式 branch/newBranch/detach/commitIsh 时 -b 创建全新分支(不复用已有分支);unique 冲突时新建 -2、-3… 工作树+分支;显式 newBranch 冲突报 git 原错。仓库内 name-based 父目录会写入本地 info/exclude,git add -A 不会把工作树当 gitlink 暂存。 |
git_worktree_remove | 删除工作树(分支保留)。只接受已注册的工作树;主工作树会被拒绝;目标内有 RUNNING 原生 agent(含调用者)时即使 force 也拒绝;force 仍可删除含未提交改动的工作树。 |
git_branch_list | 列出分支:短 sha、检出标记(调用者所在工作树的 HEAD)、上游;all 包含远程分支。 |
git_branch_create | 创建分支;switch 立即检出;from 指定基于哪个提交/分支。 |
git_branch_switch | 切换当前工作树的分支;create 表示不存在时先创建。 |
git_branch_delete | 删除分支;force = -D。git 会拒绝删除在任何工作树中已检出的分支——这是保护机制,直接呈现而非绕过。 |
git_task_start | 在全新工作树+分支中启动一个独立任务,并创建一个 cwd 冻结在该工作树的原生子 agent(继承调用者模型/权限,不扩大;有深度上限)。可配 setupCommands,失败则不启动 agent。立即返回 preparing。 |
git_task_status | 列出/查询本仓库的任务记录(状态、工作树、分支、子会话 id、摘要、commit、错误);记录在共享 git common dir,linked worktree 可见;死属主活动记录标记 interrupted,存活外部属主不覆盖。 |
git_task_cancel | 停止本进程拥有的任务(中止 setup 或排空子 agent),保留工作树与分支,持久化 cancelled;已结束幂等;外部存活任务只报告限制。 |
git_task_integrate | 把已完成任务分支合并进目标工作树并运行 verifyCommands;拒绝活动/脏/未提交/source==target/运行中写入者;冲突保留待手动解决;验证失败不算验收,重试不重复合并;不 push/deploy。 |
| Tool | Purpose |
|---|---|
git_session_binding | Call first: this conversation's binding — repo, the worktree its workspace lives in, the checked-out branch, peer worktrees. bound is true only with a dedicated (non-primary) worktree — not exclusivity; another RUNNING native agent (a child subagent included) resolving to the same worktree yields a clear occupancy error (the caller itself is ignored; idle peers do not count as writers). Skipped without an agents service. |
git_repo_status | Branch, ahead/behind, dirty entries; attaches the session's binding when querying its own repo (bound has the same non-exclusive meaning). |
git_worktree_list | Every worktree with branch/HEAD, marked primary and this session. |
git_worktree_add | Create a worktree. name → <repo>/.dsh-wt/<name> (anchored to the main repo root, never nested inside the caller's worktree); or explicit path. A name with no explicit branch/newBranch/detach/commitIsh creates a fresh branch with -b (never a silent reuse); unique collisions become a new -2, -3, … worktree+branch; explicit newBranch collisions surface git's error. A name-based parent inside the repo gets a local info/exclude rule so git add -A does not stage it as a gitlink. |
git_worktree_remove | Remove a worktree (branch kept). Only registered worktrees accepted; the primary worktree is refused; a RUNNING native agent (the caller included) in the target refuses removal even with force; force still removes uncommitted changes. |
git_branch_list | Branches with short sha, checked-out marker (the caller worktree's HEAD), upstream; all includes remotes. |
git_branch_create | Create a branch; switch checks it out; from picks the base. |
git_branch_switch | Switch the current worktree's branch; create creates it first. |
git_branch_delete | Delete a branch; force = -D. Git refuses branches checked out anywhere — that protection is surfaced, not bypassed. |
git_task_start | Start an independent task in a fresh worktree+branch and a native child agent whose cwd is frozen there (inherits the caller's model/permissions, never widened; depth-capped). Optional setupCommands run first; a failure never launches the agent. Returns preparing promptly. |
git_task_status | List/query this repository's task records (state, worktree, branch, child session id, summary, commit, error); records live in the shared git common dir and are visible from linked worktrees. A dead owner's active record is marked interrupted; a live foreign owner is not overwritten. |
git_task_cancel | Stop a task this process owns (abort setup or drain the child), preserve the worktree/branch, persist cancelled; idempotent on finished tasks; a foreign live task is only reported. |
git_task_integrate | Merge a completed task branch into a target worktree and run verifyCommands; rejects active/dirty/uncommitted/source==target/running-writer cases; conflicts are preserved for manual resolution; failed verification is not acceptance and a retry never re-merges; no push/deploy. |
当前 DSH(0.1.7-rc.2)的 ui-workspace 只声明 sidebar.workspaces.directoryFlow,不再有 sidebar.workspaces.create 链。因此浏览器端只注册一个 slot(sidebar.footer.action),并从该挂载点直接观察工作区树的 DOM,注入每行控件:
Current DSH (0.1.7-rc.2) ui-workspace declares only sidebar.workspaces.directoryFlow, not the sidebar.workspaces.create chain. The browser half therefore registers a single slot (sidebar.footer.action) and installs the per-row controls directly into the observed tree DOM:
sidebar.footer.action(渲染为空 / renders nothing,弹层打开时才渲染) — 一个无 UI 的同步挂载点。监听工作区列表,对每个工作区调用 /dsh-git-worktree/list:确保仓库根(主工作树)工作区存在并标记 <项目名>(主工作树),确保每个工作树路径都注册为工作区;失效清扫——auto 注册的、工作树已从 git 消失且无会话的文件夹会被自动注销(跟随 git)。20 秒静默轮询覆盖 agent 工具/CLI 在工作树层面的改动。同时它观察侧边栏工作区树,按标签把行匹配到工作区并注入控件。A renderless sync mount (popovers render from it). Watches the workspace list, queries /dsh-git-worktree/list per workspace: ensures the repo-root (main worktree) workspace exists and is marked <project>(主工作树), ensures every worktree path is registered as a workspace; stale sweep — auto-registered folders whose worktree disappeared from git and hold no sessions are unregistered again (the tree follows git). A 20s quiet poll covers worktree-level changes made by agent tools/CLI. The same mount observes the sidebar tree, matches rows to workspaces by label, and injects the controls.浏览器端通过 ctx.webServer 以同源方式调用 /dsh-git-worktree 前缀下的宿主路由(list / status / branches / add / remove / bindings)。读路由(list / status)对不在任何 git 仓库内的路径返回 { notARepo: true }(200);bindings 对每个输入路径逐条标记 notARepo/工作树归属;写操作与 agent 工具仍保持严格报错。按 dsh-host-webserver 的文档约定,这些路由没有鉴权——请保持默认的 loopback 绑定。
The browser talks to host routes under /dsh-git-worktree (list / status / branches / add / remove / bindings), served same-origin by ctx.webServer. The read routes (status, list) answer a path that is not inside a git repository with { notARepo: true } (200); bindings flags each input path per-row; mutations and the agent tools keep failing strict. Per dsh-host-webserver's documented posture these routes have no auth — keep the bind host on the loopback default.
同步是合并式的:同一时刻至多一次扫描在途,扫描期间的变更折叠为一次收尾重扫;仅当检测到的工作树集合真正变化时才发布给行级 UI(避免无谓重渲染)。
Sync is coalesced: at most one scan in flight, later requests folded into a trailing re-run; the row-level UI is republished only when the detected worktree key sets actually move.
插件只注册到 sidebar.footer.action(自动同步 + DOM 行集成),让左侧工作区树成为完整的绑定管理界面。当前 ui-workspace 把每个工作区渲染为平铺的项目行(不再按路径嵌套),因此每条工作树是独立的一行,插件在该行上注入对应控件:
The plugin registers only into sidebar.footer.action (auto-sync + DOM row integration), making the sidebar tree the complete binding-management surface. Current ui-workspace renders each workspace as a flat project row (no path nesting), so every worktree is its own row and the plugin injects the matching controls on it: