dsh-plugin-prompt-tool 0.1.4 → 0.2.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.
@@ -10,11 +10,22 @@ declare function parseFrontmatter(text: string): {
10
10
  body: string;
11
11
  };
12
12
  interface BuildCordisOptions {
13
- /** 首轮独立锚定轮:首个用户消息先入 next-step inbox,首步只发 anchorText。 */
13
+ /** 首轮近距离锚定:首条真实用户消息后追加一次性首句锚点。 */
14
14
  anchorFirstTurn?: boolean;
15
+ /** 自定义锚点文本;anchorCustom=true 时固定使用。 */
15
16
  anchorText?: string;
17
+ /** 自定义锚点开关:true 固定使用 anchorText;false 按任务自动选择。 */
18
+ anchorCustom?: boolean;
16
19
  /** 锚定确认后注入 preset.md;关闭时仍保留工具引导,但不注册 prompt-injector 行。 */
17
20
  injectPrompt?: boolean;
21
+ /** 子代理固定 Flash 模型:true 时给 subagent/subagent_fork 行加 agentOptions。 */
22
+ subagentFlash?: boolean;
23
+ /** 子代理 Flash 路由 provider(默认 deepseek-official)。 */
24
+ subagentFlashProvider?: string;
25
+ /** 子代理 Flash 模型名(默认 deepseek-v4-flash)。 */
26
+ subagentFlashModel?: string;
27
+ /** Windows custom-bash 可执行名或路径;默认 bash.exe(PATH 查找)。 */
28
+ bashPath?: string;
18
29
  }
19
30
  declare function buildCordis(prompt: string, options?: BuildCordisOptions): string;
20
31
  //#endregion
@@ -6,26 +6,32 @@ import { parse } from "yaml";
6
6
  * prompt-tool preset 生成核心:与 dsh 插件上下文无关的纯函数,
7
7
  * 独立成文件以便单元测试与复用。失败一律抛出(fail loud)。
8
8
  */
9
- const VENDOR_CORDIS = fileURLToPath(new URL("../vendor/dsh-anchored-standard/preset/agent.cordis.yml", import.meta.url));
10
- const LOCAL_INJECTOR_BLOCK = `# prompt-tool 附加件:锚定确认后注入 preset.md。注册在 tool-bootstrap 之后,
11
- # 不参与首轮剥离顺序;tools / 上下文剥离全部由原版 tool-bootstrap 负责
12
- # (首轮 = Minimal 真实 schema:持久 bash + str_replace_editor,无输出 cap),
13
- # 此插件只做一件事——锚定轮结束后(we 确认或兜底)注入一次提示词。
9
+ const VENDOR_CORDIS = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset/agent.cordis.yml", import.meta.url));
10
+ const ROUTER_FIRST_TURN_BLOCK = `# prompt-tool 本地附加件(最优组合):首轮只保留计划段 + 训练原句,
11
+ # 并清空自动注入 contexts;子代理直接放行。注册在 tool-bootstrap 之后,
12
+ # 不参与首轮工具目录裁剪——tools 仍由原版 tool-bootstrap 负责。
13
+ - id: router-first-turn
14
+ name: ./router-first-turn.mjs
15
+ `;
16
+ const NEAR_ANCHOR_BLOCK = `# prompt-tool 近距离首句锚定:在首条真实用户消息之后追加一次性锚点。
17
+ # 不拆轮、不挪任务;useCustom=false 时按任务自动选择 we/let 首句,
18
+ # useCustom=true 时固定使用 anchorText。
19
+ - id: near-anchor
20
+ name: ./near-anchor.mjs
21
+ config:
22
+ anchorText: |-
23
+ __ANCHOR_TEXT__
24
+ useCustom: __USE_CUSTOM__
25
+ `;
26
+ const LOCAL_INJECTOR_BLOCK = `# prompt-tool 附加件:锚定确认后注入 preset.md。注册在工具引导之后,
27
+ # 不参与首轮剥离顺序;tools / 上下文剥离由 tool-bootstrap 与
28
+ # router-first-turn 负责(首轮 = Minimal 真实 schema + 训练原句)。
14
29
  - id: prompt-injector
15
30
  name: ./prompt-injector.mjs
16
31
  config:
17
32
  promptText: |-
18
33
  __PROMPT_TOOL_TEXT__
19
34
  `;
20
- const TURN_ANCHOR_BLOCK = `# prompt-tool 可选附加件:首轮独立锚定轮。启用后首个真实用户消息先入
21
- # next-step inbox,首步只发 anchorText;模型回应锚定句后,driver 在同轮
22
- # 内自动消费任务继续执行。关闭时不生成此行。
23
- - id: turn-anchor
24
- name: ./turn-anchor.mjs
25
- config:
26
- anchorText: |-
27
- __ANCHOR_TEXT__
28
- `;
29
35
  function parseFrontmatter(text) {
30
36
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
31
37
  if (!match) return {
@@ -45,12 +51,76 @@ function parseFrontmatter(text) {
45
51
  body: text.slice(match[0].length)
46
52
  };
47
53
  }
54
+ /** dsh-router-standard 的 Flash 弱路由 persona(子代理版)。 */
55
+ const ROUTER_FLASH_PERSONA = [
56
+ "You are a helpful assistant.",
57
+ "Before acting, decide the task type (build or fix) and adopt the matching style: build → hands-on production; fix → inspect-and-plan.",
58
+ "Before acting, briefly review what you have already done in this session and continue from where you left off; do not repeat completed steps. Do not run environment checks (echo, whoami, uname, node --version, date) or exhaustive grep/glob scans.",
59
+ "Think deeply first, then produce."
60
+ ].join("\n");
61
+ /** 适配上游 context-gate:保留 near-anchor 消息;子代理沿用全量放行策略。 */
62
+ function normalizeContextGate(source) {
63
+ const block = `- id: context-gate
64
+ name: ./context-gate.mjs
65
+ config:
66
+ promoteOn: either
67
+ includeSubagents: true
68
+ allowKinds: [skill-invocation]`;
69
+ const replaced = `- id: context-gate
70
+ name: ./context-gate.mjs
71
+ config:
72
+ promoteOn: either
73
+ includeSubagents: false
74
+ allowKinds: [skill-invocation, near-anchor]`;
75
+ if (!source.includes(block)) throw new Error("vendor agent.cordis.yml missing context-gate row");
76
+ return source.replace(block, replaced);
77
+ }
78
+ /** 上游可能携带作者机器固定路径;生成时统一改写为可配置 bashPath。 */
79
+ function normalizeBashPath(source, bashPath) {
80
+ const marker = /bashPath:\s*'[^']*'/.exec(source);
81
+ if (marker === null) throw new Error("vendor agent.cordis.yml missing bashPath row");
82
+ return source.replace(marker[0], `bashPath: '${bashPath}'`);
83
+ }
84
+ /** 给 subagent/subagent_fork 行加 dsh-router-standard 的 Flash 子代理方案;关闭时保持继承主会话路由。 */
85
+ function applySubagentFlash(source, enabled, provider, model) {
86
+ if (!enabled) return source;
87
+ const agentOptions = ` agentOptions:
88
+ provider: ${provider}
89
+ model: ${model}`;
90
+ const persona = ROUTER_FLASH_PERSONA.split("\n").map((line) => ` ${line}`).join("\n");
91
+ const targets = [{
92
+ id: "tool-subagent",
93
+ toolName: "subagent",
94
+ provider: "spawn"
95
+ }, {
96
+ id: "tool-subagent-fork",
97
+ toolName: "subagent_fork",
98
+ provider: "fork"
99
+ }];
100
+ let out = source;
101
+ for (const target of targets) {
102
+ const block = ` - id: ${target.id}
103
+ name: '@deepseek-ai/dsh-tool-subagent'
104
+ config:
105
+ provider: ${target.provider}
106
+ toolName: ${target.toolName}
107
+ backgroundMode: continuable`;
108
+ const replaced = `${block}
109
+ ${agentOptions}
110
+ persona: |-
111
+ ${persona}`;
112
+ if (!out.includes(block)) throw new Error(`vendor agent.cordis.yml missing ${target.id} row`);
113
+ out = out.replace(block, replaced);
114
+ }
115
+ return out;
116
+ }
48
117
  function buildCordis(prompt, options = {}) {
49
118
  const anchorFirstTurn = options.anchorFirstTurn === true;
119
+ const anchorCustom = options.anchorCustom === true;
50
120
  const injectPrompt = options.injectPrompt !== false;
51
- const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "You are a helpful software assistant.\n\nBegin every reasoning block with 'We need'.";
121
+ const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "";
52
122
  const indent = (s) => s.split(/\r?\n/).map((l) => l.length === 0 ? "" : " " + l).join("\n");
53
- const up = readFileSync(VENDOR_CORDIS, "utf8").replace(/\r\n/g, "\n");
123
+ const up = normalizeContextGate(normalizeBashPath(applySubagentFlash(readFileSync(VENDOR_CORDIS, "utf8").replace(/\r\n/g, "\n"), options.subagentFlash === true, typeof options.subagentFlashProvider === "string" && options.subagentFlashProvider.length > 0 ? options.subagentFlashProvider : "deepseek-official", typeof options.subagentFlashModel === "string" && options.subagentFlashModel.length > 0 ? options.subagentFlashModel : "deepseek-v4-flash"), typeof options.bashPath === "string" && options.bashPath.length > 0 ? options.bashPath : "bash.exe"));
54
124
  const bootstrap = /^-\s+id:\s+tool-bootstrap\s*$/m.exec(up);
55
125
  if (!bootstrap) throw new Error("vendor agent.cordis.yml missing tool-bootstrap row");
56
126
  let cursor = up.indexOf("\n", bootstrap.index);
@@ -71,11 +141,12 @@ function buildCordis(prompt, options = {}) {
71
141
  const head = up.slice(0, insertAt);
72
142
  const tail = up.slice(insertAt);
73
143
  const separator = head.endsWith("\n\n") ? "" : head.endsWith("\n") ? "\n" : "\n\n";
74
- const parts = [];
144
+ const parts = [ROUTER_FIRST_TURN_BLOCK];
75
145
  if (anchorFirstTurn) {
76
146
  const anchorMarker = " anchorText: |-\n __ANCHOR_TEXT__";
77
- if (!TURN_ANCHOR_BLOCK.includes(anchorMarker)) throw new Error("internal error: turn-anchor template lost its anchorText placeholder");
78
- parts.push(TURN_ANCHOR_BLOCK.replace(anchorMarker, " anchorText: |-\n" + indent(anchorText)));
147
+ if (!NEAR_ANCHOR_BLOCK.includes(anchorMarker)) throw new Error("internal error: near-anchor template lost its anchorText placeholder");
148
+ const anchorBlock = NEAR_ANCHOR_BLOCK.replace(" useCustom: __USE_CUSTOM__", ` useCustom: ${anchorCustom}`).replace(anchorMarker, " anchorText: |-\n" + (anchorText.length > 0 ? indent(anchorText) : " "));
149
+ parts.push(anchorBlock);
79
150
  }
80
151
  if (injectPrompt) {
81
152
  const promptMarker = " promptText: |-\n __PROMPT_TOOL_TEXT__";
@@ -84,7 +155,7 @@ function buildCordis(prompt, options = {}) {
84
155
  }
85
156
  const extra = parts.join("\n");
86
157
  const out = head + separator + extra + "\n" + tail;
87
- if (injectPrompt && out.includes("__PROMPT_TOOL_TEXT__") || anchorFirstTurn && out.includes("__ANCHOR_TEXT__")) throw new Error("internal error: preset template placeholder was not replaced");
158
+ if (injectPrompt && out.includes("__PROMPT_TOOL_TEXT__") || anchorFirstTurn && (out.includes("__ANCHOR_TEXT__") || out.includes("__USE_CUSTOM__"))) throw new Error("internal error: preset template placeholder was not replaced");
88
159
  let parsed;
89
160
  try {
90
161
  parsed = parse(out, { logLevel: "silent" });
@@ -93,9 +164,11 @@ function buildCordis(prompt, options = {}) {
93
164
  }
94
165
  if (!Array.isArray(parsed)) throw new Error("generated agent.cordis.yml is not a YAML array");
95
166
  const ids = new Set(parsed.map((row) => row?.id));
167
+ if (!ids.has("router-first-turn")) throw new Error("generated agent.cordis.yml is missing the router-first-turn row");
96
168
  if (injectPrompt && !ids.has("prompt-injector")) throw new Error("generated agent.cordis.yml is missing the prompt-injector row");
97
169
  if (!injectPrompt && ids.has("prompt-injector")) throw new Error("generated agent.cordis.yml still contains the prompt-injector row");
98
- if (anchorFirstTurn && !ids.has("turn-anchor")) throw new Error("generated agent.cordis.yml is missing the turn-anchor row");
170
+ if (anchorFirstTurn && !ids.has("near-anchor")) throw new Error("generated agent.cordis.yml is missing the near-anchor row");
171
+ if (!anchorFirstTurn && ids.has("near-anchor")) throw new Error("generated agent.cordis.yml still contains the near-anchor row");
99
172
  return out;
100
173
  }
101
174
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-prompt-tool",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "DSH 插件:提示词工具,规范模型的思维链与回答内容,提供 Web UI 编辑 preset.md 与 AGENTS.md,并按 skills 目录注册可开关技能。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -74,21 +74,20 @@
74
74
  "cordis.patch.yml",
75
75
  "LICENSE",
76
76
  "preset",
77
- "vendor/dsh-anchored-standard/preset",
78
- "vendor/dsh-anchored-standard/LICENSE",
79
- "vendor/dsh-anchored-standard/NOTICE",
80
77
  "skills",
81
78
  "preset.md",
82
79
  "AGENTS.md",
83
80
  "plan.md",
84
- "README.md"
81
+ "README.md",
82
+ "upstream/dsh-anchored-standard"
85
83
  ],
86
84
  "scripts": {
87
85
  "build": "tsdown",
88
86
  "prepare": "tsdown",
89
87
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
90
88
  "lint": "oxlint src test tsdown.config.ts",
91
- "test": "pnpm build && node --test \"test/*.test.mjs\""
89
+ "test": "pnpm build && node --test \"test/*.test.mjs\"",
90
+ "sync:anchored": "node scripts/sync-anchored.mjs"
92
91
  },
93
92
  "dependencies": {
94
93
  "yaml": "^2.9.0"