Skip to content

Latest commit

 

History

History
724 lines (693 loc) · 77.4 KB

AGENTS.md

File metadata and controls

724 lines (693 loc) · 77.4 KB

AGENTS.md

面向后续在本仓库继续开发的 Agent / 贡献者。读完再动手。

环境与工具

  • 本机已安装 GitHub CLI(gh)与 npm 且均已认证;建仓、推送、tag、release 优先用 gh;
  • 分发双通道:GitHub(tag + Release,源码与发布说明)+ npm(公开包 dsh-gitbash-shell,用户安装入口); GitHub Release 的 published 事件自动触发 npm publish —— Trusted Publishing (OIDC), workflow 用 id-token: write + 无令牌 npm publish(见 .github/workflows/npm-publish.yml, npm 包设置里须登记同名 workflow 为 trusted publisher)。

变更记录纪律(2026-09-13 起)

  • 所有版本发布、修复、事故复盘、复现/验证记录一律写进本仓库 CHANGELOG.md,不再追加进本文件; 本文件只保留仍然有效的规则、不变量与当前事实,历史叙事由 CHANGELOG 承载(需引用时写 「见 CHANGELOG vX.Y.Z」)。
  • 发版 checklist 新增强制步骤:更新 CHANGELOG(写好新版本条目)→ 随版本提交 → 再打 tag / 发 Release;顺序不能反。
  • CHANGELOG 条目格式:倒序排列;## vX.Y.Z — YYYY-MM-DD + 类型(feat / fix / docs / chore)+ 要点 bullet + 相关链接(issue / PR / discussion / Release)。

项目一句话

dsh-gitbash-shell:Windows 上给 dsh 提供 Git Bash 命令通道的插件。 route B 架构(v0.39.0 起):不再替换宿主 ctx.shell 执行器(官方 PowerShell 执行器 原样保留,patch 纯新增——DSH STORE 收录合规,见 CHANGELOG v0.39.0),改为:

  1. 自有 bash 工具(src/bash-tool.js,对官方 tool-bash 逐调用镜像)执行在私有 GitBashSandboxExecutor 实例上(src/shell.js,继承官方 @deepseek-ai/dsh-bash-sandbox, 于私有 cordis scope 构造,永不注册为 ctx.shell 服务);
  2. 行内工具补位(v0.35.0 起,默认):src/inline-tools.js 在 agent/created 时按工具面 为官方四个 preset 注册自有 bash 工具;官方 pwsh(真 PowerShell)默认隐藏 (pwshTool 开关显示);pwsh7 旁路已随 route B 退役;gb_* FastCtx 文件工具 (src/fastctx-bridge.js,与 dsh-ops 自动让位);
  3. 声明式 preset 变体(src/index.js + src/compositions.js,variantPresets 开关默认关, 开启时以 ctx.agentPresets.register 注册 standard/minimal/ptc/cordis 四个 * · Git Bash 变体;启动时单向清理旧物化残留目录,清理不受开关影响);
  4. 发布 gitBash 宿主能力服务供 dsh-ptc-cordis-preset 联动。

核心不变量(改代码前必读)

  1. 声明式唯一路径(v0.32.0 起):宿主下限 >=0.1.7-rc(兼容门禁 plugin-compatibility.ts 读 peer @deepseek-ai/dsh,semver.satisfies 以 includePrerelease: true 求值;peer 必须标 peerDependenciesMeta.optional,否则 autoInstallPeers 场景下对只有 prerelease 的 @deepseek-ai/dsh 解析失败),era/物化/present/rows/detectPeerCoverage 已全部删除——apply() 直走声明式注册 runDeclarativeEra(v0.35.0 起受 variantPresets 开关门控,默认关=desired 为空集,见 §4l),没有「旧宿主分支」可回退。要点:四个变体由 pluginsFor({ kind, gitBash, skillsDir, pythonActive, hostExtras }) / minimalPluginsFor({ bashPath }) 生成行集(src/compositions.js,双态镜像官方 standard/minimal/ptc/cordis + Git Bash 增量:hostExtras 缺省 ↔ 0.1.7 行集,{ timeContext, toolSchedule, pluginMarket } ↔ 0.2.1 全工具行集(v0.40.2 起 alpha.3 已删 subagent 的 schedule_* deny,不产出任何 toolFilter),{ compaction } ↔ 0.2.1 minimal 行集(minimal 变体由 minimalPluginsFor({ bashPath, hostExtras }) 生成),探测式接入见第 8 条;minimal 的 PTY 行 bashPath 由 index.js 经 minimalPtyBashPath(readBashResolution) 每次注册时懒解析喂入——解析失败兜底可见默认路径,见 §4h 的两条硬边界与 issue #15);cordis 变体的 skills 直接指向 @deepseek-ai/dsh-agent-preset 包旁目录(现场解析,不拷贝);preset id 永不随官方改名(code-gitbash 保持历史 id——会话钉在 id 上,改名即 preset not found);预设清单沿用行配置 presets(Config 的普通字段,改动触发重挂载);reconcile 必须先退役再注册(宿主 registry 拒绝重复 id,与 dsh-ptc-cordis-preset v0.16.0 的翻转教训同款)。设置面:host 半顶层 await 经 src/schemastery.js 解析 schemastery 并导出 Config(开关全 volatile 探测;拿不到 schemastery 时 Config = undefined,插件照常挂载;取哪一份由宿主行为决定,见 §4k),apply 内 makeLiveReader(ctx, config) 供所有翻译层消费点(shellEnv 解析器、prompt 组装、tools/execute、post-execute、posix 指示闭包、adoptSidebar)逐次读取;client 半经 configForms 挂详情页设置卡;挂载行 id gitbash-presets → gitbash-shell(与设置命名空间同串)。绝不用顶层静态 peer import(v0.24.3 加固):@deepseek-ai/schemastery 是 peer,普通 Node 从本包位置解析不到它;顶层静态 import 一旦失败,dsh Loader 把插件行的导入失败当非致命跳过(vendor/loader/src/config/entry.ts _init():logger.error + return,永不建 fiber)⇒ 本插件连 preset 变体、执行器接线与 client 半全部消失,而宿主日志全绿(与 dsh-better-workspace issue #9 同一失败类;已用"除静态 peer import 外完全相同"的夹具插件实证)。冒烟有"不得出现静态导入行"断言。legacy settings 面已整体删除(v0.32.1):宿主侧的 namespace 注册块、makeLiveReader/dialectSettings 的 era 探测、四个 settings.get(ns) 读取器与 client 半的 settingsScope 注入 + settings.plugin.item 旧座位全部移除(宿主 settings 服务只余 describe/update;冒烟守卫「源码不得再出现 settings.register/SETTINGS_NAMESPACE」);设置面只剩行 Config + configForms 详情页卡这一条路。

  2. 执行器替换 full-access argv;Windows 未验证受限能力必须 fail-closed(Unreleased): 沙箱/生命周期基础设施沿用 @deepseek-ai/dsh-bash-sandbox;full-access 必须显式 Git for Windows argv(裸 bash 会命中 WSL 占位)。Windows read-only/workspace-write/缺失策略在工作目录准备、 Jobs admission 与 spawn 前拒绝(experimentalNative 默认 true);不得自动换工具/模式。 按 PR #26 维护者裁决,用户可实时关闭该 volatile 行 Config 开关,恢复明确标注 unconfined 的旧 Bash 行为并注销 native/path 两工具;旧 SDK 引用必须拒绝,新 native spawn 必须重验开关。 拒绝使用实际宿主 SandboxUnavailableError,导出缺失仍必须拒绝;官方 root provider 不变。 拒绝不是恢复真围栏。匹配的现代 MSYS signal pipe 有显式 SD 且缺 restricting SID, cygheap 为 VirtualAlloc;实际 Core 已补 TokenDefaultDacl。不得再用“只补默认DACL/所有未来版本 无解/用户-only cygheap 映射”的历史泛化论断。新 IPC/OS 方案必须实际正负权限验收;文件工具 policy 另属一层,拒绝不得绕过。侧栏交互终端/minimal PTY不因此获得围栏。 历史复现见 CHANGELOG v0.13.2;当前实施与独立验证见 CHANGELOG Unreleased。 0.1.7 执行器契约(v0.24.4 补齐,issue #6 复盘):① Config 的六个继承字段 (cwd/timeoutMs/maxTimeoutMs/maxOutputBytes/maxSpillBytes/graceMs)必须经 live() 探测加 .volatile() —— 0.1.7-alpha.1 起基类把它们读成 Volatile<T> 并一律 .get(), 裸值会让每一次 shell 调用抛 config.timeoutMs.get is not a function(整个 ctx.shell 失效); 插件自己的 bashPath 保持裸值。② 方法面改了:run/start 被删除,抽象面是 resolve + execute(spec): Promise<ShellExecution>,后台执行 = onExpiry: 'none' 的一次 execution,前台/后台由调用方是否 await result() 决定 —— 所以必须覆盖 execute 才能让 "全权访问走 Git Bash argv""win32 未验证受限能力拒绝启动"两条继续成立(run/start 只留给 ≤0.1.6,execute 在无 super.execute 时委派给 run);父类的 decorateResult 是私有的, 就地记忆化投影由本插件的 decorateExecution 复刻。③ settings 服务在 0.1.7 上没有 get(ns) 了(只剩 describe()/update()):读插件设置一律用 describe() 里本行 (行 id gitbash-shell)的表单值 —— 本插件踩过两处(withParityEnv 的 Linux 行尾 env、 better-sidebar 的 terminalShell 接管)后总结出的规则。

  3. 组合数据可审查(v0.32.0 起):四个变体的行集是 src/compositions.js 里已提交、可 diff 的 JS 数据(新时代的等价物;assets 目录已随物化路径删除),绝不引入运行时读内置 preset 合成行集的 逻辑——组合要可审查、可对照。对齐纪律见第 9 条(fixture 锁官方行序列)。

  4. 旧物化残留只删 marker 判定 unmodified 的(v0.32.0 保留的升级路径):cleanupLegacyTrees() 在声明式注册前跑,.plugin-managed.json(managedBy + 逐文件 sha256)是唯一判据—— unmodified → 删 + 日志;user-modified / foreign(marker 不识别)→ 绝不碰。 这是单向清理:声明式 era 本插件再也不写任何目录。

  5. gitBash 能力服务是联动契约:{ active, bashPath },仅 Windows 为 active; 形状变更要同步 dsh-ptc-cordis-preset(v0.6.0+ 依赖)。 4a. inspect-registry shim 的安装时机(v0.7.2,对齐 ptc 0.6.3):shim 曾在 apply() 里 一次性 ctx.get('cordisInspect') 采样——宿主 runner 行激活晚于本插件行,服务尚未 provide,shim 静默未安装,之后第二个 cordis 模式 preset(如内置 cordis + cordis-gitbash) 挂载即撞 "already registered" 直到重启。修复:ctx.inject(['cordisInspect'], ...) 服务 就绪那一刻安装,与行激活顺序无关;上游形状不符时仍防御式退化为裸挂。动到 shim 需重跑 「cordis 先挂 + 选 cordis-gitbash 成功」方向验证。 4b. 统一 POSIX 路径方言 + MSYS 根翻译层(v0.8.0):指令(order 126)升级为 「所有工具一律 MSYS 盘根 /c/ 形式」(含改写规则:反斜杠/盘符事实与工具输出 的 Windows 路径一律转写回 /c/);host 侧全局 tools/execute wrapper 把路径参数 字段(file_path/path/workdir,按字段名白名单——动态工具同名字段自动覆盖) 的 /x/ 前缀改写为 X:/(Node 文件工具把 /c/ 解析到当前盘符下的错误路径;bash 的 command 字段不动——那是 Git Bash 母语)。覆盖模型直调、PTC run_code 子 分发(sub-calls go through its execute)、动态 Cordis 工具。契约注记: tools/execute 官方契约写「只能改 signal」,实现层 mutableExec 同对象透传 给工具体;wrapper 防御式写入(冻结即吞异常降级),上游收紧时自动退化为 纯指令模式。指令文本改动需同步复核 smoke 的翻译断言。 设置化(v0.9.0;旧 settings.plugin.item 座位已随 v0.32.0 删除,现为 plugins.bundle.config 详情页卡——configForms 座位,两段式注册形状不变):方言默认关闭,由 posixPaths(布尔,默认 false)门控; host 侧指令 text 闭包读设置(关→空文本,组装期丢弃,零提示噪声),wrapper 每次分发读同一值(关→原样放行)。设置 schema 必须 schemastery(动态 import, 冒烟零依赖);读取一律走 makeLiveReader 的行 Config 快照(v0.32.1 起 settings.get(ns) 读取器已删)。 源头替换(v0.10.0,默认开启):开关语义升级为「模型看到的官方提示词的路径方言版本」—— 开启时 system-prompt/assemble waterfall 把 assembly 的 sections[].text、 contexts[].text(跳过自身指令防套娃)与 variables 值里的 Windows 绝对路径 原位替换为 /c/ 盘根(windowsToMsys 纯函数:引号内含空格路径整体翻译、 裸路径到空白/闭标点截止,lookbehind 排除 URL scheme 与 file://);不删减任何 官方内容、不动工具 schema(官方工具描述无盘符示例,盲改有 pattern/default 误伤风险)。指令缩为一句纯事实(源头已统一,无校准规则)。默认 posixPaths=true。 ModuleLoader 契约(v0.10.1 事故修复):client 半的 factory(require) 必须 return module.exports——漏掉 return 时模块物化为 undefined,浏览器插件行报 invalid plugin received undefined(0.9.0 埋雷、0.10.0 首启才炸;agent-lang client.js L587 同款 return,smoke 已加防回归断言)。 翻译层冻结修复(v0.10.2):registry 在构造 exec 时即 deepFreeze(arguments)—— 原地写入严格模式下抛 TypeError 被防御吞掉、静默不翻译(0.10.0/0.10.1 真机 read /c/ 报 C:/c/...); 改为纯函数返回新对象 + wrapper 替换 exec.arguments 属性(exec 本体到 tools/result 才 freeze,signal 替换是 registry 自有先例)。smoke 加冻结输入用例锁定。 两段式槽注册(v0.10.3):slots 卡片的正确形状是 slots.inject(洞名, 回调),回调体内 return slots.register(options, card)——直接把 (options, component) 作为 slots.inject 的第二三参会静默不注册、卡片永不出现 (agent-lang/better-workspace 均两段式;smoke 已加形状断言)。 输出侧回流(v0.10.4 起;v0.23.0 换挂点):成功结果的路径元数据(read/read_image/write/edit 的 path、glob paths[]、grep matches[].path、present files[].path)改写回 MSYS 方言;文件内容行与 错误结果绝不动。挂点从 v0.23.0 起是 tools/execute 的返回包装(自作结果),不再是 post-execute 的 value 决策——原因见 §4d 的铁律;整值路径统一走 driveToMsys/pathEcho(带空格目录不再 混合方言,散文面另有正则规则)。 官方 shell-env 事实(v0.11.0):方言声明进驻官方 dsh-shell-env 注册表(宿主层 服务,web 组合注入、实测下发 DSH_WEB_URL/DSH_HOME/DSH_SESSION_ID/DSH_SHELL=1,bash 工具 schema 官方措辞本就指向 $DSH_*「inspect them when needed」)——ctx.inject(['shellEnv']) 服务就绪即注册贡献者 gitbash-shell,键 DSH_PATH_DIALECT(值 msys);resolver 每次 执行读活设置(关→返回空对象,免重注册),unregister 经 envCtx.effect 挂插件 fiber 可逆; 组合里没有该服务时静默无操作。DSH_HOME/DSH_SHELL/DSH_SESSION_ID 为注册表保留键,不可也 不应覆盖。提示词源头替换(说一次)+ 运行时环境事实(按需核验)互为印证。 虚拟挂载表翻译(v0.17.0):入参翻译升级为「盘根 + Git Bash mount 表」两级——/tmp→用户 TEMP、/dev/null→\\.\NUL、/usr 系→Git 安装根(buildTranslateEnv 进程内一次探测,默认安装 + PATH 候选 bash.exe,以 <root>/usr/bin 存在性排除 WSL shim)、~→$HOME、裸盘根 /c→C:/;全部段边界 + 大小写敏感(忠实 msys 挂载表,/Tmp 不匹配),探测失败即 no-op。铁律:/dev/null 只能映射 \\.\NUL 设备路径——裸 NUL 字符串经 libuv 相对路径会在 cwd 创建真实文件(非即焚);harness 文件工具的 realpath 步骤不支持设备路径——文件工具直接 read/write /dev/null 报 EINVAL(如实、无副作用),设备语义经 bash 使用;glob 绝对 pattern 按「首个通配符前目录前缀」拆成 {path, pattern}(仅 glob 工具、绝对 pattern 覆盖已有 path);present 的嵌套 files[].path 入参 + 出参回流均覆盖;出参 TEMP 前缀回显为 /tmp。完整设计与实测矩阵见 CHANGELOG v0.17.0。 4c. run_code 程序内的路径字面量(rewriteCodePaths,v0.20.0 起已翻译;2026-09-19 立):翻译层的第三张 面孔。前两张是「工具入参 / 出参」,而 run_code 交给原生 Node 执行的程序源码里的路径字符串是程序 数据,由 Node 自己解析——/c/Users/... 会落到 <当前盘符>:\c\Users\...(实测真写出过 C:\c\Users\kanna\sandbox\...)。现在 run_code 的 code 在 tools/execute 里过一遍 rewriteCodePaths:逐字节扫描源码(注释 / 单双引号 / 模板字面量 / 正则 / 插值 / 转义),把整体 就是一条绝对 MSYS 路径的字面量按同一张挂载表翻译(盘根、/tmp、/dev/null→NUL 设备并 转义、/usr 等 Git 根、~/...)。铁律: ① 任何不确定 = 整份 no-op(未闭合字面量、走不到头的模板/正则、翻译结果里出现该字面量的引号、 反引号语境下出现插值起点),绝不半改——半改的程序比不改更糟; ② 只翻「整体是路径」的字面量:注释里的路径、带 scheme 的串、$VAR(JS 字面量是数据,不做 shell 展开)、已是 Windows 形式、相对路径一律不动; ③ 开关独立:设置卡 codePaths(默认开,21 语言词典同齐),关掉只影响程序字面量,不动工具参数 方言;posixPaths 仍是总开关。 仍不在覆盖内:动态拼出来的路径('/c/' + name)与插值模板——只有「整条路径是一个字面量」才 可判定,其余靠指令表述说清(模型侧纪律:程序里的路径写成实路径)。同源官方事实:run_code 的 process.env 为空是设计(「empty model environment」,runtime.spec 断言 env === []), os.tmpdir() 在 Windows 上得到 undefined\temp。 改动这一层必须跑 tests/smoke.mjs 的「run_code program literals」矩阵(翻译 / 不动 / 整体 no-op 三类,含 /dev/null 经 eval 求值、正则引号与除法不串味)。 4d. 失败有「两个面」,必须同方言;以及行尾值为什么是 input(v0.22.0)。 「翻译层」的第四张面孔是失败文本:一次失败同时产出 content(模型在失败调用上读到的文本、UI 卡片)与 error.message(PTC 桥交给程序的那条 ToolCallError 消息)。只翻 content 时,程序里 catch (e) { console.log(e.message) } 会打出 C:\Users\...,而同一失败在未捕获面是 /c/... ——同一错误两种方言。修法:tools/post-execute 对同一 result 的 error.message 原地改写 (注册表在 normalizeDispatchResult 按引用透传 error,直到 materializeFinalResult 才快照, 故这次写入同时到达 PTC 桥与最终记录)。铁律:① 绝不用官方 block 决策去改 message——它会重建 { message } 并丢掉 error.info(FS_NOT_FOUND 等结构化身份,记录/UI 在用),冒烟断言锁死正文 不得出现 kind: 'block';② 改写函数对冻结/缺字段/非字符串一律 no-op(内容面仍独立生效), 半改不算改;③ /tmp 回显必须覆盖句子中间内嵌的路径(msysEcho + mountTempRoot), 成功面(rewriteResultPaths)与失败面共用同一个 helper,三面同源。 行尾值:执行器注入的是 core.autocrlf=input + core.eol=lf(不是 false)。实测:在已经是 CRLF 工作区的仓库里(Windows 默认 autocrlf=true 检出的仓库),false 会让 git diff 把工作区 CRLF 与 索引 LF 逐行比对 → 整文件飘红、git add 连行尾差异一起暂存(同仓库实测 4387/4233 行,而 input/true 只有真实改动的 198/44 行);input 提交侧归一 CRLF→LF、检出侧永不写 CRLF。精确措辞(重启后实测): 它让 git diff 在 CRLF 工作区里没有幻影改动、真实改动只算自己(scratch 仓库:纯 CRLF 差异 → diff --shortstat 为空;再加一行 → 1 file changed, 1 insertion(+)),但被 touch 过的文件仍会被 git status --porcelain 列为 modified(stat 层报告,Linux 遇到 CRLF 工作区同样如此);代价是 git diff 对将被归一化的文件打一行 stderr 提示(如实告知即可)。 改这一层必须跑冒烟里的「one dialect on BOTH faces」与「post-execute branch」两例。 成功面为什么不能挂 post-execute(v0.23.0,issue #2 的结论):注册表禁止同一条决策同时带 content 与 value(postExecute 抛 cannot replace both value and content,且抛在整条瀑布 收束之后、任何监听者 try/catch 之外 → 调用直接 isError)。所以:①成功面一律在 tools/execute 里自作结果({ ...result, value };宿主 normalizeDispatchResult 会用新 value 重渲染 content), 任何决策上都不得再出现 value 键;②失败面留在 post-execute,且只产 content(value 对 失败结果本就非法),两边都不可能撞车;③结果未变时必须返回原对象,否则白白触发一次重渲染。 ④整值路径走 pathEcho(盘符前缀 + 分隔符归一 + /tmp 挂载),绝不把整值丢给散文改写器 windowsToMsys——它遇空格即断(issue #3:带空格目录回流成 /c/my dir\f.txt);散文面则只允许 「空格后那个词自带分隔符」时跨空格,免得把英文吞进路径。 改这一层必须跑冒烟里的「one dialect on BOTH faces」「post-execute branch」「whole-value path」 「success echo rides tools/execute」四例。 4e. 与 dsh-ptc-cordis-preset 的去重(v0.25.0,issue #7):联动生效后对方的 PTC 创造模式 已是 Git Bash 版,与本插件的 创造模式 · Git Bash(cordis-gitbash,PEER_COVERED_PRESET_ID)指向 同一件事;开关 suppressPeerCordis(本插件行 Config 的 volatile 布尔,默认 false) 只决定要不要摘掉本插件那一条。

    • ① 判定必须「开关 ON 且对方能力报 gitBashActive」,缺一不可:纯函数 effectivePresetIds(configured, { suppress, peerGitBash }) 是唯一判据。对方缺失/未装/未生效/尚未挂载/版本 < 0.14.0 ⇒ 一律不摘—— 绝不能因为「探测不到对方」就少注册一个变体(宁可多一条名录,不可少一个模式);默认 false 保持 0.24.x 的四变体名录不变。信号只认对方 ctx.provide('ptcCordisPreset', { id, gitBashActive }) 的主动上报,不要去 agentPresets.list() 按 name/description 猜(文本会随对方版本漂移、还可能 被用户改)。
    • ② 接线与节律:ctx.inject(['ptcCordisPreset'])(与行激活顺序无关,同 4a 的教训)+ ctx.on('loader/volatile-update') 监听本行开关,两者都触发串行 reconcile (runDeclarativeEra):不再需要的 unregister(先删 live 表再 await——宿主 registry 拒绝重复 id,必须先退役再注册)、重新需要的 register,retire/register 各打一行日志; 已挂载会话钉在组合快照上、不受影响。
    • ③ 权威状态只有一份:就是本插件这一行 Config;对方的设置卡 经 ctx.configForms.get('gitbash-shell') 绑定同一行写同一字段(官方支持编辑另一插件拥有的 命名空间)。不要再引入第二份镜像字段或双向同步逻辑。
    • ④ 服务契约:能力名 ptcCordisPreset、形状 { id, gitBashActive } 由 dsh-ptc-cordis-preset ≥0.14.0 提供(本插件侧的去重开关自 ≥0.25.0);该形状变更要同步对方仓库。改这一层必须跑 冒烟里的去重判定矩阵与接线断言。 4f. run_code 的实验性 Python 后端开关(v0.26.0):dsh 的实验性 CPython PTC 后端 (@deepseek-ai/dsh-experimental-ptc-runtime-python)替换的是 profile 级 ptc-runtime 行, 不是 preset 内的行,所以开关与运行行都归 dsh-ptc-cordis-preset:权威状态是它那一行 (ptc-cordis)Config 的布尔 pythonRuntime(默认 false),它的 bundle patch 在 boot 时按 快照条件 disable base 行 + insert 自己的 runtime 行。本仓库只做两件事:
    • ① 镜像设置卡(src/client.js):ctx.configForms.get('ptc-cordis') 读写 同一个字段 + subscribe,默认关;hasOwnProperty('pythonRuntime') 为假(对方 < 0.15.0)⇒ 整段不画; 对方未装 ⇒ 不画。绝不引入第二份镜像字段。win32 上后端构造即抛错(仅 POSIX),卡片因此 显示禁用态 + python.blocked(POSIX-only),按钮不出现;宿主侧同样拒绝写入。文案 21 语言, 必须写明「重启 dsh 后生效」与「原因见宿主启动日志」——后者是因为 pythonRuntimeIssue 这类"宿主探测原因"没有客户端可读通道,契约里明确不提供(加了就是第二份会漂移的状态)。
    • ② 组合的工作流互斥(src/compositions.js + src/index.js):workflow-ptc 构造器硬要求 ctx.ptcRuntime.language === 'typescript'(packages/workflow/workflow-ptc/src/index.ts:117), 而 preset 行在独立 PresetTree 里挂载、base 行 disable 管不到它 ⇒ python 后端期间选 standard/cordis 会让该行抛错,agent-preset-registry/mount.ts 的 audit.failed 直接拒绝 整棵 preset 挂载。修法与官方 python 参考组合一致 (snapshots/session/ptc-python-turn/cordis.yml:25-36 同款禁用两行):pythonRuntime === true 时四个变体的 workflow-ptc/tool-workflow 一律 disabled: true(用户 workflow 设置值 保留,关掉后端下次启动恢复);kind === 'ptc' 本来就关,不受影响。
    • 信号来源 = 对方能力服务:ctx.provide('ptcCordisPreset', { id, gitBashActive, pythonRuntime, pythonBackend? }) (dsh-ptc-cordis-preset ≥ 0.15.0;pythonRuntime 是用户意图,≥ 0.15.1 追加 pythonBackend: 'python' | 'node' 生效态;两者都支持 getter,读不到一律按保守值); 家族纪律同 §4e①——绝不猜,也不去读 agentPresets/配置文件推断。值变化必须重注册已经在 live 表里的变体(行内容变了,只增删名录不够),日志 peer reports the experimental CPython run_code backend: workflow rows go off in every variant。
    • 互斥的判据是「生效态」,不是「意图」(v0.26.1,重要):pythonBackendActive(capability) = 意图为真 且 生效态为 'python';pluginsFor 的形参因此叫 pythonActive(语义 = run_code 实际会用的后端)。理由:对方的 preflight 失败(包被移除 / 解释器 < 3.10 / win32)时 !!js 兜底与组合仍跑 Node,若按意图关掉 workflow-ptc/tool-workflow,用户就是白丢能力。 未生效时改打 peer has the CPython switch on but the effective backend is node|unreported …: workflow rows stay on (reason in the host log)。旧 peer 回退:peerBackend() 对缺字段/ 类型不符/getter 抛错返回 undefined(绝不把"没报"读成 'node'),此时用意图顶替 —— 对方 < 0.15.1 的行为与 v0.26.0 逐字节一致。卡片侧同理:从同一份 peer 快照读可选的 pythonBackend,意图 on + 生效 node 显示 python.degraded(21 语言);字段缺省则不显示 降级态。注意生效态要进"客户端可读"的那一份,卡片才能显示真实状态 —— 目前客户端只读得到 settings 表单快照(能力服务是宿主侧的,浏览器读不到)。Lead 裁决 A(2026-09-25):ptc 侧 不拒绝把意图落成 true(不授权任何"回写用户 settings 表单值"的行为)⇒ "意图 on + 生效 node" 这个状态是可达的;不可达的只是本插件这行降级显示(ptc v0.15.1 的 Config schema 没有 pythonBackend)。现状:ptc 卡显示 on + 红字原因,本插件卡只显示 on(hint 指向宿主日志)。 后续项 = 让对方用加法只读字段把 pythonBackend/pythonIssue 投影进行 Config 快照(不动 用户意图字段);那一刻本插件无需改动即亮起(读的是同一个键)。不要把"降级提示已生效" 写进面向用户的文案或文档。
    • 依赖边界:python 运行时包由对方 optionalDependencies 声明(精确 0.1.7-rc.2,不用 next——npmmirror 会解析到 rc.1 被兼容闸拒);本仓库不声明该依赖、不 insert 运行行 (两个插件各插一行会让 ptcRuntime 二次注册)。改这一层必须跑冒烟里的 「python switch」四例 + 真机读数(见 CHANGELOG v0.26.0 的 A/B 装置)。
    • 另注意:python 后端仅 POSIX;本插件的路径方言/执行器面按 process.platform === 'win32' 门控,因此在它唯一能跑的平台上这些层本来就不生效 —— 两侧无交互,但升级时仍要复核这条前提。

4g. 子代理/队员的方言开关(v0.27.0,subagentDialect 默认 ON):用户要求「专门给个设置是否让子代理/队员也使用 Git Bash」。 - 语义:默认 ON = 委托代理(子代理、团队队员、嵌套子代理)与主代理享受同一条方言链路;OFF = 方言只对主代理生效,委托请求回退官方 shell 语义。 - 委托身份的判据(纯函数 isDelegatedAgent):会话头 origin === 'subagent' || delegationDepth > 0 —— 与 dsh 自己的 packages/deliverables/workspace-changes/src/index.ts:59-60 同一对字段,由子代理驱动在 packages/subagent/subagent/src/child-agent.ts:139-155 打上。 未知形状一律 false(不视为委托):保留方言是有利方向,agent 缺失的诊断组装必须照旧。 - 唯一判定点 dialectApplies(dialect, agent),五个消费点共用它:① system-prompt/assemble 的源头改写与 run_code 句; ② 指令 context 的 text(context) provider;③ tools/execute 的入参翻译 + 成功面回显;④ tools/post-execute 的失败面 content/message; ⑤ shellEnv.resolve(execution) 的 DSH_PATH_DIALECT 事实。任何新方言消费点都必须接同一个 gate,否则会出现"半方言"。 - 能力边界(必须如实写进文档):dsh 每进程只有一个 shell 执行器(ctx.shell 是单例服务),所以 Git Bash 二进制本身仍是全局的; 这个开关管的是方言/翻译层。关闭后委托代理看到与写出的是 Windows 形式路径,而 Git Bash 同样接受 C:/...,行为自洽。 - 读取:本行 Config 的 volatile 布尔 subagentDialect(默认 true);缺键 = true (v.subagentDialect !== false)⇒ 老配置行为不变。21 语言文案 sub.label/sub.hint。 - 改动必须跑冒烟里的「subagent switch」三例 + 真机两态(见 CHANGELOG v0.27.0 的装置:真 root/child/nested agent + 真 assemble 调用)。

4h. bashPath 解析链与两条硬边界(v0.28.0,issue #11;懒化与 exec-path 层 v0.33.0,issue #13):用户机器上 Git 装在 Q:\Git 而 patch 写死 C:/Program Files/Git/bin/bash.exe ⇒ 每条命令 spawn ENOENT;又因本插件撤掉 pwsh-sandbox 而 dsh 每进程只允许一个 ctx.shell,整个会话的命令能力归零且无提示。本版把解析统一、并把"没有 Git Bash" 变成可引导的显式失败。v0.33.0(issue #13)补的另一半:解析结果绝不在 apply() 期冻结 —— 那一刻 loader.resolve('gitbash-executor') 会因行不在 root 树而 throw、settings 服务可能未注入,冻结的空值让 "配置正确"被误判为"未配置"而跑自动链;同一次启动里执行器(懒解析)正常而日志/弹窗/终端接管(冻结值)失败, 两条路径相反结论。修法:readConfiguredBashPath() / readBashResolution() 两个懒读取器,每个消费点 逐次重读(capability 全 getter、status 路由 fresh 重读、pathmap/翻译层、两侧终端接管),gitbash-shell 行的 值直接读 apply() 的 config 参数(rowBashPath,volatile ref 三态,不绕 settings 服务);启动日志 boot + settle 两段(失败且显式层为空时轮询 250ms×20 仅 win32,翻转后补打 settled 段;terminal 接管挂 onBashSettled 通知,boot 失败→settled 成功无需重启即可接管)。防回归断言在冒烟「explicit tier is LAZY」:源码不得再现 const configuredBashPath = effectiveConfiguredBashPath( / const bashResolution = resolveGitBashCached(。 executor 行读取二段式(同版,真机实测):dsh 0.2.0-rc.2 起 bundle-patch insert 的行挂所属插件 subtree, loader.resolve('gitbash-executor') 从 root 树查找恒 throw(不是启动时序,是永远读不到;探针 7 次采样全 throw) ——executorConfiguredBashPath 因此先试 resolve(旧宿主,行在 root 树,空串也是真实答案=自动链),throw 则 loader.entries() 迭代(含 subtree)按行自身 options.id 匹配(0.2.0 实测稳定可读)。冒烟「executor row lookup survives the subtree layout change」锁死三种 loader 形状。教训:宿主 loader 的树结构变化会静默断掉按 id 直查的 读取——面向 loader 的读取一律带 entries() 兜底。 - 两条硬边界(用户明确表态,高于任何"为了不报错"的降级提议;动这层前先问用户): ① 永不回退:找不到 Git Bash 就只报错 + 引导,绝不换 pwsh / cmd / WSL bash / MSYS2 bash 顶上, 也不做"暂时留个能跑的"。理由(用户原话):"人家大可自己卸载插件,既然人家下载了我们插件就是要用。" ② 只认 Git for Windows 的 bash:WSL(C:\Windows\System32\bash.exe、WindowsApps 别名)、 MSYS2、Cygwin 的 bash 一律不算命中,宁可报错("绝对不能用 wsl")。 - 解析链(src/bash-path.js,唯一实现;执行器与翻译层共用同一 memo),顺序即契约: 1. 设置里填的:gitbash-executor 行 config(翻译层经 executorConfiguredBashPath 的 resolve→entries() 二段式读到 —— 见上) > gitbash-shell 行 bashPath(apply() 的 config 参数,rowBashPath) > 空(=自动链)。显式值即答案,失败也不换别的。 2. 默认安装位置:C:/Program Files/Git/bin/bash.exe(主)、%ProgramFiles(x86)%、%ProgramW6432%、 %LOCALAPPDATA%/Programs/Git/bin/bash.exe。 3. PATH(用户+系统,进程级已合并)逐目录 bash.exe;黑名单在这些候选之前生效。 4. PATH 上的 git.exe 反推(<gitdir>/../bin/bash.exe、<gitdir>/../../bin/bash.exe)。 5. git --exec-path 反推 + GitForWindows 注册表(v0.33.0,issue #13):对 PATH 上第一个 git.exe (Scoop/choco 的 shim 也照常启动真身)跑 --exec-path,从输出向上 ≤5 级找「同时含 cmd/git.exe 与 usr/bin/bash.exe」的目录 ⇒ <root>/bin/bash.exe——兼容 mingw64/ucrt64/旧 flat libexec 三种布局;MSYS2 的 usr/libexec/git-core 根上没有 cmd/git.exe,天然不产出候选。随后兜底 InstallPath 注册表(HKLM\SOFTWARE\GitForWindows、HKLM\SOFTWARE\WOW6432Node\GitForWindows、 HKCU\Software\GitForWindows;官方安装器写的,Scoop/便携通常没有,故列 exec-path 之后)。两者同报 source exec-path;纯函数 execPathBashCandidates(execPath, exists) 独立可测;候选仍过下面全套判据。 6. 注册表 Path(HKCU\Environment、HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment, 覆盖"GUI 启动时 PATH 快照过期");reg query 走 argv 数组,不拼字符串。 7. 全落空 ⇒ { ok:false, path:'', tried:[…] } + bashResolutionReport() 的 fail-loud 文案。 (探测子进程一律 spawnSync + piped stdio:execFileSync 同步家族在子进程失败时把 stderr 直接泄漏到 宿主日志 —— reg 的 GBK 错误文本以乱码刷屏,v0.33.0 实测修正。) - 判据(缺一不可):黑名单(system32/windowsapps/msys/cygwin/wsl,归一化后逐段匹配,先于探测) → 形状 <root>/bin/bash.exe → <root>/cmd/git.exe|bin/git.exe → <root>/usr/bin/bash.exe → (<root>/mingw64 或 <root>/usr/bin/msys-2.0.dll) → <git> --version 含 .windows. → 一票否决:实跑候选 bash -c "uname -s" 必须是 MINGW32_NT-*/MINGW64_NT-*。 - 失败的用户可见面:① 启动日志打印完整报告(当前值/逐条探测/去哪改/下载链接/"不回退"声明); ② win32 注册 GET /dsh-gitbash-shell/api/status(可选服务 ctx.inject(['webServer']),绝不硬 inject); ③ 客户端半在 shell.overlay 上弹一次(仅 win32 且 ok:false,同 boot 一次、sessionStorage 记关闭), 内含探测清单 + 内联 bashPath 编辑(写 configForms 的 bashPath)+「去下载 Git for Windows」。 pwsh-sandbox 的 disabled: true 永远与 bash 健康无关(冒烟守卫断言它没有 !!js); cordis.patch.yml 的 gitbash-executor.config.bashPath 默认是空串(=自动链)。 - 深链结论(实测):官方设置页没有公开 API 能跳到指定插件的设置卡 (openSettings/openSection 只发给 settings.launcher/settings.onboarding 占用者,面板状态是 ui-settings-general 私有 store;ctx.shortcuts 也没有按 id 执行命令的入口)⇒ 因此弹窗自带编辑器, 用户不必去找设置页;文字指路作为兜底。上游若开放深链 API,这里可以直接换成按钮跳转。 - 改这一层必须跑冒烟里的「bash resolution」四例 + 「no-fallback guards」+「missing-bash popup」+ 真机弹窗读数。

4i. 官方侧栏终端(「新建终端」)自动接管(v0.29.0,task-25):dsh 自己的终端由 terminal-controller 行决定 shell (未配置 ⇒ subprocess.terminalEnvironment().defaultShell),与本插件的执行器是两条独立链路;在 PATH 的 bash 指向 C:\Windows\System32\bash.EXE(WSL 启动器)的机器上,新终端就是 Ubuntu。 - 官方 settings 服务写不了它(实测):SettingsService.write() 只接受 volatile 字段 (volatileForm(schema) === undefined ⇒ Plugin entry "…" has no volatile fields),而 TerminalController.Config.shell 是普通字段;探针里 settings.update('terminal', {shell}) 抛 No configurable plugin entry "terminal"。 - 可行通道 = configEditor.edit(entry, change)(官方设置 UI 的同一个 API,"ordinary fields keep normal lifecycle rules")⇒ 写入无需重启(运行中的 ctx.terminalController.config.shell 立刻是新值),落盘到 profile patch 层, 由官方 YAML 编辑器保留注释、按 id 合并。不要改官方包。 - 硬约束(Lead 裁决 2026-09-26,验收会独立 grep):① 只能经 configEditor.edit() 写入,src/ 里绝不能出现 对 profile patch(cordis.patch.yml / patchPath)的 writeFile/writeFileSync/appendFile/appendFileSync 或任何 fs 直写(冒烟有断言);② 只写空的;③ 同值不写(幂等,验收看二次启动 patch 的 md5); ④ 异值(用户显式选择)绝不覆盖,只记日志;⑤ 写入后必须读回校验,不符 ⇒ write-failed + fail-loud, 日志必须给出可执行下一步;⑥ 回退语义:关开关只阻止以后的写入,不会删除已写入的字段 —— 要彻底恢复原样需删掉该行 shell 字段并重启(README 与 21 语言 hint 都写明);⑦ 未改 shellCandidates ⇒ 菜单里仍可能列出解析到 WSL 的候选 bash,被改的是默认项 —— 文档必须如实说明,别让用户以为整条菜单被换掉。 - 只写空的:该行没有自己的 shell ⇒ 写 {path, name:'Git Bash', args:['-i']}(-i 与官方 profile() 对 bash 的默认一致); 已指向别处 ⇒ 绝不覆盖,只记日志并说明怎么交还。 - 显示名必须是 Git Bash(v0.29.1,用户实测):官方 shellCandidates 里那条 bash 在 PATH 命中 C:\Windows\System32\bash.exe 的机器上会解析到 WSL,于是「新建终端」菜单里出现两条 bash,用户分不清 ("新增的gitbash显示名称得是gitbash,不然回合wsl的混了")。name 只是 TerminalShell.name 显示名,不影响 解析/执行,但必须与 WSL 候选区分开;shellCandidates 仍不动。 - 只迁移"我们自己写下的旧默认名":name 恰为 LEGACY_TERMINAL_SHELL_NAME = 'bash'(v0.29.0 的默认值) ⇒ 新增 rename 动作只改 name(path/args 原样保留),同样写回后读回校验(路径与名字都要对), 不符 ⇒ write-failed;迁移后再跑仍是 unchanged(幂等,patch md5 不变)。 其它任何名字(My Bash、Git、用户手改、空名)⇒ 新状态 kept-user-name,不写、不动 —— 与"path 指向别处"同级:用户显式起的名就是用户选择,而且自定义名本来也不会与 WSL 候选混淆(Lead 裁决 2026-09-26)。 - 绝不写没验证过的路径:resolveShell 用 resolveExecutable() 校验配置路径,失败没有回退、直接让「新建终端」启动失败 ⇒ 只允许写 src/bash-path.js 判据全过的 Git Bash。写入后必须读回校验,不符 ⇒ write-failed + fail-loud。 - shellCandidates 不动(配置的 shell 恒排首位);菜单里可能仍有一条候选 bash(解析到 WSL)属预期。 - 开关 autoTerminalShell(volatile,默认 ON);文案 21 语言 term.label/term.hint。原 adoptSidebarShell(dsh-better-sidebar 通道)保持独立,两条互不干扰。 - 改动必须跑冒烟里的「sidebar terminal」六例 + 真机五态(空→写 name: Git Bash / 历史 bash 名字→改名迁移 / 同值→不写且 patch md5 未变 / 异值→不写 / 开关关→不写),并保留 README 的三步复验清单。

  1. 无构建:发布产物就是 src/*(assets 目录已随物化路径删除);npm test 全绿即可;安装不触发 lifecycle 脚本(保持零 allowBuilds 摩擦)。 exports 必须含 "./package.json": "./package.json"(v0.24.3 修,与 dsh-better-workspace issue #9 同源):桌面 Electron renderer 没有 ctx.loader.internal,模块发现回退 createRequire(baseUrl).resolve('<pkg>/package.json')(dsh 源码 packages/client/modules/src/index.ts locatePkgJson())——该调用遵守 exports map,缺行抛 ERR_PACKAGE_PATH_NOT_EXPORTED 并被紧邻的 catch {} 吞掉,该包被永久缓存为「非 client 包」: client 半永不进桌面启动图、设置卡与侧栏半完全不生效,而宿主日志全绿(CLI/web 宿主走 loader.internal 分支,本机自测永远测不出来)。冒烟有字段断言 + createRequire(...).resolve() 运行时断言双保险。另外三个子路径同为宿主解析面,不可删:.、./shell(执行器行名)、 ./client(client 半入口)、./locale/*.json(插件管理页元数据)。

  2. presets 配置:声明式变体清单由 gitbash-shell 行 Config 的 presets 字段配置,默认 4 个 (standard/minimal/code/cordis 的 *-gitbash id);变更经 volatile-update 触发 reconcile 重注册。

  3. preset id 永不随官方改名,组合对齐由 fixture 锁死(v0.32.0 收敛):code-gitbash 保持历史 id (会话钉在 id 上,改名即 preset not found——dsh 0.1.2 把内置 code 改名 ptc 时本插件变体 id 就没有跟随)。变体组合与官方内置 preset 的对齐由 tests/smoke.mjs 的 compositions: full variants mirror the official 0.1.7 row split 一项锁住(era 资产/marker 维度 已随物化路径删除,历史叙事见 CHANGELOG v0.6.0~v0.31.1)。

  4. 未来破坏点跟踪:官方宣布会话持久词汇(tool/code-dispatch*、日志插件名 tools-code-mode、 :code: 子调用段)将在 SESSION_FORMAT_VERSION v0→v1 迁移时改名(dsh 仓库 notes 2026-08-25-rename-code-mode-to-ptc 的 Deferred 一节)。落地时复查变体组合是否需要第三形态 (2026-08-29 复核:dsh 0.1.2-alpha.1 仍为 SESSION_FORMAT_VERSION=0,迁移未落地;2026-09-02 复核: dsh 0.1.2-alpha.4 仍为 0,Session 重构只到 branded types,消费面无变化);dsh-better-sidebar 的命名空间(terminalShell/shell)演化同样需在其升级后复核。 shell-env 键声明精简提案(2026-10-04 追记):dsh 仓库 notes proposed/simplification/2026-09-19-shell-env-key-declarations(proposed 状态)计划删除 list()、BashEnvVariable/BashEnvVariableInfo、描述对象与仅描述性校验,贡献方声明改为 显式只读键集合;register/collect/保留键/所有权冲突/未声明输出拒绝/确定性输出/effect 释放 全部保留。本插件 shellEnv.register 的 variables: { DSH_PATH_DIALECT: { description } } 是描述 对象形状——该提案实施的 dsh 升级必须同步把贡献声明改成键集合形状(描述本就没有消费方,删除 是纯减法);§4b 的官方 shell-env 事实段随改。 dsh 0.2.1 组合增量已探测式接入(v0.34.0;v0.40.2 对齐 alpha.5,rc.1 同码):官方全工具 preset 新增 time-context/tool-schedule 两行,极简 preset 新增 image-only tool-fs 与专属 compaction 组 (isolate 仅 compaction、无 pruner),alpha.4 起全工具 preset 另有 tool-plugin-market (standard/ptc 关、cordis 随 profileContext);alpha.3 已把 schedule_* 从 subagent 的 toolFilter.deny 移除 ⇒ 插件侧不再产出任何 toolFilter。本插件不抬宿主下限,由 probeHostExtras(ctx) 从 ctx.baseUrl(行挂载同一解析基准)逐包 require.resolve 探测 (四包:time-context、tool-schedule、plugin-market/tools、compaction-basic),可用才加行 (timeContext/toolSchedule/pluginMarket 仅 standard/ptc/cordis 三个 kind,compaction 仅 minimal);探测每 boot 一次、reconcile 重注册复用;任何解析异常降级为「不可用」。不要把这些行 写成无条件(包缺失拒绝整棵挂载),也不要为此抬 engines.dsh。官方下一版再加行时:先重跑 tests/align-official.mjs(pins 换新 sha)再决定是否新增探测位。 官方 mcp-client 的运行时桥接口子(v0.35.0 追记):本插件 src/fastctx-bridge.js 的 自写 MCP stdio 客户端,起因是官方 @deepseek-ai/dsh-mcp-client 只能作为静态行挂载 (command 字面量、mcp__<server>__<raw> 名字、无工具过滤)。官方若开放「运行时以编程 方式挂载/配置 MCP server 并自定义公开名与 allowlist」的 API,gb_* 桥应整体切换到官方 实现并删除自写客户端(约 120 行 JSON-RPC 一并退场)。 applyEntryPatches 的 buildMap 只递归 group 行(2026-10-12 实证):bundle patch 按 id 够不到 preset 声明 config.plugins 内嵌的行(vendor/include/src/index.ts); 「patch 翻转官方 preset 行」这条路线永不可行,除非官方把 preset 行改为 group 载体或 开放行级 restatement 的非冻结形态——届时重评,当前默认形态是 §4l 的运行时补位。 FastCtx 上游停滞监控(2026-10-12 核实):上游 yc-duan/fastctx 最后 commit 2026-08-29、release v0.2.6(8-23)后无动静;fork 维护者 T-Auto/dsh-ops 活跃 (10-11 仍有 push)。本插件锁 @dsh-ops/fastctx-win32-x64@0.2.1(构建自上游 0.2.6),上游停滞不改变已安装行为;升级决策以 fork 为准(换版本 = 改 optionalDependencies 锁 + 跑 provenance 校验 + 重测 gb_* 出参矩阵)。若 fork 也 停滞且出现安全通告:fastctxTools 开关整体关停 gb_* 即可,插件核心不受影响; 或换任意 MCP stdio 文件工具(桥的 tools/list 是动态的,FASTCTX_FILE_TOOLS 名单按 rawName 匹配,理论上兼容任何实现同四工具的 server)。

  5. 适配新版 dsh 的核对纪律(2026-09-15 立,dsh 0.1.6-alpha.1 教训):组合类插件升级 dsh 时, 绝不只看 src/compositions.js 的自身 diff——真正的漂移只存在于「本站组合 × 宿主内置 preset」之间。 每次跟随升级必须完整做一遍: ① 结构化行序列对比:取宿主当前的预设组合文本 —— packages/bundle/web-app/presets/{standard,cordis,ptc,minimal}.patch.yml(行位于 insert[0].config.plugins),抽出 - id: / name: / disabled: 三行序列,与本插件 对应变体(src/compositions.js)逐条对齐;提示词(persona 的 prefix:/suffix: 文本)与工具行同样要 diff,不要只看 id 名字。smoke 的 compositions: full variants mirror the official 0.1.7 row split 锁住对齐。 ② 行改名是致命项:dsh 0.1.6-alpha.1 把引擎行 workflow-worker-thread 改名 workflow-ptc 并删除了旧包(packages/workflow/workflow-worker-thread 整包消失)。组合里一行 import 失败会拒绝整棵 preset 挂载(agent-presets mount.ts),变体直接不可用—— 不是「少个工具」那么轻。 ③ 默认值变化同样要跟:同一版把 tool-ralph 改成默认 disabled: true(内置预设全改); 不跟就是「我们的变体替部署偷偷打开了一个已被关掉的工具」。 ④ 工具面/翻译层覆盖核对(2026-09-19 立,v0.17.0 教训):翻译层按「顶层字段名白名单 (file_path/path/workdir)+ present 嵌套形状」工作,dsh 新版本新增或改动工具的 路径参数是独立于 preset 的另一个漂移源(0.1.5 新增 present 的 files[].path 嵌套,直到 v0.17.0 才覆盖)。每次跟随升级必须盘点所有带路径参数的工具:对照宿主工具 schema (harness 源码 packages/ 或 cordis_inspect_list)检查新字段名、嵌套路径形状、出参路径 元数据字段;有遗漏则补 TRANSLATABLE_PATH_FIELDS / 嵌套形状 / rewriteResultPaths, 并在真机跑一遍「虚拟路径 × 工具」矩阵(~、/tmp、/dev/null、/usr、裸盘根、绝对 glob pattern、present 嵌套、bash workdir)。 ⑤ 审计窗口从「上一个已知可用版本」起算(v0.24.4 立,issue #6 教训):上一轮 rc.1 复核只 diff alpha.1 → rc.1,而本插件的基线是 alpha.1 —— 整个 0.1.6 → 0.1.7-alpha.1 窗口(#4587 的 volatile Config 投影 + shell 包大重构)从未被对照,于是"用户装上就每次 shell 调用报错"这种 最粗的破坏直接漏过。规则:每次跟随升级,diff 的起点必须是本插件上一个真机验证通过的宿主 版本(不是上一个 alpha/rc),并按下面这张执行器半消费面清单逐项对照: LocalBashExecutor.Config 字段与读法(裸值 vs Volatile.get())、ShellExecutor 抽象面 (run/start ↔ execute,runArgv/startArgv ↔ executeArgv)、ShellExecSpec/ ShellExecution 类型面、confine 签名、ctx.subprocess 与 ctx.sandboxPolicy 读取面、 settings 服务方法面(get(ns) 是否存在)。这类契约验证不需要 Windows:用宿主构建里的 真实类 + 打桩 spawn 驱动即可(apps/desktop/.desktop-build/targets/*/dsh/node_modules/ @deepseek-ai/dsh-bash-*),CHANGELOG v0.24.4 记录的 12 项集成检查就是这么跑的。

4j. 原生右侧栏的文件路径救援(v0.31.0,用户报告):本插件的方言第一次伸到浏览器 UI。 症状:点开对话里的文件链接(链接文本来自 shell 输出,是 /c/Users/...),官方右侧栏 (ui-sidebar-documentpreview)显示「文件不存在,可能已被移动或删除」(错误码 workspace-file/not-found)。 - 这不是 dsh 的 bug:dsh 只认 POSIX 与 Windows 盘符/UNC 两种拼写(dsh-util-workspace-path 的 isAbsoluteWorkspacePath),核心没有 MSYS 概念(DSH_PATH_DIALECT 是本插件自定义的环境变量名)。 链路是:宿主 packages/api/workspace-files → ctx.fs.resolve(path,{cwd}) → packages/fs/fs-local/src/fsio.ts:148-150 在 win32 上就是 node:path.resolve(cwd,path), 而 /c/... 对 node 不是绝对路径 ⇒ 落到 <当前盘符>:\c\... ⇒ not-found。 之所以没被 fileAddressFor 转成工作区相对路径,是因为session 的 cwd 是 Windows 拼写 (本机 session 目录名实测 --C-Users-kanna-sandbox--),与 /c/... 前缀不匹配。 - 修法在客户端半,且只有一个挂点:ctx.sidebarRight.openResource / openResourceIn —— 官方文档写明"每一种进入右侧栏的方式都是这两者之一的调用",且所有调用点都是运行时属性访问 (ctx.sidebarRight.openResource(...)、文件树经 tab-domain 的 navigator.openResourceIn(...)), 所以实例属性覆盖对全部入口生效。绝不改 dsh 源码、绝不动 profile。 - 事实来源 = 宿主:新增只读路由 GET /dsh-gitbash-shell/api/pathmap ({platform, home, tmpDir, gitRoot, mounts};mounts 直接从 GIT_MOUNTS 生成,不另抄一份), 与工具链路由同款 fenceToolRequest(GET 只需 loopback;web 服务器可能绑 0.0.0.0)。 门是宿主的事实:pathRescueActive() 要求已应答且 platform === 'win32' —— 不看客户端的 user agent(路径属于谁的文件系统与浏览器在哪无关); fact 未到时不翻译(退化为打补丁前,而不是赌一把)。 - 地址语法照官方 file-address.ts 实现:逐段 encodeURIComponent、: 保留、UNC 空首段、 ?/# 后缀保留;未改变即转发原字符串(不是"重建后看起来一样");非法转义/非 file 地址原样。 转换语义与宿主 translateMsysPath 逐例对齐(盘根、~、/tmp、/dev/null、/usr 系), 故意不实现它的 $VAR 展开——对话里的路径是文本,不是 shell 词。两边一致性有冒烟测试锁死 (23 例跑两个实现比对),改任一侧必须重跑。 - 可选注入:ctx.inject(["sidebarRight"], …),不得写进 exports.inject (硬注入不拥有的服务会让 fiber 永久 PENDING,v0.24.0 的 settingsScope 教训);包装经 sctx.effect 随 fiber 卸载并还原。 - 已知边界:只有"整体是一条路径"的链接能被救;gitRoot/tmpDir 探测不到时保持原样(退化即修复前)。 未覆盖:真实 UI 里"点击链接 → tab 显示内容"这最后一跳(需要构造会话)—— 已覆盖的是宿主路由可达(真机 curl 200)、浏览器加载新 client 半、真实浏览器 console 出现 right-sidebar path rescue armed(= apply + inject 成功)、浏览器真实请求 pathmap。 - 改这层必须跑冒烟里的 path rescue 五例 + path map route 一例(含真实文件系统断言: path.resolve(cwd, msys) 指向不存在的路径,而救回后的拼写 existsSync 为真)。

4k. schemastery 取哪一份,由宿主行为决定(v0.31.1,issue #12):插件必须与它所继承的执行器基类 用同一套语义的 schemastery。dsh 0.1.7+ 的 LocalBashExecutor.Config 把六个字段声明为 .volatile() 并在 assertServiceableBashConfig(line 80)与 resolve(line 124/132)里调 .get();用一份 没有 volatile() 的 schemastery 构造 Config ⇒ 字段退化成普通值 ⇒ 每次 shell 调用都 TypeError: config.timeoutMs.get is not a function,而插件已经把 pwsh-sandbox 关掉 ⇒ 整个会话的命令能力归零,且宿主日志全绿(插件行本身加载成功,报错也指不到插件)。 - 这不是用户的配置问题:@deepseek-ai/schemastery 是 peer,Node 从插件文件向上解析, 在 hoisted profile 里会先撞上被其他依赖提升到 profile 根的旧副本(报告者实测 3.18.2,无 volatile), 而宿主自己用的是安装目录里与 shell 包放在一起的那份(3.18.4)。hoisted 布局与依赖提升都是 pnpm 的正常行为。 - v0.24.4(issue #6)的 live() 逐字段探测只在「我们碰巧解析到的那份恰好有 volatile()」时有效 —— 这正是 #12 指出的软肋。src/schemastery.js 把它换成宿主行为驱动: ① hostWantsVolatileRefs(ParentConfig) 直接调用宿主自己的 SandboxBashExecutor.Config({}), 看 timeoutMs 解析成 { get() } 还是普通值 —— 读的是行为,不碰任何私有 schema 结构; 宿主答不上来(抛错/非函数)⇒ undefined; ② chooseCopy 纯函数:宿主要 refs 且自己的副本能服务 ⇒ 保持自己那份(能服务就不换); 自己那份不能 ⇒ 重定向到 shell 包那份并打一行日志说明取自哪里;宿主读普通值(≤0.1.6)⇒ 保持自己那份(绝不递一个它不会 .get() 的 ref —— 那是 #12 的镜像错误);宿主答不上来 ⇒ 保持历史行为(逐字段探测),不替宿主猜;要而哪儿都没有 ⇒ 保留自己那份(不改其它行为) 但 fail-loud 说明原因,不再留一个与插件看不出关系的神秘 TypeError; ③ 两个消费点共用同一份缓存解析(src/shell.js 的执行器 Config ↔ src/index.js 的行 Config 与 旧时代 settings 命名空间),否则两处可能拿到不同的 schemastery,设置项与执行器就会各说各话。 - 不得恢复静态 peer import(v0.24.3 的教训,见 §0):解析在 src/schemastery.js 内动态进行; src/shell.js 的那个顶层 await 是唯一一处,冒烟 harness 用 stripModule() 把它换成桩 (new Function 里不能出现 await)。 - 第三种组合(宿主要 refs 而两边都没有)在现实中不可达:宿主能正常跑,就说明它自己解析到了配套副本, 而"shell 包域"正是宿主的域;该分支仍保留 fail-loud 作为防御,并由单元测试覆盖。 - 改这层必须跑冒烟里的 schemastery: 五例。判断修复是否成立时,判据必须是「插件与宿主的解析结果 是否一致」,不能是「有没有 .get()」 —— 后者在旧宿主上会给出反向的错误结论(旧宿主拿到 ref 同样是崩), 见 CHANGELOG v0.31.1 的对照实验。

4l. 行内工具补位 + pwsh 替换 + FastCtx 桥(v0.35.0,用户拍板的默认形态):官方四个 preset 的 tool-bash/tool-pwsh 行嵌在 preset 声明的 config.plugins 里,而 applyEntryPatches 的 id 索引只递归 group: true 的行(vendor/include buildMap)——bundle patch 按 id 够不到 preset 组合内的行(restatement 会冻结官方行集,禁走)。因此默认路径是运行时补位: - 判定纯函数 inlineNeeds(names)(src/inline-tools.js):needsBash/denyPwsh = 「有 pwsh 无 bash」;opsPresent = 见 ops_*(dsh-ops)。变体开启时工具面已有 bash ⇒ 补位天然跳过 (按观测互斥,无显式锁)。接线 mountInlineTools(ctx, deps),时序防御:created 时两工具 皆无视为「未接线」,250ms×5 有界重试。 - 激活的是官方 @deepseek-ai/dsh-tool-bash 模块本身(经 ctx.baseUrl 的 require.resolve + createScope(agent.ctx) 手动 apply)——零行为复刻;scope 随 agent/disposed 回滚。绝不重写 bash 工具行为;agent.ctx.tools.restrict({ deny: ['pwsh'] }) 遮蔽假 pwsh(其执行器在本机就是 ctx.shell=Git Bash)。 - 路由提示词分两段:GITBASH_ROUTE_ADDENDUM(bash 优先/pwsh 仅原生场景/禁止 bash 嵌 pwsh -Command——恒加,路由与方言无关)+ GITBASH_MSYS_ADDENDUM(仅方言开时, 随 dialectApplies)。挂在既有 system-prompt/assemble 监听器里改 assembly.tools 的 bash description(schema 层,极简 preset 的 complete persona 剥不掉);幂等标记 = 「Prefer this bash tool」。官方 bash description 本身是干净 bash 方言(v0.35.0 核实), 我们只增补。 - pwsh7(v0.35.0–0.36.0 间名为 gitbash_pwsh;src/inline-tools.js):走 ctx.subprocess(绝不经 ctx.shell), spawn 系统 pwsh 7(resolveHostPwsh7:well-known > PATH,绝不 5.1、绝不自带); command/script_path 严格二选一、未声明键硬拒(学 dsh-ops #10:宿主工具收 run_in_background,本工具必须显式拒绝而非静默前台);输入路径经挂载表 MSYS→Windows, 输出回显 gated by posixPaths;仅 danger-full-access 会话放行。 - gb_* FastCtx 桥(src/fastctx-bridge.js):自写薄 MCP stdio 客户端(官方 @deepseek-ai/dsh-mcp-client 是静态行配置:无 !!js 载荷解析、无工具过滤、名字锁 mcp__ 前缀——不满足「运行时桥接+让位+方言」;官方若开放运行时桥接 API,此模块 第一个退役,见 §8);只发布 FASTCTX_FILE_TOOLS 四件,serve 不带 --enable-shell(FastCtx 的命令/后台工具在本部署不存在);载荷 optionalDependencies @dsh-ops/fastctx-win32-x64 锁版本(上游 @fastctx/win32-x64 在场优先),缺失即不发布 (一行日志);入参(path/file_path/files[].path/绝对 pattern)与出参文本全走同一挂载表; agent/created 时见 ops_* 即整体撤下(dsh-ops 让位)。gb_ 无 workspace confinement——每个 description 必须保留官方工具是沙箱默认的声明*,绝不替换官方工具。 dsh-ops 0.4.0 渐进加载(v0.42.2):ops 的 PTC 面首 face 只常驻 ops_read_files/ops_result/ops_load_sdk(T-Auto/dsh-ops #261),高级 API 次步才到; 退避名单因此提取为 OPS_PRESENCE_MARKERS 六名(三个经典标记 + 三个渐进常驻), 任一在场即撤下——新增 ops 常驻工具名时必须同步扩这份名单,否则首 face 会多挂一轮 gb_。 - 改这一层必须跑冒烟的「inline tools:」五例 + 「fastctx bridge:」三例 + 「v0.35.0 card copy/package」两例;Windows 真机验证(补位激活/假 pwsh 遮蔽/pwsh7 路由/gb_ 出参) 在 CHANGELOG v0.35.0 标注待做。 - v0.37–v0.38.5 演进追记(2026-10-13,Windows 真机连环定位后收敛为当前事实): ① pwsh 工具名为 pwsh7、pwshTool 默认 false(v0.37.0,用户拍板)——gitbash_pwsh 旧名像 bash/pwsh 混合工具易误解;pwsh 名属于宿主假工具,恒定拒绝(pre-execute 无条件 deny,理由句指路 pwsh7 与设置卡);pwsh7 提供时走 per-agent scope 注册(纯加法,不遮蔽)。 路由句分两版:GITBASH_ROUTE_ADDENDUM(开,提 pwsh7)/GITBASH_ROUTE_ADDENDUM_NO_PWSH (关,不提任何 PowerShell 工具——给模型指一条到不了的路只会教它调用被拒的名字)。 ② 官方 tool-bash 的挂载必须走 cordis 官方插件通道 agent.ctx.plugin(mod)(v0.38.4 终态):模块对象自带 inject: [tools, shell, systemPrompt, shellEnv, workingDirectory], 手动 mod.apply(ctx) 被拒(cannot get property "shell" without inject);且 ctx.plugin() 异步启动 fiber——createScope 返回时 childCtx 未赋值,必须 await fiber(thenable)再用 (v0.38.2/0.38.3 两课)。解析走 ctx.loader.internal.import(name, baseUrl, {}) (宿主挂载行包的同一条调用,tree.ts:122;桌面版 asar/源码树可达),裸 createRequire 仅兜底。 ③ 真机验证纪律(高于一切"逻辑自洽"):这条链此前从未真机验过,v0.35.3→0.38.4 连环翻出 五层(exists 注入缺失/output 契约/根 ctx restrict/解析通道/fiber 时序+服务授权)—— win32 门控的链路,真机跑通之前一律标注「待验证」,不得标完成。 ④ 设置卡折叠为自绘(v0.38.4):官方 DisclosureRow 在插件受限 require 形状下拿不到 (icon 查找 null,静默退化平铺——v0.38.0–0.38.3 真机实锤),自绘 button 头 + 条件体, 不要再试官方组件;fold 调用约定 = fold("sec.xxx", 行...)(签名与调用点必须一致, v0.38.2 曾因吞首行翻车)。四个家族插件(ptc-cordis/agent-lang/better-workspace/ide-git) 已跟进同款自绘折叠(v0.18.0/v0.11.0/v0.29.0/v0.17.0)。 ⑤ FastCtx 桥加两道(v0.38.5):glob/replace 的 pattern 前置校验(上游对非字符串 静默当「全部文件」,桥侧响亮报错);gb_glob/gb_grep 的 description 声明各自忽略规则 (官方 glob 不忽略任何 / gb_grep 尊重 .gitignore 跳 .git / gb_glob 只认裸 .ignore)。 ⑥ 真机复验已由用户完成(bash 全链路 + 27 工具全量冒烟,见 CHANGELOG v0.38.5), v0.35.0 标注的「待做」就此销账。 ⑦ route B(v0.39.0,DSH STORE 收录合规后的架构终态):AI-Scarlett/DSH-Store #1396 以 SUBMISSION_PATCH_PROTECTED 拒收(patch 同时含 @deepseek-ai/ 字样与字面 disabled: true,扫描器双正则 AND)——「替换 ctx.shell」架构与市场「不禁用官方组件」 契约本质冲突,用户裁决改造架构而非钻字面。新事实:patch 纯新增(不再撤 pwsh-sandbox、不再插执行器行;冒烟有「市场门禁自检」断言锁死);自有 bash 工具 (src/bash-tool.js,对官方 tool-bash 逐调用镜像)执行在私有执行器实例上 (inline-tools 经 mountPrivateExecutor 在 ctx.isolate('shell') 隔离后的 scope 构造, static inject 走 cordis 官方链,§4k schemastery 语义自动满足,永不注册为根 ctx.shell);官方 pwsh 重新为真(官方解析器自动优先 PowerShell 7),pwshTool 开关语义 = 「显示 pwsh 工具」(默认关 = tools.restrict 隐藏 + pre-execute 兜底拒, 理由句 hiddenPwshDenyReason() 指路设置卡),pwsh7 旁路整体退役; resolveHostModule(inline-tools 导出)是宿主包解析的唯一通道(loader 优先 + createRequire 兜底)。bashPath 显式配置只认 gitbash-shell 行(executor 行已不存在, executorConfiguredBashPath 二段式读取保留作老 profile 兼容层)。 ⑨ dsh-ops 0.3.0 互通:gb_ 退役 + 跨工具家族方言统一(v0.39.4,用户拍板):T-Auto/dsh-ops 0.3.0 以原生 Rust ops_* 文件工具取代 FastCtx 上游(走 DSH 原生 policy/sandbox 通道)并新增 PTC 批量能力与三平台支持。用户裁决:bash 不让位(ops_bash 仅 full-access + 裸 subprocess 无沙箱;受限会话只有本插件的 bash),gb_ 退役(fastctxTools 默认 false,四处 reader 同步, 显式 true 可恢复;opsPresent 让位仍兜底),方言由本插件统一——ops_* 的入参(root 顶层 + operations[].path/to + protect[] 嵌套)翻译成 Windows 形式,返回(items[].path/matches[].path/root) 与失败(content/message)回流 /c/。方法论:互通适配前先读对方源码定事实(工具名/参数形状/ 返回结构/提示挂点),对方提示走 systemPrompt.section 即被 windowsToMsys 源头替换天然覆盖,无需 改人家文案;宿主 patch 引擎对缺失行的 warn-and-skip 是固有噪音,文档说明而非硬修。 ⑧ 模块 inject 面是兼容契约(issue #27,v0.39.3):route B 镜像新版宿主 tool-bash 时把 workingDirectory 一起抄进了自有模块的硬 inject——而兼容矩阵声明兼容的 0.2.0-rc.2 没有该服务,cordis 对无法满足的 inject 让 fiber 永久 INACTIVE(apply 不执行、零日志), 激活日志还照打(「await 一个 INACTIVE fiber 会正常 resolve」)——假阳性成功,用户 bash 全灭 且无任何线索。修复(三件套,缺一不可):inject 收敛为服务最小面(tools/systemPrompt/shellEnv), 宿主可选服务一律使用点 ctx.get() 可选读取;cwd 接缝双向兼容(有服务走 ensure() 与新版 官方一致,没有的退回 rc.2 官方 resolveWorkdir 的 policy.workspaceRoot ?? header.cwd); 激活成功必须经 tools.schemas(agent) 回读确认在册,不在册打显式失败行。铁律:镜像宿主模块 时,inject/服务消费面必须取兼容矩阵内最老宿主的形状或可选化——「宿主新版本有」不等于 「矩阵内都有」;激活/注册类日志必须回读验证,成功日志本身不是证据(与 §0 静态 import、 §4j 硬注入、v0.38.2 fiber 时序同一失败家族的第无数课)。 实验 native 与 Bash 的 cwd 消费共用 src/session-workdir.js;native 同样不得硬 inject workingDirectory。服务存在时 ensure 错误不得吞掉,只有缺服务才走旧接缝。 companion 激活必须确认 apply 已执行;scope 就绪与实际注册成功分别记录。初始挂载、volatile 开关和 Jobs 刷新必须共用按 agent 检查所有权的注册入口:注销自身注册后,外部全局/同 agent 同名工具均让位,knownNames 可用时包含隐藏工具,旧宿主回退 schemas;检查失败或注册异常记录日志,不阻断其他 companion。让位后保留待命 scope,下一次刷新可恢复。

上游追踪纪律:dsh 终端面与 dsh-ops(2026-10-19 用户要求)

本插件的三个接管面都锚在别人的包结构上,上游任何一侧发新版都必须主动核对并做出适配决断, 不能等用户报障。核对触发时机:dsh 宿主发新版、dsh-ops 发新版、本插件每次发版前。

1. dsh 宿主 — 终端/持久终端面

  • experimental-terminal-bundle(持久终端):核对 bundle 的 cordis.patch.yml 四子行 (optional-terminal-registry/optional-terminal-bash/optional-terminal-pwsh/optional-tool-terminal) 的 id 与 config 键形状是否仍被 adoptTerminalBundlePatch 的行锚命中;dsh-terminal-bash 的 shellPath/shellArgs config 键与默认值(DEFAULT_BASH_SHELL/DEFAULT_BASH_ARGS)是否漂移; dsh-tool-terminal 六工具(open/close/read/send/signal/list)签名是否变化(terminalDialect 常量)。
  • terminal-controller(侧栏终端):官方行的 config.shell 形状(path/name/args)与 LEGACY_TERMINAL_SHELL_NAME 改写许可是否仍成立(terminal-shell.js 的接管假设)。
  • 工具 face 形状:route B 补位判定(inlineNeeds:has pwsh && !has bash)依赖的官方 preset win32 工具面若被上游重构(如官方直接提供 Git Bash face),补位条件与 addendum 注入要重审。

2. dsh-ops — 并存边界

  • 剔除清单边界(守卫测试锁死):OPS_SHELL_TOOLS 必须恰好 ops_bash + ops_run; ops_run_background/ops_job_*(后台四件套)与 ops_* 文件工具永不剔除(2026-10-19 用户决策)。 dsh-ops 新增 shell 类工具时,是否纳入剔除清单属用户决断——守卫测试会拦住顺手扩展,先问用户。
  • ops-adopt 假设:publishBashTool 键名、mountSessionBash 首行门、configEditor 的 edit 语义(整键替换+回读自检)若变化,接管/还原生命周期要重验(tests/ops-adopt.mjs 的 fake editor 是按当前语义建模的)。
  • 翻译桥(fastctx bridge):dsh-ops 的 FastCtx 工具入参形状(文件工具 path 数组、issue #21 的 PRIMARY 数组形)若变,参数翻译的覆盖面要重审;后台四件套的 command 参数是 bash 语义,不经 MSYS 翻译(dsh-ops 自带 bundled bash 执行),勿越界翻译。
  • 发布面变化:dsh-ops 若给 background 组件外的组件发布新 shell 类工具,或 bundled bash/pwsh pin 变化,核对与我们 face 的可见性交互。

3. 决断输出

  • 变化破坏接管/翻译桥 → 适配发版(测试先行,fake 与真机双验)。
  • 变化新增 shell 类工具/边界模糊 → 先向用户呈现事实与选项,由用户决断(不擅自扩清单)。
  • 变化仅内部实现 → 零动作,在 CHANGELOG 或本文件记录核对结论与日期。
  • 每次本插件发版前,把「上游最新版已核对」作为 checklist 项(gitbash 对 dsh 终端面 + dsh-ops)。

验证清单(改动后)

  1. npm test 全绿;

  2. 真机验证(隔离 DSH_HOME + 含 @deepseek-ai/dsh-web-app 的 profile):启动 → registered 4 preset(s) declaratively (standard-gitbash, minimal-gitbash, code-gitbash, cordis-gitbash) → 模式选择器出现四个 * · Git Bash 变体;Windows 上另验:新会话 bash 工具存在、 command -v bash 指向 Git 安装目录(mac 上 shell 解析失败日志为设计内行为)。

  3. 组合改动后:用 cordis 会话跑 agentPresets.standingKeyFor('<variant>') 挂载校验;

  4. 旧物化残留清理改动:手造带 marker 的 unmodified 树 → 启动被删 + 日志;篡改文件成 user-modified → 保留 + 提示;无 marker 的 foreign 目录 → 绝不碰。

  5. 升级 dsh 后(v0.14.0 起强制):按第 9 条把四个内置 preset 各核对一遍——行序列 + 提示词 + disabled 默认值;smoke 的 compositions: full variants mirror the official 0.1.7 row split 锁住对齐;真机确认声明式注册日志出现四个变体且模式选择器里都能挂载。

  6. 去重改动(§4e,v0.25.0 起):隔离实例(DSH_HOME 独立 + 新建 profile + 两份插件 link: 安装

    • 探针插件定时打印 agentPresets.list())里真的翻转开关看名录:默认两侧条目并存 → 写 行 Config suppressPeerCordis: true(经 configForms 或 volatile 通道)后出现 preset 'cordis-gitbash' retired … 且名录少一条 → 写回 false 后出现 preset 'cordis-gitbash' registered declaratively 且名录恢复。非 Windows 机上用探针副本把 gitBash 能力的 active 强制为 true 模拟 win32 语义; 顺带在无头浏览器确认对方设置卡上的镜像行渲染出来(同一份状态)。
  7. Python 后端开关改动(§4f,v0.26.0 起;v0.26.1 起三态):隔离实例 + 探针插件 ctx.provide('ptcCordisPreset', { id, gitBashActive: true, … }) 跑三态: ① {pythonRuntime:true, pythonBackend:'python'} → 日志 peer reports the experimental CPython run_code backend: workflow rows go off in every variant

    • 四条 retired … (peer CPython backend changed; rows are rebuilt),且四个变体的 workflow-ptc/tool-workflow 都 disabled: true; ② {pythonRuntime:true, pythonBackend:'node'} → 日志 peer has the CPython switch on but the effective backend is node: workflow rows stay on (reason in the host log), 没有重建日志,readDocument() 读数里 standard/cordis 的两行保持启用(code-gitbash 本就关); ③ {pythonRuntime:true}(旧 peer,缺 pythonBackend)→ 与 ① 相同(意图顶替,行为与 v0.26.0 一致)。 再对其中一态用 agentPresets.acquireScope('<variant>') 真挂载四个变体,要求四条 MOUNT OK(挂载审计不得有 failed 行);读数用 readDocument() 的 dump,不要只看注册成功。 非 Windows 机上无法验证 win32 分支(卡片禁用态 / 宿主拒绝),记为模拟验证。
  8. 子代理开关改动(§4g,v0.27.0 起):隔离实例 + 真 root/child/nested agent(ctx.agents.create({ parentAgent, meta: { origin: 'subagent', delegationDepth } })), 再对每个 agent 调真的 systemPrompt.assemble({ agent, scope: agent }),断言:开关 ON 时三态都拿到方言指令;OFF 时只有 root 拿到, child/nested 均无(directive=no)。macOS 上需把插件副本的两处 process.platform === 'win32' 组装门强制打开(执行器/请求面同理), 并在报告里标注"平台门被强制";win32 真机仍未覆盖。

  9. bashPath/解析链改动(§4h,v0.28.0 起):npm test 里的解析链矩阵(fake fs + fake exec)必须全绿; 真机验证用隔离 DSH_HOME + 插件副本:把副本里 6 处 process.platform === 'win32' 强制打开、 把 /dsh-gitbash-shell/api/status 的 platform 伪装成 win32,然后无头浏览器(CDP)断言: 弹窗渲染(标题/按钮/探测清单条数)、「去下载」点击后 window.open 收到 https://git-scm.com/download/win、 内联编辑 + 保存后宿主侧落盘(profiles/<p>/cordis.patch.yml 的 bashPath)、关闭后刷新不再出现。 副本改动绝不能落在本仓(用 [ "$(pwd)" = "/tmp/..." ] 之类守卫);Windows 真机部分如实标注未验证。

  10. 官方侧栏终端接管改动(§4i,v0.29.0 起):隔离实例里用副本强制 win32 门 + 伪装"已验证的 Git Bash", 只看 profile patch 的字节变化证明五态:空值→新增 - id: terminal-controller 行且 shell.name: Git Bash、用户手写行/注释原样保留; 历史 name: bash(我们自己写的默认名)→只改这一行 name 的迁移、name: My Bash(用户自起)→零写入且 md5 未变;再跑一次同值→patch md5 未变(幂等);异值→patch 未变且日志含"explicit choice is never overwritten";开关关→patch 未变。 另用 --dump-config 读 terminal-controller.config.shell.path、无头浏览器确认零 pageerror。 Windows 真机(菜单项、终端里 uname -s 是否 MINGW64_NT-10.0-<build> 或 $MSYSTEM 是否 MINGW64)如实标注未验证, 并在 README 给用户三步复验清单。判据不要写成 uname -r(v0.29.2 更正):Git Bash/MSYS2 的 uname -r 是 MSYS 运行时版本(形如 3.6.9-b4195d69.x86_64),WSL 的才是 …-microsoft-standard-WSL2 —— 两者都不能单独判定; 代码里的"一票否决"用的是 bash -c "uname -s"(MINGW(32|64)_NT-*),那个一直是对的。

发布 checklist(GitHub + npm)

  1. npm test 全绿;
  2. 更新 CHANGELOG.md(新版本条目,随版本提交——见「变更记录纪律」);
  3. npm version minor|patch(能力变化 minor,修复 patch);
  4. git push --tags;
  5. gh release create <tag>(notes 带安装命令与变更摘要)——published 事件自动触发 npm publish;
  6. 触发 npmmirror 同步(机器默认 registry 是 npmmirror,不触发要等它自行同步, 期间 dshmarket/pnpm 对新版本号解析会报 ERR_PNPM_NO_MATCHING_VERSION): curl -X PUT https://registry.npmmirror.com/dsh-gitbash-shell/sync; 注意 pnpm 的 minimumReleaseAge 冷却:刚 publish 的版本要等几分钟才能被任何 dsh plugin add/profile 依赖解析接受(profile 级操作重解析整个 lockfile,单个过新版本会拒掉整个操作);
  7. 用户侧更新 = dsh plugin --profile <name> add dsh-gitbash-shell(npm 包名);host 半变更需重启 DSH。