@suifei/dsh-ai-team 0.3.9 → 0.3.11

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.11 — 2026-11-10
4
+
5
+ - 修复空闲默认成员不接收 Main stream:owner 无 session 时先自动激活,再记录 `wake/delivered`;
6
+ - Project 增加工作目录:默认项目首次进入提示选择,新建项目必填,成员 cwd 使用项目目录;
7
+ - 多 Project 用 name + workspacePath 区分,切换时显示各自目录;
8
+ - `team` 工具新增 `members`(返回精确 id/name/role),创建 Task 时 ownerId/assigneeId 按 id 或名字解析并校验;
9
+ - Main stream wake prompt 注入成员 id 清单,避免模型把名字拼音当 ownerId。
10
+
11
+ ## 0.3.10 — 2026-11-09
12
+
13
+ - 默认成员改为小说式虚拟名:墨渊(主理人)/ 青岚(执行)/ 白砚(验收);
14
+ - 旧版 0.3.9 及以前的默认名(沈舟/林岚/顾清)在打开 Project 时自动升级;
15
+ - 只升级精确匹配的旧默认名,用户自定义名字不会被覆盖。
16
+
3
17
  ## 0.3.9 — 2026-11-09
4
18
 
5
19
  - 图标改为极简线条:重檐 + 出檐 + 立柱 + 渊水,侧栏与市场图标统一;
package/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  `@suifei/dsh-ai-team` 是 DeepSeek Harness 的按需启用插件:装进专用 `ai-team` profile 后,DSH 里出现一个三栏工作台——左侧 Project 与成员,中间主对话 / 看板 / 态势,右侧 Thread。Agent 自己认领 Task、互相交接,由做的人以外的成员验收;Human 只处理方向、信任和不可撤回的决定。
23
23
 
24
- 新 Project 自带一组可改的默认成员:**沈舟(主理人)**、**林岚(执行)**、**顾清(验收)**。名字、职责、个性、模型都可以改;模型提供商和模型名称直接读取 DSH 的 `llm` 目录,不需要手填 id。
24
+ 新 Project 自带一组可改的默认成员:**墨渊(主理人)**、**青岚(执行)**、**白砚(验收)**。名字、职责、个性、模型都可以改;模型提供商和模型名称直接读取 DSH 的 `llm` 目录,不需要手填 id。
25
25
 
26
26
  ---
27
27
 
@@ -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
  ---
@@ -77,8 +79,8 @@ dsh --profile ai-team --no-open --port 33271
77
79
  │ # UI 项目 │ 看板:待处理 / 进行中 / 待验收 / 完成│ 验收 / 退回 │
78
80
  │ │ 态势:需要你 / 团队在做 / 等待受阻 / 最近完成 │ 任务动作 │
79
81
  │ Agents │ │ │
80
- │ 沈舟 (Lead) │ │ │
81
- │ 林岚 (QA) │ │ │
82
+ │ 墨渊 (Lead) │ │ │
83
+ │ 白砚 (QA) │ │ │
82
84
  └─────────────────┴────────────────────────────────────┴─────────────┘
83
85
  ```
84
86
 
@@ -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
- | 默认成员 | 新 Project 初始化沈舟 / 林岚 / 顾清;可改名、改职责、改个性、改模型 |
104
- | 成员 | 持久 session;名字/角色/personality/notes/skills |
105
+ | 默认成员 | 新 Project 初始化墨渊 / 青岚 / 白砚;可改名、改职责、改个性、改模型 |
106
+ | 成员 | 持久 session;名字/角色/personality/notes/skills;空闲 owner 收到 Main stream 会自动激活 |
105
107
  | 模型 | Provider / Model 下拉读取 DSH `llm` 目录;可跟随系统默认 |
106
108
  | 进化 | `team.evolve` 只改自己的 personality;验收后 reflection wake |
107
109
  | 人才库 | leave/rejoin;遗留 Task 可被接管(taken-over) |
@@ -199,19 +201,10 @@ round-19 DeepSeek + Zhipu 双 provider
199
201
 
200
202
  ---
201
203
 
202
- ## 文档
203
-
204
- - [需求研究](docs/01-需求研究报告.md)
205
- - [PRD](docs/02-产品需求文档-PRD.md)
206
- - [架构设计](docs/03-架构设计报告.md)
207
- - [UI/UX/UE 研究](docs/04-UI-UX-UE研究报告.md)
208
- - [启动与执行计划](docs/05-项目启动与执行计划.md)
209
- - [会议纪要](docs/06-会议纪要.md)
210
- - [四象限设计](docs/07-四象限视图研究与设计.md)
211
- - [异常恢复与模型治理](docs/08-异常恢复与模型治理.md)
212
- - [团队进化与人才库](docs/09-团队进化与人才库.md)
213
- - [安装、卸载与升级](docs/10-安装卸载升级.md)
214
- - [M3 并发多项目与边界补齐](docs/reports/M3-并发多项目与边界补齐.md)
204
+ ## 文档(仅本地)
205
+
206
+ 需求、架构、UI 研究、验收报告保存在本机 `docs/`,**不随 GitHub 仓库或 npm 包发布**。
207
+ 仓库只保留 README、CHANGELOG 与运行所需的源码 / 技能。
215
208
 
216
209
  ---
217
210
 
package/client.js CHANGED
@@ -186,6 +186,19 @@ window.__ModuleLoader__.load({
186
186
  newProjectTitle: '新建项目',
187
187
  projectName: '项目名',
188
188
  projectGoal: '目标',
189
+ workspace: '工作目录',
190
+ workspaceUnset: '这个项目还没有工作目录',
191
+ workspaceHint: '先选择代码目录再开工;每个 Project 使用独立目录。',
192
+ workspaceChoose: '选择目录',
193
+ workspaceChange: '更换目录',
194
+ workspaceRequired: '请先设置项目工作目录。',
195
+ workspaceSaved: '项目工作目录已保存。',
196
+ workspaceDialogTitle: '设置项目工作目录',
197
+ workspaceDialogHint: 'Project 的成员会在这个目录里执行 Task;多个 Project 用不同目录隔离。',
198
+ workspacePathPlaceholder: '例如 D:/work/my-project',
199
+ workspacePickUnavailable: '当前宿主不支持目录选择,请手动输入路径。',
200
+ workspacePickFailed: '目录选择失败,请手动输入路径。',
201
+ workspaceSave: '保存目录',
189
202
  projectCreate: '创建项目',
190
203
  projectCreated: '已创建项目 {name}。',
191
204
  projectSwitched: '已切换项目。',
@@ -460,6 +473,19 @@ window.__ModuleLoader__.load({
460
473
  newProjectTitle: 'New project',
461
474
  projectName: 'Project name',
462
475
  projectGoal: 'Goal',
476
+ workspace: 'Workspace',
477
+ workspaceUnset: 'This Project has no workspace yet',
478
+ workspaceHint: 'Choose a code directory before starting; each Project uses its own directory.',
479
+ workspaceChoose: 'Choose directory',
480
+ workspaceChange: 'Change directory',
481
+ workspaceRequired: 'Set the Project workspace first.',
482
+ workspaceSaved: 'Project workspace saved.',
483
+ workspaceDialogTitle: 'Set Project workspace',
484
+ workspaceDialogHint: 'Members run Tasks in this directory; separate Projects use separate directories.',
485
+ workspacePathPlaceholder: 'e.g. D:/work/my-project',
486
+ workspacePickUnavailable: 'This host has no directory picker; type a path.',
487
+ workspacePickFailed: 'Directory picker failed; type a path.',
488
+ workspaceSave: 'Save directory',
463
489
  projectCreate: 'Create project',
464
490
  projectCreated: 'Created project {name}.',
465
491
  projectSwitched: 'Switched project.',
@@ -910,6 +936,10 @@ window.__ModuleLoader__.load({
910
936
  .at-chead { flex: 0 0 auto; padding: 14px 18px 0; }
911
937
  .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
938
  .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; }
939
+ .at-workspacebar { display: flex; align-items: center; gap: 8px; min-width: 0; margin-top: 4px; }
940
+ .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; }
941
+ .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; }
942
+ .at-workspacebtn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-tertiary); }
913
943
  .at-tabs { display: flex; gap: 4px; margin-top: 8px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
914
944
  .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
945
  .at-tab:hover { color: var(--dsw-alias-label-primary); }
@@ -1953,6 +1983,11 @@ window.__ModuleLoader__.load({
1953
1983
  const [newOpen, setNewOpen] = React.useState(false);
1954
1984
  const [nName, setNName] = React.useState('');
1955
1985
  const [nGoal, setNGoal] = React.useState('');
1986
+ const [nWorkspace, setNWorkspace] = React.useState('');
1987
+ const [workspaceOpen, setWorkspaceOpen] = React.useState(false);
1988
+ const [workspaceDraft, setWorkspaceDraft] = React.useState('');
1989
+ const [workspaceError, setWorkspaceError] = React.useState('');
1990
+ const [workspaceBusy, setWorkspaceBusy] = React.useState(false);
1956
1991
  const [nBusy, setNBusy] = React.useState(false);
1957
1992
  const [nError, setNError] = React.useState('');
1958
1993
  const [pSuccessor, setPSuccessor] = React.useState('');
@@ -2091,7 +2126,11 @@ window.__ModuleLoader__.load({
2091
2126
 
2092
2127
  if (phase === 'loading' || !data) return h(Skeleton, { t });
2093
2128
 
2094
- const project = data.projects.find((item) => item.id === projectId) ?? data.projects[0] ?? null;
2129
+ const projectRecord = data.projects.find((item) => item.id === projectId) ?? data.projects[0] ?? null;
2130
+ const project = projectRecord === null ? null : {
2131
+ ...projectRecord,
2132
+ workspacePath: projectRecord.workspacePath || (data.project?.id === projectRecord.id ? data.project.workspacePath ?? '' : ''),
2133
+ };
2095
2134
  const tasks = data.tasks;
2096
2135
  const boardTaskCount = tasks.filter((task) => ['pending', 'in_progress', 'pending_acceptance', 'done'].includes(task.status)).length;
2097
2136
  const selectedTask = tasks.find((task) => task.id === selected) ?? tasks[0] ?? null;
@@ -2201,15 +2240,54 @@ window.__ModuleLoader__.load({
2201
2240
  function openNewProject() {
2202
2241
  setNName('');
2203
2242
  setNGoal('');
2243
+ setNWorkspace('');
2204
2244
  setNError('');
2205
2245
  setNewOpen(true);
2206
2246
  }
2247
+ async function pickWorkspacePath() {
2248
+ try {
2249
+ const picked = await ctx?.uiWorkspace?.pickDirectory?.();
2250
+ return picked ?? null;
2251
+ } catch {
2252
+ return null;
2253
+ }
2254
+ }
2255
+ async function pickNewWorkspace() {
2256
+ const picked = await pickWorkspacePath();
2257
+ if (picked) setNWorkspace(picked);
2258
+ else setNError(t('workspacePickUnavailable'));
2259
+ }
2260
+ function openWorkspaceEditor() {
2261
+ setWorkspaceDraft(project?.workspacePath ?? '');
2262
+ setWorkspaceError('');
2263
+ setWorkspaceOpen(true);
2264
+ }
2265
+ async function pickProjectWorkspace() {
2266
+ const picked = await pickWorkspacePath();
2267
+ if (picked) setWorkspaceDraft(picked);
2268
+ else setWorkspaceError(t('workspacePickUnavailable'));
2269
+ }
2270
+ async function saveWorkspace() {
2271
+ if (!project || workspaceBusy || !workspaceDraft.trim()) return;
2272
+ setWorkspaceBusy(true);
2273
+ setWorkspaceError('');
2274
+ try {
2275
+ await runCommand('project.workspace.update', { workspacePath: workspaceDraft.trim() }, project.id);
2276
+ setWorkspaceOpen(false);
2277
+ say(t('workspaceSaved'));
2278
+ await reloadData();
2279
+ } catch (error) {
2280
+ setWorkspaceError(error?.message ?? String(error));
2281
+ } finally {
2282
+ setWorkspaceBusy(false);
2283
+ }
2284
+ }
2207
2285
  async function createProject() {
2208
- if (!nName.trim() || nBusy) return;
2286
+ if (!nName.trim() || !nWorkspace.trim() || nBusy) return;
2209
2287
  setNBusy(true);
2210
2288
  setNError('');
2211
2289
  try {
2212
- const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim() });
2290
+ const result = await runCommand('project.create', { name: nName.trim(), goal: nGoal.trim(), workspacePath: nWorkspace.trim() });
2213
2291
  const next = result.project;
2214
2292
  setNewOpen(false);
2215
2293
  setScope('project');
@@ -2494,6 +2572,7 @@ window.__ModuleLoader__.load({
2494
2572
  async function send() {
2495
2573
  const text = draft.trim();
2496
2574
  if (!text) return;
2575
+ if (!project?.workspacePath) { say(t('workspaceRequired')); return; }
2497
2576
  try {
2498
2577
  if (mode === 'task') {
2499
2578
  await runCommand('task.create', { title: text, resultDefinition: text, qaRequired: qaTask });
@@ -2760,6 +2839,10 @@ window.__ModuleLoader__.load({
2760
2839
  h('header', { className: 'at-chead' },
2761
2840
  h('div', { className: 'at-title', title: headerTitle }, headerTitle),
2762
2841
  headerGoal ? h('div', { className: 'at-goal', title: headerGoal }, headerGoal) : null,
2842
+ !inInbox && project ? h('div', { className: 'at-workspacebar' },
2843
+ h('span', { className: 'at-workspacepath', title: project.workspacePath || t('workspaceHint') },
2844
+ project.workspacePath ? t('workspace') + ':' + project.workspacePath : t('workspaceUnset')),
2845
+ h('button', { className: 'at-workspacebtn', type: 'button', onClick: openWorkspaceEditor }, project.workspacePath ? t('workspaceChange') : t('workspaceChoose'))) : null,
2763
2846
  inInbox ? null : h('div', { className: 'at-tabs', role: 'tablist', 'aria-label': t('tabsAria'), onKeyDown: tabKeyDown },
2764
2847
  h('button', {
2765
2848
  className: cx('at-tab', tab === 'main' && 'at-on'),
@@ -2804,7 +2887,7 @@ window.__ModuleLoader__.load({
2804
2887
  t,
2805
2888
  mode,
2806
2889
  draft,
2807
- disabled: !project,
2890
+ disabled: !project || !project.workspacePath,
2808
2891
  qaRequired: qaTask,
2809
2892
  onQaRequired: setQaTask,
2810
2893
  onMode: setMode,
@@ -2951,6 +3034,24 @@ window.__ModuleLoader__.load({
2951
3034
  profileAgent.status === 'failed' ? h('button', { className: 'at-btn', type: 'button', onClick: retryProfile, disabled: pBusy }, t('retry')) : null,
2952
3035
  h('button', { className: 'at-btn', type: 'button', onClick: distillProfile, disabled: pBusy }, t('redistill')),
2953
3036
  h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: pBusy || !pName.trim() }, pBusy ? t('saving') : t('save'))))) : null,
3037
+ workspaceOpen ? h('div', { className: 'at-modalback' },
3038
+ h('form', {
3039
+ className: 'at-modal',
3040
+ role: 'dialog',
3041
+ 'aria-modal': 'true',
3042
+ 'aria-label': t('workspaceDialogTitle'),
3043
+ onSubmit: (event) => { event.preventDefault(); saveWorkspace(); },
3044
+ },
3045
+ h('h2', { className: 'at-modaltitle' }, t('workspaceDialogTitle')),
3046
+ h('p', { className: 'at-modalmeta' }, t('workspaceDialogHint')),
3047
+ h('label', { className: 'at-field' }, h('span', null, t('workspace')),
3048
+ h('div', { className: 'at-inlineadd' },
3049
+ h('input', { value: workspaceDraft, onChange: (event) => setWorkspaceDraft(event.target.value), placeholder: t('workspacePathPlaceholder'), autoFocus: true }),
3050
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: pickProjectWorkspace }, t('workspaceChoose')))),
3051
+ workspaceError ? h('div', { className: 'at-modalerror', role: 'alert' }, workspaceError) : null,
3052
+ h('div', { className: 'at-modalactions' },
3053
+ h('button', { className: 'at-btn', type: 'button', onClick: () => setWorkspaceOpen(false), disabled: workspaceBusy }, t('cancel')),
3054
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: workspaceBusy || !workspaceDraft.trim() }, workspaceBusy ? t('saving') : t('workspaceSave'))))) : null,
2954
3055
  newOpen ? h('div', { className: 'at-modalback' },
2955
3056
  h('form', {
2956
3057
  className: 'at-modal',
@@ -2964,10 +3065,14 @@ window.__ModuleLoader__.load({
2964
3065
  h('input', { value: nName, onChange: (event) => setNName(event.target.value), autoFocus: true })),
2965
3066
  h('label', { className: 'at-field' }, h('span', null, t('projectGoal')),
2966
3067
  h('textarea', { value: nGoal, onChange: (event) => setNGoal(event.target.value) })),
3068
+ h('label', { className: 'at-field' }, h('span', null, t('workspace')),
3069
+ h('div', { className: 'at-inlineadd' },
3070
+ h('input', { value: nWorkspace, onChange: (event) => setNWorkspace(event.target.value), placeholder: t('workspacePathPlaceholder') }),
3071
+ h('button', { className: 'at-btn at-btn-small', type: 'button', onClick: pickNewWorkspace }, t('workspaceChoose')))),
2967
3072
  nError ? h('div', { className: 'at-modalerror', role: 'alert' }, nError) : null,
2968
3073
  h('div', { className: 'at-modalactions' },
2969
3074
  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));
3075
+ h('button', { className: 'at-btn at-btn-primary', type: 'submit', disabled: nBusy || !nName.trim() || !nWorkspace.trim() }, nBusy ? t('saving') : t('projectCreate'))))) : null));
2971
3076
  }
2972
3077
 
2973
3078
  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.9';
36
+ const VERSION = '0.3.11';
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') {
@@ -10,36 +10,66 @@
10
10
  export const DEFAULT_MEMBER_ROSTER = Object.freeze([
11
11
  {
12
12
  id: 'mem_lead',
13
- name: '沈舟',
13
+ name: '墨渊',
14
14
  role: '主理人',
15
15
  personality: '先定义可验收的结果,再把方向拆成 Task;不替执行者写实现,只在关键时刻做决定。',
16
16
  },
17
17
  {
18
18
  id: 'mem_maker',
19
- name: '林岚',
19
+ name: '青岚',
20
20
  role: '执行',
21
21
  personality: '认领 Task 后直接推进,先交付可运行结果和证据再汇报;遇到阻塞先找 owner。',
22
22
  },
23
23
  {
24
24
  id: 'mem_check',
25
- name: '顾清',
25
+ name: '白砚',
26
26
  role: '验收',
27
27
  personality: '独立验收,只按结果定义和证据判断;不确定就退回,不与执行者共用 claim。',
28
28
  },
29
29
  ]);
30
30
 
31
+ /** Names shipped by 0.3.9 and earlier; only these exact pins are migrated. */
32
+ const LEGACY_DEFAULT_NAMES = Object.freeze({
33
+ mem_lead: '沈舟',
34
+ mem_maker: '林岚',
35
+ mem_check: '顾清',
36
+ });
37
+
31
38
  export const DEFAULT_LEAD_ID = 'mem_lead';
32
39
 
33
40
  function agentMembers(service) {
34
41
  return Object.values(service.state.members ?? {}).filter((member) => member.kind === 'agent');
35
42
  }
36
43
 
44
+ async function upgradeLegacyDefaultNames(service, projectId, humanId) {
45
+ const renamed = [];
46
+ for (const [memberId, legacyName] of Object.entries(LEGACY_DEFAULT_NAMES)) {
47
+ const member = service.state.members?.[memberId];
48
+ if (member === undefined || member.kind !== 'agent' || member.name !== legacyName) continue;
49
+ const next = DEFAULT_MEMBER_ROSTER.find((entry) => entry.id === memberId);
50
+ if (next === undefined) continue;
51
+ await service.dispatch({
52
+ type: 'member.profile.update',
53
+ actor: { kind: 'human', memberId: humanId },
54
+ projectId,
55
+ payload: { memberId, name: next.name, reason: 'rename legacy default roster' },
56
+ });
57
+ renamed.push(memberId);
58
+ }
59
+ return renamed;
60
+ }
61
+
37
62
  /**
38
63
  * Add the default roster to an empty Project and make the Lead its owner.
39
- * @returns {Promise<{seeded: boolean, members: string[]}>}
64
+ * On an existing roster, only exact legacy default names are upgraded; custom
65
+ * names and custom roles are never touched.
66
+ * @returns {Promise<{seeded: boolean, members: string[], renamed: string[]}>}
40
67
  */
41
68
  export async function seedDefaultRoster(service, projectId, { humanId = 'mem_human' } = {}) {
42
- if (agentMembers(service).length > 0) return { seeded: false, members: [] };
69
+ if (agentMembers(service).length > 0) {
70
+ const renamed = await upgradeLegacyDefaultNames(service, projectId, humanId);
71
+ return { seeded: false, members: [], renamed };
72
+ }
43
73
  const added = [];
44
74
  for (const member of DEFAULT_MEMBER_ROSTER) {
45
75
  await service.dispatch({
@@ -67,5 +97,5 @@ export async function seedDefaultRoster(service, projectId, { humanId = 'mem_hum
67
97
  payload: { ownerMemberId: DEFAULT_LEAD_ID, reason: 'seed default roster' },
68
98
  });
69
99
  }
70
- return { seeded: true, members: added };
100
+ return { seeded: true, members: added, renamed: [] };
71
101
  }
@@ -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.9",
3
+ "version": "0.3.11",
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",