@suifei/dsh-ai-team 0.3.25 → 0.3.26
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/client.js +19 -2
- 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,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.26 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
- Task 创建增加重复保护:相同 title/resultDefinition/owner 的 active Task 会被拒绝并返回已有 Task;
|
|
6
|
+
- 主对话「任务」模式收到重复错误时自动打开已有 Task,不再产生 #3/#4 这类重复卡;
|
|
7
|
+
- `qaRequired=true` 但没有 qaChecklist 时拒绝创建,避免产生无法验收的 #4;
|
|
8
|
+
- 成员有 `pending_acceptance` Task 时不能再 claim 下一张,阻止 T2/T3 在 T1 验收前提前开工;
|
|
9
|
+
- `team() accept` 现在透传 checks,QA Agent 不再需要走 Human API。
|
|
10
|
+
|
|
3
11
|
## 0.3.25 — 2026-11-10
|
|
4
12
|
|
|
5
13
|
- 收件箱未读数按 `state=unread` 统计,决策提交后「待你处理」立即消失;
|
package/client.js
CHANGED
|
@@ -588,6 +588,10 @@ window.__ModuleLoader__.load({
|
|
|
588
588
|
conversationMock: 'spike: this host has no working layout.selectPanel(conversation).',
|
|
589
589
|
discussSent: 'Sent to the Project owner.',
|
|
590
590
|
taskSent: 'Task created.',
|
|
591
|
+
duplicateTask: 'An identical Task already exists (#{number}); opened it instead.',
|
|
592
|
+
qaNeedsChecklist: 'A QA Task needs at least one checklist item.',
|
|
593
|
+
duplicateTask: '相同 Task 已存在(#{number}),已为你打开它。',
|
|
594
|
+
qaNeedsChecklist: 'QA Task 至少需要一条验收清单。',
|
|
591
595
|
sendFailed: 'Send failed: {message}',
|
|
592
596
|
pendingHuman: 'Needs you',
|
|
593
597
|
inboxUnread: 'Inbox, {n} unread',
|
|
@@ -2513,7 +2517,12 @@ window.__ModuleLoader__.load({
|
|
|
2513
2517
|
body: JSON.stringify({ type, payload }),
|
|
2514
2518
|
});
|
|
2515
2519
|
const body = await response.json().catch(() => null);
|
|
2516
|
-
if (!response.ok)
|
|
2520
|
+
if (!response.ok) {
|
|
2521
|
+
const error = new Error(body?.error?.message ?? ('HTTP ' + response.status));
|
|
2522
|
+
error.code = body?.error?.code;
|
|
2523
|
+
error.details = body?.error?.details;
|
|
2524
|
+
throw error;
|
|
2525
|
+
}
|
|
2517
2526
|
return body;
|
|
2518
2527
|
}
|
|
2519
2528
|
function openAddAgent() {
|
|
@@ -2820,7 +2829,15 @@ window.__ModuleLoader__.load({
|
|
|
2820
2829
|
say(mode === 'task' ? t('taskSent') : t('discussSent'));
|
|
2821
2830
|
await reloadData();
|
|
2822
2831
|
} catch (error) {
|
|
2823
|
-
|
|
2832
|
+
if (error?.code === 'E_CONFLICT' && error.details?.taskId) {
|
|
2833
|
+
setSelected(error.details.taskId);
|
|
2834
|
+
setThreadOpen(true);
|
|
2835
|
+
say(tf(t, 'duplicateTask', { number: error.details.number ?? '' }));
|
|
2836
|
+
} else if (error?.message?.includes('qaChecklist')) {
|
|
2837
|
+
say(t('qaNeedsChecklist'));
|
|
2838
|
+
} else {
|
|
2839
|
+
say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
|
|
2840
|
+
}
|
|
2824
2841
|
}
|
|
2825
2842
|
}
|
|
2826
2843
|
async function sendReply() {
|
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.26';
|
|
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.26",
|
|
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",
|