dsh-plugin-prompt-tool 0.4.2 → 0.5.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.
Files changed (188) hide show
  1. package/README.md +125 -222
  2. package/engine/compaction-epoch.mjs +139 -0
  3. package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
  4. package/engine/compositions/library/compaction.yml +52 -0
  5. package/engine/compositions/library/context-gate.yml +40 -0
  6. package/engine/compositions/library/custom-bash.yml +14 -0
  7. package/engine/compositions/library/delegation.yml +85 -0
  8. package/engine/compositions/library/official-agent-instructions.yml +17 -0
  9. package/engine/compositions/library/official-persistent-shell.yml +56 -0
  10. package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
  11. package/engine/compositions/library/official-tool-bash.yml +6 -0
  12. package/engine/compositions/library/official-tool-cordis.yml +13 -0
  13. package/engine/compositions/library/official-tool-presentation.yml +7 -0
  14. package/engine/compositions/library/official-tool-skill.yml +14 -0
  15. package/engine/compositions/library/persistent-shell.yml +36 -0
  16. package/engine/compositions/library/persona.yml +9 -0
  17. package/engine/compositions/library/planning.yml +41 -0
  18. package/engine/compositions/library/prompt-config-engine.yml +16 -0
  19. package/engine/compositions/library/router-first-turn.yml +9 -0
  20. package/engine/compositions/library/run-code-env.yml +15 -0
  21. package/engine/compositions/library/skill-filesystem.yml +13 -0
  22. package/engine/compositions/library/skill-search.yml +14 -0
  23. package/engine/compositions/library/str-replace-editor.yml +10 -0
  24. package/engine/compositions/library/tool-ask-user.yml +8 -0
  25. package/engine/compositions/library/tool-bash.yml +16 -0
  26. package/engine/compositions/library/tool-bootstrap.yml +16 -0
  27. package/engine/compositions/library/tool-filter.yml +12 -0
  28. package/engine/compositions/library/tool-fs-search.yml +18 -0
  29. package/engine/compositions/library/tool-fs.yml +10 -0
  30. package/engine/compositions/library/tool-goal.yml +19 -0
  31. package/engine/compositions/library/tool-jobs.yml +23 -0
  32. package/engine/compositions/library/tool-pwsh.yml +12 -0
  33. package/engine/compositions/library/tool-todo.yml +11 -0
  34. package/engine/compositions/library/tool-web.yml +9 -0
  35. package/engine/compositions/source/local/context-gate.yml +37 -0
  36. package/engine/compositions/source/local/custom-bash.yml +11 -0
  37. package/engine/compositions/source/local/prompt-config-engine.yml +13 -0
  38. package/engine/compositions/source/local/router-first-turn.yml +5 -0
  39. package/engine/compositions/source/local/run-code-env.yml +12 -0
  40. package/engine/compositions/source/local/skill-search.yml +11 -0
  41. package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
  42. package/engine/context-gate.mjs +305 -0
  43. package/{preset → engine}/custom-bash.mjs +243 -243
  44. package/engine/executor.mjs +271 -0
  45. package/engine/fillers.mjs +273 -0
  46. package/engine/layers.mjs +220 -0
  47. package/engine/prompt-config-engine.mjs +49 -0
  48. package/{preset → engine}/router-first-turn.mjs +85 -73
  49. package/engine/run-code-env.mjs +208 -0
  50. package/engine/schema.mjs +294 -0
  51. package/{preset → engine}/shared.mjs +54 -0
  52. package/engine/strategies.mjs +223 -0
  53. package/{preset → engine}/tool-bootstrap.mjs +350 -282
  54. package/engine/tool-filter.mjs +80 -0
  55. package/engine/vendor/yaml/LICENSE +13 -0
  56. package/engine/vendor/yaml/dist/compose/compose-collection.js +88 -0
  57. package/engine/vendor/yaml/dist/compose/compose-doc.js +43 -0
  58. package/engine/vendor/yaml/dist/compose/compose-node.js +109 -0
  59. package/engine/vendor/yaml/dist/compose/compose-scalar.js +86 -0
  60. package/engine/vendor/yaml/dist/compose/composer.js +219 -0
  61. package/engine/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
  62. package/engine/vendor/yaml/dist/compose/resolve-block-scalar.js +198 -0
  63. package/engine/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
  64. package/engine/vendor/yaml/dist/compose/resolve-end.js +37 -0
  65. package/engine/vendor/yaml/dist/compose/resolve-flow-collection.js +207 -0
  66. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
  67. package/engine/vendor/yaml/dist/compose/resolve-props.js +146 -0
  68. package/engine/vendor/yaml/dist/compose/util-contains-newline.js +34 -0
  69. package/engine/vendor/yaml/dist/compose/util-empty-scalar-position.js +26 -0
  70. package/engine/vendor/yaml/dist/compose/util-flow-indent-check.js +15 -0
  71. package/engine/vendor/yaml/dist/compose/util-map-includes.js +13 -0
  72. package/engine/vendor/yaml/dist/doc/Document.js +335 -0
  73. package/engine/vendor/yaml/dist/doc/anchors.js +71 -0
  74. package/engine/vendor/yaml/dist/doc/applyReviver.js +55 -0
  75. package/engine/vendor/yaml/dist/doc/createNode.js +88 -0
  76. package/engine/vendor/yaml/dist/doc/directives.js +176 -0
  77. package/engine/vendor/yaml/dist/errors.js +57 -0
  78. package/engine/vendor/yaml/dist/index.js +17 -0
  79. package/engine/vendor/yaml/dist/log.js +11 -0
  80. package/engine/vendor/yaml/dist/nodes/Alias.js +116 -0
  81. package/engine/vendor/yaml/dist/nodes/Collection.js +147 -0
  82. package/engine/vendor/yaml/dist/nodes/Node.js +38 -0
  83. package/engine/vendor/yaml/dist/nodes/Pair.js +36 -0
  84. package/engine/vendor/yaml/dist/nodes/Scalar.js +24 -0
  85. package/engine/vendor/yaml/dist/nodes/YAMLMap.js +144 -0
  86. package/engine/vendor/yaml/dist/nodes/YAMLSeq.js +113 -0
  87. package/engine/vendor/yaml/dist/nodes/addPairToJSMap.js +63 -0
  88. package/engine/vendor/yaml/dist/nodes/identity.js +36 -0
  89. package/engine/vendor/yaml/dist/nodes/toJS.js +37 -0
  90. package/engine/vendor/yaml/dist/parse/cst-scalar.js +214 -0
  91. package/engine/vendor/yaml/dist/parse/cst-stringify.js +61 -0
  92. package/engine/vendor/yaml/dist/parse/cst-visit.js +97 -0
  93. package/engine/vendor/yaml/dist/parse/cst.js +98 -0
  94. package/engine/vendor/yaml/dist/parse/lexer.js +721 -0
  95. package/engine/vendor/yaml/dist/parse/line-counter.js +39 -0
  96. package/engine/vendor/yaml/dist/parse/parser.js +975 -0
  97. package/engine/vendor/yaml/dist/public-api.js +102 -0
  98. package/engine/vendor/yaml/dist/schema/Schema.js +37 -0
  99. package/engine/vendor/yaml/dist/schema/common/map.js +17 -0
  100. package/engine/vendor/yaml/dist/schema/common/null.js +15 -0
  101. package/engine/vendor/yaml/dist/schema/common/seq.js +17 -0
  102. package/engine/vendor/yaml/dist/schema/common/string.js +14 -0
  103. package/engine/vendor/yaml/dist/schema/core/bool.js +19 -0
  104. package/engine/vendor/yaml/dist/schema/core/float.js +43 -0
  105. package/engine/vendor/yaml/dist/schema/core/int.js +38 -0
  106. package/engine/vendor/yaml/dist/schema/core/schema.js +23 -0
  107. package/engine/vendor/yaml/dist/schema/json/schema.js +62 -0
  108. package/engine/vendor/yaml/dist/schema/tags.js +96 -0
  109. package/engine/vendor/yaml/dist/schema/yaml-1.1/binary.js +58 -0
  110. package/engine/vendor/yaml/dist/schema/yaml-1.1/bool.js +26 -0
  111. package/engine/vendor/yaml/dist/schema/yaml-1.1/float.js +46 -0
  112. package/engine/vendor/yaml/dist/schema/yaml-1.1/int.js +71 -0
  113. package/engine/vendor/yaml/dist/schema/yaml-1.1/merge.js +67 -0
  114. package/engine/vendor/yaml/dist/schema/yaml-1.1/omap.js +74 -0
  115. package/engine/vendor/yaml/dist/schema/yaml-1.1/pairs.js +78 -0
  116. package/engine/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
  117. package/engine/vendor/yaml/dist/schema/yaml-1.1/set.js +93 -0
  118. package/engine/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +101 -0
  119. package/engine/vendor/yaml/dist/stringify/foldFlowLines.js +146 -0
  120. package/engine/vendor/yaml/dist/stringify/stringify.js +129 -0
  121. package/engine/vendor/yaml/dist/stringify/stringifyCollection.js +153 -0
  122. package/engine/vendor/yaml/dist/stringify/stringifyComment.js +20 -0
  123. package/engine/vendor/yaml/dist/stringify/stringifyDocument.js +85 -0
  124. package/engine/vendor/yaml/dist/stringify/stringifyNumber.js +25 -0
  125. package/engine/vendor/yaml/dist/stringify/stringifyPair.js +150 -0
  126. package/engine/vendor/yaml/dist/stringify/stringifyString.js +336 -0
  127. package/engine/vendor/yaml/dist/util.js +11 -0
  128. package/engine/vendor/yaml/dist/visit.js +233 -0
  129. package/engine/vendor/yaml/index.js +5 -0
  130. package/engine/vendor/yaml/package.json +11 -0
  131. package/lib/client.js +4421 -781
  132. package/lib/client.js.map +1 -1
  133. package/lib/index.d.mts +504 -51
  134. package/lib/index.mjs +3871 -731
  135. package/lib/preset-core.d.mts +7 -37
  136. package/lib/preset-core.mjs +44 -283
  137. package/lib/prompt-configs-BcP6miu9.d.mts +102 -0
  138. package/lib/prompt-configs-k7wuZs0C.mjs +719 -0
  139. package/package.json +104 -98
  140. package/preset/anchored/preset.yml +344 -0
  141. package/preset/creative/preset.yml +62 -0
  142. package/preset/custom/preset.yml +13 -0
  143. package/preset/liangshen/preset.yml +66 -0
  144. package/preset/minimal/preset.yml +41 -0
  145. package/preset/ptc/preset.yml +50 -0
  146. package/preset/standard/preset.yml +49 -0
  147. package/skills/manifest.json +7 -0
  148. package/skills/sandboxmod/SKILL.md +49 -49
  149. package/skills/web ui/SKILL.md +42 -0
  150. package/templates/10-pre-step.yml +28 -0
  151. package/templates/11-merged-a.yml +11 -0
  152. package/templates/13-anchor.yml +19 -0
  153. package/templates/14-first-turn-anchor.yml +27 -0
  154. package/templates/15-guide-auto.yml +25 -0
  155. package/templates/16-custom-fallback.yml +21 -0
  156. package/templates/17-instruction-hint.yml +23 -0
  157. package/templates/18-placeholder-env-facts.yml +11 -0
  158. package/templates/19-placeholder-skill-catalog.yml +19 -0
  159. package/templates/20-system-section.yml +14 -0
  160. package/templates/30-runtime-context.yml +10 -0
  161. package/templates/31-runtime-context-placeholder.yml +18 -0
  162. package/templates/40-agent-request.yml +11 -0
  163. package/templates/50-llm-stream.yml +9 -0
  164. package/templates/60-tool-pipeline.yml +12 -0
  165. package/AGENTS.md +0 -4
  166. package/plan.md +0 -312
  167. package/preset/agent.cordis.yml +0 -443
  168. package/preset/compaction-epoch.mjs +0 -81
  169. package/preset/context-gate.mjs +0 -165
  170. package/preset/instruction-hint.mjs +0 -217
  171. package/preset/near-anchor.mjs +0 -101
  172. package/preset/preset.yml +0 -3
  173. package/preset/prompt-injector.mjs +0 -112
  174. package/preset/router-guide.mjs +0 -79
  175. package/preset.md +0 -115
  176. package/upstream/dsh-anchored-standard/LICENSE +0 -22
  177. package/upstream/dsh-anchored-standard/NOTICE +0 -19
  178. package/upstream/dsh-anchored-standard/REVISION +0 -1
  179. package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
  180. package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
  181. package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
  182. package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
  183. package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
  184. package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
  185. package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
  186. package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
  187. package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
  188. /package/{preset → engine}/skill-search.mjs +0 -0
@@ -1,202 +0,0 @@
1
- /**
2
- * anchored-context-gate — reusable unified injection control for ANY preset.
3
- *
4
- * Mount this one plugin to keep a session's first model request free of
5
- * auto-injected context, whatever its source, and to have every injection
6
- * return on the second round. It intercepts the harness's two unified
7
- * injection paths — not a per-source denylist — so it covers sources that do
8
- * not exist yet:
9
- *
10
- * a. RUNTIME CONTEXT (system-prompt/assemble): while the session is
11
- * unpromoted, the assembly's `contexts` are blanked. That covers the
12
- * WHOLE `SystemPrompt.context()` family — the sandbox and approval
13
- * policy snapshots and any third-party context provider — without
14
- * enumerating them. The loop's own snapshot projection then emits no
15
- * message during the gate (no snapshot ever existed), and at the first
16
- * promoted request it emits exactly ONE fresh snapshot: "minimal first
17
- * round, inject on the second round" falls out of the projection's
18
- * diffing, with no reinjection logic here.
19
- *
20
- * b. STEP MESSAGES (agent/pre-step): the waterfall payload carries the
21
- * CLAIMED message batch (the inbox messages this step owns). While
22
- * unpromoted, the gate keeps exactly the claimed messages plus a small
23
- * kind allowlist, and strips everything any listener appended — skill
24
- * catalog, AGENTS.md digest, time/tmux context, hooks, unknown
25
- * third-party plugins — by DEFAULT, regardless of source identity. The
26
- * default allowlist is `['skill-invocation']`: a user-initiated skill
27
- * gesture is not an automatic injection, and stripping it would lose the
28
- * skill content once the gesture scrolls out of the per-step claim.
29
- * Durable history (compaction summaries included) never passes through
30
- * this gate: it enters the request via the session surface, not the
31
- * pre-step waterfall.
32
- *
33
- * The phase is the same epoch-aware promotion machine the anchored presets
34
- * use (see compaction-epoch.mjs): a durable `tool/call` and/or
35
- * `assistant/message` (per `promoteOn`, default `either`) promotes, and a
36
- * `compaction/end` boundary demotes again — the first post-compaction request
37
- * is a "second first request" and is gated the same way. Derived from durable
38
- * events, so resume and reload preserve it.
39
- *
40
- * SUBAGENTS: by default subagents (delegationDepth > 0) skip the gate (their
41
- * first request already sees full context). `includeSubagents: true` gates
42
- * them too — their first request is clean and their own first reply or tool
43
- * call opens the gate — so a delegation cannot reintroduce an uncontrolled
44
- * first request. Keep this flag in sync with any companion phase plugin
45
- * (e.g. the tool-bootstrap row).
46
- *
47
- * CONFIG:
48
- * - `promoteOn`: 'either' (default) | 'tool-call' | 'assistant-message'.
49
- * - `includeSubagents`: boolean, default false.
50
- * - `enabled`: boolean, default true. `false` disables both interception
51
- * paths (A/B testing without touching the row set).
52
- * - `allowKinds`: message `source.kind` names allowed beyond the claimed
53
- * batch, default ['skill-invocation']. An explicitly empty array keeps
54
- * ONLY the claimed batch.
55
- *
56
- * ROW ORDER: mount this row FIRST in the composition. Waterfall after-next
57
- * transforms apply in reverse registration order, so registering first (plus
58
- * the pre-step listener's `prepend: true`) makes the gate the outermost
59
- * transform — nothing registered later re-injects past it.
60
- *
61
- * Robustness: both filters degrade to "keep everything" on their own
62
- * failures — a gate bug must never eat the user's context — and invalid
63
- * config fails at apply time, i.e. at preset mount, where it is visible.
64
- */
65
-
66
- import { createEpochPromotion } from './compaction-epoch.mjs'
67
-
68
- /** Cordis plugin name used by loader diagnostics. */
69
- export const name = 'anchored-context-gate'
70
-
71
- /**
72
- * Deliberately NO inject list: the listeners only touch services at event
73
- * time, and applying without an inject lets this row register before the
74
- * context-injecting plugins (dsh-agent-instructions, dsh-tool-skill, host
75
- * plane policy projections) when it sits first in the composition.
76
- */
77
- export const inject = []
78
-
79
- /** Durable session event types that count as a promotion signal per mode. */
80
- const PROMOTE_EVENTS = {
81
- 'tool-call': ['tool/call'],
82
- 'assistant-message': ['assistant/message'],
83
- either: ['tool/call', 'assistant/message'],
84
- }
85
-
86
- /** Every config key this plugin accepts — anything else is a typo. */
87
- const ALLOWED_KEYS = new Set(['promoteOn', 'includeSubagents', 'enabled', 'allowKinds'])
88
-
89
- /**
90
- * Message kinds allowed through the pre-step gate beyond the claimed batch.
91
- * A user-initiated skill gesture is the only default entry: it is not an
92
- * automatic injection (see the header note).
93
- */
94
- const DEFAULT_ALLOW_KINDS = ['skill-invocation']
95
-
96
- function parsePromoteOn(value) {
97
- if (value === undefined || value === 'either') return PROMOTE_EVENTS.either
98
- if (value === 'tool-call' || value === 'assistant-message') return PROMOTE_EVENTS[value]
99
- throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
100
- }
101
-
102
- /**
103
- * Validate the kind allowlist. An explicitly empty array is meaningful: keep
104
- * ONLY the claimed batch, stripping even user skill gestures.
105
- */
106
- function allowKindList(value, field) {
107
- if (value === undefined) return new Set(DEFAULT_ALLOW_KINDS)
108
- if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
109
- throw new TypeError(`${name}: ${field} must be an array of non-empty strings`)
110
- }
111
- return new Set(value)
112
- }
113
-
114
- /** Validate an optional boolean flag with a default. */
115
- function booleanOption(value, field, fallback) {
116
- if (value === undefined) return fallback
117
- if (typeof value !== 'boolean') {
118
- throw new TypeError(`${name}: ${field} must be a boolean`)
119
- }
120
- return value
121
- }
122
-
123
- /** Register the unified context gate. */
124
- export function apply(ctx, config) {
125
- const source = config === undefined ? {} : config
126
- if (typeof source !== 'object' || source === null || Array.isArray(source)) {
127
- throw new TypeError(`${name}: config must be an object`)
128
- }
129
- const unknown = Object.keys(source).filter((key) => !ALLOWED_KEYS.has(key))
130
- if (unknown.length > 0) {
131
- throw new TypeError(
132
- `${name}: unknown config key(s) ${unknown.join(', ')} — allowed keys: ${[...ALLOWED_KEYS].sort().join(', ')}`,
133
- )
134
- }
135
- const promoteEvents = parsePromoteOn(source.promoteOn)
136
- const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)
137
- const enabled = booleanOption(source.enabled, 'enabled', true)
138
- const allowKinds = allowKindList(source.allowKinds, 'allowKinds')
139
-
140
- const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
141
- ctx.on('session/event', (session, event) => promotion.observe(session, event))
142
-
143
- let warned = false
144
- const warnOnce = (message) => {
145
- if (warned) return
146
- warned = true
147
- try {
148
- ctx.logger.warn(message)
149
- } catch {
150
- // Logger unavailable — the guard exists only to avoid spamming.
151
- }
152
- }
153
-
154
- // Path (a): blank the dynamic runtime-context contributions while the
155
- // session is unpromoted. Covers the whole SystemPrompt.context() family
156
- // without enumerating it; the loop's snapshot projection then stays silent
157
- // and diffs exactly ONE fresh snapshot in at the first promoted request.
158
- ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
159
- // Downstream errors propagate untouched; only this filter's own logic is guarded.
160
- const assembled = await next()
161
- if (enabled === false) return assembled
162
- try {
163
- if (promotion.status(context.agent).promoted) return assembled
164
- if (!Array.isArray(assembled.contexts) || assembled.contexts.length === 0) return assembled
165
- return { ...assembled, contexts: [] }
166
- } catch (error) {
167
- // A gate bug must never break assembly: degrade to the assembled value.
168
- warnOnce(`${name}: runtime-context suppression failed, keeping contexts: ${String((error && error.message) || error)}`)
169
- return assembled
170
- }
171
- })
172
-
173
- // Path (b): claimed-baseline deny on the pre-step waterfall. The payload's
174
- // `messages` is the batch this step CLAIMED from the inbox — the baseline
175
- // every injection appends to. Keep that baseline plus the kind allowlist,
176
- // strip every appended message regardless of its source identity.
177
- ctx.on('agent/pre-step', async ({ agent, messages: claimed }, next) => {
178
- // Downstream errors propagate untouched; only this filter's own logic is guarded.
179
- const decision = await next()
180
- if (decision.kind === 'reject') return decision
181
- if (enabled === false) return decision
182
- try {
183
- if (promotion.status(agent).promoted) return decision
184
- if (!Array.isArray(decision.messages)) return decision
185
- if (!Array.isArray(claimed)) return decision
186
- const baseline = new Set(claimed)
187
- const baselineIds = new Set(claimed
188
- .map((message) => message?.id)
189
- .filter((id) => id !== undefined && id !== null))
190
- const kept = decision.messages.filter((message) =>
191
- baseline.has(message)
192
- || (message?.id !== undefined && message?.id !== null && baselineIds.has(message.id))
193
- || allowKinds.has(message?.source?.kind),
194
- )
195
- return kept.length === decision.messages.length ? decision : { ...decision, messages: kept }
196
- } catch (error) {
197
- // A gate bug must never eat context: degrade to keeping every message.
198
- warnOnce(`${name}: pre-step gate failed, keeping injected context: ${String((error && error.message) || error)}`)
199
- return decision
200
- }
201
- }, { prepend: true })
202
- }
@@ -1,219 +0,0 @@
1
- /**
2
- * custom-bash — a Windows-capable `bash` tool that registers under the SAME
3
- * name (`bash`) as the official persistent bash, with a Minimal-compatible
4
- * description, but executes through `ctx.subprocess.spawn` instead of a PTY.
5
- *
6
- * WHY: DeepSeek's first-request trajectory anchor keys on the tool SCHEMA
7
- * matching the RL training distribution (issue #11: persistent
8
- * bash + str_replace_editor anchored 5/5 at maxTokens=256000, pwsh/read
9
- * 8/8 standard-like). The official persistent bash uses a PTY, and DSH's PTY
10
- * backend is linux/darwin-only — `subprocess-local` throws "terminal
11
- * inspection is unsupported on platform win32". A custom tool that presents
12
- * the same name and a Minimal-like description but spawns Git Bash through
13
- * the ordinary (cross-platform) subprocess seam keeps the schema anchor
14
- * without the PTY dependency.
15
- *
16
- * Executable resolution (config `bashPath`, issue #24 — no hardcoded install
17
- * path): an explicit non-empty `bashPath` wins unconditionally. Unset, the
18
- * Git Bash executable is INFERRED, in probe order:
19
- * 1. the `git` executable on PATH — its install root carries `bin\bash.exe`
20
- * one level up from `cmd\`, beside `bin\`, or two levels up from
21
- * `mingw64\bin\` (the standard installer, choco, and winget all resolve
22
- * here; a scoop SHIM does not — its directory is the shims root, not the
23
- * app — which is what step 2 covers);
24
- * 2. the well-known Git-for-Windows roots derived from environment variables
25
- * (`ProgramFiles`, `ProgramFiles(x86)`, per-user `LOCALAPPDATA\Programs
26
- * \Git`, scoop's `~\scoop\apps\git\current` junction);
27
- * 3. plain `bash` through `ctx.subprocess.resolveExecutable` (PATH lookup —
28
- * last resort, since on Windows that may pick the WSL shim; WSL bash is
29
- * still true bash, only the filesystem paths shift to /mnt/…).
30
- *
31
- * If NOTHING resolves, the tool fails with an actionable error naming the
32
- * remedies — it does NOT silently execute under a different shell: the
33
- * schema above promises `bash -c` semantics, and pwsh/cmd are different
34
- * command languages. PowerShell stays available as its OWN tool (`pwsh`,
35
- * present in the promoted catalog on Windows, unlockable via
36
- * dev_tool_search).
37
- *
38
- * Semantics mirror the official bash tool: `bash -c <command>` in a fresh
39
- * process, bounded output, non-zero exit reported not thrown. No sandbox
40
- * confinement on Windows (the sandbox backend is linux-only); the tool
41
- * description says so. The bootstrap catalog pairs this with
42
- * `str_replace_editor` (Minimal's two tools).
43
- */
44
-
45
- import { access } from 'node:fs/promises'
46
- import { dirname, join } from 'node:path'
47
-
48
- /** Cordis plugin name used by loader diagnostics. */
49
- export const name = 'custom-bash'
50
-
51
- /** The subprocess and tools services must exist before this tool can register. */
52
- export const inject = ['subprocess', 'tools']
53
-
54
- const DEFAULT_TIMEOUT_MS = 120000
55
- const DEFAULT_MAX_OUTPUT_BYTES = 64000
56
-
57
- /**
58
- * Git Bash candidate paths, in probe order (see the header): the `git`
59
- * executable's install root first, then the well-known env-derived roots.
60
- * Exported for tests; pure — existence probing happens at the call site.
61
- */
62
- export function bashCandidates(env, gitExe) {
63
- const candidates = []
64
- // git at <root>\cmd\git.exe (installer/scoop) or <root>\bin\git.exe →
65
- // <root>\bin\bash.exe; <root>\mingw64\bin\git.exe (portable) → two up.
66
- // A bare relative name means `git` did not actually resolve to a path.
67
- if (typeof gitExe === 'string' && /[/\\]/.test(gitExe)) {
68
- const dir = dirname(gitExe)
69
- const root = dirname(dir)
70
- candidates.push(
71
- join(root, 'bin', 'bash.exe'),
72
- join(dir, 'bash.exe'),
73
- join(dirname(root), 'bin', 'bash.exe'),
74
- )
75
- }
76
- if (env.ProgramFiles) candidates.push(join(env.ProgramFiles, 'Git', 'bin', 'bash.exe'))
77
- if (env['ProgramFiles(x86)']) candidates.push(join(env['ProgramFiles(x86)'], 'Git', 'bin', 'bash.exe'))
78
- if (env.LOCALAPPDATA) candidates.push(join(env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe'))
79
- if (env.USERPROFILE) candidates.push(join(env.USERPROFILE, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'))
80
- // Layouts overlap (a `bin` git.exe derives the same bash twice) — probe
81
- // order survives the dedupe, insertion order is preserved.
82
- return [...new Set(candidates)]
83
- }
84
-
85
- /** Tool parameter schema for the model-facing command. */
86
- const commandSchema = {
87
- type: 'object',
88
- properties: {
89
- command: {
90
- type: 'string',
91
- description: 'The bash command to execute (`bash -c` string domain).',
92
- },
93
- workdir: {
94
- type: 'string',
95
- description: 'Optional working directory; defaults to the session cwd.',
96
- },
97
- },
98
- required: ['command'],
99
- additionalProperties: false,
100
- }
101
-
102
- /** Register the model-facing `bash` tool. */
103
- export function apply(ctx, config) {
104
- const explicitBashPath = typeof config?.bashPath === 'string' && config.bashPath.length > 0 ? config.bashPath : undefined
105
- const timeoutMs = Number.isSafeInteger(config?.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS
106
- const maxOutputBytes = Number.isSafeInteger(config?.maxOutputBytes) && config.maxOutputBytes > 0 ? config.maxOutputBytes : DEFAULT_MAX_OUTPUT_BYTES
107
-
108
- // The inferred executable is memoized per plugin instance: candidate probing
109
- // walks the filesystem, and the answer cannot change within a mount. A
110
- // failed inference is NOT memoized — the plain `bash` fallback resolves
111
- // fresh on every execute until some probe succeeds.
112
- let inferredShell
113
- const exists = (path) => access(path).then(() => true, () => false)
114
- const resolveShell = async (signal) => {
115
- if (explicitBashPath !== undefined) {
116
- // A misconfigured explicit path must fail as itself, not as a
117
- // discovery miss — the raw resolution error says which path failed.
118
- return ctx.subprocess.resolveExecutable(explicitBashPath, undefined, signal)
119
- }
120
- if (inferredShell !== undefined) {
121
- return ctx.subprocess.resolveExecutable(inferredShell, undefined, signal)
122
- }
123
- let gitExe
124
- try {
125
- gitExe = await ctx.subprocess.resolveExecutable('git', undefined, signal)
126
- } catch {
127
- // git unresolvable → the env-derived candidates below still apply
128
- }
129
- for (const candidate of bashCandidates(process.env, gitExe)) {
130
- if (!(await exists(candidate))) continue
131
- try {
132
- inferredShell = await ctx.subprocess.resolveExecutable(candidate, undefined, signal)
133
- return inferredShell
134
- } catch {
135
- // Exists but unresolvable (EPERM, a broken scoop junction): keep
136
- // probing — one bad root must not block the rest of the chain, and
137
- // nothing is memoized so later executes can still find a good one.
138
- continue
139
- }
140
- }
141
- try {
142
- return await ctx.subprocess.resolveExecutable('bash', undefined, signal)
143
- } catch (error) {
144
- // Total discovery failure (no Git Bash root, no env root, no bash on
145
- // PATH): name the remedies instead of leaking a raw ENOENT. Never
146
- // fall back to pwsh/cmd here — the schema promises `bash -c`
147
- // semantics; a different shell would silently break every command.
148
- throw new Error(`bash executable not found — install Git for Windows, expose a bash on PATH, or set the custom-bash \`bashPath\` config (${String((error && error.message) || error)})`)
149
- }
150
- }
151
-
152
- ctx.tools.register({
153
- name: 'bash',
154
- description: [
155
- 'Run commands in a bash shell (Git Bash on Windows)',
156
- '* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
157
- "* You don't have access to the internet via this tool.",
158
- '* You do have access to a mirror of common linux and python packages via apt and pip.',
159
- '* State does NOT persist across command calls: each call runs in a fresh shell.',
160
- "* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.",
161
- '* Please avoid commands that may produce a very large amount of output.',
162
- '* NOTE: runs without OS sandbox confinement on Windows (no landlock); treat output as untrusted.',
163
- ].join('\n'),
164
- parameters: commandSchema,
165
- output: {
166
- schema: {
167
- type: 'object',
168
- additionalProperties: false,
169
- properties: {
170
- text: { type: 'string' },
171
- },
172
- required: ['text'],
173
- },
174
- render: (_args, value) => [{ type: 'text', text: value.text }],
175
- },
176
- async execute(args, exec) {
177
- const shell = await resolveShell(exec?.signal)
178
- const workdir = typeof args.workdir === 'string' && args.workdir.length > 0
179
- ? args.workdir
180
- : exec?.agent?.session?.header?.cwd
181
- const signal = exec?.signal
182
- const handle = ctx.subprocess.spawn({
183
- argv: [shell, '-c', args.command],
184
- ...workdir !== undefined ? { cwd: workdir } : {},
185
- stdio: {
186
- stdin: 'ignore',
187
- stdout: { maxBytes: maxOutputBytes },
188
- stderr: { maxBytes: maxOutputBytes },
189
- },
190
- ...signal !== undefined ? { signal } : {},
191
- graceMs: 3000,
192
- })
193
- let outcome
194
- try {
195
- outcome = await handle.done
196
- } catch (error) {
197
- // A spawn-level failure (bad executable, EPERM) surfaces as a throw,
198
- // which the runtime turns into an isError result.
199
- throw new Error(`bash spawn failed: ${String(error)}`)
200
- }
201
- let stdout = ''
202
- let stderr = ''
203
- try {
204
- stdout = handle.collected.stdout.readFrom(0).text
205
- stderr = handle.collected.stderr.readFrom(0).text
206
- } catch {
207
- // Collected readers may be unavailable on some backends; tolerate.
208
- }
209
- const text = [stdout, stderr].filter((part) => part.length > 0).join('\n')
210
- const tail = text.length > 0 ? text : `exit code: ${outcome.exitCode} (no output)`
211
- if (outcome.exitCode !== 0) {
212
- // Non-zero exit is a reported failure, not a throw: the model sees the
213
- // command output plus the exit code.
214
- throw new Error(tail)
215
- }
216
- return { text: tail }
217
- },
218
- })
219
- }
@@ -1,131 +0,0 @@
1
- /**
2
- * dev-tool-search — on-demand tool discovery and unlock, the tool-search
3
- * pattern for the anchored preset.
4
- *
5
- * The promoted phase keeps only a minimal resident set (shell +
6
- * str_replace_editor + the discovery tools) instead of dumping the whole
7
- * Standard catalog at once. This plugin registers ONE small tool:
8
- *
9
- * - `dev_tool_search` — search the FULL assembled catalog by keyword and
10
- * return matching tool names with short descriptions; optionally unlock
11
- * tools by exact name (array `toolNames`). Unlocked names are recorded as
12
- * durable `tool/call` arguments, and tool-bootstrap.mjs's assemble filter
13
- * exposes them from the next request on (resume-safe).
14
- *
15
- * The tool description is deliberately an INDEX of what the minimal resident
16
- * set cannot do: the model should reach for dev_tool_search the moment a task
17
- * needs internet, delegation, workflows, goals, images, background jobs, or
18
- * multi-agent coordination — not try to work around them with bash.
19
- */
20
-
21
- /** Cordis plugin name used by loader diagnostics. */
22
- export const name = 'dev-tool-search'
23
-
24
- /** The tools registry must exist before this tool can register. */
25
- export const inject = ['tools']
26
-
27
- const MAX_RESULTS = 25
28
-
29
- /** Minimal JSON schema compiler for tool parameters (zero dependencies). */
30
- function toJsonSchema(spec) {
31
- const properties = {}
32
- const required = []
33
- for (const [key, meta] of Object.entries(spec || {})) {
34
- const prop = { type: meta.type }
35
- if (meta.description) prop.description = meta.description
36
- properties[key] = prop
37
- if (meta.required) required.push(key)
38
- }
39
- return { type: 'object', properties, required, additionalProperties: false }
40
- }
41
-
42
- /**
43
- * The capability index: resident minimal tools (bash / str_replace_editor /
44
- * skill_search / skill_load) cannot cover these, so the model must search
45
- * and unlock them on demand. Kept in the description so the model KNOWS what
46
- * exists without a full catalog dump.
47
- */
48
- const UNLOCKABLE_INDEX = [
49
- 'web_search — internet search and web retrieval',
50
- 'subagent / subagent_fork — delegate work to sub-agents',
51
- 'workflow — run multi-agent workflow scripts',
52
- 'ralph — fresh-agent iterative loop',
53
- 'create_goal / get_goal / update_goal — long-running goals',
54
- 'read_image — read image files',
55
- 'job_list / job_output / job_kill — background jobs',
56
- 'interrupt_agent / send_message / list_agents — multi-agent control',
57
- 'todo_write — task tracking',
58
- 'ask_user_question — ask the user',
59
- ]
60
-
61
- /** Register the model-facing `dev_tool_search` tool. */
62
- export function apply(ctx) {
63
- ctx.tools.register({
64
- name: 'dev_tool_search',
65
- description: [
66
- 'Discover and unlock tools that are NOT currently available.',
67
- '',
68
- 'This session starts with a minimal resident set: bash, str_replace_editor, skill_search, skill_load. Everything else is unlocked on demand through this tool.',
69
- '',
70
- 'If the current task needs any of the following, call dev_tool_search FIRST — do not try to work around them with bash:',
71
- ...UNLOCKABLE_INDEX.map((line) => `- ${line}`),
72
- '',
73
- 'Usage: pass `query` to search the catalog (returns matching tool names + descriptions), then pass `toolNames` with exact names to unlock them. Unlocked tools appear from the next request on and stay unlocked for the session.',
74
- ].join('\n'),
75
- parameters: toJsonSchema({
76
- query: { type: 'string', required: false, description: 'search keywords (e.g. "web", "subagent")' },
77
- toolNames: { type: 'array', required: false, description: 'exact tool names to unlock', items: { type: 'string' } },
78
- }),
79
- output: {
80
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
81
- render: (_a, v) => [{ type: 'text', text: v.text }],
82
- },
83
- async execute(args, exec) {
84
- const query = typeof args.query === 'string' ? args.query.trim() : ''
85
- const unlock = Array.isArray(args.toolNames) ? args.toolNames.filter((name) => typeof name === 'string' && name.length > 0) : []
86
-
87
- const lines = []
88
- if (unlock.length > 0) {
89
- lines.push(`Unlocked for the next request: ${unlock.join(', ')}`)
90
- }
91
- if (query.length === 0 && unlock.length === 0) {
92
- lines.push('Provide `query` to search the catalog, or `toolNames` to unlock tools.')
93
- return { text: lines.join('\n') }
94
- }
95
- if (query.length === 0) {
96
- return { text: lines.join('\n') || 'Nothing to do.' }
97
- }
98
-
99
- try {
100
- // The executing agent IS the viewing scope: preset tools register into
101
- // the agent-scope layer of the tools registry, and schemas() with no
102
- // scope only sees the global layer — every preset-provided tool would
103
- // be invisible to keyword search (issue #24). Same pattern as the
104
- // harness's own code mode (`registry.schemas(exec.agent)`).
105
- const schemas = ctx.tools.schemas(exec?.agent)
106
- const wanted = query.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean)
107
- const all = schemas.filter((schema) => {
108
- const haystack = `${schema.name} ${schema.description ?? ''}`.toLowerCase()
109
- return wanted.every((token) => haystack.includes(token))
110
- })
111
- const matches = all.slice(0, MAX_RESULTS)
112
- if (all.length === 0) {
113
- lines.push(`No tools match "${query}".`)
114
- } else {
115
- lines.push(`Matching tools (${matches.length}${all.length > MAX_RESULTS ? ` of ${all.length}` : ''}):`)
116
- for (const schema of matches) {
117
- const desc = (schema.description || '').split('\n')[0].slice(0, 90)
118
- lines.push(`- ${schema.name}: ${desc}`)
119
- }
120
- if (all.length > MAX_RESULTS) {
121
- lines.push(`(truncated at ${MAX_RESULTS} — add tokens to narrow the query, e.g. "mcp browser" or "mcp tavily")`)
122
- }
123
- lines.push('Unlock with dev_tool_search({"toolNames": ["<exact name>"]}).')
124
- }
125
- } catch (error) {
126
- lines.push(`catalog search unavailable: ${String((error && error.message) || error)}`)
127
- }
128
- return { text: lines.join('\n') }
129
- },
130
- })
131
- }