dsh-paoding 0.4.2 → 0.4.4

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.
@@ -58,6 +58,41 @@ export function parseVersionTriple(text) {
58
58
  return [Number(m[1]), Number(m[2]), Number(m[3])]
59
59
  }
60
60
 
61
+ /**
62
+ * 解析裸版本号文本为 { triple, prerelease }:三元组同 parseVersionTriple,
63
+ * prerelease 保留原文(`-` 与 `+` 之间,0.2.1-rc.2 → 'rc.2';正式版 null)。
64
+ * 角色委派行形态门(isPresetRoleHost)只在 0.2.1 这一个点上需要分辨
65
+ * prerelease——守卫是 0.2.1-rc.2 才落地的,rc.0/rc.1 仍认内联形状。
66
+ */
67
+ export function parseVersionInfo(text) {
68
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(text ?? '').trim())
69
+ if (!m) return null
70
+ return { triple: [Number(m[1]), Number(m[2]), Number(m[3])], prerelease: m[4] ?? null }
71
+ }
72
+
73
+ /** prerelease 逐 token 比较(数字 token 按数值,其余按字典序):a≥b 返回正数。 */
74
+ function comparePrerelease(a, b) {
75
+ const as = String(a).split('.')
76
+ const bs = String(b).split('.')
77
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
78
+ const av = as[i]
79
+ const bv = bs[i]
80
+ if (av === undefined) return -1 // 前缀更短 = 更小(1.0.0-alpha < 1.0.0-alpha.1)
81
+ if (bv === undefined) return 1
82
+ const an = /^\d+$/.test(av)
83
+ const bn = /^\d+$/.test(bv)
84
+ if (an && bn) {
85
+ const d = Number(av) - Number(bv)
86
+ if (d !== 0) return d
87
+ } else if (an !== bn) {
88
+ return an ? -1 : 1 // semver:数字 token 恒小于字母 token
89
+ } else if (av !== bv) {
90
+ return av < bv ? -1 : 1
91
+ }
92
+ }
93
+ return 0
94
+ }
95
+
61
96
  /** 三元组逐段与基准比较:≥ 基准返回 true(prerelease 已在上游剥掉)。 */
62
97
  function atOrAfterTriple(triple, base) {
63
98
  for (let i = 0; i < 3; i++) {
@@ -101,22 +136,35 @@ export async function detectPresetSystem({ dshHome, runtimeSystem = null } = {})
101
136
  }
102
137
 
103
138
  /**
104
- * 从当前进程 argv[1] 推断正在运行的宿主版本:沿脚本所在目录向上找最近的
105
- * package.json,name 是 @deepseek-ai/dsh 即取其 version。插件跑在宿主进程里,
106
- * 这比 PATH 上的 `dsh --version` 又快又准(无需子进程);找不到(CLI / 测试 /
107
- * 任意非 dsh 进程)返回 null。找到的第一个 package.json 不是 dsh 即止——
108
- * 向上穿越无关包目录没有意义。
139
+ * 计入 argv rung 的宿主包名(显式集合,禁止 `@deepseek-ai/dsh-` 前缀泛匹配:
140
+ * dsh-persona / dsh-tool-subagent 等插件包不是宿主,误认会让插件包版本冒充
141
+ * 宿主版本驱动版本门控)。desktop 宿主与其打包的 dsh runtime 同版本同轨
142
+ * (实测 0.2.1-rc.2 两侧一致),取 dsh-desktop-host 包版本即宿主版本。
143
+ */
144
+ const HOST_ARGV_PACKAGE_NAMES = new Set(['@deepseek-ai/dsh', '@deepseek-ai/dsh-desktop-host'])
145
+
146
+ /**
147
+ * 从进程 argv[1](entry 可注入供测试,默认 process.argv[1])推断正在运行的
148
+ * 宿主版本:沿脚本所在目录向上找最近的 package.json,name 是宿主包名
149
+ * (HOST_ARGV_PACKAGE_NAMES:CLI 宿主 @deepseek-ai/dsh,或 desktop 宿主
150
+ * @deepseek-ai/dsh-desktop-host)即取其 version。插件跑在宿主进程里,这比
151
+ * PATH 上的 `dsh --version` 又快又准(无需子进程);找不到(CLI / 测试 /
152
+ * 任意非宿主进程)返回 null。找到的第一个 package.json 不是宿主即止——向上
153
+ * 穿越无关包目录没有意义;中间层没有 package.json 则继续向上(宿主包缺
154
+ * 元数据时仍能命中外层 dsh 包)。desktop 宿主进程 argv[1] 指向
155
+ * .../node_modules/@deepseek-ai/dsh-desktop-host/lib/index.js,向上一级即
156
+ * 命中 dsh-desktop-host 包——此前只认 @deepseek-ai/dsh,desktop 宿主探测
157
+ * 落空、错误回落 PATH 上的 dsh CLI(版本可能远低于正在运行的宿主)。
109
158
  */
110
- function hostVersionFromArgv() {
159
+ export function hostVersionFromArgv(entry = process.argv[1]) {
111
160
  try {
112
- const entry = process.argv[1]
113
161
  if (typeof entry !== 'string' || entry === '') return null
114
162
  let dir = path.dirname(path.resolve(entry))
115
163
  for (let i = 0; i < 6 && dir !== path.dirname(dir); i++) {
116
164
  const pkgPath = path.join(dir, 'package.json')
117
165
  if (existsSync(pkgPath)) {
118
166
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
119
- return pkg?.name === '@deepseek-ai/dsh' ? String(pkg.version ?? '') : null
167
+ return HOST_ARGV_PACKAGE_NAMES.has(pkg?.name) ? String(pkg.version ?? '') : null
120
168
  }
121
169
  dir = path.dirname(dir)
122
170
  }
@@ -126,7 +174,14 @@ function hostVersionFromArgv() {
126
174
  }
127
175
  }
128
176
 
129
- /** `dsh --version` 取裸版本号文本;任何失败(ENOENT / 超时 / 非零退出)返回 null。 */
177
+ /**
178
+ * argv rung 之后的 CLI 回落:PATH 上 `dsh --version` 取裸版本号文本;任何失败
179
+ * (ENOENT / 超时 / 非零退出)返回 null。只在 argv 未命中宿主包时才会走到
180
+ * 这里——desktop 宿主 argv[1] 命中 dsh-desktop-host 包,不会落到本级;而
181
+ * PATH 上的 dsh CLI 版本可能低于正在运行的宿主(用户升了 desktop、PATH 上
182
+ * 还是旧 CLI,实测 desktop 0.2.1-rc.2 在跑而 PATH dsh 是 0.2.0-rc.2),本级
183
+ * 结果只代表「PATH 上的 CLI」,故探测优先级低于 argv。
184
+ */
130
185
  function dshVersionText() {
131
186
  try {
132
187
  const res = spawnSync('dsh', ['--version'], { timeout: 10_000, encoding: 'utf8' })
@@ -157,14 +212,19 @@ function dshVersionText() {
157
212
  * plugins: 落 8 空格、插件条目行落 10 空格。name/description 用 JSON.stringify
158
213
  * 落值——JSON 字符串即合法 YAML 双引号标量,换行/引号自动转义。agentYmlText
159
214
  * 先把 restrict 行的 `name: ./restrict.mjs` 与 delegation-labels 行的
160
- * `name: ./delegation-labels.mjs` 替换为对应绝对 file: URL(两个本地插件文件
161
- * 均无「相对 preset 目录」语义),再整体 +10 缩进;空行保持空行(块标量相对
215
+ * `name: ./delegation-labels.mjs` 替换为对应绝对 file: URL(本地插件文件
216
+ * 均无「相对 preset 目录」语义,必须绝对化);0.2.1+ AgentPreset 形态的产物
217
+ * 另含 `name: ./role-restrict.mjs` 的角色面行(嵌套在 paoding-role-* 子声明
218
+ * 行的 plugins 里),同理经 roleRestrictFileUrl 绝对化——未提供该 URL 而产物
219
+ * 里有该行属硬错误(与 delegation-labels 同口径,防相对名字静默进 patch 让
220
+ * preset 挂掉),提供了而产物里没有则无害(旧形态产物照常构造)。
221
+ * 再整体 +10 缩进;空行保持空行(块标量相对
162
222
  * 缩进不变),`!!js` 表达式行原样保留(宿主 patch schema 支持,求值仍由
163
223
  * cordis 装载时完成)。文本里出现 delegation-labels 行而未提供
164
224
  * delegationLabelsFileUrl 时硬错误——漏传会让相对名字静默进 patch,宿主
165
225
  * 解析不了、preset 直接挂掉。
166
226
  */
167
- export function buildPresetRowText({ presetId, name = null, description = null, order = null, agentYmlText, restrictFileUrl, delegationLabelsFileUrl = null }) {
227
+ export function buildPresetRowText({ presetId, name = null, description = null, order = null, agentYmlText, restrictFileUrl, delegationLabelsFileUrl = null, roleRestrictFileUrl = null }) {
168
228
  if (typeof presetId !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(presetId)) {
169
229
  throw new Error(`buildPresetRowText: 非法 presetId: ${presetId}`)
170
230
  }
@@ -174,12 +234,15 @@ export function buildPresetRowText({ presetId, name = null, description = null,
174
234
  if (typeof restrictFileUrl !== 'string' || restrictFileUrl === '') {
175
235
  throw new Error('buildPresetRowText: restrictFileUrl 不能为空')
176
236
  }
177
- const referencesLabelsRow = String(agentYmlText)
178
- .split(/\r?\n/)
179
- .some((line) => line.trim() === 'name: ./delegation-labels.mjs')
237
+ const agentLines = String(agentYmlText).split(/\r?\n/)
238
+ const referencesLabelsRow = agentLines.some((line) => line.trim() === 'name: ./delegation-labels.mjs')
180
239
  if (referencesLabelsRow && (typeof delegationLabelsFileUrl !== 'string' || delegationLabelsFileUrl === '')) {
181
240
  throw new Error('buildPresetRowText: agentYmlText 引用了 ./delegation-labels.mjs,但未提供 delegationLabelsFileUrl')
182
241
  }
242
+ const referencesRoleRestrictRow = agentLines.some((line) => line.trim() === 'name: ./role-restrict.mjs')
243
+ if (referencesRoleRestrictRow && (typeof roleRestrictFileUrl !== 'string' || roleRestrictFileUrl === '')) {
244
+ throw new Error('buildPresetRowText: agentYmlText 引用了 ./role-restrict.mjs(0.2.1+ AgentPreset 形态),但未提供 roleRestrictFileUrl')
245
+ }
183
246
  const lines = [
184
247
  ` - id: ${presetRowId(presetId)}`,
185
248
  ` name: '${PRESET_ROW_NAME}'`,
@@ -190,7 +253,7 @@ export function buildPresetRowText({ presetId, name = null, description = null,
190
253
  if (typeof description === 'string' && description !== '') lines.push(` description: ${JSON.stringify(description)}`)
191
254
  if (Number.isInteger(order)) lines.push(` order: ${order}`)
192
255
  lines.push(' plugins:')
193
- for (const line of String(agentYmlText).split(/\r?\n/)) {
256
+ for (const line of agentLines) {
194
257
  if (line.trim() === '') {
195
258
  lines.push('')
196
259
  continue
@@ -200,6 +263,7 @@ export function buildPresetRowText({ presetId, name = null, description = null,
200
263
  const fileUrl =
201
264
  trimmed === 'name: ./restrict.mjs' ? restrictFileUrl
202
265
  : trimmed === 'name: ./delegation-labels.mjs' ? delegationLabelsFileUrl
266
+ : trimmed === 'name: ./role-restrict.mjs' ? roleRestrictFileUrl
203
267
  : null
204
268
  const replaced = fileUrl !== null ? `${line.match(/^[ \t]*/)[0]}name: ${fileUrl}` : line
205
269
  lines.push(` ${replaced}`)
@@ -521,9 +585,36 @@ export function readPresetDirMeta(dir) {
521
585
  * 不拖垮其余补写(整盘重生成可修复)。
522
586
  * 返回实际补写成功的 presetId 列表(既没补也没撤时为空数组)。
523
587
  */
588
+ /**
589
+ * 判定一份 agent.cordis.yml 产物文本的角色委派行形态(backfill 自愈的防毒
590
+ * 守卫用,与标记第四行同词汇表):8 空格缩进的内联 `persona: |-` → inline
591
+ * (0.2.1 宿主的 dsh-tool-subagent 会拒绝);顶层 `agentPreset: paoding-role-`
592
+ * 引用 → agent-preset。两者都无(角色被 roles_remove 清空且无自定义角色等)
593
+ * 返回 null——产物不含任何 0.2.1 守卫键,对两代宿主都安全。两种形状同时在场
594
+ * 视为 inline(混合/损坏产物按旧形状从严处理,交给整盘重生成修复)。
595
+ */
596
+ export function detectAgentRolesFlavor(agentYmlText) {
597
+ if (/^ persona: \|-/m.test(agentYmlText)) return ROLES_INLINE
598
+ if (/^ agentPreset: paoding-role-/m.test(agentYmlText)) return ROLES_AGENT_PRESET
599
+ return null
600
+ }
601
+
602
+ /**
603
+ * 轨道迁移对账:把 $DSH_HOME/cordis.patch.yml MANAGED 块里缺失的编排类 preset
604
+ * 声明行按磁盘产物补写回来。只补「行缺失」:行在则零写盘(内容对账是
605
+ * ensurePresetInstalled 的整盘重生成路径,不在这里做)。
606
+ *
607
+ * 形态防毒守卫:补行内容 = 磁盘产物全文内联,若产物角色行形态与宿主所需
608
+ * (presetRolesFlavorOf(detectHostVersionTriple()))不符——典型是宿主已升
609
+ * 0.2.1 而磁盘还是内联形状——补出去的声明行会让 0.2.1 宿主挂编排 preset 的
610
+ * 新建会话直接被 tool-subagent 守卫拒绝。此时跳过该 preset 并 warn(可整盘
611
+ * 重生成修复),绝不按旧产物写行;反方向(宿主降级、磁盘已是新形状)同理。
612
+ * 守卫经 DSH_PAODING_FAKE_HOST_VERSION 可测。
613
+ */
524
614
  export function backfillHomePatchRows(dshHome, presetIds) {
525
615
  const rowsOnDisk = readHomePatchRows(dshHome)
526
616
  const wanted = new Set((Array.isArray(presetIds) ? presetIds : []).filter((id) => ORCHESTRATOR_PRESET_ID_RE.test(id)))
617
+ const hostRolesFlavor = presetRolesFlavorOf(detectHostVersionTriple())
527
618
  const rows = []
528
619
  const backfilled = []
529
620
  for (const presetId of wanted) {
@@ -531,10 +622,23 @@ export function backfillHomePatchRows(dshHome, presetIds) {
531
622
  const dir = path.join(dshHome, '.agent-presets', presetId)
532
623
  try {
533
624
  const agentYmlText = readFileSync(path.join(dir, 'agent.cordis.yml'), 'utf8')
625
+ // 形态防毒守卫(见上方 doc 注释):磁盘产物形态 ≠ 宿主所需 → 不补行。
626
+ const diskRolesFlavor = detectAgentRolesFlavor(agentYmlText)
627
+ if (diskRolesFlavor !== null && diskRolesFlavor !== hostRolesFlavor) {
628
+ throw new Error(
629
+ `磁盘产物角色行形态 ${diskRolesFlavor} 与宿主所需 ${hostRolesFlavor} 不符——按旧产物补行会让 0.2.1 宿主被 tool-subagent 守卫拒绝,请整盘重生成换代`,
630
+ )
631
+ }
534
632
  const restrictFile = path.join(dir, 'restrict.mjs')
535
633
  if (!existsSync(restrictFile)) throw new Error('restrict.mjs 缺失')
536
634
  const labelsFile = path.join(dir, 'delegation-labels.mjs')
537
635
  if (!existsSync(labelsFile)) throw new Error('delegation-labels.mjs 缺失')
636
+ // role-restrict.mjs 仅 0.2.1+ AgentPreset 形态产物含(角色面行)——按产物
637
+ // 里有没有该行决定是否必需,目录轨老产物(inline 形态)不受影响。
638
+ const roleRestrictFile = path.join(dir, 'role-restrict.mjs')
639
+ if (agentYmlText.includes('name: ./role-restrict.mjs') && !existsSync(roleRestrictFile)) {
640
+ throw new Error('role-restrict.mjs 缺失')
641
+ }
538
642
  const meta = readPresetDirMeta(dir)
539
643
  rows.push(
540
644
  buildPresetRowText({
@@ -545,6 +649,7 @@ export function backfillHomePatchRows(dshHome, presetIds) {
545
649
  agentYmlText,
546
650
  restrictFileUrl: pathToFileURL(restrictFile).href,
547
651
  delegationLabelsFileUrl: pathToFileURL(labelsFile).href,
652
+ ...(existsSync(roleRestrictFile) ? { roleRestrictFileUrl: pathToFileURL(roleRestrictFile).href } : {}),
548
653
  }),
549
654
  )
550
655
  backfilled.push(presetId)
@@ -560,12 +665,13 @@ export function backfillHomePatchRows(dshHome, presetIds) {
560
665
  }
561
666
 
562
667
  /**
563
- * 解析 .generator-version 标记文本为 { version, track, worker }。v3 格式三行:
564
- * 首行生成器版本、次行预设安装轨道(declarative / directory)、三行 workflow
565
- * worker 形态(ptc / worker-thread);旧格式(v2 两行 = 0.3.6 及以前、单行 =
566
- * 0.3.4 及以前)解析出 track / worker 为 null —— 恰是「宿主已换代但产物版本
567
- * 未动」的迁移触发态(轨道翻转、worker 换代都靠它触发自愈)。空文本 / 全空白
568
- * 返回 null(按未生成处理)。
668
+ * 解析 .generator-version 标记文本为 { version, track, worker, roles }。v4 格式
669
+ * 四行:首行生成器版本、次行预设安装轨道(declarative / directory)、三行
670
+ * workflow worker 形态(ptc / worker-thread)、四行角色委派行形态(inline /
671
+ * agent-preset);v3 三行(0.4.3 及以前)解析出 roles 为 null、v2 两行
672
+ *(0.3.6 及以前)track / worker 也为 null —— null 恰是「宿主已换代但产物版本
673
+ * 未动」的迁移触发态(轨道翻转、worker 换代、角色行换代都靠它触发自愈)。
674
+ * 空文本 / 全空白返回 null(按未生成处理)。
569
675
  */
570
676
  export function parseGeneratorMarker(text) {
571
677
  const lines = String(text ?? '')
@@ -575,15 +681,17 @@ export function parseGeneratorMarker(text) {
575
681
  if (lines.length === 0) return null
576
682
  const track = lines[1] === 'declarative' || lines[1] === 'directory' ? lines[1] : null
577
683
  const worker = lines[2] === 'ptc' || lines[2] === 'worker-thread' ? lines[2] : null
578
- return { version: lines[0], track, worker }
684
+ const roles = lines[3] === 'inline' || lines[3] === 'agent-preset' ? lines[3] : null
685
+ return { version: lines[0], track, worker, roles }
579
686
  }
580
687
 
581
688
  /**
582
- * 生成 .generator-version 标记文本(版本 + 轨道 + worker 形态三行,v3 格式)。
583
- * worker 取 WORKER_PTC / WORKER_WORKER_THREAD(允许调用方先探测再传入)。
689
+ * 生成 .generator-version 标记文本(版本 + 轨道 + worker 形态 + 角色形态四行,
690
+ * v4 格式)。worker 取 WORKER_PTC / WORKER_WORKER_THREAD、roles 取 ROLES_INLINE /
691
+ * ROLES_AGENT_PRESET(允许调用方先探测再传入)。
584
692
  */
585
- export function formatGeneratorMarker(version, presetSystem, worker) {
586
- return `${version}\n${presetSystem}\n${worker}\n`
693
+ export function formatGeneratorMarker(version, presetSystem, worker, roles) {
694
+ return `${version}\n${presetSystem}\n${worker}${roles ? `\n${roles}` : ''}\n`
587
695
  }
588
696
 
589
697
  // ── workflow worker 形态(宿主版本 → 生成期换行)────────────────────────────
@@ -592,6 +700,44 @@ export function formatGeneratorMarker(version, presetSystem, worker) {
592
700
  export const WORKER_PTC = 'ptc'
593
701
  export const WORKER_WORKER_THREAD = 'worker-thread'
594
702
 
703
+ // ── 角色委派行形态(宿主版本 → 生成期换形)──────────────────────────────────
704
+
705
+ /** 生成产物里角色委派行的两种形态(标记第四行的合法取值)。 */
706
+ export const ROLES_INLINE = 'inline'
707
+ export const ROLES_AGENT_PRESET = 'agent-preset'
708
+
709
+ /**
710
+ * 该宿主版本的角色委派行形态:≥0.2.1(含 0.2.1-rc.*,parseVersionTriple 把
711
+ * prerelease 剥成 0.2.1 三元组,逐段比较天然命中)走 AgentPreset 形态——
712
+ * dsh-tool-subagent 从 0.2.1-rc.2 起在 apply() 硬拒配置里的 persona /
713
+ * toolFilter 键(报 "persona is no longer supported; configure persona and
714
+ * tool plugins in an Agent Preset and select it with agentPreset"),内联
715
+ * 形状的委派行让 0.2.1+ 宿主上挂编排 preset 的新建会话直接抛错;< 0.2.1 保持
716
+ * 内联形态(老宿主不认 agentPreset 键,产物零 diff 不变量随之保留)。版本
717
+ * 探测不到时返回 false(保守侧:保留源模板原形状),与 isPtcWorkerHost 的
718
+ * 保守哲学一致。
719
+ */
720
+ export function isPresetRoleHost(triple, prerelease) {
721
+ if (!Array.isArray(triple) || triple.length !== 3 || triple.some((n) => !Number.isInteger(n))) return false
722
+ if (!atOrAfterTriple(triple, [0, 2, 1])) return false
723
+ // 0.2.1 这一个点上分辨 prerelease:tool-subagent 守卫是 0.2.1-rc.2 落地的,
724
+ // rc.0 / rc.1 仍认内联形状,换 agentPreset 形态反而会被当未知键拒绝。正式版
725
+ // (prerelease null)与更高的 triple 一律新形态;prerelease 未知(缺省
726
+ // undefined = 旧调用方 / 旧测试只传三元组)保持原行为按新形态处理。
727
+ if (triple[0] === 0 && triple[1] === 2 && triple[2] === 1 && prerelease !== null && prerelease !== undefined) {
728
+ return comparePrerelease(prerelease, 'rc.2') >= 0
729
+ }
730
+ return true
731
+ }
732
+
733
+ /**
734
+ * 生成器标记第四行的角色形态取值(composeGenerated 的 isPresetRoleHost 同一
735
+ * 判定,探测不到按 inline 落盘——与保守生成侧一致,重生成后产物可再次对账)。
736
+ */
737
+ export function presetRolesFlavorOf(hostVersionTriple, prerelease) {
738
+ return isPresetRoleHost(hostVersionTriple, prerelease) ? ROLES_AGENT_PRESET : ROLES_INLINE
739
+ }
740
+
595
741
  /**
596
742
  * 探测正在运行的宿主版本三元组:进程 argv 宿主版本(插件与宿主同进程时零开销
597
743
  * 且反映正在运行的宿主)> PATH 上的 `dsh --version` > null。与
@@ -599,11 +745,26 @@ export const WORKER_WORKER_THREAD = 'worker-thread'
599
745
  * 的磁盘形态没有可区分的单数包,探测不到就返回 null 由调用方按未知处理。
600
746
  */
601
747
  export function detectHostVersionTriple() {
602
- const argvVersion = hostVersionFromArgv()
603
- if (argvVersion !== null) return parseVersionTriple(argvVersion)
604
- const versionText = dshVersionText()
605
- if (versionText !== null) return parseVersionTriple(versionText)
606
- return null
748
+ return detectHostVersionInfo()?.triple ?? null
749
+ }
750
+
751
+ /**
752
+ * 宿主版本信息(三元组 + prerelease 原文):探测链与 detectHostVersionTriple
753
+ * 完全同源(假版本钩子 > 进程 argv 宿主版本 > PATH 上的 dsh --version),
754
+ * 只是 prerelease 不剥——角色委派行形态门要分辫 0.2.1-rc.1 与 0.2.1-rc.2
755
+ *(守卫落地点),其余门(轨道 / worker)消费 triple 即可。
756
+ */
757
+ export function detectHostVersionInfo() {
758
+ // 测试 / 诊断钩子(与 DSH_PAODING_CONFIG / DSH_PAODING_CWD 同款环境变量惯例):
759
+ // 显式假版本号最高优先,让无 dsh 进程的测试与排查能驱动版本门控分支;非法值
760
+ // 忽略、落回正常探测链。
761
+ const fake = process.env.DSH_PAODING_FAKE_HOST_VERSION
762
+ let versionText = null
763
+ if (typeof fake === 'string' && fake.trim() !== '') versionText = fake.trim()
764
+ if (versionText === null) versionText = hostVersionFromArgv()
765
+ if (versionText === null) versionText = dshVersionText()
766
+ if (versionText === null) return null
767
+ return parseVersionInfo(versionText)
607
768
  }
608
769
 
609
770
  /**
@@ -48,7 +48,7 @@ import { composeGenerated, extractMainAgentAllow } from './compose.mjs'
48
48
  import { smartDefaults } from './alloc.mjs'
49
49
  import {
50
50
  buildPresetRowText,
51
- detectHostVersionTriple,
51
+ detectHostVersionInfo,
52
52
  detectPresetSystem,
53
53
  listOrchestratorPresetIds,
54
54
  presetRowId,
@@ -61,10 +61,11 @@ import {
61
61
  /**
62
62
  * 预设源文件清单(generateAndInstall 原样复制件;agent.cordis.yml 走重写,
63
63
  * 不在此列)。collectState 的存在性检查与物化复制共用同一份,加源文件只改
64
- * 这一处。delegation-labels.mjs 与 restrict.mjs 同生命周期:都是本地 cordis
65
- * 插件,声明行轨内联时按绝对 file: URL 引用。
64
+ * 这一处。restrict.mjs / delegation-labels.mjs / role-restrict.mjs 同生命周期:
65
+ * 都是本地 cordis 插件,声明行轨内联时按绝对 file: URL 引用(role-restrict.mjs
66
+ * 仅 0.2.1+ AgentPreset 形态产物引用,老宿主复制了不引用、无害)。
66
67
  */
67
- export const PRESET_ASIS_FILES = ['preset.yml', 'restrict.mjs', 'delegation-labels.mjs']
68
+ export const PRESET_ASIS_FILES = ['preset.yml', 'restrict.mjs', 'delegation-labels.mjs', 'role-restrict.mjs']
68
69
 
69
70
  /**
70
71
  * 预设源文件存在性检查(collectState 第一步):全部源文件缺任一件即硬错误,
@@ -249,9 +250,13 @@ export async function collectState({ dshHome, configFile, profile = null, patche
249
250
  //(见 preset-system.detectPresetSystem 的降级链)。
250
251
  const presetSystem = await detectPresetSystem({ dshHome, runtimeSystem: runtimeFacts?.presetSystem ?? null })
251
252
 
252
- // 宿主版本三元组(argv 宿主版本 > dsh --version > null,探测不到按未知处理):
253
- // workflow worker 行的生成期形态判定(isPtcWorkerHost)与标记 worker 行都从这里取。
254
- const hostVersionTriple = detectHostVersionTriple()
253
+ // 宿主版本信息(argv 宿主版本 > dsh --version > null,探测不到按未知处理):
254
+ // workflow worker 行的生成期形态判定(isPtcWorkerHost)与标记 worker 行取
255
+ // triple;角色委派行形态判定(isPresetRoleHost)另取 prerelease——0.2.1 守卫
256
+ // 是 0.2.1-rc.2 落地的,rc.0/rc.1 保持内联形状。
257
+ const hostVersion = detectHostVersionInfo()
258
+ const hostVersionTriple = hostVersion?.triple ?? null
259
+ const hostPrerelease = hostVersion?.prerelease ?? null
255
260
 
256
261
  // 已落盘 preset 目录扫描(编排类):.agent-presets 下的 orchestrator 基础
257
262
  // preset 与 orchestrator-<slug> 工作区专属 preset。扫描失败(目录尚不存在、
@@ -288,6 +293,8 @@ export async function collectState({ dshHome, configFile, profile = null, patche
288
293
  presetSystem,
289
294
  // 宿主版本三元组(null = 探测不到):workflow worker 行生成期形态判定用
290
295
  hostVersionTriple,
296
+ // 宿主 prerelease 原文(null = 正式版 / 探测不到):角色委派行形态判定用
297
+ hostPrerelease,
291
298
  // 主 persona 尾部追加的默认常量(配置键 main_agent_persona_extra 缺省值):
292
299
  // 供 UI state(serializeState → /api/paoding/state)展示与「恢复默认」用。
293
300
  mainPersonaExtraDefault: DEFAULT_MAIN_AGENT_PERSONA_EXTRA,
@@ -391,7 +398,7 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
391
398
  // Compose the generated config; a composition error is a hard error.
392
399
  //(这层包装无增值,让 composeGenerated 的原始错误直接上抛。)宿主版本三元组
393
400
  // 驱动 workflow worker 行的生成期换行(≥0.1.6 → workflow-ptc,null 按现代宿主)。
394
- const composed = composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas, state?.hostVersionTriple ?? null)
401
+ const composed = composeGenerated(srcText, blocks, personaBlocks, assignments, inventory, restrictBase, mainAgentSkillMetas, state?.hostVersionTriple ?? null, state?.hostPrerelease)
395
402
  const generatedText = composed.text
396
403
  const roleResults = composed.roleResults
397
404
 
@@ -533,8 +540,8 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
533
540
  // declarative(0.1.7+):目录扫描机制已删,必须把本 preset 以一条 `- insert:`
534
541
  // 声明行写进 home patch 托管块才算安装完成(preset 元信息与写 preset.yml 同
535
542
  // 源:显示名取 displayName ?? SRC preset.yml 的 name,description / order 取
536
- // SRC;restrict.mjs / delegation-labels.mjs 用绝对 file: URL——声明行没有
537
- // 「相对 preset 目录」语义)。
543
+ // SRC;restrict.mjs / delegation-labels.mjs / role-restrict.mjs 用绝对 file: URL——声明行没有
544
+ // 「相对 preset 目录」语义;role-restrict URL 仅供 0.2.1+ AgentPreset 形态产物的角色面行消费)。
538
545
  // directory(≤0.1.6):上面落盘即完成;home patch 里若有残留托管声明块
539
546
  //(宿主刚从 0.1.7 降级等场景)整块撤下自愈——残留声明行会让 profile 启动失败。
540
547
  if (wrote) {
@@ -549,6 +556,7 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
549
556
  agentYmlText: generatedText,
550
557
  restrictFileUrl: pathToFileURL(path.join(dstDir, 'restrict.mjs')).href,
551
558
  delegationLabelsFileUrl: pathToFileURL(path.join(dstDir, 'delegation-labels.mjs')).href,
559
+ roleRestrictFileUrl: pathToFileURL(path.join(dstDir, 'role-restrict.mjs')).href,
552
560
  }),
553
561
  ])
554
562
  } else if (readHomePatchRows(dshHome).length > 0) {
@@ -24,9 +24,11 @@ export const PROFILE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
24
24
  * 主 agent persona 尾部追加规则(默认值)。
25
25
  * 用户可在配置 UI 的「人设追加」里编辑/清空;配置键 main_agent_persona_extra:
26
26
  * null/缺省 = 用本默认;'' = 明确清空(不追加);其他字符串 = 覆盖。
27
- * {{cwd}}/{{model}} 是 persona 模板占位符,DSH 运行时替换,必须原样保留。
27
+ * 禁用 {{cwd}} 占位:该文本拼进部署主代理 persona,DSH 0.2.1 的
28
+ * deployment:persona-prefix 段只注册 provider/model,未知变量直接抛错;
29
+ * 「当前工作目录」由宿主 env 上下文提供,无需模板变量。
28
30
  */
29
- export const DEFAULT_MAIN_AGENT_PERSONA_EXTRA = `Codegraph MCP default project may be a DIFFERENT repository than {{cwd}}. Never call codegraph_* tools without passing projectPath = {{cwd}} (the absolute path of your working directory). If {{cwd}} has no .codegraph index, fall back to glob/grep/read directly and do not loop or comment on project mismatches.`
31
+ export const DEFAULT_MAIN_AGENT_PERSONA_EXTRA = `Codegraph MCP default project may be a DIFFERENT repository than your current working directory. Never call codegraph_* tools without passing projectPath = the absolute path of your current working directory. If that directory has no .codegraph index, fall back to glob/grep/read directly and do not loop or comment on project mismatches.`
30
32
 
31
33
  export function warn(message) {
32
34
  console.warn(`warning: ${message}`)