dsh-plugin-prompt-tool 0.3.1 → 0.4.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.
package/lib/index.mjs CHANGED
@@ -33,7 +33,16 @@ const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injecto
33
33
  const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
34
34
  const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
35
35
  const PRESET_TEMPLATE_GUIDE = fileURLToPath(new URL("../preset/router-guide.mjs", import.meta.url));
36
- const VENDOR_PRESET_DIR = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset", import.meta.url));
36
+ const LOCAL_PRESET_DIR = fileURLToPath(new URL("../preset", import.meta.url));
37
+ const LOCAL_PRESET_SCRIPTS = [
38
+ "shared.mjs",
39
+ "context-gate.mjs",
40
+ "compaction-epoch.mjs",
41
+ "custom-bash.mjs",
42
+ "instruction-hint.mjs",
43
+ "skill-search.mjs",
44
+ "tool-bootstrap.mjs"
45
+ ];
37
46
  const Config = z.object({
38
47
  text: z.string().default(""),
39
48
  agentsText: z.string().default(""),
@@ -51,6 +60,7 @@ const Config = z.object({
51
60
  subagentFlashProvider: z.string().default("deepseek-official"),
52
61
  subagentFlashModel: z.string().default("deepseek-v4-flash"),
53
62
  bootstrapMaxTokens: z.natural().default(0),
63
+ usePtcMode: z.boolean().default(true),
54
64
  skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
55
65
  skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
56
66
  residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
@@ -71,6 +81,7 @@ const PromptSettingsSchema = z.object({
71
81
  guideCustom: z.boolean().default(false),
72
82
  subagentFlash: z.boolean().default(false),
73
83
  bootstrapMaxTokens: z.natural().default(0),
84
+ usePtcMode: z.boolean().default(true),
74
85
  deepseekAvailable: z.boolean().default(true),
75
86
  injectPrompt: z.boolean().default(true),
76
87
  skillSwitches: z.dict(z.boolean()).default({}),
@@ -228,28 +239,6 @@ function warn(ctx, message) {
228
239
  ctx.logger?.warn(message);
229
240
  } catch {}
230
241
  }
231
- /** 把上游 instruction-hint.mjs 的提示文本替换为读同目录 agents-instruction.txt。 */
232
- function patchInstructionHint(source) {
233
- source = source.replace(/\r\n/g, "\n");
234
- const original = [
235
- " const text = [",
236
- " ...sections,",
237
- " 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
238
- " ].join(' ')"
239
- ].join("\n");
240
- const replacement = [
241
- " const agentsInstructionText = readFileSync(new URL('./agents-instruction.txt', import.meta.url), 'utf8').trim()",
242
- " const text = agentsInstructionText.length > 0",
243
- " ? agentsInstructionText",
244
- " : [",
245
- " ...sections,",
246
- " 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
247
- " ].join(' ')"
248
- ].join("\n");
249
- if (!source.includes("import { createEpochPromotion }")) throw new Error("instruction-hint.mjs import marker missing");
250
- if (!source.includes(original)) throw new Error("instruction-hint.mjs text marker missing");
251
- return source.replace("import { createEpochPromotion } from './compaction-epoch.mjs'", "import { readFileSync } from 'node:fs'\nimport { createEpochPromotion } from './compaction-epoch.mjs'").replace(original, replacement);
252
- }
253
242
  const MAX_SETTINGS_BRIDGE_BODY = 65536;
254
243
  /** 仅允许本机回环请求,镜像官方 settings bridge 的边界。 */
255
244
  function isLoopbackRequest(req) {
@@ -296,7 +285,8 @@ const TUI_BOOLEAN_SWITCHES = [
296
285
  ["anchorFirstTurn", "追加任务引导"],
297
286
  ["anchorCustom", "使用自定义引导(首句)"],
298
287
  ["guideCustom", "使用自定义引导(每轮)"],
299
- ["subagentFlash", "子代理固定 Flash 模型"]
288
+ ["subagentFlash", "子代理固定 Flash 模型"],
289
+ ["usePtcMode", "使用 PTC 模式"]
300
290
  ];
301
291
  /** 把布尔开关渲染成 dsh-tui 命令输出。 */
302
292
  function renderTuiStatus(source) {
@@ -335,7 +325,7 @@ function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekSta
335
325
  handler: async (invocation) => {
336
326
  const usage = () => ({
337
327
  kind: "error",
338
- text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
328
+ text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash|usePtcMode>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
339
329
  });
340
330
  const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
341
331
  const source = getSource();
@@ -656,26 +646,6 @@ function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
656
646
  };
657
647
  });
658
648
  }
659
- /** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
660
- function patchToolBootstrap(source) {
661
- source = source.replace(/\r\n/g, "\n");
662
- const original = [
663
- " const assembled = await next()",
664
- " try {",
665
- " const status = promotion.status(context.agent)"
666
- ].join("\n");
667
- const replacement = [
668
- " const assembled = await next()",
669
- " try {",
670
- " // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
671
- " // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
672
- " // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
673
- " if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
674
- " const status = promotion.status(context.agent)"
675
- ].join("\n");
676
- if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
677
- return source.replace(original, replacement);
678
- }
679
649
  function writePreset(prompt, options) {
680
650
  const presetDir = options.presetDir;
681
651
  mkdirSync(presetDir, { recursive: true });
@@ -689,21 +659,16 @@ function writePreset(prompt, options) {
689
659
  subagentFlash: options.subagentFlash,
690
660
  subagentFlashProvider: options.subagentFlashProvider,
691
661
  subagentFlashModel: options.subagentFlashModel,
692
- bootstrapMaxTokens: options.bootstrapMaxTokens
662
+ bootstrapMaxTokens: options.bootstrapMaxTokens,
663
+ usePtcMode: options.usePtcMode
693
664
  }), "utf8");
694
665
  const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
695
666
  writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
696
- for (const file of readdirSync(VENDOR_PRESET_DIR)) {
697
- if (!file.endsWith(".mjs")) continue;
698
- const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
699
- writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
700
- }
667
+ for (const file of LOCAL_PRESET_SCRIPTS) writeFileSync(join(presetDir, file), readFileSync(join(LOCAL_PRESET_DIR, file), "utf8"), "utf8");
668
+ rmSync(join(presetDir, "dev-tool-search.mjs"), { force: true });
701
669
  const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
702
- if (options.agentsInstructionText !== void 0) {
703
- writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
704
- const hintPath = join(presetDir, "instruction-hint.mjs");
705
- writeFileSync(hintPath, patchInstructionHint(readFileSync(hintPath, "utf8")), "utf8");
706
- } else rmSync(agentsInstructionPath, { force: true });
670
+ if (options.agentsInstructionText !== void 0) writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
671
+ else rmSync(agentsInstructionPath, { force: true });
707
672
  const injectorPath = join(presetDir, "prompt-injector.mjs");
708
673
  if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
709
674
  else rmSync(injectorPath, { force: true });
@@ -785,7 +750,8 @@ function apply(ctx, config) {
785
750
  guideText: config.guideText,
786
751
  guideCustom: config.guideCustom,
787
752
  subagentFlash: config.subagentFlash && getDeepseekAvailable(),
788
- bootstrapMaxTokens: config.bootstrapMaxTokens
753
+ bootstrapMaxTokens: config.bootstrapMaxTokens,
754
+ usePtcMode: config.usePtcMode
789
755
  };
790
756
  installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
791
757
  let currentSource = () => ({
@@ -801,6 +767,7 @@ function apply(ctx, config) {
801
767
  guideCustom: runtime.guideCustom,
802
768
  subagentFlash: runtime.subagentFlash,
803
769
  bootstrapMaxTokens: runtime.bootstrapMaxTokens,
770
+ usePtcMode: runtime.usePtcMode,
804
771
  deepseekAvailable: getDeepseekAvailable(),
805
772
  injectPrompt: runtime.injectPrompt,
806
773
  skillSwitches: runtime.skillSwitches,
@@ -824,12 +791,13 @@ function apply(ctx, config) {
824
791
  guideText: typeof next.guideText === "string" ? next.guideText : config.guideText,
825
792
  guideCustom: typeof next.guideCustom === "boolean" ? next.guideCustom : config.guideCustom,
826
793
  subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable(),
827
- bootstrapMaxTokens: Number.isSafeInteger(next.bootstrapMaxTokens) && next.bootstrapMaxTokens >= 0 ? next.bootstrapMaxTokens : config.bootstrapMaxTokens
794
+ bootstrapMaxTokens: Number.isSafeInteger(next.bootstrapMaxTokens) && next.bootstrapMaxTokens >= 0 ? next.bootstrapMaxTokens : config.bootstrapMaxTokens,
795
+ usePtcMode: typeof next.usePtcMode === "boolean" ? next.usePtcMode : config.usePtcMode
828
796
  };
829
797
  const promptChanged = next.promptText !== current;
830
798
  const agentsChanged = next.agentsText !== currentAgents;
831
799
  const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
832
- const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.guideText !== nextRuntime.guideText || runtime.guideCustom !== nextRuntime.guideCustom || runtime.subagentFlash !== nextRuntime.subagentFlash || runtime.bootstrapMaxTokens !== nextRuntime.bootstrapMaxTokens;
800
+ const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.guideText !== nextRuntime.guideText || runtime.guideCustom !== nextRuntime.guideCustom || runtime.subagentFlash !== nextRuntime.subagentFlash || runtime.bootstrapMaxTokens !== nextRuntime.bootstrapMaxTokens || runtime.usePtcMode !== nextRuntime.usePtcMode;
833
801
  if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
834
802
  needsInitialApply = false;
835
803
  if (promptChanged) current = next.promptText;
@@ -846,6 +814,7 @@ function apply(ctx, config) {
846
814
  runtime.guideCustom = nextRuntime.guideCustom;
847
815
  runtime.subagentFlash = nextRuntime.subagentFlash;
848
816
  runtime.bootstrapMaxTokens = nextRuntime.bootstrapMaxTokens;
817
+ runtime.usePtcMode = nextRuntime.usePtcMode;
849
818
  skillSwitches = runtime.skillSwitches;
850
819
  if (skillSwitchesChanged) invalidateSkills?.();
851
820
  let residentAgentsWritten = false;
@@ -867,6 +836,7 @@ function apply(ctx, config) {
867
836
  subagentFlashProvider: config.subagentFlashProvider,
868
837
  subagentFlashModel: config.subagentFlashModel,
869
838
  bootstrapMaxTokens: runtime.bootstrapMaxTokens,
839
+ usePtcMode: runtime.usePtcMode,
870
840
  agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
871
841
  presetDir: config.presetDir,
872
842
  presetOrder: config.presetOrder
@@ -893,6 +863,7 @@ function apply(ctx, config) {
893
863
  guideCustom: config.guideCustom,
894
864
  subagentFlash: config.subagentFlash && getDeepseekAvailable(),
895
865
  bootstrapMaxTokens: config.bootstrapMaxTokens,
866
+ usePtcMode: config.usePtcMode,
896
867
  deepseekAvailable: getDeepseekAvailable(),
897
868
  injectPrompt: config.injectPrompt,
898
869
  skillSwitches: { ...config.skillSwitches },
@@ -30,7 +30,11 @@ interface BuildCordisOptions {
30
30
  subagentFlashModel?: string;
31
31
  /** 首轮输出封顶(bootstrapMaxTokens);0 或未设置 = 本项目默认无封顶。 */
32
32
  bootstrapMaxTokens?: number;
33
+ /** 使用 PTC 模式:默认 true——晋升后把 wire 切换为 Code Mode(单一 run_code,完整插件工具经生成 SDK 调用);显式 false 时晋升后恢复原生完整工具目录。 */
34
+ usePtcMode?: boolean;
33
35
  }
36
+ /** 给生成的 tool-bootstrap.mjs 打补丁:子代理全量放行 + 可选的 PTC 呈现切换。 */
37
+ declare function patchToolBootstrap(source: string): string;
34
38
  declare function buildCordis(prompt: string, options?: BuildCordisOptions): string;
35
39
  //#endregion
36
- export { BuildCordisOptions, SkillFrontmatter, buildCordis, parseFrontmatter };
40
+ export { BuildCordisOptions, SkillFrontmatter, buildCordis, parseFrontmatter, patchToolBootstrap };
@@ -6,7 +6,7 @@ import { parse } from "yaml";
6
6
  * prompt-tool preset 生成核心:与 dsh 插件上下文无关的纯函数,
7
7
  * 独立成文件以便单元测试与复用。失败一律抛出(fail loud)。
8
8
  */
9
- const VENDOR_CORDIS = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset/agent.cordis.yml", import.meta.url));
9
+ const CORDIS_TEMPLATE = fileURLToPath(new URL("../preset/agent.cordis.yml", import.meta.url));
10
10
  const ROUTER_FIRST_TURN_BLOCK = `# prompt-tool 本地附加件(最优组合):首轮只保留计划段 + 训练原句,
11
11
  # 并清空自动注入 contexts;子代理直接放行。注册在 tool-bootstrap 之后,
12
12
  # 不参与首轮工具目录裁剪——tools 仍由原版 tool-bootstrap 负责。
@@ -69,33 +69,16 @@ const ROUTER_FLASH_PERSONA = [
69
69
  "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.",
70
70
  "Think deeply first, then produce."
71
71
  ].join("\n");
72
- /** 适配上游 context-gate:保留 near-anchor 消息;子代理沿用全量放行策略。 */
73
- function normalizeContextGate(source) {
74
- const block = `- id: context-gate
75
- name: ./context-gate.mjs
76
- config:
77
- promoteOn: either
78
- includeSubagents: true
79
- allowKinds: [skill-invocation]`;
80
- const replaced = `- id: context-gate
81
- name: ./context-gate.mjs
82
- config:
83
- promoteOn: either
84
- includeSubagents: false
85
- allowKinds: [skill-invocation, near-anchor, router-guide]`;
86
- if (!source.includes(block)) throw new Error("vendor agent.cordis.yml missing context-gate row");
87
- return source.replace(block, replaced);
88
- }
89
- /** 按开关向上游 tool-bootstrap 行注入 bootstrapMaxTokens;0/未设置保持无封顶。 */
90
- function applyBootstrapMaxTokens(source, value) {
91
- if (value === void 0 || !Number.isSafeInteger(value) || value <= 0) return source;
72
+ /** 按开关向本地 tool-bootstrap 行注入 usePtcMode / bootstrapMaxTokens。 */
73
+ function applyToolBootstrapOptions(source, usePtcMode, bootstrapMaxTokens) {
92
74
  const rowStart = source.indexOf("- id: tool-bootstrap\n name: ./tool-bootstrap.mjs\n config:\n");
93
- if (rowStart < 0) throw new Error("vendor agent.cordis.yml missing tool-bootstrap config block");
75
+ if (rowStart < 0) throw new Error("preset/agent.cordis.yml missing tool-bootstrap config block");
94
76
  const include = source.indexOf(" includeSubagents: true\n", rowStart);
95
- if (include < 0) throw new Error("vendor agent.cordis.yml tool-bootstrap row missing includeSubagents");
77
+ if (include < 0) throw new Error("preset/agent.cordis.yml tool-bootstrap row missing includeSubagents");
96
78
  const at = include + 26 + 1;
97
- return source.slice(0, at) + ` bootstrapMaxTokens: ${value}
98
- ` + source.slice(at);
79
+ const lines = [` usePtcMode: ${usePtcMode}`];
80
+ if (bootstrapMaxTokens !== void 0 && Number.isSafeInteger(bootstrapMaxTokens) && bootstrapMaxTokens > 0) lines.push(` bootstrapMaxTokens: ${bootstrapMaxTokens}`);
81
+ return source.slice(0, at) + lines.join("\n") + "\n" + source.slice(at);
99
82
  }
100
83
  /** 给 subagent/subagent_fork 行加 dsh-router-standard 的 Flash 子代理方案;关闭时保持继承主会话路由。 */
101
84
  function applySubagentFlash(source, enabled, provider, model) {
@@ -125,24 +108,140 @@ function applySubagentFlash(source, enabled, provider, model) {
125
108
  ${agentOptions}
126
109
  persona: |-
127
110
  ${persona}`;
128
- if (!out.includes(block)) throw new Error(`vendor agent.cordis.yml missing ${target.id} row`);
111
+ if (!out.includes(block)) throw new Error(`preset/agent.cordis.yml missing ${target.id} row`);
129
112
  out = out.replace(block, replaced);
130
113
  }
131
114
  return out;
132
115
  }
116
+ /** 给生成的 tool-bootstrap.mjs 打补丁:子代理全量放行 + 可选的 PTC 呈现切换。 */
117
+ function patchToolBootstrap(source) {
118
+ source = source.replace(/\r\n/g, "\n");
119
+ const original = [
120
+ " const assembled = await next()",
121
+ " try {",
122
+ " const status = promotion.status(context.agent)"
123
+ ].join("\n");
124
+ const replacement = [
125
+ " const assembled = await next()",
126
+ " try {",
127
+ " const agent = context.agent",
128
+ " if (agent === undefined) return assembled",
129
+ " // prompt-tool patch: subagents skip catalog narrowing and use assembled tools directly.",
130
+ " // Callers (such as dsh-mnemon) already filter assembled.tools through their own whitelists.",
131
+ " // New plugin tools with any prefix therefore appear in the subagent first session automatically.",
132
+ " agentBySession.set(agent.session, agent)",
133
+ " if ((agent.session?.header?.delegationDepth ?? 0) > 0) {",
134
+ " if (usePtcMode) applyCodePresentation(agent)",
135
+ " return assembled",
136
+ " }",
137
+ " const status = promotion.status(agent)"
138
+ ].join("\n");
139
+ if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
140
+ source = source.replace(original, replacement);
141
+ const allowedOriginal = "const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'compactionTools', 'includeSubagents'])";
142
+ const allowedReplacement = "const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'compactionTools', 'includeSubagents', 'usePtcMode'])";
143
+ if (!source.includes(allowedOriginal)) throw new Error("tool-bootstrap.mjs ALLOWED_KEYS marker missing");
144
+ source = source.replace(allowedOriginal, allowedReplacement);
145
+ const parseOriginal = " const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)";
146
+ const parseReplacement = " const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)\n const usePtcMode = booleanOption(source.usePtcMode, 'usePtcMode', true)";
147
+ if (!source.includes(parseOriginal)) throw new Error("tool-bootstrap.mjs includeSubagents parse marker missing");
148
+ source = source.replace(parseOriginal, parseReplacement);
149
+ const trackerOriginal = [" const promotion = createEpochPromotion(promoteEvents, { includeSubagents })", " ctx.on('session/event', (session, event) => promotion.observe(session, event))"].join("\n");
150
+ const trackerReplacement = [
151
+ " const promotion = createEpochPromotion(promoteEvents, { includeSubagents })",
152
+ "",
153
+ " // prompt-tool patch: optional Code Mode (PTC) wire presentation after promotion.",
154
+ " const presentationBySession = new WeakMap()",
155
+ " const agentBySession = new WeakMap()",
156
+ " const presentationState = (session) => {",
157
+ " let state = presentationBySession.get(session)",
158
+ " if (state === undefined) {",
159
+ " state = { applied: false, disposer: undefined }",
160
+ " presentationBySession.set(session, state)",
161
+ " }",
162
+ " return state",
163
+ " }",
164
+ " const applyCodePresentation = (agent) => {",
165
+ " const session = agent?.session",
166
+ " if (session === undefined) return",
167
+ " const state = presentationState(session)",
168
+ " if (state.applied) return",
169
+ " const tools = agent.ctx?.tools",
170
+ " if (tools === undefined || typeof tools.presentAs !== 'function') return",
171
+ " state.disposer = tools.presentAs('code')",
172
+ " state.applied = true",
173
+ " }",
174
+ " const releaseCodePresentation = (session) => {",
175
+ " const state = presentationBySession.get(session)",
176
+ " if (state === undefined) return",
177
+ " if (typeof state.disposer === 'function') {",
178
+ " try { state.disposer() } catch { /* never brick the session */ }",
179
+ " }",
180
+ " state.disposer = undefined",
181
+ " state.applied = false",
182
+ " }",
183
+ " ctx.on('session/event', (session, event) => promotion.observe(session, event))",
184
+ "",
185
+ " ctx.on('session/event', (session, event) => {",
186
+ " if (!usePtcMode) return",
187
+ " if (event.type === 'compaction/end') {",
188
+ " releaseCodePresentation(session)",
189
+ " return",
190
+ " }",
191
+ " if (event.type !== 'step/end' && event.type !== 'turn/end') return",
192
+ " const agent = agentBySession.get(session)",
193
+ " if (agent !== undefined && promotion.status(agent).promoted) applyCodePresentation(agent)",
194
+ " })"
195
+ ].join("\n");
196
+ if (!source.includes(trackerOriginal)) throw new Error("tool-bootstrap.mjs promotion tracker marker missing");
197
+ source = source.replace(trackerOriginal, trackerReplacement);
198
+ const promotedStart = source.indexOf(" if (status.promoted) {\n // PROMOTED:");
199
+ const promotedEnd = source.indexOf(" // Controlled phase:", promotedStart);
200
+ if (promotedStart < 0 || promotedEnd < 0) throw new Error("tool-bootstrap.mjs promoted/controlled marker missing");
201
+ const promotedReplacement = [
202
+ " if (status.promoted) {",
203
+ " // prompt-tool patch: both modes keep the assembled catalog after promotion.",
204
+ " // usePtcMode switches the wire presentation instead of narrowing resident tools.",
205
+ " if (usePtcMode) applyCodePresentation(agent)",
206
+ " return assembled",
207
+ " }",
208
+ ""
209
+ ].join("\n");
210
+ source = source.slice(0, promotedStart) + promotedReplacement + source.slice(promotedEnd);
211
+ const residentOriginal = "const RESIDENT_DISCOVERY_TOOLS = ['dev_tool_search', 'skill_search', 'skill_load']\n\n";
212
+ if (source.includes(residentOriginal)) source = source.replace(residentOriginal, "");
213
+ const unlockedStart = source.indexOf("/**\n * Tool names the model explicitly unlocked via `dev_tool_search`");
214
+ const unlockedEnd = source.indexOf(" /** Narrow the assembled catalog", unlockedStart);
215
+ if (unlockedStart >= 0 && unlockedEnd > unlockedStart) source = source.slice(0, unlockedStart) + source.slice(unlockedEnd);
216
+ const headerStart = source.indexOf(" * POST-PROMOTION RESIDENT SET");
217
+ const headerEnd = source.indexOf(" * COMPACTION (local addition)", headerStart);
218
+ if (headerStart >= 0 && headerEnd > headerStart) {
219
+ const headerReplacement = [
220
+ " * POST-PROMOTION CATALOG (prompt-tool patch): after promotion both modes",
221
+ " * keep the assembled catalog. `usePtcMode` switches the wire presentation",
222
+ " * to Code Mode (PTC, single run_code) instead of narrowing the resident set.",
223
+ " * The controlled phase below still narrows the catalog before promotion and",
224
+ " * after compaction.",
225
+ ""
226
+ ].join(String.fromCharCode(10));
227
+ source = source.slice(0, headerStart) + headerReplacement + source.slice(headerEnd);
228
+ }
229
+ return source;
230
+ }
133
231
  function buildCordis(prompt, options = {}) {
134
232
  const anchorFirstTurn = options.anchorFirstTurn === true;
135
233
  const anchorCustom = options.anchorCustom === true;
136
234
  const guideCustom = options.guideCustom === true;
137
235
  const guideText = typeof options.guideText === "string" && options.guideText.length > 0 ? options.guideText : "";
138
236
  const injectPrompt = options.injectPrompt !== false;
237
+ const usePtcMode = options.usePtcMode !== false;
139
238
  const anchorText = typeof options.anchorText === "string" && options.anchorText.length > 0 ? options.anchorText : "";
140
239
  const indent = (s) => s.split(/\r?\n/).map((l) => l.length === 0 ? "" : " " + l).join("\n");
141
- const up = applyBootstrapMaxTokens(normalizeContextGate(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")), options.bootstrapMaxTokens);
240
+ const up = applyToolBootstrapOptions(applySubagentFlash(readFileSync(CORDIS_TEMPLATE, "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"), usePtcMode, options.bootstrapMaxTokens);
142
241
  const bootstrap = /^-\s+id:\s+tool-bootstrap\s*$/m.exec(up);
143
- if (!bootstrap) throw new Error("vendor agent.cordis.yml missing tool-bootstrap row");
242
+ if (!bootstrap) throw new Error("preset/agent.cordis.yml missing tool-bootstrap row");
144
243
  let cursor = up.indexOf("\n", bootstrap.index);
145
- if (cursor < 0) throw new Error("vendor agent.cordis.yml has no top-level row after tool-bootstrap");
244
+ if (cursor < 0) throw new Error("preset/agent.cordis.yml has no top-level row after tool-bootstrap");
146
245
  cursor += 1;
147
246
  let insertAt = -1;
148
247
  while (cursor <= up.length) {
@@ -155,7 +254,7 @@ function buildCordis(prompt, options = {}) {
155
254
  if (lineEnd < 0) break;
156
255
  cursor = lineEnd + 1;
157
256
  }
158
- if (insertAt < 0) throw new Error("vendor agent.cordis.yml has no top-level row after tool-bootstrap");
257
+ if (insertAt < 0) throw new Error("preset/agent.cordis.yml has no top-level row after tool-bootstrap");
159
258
  const head = up.slice(0, insertAt);
160
259
  const tail = up.slice(insertAt);
161
260
  const separator = head.endsWith("\n\n") ? "" : head.endsWith("\n") ? "\n" : "\n\n";
@@ -191,4 +290,4 @@ function buildCordis(prompt, options = {}) {
191
290
  return out;
192
291
  }
193
292
  //#endregion
194
- export { buildCordis, parseFrontmatter };
293
+ export { buildCordis, parseFrontmatter, patchToolBootstrap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-prompt-tool",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "DSH 插件:提示词工具,规范模型的思维链与回答内容,提供 Web UI 编辑 preset.md 与 AGENTS.md,并按 skills 目录注册可开关技能。",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,30 +36,30 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
40
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
41
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
42
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
43
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.6",
44
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
45
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
46
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
47
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
40
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
41
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
42
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
43
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
44
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
45
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
46
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
47
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.7",
48
48
  "@deepseek-ai/schemastery": "^3.18.1",
49
49
  "react": "^18.2.0",
50
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6"
50
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.7"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@deepseek-ai/cordis": "^4.0.1",
54
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
55
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
56
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
57
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
58
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
59
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
60
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
61
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
62
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
54
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
55
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
56
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
57
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
58
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
59
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
60
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
61
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
62
+ "@deepseek-ai/dsh-skill": "^0.1.0-rc.7",
63
63
  "@types/node": "^26.2.0",
64
64
  "@types/react": "~18.3.1",
65
65
  "clsx": "^2.1.1",
package/plan.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # dsh-plugin-prompt-tool 重新设计计划(v2)
2
- > **v2.15 上游同步(当前实现)**:同步上游到 `25f21ae`;移除
2
+ > **v2.19 preset 脚本审查清理(当前实现)**:抽取 `shared.mjs` 公共工具;
3
+ > router-first-turn 不再重复清 contexts(交给 context-gate);router-first-turn /
4
+ > router-guide / prompt-injector 统一使用 epoch-aware 晋升状态;修正 stale 注释与未用变量。
5
+
6
+ > **v2.18 全部预设脚本自有化**:`preset/` 新增 shared / context-gate /
7
+ > compaction-epoch / custom-bash / instruction-hint / skill-search / tool-bootstrap 最终快照;
8
+ > 公共配置校验、晋升解析、消息工具合并进 `shared.mjs`;运行时不再直读 `upstream/` 任何文件,
9
+ > `upstream/` 仅作溯源与 sync 对照。
10
+
11
+ > **v2.17 agent.cordis.yml 脱离上游**:新增自有模板 `preset/agent.cordis.yml`;
12
+ > 运行时不再直读 `upstream/.../agent.cordis.yml`,动态项仅由 `buildCordis` 注入。
13
+
14
+ > **v2.16 使用 PTC 模式开关**:新增 `usePtcMode`(默认开启)。
15
+ > 开启=晋升后把 wire 切换为 Code Mode(PTC,单一 run_code),完整插件工具经生成 SDK 调用;
16
+ > 关闭=晋升后恢复原生完整工具目录;两种模式都不再依赖 `dev_tool_search`,生成 preset 时直接移除该行且不复制 `dev-tool-search.mjs`。
17
+
18
+ > **v2.15 上游同步**:同步上游到 `25f21ae`;移除
3
19
  > `customBashPath` 生成时改写,直接沿用上游 custom-bash 运行时探测。
4
20
  > 新增 `bootstrapMaxTokens` 开关(0=关闭;正整数写入上游 tool-bootstrap)。
5
21
 
@@ -61,8 +77,8 @@ artifacts/(本地调试残留)。
61
77
  | 首轮输出预算 | `bootstrapMaxTokens: 1024` 固定 | **opt-in,默认无 cap**(adapter 默认 256000 流过) | 实测(issue #11):Minimal schema 在 256000 下锚定 5/5(`We need` 首行、`we` 1.4、`let me` 0.0),而 pwsh/read 等标准家族 schema 在 256000 下 11/11 全失败——**工具 schema 是决定性变量,不是 cap** |
62
78
  | agent.cordis.yml 结构 | 无 Minimal 组 | 新增 `persistent-shell` 组(dsh-terminal + dsh-terminal-bash + dsh-tool-bash-persistent)与 `bootstrap-filesystem` 组(dsh-fs-local + dsh-tool-str-replace-editor),与官方 minimal 字节一致;`tool-bash` 行全平台禁用(避免 `bash` 名重复注册) | 完全复制;prompt-injector 仍插在 tool-bootstrap 行后 |
63
79
  | tool-bootstrap 参数 | `shellTools`+`commonTools` | `bootstrapTools: [bash, str_replace_editor]`;cap 监听器仅在显式设置 cap 时注册 | prompt-injector 逻辑不变(we 检测 + promoted 检测与参数无关) |
64
- | 验证工具 | 无 | `verify/run-verify.mjs` + `verify-runner.mjs`:preset-roster compose + CLI driver,`--stop-after-first-assistant` 只测首请求(秒级) | 本地 harness 源码未构建(`apps/cli/lib/bin.js` 不存在),暂继续用 prebuilt rc.6 的 dsh web HTTP API;将来构建 harness 后可切换 |
65
- | cap 行为差异 | — | rc.5 源码 checkout 上 cap 能到达首请求;rc.6 prebuilt 包会覆盖为 adapterDefaults | 与我们无关(默认不设 cap) |
80
+ | 验证工具 | 无 | `verify/run-verify.mjs` + `verify-runner.mjs`:preset-roster compose + CLI driver,`--stop-after-first-assistant` 只测首请求(秒级) | 本地 harness 源码未构建(`apps/cli/lib/bin.js` 不存在),暂继续用 prebuilt rc.7 的 dsh web HTTP API;将来构建 harness 后可切换 |
81
+ | cap 行为差异 | — | rc.5 源码 checkout 上 cap 能到达首请求;rc.6 prebuilt 包会覆盖为 adapterDefaults;rc.7 修复 max-tokens 截断后的 replay 续跑 | 默认不设 cap;开启 bootstrapMaxTokens 时 rc.7 更稳 |
66
82
 
67
83
  结论:**设计骨架不变**(完全复制原版 + prompt-injector 附加件),只是复制源更新为
68
84
  新版文件,测试断言从 `pwsh/read + 1024` 改为 `bash/str_replace_editor + 无 cap`。