@suifei/dsh-ai-team 0.3.10 → 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 +8 -0
- package/README.md +4 -2
- package/client.js +110 -5
- package/index.js +57 -24
- package/lib/host/member-tools.js +40 -6
- package/lib/host/project-registry.js +2 -2
- package/lib/host/project-runtime.js +2 -2
- package/lib/host/service.js +19 -1
- package/lib/host/waker.js +5 -1
- package/lib/ledger/fold.js +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.3.10 — 2026-11-09
|
|
4
12
|
|
|
5
13
|
- 默认成员改为小说式虚拟名:墨渊(主理人)/ 青岚(执行)/ 白砚(验收);
|
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
|
|
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;名字/角色/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) |
|
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
|
|
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.
|
|
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
|
-
|
|
226
|
-
if (member
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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:
|
|
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
|
|
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') {
|
package/lib/host/member-tools.js
CHANGED
|
@@ -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.
|
|
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:
|
|
179
|
-
...(
|
|
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 });
|
package/lib/host/service.js
CHANGED
|
@@ -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}
|
|
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') {
|
package/lib/ledger/fold.js
CHANGED
|
@@ -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.
|
|
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",
|