Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
2878 lines (2751 loc) · 137 KB
/
Copy pathindex.js
File metadata and controls
2878 lines (2751 loc) · 137 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* dsh-gitbash-shell — host half (route B: the own bash tool + preset variants).
*
* Installed as a profile bundle, this plugin gives dsh a Git Bash command
* channel WITHOUT touching the host's official PowerShell executor (route B,
* v0.39.0 — the executor instance lives in ./shell.js and is constructed in a
* private scope for the plugin's own bash tool). On startup it also registers
* four Git Bash agent preset variants, mirroring how dsh-ptc-cordis-preset
* ships its 'ptc-cordis' preset:
*
* standard-gitbash — 标准模式 · Git Bash (tool-bash on, tool-pwsh off)
* minimal-gitbash — 极简模式 · Git Bash (persistent Git Bash terminal)
* code-gitbash — PTC 模式 · Git Bash (Code Mode presentation)
* cordis-gitbash — 创造模式 · Git Bash (self-modifying toolset; skills
* copied from the INSTALLED shipped 'cordis' preset so
* the guidance tracks the deployment)
*
* Each preset directory carries a .plugin-managed.json marker recording the
* hash of every file this plugin wrote. Ownership rules (per preset):
* - absent → materialize;
* - foreign → written by someone else: never touch;
* - user-modified → ours but edited: never touch again (delete the
* directory to re-materialize);
* - unmodified → refreshed only when the plugin version changed (or,
* for cordis-gitbash, when the live skills source
* drifted); otherwise idle.
*
* Uninstall hygiene mirrors dsh-ptc-cordis-preset: on disposal, a package
* directory that still exists means reload/update/restart — keep the
* presets; a vanished package.json means uninstall — remove each preset the
* user never modified.
*
* ERA SPLIT (v0.6.0): dsh renamed the built-in `code` preset to `ptc` in
* 0.1.2 with no compatibility alias (tool-presentation `mode` value, plus new
* built-in rows), so each variant ships BOTH committed era texts where the
* built-in changed (minimal did not — one text serves both). The era is
* probed per boot from the roster (`ptc` → dsh >= 0.1.2, else `code`),
* recorded in the marker (`base`), and a flipped detection re-materializes
* on the next startup — either upgrade order converges, and preset IDS never
* change (sessions are pinned to them; `code-gitbash` keeps its historical
* id even though its dsh >= 0.1.2 text presents through the `ptc` built-in).
*/
import { createHash } from 'node:crypto'
import {
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'node:fs'
import { homedir, tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { PRESET_META, minimalPluginsFor, pluginsFor } from './compositions.js'
import { loadSchemastery } from './schemastery.js'
import { applyToolRouting, mountInlineTools } from './inline-tools.js'
import { mountFastctx } from './fastctx-bridge.js'
/**
* The executor base class's own Config schema, when this deployment has it.
*
* Dynamic and optional on purpose: `@deepseek-ai/dsh-bash-sandbox` belongs to
* another row of THIS plugin, and a deployment that cannot resolve it must
* still mount every other face (the static-peer-import lesson, v0.24.3). What
* we need it for is the host's OWN behavioural answer to "do you read your
* shell limits through `.get()`?" — see src/schemastery.js.
*/
async function executorConfigSchema() {
try {
const mod = await import('@deepseek-ai/dsh-bash-sandbox')
return (mod.SandboxBashExecutor ?? mod.default)?.Config
} catch {
return undefined
}
}
/**
* The schemastery module, resolved LAZILY (v0.24.3): `@deepseek-ai/schemastery`
* is a PEER — plain Node cannot resolve it from this package, only the host's
* own resolution (the profile shared fallback) supplies it. A deployment whose
* fallback lacks it used to fail the *static* import at the top of this file,
* and the Loader treats a failed plugin import as a non-fatal skip
* (`vendor/loader/src/config/entry.ts` `_init()`: logger.error + return, no
* fiber) — the row then silently never mounted, which for THIS plugin means no
* preset variants, no executor wiring and no client half, while every host log
* stays green. Same all-green failure class as dsh-better-workspace issue #9;
* verified with a fixture plugin whose only difference was the static peer
* import. Deferring the import hides the schema where the module is absent
* instead of killing the row.
*
* WHICH copy is resolved is no longer left to our own resolution order
* (v0.31.1, issue #12): src/schemastery.js asks the host what it expects and
* picks a copy that agrees with it. Both call sites share the one cached
* answer, so the settings namespace and the executor can never disagree.
*/
let Schema = null
try {
Schema = (await loadSchemastery({ parentConfig: await executorConfigSchema() })).z ?? null
} catch (error) {
Schema = null
console.warn(
'[gitbash-shell] @deepseek-ai/schemastery is not resolvable here; the row Config surface is absent'
+ ' (the preset registration and the client half do not depend on it): '
+ (error && error.message || String(error)),
)
}
/** Plugin identity for cordis.yml rows. */
export const name = 'dsh-gitbash-shell'
/** The preset roster is a hard dependency: without it there is nothing to do. */
export const inject = ['agentPresets']
const TAG = '[gitbash-shell]'
const MANAGED_BY = 'dsh-gitbash-shell'
const MARKER_FILE = '.plugin-managed.json'
/** Default preset ids this plugin materializes, in roster order (configurable). */
export const PRESET_IDS = ['standard-gitbash', 'minimal-gitbash', 'code-gitbash', 'cordis-gitbash']
/**
* Variant preset id -> the OFFICIAL successor id, for the deployment-default
* migration (v0.35.0). Every target is verified against the LIVE roster
* before any write — the official ids renamed once already (code→ptc), so a
* missing successor stands the migration down instead of writing an unknown
* id. `code-gitbash` deliberately maps to `ptc`: that variant has described
* the official PTC mode since dsh 0.1.2 renamed it.
*/
export const VARIANT_DEFAULT_SUCCESSORS = Object.freeze({
'standard-gitbash': 'standard',
'minimal-gitbash': 'minimal',
'code-gitbash': 'ptc',
'cordis-gitbash': 'cordis',
})
/**
* The variant dsh-ptc-cordis-preset already covers while cooperating: its
* `PTC 创造模式` is materialized against Git Bash too, so our own
* `创造模式 · Git Bash` describes the same mode (their issue #7).
*/
export const PEER_COVERED_PRESET_ID = 'cordis-gitbash'
/**
* Service dsh-ptc-cordis-preset publishes:
* `{ id, gitBashActive, pythonRuntime, pythonBackend? }`. `pythonRuntime` is
* the user's INTENT for the experimental CPython run_code backend (the field
* the settings card writes); `pythonBackend` ('python' | 'node', v0.15.1+,
* additive) is the backend that is ACTUALLY active after the peer's own
* preflight — package resolvable, interpreter qualified, POSIX host.
*/
export const PEER_CAPABILITY = 'ptcCordisPreset'
/**
* The peer's CPython intent field — on its capability AND on its row Config
* (the field both settings cards read and write).
*/
export const PEER_PYTHON_FIELD = 'pythonRuntime'
/** The peer's EFFECTIVE backend field on the capability ('python' | 'node'). */
export const PEER_PYTHON_BACKEND_FIELD = 'pythonBackend'
/**
* Read one boolean fact off the peer's capability object. A getter is honoured
* so a live peer can report its current switch rather than a boot-time copy;
* anything absent, of the wrong type, or throwing reads as false — the
* conservative direction is always "the official Node backend is in use".
* @param {object|undefined} capability - the peer's published capability.
* @param {string} field - the field name to read.
* @returns {boolean} the fact, false when it cannot be established.
*/
export function peerFact(capability, field) {
if (capability === null || capability === undefined) return false
try {
const raw = capability[field]
const value = typeof raw === 'function' ? raw.call(capability) : raw
return value === true
} catch {
return false
}
}
/**
* The peer's EFFECTIVE run_code backend, as the closed two-value vocabulary it
* publishes. `undefined` means "the peer does not report one" — an older peer,
* a getter that threw, or a value outside the vocabulary — and every caller
* must then fall back to the INTENT, which is the pre-v0.15.1 behavior.
* @param {object|undefined} capability - the peer's published capability.
* @returns {'python'|'node'|undefined} the reported backend.
*/
export function peerBackend(capability) {
if (capability === null || capability === undefined) return undefined
try {
const raw = capability[PEER_PYTHON_BACKEND_FIELD]
const value = typeof raw === 'function' ? raw.call(capability) : raw
return value === 'python' || value === 'node' ? value : undefined
} catch {
return undefined
}
}
/**
* Is the experimental CPython backend ACTUALLY the one run_code will use? The
* composition's workflow mutex keys on this, never on the bare intent: when the
* peer's preflight fails (package removed, interpreter too old, win32) the
* composition still runs Node, so forcing the workflow rows off would cost the
* user a capability for nothing.
*
* An older peer without the effective field reports no backend, and the intent
* stands in for it — exactly the pre-v0.15.1 behavior, so the linkage never
* changes shape because a field is missing.
* @param {object|undefined} capability - the peer's published capability.
* @returns {boolean} true only while the CPython backend is the active one.
*/
export function pythonBackendActive(capability) {
const intent = peerFact(capability, PEER_PYTHON_FIELD)
const reported = peerBackend(capability)
if (reported === undefined) return intent
return intent && reported === 'python'
}
/**
* Is this agent a DELEGATED one — a subagent, a team member, or a nested child
* of either? The canonical in-tree signal is the session header pair dsh's own
* workspace-changes plugin uses
* (`packages/deliverables/workspace-changes/src/index.ts:59-60`), stamped when
* the subagent driver creates a child
* (`packages/subagent/subagent/src/child-agent.ts:139-155`):
* `origin === 'subagent'` or a non-zero `delegationDepth`.
*
* Unknown shapes read as NOT delegated: applying the dialect is the helpful
* direction, and a diagnostic assembly without an agent must keep it.
* @param {object|undefined} agent - the assembling/executing agent, when known.
* @returns {boolean} true only when the agent is provably delegated.
*/
export function isDelegatedAgent(agent) {
try {
const header = agent && agent.session ? agent.session.header : undefined
if (header === null || typeof header !== 'object') return false
if (header.origin === 'subagent') return true
return typeof header.delegationDepth === 'number' && header.delegationDepth > 0
} catch {
return false
}
}
/**
* Does the Git Bash dialect apply to THIS agent (v0.27.0)? The row switch
* `subagentDialect` (default ON) decides whether delegated agents — subagents,
* team members, nested children — participate in the dialect at all; the main
* agent always does. A closed function so every consumer (prompt assembly,
* dispatch translation, result echo, failure text, the shell-env fact) answers
* the same way.
*
* Note the honest bound: dsh has exactly ONE shell provider per process, so
* the executor binary stays Git Bash for every agent; this gate governs the
* dialect/translation layers that this plugin owns.
* @param {object} dialect - the live dialect switches.
* @param {object|undefined} agent - the agent that owns this assembly/execution.
* @returns {boolean} true when the dialect applies to this agent.
*/
export function dialectApplies(dialect, agent) {
if (!dialect || dialect.subagentDialect !== false) return true
return !isDelegatedAgent(agent)
}
/**
* The roster this plugin serves: the configured list, minus the variant a peer
* already covers. Two independent facts must BOTH hold before anything is
* dropped — the user's `suppressPeerCordis` switch (row Config, default OFF)
* and the peer actually reporting a Git Bash-materialized preset. A peer that
* is absent, inactive, or mounted later than this row therefore never costs
* the user a variant, and the default keeps the historical four-variant roster
* byte for byte.
* @param {string[]} configured - the row's `presets` list (empty/absent = default).
* @param {object} [options] - live decision inputs.
* @param {boolean} [options.suppress] - the live `suppressPeerCordis` switch.
* @param {boolean} [options.peerGitBash] - the peer capability's `gitBashActive`.
* @returns {string[]} the ids to register (or materialize), in configured order.
*/
export function effectivePresetIds(configured, { suppress, peerGitBash } = {}) {
const list = Array.isArray(configured) && configured.length > 0 ? [...configured] : [...PRESET_IDS]
if (suppress !== true || peerGitBash !== true) return list
return list.filter((id) => id !== PEER_COVERED_PRESET_ID)
}
/**
* The ONE bash resolver (v0.28.0, issue #11): the executor (src/shell.js) and
* this translation layer read the same memoized result, so "dialect translated
* to Q:/Git but the executor still spawns C:/Program Files/Git" cannot happen.
*/
import {
DEFAULT_GIT_BASH, GIT_BASH_DOWNLOAD_URL, bashResolutionReport,
effectiveConfiguredBashPath, executorConfiguredBashPath, resolveGitBashCached,
} from './bash-path.js'
import { adoptTerminalShell, terminalAdoptReport } from './terminal-shell.js'
import {
clearToolJob, currentToolJob, defaultRunnerIo, fenceToolRequest,
probeToolchain, readJsonBody, startToolJob,
} from './tool-runner.js'
/** The shipped preset whose skills/ dir seeds cordis-gitbash. */
const here = dirname(fileURLToPath(import.meta.url))
const pkgDir = join(here, '..')
// ── tree hashing ────────────────────────────────────────────────────────────
/** Every file under `root`, as sorted relative POSIX-style paths. */
function walkFiles(root, rel = '') {
const out = []
let entries
try {
entries = readdirSync(join(root, rel), { withFileTypes: true })
} catch {
return out
}
for (const e of entries) {
const r = rel ? `${rel}/${e.name}` : e.name
if (e.isDirectory()) out.push(...walkFiles(root, r))
else if (e.isFile()) out.push(r)
}
return out.sort()
}
/** Map of relative path → sha256 for every file under `root`. */
function hashTree(root) {
const files = {}
for (const rel of walkFiles(root)) {
if (rel === MARKER_FILE) continue
files[rel] = createHash('sha256').update(readFileSync(join(root, rel))).digest('hex')
}
return files
}
/** The parsed marker, or null when the tree is not ours. */
function readMarker(target) {
try {
const m = JSON.parse(readFileSync(join(target, MARKER_FILE), 'utf8'))
if (m && m.managedBy === MANAGED_BY && m.files && typeof m.files === 'object') return m
} catch {
/* absent or unreadable → not ours */
}
return null
}
/** Classify a preset directory: absent | foreign | user-modified | unmodified. */
function classify(target) {
if (!existsSync(target)) return 'absent'
const marker = readMarker(target)
if (!marker) return 'foreign'
const current = hashTree(target)
const recorded = marker.files
const keys = Object.keys(recorded)
if (keys.length !== Object.keys(current).length) return 'user-modified'
for (const k of keys) if (current[k] !== recorded[k]) return 'user-modified'
return 'unmodified'
}
/** 'skills/<rel>' → sha256 map of a skills source tree, or null when absent. */
function skillsHashes(source) {
if (!source || !existsSync(source)) return null
const out = {}
for (const rel of walkFiles(source)) {
out[`skills/${rel}`] = createHash('sha256').update(readFileSync(join(source, rel))).digest('hex')
}
return out
}
// ── inspect-registry compatibility shim ─────────────────────────────────────
//
// The host-plane runner's inspect registry is a process-global singleton and
// its `register` THROWS on a duplicate provider id. `tool-cordis` registers
// the same provider ids from every preset that mounts it, so a process
// hosting both the built-in Creation mode and any cordis-derived preset
// (cordis-gitbash here, ptc-cordis from dsh-ptc-cordis-preset) fails the
// SECOND mount. Two registrants of the SAME package produce identical
// manifests, so replacing the stored entry instead of throwing is a no-op for
// consumers, and the identity-guarded disposer keeps teardown consistent.
// Installed once at boot by this always-mounted host row, so every later
// cordis toolset mount coexists.
const SHIM_FLAG = '__gitbashShellRegisterShim'
/** Wrap one inspect registry's `register` to tolerate duplicate registrations. */
export function installRegisterShim(reg) {
const restore = () => {}
if (!reg || typeof reg.register !== 'function' || !(reg.providers instanceof Map)) {
return { installed: false, restore }
}
if (reg.register[SHIM_FLAG] === true) return { installed: false, restore }
const original = reg.register
try {
const wrapped = function register(registration) {
try {
return original.call(this, registration)
} catch (error) {
const message = error && error.message ? error.message : String(error)
if (!message.includes('is already registered')) throw error
const manifest = registration && registration.manifest
if (!manifest || typeof manifest.id !== 'string') throw error
const stored = { ...registration, manifest }
this.providers.set(manifest.id, stored)
const self = this
return () => {
if (self.providers.get(manifest.id) === stored) self.providers.delete(manifest.id)
}
}
}
try { Object.defineProperty(wrapped, SHIM_FLAG, { value: true }) } catch { /* cosmetic */ }
reg.register = wrapped
return {
installed: true,
restore: () => {
try { if (reg.register === wrapped) reg.register = original } catch { /* never block */ }
},
}
} catch {
return { installed: false, restore }
}
}
// ── unified path dialect: settings namespace + gate (v0.9.0) ───────────────
//
// The dialect (directive + translation wrapper) is OFF by default and gated
// by the posixPaths boolean on this plugin's row Config (volatile, read live
// per dispatch), flipped from the plugin's settings card (src/client.js). The
// directive's text closure returns '' while off — empty context contributions
// are dropped at assembly, so a disabled dialect adds zero prompt noise; the
// wrapper reads the same value per dispatch and passes calls through.
/**
* dsh >= 0.1.7 marks a Config field live-editable without remount; a
* 0.1.6-era schemastery predates the method and the guard keeps this module
* loadable there (values then behave as ordinary config, read through the
* registered settings namespace instead).
*/
function live(schema) {
return typeof schema.volatile === 'function' ? schema.volatile() : schema
}
/**
* Row Config = the settings surface on dsh >= 0.1.7 (values persist under the
* row id; the patch row id is 'gitbash-shell', the same string as the old
* settings namespace, so the one-shot legacy settings.yaml import maps old
* user values onto the new home). Inert metadata on older hosts, and absent
* (undefined) when schemastery is unresolvable: cordis then passes the row
* config through unvalidated instead of the whole row disappearing.
*/
export const Config = Schema === null ? undefined : Schema.object({
// Row-level knob predating the Config surface (invariant 6): which preset
// variants to serve. Ordinary config — an edit remounts the plugin, which
// is the right weight for a list that changes the roster.
presets: Schema.array(Schema.string()).default(PRESET_IDS),
posixPaths: live(Schema.boolean().default(true)),
virtualMounts: live(Schema.boolean().default(true)),
globSplit: live(Schema.boolean().default(true)),
errorDialect: live(Schema.boolean().default(true)),
codePaths: live(Schema.boolean().default(true)),
/**
* Whether DELEGATED agents (subagents, team members, nested children) also
* use the Git Bash dialect (v0.27.0). Default ON — the historical behavior;
* OFF keeps the dialect to the main agent, so a delegated request carries the
* official shell semantics (no prompt rewrite, no path-argument translation,
* no echo rewrite, no DSH_PATH_DIALECT fact). The executor itself is one
* per-process provider, so Git Bash stays the binary either way.
*/
subagentDialect: live(Schema.boolean().default(true)),
gitAutocrlf: live(Schema.boolean().default(true)),
bashPath: live(Schema.string().default('')),
adoptSidebar: live(Schema.boolean().default(true)),
/**
* Whether the OFFICIAL sidebar terminal ("new terminal") is switched to the
* resolved Git Bash (v0.29.0, task-25). Default ON — the user asked for
* exactly this ("自动检测 … 帮忙补上一个 gitbash 可以用"). On Windows without
* this, dsh resolves the terminal shell from the environment, which on a
* machine whose PATH `bash` is the WSL launcher means the new terminal starts
* Ubuntu, not Git Bash. Only ever writes when the terminal has NO shell of
* its own: an explicit choice is never overwritten.
*/
autoTerminalShell: live(Schema.boolean().default(true)),
/**
* Dedupe switch against dsh-ptc-cordis-preset (their issue #7, v0.25.0):
* ON while that plugin is installed AND materializing its `PTC 创造模式`
* against Git Bash drops OUR `创造模式 · Git Bash` from the roster, because
* the two describe the same mode. Default OFF — the historical four-variant
* roster is unchanged until asked for. The same state is rendered on their
* settings card too (their card binds this row through configForms), so
* changing it on either side changes it here.
*/
suppressPeerCordis: live(Schema.boolean().default(false)),
/**
* Serve the four `* · Git Bash` VARIANT presets (v0.34.x behaviour) instead
* of relying on the inline tool patch (v0.35.0). Default OFF for every user
* — new and existing — per the owner's explicit call: the official presets
* carry Git Bash through the per-agent tool patch, the roster stays the
* official four, and the variants remain one switch away for anyone who
* wants the duplicated roster back. OFF also stops the reconcile from
* registering variants; a session pinned to a variant id must re-select an
* official preset (the roster flip is announced in the CHANGELOG).
*/
variantPresets: live(Schema.boolean().default(false)),
/**
* Show the host `pwsh` tool to agents (v0.39.0 semantics; default OFF).
* Route B keeps the official pwsh executor in place, so the tool is REAL
* PowerShell (7 first per the official resolver) — showing it is a pure
* product choice. OFF (default) hides it per agent: the host stays
* bash-first. The retired v0.37 `pwsh7` side-channel is gone; this switch
* now controls the real thing.
*/
pwshTool: live(Schema.boolean().default(false)),
// Maintainer PR #26 decision: one live opt-out for refusal and companions.
experimentalNative: live(Schema.boolean().default(true)),
/**
* Publish the FastCtx file tools (`gb_*`) through the local MCP bridge.
* Default ON; Windows-only payload, stands down entirely when dsh-ops is
* present (its ops_* tools already cover the same ground).
*/
fastctxTools: live(Schema.boolean().default(false)),
})
/** Read one Config value across eras: Volatile ref (>= 0.1.7) or plain value. */
export function valueOf(value) {
return value && typeof value.get === 'function' ? value.get() : value
}
/**
* This row's OWN live `bashPath` (issue #13): apply()'s `config` parameter IS
* the `gitbash-shell` row's resolved Config, so reading it here replaces the
* settings-service detour (`settingsBashPath`) that could read '' at apply
* time on hosts where the service is describe()/update()-only or not injected
* yet. Volatile refs mean a settings edit lands on the next read, no listener.
* @param {object|undefined} config - apply()'s resolved Config (may be absent).
* @returns {string} the row's `bashPath`, '' when unset or unreadable.
*/
export function rowBashPath(config) {
try {
const value = config && typeof config === 'object' ? valueOf(config.bashPath) : undefined
return typeof value === 'string' ? value.trim() : ''
} catch {
return ''
}
}
/**
* Era-aware live settings reader for everything wired inside apply(): the
* NEW era reads this plugin's own Config refs (no listener needed — every
* consumer here already re-reads per dispatch/assembly), the OLD era keeps
* reading the registered namespace through the settings service.
* @param {object} ctx - the plugin's mounting context.
* @param {object|undefined} config - apply()'s resolved Config.
* @returns {{ dialect: () => object, posix: () => boolean, adoptSidebar: () => boolean, suppressPeerCordis: () => boolean }}
*/
function makeLiveReader(ctx, config) {
const hasConfig = !!(config && typeof config === 'object')
const dialect = () => {
if (!hasConfig) {
// No row Config (schemastery unresolvable): everything off except the
// opt-out booleans, the exact shape the old fallback returned.
return {
posixPaths: false, virtualMounts: false, globSplit: false, errorDialect: false,
codePaths: false, gitAutocrlf: false, bashPath: '', subagentDialect: true, autoTerminalShell: true,
}
}
const bash = valueOf(config.bashPath)
return {
posixPaths: valueOf(config.posixPaths) === true,
virtualMounts: valueOf(config.virtualMounts) === true,
globSplit: valueOf(config.globSplit) === true,
errorDialect: valueOf(config.errorDialect) === true,
codePaths: valueOf(config.codePaths) === true,
gitAutocrlf: valueOf(config.gitAutocrlf) === true,
// Default ON: delegated agents keep the dialect unless the user opts out.
subagentDialect: valueOf(config.subagentDialect) !== false,
bashPath: typeof bash === 'string' ? bash : '',
adoptSidebar: valueOf(config.adoptSidebar) !== false,
// Default ON (v0.29.0): the official terminal is ours unless opted out.
autoTerminalShell: valueOf(config.autoTerminalShell) !== false,
// Default OFF (v0.35.0): the official presets carry Git Bash through the
// inline tool patch; the variant roster is opt-in.
variantPresets: valueOf(config.variantPresets) === true,
// Default ON (v0.35.0): the pwsh replacement fills the denied host tool.
pwshTool: valueOf(config.pwshTool) !== false,
// Default OFF (v0.39.4, dsh-ops 0.3.0): FastCtx is dead upstream and dsh-ops ships stronger native ops_* file tools; an explicit true re-enables the bridge. It still stands down when dsh-ops is present.
fastctxTools: valueOf(config.fastctxTools) === true,
}
}
return {
dialect,
posix: () => dialect().posixPaths === true,
adoptSidebar: () => dialect().adoptSidebar !== false,
autoTerminalShell: () => dialect().autoTerminalShell !== false,
variantPresets: () => (hasConfig ? valueOf(config.variantPresets) === true : false),
pwshTool: () => (hasConfig ? valueOf(config.pwshTool) !== false : true),
experimentalNative: () => (hasConfig ? valueOf(config.experimentalNative) !== false : true),
fastctxTools: () => (hasConfig ? valueOf(config.fastctxTools) === true : false),
// Default OFF: only an explicit true asks for deduplication.
suppressPeerCordis: () => (hasConfig ? valueOf(config.suppressPeerCordis) === true : false),
}
}
/** The full directive text, injected only while posixPaths is on. */
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.'
/** Strict drive-root-only directive (v0.19.0): used while virtualMounts is off. */
const POSIX_DIRECTIVE_TEXT_STRICT = 'The working shell is Git for Windows bash: paths use MSYS drive roots (/c/Users/...), and every tool accepts that form directly.'
/**
* The run_code sentence is added ONLY for assemblies whose tool list actually
* carries run_code (v0.20.1): a preset without it — standard/cordis sessions,
* or a code-mode session whose tool row is disabled — has no program text to
* write paths into, so the sentence would be pure noise there. Cheap and
* honest: the tool list of the very assembly being rendered decides.
*/
const RUN_CODE_DIRECTIVE_TEXT = ' In a run_code program, compatible plain MSYS path literals are translated on a best-effort basis; interpolated or tagged templates and uncertain syntax are not rewritten. For direct native Node fs/path APIs, prefer explicit Windows drive-letter or cwd-relative paths. Do not rely on literal rewriting for dynamic paths, shell command strings, routes or search data; tool path arguments are translated separately. Shell expansion is unavailable in program strings, so do not rely on $VARS.'
const RUN_CODE_DIRECTIVE_OFF_TEXT = ' In a run_code program, automatic path-literal rewriting is OFF. Direct native Node fs/path APIs require Windows drive-letter or cwd-relative paths, not MSYS /c/... strings. Tool path arguments are still translated separately; program strings do not perform shell expansion.'
/** The run_code sentence for this assembly, or '' when the tool is not offered. */
export function runCodeHintFor(tools, codePaths = true) {
if (!Array.isArray(tools)) return ''
for (const tool of tools) {
const name = tool && typeof tool.name === 'string' ? tool.name : ''
if (name === 'run_code') return codePaths === true ? RUN_CODE_DIRECTIVE_TEXT : RUN_CODE_DIRECTIVE_OFF_TEXT
}
return ''
}
// Official runtime counterpart to the directive (v0.11.0): dsh-shell-env is
// the host-plane registry behind the model-visible $DSH_* facts, and the
// bash tool's schema tells the model to inspect them — so the dialect also
// lives there, verifiable at runtime instead of only stated in the prompt.
const PATH_DIALECT_KEY = 'DSH_PATH_DIALECT'
const PATH_DIALECT_VALUE = 'msys'
const PATH_DIALECT_DESCRIPTION = 'Path dialect for tool calls and tool results: MSYS drive roots (/c/Users/...) plus the bash-native mounts (~, /tmp, /dev/null, /usr); every tool accepts these forms directly.'
// v0.21.1 — line endings for the MODEL's git commands live in the EXECUTOR
// (src/shell.js, withParityEnv): Windows Git defaults to core.autocrlf=true, so
// an LF file the model writes comes back CRLF after a checkout (scripts break on
// the stray \r, byte assertions fail), while a Linux guest has autocrlf=false.
// They cannot ride the shell-env registry below: that registry accepts DSH_*
// facts only, and a non-DSH key throws the whole contribution away.
// Windows absolute path -> MSYS drive-root form, for rewriting PROSE in place
// (v0.10.0). Quoted spaced paths ("C:\Program Files\Git") are matched whole; a bare
// path stops at a closing punctuation and crosses a space ONLY into a token that
// itself carries a separator — so "C:\Program Files\Git\bin" comes back whole (v0.23.0)
// while "see C:\Users\kanna for details" keeps its prose. An unquoted path whose LAST
// segment holds the space (…\my dir\f.txt") stays ambiguous in prose; the model meets such
// a path as a whole-value FIELD, which never goes through here (driveToMsys).
// The lookbehind set rejects URL schemes (https:), file:// forms, and anything
// already mid-word, so only real drive-letter paths are translated. Separators
// normalize to single slashes. The separator group after the colon consumes
// EVERY consecutive separator (`[\\/]+`), not just one (v0.26.0, issue #10):
// JSON-escaped Windows paths ("D:\\dsh\\工作" as the host renders
// `${JSON.stringify(workspaceRoot)}` in sandbox:policy) carry a DOUBLED
// backslash, and a single-separator atom left the second one at the head of
// `rest`, where the separator normalization turned it into a leading slash —
// "/d/" + "/dsh/工作" = "/d//dsh/工作", a non-canonical workspace root the
// model then copied. Single-separator inputs are byte-for-byte unchanged.
const BARE_WIN_PATH = /(?<![A-Za-z0-9:\\/"'`])([A-Za-z]):[\\/]+([^\s"'`<>|),;:!?]+(?: [^\s"'`<>|),;:!?]*[\\/][^\s"'`<>|),;:!?]*)*)/g
const QUOTED_WIN_PATH = /(["'`])([A-Za-z]):[\\/]+([^`]*?)\1/g
/** Rewrite every Windows absolute path in a text to the MSYS form; pure. */
export function windowsToMsys(text) {
if (typeof text !== 'string' || text.length === 0) return text
return text
.replace(QUOTED_WIN_PATH, (_whole, quote, drive, rest) =>
quote + '/' + drive.toLowerCase() + '/' + rest.replace(/[\\/]+/g, '/') + quote)
.replace(BARE_WIN_PATH, (_whole, drive, rest) =>
'/' + drive.toLowerCase() + '/' + rest.replace(/[\\/]+/g, '/'))
}
/**
* The MSYS echo of one text the Windows layer produced (v0.22.0): the
* drive-root translation plus the /tmp mount, so a path that came back out
* of the host reads as the very path the model wrote. ONE helper for every
* echo face — success metadata, failure content, failure message — because
* those faces must never disagree about the dialect.
*/
function msysEcho(value, env) {
const out = windowsToMsys(value)
if (typeof out !== 'string' || out.length === 0) return out
const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
if (!tempRoot) return out
return mountTempRoot(out, tempRoot)
}
/**
* Swap every /tmp-mount path inside a text for the short bash spelling;
* pure. Whole-value path fields hit the first branch; a diagnostic that
* QUOTES a temp path ("ENOENT ... open /c/Users/u/AppData/.../x.txt") is
* the reason this is an embedded scan rather than a prefix test — the
* model must recognise its own /tmp path in an error the way it does in a
* successful result. Case-insensitive like the Windows filesystem, and
* anchored on a path boundary so a sibling name (…/Temperature) never
* matches.
*/
function mountTempRoot(text, tempRoot) {
const haystack = text.toLowerCase()
const needle = tempRoot.toLowerCase()
let out = ''
let cursor = 0
let hits = 0
for (;;) {
const at = haystack.indexOf(needle, cursor)
if (at < 0) break
const after = at + needle.length
const boundary = after === text.length || text.charCodeAt(after) === 47
if (boundary) {
out += text.slice(cursor, at) + '/tmp'
hits++
} else {
out += text.slice(cursor, after)
}
cursor = after
}
return hits === 0 ? text : out + text.slice(cursor)
}
/** Normalize backslash separators in a RELATIVE result path to slashes; pure. */
function posixSeparators(value) {
return typeof value === 'string' && value.includes(String.fromCharCode(92))
? value.split(String.fromCharCode(92)).join('/')
: value
}
/**
* One WHOLE path value -> the MSYS dialect (v0.23.0). windowsToMsys is a PROSE
* rewriter: its bare pattern stops at whitespace, so a path whose DIRECTORY
* contains a space came back half-translated — "C:\my dir\f.txt" turned into
* "/c/my dir\f.txt", and the model met that mixed form in a SUCCESSFUL
* read/write/edit result (issue #3). A whole-value field IS exactly one path, so
* the drive prefix is translated directly and every remaining separator
* normalizes; no prose pattern is involved. Relative values (grep match paths)
* only normalize. A UNC root becomes the MSYS spelling "//server/share";
* anything without a drive prefix passes through unchanged. Pure.
*/
function driveToMsys(value) {
if (typeof value !== 'string' || value.length === 0) return value
const drive = /^([A-Za-z]):(?=[\\/])/.exec(value)
if (drive) return '/' + drive[1].toLowerCase() + '/' + posixSeparators(value.slice(2)).replace(/^\/+/, '')
return posixSeparators(value)
}
/**
* The MSYS echo of one whole path VALUE — never prose (v0.23.0): the drive root
* and separators through driveToMsys, then the /tmp mount. Every whole-value
* face (read/write/edit path, glob paths[], grep matches[].path, present
* files[].path) goes through here, so none of them can disagree about the
* dialect, and a spaced directory no longer leaks a backslash.
*/
function pathEcho(value, env) {
const out = driveToMsys(value)
if (typeof out !== 'string' || out.length === 0) return out
const tempRoot = env && env.tmpDir ? driveToMsys(env.tmpDir) : ''
return tempRoot ? mountTempRoot(out, tempRoot) : out
}
/**
* Rewrite Windows absolute paths inside an ERROR result's text blocks to
* the MSYS dialect (pure, v0.17.1). Error results cannot carry a
* replacement `value` (the registry rejects that), but replacing `content`
* is the official post-execute channel, so the message the model reads
* stays in one dialect — it must not relearn Windows forms from failure
* text. Non-drive-letter diagnostics (device paths, URLs) never match
* windowsToMsys and are left verbatim.
*/
/**
* The only tools whose ERROR content is dialect-translated (v0.17.1).
* These messages are harness-generated path diagnostics (cannot read
* "C:\..."), so rewriting them is safe. Every other tool — bash above all,
* whose failed runs carry raw command stdout/stderr — keeps its error
* content VERBATIM: output is data, never rewritten (the same red line as
* file content in successful results).
*/
// ops_* six (v0.39.4, dsh-ops 0.3.0): their failures quote Windows paths too —
// the error content and error.message must answer in the same /c/ dialect.
const ERROR_CONTENT_TOOLS = new Set(['read', 'read_image', 'write', 'edit', 'glob', 'grep', 'present', 'ops_inspect_local_file', 'ops_glob', 'ops_grep', 'ops_replace', 'ops_batch', 'ops_result'])
export function rewriteErrorContent(content, options) {
if (!Array.isArray(content)) return content
const nulHint = !options || options.nulHint !== false
const env = options && options.env ? options.env : null
let changed = false
const next = content.map((block) => {
if (!block || typeof block !== 'object' || typeof block.text !== 'string') return block
const out = msysEcho(block.text, env)
if (out === block.text) return block
changed = true
return { ...block, text: out }
})
// /dev/null x file tools: the harness realpath step rejects device
// paths, so the failure is EXPECTED — append one guidance line (v0.18.0)
// instead of leaving the model to puzzle over EINVAL. The original
// diagnostic stays verbatim; only a hint block is appended.
const nulHit = content.some((block) => {
const t = block && typeof block === 'object' && typeof block.text === 'string' ? block.text : ''
return t.includes('EINVAL') && t.includes('NUL')
})
if (nulHint && nulHit) {
next.push({ type: 'text', text: 'hint: /dev/null is the NUL device — file tools cannot target device paths (their realpath step rejects them); discard output through bash instead (echo ... > /dev/null)' })
changed = true
}
return changed ? next : content
}
/**
* The SECOND face of a failure (v0.22.0). The content blocks are what the
* model reads on a failed call and what the UI card shows; error.message is
* what the PTC bridge hands a running program as its caught ToolCallError
* message (the durable record keeps only the structured identity). Rewriting
* only the content left a program that printed a caught error holding the
* Windows form while every other face said the MSYS one. Pure; options
* mirror rewriteErrorContent.
*/
export function rewriteErrorMessage(message, options) {
if (typeof message !== 'string' || message.length === 0) return message
return msysEcho(message, options && options.env ? options.env : null)
}
/**
* Apply rewriteErrorMessage to a failure RESULT in place (v0.22.0). The
* registry keeps result.error by reference through the whole post-execute
* waterfall and only snapshots the projection afterwards
* (normalizeDispatchResult -> materializeFinalResult), so this write reaches
* the PTC bridge, which is where a program meets its caught error.
* post-execute block decision: a block rebuilds the error as a bare message
* and would drop the structured identity (FS_NOT_FOUND and friends) that the
* record and the UI keep. Defensive — a frozen or absent error object leaves
* the result untouched, and the content face still carries the dialect alone.
*/
export function rewriteFailureMessage(result, env) {
try {
const error = result && result.error
if (!error || typeof error !== 'object' || typeof error.message !== 'string') return false
const rewritten = msysEcho(error.message, env)
if (rewritten === error.message) return false
error.message = rewritten
return error.message === rewritten
} catch {
return false
}
}
export function rewriteResultPaths(name, value, env) {
if (!value || typeof value !== 'object') return value
// v0.17.0: paths under the user TEMP directory echo back in the bash
// /tmp dialect (that IS what /tmp means in Git Bash), so the model sees
// the same short root bash itself prints for $TMP.
// v0.23.0: every value around here is ONE path, so it goes through
// pathEcho — the prose rewriter stops at whitespace and left a spaced
// directory half-translated (issue #3). posixSeparators now lives inside
// pathEcho, so the three nested branches below need no second pass.
const toMsys = (p) => pathEcho(p, env)
if (name === 'read' || name === 'read_image' || name === 'write' || name === 'edit') {
if (typeof value.path === 'string' && value.path !== '') {
const out = toMsys(value.path)
if (out !== value.path) return { ...value, path: out }
}
return value
}
if (name === 'present' && Array.isArray(value.files)) {
let changed = false
const files = value.files.map((file) => {
if (!file || typeof file !== 'object' || typeof file.path !== 'string' || file.path === '') return file
const out = toMsys(file.path)
if (out === file.path) return file
changed = true
return { ...file, path: out }
})
return changed ? { ...value, files } : value
}
// v0.39.4 (dsh-ops 0.3.0): ops_* tools answer with the same paths the
// model must keep speaking /c/ about. Their Rust worker names every path
// field `path` (items[] / matches[]) and ops_batch/ops_result echo the
// operation `root`; detailsRef ids are opaque, never rewritten.
if (name === 'ops_grep' && Array.isArray(value.matches)) {
let changed = false
const matches = value.matches.map((match) => {
if (!match || typeof match !== 'object' || typeof match.path !== 'string' || match.path === '') return match
const out = toMsys(match.path)
if (out === match.path) return match
changed = true
return { ...match, path: out }
})
return changed ? { ...value, matches } : value
}
if ((name === 'ops_inspect_local_file' || name === 'ops_glob' || name === 'ops_replace') && Array.isArray(value.items)) {
let changed = false
const items = value.items.map((item) => {
if (!item || typeof item !== 'object' || typeof item.path !== 'string' || item.path === '') return item
const out = toMsys(item.path)
if (out === item.path) return item
changed = true
return { ...item, path: out }
})
return changed ? { ...value, items } : value
}
if ((name === 'ops_batch' || name === 'ops_result') && typeof value.root === 'string' && value.root !== '') {
const out = toMsys(value.root)
if (out !== value.root) return { ...value, root: out }
return value
}
if (name === 'glob' && Array.isArray(value.paths)) {
let changed = false
const paths = value.paths.map((p) => {
if (typeof p !== 'string') return p
const out = toMsys(p)
if (out === p) return p
changed = true
return out
})
return changed ? { ...value, paths } : value
}
if (name === 'grep' && Array.isArray(value.matches)) {
let changed = false
const matches = value.matches.map((m) => {
if (!m || typeof m !== 'object' || typeof m.path !== 'string') return m
const out = toMsys(m.path)
if (out === m.path) return m
changed = true
return { ...m, path: out }
})
return changed ? { ...value, matches } : value
}
return value
}
// ── MSYS drive-root path translation (Windows, EVERY tool dispatch) ────────
//
// The directive below tells the model to use POSIX-style MSYS paths (/c/...)
// for EVERY tool. Bash digests those natively, but the Node-backed file tools
// resolve an MSYS root against the current drive (/c/Users -> C:/c/Users), so a
// global tools/execute wrapper rewrites the path-bearing ARGUMENT FIELDS (by
// field NAME, not by tool name — dynamic tools using the standard field names
// are covered too) into the drive-letter form the host accepts. The bash
// command field is deliberately untouched: MSYS roots are Git Bash native
// there. Defensive by design: the waterfall hands the same mutable exec object
// to the tool body, but if a future registry freezes it the write throws, is
// swallowed, and the call proceeds with the model original arguments (the
// directive still stands).
const MSYS_DRIVE_ROOT = /^\/([a-z])\/(.*)$/i
const MSYS_BARE_DRIVE = /^\/([a-z])$/i
// 'root' (v0.39.4): dsh-ops ops_batch/ops_result name their path argument `root`;
// no official tool uses that key, so the extra alias is collision-free.
const TRANSLATABLE_PATH_FIELDS = new Set(['file_path', 'path', 'workdir', 'root'])
// ── virtual mounts + env (v0.17.0) ─────────────────────────────────────
//
// Git Bash resolves a few POSIX roots through its OWN mount table before
// any drive root applies (`mount` inside Git Bash): /tmp is the user TEMP
// dir (usertemp), /dev/null is the Windows NUL device, and /usr /bin /etc
// /var /home /root /mnt live under the Git install root. The model carries
// those bash habits into EVERY tool, so the translation layer mirrors the
// same table or the file tools silently misplace them (/tmp becomes a
// fresh C:/tmp on the current drive; /dev/null becomes a real file under
// C:/dev). All matches are segment-bounded and case-sensitive, exactly
// like the msys mount table itself (/Tmp does NOT match).
//
// /home is deliberately the GIT mount (git-root/home, usually empty), NOT
// the user profile: bash itself resolves /home/x there, while the home
// SHORTHAND ~ expands to $HOME (= C:/Users/<u>) — mirroring both keeps
// file tools and bash in perfect agreement. A bare '/' is never
// translated: it would scope tools to the whole Git install tree.
/** The Windows null device — the exact target of Git Bash's /dev/null. */
const NUL_DEVICE = '\\\\.\\NUL'
/** Git-root mounts: MSYS path -> subdirectory under the Git install root. */
const GIT_MOUNTS = new Map([
['/usr', 'usr'],
['/bin', 'usr/bin'],
['/etc', 'etc'],
['/var', 'var'],
['/home', 'home'],
['/root', 'root'],
['/mnt', 'mnt'],
])
const translateEnvCache = new Map()
/**
* The runtime facts the virtual mounts resolve against (all optional — a
* missing fact simply leaves its paths untranslated). Probed once per
* distinct bashPath setting (v0.19.0: a settings-configured bash.exe joins
* the candidates FIRST, fixing /usr translation for custom installs);
* tmpdir/homedir are cheap; the Git root walks the candidate bash.exe list
* (the default install plus every PATH entry) and accepts a root only when
* <root>/usr/bin exists, which excludes the WSL bash shim in WindowsApps.
*/
/**
* One dispatch's path-bearing arguments in the host dialect (pure). The
* tools/execute wrapper's decision surface, exported so a test can drive the
* whole argument face without a Windows host — the shape issue #8 escaped
* through: pure helpers were covered, the wiring that feeds them was not.