@suifei/dsh-ai-team 0.3.18 → 0.3.19

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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.19 — 2026-11-10
4
+
5
+ - 新增 `team askHuman`:Agent 必须把每个 Human 决策问题、选项、推荐项逐条提交;
6
+ - 收件箱决策项直接展开问题/选项/推荐,不再要求翻报告或 Thread;
7
+ - 顶部增加「待你处理 · N」一键入口;
8
+ - Thread 中 @Human 的普通消息仍自动生成决策项。
9
+
3
10
  ## 0.3.18 — 2026-11-10
4
11
 
5
12
  - 打开 Project 时回填已有 Thread 中对 Human 的拍板请求到收件箱;
package/README.md CHANGED
@@ -87,7 +87,7 @@ dsh --profile ai-team --no-open --port 33271
87
87
  - **主对话**:Human 的一句话进入 Main stream,Project owner 拆 Task;顶部「团队动态」直接显示正在推进的 Task 和运行中的成员,点击 Task 可打开 Thread。
88
88
  - **看板**:四列;进行中的卡片显示 accountable owner 与执行者;数量只统计 active Task。
89
89
  - **态势**:四象限——需要你、团队在做、等待受阻、最近完成。点击条目打开对应 Task 的 Thread。
90
- - **收件箱**:跨 Project 聚合;失败、需要你决定、关注 Task 的完成报告,以及 Agent 在 Thread 里明确请你拍板的消息都在这里。
90
+ - **收件箱**:跨 Project 聚合;顶部「待你处理 · N」一键进入;决策项直接展开全部问题与选项,不用再翻 Thread/报告。
91
91
  - **直接对话**:点击成员名字直接打开该 Agent 的 DSH 原生 Session;没有 session 会先激活;打不开时落到成员资料,资料里有「直接对话」按钮。
92
92
  - **成员任务**:成员资料里点「他的任务」,查看他 owner/assignee 的 Task。
93
93
  - **成员资料**:点「⋯」改名、职责、personality、provider/model、运行配方、notes、skills、re-distill、离队/人才库。
@@ -102,7 +102,7 @@ dsh --profile ai-team --no-open --port 33271
102
102
  | Task | 唯一 accountable owner;pending / in_progress / pending_acceptance / done / cancelled |
103
103
  | Thread | append-only;回复带 `baseSeq`,旧基线会被拒并说明变化 |
104
104
  | 验收 | maker 不能验收;QA 硬门禁;`qaChecklist` 逐项通过 |
105
- | 收件箱 | Human/Agent 各自拉取;跨 Project 聚合;Thread 中提到 Human 会自动生成决策项 |
105
+ | 收件箱 | Human/Agent 各自拉取;跨 Project 聚合;顶部待办入口;`askHuman` 结构化问题与选项直接展开 |
106
106
  | 关注 | 关注 Task;完成后收到 `followed_task_update` |
107
107
  | 默认成员 | 新 Project 初始化墨渊 / 青岚 / 白砚;可改名、改职责、改个性、改模型 |
108
108
  | 成员 | 持久 session;点击名字直接对话;「⋯」资料里有任务视图与直接对话 |
package/client.js CHANGED
@@ -277,6 +277,7 @@ window.__ModuleLoader__.load({
277
277
  discussSent: '已发送给 Project owner。',
278
278
  taskSent: '已创建 Task。',
279
279
  sendFailed: '发送失败:{message}',
280
+ pendingHuman: '待你处理',
280
281
  inboxUnread: '收件箱,{n} 条未读',
281
282
  dismiss: '关闭提示',
282
283
  retry: '重试',
@@ -578,6 +579,7 @@ window.__ModuleLoader__.load({
578
579
  discussSent: 'Sent to the Project owner.',
579
580
  taskSent: 'Task created.',
580
581
  sendFailed: 'Send failed: {message}',
582
+ pendingHuman: 'Needs you',
581
583
  inboxUnread: 'Inbox, {n} unread',
582
584
  dismiss: 'Dismiss',
583
585
  retry: 'Retry',
@@ -904,6 +906,7 @@ window.__ModuleLoader__.load({
904
906
  .at-top { height: 40px; flex: 0 0 auto; display: flex; align-items: center; gap: 8px; min-width: 0; padding: 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); }
905
907
  .at-brand { color: var(--dsw-alias-label-primary); font-size: 12px; font-weight: 500; white-space: nowrap; }
906
908
  .at-spike { display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border: 1px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-xs); color: var(--dsw-alias-label-tertiary); font-size: 10px; line-height: 16px; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap; }
909
+ .at-pending { margin-left: auto; height: 24px; padding: 0 8px; border: 1px solid var(--dsw-alias-state-warning-primary); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-base); color: var(--dsw-alias-state-warning-primary); font: inherit; font-size: 11px; line-height: 16px; cursor: pointer; white-space: nowrap; }
907
910
  .at-topproject { margin-left: auto; min-width: 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
908
911
  .at-mobile { display: none; position: relative; min-width: 0; }
909
912
  .at-mobilesummary { display: inline-flex; align-items: center; gap: 6px; min-height: 28px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); color: var(--dsw-alias-label-secondary); font-size: 11px; cursor: pointer; list-style: none; }
@@ -947,6 +950,11 @@ window.__ModuleLoader__.load({
947
950
  .at-inboxitem { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 11px 13px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-layer-2); }
948
951
  .at-inboxitem.at-inboxdone { opacity: 0.62; }
949
952
  .at-inboxbody { min-width: 0; flex: 1; }
953
+ .at-inboxquestions { margin: 6px 0 2px; padding-left: 20px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 19px; }
954
+ .at-inboxquestions li { margin: 3px 0; }
955
+ .at-inboxquestion { display: block; color: var(--dsw-alias-label-primary); }
956
+ .at-inboxoptions { display: block; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
957
+ .at-inboxrecommend { display: block; color: var(--dsw-alias-state-business-primary); font-size: 11px; }
950
958
  .at-inboxtitle { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; }
951
959
  .at-inboxmeta { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
952
960
  .at-inboxstate { color: var(--dsw-alias-label-caption); font-size: 10.5px; }
@@ -2950,10 +2958,18 @@ window.__ModuleLoader__.load({
2950
2958
  h('div', { className: 'at-inboxbody' },
2951
2959
  h('div', { className: 'at-inboxtitle' },
2952
2960
  item.type === 'decision_required'
2953
- ? t('inboxDecisionTitle')
2961
+ ? (item.ref?.title || t('inboxDecisionTitle'))
2954
2962
  : item.type === 'followed_task_update'
2955
2963
  ? tf(t, 'inboxCompletedTitle', { title: item.ref?.title ?? item.ref?.taskId ?? '' })
2956
2964
  : t('inboxItemTitle')),
2965
+ Array.isArray(item.ref?.questions) && item.ref.questions.length
2966
+ ? h('ol', { className: 'at-inboxquestions' }, item.ref.questions.map((question, index) => h('li', { key: question.id ?? index },
2967
+ h('span', { className: 'at-inboxquestion' }, question.text),
2968
+ Array.isArray(question.options) && question.options.length
2969
+ ? h('span', { className: 'at-inboxoptions' }, question.options.join(' / '))
2970
+ : null,
2971
+ question.recommendation ? h('span', { className: 'at-inboxrecommend' }, '推荐:' + question.recommendation) : null)))
2972
+ : null,
2957
2973
  h('div', { className: 'at-inboxmeta' },
2958
2974
  item.projectName ? item.projectName + ' · ' : '',
2959
2975
  item.ref?.category ? item.ref.category + ' · ' : '',
@@ -3024,6 +3040,7 @@ window.__ModuleLoader__.load({
3024
3040
  h('div', { className: 'at-top' },
3025
3041
  h('span', { className: 'at-brand' }, t('panel')),
3026
3042
  h('span', { className: 'at-spike', title: t('spikeTitle') }, t('spike')),
3043
+ inboxUnread > 0 ? h('button', { className: 'at-pending', type: 'button', onClick: switchInbox }, t('pendingHuman') + ' · ' + unread) : null,
3027
3044
  h('details', { className: 'at-mobile', ref: mobileRef },
3028
3045
  h('summary', { className: 'at-mobilesummary', 'aria-label': t('menuAria') }, h(Ico, { name: 'menu', size: 14 }), h('span', null, t('menu'))),
3029
3046
  h('div', { className: 'at-mobilepop' }, h(LeftPanel, leftProps))),
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.18';
37
+ const VERSION = '0.3.19';
38
38
  const PROJECT_ID = 'prj_main';
39
39
  const HUMAN_ID = 'mem_human';
40
40
 
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { taskCard } from './views.js';
9
9
 
10
- const ACTIONS = ['tasks', 'members', 'inbox', 'recall', 'create', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
10
+ const ACTIONS = ['tasks', 'members', 'inbox', 'recall', 'create', 'askHuman', 'member', 'evolve', 'distill', 'note', 'skill', 'claim', 'submit', 'thread', 'reply', 'accept', 'requestChanges'];
11
11
 
12
12
  function failure(error) {
13
13
  return {
@@ -53,7 +53,7 @@ function rosterText(state) {
53
53
  export function createTeamTool(options) {
54
54
  return {
55
55
  name: 'team',
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.',
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. When Human must decide something, call askHuman with every question and options—do not bury questions in a report. Use recall to search your notes/skills and your past Tasks/Threads. Claim before working; read a Thread before replying; submit evidence before acceptance.',
57
57
  parameters: {
58
58
  type: 'object',
59
59
  properties: {
@@ -72,6 +72,20 @@ export function createTeamTool(options) {
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
74
  qaChecklist: { type: 'array', items: { type: 'string' }, description: 'QA checklist items; all must pass before acceptance.' },
75
+ questions: {
76
+ type: 'array',
77
+ description: 'Human decision questions for askHuman; include every question instead of pointing at a report.',
78
+ items: {
79
+ type: 'object',
80
+ properties: {
81
+ text: { type: 'string', description: 'The question.' },
82
+ options: { type: 'array', items: { type: 'string' }, description: 'Optional answer choices.' },
83
+ recommendation: { type: 'string', description: 'Optional recommended answer.' },
84
+ blocking: { type: 'string', description: 'What this decision blocks.' },
85
+ },
86
+ required: ['text'],
87
+ },
88
+ },
75
89
  personality: { type: 'string', description: 'New working personality for evolve/distill.' },
76
90
  reason: { type: 'string', description: 'Why the personality changed.' },
77
91
  memberId: { type: 'string', description: 'Target member for member/distill/note/skill.' },
@@ -216,6 +230,18 @@ export function createTeamTool(options) {
216
230
  });
217
231
  return { ok: true, task: taskCard(options.getState(), result.task) };
218
232
  }
233
+ if (args.action === 'askHuman') {
234
+ if (!Array.isArray(args.questions) || args.questions.length === 0) {
235
+ return failure({ code: 'E_INVALID_ARGUMENT', message: 'questions is required; list every question Human must answer' });
236
+ }
237
+ const result = await command('decision.request', {
238
+ title: typeof args.title === 'string' && args.title.trim() !== '' ? args.title.trim() : '需要你决策',
239
+ taskId: typeof args.taskId === 'string' && args.taskId !== '' ? args.taskId : null,
240
+ questions: args.questions,
241
+ source: 'team',
242
+ });
243
+ return { ok: true, item: result.item, duplicate: result.duplicate === true };
244
+ }
219
245
  if (args.action === 'inbox') {
220
246
  const items = Object.values(state.inbox ?? {})
221
247
  .filter((item) => item.memberId === member.id && !['resolved', 'dismissed', 'superseded'].includes(item.state));
@@ -232,6 +232,7 @@ export class TeamService {
232
232
  case 'task.follow': return await this.follow(command, payload);
233
233
  case 'thread.post': return await this.postThread(command, payload);
234
234
  case 'inbox.ack': return await this.ack(command, payload);
235
+ case 'decision.request': return await this.requestDecision(command, payload);
235
236
  default: fail(ERRORS.INVALID_ARGUMENT, `unknown command: ${String(command.type)}`);
236
237
  }
237
238
  }
@@ -889,6 +890,59 @@ export class TeamService {
889
890
  return { thread: outcome.thread, entry: outcome.entry };
890
891
  }
891
892
 
893
+ /** Surface an explicit Human decision request with all questions inline. */
894
+ async requestDecision(command, payload) {
895
+ const humanId = this.state.humanMemberId
896
+ ?? Object.values(this.state.members ?? {}).find((member) => member.kind === 'human')?.id
897
+ ?? null;
898
+ if (humanId === null) fail(ERRORS.NOT_FOUND, 'Human member not found');
899
+ const raw = Array.isArray(payload.questions) ? payload.questions : [];
900
+ if (raw.length === 0 || raw.length > 20) fail(ERRORS.INVALID_ARGUMENT, 'questions must contain 1..20 items');
901
+ const questions = raw.map((question, index) => {
902
+ if (typeof question === 'string') return { id: `q${index + 1}`, text: question.trim() };
903
+ if (question === null || typeof question !== 'object') fail(ERRORS.INVALID_ARGUMENT, `questions[${index}] must be a string or object`);
904
+ const text = typeof question.text === 'string' ? question.text.trim() : '';
905
+ if (text === '') fail(ERRORS.INVALID_ARGUMENT, `questions[${index}].text is required`);
906
+ const options = Array.isArray(question.options) ? question.options.filter((option) => typeof option === 'string' && option.trim() !== '').slice(0, 12) : [];
907
+ return {
908
+ id: typeof question.id === 'string' && question.id !== '' ? question.id : `q${index + 1}`,
909
+ text,
910
+ ...(options.length ? { options } : {}),
911
+ ...(typeof question.recommendation === 'string' && question.recommendation !== '' ? { recommendation: question.recommendation } : {}),
912
+ ...(typeof question.blocking === 'string' && question.blocking !== '' ? { blocking: question.blocking } : {}),
913
+ };
914
+ });
915
+ const title = typeof payload.title === 'string' && payload.title.trim() !== '' ? payload.title.trim() : '需要你决策';
916
+ const taskId = typeof payload.taskId === 'string' ? payload.taskId : null;
917
+ const duplicate = Object.values(this.state.inbox ?? {}).find((item) =>
918
+ item.memberId === humanId
919
+ && item.type === 'decision_required'
920
+ && item.ref?.title === title
921
+ && item.ref?.taskId === taskId
922
+ && !['resolved', 'dismissed', 'superseded'].includes(item.state));
923
+ if (duplicate !== undefined) return { item: duplicate, duplicate: true };
924
+ const item = createInboxItem({
925
+ memberId: humanId,
926
+ type: 'decision_required',
927
+ ref: {
928
+ taskId,
929
+ title,
930
+ questions,
931
+ ...(typeof payload.source === 'string' && payload.source !== '' ? { source: payload.source } : {}),
932
+ },
933
+ requiresDecision: true,
934
+ }, { at: this.now() });
935
+ await this.ledger.append({
936
+ type: 'inbox/item-created',
937
+ payload: { itemId: item.id, ...item },
938
+ actor: command.actor,
939
+ correlationId: command.correlationId,
940
+ projectId: command.projectId,
941
+ at: this.now(),
942
+ });
943
+ return { item, duplicate: false };
944
+ }
945
+
892
946
  async ack(command, payload) {
893
947
  const state = this.state;
894
948
  const item = state.inbox?.[payload.itemId];
package/lib/host/waker.js CHANGED
@@ -165,7 +165,7 @@ export function planWakes(state, result, actor = {}) {
165
165
  null,
166
166
  'human_message',
167
167
  `human_message:${event.seq ?? event.payload?.seq}`,
168
- `你是 Project owner。Human 在 Main stream 说:“${body}”。本轮必须先用 team action=members 查看成员,再用 action=create 创建至少一个 Task;ownerId 必须使用成员 id,写明可验证的 resultDefinition。可用成员:${roster || '无'}。如果需求很大或外部链接只是参考,先创建“调研/澄清/拆解”Task,不要直接开始实现;如果确实无法创建,必须明确说明原因。resultDefinition 只写验收标准,不要写“等待 Human 验收”;不要只读文件或只回一段话结束。`,
168
+ `你是 Project owner。Human 在 Main stream 说:“${body}”。本轮必须先用 team action=members 查看成员,再用 action=create 创建至少一个 Task;ownerId 必须使用成员 id,写明可验证的 resultDefinition。可用成员:${roster || '无'}。如果需求很大或外部链接只是参考,先创建“调研/澄清/拆解”Task,不要直接开始实现;如果确实无法创建,必须明确说明原因。需要 Human 拍板时,必须调用 team action=askHuman,把每个问题和选项逐条放进 questions,不要只写“见报告/§6/已提交 Human”。resultDefinition 只写验收标准,不要写“等待 Human 验收”;不要只读文件或只回一段话结束。`,
169
169
  );
170
170
  }
171
171
  if (event?.type === 'inbox/item-created') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suifei/dsh-ai-team",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
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",