@yangdcm/dsh-expert-team 1.3.2 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,45 @@
3
3
  本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
4
4
  `package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
5
5
 
6
+ ## 1.3.4
7
+
8
+ **把 1.3.2 留下的 7 个"有持久化、无消费者"的设置真的接上**(display 4 + roster 3),并清掉当时的假承诺。
9
+
10
+ - **display 四项**(此前只有存储层、零消费者;根因是客户端**全仓只有一处 `/settings` 拉取**、且只在设置表单里):
11
+ - `pollMs`:浮层/面板轮询与画布 `--watch` 轮询都读它(此前分别写死 3000/1200 与 3000);
12
+ - `capsuleMs`:**真正实现**"胶囊 N 毫秒后自动消失"。此前只入队、**永不自动出队** —— 旧代码里的
13
+ `4000` 是**同文本去重窗口**、被误当成停留时长,所以这个功能**压根不存在**;`0 = 不自动消失` 保留;
14
+ - `panelWidth`:设置值 = **默认宽度**;拖动结束**防抖写回设置**(单一真源,localStorage 只作首帧缓存),
15
+ 并把三处互不一致的默认/值域对齐(设置 280..900/420、拖动 clamp 640→900、CSS 与画布回退 360→420);
16
+ - `defaultTab`:面板初始页签取设置(用户点过就以点击为准)。
17
+ - 新增**客户端设置桥** `useDisplaySettings()`/`EXPERT_DISPLAY`:挂载时拉一次,**保存成功即当场生效**
18
+ (不必重启/刷新);失败静默回默认值,不影响面板工作。
19
+ - **roster 三项**:命令行**没给**对应参数时用设置兜底,优先级 **flag > 设置 > 常量**:
20
+ - `deliverable`(`artifacts-only` ⇒ `noCode`)、`persist`、`defaultRoles`;
21
+ - **档位收窄的基线也跟着设置走**(否则"设了默认班底"会让 `--tier` 静默失效);
22
+ - 落点是 `createRun` 的**唯一一处**(覆盖 `/team`、浮层决策、`/decide` 等所有建 run 入口),
23
+ 判定抽成纯函数 `resolveRosterDefaults()` 便于单测;`--profile` 点过名的仍优先于设置。
24
+ - **清掉假承诺**:`INERT_SETTINGS` 现为**空对象**(7 项全部接线,机制保留作棘轮落点);
25
+ 标记文案改为**版本中立**(`(暂未生效:尚未接线 —— …)`),并**移除用户可见文案里的行号**
26
+ (行号会腐烂、且对用户无意义 —— 证据一律写进代码注释)。
27
+ - **新增护栏** `settings-wiring.test.mjs`(进 `npm run test:all`):roster 三项**行为级**(真跑 `createRun`,
28
+ 断言 `STATE.json`/`ROSTER.json`/`RUN.log` 三处一致 + flag 优先级 + 档位收窄基线)、画布轮询**行为级**
29
+ (生成的 live 脚本里就是设置值;越界回默认)、`gates.loopGuard` 的 **getter 语义**(同一钩子实例下改设置
30
+ ⇒ 行为**双向即时翻转**、**无需重建钩子**),display 四项为**源码级**断言(本包无 DOM、不引依赖)。
31
+ - **更正 1.3.2 的描述**:1.3.2 曾写"待 1.3.3 接线",而 1.3.3 实际发的是宿主 schema 的枚举中文描述,
32
+ 接线在本版完成 —— 因此标记文案改为**不承诺版本号**。
33
+
34
+ ## 1.3.3
35
+
36
+ **宿主 schema 的枚举成员带上中文描述**(1.3.2 记录为"跳过"的那条,实测后补上)。
37
+
38
+ - 1.3.2 记的是"无法核实 schemastery 的可靠写法 ⇒ 跳过";随后查到其类型定义里
39
+ `description(text)` 的注释就写着 **"for documentation or form UIs"**,且用真 schemastery 实测:
40
+ `z.const('developer').description('技术开发者')` 能把标签写进成员的 `meta.description`,
41
+ schema 照常建成、**值域/类型/默认值完全不变** ⇒ 于是补上(`buildHostSchema` 里按
42
+ "方法确实存在才调用" 的既定纪律挂描述)。
43
+ - 本版**无功能改动之外的任何行为变化**;对设置页无影响(那一侧用 1.3.2 的 `labels`)。
44
+
6
45
  ## 1.3.2
7
46
 
8
47
  **诚实性收口:把"点了没反应"的设置项要么接线、要么如实标注。**
package/client.js CHANGED
@@ -22,7 +22,7 @@ window.__ModuleLoader__.load({
22
22
  var h = createElement
23
23
 
24
24
  var CSS =
25
- '.exp-panel{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:100vw;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,var(--bg,#fff));border-left:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));box-shadow:-10px 0 32px rgba(0,0,0,.16);z-index:120;font-size:13px;color:var(--dsw-alias-label-primary,var(--text,#1f2328));font-family:-apple-system,"Segoe UI","PingFang SC",sans-serif}' +
25
+ '.exp-panel{position:fixed;top:0;right:0;bottom:0;width:420px;max-width:100vw;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base,var(--bg,#fff));border-left:1px solid var(--dsw-alias-border-l2,var(--border,#d0d7de));box-shadow:-10px 0 32px rgba(0,0,0,.16);z-index:120;font-size:13px;color:var(--dsw-alias-label-primary,var(--text,#1f2328));font-family:-apple-system,"Segoe UI","PingFang SC",sans-serif}' +
26
26
  '.exp-head{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,var(--border,#d0d7de));background:linear-gradient(180deg,var(--dsw-alias-bg-module-platform,var(--bg-subtle,#f6f8fa)),var(--dsw-alias-bg-base,var(--bg,#fff)));display:flex;align-items:center;justify-content:space-between;flex:none}' +
27
27
  '.exp-head.exp-grab{cursor:move;user-select:none}' +
28
28
  '.exp-dock-handle{position:absolute;left:-2px;top:0;bottom:0;width:9px;cursor:col-resize;z-index:3;transition:background .12s}' +
@@ -293,6 +293,74 @@ window.__ModuleLoader__.load({
293
293
  function setOpen() { open = !open; notify() }
294
294
  function subscribe(f) { listeners.add(f); return function () { listeners.delete(f) } }
295
295
  function useExternal(getValue) { var s = useState(getValue()); useEffect(function () { return subscribe(function () { s[1](getValue()) }) }, []); return s[0] }
296
+
297
+ // ── 设置桥(1.3.4):`display.*` 的**唯一**运行态来源 ───────────────────────────
298
+ // 为什么必须有它:此前全仓只有一处 `/settings` 拉取,而且只在设置表单里 —— display 四项因此
299
+ // 永远流不到运行态(轮询间隔/胶囊停留/面板宽度/默认页签各自硬编码或读 localStorage)。
300
+ // 这里建**一条**共享的路:面板挂载时拉一次;设置页保存成功后复用同一条路把新值通知出去
301
+ // ⇒ 改设置**当场生效**(同一 client、进程内),不必重启。
302
+ var EXPERT_DISPLAY = { pollMs: 3000, capsuleMs: 4000, panelWidth: 420, defaultTab: 'team' }
303
+ var DISPLAY_TABS = ['team', 'tasks', 'board', 'info']
304
+ var DISPLAY_RANGE = { pollMs: [1000, 60000], capsuleMs: [0, 60000], panelWidth: [280, 900] }
305
+ var displayLoaded = false
306
+ var displayLoading = null
307
+ function clampInt(v, lo, hi, def) { var n = Number(v); if (!isFinite(n)) return def; n = Math.round(n); return Math.max(lo, Math.min(hi, n)) }
308
+ /** 从 `/settings` 响应挑出 display 四项并按值域夹紧;缺项/越界一律回退当前值(不制造 NaN/空白)。 */
309
+ function pickDisplay(settings) {
310
+ var d = (settings && settings.display) || {}
311
+ return {
312
+ pollMs: clampInt(d.pollMs, DISPLAY_RANGE.pollMs[0], DISPLAY_RANGE.pollMs[1], EXPERT_DISPLAY.pollMs),
313
+ capsuleMs: clampInt(d.capsuleMs, DISPLAY_RANGE.capsuleMs[0], DISPLAY_RANGE.capsuleMs[1], EXPERT_DISPLAY.capsuleMs),
314
+ panelWidth: clampInt(d.panelWidth, DISPLAY_RANGE.panelWidth[0], DISPLAY_RANGE.panelWidth[1], EXPERT_DISPLAY.panelWidth),
315
+ defaultTab: DISPLAY_TABS.indexOf(String(d.defaultTab)) >= 0 ? String(d.defaultTab) : EXPERT_DISPLAY.defaultTab
316
+ }
317
+ }
318
+ /** 应用一份设置:换对象(useExternal 才看得见变化)+ 未拖动过宽度时采纳设置值 + 通知重渲染。 */
319
+ function applyDisplaySettings(settings) {
320
+ var next = pickDisplay(settings)
321
+ var changed = next.pollMs !== EXPERT_DISPLAY.pollMs || next.capsuleMs !== EXPERT_DISPLAY.capsuleMs || next.defaultTab !== EXPERT_DISPLAY.defaultTab
322
+ EXPERT_DISPLAY = next
323
+ // 宽度:设置值是**默认宽度**;用户拖过就以拖动结果为准(拖动结束会把宽度写回设置)。
324
+ if (!panelWidthUserSet && next.panelWidth !== panelWidth) { panelWidth = clampW(next.panelWidth); changed = true }
325
+ if (changed) notify()
326
+ return next
327
+ }
328
+ function fetchSettingsPayload() {
329
+ return fetch('/plugins/dsh-expert-team/settings').then(function (r) {
330
+ return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
331
+ })
332
+ }
333
+ /** 拉一次设置(幂等:同一次加载只拉一次;失败静默 —— 面板照旧用默认值工作)。 */
334
+ function loadDisplaySettings() {
335
+ if (displayLoaded) return displayLoading || Promise.resolve(EXPERT_DISPLAY)
336
+ displayLoading = fetchSettingsPayload().then(function (res) {
337
+ displayLoaded = true
338
+ if (res.ok && res.d && res.d.ok && res.d.settings) applyDisplaySettings(res.d.settings)
339
+ return EXPERT_DISPLAY
340
+ }).catch(function () { displayLoaded = true; return EXPERT_DISPLAY })
341
+ return displayLoading
342
+ }
343
+ /** 运行态取 display 配置(挂载时拉一次;保存后由 applyDisplaySettings 通知更新)。 */
344
+ function useDisplaySettings() {
345
+ var v = useExternal(function () { return EXPERT_DISPLAY })
346
+ useEffect(function () { loadDisplaySettings() }, [])
347
+ return v
348
+ }
349
+ /** 拖动结束把宽度**防抖**写回设置(单一真源);localStorage 只作首帧缓存。 */
350
+ var widthSaveTimer = null
351
+ function persistDisplayWidth(w) {
352
+ if (widthSaveTimer) clearTimeout(widthSaveTimer)
353
+ widthSaveTimer = setTimeout(function () {
354
+ widthSaveTimer = null
355
+ try {
356
+ fetch('/plugins/dsh-expert-team/settings', {
357
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ 'display.panelWidth': w })
358
+ }).then(function (r) { return r.json().catch(function () { return null }) }).then(function (d) {
359
+ if (d && d.ok && d.settings) applyDisplaySettings(d.settings)
360
+ }).catch(function () {})
361
+ } catch (e) {}
362
+ }, 400)
363
+ }
296
364
  /** 对话流 workflow 卡是否已回退到 dsh 原生(localStorage 开关,刷新生效)。 */
297
365
  function nativeWfOn() { try { return window.localStorage.getItem('et-native-workflow') === '1' } catch (e) { return false } }
298
366
  function useCurrentSession() { return useExternal(function () { return currentSessionId }) }
@@ -306,8 +374,12 @@ window.__ModuleLoader__.load({
306
374
  function saveLS(key, v) { try { window.localStorage.setItem(key, JSON.stringify(v)) } catch (e) {} }
307
375
  var dockState = loadLS('et-dock', true)
308
376
  var panelPos = loadLS('et-pos', { x: 0, y: 0 })
309
- var panelWidth = loadLS('et-width', 360)
310
- function clampW(w) { return Math.max(280, Math.min(640, w)) }
377
+ // 1.3.4:默认宽度来自设置 `display.panelWidth`(首帧用 localStorage 缓存避免闪烁);
378
+ // 值域与设置对齐 280..900(此前 clamp 640、CSS 写死 360 —— 三处互不一致)。
379
+ var panelWidthLS = loadLS('et-width', null)
380
+ var panelWidthUserSet = typeof panelWidthLS === 'number'
381
+ var panelWidth = clampW(typeof panelWidthLS === 'number' ? panelWidthLS : EXPERT_DISPLAY.panelWidth)
382
+ function clampW(w) { return Math.max(280, Math.min(900, w)) }
311
383
  function setDock(v) { if (dockState === v) return; dockState = v; if (!v) { panelPos = { x: ((window && window.innerWidth) || 1280) - panelWidth, y: 80 }; saveLS('et-pos', panelPos) } saveLS('et-dock', dockState); notify() }
312
384
  function setPanelPos(x, y) { panelPos = { x: x, y: y }; saveLS('et-pos', panelPos); notify() }
313
385
  /**
@@ -323,7 +395,7 @@ window.__ModuleLoader__.load({
323
395
  if (changed) notify()
324
396
  } catch (e) {}
325
397
  }
326
- function setPanelWidth(w) { panelWidth = clampW(w); saveLS('et-width', panelWidth); notify() }
398
+ function setPanelWidth(w) { panelWidth = clampW(w); panelWidthUserSet = true; saveLS('et-width', panelWidth); notify(); persistDisplayWidth(panelWidth) }
327
399
 
328
400
  /**
329
401
  * ── 对话流卡片 → 右侧面板的「聚焦意图」通道 ──
@@ -1081,12 +1153,10 @@ window.__ModuleLoader__.load({
1081
1153
  useEffect(function () {
1082
1154
  var alive = true
1083
1155
  setLoading(true); setLoadErr('')
1084
- fetch('/plugins/dsh-expert-team/settings').then(function (r) {
1085
- return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
1086
- }).then(function (res) {
1156
+ fetchSettingsPayload().then(function (res) {
1087
1157
  if (!alive) return
1088
1158
  setLoading(false)
1089
- if (res.ok && res.d && res.d.ok && res.d.schema) { setData(res.d); return }
1159
+ if (res.ok && res.d && res.d.ok && res.d.schema) { applyDisplaySettings(res.d.settings); setData(res.d); return }
1090
1160
  var detail = (res.d && (res.d.error || (res.d.errors || []).join(';'))) || ''
1091
1161
  setLoadErr(t('读取设置失败:HTTP ', 'Reading settings failed: HTTP ') + res.status + (detail ? ' ' + t('(', '(') + detail + t(')', ')') : ''))
1092
1162
  }).catch(function (e) {
@@ -1107,6 +1177,7 @@ window.__ModuleLoader__.load({
1107
1177
  return
1108
1178
  }
1109
1179
  setData(function (prev) { return { schema: (prev && prev.schema) || [], settings: res.d.settings, ok: true } })
1180
+ applyDisplaySettings(res.d.settings) // 1.3.4:设置页改完 display 四项**当场生效**(不必重启/刷新)
1110
1181
  setMsg('已保存') // 1.3.2:经逐项核查,没有任何设置需要重启(见 lib/command.js 的 needsRestart 注释)⇒ 死分支已删
1111
1182
  }).catch(function (e) { setErr(String(e && e.message ? e.message : e)); setMsg('') })
1112
1183
  }
@@ -1179,6 +1250,8 @@ window.__ModuleLoader__.load({
1179
1250
  var pos = usePanelPos()
1180
1251
  useLang() // re-render when the shell language changes
1181
1252
  var pw = usePanelWidth()
1253
+ // 1.3.4:display 四项在这里取(挂载时拉一次设置;保存后即时通知更新)
1254
+ var dispCfg = useDisplaySettings()
1182
1255
  var dataS = useState(null), data = dataS[0], setData = dataS[1]
1183
1256
  var runsMetaS = useState([]); var runsMeta = runsMetaS[0], setRunsMeta = runsMetaS[1]
1184
1257
  var selTask = useState(null); var selTaskV = selTask[0], setSelTask = selTask[1]
@@ -1196,7 +1269,7 @@ window.__ModuleLoader__.load({
1196
1269
  var prevDecRef = useRef(0)
1197
1270
  var hoverS = useState(null); var hoverId = hoverS[0], setHover = hoverS[1]
1198
1271
  var view = useState('dag'); var viewV = view[0], setView = view[1]
1199
- var tabS = useState('team'); var tab = tabS[0], setTab = tabS[1]
1272
+ var tabS = useState(null); var tab = tabS[0] || dispCfg.defaultTab; var setTab = tabS[1] // 1.3.4:初始页签来自设置 display.defaultTab
1200
1273
  var listOpenS = useState(false); var listOpen = listOpenS[0], setListOpen = listOpenS[1]
1201
1274
  // 流转视图(波次带)自己的 UI 状态:宽视图 / 波折叠 / 未绑定任务展开
1202
1275
  var wideS = useState(false); var wideV = wideS[0], setWide = wideS[1]
@@ -1315,18 +1388,19 @@ window.__ModuleLoader__.load({
1315
1388
  var m = (d && d.members) || {}
1316
1389
  var busy = Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
1317
1390
  clearInterval(pollRef.current)
1318
- pollRef.current = setInterval(load, busy ? 1200 : 3000)
1391
+ // 1.3.4:间隔来自设置 display.pollMs(忙碌时按同一意图加速到 40%,下限 300ms)
1392
+ pollRef.current = setInterval(load, busy ? Math.max(300, Math.round(dispCfg.pollMs * 0.4)) : dispCfg.pollMs)
1319
1393
  }
1320
1394
  useEffect(function () {
1321
1395
  if (!isOpen && !viewMode) return undefined // panel closed & not canvas → no polling
1322
- load(); pollRef.current = setInterval(load, 3000)
1396
+ load(); pollRef.current = setInterval(load, dispCfg.pollMs)
1323
1397
  // catch up instantly when the user returns to the tab / window
1324
1398
  var onVis = function () { if (document.visibilityState === 'visible') load() }
1325
1399
  var onFocus = function () { load() }
1326
1400
  document.addEventListener('visibilitychange', onVis)
1327
1401
  window.addEventListener('focus', onFocus)
1328
1402
  return function () { clearInterval(pollRef.current); document.removeEventListener('visibilitychange', onVis); window.removeEventListener('focus', onFocus) }
1329
- }, [sessionId, selRunV, isOpen, viewMode])
1403
+ }, [sessionId, selRunV, isOpen, viewMode, dispCfg.pollMs])
1330
1404
 
1331
1405
  // Make the docked panel reserve space (shift the shell frame) instead of
1332
1406
  // covering the workspace; clears when closed or floating (md-preview trick).
@@ -1710,7 +1784,7 @@ window.__ModuleLoader__.load({
1710
1784
  // 可画宽度三源:实测容器 → 全屏画布按窗口估 → 停靠侧栏宽度偏好。
1711
1785
  // 「宽视图」开关终于有作用了:放宽单卡上限(否则 12 波里每波只有 2–3 人时,
1712
1786
  // 卡片被 176px 封顶、右侧大片空白)。
1713
- var baseW = flowW || (viewMode ? Math.max(320, ((typeof window !== 'undefined' && window.innerWidth) || 1200) - 340) : (typeof pw === 'number' ? pw : 360))
1787
+ var baseW = flowW || (viewMode ? Math.max(320, ((typeof window !== 'undefined' && window.innerWidth) || 1200) - 340) : (typeof pw === 'number' ? pw : 420))
1714
1788
  var avail = Math.max(240, baseW - 26)
1715
1789
  var widest = 1
1716
1790
  W.waves.forEach(function (w) { widest = Math.max(widest, w.members.length) })
@@ -2335,8 +2409,14 @@ window.__ModuleLoader__.load({
2335
2409
  try {
2336
2410
  var last = activityQ[0]
2337
2411
  if (last && last.text === text && Date.now() - (last.ts || 0) < 4000) return
2338
- activityQ.unshift({ icon: icon, text: String(text || '').slice(0, 90), ts: Date.now() })
2412
+ var entry = { icon: icon, text: String(text || '').slice(0, 90), ts: Date.now() }
2413
+ activityQ.unshift(entry)
2339
2414
  if (activityQ.length > 8) activityQ.length = 8
2415
+ // 1.3.4:停留时长来自设置 `display.capsuleMs`(0 = 不自动消失)。
2416
+ // ⚠️ 上面那个 4000 是**同文本去重窗口**,不是停留时长 —— 旧实现把它当停留时长,
2417
+ // 于是"胶囊 N 毫秒后消失"这个功能**压根不存在**(只入队、永不自动出队,>8 才截断)。
2418
+ var ttl = EXPERT_DISPLAY.capsuleMs
2419
+ if (ttl > 0) setTimeout(function () { var i = activityQ.indexOf(entry); if (i >= 0) { activityQ.splice(i, 1); notify() } }, ttl)
2340
2420
  notify()
2341
2421
  } catch (e) {}
2342
2422
  }
package/lib/command.js CHANGED
@@ -1704,6 +1704,57 @@ function resolveLoopGuard(config, base) {
1704
1704
  return LOOP_GUARD_ENABLED;
1705
1705
  }
1706
1706
 
1707
+ // ── roster 三项:命令行**未显式给出**时用设置兜底(1.3.4 接线)──────────────────────────
1708
+ //
1709
+ // 为什么放在"建 run 的入口"而不是解析器里:`parseTeamCommand` 是**纯解析**(拿不到设置,也不该拿 ——
1710
+ // 它是被测得很细的纯函数)。把"flag 没给 ⇒ 取设置"放在 `createRun` 的**唯一落点**,才能同时覆盖
1711
+ // `/team`、浮层决策、`/decide` 等所有建 run 的入口,而不是每个入口各接一遍(那就有了第二个真源)。
1712
+ //
1713
+ // ⚠️ 键写成**限定路径**(`roster.persist` 等)不是装饰:`settings-consumers.test.mjs` 对**歧义叶子**
1714
+ // (`persist`/`deliverable`/`defaultRoles` 在本仓另有同名的不同概念,如 `mode.persist`、`--deliverable`)
1715
+ // **只认限定路径**命中的消费者 —— 写成 `r.persist` 会被判成"没有消费者",而那正是本仓最想避免的假绿。
1716
+ /** 读设置的 roster 三项(键 = spec 的限定路径;值已按 spec 归一)。 */
1717
+ export function rosterSettings() {
1718
+ const r = (currentSettings() || {}).roster || {};
1719
+ return {
1720
+ 'roster.defaultRoles': Array.isArray(r.defaultRoles) && r.defaultRoles.length ? r.defaultRoles.slice() : null,
1721
+ 'roster.deliverable': r.deliverable === 'artifacts-only' ? 'artifacts-only' : 'code+artifacts',
1722
+ 'roster.persist': r.persist === true,
1723
+ };
1724
+ }
1725
+
1726
+ /**
1727
+ * 把设置落到一次建 run 的 `mode` 上:**flag > 设置 > 常量**。
1728
+ * 纯函数(设置与常量都由参数传入)⇒ 可单测;`createRun` 只负责调用它。
1729
+ *
1730
+ * @param mode - `parseTeamCommand` 的产物(`persist`/`noCode`/`roles` 都是"flag 给了才为真/非空")
1731
+ * @param rosterSet - `rosterSettings()` 的产物(可为 undefined)
1732
+ * @param fallbackRoles - 常量兜底班底(默认 `DEFAULT_ROLES`)
1733
+ * @returns `{ persist, noCode, roles, rolesBase, rolesFromSettings }`
1734
+ * - `rolesBase`:**档位收窄的基线**(设置了 `defaultRoles` 就用它,否则用常量);
1735
+ * - `rolesFromSettings`:告知调用方"这个班底来自设置、不是用户点名" ⇒ 档位收窄仍应生效。
1736
+ */
1737
+ export function resolveRosterDefaults(mode, rosterSet, fallbackRoles) {
1738
+ const m = mode && typeof mode === 'object' ? mode : {};
1739
+ const base = Array.isArray(fallbackRoles) && fallbackRoles.length ? fallbackRoles.slice() : DEFAULT_ROLES.slice();
1740
+ const settingsRoles = rosterSet && Array.isArray(rosterSet['roster.defaultRoles']) && rosterSet['roster.defaultRoles'].length
1741
+ ? rosterSet['roster.defaultRoles'].slice() : null;
1742
+ const explicitRoles = Array.isArray(m.roles) && m.roles.length ? m.roles.slice() : null;
1743
+ return {
1744
+ persist: m.persist === true ? true : Boolean(rosterSet && rosterSet['roster.persist'] === true),
1745
+ noCode: m.noCode === true ? true : Boolean(rosterSet && rosterSet['roster.deliverable'] === 'artifacts-only'),
1746
+ roles: explicitRoles || settingsRoles,
1747
+ rolesBase: settingsRoles || base,
1748
+ rolesFromSettings: !explicitRoles && Boolean(settingsRoles),
1749
+ };
1750
+ }
1751
+
1752
+ /** 画布 live 模式的轮询间隔(毫秒):来自设置 `display.pollMs`(1.3.4 接线),越界回默认 3000。 */
1753
+ function canvasPollMs() {
1754
+ const n = Number(((currentSettings() || {}).display || {}).pollMs);
1755
+ return Number.isFinite(n) && n >= 1000 && n <= 60000 ? Math.round(n) : 3000;
1756
+ }
1757
+
1707
1758
  /**
1708
1759
  * 解析档位门是软还是硬。**非法值一律回默认(soft)**,不猜也不报错 ——
1709
1760
  * 一个拼错的开关值把流程卡死(hard)比静默放行更糟,而"卡死"正是最贵的失败模式。
@@ -3092,9 +3143,9 @@ function buildCanvasHtml({ runId, goal, state, roster, tasks, subagents, dir, cw
3092
3143
  <ol class="path" id="path">${stepper}</ol>
3093
3144
  <section><h2>团队编制 <span class="live" id="live-ts"></span></h2><div class="cards" id="roster">${rosterCards || '<div class="empty">(暂无成员)</div>'}</div></section>
3094
3145
  <section><h2>任务看板</h2><div class="board" id="board">${boardHtml || '<div class="empty">(暂无任务)</div>'}</div></section>
3095
- <div class="hint">本画布由 <code>/team canvas</code> 动态生成(数据源:${esc(dir)})。任务/成员状态随各阶段推进更新;想刷新就再跑一次 <code>/team canvas</code>。${watch ? ' <b>── 当前为 live 模式:每 3 秒轮询 host 状态接口自动刷新。</b>' : ''}</div>
3146
+ <div class="hint">本画布由 <code>/team canvas</code> 动态生成(数据源:${esc(dir)})。任务/成员状态随各阶段推进更新;想刷新就再跑一次 <code>/team canvas</code>。${watch ? ` <b>── 当前为 live 模式:每 ${Math.round(canvasPollMs() / 1000)} 秒轮询 host 状态接口自动刷新。</b>` : ''}</div>
3096
3147
  </div>
3097
- ${watch ? watchScript(runId, cwd) : ''}
3148
+ ${watch ? watchScript(runId, cwd, canvasPollMs()) : ''}
3098
3149
  </body>
3099
3150
  </html>`;
3100
3151
  }
@@ -3103,7 +3154,9 @@ ${watch ? watchScript(runId, cwd) : ''}
3103
3154
  // (md-preview-style host HTTP endpoint, avoiding typert Remotes) and live-updates
3104
3155
  // the phase stepper, roster cards and task board. Falls back silently if the
3105
3156
  // route is unavailable (e.g. webServer not mounted).
3106
- function watchScript(runId, cwd) {
3157
+ function watchScript(runId, cwd, pollMs) {
3158
+ // 1.3.4:轮询间隔来自设置 `display.pollMs`(此前是写死的 3000)。
3159
+ const per = Number.isFinite(Number(pollMs)) && Number(pollMs) >= 1000 ? Math.round(Number(pollMs)) : 3000;
3107
3160
  const base = '"/plugins/dsh-expert-team/state"';
3108
3161
  const stateUrl = `(${base}+"?cwd="+encodeURIComponent(${JSON.stringify(cwd)})+"&run="+encodeURIComponent(${JSON.stringify(runId)}))`;
3109
3162
  return `<script>
@@ -3134,7 +3187,7 @@ function watchScript(runId, cwd) {
3134
3187
  }catch(e){}
3135
3188
  }
3136
3189
  function poll(){ fetch(URL).then(function(r){if(!r.ok)return null;return r.json();}).then(function(d){if(d)render(d);}).catch(function(){}); }
3137
- poll(); setInterval(poll, 3000);
3190
+ poll(); setInterval(poll, ${per});
3138
3191
  })();
3139
3192
  </script>`;
3140
3193
  }
@@ -4252,6 +4305,16 @@ async function settleRuns(cwd, runArg, ctx) {
4252
4305
 
4253
4306
  async function createRun(ctx, invocation, cwd, mode) {
4254
4307
  if (mode.profile) mode = resolveProfile(mode.profile, mode); // apply a named profile (roles/deliverable/note)
4308
+ // ── roster 三项兜底(1.3.4 接线):**flag > 设置 > 常量**,唯一落点 ────────────────────
4309
+ // 放在 `--profile` 之后:命名档位(`resolveProfile`)显式点过的 roles/deliverable 仍优先于设置,
4310
+ // 与"用户在命令里点名 > 控制台偏好"同一条纪律。
4311
+ const rosterSet = rosterSettings();
4312
+ const rd = resolveRosterDefaults(mode, rosterSet, DEFAULT_ROLES);
4313
+ mode.persist = rd.persist;
4314
+ mode.noCode = rd.noCode;
4315
+ mode.rolesBase = rd.rolesBase;
4316
+ mode.rolesFromSettings = rd.rolesFromSettings;
4317
+ if (rd.roles && !(mode.roles && mode.roles.length)) mode.roles = rd.roles;
4255
4318
  // ── G 线(档位):先定档位,再建 run(档位要写进 TASK/ROSTER/STATE 三份工件)──────────
4256
4319
  // ① 显式 `--tier` **认不出来就拒绝**:静默退回默认会让用户以为自己选的那个生效了 ——
4257
4320
  // 这正是本仓反复复发的"沉默 ≠ 允许"(SKILL §7.29)。
@@ -4275,8 +4338,11 @@ async function createRun(ctx, invocation, cwd, mode) {
4275
4338
  // ③ **档位真正省派工的地方**:显式 `--tier` 且用户没点过角色名 ⇒ 按档定班底。
4276
4339
  // (没显式指定时**不**在这里收窄:档位选择发生在浮层,用户选完那一刻才收窄 —— 见 /decide 路由。)
4277
4340
  // `--roles` / `--profile` 点过名的,一律不动(`narrowedRoles` 只看"是不是基线班底")。
4278
- if (pickedTier && !(mode.roles && mode.roles.length)) {
4279
- const next = narrowedRoles(pickedTier, DEFAULT_ROLES, DEFAULT_ROLES);
4341
+ // 1.3.4:班底来自**设置**(`roster.defaultRoles`)时也算"基线" ⇒ 收窄仍应生效,
4342
+ // 只是基线换成设置里那套(否则"设了默认班底"会让档位收窄静默失效)。
4343
+ if (pickedTier && (!(mode.roles && mode.roles.length) || mode.rolesFromSettings)) {
4344
+ const narrowBase = mode.rolesBase || DEFAULT_ROLES;
4345
+ const next = narrowedRoles(pickedTier, (mode.roles && mode.roles.length ? mode.roles : narrowBase), narrowBase);
4280
4346
  if (next) { mode.roles = next; mode.rolesNarrowedBy = pickedTier; }
4281
4347
  }
4282
4348
  // 批 0-3:planDiscarded **代码强制** —— 用户明确丢弃过的同目标团队,不得由 lead 自动重建。
@@ -4515,7 +4581,7 @@ function warnLeadToolFaceOnce(status, detail) {
4515
4581
  LEAD_TOOLFACE_WARNED.add(status);
4516
4582
  console.warn(`[expert-team] lead 工具面**未**收窄(${status}):${detail}`);
4517
4583
  }
4518
- 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, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, resolveLoopGuard, LOOP_GUARD_ENV, effectiveLoopGuard: () => LOOP_GUARD_ENABLED, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
4584
+ 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, resolveLeadToolFace, LEAD_TOOLFACE_ENV, effectiveLeadToolFace: () => LEAD_TOOLFACE, resolveLoopGuard, LOOP_GUARD_ENV, effectiveLoopGuard: () => LOOP_GUARD_ENABLED, resolveRosterDefaults, rosterSettings, createRun, watchScript, canvasPollMs, installHostSettings, hostValues, hostScope, hostSettingsNote, updateHostSettings, pickFileOnly, pickHostExpressible, buildHostSchema, hostBase, hostSchemaPaths, reapplySettingsDerived, currentSettings, mergeSettings };
4519
4585
 
4520
4586
  export function apply(ctx, config) {
4521
4587
  // 留一份 config:设置在运行时改变(官方面板 / 浮层)时要**用同一份 config** 重算上限与档位门,
@@ -5469,10 +5535,13 @@ export function apply(ctx, config) {
5469
5535
 
5470
5536
  // ── F 线:设置控制台路由(GET 读 / POST 写)──────────────────────────────
5471
5537
  // 走 `withRoute`(B 线第 9 项抽出的共享件):方法校验 / 405 空体 / 500 兜底一条不落。
5472
- // **诚实边界(2026-09-15 更正)**:此前这里写"`roster`/`gates` 两类改完要重启、`display`
5473
- // 类即时生效"—— **两句都不实**:前者由 `reapplySettingsDerived()` 当场重算(见下),
5474
- // 后者在 1.3.2 审核中查明**根本没有消费者**(`display.*` 只有存储层,待 1.3.3 接线,
5475
- // 已在 hint 上如实标注「暂未生效」)。响应里不再声称任何一类需要重启。
5538
+ // **诚实边界(2026-09-15 更正 → 1.3.4 接线完成)**:此前这里写"`roster`/`gates` 两类改完要
5539
+ // 重启、`display` 类即时生效"—— 当时两句都不实。现状(都已在测试里钉住):
5540
+ // · `roster`/`gates`:由 `reapplySettingsDerived()` 当场重算,`roster.defaultRoles` 等在建 run
5541
+ // 时读(见 `resolveRosterDefaults()`)⇒ **无需重启**;
5542
+ // · `display.*`:1.3.4 起**真的有消费者** —— 浮层/面板由 `client.js` 的 `useDisplaySettings()`
5543
+ // (单一设置桥 `EXPERT_DISPLAY`)读,画布 live 轮询由本文件的 `canvasPollMs()` 读。
5544
+ // 响应里不声称任何一类需要重启(1.3.2 起 `needsRestart` 恒 false)。
5476
5545
  const disposeSettings = registerLocal({
5477
5546
  kind: 'exact',
5478
5547
  path: '/plugins/dsh-expert-team/settings',
@@ -61,7 +61,17 @@ export function buildHostSchema(z) {
61
61
  field = z.number();
62
62
  if (item.min != null && typeof field.min === 'function') field = field.min(item.min);
63
63
  if (item.max != null && typeof field.max === 'function') field = field.max(item.max);
64
- } else field = z.union(item.values.map((v) => z.const(v)));
64
+ } else {
65
+ // 枚举:值域仍是 `union([const(v)…])`(**类型/值域不动**);若规格层给了中文标签,
66
+ // 就在成员上挂 `.description(label)`。可行性已实测:真 schemastery 下 schema 照常建成,
67
+ // 标签进了成员的 `meta.description`(该方法的类型注释写明"for documentation or form UIs")。
68
+ // 方法不存在时**跳过**(本模块既定纪律:只有确实存在的方法才调用 —— 这里对应宿主/版本差异)。
69
+ field = z.union(item.values.map((v) => {
70
+ const c = z.const(v);
71
+ const label = item.labels && item.labels[v];
72
+ return label && typeof c.description === 'function' ? c.description(label) : c;
73
+ }));
74
+ }
65
75
  if (typeof field.default === 'function') field = field.default(item.default);
66
76
  inner[key] = field;
67
77
  }
package/lib/settings.js CHANGED
@@ -62,36 +62,29 @@ export const SETTINGS_SPEC = {
62
62
  },
63
63
  };
64
64
 
65
- // ── 「暂未生效」的项:**单一真源**(2026-09-15,1.3.2 诚实性收口)────────────────────
65
+ // ── 「暂未生效」的项:**单一真源**(1.3.2 建立 → 1.3.4 接线完成,故现在为空)──────────
66
66
  //
67
- // 为什么需要这张表:审核(两轮独立只读核验 + 全库 grep)发现 20 个设置项里有 10 项的**设置值
68
- // 没有任何消费者** —— 用户在设置页勾了、改了,什么都不会发生。其中 3 项已在本版处理
69
- // (`gates.boundaryTasks`/`gates.boundarySpec` 的行为本就**无条件强制**,故直接删除 spec;
70
- // `gates.loopGuard` 是"接错了源",已接线)。剩下 7 项**本版只标注、不接线**(留 1.3.3)。
67
+ // 这张表是**两处消费的同一个真源**:
68
+ // `decorateHint()` 把标记追加到 hint(设置页里用户可见);
69
+ // `settings-consumers.test.mjs` 用它当白名单,语义是**集合相等** ——
70
+ // 新增一个没有消费者的设置项 红;接线后忘了删这一行 也红(清单只减不增)。
71
71
  //
72
- // 为什么不干脆删掉这 7 项:它们描述的是**真要实现的能力**(浮层宽度、默认页签、默认班底…),
73
- // 不是误加的开关。但"存在即承诺"——把 hint 写成承诺、行为却是空的,与删掉那两条装饰开关的理由
74
- // 完全一样(不能双重标准)。所以:**如实标注"暂未生效"**,直到真的接上。
72
+ // 历史:1.3.2 的审计(两轮独立只读核验)发现 20 个设置项里 **10 项的设置值没有任何消费者**。
73
+ // 当时处理了 3 项(`gates.boundaryTasks`/`gates.boundarySpec` 的行为本就**无条件强制**,直接删 spec;
74
+ // `gates.loopGuard` 是"接错了源",已接线),剩下 7 项(display 4 + roster 3)登记在此、界面标注。
75
+ // **1.3.4 把这 7 项全部接线** ⇒ 表空 = "零装饰性开关"的证明。机制保留:将来若又要"先上表盘、后做行为",
76
+ // 把限定路径写进来即可(界面标注与 ratchet 白名单会自动跟上;接线后删掉那一行,两处同时消失)。
75
77
  //
76
- // 用法(两处消费,同一个真源):
77
- // `decorateHint()` 把标记追加到 hint(设置页表单可见);
78
- // `settings-consumers.test.mjs` 用它做白名单,语义是**集合相等**:
79
- // 新增死开关 ⇒ 红;接线后忘了删这一行 ⇒ 也红(逼着清单只减不增)。
80
- // 键 = 限定路径;值 = 一句"为什么现在没用"(写**实测来源**,不写猜测)。
81
- export const INERT_SETTINGS = {
82
- 'display.pollMs': '现值硬编码在浮层(client.js:1317 的 1200/3000)',
83
- 'display.capsuleMs': '现值硬编码在浮层(client.js:2336 的 4000)',
84
- 'display.panelWidth': '现值来自 localStorage 的 `et-width`(client.js:309),与设置无关',
85
- 'display.defaultTab': '现值硬编码为 team(client.js:1198)',
86
- 'roster.deliverable': '现由 `/team --no-code` 派生(mode.noCode),设置值不参与',
87
- 'roster.persist': '现仅由 `/team --persist` 置位(mode.persist),设置值不参与',
88
- 'roster.defaultRoles': '现用常量 DEFAULT_ROLES 或 `--roles`,设置值不参与',
89
- };
78
+ // ⚠️ 两条文案纪律(1.3.4 的事故换来的):
79
+ // · **不承诺版本号** —— 1.3.2 写"待 1.3.3 接线",而 1.3.3 发布时并未接线 ⇒ 那句话本身成了假承诺;
80
+ // · **不写行号** —— 行号会腐烂(实测登记后不久就偏移),且对用户毫无意义;
81
+ // 要留证据就写进紧邻的**代码注释**(本仓注释里保留完整出处)。
82
+ export const INERT_SETTINGS = {};
90
83
 
91
- /** 「暂未生效」标记文本(只此一份,`decorateHint` 是唯一加工点)。 */
84
+ /** 「暂未生效」标记文本(只此一份,`decorateHint` 是唯一加工点)。**版本中立**。 */
92
85
  export function inertMark(path) {
93
86
  const why = INERT_SETTINGS[String(path)];
94
- return why ? `(暂未生效:待 1.3.3 接线 —— ${why})` : '';
87
+ return why ? `(暂未生效:尚未接线 —— ${why})` : '';
95
88
  }
96
89
 
97
90
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangdcm/dsh-expert-team",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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",
@@ -84,7 +84,7 @@
84
84
  "check:name": "node scripts/rename-package.mjs --check",
85
85
  "test:regression": "node regression.test.mjs",
86
86
  "test:e2e": "node e2e.test.mjs",
87
- "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",
87
+ "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",
88
88
  "test:toolcard": "node tool-card-status.test.mjs",
89
89
  "test:flow": "node flow.test.mjs",
90
90
  "test:decide": "node plan-decide.test.mjs",