@suifei/dsh-ai-team 0.3.10 → 0.3.12

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.12 — 2026-11-10
4
+
5
+ - 点击成员名字直接打开该 Agent 的 DSH 原生 Session,可与它直接对话;
6
+ - 没有 session 的空闲成员会先激活再打开;
7
+ - 「⋯」仍打开成员资料,资料内也增加「直接对话」按钮。
8
+
9
+ ## 0.3.11 — 2026-11-10
10
+
11
+ - 修复空闲默认成员不接收 Main stream:owner 无 session 时先自动激活,再记录 `wake/delivered`;
12
+ - Project 增加工作目录:默认项目首次进入提示选择,新建项目必填,成员 cwd 使用项目目录;
13
+ - 多 Project 用 name + workspacePath 区分,切换时显示各自目录;
14
+ - `team` 工具新增 `members`(返回精确 id/name/role),创建 Task 时 ownerId/assigneeId 按 id 或名字解析并校验;
15
+ - Main stream wake prompt 注入成员 id 清单,避免模型把名字拼音当 ownerId。
16
+
3
17
  ## 0.3.10 — 2026-11-09
4
18
 
5
19
  - 默认成员改为小说式虚拟名:墨渊(主理人)/ 青岚(执行)/ 白砚(验收);
package/README.md CHANGED
@@ -64,6 +64,8 @@ dsh --profile ai-team --no-open --port 33271
64
64
 
65
65
  打开终端输出的 URL,点侧栏 **文渊阁**。
66
66
 
67
+ 第一次进入 Project 先设置 **工作目录**:默认 Project 会显示「这个项目还没有工作目录」,点「选择目录」或用输入框填路径;新建项目时也会要求填工作目录。每个 Project 独立一个目录,成员只在这个目录里执行 Task。
68
+
67
69
  > 插件只应安装在可信机器 / 可信 Workspace:成员以 `danger-full-access` 运行,且 profile patch 会把 sandbox / approval / permission 固定为 `danger-full-access / never`。普通 `desktop`/`web` profile 不安装时保持原有 preset roster。
68
70
 
69
71
  ---
@@ -94,14 +96,14 @@ dsh --profile ai-team --no-open --port 33271
94
96
 
95
97
  | 能力 | 说明 |
96
98
  |---|---|
97
- | Project | 具名 owner;并发 runtime;每 Project 独立账本、Waker、SSE |
99
+ | Project | 具名 owner;独立工作目录;并发 runtime;每 Project 独立账本、Waker、SSE |
98
100
  | Task | 唯一 accountable owner;pending / in_progress / pending_acceptance / done / cancelled |
99
101
  | Thread | append-only;回复带 `baseSeq`,旧基线会被拒并说明变化 |
100
102
  | 验收 | maker 不能验收;QA 硬门禁;`qaChecklist` 逐项通过 |
101
103
  | 收件箱 | Human/Agent 各自拉取;跨 Project 聚合与项目内 ack |
102
104
  | 关注 | 关注 Task;完成后收到 `followed_task_update` |
103
105
  | 默认成员 | 新 Project 初始化墨渊 / 青岚 / 白砚;可改名、改职责、改个性、改模型 |
104
- | 成员 | 持久 session;名字/角色/personality/notes/skills |
106
+ | 成员 | 持久 session;点击名字直接打开该 Agent 的 DSH Session 对话;「⋯」编辑资料 |
105
107
  | 模型 | Provider / Model 下拉读取 DSH `llm` 目录;可跟随系统默认 |
106
108
  | 进化 | `team.evolve` 只改自己的 personality;验收后 reflection wake |
107
109
  | 人才库 | leave/rejoin;遗留 Task 可被接管(taken-over) |
package/client.js CHANGED
@@ -178,14 +178,30 @@ window.__ModuleLoader__.load({
178
178
  projectSwitchMock: 'spike:项目切换只更新视图标题;按项目读取账本在 M1 接入。',
179
179
  archivedMock: 'spike:归档项目只读;取消归档在 M3 接入。',
180
180
  agentMock: 'spike:成员详情(打开会话 / 发消息 / 记忆与技能)在 M3 接入。',
181
- agentOpen: '查看 {name} 的详情',
181
+ agentOpen: '与 {name} 对话',
182
182
  moreActions: '更多操作',
183
+ agentChat: '直接对话',
184
+ agentChatOpening: '正在打开对话…',
185
+ agentChatUnavailable: '宿主没有可用的 Session 打开能力。',
183
186
  newProject: '新建项目',
184
187
  addAgent: '添加成员',
185
188
  newProjectMock: 'spike:新建项目属于 S-01 流程,M1 之后接入。',
186
189
  newProjectTitle: '新建项目',
187
190
  projectName: '项目名',
188
191
  projectGoal: '目标',
192
+ workspace: '工作目录',
193
+ workspaceUnset: '这个项目还没有工作目录',
194
+ workspaceHint: '先选择代码目录再开工;每个 Project 使用独立目录。',
195
+ workspaceChoose: '选择目录',
196
+ workspaceChange: '更换目录',
197
+ workspaceRequired: '请先设置项目工作目录。',
198
+ workspaceSaved: '项目工作目录已保存。',
199
+ workspaceDialogTitle: '设置项目工作目录',
200
+ workspaceDialogHint: 'Project 的成员会在这个目录里执行 Task;多个 Project 用不同目录隔离。',
201
+ workspacePathPlaceholder: '例如 D:/work/my-project',
202
+ workspacePickUnavailable: '当前宿主不支持目录选择,请手动输入路径。',
203
+ workspacePickFailed: '目录选择失败,请手动输入路径。',
204
+ workspaceSave: '保存目录',
189
205
  projectCreate: '创建项目',
190
206
  projectCreated: '已创建项目 {name}。',
191
207
  projectSwitched: '已切换项目。',
@@ -452,14 +468,30 @@ window.__ModuleLoader__.load({
452
468
  projectSwitchMock: 'spike: switching projects only updates the header; per-project ledger reads land with M1.',
453
469
  archivedMock: 'spike: archived projects are read-only; unarchive lands with M3.',
454
470
  agentMock: 'spike: member detail (open session / message / memory & skills) lands with M3.',
455
- agentOpen: 'Open details for {name}',
471
+ agentOpen: 'Chat with {name}',
456
472
  moreActions: 'More actions',
473
+ agentChat: 'Direct chat',
474
+ agentChatOpening: 'Opening conversation…',
475
+ agentChatUnavailable: 'This host cannot open a Session.',
457
476
  newProject: 'New project',
458
477
  addAgent: 'Add member',
459
478
  newProjectMock: 'spike: project creation belongs to S-01 and lands after M1.',
460
479
  newProjectTitle: 'New project',
461
480
  projectName: 'Project name',
462
481
  projectGoal: 'Goal',
482
+ workspace: 'Workspace',
483
+ workspaceUnset: 'This Project has no workspace yet',
484
+ workspaceHint: 'Choose a code directory before starting; each Project uses its own directory.',
485
+ workspaceChoose: 'Choose directory',
486
+ workspaceChange: 'Change directory',
487
+ workspaceRequired: 'Set the Project workspace first.',
488
+ workspaceSaved: 'Project workspace saved.',
489
+ workspaceDialogTitle: 'Set Project workspace',
490
+ workspaceDialogHint: 'Members run Tasks in this directory; separate Projects use separate directories.',
491
+ workspacePathPlaceholder: 'e.g. D:/work/my-project',
492
+ workspacePickUnavailable: 'This host has no directory picker; type a path.',
493
+ workspacePickFailed: 'Directory picker failed; type a path.',
494
+ workspaceSave: 'Save directory',
463
495
  projectCreate: 'Create project',
464
496
  projectCreated: 'Created project {name}.',
465
497
  projectSwitched: 'Switched project.',
@@ -910,6 +942,10 @@ window.__ModuleLoader__.load({
910
942
  .at-chead { flex: 0 0 auto; padding: 14px 18px 0; }
911
943
  .at-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; color: var(--dsw-alias-label-primary); font-size: 16px; font-weight: 600; line-height: 22px; letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
912
944
  .at-goal { min-width: 0; margin-top: 2px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
945
+ .at-workspacebar { display: flex; align-items: center; gap: 8px; min-width: 0; margin-top: 4px; }
946
+ .at-workspacepath { flex: 1 1 auto; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
947
+ .at-workspacebtn { flex: 0 0 auto; padding: 2px 7px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; }
948
+ .at-workspacebtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-tertiary); }
913
949
  .at-tabs { display: flex; gap: 4px; margin-top: 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
914
950
  .at-tab { min-height: 36px; display: inline-flex; align-items: center; gap: 6px; padding: 0 12px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 13px; font-weight: 500; cursor: pointer; }
915
951
  .at-tab:hover { color: var(--dsw-alias-label-primary); }
@@ -1452,7 +1488,7 @@ window.__ModuleLoader__.load({
1452
1488
  h('span', { className: 'at-prowtitle' }, project.name));
1453
1489
  }
1454
1490
 
1455
- function AgentRow({ t, agent, onAgent }) {
1491
+ function AgentRow({ t, agent, onAgent, onAgentMore }) {
1456
1492
  const statusText = t(AGENT_STATUS[agent.status] ?? 'agentIdle');
1457
1493
  const config = agent.config ?? null;
1458
1494
  const model = config?.model ? config.model : t('agentDefaultModel');
@@ -1480,11 +1516,11 @@ window.__ModuleLoader__.load({
1480
1516
  type: 'button',
1481
1517
  'aria-label': tf(t, 'agentOpen', { name: agent.name }),
1482
1518
  title: t('moreActions'),
1483
- onClick: () => onAgent(agent),
1519
+ onClick: () => onAgentMore(agent),
1484
1520
  }, '⋯'));
1485
1521
  }
1486
1522
 
1487
- function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAddAgent, onRecruit }) {
1523
+ function LeftPanel({ t, data, projectId, scope, tab, inboxUnread, onBack, onInbox, onBoard, onProject, onNewProject, onAgent, onAgentMore, onAddAgent, onRecruit }) {
1488
1524
  const projects = data.projects.filter((project) => !project.archived);
1489
1525
  const activeAgents = data.agents.filter((agent) => agent.status !== 'retired');
1490
1526
  const talentAgents = data.agents.filter((agent) => agent.status === 'retired');
@@ -1534,7 +1570,7 @@ window.__ModuleLoader__.load({
1534
1570
  h('button', { className: 'at-mini', type: 'button', 'aria-label': t('addAgent'), title: t('addAgent'), onClick: onAddAgent }, h(Ico, { name: 'plus', size: 12 })),
1535
1571
  ),
1536
1572
  activeAgents.length
1537
- ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent })))
1573
+ ? h('div', { role: 'list', 'aria-label': t('agentsAria') }, activeAgents.map((agent) => h(AgentRow, { key: agent.id, t, agent, onAgent, onAgentMore })))
1538
1574
  : h('div', { className: 'at-colempty' }, t('emptyColumn')),
1539
1575
  talentAgents.length ? h('details', { className: 'at-archived' },
1540
1576
  h('summary', null, tf(t, 'talentPoolCount', { n: talentAgents.length })),
@@ -1953,6 +1989,11 @@ window.__ModuleLoader__.load({
1953
1989
  const [newOpen, setNewOpen] = React.useState(false);
1954
1990
  const [nName, setNName] = React.useState('');
1955
1991
  const [nGoal, setNGoal] = React.useState('');
1992
+ const [nWorkspace, setNWorkspace] = React.useState('');
1993
+ const [workspaceOpen, setWorkspaceOpen] = React.useState(false);
1994
+ const [workspaceDraft, setWorkspaceDraft] = React.useState('');
1995
+ const [workspaceError, setWorkspaceError] = React.useState('');
1996
+ const [workspaceBusy, setWorkspaceBusy] = React.useState(false);
1956
1997
  const [nBusy, setNBusy] = React.useState(false);
1957
1998
  const [nError, setNError] = React.useState('');
1958
1999
  const [pSuccessor, setPSuccessor] = React.useState('');
@@ -2091,7 +2132,11 @@ window.__ModuleLoader__.load({
2091
2132
 
2092
2133
  if (phase === 'loading' || !data) return h(Skeleton, { t });
2093
2134
 
2094
- const project = data.projects.find((item) => item.id === projectId) ?? data.projects[0] ?? null;
2135
+ const projectRecord = data.projects.find((item) => item.id === projectId) ?? data.projects[0] ?? null;
2136
+ const project = projectRecord === null ? null : {
2137
+ ...projectRecord,
2138
+ workspacePath: projectRecord.workspacePath || (data.project?.id === projectRecord.id ? data.project.workspacePath ?? '' : ''),
2139
+ };
2095
2140
  const tasks = data.tasks;
2096
2141
  const boardTaskCount = tasks.filter((task) => ['pending', 'in_progress', 'pending_acceptance', 'done'].includes(task.status)).length;
2097
2142
  const selectedTask = tasks.find((task) => task.id === selected) ?? tasks[0] ?? null;
@@ -2201,15 +2246,54 @@ window.__ModuleLoader__.load({
2201
2246
  function openNewProject() {
2202
2247
  setNName('');
2203
2248
  setNGoal('');
2249
+ setNWorkspace('');
2204
2250
  setNError('');
2205
2251
  setNewOpen(true);
2206
2252
  }
2253
+ async function pickWorkspacePath() {
2254
+ try {
2255
+ const picked = await ctx?.uiWorkspace?.pickDirectory?.();
2256
+ return picked ?? null;
2257
+ } catch {
2258
+ return null;
2259
+ }
2260
+ }
2261
+ async function pickNewWorkspace() {
2262
+ const picked = await pickWorkspacePath();
2263
+ if (picked) setNWorkspace(picked);
2264
+ else setNError(t('workspacePickUnavailable'));
2265
+ }
2266
+ function openWorkspaceEditor() {
2267
+ setWorkspaceDraft(project?.workspacePath ?? '');
2268
+ setWorkspaceError('');
2269
+ setWorkspaceOpen(true);
2270
+ }
2271
+ async function pickProjectWorkspace() {
2272
+ const picked = await pickWorkspacePath();
2273
+ if (picked) setWorkspaceDraft(picked);
2274
+ else setWorkspaceError(t('workspacePickUnavailable'));
2275
+ }
2276
+ async function saveWorkspace() {
2277
+ if (!project || workspaceBusy || !workspaceDraft.trim()) return;
2278
+ setWorkspaceBusy(true);
2279
+ setWorkspaceError('');
2280
+ try {
2281
+ await runCommand('project.workspace.update', { workspacePath: workspaceDraft.trim() }, project.id);
2282
+ setWorkspaceOpen(false);
2283
+ say(t('workspaceSaved'));
2284
+ await reloadData();
2285
+ } catch (error) {
2286
+ setWorkspaceError(error?.message ?? String(error));
2287
+ } finally {
2288
+ setWorkspaceBusy(false);
2289
+ }
2290
+ }
2207
2291
  async function createProject() {
2208
- if (!nName.trim() || nBusy) return;
2292
+ if (!nName.trim() || !nWorkspace.trim() || nBusy) return;
2209
2293
  setNBusy(true);
2210
2294
  setNError('');
2211
2295
  try {
2212
- const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim() });
2296
+ const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim(), workspacePath: nWorkspace.trim() });
2213
2297
  const next = result.project;
2214
2298
  setNewOpen(false);
2215
2299
  setScope('project');
@@ -2311,6 +2395,30 @@ window.__ModuleLoader__.load({
2311
2395
  setMBusy(false);
2312
2396
  }
2313
2397
  }
2398
+ async function openAgentChat(agent) {
2399
+ closeMobile();
2400
+ if (ctx?.uiWorkspace?.openSession === undefined) {
2401
+ say(t('agentChatUnavailable'));
2402
+ openProfile(agent);
2403
+ return;
2404
+ }
2405
+ if (agent.sessionId) {
2406
+ ctx.uiWorkspace.openSession(agent.sessionId);
2407
+ return;
2408
+ }
2409
+ try {
2410
+ say(t('agentChatOpening'));
2411
+ const activated = await runCommand('member.activate', {
2412
+ memberId: agent.id,
2413
+ prompt: '你正在与 Human 直接对话。先简要说明你是谁、当前负责什么;不要创建 Task,除非 Human 明确要求。',
2414
+ });
2415
+ await reloadData();
2416
+ if (activated?.sessionId) ctx.uiWorkspace.openSession(activated.sessionId);
2417
+ } catch (error) {
2418
+ say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
2419
+ openProfile(agent);
2420
+ }
2421
+ }
2314
2422
  function openProfile(agent) {
2315
2423
  setProfileAgent(agent);
2316
2424
  setPName(agent.name ?? '');
@@ -2494,6 +2602,7 @@ window.__ModuleLoader__.load({
2494
2602
  async function send() {
2495
2603
  const text = draft.trim();
2496
2604
  if (!text) return;
2605
+ if (!project?.workspacePath) { say(t('workspaceRequired')); return; }
2497
2606
  try {
2498
2607
  if (mode === 'task') {
2499
2608
  await runCommand('task.create', { title: text, resultDefinition: text, qaRequired: qaTask });
@@ -2674,7 +2783,8 @@ window.__ModuleLoader__.load({
2674
2783
  onBoard: switchBoard,
2675
2784
  onProject: switchProject,
2676
2785
  onNewProject: () => { closeMobile(); openNewProject(); },
2677
- onAgent: (agent) => { closeMobile(); openProfile(agent); },
2786
+ onAgent: (agent) => { closeMobile(); void openAgentChat(agent); },
2787
+ onAgentMore: (agent) => { closeMobile(); openProfile(agent); },
2678
2788
  onAddAgent: () => { closeMobile(); openAddAgent(); },
2679
2789
  onRecruit: (agent) => { closeMobile(); recruit(agent); },
2680
2790
  };
@@ -2760,6 +2870,10 @@ window.__ModuleLoader__.load({
2760
2870
  h('header', { className: 'at-chead' },
2761
2871
  h('div', { className: 'at-title', title: headerTitle }, headerTitle),
2762
2872
  headerGoal ? h('div', { className: 'at-goal', title: headerGoal }, headerGoal) : null,
2873
+ !inInbox && project ? h('div', { className: 'at-workspacebar' },
2874
+ h('span', { className: 'at-workspacepath', title: project.workspacePath || t('workspaceHint') },
2875
+ project.workspacePath ? t('workspace') + ':' + project.workspacePath : t('workspaceUnset')),
2876
+ h('button', { className: 'at-workspacebtn', type: 'button', onClick: openWorkspaceEditor }, project.workspacePath ? t('workspaceChange') : t('workspaceChoose'))) : null,
2763
2877
  inInbox ? null : h('div', { className: 'at-tabs', role: 'tablist', 'aria-label': t('tabsAria'), onKeyDown: tabKeyDown },
2764
2878
  h('button', {
2765
2879
  className: cx('at-tab', tab === 'main' && 'at-on'),
@@ -2804,7 +2918,7 @@ window.__ModuleLoader__.load({
2804
2918
  t,
2805
2919
  mode,
2806
2920
  draft,
2807
- disabled: !project,
2921
+ disabled: !project || !project.workspacePath,
2808
2922
  qaRequired: qaTask,
2809
2923
  onQaRequired: setQaTask,
2810
2924
  onMode: setMode,
@@ -2947,10 +3061,29 @@ window.__ModuleLoader__.load({
2947
3061
  pError ? h('div', { className: 'at-modalerror', role: 'alert' }, pError) : null,
2948
3062
  h('div', { className: 'at-modalactions' },
2949
3063
  h('button', { className: 'at-btn', type: 'button', onClick: () => setProfileAgent(null), disabled: pBusy }, t('cancel')),
3064
+ h('button', { className: 'at-btn', type: 'button', onClick: () => { const target = profileAgent; setProfileAgent(null); void openAgentChat(target); } }, t('agentChat')),
2950
3065
  h('button', { className: 'at-btn', type: 'button', onClick: leaveProfile, disabled: pBusy }, t('leaveTeam')),
2951
3066
  profileAgent.status === 'failed' ? h('button', { className: 'at-btn', type: 'button', onClick: retryProfile, disabled: pBusy }, t('retry')) : null,
2952
3067
  h('button', { className: 'at-btn', type: 'button', onClick: distillProfile, disabled: pBusy }, t('redistill')),
2953
3068
  h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: pBusy || !pName.trim() }, pBusy ? t('saving') : t('save'))))) : null,
3069
+ workspaceOpen ? h('div', { className: 'at-modalback' },
3070
+ h('form', {
3071
+ className: 'at-modal',
3072
+ role: 'dialog',
3073
+ 'aria-modal': 'true',
3074
+ 'aria-label': t('workspaceDialogTitle'),
3075
+ onSubmit: (event) => { event.preventDefault(); saveWorkspace(); },
3076
+ },
3077
+ h('h2', { className: 'at-modaltitle' }, t('workspaceDialogTitle')),
3078
+ h('p', { className: 'at-modalmeta' }, t('workspaceDialogHint')),
3079
+ h('label', { className: 'at-field' }, h('span', null, t('workspace')),
3080
+ h('div', { className: 'at-inlineadd' },
3081
+ h('input', { value: workspaceDraft, onChange: (event) => setWorkspaceDraft(event.target.value), placeholder: t('workspacePathPlaceholder'), autoFocus: true }),
3082
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: pickProjectWorkspace }, t('workspaceChoose')))),
3083
+ workspaceError ? h('div', { className: 'at-modalerror', role: 'alert' }, workspaceError) : null,
3084
+ h('div', { className: 'at-modalactions' },
3085
+ h('button', { className: 'at-btn', type: 'button', onClick: () => setWorkspaceOpen(false), disabled: workspaceBusy }, t('cancel')),
3086
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: workspaceBusy || !workspaceDraft.trim() }, workspaceBusy ? t('saving') : t('workspaceSave'))))) : null,
2954
3087
  newOpen ? h('div', { className: 'at-modalback' },
2955
3088
  h('form', {
2956
3089
  className: 'at-modal',
@@ -2964,10 +3097,14 @@ window.__ModuleLoader__.load({
2964
3097
  h('input', { value: nName, onChange: (event) => setNName(event.target.value), autoFocus: true })),
2965
3098
  h('label', { className: 'at-field' }, h('span', null, t('projectGoal')),
2966
3099
  h('textarea', { value: nGoal, onChange: (event) => setNGoal(event.target.value) })),
3100
+ h('label', { className: 'at-field' }, h('span', null, t('workspace')),
3101
+ h('div', { className: 'at-inlineadd' },
3102
+ h('input', { value: nWorkspace, onChange: (event) => setNWorkspace(event.target.value), placeholder: t('workspacePathPlaceholder') }),
3103
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: pickNewWorkspace }, t('workspaceChoose')))),
2967
3104
  nError ? h('div', { className: 'at-modalerror', role: 'alert' }, nError) : null,
2968
3105
  h('div', { className: 'at-modalactions' },
2969
3106
  h('button', { className: 'at-btn', type: 'button', onClick: () => setNewOpen(false), disabled: nBusy }, t('cancel')),
2970
- h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: nBusy || !nName.trim() }, nBusy ? t('saving') : t('projectCreate'))))) : null));
3107
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: nBusy || !nName.trim() || !nWorkspace.trim() }, nBusy ? t('saving') : t('projectCreate'))))) : null));
2971
3108
  }
2972
3109
 
2973
3110
  function PavilionMark({ size = 18 }) {
package/index.js CHANGED
@@ -33,7 +33,7 @@ export const name = 'ai-team';
33
33
  /** The workbench only makes sense where the Web carrier exists. */
34
34
  export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
35
35
 
36
- const VERSION = '0.3.10';
36
+ const VERSION = '0.3.12';
37
37
  const PROJECT_ID = 'prj_main';
38
38
  const HUMAN_ID = 'mem_human';
39
39
 
@@ -91,6 +91,7 @@ async function boot() {
91
91
  projectId: project.id,
92
92
  projectName: project.name,
93
93
  projectGoal: project.goal,
94
+ workspacePath: project.workspacePath ?? '',
94
95
  });
95
96
  runtimes.set(project.id, runtime);
96
97
  }
@@ -208,6 +209,11 @@ export function apply(ctx) {
208
209
  let host = null;
209
210
  let hostPromise = null;
210
211
  const wakeKeys = new Set();
212
+ const activationTasks = new Map();
213
+ const projectCwd = (projectRuntime, member) => member?.workspace
214
+ || projectRuntime?.service?.state?.projects?.[projectRuntime.id]?.workspacePath
215
+ || projectRuntime?.dir
216
+ || undefined;
211
217
  const wakeFromResult = async (projectRuntime, result, actor = {}) => {
212
218
  if (projectRuntime === null || projectRuntime === undefined) return;
213
219
  const plans = planWakes(projectRuntime.service.state, result, actor);
@@ -220,31 +226,54 @@ export function apply(ctx) {
220
226
  const providers = typeof subagents.list === 'function' ? subagents.list() : [];
221
227
  if (providers.length === 0) return;
222
228
  const parentInfo = await ensureParent();
229
+ const wakePrompt = async (plan, member) => describeWake(plan, projectRuntime.service.state)
230
+ + await formatSkillCatalog(skillsService, { cwd: projectCwd(projectRuntime, member) })
231
+ + await loadSkillContext(member, projectRuntime.dir);
223
232
  for (const plan of plans) {
224
233
  if (wakeKeys.has(plan.key)) continue;
225
- const member = projectRuntime.service.state.members?.[plan.memberId];
226
- if (member?.sessionId === undefined || member.sessionId === null) continue;
234
+ let member = projectRuntime.service.state.members?.[plan.memberId];
235
+ if (member === undefined) continue;
227
236
  // Reserve before the async delivery: the HTTP result and the ledger
228
237
  // event both schedule the same plan, and only one may deliver it.
229
238
  wakeKeys.add(plan.key);
230
- const runtime = new MemberRuntime({
231
- subagents, sessions, permissionPresets, sandboxPolicy,
232
- agents: ctx.get('agents'),
233
- agentPresets: ctx.get('agentPresets'),
234
- direct: true,
235
- parent: parentInfo.agent,
236
- provider: providers[0],
237
- preset: 'standard',
238
- cwd: member.workspace ?? projectRuntime.dataDir,
239
- });
239
+ const cwd = projectCwd(projectRuntime, member);
240
240
  try {
241
- const delivered = await runtime.wake(member, {
242
- prompt: describeWake(plan, projectRuntime.service.state)
243
- + await formatSkillCatalog(skillsService, { cwd: member.workspace ?? undefined })
244
- + await loadSkillContext(member, projectRuntime.dataDir),
245
- });
241
+ // Idle default members have no Agent session yet. A wake must
242
+ // activate them first; otherwise the first Human message is
243
+ // silently dropped in a fresh Project.
244
+ let messageId = null;
245
+ if (member.sessionId === undefined || member.sessionId === null) {
246
+ const activationKey = `${projectRuntime.id}:${member.id}`;
247
+ let activation = activationTasks.get(activationKey);
248
+ if (activation === undefined) {
249
+ activation = activateMember({
250
+ payload: { memberId: member.id, prompt: await wakePrompt(plan, member) },
251
+ }, projectRuntime);
252
+ activationTasks.set(activationKey, activation);
253
+ activation.finally(() => activationTasks.delete(activationKey)).catch(() => {});
254
+ }
255
+ const activated = await activation;
256
+ if (activated.error !== undefined) {
257
+ throw Object.assign(new Error(activated.error.message), { code: activated.error.code });
258
+ }
259
+ messageId = activated.messageId ?? null;
260
+ member = projectRuntime.service.state.members?.[member.id] ?? member;
261
+ } else {
262
+ const runtime = new MemberRuntime({
263
+ subagents, sessions, permissionPresets, sandboxPolicy,
264
+ agents: ctx.get('agents'),
265
+ agentPresets: ctx.get('agentPresets'),
266
+ direct: true,
267
+ parent: parentInfo.agent,
268
+ provider: providers[0],
269
+ preset: 'standard',
270
+ cwd,
271
+ });
272
+ const delivered = await runtime.wake(member, { prompt: await wakePrompt(plan, member) });
273
+ messageId = delivered.messageId ?? null;
274
+ }
246
275
  await projectRuntime.service.recordWake(plan.memberId, {
247
- ok: true, taskId: plan.taskId, reason: plan.reason, messageId: delivered.messageId,
276
+ ok: true, taskId: plan.taskId, reason: plan.reason, messageId,
248
277
  });
249
278
  } catch (error) {
250
279
  wakeKeys.delete(plan.key);
@@ -432,12 +461,12 @@ export function apply(ctx) {
432
461
  parent: parentInfo.agent,
433
462
  provider: providers[0],
434
463
  preset: 'standard',
435
- cwd: member.workspace ?? projectRuntime.dataDir,
464
+ cwd: projectCwd(projectRuntime, member),
436
465
  });
437
466
  const result = await runtime.activate(activationMember, {
438
467
  prompt: `${memberIdentity(activationMember)}\n\n${typeof body.payload?.prompt === 'string'
439
468
  ? body.payload.prompt
440
- : '你被加入 AI Team。先读取项目简报与收件箱,再处理分配给你的任务。'}${await formatSkillCatalog(skillsService, { cwd: activationMember.workspace ?? undefined })}${await loadSkillContext(activationMember, projectRuntime.dataDir)}`,
469
+ : '你被加入 AI Team。先读取项目简报与收件箱,再处理分配给你的任务。'}${await formatSkillCatalog(skillsService, { cwd: projectCwd(projectRuntime, activationMember) })}${await loadSkillContext(activationMember, projectRuntime.dir)}`,
441
470
  });
442
471
  let bound;
443
472
  try {
@@ -536,7 +565,7 @@ export function apply(ctx) {
536
565
  }
537
566
  };
538
567
 
539
- async function activateProject(nextProjectId, projectName, projectGoal) {
568
+ async function activateProject(nextProjectId, projectName, projectGoal, workspacePath = '') {
540
569
  await ready;
541
570
  if (booted === null) throw new Error('team ledger is unavailable');
542
571
  let next = projectRuntime(nextProjectId);
@@ -546,6 +575,7 @@ export function apply(ctx) {
546
575
  projectId: nextProjectId,
547
576
  projectName,
548
577
  projectGoal,
578
+ workspacePath,
549
579
  });
550
580
  booted.runtimes.set(nextProjectId, next);
551
581
  attachRuntime(next);
@@ -689,6 +719,7 @@ export function apply(ctx) {
689
719
  ...project,
690
720
  owner: active?.owner ?? null,
691
721
  goal: active?.goal ?? '',
722
+ workspacePath: active?.workspacePath ?? project.workspacePath ?? '',
692
723
  archived: false,
693
724
  active: project.id === booted.activeProjectId,
694
725
  };
@@ -828,10 +859,12 @@ export function apply(ctx) {
828
859
  if (body.type === 'project.create') {
829
860
  const name = typeof body.payload?.name === 'string' ? body.payload.name.trim() : '';
830
861
  const goal = typeof body.payload?.goal === 'string' ? body.payload.goal.trim() : '';
862
+ const workspacePath = typeof body.payload?.workspacePath === 'string' ? body.payload.workspacePath.trim() : '';
831
863
  if (name === '') return send(res, { error: { code: 'E_INVALID_ARGUMENT', message: 'project name is required' } }, 400);
864
+ if (workspacePath === '') return send(res, { error: { code: 'E_INVALID_ARGUMENT', message: 'project workspacePath is required' } }, 400);
832
865
  const id = ids.project();
833
- booted.registry.add({ id, name, goal });
834
- const next = await activateProject(id, name, goal);
866
+ booted.registry.add({ id, name, goal, workspacePath });
867
+ const next = await activateProject(id, name, goal, workspacePath);
835
868
  return send(res, { ok: true, project: next.service.state.projects[id] ?? { id, name } });
836
869
  }
837
870
  if (body.type === 'project.activate') {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { taskCard } from './views.js';
9
9
 
10
- const ACTIONS = ['tasks', 'inbox', 'recall', 'create', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
10
+ const ACTIONS = ['tasks', 'members', 'inbox', 'recall', 'create', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
11
11
 
12
12
  function failure(error) {
13
13
  return {
@@ -29,6 +29,20 @@ function projectId(state, requested) {
29
29
  return Object.keys(state.projects ?? {})[0] ?? null;
30
30
  }
31
31
 
32
+ function resolveMember(state, value) {
33
+ if (typeof value !== 'string' || value === '') return null;
34
+ const exact = state.members?.[value];
35
+ if (exact?.kind === 'agent') return exact;
36
+ return Object.values(state.members ?? {}).find((member) => member.kind === 'agent' && member.name === value) ?? null;
37
+ }
38
+
39
+ function rosterText(state) {
40
+ return Object.values(state.members ?? {})
41
+ .filter((member) => member.kind === 'agent' && member.status !== 'retired')
42
+ .map((member) => `${member.name}=${member.id}`)
43
+ .join('、');
44
+ }
45
+
32
46
  /**
33
47
  * @param {{
34
48
  * getState: () => any,
@@ -39,7 +53,7 @@ function projectId(state, requested) {
39
53
  export function createTeamTool(options) {
40
54
  return {
41
55
  name: 'team',
42
- description: 'Read and act on the AI Team board. The Project owner can create tasks with action=create and must set resultDefinition and ownerId. Use recall to search your notes/skills and your past Tasks/Threads. Use member to inspect a colleague, evolve for only your own personality, distill (Project owner/Human) or note/skill for durable memory. Use tasks for ids/revisions; claim before working; read a Thread before replying; submit evidence before acceptance.',
56
+ description: 'Read and act on the AI Team board. The Project owner can create tasks with action=create and must set resultDefinition and ownerId. Use members for the exact roster id/name/role; use tasks for ids/revisions. Use recall to search your notes/skills and your past Tasks/Threads. Use member to inspect a colleague, evolve for only your own personality, distill (Project owner/Human) or note/skill for durable memory. Claim before working; read a Thread before replying; submit evidence before acceptance.',
43
57
  parameters: {
44
58
  type: 'object',
45
59
  properties: {
@@ -54,8 +68,8 @@ export function createTeamTool(options) {
54
68
  body: { type: 'string', description: 'Thread reply body.' },
55
69
  title: { type: 'string', description: 'Task title for create.' },
56
70
  resultDefinition: { type: 'string', description: 'Acceptance definition for create.' },
57
- ownerId: { type: 'string', description: 'Accountable owner for create; defaults to you.' },
58
- assigneeId: { type: 'string', description: 'Optional assignee for create.' },
71
+ ownerId: { type: 'string', description: 'Accountable owner id from members for create; defaults to you.' },
72
+ assigneeId: { type: 'string', description: 'Optional assignee id from members for create.' },
59
73
  qaRequired: { type: 'boolean', description: 'Require a QA member to accept this task.' },
60
74
  qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance.' },
61
75
  personality: { type: 'string', description: 'New working personality for evolve/distill.' },
@@ -89,6 +103,18 @@ export function createTeamTool(options) {
89
103
  .map((task) => taskCard(state, task));
90
104
  return { ok: true, projectId: pid, tasks };
91
105
  }
106
+ if (args.action === 'members') {
107
+ const members = Object.values(state.members ?? {})
108
+ .filter((candidate) => candidate.kind === 'agent' && candidate.status !== 'retired')
109
+ .map((candidate) => ({
110
+ id: candidate.id,
111
+ name: candidate.name,
112
+ role: candidate.role,
113
+ status: candidate.status,
114
+ qa: /验收|qa|quality|test|verification/i.test(candidate.role ?? ''),
115
+ }));
116
+ return { ok: true, projectId: pid, members };
117
+ }
92
118
  if (args.action === 'recall') {
93
119
  const query = typeof args.query === 'string' ? args.query.trim().toLowerCase() : '';
94
120
  if (query === '') return failure({ code: 'E_INVALID_ARGUMENT', message: 'query is required' });
@@ -172,11 +198,19 @@ export function createTeamTool(options) {
172
198
  if (typeof args.resultDefinition !== 'string' || args.resultDefinition.trim() === '') {
173
199
  return failure({ code: 'E_INVALID_ARGUMENT', message: 'resultDefinition is required' });
174
200
  }
201
+ const owner = args.ownerId === undefined ? member : resolveMember(state, args.ownerId);
202
+ if (owner === null) {
203
+ return failure({ code: 'E_INVALID_ARGUMENT', message: `ownerId ${String(args.ownerId)} is not a Team member; use members to get ids (${rosterText(state)})` });
204
+ }
205
+ const assignee = args.assigneeId === undefined ? null : resolveMember(state, args.assigneeId);
206
+ if (args.assigneeId !== undefined && assignee === null) {
207
+ return failure({ code: 'E_INVALID_ARGUMENT', message: `assigneeId ${String(args.assigneeId)} is not a Team member; use members to get ids (${rosterText(state)})` });
208
+ }
175
209
  const result = await command('task.create', {
176
210
  title: args.title,
177
211
  resultDefinition: args.resultDefinition,
178
- ownerId: typeof args.ownerId === 'string' && args.ownerId !== '' ? args.ownerId : member.id,
179
- ...(typeof args.assigneeId === 'string' && args.assigneeId !== '' ? { assigneeId: args.assigneeId } : {}),
212
+ ownerId: owner.id,
213
+ ...(assignee === null ? {} : { assigneeId: assignee.id }),
180
214
  ...(args.qaRequired === true ? { qaRequired: true } : {}),
181
215
  ...(Array.isArray(args.qaChecklist) ? { qaChecklist: args.qaChecklist } : {}),
182
216
  });
@@ -41,10 +41,10 @@ export class ProjectRegistry {
41
41
  return this.projects.map((project) => ({ ...project, active: project.id === this.activeProjectId }));
42
42
  }
43
43
 
44
- add({ id, name, createdAt, goal }) {
44
+ add({ id, name, createdAt, goal, workspacePath }) {
45
45
  if (typeof id !== 'string' || id === '') throw new Error('project id is required');
46
46
  if (!this.projects.some((project) => project.id === id)) {
47
- this.projects.push({ id, name: name ?? id, goal: goal ?? '', createdAt: createdAt ?? Date.now() });
47
+ this.projects.push({ id, name: name ?? id, goal: goal ?? '', workspacePath: workspacePath ?? '', createdAt: createdAt ?? Date.now() });
48
48
  this.save();
49
49
  }
50
50
  return this.projects.find((project) => project.id === id);
@@ -25,7 +25,7 @@ export class ProjectRuntime {
25
25
  this.service = service;
26
26
  }
27
27
 
28
- static async open({ rootDir, projectId = DEFAULT_PROJECT_ID, now = undefined, seed = true, projectName = '我的项目', projectGoal = '把事交给团队' }) {
28
+ static async open({ rootDir, projectId = DEFAULT_PROJECT_ID, now = undefined, seed = true, projectName = '我的项目', projectGoal = '把事交给团队', workspacePath = '' }) {
29
29
  const dir = join(rootDir, 'projects', projectId);
30
30
  mkdirSync(dir, { recursive: true });
31
31
  const ledger = openLedger({ dir, projectId, fsync: true });
@@ -57,7 +57,7 @@ export class ProjectRuntime {
57
57
  type: 'project.create',
58
58
  actor: { kind: 'human', memberId: HUMAN_ID },
59
59
  projectId,
60
- payload: { id: projectId, name: projectName, goal: projectGoal },
60
+ payload: { id: projectId, name: projectName, goal: projectGoal, workspacePath },
61
61
  });
62
62
  }
63
63
  await seedDefaultRoster(service, projectId, { humanId: HUMAN_ID });
@@ -67,7 +67,7 @@ export class TeamService {
67
67
  bootstrap(projectId, threadTaskId) {
68
68
  const state = this.state;
69
69
  const projects = Object.values(state.projects ?? {}).map((p) => ({
70
- id: p.id, name: p.name, goal: p.goal ?? '', owner: p.ownerName ?? p.ownerMemberId ?? p.ownerId, active: p.status !== 'archived',
70
+ id: p.id, name: p.name, goal: p.goal ?? '', owner: p.ownerName ?? p.ownerMemberId ?? p.ownerId, workspacePath: p.workspacePath ?? '', active: p.status !== 'archived',
71
71
  }));
72
72
  const members = Object.values(state.members ?? {}).map((m) => ({
73
73
  id: m.id,
@@ -143,6 +143,7 @@ export class TeamService {
143
143
  switch (command.type) {
144
144
  case 'project.create': return await this.createProject(command, payload);
145
145
  case 'project.owner.update': return await this.changeProjectOwner(command, payload);
146
+ case 'project.workspace.update': return await this.changeProjectWorkspace(command, payload);
146
147
  case 'stream.post': return await this.postStream(command, payload);
147
148
  case 'member.add': return await this.addMember(command, payload);
148
149
  case 'member.bind': return await this.bindMemberSession(command, payload);
@@ -213,6 +214,23 @@ export class TeamService {
213
214
  return { project };
214
215
  }
215
216
 
217
+ async changeProjectWorkspace(command, payload) {
218
+ if (command.actor.kind !== 'human') fail(ERRORS.UNAUTHORIZED, 'only Human may change the Project workspace');
219
+ const projectId = command.projectId;
220
+ const project = this.state.projects?.[projectId];
221
+ if (project === undefined) fail(ERRORS.NOT_FOUND, `project ${String(projectId)} not found`);
222
+ if (typeof payload.workspacePath !== 'string' || payload.workspacePath.trim() === '') {
223
+ fail(ERRORS.INVALID_ARGUMENT, 'workspacePath is required');
224
+ }
225
+ const workspacePath = payload.workspacePath.trim();
226
+ await this.append(command, 'project/workspace-changed', {
227
+ projectId,
228
+ workspacePath,
229
+ reason: payload.reason ?? '',
230
+ });
231
+ return { project: { ...project, workspacePath } };
232
+ }
233
+
216
234
  async changeProjectOwner(command, payload) {
217
235
  if (command.actor.kind !== 'human') fail(ERRORS.UNAUTHORIZED, 'only Human may change the Project owner');
218
236
  const projectId = command.projectId;
package/lib/host/waker.js CHANGED
@@ -156,12 +156,16 @@ export function planWakes(state, result, actor = {}) {
156
156
  if (event?.type === 'stream/posted' && event.payload?.kind === 'human_message') {
157
157
  const project = Object.values(state.projects ?? {})[0];
158
158
  const body = typeof event.payload?.body === 'string' ? event.payload.body : '';
159
+ const roster = Object.values(state.members ?? {})
160
+ .filter((member) => member.kind === 'agent' && member.status !== 'retired')
161
+ .map((member) => `${member.name}=${member.id}(${member.role || '成员'})`)
162
+ .join('、');
159
163
  add(
160
164
  project?.ownerMemberId,
161
165
  null,
162
166
  'human_message',
163
167
  `human_message:${event.seq ?? event.payload?.seq}`,
164
- `你是 Project owner。Human 在 Main stream 说:“${body}”。调用 team 工具查看 tasks;需要开工就用 create 创建 Task,写明可验证的 resultDefinition,并把 owner 指给合适的成员。resultDefinition 只写验收标准,不要写“等待 Human 验收”;验收可由任何未认领该卡的 Agent 执行。不要只回一段话代替卡。`,
168
+ `你是 Project owner。Human 在 Main stream 说:“${body}”。本轮必须先用 team action=members 查看成员,再用 action=create 创建至少一个 Task;ownerId 必须使用成员 id,写明可验证的 resultDefinition。可用成员:${roster || '无'}。如果需求很大或外部链接只是参考,先创建“调研/澄清/拆解”Task,不要直接开始实现;如果确实无法创建,必须明确说明原因。resultDefinition 只写验收标准,不要写“等待 Human 验收”;不要只读文件或只回一段话结束。`,
165
169
  );
166
170
  }
167
171
  if (event?.type === 'inbox/item-created') {
@@ -637,6 +637,19 @@ export function applyEvent(state, event) {
637
637
  return advance(state, event, { projects: { ...state.projects, [project.id]: project } });
638
638
  }
639
639
 
640
+ case 'project/workspace-changed': {
641
+ const projectId = textOr(payload.projectId, event.projectId);
642
+ const workspacePath = textOr(payload.workspacePath, null);
643
+ const project = state.projects?.[projectId];
644
+ if (project === undefined || workspacePath === null) return advance(state, event);
645
+ return advance(state, event, {
646
+ projects: {
647
+ ...state.projects,
648
+ [projectId]: { ...project, workspacePath },
649
+ },
650
+ });
651
+ }
652
+
640
653
  case 'project/owner-changed': {
641
654
  const projectId = textOr(payload.projectId, event.projectId);
642
655
  const ownerMemberId = textOr(payload.ownerMemberId, null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
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",