dsh-plugin-git-commit-push 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,382 @@
1
- # Temporary Holding Version
1
+ # dsh-plugin-git-commit-push
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [English](README.en.md) | 中文
4
+ DSH(DeepSeek Harness)Git 提交推送插件,一次调用完成:汇总改动,按 Conventional Commits 为每个变更文件自动生成提交信息,必要时询问打标签,然后推送到当前项目配置的 Git 远程仓库。
5
+
6
+ - 用户可通过 `/git-commit-push` 斜杠命令, 0 Token 即可完成一次提交与推送(不经过模型,脚本自动按规则生成简单明了的 Commits );
7
+ - DSH 可使用 `git_commit_push` 工具, 极少 Token 即可完成高质量的注释和一次提交与推送。
8
+
9
+ 本插件是早期 `.agents/skills/git-commit-push` 这个 Skill 的工具化替代,把纯 Skill 工作需要模型跑十来条 Shell,读一堆 Git 原始输出的流程,压成 **1–2 次工具调用 + 一张紧凑卡片**。
10
+
11
+ 装好之后你得到三样东西:
12
+
13
+ | 表面 | 名字 | 谁用它 |
14
+ | -------- | ------------------------------------------------- | ---------------------------- |
15
+ | 工具 | `git_commit_push`(`prepare` / `apply` / `auto`) | 模型 |
16
+ | 斜杠命令 | `/git-commit-push` | 人,**完全不经过模型** |
17
+ | Skill | `git-commit-push` | 模型,按需加载的完整流程说明 |
18
+
19
+ - 平台:**Windows 与 macOS / Linux 都可用**(见「跨平台」)
20
+ - 要求:DSH `>=0.2.0-rc.1 <0.3.0`、Node `>=20`、git `>=2.36`
21
+
22
+ ## 安装
23
+
24
+ ### 方式 A:插件页(推荐)
25
+
26
+ DSH → **设置 → 插件** → 安装框里填包名:
27
+
28
+ ```
29
+ dsh-plugin-git-commit-push
30
+ ```
31
+
32
+ 装完按提示**重启 DSH**。同一个页面还能**启用 / 停用 / 卸载**它——这正是本包声明
33
+ `dsh.bundle.patch`、把自己做成「组合包」(bundle)的原因。没有这个声明的包,插件页会回一句
34
+ **「这个包没有声明组合包,不能作为插件管理」**(host 侧错误码 `not-bundle`)。
35
+
36
+ ### 方式 B:命令行
37
+
38
+ ```sh
39
+ dsh plugin --profile <profile> add dsh-plugin-git-commit-push # 装
40
+ dsh plugin --profile <profile> remove dsh-plugin-git-commit-push # 卸
41
+ ```
42
+
43
+ `<profile>` 是你的 profile 名(如 `web`、`headless`、自定义名)。装/卸都会改 profile 的
44
+ `package.json`(依赖 + `dsh.profile.bundles`),**需要重启 DSH** 才生效。
45
+
46
+ ### 方式 C:源码 / 离线安装(开发用)
47
+
48
+ 克隆本仓库后,用仓库里的脚本把 checkout 以 `link:` 形式挂进 profile:
49
+
50
+ ```powershell
51
+ # Windows(默认 profile: desktop)
52
+ powershell -ExecutionPolicy Bypass -File .\setup.ps1
53
+ powershell -ExecutionPolicy Bypass -File .\setup.ps1 -Profile web
54
+ powershell -ExecutionPolicy Bypass -File .\setup.ps1 -Uninstall
55
+ ```
56
+
57
+ ```bash
58
+ # macOS / Linux
59
+ sh setup.sh # 默认 profile: desktop
60
+ sh setup.sh web # 指定 profile
61
+ sh setup.sh web --uninstall
62
+ ```
63
+
64
+ 两个脚本做两件事(幂等、带备份):把 checkout 写成 profile 的 `link:` 依赖、把包名加进
65
+ `dsh.profile.bundles`,然后在 profile 目录跑 `pnpm install`。**挂载行不在脚本里**——它由本包自己的
66
+ 组合包 patch 提供(见「附:DSH 组合包契约」)。脚本还会**清理**老版本写进 profile `cordis.patch.yml`
67
+ 的那段挂载行,避免同一个 id 被挂两次。
68
+
69
+ > 三种方式不要混用:同一个包在 profile 里只应有一处挂载。
70
+
71
+ ### 装完确认
72
+
73
+ 重启 DSH 后:
74
+
75
+ - 模型能看到 `git_commit_push` 工具,输入框里能敲 `/git-commit-push`;
76
+ - 设置 → 插件里能看到本包(标题「Git 提交与推送」,带图标),可启用 / 停用 / 卸载;
77
+ - 模型技能目录里有 `git-commit-push`;
78
+ - 设置里能找到本插件的**配置表单**(11 个字段,改完即时生效、无需重启)。
79
+
80
+ ## 触发条件(务必先读)
81
+
82
+ **只在两种情况下使用,绝不自动触发:**
83
+
84
+ 1. **用户敲斜杠命令 `/git-commit-push`** —— 由命令直接执行,完全不经过模型;
85
+ 2. **用户明确要求** git 提交/推送 —— 「提交」「commit」「推送」「push」这类直白指令。
86
+
87
+ **改完代码、任务完成、会话快结束、用户说「存档」「好了」——都不是触发条件。** 编辑文件不等于要求提交。
88
+ 拿不准时先问一句,而不是直接提交。
89
+
90
+ 这条规则写在三个模型可见的位置:`git_commit_push` 的工具描述(模型选工具时读到的第一手信息)、
91
+ 随包出货的 [SKILL.md](./SKILL.md)(挂载时注册为嵌入式 skill),以及命令自身的描述。
92
+ 插件无法在 `execute` 内强制执行该规则——调用发生时决定已经做出。
93
+
94
+ ## 「0 token 完成 commit push」这个表述准确吗?
95
+
96
+ **部分准确,需要限定。** 准确的划分是「哪条路径、消耗什么」:
97
+
98
+ | 路径 | 模型 token | 说明 |
99
+ | --------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | **1. 斜杠命令 `/git-commit-push`** | **0(真的 0)** | 完全不产生模型请求。命令的发现、执行与 UI 输出**不产生模型 token**,结果只渲染在 UI 里、**不进对话历史**。这是**唯一**真正 0 token 的路径。 |
101
+ | **2. 工具 `git_commit_push(auto)`** | 少量 | 模型要生成这次工具调用(参数 + 思考)。提交信息由插件自己写,所以**没有**第二轮;返回卡片约 200 token。一次往返。 |
102
+ | **3. 工具 `prepare` + `apply`(默认)** | 约 2 次往返 | 只有 `prepare` 卡片(约 200 token)和 `apply` 的 `message` 参数进上下文,**不读 diff**。 |
103
+
104
+ 所以:
105
+
106
+ - 说「**斜杠命令 0 token**」—— 准确。
107
+ - 说「**`git_commit_push` 工具 0 token**」—— **不准确**:模型仍要为每次调用付 token(参数 + 卡片)。
108
+ - 说「**省 token**」—— 准确,但省的是**比较级**:省掉读 git 原始输出(数千 token → 约 200)与多轮往返。
109
+
110
+ 还有一个**固定成本**必须说清楚:只要插件装着且工具对模型可见,它的 schema(描述约 1.5 KB + 参数)会进入**每次**请求,约 **600 token**。
111
+ **若你只想用 `/git-commit-push`、从不让模型调用它**,把工具移出模型可见面(或改用 `deferLoading` 按需加载)可以省掉这笔固定成本。
112
+
113
+ 一句话:**`/git-commit-push` 是 0 token;工具路径是「更省 token」,不是 0。**
114
+
115
+ ## 用
116
+
117
+ ```
118
+ git_commit_push({ action: "prepare" }) # 只汇总,不改仓库
119
+ git_commit_push({ action: "apply", message: "feat(x): …" }) # 提交 + 推送
120
+ git_commit_push({ action: "auto" }) # 用规则生成的信息直接提交
121
+ git_commit_push({ action: "apply", message: "…", tag: "v1.2.3" })
122
+ git_commit_push({ action: "prepare", cwd: "/path/to/repo" }) # 会话目录不是仓库时
123
+ ```
124
+
125
+ `/git-commit-push` 变体(**0 token,完全不经过模型**):
126
+ `/git-commit-push`、`/git-commit-push --prepare`、`/git-commit-push --no-push`、`/git-commit-push --en`、`/git-commit-push --tag=v1.2.3`、`/git-commit-push 修复登录超时`。
127
+
128
+ ### 卡片长什么样
129
+
130
+ 预览(`prepare` / `/git-commit-push --prepare`)——第一行就是结论,每个文件后面跟着**它自己的拟定注释**:
131
+
132
+ ```
133
+ 🔎 **改动预览(未提交)** · `main` · 3 个文件 · +48 / -12
134
+ 状态:新增 1 / 修改 2
135
+ 新增 src/foo/bar.ts +40/-0 → feat(foo): 新增 bar 组件
136
+ 修改 src/foo/baz.ts +8/-10 → fix(foo): 修正 parseThing 判断
137
+ 删除 src/old.ts → refactor: 移除 old
138
+ 标签依据:版本号 1.2.3 → 1.2.4(package.json)
139
+ 最近提交风格:"feat(ui): 新增主题切换" "fix(api): 修正重试判断"
140
+ 拟定标题:`feat(foo): 更新 bar 组件`
141
+ (仅预览,未提交未推送)
142
+ ```
143
+
144
+ 提交后:
145
+
146
+ ```
147
+ ✅ **Git 提交并推送成功** · `main` · `a1b2c3d`
148
+ 信息:feat(foo): 更新 bar 组件
149
+ 提交 3 个文件 · +48 / -12
150
+ - feat(foo): 新增 bar 组件 · src/foo/bar.ts
151
+ - fix(foo): 修正 parseThing 判断 · src/foo/baz.ts
152
+ - refactor: 移除 old · src/old.ts
153
+ 标签:v1.2.4
154
+ 推送:已推送(含标签)
155
+ ```
156
+
157
+ 四种结果一眼可辨,不会再出现「卡片只有一行、以为没干活」:
158
+
159
+ | 首行 | 含义 |
160
+ | ------------------------------- | ------------------------------------------- |
161
+ | `✅ **Git 提交并推送成功**` | 提交也推送了 |
162
+ | `✅ **Git 提交成功(未推送)**` | 提交成功,`autoPush` 关闭或本次 `--no-push` |
163
+ | `⚠️ **已提交,但推送失败**` | 提交在本地,推送失败(`说明:` 里有原因) |
164
+ | `❌ **提交失败**` | 提交没成功,改动原样留在工作区 |
165
+
166
+ 其他状态:`ℹ️ **没有需要提交的改动**`、`⚠️ **当前目录不是 Git 仓库**`(并列出候选仓库)、`❌ **未初始化 Git**`、`❌ **找不到 git**`。
167
+
168
+ ### 多文件:一次提交,每个文件一行自己的注释
169
+
170
+ 多文件时**仍然是一次提交**,但正文里每个文件一行自己的 Conventional 注释,而不是一句笼统的话盖住所有文件:
171
+
172
+ ```
173
+ feat(foo): 更新 bar 组件
174
+
175
+ - feat(foo): 新增 bar 组件 · src/foo/bar.ts
176
+ - fix(foo): 修正 parseThing 判断 · src/foo/baz.ts
177
+ - refactor: 移除 old · src/old.ts
178
+ ```
179
+
180
+ - 每条注释**只描述那一个文件**:类型取自它自己(`docs/` → `docs:`、`*.test.ts` → `test:`),scope 取它所在的目录(与类型重复时自动省略,不会出现 `docs(docs)`),简述优先用它 diff 里声明的符号(如 `更新 decideRetry`)。
181
+ - 你只给**标题**(单行 `message`)时,插件自动补上这些逐文件注释;你自己写了正文(多行 `message`)则**原样使用**,插件不再添加任何注释。
182
+ - **单文件提交没有正文**——标题已经说完了。
183
+ - 正文最多列 `maxFilesShown` 个文件(默认 12),其余折叠成一行 `- …另有 N 个文件`。
184
+
185
+ ## 配置
186
+
187
+ **首选:在 DSH 自己的设置里改。** 本插件导出 Cordis `Config` schema,所以它的挂载行接受 `config` 映射——
188
+ DSH 的 settings 服务据此自动生成一个命名空间,设置页把它渲染成表单,
189
+ `@deepseek-ai/dsh-config-editor` 把选择写进当前 profile 的 `cordis.patch.yml`(就在本包插入的那一行上),
190
+ 并通过 Loader 立即应用。所有字段都声明为 **volatile**,因此改完**不 remount、不用重启 DSH**,
191
+ 下一次工具调用就生效。
192
+
193
+ **三层来源,自上而下覆盖:**
194
+
195
+ | 层 | 位置 | 谁写它 |
196
+ | --- | ------------------------------------------------------------------------------------- | ---------------------------------- |
197
+ | 1 | 挂载行的 `config`(profile 的 `cordis.patch.yml`) | DSH 设置页的表单 |
198
+ | 2 | `<DSH_HOME>/git-commit-push.config.json`(默认 `~/.dsh/git-commit-push.config.json`) | 你自己编辑;`link:`/离线安装的兜底 |
199
+ | 3 | 包内模板 `git-commit-push.config.json` | 随包出货 |
200
+ | — | 内置默认值 | 兜住以上都没设的字段 |
201
+
202
+ 文件缺失不是错误(用默认值);文件存在但不是合法 JSON 时,结果卡片里会追加一行
203
+ 「配置未生效:…」,而不是静默忽略。没写的键保持默认值。
204
+
205
+ | 键 | 默认 | 说明 | 表单 |
206
+ | --------------------------- | -------- | ----------------------------------------------------------------------------- | ---- |
207
+ | `autoPush` | `true` | 提交后推送 | ✅ |
208
+ | `autoAdd` | `true` | 提交前 `git add -A` | ✅ |
209
+ | `tagOnVersionChange` | `true` | 版本文件变动 → 询问打 tag | ✅ |
210
+ | `tagOnBreaking` | `true` | 检测到公共声明被删除 → 询问 | ✅ |
211
+ | `tagOnFileCount` | `10` | 改动文件数 ≥ N → 询问(`0` 关闭) | ✅ |
212
+ | `tagPrefix` | `"v"` | 建议标签前缀 | ✅ |
213
+ | `askBeforeTag` | `true` | `false` 则不问,直接打建议标签(唯一会「不问就打」的开关) | ✅ |
214
+ | `askTimeoutMs` | `120000` | 标签询问等待上限 | ✅ |
215
+ | `defaultLanguage` | `"zh"` | 规则生成信息的语言(`zh`/`en`) | ✅ |
216
+ | `maxFilesShown` | `12` | 卡片最多列几个文件 | ✅ |
217
+ | `pinnedIdentity.name/email` | 空 | 非空时以 `-c user.name/-c user.email` **仅对本次提交**生效;不改你的 git 配置 | ✅ |
218
+
219
+ > 表单所在的具体位置取决于你的 DSH 版本(设置里的插件/配置入口)。**设置 → 插件 → 插件列表**那个标签是**只读**的
220
+ > (官方描述:inspect plugins _without changing their configuration_),可编辑的表单来自 settings 服务那条路径。
221
+ > 字段说明是中文(提交信息的默认语言),DSH 目前没有按字段本地化。
222
+
223
+ **为什么还需要 JSON 文件**:`Config` schema 需要 `@deepseek-ai/schemastery`。本包把它声明为**普通依赖**,
224
+ npm 安装时随包装上;而 `link:`(源码 checkout)安装时 pnpm 不会装链接目标的依赖,宿主的模块解析也未必覆盖到它。
225
+ 那种情况下插件**照常工作**,只是没有表单(`Config` 为 `undefined`),此时 JSON 文件就是唯一的配置入口——
226
+ 这也是它继续存在、并且优先级高于模板的原因。
227
+
228
+ 环境变量:
229
+
230
+ - `DSH_HOME`:DSH 主目录,默认 `~/.dsh`(Windows 下默认 `%USERPROFILE%\.dsh`)。它同时决定用户配置文件的位置。
231
+
232
+ ## Skill `git-commit-push`
233
+
234
+ 包里的 [SKILL.md](./SKILL.md) 在插件挂载时会注册成**嵌入式 skill**(`ctx.skills.register(...)`),
235
+ 所以装完即用,不需要你手工往技能目录里拷文件。skill 名与包名一致(`dsh-plugin-` 前缀之外的部分):
236
+ 工具、命令、skill、npm 包只用一个名字,不必记两套。
237
+
238
+ - 想**覆盖**它:在工作区放一个同名项目级 skill(`.agents/skills/git-commit-push/SKILL.md`)。注册表按优先级排序,
239
+ **项目级 > 运行时注册**,你自己的版本会生效。
240
+ - 环境里没有技能注册表也能正常工作:skill 与 `/git-commit-push` 命令都是**可选能力**(用 scoped
241
+ `ctx.inject` 等它出现),缺任何一个都不影响 `git_commit_push` 工具本身。
242
+ - **从早期版本迁过来**:如果你曾把老 SKILL.md 手工装到 `~/.agents/skills/git-commit/`,那是一份**另一个名字**的
243
+ 用户级 skill,会与随包的 `git-commit-push` 同时出现在技能目录里。内容已被本包接管,建议删掉那个目录
244
+ (或保留它并在里面写你自己的规则——两者名字不同,互不覆盖)。
245
+
246
+ ## 安全边界
247
+
248
+ **绝不**:修改 `.gitignore`、改 git config(含 `user.name`/`user.email` 的写入)、`push --force`、`reset --hard`、`git clean`、`checkout -- <path>`、`commit --no-verify`。
249
+ [lib/git.js](./lib/git.js) 是唯一与 git 对话的地方,动词表是固定的——想加破坏性命令,得先改那里。
250
+
251
+ **自动处理**:无 upstream 时 `push -u origin <当前分支>`;推送被拒(远程有新提交)时 `pull --rebase` 后重推一次;rebase 冲突则**只 abort 本次自己启动的 rebase**(先探测 `rebase-merge`/`rebase-apply`,绝不丢弃你原有的 rebase 进度)并如实报告;tag 已存在或名字非法则跳过并说明。
252
+
253
+ **打标签从严**:只有你明确同意(或 `askBeforeTag: false`)才会打标签。但**显式传入 `tag` 参数视为指令,直接执行**——不再经过提问(此前这里有个 bug:显式 tag 也会走提问,没有可用提问者时被静默丢弃,实测抓到并修复)。提问服务不可用、你不在现场(子代理调用)、等待超时——一律**不打**,并在卡片里告诉你怎么用 `tag` 参数补打。
254
+
255
+ **明确不猜**:会话目录不是仓库时,返回其下的候选仓库让你用 `cwd` 指定,**不会**随便挑一个提交。git 未安装与「不是仓库」是两种不同失败,不会互相误报。
256
+
257
+ ## 跨平台
258
+
259
+ | 能力 | Windows | macOS / Linux |
260
+ | ------------------ | ------------------------------------- | -------------------------------- |
261
+ | 运行时(插件本体) | ✅ | ✅ 已审计:无遗漏的平台分支 |
262
+ | 安装脚本(方式 C) | `setup.ps1`(PowerShell) | `setup.sh`(POSIX sh) |
263
+ | 卸载 | `setup.ps1 -Uninstall` | `setup.sh <profile> --uninstall` |
264
+ | profile 清单编辑 | 两者调用同一个 `lib/profile-edit.mjs` | 同 |
265
+
266
+ 运行时跨平台审计结果(`index.js` + `lib/*`):
267
+
268
+ - 唯一的平台分支是 `DEV_NULL`(Windows `NUL` / 其他 `/dev/null`),只用于**测试夹具**隔离 git 全局配置。插件正式的 git 调用**刻意保留用户全局配置**——凭据助手、`pull.rebase`、`core.autocrlf` 都在那里,清空会改变用户仓库的行为。
269
+ - 无硬编码盘符、无 `C:\`、不依赖 `powershell`;模块用相对 `./` 说明符(大小写敏感文件系统上安全);临时目录统一用 `os.tmpdir()`。
270
+ - `setup.sh` 刻意避开两类跨平台陷阱:**不用 `sed -i`**(BSD/macOS 与 GNU 参数不同)、**不用 `readlink -f`**(macOS 无),改用 POSIX `awk` 与 `cd`+`pwd`。
271
+
272
+ ## 自检
273
+
274
+ 不需要 DSH,也不碰你的仓库(测试在系统临时目录里建自己的仓库,用完删掉):
275
+
276
+ ```bash
277
+ npm test # = node self-test.mjs && node self-test-git.mjs
278
+ node self-test.mjs # 纯逻辑 + 打包 / 配置 / 表单 / skill 契约(86 项)
279
+ node self-test-git.mjs # 真实 git:porcelain/-z 分帧、rename 归属、版本号识别、端到端提交、逐文件注释、卡片结论(24 项)
280
+ node capture-git-format.mjs # 只打印真实 git 的 -z 原始字节,用于诊断分帧问题
281
+ ```
282
+
283
+ Windows 上用 DSH 自带的 node:
284
+
285
+ ```powershell
286
+ & "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" self-test.mjs
287
+ ```
288
+
289
+ `self-test-git.mjs` 里有一条测试**故意独立于实现**:它同时用 `git status --porcelain -z` 和**非 NUL 的普通格式**问 git 同一个问题,要求两者描述同一组路径。这样解析器写错时测试会失败,而不是跟着实现一起错。
290
+
291
+ 这条测试的由来值得记一笔:第一版 `lib/survey.js` 从一个**不导出该名字**的模块 import 了一个函数。ESM 链接期错误让整个插件图无法求值——`git_commit_push` 和 `/git-commit-push` 都不会注册。而当时的纯逻辑测试因为不 import `survey.js`,根本碰不到它。现在两个测试文件都显式 import 完整模块图,并且 `apply()` 在注册前会用 `toolDefinitionProblems()` 自检 schema。
292
+
293
+ `self-test.mjs` 最后两节查的是**打包与运行时契约**,不是算法:
294
+
295
+ - `dsh.bundle.patch` 指向的文件存在且非空、该 patch 只插入**一行**挂载条目(两行就是挂两次)、`id` 是稳定的 `git-commit-push`、`name` 等于包名、两个安装脚本都不再自己写挂载行;
296
+ - **`files` 白名单是否覆盖入口点 import 的每一个相对模块**——这是 npm 发布最常见的翻车点:包能装上,一加载就找不到模块;
297
+ - 插件页要读的 `exports` 子路径、`locale/*.json`、`icon`(相对路径、类型、≤256 KiB);
298
+ - 每个 DSH peer 都是 `optional`(否则 pnpm 会试图把宿主包装进用户 profile)、`dsh.manifestVersion`、`engines.dsh`;
299
+ - **配置三层来源与字段表的一致性**:字段表 ↔ 内置默认值 ↔ 包内模板三者不许漂移、UI(挂载行 config)胜过 JSON 文件、`pinnedIdentity` 按 key 合并、以及"只有与默认值不同的解析值才算用户改过"这条判定;
300
+ - **设置表单可发布或可降级**:`@deepseek-ai/schemastery` 能解析时 `Config` 必须建出来且每个字段都是 volatile;解析不到时 `Config` 必须是 `undefined` 且**工具照样注册**;
301
+ - 配置优先级(用户文件 > 包内模板)与**损坏的配置必须被报告**;
302
+ - SKILL.md 解析出的 skill 定义满足注册表 `validateRuntimeSkill` 的规则,且 `apply()` 在模拟宿主上确实注册了工具 + 命令 + skill。
303
+
304
+ 它自带两个只认本项目所用语法的小型读取器(YAML patch、skill frontmatter)——本包刻意零依赖,测试不能为了读三五行 YAML 引进一个 parser;读不懂的行会让测试**失败**而不是被忽略。
305
+
306
+ ## 结构
307
+
308
+ ```
309
+ index.js 插件入口:工具定义、/git-commit-push 命令、skill 注册、编排(prepare/apply/auto)
310
+ cordis.patch.yml 组合包 patch:唯一一处挂载行(dsh.bundle.patch 指向它)
311
+ icon.svg 插件页图标(package.json 的 icon)
312
+ locale/en.json 插件页显示文本(meta.title / meta.description,英文)
313
+ locale/zh.json 同上,中文
314
+ lib/git.js 唯一的 git 调用层:固定 argv、超时、输出上限、porcelain 解析、平台探测
315
+ lib/analyze.js 改动分类 + 规则化 Conventional Commits 生成 + 卡片渲染
316
+ lib/survey.js 一次仓库摸底:status / numstat / log / 有界 diff
317
+ lib/config.js 配置读取 + 字段表:挂载行 config > 用户文件 > 模板 > 默认值,并报告损坏的文件
318
+ lib/schema.js Cordis Config(schemastery):可视化表单 + volatile 字段;库不可达时优雅降级
319
+ lib/skill.js 从 SKILL.md 解析出运行时 skill 定义(含 frontmatter 解析)
320
+ lib/profile-edit.mjs 两个安装脚本共用的 profile 清单编辑器(幂等、保留未知字段、无 BOM、自校验)
321
+ setup.ps1 Windows 安装 / 卸载(方式 C)
322
+ setup.sh macOS / Linux 安装 / 卸载(方式 C)
323
+ self-test.mjs 纯逻辑 + 打包 / 配置 / 表单契约自检(86 项)
324
+ self-test-git.mjs 真实 git 集成自检(24 项,自建临时仓库)
325
+ capture-git-format.mjs 打印真实 git 的 -z 原始字节(诊断分帧问题)
326
+ e2e-check.mjs 直连调用 run(),用于不重启验证提交路径
327
+ ```
328
+
329
+ 设计取舍:工具定义是**手写对象**而不是 `defineTool(...)`;运行时的宿主 import 只有一处例外——
330
+ `lib/schema.js` 用 `createRequire` 取 `@deepseek-ai/schemastery` 来声明 `Config`,而且**整段包在 try/catch 里**:
331
+ 取不到就 `Config === undefined`(没有表单),插件与工具照常工作。其余模块仍然只 import Node 内置与相对路径,
332
+ 因为本包既可能以 `link:` 挂在 profile 外,也可能以 npm 包形式躺在 `node_modules` 里,
333
+ 装载不应因为宿主的模块解析没覆盖到本包而失败。(`@deepseek-ai/schemastery` 声明为普通 `dependencies`,
334
+ npm 安装时随包装上;`link:` 安装时 pnpm 不装链接目标的依赖,所以这条降级路径是真实存在的,不是理论情况。)
335
+
336
+ 代价是必须手写**真正的 JSON Schema**:`parameters` 需要 `type: "object"` + `properties` + `required: []`,`output.schema` 的 `required` 必须是**字符串数组**
337
+ (`defineTool` 的 per-property `required: true` 语法只由 `defineTool` 自己编译;手写定义直接送进注册表会被拒,且是在**注册时**抛错,整个插件都装不上)。`toolDefinitionProblems()` 就是这条规则的回归测试。
338
+
339
+ `exports` 里除 `.` 之外还导出 `./package.json` 与 `./locale/*`:插件页读显示文本时走的是 Node 的模块解析(`readPluginMeta` 解析 `<specifier>/package.json` 与 `<specifier>/locale/en.json`),只有 `.` 的 exports 映射会让这两个查找得到 `ERR_PACKAGE_PATH_NOT_EXPORTED`,标题就退化成整串模块说明符。
340
+
341
+ `peerDependencies` 只声明 `@deepseek-ai/dsh-tools` 且标为 **optional**:它的作用是让 DSH 的兼容性检查
342
+ (`evaluatePluginCompatibility`,只读 `peerDependencies`)能拿宿主版本比对;标 optional 则保证 pnpm
343
+ 永远不会为了满足它去下载宿主包。本包实际不 import 它。
344
+
345
+ ## 附:DSH 组合包契约(本包踩过的几个点)
346
+
347
+ 以下各点都照着 `dsh` 打包产物里的实现核对过(`packages/boot/plugin-manager`、`packages/boot/app-boot`、
348
+ `packages/boot/package-manifest`、`packages/skill/skill`),不是推测。
349
+
350
+ 1. **组合包 = `package.json` 里的 `dsh.bundle.patch`**:一个文件路径,或有序的文件路径数组,相对包目录。`dsh.profile.bundles` 里选中的名字,launcher 用 `bundlePatchFiles` / `bundlePatchPaths` 解析后把该 patch 当成一层应用;解析不出 `dsh.bundle` 就抛 `profile bundle "…" declares no dsh.bundle in its package.json` 并**跳过该层**(记进 `skippedBundles`),不会拖垮启动。
351
+ 2. **插件页只管理组合包**。`listBundles()` 会把「已选中却没有 `dsh.bundle`」的名字列成 `error.code = "not-bundle"`(页面文案就是那句「这个包没有声明组合包,不能作为插件管理」);启用/停用只改 `dsh.profile.bundles` 的成员关系、保留依赖,卸载才动依赖。未选中的普通依赖干脆不列出来。
352
+ 3. **兼容性只查 `@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-*` 类型的 peer**,用 `includePrerelease` 参与比较,运行版本取 `dsh-app-boot` 的版本。`peerDependenciesMeta.optional` 不影响这项检查。
353
+ 4. **`dsh.manifestVersion` 与 `engines.dsh` 目前只作声明**(安装器与 loader 都不强制),但它们是 `@deepseek-ai/dsh-package-manifest` 记载的公开作者字段,所以本包照写。
354
+ 5. **显示元数据**由 `readPluginMeta` 通过 Node 解析 `<包名>/package.json`、`<包名>/locale/*.json` 与 `package.json` 的 `icon`(相对路径、SVG/PNG/JPEG/WebP、≤256 KiB、必须留在包目录内)得到,`locale/en.json` 是基准文件。
355
+ 6. **嵌入式 skill** 用 `ctx.skills.register({ name, description, content, … })`:`name` 必须匹配 `/^[a-z0-9]+(?:-[a-z0-9]+)*$/`,`description` 与 `content` 必须是非空字符串(加载时按 `validateDefinition` 再校验一次),`provider` 由注册表填成 `runtime`,优先级为 **项目级 > 运行时 > 用户级**。
356
+ 7. **可视化配置 = 导出 `Config` schema(schemastery,zod 风格)。** DSH 的设置服务(`@deepseek-ai/dsh-settings` + `@deepseek-ai/dsh-config-editor`)为声明了 `Config` 的条目自动生成命名空间与表单(`SettingsNamespaceView.autoGenerate`),写入落到该条目在 profile patch 里的 `config`;插件可 `settings.configure({ auto: false })` 退出自动表单。**被 `.volatile()` 标记的字段**只提交新值并广播 `loader/volatile-update`(Loader 用 `equalExceptVolatile` 比较),**不 remount**;普通字段变化会重新挂载该行。volatile 的放置有硬规则:必须在固定对象路径上,不能落在 dict 值、数组项、map key 或 union/lazy 分支里(`validateVolatileSchema` 会抛错)。
357
+ 8. **设置页不是万能的**:`@deepseek-ai/dsh-settings` 只展示"活动且可唯一定位条目的 volatile 字段",**插件列表**标签是只读的,而 `pluginManager/*` 那套 RPC 只负责装/启用/停用/卸载——所以"能可视化配置"的唯一条件是**插件自己声明 `Config`**。
358
+
359
+ 另外两条容易踩的:
360
+
361
+ - Loader 的 `insert` 是**追加**语义、且不去重——同一个 `id` 插两次就是挂载两份,所以挂载点必须唯一。
362
+ - profile 的 `pnpm-workspace.yaml` 通常带 `autoInstallPeers: false` 与 `nodeLinker: hoisted`:peer 不会被自动安装,未满足时只有一行警告。这也是本包把 peer 标成 optional 的原因之一。
363
+
364
+ ## 维护者:发布到 npm
365
+
366
+ ```sh
367
+ npm login # 或 CI 里的 NPM_TOKEN
368
+ npm test # prepublishOnly 也会跑一遍
369
+ npm publish # publishConfig 已把 registry 固定为 registry.npmjs.org
370
+ ```
371
+
372
+ - `publishConfig.registry` 显式写成 npm 官方源:本机 `~/.npmrc` 若指向 npmmirror(只读镜像),不加这一行容易把 `npm publish` 发到镜像上而失败。
373
+ - 发布后 npmmirror 等镜像有同步延迟,用户立刻装可能拿不到最新版。
374
+ - 版本按 SemVer 递增;`dsh.manifestVersion` 是**清单格式**标识,与包版本无关,不要跟着改。
375
+ - 改了运行时行为就同步 `engines.dsh` 与 `peerDependencies` 里 `@deepseek-ai/dsh-tools` 的范围,两者决定插件页会不会给出「与 DSH 不兼容」的提示。
376
+ - `dependencies` 里的 `@deepseek-ai/schemastery` 是**真实依赖**(设置表单要用):用户那边由 pnpm 正常安装。
377
+ 改它的范围前先确认目标 DSH 版本里 schema API(`.default/.description/.min/.volatile`)没变,否则表单会走降级路径
378
+ (`Config === undefined`,功能不受影响但没有可视化配置)。
379
+
380
+ ## 许可
381
+
382
+ MIT © ygzhang-lab。见 [LICENSE](./LICENSE)。
package/SKILL.md ADDED
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: git-commit-push
3
+ description: 一次调用完成 Git 提交和推送:汇总改动,按 Conventional Commits 为每个变更文件自动生成提交信息,必要时询问打标签,然后推送到当前项目配置的 Git 远程仓库。仅当用户明确要求时才使用(用户敲 /git-commit-push 斜杠命令,或直接说「git 提交 / 提交 / commit / 推送 / push」)。用 DSH 的 git_commit_push 工具一步完成:检查仓库、汇总改动、按 Conventional Commits 生成信息、提交、必要时询问打 tag、推送;未初始化 Git 则不做任何操作。绝不自动触发。
4
+ ---
5
+
6
+ # Skill: git-commit-push
7
+
8
+ ## 触发条件(硬性约束)
9
+
10
+ **只在下面两种情况下使用,任何其他情况都不得触发:**
11
+
12
+ 1. 用户敲了斜杠命令 **`/git-commit-push`**(此时命令自己跑完,不经过你);
13
+ 2. 用户**明确要求** git 提交或推送——「提交」「提交一下」「commit」「推送」「push」「推上去」「git 提交/推送」这类直白指令。
14
+
15
+ **绝不自动触发。** 即使出现下列情形,也不要调用本工具,也不要主动提议:
16
+
17
+ - 你或用户改完代码、写完文件之后 —— 改完不等于要提交;
18
+ - 一个任务「看起来完成了」「该收尾了」;
19
+ - 会话即将结束、上下文快满了;
20
+ - 用户说「完成」「好了」「继续」「存档」但没有明说要 commit/push;
21
+ - 用户只是让你生成、整理或 review 改动(例如「总结这次改动」),并未要求提交。
22
+
23
+ 拿不准时**先问一句**是否要提交,而不是直接提交。
24
+
25
+ ## 具体动作全部交给工具
26
+
27
+ 动作由 **`git_commit_push` 工具**(DSH 插件 `dsh-plugin-git-commit-push`)完成,**不要**自己敲 `git status` / `git add` / `git commit` / `git push`。
28
+
29
+ 插件在 Host 进程内直接读仓库,只回传一张紧凑卡片(改动文件、行数统计、推断的 type/scope、近几条提交风格、每个文件自己的拟定注释、以及整体拟定标题)。
30
+ 所以**不要**读 diff、不要读 `.gitignore`、不要查 `git log` —— 卡片里已经有了,那些输出只会白烧 token。
31
+ tag 询问也由插件内置完成(选项框直接弹给用户,不消耗 token),**不要**再用 `ask_user_question` 问 tag。
32
+
33
+ **省的是什么**:省掉的是「读 git 原始输出」(数千 token → 约 200)与多余往返。工具路径本身**不是** 0 token —— 模型仍要为每次工具调用与卡片付 token;只有用户直接敲 `/git-commit-push`(完全不经过模型)才是真的 0 token。所以**不要**以「省 token」为理由主动多调一次工具。
34
+
35
+ 插件在 Windows 与 macOS/Linux 上行为一致;`cwd` 参数请用该平台的原生绝对路径(Windows `D:\proj`,macOS `/Users/me/proj`)。
36
+
37
+ ## 用法
38
+
39
+ 用户明确要求提交/推送后(**这是触发之后的动作,不是触发条件**):
40
+
41
+ **默认(推荐,质量最好)**:两步入
42
+
43
+ 1. `git_commit_push({ action: "prepare" })` — 只汇总,不改仓库。
44
+ 2. 卡片里每个文件后面都跟着**它自己的拟定注释**(`→ feat(api): 更新 decideRetry`)。你只需要写**标题**,然后
45
+ `git_commit_push({ action: "apply", message: "<type>(<scope>): <中文简述>" })`。
46
+ 插件会自动把这些逐文件注释作为正文补上——**不要**再把每个文件的注释抄进 `message`。
47
+ 确实想自己写正文时,把首行作 subject、后续行作 body 一起放进 `message`(此时插件不再添加任何注释)。
48
+
49
+ **极简(用户说得很随意,如「提一下」)**:`git_commit_push({ action: "auto" })` — 直接用规则生成的信息提交。
50
+
51
+ **其他参数**:`tag: "v1.2.3"`(明确指定标签,跳过询问)、`push: false`(本次不推送)、`cwd: "<仓库路径>"`(会话目录不是仓库时指定目标)。
52
+
53
+ ## 提交信息规范
54
+
55
+ **标题一行**:`<type>(<scope>): <subject>`,subject 用简体中文,与仓库既有风格一致。类型:`feat` 新功能、`fix` 修复、`docs` 文档、`style` 样式、`refactor` 重构、`perf` 性能、`test` 测试、`build` 构建、`ci` CI、`chore` 杂项。
56
+
57
+ **正文逐文件**:一次提交里,body 每个文件一行自己的 Conventional 注释,格式 `- <type>(<scope>): <简述> · <路径>`:
58
+
59
+ ```
60
+ feat(api): 更新 decideRetry
61
+
62
+ - feat(api): 更新 decideRetry · src/api/retry.ts
63
+ - docs: 更新文档 guide · docs/guide.md
64
+ - test(api): 新增 retry.test.ts · src/api/retry.test.ts
65
+ ```
66
+
67
+ 每条注释只描述那一个文件(类型、scope、符号都取自它自己)。**单文件提交不写正文**;超过 `maxFilesShown`(默认 12)个文件时,其余折叠成一行 `- …另有 N 个文件`。
68
+ 需要 `BREAKING CHANGE: ...` 时它属于正文,而你一旦自己写正文,自动注释就不再生效——要么把要保留的注释一并写进去,要么先用 `prepare` 看卡片再决定。
69
+
70
+ ## 边界
71
+
72
+ - 工具自动处理:`git add -A`、提交、tag 判断与询问、推送、无 upstream 时 `-u origin <当前分支>`、推送被拒时 `pull --rebase` 后重推一次、hook 失败即如实报告(不绕过)。
73
+ - 工具**绝不**:修改 `.gitignore`、改 git config、`push --force`、`reset --hard`。
74
+ - 未初始化 Git:工具返回「未初始化 Git」卡片,**只回一句提示并结束**,不做任何其他操作。
75
+ - 推送失败/冲突/凭据缺失:按卡片里的 `error` 字段如实转述给用户,给出选项(保留本地 / 手动处理),**不要**擅自 force push 或丢弃改动。
76
+
77
+ ## 输出
78
+
79
+ 只向用户展示卡片里的关键结果:卡片第一行就是结论(`✅ Git 提交并推送成功` / `✅ Git 提交成功(未推送)` / `⚠️ 已提交,但推送失败` / `❌ 提交失败`),然后是提交信息、文件数、逐文件注释、tag、推送结果。不展示推理过程。
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Capture byte-exact `git status --porcelain=v1 -z` output from real git in a
3
+ * throwaway repository, so the plugin's two porcelain parsers can be checked
4
+ * against ground truth rather than against a guess about how git frames a
5
+ * rename record.
6
+ *
7
+ * This writes only inside a fresh directory under the OS temp directory, and
8
+ * removes it afterwards. It never touches your repositories.
9
+ *
10
+ * Run it with node (any platform):
11
+ *
12
+ * node capture-git-format.mjs
13
+ *
14
+ * On Windows with the bundled runtime:
15
+ * & "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" capture-git-format.mjs
16
+ */
17
+ import { execFileSync } from 'node:child_process'
18
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, renameSync } from 'node:fs'
19
+ import { tmpdir } from 'node:os'
20
+ import { join } from 'node:path'
21
+
22
+ import { DEV_NULL, numstat, parseStatusZ } from './lib/git.js'
23
+
24
+ const cwd = mkdtempSync(join(tmpdir(), 'git-commit-push-probe-'))
25
+ const GIT_ENV = {
26
+ ...process.env,
27
+ LC_ALL: 'C',
28
+ GIT_OPTIONAL_LOCKS: '0',
29
+ GIT_CONFIG_GLOBAL: DEV_NULL,
30
+ GIT_CONFIG_SYSTEM: DEV_NULL,
31
+ }
32
+
33
+ function git(args) {
34
+ return execFileSync('git', args, { cwd, encoding: 'utf8', env: GIT_ENV })
35
+ }
36
+
37
+ /** Render a raw string with NULs and newlines visible, for eyeballing. */
38
+ function visible(text) {
39
+ return text.replace(/\u0000/g, '\\0').replace(/\n/g, '\\n\n')
40
+ }
41
+
42
+ try {
43
+ git(['init', '-q', '-b', 'main'])
44
+ git(['-c', 'user.name=Probe', '-c', 'user.email=probe@example.com', 'commit', '-q', '--allow-empty', '-m', 'init'])
45
+ mkdirSync(join(cwd, 'src'), { recursive: true })
46
+ writeFileSync(join(cwd, 'src', 'keep.js'), 'export const keep = 1\n')
47
+ writeFileSync(join(cwd, 'src', 'old-name.js'), 'export const moved = 1\n')
48
+ writeFileSync(join(cwd, 'src', 'gone.js'), 'export const gone = 1\n')
49
+ writeFileSync(join(cwd, 'src', '中文.js'), 'export const zh = 1\n')
50
+ git(['add', '-A'])
51
+ git(['-c', 'user.name=Probe', '-c', 'user.email=probe@example.com', 'commit', '-q', '-m', 'base'])
52
+
53
+ // 1. a plain modification
54
+ writeFileSync(join(cwd, 'src', 'keep.js'), 'export const keep = 2\n')
55
+ // 2. a staged rename
56
+ git(['mv', 'src/old-name.js', 'src/new-name.js'])
57
+ // 3. a deletion
58
+ rmSync(join(cwd, 'src', 'gone.js'))
59
+ // 4. an untracked file
60
+ writeFileSync(join(cwd, 'src', 'brand-new.js'), 'export const fresh = 1\n')
61
+ // 5. a modification to a non-ascii path
62
+ writeFileSync(join(cwd, 'src', '中文.js'), 'export const zh = 2\n')
63
+ // 6. a path containing a space
64
+ writeFileSync(join(cwd, 'src', 'two words.js'), 'export const two = 1\n')
65
+
66
+ const raw = git(['status', '--porcelain=v1', '-z', '--untracked-files=all'])
67
+
68
+ console.log('\n=== raw `status --porcelain=v1 -z` output ===\n')
69
+ console.log(visible(raw))
70
+
71
+ console.log('\n=== NUL-split tokens (index: value, JSON) ===\n')
72
+ raw.split('\u0000').forEach((token, index) => {
73
+ console.log(`${String(index).padStart(2)}: ${JSON.stringify(token)}`)
74
+ })
75
+
76
+ console.log('\n=== what the plugin parses it into ===\n')
77
+ const parsed = parseStatusZ(raw)
78
+ for (const entry of parsed) console.log(` xy=${JSON.stringify(entry.xy)} path=${JSON.stringify(entry.path)}${entry.origPath === undefined ? '' : ` orig=${JSON.stringify(entry.origPath)}`}`)
79
+
80
+ console.log('\n=== `diff --numstat -z HEAD` and the plugin\'s parse ===\n')
81
+ const rawNumstat = git(['diff', '--numstat', '-z', 'HEAD'])
82
+ console.log(visible(rawNumstat))
83
+ const stats = await numstat(cwd, false)
84
+ for (const [path, stat] of stats) console.log(` ${JSON.stringify(path)} -> +${stat.added}/-${stat.deleted}${stat.binary ? ' binary' : ''}`)
85
+
86
+ console.log('\n=== expected shape ===\n')
87
+ console.log(' 6 changes: M keep.js, R new-name.js (orig old-name.js), D gone.js,')
88
+ console.log(' ?? brand-new.js, M 中文.js, ?? "two words.js"')
89
+ console.log('\nCompare the two lists above. A rename whose "orig" is missing, or a\npath that swallowed the next entry, means parseStatusZ needs fixing.\n')
90
+ } finally {
91
+ rmSync(cwd, { recursive: true, force: true })
92
+ console.log(`cleaned up ${cwd}\n`)
93
+ }
@@ -0,0 +1,35 @@
1
+ # The mount row this package contributes as a DSH profile bundle.
2
+ #
3
+ # WHY THIS FILE EXISTS
4
+ # `package.json` declares `dsh.bundle.patch: ./cordis.patch.yml`. That single
5
+ # declaration is what makes the package a *bundle* to DSH: the launcher reads
6
+ # this patch when the package is listed in the profile's `dsh.profile.bundles`,
7
+ # and the plugin manager lists and manages the package only when it finds a
8
+ # bundle declaration. Without it the package is a plain dependency — and the
9
+ # Plugins page refuses every action with "这个包没有声明组合包,不能作为插件管理"
10
+ # (`not-bundle`).
11
+ #
12
+ # The row names the package exactly as npm publishes it, so a registry install
13
+ # (`pnpm add dsh-plugin-git-commit-push` inside the profile) mounts with no
14
+ # further wiring; the same line serves a `link:` install from a checkout.
15
+ #
16
+ # WHY THE ROW LIVES HERE, NOT IN THE PROFILE PATCH
17
+ # An earlier revision wrote this same row into the profile's own
18
+ # `cordis.patch.yml` and declared no bundle, on the theory that the bundle
19
+ # channel was insufficient. The measurement behind that theory was right and its
20
+ # conclusion was wrong: with the package in `dependencies` and in
21
+ # `dsh.profile.bundles` but no `dsh.bundle.patch`, the launcher throws
22
+ # "profile bundle X declares no dsh.bundle in its package.json" and skips the
23
+ # layer, so nothing was mounted. Declaring the bundle is the fix; the row just
24
+ # has to come from the bundle itself.
25
+ #
26
+ # Insert is append-only in the Loader's patch dialect (see the
27
+ # `cordis-composition-reference` skill): two inserts of the same id mount the
28
+ # plugin TWICE. The profile patch is therefore kept free of this row, and both
29
+ # installers strip a legacy block if they find one.
30
+ #
31
+ # `id` is the Loader entry identity. The plugin manager's per-component switch
32
+ # and any profile override address the row by it, so it must stay stable.
33
+ - insert:
34
+ - id: git-commit-push
35
+ name: 'dsh-plugin-git-commit-push'
package/e2e-check.mjs ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * One-shot manual check: run the plugin's `run()` directly against a repo, so
3
+ * the FIXED code path can be exercised without waiting for a host reload.
4
+ *
5
+ * Usage:
6
+ * node e2e-check.mjs <repo-dir> [message] [tag]
7
+ */
8
+ import { run } from './index.js'
9
+
10
+ const [dir, message, tag] = process.argv.slice(2)
11
+ if (dir === undefined) {
12
+ console.error('usage: node e2e-check.mjs <repo-dir> [message] [tag]')
13
+ process.exit(2)
14
+ }
15
+
16
+ const request = { action: 'apply', cwd: dir, push: false }
17
+ if (message !== undefined) request.message = message
18
+ if (tag !== undefined) request.tag = tag
19
+
20
+ // No agent and no `get`: the hardest possible caller. This is the state that
21
+ // used to drop an explicitly requested tag on the floor.
22
+ const ctx = { get: () => undefined }
23
+ const result = await run(ctx, request, undefined)
24
+
25
+ console.log(JSON.stringify(result, null, 2))
26
+ process.exit(result.ok ? 0 : 1)
@@ -0,0 +1,17 @@
1
+ {
2
+ "$comment": "Settings for dsh-plugin-git-commit-push. PREFERRED: edit them in DSH's own settings form (the plugin exports a Config schema, so its row is editable there and applies live). This file is the fallback layer: put a copy at <DSH_HOME>/git-commit-push.config.json (default ~/.dsh/git-commit-push.config.json) and it wins over the template below, but loses to anything set in the form. The next tool call picks changes up — no DSH restart. Omitted keys keep the built-in defaults; a malformed file is reported in the result card rather than ignored.",
3
+ "autoPush": true,
4
+ "autoAdd": true,
5
+ "tagOnVersionChange": true,
6
+ "tagOnFileCount": 10,
7
+ "tagOnBreaking": true,
8
+ "tagPrefix": "v",
9
+ "askBeforeTag": true,
10
+ "askTimeoutMs": 120000,
11
+ "defaultLanguage": "zh",
12
+ "maxFilesShown": 12,
13
+ "pinnedIdentity": {
14
+ "name": "",
15
+ "email": ""
16
+ }
17
+ }