dsh-gitbash-shell 0.43.1 → 0.43.3
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/CHANGELOG.md +51 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/inline-tools.js +28 -0
- package/src/tool-runner.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,57 @@
|
|
|
3
3
|
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
|
|
4
4
|
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。
|
|
5
5
|
|
|
6
|
+
## [0.43.3] — 2026-10-27
|
|
7
|
+
|
|
8
|
+
### fix:工具面晚于 `agent/created` 落定时,bash 补位永久错过(真机复现;PR #36,致谢 @qiantao448-ux 首次贡献)
|
|
9
|
+
|
|
10
|
+
- **症状**:冷启动后某个会话既没有 `bash`、又留着一个被拦截的 `pwsh`。插件日志里**没有**
|
|
11
|
+
`own bash tool activated`,但 `tools/pre-execute` 的拒绝句照旧生效——两层里只有调用层活着,
|
|
12
|
+
「注册 bash + 遮蔽 pwsh」那一次 `reconcile` 从没跑成。
|
|
13
|
+
- **根因**:`reconcile` 只由 `agent/created`、挂载时的存量 sweep 和设置开关变化触发;面为空时
|
|
14
|
+
250ms×5 有界重试,**过期即终局**。桌面端启动要先挂 profile 级插件再挂预设行,这个窗口很容易
|
|
15
|
+
被跨过去,而宿主其实有 `tools/change` 事件——本插件没监听,也没有第二个落点。
|
|
16
|
+
- **修法**:在 `tools/pre-execute` 里做一次**惰性再对账**——该 agent 还没有 bash scope 时重跑
|
|
17
|
+
决策。第一次工具调用是"面一定已落定"的最早时刻;`state.scope` 一旦存在即永久跳过,幂等,
|
|
18
|
+
且不监听 `tools/change`(不会自己触发自己)。
|
|
19
|
+
- **真机验证**:Windows 桌面端(bash 5.3,`pwshTool` 关闭),重启后日志出现
|
|
20
|
+
`own bash tool ready (private Git Bash executor at C:/Program Files/Git/bin/bash.exe)` 与
|
|
21
|
+
`own bash tool activated for this agent`,`bash` 工具可用、`uname -s` 返回 `MINGW64_NT-*`。
|
|
22
|
+
|
|
23
|
+
### fix:面里没有 pwsh 时撤掉自己注册的 bash(遮蔽型 preset 不再漏 shell)
|
|
24
|
+
|
|
25
|
+
- 自有 bash 注册在 **agent 自有 scope**;宿主契约写明掩码只裁全局工具、**scope 注册保持可见**,
|
|
26
|
+
所以一旦注册成功,任何 preset 的 `restrict({ allow })` 都拿不回来。若在面还没落定时抢注,
|
|
27
|
+
纯聊天类 preset(只放行两个工具的白名单)会凭空多出一条命令通道。
|
|
28
|
+
- 修法:`reconcile` 观测到的 `names` 是**解开自身 pwsh 掩码之后**的面,权威;只要它非空且不含
|
|
29
|
+
`pwsh`,就 dispose 自己那个 scope 并撤掉 ops shell 遮蔽,下一轮再按真实面决策。
|
|
30
|
+
`names.length > 0` 兜住"注册表读不到/读到空"的情形,避免把读取失败误判成"被掩码"。
|
|
31
|
+
- 与上一条互为正反面:一条负责"该有时一定要有",一条负责"不该有时一定没有"。
|
|
32
|
+
|
|
33
|
+
### 补充(维护者):回归测试锁定两处新行为
|
|
34
|
+
|
|
35
|
+
- `tests/host-regressions.mjs` 新增两测试:①`inlineNeeds` 对 pwsh-less/空面的负例(撤回永不
|
|
36
|
+
重建的直接依据:`needsBash` 要求 `pwsh` 在场);②源码断言锁 lazy 再对账的守卫形状
|
|
37
|
+
(scope/pending 幂等 + try/catch 不破坏工具调用)与 stand-down 分支的位置与顺序
|
|
38
|
+
(条件 `names.length > 0 && !names.includes('pwsh') && state.scope`,scope 置空先于
|
|
39
|
+
`needsBash` 判断,dispose 真实发生并留日志)。全量 336 项 0 fail。
|
|
40
|
+
|
|
41
|
+
## [0.43.2] — 2026-10-23
|
|
42
|
+
|
|
43
|
+
### fix:被我们杀掉的 run 统一归 `code=-1`(Windows `taskkill /F` 退出码语义)
|
|
44
|
+
|
|
45
|
+
- **0.43.1 的 CI(windows-latest)仍红,这次咬到的是生产代码**:`taskkill /F` 强杀的进程在
|
|
46
|
+
Windows 上 exit code 是 **1**(POSIX 是 signal → null → 我们的 `-1`)。超时/取消/看门狗三种
|
|
47
|
+
被击杀结局在 win 上会报告 `code=1`——读起来像 winget 失败,实际是被我们杀掉的,verdict 也
|
|
48
|
+
因此可能被外部观察者误读(verdictFrom 本身只信标志,不受影响)。
|
|
49
|
+
- **修法**:`defaultRunnerIo` 的 `close` 处理里,任一击杀标志(cancelled / timedOut /
|
|
50
|
+
networkRetry)命中时 `code` 统一归 `-1`——被我们杀掉的 run 没有「自然退出码」,跨平台语义
|
|
51
|
+
一致。自然退出(包括失败)的 code 透传不变。
|
|
52
|
+
- 教训:win32 分支的**杀进程退出码**语义 macOS 不可见——本仓库第三次被同一类问题咬
|
|
53
|
+
(v0.39.4 ns 字面量 → v0.43.0/1 pathname 双盘符 → 本次 taskkill 退出码);CI windows 矩阵
|
|
54
|
+
是唯一能看到它们的场合,发版纪律「CI 全绿才收口」的价值再次实证。
|
|
55
|
+
- 全量 334 项本机 0 fail;以 CI(ubuntu+windows × Node 22/24)全绿为收口门。
|
|
56
|
+
|
|
6
57
|
## [0.43.1] — 2026-10-23
|
|
7
58
|
|
|
8
59
|
### fix:测试装置的 Windows 路径 bug(v0.43.0 的 CI 修复,无功能变化)
|
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-external/dsh-gitbash-shell",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.3",
|
|
4
4
|
"main": "./src/index.js",
|
|
5
5
|
"description": "Windows 全模式 Git Bash:自带 bash 工具(私有 Git Bash 执行器实例,官方 PowerShell 执行器原样保留),官方四个模式经行内工具补位直接获得 Git Bash(v0.35.0 起;v0.39.0 起不再替换宿主执行器),四个 *·Git Bash 变体 preset 可开关;官方 pwsh 工具默认隐藏、开关可见(真 PowerShell);附带 gb_* FastCtx 文件工具,检测到 dsh-ops 自动让位。联动:dsh-ptc-cordis-preset(PTC 创造模式带 Git Bash 行且可去重)+ dsh-better-sidebar(终端标签自动切 Git Bash)。",
|
|
6
6
|
"engines": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gitbash-shell",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.3",
|
|
4
4
|
"description": "Windows 上给 agent 一个真正的 Git Bash 命令工具:私有 Git Bash 执行器实例(官方 PowerShell 执行器原样保留),统一 MSYS 盘根路径方言,官方 pwsh 工具默认隐藏可开关,物化四个内置预设的 Git Bash 变体,侧栏终端接管可开关。· Give agents a real Git Bash tool on Windows: a private Git Bash executor instance (the official PowerShell executor stays untouched), a unified MSYS drive-root path dialect, an opt-in official pwsh tool, and four Git Bash preset variants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/inline-tools.js
CHANGED
|
@@ -502,6 +502,21 @@ export function mountInlineTools(ctx, deps) {
|
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
|
+
// A face WITHOUT pwsh is not the official win32 composition: something
|
|
506
|
+
// masked the host tool away, and the only such mask in the wild is a
|
|
507
|
+
// preset gate (`tools.restrict({ allow })`, e.g. the pure-chat preset).
|
|
508
|
+
// Our bash lives in an AGENT-OWNED scope, and scoped registrations stay
|
|
509
|
+
// visible through masks by contract — so a registration made while the
|
|
510
|
+
// face was still settling would leak a shell into that preset for the
|
|
511
|
+
// rest of the agent's life. `names` above is the PRE-MASK observation
|
|
512
|
+
// (our own deny was lifted first), so it is authoritative: stand down.
|
|
513
|
+
if (names.length > 0 && !names.includes('pwsh') && state.scope) {
|
|
514
|
+
const withdrawn = state.scope
|
|
515
|
+
state.scope = undefined
|
|
516
|
+
if (state.denyOpsShell) { state.denyOpsShell(); state.denyOpsShell = undefined }
|
|
517
|
+
void Promise.resolve(withdrawn.dispose()).catch((error) => log(`own bash withdrawal failed: ${error?.message ?? error}`))
|
|
518
|
+
log('own bash tool withdrawn for this agent (the tool face carries no pwsh: a preset mask owns the tool set)')
|
|
519
|
+
}
|
|
505
520
|
if (needs.needsBash && !state.scope) {
|
|
506
521
|
startBash(agent, state)
|
|
507
522
|
}
|
|
@@ -576,6 +591,19 @@ export function mountInlineTools(ctx, deps) {
|
|
|
576
591
|
// ON the REAL tool answers for itself.
|
|
577
592
|
ctx.on('tools/pre-execute', (exec, next) => {
|
|
578
593
|
try {
|
|
594
|
+
// THE FACE CAN SETTLE AFTER `agent/created` (Unreleased). Preset rows
|
|
595
|
+
// and plugin tools keep registering while the Loader walks the tree,
|
|
596
|
+
// so on a slow boot the bounded retry above can burn through its five
|
|
597
|
+
// 250ms attempts on an empty face and be FINAL: the agent then never
|
|
598
|
+
// gets its bash and keeps a visible (but denied) pwsh. The first tool
|
|
599
|
+
// call is the first moment the face is guaranteed to be settled, so
|
|
600
|
+
// re-run the decision there while this agent still owns no bash scope.
|
|
601
|
+
// Idempotent: once the scope exists this branch never fires again.
|
|
602
|
+
const lazyAgent = exec?.agent
|
|
603
|
+
const lazyState = lazyAgent ? tracked.get(lazyAgent) : undefined
|
|
604
|
+
if (lazyState && !lazyState.scope && !lazyState.pending) {
|
|
605
|
+
try { reconcile(lazyAgent) } catch (error) { log(`lazy re-reconcile failed: ${error?.message ?? error}`) }
|
|
606
|
+
}
|
|
579
607
|
if (exec?.name === 'pwsh' && !opsSeen && deps.pwshEnabled?.() !== true) {
|
|
580
608
|
return { kind: 'deny', reason: hiddenPwshDenyReason() }
|
|
581
609
|
}
|
package/src/tool-runner.js
CHANGED
|
@@ -165,8 +165,13 @@ export function defaultRunnerIo(options = {}) {
|
|
|
165
165
|
clearTimeout(timer)
|
|
166
166
|
if (retryTimer !== null) { clearTimeout(retryTimer); retryTimer = null }
|
|
167
167
|
if (current !== null && current.child === child) current = null
|
|
168
|
+
// A run WE killed has no meaningful exit code. POSIX reports null
|
|
169
|
+
// (→ -1 below), but Windows' taskkill /F makes the victim exit 1 —
|
|
170
|
+
// which would read as a winget failure instead of the kill it was,
|
|
171
|
+
// so any flagged ending normalizes to -1.
|
|
172
|
+
const killed = flags.cancelled === true || flags.timedOut === true || flags.networkRetry === true
|
|
168
173
|
resolve({
|
|
169
|
-
code: typeof code === 'number' ? code : -1,
|
|
174
|
+
code: typeof code === 'number' && !killed ? code : -1,
|
|
170
175
|
output,
|
|
171
176
|
...(flags.cancelled === true ? { cancelled: true } : {}),
|
|
172
177
|
...(flags.timedOut === true ? { timedOut: true } : {}),
|