仓库里的 .dsh 目录,怎样变成整个工作区的能力包
项目根的 .dsh 目录被 SkillFilesystem 自动发现(rank 100,项目优先),里面的 skills 随仓库走;更微妙的是自改边界——宿主用 write/edit 工具改自己的技能文件时,observeHostMutation 把这次改动单独记账,不让文件监听触发自激重载环。仓库即插件,自改有闸门。
Written in Chinese
你 cd 进一个仓库、打开 DSH,它立刻就懂这个仓库怎么构建、怎么测试——不是因为你每次都要重新配置,而是因为仓库根那个 .dsh 目录被自动发现了。这一章讲两件彼此相关的事:第一,.dsh 目录怎样让一个仓库自动变成”这个工作区专属的能力包”;第二,当模型用写文件工具去改这个 .dsh 里的技能时,系统怎样保证”自己改自己”不会触发无限重载环——这是”仓库即插件”模式下最难的那条边界。
.dsh:仓库根自动变成能力根
第 39 章讲过六个技能根,其中 项目 .dsh/skills 的 rank 是 100——最高优先级。它不是靠配置声明进来的,而是 findProjectRoot 自动找到的:从当前 cwd 向上爬,碰到 .git 或 package.json 就认定那是项目根,然后它下面的 .dsh/skills 就成了这个工作区的技能根。
本节源码依据(3 处)
为什么项目根要”向上找”而不是”认 cwd”。 如果你在仓库的一个子目录里启动 DSH,cwd 不是仓库根,但这个仓库的技能仍然应该生效——因为它是整个仓库的约定,不是某个子目录的。findProjectRoot 向上爬到 .git,等于声明:技能跟着仓库走,不跟着你碰巧站在哪个目录走。 这也是”仓库即能力包”的落点:.dsh 里的东西随仓库提交、随仓库分发,任何克隆它的人一进来就继承了这套工作方式。
自改边界:宿主改自己的技能,不触发重载环
这是本章最微妙的部分。模型可以用 write/edit 工具去改一个技能文件——这本身是被允许的(它在改进自己的操作指南)。但问题来了:技能文件被改了,文件监听器应该重载它;可如果监听器一看到变化就重载,重载又可能触发模型再改,就成了自激环。
本节源码依据(4 处)
为什么”记账再跳过”能破环。 关键是把两种”文件变了”区分开:一是你在编辑器里手动改(或外部进程改)——这是真变更,该重载;二是模型自己刚用 write 改的——这是它明知故犯、自己要求的改动,监听器再为它触发一次重载是多余的、还可能绕回模型。observeHostMutation 在宿主写文件的那一刻就把路径记进 observed;稍后 chokidar 的事件到达时,isRelevantWatchEvent 发现这个路径已经在 observed 里,就把它当”已知的宿主自改”跳过。环的本质是 A 触发 B、B 又触发 A;这里 A(宿主写)被打上标记,B(监听重载)见到标记就不动,环就断了。
| 改动来源 | 是否记入 observed | 监听器是否重载 | 设计意图 |
|---|---|---|---|
| 模型用 write/edit 改技能 | 是 | 否(记账后跳过) | 自改不触发自激环 |
| 编辑器/外部进程改 | 否 | 是 | 真外部变更要生效 |
| 删除/新增整个技能根 | 否(按目录事件) | 是 | 结构变化要重载 |
.dsh 不只是技能目录:整个仓库即能力包
把视角从”技能”拉远:.dsh 是仓库根的一个约定目录,技能只是它的一部分。同一个项目根被发现之后,它下面的多类能力(技能文件、未来还可能挂的 MCP 配置、bundles)都随这个仓库一起被识别——这就是”仓库即插件”:仓库本身就是一个可被自动装载的能力包,不需要任何人在部署方的配置里逐个登记。
本节源码依据(3 处)
为什么”仓库即插件”比”逐目录登记”更好。 如果每加一个仓库都要在部署方配置里登记一遍技能目录,那仓库的便携性就没了——克隆到别人机器上、CI 容器里,都得重新配。.dsh 自动发现把”声明能力”这件事收进仓库本身:仓库带着自己的 .dsh 走,到哪儿都自带那套技能。代价是要处理好”自改会不会炸”(下一节),这个代价花得值。
自改闸门:从”记账”到”放行”的完整链路
把自改边界拆开看,它其实是一条三段链路:宿主写文件 → 记账 → 监听器放行或拦截。selfMod 配置控制这套自改是否开启;一旦开启,宿主用来改技能的工具名被记下来,后续 fs/observed 事件就能对上号。
本节源码依据(6 处)
awaitWriteFinish 是这条链路的一个隐性关键:编辑器保存或模型 write 往往不是一个原子事件,而是一连串写入;chokidar 等到文件稳定写完才触发一次,避免”写一半”被当成一次技能变更。配合 observeHostMutation 的记账,宿主一次完整的自改最终只对应一次被跳过的重载——既不漏,也不炸。
tool-cordis:自改的”只读勘探”一侧
模型要改进运行时,第一步是”看清楚现在有什么”。tool-cordis 提供两个只读工具:cordis_inspect_list 列出 Host/Client 上所有 Inspect Provider(服务、事件、工具 schema),cordis_inspect_query 对某个 Provider 跑只读查询。它们全程只读,不能调业务 Service、不能改运行时。
本节源码依据(3 处)
为什么勘探是只读的。 让模型先”读清楚”再动手,而不是边猜边改:只读勘探把”了解现状”和”修改运行时”分成两件事,前者随便用、后者有边界。模型想看某个 Service 的方法签名,用 cordis_inspect_query 读;想真的加工具,那是另一套带审批/沙箱的路径。这种”只读发现 + 受控写”的分层,和第 39 章”目录清单常驻、正文按需”是同一种克制。
监听器是有界的:maxProjects 淘汰与按项目共享
自动监听仓库听起来美好,但一台机器上你可能开几十个 cwd、跨几十个仓库——每个仓库都挂一个文件监听器,句柄和 CPU 都会炸。SkillWatchManager 因此做了两件约束:把同一个项目根下的多个根(.dsh/skills 和 .agents/skills)归到一个 owner 共享;超过 maxProjects 就按最久未用淘汰最早的项目。
本节源码依据(4 处)
为什么要”按项目分组”而不是按目录。 一个仓库里 .dsh/skills 和 .agents/skills 是两个物理目录,但它们属于同一个项目根、同一个 owner。按项目归组后,这个仓库被淘汰时是”整组释放”,不会留下一个孤儿监听器;反过来,两个项目碰巧指向同一个共享目录(比如 monorepo 的软链),owners 引用计数让它被多个 cwd 共享、而不是开两个监听器盯同一份文件。这和预设的 generation.users 是同一种”资源按归属共享、最后一个用户走才销毁”的模式。
ancestor 模式:仓库还没克隆进来时,先盯它爹
还有个边角情况:你在一个还不存在的目录里启动 DSH(比如刚 git clone 之前、或者 CI 里仓库即将被拉下来)。.dsh/skills 此刻根本不存在,没法直接监。resolveRootWatchMode 会向上爬到第一个真实存在的祖先目录,改成 ancestor 模式——先盯这个祖先,等目标路径被创建(addDir)的那一刻,再升级成对真正根的监听。
本节源码依据(3 处)
为什么不直接”报错说目录不存在”。 因为工作流常常是”先进目录、再 clone 仓库”或”CI 先起 harness、再拉代码”。直接报错等于让 harness 在这种合法时序下起不来。ancestor 模式把”目录暂时没有”当成一种可监听的状态而不是错误:先蹲在祖先目录上等,目标一出现就接管。这是把对文件系统的”乐观等待”做进了发现层,和 discoverPresets 对 absent root 返回空数组不 throw 是同一种宽容。
| 监听模式 | 何时用 | 盯谁 | 触发动作 |
|---|---|---|---|
| root | 根目录已存在 | 直接盯 .dsh/skills | 目录内变更即重收 |
| ancestor | 根目录还不存在 | 盯最近存在的祖先 + nextPath | 目标 addDir 后升级成 root |
小结
仓库里的 .dsh 目录被 findProjectRoot 自动锚定成 rank 100 的能力根,让仓库即能力包;自改边界靠 observeHostMutation 记账——宿主自己写的技能文件打标后,文件监听器跳过重载,断掉自激环;tool-cordis 提供只读勘探,让模型先读清再动手。下一章看这些能力包怎样被打包成”预设”交给不同会话——第 42 章:一组插件声明怎样变成一个可共享、可复用的 Agent 配置。