@yangdcm/dsh-expert-team 1.2.0 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
4
4
  `package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
5
5
 
6
+ ## 1.2.2
7
+
8
+ **修掉"lead 工具面收窄静默失效"**(拿真机启动日志换来的)
9
+
10
+ - 症状:每次创建 agent 都刷一行 `lead 工具面**未**收窄(nothing-to-deny)… 宿主里这些名字一个都不存在`,
11
+ 而实际上模型可见的 `bash/write/edit/grep/glob` **存在** —— 结论不成立,收窄也没发生。
12
+ - 根因:宿主 `tools.view(scope)` **不传 scope = 全局视图**;0.1.5 起模型可见工具由 preset 注册在
13
+ **agent 平面**,于是全局视图"非空但缺这几个名字",纯函数据实报 `nothing-to-deny`。
14
+ 宿主自己的 `restrict()` 用的就是 `scopeOf(this.ctx)`,我们却用了不传 scope 的 `view()`。
15
+ - 修法:新增 `agentScopedToolNames()`(动态 import `@deepseek-ai/dsh-scope` 取 `scopeOf(agent.ctx)`,
16
+ 再 `view(scope)`);取不到 scope 时返回 `knownNames: undefined` ⇒ 如实报 **no-known-names**
17
+ ("我不知道有什么"),绝不再退化成"宿主里没有这些工具"这种不成立的结论。
18
+ - 告警去重:同一 status 每进程只喊一次(十行同样的 warn 会把"响亮"变成噪声,
19
+ 而噪声的代价是所有告警一起被降权)。两种零仍然分得清。
20
+ ## 1.2.1
21
+
22
+ **卸载回收覆盖历史副本**(1.2.0 的补丁)
23
+
24
+ - `/team uninstall` 此前只遍历登记清单,而 **1.2.0 之前铺下的 skill/preset 没有登记过**
25
+ (那时还没有清单)—— 从 1.1.x 升上来的用户跑它只会得到"没有需要回收的副本",
26
+ 而 `$DSH_HOME/skills/expert-team` 里的旧副本仍在(`gate:sync` 会一直报漂移)。
27
+ 现在除清单外还会看两个众所周知的自举落点,判据仍是"有戳或身份对得上",
28
+ 用户自己写的同名内容照样不动。
6
29
  ## 1.2.0
7
30
 
8
31
  **安全加固(本机来源守卫)+ 自举安装收口 + 设置接进宿主命名空间**
package/lib/command.js CHANGED
@@ -41,7 +41,7 @@ import { collectTokenUsage } from './metrics/token-usage.js';
41
41
  import { renderTokenSection } from './metrics/tokens.js';
42
42
  // A 线(token 成本治理 · 第 2 步):**收窄 lead 自己的工具面**。判定全在
43
43
  // `lib/lead-toolface.js`(纯函数、零宿主依赖、可单测);这里只负责在 agent **创建时刻**接线。
44
- import { planLeadToolFace, shouldNarrowLeadToolFace } from './lead-toolface.js';
44
+ import { planLeadToolFace, shouldNarrowLeadToolFace, agentScopedToolNames } from './lead-toolface.js';
45
45
  // G 线(档位):档位词表/裁剪表/suggestTier 的**唯一真源**。SKILL 的裁剪表、`/team status`
46
46
  // 的输出、浮层候选项全部从这里来 —— 两处各写一份就是又一次「一个事实多份拷贝」。
47
47
  import { normalizeTier, suggestTier, tierSummaryLine, tierDetailLines, tierChoices, TIER_SPEC, TIER_LABELS_ZH, DEFAULT_TIER, narrowedRoles } from './tier.js';
@@ -849,16 +849,32 @@ async function uninstallInstalled() {
849
849
  const rec = await readInstalled();
850
850
  const removed = [];
851
851
  const skipped = [];
852
- for (const [kind, info] of Object.entries(rec)) {
853
- const dir = info && info.path;
854
- if (!dir) continue;
855
- if (await pathExists(dir) && await isOwnedCopy(dir, kind)) {
852
+ const seen = new Set();
853
+ /**
854
+ * 回收一个候选目录(幂等;只删"本插件的副本")。
855
+ * 为什么要 `seen`:清单里的路径与下面两个**历史默认路径**可能重合,重复 rm 会把
856
+ * "已回收"报两遍,也会让第二次那条 `pathExists` 失败被误记成"保留"。
857
+ */
858
+ const reclaim = async (kind, dir) => {
859
+ if (!dir || seen.has(dir)) return;
860
+ seen.add(dir);
861
+ if (!(await pathExists(dir))) return;
862
+ if (await isOwnedCopy(dir, kind)) {
856
863
  try { await rm(dir, { recursive: true, force: true }); removed.push(`${kind}:${dir}`); }
857
864
  catch (e) { skipped.push(`${kind}:${dir}(删除失败:${String(e && e.message ? e.message : e)})`); }
858
- } else if (await pathExists(dir)) {
865
+ } else {
859
866
  skipped.push(`${kind}:${dir}(不是本插件的副本 ⇒ 保留)`);
860
867
  }
861
- }
868
+ };
869
+
870
+ for (const [kind, info] of Object.entries(rec)) await reclaim(kind, info && info.path);
871
+
872
+ // 1.2.0 之前的副本**没有登记过**(那时还没有清单)—— 而它们恰恰是这个命令最该处理的历史遗留。
873
+ // 所以除了清单,还要看两个众所周知的自举落点;判据仍是 `isOwnedCopy`(无戳时按身份判定),
874
+ // 用户自己写的同名 skill/preset 依旧不会被删。
875
+ await reclaim('skill', join(dshHome(), 'skills', 'expert-team'));
876
+ await reclaim('preset', join(dshHome(), '.agent-presets', 'expert-team'));
877
+
862
878
  try { await rm(installedManifestPath(), { force: true }); } catch { /* 清单删不掉就留着 */ }
863
879
  const lines = ['# /team uninstall', ''];
864
880
  lines.push(removed.length ? '已回收:' : '没有需要回收的副本。');
@@ -4393,7 +4409,19 @@ async function executeTeamCommand(ctx, invocation) {
4393
4409
  /** 并发写追踪器:**进程级单例**(跨工具调用累积;`_live` 暴露以便单测重置与断言)。 */
4394
4410
  const WRITE_TRACER = createWriteTracer();
4395
4411
 
4396
- export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
4412
+ /**
4413
+ * lead 工具面告警去重:同一 status 每个进程只喊一次。
4414
+ * 为什么:该告警按 **agent 创建**触发,一个会话里每建一个 agent 都会喊 —— 十行同样的 warn
4415
+ * 会把"响亮"变成"噪声",而噪声的代价是所有告警一起被降权(本仓反复记录过的失败模式)。
4416
+ * 去重只影响**重复次数**,不影响"第一次一定喊",也不合并不同 status(两种零仍然分得清)。
4417
+ */
4418
+ const LEAD_TOOLFACE_WARNED = new Set();
4419
+ function warnLeadToolFaceOnce(status, detail) {
4420
+ if (LEAD_TOOLFACE_WARNED.has(status)) return;
4421
+ LEAD_TOOLFACE_WARNED.add(status);
4422
+ console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
4423
+ }
4424
+ export const _live = { pushActivity, phaseAccountingViolations, loggedPhases, authorityViolations, WRITE_TRACER, createWriteTracer, formatConflict, summarizeTool, parseLogLine, roleOfSub, mapRoleToSub, membersFromState, buildRoleSubMap, resolveSubRoles, childSessionTiming, SUB_HEADER_CACHE, workflowEventIndex, workflowChildLabels, workflowChildMeta, workflowRuns, WF_EVENT_CACHE, rememberSessionRun, sessionRunFor, runOwnerSession, SESSION_RUNS, parseTeamCommand, deriveMemberEntries, schemaViolations, runHealth, RUN_STALL_MS, scaffoldFingerprint, SCAFFOLD_REQUIRED, strandedTasks, settleStranded, IN_FLIGHT_STATUSES, normalizeCoverage, SCHEMA_WARN_SEEN, pushActivityEvent, DEFAULT_LIMITS, LIMITS, resolveLimits, capacityViolations, DEFAULT_ROUND_LIMITS, ROUND_LIMITS, ROUND_LIMIT_ENV, resolveRoundLimits, ROUND_LIMIT_OF_KIND, roundOf, isQualityTask, normTitle, roundLimitViolations, reworkLoopWriteGuard, mutateTasks, readStandingRules, appendStandingRule, rulesRun, scopeOverlapWarnings, applyTaskStatus, waitRun, eventFamily, verdictFromToken, normalizeRoleName, truncateCodepoints, filterRunScopedSubs, runCreatedAtMs, runLogTail, liveFiles, LIVE_FILES_CACHE, DEFAULT_ROLES, resolveTierGate, TIER_GATE_ENV, snapshotRun, settingsPath, loadSettingsSync, currentSettings, limitsBaseFromSettings, roundLimitsBaseFromSettings, effectiveTierGate: () => TIER_GATE, ensureSkillInstalled, ensurePresetInstalled, uninstallInstalled, buildSkillRegistration, parseSkillMarkdown, PLUGIN_VERSION, INSTALL_STAMP, runtimeSkillRegistered: () => RUNTIME_SKILL_REGISTERED, agentScopedToolNames, warnLeadToolFaceOnce, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
4397
4425
 
4398
4426
  export function apply(ctx, config) {
4399
4427
  // 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
@@ -4573,7 +4601,7 @@ export function apply(ctx, config) {
4573
4601
  // 为什么是这个时刻:`agent/created` 由宿主 `announce()` 在**创建时刻**发出;宿主自己对
4574
4602
  // 子代理也是在组合(setup)阶段调 `childCtx.tools.restrict(...)`(`dsh-subagent:554`)。
4575
4603
  // 即:**照抄宿主自己的时序**,不另造。
4576
- ctx.on('agent/created', ({ agent }) => {
4604
+ ctx.on('agent/created', async ({ agent }) => {
4577
4605
  try {
4578
4606
  const agents = ctx.get('agents');
4579
4607
  // ① preset 必须是专家团(`composedPreset` 读活动作用域链,未写会话头也答得出)
@@ -4583,12 +4611,16 @@ export function apply(ctx, config) {
4583
4611
  const isRoot = agents?.roots?.().includes(agent) === true;
4584
4612
  if (!shouldNarrowLeadToolFace({ presetId, isRoot }).apply) return; // 无关 agent:静默返回,不是失败
4585
4613
 
4586
- const knownNames = ctx.get('tools')?.view?.()?.restrictableNames;
4614
+ // **必须取 agent 作用域**的名字清单:`view()` 不传 scope = 全局视图,而模型可见工具
4615
+ // 在 0.1.5 起由 preset 注册在 agent 平面 ⇒ 全局视图非空但缺那几个名字,收窄会静默失效
4616
+ // 且给出"宿主里没有这些工具"的错误结论(拿真机日志换来的教训,详见 lead-toolface.js)。
4617
+ const { knownNames, reason } = await agentScopedToolNames({ agentCtx: agent.ctx });
4587
4618
  const plan = planLeadToolFace({ platform: process.platform, knownNames });
4588
4619
  if (!plan.effective) {
4589
4620
  // **两种零都要出声**:`no-known-names`(我不知道宿主有什么)与
4590
4621
  // `nothing-to-deny`(宿主确实没有这些)不是同一件事,不能长成同一个样子。
4591
- console.warn(`[expert-team] lead 工具面**未**收窄(${plan.status}):${plan.notes.join(' ')}`);
4622
+ // 但同一状态**只喊一次**:每个会话都刷会把 warn 变成噪声,而噪声会让所有告警一起被降权。
4623
+ warnLeadToolFaceOnce(plan.status, `${plan.notes.join(' ')}(清单来源:${reason})`);
4592
4624
  return;
4593
4625
  }
4594
4626
  // 成功不打印:每个会话都刷一行会把 warn 变成噪声(本仓纪律:warn 必须保持可见)。
@@ -177,3 +177,49 @@ export function shouldNarrowLeadToolFace({ presetId, isRoot } = {}) {
177
177
  if (isRoot !== true) return { apply: false, reason: 'is-subagent' };
178
178
  return { apply: true, reason: 'lead' };
179
179
  }
180
+
181
+ /**
182
+ * 取**agent 作用域**下的"可限制工具名清单"。
183
+ *
184
+ * ── 为什么必须带 scope(这一条是拿真机日志换来的)────────────────────────────
185
+ * 宿主 `tools.view(scope)` 的文档写得很直白:**不传 scope = 全局视图**。而 0.1.5 起模型可见的
186
+ * `bash/write/edit/grep/glob` 由 **preset 注册在 agent 平面**,不再是全局工具 ——
187
+ * 于是 `view()`(全局)会返回一个**非空但缺这几个名字**的集合,`planLeadToolFace` 据此报
188
+ * `nothing-to-deny`,收窄**静默失效**,而日志还在说"宿主里这些名字一个都不存在"(不属实)。
189
+ * 宿主自己的 `restrict()` 用的就是 `scopeOf(this.ctx)` —— 本函数照抄同一条取 scope 的路。
190
+ *
191
+ * scope 是 `@deepseek-ai/dsh-scope` 里的**模块私有 Symbol**(`Symbol("dsh.scope")`,不是
192
+ * `Symbol.for`),因此只能经该包的 `scopeOf()` 取得 ⇒ **动态 import**:仓库里解析不到该包时
193
+ * 返回 `knownNames: undefined`,由调用方如实报 `no-known-names`("我不知道有什么"),
194
+ * 绝不退化成"宿主里没有这些工具"那种**不成立**的结论。
195
+ *
196
+ * @param {object} input
197
+ * @param {object} [input.agentCtx] - `agent.ctx`(scope 就挂在它上面)
198
+ * @param {object} [input.tools] - 工具服务(默认取 `agentCtx.tools`)
199
+ * @param {Function} [input.loadScope] - **仅供测试**注入 `@deepseek-ai/dsh-scope` 的加载器
200
+ * @returns {Promise<{knownNames: Set<string>|undefined, reason: string}>}
201
+ */
202
+ export async function agentScopedToolNames({ agentCtx, tools, loadScope } = {}) {
203
+ const loader = typeof loadScope === 'function' ? loadScope : () => import('@deepseek-ai/dsh-scope');
204
+ let scopeOf;
205
+ try {
206
+ const mod = await loader();
207
+ scopeOf = mod && mod.scopeOf;
208
+ } catch (e) {
209
+ return { knownNames: undefined, reason: 'scope-module-unavailable:' + String((e && e.message) || e) };
210
+ }
211
+ if (typeof scopeOf !== 'function') return { knownNames: undefined, reason: 'scope-of-missing' };
212
+ const service = tools || (agentCtx && agentCtx.tools);
213
+ if (!service || typeof service.view !== 'function') return { knownNames: undefined, reason: 'tools-service-unavailable' };
214
+ let scope;
215
+ try { scope = scopeOf(agentCtx); } catch { return { knownNames: undefined, reason: 'scope-undetectable' }; }
216
+ if (scope === undefined) return { knownNames: undefined, reason: 'not-a-scoped-ctx' };
217
+ try {
218
+ const view = service.view(scope);
219
+ const names = view && view.restrictableNames;
220
+ const size = names && typeof names.size === 'number' ? names.size : 0;
221
+ return { knownNames: names, reason: size > 0 ? 'ok' : 'empty-scoped-view' };
222
+ } catch (e) {
223
+ return { knownNames: undefined, reason: 'view-failed:' + String((e && e.message) || e) };
224
+ }
225
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "dsh「专家团」bundle:一句自然语言自动组建/持久化一支 12 角色多智能体团队,共享工作区协议 + 阶段门控编排 + 结构化交接 + 质量门禁/自动调度,实现者直接改代码并产出持久工件;带 live 团队浮层(质量门禁/覆盖率/工件预览)。 · Role-based multi-agent expert team for DeepSeek Harness: one sentence in, a staged and gated team delivery out.",
5
5
  "type": "module",
6
6
  "license": "MIT",