dsh-plugin-prompt-tool 0.4.2 → 0.6.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 (191) hide show
  1. package/README.md +131 -222
  2. package/engine/anchor-match.mjs +117 -0
  3. package/engine/compaction-epoch.mjs +139 -0
  4. package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
  5. package/engine/compositions/library/compaction.yml +52 -0
  6. package/engine/compositions/library/context-gate.yml +40 -0
  7. package/engine/compositions/library/custom-bash.yml +14 -0
  8. package/engine/compositions/library/delegation.yml +85 -0
  9. package/engine/compositions/library/official-agent-instructions.yml +17 -0
  10. package/engine/compositions/library/official-persistent-shell.yml +56 -0
  11. package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
  12. package/engine/compositions/library/official-tool-bash.yml +6 -0
  13. package/engine/compositions/library/official-tool-cordis.yml +13 -0
  14. package/engine/compositions/library/official-tool-presentation.yml +7 -0
  15. package/engine/compositions/library/official-tool-skill.yml +14 -0
  16. package/engine/compositions/library/persistent-shell.yml +36 -0
  17. package/engine/compositions/library/persona.yml +9 -0
  18. package/engine/compositions/library/planning.yml +41 -0
  19. package/engine/compositions/library/prompt-config-engine.yml +16 -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/run-code-env.yml +12 -0
  39. package/engine/compositions/source/local/skill-search.yml +11 -0
  40. package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
  41. package/engine/context-gate.mjs +305 -0
  42. package/{preset → engine}/custom-bash.mjs +243 -243
  43. package/engine/executor.mjs +278 -0
  44. package/engine/fillers.mjs +273 -0
  45. package/engine/interpolate.mjs +66 -0
  46. package/engine/layers.mjs +224 -0
  47. package/engine/prompt-config-engine.mjs +49 -0
  48. package/engine/run-code-env.mjs +208 -0
  49. package/engine/schema.mjs +312 -0
  50. package/engine/session-vars.mjs +52 -0
  51. package/{preset → engine}/shared.mjs +35 -0
  52. package/engine/strategies.mjs +213 -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 +4724 -775
  132. package/lib/client.js.map +1 -1
  133. package/lib/index.d.mts +530 -51
  134. package/lib/index.mjs +4164 -740
  135. package/lib/preset-core.d.mts +7 -37
  136. package/lib/preset-core.mjs +43 -285
  137. package/lib/prompt-configs-B4vH09wx.d.mts +100 -0
  138. package/lib/prompt-configs-ThS4iXPg.mjs +771 -0
  139. package/package.json +36 -29
  140. package/preset/anchored/preset.yml +342 -0
  141. package/preset/creative/preset.yml +65 -0
  142. package/preset/creative/skills/cordis-plugin-development/SKILL.md +420 -0
  143. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +165 -0
  144. package/preset/custom/preset.yml +13 -0
  145. package/preset/liangshen/preset.yml +72 -0
  146. package/preset/minimal/preset.yml +44 -0
  147. package/preset/ptc/preset.yml +56 -0
  148. package/preset/standard/preset.yml +55 -0
  149. package/skills/manifest.json +7 -0
  150. package/skills/sandboxmod/SKILL.md +49 -49
  151. package/skills/web ui/SKILL.md +42 -0
  152. package/templates/10-pre-step.yml +28 -0
  153. package/templates/11-merged-a.yml +11 -0
  154. package/templates/13-anchor.yml +19 -0
  155. package/templates/14-first-turn-anchor.yml +27 -0
  156. package/templates/15-guide-auto.yml +25 -0
  157. package/templates/16-custom-fallback.yml +21 -0
  158. package/templates/17-instruction-hint.yml +23 -0
  159. package/templates/18-placeholder-env-facts.yml +11 -0
  160. package/templates/19-placeholder-skill-catalog.yml +19 -0
  161. package/templates/20-system-section.yml +19 -0
  162. package/templates/30-runtime-context.yml +10 -0
  163. package/templates/31-runtime-context-placeholder.yml +18 -0
  164. package/templates/40-agent-request.yml +11 -0
  165. package/templates/50-llm-stream.yml +9 -0
  166. package/templates/60-tool-pipeline.yml +12 -0
  167. package/AGENTS.md +0 -4
  168. package/plan.md +0 -312
  169. package/preset/agent.cordis.yml +0 -443
  170. package/preset/compaction-epoch.mjs +0 -81
  171. package/preset/context-gate.mjs +0 -165
  172. package/preset/instruction-hint.mjs +0 -217
  173. package/preset/near-anchor.mjs +0 -101
  174. package/preset/preset.yml +0 -3
  175. package/preset/prompt-injector.mjs +0 -112
  176. package/preset/router-first-turn.mjs +0 -73
  177. package/preset/router-guide.mjs +0 -79
  178. package/preset.md +0 -115
  179. package/upstream/dsh-anchored-standard/LICENSE +0 -22
  180. package/upstream/dsh-anchored-standard/NOTICE +0 -19
  181. package/upstream/dsh-anchored-standard/REVISION +0 -1
  182. package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
  183. package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
  184. package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
  185. package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
  186. package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
  187. package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
  188. package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
  189. package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
  190. package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
  191. /package/{preset → engine}/skill-search.mjs +0 -0
@@ -1,231 +0,0 @@
1
- /**
2
- * instruction-hint — replace `dsh-agent-instructions`' full AGENTS.md/CLAUDE.md
3
- * injection with a minimal "these files exist" hint.
4
- *
5
- * WHY: the full workspace-instruction digest is a large injected block. After
6
- * the anchored bootstrap promotes, we want the model to KNOW the instruction
7
- * files exist (so it reads them before acting) without dumping their content
8
- * into every request. The model reads the files itself via the filesystem
9
- * tools when it needs them.
10
- *
11
- * Behavior:
12
- * - After the session records its first durable promotion signal
13
- * (`promoteOn`, default `either`), ONE hint message is injected, listing
14
- * which instruction files were found:
15
- * - user-global: `$DSH_HOME/AGENTS.md`
16
- * - project chain: AGENTS.md / CLAUDE.md / AGENTS.local.md / CLAUDE.local.md
17
- * walking up from the session cwd to the project root (a directory
18
- * containing `.git`, or the cwd itself).
19
- * - The hint is ONCE PER SESSION, DERIVED FROM DURABLE EVENTS: the guard
20
- * scans the session log for an existing `instruction-hint` message (then
21
- * O(1)), so a process restart — whose in-memory state starts empty —
22
- * cannot inject a second copy. A duplicate would collide with the first
23
- * message's deterministic id (`instruction-hint-<sessionId>`) and break
24
- * history replay.
25
- * - The hint instructs the model to READ the files before acting when
26
- * relevant, without embedding their content.
27
- * - Files are probed via `ctx.fs` (the host filesystem seam); a missing fs
28
- * service or an unreadable probe degrades to no hint (never throws).
29
- * - Pre-promotion requests get NO hint (matches the anchored bootstrap).
30
- * - Subagents skip the phase wait by default (their first request already
31
- * counts as promoted); `includeSubagents: true` makes a subagent's own
32
- * first reply or tool call open the hint — which also keeps the injection
33
- * out of the context gate's stripped first request (the gate strips
34
- * non-claimed messages while unpromoted).
35
- *
36
- * ROW ORDER: this plugin registers its `agent/pre-step` handler with
37
- * `prepend: true` and after `context-gate`/`tool-bootstrap`, so it runs
38
- * inside the gate's outermost strip — but it emits AFTER promotion, when the
39
- * strip is inactive. The hint source kind is `instruction-hint`, which is
40
- * not in the gate's claimed-baseline allowlist, so the gate can strip it
41
- * only while the session is unpromoted (never the intended path).
42
- */
43
-
44
- import { createEpochPromotion } from './compaction-epoch.mjs'
45
-
46
- /** Cordis plugin name used by loader diagnostics. */
47
- export const name = 'instruction-hint'
48
-
49
- /** Durable session event types that count as a promotion signal per mode. */
50
- const PROMOTE_EVENTS = {
51
- 'tool-call': ['tool/call'],
52
- 'assistant-message': ['assistant/message'],
53
- either: ['tool/call', 'assistant/message'],
54
- }
55
-
56
- /** Candidate file names, in probe order, for the project chain and user-global. */
57
- const PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
58
- const USER_GLOBAL_CANDIDATE = 'AGENTS.md'
59
-
60
- function parsePromoteOn(value) {
61
- if (value === undefined || value === 'either') return PROMOTE_EVENTS.either
62
- if (value === 'tool-call' || value === 'assistant-message') return PROMOTE_EVENTS[value]
63
- throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
64
- }
65
-
66
- /** Every config key this plugin accepts — anything else is a typo. */
67
- const ALLOWED_KEYS = new Set(['promoteOn', 'includeSubagents'])
68
-
69
- /** Validate an optional boolean flag with a default. */
70
- function booleanOption(value, field, fallback) {
71
- if (value === undefined) return fallback
72
- if (typeof value !== 'boolean') {
73
- throw new TypeError(`${name}: ${field} must be a boolean`)
74
- }
75
- return value
76
- }
77
-
78
- /** Find the project root: first ancestor containing any root marker (e.g. .git). */
79
- async function findProjectRoot(fs, cwd, signal) {
80
- let current = cwd
81
- for (;;) {
82
- for (const marker of ['.git', '.hg', '.svn']) {
83
- try {
84
- const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
85
- const info = await fs.stat(target, signal)
86
- if (info !== undefined) return current
87
- } catch {
88
- // Probe failure = marker absent; continue.
89
- }
90
- }
91
- const parent = parentPath(current)
92
- if (parent === current || parent.length === 0) return cwd
93
- current = parent
94
- }
95
- }
96
-
97
- /** List instruction files present in one directory (project candidates). */
98
- async function presentInDir(fs, dir, candidates, signal) {
99
- const found = []
100
- for (const candidate of candidates) {
101
- try {
102
- const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
103
- const info = await fs.stat(target, signal)
104
- if (info !== undefined && info.type === 'file') found.push(candidate)
105
- } catch {
106
- // Absent or unreadable — skip.
107
- }
108
- }
109
- return found
110
- }
111
-
112
- /** Join one path segment onto a directory (platform-agnostic string join). */
113
- function joinPath(dir, segment) {
114
- if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
115
- const sep = dir.includes('\\') ? '\\' : '/'
116
- return dir + sep + segment
117
- }
118
-
119
- /** Parent of an absolute Windows or POSIX path. */
120
- function parentPath(path) {
121
- const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
122
- if (idx <= 0) return path
123
- const parent = path.slice(0, idx)
124
- return parent.length === 0 ? path : parent
125
- }
126
-
127
- /** Register the post-promotion instruction-hint injector. */
128
- export function apply(ctx, config) {
129
- const source = config === undefined ? {} : config
130
- if (typeof source !== 'object' || source === null || Array.isArray(source)) {
131
- throw new TypeError(`${name}: config must be an object`)
132
- }
133
- const unknown = Object.keys(source).filter((key) => !ALLOWED_KEYS.has(key))
134
- if (unknown.length > 0) {
135
- throw new TypeError(
136
- `${name}: unknown config key(s) ${unknown.join(', ')} — allowed keys: ${[...ALLOWED_KEYS].sort().join(', ')}`,
137
- )
138
- }
139
- const promoteEvents = parsePromoteOn(source.promoteOn)
140
- const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)
141
- const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
142
- ctx.on('session/event', (session, event) => promotion.observe(session, event))
143
-
144
- /**
145
- * Sessions whose hint is already durable in the event log — the
146
- * restart-safe replacement for an in-memory "already hinted" set. Seeded by
147
- * a one-time scan, then maintained incrementally through `session/event`.
148
- */
149
- const hinted = new Map()
150
- const hintIsDurable = (session) => {
151
- const known = hinted.get(session.id)
152
- if (known !== undefined) return known
153
- const found = (Array.isArray(session.events) ? session.events : []).some((event) =>
154
- event.type === 'user/message' && event.data?.source?.kind === 'instruction-hint',
155
- )
156
- hinted.set(session.id, found)
157
- return found
158
- }
159
- ctx.on('session/event', (session, event) => {
160
- if (event.type === 'user/message' && event.data?.source?.kind === 'instruction-hint') {
161
- hinted.set(session.id, true)
162
- }
163
- })
164
-
165
- let warned = false
166
- const warnOnce = (message) => {
167
- if (warned) return
168
- warned = true
169
- try {
170
- ctx.logger.warn(message)
171
- } catch {
172
- // Logger unavailable — the guard exists only to avoid spamming.
173
- }
174
- }
175
-
176
- ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
177
- const decision = await next()
178
- try {
179
- if (promotion.status(agent).promoted !== true) return decision
180
- const session = agent.session
181
- if (session === undefined || hintIsDurable(session)) return decision
182
- hinted.set(session.id, true)
183
-
184
- const fs = ctx.get('fs')
185
- if (fs === undefined) return decision
186
- const cwd = session.header.cwd ?? process.cwd()
187
-
188
- const projectFiles = []
189
- const root = await findProjectRoot(fs, cwd, signal)
190
- projectFiles.push(...await presentInDir(fs, root, PROJECT_CANDIDATES, signal))
191
-
192
- const userGlobalFiles = []
193
- try {
194
- const dshHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
195
- if (dshHome !== undefined) {
196
- userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_CANDIDATE], signal))
197
- }
198
- } catch {
199
- // Unreadable home probe — ignore.
200
- }
201
-
202
- const sections = []
203
- if (projectFiles.length > 0) {
204
- sections.push(`Workspace instruction files exist: ${projectFiles.join(', ')} (project root: ${root}).`)
205
- }
206
- if (userGlobalFiles.length > 0) {
207
- sections.push(`A user-global instruction file exists: ${USER_GLOBAL_CANDIDATE}.`)
208
- }
209
- if (sections.length === 0) return decision
210
-
211
- const text = [
212
- ...sections,
213
- 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
214
- ].join(' ')
215
-
216
- return {
217
- ...decision,
218
- messages: [...decision.messages, {
219
- id: `instruction-hint-${session.id}`,
220
- role: 'user',
221
- content: [{ type: 'text', text }],
222
- source: { kind: 'instruction-hint', form: 'hint' },
223
- }],
224
- }
225
- } catch (error) {
226
- // A hint bug must never hurt the session: skip the hint.
227
- warnOnce(`${name}: hint injection failed, skipping: ${String((error && error.message) || error)}`)
228
- return decision
229
- }
230
- }, { prepend: true })
231
- }
@@ -1,3 +0,0 @@
1
- name: Anchored Standard (experimental)
2
- description: Bootstrap with the Minimal preset's real tool pair (persistent bash + str_replace_editor) and no auto-injected workspace or skill context, then expose the full Standard catalog after the first durable tool call or reply.
3
- order: 5
@@ -1,142 +0,0 @@
1
- /**
2
- * skill-search — on-demand skill discovery and loading, replacing
3
- * `dsh-tool-skill`'s full-catalog injection.
4
- *
5
- * WHY: the available-skills reminder (`<available_skills>`, ~9KB with many
6
- * skills) is injected into the first step by dsh-tool-skill and again after
7
- * every promotion/compaction. That large injected block perturbs the
8
- * trajectory (issue #6: 0/9 anchored with the catalog present vs ~81%
9
- * without). We remove the catalog injection entirely and expose two small
10
- * tools instead — the Claude tool-search pattern:
11
- *
12
- * - `skill_search` — list skills whose name/description match a query
13
- * (summaries only, bounded; no bodies). The model discovers what exists
14
- * without a 9KB dump.
15
- * - `skill_load` — load ONE skill's full instructions by exact name and
16
- * inject them for the NEXT request via `agent.inject` (the non-waking
17
- * next-step inbox). The model (or the user) calls this only when the
18
- * skill is actually needed.
19
- *
20
- * Discovery reads `ctx.skills` scoped to the calling agent, exactly like
21
- * dsh-tool-skill. If skills are unavailable the tools answer with a short
22
- * message instead of throwing.
23
- *
24
- * NOTE: this plugin REPLACES the `dsh-tool-skill` row in the composition —
25
- * the composition must NOT mount both, or the catalog injection returns.
26
- */
27
-
28
- /** Cordis plugin name used by loader diagnostics. */
29
- export const name = 'skill-search'
30
-
31
- /** The agent, tools, and skills services must exist before these tools can register. */
32
- export const inject = ['agents', 'tools', 'skills']
33
-
34
- const MAX_RESULTS = 20
35
-
36
- /** Minimal JSON schema compiler for tool parameters (zero dependencies). */
37
- function toJsonSchema(spec) {
38
- const properties = {}
39
- const required = []
40
- for (const [key, meta] of Object.entries(spec || {})) {
41
- const prop = { type: meta.type }
42
- if (meta.description) prop.description = meta.description
43
- properties[key] = prop
44
- if (meta.required) required.push(key)
45
- }
46
- return { type: 'object', properties, required, additionalProperties: false }
47
- }
48
-
49
- /** Register the two on-demand skill tools. */
50
- export function apply(ctx) {
51
- /** Normalize a query into lowercase tokens for simple substring matching. */
52
- const tokens = (text) => (text || '').toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean)
53
-
54
- ctx.tools.register({
55
- name: 'skill_search',
56
- description: 'Search the available skills by keyword and return matching skill names with short descriptions. This session keeps NO skill catalog in the prompt — if a task looks like it matches a skill (document conversion, image processing, game reviews, markdown, PDF, spreadsheets, …), call skill_search FIRST to find it, then skill_load to activate it. Do NOT assume skill names from memory.',
57
- parameters: toJsonSchema({
58
- query: { type: 'string', required: true, description: 'search keywords (e.g. "pdf", "obsidian", "game review")' },
59
- }),
60
- output: {
61
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
62
- render: (_a, v) => [{ type: 'text', text: v.text }],
63
- },
64
- async execute(args, exec) {
65
- const wanted = tokens(args.query)
66
- const scope = exec?.agent ?? ctx
67
- try {
68
- const all = await ctx.skills.list({
69
- scope,
70
- cwd: exec?.agent?.session?.header?.cwd,
71
- signal: exec?.signal,
72
- })
73
- const matches = all.filter((skill) => {
74
- if (wanted.length === 0) return true
75
- const haystack = tokens(`${skill.name} ${skill.description ?? ''} ${skill.whenToUse ?? ''}`).join(' ')
76
- return wanted.every((token) => haystack.includes(token))
77
- })
78
- const head = matches.slice(0, MAX_RESULTS)
79
- const lines = head.map((skill) => {
80
- const desc = (skill.description || '').split('\n')[0]
81
- return `- ${skill.name}: ${desc}`
82
- })
83
- if (lines.length === 0) return { text: `No skills match "${args.query}". Use skill_search with other keywords.` }
84
- const extra = matches.length > MAX_RESULTS ? `\n…(${matches.length - MAX_RESULTS} more)` : ''
85
- return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}\n\nLoad one with skill_load (exact name).` }
86
- } catch (error) {
87
- return { text: `skill_search unavailable: ${String((error && error.message) || error)}` }
88
- }
89
- },
90
- })
91
-
92
- ctx.tools.register({
93
- name: 'skill_load',
94
- description: 'Load the full instructions of ONE skill by its exact name (from skill_search results) and inject them for the next request. Call this before acting on a task that matches the skill.',
95
- parameters: toJsonSchema({
96
- name: { type: 'string', required: true, description: 'exact skill name (kebab-case, from skill_search)' },
97
- }),
98
- output: {
99
- schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
100
- render: (_a, v) => [{ type: 'text', text: v.text }],
101
- },
102
- async execute(args, exec) {
103
- try {
104
- const agent = exec?.agent
105
- if (agent === undefined) return { text: 'skill_load requires an agent context.' }
106
- const skill = await ctx.skills.get(args.name, {
107
- scope: agent,
108
- cwd: agent.session.header.cwd,
109
- signal: exec?.signal,
110
- })
111
- if (skill === undefined) {
112
- return { text: `No skill named "${args.name}". Run skill_search to list available skills.` }
113
- }
114
- const body = extractSkillBody(skill)
115
- if (body.length === 0) {
116
- return { text: `Skill "${args.name}" has no loadable body.` }
117
- }
118
- // Queue the skill content as a non-waking next-step context message,
119
- // exactly like dsh-tool-skill's invocation injection.
120
- agent.inject({
121
- id: `skill-load-${args.name}-${Date.now()}`,
122
- role: 'user',
123
- content: [{ type: 'text', text: body }],
124
- source: { kind: 'skill-invocation', name: args.name, form: 'instructions' },
125
- })
126
- return { text: `Skill "${args.name}" loaded; its instructions will be injected for the next request.` }
127
- } catch (error) {
128
- return { text: `skill_load failed: ${String((error && error.message) || error)}` }
129
- }
130
- },
131
- })
132
- }
133
-
134
- /** Extract the model-facing body of a loaded skill definition. */
135
- function extractSkillBody(skill) {
136
- const content = skill?.content ?? skill?.instructions ?? skill?.body
137
- if (typeof content === 'string') return content
138
- if (Array.isArray(content)) {
139
- return content.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join('\n')
140
- }
141
- return ''
142
- }
@@ -1,301 +0,0 @@
1
- /**
2
- * Anchored tool bootstrap — keep the FIRST model request on the Minimal
3
- * preset's REAL tool schema (persistent `bash` + `str_replace_editor`), then
4
- * narrow the catalog to a minimal RESIDENT set once the session has produced
5
- * its first durable promotion signal. Injected-context control lives in the
6
- * companion `context-gate` plugin, not here.
7
- *
8
- * The phase is derived from durable session events, so resume and reload
9
- * preserve it. By default (`promoteOn: 'either'`) a session promotes after the
10
- * first `tool/call` OR the first `assistant/message`, whichever comes first:
11
- * request #1 always sees the bootstrap catalog and request #2 always sees the
12
- * resident catalog. The original `'tool-call'` mode is kept for compatibility,
13
- * but it can trap a session in bootstrap forever when the first model reply
14
- * makes no tool call — the `'either'` default removes that trap while keeping
15
- * the first-request anchor intact.
16
- *
17
- * First-request conditions established by the reproduction work (issues #6
18
- * and #11, 2026-08-15):
19
- *
20
- * 1. Tool schema. The API-visible first-request catalog decides whether the
21
- * session anchors on the Minimal trajectory. At the adapter-default
22
- * maxTokens (256000 on the official endpoint) the Minimal tool pair —
23
- * persistent `bash` + `str_replace_editor` — anchored 5/5 runs with zero
24
- * `let me` first-lines, while every standard-family schema (pwsh/read,
25
- * pwsh only, sandboxed bash/read) fell into standard-like behavior
26
- * (11/11). Bootstrap therefore exposes exactly the Minimal pair, not
27
- * Standard's `pwsh`/`read`.
28
- *
29
- * 2. Output budget. On the official endpoint the first request's `max_tokens`
30
- * also dominated the trajectory anchor at 1024 (`We need` style in 26/32
31
- * runs against 0/5 at 256000, independent of tool descriptions). The
32
- * Minimal tool schema, however, anchors at 256000 WITHOUT any cap, and the
33
- * cap's delivery depends on the profile package's `prepareCall` behavior
34
- * (it reaches the request on the 0.1.0-rc.5 source checkout; a prebuilt
35
- * rc.6-reporting profile package observed in issue #11 overwrote it with
36
- * `adapterDefaults.maxTokens`). `bootstrapMaxTokens` is therefore OPT-IN:
37
- * leave it unset to run the Minimal schema at the adapter default, or set
38
- * it to cap the first request. When set, the cap is stripped after
39
- * promotion — the next request's seed proposal carries the previous
40
- * header's maxTokens forward, so the release must be explicit.
41
- *
42
- * 3. Injected context is NOT this plugin's concern: the companion
43
- * `context-gate` plugin (shared/context-gate.mjs, mounted as the FIRST
44
- * row) owns the unified injection control — runtime-context suppression
45
- * on the assembly path and a claimed-baseline deny on the pre-step
46
- * waterfall, both keyed to the same epoch-aware promotion phase. Mount it
47
- * separately for context control alone; this file narrows only the tool
48
- * catalog (plus the optional output cap below).
49
- *
50
- * SUBAGENTS: by default subagents (delegationDepth > 0) are always promoted
51
- * (resident catalog from their first request). `includeSubagents: true`
52
- * makes them follow the same bootstrap phase — their first request also sees
53
- * the bootstrap pair, and their own first reply or tool call promotes them.
54
- * Keep this flag in sync with the context-gate row's flag.
55
- *
56
- * POST-PROMOTION RESIDENT SET (local addition, user-measured): the promoted
57
- * phase does NOT dump the whole Standard catalog at once — that dump pulls
58
- * the trajectory back to standard-like behavior (the root cause of the
59
- * post-promotion regression measured on the zero variant). Instead the
60
- * catalog narrows to the bootstrap tool pair PLUS the three discovery tools
61
- * (`dev_tool_search`, `skill_search`, `skill_load`) plus whatever the model
62
- * explicitly unlocked via `dev_tool_search`. Heavier Standard tools
63
- * (web_search, subagent, workflow, …) are one `dev_tool_search` call away;
64
- * unlocked names are derived from durable `tool/call` events, so resume and
65
- * reload keep them. read/write/edit/glob/grep/todo/ask are deliberately NOT
66
- * resident: bash + str_replace_editor cover file work.
67
- *
68
- * COMPACTION (local addition): a compaction rewrites the whole surface, so the
69
- * first post-compaction request is a "second first request". Promotion is
70
- * epoch-aware (see compaction-epoch.mjs): after `compaction/end` the session
71
- * falls back to the controlled phase — the bootstrap pair plus
72
- * `compactionTools` (a core work set, default none) — until a NEW durable
73
- * promotion signal exists past that boundary. The model is mid-task and needs
74
- * to keep working, but still faces a small catalog instead of the full
75
- * Standard set.
76
- *
77
- * Robustness:
78
- * - Promotion decisions are memoized per session id for this process; the
79
- * durable event scan runs once per session per process, then O(1).
80
- * - Subagents (delegationDepth > 0) are always promoted (resident catalog)
81
- * unless `includeSubagents: true`.
82
- * - A missing bootstrap tool degrades to the full catalog with a one-time
83
- * warning instead of throwing, so a composition drift can never brick
84
- * every request of a session.
85
- * - Invalid config (bad tool lists, unknown `promoteOn`, malformed flags,
86
- * non-positive `bootstrapMaxTokens`) fails at apply time, i.e. at preset
87
- * mount, where it is visible and fixable.
88
- */
89
-
90
- import { createEpochPromotion } from './compaction-epoch.mjs'
91
-
92
- /** Cordis plugin name used by loader diagnostics. */
93
- export const name = 'anchored-tool-bootstrap'
94
-
95
- /**
96
- * Deliberately NO inject list: the listeners only touch services at event
97
- * time. Keep this row right AFTER the context-gate row in agent.cordis.yml:
98
- * waterfall after-next transforms apply in reverse registration order, so the
99
- * tool filter here must register before any plugin that touches the same
100
- * assembly. The optional budget listener registers with `prepend: true` so a
101
- * later listener can never override the first-round cap after we set it.
102
- */
103
- export const inject = []
104
-
105
- /** Durable session event types that count as a promotion signal per mode. */
106
- const PROMOTE_EVENTS = {
107
- 'tool-call': ['tool/call'],
108
- 'assistant-message': ['assistant/message'],
109
- either: ['tool/call', 'assistant/message'],
110
- }
111
-
112
- /** Every config key this plugin accepts — anything else is a typo. */
113
- const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'compactionTools', 'includeSubagents'])
114
-
115
- /** Validate an optional boolean flag with a default. */
116
- function booleanOption(value, field, fallback) {
117
- if (value === undefined) return fallback
118
- if (typeof value !== 'boolean') {
119
- throw new TypeError(`${name}: ${field} must be a boolean`)
120
- }
121
- return value
122
- }
123
-
124
- /**
125
- * The default first-request catalog: the OFFICIAL Minimal preset's exact tool
126
- * pair — the persistent `bash` shell and `str_replace_editor`. Issue #11
127
- * measured this schema anchoring 5/5 at the adapter-default maxTokens while
128
- * every standard-family schema failed 11/11.
129
- */
130
- const DEFAULT_BOOTSTRAP_TOOLS = ['bash', 'str_replace_editor']
131
-
132
- /** Discovery tools always resident after promotion (the tool-search pattern). */
133
- const RESIDENT_DISCOVERY_TOOLS = ['dev_tool_search', 'skill_search', 'skill_load']
134
-
135
- function stringList(value, field) {
136
- if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string' || item.length === 0)) {
137
- throw new TypeError(`${name}: ${field} must be a non-empty array of non-empty strings`)
138
- }
139
- return [...new Set(value)]
140
- }
141
-
142
- function stringListOrEmpty(value, field) {
143
- if (value === undefined) return []
144
- return stringList(value, field)
145
- }
146
-
147
- function parsePromoteOn(value) {
148
- if (value === undefined || value === 'either') return PROMOTE_EVENTS.either
149
- if (value === 'tool-call' || value === 'assistant-message') return PROMOTE_EVENTS[value]
150
- throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
151
- }
152
-
153
- /**
154
- * Validate the optional first-request output cap. `undefined` means NO cap:
155
- * the Minimal tool schema anchors at the adapter-default maxTokens, and the
156
- * cap's delivery is profile-package dependent (see the header note), so it is
157
- * opt-in rather than the default.
158
- */
159
- function optionalPositiveInt(value, field) {
160
- if (value === undefined) return undefined
161
- if (!Number.isSafeInteger(value) || value <= 0) {
162
- throw new TypeError(`${name}: ${field} must be a positive safe integer`)
163
- }
164
- return value
165
- }
166
-
167
- /** Register the per-session bootstrap filters. */
168
- export function apply(ctx, config) {
169
- const source = config === undefined ? {} : config
170
- if (typeof source !== 'object' || source === null || Array.isArray(source)) {
171
- throw new TypeError(`${name}: config must be an object`)
172
- }
173
- const unknown = Object.keys(source).filter((key) => !ALLOWED_KEYS.has(key))
174
- if (unknown.length > 0) {
175
- throw new TypeError(
176
- `${name}: unknown config key(s) ${unknown.join(', ')} — allowed keys: ${[...ALLOWED_KEYS].sort().join(', ')}`,
177
- )
178
- }
179
- const bootstrapTools = stringList(source.bootstrapTools, 'bootstrapTools')
180
- const promoteEvents = parsePromoteOn(source.promoteOn)
181
- const bootstrapMaxTokens = optionalPositiveInt(source.bootstrapMaxTokens, 'bootstrapMaxTokens')
182
- const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)
183
- // Core work set exposed after a compaction, before re-promotion. Empty
184
- // means "no compaction recovery catalog": the session stays on the
185
- // bootstrap pair until a new promotion signal.
186
- const compactionTools = stringListOrEmpty(source.compactionTools, 'compactionTools')
187
-
188
- const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
189
- ctx.on('session/event', (session, event) => promotion.observe(session, event))
190
-
191
- let warned = false
192
- const warnOnce = (message) => {
193
- if (warned) return
194
- warned = true
195
- try {
196
- ctx.logger.warn(message)
197
- } catch {
198
- // Logger unavailable — the guard exists only to avoid spamming.
199
- }
200
- }
201
-
202
- /**
203
- * Tool names the model explicitly unlocked via `dev_tool_search` for one
204
- * session. Derived from durable `tool/call` events so resume/reload keeps
205
- * them. The event's `arguments` is the raw JSON string the model produced;
206
- * we parse it defensively and read the `toolNames` array.
207
- */
208
- const unlockedFor = (session) => {
209
- const unlocked = new Set()
210
- if (session === undefined || !Array.isArray(session.events)) return unlocked
211
- for (const event of session.events) {
212
- if (event.type !== 'tool/call') continue
213
- if (event.data?.name !== 'dev_tool_search') continue
214
- let args
215
- try {
216
- args = JSON.parse(event.data.arguments)
217
- } catch {
218
- continue
219
- }
220
- if (args === null || typeof args !== 'object' || Array.isArray(args)) continue
221
- const names = args.toolNames
222
- if (Array.isArray(names)) for (const name of names) if (typeof name === 'string' && name.length > 0) unlocked.add(name)
223
- }
224
- return unlocked
225
- }
226
-
227
- /** Narrow the assembled catalog to a keep-set; validate required names. */
228
- const keepTools = (assembled, keep, missingAllowsFullCatalog) => {
229
- const available = new Set(assembled.tools.map((tool) => tool.name))
230
- const missing = [...keep].filter((toolName) => !available.has(toolName))
231
- if (missing.length > 0) {
232
- warnOnce(
233
- `${name}: expected every phase tool; missing=${JSON.stringify(missing)} — `
234
- + (missingAllowsFullCatalog ? 'bootstrap disabled, full catalog exposed' : 'continuing with what is available'),
235
- )
236
- if (missingAllowsFullCatalog) return assembled
237
- }
238
- return {
239
- ...assembled,
240
- tools: assembled.tools.filter((tool) => keep.has(tool.name)),
241
- }
242
- }
243
-
244
- ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
245
- // Downstream errors propagate untouched; only this filter's own logic is guarded.
246
- const assembled = await next()
247
- try {
248
- const status = promotion.status(context.agent)
249
- if (status.promoted) {
250
- // PROMOTED: keep the minimal resident set — the bootstrap pair + the
251
- // discovery tools + whatever the model explicitly unlocked via
252
- // dev_tool_search — instead of dumping the whole Standard catalog at
253
- // once (the post-promotion regression fix; see the header note).
254
- const keep = new Set([...bootstrapTools, ...RESIDENT_DISCOVERY_TOOLS, ...unlockedFor(context.agent?.session)])
255
- return keepTools(assembled, keep, false)
256
- }
257
- // Controlled phase: the bootstrap pair; after a compaction, plus the
258
- // compaction work set so mid-task work can continue. Context control is
259
- // NOT here: the companion `context-gate` plugin owns it (see the header
260
- // note), so this filter touches only the tool catalog.
261
- const { boundary } = status
262
- const keep = new Set(bootstrapTools)
263
- if (boundary >= 0) for (const toolName of compactionTools) keep.add(toolName)
264
- return keepTools(assembled, keep, true)
265
- } catch (error) {
266
- // A filter bug must never brick a session: degrade to the full catalog.
267
- warnOnce(`${name}: bootstrap filter failed, exposing the full catalog: ${String((error && error.message) || error)}`)
268
- return assembled
269
- }
270
- })
271
-
272
- // Optionally cap the first model request's output budget while bootstrapping.
273
- // Unset (`bootstrapMaxTokens` omitted) means the adapter default flows — the
274
- // Minimal tool schema anchors at 256000 without a cap (issue #11).
275
- if (bootstrapMaxTokens !== undefined) {
276
- // Same registration discipline as the pre-step strip below: `prepend`
277
- // keeps this listener the OUTERMOST transform of the agent/request
278
- // waterfall for the same registration-order reasons (loader row
279
- // application is concurrent; row order alone does not decide listener
280
- // order — see issue #6 and upstream PR #13), so a later listener can
281
- // never override the first-round budget after we set it.
282
- ctx.on('agent/request', async (payload, next) => {
283
- const resolved = await next()
284
- const agent = payload.agent
285
- if (promotion.status(agent).promoted) {
286
- // The next request's seed proposal carries the previous header's
287
- // maxTokens forward, so the injected cap must be stripped explicitly —
288
- // otherwise it would persist for the whole session.
289
- if (resolved.maxTokens === bootstrapMaxTokens) {
290
- const { maxTokens: _bootstrap, ...rest } = resolved
291
- return rest
292
- }
293
- return resolved
294
- }
295
- return {
296
- ...resolved,
297
- maxTokens: bootstrapMaxTokens,
298
- }
299
- }, { prepend: true })
300
- }
301
- }
File without changes