@yangdcm/dsh-expert-team 1.3.14 → 1.3.16

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.
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 会话模型的 **reasoningEffort 预检**(2026-09-15,**只告警,绝不阻断**)。
3
+ *
4
+ * ── 为什么需要它(一次真实故障的定性)────────────────────────────────────────
5
+ * 用户报 `model "deepseek-flash" does not support reasoning effort "low"`。定性结论:
6
+ * · **不是本插件的错,也不是宿主缺 low**;
7
+ * · 是用户 `~/.dsh/settings.yaml` 里会话默认路由(命名空间 `agent-default-model`)的
8
+ * `deepseek-flash` 条目**漏写 `reasoningEfforts`** ⇒ 宿主能力表里该模型**只剩 `off`**
9
+ * ⇒ **任何显式 effort 都被拒**(`dsh-llm` 的 `resolveCallWithInfo`:
10
+ * `reasoning === undefined` 时,只要传了 `reasoningEffort` 就抛 `UNSUPPORTED_REASONING_EFFORT`)。
11
+ * · 而本 preset 里 **4 个角色声明 `low`、8 个声明 `high`** ⇒ 在这个路由下 **12 个角色全会失败**。
12
+ * "只有 low 报错"是假象:**先派谁先报谁**。
13
+ * · 宿主在**任何网络 I/O 之前**就抛(`dsh-tool-subagent` 的 preflight → `llm.resolveCallConfig`)。
14
+ *
15
+ * ⇒ 插件**无法**在派工前改变宿主行为(派工由宿主 `tool-subagent` + LLM 运行时执行),
16
+ * 能做的是**提前告警**:在会话路由能力表与 preset 声明对不上时,打**一行**可操作的提示。
17
+ * 这也是本仓唯一诚实的做法 —— 静默失败最贵。
18
+ *
19
+ * ── 纪律 ─────────────────────────────────────────────────────────────────
20
+ * · **只告警,不阻断、不改 preset 的 effort 分档**(那是设计意图);
21
+ * · 任何异常/读不到 ⇒ **静默跳过**(fail-open),绝不让插件加载失败、绝不刷屏;
22
+ * · 一次加载**最多一行**(进程内去重),服务晚挂则**就绪后重探**(同 `scheduleOptionalPluginCheck`)。
23
+ */
24
+
25
+ /** 宿主合法 effort 值域(`dsh-llm` 的 `LlmReasoningEffortId`)。 */
26
+ export const EFFORT_DOMAIN = Object.freeze(['off', 'low', 'high', 'max']);
27
+
28
+ /**
29
+ * 从 preset 源文件里提取**声明了哪些 effort、各有几个角色**(纯函数,按行正则,不引 YAML 解析器)。
30
+ * 只认 `reasoningEffort: <值>` 这种行;顺序按出现次数降序、值去重。
31
+ * @returns {{efforts:string[], counts:Record<string,number>}}
32
+ */
33
+ export function declaredEffortsFromPresetSource(src) {
34
+ const counts = Object.create(null);
35
+ const re = /^\s*(?:-\s*)?reasoningEffort:\s*['"]?([A-Za-z]+)['"]?\s*$/gm;
36
+ let m;
37
+ while ((m = re.exec(String(src || ''))) !== null) {
38
+ const v = String(m[1]).toLowerCase();
39
+ if (!v) continue;
40
+ counts[v] = (counts[v] || 0) + 1;
41
+ }
42
+ const efforts = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
43
+ return { efforts, counts };
44
+ }
45
+
46
+ /**
47
+ * 判定"当前会话模型能不能承接 preset 声明的 effort"(纯函数,便于单测)。
48
+ * @param {object} input
49
+ * @param {{provider:string,model:string}|null} input.selection - `agentDefaultModel.currentSelection()`
50
+ * @param {string[]} input.required - preset 声明的 effort 值
51
+ * @param {object|null} input.info - `llm.resolveModelInfo(provider, model)` 的结果
52
+ * @param {Error|null} [input.infoError] - 读模型信息时抛的错(有值 ⇒ 我们不假装知道)
53
+ * @returns {{status:string, warn:boolean, message:string, available:string[], missing:string[]}}
54
+ */
55
+ export function effortPreflightPlan({ selection, required, info, infoError } = {}) {
56
+ const req = Array.isArray(required) ? required.filter(Boolean) : [];
57
+ const sel = selection && selection.provider && selection.model ? selection : null;
58
+ if (!sel) return { status: 'no-route', warn: false, message: '', available: [], missing: req };
59
+ if (!req.length) return { status: 'no-preset-efforts', warn: false, message: '', available: [], missing: [] };
60
+ const where = `${sel.provider}/${sel.model}`;
61
+ if (infoError || !info) {
62
+ // 读不到能力表 ⇒ **不假装知道**(也不打扰):模型可能还没注册/适配器未就绪。
63
+ return { status: 'unknown', warn: false, message: '', available: [], missing: [] };
64
+ }
65
+ const reasoning = info.reasoning;
66
+ const available = Array.isArray(reasoning && reasoning.efforts) ? reasoning.efforts.map((e) => String(e && e.id)).filter(Boolean) : [];
67
+ const set = new Set(available);
68
+ const missing = req.filter((r) => !set.has(String(r).toLowerCase()));
69
+ if (!reasoning) {
70
+ // 宿主语义:reasoning === undefined ⇒ **任何** reasoningEffort 都会被拒。
71
+ return {
72
+ status: 'no-reasoning-support',
73
+ warn: true,
74
+ available,
75
+ missing: req,
76
+ message: `[expert-team] 当前会话模型 ${where} 未声明任何 reasoningEfforts(宿主视为"不支持 reasoning effort")⇒ preset 里带 effort 的角色派工会被拒(UNSUPPORTED_REASONING_EFFORT)。修法:在 ~/.dsh/settings.yaml 的 agent-default-model 条目给该模型补 reasoningEfforts(${EFFORT_DOMAIN.join('/')}),或把会话切到官方路由。`,
77
+ };
78
+ }
79
+ if (missing.length) {
80
+ return {
81
+ status: 'missing-efforts',
82
+ warn: true,
83
+ available,
84
+ missing,
85
+ message: `[expert-team] 当前会话模型 ${where} 只声明了 reasoningEfforts=[${available.join(',') || '(空)'}],而 preset 里声明的 [${missing.join(',')}] 不在其中 ⇒ 这些角色的派工会被宿主拒绝(UNSUPPORTED_REASONING_EFFORT;宿主在任何网络 I/O 之前就拒)。修法:在 ~/.dsh/settings.yaml 的 agent-default-model 条目补 reasoningEfforts(${EFFORT_DOMAIN.join('/')}),或把会话切到官方路由。`,
86
+ };
87
+ }
88
+ return { status: 'ok', warn: false, message: '', available, missing: [] };
89
+ }
90
+
91
+ /** 默认的有界重探节奏(服务晚挂时用;与 `OPTIONAL_PROBE_DELAYS_MS` 同一套纪律)。 */
92
+ export const EFFORT_PROBE_DELAYS_MS = Object.freeze([250, 1000, 3000]);
93
+
94
+ /**
95
+ * 接线:读 preset 声明 → 读当前路由与模型能力 → 下单(不阻断)→ 结论在**最后一轮**才播报。
96
+ * deps 全部注入,便于用假 ctx 单测。
97
+ * @param {object} deps
98
+ * @param {() => Promise<string>} [deps.readPresetSource] - 读 preset 源文本(读不到 ⇒ 静默跳过)
99
+ * @param {(ctx:object)=>{provider:string,model:string}|null} [deps.readSelection]
100
+ * @param {(provider:string, model:string)=>Promise<object>} [deps.readModelInfo]
101
+ * @param {(line:string)=>void} [deps.warn] - 播报出口(默认 console.warn)
102
+ * @param {(type:string,payload:object)=>void} [deps.onEvent]
103
+ */
104
+ export function createEffortPreflight(deps = {}) {
105
+ const { readPresetSource, readSelection, readModelInfo, onEvent } = deps;
106
+ const say = typeof deps.warn === 'function' ? deps.warn : (line) => console.warn(line);
107
+ const emit = typeof onEvent === 'function' ? onEvent : () => {};
108
+ let done = false;
109
+ return async function runEffortPreflight(ctx, { isLast = true } = {}) {
110
+ if (done) return { status: 'already-done', warn: false };
111
+ try {
112
+ const src = await readPresetSource();
113
+ const { efforts: required, counts } = declaredEffortsFromPresetSource(src);
114
+ const selection = readSelection(ctx);
115
+ if (!selection) { if (isLast) done = true; return { status: 'no-route', warn: false }; }
116
+ let info = null, infoError = null;
117
+ try { info = await readModelInfo(selection.provider, selection.model); } catch (e) { infoError = e; }
118
+ const plan = effortPreflightPlan({ selection, required, info, infoError });
119
+ if (isLast) done = true;
120
+ if (plan.warn) {
121
+ const detail = Object.keys(counts).length ? `(preset 里 ${Object.entries(counts).map(([k, v]) => `${v} 个用 ${k}`).join('、')})` : '';
122
+ try { say(detail ? plan.message.replace('。修法:', `${detail}。修法:`) : plan.message); } catch { /* 打印失败也不能影响加载 */ }
123
+ emit('effort-preflight-warn', { status: plan.status, provider: selection.provider, model: selection.model, available: plan.available, missing: plan.missing });
124
+ }
125
+ return plan;
126
+ } catch (e) {
127
+ // 探测自身出错 ⇒ 静默(fail-open),但留一条事件供排查
128
+ try { emit('effort-preflight-error', { error: String((e && e.message) || e) }); } catch { /* ignore */ }
129
+ if (isLast) done = true;
130
+ return { status: 'error', warn: false };
131
+ }
132
+ };
133
+ }
134
+ /** 仅供测试:复位"只播报一次"的开关(由 command.js 包一层,见 `_resetEffortPreflight`)。 */
135
+ export function _resetEffortPreflightState() { /* 开关在 createEffortPreflight 的闭包里,测试用新实例即可 */ }
@@ -66,13 +66,14 @@ export function runScopedTarget(absPath, teamRootAbs) {
66
66
  const parts = abs.slice(root.length + sep.length).split(sep).filter(Boolean);
67
67
  if (parts.length !== 2) return null;
68
68
  const [runId, base] = parts;
69
- if (base === 'TASKS.json') return { runId, kind: 'tasks', abs };
70
- if (base === 'SPEC.md') return { runId, kind: 'spec', abs };
69
+ // `base`(文件名)随 target 一起返回:R1 工件归属门禁要靠它查表(见 `lib/artifact-ownership.js`)。
70
+ if (base === 'TASKS.json') return { runId, kind: 'tasks', base, abs };
71
+ if (base === 'SPEC.md') return { runId, kind: 'spec', base, abs };
71
72
  // 其余 run 内文件:**看得见,但不拦**(`violationsFor` 对非 tasks/spec 一律返回空)。
72
73
  // 为什么现在要「看见」它们(2026-09-13,设计稿 §十二 第 3 步):并发写留痕需要观察**所有** run 工件 ——
73
74
  // 而实测那次并发事故发生在 `CONTRACT.md` 上(T29「CONTRACT 单写者收口」),恰恰不是这两份受门禁保护的文件。
74
75
  // ⚠️ 这是**只读扩展**:对 tasks/spec 的判定与拦截行为**逐字未变**。
75
- return { runId, kind: 'other', abs };
76
+ return { runId, kind: 'other', base, abs };
76
77
  }
77
78
 
78
79
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.3.14",
3
+ "version": "1.3.16",
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",
@@ -88,7 +88,7 @@
88
88
  "check:name": "node scripts/rename-package.mjs --check",
89
89
  "test:regression": "node regression.test.mjs",
90
90
  "test:e2e": "node e2e.test.mjs",
91
- "test:all": "node bootstrap.test.mjs && node host-settings.test.mjs && node smoke.test.mjs && node regression.test.mjs && node e2e.test.mjs && node flow.test.mjs && node plan-decide.test.mjs && node plan-discard.test.mjs && node models-honesty.test.mjs && node evidence-gate.test.mjs && node sync-gate.test.mjs && node artifact-writer.test.mjs && node dag.test.mjs && node metrics.test.mjs && node broken-chain.test.mjs && node schema-warn.test.mjs && node template-copy.test.mjs && node task-binding.test.mjs && node role-merge.test.mjs && node wf-recovery.test.mjs && node panorama.test.mjs && node run-ownership.test.mjs && node role-identity.test.mjs && node command-parse.test.mjs && node member-registry.test.mjs && node schema-check.test.mjs && node run-health.test.mjs && node cordis-fs-port.test.mjs && node scaffold-fingerprint.test.mjs && node preset-lint.test.mjs && node stranded-tasks.test.mjs && node agent-scope-guard.test.mjs && node schema-warn-noise.test.mjs && node tool-card-status.test.mjs && node dag-status.test.mjs && node capacity-limits.test.mjs && node task-cas.test.mjs && node write-lock.test.mjs && node card-stall.test.mjs && node multi-session-and-confirm.test.mjs && node standing-rules.test.mjs && node scope-overlap.test.mjs && node task-cancel-protect.test.mjs && node wait-run.test.mjs && node rework-loop.test.mjs && node client-css-integrity.test.mjs && node interception.test.mjs && node mutation-catalog.test.mjs && node write-bypass-ratchet.test.mjs && node asi-hazards.test.mjs && node dispatch-contract.test.mjs && node closure-ledger.test.mjs && node rework-nature.test.mjs && node loop-guard.test.mjs && node vocab-consistency.test.mjs && node state-no-write.test.mjs && node routes-shared.test.mjs && node metrics-render.test.mjs && node metrics-collect.test.mjs && node first-runnable.test.mjs && node closing-budget.test.mjs && node scan-single-source.test.mjs && node tier.test.mjs && node dispatch-ledger.test.mjs && node tier-gate.test.mjs && node tier-badge.test.mjs && node validate-module.test.mjs && node settings.test.mjs && node settings-page.test.mjs && node policy.test.mjs && node phase-accounting.test.mjs && node log-parse-module.test.mjs && node command-parse-module.test.mjs && node authority.test.mjs && node write-tracer.test.mjs && node token-accounting.test.mjs && node lead-toolface.test.mjs && node settings-consumers.test.mjs && node settings-wiring.test.mjs && node state-perf-guard.test.mjs && node state-sections.test.mjs && node artifact-ownership.test.mjs",
91
+ "test:all": "node bootstrap.test.mjs && node host-settings.test.mjs && node smoke.test.mjs && node regression.test.mjs && node e2e.test.mjs && node flow.test.mjs && node plan-decide.test.mjs && node plan-discard.test.mjs && node models-honesty.test.mjs && node evidence-gate.test.mjs && node sync-gate.test.mjs && node artifact-writer.test.mjs && node dag.test.mjs && node metrics.test.mjs && node broken-chain.test.mjs && node schema-warn.test.mjs && node template-copy.test.mjs && node task-binding.test.mjs && node role-merge.test.mjs && node wf-recovery.test.mjs && node panorama.test.mjs && node run-ownership.test.mjs && node role-identity.test.mjs && node command-parse.test.mjs && node member-registry.test.mjs && node schema-check.test.mjs && node run-health.test.mjs && node cordis-fs-port.test.mjs && node scaffold-fingerprint.test.mjs && node preset-lint.test.mjs && node stranded-tasks.test.mjs && node agent-scope-guard.test.mjs && node schema-warn-noise.test.mjs && node tool-card-status.test.mjs && node dag-status.test.mjs && node capacity-limits.test.mjs && node task-cas.test.mjs && node write-lock.test.mjs && node card-stall.test.mjs && node multi-session-and-confirm.test.mjs && node standing-rules.test.mjs && node scope-overlap.test.mjs && node task-cancel-protect.test.mjs && node wait-run.test.mjs && node rework-loop.test.mjs && node client-css-integrity.test.mjs && node interception.test.mjs && node mutation-catalog.test.mjs && node write-bypass-ratchet.test.mjs && node asi-hazards.test.mjs && node dispatch-contract.test.mjs && node closure-ledger.test.mjs && node rework-nature.test.mjs && node loop-guard.test.mjs && node vocab-consistency.test.mjs && node state-no-write.test.mjs && node routes-shared.test.mjs && node metrics-render.test.mjs && node metrics-collect.test.mjs && node first-runnable.test.mjs && node closing-budget.test.mjs && node scan-single-source.test.mjs && node tier.test.mjs && node dispatch-ledger.test.mjs && node tier-gate.test.mjs && node tier-badge.test.mjs && node validate-module.test.mjs && node settings.test.mjs && node settings-page.test.mjs && node policy.test.mjs && node phase-accounting.test.mjs && node log-parse-module.test.mjs && node command-parse-module.test.mjs && node authority.test.mjs && node write-tracer.test.mjs && node token-accounting.test.mjs && node lead-toolface.test.mjs && node settings-consumers.test.mjs && node settings-wiring.test.mjs && node state-perf-guard.test.mjs && node state-sections.test.mjs && node artifact-ownership.test.mjs && node subagent-bar.test.mjs && node r1-ownership-gate.test.mjs && node artifact-redirect-watch.test.mjs && node effort-preflight.test.mjs",
92
92
  "test:toolcard": "node tool-card-status.test.mjs",
93
93
  "test:flow": "node flow.test.mjs",
94
94
  "test:decide": "node plan-decide.test.mjs",
@@ -87,7 +87,7 @@ whenToUse: 用户用 /team 发起、要求组建「专家团」,**或(自动
87
87
 
88
88
  团队通过 `<run-dir>/` 下的工件共享上下文,而不是互相看对方的完整对话历史。规则:
89
89
 
90
- - **R1(唯一权威表述 · 本节是唯一权威处,其它文件只许引用、不得另写定义)**:**run 工件一律由产出它的角色自己 `write` 到 `<run-dir>/`**;角色只回 `path` + 摘要 + `verdict`;lead **没有 `write` 工具,只读工件做门控与裁决**;交付时用 `dsh_im_return_file` 把关键工件发给用户。
90
+ - **R1(唯一权威表述 · 本节是唯一权威处,其它文件只许引用、不得另写定义)**:**run 工件一律由产出它的角色自己 `write` 到 `<run-dir>/`**;角色只回 `path` + 摘要 + `verdict`;lead **没有 `write` 工具,只读工件做门控与裁决**;交付时用 `dsh_im_return_file` 把关键工件发给用户。 **机读真源**:`lib/artifact-ownership.js` 的 `ARTIFACT_OWNERS`("哪份工件归谁"只有这一份;协议文本与它不一致时**以代码为准**)。 **门禁强度(如实说,别当成"不可能违反")**:`write`/`edit` 通道有**硬门禁** —— 挂在宿主 `tools/pre-execute`(**写盘之前**),判据是**创建放行 / 覆写别人的工件当场拒绝**;但 preset 里持有 `bash` 的角色(backend/frontend/researcher/qa/dba/devops)理论上可用重定向绕过,首版**刻意不对 bash 参数做启发式检查**(易误伤)。
91
91
  - 每个角色**只读它上一阶段产出的工件 + 直接输入**,结论写进 `<run-dir>/` 的对应工件(写法见上一条 R1),返回值只带 `path` + 摘要 + `verdict` 这些小字段。
92
92
  - **任务看板(唯一名 = `任务看板.md`)**:`team/<run-id>/任务看板.md`(任务计划 + 状态表 + 当前阶段)**由你指派的一名成员维护**(有 `write` 的那个,通常 pm;每个阶段结束时更新一次),把「任务安排 + 执行进度」写进该工件(工件以 `path` 可核验,交付时由 lead 用 `dsh_im_return_file` 发给用户;「聊天框可点击产出文件行」的机制未独立证实,不作为承诺)。
93
93
  - **为什么要角色落盘**:工件必须成为**产出者本轮的产出文件** —— `path` 可核验、可复算,lead 只按 `path` 读盘做门控与裁决;交付时由 lead 用 `dsh_im_return_file` 把关键工件直接发给用户。⚠️ **如实标注**:(该"只认 lead 的 write"机制未独立证实,见 `RESEARCH.md` §7),本协议不依赖它。
@@ -109,7 +109,7 @@ persist 模式下,`members` 记录每个角色的可继续子 agent id,供 r
109
109
  "mode": "one-shot | persist",
110
110
  "deliverable": "code+artifacts | artifacts-only",
111
111
  "coverage": [ { "constraint": "<用户约束>", "tasks": ["be-1", "fe-2"] } ],
112
- "members": ["backend:<subagentId>", "frontend:<subagentId>"],
112
+ "members": ["<agentSessionId>:backend", "<agentSessionId>:frontend"], // 形状 = <agentId>:<role>(真源:lib/command.js 的 roleOfAgent / membersFromState)
113
113
  "updatedAt": "<iso>"
114
114
  }
115
115
  ```