@suifei/dsh-ai-team 0.3.13 → 0.3.15

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
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.15 — 2026-11-10
4
+
5
+ - 成员 Session 自动挂到 Project workspace,不再散落在 DSH「未分组」;
6
+ - Task Thread 增加「对话」按钮,直接打开 assignee/owner 的真实 Session;
7
+ - 成员行增加直接对话按钮;点名字进入成员任务视图;
8
+ - 团队动态只在有 active Task 时显示(或全新项目刚唤醒时),已完成不再显示“正在拆解”;
9
+ - `huashu-nuwa` 从 Team 技能目录、prompt catalog 与 notes 加载中排除。
10
+
11
+ ## 0.3.14 — 2026-11-10
12
+
13
+ - 主对话顶部增加「团队动态」:显示正在推进的 Task、owner 和运行中的成员;
14
+ - 点击团队动态里的 Task 直接打开 Thread;没有 Task 时显示“成员已唤醒,正在拆解”;
15
+ - 解决 Human 发消息后主对话没有反馈、只能去任务看板找 Task 的问题。
16
+
3
17
  ## 0.3.13 — 2026-11-10
4
18
 
5
19
  - 点击成员名字进入成员视图:该成员负责/执行的任务列表 + 直接对话 + 资料入口;
package/README.md CHANGED
@@ -84,7 +84,7 @@ dsh --profile ai-team --no-open --port 33271
84
84
  └─────────────────┴────────────────────────────────────┴─────────────┘
85
85
  ```
86
86
 
87
- - **主对话**:Human 的一句话进入 Main stream,Project owner 拆 Task;Composer 支持讨论 / 任务模式。
87
+ - **主对话**:Human 的一句话进入 Main stream,Project owner 拆 Task;顶部「团队动态」直接显示正在推进的 Task 和运行中的成员,点击 Task 可打开 Thread。
88
88
  - **看板**:四列;进行中的卡片显示 accountable owner 与执行者;数量只统计 active Task。
89
89
  - **态势**:四象限——需要你、团队在做、等待受阻、最近完成。点击条目打开对应 Task 的 Thread。
90
90
  - **收件箱**:跨 Project 聚合;失败决策、需要你决定、关注 Task 的完成报告都在这里。
@@ -215,7 +215,7 @@ round-19 DeepSeek + Zhipu 双 provider
215
215
  - 单机、单 Human;不支持多 Human 共享 Team;
216
216
  - billing/网络注入是测试 adapter,不是物理断网;
217
217
  - SSE 分桶用两个真实 provider 的独立订阅端验证,不是两个浏览器窗口;
218
- - nuwa / distilly / darwin 不内置,有则用、无则不用;
218
+ - distilly / darwin 不内置,有则用、无则不用;`huashu-nuwa` 已被 Team 技能目录排除。
219
219
  - desktop profile 由插件 patch 固定为 `danger-full-access / never`,只应在可信机器使用。
220
220
 
221
221
  ---
package/client.js CHANGED
@@ -42,6 +42,7 @@ window.__ModuleLoader__.load({
42
42
  enterHint: 'Ctrl/⌘ + Enter 发送',
43
43
  thread: '讨论',
44
44
  closeThread: '关闭讨论',
45
+ threadChat: '对话',
45
46
  follow: '关注',
46
47
  unfollow: '取消关注',
47
48
  convert: '转为 Task',
@@ -188,6 +189,11 @@ window.__ModuleLoader__.load({
188
189
  memberNoTasksBody: '在项目里创建或指派 Task 后,会出现在这里。',
189
190
  taskOwner: '负责人',
190
191
  taskAssignee: '执行',
192
+ teamPulseTitle: '团队动态',
193
+ teamPulseCount: '{n} 个 Task 正在推进',
194
+ teamPulseWorking: '成员已接单',
195
+ teamPulseStarting: '成员已唤醒,正在拆解任务…',
196
+ teamPulseAgents: '运行中:{names}',
191
197
  newProject: '新建项目',
192
198
  addAgent: '添加成员',
193
199
  newProjectMock: 'spike:新建项目属于 S-01 流程,M1 之后接入。',
@@ -337,6 +343,7 @@ window.__ModuleLoader__.load({
337
343
  enterHint: 'Ctrl/⌘ + Enter to send',
338
344
  thread: 'Discussion',
339
345
  closeThread: 'Close discussion',
346
+ threadChat: 'Chat',
340
347
  follow: 'Follow',
341
348
  unfollow: 'Unfollow',
342
349
  convert: 'Turn into task',
@@ -483,6 +490,11 @@ window.__ModuleLoader__.load({
483
490
  memberNoTasksBody: 'Create or assign a Task in the Project and it appears here.',
484
491
  taskOwner: 'Owner',
485
492
  taskAssignee: 'Assignee',
493
+ teamPulseTitle: 'Team activity',
494
+ teamPulseCount: '{n} Task(s) in motion',
495
+ teamPulseWorking: 'Members picked up the work',
496
+ teamPulseStarting: 'Members are awake and breaking the work down…',
497
+ teamPulseAgents: 'Running: {names}',
486
498
  newProject: 'New project',
487
499
  addAgent: 'Add member',
488
500
  newProjectMock: 'spike: project creation belongs to S-01 and lands after M1.',
@@ -946,8 +958,21 @@ window.__ModuleLoader__.load({
946
958
  .at-btn-small { min-height: 24px; padding: 0 8px; font-size: 11px; }
947
959
  .at-agentconfig { min-width: 0; display: flex; gap: 4px; align-items: center; color: var(--dsw-alias-label-tertiary); font-size: 10.5px; line-height: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
948
960
  .at-agentfail { flex: none; color: var(--dsw-alias-state-warn-primary); }
949
- .at-agentmore { flex: 0 0 auto; width: 28px; height: 28px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; cursor: pointer; }
950
- .at-agentmore:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
961
+ .at-agentmore, .at-agentchat { flex: 0 0 auto; width: 28px; height: 28px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; cursor: pointer; }
962
+ .at-agentmore:hover, .at-agentchat:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
963
+ .at-agentchat { display: inline-flex; align-items: center; justify-content: center; }
964
+ .at-teampulse { width: 100%; max-width: 760px; margin: 4px auto 10px; padding: 10px 12px; border: 1px solid var(--dsw-alias-state-business-primary); border-radius: var(--dsw-radius-md); background: var(--dsw-alias-bg-module-platform); }
965
+ .at-teampulsehead { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
966
+ .at-teampulsetitle { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); }
967
+ .at-teampulsecount { font-size: 11px; color: var(--dsw-alias-state-business-primary); }
968
+ .at-teampulselist { display: flex; flex-direction: column; gap: 4px; }
969
+ .at-teampulsetask { display: flex; align-items: center; gap: 7px; width: 100%; padding: 4px 6px; border: 0; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-primary); font: inherit; text-align: left; cursor: pointer; }
970
+ .at-teampulsetask:hover { background: var(--dsw-alias-interactive-bg-hover); }
971
+ .at-teampulseno { flex: 0 0 auto; color: var(--dsw-alias-label-tertiary); font-size: 11px; font-variant-numeric: tabular-nums; }
972
+ .at-teampulsetasktitle { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
973
+ .at-teampulseowner { flex: 0 0 auto; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
974
+ .at-teampulsehint { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
975
+ .at-teampulseagents { margin-top: 6px; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
951
976
  .at-memberview { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 18px; }
952
977
  .at-memberviewhead { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
953
978
  .at-memberviewid { display: flex; flex-direction: column; min-width: 0; flex: 1 1 180px; }
@@ -1512,7 +1537,7 @@ window.__ModuleLoader__.load({
1512
1537
  h('span', { className: 'at-prowtitle' }, project.name));
1513
1538
  }
1514
1539
 
1515
- function AgentRow({ t, agent, onAgent, onAgentMore }) {
1540
+ function AgentRow({ t, agent, onAgent, onAgentMore, onAgentChat }) {
1516
1541
  const statusText = t(AGENT_STATUS[agent.status] ?? 'agentIdle');
1517
1542
  const config = agent.config ?? null;
1518
1543
  const model = config?.model ? config.model : t('agentDefaultModel');
@@ -1535,6 +1560,13 @@ window.__ModuleLoader__.load({
1535
1560
  ),
1536
1561
  ),
1537
1562
  h('span', { className: 'at-sr' }, statusText),
1563
+ h('button', {
1564
+ className: 'at-agentchat',
1565
+ type: 'button',
1566
+ 'aria-label': tf(t, 'agentOpen', { name: agent.name }),
1567
+ title: t('agentChat'),
1568
+ onClick: () => onAgentChat(agent),
1569
+ }, h(Ico, { name: 'reply', size: 13 })),
1538
1570
  h('button', {
1539
1571
  className: 'at-agentmore',
1540
1572
  type: 'button',
@@ -1544,6 +1576,30 @@ window.__ModuleLoader__.load({
1544
1576
  }, '⋯'));
1545
1577
  }
1546
1578
 
1579
+ function TeamPulse({ t, agents, tasks, onOpen }) {
1580
+ const active = tasks.filter((task) => ['pending', 'in_progress', 'pending_acceptance'].includes(task.status)).slice(-5);
1581
+ const running = agents.filter((agent) => agent.status === 'running');
1582
+ const starting = active.length === 0 && tasks.length === 0 && running.length > 0;
1583
+ if (active.length === 0 && !starting) return null;
1584
+ return h('section', { className: 'at-teampulse', 'aria-label': t('teamPulseTitle') },
1585
+ h('div', { className: 'at-teampulsehead' },
1586
+ h('span', { className: 'at-teampulsetitle' }, t('teamPulseTitle')),
1587
+ h('span', { className: 'at-teampulsecount' }, active.length ? t('teamPulseCount').replace('{n}', String(active.length)) : t('teamPulseWorking'))),
1588
+ active.length
1589
+ ? h('div', { className: 'at-teampulselist' }, active.map((task) => h('button', {
1590
+ key: task.id,
1591
+ className: 'at-teampulsetask',
1592
+ type: 'button',
1593
+ onClick: () => onOpen(task),
1594
+ },
1595
+ h('span', { className: 'at-teampulseno' }, '#' + taskNo(task)),
1596
+ h('span', { className: 'at-teampulsetasktitle' }, task.title),
1597
+ h(StatusPill, { t, status: task.status }),
1598
+ h('span', { className: 'at-teampulseowner' }, task.owner ?? ''))))
1599
+ : h('div', { className: 'at-teampulsehint' }, t('teamPulseStarting')),
1600
+ running.length ? h('div', { className: 'at-teampulseagents' }, t('teamPulseAgents').replace('{names}', running.map((agent) => agent.name).join('、'))) : null);
1601
+ }
1602
+
1547
1603
  function MemberView({ t, agent, tasks, onBack, onOpenTask, onOpenChat, onOpenProfile }) {
1548
1604
  return h('div', { className: 'at-memberview' },
1549
1605
  h('div', { className: 'at-memberviewhead' },
@@ -1571,7 +1627,7 @@ window.__ModuleLoader__.load({
1571
1627
  : h(EmptyState, { t, title: t('memberNoTasks'), body: t('memberNoTasksBody'), compact: true })));
1572
1628
  }
1573
1629
 
1574
- function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAgentMore, onAddAgent, onRecruit }) {
1630
+ function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAgentMore, onAgentChat, onAddAgent, onRecruit }) {
1575
1631
  const projects = data.projects.filter((project) => !project.archived);
1576
1632
  const activeAgents = data.agents.filter((agent) => agent.status !== 'retired');
1577
1633
  const talentAgents = data.agents.filter((agent) => agent.status === 'retired');
@@ -1621,7 +1677,7 @@ window.__ModuleLoader__.load({
1621
1677
  h('button', { className: 'at-mini', type: 'button', 'aria-label': t('addAgent'), title: t('addAgent'), onClick: onAddAgent }, h(Ico, { name: 'plus', size: 12 })),
1622
1678
  ),
1623
1679
  activeAgents.length
1624
- ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent, onAgentMore })))
1680
+ ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent, onAgentMore, onAgentChat })))
1625
1681
  : h('div', { className: 'at-colempty' }, t('emptyColumn')),
1626
1682
  talentAgents.length ? h('details', { className: 'at-archived' },
1627
1683
  h('summary', null, tf(t, 'talentPoolCount', { n: talentAgents.length })),
@@ -1735,7 +1791,7 @@ window.__ModuleLoader__.load({
1735
1791
  task.submission?.summary ? h('p', { className: 'at-donetext' }, task.submission.summary) : null);
1736
1792
  }
1737
1793
 
1738
- function ThreadPanel({ t, task, thread, project, following, onToggleFollow, onClose, converting, convert, reply, onReplyChange, onReplySend, replyBase, onContinue, onContext, note, onDismissNote, acceptChecked, onAcceptChecked, qaChecks, onQaCheck, onAccept, onChanges, actions, panelRef, replyRef }) {
1794
+ function ThreadPanel({ t, task, thread, project, following, onToggleFollow, onClose, onChat, converting, convert, reply, onReplyChange, onReplySend, replyBase, onContinue, onContext, note, onDismissNote, acceptChecked, onAcceptChecked, qaChecks, onQaCheck, onAccept, onChanges, actions, panelRef, replyRef }) {
1739
1795
  const headSeq = thread?.headSeq ?? thread?.entries?.length ?? 0;
1740
1796
  const stale = replyBase !== null && replyBase !== undefined && headSeq > replyBase;
1741
1797
  const entries = Array.isArray(thread?.entries) ? thread.entries : [];
@@ -1745,6 +1801,7 @@ window.__ModuleLoader__.load({
1745
1801
  h('div', { className: 'at-rhmeta', title: t('thread') + ' · #' + taskNo(task) }, t('thread') + ' · #' + taskNo(task)),
1746
1802
  h('div', { className: 'at-rhtitle', title: task.title }, task.title)),
1747
1803
  h(FollowToggle, { t, taskNo: taskNo(task), following, onToggle: onToggleFollow }),
1804
+ onChat ? h('button', { className: 'at-rbtn', type: 'button', onClick: onChat }, t('threadChat')) : null,
1748
1805
  h('button', { className: 'at-rbtn', type: 'button', 'aria-expanded': Boolean(converting), onClick: convert.onOpen }, t('convert')),
1749
1806
  h('button', { className: 'at-rbtn at-iconbtn', type: 'button', 'aria-label': t('closeThread'), title: t('closeThread'), onClick: onClose }, h(Ico, { name: 'close', size: 14 }))),
1750
1807
  note ? h('div', { className: 'at-rnote', role: 'status' },
@@ -2491,6 +2548,15 @@ window.__ModuleLoader__.load({
2491
2548
  openProfile(agent);
2492
2549
  }
2493
2550
  }
2551
+ function openTaskAgentChat(task) {
2552
+ const memberId = task?.assigneeId ?? task?.ownerId;
2553
+ const agent = data.agents.find((candidate) => candidate.id === memberId) ?? null;
2554
+ if (agent === null) {
2555
+ say(t('agentChatUnavailable'));
2556
+ return;
2557
+ }
2558
+ void openAgentChat(agent);
2559
+ }
2494
2560
  function openProfile(agent) {
2495
2561
  setProfileAgent(agent);
2496
2562
  setPName(agent.name ?? '');
@@ -2859,6 +2925,7 @@ window.__ModuleLoader__.load({
2859
2925
  onNewProject: () => { closeMobile(); openNewProject(); },
2860
2926
  onAgent: (agent) => { closeMobile(); openMemberView(agent); },
2861
2927
  onAgentMore: (agent) => { closeMobile(); openProfile(agent); },
2928
+ onAgentChat: (agent) => { closeMobile(); void openAgentChat(agent); },
2862
2929
  onAddAgent: () => { closeMobile(); openAddAgent(); },
2863
2930
  onRecruit: (agent) => { closeMobile(); recruit(agent); },
2864
2931
  };
@@ -2921,6 +2988,7 @@ window.__ModuleLoader__.load({
2921
2988
  ? h(Situation, { t, quadrants, tasks, onItem: openQuadrantItem, onViewAll: viewQuadrant })
2922
2989
  : h('div', { className: 'at-stream', role: 'tabpanel', id: 'at-panel-main', 'aria-labelledby': 'at-tab-main' },
2923
2990
  h('div', { className: 'at-streaminner', role: 'log', 'aria-live': 'polite', 'aria-label': t('streamAria') },
2991
+ h(TeamPulse, { t, agents: data.agents, tasks, onOpen: openTask }),
2924
2992
  data.stream.length
2925
2993
  ? data.stream.map((item) => h(StreamMessage, {
2926
2994
  key: item.id,
@@ -3017,6 +3085,7 @@ window.__ModuleLoader__.load({
3017
3085
  following: followingOf(selectedTask),
3018
3086
  onToggleFollow: () => toggleFollow(selectedTask),
3019
3087
  onClose: closeThread,
3088
+ onChat: () => openTaskAgentChat(selectedTask),
3020
3089
  converting,
3021
3090
  convert: {
3022
3091
  onOpen: openConvert,
package/index.js CHANGED
@@ -20,6 +20,7 @@ import { classifyLlmFailure, retryDecision } from './lib/runtime/failure-policy.
20
20
  import { memberIdentity } from './lib/domain/member-profile.js';
21
21
  import { loadSkillContext } from './lib/runtime/skill-loader.js';
22
22
  import { formatSkillCatalog } from './lib/runtime/skill-catalog.js';
23
+ import { isSkillExcluded } from './lib/runtime/skill-policy.js';
23
24
  import { installFileSkillProvider } from './lib/runtime/skill-provider.js';
24
25
  import { apply as applySkillOpt } from './lib/vendor/skillopt/src/index.js';
25
26
  import { createMemberPermissionGuard } from './lib/host/permission-guard.js';
@@ -33,7 +34,7 @@ export const name = 'ai-team';
33
34
  /** The workbench only makes sense where the Web carrier exists. */
34
35
  export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
35
36
 
36
- const VERSION = '0.3.13';
37
+ const VERSION = '0.3.15';
37
38
  const PROJECT_ID = 'prj_main';
38
39
  const HUMAN_ID = 'mem_human';
39
40
 
@@ -126,13 +127,47 @@ export function apply(ctx) {
126
127
  }
127
128
  return null;
128
129
  };
130
+ const workspaceForProject = async (projectRuntime) => {
131
+ const registry = ctx.get('workspaceRegistry');
132
+ const path = projectRuntime?.service?.state?.projects?.[projectRuntime.id]?.workspacePath;
133
+ if (registry === undefined || typeof path !== 'string' || path === '') return null;
134
+ try {
135
+ const existing = await registry.resolveByPath?.(path);
136
+ if (existing !== undefined && existing !== null) return existing;
137
+ const created = await registry.create?.(path, projectRuntime.service.state.projects[projectRuntime.id]?.name ?? '文渊阁');
138
+ return created ?? null;
139
+ } catch (error) {
140
+ console.warn('[ai-team] workspace unavailable:', error?.message ?? error);
141
+ return null;
142
+ }
143
+ };
144
+ const attachMemberWorkspace = async (projectRuntime, sessionId) => {
145
+ if (typeof sessionId !== 'string' || sessionId === '') return;
146
+ const workspace = await workspaceForProject(projectRuntime);
147
+ if (workspace === null || workspace === undefined || typeof workspace.attachSession !== 'function') return;
148
+ try {
149
+ await workspace.attachSession(sessionId);
150
+ } catch (error) {
151
+ console.warn('[ai-team] member session workspace attach failed:', error?.message ?? error);
152
+ }
153
+ };
129
154
  const access = validatePermissionProfile({
130
155
  permissionPresets: ctx.get('permissionPresets'),
131
156
  sandboxPolicy: ctx.get('sandboxPolicy'),
132
157
  });
133
158
  const ready = access.ok
134
159
  ? boot()
135
- .then((value) => { booted = value; return value; })
160
+ .then(async (value) => {
161
+ booted = value;
162
+ for (const runtime of value.runtimes.values()) {
163
+ for (const member of Object.values(runtime.service.state.members ?? {})) {
164
+ if (member.kind === 'agent' && typeof member.sessionId === 'string') {
165
+ await attachMemberWorkspace(runtime, member.sessionId);
166
+ }
167
+ }
168
+ }
169
+ return value;
170
+ })
136
171
  .catch((error) => {
137
172
  bootError = error;
138
173
  console.error('[ai-team] ledger boot failed:', error?.message ?? error);
@@ -479,6 +514,7 @@ export function apply(ctx) {
479
514
  throw error;
480
515
  }
481
516
  activations.set(result.childId, result.activation);
517
+ await attachMemberWorkspace(projectRuntime, result.childId);
482
518
  return {
483
519
  ok: true,
484
520
  member: bound.member,
@@ -660,7 +696,7 @@ export function apply(ctx) {
660
696
  teamTool: ctx.get('tools')?.definitions?.().some((definition) => definition.name === 'team') ?? null,
661
697
  skillsService: skillsService !== null && skillsService !== undefined,
662
698
  skillNames: skillsService?.list
663
- ? await Promise.resolve(skillsService.list({})).then((rows) => rows.map((skill) => skill.name)).catch(() => [])
699
+ ? await Promise.resolve(skillsService.list({})).then((rows) => rows.map((skill) => skill.name).filter((skillName) => !isSkillExcluded(skillName))).catch(() => [])
664
700
  : [],
665
701
  skilloptTools: tools?.definitions?.().map((definition) => definition.name).filter((name) => name.startsWith('skillopt_')) ?? [],
666
702
  presets: await Promise.resolve(ctx.get('agentPresets')?.list?.()).catch(() => []),
@@ -5,6 +5,8 @@
5
5
  * demand instead of guessing.
6
6
  */
7
7
 
8
+ import { isSkillExcluded } from './skill-policy.js';
9
+
8
10
  function clean(value, limit) {
9
11
  const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
10
12
  return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
@@ -16,8 +18,10 @@ export async function formatSkillCatalog(skills, options = {}) {
16
18
  ...(options.cwd ? { cwd: options.cwd } : {}),
17
19
  }).catch(() => []);
18
20
  if (!Array.isArray(summaries) || summaries.length === 0) return '';
21
+ const available = summaries.filter((skill) => !isSkillExcluded(skill?.name));
22
+ if (available.length === 0) return '';
19
23
  const limit = Number.isSafeInteger(options.limit) && options.limit > 0 ? options.limit : 12;
20
- const rows = summaries.slice(0, limit).map((skill) => {
24
+ const rows = available.slice(0, limit).map((skill) => {
21
25
  const name = clean(skill?.name, 80);
22
26
  const description = clean(skill?.description, 240);
23
27
  const when = clean(skill?.whenToUse, 180);
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
+ import { isSkillExcluded } from './skill-policy.js';
3
4
 
4
5
  const MAX_SKILL_CHARS = 1800;
5
6
 
@@ -11,6 +12,7 @@ export async function loadSkillContext(member, dataDir) {
11
12
  for (const raw of skills.slice(0, 4)) {
12
13
  const name = basename(String(raw)).replace(/[^a-zA-Z0-9._-]/g, '');
13
14
  if (name === '' || name === '.' || name === '..' || name.startsWith('.')) continue;
15
+ if (isSkillExcluded(name)) continue;
14
16
  const candidates = [join(dataDir, 'skills', name, 'SKILL.md'), join(dataDir, 'skills', `${name}.md`)];
15
17
  for (const candidate of candidates) {
16
18
  try {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Skills that must not be exposed by the AI Team plugin.
3
+ *
4
+ * These may still exist in the user's global DSH skill directory; the Team
5
+ * provider and prompt catalog filter them so they never enter a member's Team
6
+ * context or the workbench skill list.
7
+ */
8
+ export const EXCLUDED_SKILL_NAMES = Object.freeze(new Set(['huashu-nuwa']));
9
+
10
+ export function isSkillExcluded(name) {
11
+ return typeof name === 'string' && EXCLUDED_SKILL_NAMES.has(name);
12
+ }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
+ import { isSkillExcluded } from './skill-policy.js';
10
11
 
11
12
  const MAX_BODY_BYTES = 512 * 1024;
12
13
 
@@ -61,6 +62,7 @@ function scanRoot(root, source) {
61
62
  const fields = parseFrontmatter(text);
62
63
  const name = unquote(fields.name) || entry.name;
63
64
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) continue;
65
+ if (isSkillExcluded(name)) continue;
64
66
  candidates.push({
65
67
  name,
66
68
  description: unquote(fields.description) || `Skill ${name}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "type": "module",
5
5
  "description": "文渊阁 (Wenyuan Pavilion) — AI Team mode for DeepSeek Harness: concurrent projects, owned tasks, threads, inbox and an append-only team ledger",
6
6
  "license": "MIT",