dsh-gitbash-shell 0.22.0 → 0.24.0

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/cordis.patch.yml CHANGED
@@ -34,7 +34,10 @@
34
34
  timeoutMs: 60000
35
35
  bashPath: "C:/Program Files/Git/bin/bash.exe"
36
36
 
37
- - id: gitbash-presets
37
+ # Row id 'gitbash-shell' (was 'gitbash-presets' through v0.23.0): dsh >= 0.1.7
38
+ # keys a plugin's live Config form and the one-shot legacy settings.yaml
39
+ # import by the row id, so it now matches the settings namespace string.
40
+ - id: gitbash-shell
38
41
  name: dsh-gitbash-shell
39
42
 
40
43
  # Edit the dsh-better-sidebar row when it is mounted (their own bundle
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-gitbash-shell",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "main": "./src/index.js",
5
5
  "description": "Windows 全模式 Git Bash:用 Git for Windows bash 替换 PowerShell 执行器,并物化 standard/minimal/code/cordis 的 Git Bash 变体 preset。双 era 适配 dsh <= 0.1.1(内置 id 'code')与 >= 0.1.2(改名 'ptc'),启动探测自动选择,preset id 永不变。联动:dsh-ptc-cordis-preset(两者同装时 PTC 创造模式自动物化为 Git Bash 版)+ dsh-better-sidebar(终端 UI 标签与模型 terminal_* 工具自动切到 Git Bash)。",
6
6
  "engines": {
package/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" fill="none">
2
+ <rect x="4" y="6" width="28" height="24" rx="3.5" stroke="#4493f8" stroke-width="1.8"/>
3
+ <path d="M4 12h28" stroke="#4493f8" stroke-width="1.4"/>
4
+ <circle cx="7.6" cy="9" r="1" fill="#4493f8"/>
5
+ <circle cx="11" cy="9" r="1" fill="#4493f8"/>
6
+ <path d="M10 18.5l4 3.5-4 3.5M17 25.5h8" stroke="#f0883e" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/>
7
+ </svg>
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Git Bash Shell",
4
+ "description": "Swaps Windows' PowerShell shell for Git for Windows bash: a bash-sandbox-compatible executor, a unified MSYS path dialect across every tool (posixPaths and friends, live-editable), and the four * Git Bash agent presets declared straight onto the roster on dsh 0.1.7+."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Git Bash Shell",
4
+ "description": "把 Windows 的 PowerShell shell 换成 Git for Windows bash:兼容 bash-sandbox 的执行器、全工具统一的 MSYS 路径方言(posixPaths 等开关可实时编辑),以及 dsh 0.1.7+ 上直接声明进取的四个 * · Git Bash 预设。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-gitbash-shell",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Windows 上让 agent 的每条 shell 命令改经 Git for Windows bash 执行;物化四个内置预设的 Git Bash 变体,统一 MSYS 盘根路径方言,侧栏终端接管可开关。· Run every agent shell command through Git for Windows bash on Windows; materialize Git Bash variants of the built-in presets, unify the MSYS drive-root path dialect, and adopt the sidebar terminal behind a switch.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -11,7 +11,8 @@
11
11
  "exports": {
12
12
  ".": "./src/index.js",
13
13
  "./shell": "./src/shell.js",
14
- "./client": "./src/client.js"
14
+ "./client": "./src/client.js",
15
+ "./locale/*.json": "./locale/*.json"
15
16
  },
16
17
  "engines": {
17
18
  "node": ">=18",
@@ -38,7 +39,9 @@
38
39
  "dsh.plugin.json",
39
40
  "README.md",
40
41
  "README_EN.md",
41
- "LICENSE"
42
+ "LICENSE",
43
+ "locale",
44
+ "icon.svg"
42
45
  ],
43
46
  "scripts": {
44
47
  "test": "node --test tests/smoke.mjs",
@@ -63,5 +66,9 @@
63
66
  "homepage": "https://github.com/KannaKuron/dsh-gitbash-shell",
64
67
  "bugs": {
65
68
  "url": "https://github.com/KannaKuron/dsh-gitbash-shell/issues"
69
+ },
70
+ "icon": "./icon.svg",
71
+ "devDependencies": {
72
+ "@deepseek-ai/schemastery": "^3.18.1"
66
73
  }
67
74
  }
package/src/client.js CHANGED
@@ -1058,11 +1058,46 @@ window.__ModuleLoader__.load({
1058
1058
 
1059
1059
  exports.name = "dsh-gitbash-shell/client";
1060
1060
 
1061
- /** Required client services: locale runtime, settings scopes, slots. */
1062
- exports.inject = ["locale", "settingsScope", "slots"];
1061
+ /**
1062
+ * Required client services: only era-guaranteed ones are hard-injected;
1063
+ * the settings face is acquired OPTIONALLY (dsh 0.1.7 removed the
1064
+ * settingsScope service and a hard inject would leave this fiber PENDING
1065
+ * forever, taking the card down with it).
1066
+ */
1067
+ exports.inject = ["locale", "slots"];
1063
1068
 
1064
1069
  exports.apply = function (ctx) {
1065
- var scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
1070
+ // Era-split settings face (same contract both eras: getSnapshot/set/unset).
1071
+ var scope = null;
1072
+
1073
+ // OLD era (dsh <= 0.1.6): bound settings scope.
1074
+ try {
1075
+ ctx.inject(["settingsScope"], function (sctx) {
1076
+ try {
1077
+ var svc = sctx && sctx.settingsScope;
1078
+ if (svc && typeof svc.bind === "function") scope = svc.bind({ namespace: SETTINGS_NAMESPACE });
1079
+ } catch (error) {
1080
+ console.warn(TAG + " settingsScope acquisition failed:", error && error.message ? error.message : error);
1081
+ }
1082
+ });
1083
+ } catch (error) {
1084
+ console.warn(TAG + " settingsScope wiring failed:", error && error.message ? error.message : error);
1085
+ }
1086
+
1087
+ // NEW era (dsh >= 0.1.7): one ConfigForm per live profile entry; the form
1088
+ // key is the row id "gitbash-shell" (same string as the old namespace).
1089
+ try {
1090
+ ctx.inject(["configForms"], function (fctx) {
1091
+ try {
1092
+ var forms = fctx && fctx.configForms;
1093
+ if (forms && typeof forms.get === "function") scope = forms.get(SETTINGS_NAMESPACE);
1094
+ } catch (error) {
1095
+ console.warn(TAG + " configForms acquisition failed:", error && error.message ? error.message : error);
1096
+ }
1097
+ });
1098
+ } catch (error) {
1099
+ console.warn(TAG + " configForms wiring failed:", error && error.message ? error.message : error);
1100
+ }
1066
1101
  /* The card's only copy entry point: a live lookup, never a captured
1067
1102
  dictionary — the language preference switches without a reload. */
1068
1103
  var t = translatorOf(ctx);
@@ -0,0 +1,296 @@
1
+ /**
2
+ * dsh-gitbash-shell — declarative composition rows (dsh >= 0.1.7).
3
+ *
4
+ * dsh 0.1.7 removed directory presets; a preset is a definition registered
5
+ * through agentPresets.register(). This module is the committed, reviewable
6
+ * composition DATA for the four Git Bash variants — the new-era counterpart
7
+ * of the committed assets/<variant>/agent.cordis*.yml texts the materializer
8
+ * writes on older hosts.
9
+ *
10
+ * Every variant mirrors the official 0.1.7 preset it replaces (packages/
11
+ * bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
12
+ * one Git Bash delta: the bash rows are always on and the pwsh rows are
13
+ * always off (the host executor is this plugin's shell.js — on non-Windows
14
+ * hosts that is the native stack anyway, which is exactly what the old
15
+ * materialized variants encoded).
16
+ */
17
+
18
+ /** Default Git Bash binary — must match src/shell.js and the patch config. */
19
+ const DEFAULT_GIT_BASH = 'C:/Program Files/Git/bin/bash.exe'
20
+
21
+ function off(value) {
22
+ return value === true ? { disabled: true } : {}
23
+ }
24
+
25
+ const PLAN_SECTION = [
26
+ 'You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user\'s conversational agreement — including an answer confirming you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.',
27
+ '',
28
+ 'Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.',
29
+ '',
30
+ 'The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs to exit_plan_mode.',
31
+ '',
32
+ 'Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.',
33
+ '',
34
+ 'Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.',
35
+ '',
36
+ 'When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch the session mode manually; do not proceed with implementation.',
37
+ ].join('\n')
38
+
39
+ /**
40
+ * The shared full-tool rows for standard / ptc / cordis variants (pure).
41
+ * @param {object} input
42
+ * @param {'standard'|'ptc'|'cordis'} input.kind - which official preset this
43
+ * variant mirrors (workflow rows follow it: enabled on standard/cordis,
44
+ * disabled on ptc).
45
+ * @param {boolean} input.gitBash - bash rows always on, pwsh rows always off.
46
+ * @param {string|undefined} input.skillsDir - Creation authoring skills
47
+ * directory (cordis variant only; resolved beside the agent-preset package).
48
+ * @returns {object[]} the declarative plugins list.
49
+ */
50
+ export function pluginsFor({ kind, gitBash, skillsDir }) {
51
+ const win = typeof process !== 'undefined' && process.platform === 'win32'
52
+ const bashDisabled = gitBash ? false : win
53
+ const pwshDisabled = gitBash ? true : !win
54
+ const workflowOn = kind !== 'ptc'
55
+ const rows = [
56
+ {
57
+ id: 'persona',
58
+ name: '@deepseek-ai/dsh-persona',
59
+ config: {
60
+ prefix: 'You are a coding agent powered by the {{model}} model.',
61
+ suffix: 'Your working directory is {{cwd}}.',
62
+ },
63
+ },
64
+ {
65
+ id: 'agent-instructions',
66
+ name: '@deepseek-ai/dsh-agent-instructions',
67
+ config: { maxBytes: 65536 },
68
+ },
69
+ { id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', ...off(bashDisabled) },
70
+ { id: 'tool-pwsh', name: '@deepseek-ai/dsh-tool-pwsh', ...off(pwshDisabled) },
71
+ { id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
72
+ {
73
+ id: 'tool-fs-search',
74
+ name: '@deepseek-ai/dsh-tool-fs-search',
75
+ config: { sampleOverCapGlobResults: false },
76
+ },
77
+ { id: 'tool-jobs', name: '@deepseek-ai/dsh-tool-jobs' },
78
+ {
79
+ id: 'skill-filesystem',
80
+ name: '@deepseek-ai/dsh-skill-filesystem',
81
+ config: kind === 'cordis' && skillsDir ? { customSkillDirs: [skillsDir] } : {},
82
+ },
83
+ { id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
84
+ { id: 'command-goal', name: '@deepseek-ai/dsh-command-goal' },
85
+ { id: 'tool-goal', name: '@deepseek-ai/dsh-tool-goal' },
86
+ {
87
+ id: 'planning',
88
+ name: 'cordis:group',
89
+ group: true,
90
+ isolate: { planMode: true },
91
+ config: [
92
+ {
93
+ id: 'plan-mode',
94
+ name: '@deepseek-ai/dsh-plan-mode',
95
+ config: { section: PLAN_SECTION },
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ id: 'compaction',
101
+ name: 'cordis:group',
102
+ group: true,
103
+ isolate: { compaction: true, toolResultPruner: true },
104
+ config: [
105
+ { id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
106
+ { id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
107
+ {
108
+ id: 'tool-result-pruner',
109
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner',
110
+ config: { thresholdChars: 8192, headChars: 4096, tailChars: 1024 },
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ id: 'delegation',
116
+ name: 'cordis:group',
117
+ group: true,
118
+ isolate: { workflowEngine: true },
119
+ config: [
120
+ { id: 'tool-subagent-control', name: '@deepseek-ai/dsh-tool-subagent-control' },
121
+ { id: 'tool-subagent-list-agents', name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' },
122
+ {
123
+ id: 'tool-subagent',
124
+ name: '@deepseek-ai/dsh-tool-subagent',
125
+ config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true, backgroundMode: 'continuable' },
126
+ },
127
+ {
128
+ id: 'tool-subagent-fork',
129
+ name: '@deepseek-ai/dsh-tool-subagent',
130
+ config: { provider: 'fork', toolName: 'subagent_fork', backgroundMode: 'continuable' },
131
+ },
132
+ {
133
+ id: 'tool-subagent-codex',
134
+ name: '@deepseek-ai/dsh-tool-subagent',
135
+ disabled: true,
136
+ config: { provider: 'codex', toolName: 'subagent_codex', backgroundMode: 'one-shot', maxDepth: 'provider-managed' },
137
+ },
138
+ {
139
+ id: 'tool-subagent-claude-code',
140
+ name: '@deepseek-ai/dsh-tool-subagent',
141
+ disabled: true,
142
+ config: { provider: 'claude-code', toolName: 'subagent_claude_code', backgroundMode: 'one-shot', maxDepth: 'provider-managed' },
143
+ },
144
+ {
145
+ id: 'workflow-ptc',
146
+ name: '@deepseek-ai/dsh-workflow-ptc',
147
+ ...off(!workflowOn),
148
+ config: { provider: 'spawn' },
149
+ },
150
+ { id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow', ...off(!workflowOn) },
151
+ {
152
+ id: 'tool-ralph',
153
+ name: '@deepseek-ai/dsh-tool-ralph',
154
+ disabled: true,
155
+ config: { subagentProvider: 'spawn', maxRounds: 64 },
156
+ },
157
+ ],
158
+ },
159
+ { id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user' },
160
+ {
161
+ id: 'tool-todo',
162
+ name: '@deepseek-ai/dsh-tool-todo',
163
+ config: { allowParallelInProgress: true },
164
+ },
165
+ {
166
+ id: 'tool-web',
167
+ name: '@deepseek-ai/dsh-tool-web',
168
+ config: { fetch: true, searchTimeoutMs: 60000 },
169
+ },
170
+ ]
171
+ if (kind === 'ptc') {
172
+ rows.push({
173
+ id: 'tool-presentation',
174
+ name: '@deepseek-ai/dsh-agent-tool-presentation',
175
+ config: { mode: 'ptc' },
176
+ })
177
+ }
178
+ if (kind === 'cordis') {
179
+ rows.push({ id: 'tool-cordis', name: '@deepseek-ai/dsh-tool-cordis' })
180
+ }
181
+ rows.push({ id: 'present', name: '@deepseek-ai/dsh-tool-present' })
182
+ rows.push({
183
+ id: 'tool-plugin-manager',
184
+ name: '@deepseek-ai/dsh-plugin-manager/tools',
185
+ // The official 0.1.7 rows: ptc/standard ship it disabled, cordis keeps it
186
+ // on wherever a profile context exists (every web deployment does).
187
+ ...off(kind !== 'cordis'),
188
+ })
189
+ return rows
190
+ }
191
+
192
+ /**
193
+ * The minimal variant: the official single-tool preset with the Git Bash
194
+ * delta — bash rows always on (the terminal row pins the Git Bash binary on
195
+ * Windows), pwsh rows always off (pure).
196
+ * @returns {object[]} the declarative plugins list.
197
+ */
198
+ export function minimalPluginsFor() {
199
+ const win = typeof process !== 'undefined' && process.platform === 'win32'
200
+ return [
201
+ {
202
+ id: 'persona',
203
+ name: '@deepseek-ai/dsh-persona',
204
+ config: {
205
+ prefix: 'You are a helpful software engineer assistant.',
206
+ complete: true,
207
+ includeRuntimeContext: false,
208
+ },
209
+ },
210
+ {
211
+ id: 'persistent-shell',
212
+ name: 'cordis:group',
213
+ group: true,
214
+ isolate: { terminals: true },
215
+ config: [
216
+ { id: 'pty', name: '@deepseek-ai/dsh-terminal' },
217
+ {
218
+ id: 'terminal-bash',
219
+ name: '@deepseek-ai/dsh-terminal-bash',
220
+ disabled: false,
221
+ config: {
222
+ timeoutMs: 300000,
223
+ shellPath: win ? DEFAULT_GIT_BASH : '/bin/bash',
224
+ },
225
+ },
226
+ {
227
+ id: 'persistent-bash',
228
+ name: '@deepseek-ai/dsh-tool-bash-persistent',
229
+ disabled: false,
230
+ config: {
231
+ timeoutMs: 300000,
232
+ description: [
233
+ 'Run commands in a bash shell (Git for Windows bash on Windows: native paths like C:/... work, and PATH plus the rest of the environment are inherited from the host)',
234
+ '* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
235
+ '* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.',
236
+ '* State is persistent across command calls and discussions with the user.',
237
+ '* To inspect a particular line range of a file, e.g. lines 10-25, try \'sed -n 10,25p /path/to/the/file\'.',
238
+ '* Please avoid commands that may produce a very large amount of output.',
239
+ '* Please run long lived commands in the background, e.g. \'sleep 10 &\' or start a server in the background.',
240
+ ].join('\n'),
241
+ },
242
+ },
243
+ {
244
+ id: 'terminal-pwsh',
245
+ name: '@deepseek-ai/dsh-terminal-bash',
246
+ disabled: true,
247
+ config: { shellDialect: 'pwsh', timeoutMs: 300000 },
248
+ },
249
+ {
250
+ id: 'persistent-pwsh',
251
+ name: '@deepseek-ai/dsh-tool-pwsh-persistent',
252
+ disabled: true,
253
+ config: {
254
+ timeoutMs: 300000,
255
+ description: [
256
+ 'Run commands in a PowerShell shell',
257
+ '* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
258
+ "* You don't have access to the internet via this tool.",
259
+ '* Use native Windows paths (C:/...) and $env:NAME variables; this is PowerShell, not bash.',
260
+ '* Please avoid commands that may produce a very large amount of output.',
261
+ '* Please run long lived commands in the background, e.g. \'Start-Job\' or start a server with Start-Process.',
262
+ ].join('\n'),
263
+ },
264
+ },
265
+ ],
266
+ },
267
+ ]
268
+ }
269
+
270
+ /** Display metadata for the four registrations (mirrors the committed preset.yml files). */
271
+ export const PRESET_META = {
272
+ 'standard-gitbash': {
273
+ kind: 'standard',
274
+ name: '标准模式 · Git Bash',
275
+ description: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。(Shell 使用 Git Bash)',
276
+ order: 1,
277
+ },
278
+ 'code-gitbash': {
279
+ kind: 'ptc',
280
+ name: 'PTC 模式 · Git Bash',
281
+ description: '功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。(Shell 使用 Git Bash)',
282
+ order: 2,
283
+ },
284
+ 'minimal-gitbash': {
285
+ kind: 'minimal',
286
+ name: '极简模式 · Git Bash',
287
+ description: '仅提供持久 shell 的单工具编码 Agent。(Shell 使用 Git Bash)',
288
+ order: 3,
289
+ },
290
+ 'cordis-gitbash': {
291
+ kind: 'cordis',
292
+ name: '创造模式 · Git Bash',
293
+ description: '用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。(Shell 使用 Git Bash)',
294
+ order: 4,
295
+ },
296
+ }
package/src/index.js CHANGED
@@ -52,8 +52,11 @@ import {
52
52
  } from 'node:fs'
53
53
  import { homedir, tmpdir } from 'node:os'
54
54
  import { dirname, join } from 'node:path'
55
+ import Schema from '@deepseek-ai/schemastery'
55
56
  import { fileURLToPath } from 'node:url'
56
57
 
58
+ import { PRESET_META, minimalPluginsFor, pluginsFor } from './compositions.js'
59
+
57
60
  /** Plugin identity for cordis.yml rows. */
58
61
  export const name = 'dsh-gitbash-shell'
59
62
 
@@ -662,6 +665,79 @@ export function installRegisterShim(reg) {
662
665
 
663
666
  const SETTINGS_NAMESPACE = 'gitbash-shell'
664
667
 
668
+ /**
669
+ * dsh >= 0.1.7 marks a Config field live-editable without remount; a
670
+ * 0.1.6-era schemastery predates the method and the guard keeps this module
671
+ * loadable there (values then behave as ordinary config, read through the
672
+ * registered settings namespace instead).
673
+ */
674
+ function live(schema) {
675
+ return typeof schema.volatile === 'function' ? schema.volatile() : schema
676
+ }
677
+
678
+ /**
679
+ * Row Config = the settings surface on dsh >= 0.1.7 (values persist under the
680
+ * row id; the patch row id is 'gitbash-shell', the same string as the old
681
+ * settings namespace, so the one-shot legacy settings.yaml import maps old
682
+ * user values onto the new home). Inert metadata on older hosts.
683
+ */
684
+ export const Config = Schema.object({
685
+ // Row-level knob predating the Config surface (invariant 6): which preset
686
+ // variants to serve. Ordinary config — an edit remounts the plugin, which
687
+ // is the right weight for a list that changes the roster.
688
+ presets: Schema.array(Schema.string()).default(PRESET_IDS),
689
+ posixPaths: live(Schema.boolean().default(true)),
690
+ virtualMounts: live(Schema.boolean().default(true)),
691
+ globSplit: live(Schema.boolean().default(true)),
692
+ errorDialect: live(Schema.boolean().default(true)),
693
+ codePaths: live(Schema.boolean().default(true)),
694
+ gitAutocrlf: live(Schema.boolean().default(true)),
695
+ bashPath: live(Schema.string().default('')),
696
+ adoptSidebar: live(Schema.boolean().default(true)),
697
+ })
698
+
699
+ /** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
700
+ export function valueOf(value) {
701
+ return value && typeof value.get === 'function' ? value.get() : value
702
+ }
703
+
704
+ /**
705
+ * Era-aware live settings reader for everything wired inside apply(): the
706
+ * NEW era reads this plugin's own Config refs (no listener needed — every
707
+ * consumer here already re-reads per dispatch/assembly), the OLD era keeps
708
+ * reading the registered namespace through the settings service.
709
+ * @param {object} ctx - the plugin's mounting context.
710
+ * @param {object|undefined} config - apply()'s resolved Config.
711
+ * @returns {{ dialect: () => object, posix: () => boolean, adoptSidebar: () => boolean }}
712
+ */
713
+ function makeLiveReader(ctx, config) {
714
+ let legacy = false
715
+ try {
716
+ const settings = ctx.get('settings')
717
+ legacy = !!(settings && typeof settings.register === 'function')
718
+ } catch { legacy = false }
719
+ const hasConfig = !!(config && typeof config === 'object')
720
+ const dialect = () => {
721
+ if (legacy || !hasConfig) return readDialectSettings(ctx)
722
+ const bash = valueOf(config.bashPath)
723
+ return {
724
+ posixPaths: valueOf(config.posixPaths) === true,
725
+ virtualMounts: valueOf(config.virtualMounts) === true,
726
+ globSplit: valueOf(config.globSplit) === true,
727
+ errorDialect: valueOf(config.errorDialect) === true,
728
+ codePaths: valueOf(config.codePaths) === true,
729
+ gitAutocrlf: valueOf(config.gitAutocrlf) === true,
730
+ bashPath: typeof bash === 'string' ? bash : '',
731
+ adoptSidebar: valueOf(config.adoptSidebar) !== false,
732
+ }
733
+ }
734
+ return {
735
+ dialect,
736
+ posix: () => dialect().posixPaths === true,
737
+ adoptSidebar: () => dialect().adoptSidebar !== false,
738
+ }
739
+ }
740
+
665
741
  /** The full directive text, injected only while posixPaths is on. */
666
742
  const POSIX_DIRECTIVE_TEXT = 'The working shell is Git for Windows bash: paths use MSYS drive roots (/c/Users/...), and every tool accepts that form directly — including the bash-native habits (~ home shorthand, /tmp, /dev/null, /usr/bin/...), which resolve in every tool exactly as bash itself resolves them.'
667
743
 
@@ -756,13 +832,17 @@ export function readAdoptSidebar(ctxLike) {
756
832
  }
757
833
  }
758
834
 
759
- // Windows absolute path -> MSYS drive-root form, for rewriting the OFFICIAL
760
- // prompt text in place (v0.10.0). Two patterns keep quoted spaced paths
761
- // ("C:\Program Files\Git") whole while bare paths stop at whitespace or a
762
- // closing punctuation; the lookbehind set rejects URL schemes (https:),
763
- // file:// forms, and anything already mid-word, so only real drive-letter
764
- // paths are translated. Separators normalize to single slashes.
765
- const BARE_WIN_PATH = /(?<![A-Za-z0-9:\\/"'`])([A-Za-z]):(?:\\|\/)([^\s"'`<>|),;:!?]+)/g
835
+ // Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
836
+ // (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
837
+ // path stops at a closing punctuation and crosses a space ONLY into a token that
838
+ // itself carries a separator — so "C:\Program Files\Git\bin" comes back whole (v0.23.0)
839
+ // while "see C:\Users\kanna for details" keeps its prose. An unquoted path whose LAST
840
+ // segment holds the space (…\my dir\f.txt") stays ambiguous in prose; the model meets such
841
+ // a path as a whole-value FIELD, which never goes through here (driveToMsys).
842
+ // The lookbehind set rejects URL schemes (https:), file:// forms, and anything
843
+ // already mid-word, so only real drive-letter paths are translated. Separators
844
+ // normalize to single slashes.
845
+ const BARE_WIN_PATH = /(?<![A-Za-z0-9:\\/"'`])([A-Za-z]):(?:\\|\/)([^\s"'`<>|),;:!?]+(?: [^\s"'`<>|),;:!?]*[\\/][^\s"'`<>|),;:!?]*)*)/g
766
846
  const QUOTED_WIN_PATH = /(["'`])([A-Za-z]):(?:\\|\/)([^`]*?)\1/g
767
847
 
768
848
  /** Rewrite every Windows absolute path in a text to the MSYS form; pure. */
@@ -785,7 +865,7 @@ export function windowsToMsys(text) {
785
865
  function msysEcho(value, env) {
786
866
  const out = windowsToMsys(value)
787
867
  if (typeof out !== 'string' || out.length === 0) return out
788
- const tempRoot = env && env.tmpDir ? windowsToMsys(env.tmpDir) : ''
868
+ const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
789
869
  if (!tempRoot) return out
790
870
  return mountTempRoot(out, tempRoot)
791
871
  }
@@ -829,6 +909,38 @@ function posixSeparators(value) {
829
909
  : value
830
910
  }
831
911
 
912
+ /**
913
+ * One WHOLE path value -> the MSYS dialect (v0.23.0). windowsToMsys is a PROSE
914
+ * rewriter: its bare pattern stops at whitespace, so a path whose DIRECTORY
915
+ * contains a space came back half-translated — "C:\my dir\f.txt" turned into
916
+ * "/c/my dir\f.txt", and the model met that mixed form in a SUCCESSFUL
917
+ * read/write/edit result (issue #3). A whole-value field IS exactly one path, so
918
+ * the drive prefix is translated directly and every remaining separator
919
+ * normalizes; no prose pattern is involved. Relative values (grep match paths)
920
+ * only normalize. A UNC root becomes the MSYS spelling "//server/share";
921
+ * anything without a drive prefix passes through unchanged. Pure.
922
+ */
923
+ function driveToMsys(value) {
924
+ if (typeof value !== 'string' || value.length === 0) return value
925
+ const drive = /^([A-Za-z]):(?=[\\/])/.exec(value)
926
+ if (drive) return '/' + drive[1].toLowerCase() + '/' + posixSeparators(value.slice(2)).replace(/^\/+/, '')
927
+ return posixSeparators(value)
928
+ }
929
+
930
+ /**
931
+ * The MSYS echo of one whole path VALUE — never prose (v0.23.0): the drive root
932
+ * and separators through driveToMsys, then the /tmp mount. Every whole-value
933
+ * face (read/write/edit path, glob paths[], grep matches[].path, present
934
+ * files[].path) goes through here, so none of them can disagree about the
935
+ * dialect, and a spaced directory no longer leaks a backslash.
936
+ */
937
+ function pathEcho(value, env) {
938
+ const out = driveToMsys(value)
939
+ if (typeof out !== 'string' || out.length === 0) return out
940
+ const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
941
+ return tempRoot ? mountTempRoot(out, tempRoot) : out
942
+ }
943
+
832
944
  /**
833
945
  * Rewrite Windows absolute paths inside an ERROR result's text blocks to
834
946
  * the MSYS dialect (pure, v0.17.1). Error results cannot carry a
@@ -918,7 +1030,11 @@ export function rewriteResultPaths(name, value, env) {
918
1030
  // v0.17.0: paths under the user TEMP directory echo back in the bash
919
1031
  // /tmp dialect (that IS what /tmp means in Git Bash), so the model sees
920
1032
  // the same short root bash itself prints for $TMP.
921
- const toMsys = (p) => msysEcho(p, env)
1033
+ // v0.23.0: every value around here is ONE path, so it goes through
1034
+ // pathEcho — the prose rewriter stops at whitespace and left a spaced
1035
+ // directory half-translated (issue #3). posixSeparators now lives inside
1036
+ // pathEcho, so the three nested branches below need no second pass.
1037
+ const toMsys = (p) => pathEcho(p, env)
922
1038
  if (name === 'read' || name === 'read_image' || name === 'write' || name === 'edit') {
923
1039
  if (typeof value.path === 'string' && value.path !== '') {
924
1040
  const out = toMsys(value.path)
@@ -930,7 +1046,7 @@ export function rewriteResultPaths(name, value, env) {
930
1046
  let changed = false
931
1047
  const files = value.files.map((file) => {
932
1048
  if (!file || typeof file !== 'object' || typeof file.path !== 'string' || file.path === '') return file
933
- const out = posixSeparators(toMsys(file.path))
1049
+ const out = toMsys(file.path)
934
1050
  if (out === file.path) return file
935
1051
  changed = true
936
1052
  return { ...file, path: out }
@@ -941,7 +1057,7 @@ export function rewriteResultPaths(name, value, env) {
941
1057
  let changed = false
942
1058
  const paths = value.paths.map((p) => {
943
1059
  if (typeof p !== 'string') return p
944
- const out = posixSeparators(toMsys(p))
1060
+ const out = toMsys(p)
945
1061
  if (out === p) return p
946
1062
  changed = true
947
1063
  return out
@@ -952,7 +1068,7 @@ export function rewriteResultPaths(name, value, env) {
952
1068
  let changed = false
953
1069
  const matches = value.matches.map((m) => {
954
1070
  if (!m || typeof m !== 'object' || typeof m.path !== 'string') return m
955
- const out = posixSeparators(toMsys(m.path))
1071
+ const out = toMsys(m.path)
956
1072
  if (out === m.path) return m
957
1073
  changed = true
958
1074
  return { ...m, path: out }
@@ -1590,7 +1706,7 @@ function adoptSidebarShell(ctx, bashPath) {
1590
1706
 
1591
1707
  const run = () => {
1592
1708
  tried += 1
1593
- const ok = reconcileSidebar(readAdoptSidebar(ctx))
1709
+ const ok = reconcileSidebar(liveSettings.adoptSidebar())
1594
1710
  if (ok) {
1595
1711
  // The namespace scope may register after the settings service; hook
1596
1712
  // the live watch once it exists (idempotent via the effect disposer).
@@ -1682,6 +1798,105 @@ function purgeOrphans(userRoot, keep) {
1682
1798
  }
1683
1799
  }
1684
1800
 
1801
+ // ── declarative registration (dsh >= 0.1.7) ──────────────────────────────────
1802
+ //
1803
+ // dsh 0.1.7 removed directory presets: nothing reads ~/.dsh/.agent-presets
1804
+ // any more, and a preset is a definition registered through
1805
+ // agentPresets.register(). The era probe is the register method itself; the
1806
+ // composition data lives in src/compositions.js (the new-era counterpart of
1807
+ // the committed assets/<variant>/ texts).
1808
+
1809
+ /** Resolve the Creation-skills dir beside @deepseek-ai/dsh-agent-preset. */
1810
+ async function resolveSkillsDir() {
1811
+ try {
1812
+ const { createRequire } = await import('node:module')
1813
+ const here = createRequire(import.meta.url)
1814
+ const pkg = here.resolve('@deepseek-ai/dsh-agent-preset/package.json')
1815
+ return join(dirname(pkg), 'skills')
1816
+ } catch {
1817
+ return undefined
1818
+ }
1819
+ }
1820
+
1821
+ /** The legacy preset root, best effort (roster roots are gone on 0.1.7). */
1822
+ function legacyPresetRoot() {
1823
+ try {
1824
+ const env = process.env.DSH_HOME
1825
+ if (env && env.trim() !== '') return join(env, '.agent-presets')
1826
+ return join(homedir(), '.dsh', '.agent-presets')
1827
+ } catch {
1828
+ return undefined
1829
+ }
1830
+ }
1831
+
1832
+ /**
1833
+ * Remove the stale materialized trees a previous (pre-0.1.7-host) version
1834
+ * left behind, when they are still byte-identical to what we wrote (the
1835
+ * marker judges). User-modified and foreign trees stay untouched — the same
1836
+ * ownership rules the materializer itself follows.
1837
+ */
1838
+ function cleanupLegacyTrees(presetIds) {
1839
+ const root = legacyPresetRoot()
1840
+ if (!root) return
1841
+ for (const presetId of presetIds) {
1842
+ const target = join(root, presetId)
1843
+ const state = classify(target)
1844
+ if (state === 'unmodified') {
1845
+ try {
1846
+ rmSync(target, { recursive: true, force: true })
1847
+ console.log(TAG + ' removed the stale materialized preset at ' + target + ' (directory presets are no longer read on this host; the declarative registration replaces it)')
1848
+ } catch (error) {
1849
+ console.log(TAG + ' stale preset cleanup failed: ' + (error && error.message ? error.message : error))
1850
+ }
1851
+ } else if (state === 'user-modified') {
1852
+ console.log(TAG + ' a user-modified preset tree remains at ' + target + ' — left untouched (this host ignores it; delete it manually if unwanted)')
1853
+ }
1854
+ }
1855
+ }
1856
+
1857
+ /**
1858
+ * Register one variant definition; returns the unregister function. Mount
1859
+ * failures stay visible through the roster's broken diagnostic instead of
1860
+ * failing the boot.
1861
+ */
1862
+ async function registerVariant(ctx, presetId, { gitBashActive, skillsDir }) {
1863
+ const meta = PRESET_META[presetId]
1864
+ if (!meta) return undefined
1865
+ const plugins = meta.kind === 'minimal'
1866
+ ? minimalPluginsFor()
1867
+ : pluginsFor({ kind: meta.kind, gitBash: gitBashActive, skillsDir })
1868
+ return ctx.agentPresets.register({
1869
+ id: presetId,
1870
+ name: meta.name,
1871
+ description: meta.description,
1872
+ order: meta.order,
1873
+ plugins,
1874
+ })
1875
+ }
1876
+
1877
+ /**
1878
+ * Declarative-era wiring: register every configured variant once at startup
1879
+ * and keep exactly one registration per variant for the plugin's lifetime.
1880
+ * The roster picks the entries up immediately; sessions already pinned to a
1881
+ * variant keep their revision until recomposed.
1882
+ */
1883
+ async function runDeclarativeEra(ctx, presetIds) {
1884
+ cleanupLegacyTrees(presetIds)
1885
+ const gitBashActive = process.platform === 'win32'
1886
+ const skillsDir = await resolveSkillsDir()
1887
+ const unregisters = []
1888
+ for (const presetId of presetIds) {
1889
+ try {
1890
+ const unregister = await registerVariant(ctx, presetId, { gitBashActive, skillsDir })
1891
+ if (unregister) unregisters.push(unregister)
1892
+ } catch (error) {
1893
+ console.log(TAG + " preset '" + presetId + "' registration failed: " + (error && error.message ? error.message : error))
1894
+ }
1895
+ }
1896
+ console.log(TAG + ' registered ' + unregisters.length + ' preset(s) declaratively (' + presetIds.join(', ') + ')')
1897
+ ctx.effect(() => () => { for (const off of unregisters) void off() }, 'dsh-gitbash-shell: declarative preset registrations')
1898
+ }
1899
+
1685
1900
  export async function apply(ctx, config = {}) {
1686
1901
  const presetIds = Array.isArray(config.presets) && config.presets.length > 0 ? config.presets : PRESET_IDS
1687
1902
 
@@ -1717,12 +1932,28 @@ export async function apply(ctx, config = {}) {
1717
1932
  const disposeGitBash = ctx.provide('gitBash', gitBashCapability)
1718
1933
  ctx.effect(() => disposeGitBash, 'dsh-gitbash-shell: gitBash capability')
1719
1934
 
1720
- // ── settings namespace: the posixPaths switch (default ON, v0.10.0) ──────
1935
+ // Era-aware live settings: NEW hosts read this plugin's own Config refs
1936
+ // (volatile, re-read per dispatch/assembly), OLD hosts keep the registered
1937
+ // namespace. Every consumer below already re-reads on every use, so a
1938
+ // flipped knob lands on the next dispatch with zero wiring.
1939
+ const liveSettings = makeLiveReader(ctx, config)
1940
+
1941
+ // ── settings namespace: the posixPaths switch (default ON, v0.1.0) — OLD
1942
+ // hosts only; on dsh >= 0.1.7 the row Config above IS the surface.
1943
+
1721
1944
  // Served on the host so the Plugins tab pairs it with the browser card
1722
1945
  // (the tab dispatches Host-served namespaces ∩ registered cards). Dynamic
1723
1946
  // imports keep the zero-dependency smoke path importable; the schema MUST
1724
1947
  // be a callable schemastery object (dsh-settings calls schema(merged)).
1725
- try {
1948
+ const legacySettings = (() => {
1949
+ try {
1950
+ const settings = ctx.get('settings')
1951
+ return !!(settings && typeof settings.register === 'function')
1952
+ } catch {
1953
+ return false
1954
+ }
1955
+ })()
1956
+ if (legacySettings) try {
1726
1957
  ctx.inject(['settings'], (sctx) => {
1727
1958
  Promise.all([import('@deepseek-ai/dsh-settings'), import('@deepseek-ai/schemastery')])
1728
1959
  .then(([ds, sm]) => {
@@ -1792,7 +2023,7 @@ export async function apply(ctx, config = {}) {
1792
2023
  // own env layer (src/shell.js), not this registry.
1793
2024
  variables: { [PATH_DIALECT_KEY]: { description: PATH_DIALECT_DESCRIPTION } },
1794
2025
  resolve() {
1795
- return readPosixPaths(envCtx) ? { [PATH_DIALECT_KEY]: PATH_DIALECT_VALUE } : {}
2026
+ return liveSettings.posix() ? { [PATH_DIALECT_KEY]: PATH_DIALECT_VALUE } : {}
1796
2027
  },
1797
2028
  })
1798
2029
  envCtx.effect(() => unregister, 'dsh-gitbash-shell: shellEnv path-dialect fact')
@@ -1820,7 +2051,7 @@ export async function apply(ctx, config = {}) {
1820
2051
  try {
1821
2052
  ctx.on('system-prompt/assemble', (assembly, _assembleContext, next) => {
1822
2053
  try {
1823
- if (readPosixPaths(ctx) && assembly && typeof assembly === 'object') {
2054
+ if (liveSettings.posix() && assembly && typeof assembly === 'object') {
1824
2055
  for (const section of Array.isArray(assembly.sections) ? assembly.sections : []) {
1825
2056
  if (section && typeof section.text === 'string') section.text = windowsToMsys(section.text)
1826
2057
  }
@@ -1858,90 +2089,121 @@ export async function apply(ctx, config = {}) {
1858
2089
  // ── MSYS path translation on every tool dispatch (Windows only) ─────────
1859
2090
  // Covers every preset and mode: the wrapper sits on the global
1860
2091
  // tools/execute waterfall, through which model-direct calls, PTC run_code
1861
- // sub-dispatches, and dynamic-tool calls all pass.
2092
+ // sub-dispatches, and dynamic-tool calls all pass. It carries BOTH dialect
2093
+ // faces of one call — the path-bearing ARGUMENTS on the way in, and the
2094
+ // path-bearing RESULT metadata on the way out.
2095
+ //
2096
+ // The result face rides HERE, not tools/post-execute (v0.23.0, issue #2).
2097
+ // The registry rejects a post-execute decision that replaces `value` while
2098
+ // any listener on that waterfall replaced `content` —
2099
+ // "tools/post-execute accept decision cannot replace both value and content",
2100
+ // raised after the waterfall settles and outside every listener try/catch,
2101
+ // so the call ended as an isError and the model lost the result. An
2102
+ // around-dispatch wrapper may author the result instead: the registry re-runs
2103
+ // the owning output contract on what we return (normalizeDispatchResult ->
2104
+ // createSuccessResult -> render), so the content the model reads is rendered
2105
+ // FROM the dialect value handed back, and this plugin never projects a
2106
+ // `value` on a decision at all — the collision cannot happen. An unchanged
2107
+ // value returns the ORIGINAL result object, so the registry re-renders
2108
+ // nothing. Only a SUCCESSFUL result is touched; failures keep their value
2109
+ // (and their dialect rides the post-execute listener below).
1862
2110
  if (process.platform === 'win32') {
1863
2111
  try {
1864
2112
  ctx.on('tools/execute', (exec, next) => {
2113
+ let echo = false
2114
+ let echoEnv = null
1865
2115
  try {
1866
2116
  const dialect = readDialectSettings(ctx)
1867
- if (exec && exec.arguments && typeof exec.arguments === 'object' && dialect.posixPaths) {
2117
+ if (dialect.posixPaths) {
2118
+ echo = true
1868
2119
  const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
1869
- let translated = translatePathArguments(exec.arguments, env)
1870
- if (exec.name === 'glob' && dialect.globSplit) {
1871
- const globTranslated = translateGlobArguments(translated, env)
1872
- if (globTranslated !== translated) translated = globTranslated
1873
- }
1874
- // v0.20.0: a run_code program is DATA for a native Node process, so
1875
- // a '/c/...' literal inside it never reached this layer and landed
1876
- // on the current drive as '<drive>:\\c\\...'. The same mount table
1877
- // now covers the program's path literals (see rewriteCodePaths;
1878
- // anything unclear leaves the code untouched).
1879
- if (exec.name === 'run_code' && dialect.codePaths && typeof translated.code === 'string') {
1880
- const rewritten = rewriteCodePaths(translated.code, env)
1881
- const next = programPrelude(env) + rewritten
1882
- if (next !== translated.code) translated = { ...translated, code: next }
2120
+ echoEnv = env
2121
+ if (exec && exec.arguments && typeof exec.arguments === 'object') {
2122
+ let translated = translatePathArguments(exec.arguments, env)
2123
+ if (exec.name === 'glob' && dialect.globSplit) {
2124
+ const globTranslated = translateGlobArguments(translated, env)
2125
+ if (globTranslated !== translated) translated = globTranslated
2126
+ }
2127
+ // v0.20.0: a run_code program is DATA for a native Node process, so
2128
+ // a '/c/...' literal inside it never reached this layer and landed
2129
+ // on the current drive as '<drive>:\\c\\...'. The same mount table
2130
+ // now covers the program path literals (see rewriteCodePaths;
2131
+ // anything unclear leaves the code untouched).
2132
+ if (exec.name === 'run_code' && dialect.codePaths && typeof translated.code === 'string') {
2133
+ const rewritten = rewriteCodePaths(translated.code, env)
2134
+ const nextCode = programPrelude(env) + rewritten
2135
+ if (nextCode !== translated.code) translated = { ...translated, code: nextCode }
2136
+ }
2137
+ if (translated !== exec.arguments) exec.arguments = translated
1883
2138
  }
1884
- if (translated !== exec.arguments) exec.arguments = translated
1885
2139
  }
1886
2140
  } catch { /* never block a call on translation */ }
1887
- return next()
2141
+ if (!echo) return next()
2142
+ return next().then((result) => {
2143
+ try {
2144
+ if (!result || typeof result !== 'object' || result.isError !== false) return result
2145
+ if (!result.value || typeof result.value !== 'object') return result
2146
+ const value = rewriteResultPaths(exec && exec.name, result.value, echoEnv)
2147
+ return value === result.value ? result : { ...result, value }
2148
+ } catch { return result }
2149
+ })
1888
2150
  })
1889
- console.log(TAG + ' MSYS drive-root translation active on tool dispatch')
2151
+ console.log(TAG + ' MSYS path translation active on tool dispatch (arguments and result metadata)')
1890
2152
  } catch (error) {
1891
2153
  console.log(TAG + ' tools/execute wiring failed: ' + (error?.message ?? error))
1892
2154
  }
1893
2155
  }
1894
2156
 
1895
- // ── result-path dialect on tools/post-execute (Windows only, gated; v0.10.4) ──
1896
- // Successful results carry Windows-form path METADATA (read.path, glob
1897
- // paths[], grep matches[].path) out of the Node fs layer. The official
1898
- // post-execute waterfall allows replacing the value projection, so while
1899
- // posixPaths is on those metadata fields flow back in the MSYS dialect and
1900
- // the model never sees a Windows path echoed by a successful call. File
1901
- // content and error results are untouched (see rewriteResultPaths).
2157
+ // ── failure dialect on tools/post-execute (Windows only, gated; v0.17.1) ─
2158
+ // A FAILURE has two faces: `content` (the text the model reads on the failed
2159
+ // call, and what the UI card shows) and `error.message` (what the PTC bridge
2160
+ // hands a running program as its caught ToolCallError, while the durable
2161
+ // record keeps the structured identity). Rewriting only the content left a
2162
+ // program that printed a caught error holding the Windows form while every
2163
+ // other face said MSYS (v0.22.0).
2164
+ //
2165
+ // This listener projects CONTENT ONLY, and that is deliberate (v0.23.0,
2166
+ // issue #2): a decision carrying both `content` and `value` makes the
2167
+ // registry throw, and a replacement value is illegal on a failed result
2168
+ // anyway. The SUCCESS face therefore lives on the tools/execute wrapper
2169
+ // above, and this listener can never collide with any other plugin.
2170
+ //
2171
+ // error.message is rewritten IN PLACE on the result the waterfall was handed:
2172
+ // the registry keeps `result.error` by reference until the final
2173
+ // materialization, so this write reaches the PTC bridge and the record.
2174
+ // A post-execute `block` decision would rebuild the error as a bare message
2175
+ // and drop its structured identity — never do that.
1902
2176
  if (process.platform === 'win32') {
1903
2177
  try {
1904
2178
  ctx.on('tools/post-execute', (exec, result, next) => {
1905
- let patch
1906
2179
  let contentPatch
1907
2180
  try {
1908
- const dialect = readDialectSettings(ctx)
1909
- if (dialect.posixPaths && result && typeof result === 'object') {
1910
- if (result.isError === false && result.value && typeof result.value === 'object') {
1911
- const value = rewriteResultPaths(exec && exec.name, result.value, dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null)
1912
- if (value !== result.value) patch = value
1913
- } else if (result.isError === true && dialect.errorDialect && exec && Array.isArray(result.content)) {
1914
- // v0.22.0: BOTH faces of a failure carry the dialect — the
1915
- // content the model reads on a failed call, and the result's own
1916
- // error.message, which a PTC program receives as its caught
1917
- // ToolCallError message and the durable record keeps.
1918
- const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
1919
- if (exec.name === 'run_code') {
1920
- // paths only — the /dev/null hint is for file tools
1921
- const content = rewriteErrorContent(result.content, { nulHint: false, env })
1922
- if (content !== result.content) contentPatch = content
1923
- rewriteFailureMessage(result, env)
1924
- } else if (ERROR_CONTENT_TOOLS.has(exec.name)) {
1925
- const content = rewriteErrorContent(result.content, { env })
1926
- if (content !== result.content) contentPatch = content
1927
- rewriteFailureMessage(result, env)
1928
- }
2181
+ const dialect = liveSettings.dialect()
2182
+ if (dialect.posixPaths && dialect.errorDialect && exec && result && typeof result === 'object'
2183
+ && result.isError === true && Array.isArray(result.content)) {
2184
+ const env = dialect.virtualMounts ? buildTranslateEnv(dialect.bashPath) : null
2185
+ if (exec.name === 'run_code') {
2186
+ // paths only — the /dev/null hint is for file tools
2187
+ const content = rewriteErrorContent(result.content, { nulHint: false, env })
2188
+ if (content !== result.content) contentPatch = content
2189
+ rewriteFailureMessage(result, env)
2190
+ } else if (ERROR_CONTENT_TOOLS.has(exec.name)) {
2191
+ const content = rewriteErrorContent(result.content, { env })
2192
+ if (content !== result.content) contentPatch = content
2193
+ rewriteFailureMessage(result, env)
1929
2194
  }
1930
2195
  }
1931
2196
  } catch { /* never block a result */ }
1932
2197
  const chain = next()
1933
- if (patch === undefined && contentPatch === undefined) return chain
2198
+ if (contentPatch === undefined) return chain
1934
2199
  return chain.then((decision) => {
1935
2200
  try {
1936
- if (decision && decision.kind === 'accept') {
1937
- if (patch !== undefined) return { ...decision, value: patch }
1938
- return { ...decision, content: contentPatch }
1939
- }
2201
+ if (decision && decision.kind === 'accept') return { ...decision, content: contentPatch }
1940
2202
  } catch { /* keep the downstream decision */ }
1941
2203
  return decision
1942
2204
  })
1943
2205
  })
1944
- console.log(TAG + ' result-path dialect active on tools/post-execute')
2206
+ console.log(TAG + ' failure dialect active on tools/post-execute (content + error.message)')
1945
2207
  } catch (error) {
1946
2208
  console.log(TAG + ' tools/post-execute wiring failed: ' + (error?.message ?? error))
1947
2209
  }
@@ -1965,10 +2227,9 @@ export async function apply(ctx, config = {}) {
1965
2227
  order: 126,
1966
2228
  text: () => {
1967
2229
  try {
1968
- const settings = pctx.get('settings')
1969
- const value = settings && typeof settings.get === 'function' ? settings.get(SETTINGS_NAMESPACE) : undefined
1970
- if (!value || value.posixPaths !== true) return ''
1971
- return value.virtualMounts === false ? POSIX_DIRECTIVE_TEXT_STRICT : POSIX_DIRECTIVE_TEXT
2230
+ const dialect = liveSettings.dialect()
2231
+ if (dialect.posixPaths !== true) return ''
2232
+ return dialect.virtualMounts === false ? POSIX_DIRECTIVE_TEXT_STRICT : POSIX_DIRECTIVE_TEXT
1972
2233
  } catch {
1973
2234
  return ''
1974
2235
  }
@@ -1992,6 +2253,19 @@ export async function apply(ctx, config = {}) {
1992
2253
  adoptSidebarShell(ctx, gitBashCapability.bashPath)
1993
2254
  }
1994
2255
 
2256
+ // ── era split: declarative registration on dsh >= 0.1.7 ──────────────────
2257
+ // The register method IS the era signal. On the new host the whole
2258
+ // materialization path below is dead code (nothing reads the directory any
2259
+ // more), so this branch serves the variants and returns.
2260
+ if (ctx.agentPresets && typeof ctx.agentPresets.register === 'function') {
2261
+ try {
2262
+ await runDeclarativeEra(ctx, presetIds)
2263
+ } catch (error) {
2264
+ console.log(TAG + ' declarative registration failed: ' + (error && error.message ? error.message : error))
2265
+ }
2266
+ return
2267
+ }
2268
+
1995
2269
  const roots = ctx.agentPresets?.roots ?? []
1996
2270
  const userRoot = firstUserRoot(roots)
1997
2271
  if (!userRoot) {
@@ -2068,4 +2342,4 @@ export async function apply(ctx, config = {}) {
2068
2342
  }
2069
2343
 
2070
2344
  // Test surface: pure helpers, no Cordis context required.
2071
- export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, injectPluginManagerRow, hostHasPluginManagerTools, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }
2345
+ export const _internal = { PRESET_IDS, translateMsysPath, translatePathArguments, rewriteCodePaths, scanCodeLiterals, programPrelude, translateGlobArguments, buildTranslateEnv, rewriteErrorContent, rewriteErrorMessage, rewriteFailureMessage, msysEcho, driveToMsys, pathEcho, ERROR_CONTENT_TOOLS, readPosixPaths, readAdoptSidebar, readDialectSettings, windowsToMsys, rewriteResultPaths, MARKER_FILE, classify, materialize, cleanupOnDispose, firstUserRoot, hashTree, skillsHashes, syncDecision, installRegisterShim, baseForRoster, detectBase, pickComposition, personaEraForText, detectPersonaEra, injectPresentRow, hostHasToolPresent, detectPresentSupport, injectPluginManagerRow, hostHasPluginManagerTools, rowFormOf, rowFormsOf, alignEngineRow, alignRalphRow, ROW_SOURCE }