@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 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: 'A one-line task; add the definition of done later',
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 = () => { if (draft.trim() && !disabled) onSend(); };
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) throw new Error(body?.error?.message ?? ('HTTP ' + response.status));
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
- await runCommand('task.create', { title: text, resultDefinition: text, qaRequired: qaTask });
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
- say(tf(t, 'sendFailed', { message: error?.message ?? String(error) }));
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.25';
37
+ const VERSION = '0.3.27';
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.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",