dsh-crew
English | 中文
在 DeepSeek Harness (dsh) 里, 用一个小团队(多个角色 agent)来完成工作。
你自己的 dsh 会话会变成产品经理(PM)。PM 是唯一直接和你对话的角色。它先写清楚 "什么算做完",请你确认,然后启动架构师做设计、工程师写代码、评审来把关。 各角色之间不能互相说话——他们通过磁盘上的文件协作,消息由 PM 转达。
0.9.0 版本。
- "什么算做完"写成你自己仓库里的一节,而不是一份会被丢掉的文件。
- 编程语言和技术栈先定下来,开工之前由你批准。
- QA 的用例留在磁盘上,后面每个任务都会再跑一遍。
- 每个做完的任务都会把四道评审记进你的仓库:任务小节顶上一行。
- 范围或契约的每一次变更,都有一份书面的变更请求文档。
- 推送必须有你的许可,推完之后 PM 会盯着 CI。
- 作业中断之后还能接着干。
- 角色:PM、调研、架构师、工程师、QA、代码评审、安全评审、文档评审。
两个平面
dsh 把面向模型的工具放在**agent 预设(preset)**里,而不是 profile 里。dsh-crew 遵循 这一点,把自己拆成两半:
| 部分 | 放在哪 | 为什么 |
|---|---|---|
| PM 规则 | 宿主平面(你的 profile) | 它不需要任何工具,所以在任何预设、任何会话里都生效 |
| 角色工具 | crew agent 预设 | 角色的白/黑名单会在子 agent 启动时按预设的工具集校验,名字必须和预设定义在同一个地方 |
装好插件后第一次启动 dsh,预设会被写入 $DSH_HOME/.agent-presets/crew。
想用角色,就把会话切到 Crew 预设。在别的预设里,PM 依然是 PM,它会发现自己没有
角色工具,并请你选择:换到 crew 预设,还是由它自己独立完成。
crew 预设就是 dsh 自带的 standard 预设,只改了一处:去掉 subagent、
subagent_fork、workflow、ralph 和产品化子 agent,换成 crew 角色。所以在这个预设
里,只有 crew 角色能启动 agent。
为什么团队是"扁平"的
dsh 对 agent 有三条硬规则,本设计完全按它来:
| dsh 规则 | 在这里意味着 |
|---|---|
| 消息只能发给直接子 agent | 每个角色都是 PM 的直接子 agent,PM 能联系所有人 |
子 agent 只能回复直接父 agent(report) | 所有回答都回到 PM |
| 两个子 agent 之间完全不能通信 | 角色之间用文件协作,不用聊天 |
如果由架构师去启动工程师,PM 就完全联系不到工程师了。所以只有 PM 能启动 agent。 这一条由四道彼此独立的保险守住:
- 每个角色都被禁用了全部 crew 委派工具;
- 每个角色工具都设了
maxDepth: 1,所以 crew 的子 agent 不能再启动一个 crew 子 agent——而且这一道不依赖任何工具名,改预设也削弱不了它; - crew 预设本身还去掉了其他所有能启动 agent 的方式,角色无法绕过名单从
workflow、ralph或裸subagent走; - dsh 自己在发消息时查血缘:兄弟不是孩子,所以哪怕角色手里有那个工具,消息也会被拒绝。 这一道不点任何工具名、不靠任何提示词措辞,所以改过滤器或改 persona 都削弱不了它。
"角色"到底是什么
角色不是 PM 临时粘贴的一段提示词,而是基于 @deepseek-ai/dsh-tool-subagent 生成的
真实委派工具:
| 角色 | 工具 | 人设文件 | 可用工具 |
|---|---|---|---|
| 调研 | crew_researcher | roles/researcher.md | 只有 read、glob、grep、write、web_search——没有 shell |
| 架构师 | crew_architect | roles/architect.md | 除 crew 工具外都能用 |
| 工程师 | crew_engineer | roles/engineer.md | 除 crew 工具外都能用 |
| 测试工程师 | crew_test_engineer | roles/test-engineer.md | 除 crew 工具外都能用 |
| 代码工程师 | crew_code_engineer | roles/code-engineer.md | 除 crew 工具外都能用 |
| QA | crew_qa | roles/qa.md | 除 crew 工具外都能用——它必须真的跑起来 |
| 代码评审 | crew_code_reviewer | roles/code-reviewer.md | 只有 read、glob、grep |
| 安全评审 | crew_security_reviewer | roles/security-reviewer.md | 只有 read、glob、grep |
| 文档评审 | crew_doc_reviewer | roles/doc-reviewer.md | 只有 read、glob、grep |
所以评审无法修改文件,即使它自己想改也不行。人设会作为那个子 agent 自己的系统 提示词固定下来。
每份人设里还各有一节 "你能写什么":这个角色能写哪几类文件,以及哪几类它必须拒绝
——哪怕简报把它递过来也不写,首先就是那份判它自己工作的文档。读不受限。 这十节
都在 roles/ 里,装好这个包就能自己读。
这九个角色里有三个会去做一个任务,PM 启动哪一个取决于任务的形状:crew_engineer
一个人把一个任务的单元测试和产品代码都写掉,这是默认;而 crew_test_engineer 和
crew_code_engineer 把一个任务拆成两半——一个只写单元测试,另一个只写产品代码,而且
在写的过程中谁也看不到对方那一半。下面双人形状那一节讲它怎么跑、它证明什么。
评审改用"白名单",是两次实测逼出来的:
- 只禁
write和edit时,它用echo hello > file照样建出了文件——shell 本身 就是写文件的工具。 - 连
bash也禁掉之后,它自己报出来的工具里仍有workflow、ralph和一整套控制 桌面的 MCP 工具——每一个都是缺口。
黑名单永远列不全"以后才装上的工具",白名单不需要列。diff 由 PM 贴进评审任务里, 需要跑的命令也由 PM 代跑。
修改角色
角色人设就是 roles/ 下的普通 markdown。把文件复制到 ~/.dsh/crew/roles/,用同名
即可替换自带版本。唯一限制:提示词里不能出现 {{——dsh 会把它当变量解析,插件会在
启动时直接报错并告诉你是哪个文件。
角色的工具名单和按角色指定模型,配置在角色所在的位置:
~/.dsh/.agent-presets/crew/agent.cordis.yml 里的 dsh-crew-roles 那一行。
这个文件在装好的 preset 文件夹里,而 dsh-crew 升级时会整个替换该文件夹。你改过的
文件会以 agent.cordis.yml.bak 的名字留在旁边,启动日志也会点名——但里面的设置
不会自动回来。升级之后,请把你的改动重新抄进新文件。
一次作业怎么跑
-
PM 先把你的需求分到两条通道之一:
ask(只回答)或team(完整流程)。 任何大小的改动——一个错别字、一次改名、一行修复、一整个功能——都走team, 并且都会有一个里程碑:里面至少一个任务、一轮 QA,以及代码、安全、文档三个 评审各一轮。一个里程碑不等于发版:它是"一次完整循环加一次提交";推送和打 tag 在它之外,每一次都还要你自己单独同意。分不清你要的是答案还是改动时,它会问你。 -
它会问用哪种语言。它绝不猜。
-
它会访谈你,而这场访谈是有方法的:一轮只问一个问题,每个都带推荐答案, 你回答之后它才问下一个,绝不一次丢给你一串。它会按"自己缺的是哪一类东西"来挑 问题的种类,其中一类是"这件事本身是不是该问的问题?"——那是它可以说"你可能在解 一个错的问题"的许可,而且要早说,趁改方向还便宜的时候。它会在"开局文档每一节都 能写下来、不留一处猜测"的那一刻停下来。它会先在仓库里查清所有能查到的事实;凡是 "查一下"不够的,它会启动
crew_researcher:每条结论都要给出来源——所以只有文件 回答不了的问题才会问到你。 -
它先定下编程语言和技术栈,并且要你批准。 如果仓库里已经有一套,那就是它。PM 会去读依赖清单、锁文件、测试目录和 CI 配置,说明它看到了什么,你一句话确认即可。
只有当真的需要做选择时——空仓库、新服务——PM 才会先启动
crew_researcher。调研角色 会报告:这类项目现在通常用什么来做(每条结论都要给出来源)、这台机器上已经装了什么、 每个选项的代价分别是什么。它只列选项,不许给出推荐。然后由 PM 给出它推荐的那一个,并写明备选是什么、为什么没选它,把一段 语言与技术栈 写进文档里:语言和版本、包管理器、框架、数据库,以及测试框架和 确切的测试命令。最后这一项最重要:工程师用它写测试,QA 也用它写用例,所以只能有 一个答案,不能有五个。
你和文档一起确认这套技术栈;确认之后,它只能通过 CRD 才能改——CRD 是"变更请求文档",下面会细说。项目里已经有的库, 工程师可以自己挑;要新增一个依赖,必须回来问 PM。
-
它写出开局文档。一件作业一份,而且文件名里带着这件作业:
docs/design/prd-<日期>-<作业 slug>.md(PRD,product requirements document, 产品需求文档)——小活也用它,真正的产品也用它。名字这两半都少不了:同一天可能开 两件作业,而固定的文件名会悄悄覆盖上一件作业的 PRD。设计文档同形,叫docs/design/hld-<日期>-<作业 slug>.md;而docs/design/tasks.md保持原名—— 它是全仓库一张表,不是一件作业一张。 轻重在内容里,不在文件名上:小活的 PRD 就是三段话,目标、不做的事,以及那段 语言与技术栈。它会说明它把这件活判成多大,一个词就能改。开工前必须你确认。 大活的 PRD 会被切成里程碑(milestone):三到六个停靠点,每一个都是你能亲眼 看到、能自己判断的东西,用你的话来写,而不是用代码的话来写。M1就是概念验证 (PoC):把最有风险的那条路打通一条最细的真实链路,真的跑起来。里程碑清单要单独 给你确认一遍,因为它决定了你在什么时候有发言权。"什么算做完"是一节,永远不是一份单独的文件。 每个里程碑都带一节 DoD (definition of done,"什么算做完"),任务表
docs/design/tasks.md里的每一行 任务也各带一节。一节 DoD 说两件事:这一件事怎么算做完,以及别人怎么验—— 哪个 QA 用例、哪条确切的命令。两者都在你的仓库里,所以作业结束很久之后,当初 承诺过什么仍然读得到。再也没有dod.md,也没有全局编号的验收检查表:一条 检查就是"T-05 的 DoD 第 2 条",写在它要管的那件事旁边。 -
它先把你给的作业名转成一个短 slug——只用小写字母、数字和
-,别的都不要——把 转好的 slug 告诉你,再用它创建crew/<job-slug>分支。然后,如果是大活, 它会启动crew_architect,产出高层设计、决策记录(ADR),以及任务表docs/design/tasks.md——每一行都带一节 DoD。小活没有架构师,所以那张同样的表由 PM 自己写,位置一样、形状一样,变的只有打字的人。拆模块,每条边界一份契约。 架构师还会把系统拆成模块——先找仓库里已经有的 东西来复用,再考虑新建——并且当两个或更多模块之间要互相调用时,为每一条边界写 一个契约文件,放在
docs/design/api/:两边怎么通话(进程内调用、HTTP、gRPC、 事件消息等)、数据格式、每个调用的输入、输出和可能的错误,以及这个形状怎么让 调用方不容易用错。它只定风格,不定具体的库。契约为什么要紧。 这些契约正是两个工程师能同时开工的前提,因为 crew 角色 之间不能互相说话。正因为不能对表,每个契约还会为两边各指定一个测试:被调用 方证明自己的回答和文件写的一模一样,调用方则针对按文件搭出来的桩(stub)来测。 另外,只要存在边界,第一个任务就是走骨架(walking skeleton):由一个工程师 单独把最有风险的那条边界打通一条最细的真实链路,跑通之后其他任务才并行开工。 契约对不上,在这里修最便宜。
每个任务都要归到你确认过的某个里程碑下面——架构师不能增加、改名或调整这些里程碑 的顺序。之后必须由
crew_doc_reviewer全部通过,才允许写第一行代码。 -
它为每个任务启动一个
crew_engineer,一次只做一个里程碑。 只有当两个任务的文件列表不重叠时,工程师才会同时跑,而且绝不跨里程碑同时跑。每个工程师都先写测试:先写一个单元测试, 跑一遍,确认它是因为"功能还不存在"而失败,然后才写刚好能让它通过的最少代码。 它的汇报里必须给你看先失败的那次运行,再看通过的那次运行。每一个这样的测试都是 一个真实的文件,放在你项目自己的测试目录里,写在任务行的文件清单中,并和代码一起 提交——绝不是谁在 shell 里跑过一次的命令。如果工程师认为某个任务 没法先写测试,它必须先问 PM,在得到答复前一行代码都不写。一个任务做完的判据,是它自己的单元测试通过,别的都不拦着它:QA 和三个评审 这时还没跑,所以它们谁也不负责宣布一个任务做完。那一行任务仍然记四个结论,而一道 还没跑的检查,就老实写成
not run并给出理由,绝不写成pass。每一行任务还带一个形状,默认是
solo(单人)。 单人就是上面这一段,第二种形状 出现之后,它一个字都没改。标了pair(双人)的那一行,由两个永远碰不到面的工程师 来做:一个只写单元测试,另一个只写产品代码。这份清单后面的 双人形状那一节会讲:它买到什么、PM 怎么跑它,以及一次全绿证明不了什么。 -
QA 和三个评审一个里程碑只跑一轮,在里程碑最后跑——不是每个任务跑一遍。PM 在 最后一个任务落地、编码停下来之后才开始,因为一条阻塞发现会改动代码,把早跑的检查 作废。只有改动过的部分在范围内,这个里程碑之外的东西都不在——评审员再不喜欢 别处的东西也一样。
先一轮 QA,分两步。 先一个
crew_qa把各任务的 DoD 章节变成一份用例清单, 一条一行,别的什么都不写——它不读代码,因为被测的那一方不该出题。PM 读完这份 清单,然后一条用例一个 agent,全部并行铺开:每个 agent 把自己那一条写成真实的 测试文件,放在docs/qa/<task-id>/,用你项目自己的测试框架,旁边配一个run.sh, 并把整套测试跑一遍。然后另外三个,在同一条消息里各一轮、并行:
- 代码评审——先看正确性,再看驱动这次改动的测试,然后是复用、能否更简单、 可读性,以及是否符合本仓库自己的代码风格。后面这四项评审员也可以判定为 "阻塞",但前提是它必须给出它想要的具体替代写法;给不出就只能记为"可选"。
- 安全评审——仅当改动涉及网络、登录鉴权、密钥、项目外的文件、shell、 用户输入、客户数据或新依赖时才做。这张清单就是"有风险"这个词的全部判据, 没有第二张。
- 文档评审——这个里程碑改过的文档,一份文档一个 agent。
只有因为某个评审自己的发现而做的改动,才会把那个评审叫回来:代码改动重跑代码 评审,文档改动重跑文档评审,安全改动重跑安全评审。三个从不一起重跑,而第二轮只 复查"阻塞项"。要是两边仍然谈不拢,PM 会停下来,把双方的说法都摆到你面前。
代价,明说,因为这是知情之后选的。 一轮放在最后,缺陷会被更晚发现,上面已经压 了更多活,所以返工面更宽。它换来的东西要求那一轮必须是完整的一轮:每个任务 DoD 章节里的每一条,不管测试跑出来是什么结果。
QA 的用例留在磁盘上,计划不留。 用例一写出来,同样的内容就有了能跑起来的形式, 所以计划随作业一起丢掉。计划里只有一段不能丢:"有什么我在这里测不到、为什么"—— 那一段写进
docs/qa/gaps.md,一份常备的清单,说明你这个产品里有哪些东西没有用例 能判,后面的作业会把它一条条变短。bash docs/qa/run-all.sh会跑所有任务的用例, PM 还会把这条命令接进你项目默认的测试命令里,这样早先的用例不靠谁记得就会守着 后面的改动。老用例开始失败就是"阻塞级"的回归缺陷,任何人都不许把它改绿。如果你的 测试运行器看不到那个文件夹,PM 会加上让它看得见的那一行配置;"这些用例跑不了"是 PM 要拿来问你的问题,不是可以就此停下的结论。 -
PM 负责提交——工程师完全不碰 git。只暂存该任务拥有的文件,绝不
git add -A。 -
里程碑评审——PM 会停下来问你。 当这个里程碑里的所有任务都过了上面那几道关 并且已经提交,PM 会向你汇报:现在能做什么了、你自己动手试一试的确切命令、哪些是 故意还没做的、测试结果,以及发布方面到了哪一步。然后你来决定:发布这个里程碑、 先不发布继续做、要改点什么、还是停下——一个问题,四个答案。如果你要改 的东西动到了 PRD,计划会先回到架构师和文档评审员那里,之后才允许继续写代码。 上一个里程碑你没答复,下一个就不会开始。小活没有这一次停下来的评审:它就是一件事, 最后汇报一次。
-
决定要发布的里程碑,会有两份计划;它们长什么样是查出来的,不是猜的。 这类计划差别很大。npm 包发出去的版本撤不回来。手机 App 要等应用商店审核。网站服务 靠重新部署回滚。数据库表结构需要一份能安全跑两次的迁移脚本。
所以 PM 会启动 crew_researcher,去查你这类项目的这两份计划通常包含什么,每条
结论都要有来源和日期。它会先读你仓库里已经在做的事:CI 配置、更新日志、已有的标签、
发布脚本。然后它写下两个文件:
docs/release/<milestone>-release.md——版本号和定它的规则、给用户看的发布 说明、按顺序的确切步骤和每一步谁批准、开始之前必须成立的前提、事后怎么确认真的 成功了,以及怎么撤回。如果撤不回来,计划里就直接这么写。docs/release/<milestone>-upgrade.md——谁在从哪些版本升上来、每一处破坏性 变更以及用户必须做什么、迁移步骤以及能不能安全跑两次、跳过一个版本会怎样、怎么 退回去以及会丢什么数据、要花多久,以及期间什么会停服。
不发布的里程碑不写计划,只给一份发布差距清单,就放在你仓库里的
docs/release/<milestone>-gaps.md:一段老实话,说明它不发布,以及还缺什么才够得上
发布。下一个里程碑会把同一个文件改得更短。另外,批准计划不等于批准
推送——每一次推送和发布,都还要单独再问你一次。
12. PM 会把面向读者的文件更新到与成果一致。README.md 永远是英文;如果这次作业你选了
别的语言,它会在旁边再维护一个内容相同的文件,例如 README-zh.md、
README-ja.md。只要这次改动是用户能察觉的,它还会在 CHANGELOG.md 里加一条;
如果你仓库自己的规则或目录结构动了,它也会改 CLAUDE.md。
如果这次改动读者根本看不到,它就不动这些文件,并在总结里说明。
13. 最后再由 crew_doc_reviewer 收一次尾——这是第 8 步那轮文档评审的尾巴,不是
第二轮。它只读那一轮之后才落地的东西:上面那些面向读者的文件,README 也在内。它
检查文档能不能照着开工、是否前后一致(同一个东西只用一个名字、格式统一、多语言
版本内容一致),以及是否好读——读者设定为大约 14 岁、母语不是英语的人。它靠"数"
出来判断:句子多长、有没有俚语、有没有没解释就用的术语,而不是凭口味。措辞问题
它也可以判为"阻塞",但前提是它必须自己写出替换的句子。
14. 推送与 CI,前提是你许可。 PM 先确认远端、workflow 和可用的 gh 都在,然后
每一次推送前都问你——包括修完之后的再次推送。它只推你说"可以"的那些——crew/*
分支、main,或发布标签——盯住这次运行,CI 挂了就把真实报错发回给拥有这些文件的
工程师。
15. 合并与清理,只在你要求时才做。 PM 会自己把 crew/<job-slug> 分支合并进
main。它会分三次问你——一次为了合并,一次为了推 main,一次为了删分支——一次
"可以"绝不覆盖下一件事。合并永远不用 squash,所以"一个任务一个提交"和它带的
test-first 证据都留在历史里、还能读。推 main 之前,它会告诉你这次推送会不会
触发发布类 workflow,并点名它读过的文件;你如果还是说"可以",它就推。它只有在
证明了工作真的已经合并、并且真的已经在远端之后,才会问要不要删分支——包括证明
远端分支上没有 main 里没有的东西:git push origin --delete 本身没有任何保护。
设了 trustRootAgent: false 时,远端删除会被特意拒绝;这时 PM 会把命令交给你
自己执行,而不是重试。工作分支就那样留着,也是一种正常的结局。
16. 一个 bug 会变成任务表里的一行,而且"修好算什么样"由 PM 在动手之前写。
一个真的 bug——你报的、QA 找到的、评审发现的——会在 docs/design/tasks.md 里
拿到自己的一行,由 PM 在任何工程师动手之前写好。那一行装两样东西:报上来的现象
(谁看到的、什么命令、发生了什么、本来期望什么),以及它那一节 DoD:必须存在
并且必须通过的那个失败用例,还有必须改变的那个行为。修它的工程师永远不写这一节。
测试先行确实会产出一个测试,但那是修的人自己写的——这正是"只修了症状"能过关的
方式:在它动手之前,没有第二个人说过"修好的标准是什么"。改一个错别字这种一行的
修复不走这套:那仍然只是一条写得好的提交信息。
**然后,修它的过程可能会来问你,而且每一次选择都会被写下来。** 这件事可能在第 8
步里的任何时候发生。工程师修一个 bug——QA 报的缺陷、代码评审的阻塞发现,或者它
自己撞见的 bug——会先找出至少两个真的可行的办法。如果这几个办法只是写法不同,它自己
挑一个,并在汇报里说明它比较过哪几个。如果差别会留在代码里,它就停下来。下面这
六条里只要有一条在几个办法之间不一样,差别就留在了代码里:
- 哪个模块为这个行为负责;
- 检查或修正放在哪一层;
- 会不会碰到 `docs/design/api/` 里的模块边界契约;
- 会不会改公开的名字、命令、配置项或输出格式;
- 你看得见的行为会不会变;
- 快慢或兼容性会不会变。
停下来之后,它把这个 bug 的病因和它找到的每一个办法交给 PM:每个办法要改哪些
文件、代价是什么、以后会痛在哪里,还有它自己推荐哪一个。然后 PM 按 CRD 那条同样
的分界线来定:差别你看得见的,它当场就问你;差别只留在代码内部的,它自己定,并在
下一次里程碑评审时告诉你。新功能和重构不走这条路。
决定会先写下来,之后才允许开工,而且里面要有**全部选项**。不管活多大,它都只有
一个去处:一条 **ADR**——放在 `docs/decisions/adr/` 的决策记录。大活可以
由架构师来写;小活没有架构师,就由 PM 自己写。ADR 里有:
这个 bug 的病因、每一个选项及其代价、
以后会痛在哪里、**为什么它输了**、选中了哪一个、是谁定的,以及理由。选项那一节
**原样引用工程师自己写的那份问题文件**,PM 只补"决定"和"理由"两节——这样它没法
悄悄把选项改成对自己的决定有利的形状;也不许写成"选项:见 Q-03",因为那个文件会
随作业一起丢掉。**每条 ADR
都是写给你看的**:一个从没读过代码的人也要能分清这些选项的差别,而且推荐的那一个
会被标出来。设计不会停下来等你挑——架构师照自己推荐的那一个继续做,而 PM 会在里程
碑评审时,把这个里程碑期间每一条 ADR 的选项摆在你面前。你可以推翻其中任何一条;
那就是一次 CRD,已经照旧方案做完的任务会重做。
双人形状
docs/design/tasks.md 里每一行任务都带一个形状,默认是 solo(单人):一个工程师
先写一个会失败的单元测试,再写刚好能让它通过的代码,就是上面一次作业怎么跑里写的
那样。
另一种形状是 pair(双人):把一个任务分给两个永远碰不到面的工程师。
crew_test_engineer只写这个任务拥有的单元测试文件。crew_code_engineer只写产品代码。- 两个人各在自己的一棵 git 工作树(worktree)里干活。在两半还在写的时候,单元测试 根本不在写代码那一半的树里,所以那是"读不到",不是"不该读"。
- 两个人读同样的两份文档,别的都不读:那一行任务的 DoD 一节,以及架构师用来 钉死两半之间那条线的接口 ADR。
- 两个人之间不能对话。这不是礼貌问题,是平台决定的:兄弟 agent 不是自己的孩子,所以 哪怕角色手里有那个工具,消息也会被拒绝。
- 由 PM 合并两半,并且恰好跑一次项目自己的测试命令,然后把跑出来的结果原样报告。
这是独立验证(independent verification),安全关键工程里用的那一种:两个人在不说话 的前提下各读一遍同一份文档,这样两份读法不一样的地方就会显现出来,而不是被谈平。
它不是结对编程,而这个对比正是把它说清楚的最好办法。两个人坐在一个键盘前会持续 沟通、持续检查,他们的目标是收敛成一份共同理解。本形状把沟通全部拿掉,要的正好 相反:两份读法不许收敛,因为它们不一样的那个地方才是全部意义所在。所以它不是 "把聊天关掉的结对编程",它是另一门东西,本仓库里一律叫它双人形状。
它买到什么。 测试先行给你的是一个在代码存在之前就红过的单元测试。但在单人形状里, 那个单元测试是由马上要写代码的同一个 agent 写的,所以它可能被写成迎合那个 agent 本来 就打算写的代码。双人形状从结构上拿掉了这个可能:写检查的人故意不是写代码的人。它买到 的第二样东西更大——对同一份文档的两次独立阅读。文档在哪里允许两种读法,两半就在 那里对不上,而你是在合并时发现它,不是在线上发现它。这里的分歧不是意外,它是你能拿到 的最便宜的信号:一份大家都已经点过头的文档其实并不清楚。
PM 怎么跑一个双人任务
-
开两棵 git 工作树,一半一棵,各在自己的一个分支上,都从同一个基点长出来:
git worktree add -b <tests branch> <tests tree path> <base> git worktree add -b <code branch> <code tree path> <base>新开的工作树里只有 git 跟踪的东西。你项目自己的检查除此之外还需要什么,都要在这 同一步里、在任何一个工程师收到简报之前,放进两棵树里。少了它不会报错—— 检查会安静地变弱:一道检查跑不了自己的一部分时,它可能会出声说明然后继续,而 整次运行照样是绿的。在本仓库里,这就是每棵树一条软链接;少了它,
tools/verify-mount.mjs会跳过角色工具那一半,而那棵树看起来仍然是绿的。 -
两半在同一条消息里收到简报、同时开工,谁都没有先手。每份简报带着这一半自己的 工作树路径、只带这一半的文件清单(两份清单永不重叠)、那一行任务的 DoD 一节, 以及接口 ADR 的路径。
-
首次会合。 PM 合并两半,跑一次项目自己的测试命令,把输出原样报告。它绝不改点 什么再跑一次去换一个更好看的结果:重复这次运行会让整套东西塌回普通的测试先行, 而且是最坏的一种——每一处不一致都被读成"代码错了"然后改掉,一次分歧都不会被上报。
-
红灯会让两半各自回去查自己那一半,一次。 那之后仍然对不上的,就是分歧,而且要 写下来:文档说了什么、每一半从里面读出了什么、两份读法在哪里分开。由 PM 定;两种 读法都站得住时,它把这件事交给你。写单元测试那一半永远不许为了消掉分歧而弱化 断言;只有 PM 能批准改动一个单元测试的要求,而且那个改动必须能追回 DoD 一节的原话。
-
修是在合并后的树里写的,在那里写代码那一半已经能读到单元测试了。独立性到此 结束,这是明知故犯:那一半独立的读法已经落在盘上、已经进了证据,之后还硬把它蒙住 换不到任何新信号,只会让修变难。
-
PM 删掉两棵工作树和两个分支,并把三份证据交给代码评审:写单元测试那一半的红灯、 首次会合那一次的结果,以及分歧记录——那次会合是绿的时候,这份记录是空的。
它在哪存在,在哪不存在
- 只在有架构师的作业里。 两个工程师在写第一行之前,必须落在同样的五件事上:
从哪里 import、导出的名字、签名、返回值的形状、出错时会怎样。他们看不到对方,所以
这五件里任何一件落得不一样,合并后那次运行就是红的,而这种红谁也学不到东西——那是
撞名字,不是分歧——而且它发生得太频繁,真正的信号会被淹掉。架构师把这五件钉在
接口 ADR 里,而且只有架构师能改它。小活没有架构师,所以小活的每一行都是
solo。 - 两半必须动同一个文件时不能用。 双人任务的两份文件清单不许重叠,而一个文件不可能
同时在两份清单里。要么把任务拆到两半拥有不同的文件,要么它就留在
solo。 - 它随它所在的那张表一起确认,绝不逐行问。 架构师写任务表时会给每一行提一个形状。 小作业里 PM 自己写那张表,你连开场文档一起盖章——但小作业根本没有双人形状。 大作业(双人任务唯一能存在的那条路)里,架构师是在你确认完开场文档之后才写那张表的, 所以由 PM 确认形状,你在里程碑评审时看到它们。两条路都是一张表一次点头: 五十个任务的作业不等于五十个决定。架构师带来的是一整张表的一个默认值,加上一份例外 清单,每个例外都带它的理由:这一行的 DoD 一节它怎么写都写不锋利;这一行坐在一个模块边界 契约上;做错的后果是钱、权限或数据;这块地方以前的任务出过缺陷。
- 它更贵,而那个数字是估计。 同一个任务,双人大约比单人多花 35% 到 75% 的力气: 写的部分被拆成两半,但读文档那部分被做了两遍,而在小任务上读常常是更大的一块。 墙上时间可能反而更短,因为两半是同时写的。这些数字都不是实测。
三种会写"检查产品的东西"的角色
现在这样的角色有三个,很容易混,而且其中一个名字本身就在招人误会:
crew_test_engineer 是程序员,不是 QA。
crew_test_engineer | crew_code_engineer | crew_qa | |
|---|---|---|---|
| 它是谁 | 程序员 | 程序员 | QA |
| 它写什么 | 单元测试 | 产品代码 | 用例:验收、黑盒 |
| 粒度 | 一个单元测试管一个行为 | — | 一条用例管一条 DoD 条目,按你会看到的方式验 |
| 时机 | 代码存在之前 | — | 代码写完之后 |
| 家 | 你项目自己的测试目录;任务拥有的文件,和代码一起提交 | 产品代码文件 | 只在 docs/qa/<task-id>/,别处没有 |
| 能看到代码吗 | 不能——它在自己的工作树里,那里还没有代码 | — | 写用例清单的那个 agent 不看;写单条用例的 agent 可以 |
| 范围 | 只有这一个任务 | 只有这一个任务 | 这个任务,外加之前每个任务的用例再跑一遍 |
四条区别,没有一条是可选的:粒度(一个单元行为 对 一条验收条目)、时机(代码之前
对 代码之后)、家(你项目自己的测试目录 对 docs/qa/)、范围(只有这一个任务 对
每个任务的用例作为回归再跑一遍)。
全绿证明不了什么
这一半比前面几节更值得读两遍,所以它写在这里,而不是缩成一句附注。
首次会合全绿,只说明一件事:两份读法对上了。 它不说明文档是清楚的,而且任何 报告——两个工程师的、PM 的、评审的——都不许声称它说明了这件事。一份报告如果把 一次全绿的首次会合写成"这一节 DoD 没有歧义",那对代码评审来说是一条阻塞级的发现, 因为以后会有人拿这句话往上盖东西。
一份文档有两种歧义,而本形状只抓得住一种。 一种让两个读者产生分歧,那正是双人形状 为之而生的那一种。另一种让两个读者从同一句含糊的话里读出同一个错意思,对这一种 本形状完全瞎:两半正好对上、运行全绿、什么都不会上报。这种瞎掉的情形很常见,而且是 实测出来的,不是担心出来的:横跨 5 个 harness、23 个模型、48 个实现,同时失败的次数 是独立性模型预测值的 3.7 倍(N-Version Programming with Coding Agents,arXiv, 2026-06),而且它们集中在规格说明书最薄弱的地方——也就是说,它是穿着"最好的结果"那身 衣服来的。给两半配不同的模型堵不住它:完全相关的失败换模型、换 harness 都还在,而一侧 用更弱的模型只会让 PM 被大量假分歧埋掉。所以两半是故意跑同一个模型的,而且 本形状不是最后一道网:QA——在后面、闭眼、按文档自己写用例——才是这个团队对 "共同误读"的那道网;而首次会合是绿的,也不会让代码评审的活变少一点。
还有一个天花板。 这套东西能买到的一切,上限就是那一节 DoD 的质量,而 那一节 DoD 没有第二双眼睛:没有谁会像这两个工程师对代码做两次独立阅读那样,去对 它做一次独立的第二遍阅读。这是本设计最根本的局限,写在这里,而不是留给你以后自己撞上。
重要的事不会只留在聊天消息里
crew 是扁平的:PM 和每个角色单独说话,两个角色之间永远不能通话。所以一条消息只到达 一个角色,然后就消失了。正因如此,crew 靠文档说话——角色的汇报指向它写下的文件, PM 的答复指向它改过的文档以及那份文档的新版本号。这样,正在做同一条边界两侧的两个 工程师读到的是同一个文件;明天才启动的角色,读到的和一小时前启动的角色一样。
在这之上,每一条变更请求都有自己的文件。只要有人——你、某个角色,或者 PM 自己——
提出的东西会改变你最终拿到的结果(范围、某条 DoD 条目、里程碑清单),或者会改变两个
模块之间怎么通话(边界契约),PM 就先写下
docs/decisions/crd/NNNN-<short-name>.md:谁提的、想要什么、为什么、会动到哪些文档和任务、
代价是什么,以及决定和理由。还没定的 CRD,一行代码都不会开工;被拒绝的 CRD 也会留着,
作为"这条路我们没走"的记录。
谁来定:
- 只改契约、你完全看不到差别的修补,由 PM 自己定。它写好 CRD,派架构师去改契约 文件,然后在下一次里程碑评审时告诉你。
- 凡是动到范围、某条 DoD 条目或里程碑清单的,必须你同意。 PM 写好 CRD 就停下来问你。 在你答复之前,不升版本号,也不开任务。
小问题不会变成 CRD:角色的问题只要文件能回答,就只是作业目录里的一条记录;代码上的 评审意见就是评审意见。只有范围和契约这两件重做起来最贵的事才配一个文件。
"怎么做"的决定则写成一条 ADR,不分活的大小。 分辨两者只要一个问题:这件事是有人
要求的吗? 有人要求——你、QA、某次评审——那就是变更请求,写 CRD。没人要求,是干活时
撞上的选择,那就是 ADR,放在 docs/decisions/adr/。别的都不参与决定它的去处:不看活
多大,也不看有没有架构师。小活没有架构师,所以由 PM 自己写这条 ADR。
一份文档放在哪,取决于它能活多久。 比作业活得久的东西在你的仓库里,放在 docs/
下面,每个目录的名字就说清它装的是什么:PRD、任务表和设计在 docs/design/
(每条模块边界一个契约文件,在 docs/design/api/)——每一节 DoD 也跟着住在那里,
所以当初"算做完"的标准明年还读得到;决策记录和变更请求在
docs/decisions/(adr/ 和 crd/)、QA 可重复运行的用例和那份"哪些东西没有用例能判"
的常备清单在 docs/qa/、
每个要发布的里程碑的发布计划和升级计划在 docs/release/(不发布的里程碑,它的发布
差距清单也放在这里)、研究员的答案在 docs/research/。
只属于这一次作业的东西放在仓库外的 ~/.dsh/crew/jobs/<job-slug>/,这样你的
git status 保持干净:作业状态(state.json)、QA 的测试计划,以及
角色留给 PM 的 Q- 问题文件。整个目录会在作业结束时丢掉;而一次测试运行的输出从来
就不是文件。"什么算做完"故意不放在这里了:它是这件作业自己那份 PRD 或
docs/design/tasks.md 里的一节 DoD,在你的仓库里——因为一份单独的文件,就是一份会被
丢掉的文件。
丢掉之前,里面持久的那一半必须先搬出去。 这是作业收尾时一个真实的步骤,而且它排
在 PM 给你最终总结之后——不是 DoD 条目全绿的那一刻,因为想清楚一件事往往还要再往
后一阵。下次也要守的规则搬进 principles.md,"怎么做"的决定搬进一条 ADR,"做什么"或
契约的决定搬进一个 CRD,这次改动的理由和真实的测试数字写进提交信息,QA 那段"有什么我
在这里测不到、为什么"写进 docs/qa/gaps.md,还有两样是丢过一次之后才补上的——一条 DoD
条目自己的文字,以及一个任务拥有哪些文件,都搬进 docs/design/tasks.md。
"不需要了"必须是挣来的。ADR 把工程师的
选项原样抄进来、而不是指过去,也是同一个道理。
中断之后
光有状态文件还不够——下一次会话得知道它的存在。所以 dsh-crew 每一轮都会读作业 目录,只要还有没做完的作业,就把一段简短提示摆到 PM 面前:
Unfinished crew work: 1 job left in /home/you/.dsh/crew/jobs.
- "add-sso-login" in /home/you/project (branch crew/add-sso-login):
5 of 9 tasks done, 2 blocked. Last change 2026-08-18 09:12.
PM 必须先告诉你,再问一个问题:继续,还是重来。没有你的回答,两件事它都不会做。
属于别的目录的作业会被忽略;读不出来的状态文件会如实报告,而不是当作已完成。
把 resumeNotice 设为 false 可以整体关掉。
git 保护
host/git-guard.js 会检查每一条 shell 命令。你自己的会话是根 agent,被信任:它的 git
和发布命令直接放行。每个团队角色都是子 agent,保护会拒绝子 agent 发出的:
- 推送
main、master、trunk、develop、HEAD,或没有写明分支的推送; - 任何标签推送、远端删除、
--mirror、--all、强制推送; npm/pnpm/yarn/bun publish、npm dist-tag、gh release create;- 推送到"GitHub Actions 的 CI 在分支 push 时会发布"的仓库;
- 任何点名审批文件的 shell 命令——你自己的会话也一样,所以没有 agent 能用 shell
命令给自己授权。这个名字是按"整个文件名"来匹配的,所以只是包含它的更长的名字
不会被牵连:
crew/push-ok-flow分支、push-okay.md文件、push-ok.bak备份, 都不会被当成审批文件。
子 agent 的其他分支推送需要你创建一次性审批:
mkdir -p ~/.dsh/crew && touch ~/.dsh/crew/push-ok
一次推送用掉后,保护会立刻删除该文件。一次审批,只能推一次。
子 agent 被拒绝时,只会被告知"去请用户批准",不会看到上面那两条命令。只有你 自己的会话才看得到它们,所以刚被拒绝的那个 agent 不会同时拿到配方。
把 trustRootAgent 设为 false,就能让保护像对待子 agent 一样对待你自己的会话。
approvalFile 必须指向一个文件,不能是文件夹。写成 ~/.dsh/crew/ 这样,被保护的
名字就会变成 crew,所以保护会直接拒绝加载,并在报错里告诉你要点名文件本身。
三条老实话。每一条都是真的缺口,不是免责声明。
- 它是基于命令文本判断的,所以更像安全带,而不是一把锁。藏在脚本文件里的命令
仍可能绕过,由 shell 拼出来的文件名也一样绕得过。反过来也有代价:只要命令里
提到审批文件的名字,就会被拒绝,你自己的会话也一样,所以提交信息里带
push-ok的命令跑不起来。 - 它只读
bash和pwsh。 一个能写文件的角色——工程师就能——可以直接把审批 文件写出来,这个调用保护根本看不见。这里没有任何东西能拦住它;拦住它的是 dsh 自己的"写文件"审批弹窗。真正的关口仍然是 dsh 自己的审批弹窗。 - 发布型 workflow 的扫描只覆盖 GitHub。 保护只读
.github/workflows,也只认 GitHub 的on: push:写法。GitLab、CircleCI、Jenkins、Azure Pipelines 都不在 它的覆盖范围内,这是故意的:把 GitHub 的触发规则半途套到别的 CI 系统上会造出 误挡,而误挡比不挡更糟,因为它会教你不看内容就说"可以"。
所以保护只是给子 agent 的 GitHub 兜底。更宽的检查是 PM 自己的判断:上面第 15 步
「合并与清理」里,它还会读 .gitlab-ci.yml、.circleci/config.yml、Jenkinsfile
和 azure-pipelines.yml(存在的话),并在推 main 之前把读到的结果告诉你。
安装
dsh plugin --profile tui add dsh-crew # 或 --profile web
然后重启 dsh。启动时会把 crew 预设写入 $DSH_HOME/.agent-presets/crew(如果那里
已有别人写的 crew 文件夹,则原样保留)。要用角色,请把会话切到 Crew 预设。
不启动 dsh 也可以自检:
npm test # git 保护规则、插件挂载、所有 QA 用例,最后是 Verdicts 门;不需要 dsh
本仓库自己的 CI 会在每次推送时跑 npm test,只有推 v* 标签才会发布。有一个缺口
值得知道:在没有装 @deepseek-ai/dsh-tool-subagent 的机器上,tools/verify-mount.mjs
会跳过角色工具那一半检查,而 CI 就是这样的机器。它会出声说明跳过了哪一半,所以一次
全绿的意思是"公共运行器能检查的都检查了",不是"全都检查了"。
npm test 最后跑的那一道,管的是团队自己的记录。node tools/verify-tasks.mjs 读
docs/design/tasks.md——那里每个任务小节都带一条 Verdicts 行,也就是 PM 对四道
评审(code、security、qa、doc)的报告。下面几种情况它会变红:
- 某个任务小节没有
- **Verdicts**:行,或者有不止一条; - 四个值里缺任何一个;
- 某个值写着
not run或skipped,但破折号后面没有它自己的理由; - 某个值写着
changes needed,却没有点名哪个任务号去修。
每次跑它都会把总数大声打出来:还有多少个值是 not run,多少个是 skipped。
通过不等于干净。 那一行是 PM 写的,而评审员按设计不能写文件。所以这道门能证明的是
这一行被写下来了、每次跳过都留了一句理由;它不能证明评审真的跑过——PM 直接写
code: pass 也能过,而且任何自动检查都补不了这个洞。它之所以存在:本仓库自己那次作业
里,PM 跳过了大约 20 个任务的代码评审、以及这次作业大部分的文档评审,而当时什么都没有
变红,是用户开口问才发现的。这道门堵不住这件事,它只是让下一次这样的跳过当天就看得见,
而不是二十个任务之后才看得见。
配置
全部可选,各自放在所属的平面。
PM 与 git 保护——你 profile 的 cordis.patch.yml 里 dsh-crew-core 和
dsh-crew-git-guard 两行:
| 配置 | 默认值 | 作用 |
|---|---|---|
rolesDir | ~/.dsh/crew/roles | 用同名文件替换自带的角色人设 |
limits.liveAgents | 20 | 同时活跃的团队 agent 数 |
limits.reviewRounds | 3 | 评审轮次上限,超过就交给你决定 |
installPreset | true | 是否把 crew 预设写入 $DSH_HOME/.agent-presets |
jobsDir | ~/.dsh/crew/jobs | 作业状态存放位置,也是中断提示读取的位置 |
resumeNotice | true | 会话开始时把未完成的作业摆到 PM 面前 |
enabled(保护) | true | 关闭 git 保护——不建议 |
trustRootAgent(保护) | true | 信任你自己的会话(PM)执行任意 git 或发布命令 |
approvalFile | ~/.dsh/crew/push-ok | 一次性推送审批文件。必须是文件路径,不能是文件夹——结尾带斜线会在启动时报错 |
角色——~/.dsh/.agent-presets/crew/agent.cordis.yml 里的 dsh-crew-roles 一行:
| 配置 | 默认值 | 作用 |
|---|---|---|
rolesDir | ~/.dsh/crew/roles | 同样的人设覆盖目录 |
roleAllow | 评审:read, glob, grep;调研:read, glob, grep, write, web_search | 该角色只能用这些,其余一律关闭 |
roleDeny | 架构师、工程师、测试工程师、代码工程师、QA:crew 工具 | 该角色除这些外都能用 |
roleModels | 会话模型 | 按角色指定 provider 和 model |
roleAllow、roleDeny 和 roleModels 里的键,是这个角色的工具名去掉 crew_ 前缀——
researcher、architect、engineer、test_engineer、code_engineer、qa、
code_reviewer、security_reviewer、doc_reviewer。那个文件自己的注释里列了全部九个。
你写在那里的名单必须至少点出一个工具,而且必须是一个列表。 空列表会让 dsh-crew
拒绝启动;空字符串、0、false、{},以及任何不是"工具名列表"的值,同样会
拒绝启动,报错信息里会点名是哪个字段、哪个角色键。早先的版本会安静地吃掉这种值、
把你写的那一行丢掉;而当它是那个角色唯一的名单时,那个子 agent
就一条过滤规则都没有了——一个本该只读的评审拿到了这个预设注册的全部工具,
bash、write、edit 都在里面,而且没有任何提示。
"不加限制"这件事没有写法:要放宽一个角色,就把它可以用的工具列出来。
想回到出厂名单,就删掉那一行,或者把它设成什么都不写(YAML 里一个裸的 ~),那仍然
表示"用出厂名单"。这一条落在哪个版本,见 CHANGELOG.md。
许可
MIT
