dsh-paoding 0.3.6 → 0.3.8

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.
@@ -6,7 +6,11 @@
6
6
  * (renderCustomRoleBlocks / firstLine / insertCustomRoles)、roles_remove 委派行
7
7
  * span 删除(delegationRowSpan / composeMainPersonaEdit)、角色专用模型注入
8
8
  * (injectRoleAgentOptions,roles.<toolName>.model / .provider → 内置角色委派块
9
- * 的 agentOptions 子块)、角色可续模式改写/兜底注入(injectRoleBackgroundMode,
9
+ * 的 agentOptions 子块)、角色启动方式改写(injectRoleStartMode,
10
+ * roles.<toolName>.start_mode = 'fork' → 内置角色委派块的块级 provider 行原位
11
+ * 改写为 fork;'fresh' 不动源行 —— 源模板四块各预置 `provider: spawn`,全默认
12
+ * 产物与源零 diff;块内无该行时不兜底注入,与 backgroundMode 的兜底口径不同,
13
+ * 见函数注释)、角色可续模式改写/兜底注入(injectRoleBackgroundMode,
10
14
  * roles.<toolName>.background_mode = 'continuable' → 内置角色委派块的
11
15
  * backgroundMode 行:源模板已预置该行时原位改写其值,块内无该行时在 toolName
12
16
  * 行行尾兜底注入;'one-shot' 不动源行 —— 源模板四块各预置
@@ -16,6 +20,9 @@
16
20
  * allow 过滤走白名单语义(usableWith / presetUniverse ∪ inventory,见
17
21
  * filterUsableTools 上方说明):手写进配置、既不在 preset 自带工具面也不在
18
22
  * 检测库存的名字一律从 allow 丢弃,不再「非 host 依赖名直通」。
23
+ * workflow worker 行按宿主版本生成期适配(adaptWorkflowWorkerRow:≥0.1.6 把
24
+ * 0.1.5-only 的 workflow-worker-thread 条目换成 workflow-ptc;探测不到版本
25
+ * 保守保留源行,产物零 diff 性质随之保留)。
19
26
  * 自定义角色另有三道防护/自动化:toolName 保留名校验(assertCustomToolName,
20
27
  * 内置角色 / restrictBase / subagent* / mcp__ 前缀 / YAML 字面量一律 throw);
21
28
  * tools 为空时拒绝安装零工具角色(renderCustomRoleBlocks 直接 throw,防
@@ -29,6 +36,43 @@
29
36
  import { DEFAULT_MAIN_AGENT_PERSONA_EXTRA, ROLES, warn } from './util.mjs'
30
37
  import { normalizeModelRef, yamlScalar } from './config.mjs'
31
38
  import { isHostDependent } from './host.mjs'
39
+ import { isPtcWorkerHost } from './preset-system.mjs'
40
+
41
+ // ── workflow worker 行的宿主版本适配 ─────────────────────────────────────────
42
+
43
+ // 源模板里 workflow worker 行的原样块(delegation 组 config 内,4 空格条目缩进)。
44
+ // 宿主 ≥0.1.6 时整块换成 workflow-ptc(0.1.6-alpha.1 起
45
+ // @deepseek-ai/dsh-workflow-worker-thread 包已删除,残留行让 preset 的
46
+ // delegation 组报 never started,0.2.0 Plugin Manager 显示「加载失败」;
47
+ // workflow-ptc 是 0.1.6–0.2.0 各版 standard 组合的统一形态,ptcRuntime 服务由
48
+ // base bundle 提供,preset 内不需要再列 ptc-runtime 行)。行不在任何 span 定位
49
+ // 区间内,调用点在全部 edits 应用之后,与偏移量零交互。
50
+ const WORKER_THREAD_BLOCK = [
51
+ ' - id: workflow-worker-thread',
52
+ " name: '@deepseek-ai/dsh-workflow-worker-thread'",
53
+ ' config:',
54
+ ' provider: spawn',
55
+ ].join('\n')
56
+ const WORKER_PTC_BLOCK = [
57
+ ' - id: workflow-ptc',
58
+ " name: '@deepseek-ai/dsh-workflow-ptc'",
59
+ ' config:',
60
+ ' provider: spawn',
61
+ ].join('\n')
62
+
63
+ /**
64
+ * Host-version adaptation of the delegation group's workflow worker row:
65
+ * ≥0.1.6 swaps the 0.1.5-only workflow-worker-thread entry for workflow-ptc
66
+ * (see isPtcWorkerHost). A missing source block means template drift — a hard
67
+ * error, mirroring how missing role blocks abort generation.
68
+ */
69
+ export function adaptWorkflowWorkerRow(text, hostVersionTriple) {
70
+ if (!isPtcWorkerHost(hostVersionTriple)) return text
71
+ if (!text.includes(WORKER_THREAD_BLOCK)) {
72
+ throw new Error('workflow-worker-thread block not found in source agent.cordis.yml')
73
+ }
74
+ return text.replace(WORKER_THREAD_BLOCK, WORKER_PTC_BLOCK)
75
+ }
32
76
 
33
77
  // ── restrict.mjs main-agent allow base ──────────────────────────────────────
34
78
 
@@ -76,6 +120,35 @@ function normalizeRoleModel(toolName, rawModel, rawProvider) {
76
120
  return { model, provider }
77
121
  }
78
122
 
123
+ /** 角色启动方式兜底(自定义块渲染与内置角色注入两处共用):assignments 可能绕过
124
+ * config 规范化(UI 手工构造 / 测试直传),compose 层按「恰好 'fork' 才认」口径
125
+ * 兜底 —— 与 background_mode 的「非 'continuable' 即 one-shot」同款;其余一律
126
+ * 'fresh'(零 diff 不动源行)。 */
127
+ function roleStartModeOf(role) {
128
+ return role?.start_mode === 'fork' ? 'fork' : 'fresh'
129
+ }
130
+
131
+ /**
132
+ * 角色专用模型解析(fork 互斥判定版,normalizeRoleModel 的调用方统一走这里):
133
+ * 先归一 model / provider(含 provider-alone warn),再判 fork 互斥 ——
134
+ * start_mode: 'fork' 的子 agent 以主 agent(父级)的提供方与模型启动(DSH fork
135
+ * 后端为 KV cache 复用的硬约束),专用模型不生效:warn 说明后按缺省处理(不注入
136
+ * agentOptions),与 provider-alone 的「warn 后忽略」同款。fork + provider-alone
137
+ * 不叠加 fork warn(provider-alone 已在 normalizeRoleModel 里 warn 过)。'fresh'
138
+ * 行为与 normalizeRoleModel 完全一致。
139
+ */
140
+ function resolveRoleModel(toolName, rawModel, rawProvider, startMode) {
141
+ const { model, provider } = normalizeRoleModel(toolName, rawModel, rawProvider)
142
+ if (startMode === 'fork' && model !== null) {
143
+ warn(
144
+ `roles.${toolName}: start_mode: 'fork' 的子 agent 继承主 agent 的模型,` +
145
+ `专用模型 '${model}' 不生效,已忽略(agentOptions 不注入)`,
146
+ )
147
+ return { model: null, provider: null }
148
+ }
149
+ return { model, provider }
150
+ }
151
+
79
152
  /**
80
153
  * 自定义角色 toolName 保留名校验:与内置角色(ROLES)、restrict.mjs 主 agent
81
154
  * allow 白名单(restrictBase)、顶层 subagent / subagent_fork 撞名的 toolName
@@ -163,14 +236,19 @@ export function renderCustomRoleBlocks(roles, skillsMap, restrictBase = []) {
163
236
  toolName,
164
237
  skillsMap,
165
238
  )
166
- // 角色专用模型与可续模式(roles.<toolName>.model / .provider / .background_mode):
167
- // 自定义角色与内置角色同形状(provider: spawn / toolName / agentOptions? /
168
- // backgroundMode / persona / toolFilter)。先归一再判 provider-alone(防 UI /
169
- // 手工构造的 assignments 绕过规范化);仅 model 非 null 才插 agentOptions 子块,
170
- // provider-alone 仅 warn 忽略。backgroundMode 行恒写(与内置块源模板预置口径
171
- // 一致:行始终可见),continuable → continuable,one-shot/缺省/异常值 →
172
- // one-shot(与 config 解析回落口径一致)。
173
- const { model, provider } = normalizeRoleModel(toolName, role.model ?? null, role.provider ?? null)
239
+ // 角色专用模型、启动方式与可续模式(roles.<toolName>.model / .provider /
240
+ // .start_mode / .background_mode):自定义角色与内置角色同形状(provider /
241
+ // toolName / agentOptions? / backgroundMode / persona / toolFilter)。启动方式
242
+ // 恰好 'fork' 才认(其余一律 fresh,见 roleStartModeOf):fork 时块级
243
+ // provider 行落 fork(子 agent 以主 agent 已完成的对话轮次为初始内容启动),
244
+ // 否则恒 spawn。先归一再判 provider-alone(防 UI / 手工构造的 assignments 绕过
245
+ // 规范化);fork 与专用模型互斥(fork 子 agent 继承主 agent 的提供方与模型),
246
+ // 经 resolveRoleModel 判定 —— 命中 warn 后按缺省处理,仅 model 非 null 才插
247
+ // agentOptions 子块,provider-alone 仅 warn 忽略。backgroundMode 行恒写(与
248
+ // 内置块源模板预置口径一致:行始终可见),continuable → continuable,
249
+ // one-shot/缺省/异常值 → one-shot(与 config 解析回落口径一致)。
250
+ const startMode = roleStartModeOf(role)
251
+ const { model, provider } = resolveRoleModel(toolName, role.model ?? null, role.provider ?? null, startMode)
174
252
  // agentOptions 子行(键序固定 provider 在前、model 在后,与 dsh-tool-subagent
175
253
  // Config 字段序一致);model 缺省时整块不出现。
176
254
  const agentOptionsLines = []
@@ -188,7 +266,9 @@ export function renderCustomRoleBlocks(roles, skillsMap, restrictBase = []) {
188
266
  ` - id: delegation-${toolName}`,
189
267
  ` name: '@deepseek-ai/dsh-tool-subagent'`,
190
268
  ' config:',
191
- ' provider: spawn',
269
+ // 块级 provider = 启动方式(spawn 全新启动 / fork 继承主 agent 已完成
270
+ // 轮次),与 10 空格缩进的 agentOptions.provider(LLM 路由)分属两处。
271
+ ` provider: ${startMode === 'fork' ? 'fork' : 'spawn'}`,
192
272
  // toolName / 工具条目过 yamlScalar:纯数字、YAML 字面量形态等歧义名
193
273
  // 必须带引号落盘,否则委派行会被解析成数字/布尔/null。
194
274
  ` toolName: ${yamlScalar(toolName)}`,
@@ -357,11 +437,12 @@ export function delegationRowSpan(srcText, role) {
357
437
  }
358
438
 
359
439
  /**
360
- * 内置角色委派块边界定位(roleToolNameLineEnd / injectRoleBackgroundMode 共用):
440
+ * 内置角色委派块边界定位(roleToolNameLineEnd / injectRoleStartMode /
441
+ * injectRoleBackgroundMode 共用):
361
442
  * 块定位 `- id: delegation-<rowId>`(rowId = 角色名 `_` 换 `-`,找不到 throw);
362
443
  * 块结束 = min(其后首个 `\n - id: delegation-` 下标, '# ── remaining
363
444
  * model-facing rows' 注释下标, srcText.length),保证块内行搜索不越界命中
364
- * 别的块。errorKey 用于把报错归因到具体配置键(model / background_mode)。
445
+ * 别的块。errorKey 用于把报错归因到具体配置键(model / start_mode / background_mode)。
365
446
  */
366
447
  function roleDelegationBlockBounds(srcText, role, errorKey) {
367
448
  const rowId = role.replace(/_/g, '-') // 委派行 id 用连字符(toolName 用下划线)
@@ -463,6 +544,37 @@ export function injectRoleBackgroundMode(srcText, role) {
463
544
  return { start: lineEnd, end: lineEnd, text: '\n backgroundMode: continuable' }
464
545
  }
465
546
 
547
+ /**
548
+ * 角色启动方式改写(roles.<toolName>.start_mode = 'fork' 生效路径):把该内置
549
+ * 角色委派块内 8 空格缩进的块级 `provider: spawn` 行原位改写为
550
+ * `provider: fork`,子 agent 以主 agent(父级)已完成的对话轮次为初始内容启动。
551
+ * 锚点定位复用 roleDelegationBlockBounds,行搜索限定块内(不越界命中别的块);
552
+ * 行首带 \n 且恰 8 空格,10 空格缩进的 agentOptions.provider 子行(LLM 路由)
553
+ * 形态不匹配,天然区分 —— 块级 provider(启动提供方)与 agentOptions 子行
554
+ * (模型路由)分属两处,互不相干。
555
+ * 返回 span edit { start, end, text }(整行原位改写,坐标为源文本原坐标,与
556
+ * allow/persona/删除 edits 一起按 start 降序应用)。无任何改动时返回 null:
557
+ * - 'fresh'(含缺省):不动源行 —— 源模板四块各预置 `provider: spawn`,全默认
558
+ * 配置的生成产物与源逐字节一致(零 diff 不变量);
559
+ * - 行已是 fork:幂等返回 null;
560
+ * - 块内无 provider 行:不兜底注入,静默保留原样 —— 与 injectRoleBackgroundMode
561
+ * 的「缺行兜底注入」口径刻意不同:backgroundMode 是产品旋钮(源模板为可见性
562
+ * 预置),而块级 provider 行是委派块的骨架配置,缺失即模板损坏;半截注入
563
+ * 救不了坏模板,静默保留比产出难排查的配置更稳妥,模板漂移由恒等性金测兜底。
564
+ * 调用方(composeGenerated)仅对 'fork' 调本函数。
565
+ */
566
+ export function injectRoleStartMode(srcText, role) {
567
+ const { idx, blockEnd } = roleDelegationBlockBounds(srcText, role, 'start_mode')
568
+ // 块内 span 查块级 provider 行:8 空格缩进(config 键层),行首带 \n 保证不误
569
+ // 匹配块首部分行或 persona 内容行;锚点含 'spawn' 值,10 空格的
570
+ // agentOptions.provider(其缩进多两格,前缀不命中)天然被排除。
571
+ const providerMarker = '\n provider: spawn'
572
+ const providerIdx = srcText.indexOf(providerMarker, idx)
573
+ if (providerIdx === -1 || providerIdx >= blockEnd) return null
574
+ const lineStart = providerIdx + 1 // 跳过行首 \n,定位到行首
575
+ return { start: lineStart, end: lineStart + providerMarker.length - 1, text: ' provider: fork' }
576
+ }
577
+
466
578
  /**
467
579
  * 主 persona 整块改造(删委派 bullet + 自定义角色自动委派行),返回一个
468
580
  * span edit { start, end, text }(start/end 为源文本原坐标、随其它 edits 降序应用),
@@ -559,7 +671,7 @@ export function filterUsableTools(tools, presetUniverse, inventory) {
559
671
  * block / restrict config.allow injection) plus the per-role diff info for
560
672
  * the report.
561
673
  */
562
- export function composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas = {}) {
674
+ export function composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas = {}, hostVersionTriple = null) {
563
675
  const skillsMap = assignments.skills ?? {}
564
676
  const customToolNames = Object.keys(assignments.roles).filter((n) => !ROLES.includes(n))
565
677
  // roles_remove:被删除的内置角色不产 allow/persona edits(委派行整条删除时避免重叠),
@@ -669,22 +781,31 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
669
781
  const personaEdit = composeMainPersonaEdit(srcText, removedRoles, customBullets)
670
782
  if (personaEdit !== null) edits.push(personaEdit)
671
783
 
672
- // 角色专用模型 / 可续模式注入(roles.<toolName>.model / .provider → 委派块
673
- // agentOptions;roles.<toolName>.background_mode = 'continuable' → 委派块
674
- // backgroundMode 行改写/兜底注入):与 roles_remove 委派行删除 edits 同段收集。
675
- // 被删角色不注入(委派行已整条删除,模型 / background_mode 配置 warn 忽略);
676
- // 仅 model 非 null 才注入 agentOptions,provider-alone 仅 warn 忽略(先 normalize
677
- // 再判断,防手工构造的 assignments 绕过规范化);background_mode 仅 'continuable'
784
+ // 角色专用模型 / 启动方式 / 可续模式注入(roles.<toolName>.model / .provider →
785
+ // 委派块 agentOptions;roles.<toolName>.start_mode = 'fork' → 委派块块级
786
+ // provider 行原位改写 fork;roles.<toolName>.background_mode = 'continuable' →
787
+ // 委派块 backgroundMode 行改写/兜底注入):与 roles_remove 委派行删除 edits 同段
788
+ // 收集。被删角色不注入(委派行已整条删除,模型 / start_mode / background_mode
789
+ // 配置 warn 忽略);fork 与专用模型互斥(fork 子 agent 继承主 agent 的提供方与
790
+ // 模型,KV cache 复用硬约束),经 resolveRoleModel 判定 —— 命中 warn 后按缺省
791
+ // 处理(不注入 agentOptions),provider-alone 仅 warn 忽略(先 normalize 再判断,
792
+ // 防手工构造的 assignments 绕过规范化);start_mode 仅 'fork' 才调
793
+ // injectRoleStartMode('fresh'/缺省不调 —— 源模板四块各预置的 `provider: spawn`
794
+ // 原样保留,全默认配置生成产物与源逐字节一致),background_mode 仅 'continuable'
678
795
  // 才调 injectRoleBackgroundMode('one-shot'/缺省不调 —— 源模板四块各预置的
679
796
  // `backgroundMode: one-shot` 原样保留,全默认配置生成产物与源逐字节一致)。
680
797
  // 键序与收集序:源块已有该行时走改写路径(edit start = 既有行行首,位于
681
798
  // toolName 行行尾之后 → 降序应用先改写该行,agentOptions 随后插在更前的
682
799
  // toolName 行行尾,键序 toolName → agentOptions? → backgroundMode 自然成立);
683
- // 源块无该行时走兜底注入路径,与 agentOptions 同锚点(start 相等)→ backgroundMode
800
+ // startMode 改写的 provider 行在 toolName 行之前(块内键序 provider → toolName),
801
+ // edit start 更小 → 降序应用最后生效,与上述键序互不干扰;源块无该行时走兜底
802
+ // 注入路径,与 agentOptions 同锚点(start 相等)→ backgroundMode
684
803
  // edit 必须先于 agentOptions edit 收集(先收集者居后),与自定义块键序一致。
685
804
  for (const role of ROLES) {
686
805
  const rawModel = assignments.roles[role]?.model ?? null
687
806
  const rawProvider = assignments.roles[role]?.provider ?? null
807
+ // compose 层兜底:恰好 'fork' 才认,其余(缺省/异常值)一律 fresh(见 roleStartModeOf)。
808
+ const startMode = roleStartModeOf(assignments.roles[role])
688
809
  const rawBackgroundMode = assignments.roles[role]?.background_mode ?? null
689
810
  if (removedRoles.includes(role)) {
690
811
  const model = normalizeModelRef(rawModel, `roles.${role}.model`)
@@ -692,6 +813,9 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
692
813
  if (model !== null || provider !== null) {
693
814
  warn(`roles.${role}.model: 该角色已在 roles_remove 中删除,模型配置被忽略`)
694
815
  }
816
+ if (startMode === 'fork') {
817
+ warn(`roles.${role}.start_mode: 该角色已在 roles_remove 中删除,start_mode 配置被忽略`)
818
+ }
695
819
  if (rawBackgroundMode === 'continuable') {
696
820
  warn(`roles.${role}.background_mode: 该角色已在 roles_remove 中删除,background_mode 配置被忽略`)
697
821
  }
@@ -701,8 +825,12 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
701
825
  const backgroundModeEdit = injectRoleBackgroundMode(srcText, role)
702
826
  if (backgroundModeEdit !== null) edits.push(backgroundModeEdit)
703
827
  }
704
- const { model, provider } = normalizeRoleModel(role, rawModel, rawProvider)
828
+ const { model, provider } = resolveRoleModel(role, rawModel, rawProvider, startMode)
705
829
  if (model !== null) edits.push(injectRoleAgentOptions(srcText, role, model, provider))
830
+ if (startMode === 'fork') {
831
+ const startModeEdit = injectRoleStartMode(srcText, role)
832
+ if (startModeEdit !== null) edits.push(startModeEdit)
833
+ }
706
834
  }
707
835
  // 注入 edit(零宽,位于 toolName 行行尾)与该块的 allow edit / persona edit
708
836
  // 区间天然不相交:委派块内 toolName 行先于 persona / toolFilter 出现,插入点
@@ -713,6 +841,9 @@ export function composeGenerated(srcText, blocks, personaBlocks, assignments, in
713
841
  edits.sort((a, b) => b.start - a.start)
714
842
  for (const edit of edits) out = out.slice(0, edit.start) + edit.text + out.slice(edit.end)
715
843
 
844
+ // workflow worker 行宿主版本适配(≥0.1.6 换 workflow-ptc;见 adaptWorkflowWorkerRow)。
845
+ out = adaptWorkflowWorkerRow(out, hostVersionTriple)
846
+
716
847
  const customBlock = renderCustomRoleBlocks(assignments.roles, skillsMap, restrictBase)
717
848
  if (customBlock !== '') out = insertCustomRoles(out, customBlock)
718
849
 
@@ -2,10 +2,12 @@
2
2
  * dsh-paoding.config.yml 键规范化与读写:main_agent_display_name /
3
3
  * roles.<toolName>.name /
4
4
  * roles.<toolName>.model / roles.<toolName>.provider(角色专用模型,生成层映射到
5
- * dsh-tool-subagent 的 agentOptions)/ roles.<toolName>.background_mode(可续模式,
5
+ * dsh-tool-subagent 的 agentOptions)/ roles.<toolName>.start_mode(角色启动方式,
6
+ * 生成层把内置委派块的块级 provider 行改写为 fork,'fresh' | 'fork')/
7
+ * roles.<toolName>.background_mode(可续模式,
6
8
  * 生成层映射到 dsh-tool-subagent 的 backgroundMode,'one-shot' | 'continuable')/
7
9
  * roles_remove 解析(normalizeRoleName /
8
- * normalizeModelRef / normalizeBackgroundMode / parseRolesRemove / yamlScalar)、
10
+ * normalizeModelRef / normalizeBackgroundMode / normalizeStartMode / parseRolesRemove / yamlScalar)、
9
11
  * 目标字段集合的规范化与写出(normalizeTargetFields / writeTargetFields —— 顶层与
10
12
  * workspaces.<path> 条目共用同一套)、按工作区条目段(normalizeWorkspaces)、
11
13
  * 文件加载与规范化(loadConfig / normalizeConfig)、序列化与落盘(serializeConfig /
@@ -86,6 +88,27 @@ export function normalizeBackgroundMode(value, label = 'background_mode') {
86
88
  return value
87
89
  }
88
90
 
91
+ /**
92
+ * 校验并规范化 roles.<toolName>.start_mode(角色启动方式,映射到 dsh-tool-subagent
93
+ * 委派块的块级 provider 行:'fresh' = spawn 全新启动,'fork' = 以主 agent 已完成的
94
+ * 对话轮次为初始内容启动)。合法值仅 'fresh' | 'fork':缺省/null → 缺省值
95
+ * 'fresh'(静默,与 name/model/provider 的 null 口径一致);非字符串或字符串但
96
+ * 不在合法值集 → warn 后回落 'fresh'(与 normalizeBackgroundMode 的非法值处理
97
+ * 同款:类型/取值不对降级提醒而非中断加载)。返回恒为合法值之一,生成层
98
+ * (compose)据此决定是否把块级 provider 行改写为 fork。
99
+ * 注意:本键与 roles.<toolName>.provider(LLM 路由,与 model 配对映射到
100
+ * agentOptions 子行)毫无关系 —— 子 agent 的启动提供方只有委派块里那行块级
101
+ * provider,本键是它的配置面。
102
+ */
103
+ export function normalizeStartMode(value, label = 'start_mode') {
104
+ if (value === null || value === undefined) return 'fresh'
105
+ if (typeof value !== 'string' || (value !== 'fresh' && value !== 'fork')) {
106
+ warn(`${label}: 非法值已忽略(${JSON.stringify(value)}),回落 'fresh'(可选: 'fresh' / 'fork')`)
107
+ return 'fresh'
108
+ }
109
+ return value
110
+ }
111
+
89
112
  /**
90
113
  * 解析配置键 roles_remove:string[],只收 ROLES 内置角色名(search_external /
91
114
  * design / implement);含其他名字 warn 并忽略;去重保序。非数组 → []。
@@ -138,7 +161,7 @@ function yamlFlowScalar(value) {
138
161
  /**
139
162
  * Load the config file when present. Returns null when the file does not
140
163
  * exist; throws when it exists but cannot be parsed. Shape:
141
- * { profile, roles: { <toolName>: { name?, model?, provider?, background_mode?, persona?, tools[] } },
164
+ * { profile, roles: { <toolName>: { name?, model?, provider?, start_mode?, background_mode?, persona?, tools[] } },
142
165
  * roles_remove[] (内置角色删除), main_agent_extra[], main_agent_remove[],
143
166
  * main_agent_skills[], main_agent_skills_inline[],
144
167
  * main_agent_persona_extra? (string|null),
@@ -220,7 +243,7 @@ function normalizeWorkspaces(raw) {
220
243
 
221
244
  /**
222
245
  * 目标字段集合规范化:顶层(除 profile 外)与 workspaces.<path> 条目共用的
223
- * 同一套键 —— roles(条目含 background_mode)/ roles_remove / main_agent_extra(含 has_main_agent_extra
246
+ * 同一套键 —— roles(条目含 start_mode / background_mode)/ roles_remove / main_agent_extra(含 has_main_agent_extra
224
247
  * 内部标记)/ main_agent_remove / main_agent_skills / main_agent_skills_inline /
225
248
  * main_agent_persona_extra / main_agent_display_name / skills。
226
249
  * 语义与抽取前逐字一致;
@@ -243,18 +266,21 @@ export function normalizeTargetFields(raw) {
243
266
  warn(`roles.${toolName}.${key}: 非字符串值已忽略(${JSON.stringify(v)})`)
244
267
  }
245
268
  }
246
- // 角色条目字段顺序 { name?, model?, provider?, background_mode?, persona?, tools? }:
269
+ // 角色条目字段顺序 { name?, model?, provider?, start_mode?, background_mode?, persona?, tools? }:
247
270
  // name(roles.<toolName>.name,内置角色可选显示名)与主 agent 显示名同约束,
248
271
  // 仅字符串收(trim 后空/违规由 normalizeRoleName 处理);缺省/null → null
249
272
  // (= 保持该角色默认身份句)。model / provider(roles.<toolName>.model /
250
273
  // .provider,角色专用模型)镜像同款收法,违规由 normalizeModelRef 处理;
251
274
  // 缺省/null → null(= 不注入 agentOptions,子 agent 继承主 agent 当前模型)。
252
- // background_mode(roles.<toolName>.background_mode,角色可续模式)仅收
275
+ // start_mode(roles.<toolName>.start_mode,角色启动方式)仅收 'fresh' | 'fork',
276
+ // 其余 warn 回落 'fresh'(normalizeStartMode)。background_mode
277
+ // (roles.<toolName>.background_mode,角色可续模式)仅收
253
278
  // 'one-shot' | 'continuable',其余 warn 回落 'one-shot'(normalizeBackgroundMode)。
254
279
  roles[toolName] = {
255
280
  name: typeof raw.name === 'string' ? normalizeRoleName(raw.name, `roles.${toolName}.name`) : null,
256
281
  model: typeof raw.model === 'string' ? normalizeModelRef(raw.model, `roles.${toolName}.model`) : null,
257
282
  provider: typeof raw.provider === 'string' ? normalizeModelRef(raw.provider, `roles.${toolName}.provider`) : null,
283
+ start_mode: normalizeStartMode(raw.start_mode, `roles.${toolName}.start_mode`),
258
284
  background_mode: normalizeBackgroundMode(raw.background_mode, `roles.${toolName}.background_mode`),
259
285
  persona: typeof raw.persona === 'string' && raw.persona.trim() !== '' ? raw.persona : null,
260
286
  tools,
@@ -359,7 +385,7 @@ function writeTargetFields(lines, pad, target) {
359
385
  // roles.<toolName>.model / .provider:仅非 null 写出(null/缺省 = 不注入
360
386
  // agentOptions,子 agent 继承主 agent 当前模型);写前过一次 normalizeModelRef
361
387
  // (与上方 name 写法一致,防手工构造的 config 绕过规范化),字段序 name →
362
- // model → provider → background_mode → persona → tools 固定。
388
+ // model → provider → start_mode → background_mode → persona → tools 固定。
363
389
  if (role.model !== null && role.model !== undefined) {
364
390
  const model = normalizeModelRef(role.model, `roles.${toolName}.model`)
365
391
  if (model !== null) lines.push(`${pad} model: ${yamlScalar(model)}`)
@@ -368,6 +394,10 @@ function writeTargetFields(lines, pad, target) {
368
394
  const provider = normalizeModelRef(role.provider, `roles.${toolName}.provider`)
369
395
  if (provider !== null) lines.push(`${pad} provider: ${yamlScalar(provider)}`)
370
396
  }
397
+ // roles.<toolName>.start_mode:恒写出(含缺省 fresh)—— 与 background_mode 同
398
+ // 款「旋钮可见、读写往返幂等」口径(loadConfig 也恒收此键)。写前过一次判定
399
+ // (防手工构造的 config 绕过规范化):非 'fork' 一律按 'fresh' 落盘。
400
+ lines.push(`${pad} start_mode: ${role.start_mode === 'fork' ? 'fork' : 'fresh'}`)
371
401
  // roles.<toolName>.background_mode:恒写出(含缺省 one-shot)—— 配置文件里
372
402
  // 旋钮对用户可见、读写往返幂等(loadConfig 也恒收此键)。写前过一次判定
373
403
  // (防手工构造的 config 绕过规范化):非 'continuable' 一律按 'one-shot' 落盘。
@@ -20,8 +20,10 @@
20
20
  * 写盘并明确报错,与仓库「配置不可信时拒绝落盘」口径一致。
21
21
  * 4) 轨道迁移对账 backfillHomePatchRows / listOrchestratorPresetIds /
22
22
  * parseGeneratorMarker / formatGeneratorMarker:宿主只升 dsh 不动插件时
23
- * (标记版本一致、轨道翻转——升级最常见路径),按磁盘产物补写缺失声明行
24
- * 而不整盘重生成;生成器标记记「版本 + 轨道」两行,任一不符即触发自愈。
23
+ * (标记版本一致、轨道翻转或 workflow worker 形态换代——升级最常见路径),
24
+ * 按磁盘产物补写缺失声明行而不整盘重生成;生成器标记记「版本 + 轨道 +
25
+ * worker 形态」三行,任一不符即触发自愈(worker 形态换代必须整盘重生成,
26
+ * 见 detectHostVersionTriple / isPtcWorkerHost)。
25
27
  * 零运行时依赖(node:child_process / node:fs / node:path / node:url);被 state 引用。
26
28
  */
27
29
  import { spawnSync } from 'node:child_process'
@@ -55,16 +57,20 @@ export function parseVersionTriple(text) {
55
57
  return [Number(m[1]), Number(m[2]), Number(m[3])]
56
58
  }
57
59
 
58
- /** 三元组是否达到声明行轨(0.1.7+):逐段数值比较,prerelease 已在上游剥掉。 */
59
- export function isDeclarativeTriple(triple) {
60
- if (!Array.isArray(triple) || triple.length !== 3 || triple.some((n) => !Number.isInteger(n))) return false
61
- const base = [0, 1, 7]
60
+ /** 三元组逐段与基准比较:≥ 基准返回 true(prerelease 已在上游剥掉)。 */
61
+ function atOrAfterTriple(triple, base) {
62
62
  for (let i = 0; i < 3; i++) {
63
63
  if (triple[i] !== base[i]) return triple[i] > base[i]
64
64
  }
65
65
  return true
66
66
  }
67
67
 
68
+ /** 三元组是否达到声明行轨(0.1.7+):逐段数值比较,prerelease 已在上游剥掉。 */
69
+ export function isDeclarativeTriple(triple) {
70
+ if (!Array.isArray(triple) || triple.length !== 3 || triple.some((n) => !Number.isInteger(n))) return false
71
+ return atOrAfterTriple(triple, [0, 1, 7])
72
+ }
73
+
68
74
  /**
69
75
  * 探测当前宿主的预设安装轨道,返回 'declarative' | 'directory'。逐级降级:
70
76
  * 1) runtimeSystem 直通:Web 插件运行时对宿主服务的实时探测结果最权威;
@@ -536,10 +542,12 @@ export function backfillHomePatchRows(dshHome, presetIds) {
536
542
  }
537
543
 
538
544
  /**
539
- * 解析 .generator-version 标记文本为 { version, track }。v2 格式两行:首行
540
- * 生成器版本、次行预设安装轨道(declarative / directory);旧格式(单行版本,
541
- * 0.3.4 及以前)解析出 track: null —— 恰是「宿主轨道已翻转但产物版本未动」的
542
- * 迁移触发态。空文本 / 全空白返回 null(按未生成处理)。
545
+ * 解析 .generator-version 标记文本为 { version, track, worker }。v3 格式三行:
546
+ * 首行生成器版本、次行预设安装轨道(declarative / directory)、三行 workflow
547
+ * worker 形态(ptc / worker-thread);旧格式(v2 两行 = 0.3.6 及以前、单行 =
548
+ * 0.3.4 及以前)解析出 track / worker 为 null —— 恰是「宿主已换代但产物版本
549
+ * 未动」的迁移触发态(轨道翻转、worker 换代都靠它触发自愈)。空文本 / 全空白
550
+ * 返回 null(按未生成处理)。
543
551
  */
544
552
  export function parseGeneratorMarker(text) {
545
553
  const lines = String(text ?? '')
@@ -548,10 +556,49 @@ export function parseGeneratorMarker(text) {
548
556
  .filter((l) => l !== '')
549
557
  if (lines.length === 0) return null
550
558
  const track = lines[1] === 'declarative' || lines[1] === 'directory' ? lines[1] : null
551
- return { version: lines[0], track }
559
+ const worker = lines[2] === 'ptc' || lines[2] === 'worker-thread' ? lines[2] : null
560
+ return { version: lines[0], track, worker }
561
+ }
562
+
563
+ /**
564
+ * 生成 .generator-version 标记文本(版本 + 轨道 + worker 形态三行,v3 格式)。
565
+ * worker 取 WORKER_PTC / WORKER_WORKER_THREAD(允许调用方先探测再传入)。
566
+ */
567
+ export function formatGeneratorMarker(version, presetSystem, worker) {
568
+ return `${version}\n${presetSystem}\n${worker}\n`
552
569
  }
553
570
 
554
- /** 生成 .generator-version 标记文本(版本 + 轨道两行,v2 格式)。 */
555
- export function formatGeneratorMarker(version, presetSystem) {
556
- return `${version}\n${presetSystem}\n`
571
+ // ── workflow worker 形态(宿主版本 → 生成期换行)────────────────────────────
572
+
573
+ /** 生成产物里 workflow worker 行的两种形态(标记第三行的合法取值)。 */
574
+ export const WORKER_PTC = 'ptc'
575
+ export const WORKER_WORKER_THREAD = 'worker-thread'
576
+
577
+ /**
578
+ * 探测正在运行的宿主版本三元组:进程 argv 宿主版本(插件与宿主同进程时零开销
579
+ * 且反映正在运行的宿主)> PATH 上的 `dsh --version` > null。与
580
+ * detectPresetSystem 的前两级同源复用,但不再往下走文件探测——0.1.5 与 0.1.6
581
+ * 的磁盘形态没有可区分的单数包,探测不到就返回 null 由调用方按未知处理。
582
+ */
583
+ export function detectHostVersionTriple() {
584
+ const argvVersion = hostVersionFromArgv()
585
+ if (argvVersion !== null) return parseVersionTriple(argvVersion)
586
+ const versionText = dshVersionText()
587
+ if (versionText !== null) return parseVersionTriple(versionText)
588
+ return null
589
+ }
590
+
591
+ /**
592
+ * 该宿主版本的 workflow worker 行形态:≥0.1.6 用 @deepseek-ai/dsh-workflow-ptc
593
+ * (0.1.6-alpha.1 起 workflow-worker-thread 包已删除,残留行会让 preset 的
594
+ * delegation 组报 never started,0.2.0 Plugin Manager 显示为「加载失败」);
595
+ * 0.1.5 只认 @deepseek-ai/dsh-workflow-worker-thread。版本探测不到时返回 false
596
+ *(保守侧:保留源模板原行)——「探测不到」走不出插件运行时(argv 必命中),
597
+ * 多见于无 dsh 在 PATH 的原始环境;保守默认让 0.1.5 全场景可用,也与
598
+ * detectPresetSystem 的安全侧兜底哲学一致(产物零 diff 的 golden 性质随之保留:
599
+ * 显式 null 不改源文)。
600
+ */
601
+ export function isPtcWorkerHost(triple) {
602
+ if (!Array.isArray(triple) || triple.length !== 3 || triple.some((n) => !Number.isInteger(n))) return false
603
+ return atOrAfterTriple(triple, [0, 1, 6])
557
604
  }
@@ -48,6 +48,7 @@ import { composeGenerated, extractMainAgentAllow } from './compose.mjs'
48
48
  import { smartDefaults } from './alloc.mjs'
49
49
  import {
50
50
  buildPresetRowText,
51
+ detectHostVersionTriple,
51
52
  detectPresetSystem,
52
53
  listOrchestratorPresetIds,
53
54
  presetRowId,
@@ -232,6 +233,10 @@ export async function collectState({ dshHome, configFile, profile = null, patche
232
233
  //(见 preset-system.detectPresetSystem 的降级链)。
233
234
  const presetSystem = await detectPresetSystem({ dshHome, runtimeSystem: runtimeFacts?.presetSystem ?? null })
234
235
 
236
+ // 宿主版本三元组(argv 宿主版本 > dsh --version > null,探测不到按未知处理):
237
+ // workflow worker 行的生成期形态判定(isPtcWorkerHost)与标记 worker 行都从这里取。
238
+ const hostVersionTriple = detectHostVersionTriple()
239
+
235
240
  // 已落盘 preset 目录扫描(编排类):.agent-presets 下的 orchestrator 基础
236
241
  // preset 与 orchestrator-<slug> 工作区专属 preset。扫描失败(目录尚不存在、
237
242
  // 无读权限等)一律降级为空列表,绝不影响检测结果本体;generateAndInstall
@@ -265,6 +270,8 @@ export async function collectState({ dshHome, configFile, profile = null, patche
265
270
  installedPresets,
266
271
  // 预设安装轨道(generateAndInstall 据此双轨收尾;serializeState 透传给面板)
267
272
  presetSystem,
273
+ // 宿主版本三元组(null = 探测不到):workflow worker 行生成期形态判定用
274
+ hostVersionTriple,
268
275
  // 主 persona 尾部追加的默认常量(配置键 main_agent_persona_extra 缺省值):
269
276
  // 供 UI state(serializeState → /api/paoding/state)展示与「恢复默认」用。
270
277
  mainPersonaExtraDefault: DEFAULT_MAIN_AGENT_PERSONA_EXTRA,
@@ -366,8 +373,9 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
366
373
  }
367
374
 
368
375
  // Compose the generated config; a composition error is a hard error.
369
- //(这层包装无增值,让 composeGenerated 的原始错误直接上抛。)
370
- const composed = composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas)
376
+ //(这层包装无增值,让 composeGenerated 的原始错误直接上抛。)宿主版本三元组
377
+ // 驱动 workflow worker 行的生成期换行(≥0.1.6 → workflow-ptc,null 按现代宿主)。
378
+ const composed = composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas, state?.hostVersionTriple ?? null)
371
379
  const generatedText = composed.text
372
380
  const roleResults = composed.roleResults
373
381