@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 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) throw new Error(body?.error?.message ?? ('HTTP ' + response.status));
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
- say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
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.25';
37
+ const VERSION = '0.3.26';
38
38
  const PROJECT_ID = 'prj_main';
39
39
  const HUMAN_ID = 'mem_human';
40
40
 
@@ -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
@@ -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', {
@@ -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
  });
@@ -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: payload.title,
775
- resultDefinition: payload.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: payload.qaRequired === true,
782
- qaChecklist: Array.isArray(payload.qaChecklist) ? payload.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.25",
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",