@suifei/dsh-ai-team 0.3.25 → 0.3.27
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 +15 -0
- package/client.js +78 -8
- package/index.js +1 -1
- package/lib/domain/policy.js +6 -0
- package/lib/domain/task.js +7 -0
- package/lib/host/member-tools.js +3 -1
- package/lib/host/service.js +22 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.27 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
- 主对话「任务」模式拆成标题 + 验收标准;不再把 Human 原话直接当 resultDefinition;
|
|
6
|
+
- 开启 QA 时必须逐行填写 checklist,否则不能提交;
|
|
7
|
+
- 修复真实项目 #3/#4 的根因:重复创建、宽泛结果定义、QA 空清单;
|
|
8
|
+
- 重复 Task 的第二条会打开已有卡,不再静默新建。
|
|
9
|
+
|
|
10
|
+
## 0.3.26 — 2026-11-10
|
|
11
|
+
|
|
12
|
+
- Task 创建增加重复保护:相同 title/resultDefinition/owner 的 active Task 会被拒绝并返回已有 Task;
|
|
13
|
+
- 主对话「任务」模式收到重复错误时自动打开已有 Task,不再产生 #3/#4 这类重复卡;
|
|
14
|
+
- `qaRequired=true` 但没有 qaChecklist 时拒绝创建,避免产生无法验收的 #4;
|
|
15
|
+
- 成员有 `pending_acceptance` Task 时不能再 claim 下一张,阻止 T2/T3 在 T1 验收前提前开工;
|
|
16
|
+
- `team() accept` 现在透传 checks,QA Agent 不再需要走 Human API。
|
|
17
|
+
|
|
3
18
|
## 0.3.25 — 2026-11-10
|
|
4
19
|
|
|
5
20
|
- 收件箱未读数按 `state=unread` 统计,决策提交后「待你处理」立即消失;
|
package/client.js
CHANGED
|
@@ -34,7 +34,12 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
tabSituation: '态势',
|
|
35
35
|
tabsAria: '项目视图',
|
|
36
36
|
composer: '写一条消息…',
|
|
37
|
-
taskPlaceholder: '
|
|
37
|
+
taskPlaceholder: '任务标题:要做什么',
|
|
38
|
+
taskDefinitionPlaceholder: '验收标准(必填):怎样算完成、拿什么证据',
|
|
39
|
+
taskDefinitionAria: 'Task 验收标准',
|
|
40
|
+
qaChecklistPlaceholder: 'QA 清单,每行一条(开启 QA 时必填)',
|
|
41
|
+
qaChecklistAria: 'QA 验收清单',
|
|
42
|
+
taskDefinitionRequired: '请先写验收标准,再创建 Task。',
|
|
38
43
|
discuss: '讨论',
|
|
39
44
|
taskMode: '任务',
|
|
40
45
|
send: '发送',
|
|
@@ -341,7 +346,12 @@ window.__ModuleLoader__.load({
|
|
|
341
346
|
tabSituation: 'Situation',
|
|
342
347
|
tabsAria: 'Project views',
|
|
343
348
|
composer: 'Write a message…',
|
|
344
|
-
taskPlaceholder: '
|
|
349
|
+
taskPlaceholder: 'Task title: what to do',
|
|
350
|
+
taskDefinitionPlaceholder: 'Definition of done (required): how it is accepted, with what evidence',
|
|
351
|
+
taskDefinitionAria: 'Definition of done',
|
|
352
|
+
qaChecklistPlaceholder: 'QA checklist, one per line (required when QA is on)',
|
|
353
|
+
qaChecklistAria: 'QA checklist',
|
|
354
|
+
taskDefinitionRequired: 'Write the definition of done before creating a Task.',
|
|
345
355
|
discuss: 'Discuss',
|
|
346
356
|
taskMode: 'Task',
|
|
347
357
|
send: 'Send',
|
|
@@ -588,6 +598,10 @@ window.__ModuleLoader__.load({
|
|
|
588
598
|
conversationMock: 'spike: this host has no working layout.selectPanel(conversation).',
|
|
589
599
|
discussSent: 'Sent to the Project owner.',
|
|
590
600
|
taskSent: 'Task created.',
|
|
601
|
+
duplicateTask: 'An identical Task already exists (#{number}); opened it instead.',
|
|
602
|
+
qaNeedsChecklist: 'A QA Task needs at least one checklist item.',
|
|
603
|
+
duplicateTask: '相同 Task 已存在(#{number}),已为你打开它。',
|
|
604
|
+
qaNeedsChecklist: 'QA Task 至少需要一条验收清单。',
|
|
591
605
|
sendFailed: 'Send failed: {message}',
|
|
592
606
|
pendingHuman: 'Needs you',
|
|
593
607
|
inboxUnread: 'Inbox, {n} unread',
|
|
@@ -974,6 +988,9 @@ window.__ModuleLoader__.load({
|
|
|
974
988
|
.at-inboxactions { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; }
|
|
975
989
|
.at-qacheck { display: inline-flex; align-items: center; gap: 4px; color: var(--dsw-alias-label-tertiary); font-size: 11px; white-space: nowrap; }
|
|
976
990
|
.at-qacheck input { accent-color: var(--dsw-alias-state-business-primary); }
|
|
991
|
+
.at-taskfields { display: flex; flex-direction: column; gap: 6px; padding: 0 10px 8px; }
|
|
992
|
+
.at-taskdef, .at-taskqa { width: 100%; box-sizing: border-box; resize: vertical; min-height: 42px; padding: 7px 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; }
|
|
993
|
+
.at-taskdef::placeholder, .at-taskqa::placeholder { color: var(--dsw-alias-label-caption); }
|
|
977
994
|
.at-qabadge { display: inline-flex; align-items: center; margin-left: 5px; padding: 0 5px; border: 1px solid var(--dsw-alias-state-business-primary); border-radius: 999px; color: var(--dsw-alias-state-business-primary); font-size: 9px; line-height: 15px; vertical-align: 1px; }
|
|
978
995
|
.at-talented { opacity: 0.82; }
|
|
979
996
|
.at-btn-small { min-height: 24px; padding: 0 8px; font-size: 11px; }
|
|
@@ -1915,8 +1932,12 @@ window.__ModuleLoader__.load({
|
|
|
1915
1932
|
h('button', { className: 'at-send', type: 'button', disabled: !reply.trim(), onClick: onReplySend }, t('send'))))) : null);
|
|
1916
1933
|
}
|
|
1917
1934
|
|
|
1918
|
-
function Composer({ t, mode, draft, disabled, qaRequired, onQaRequired, onMode, onDraft, onSend }) {
|
|
1919
|
-
const send = () => {
|
|
1935
|
+
function Composer({ t, mode, draft, definition, qaChecklist, disabled, qaRequired, onQaRequired, onDefinition, onQaChecklist, onMode, onDraft, onSend }) {
|
|
1936
|
+
const send = () => {
|
|
1937
|
+
if (!draft.trim() || disabled) return;
|
|
1938
|
+
if (mode === 'task' && (!definition.trim() || (qaRequired && !qaChecklist.trim()))) return;
|
|
1939
|
+
onSend();
|
|
1940
|
+
};
|
|
1920
1941
|
return h('div', { className: 'at-composer' },
|
|
1921
1942
|
h('div', { className: 'at-composerinner' },
|
|
1922
1943
|
h('div', { className: cx('at-box', 'at-composerbox', mode === 'task' && 'at-taskmode') },
|
|
@@ -1934,6 +1955,25 @@ window.__ModuleLoader__.load({
|
|
|
1934
1955
|
}
|
|
1935
1956
|
},
|
|
1936
1957
|
}),
|
|
1958
|
+
mode === 'task' ? h('div', { className: 'at-taskfields' },
|
|
1959
|
+
h('textarea', {
|
|
1960
|
+
className: 'at-taskdef',
|
|
1961
|
+
rows: 2,
|
|
1962
|
+
value: definition,
|
|
1963
|
+
disabled,
|
|
1964
|
+
placeholder: t('taskDefinitionPlaceholder'),
|
|
1965
|
+
'aria-label': t('taskDefinitionAria'),
|
|
1966
|
+
onChange: (event) => onDefinition(event.target.value),
|
|
1967
|
+
}),
|
|
1968
|
+
qaRequired ? h('textarea', {
|
|
1969
|
+
className: 'at-taskqa',
|
|
1970
|
+
rows: 2,
|
|
1971
|
+
value: qaChecklist,
|
|
1972
|
+
disabled,
|
|
1973
|
+
placeholder: t('qaChecklistPlaceholder'),
|
|
1974
|
+
'aria-label': t('qaChecklistAria'),
|
|
1975
|
+
onChange: (event) => onQaChecklist(event.target.value),
|
|
1976
|
+
}) : null) : null,
|
|
1937
1977
|
h('div', { className: 'at-barrow' },
|
|
1938
1978
|
h('div', { className: 'at-mode', role: 'group', 'aria-label': t('modeAria') },
|
|
1939
1979
|
h('button', { type: 'button', 'aria-pressed': mode === 'discuss', onClick: () => onMode('discuss') }, t('discuss')),
|
|
@@ -1942,7 +1982,7 @@ window.__ModuleLoader__.load({
|
|
|
1942
1982
|
h('input', { type: 'checkbox', checked: qaRequired, onChange: (event) => onQaRequired(event.target.checked) }),
|
|
1943
1983
|
h('span', null, t('qaRequired'))) : null,
|
|
1944
1984
|
h('span', { className: 'at-hint' }, t('enterHint')),
|
|
1945
|
-
h('button', { className: 'at-send', type: 'button', disabled: disabled || !draft.trim(), onClick: send }, mode === 'task' ? t('createTask') : t('send'))))));
|
|
1985
|
+
h('button', { className: 'at-send', type: 'button', disabled: disabled || !draft.trim() || (mode === 'task' && (!definition.trim() || (qaRequired && !qaChecklist.trim()))), onClick: send }, mode === 'task' ? t('createTask') : t('send'))))));
|
|
1946
1986
|
}
|
|
1947
1987
|
|
|
1948
1988
|
function QuadrantCard({ t, meta, quadrant, tasks, items, count, onItem, onViewAll }) {
|
|
@@ -2115,6 +2155,8 @@ window.__ModuleLoader__.load({
|
|
|
2115
2155
|
const [threadOpen, setThreadOpen] = React.useState(false);
|
|
2116
2156
|
const [mode, setMode] = React.useState('discuss');
|
|
2117
2157
|
const [qaTask, setQaTask] = React.useState(false);
|
|
2158
|
+
const [taskDefinition, setTaskDefinition] = React.useState('');
|
|
2159
|
+
const [qaChecklistText, setQaChecklistText] = React.useState('');
|
|
2118
2160
|
const [draft, setDraft] = React.useState('');
|
|
2119
2161
|
const [note, setNote] = React.useState('');
|
|
2120
2162
|
const [rightNote, setRightNote] = React.useState('');
|
|
@@ -2513,7 +2555,12 @@ window.__ModuleLoader__.load({
|
|
|
2513
2555
|
body: JSON.stringify({ type, payload }),
|
|
2514
2556
|
});
|
|
2515
2557
|
const body = await response.json().catch(() => null);
|
|
2516
|
-
if (!response.ok)
|
|
2558
|
+
if (!response.ok) {
|
|
2559
|
+
const error = new Error(body?.error?.message ?? ('HTTP ' + response.status));
|
|
2560
|
+
error.code = body?.error?.code;
|
|
2561
|
+
error.details = body?.error?.details;
|
|
2562
|
+
throw error;
|
|
2563
|
+
}
|
|
2517
2564
|
return body;
|
|
2518
2565
|
}
|
|
2519
2566
|
function openAddAgent() {
|
|
@@ -2812,7 +2859,18 @@ window.__ModuleLoader__.load({
|
|
|
2812
2859
|
if (!project?.workspacePath) { say(t('workspaceRequired')); return; }
|
|
2813
2860
|
try {
|
|
2814
2861
|
if (mode === 'task') {
|
|
2815
|
-
|
|
2862
|
+
const definition = taskDefinition.trim();
|
|
2863
|
+
const checklist = qaChecklistText.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
2864
|
+
if (definition === '') { say(t('taskDefinitionRequired')); return; }
|
|
2865
|
+
if (qaTask && checklist.length === 0) { say(t('qaNeedsChecklist')); return; }
|
|
2866
|
+
await runCommand('task.create', {
|
|
2867
|
+
title: text,
|
|
2868
|
+
resultDefinition: definition,
|
|
2869
|
+
qaRequired: qaTask,
|
|
2870
|
+
...(qaTask ? { qaChecklist: checklist } : {}),
|
|
2871
|
+
});
|
|
2872
|
+
setTaskDefinition('');
|
|
2873
|
+
setQaChecklistText('');
|
|
2816
2874
|
} else {
|
|
2817
2875
|
await runCommand('stream.post', { body: text });
|
|
2818
2876
|
}
|
|
@@ -2820,7 +2878,15 @@ window.__ModuleLoader__.load({
|
|
|
2820
2878
|
say(mode === 'task' ? t('taskSent') : t('discussSent'));
|
|
2821
2879
|
await reloadData();
|
|
2822
2880
|
} catch (error) {
|
|
2823
|
-
|
|
2881
|
+
if (error?.code === 'E_CONFLICT' && error.details?.taskId) {
|
|
2882
|
+
setSelected(error.details.taskId);
|
|
2883
|
+
setThreadOpen(true);
|
|
2884
|
+
say(tf(t, 'duplicateTask', { number: error.details.number ?? '' }));
|
|
2885
|
+
} else if (error?.message?.includes('qaChecklist')) {
|
|
2886
|
+
say(t('qaNeedsChecklist'));
|
|
2887
|
+
} else {
|
|
2888
|
+
say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
|
|
2889
|
+
}
|
|
2824
2890
|
}
|
|
2825
2891
|
}
|
|
2826
2892
|
async function sendReply() {
|
|
@@ -3154,7 +3220,11 @@ window.__ModuleLoader__.load({
|
|
|
3154
3220
|
draft,
|
|
3155
3221
|
disabled: !project || !project.workspacePath,
|
|
3156
3222
|
qaRequired: qaTask,
|
|
3223
|
+
definition: taskDefinition,
|
|
3224
|
+
qaChecklist: qaChecklistText,
|
|
3157
3225
|
onQaRequired: setQaTask,
|
|
3226
|
+
onDefinition: setTaskDefinition,
|
|
3227
|
+
onQaChecklist: setQaChecklistText,
|
|
3158
3228
|
onMode: setMode,
|
|
3159
3229
|
onDraft: setDraft,
|
|
3160
3230
|
onSend: send,
|
package/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export const name = 'ai-team';
|
|
|
34
34
|
/** The workbench only makes sense where the Web carrier exists. */
|
|
35
35
|
export const inject = ['webServer', 'permissionPresets', 'sandboxPolicy'];
|
|
36
36
|
|
|
37
|
-
const VERSION = '0.3.
|
|
37
|
+
const VERSION = '0.3.27';
|
|
38
38
|
const PROJECT_ID = 'prj_main';
|
|
39
39
|
const HUMAN_ID = 'mem_human';
|
|
40
40
|
|
package/lib/domain/policy.js
CHANGED
|
@@ -48,6 +48,12 @@ export function memberHasInProgress(tasksById, memberId) {
|
|
|
48
48
|
return Object.values(tasksById ?? {}).find((candidate) => candidate.assigneeId === memberId && candidate.status === 'in_progress');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** A member must not start the next Task while their previous Task awaits QA. */
|
|
52
|
+
export function memberHasPendingAcceptance(tasksById, memberId) {
|
|
53
|
+
if (memberId === null) return undefined;
|
|
54
|
+
return Object.values(tasksById ?? {}).find((candidate) => candidate.assigneeId === memberId && candidate.status === 'pending_acceptance');
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
/**
|
|
52
58
|
* A reviewer is eligible when the task is in pending_acceptance and the
|
|
53
59
|
* reviewer never claimed it (Agent) or is Human. Human self-accept is allowed
|
package/lib/domain/task.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
assertRevision,
|
|
9
9
|
dependenciesDone,
|
|
10
10
|
memberHasInProgress,
|
|
11
|
+
memberHasPendingAcceptance,
|
|
11
12
|
} from './policy.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -103,6 +104,12 @@ export function claimTask(task, actor, tasksById, options = {}) {
|
|
|
103
104
|
details: { taskId: task.id, blockedBy: task.blockedBy },
|
|
104
105
|
});
|
|
105
106
|
}
|
|
107
|
+
const awaiting = memberHasPendingAcceptance(tasksById, memberId);
|
|
108
|
+
if (awaiting !== undefined) {
|
|
109
|
+
fail(ERRORS.CONFLICT, 'member has a task awaiting acceptance; wait for QA before claiming the next one', {
|
|
110
|
+
details: { memberId, taskId: awaiting.id },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
106
113
|
const busy = memberHasInProgress(tasksById, memberId);
|
|
107
114
|
if (busy !== undefined) {
|
|
108
115
|
fail(ERRORS.CONFLICT, 'member already has an in_progress task', {
|
package/lib/host/member-tools.js
CHANGED
|
@@ -71,7 +71,8 @@ export function createTeamTool(options) {
|
|
|
71
71
|
ownerId: { type: 'string', description: 'Accountable owner id from members for create; defaults to you.' },
|
|
72
72
|
assigneeId: { type: 'string', description: 'Optional assignee id from members for create.' },
|
|
73
73
|
qaRequired: { type: 'boolean', description: 'Require a QA member to accept this task.' },
|
|
74
|
-
qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance.' },
|
|
74
|
+
qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance; required when qaRequired is true.' },
|
|
75
|
+
allowDuplicate: { type: 'boolean', description: 'Set true only when an identical active Task is intentionally required.' },
|
|
75
76
|
checks: {
|
|
76
77
|
type: 'array',
|
|
77
78
|
description: 'Acceptance checks for accept; include every qaChecklist item with passed=true/false.',
|
|
@@ -237,6 +238,7 @@ export function createTeamTool(options) {
|
|
|
237
238
|
resultDefinition: args.resultDefinition,
|
|
238
239
|
ownerId: owner.id,
|
|
239
240
|
...(assignee === null ? {} : { assigneeId: assignee.id }),
|
|
241
|
+
...(args.allowDuplicate === true ? { allowDuplicate: true } : {}),
|
|
240
242
|
...(args.qaRequired === true ? { qaRequired: true } : {}),
|
|
241
243
|
...(Array.isArray(args.qaChecklist) ? { qaChecklist: args.qaChecklist } : {}),
|
|
242
244
|
});
|
package/lib/host/service.js
CHANGED
|
@@ -769,17 +769,35 @@ export class TeamService {
|
|
|
769
769
|
if (project === undefined) fail(ERRORS.NOT_FOUND, `project ${String(projectId)} not found`);
|
|
770
770
|
const number = Math.max(0, ...Object.values(state.tasks ?? {}).map((task) => task.number ?? 0)) + 1;
|
|
771
771
|
const ownerId = payload.ownerId ?? project.ownerMemberId ?? project.ownerId;
|
|
772
|
+
const qaRequired = payload.qaRequired === true;
|
|
773
|
+
const qaChecklist = Array.isArray(payload.qaChecklist) ? payload.qaChecklist.filter((item) => typeof item === 'string' && item.trim() !== '') : [];
|
|
774
|
+
if (qaRequired && qaChecklist.length === 0) {
|
|
775
|
+
fail(ERRORS.INVALID_ARGUMENT, 'qaRequired needs at least one qaChecklist item');
|
|
776
|
+
}
|
|
777
|
+
const title = typeof payload.title === 'string' ? payload.title.trim() : '';
|
|
778
|
+
const resultDefinition = typeof payload.resultDefinition === 'string' ? payload.resultDefinition.trim() : '';
|
|
779
|
+
const duplicate = Object.values(state.tasks ?? {}).find((candidate) =>
|
|
780
|
+
candidate.tombstone !== true
|
|
781
|
+
&& ['pending', 'in_progress', 'pending_acceptance'].includes(candidate.status)
|
|
782
|
+
&& candidate.ownerId === ownerId
|
|
783
|
+
&& candidate.title === title
|
|
784
|
+
&& candidate.resultDefinition === resultDefinition);
|
|
785
|
+
if (duplicate !== undefined && payload.allowDuplicate !== true) {
|
|
786
|
+
fail(ERRORS.CONFLICT, 'an active Task with the same title, result definition and owner already exists', {
|
|
787
|
+
details: { taskId: duplicate.id, number: duplicate.number, title: duplicate.title },
|
|
788
|
+
});
|
|
789
|
+
}
|
|
772
790
|
const { task, event } = createTask({
|
|
773
791
|
number,
|
|
774
|
-
title
|
|
775
|
-
resultDefinition
|
|
792
|
+
title,
|
|
793
|
+
resultDefinition,
|
|
776
794
|
ownerId,
|
|
777
795
|
assigneeId: payload.assigneeId,
|
|
778
796
|
blockedBy: payload.blockedBy,
|
|
779
797
|
writeScopes: payload.writeScopes,
|
|
780
798
|
irreversible: payload.irreversible,
|
|
781
|
-
qaRequired
|
|
782
|
-
qaChecklist
|
|
799
|
+
qaRequired,
|
|
800
|
+
qaChecklist,
|
|
783
801
|
originRefs: payload.originRefs,
|
|
784
802
|
at: this.now(),
|
|
785
803
|
});
|
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.27",
|
|
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",
|