dsh-plugin-prompt-tool 2.1.0 → 3.0.1

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 (206) hide show
  1. package/README.md +100 -233
  2. package/engine/actions/append-context.mjs +3 -24
  3. package/engine/actions/assembly.mjs +70 -94
  4. package/engine/actions/catalog.mjs +65 -28
  5. package/engine/actions/content.mjs +8 -152
  6. package/engine/actions/examples.mjs +8 -4
  7. package/engine/actions/inbox-prepend.mjs +4 -5
  8. package/engine/actions/index.mjs +17 -9
  9. package/engine/actions/inject-text.mjs +24 -4
  10. package/engine/actions/pre-step-filter.mjs +3 -4
  11. package/engine/actions/registration.mjs +20 -28
  12. package/engine/actions/request-params.mjs +21 -1
  13. package/engine/actions/shared.mjs +22 -13
  14. package/engine/actions/stream-output.mjs +26 -0
  15. package/engine/actions/tool-gate.mjs +31 -0
  16. package/engine/actions/tool-result.mjs +36 -0
  17. package/engine/actions/tool-visibility.mjs +248 -0
  18. package/engine/agent-capability.mjs +67 -0
  19. package/engine/anchor-match.mjs +14 -21
  20. package/engine/audience.d.mts +2 -0
  21. package/engine/audience.mjs +102 -0
  22. package/engine/branch.d.mts +4 -0
  23. package/engine/branch.mjs +61 -18
  24. package/engine/compaction-epoch.d.mts +25 -0
  25. package/engine/compaction-epoch.mjs +27 -4
  26. package/engine/compositions/source/local/filesystem-editor.yml +3 -3
  27. package/engine/compositions/source/local/instruction-hint.yml +2 -4
  28. package/engine/compositions/source/local/module-file-tools.yml +2 -0
  29. package/engine/compositions/source/local/module-import-tools.yml +4 -0
  30. package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
  31. package/engine/compositions/source/local/module-rule-tools.yml +5 -0
  32. package/engine/compositions/source/local/module-variable-tools.yml +5 -0
  33. package/engine/compositions/source/local/present-as.yml +26 -0
  34. package/engine/compositions/source/local/tool-config-engine.yml +2 -3
  35. package/engine/compositions/source/local/tool-search.yml +11 -0
  36. package/engine/condition-facts.mjs +84 -0
  37. package/engine/conditions/anchor.mjs +63 -23
  38. package/engine/conditions/availability.mjs +1 -1
  39. package/engine/conditions/composite.mjs +3 -3
  40. package/engine/conditions/count.mjs +42 -11
  41. package/engine/conditions/evaluation.mjs +74 -19
  42. package/engine/conditions/examples.mjs +2 -1
  43. package/engine/conditions/index.d.mts +15 -0
  44. package/engine/conditions/index.mjs +23 -9
  45. package/engine/conditions/operators.mjs +4 -0
  46. package/engine/conditions/phase.mjs +9 -3
  47. package/engine/conditions/preset.mjs +1 -1
  48. package/engine/conditions/session.mjs +8 -2
  49. package/engine/conditions/subject.mjs +144 -65
  50. package/engine/conditions/text.mjs +26 -8
  51. package/engine/conditions/variable.mjs +83 -0
  52. package/engine/context-activation.d.mts +11 -0
  53. package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
  54. package/engine/delivery.d.mts +2 -0
  55. package/engine/delivery.mjs +93 -0
  56. package/engine/discovery.d.mts +38 -0
  57. package/engine/discovery.mjs +146 -0
  58. package/engine/env-safety.mjs +14 -0
  59. package/engine/executor.d.mts +28 -0
  60. package/engine/executor.mjs +290 -205
  61. package/engine/fillers.mjs +39 -8
  62. package/engine/fixed-registration.mjs +12 -0
  63. package/engine/history.mjs +71 -0
  64. package/engine/instruction-blocks.d.mts +13 -0
  65. package/engine/instruction-blocks.mjs +21 -0
  66. package/engine/instruction-files.d.mts +5 -0
  67. package/engine/instruction-files.mjs +97 -0
  68. package/engine/instruction-hint.mjs +69 -103
  69. package/engine/interpolate.d.mts +7 -0
  70. package/engine/interpolate.mjs +44 -13
  71. package/engine/layers.mjs +163 -227
  72. package/engine/machine-paths.mjs +40 -0
  73. package/engine/module-file-tools.mjs +8 -0
  74. package/engine/module-import-tools.mjs +10 -0
  75. package/engine/module-manifest-tools.mjs +8 -0
  76. package/engine/module-rule-tools.mjs +10 -0
  77. package/engine/module-variable-tools.mjs +10 -0
  78. package/engine/order.d.mts +14 -0
  79. package/engine/order.mjs +0 -3
  80. package/engine/path-safety.d.mts +2 -0
  81. package/engine/path-safety.mjs +58 -0
  82. package/engine/pre-step-source.d.mts +69 -0
  83. package/engine/pre-step-source.mjs +103 -0
  84. package/engine/present-as.mjs +77 -0
  85. package/engine/promotion-pool.d.mts +9 -0
  86. package/engine/promotion-pool.mjs +69 -0
  87. package/engine/record.mjs +2 -0
  88. package/engine/rule-engine.mjs +3 -2
  89. package/engine/rule-facts.d.mts +11 -0
  90. package/engine/rule-facts.mjs +60 -0
  91. package/engine/rule-runtime.d.mts +46 -0
  92. package/engine/rule-runtime.mjs +98 -13
  93. package/engine/rule-spec.d.mts +106 -0
  94. package/engine/rule-spec.mjs +301 -27
  95. package/engine/run-code-env.mjs +73 -19
  96. package/engine/schema.mjs +165 -243
  97. package/engine/sdk-strip.mjs +2 -2
  98. package/engine/service-names.d.mts +10 -0
  99. package/engine/service-names.mjs +10 -0
  100. package/engine/shared.d.mts +21 -0
  101. package/engine/shared.mjs +46 -13
  102. package/engine/skill-search.mjs +125 -53
  103. package/engine/source-kind.d.mts +9 -0
  104. package/engine/source-kind.mjs +16 -0
  105. package/engine/strategy-ids.mjs +5 -0
  106. package/engine/template-evaluation.d.mts +16 -0
  107. package/engine/template-evaluation.mjs +178 -0
  108. package/engine/template-frame.d.mts +16 -0
  109. package/engine/template-frame.mjs +69 -0
  110. package/engine/tool-config-engine.mjs +38 -34
  111. package/engine/tool-definition.d.mts +4 -0
  112. package/engine/tool-definition.mjs +5 -1
  113. package/engine/tool-git-bash.mjs +5 -1
  114. package/engine/tool-scope.mjs +22 -0
  115. package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
  116. package/engine/trigger.mjs +15 -270
  117. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  118. package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
  119. package/engine/vendor/yaml/package.json +1 -1
  120. package/lib/client.js +8312 -6724
  121. package/lib/client.js.map +1 -1
  122. package/lib/index.d.mts +16 -2120
  123. package/lib/index.mjs +26914 -5816
  124. package/modules/ponytail/module.yml +83 -203
  125. package/modules/ptc-mode/module.yml +45 -0
  126. package/modules/soul/assets/core-hard.md +6 -0
  127. package/modules/soul/assets/criteria.md +12 -0
  128. package/modules/soul/assets/echo.md +12 -0
  129. package/modules/soul/assets/layers.md +11 -0
  130. package/modules/soul/assets/ledger.md +9 -0
  131. package/modules/soul/assets/manifest.yml +20 -0
  132. package/modules/soul/assets/reference/00-layers.md +27 -0
  133. package/modules/soul/assets/reference/01-criteria.md +32 -0
  134. package/modules/soul/assets/reference/02-echo.md +161 -0
  135. package/modules/soul/assets/reference/03-vital.md +183 -0
  136. package/modules/soul/assets/reference/04-manifest.json +106 -0
  137. package/modules/soul/assets/reference/05-doctor.md +67 -0
  138. package/modules/soul/assets/reference/06-reference.md +189 -0
  139. package/modules/soul/assets/reference/07-bundle.json +431 -0
  140. package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
  141. package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
  142. package/modules/soul/assets/reference/10-echo-anatomy.md +109 -0
  143. package/modules/soul/assets/reference/11-self.md +80 -0
  144. package/modules/soul/assets/reference/12-ledger.md +159 -0
  145. package/modules/soul/assets/reference/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -0
  146. package/modules/soul/assets/reference/ECHO/345/205/255/345/261/202/344/270/216mnemon/345/215/217/345/220/214/350/257/264/346/230/216/344/271/246.md +229 -0
  147. package/modules/soul/assets/reference/README.md +12 -0
  148. package/modules/soul/assets/templates/agents-template.md +6 -0
  149. package/modules/soul/assets/vital.md +9 -0
  150. package/modules/soul/module.yml +311 -0
  151. package/modules/superpowers/module.yml +122 -0
  152. package/modules/tool-surface/module.yml +51 -47
  153. package/package.json +37 -40
  154. package/skills/dsh-module/SKILL.md +67 -147
  155. package/skills/dsh-module/reference.md +113 -86
  156. package/templates/10-pre-step.yml +2 -3
  157. package/templates/14-first-turn-anchor.yml +10 -14
  158. package/templates/15-guide-auto.yml +32 -28
  159. package/templates/16-custom-fallback.yml +33 -21
  160. package/templates/20-system-section.yml +1 -4
  161. package/templates/50-llm-stream.yml +4 -10
  162. package/templates/60-tool-pipeline.yml +12 -10
  163. package/templates/63-append-context.yml +4 -6
  164. package/templates/65-turn-stop.yml +2 -2
  165. package/templates/67-inbox-prepend.yml +2 -2
  166. package/templates/70-subagent-maintenance.yml +9 -10
  167. package/templates/75-subagent-end.yml +2 -2
  168. package/templates/80-tool-surface.yml +41 -57
  169. package/templates/tools/40-delegate-module-import.yml +22 -0
  170. package/templates/tools/50-delegate-module-rules.yml +14 -0
  171. package/templates/tools/60-delegate-module-variables.yml +17 -0
  172. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  173. package/engine/actions/decision.mjs +0 -54
  174. package/engine/actions/guard.mjs +0 -80
  175. package/engine/actions/sdk-strip.mjs +0 -30
  176. package/engine/character-tools.mjs +0 -8
  177. package/engine/classify-task.mjs +0 -53
  178. package/engine/compositions/source/local/character-tools.yml +0 -5
  179. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  180. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  181. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  182. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  183. package/engine/condition.mjs +0 -96
  184. package/engine/declared-triggers.mjs +0 -93
  185. package/engine/session-var-tools.mjs +0 -8
  186. package/engine/session-vars.mjs +0 -54
  187. package/engine/st-macros.d.mts +0 -22
  188. package/engine/st-macros.mjs +0 -208
  189. package/engine/st-render.mjs +0 -109
  190. package/engine/st-world-book.d.mts +0 -10
  191. package/engine/subagent-tool-policy-core.mjs +0 -400
  192. package/engine/subagent-tool-policy.mjs +0 -352
  193. package/engine/trigger-spec.mjs +0 -173
  194. package/engine/world-book-tools.mjs +0 -8
  195. package/lib/migrate-rules.d.mts +0 -1
  196. package/lib/migrate-rules.mjs +0 -610
  197. package/lib/module-package-DqJZaE71.mjs +0 -2
  198. package/lib/module-package-q2gDRu9H.mjs +0 -2869
  199. package/lib/rule-builder-DMrJten6.mjs +0 -14767
  200. package/templates/61-deny-tools-presentation.yml +0 -27
  201. package/templates/62-guard-deny-tools.yml +0 -20
  202. package/templates/policies/legacy-defaults.yml +0 -34
  203. package/templates/tools/40-delegate-character.yml +0 -15
  204. package/templates/tools/50-delegate-worldbook.yml +0 -15
  205. package/templates/tools/60-delegate-session-var.yml +0 -15
  206. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -4,7 +4,10 @@
4
4
  */
5
5
 
6
6
  import { getService, parseToolNames } from './shared.mjs'
7
+ import { machinePaths } from './machine-paths.mjs'
8
+ import { SAFE_ENV_KEYS } from './env-safety.mjs'
7
9
  import { createInstructionHintResolver } from './instruction-hint.mjs'
10
+ import { DISCOVERY_SERVICE } from './service-names.mjs'
8
11
 
9
12
  const name = 'prompt-config-engine'
10
13
 
@@ -13,30 +16,57 @@ function explicitTemplate(config) {
13
16
  return texts.length > 0 ? texts.join('\n\n') : typeof config.params?.text === 'string' ? config.params.text : ''
14
17
  }
15
18
 
19
+ /** 只读同规则工具面声明;文案、条件和投递仍归普通注入动作。 */
20
+ function createToolCatalogResolver(config) {
21
+ if (config.params?.templateEvaluation === true) throw new TypeError('tool-catalog is read-only and does not support templateEvaluation')
22
+ if (typeof config.params?.action !== 'string' || config.params.action.trim().length === 0) {
23
+ throw new TypeError('tool-catalog requires params.action referencing a tool-visibility action')
24
+ }
25
+ const text = explicitTemplate(config)
26
+ return ({ ctx, agent }) => {
27
+ if (text.trim().length === 0) return null
28
+ const source = config.toolCatalogSource
29
+ if (source === undefined) throw new TypeError('tool-catalog requires a same-rule tool-visibility action')
30
+ const deny = new Set(source.deny ?? [])
31
+ const resident = [...new Set(source.allow)].filter(tool => !deny.has(tool))
32
+ const adapter = source.discovery === undefined ? undefined
33
+ : getService(ctx, DISCOVERY_SERVICE)?.get?.(agent, source.discovery.kind)
34
+ if (source.discovery !== undefined && typeof adapter?.tool !== 'string') {
35
+ throw new Error(`tool-catalog discovery adapter ${source.discovery.kind} is unavailable`)
36
+ }
37
+ return { text, variables: { RESIDENT_TOOLS: resident.join(', '), DISCOVERY_TOOL: adapter?.tool ?? '' } }
38
+ }
39
+ }
40
+
16
41
  /**
17
42
  * env-facts:机器事实动态填充器。
18
43
  * params.envKeys 逗号分隔环境变量白名单:取值归模板/预设,留空 = 不附加宿主环境
19
- * 变量(CWD/WORKSPACE 仍由会话事实提供)。
44
+ * 变量(CWD/WORKSPACE 仍由会话事实提供)。白名单外的键在构造期拒绝,不读取任意 process.env。
20
45
  * 只提供 facts 变量;正文必须由 text/params.text 显式模板声明。
21
46
  */
22
47
  function createEnvFactsResolver(config) {
23
48
  const template = explicitTemplate(config)
24
- if (template.trim().length === 0) return () => null
25
49
  const keys = parseToolNames(typeof config.params?.envKeys === 'string' ? config.params.envKeys : '')
50
+ const rejected = keys.filter((key) => !SAFE_ENV_KEYS.has(key))
51
+ if (rejected.length > 0) {
52
+ throw new TypeError(`${name}: env-facts envKeys not in SAFE_ENV_KEYS: ${rejected.join(', ')} — safe keys: ${[...SAFE_ENV_KEYS].join(', ')}`)
53
+ }
54
+ if (template.trim().length === 0) return () => null
26
55
  return ({ agent }) => {
27
56
  const session = agent?.session
28
- const cwd = session?.header?.cwd ?? process.cwd()
29
- const builtinHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : '')
57
+ // 路径类事实取唯一来源;白名单(envKeys)与「空串不算有值」是本处的策略,不外包。
58
+ const paths = machinePaths(session)
30
59
  const facts = {}
31
60
  for (const key of keys) {
32
61
  let value
33
- if (key === 'CWD' || key === 'WORKSPACE') value = cwd
34
- else if (key === 'DSH_HOME') value = builtinHome
62
+ if (key === 'CWD') value = paths.CWD
63
+ else if (key === 'WORKSPACE') value = paths.WORKSPACE
64
+ else if (key === 'DSH_HOME') value = paths.DSH_HOME
35
65
  else value = process.env[key]
36
66
  if (typeof value === 'string' && value.length > 0) facts[key] = value
37
67
  }
38
- facts.WORKSPACE = process.env.DSH_WORKSPACE ?? cwd
39
- facts.CWD = cwd
68
+ facts.WORKSPACE = paths.WORKSPACE
69
+ facts.CWD = paths.CWD
40
70
  const envFacts = Object.entries(facts).map(([key, value]) => `- ${key}=${value}`).join('\n')
41
71
  return { text: template, variables: { ...facts, ENV_FACTS: envFacts } }
42
72
  }
@@ -138,6 +168,7 @@ export const FILLERS = {
138
168
  'instruction-hint': (config) => createInstructionHintResolver(config),
139
169
  'env-facts': (config) => createEnvFactsResolver(config),
140
170
  'skill-catalog': (config) => createSkillCatalogResolver(config),
171
+ 'tool-catalog': (config) => createToolCatalogResolver(config),
141
172
  }
142
173
 
143
174
  export function createPlaceholderResolver(config) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 「固定注册效果」判据的唯一实现:只有 `system-section` 层的 `complete` /
3
+ * `suppressRuntimeContext` 是注册期效果(无逐轮求值时机)。其他层写同名键是死配置。
4
+ * `layer` 是声明缺省层——config 可能省略 layer,回退规则与
5
+ * `injectionConfigSpec` 的 `config.layer ?? rule.layer ?? 'pre-step'` 一致。
6
+ */
7
+ import { record } from './record.mjs'
8
+
9
+ export function isFixedRegistration(config, layer) {
10
+ return record(config) && (config.layer ?? layer) === 'system-section'
11
+ && (config.params?.complete === true || config.params?.suppressRuntimeContext === true)
12
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * history — 会话历史的**唯一**读取入口,给出两个互不替代的视图。
3
+ *
4
+ * 1. `currentEvents()`:**当前模型可见上下文**,即 surface 有序节点(`session.surface.nodes`)指向的事件。
5
+ * 压缩 / 位置替换遮蔽掉的节点不在其中,这正是「当前上下文」与「本会话曾经发生过」的差别。
6
+ * 2. `historyEvents()`:**完整历史**,按 log 序返回本会话全部 durable 事件,供「必须看全量」的消费点。
7
+ *
8
+ * 两个视图都是纯函数:每次都读当场快照,不缓存、不跨会话保存任何状态。
9
+ */
10
+
11
+ /**
12
+ * 完整历史:`snapshotEvents()` 的全量不可变快照。
13
+ * 宿主缺该接口(或返回非数组)时按空日志降级,不读取旧 `events` 数组。
14
+ */
15
+ export function historyEvents(session) {
16
+ const snapshot = session?.snapshotEvents?.()
17
+ return Array.isArray(snapshot) ? snapshot : []
18
+ }
19
+
20
+ /**
21
+ * 一次工具实际调用的 durable 事件:顶层 `tool/call`,以及 PTC `run_code` 内的
22
+ * `tool/ptc-dispatch`。只观察「执行了哪个工具」时读两者;观察「模型动作 / 相位晋升」
23
+ * 仍只看顶层 `tool/call`,不要把本集合直接替换过去。
24
+ */
25
+ export const TOOL_INVOCATION_EVENT_TYPES = Object.freeze(['tool/call', 'tool/ptc-dispatch'])
26
+
27
+ /**
28
+ * 当前上下文是否**真的**受限:surface 在场(`nodes` 是数组)且每个节点都指向日志里的事件。
29
+ * 判据只有这一处 —— `currentEvents()` 的降级与消费点的视图口径都由它回答,避免两边静默分叉。
30
+ * 空 `nodes` 是受限(可见上下文为空),不是降级。
31
+ * `events` 是调用方已经取好的 `historyEvents(session)`(可选,省掉一次快照读)。
32
+ */
33
+ export function viewRestricted(session, events = historyEvents(session)) {
34
+ const nodes = session?.surface?.nodes
35
+ return Array.isArray(nodes) && nodes.every(seq => events[seq] !== undefined)
36
+ }
37
+
38
+ /**
39
+ * 当前模型可见上下文:按 surface 节点序取出对应事件。
40
+ *
41
+ * `surface.nodes` 存的是 log seq,而 seq 就是 log 下标(`snapshotEvents()` 返回 [0, seq) 的切片),
42
+ * 因此一次快照加索引即可映射,无需折叠、无需已 deprecated 的 `eventAt()`。
43
+ *
44
+ * 降级:surface 缺失、`nodes` 非数组,或任何节点越界(seq >= 日志长度)时退回完整历史 ——
45
+ * 「宁可多看见,不少看见」,且等价于迁移前的旧行为。空 `nodes` 是合法状态(新会话尚无消息),
46
+ * 按「当前上下文为空」返回空数组,**不**降级。
47
+ * shortcut: 与 `deriveMessages()` 的差别是这里不跳过派生出 null 的空内容节点(判据读事件而非正文);
48
+ * 需要正文的消费点出现时再按 `deriveMessages()` 收窄。
49
+ * 同一 seq 可在 `nodes` 中出现多次(位置替换后同一事件占多个位置),按条数计数的消费点需自行按 seq 去重。
50
+ */
51
+ export function currentEvents(session) {
52
+ const events = historyEvents(session)
53
+ if (!viewRestricted(session, events)) return events
54
+ return session.surface.nodes.map(seq => events[seq])
55
+ }
56
+
57
+ /**
58
+ * surface 只承载**消息类**事件——`system/message`、`developer/message`、`user/message`、
59
+ * `assistant/message`、`tool/result`(真值源:宿主 `packages/core/session/src/surface.ts` 的
60
+ * `SURFACE_EVENT_TYPES`)。非该集合的事件(`tool/call`、`turn/start`、`compaction/end`)永远不是
61
+ * surface 节点,按 `currentEvents()` 读它们只会读到空 —— 它们只能按完整历史读。
62
+ * shortcut: 宿主扩大 `SURFACE_EVENT_TYPES` 时这里要同步,否则新的消息类事件会被当成 log-only
63
+ * (count 多计、session 恒不命中)。
64
+ */
65
+ export const SURFACE_MESSAGE_TYPES = new Set([
66
+ 'system/message',
67
+ 'developer/message',
68
+ 'user/message',
69
+ 'assistant/message',
70
+ 'tool/result',
71
+ ])
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `instruction-blocks.mjs` 的手写声明:只声明 `src/` 消费的导出,形状按实现写实。
3
+ */
4
+
5
+ /** 一个官方指令标题块;`index` 是标题行首在正文中的位置。 */
6
+ export interface InstructionBlock {
7
+ action: 'set' | 'replace' | 'remove'
8
+ path: string
9
+ index: number
10
+ }
11
+
12
+ /** 解析系统提醒正文里的 4 种官方指令标题。 */
13
+ export declare function instructionBlocks(text: string): InstructionBlock[]
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 官方系统提醒正文里的指令文件标题:filter 分段与 hint 路径提取共用的唯一解析。
3
+ * 只认这 4 种行首标题;path 原样返回(调用方各自 trim/匹配),index 是标题行首位置。
4
+ */
5
+ const ACTIONS = {
6
+ 'Instructions from:': 'set',
7
+ 'Additional instructions from:': 'set',
8
+ 'Updated instructions from:': 'replace',
9
+ 'Instructions removed:': 'remove',
10
+ }
11
+
12
+ const HEADING_RE = new RegExp('^(' + Object.keys(ACTIONS).join('|') + ') ([^\r\n]+)\r?$', 'gm')
13
+
14
+ export function instructionBlocks(text) {
15
+ if (typeof text !== 'string' || text.length === 0) return []
16
+ return [...text.matchAll(HEADING_RE)].map(match => ({
17
+ action: ACTIONS[match[1]],
18
+ path: match[2],
19
+ index: match.index,
20
+ }))
21
+ }
@@ -0,0 +1,5 @@
1
+ /** 宿主依赖面:只声明 src/ 用到的符号,引擎全貌不在此(见 docs/engine-reuse.md 的类型边界)。 */
2
+ export declare const PROJECT_INSTRUCTION_CANDIDATES: readonly string[]
3
+ export declare const USER_GLOBAL_INSTRUCTION_FILE: string
4
+ export declare function ancestorDirs(cwd: string): string[]
5
+ export declare function instructionPlan(cwd: string, markerDirs: ReadonlySet<string>): { root: string; dirs: string[] }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * instruction-files — 指令文件探测的判据。
3
+ *
4
+ * 探测判据(候选清单、项目根、目录链)是纯函数;读盘由两个消费者各自完成——
5
+ * 引擎用本文件提供的异步执行器(collectInstructionFiles),宿主用 node:fs 同步 +
6
+ * 信任根校验。本文件随 engine/ 一起复制;行 instruction-hint 只消费这里的执行器。
7
+ */
8
+ import { dirname, join } from 'node:path'
9
+ import { machinePaths } from './machine-paths.mjs'
10
+
11
+ /** 项目目录链中按优先级探测的指令文件名。 */
12
+ export const PROJECT_INSTRUCTION_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
13
+ export const USER_GLOBAL_INSTRUCTION_FILE = 'AGENTS.md'
14
+
15
+ /** 引擎侧的项目根标记;宿主有自己的集合(例如只要 .git),因此这里不做统一。 */
16
+ export const ENGINE_PROJECT_ROOT_MARKERS = ['.git', '.hg', '.svn']
17
+
18
+ /** cwd 到文件系统根:含自身,父目录等于自身时停止。 */
19
+ export function ancestorDirs(cwd) {
20
+ const dirs = [cwd]
21
+ for (;;) {
22
+ const parent = dirname(dirs[dirs.length - 1])
23
+ if (parent === dirs[dirs.length - 1]) return dirs
24
+ dirs.push(parent)
25
+ }
26
+ }
27
+
28
+ /** 最近的含 marker 祖先;没有 marker 或 marker 不在祖先链上时回落 cwd。 */
29
+ function resolveProjectRoot(cwd, markerDirs) {
30
+ for (const dir of ancestorDirs(cwd)) if (markerDirs.has(dir)) return dir
31
+ return cwd
32
+ }
33
+
34
+ /** 项目根到 cwd 的目录链(广到窄);root 必来自 ancestorDirs(cwd),因此一定在链上。 */
35
+ function instructionDirs(cwd, root) {
36
+ const dirs = ancestorDirs(cwd)
37
+ return dirs.slice(0, dirs.indexOf(root) + 1).reverse()
38
+ }
39
+
40
+ /**
41
+ * 一次给出项目根与目录链(广到窄);要「近者优先」的注入顺序由调用方自行反转。
42
+ * markerDirs 的字符串必须取自 ancestorDirs(同一套规范化,UNC 共享名根带尾分隔符)。
43
+ */
44
+ export function instructionPlan(cwd, markerDirs) {
45
+ const root = resolveProjectRoot(cwd, markerDirs)
46
+ return { root, dirs: instructionDirs(cwd, root) }
47
+ }
48
+
49
+ /** 列出某目录里的候选文件(按候选顺序);探测失败逐条跳过,不阻断会话。 */
50
+ async function presentInDir(fs, dir, candidates, signal) {
51
+ const found = []
52
+ for (const candidate of candidates) {
53
+ try {
54
+ const target = await fs.resolve(join(dir, candidate), { cwd: dir, signal })
55
+ const info = await fs.stat(target, signal)
56
+ if (info !== undefined && info.type === 'file') found.push(candidate)
57
+ } catch {
58
+ // 不存在或不可读——跳过。
59
+ }
60
+ }
61
+ return found
62
+ }
63
+
64
+ /**
65
+ * 探测 cwd 到项目根的完整目录链,以及 DSH_HOME 下的用户级指令文件。
66
+ * 计划与读盘分离:先由 ancestorDirs 得到祖先,再异步探 marker,最后交给纯函数选根。
67
+ */
68
+ export async function collectInstructionFiles(fs, cwd, signal, home) {
69
+ const markerDirs = new Set()
70
+ for (const dir of ancestorDirs(cwd)) {
71
+ for (const marker of ENGINE_PROJECT_ROOT_MARKERS) {
72
+ try {
73
+ const target = await fs.resolve(join(dir, marker), { cwd: dir, signal })
74
+ if (await fs.stat(target, signal) !== undefined) { markerDirs.add(dir); break }
75
+ } catch {
76
+ // 标记不存在——继续看上一层。
77
+ }
78
+ }
79
+ // 最近 marker 即项目根:再向上的探测不会改变 instructionPlan 的结果。
80
+ if (markerDirs.has(dir)) break
81
+ }
82
+ const plan = instructionPlan(cwd, markerDirs)
83
+ const projectFiles = []
84
+ // 注入顺序要求近者优先:cwd 先于项目根;根目录下的文件用裸候选名。
85
+ for (const dir of [...plan.dirs].reverse()) {
86
+ for (const candidate of await presentInDir(fs, dir, PROJECT_INSTRUCTION_CANDIDATES, signal)) {
87
+ projectFiles.push(dir === plan.root ? candidate : join(dir, candidate))
88
+ }
89
+ }
90
+ const userGlobalFiles = []
91
+ // 路径类事实取唯一来源;本处需要 undefined 表示「没有可探测的全局目录」,故把空串折回缺省。
92
+ const dshHome = home ?? (machinePaths().DSH_HOME || undefined)
93
+ if (dshHome !== undefined) {
94
+ userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_INSTRUCTION_FILE], signal))
95
+ }
96
+ return { root: plan.root, projectFiles, userGlobalFiles, userGlobalHome: dshHome }
97
+ }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * instruction-hint — 通用内置指令文件提示引擎。
2
+ * instruction-hint — 通用内置指令文件提示引擎(可挂载的行 + 消息构造)。
3
3
  *
4
- * 本模块同时是「可 import 的纯函数库」与「可被组合源挂载的 plugin」:
4
+ * 探测(候选清单、项目根、目录链)归 engine/instruction-files.mjs,本模块只消费它的
5
+ * 异步执行器。两个入口:
5
6
  * - prompt-config 的 strategy=placeholder + fill=instruction-hint
6
- * 直接使用本模块的探测与消息构造;
7
+ * 直接使用本模块的消息构造与 instruction-files 的探测结果;
7
8
  * - plugin 形态(见文件末尾 apply):**挂本行 = 开启**「晋升后把
8
9
  * agent-instructions 全文替换为一次性 hint,后续全文消息丢弃」的转换。
9
10
  *
@@ -19,97 +20,30 @@
19
20
  */
20
21
 
21
22
  import { randomUUID } from 'node:crypto'
22
- import { readFileSync } from 'node:fs'
23
+ import { existsSync, readFileSync } from 'node:fs'
24
+ import { basename, join, resolve } from 'node:path'
23
25
  import { createEpochPromotion } from './compaction-epoch.mjs'
26
+ import {
27
+ ancestorDirs,
28
+ collectInstructionFiles,
29
+ ENGINE_PROJECT_ROOT_MARKERS,
30
+ PROJECT_INSTRUCTION_CANDIDATES,
31
+ USER_GLOBAL_INSTRUCTION_FILE,
32
+ } from './instruction-files.mjs'
24
33
  import { interpolateStatic } from './interpolate.mjs'
34
+ import { machinePaths } from './machine-paths.mjs'
35
+ import { resolveRealWithin } from './path-safety.mjs'
25
36
  import { booleanOption, createWarnOnce, getService, parsePromoteOn, validateConfig } from './shared.mjs'
37
+ import { PRE_STEP_COORDINATOR_SERVICE } from './service-names.mjs'
38
+ import { instructionBlocks } from './instruction-blocks.mjs'
26
39
 
27
40
  export const name = 'instruction-hint'
28
41
 
29
42
  /** 无 inject:监听器只在使用时触碰 ctx 服务(同 tool-bootstrap / context-gate 纪律)。 */
30
43
  export const inject = []
31
44
 
32
- /** 项目目录链中按优先级探测的指令文件名。 */
33
- export const PROJECT_INSTRUCTION_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
34
- export const USER_GLOBAL_INSTRUCTION_CANDIDATE = 'AGENTS.md'
35
45
  const templateOf = (templates, key) => typeof templates?.[key] === 'string' ? templates[key] : ''
36
46
 
37
- /** Join one path segment onto a directory (platform-agnostic string join). */
38
- export function joinPath(dir, segment) {
39
- if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
40
- const sep = dir.includes('\\') ? '\\' : '/'
41
- return dir + sep + segment
42
- }
43
-
44
- /** Parent of an absolute Windows or POSIX path. */
45
- export function parentPath(path) {
46
- const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
47
- if (idx <= 0) return path
48
- const parent = path.slice(0, idx)
49
- return parent.length === 0 ? path : parent
50
- }
51
-
52
- /** Find the project root: first ancestor containing any root marker. */
53
- export async function findProjectRoot(fs, cwd, signal) {
54
- let current = cwd
55
- for (;;) {
56
- for (const marker of ['.git', '.hg', '.svn']) {
57
- try {
58
- const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
59
- const info = await fs.stat(target, signal)
60
- if (info !== undefined) return current
61
- } catch {
62
- // Probe failure = marker absent; continue.
63
- }
64
- }
65
- const parent = parentPath(current)
66
- if (parent === current || parent.length === 0) return cwd
67
- current = parent
68
- }
69
- }
70
-
71
- /** List instruction files present in one directory. */
72
- export async function presentInDir(fs, dir, candidates, signal) {
73
- const found = []
74
- for (const candidate of candidates) {
75
- try {
76
- const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
77
- const info = await fs.stat(target, signal)
78
- if (info !== undefined && info.type === 'file') found.push(candidate)
79
- } catch {
80
- // Absent or unreadable — skip.
81
- }
82
- }
83
- return found
84
- }
85
-
86
- /**
87
- * 探测 cwd 到项目根的完整目录链,以及 DSH_HOME 下的用户级 AGENTS.md。
88
- * home 未传时使用当前宿主的 DSH_HOME/USERPROFILE 推导值。
89
- */
90
- export async function collectInstructionFiles(fs, cwd, signal, home) {
91
- const root = await findProjectRoot(fs, cwd, signal)
92
- const projectFiles = []
93
- let probed = cwd
94
- for (;;) {
95
- for (const candidate of await presentInDir(fs, probed, PROJECT_INSTRUCTION_CANDIDATES, signal)) {
96
- projectFiles.push(probed === root ? candidate : joinPath(probed, candidate))
97
- }
98
- if (probed === root) break
99
- const parent = parentPath(probed)
100
- if (parent === probed || parent.length === 0) break
101
- probed = parent
102
- }
103
-
104
- const userGlobalFiles = []
105
- const dshHome = home ?? process.env.DSH_HOME
106
- ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
107
- if (dshHome !== undefined) {
108
- userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_INSTRUCTION_CANDIDATE], signal))
109
- }
110
- return { root, projectFiles, userGlobalFiles, userGlobalHome: dshHome }
111
- }
112
-
113
47
  /**
114
48
  * 用显式模板格式化探测结果;无文件或无正文模板时返回空字符串。
115
49
  * scope 限定只报告某一来源:all(默认)/ global($DSH_HOME/AGENTS.md)/ project(cwd→项目根链)。
@@ -117,14 +51,14 @@ export async function collectInstructionFiles(fs, cwd, signal, home) {
117
51
  export function buildInstructionHintText({ root, projectFiles = [], userGlobalFiles = [], userGlobalHome } = {}, scope = 'all', templates = {}) {
118
52
  const sections = []
119
53
  if (scope !== 'global' && projectFiles.length > 0) {
120
- const text = interpolateStatic(templateOf(templates, 'projectTemplate'), { FILES: projectFiles.join(', '), ROOT: root ?? '' })
54
+ const text = interpolateStatic(templateOf(templates, 'projectTemplate'), { FILES: projectFiles.join(', '), ROOT: root ?? '' }, undefined, 'projectTemplate')
121
55
  if (text.trim().length > 0) sections.push(text)
122
56
  }
123
57
  if (scope !== 'project' && userGlobalFiles.length > 0) {
124
58
  const paths = typeof userGlobalHome === 'string' && userGlobalHome.length > 0
125
- ? userGlobalFiles.map((name) => joinPath(userGlobalHome, name))
59
+ ? userGlobalFiles.map((name) => join(userGlobalHome, name))
126
60
  : userGlobalFiles
127
- const text = interpolateStatic(templateOf(templates, 'globalTemplate'), { FILES: paths.join(', '), ROOT: root ?? '' })
61
+ const text = interpolateStatic(templateOf(templates, 'globalTemplate'), { FILES: paths.join(', '), ROOT: root ?? '' }, undefined, 'globalTemplate')
128
62
  if (text.trim().length > 0) sections.push(text)
129
63
  }
130
64
  if (sections.length === 0) return ''
@@ -139,13 +73,54 @@ function hintScope(value) {
139
73
  return typeof value === 'string' && HINT_SCOPES.has(value) ? value : 'all'
140
74
  }
141
75
 
76
+ /** params.file 的白名单:与探测清单同源,只允许指令文件候选名。 */
77
+ const INSTRUCTION_FILE_NAMES = new Set([...PROJECT_INSTRUCTION_CANDIDATES, USER_GLOBAL_INSTRUCTION_FILE])
78
+
79
+ /**
80
+ * 指令文件的信任根:cwd 的目录链,向上到最近的项目根为止(与探测范围一致);
81
+ * 没有 marker 时只有 cwd。不延伸到项目根之上——那会把同一卷上的任意路径都算进来。
82
+ */
83
+ function instructionRoots(cwd) {
84
+ const dirs = ancestorDirs(cwd)
85
+ for (const [index, dir] of dirs.entries()) {
86
+ if (ENGINE_PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(dir, marker)))) return dirs.slice(0, index + 1)
87
+ }
88
+ return [cwd]
89
+ }
90
+
91
+ /**
92
+ * 文件即真相:每次注入都重读绑定文件,外部编辑立即生效。
93
+ * 信任根 = cwd 的目录链到最近项目根(探测同一范围,故项目根 AGENTS.md 合法)+ DSH_HOME。
94
+ * 越界、链接逃逸与非法名字直接抛出(executor 会 warn/skip 该配置);
95
+ * 目标或其父目录不存在时按缺失文件返回空串。
96
+ */
97
+ function readBoundInstructionFile(file, cwd) {
98
+ if (!INSTRUCTION_FILE_NAMES.has(basename(file))) {
99
+ throw new Error(`instruction-hint: params.file must be an instruction file name (${[...INSTRUCTION_FILE_NAMES].join(', ')}), got ${JSON.stringify(file)}`)
100
+ }
101
+ let real
102
+ try {
103
+ real = resolveRealWithin(resolve(cwd, file), [...instructionRoots(cwd), machinePaths().DSH_HOME], 'instruction file')
104
+ } catch (error) {
105
+ if (error?.code === 'ENOENT') return ''
106
+ throw error
107
+ }
108
+ try {
109
+ return readFileSync(real, 'utf8').trim()
110
+ } catch {
111
+ return ''
112
+ }
113
+ }
114
+
142
115
  /**
143
116
  * prompt-config placeholder fill=instruction-hint 的 resolver。
144
117
  * params.text(自定义提示)优先;params.file(文件卡绑定的单个指令文件)次之——
145
118
  * 运行时读该文件正文并加 `Instructions from:` 头注入;最后按 params.scope 探测来源
146
119
  * (all / global / project)按显式 projectTemplate/globalTemplate 生成提示。
147
120
  * 未配置提示模板时返回 null;引擎不提供默认引导文案。
148
- * 文件缺失、不可读或空内容时返回 null,不注入任何消息。
121
+ * params.file 不是任意文件读取:名字必须是指令文件候选名,真实路径必须在会话 cwd 到
122
+ * 项目根的目录链或 DSH_HOME 内;越界、链接逃逸与非法名字在解析期抛错(executor 按单配置
123
+ * 告警跳过)。文件缺失、不可读或空内容时返回 null。
149
124
  */
150
125
  export function createInstructionHintResolver(config = {}) {
151
126
  const customText = typeof config?.params?.text === 'string' ? config.params.text.trim()
@@ -157,14 +132,6 @@ export function createInstructionHintResolver(config = {}) {
157
132
  const fileLabel = typeof config?.params?.displayPath === 'string' && config.params.displayPath.trim().length > 0
158
133
  ? config.params.displayPath.trim()
159
134
  : file
160
- /** 文件即真相:每次注入都重读绑定文件,外部编辑立即生效。 */
161
- const readBoundFile = () => {
162
- try {
163
- return readFileSync(file, 'utf8').trim()
164
- } catch {
165
- return ''
166
- }
167
- }
168
135
 
169
136
  return async ({ ctx, agent, session }) => {
170
137
  const id = `instruction-hint-${session.id}-${randomUUID()}`
@@ -172,7 +139,7 @@ export function createInstructionHintResolver(config = {}) {
172
139
  return { id, text: customText, source: { kind: 'instruction-hint', form: 'hint' } }
173
140
  }
174
141
  if (file.length > 0) {
175
- const content = readBoundFile()
142
+ const content = readBoundInstructionFile(file, session.header?.cwd ?? process.cwd())
176
143
  return content.length > 0
177
144
  ? { id, text: `Instructions from: ${fileLabel}\n\n${content}`, source: { kind: 'instruction-file', form: 'instructions' } }
178
145
  : null
@@ -189,16 +156,15 @@ export function createInstructionHintResolver(config = {}) {
189
156
  }
190
157
  }
191
158
 
192
- const INSTRUCTION_FROM_RE = /(?:^|\n) *(?:Additional |Updated )?Instructions from: ([^\n]+)/g
193
-
194
- /** 从 agent-instructions 注入消息提取参考文件路径。 */
159
+ /** 从 agent-instructions 注入消息提取参考文件路径;removed 不是参考文件。 */
195
160
  export function extractInstructionPaths(message) {
196
161
  const paths = []
197
162
  const blocks = Array.isArray(message?.content) ? message.content : []
198
163
  for (const block of blocks) {
199
164
  if (block?.type !== 'text' || typeof block.text !== 'string') continue
200
- for (const match of block.text.matchAll(INSTRUCTION_FROM_RE)) {
201
- const path = match[1].trim()
165
+ for (const parsed of instructionBlocks(block.text)) {
166
+ if (parsed.action === 'remove') continue
167
+ const path = parsed.path.trim()
202
168
  if (path !== '' && !paths.includes(path)) paths.push(path)
203
169
  }
204
170
  }
@@ -209,7 +175,7 @@ export function extractInstructionPaths(message) {
209
175
  export function buildInstructionHint(original, paths, sourceName = 'instruction-hint', templates = {}) {
210
176
  const template = templateOf(templates, 'messageTemplate')
211
177
  if (template.trim().length === 0) return original
212
- const text = interpolateStatic(template, { FILES: paths.join(', '), SUFFIX: templateOf(templates, 'suffixTemplate') })
178
+ const text = interpolateStatic(template, { FILES: paths.join(', '), SUFFIX: templateOf(templates, 'suffixTemplate') }, undefined, 'messageTemplate')
213
179
  if (text.trim().length === 0) return original
214
180
  return {
215
181
  id: typeof original?.id === 'string' && original.id !== '' ? original.id : `instruction-hint-${randomUUID()}`,
@@ -284,7 +250,7 @@ export function apply(ctx, config) {
284
250
  if (!promotion.status(agent).promoted) return decision
285
251
  if (!Array.isArray(decision.messages)) return decision
286
252
  if (agent?.session === undefined) return decision
287
- const coordinator = getService(ctx, 'promptToolPreStep')
253
+ const coordinator = getService(ctx, PRE_STEP_COORDINATOR_SERVICE)
288
254
  const messages = typeof coordinator?.filterInstructions === 'function'
289
255
  ? coordinator.filterInstructions(agent, decision.messages)
290
256
  : decision.messages
@@ -0,0 +1,7 @@
1
+ /** `interpolate.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
2
+
3
+ /** 保留名判据(内建变量 + 动态宏);任何值都接受,非字符串按空名处理。 */
4
+ export function isReservedInterpolationName(key: unknown): boolean
5
+
6
+ /** ST 宏写法归一(`{{roll 1d6}}` / `{{roll:1d6}}` → `{{roll::1d6}}`),幂等。 */
7
+ export function normalizeMacroSyntax(text: string): string