@co0ontty/wand 4.82.0 → 4.83.1

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.
Files changed (91) hide show
  1. package/README.md +47 -9
  2. package/dist/agent-dispatch.d.ts +3 -2
  3. package/dist/agent-dispatch.js +19 -3
  4. package/dist/ai-team-delivery-types.d.ts +40 -0
  5. package/dist/ai-team-delivery-types.js +1 -0
  6. package/dist/ai-team-delivery.d.ts +13 -0
  7. package/dist/ai-team-delivery.js +89 -0
  8. package/dist/ai-team-employee-binding.d.ts +16 -0
  9. package/dist/ai-team-employee-binding.js +77 -0
  10. package/dist/ai-team-prompts.js +5 -0
  11. package/dist/ai-team-runner.d.ts +9 -2
  12. package/dist/ai-team-runner.js +52 -7
  13. package/dist/ai-team-types.d.ts +17 -3
  14. package/dist/ai-team-types.js +43 -3
  15. package/dist/build-info.json +3 -3
  16. package/dist/cli.js +36 -0
  17. package/dist/config.d.ts +18 -10
  18. package/dist/config.js +63 -105
  19. package/dist/decision-client.d.ts +4 -0
  20. package/dist/decision-client.js +91 -0
  21. package/dist/decision-runner.d.ts +9 -0
  22. package/dist/decision-runner.js +36 -0
  23. package/dist/decision-service.d.ts +46 -0
  24. package/dist/decision-service.js +230 -0
  25. package/dist/decision-skill.d.ts +6 -0
  26. package/dist/decision-skill.js +70 -0
  27. package/dist/decision-tool.d.ts +21 -0
  28. package/dist/decision-tool.js +346 -0
  29. package/dist/decision-types.d.ts +36 -0
  30. package/dist/decision-types.js +123 -0
  31. package/dist/decision-worker.py +86 -0
  32. package/dist/default-employee.d.ts +4 -1
  33. package/dist/default-employee.js +11 -1
  34. package/dist/env-utils.js +3 -0
  35. package/dist/git-quick-commit.js +7 -1
  36. package/dist/message-truncator.d.ts +1 -1
  37. package/dist/message-truncator.js +29 -22
  38. package/dist/models.d.ts +1 -0
  39. package/dist/models.js +31 -4
  40. package/dist/process-manager.d.ts +39 -1
  41. package/dist/process-manager.js +112 -8
  42. package/dist/provider-catalog.d.ts +47 -0
  43. package/dist/provider-catalog.js +138 -0
  44. package/dist/provider-cli-updater.d.ts +1 -1
  45. package/dist/pty-foreground.d.ts +37 -0
  46. package/dist/pty-foreground.js +56 -0
  47. package/dist/pty-turn-activity.d.ts +6 -5
  48. package/dist/pty-turn-activity.js +6 -5
  49. package/dist/retention.d.ts +29 -0
  50. package/dist/retention.js +142 -0
  51. package/dist/server-ai-team-routes.d.ts +2 -1
  52. package/dist/server-ai-team-routes.js +27 -9
  53. package/dist/server-decision-routes.d.ts +10 -0
  54. package/dist/server-decision-routes.js +52 -0
  55. package/dist/server-employee-routes.js +9 -1
  56. package/dist/server-session-routes.d.ts +2 -2
  57. package/dist/server-session-routes.js +97 -1
  58. package/dist/server-settings-routes.js +10 -36
  59. package/dist/server-update-routes.js +2 -1
  60. package/dist/server-workspace-routes.js +8 -0
  61. package/dist/server.js +37 -5
  62. package/dist/session-completion-state.d.ts +26 -0
  63. package/dist/session-completion-state.js +30 -0
  64. package/dist/session-completion.d.ts +12 -0
  65. package/dist/session-completion.js +62 -0
  66. package/dist/session-provider.d.ts +1 -15
  67. package/dist/session-provider.js +4 -54
  68. package/dist/session-registry.d.ts +5 -0
  69. package/dist/session-registry.js +19 -1
  70. package/dist/session-transport.js +2 -0
  71. package/dist/storage.d.ts +12 -0
  72. package/dist/storage.js +119 -16
  73. package/dist/structured-client-protocol.js +36 -1
  74. package/dist/structured-provider-common.js +3 -3
  75. package/dist/structured-session-manager.d.ts +13 -1
  76. package/dist/structured-session-manager.js +196 -27
  77. package/dist/system-employee.js +1 -0
  78. package/dist/task-types.d.ts +4 -2
  79. package/dist/tool-preview.d.ts +8 -0
  80. package/dist/tool-preview.js +158 -0
  81. package/dist/types.d.ts +35 -2
  82. package/dist/web-ui/content/ai-teams.js +13 -13
  83. package/dist/web-ui/content/scripts.js +66 -64
  84. package/dist/web-ui/content/styles.css +1 -1
  85. package/dist/web-ui/content/tailwind.css +3 -0
  86. package/dist/web-ui/embedded-assets.js +3 -3
  87. package/dist/web-ui/provider-identity.d.ts +7 -13
  88. package/dist/web-ui/provider-identity.js +8 -71
  89. package/package.json +3 -2
  90. package/skills/wand-decision/SKILL.md +61 -0
  91. package/skills/wand-decision/scripts/decide.mjs +16 -0
@@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto";
3
3
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { freezeTeamEmployees, projectTeamEmployees, teamMemberEmployee } from "./ai-team-employee-binding.js";
7
+ import { buildAiTeamDeliverySummary } from "./ai-team-delivery.js";
6
8
  import { dispatchAgentForTask, resolveTaskDispatchTarget, sendToAgentSession, stopAgentSession, } from "./agent-dispatch.js";
7
9
  import { AI_TEAM_REPORT_DIR, aiTeamChatHistoryPath, aiTeamHandoffPath, aiTeamReportPath, buildLeaderFollowupPrompt, buildLeaderFormatRetryPrompt, buildAiTeamObjective, buildLeaderKickoffPrompt, buildMemberPrompt, leaderOf, parseLeaderDecision, renderChatHistoryFile, renderHandoffFile, } from "./ai-team-prompts.js";
8
10
  import { AI_TEAM_STARTUP_WINDOW_MS, classifyCandidateFailure, classifyStartupFailure, isDegradableWorkFailure, isModelUnknownBeforeDispatch, } from "./ai-team-availability.js";
@@ -39,10 +41,11 @@ export function createAiTeamSessionOps(deps) {
39
41
  const { session } = await dispatchAgentForTask(deps, input);
40
42
  return session.id;
41
43
  },
42
- send: (sessionId, text) => sendToAgentSession(deps, sessionId, text),
44
+ send: (sessionId, text, teamRequestStarted) => sendToAgentSession(deps, sessionId, text, teamRequestStarted),
43
45
  stop: (sessionId) => stopAgentSession(deps, sessionId),
44
46
  snapshot: (sessionId) => deps.sessions.getLatest(sessionId),
45
47
  ownerOf: (sessionId) => deps.sessions.ownerOf(sessionId),
48
+ consumeUnacceptedStartup: (sessionId, requestId) => deps.structured?.consumeUnacceptedTeamStartup(sessionId, requestId) ?? false,
46
49
  };
47
50
  }
48
51
  export function createAiTeamChatOps(deps) {
@@ -101,7 +104,8 @@ export function displayAiTeam(snapshot, current) {
101
104
  updatedAt: current.updatedAt,
102
105
  members: snapshot.members.map((member) => {
103
106
  const live = byId.get(member.id);
104
- return live ? { ...member, name: live.name, avatar: live.avatar } : member;
107
+ return live && !member.employeeId && !live.employeeId
108
+ ? { ...member, name: live.name, avatar: live.avatar } : member;
105
109
  }),
106
110
  };
107
111
  }
@@ -346,6 +350,7 @@ export class AiTeamRunner {
346
350
  recheckTimers = new Map();
347
351
  /** 已经确定「不降级、按既有路径记 failed」的步骤,防止 finishStep 再判一次形成回环。 */
348
352
  noDegrade = new Set();
353
+ teamRequestIds = new Map();
349
354
  sweepTimer = null;
350
355
  /** 脱敏参照目录只算一次:os.homedir() 读 env,configDir 随 storage 定,运行期不变。 */
351
356
  redactionDirsValue = null;
@@ -386,9 +391,10 @@ export class AiTeamRunner {
386
391
  }
387
392
  // ── 公开操作 ──
388
393
  async start(input) {
389
- const team = this.storage.getAiTeam(input.teamId);
390
- if (!team)
394
+ const definition = this.storage.getAiTeam(input.teamId);
395
+ if (!definition)
391
396
  throw new Error("团队不存在。");
397
+ const team = freezeTeamEmployees(definition, this.storage);
392
398
  const task = this.storage.getWandTask(input.taskId);
393
399
  if (!task)
394
400
  throw new Error("任务不存在。");
@@ -594,14 +600,16 @@ export class AiTeamRunner {
594
600
  const task = (binding?.workspaceTaskId
595
601
  ? this.storage.getWandTaskByWorkspaceTaskId(binding.workspaceTaskId) : null)
596
602
  ?? this.storage.getWandTask(run.taskId);
603
+ const displayTeam = projectTeamEmployees(displayAiTeam(run.team, currentTeam), this.storage);
597
604
  return {
598
605
  run,
606
+ delivery: buildAiTeamDeliverySummary({ run, steps, displayTeam, relayTurns: messages, memberStates }),
599
607
  chatTitle: aiTeamChatTitle(task?.title),
600
608
  chatTitleUpdatedAt: task?.updatedAt ?? "",
601
609
  steps,
602
610
  memberStates,
603
611
  // 别把展示字段写回 run.team:运行快照里的执行候选/职责由 runner 独立管理。
604
- displayTeam: displayAiTeam(run.team, currentTeam),
612
+ displayTeam,
605
613
  chatTurns: messages.slice(-AI_TEAM_DETAIL_CHAT_TURNS),
606
614
  };
607
615
  }
@@ -788,6 +796,7 @@ export class AiTeamRunner {
788
796
  this.liveTimers.clear();
789
797
  this.lastLiveKey.clear();
790
798
  this.noDegrade.clear();
799
+ this.teamRequestIds.clear();
791
800
  }
792
801
  /** 等当前排队的所有操作跑完(测试用)。 */
793
802
  async idle() {
@@ -952,6 +961,10 @@ export class AiTeamRunner {
952
961
  // 按真实角色身份发出正常对话气泡并展示完整报错,而不是次级 notice 小字。
953
962
  const leader = leaderOf(run.team);
954
963
  if (outcome.kind === "failed" && outcome.sessionError) {
964
+ // Leaders keep their existing wait-for-user rule, not worker candidate fallback.
965
+ const requestId = this.teamRequestIds.get(step.id);
966
+ if (step.sessionId && requestId)
967
+ this.ops.consumeUnacceptedStartup?.(step.sessionId, requestId);
955
968
  this.saveStep({ ...step, status: "failed", report: outcome.text, endedAt });
956
969
  this.postTurn(run, {
957
970
  role: "assistant",
@@ -1288,8 +1301,12 @@ export class AiTeamRunner {
1288
1301
  throw new Error("成员不存在");
1289
1302
  if (!agent)
1290
1303
  throw new Error("成员没有配置可用的 CLI 候选");
1304
+ if (member.employeeId && !teamMemberEmployee(member))
1305
+ throw new Error("员工执行快照缺失,不能重绑定旧运行。");
1291
1306
  if (reusable) {
1292
- await this.ops.send(reusable, prompt.message);
1307
+ await this.ops.send(reusable, prompt.message, (_sessionId, requestId) => {
1308
+ this.teamRequestIds.set(step.id, requestId);
1309
+ });
1293
1310
  }
1294
1311
  else {
1295
1312
  const task = this.storage.getWandTask(run.taskId);
@@ -1301,9 +1318,15 @@ export class AiTeamRunner {
1301
1318
  prompt: prompt.message,
1302
1319
  automationId: `ai-team:${run.id}`,
1303
1320
  systemPrompt: prompt.system,
1321
+ employee: teamMemberEmployee(member),
1322
+ employeeCandidateIndex: member.employeeId ? info.usedCandidate : undefined,
1323
+ teamRequestStarted: (_sessionId, requestId) => { this.teamRequestIds.set(step.id, requestId); },
1304
1324
  });
1305
1325
  running = { ...running, sessionId };
1306
1326
  this.saveStep(running);
1327
+ // Immediate async spawn failure can precede open() returning and the session→step link.
1328
+ if (this.ops.snapshot(sessionId)?.status === "failed")
1329
+ this.ingest({ type: "status", sessionId });
1307
1330
  }
1308
1331
  }
1309
1332
  catch (error) {
@@ -1315,6 +1338,7 @@ export class AiTeamRunner {
1315
1338
  this.disableHostKind(run.id, agent.kind);
1316
1339
  }
1317
1340
  else if (agent && step.kind === "work" && kind === "spawn-missing"
1341
+ && (!member?.employeeId || reusable === null)
1318
1342
  && !existsSync(path.join(run.cwd, step.reportPath)) && !this.noDegrade.has(step.id)) {
1319
1343
  // §3.4 触发点 1:会话根本没起来,ENOENT 对 structured / pty 同样可靠(修正 B4 排除的是
1320
1344
  // PTY 的异步分类)。传 `running` 而不是入参 `step`:复用会话时 sessionId 只在 running 上。
@@ -1432,6 +1456,15 @@ export class AiTeamRunner {
1432
1456
  const member = this.member(run.team, step.memberId);
1433
1457
  if (!member)
1434
1458
  return null;
1459
+ const requestId = this.teamRequestIds.get(step.id);
1460
+ const unaccepted = !!(step.sessionId && requestId
1461
+ && this.ops.consumeUnacceptedStartup?.(step.sessionId, requestId));
1462
+ // Bound employees may retry only this request's explicit unaccepted fact, never a text/time guess.
1463
+ if (member.employeeId) {
1464
+ if (!unaccepted || existsSync(path.join(run.cwd, step.reportPath)))
1465
+ return null;
1466
+ return { kind: "spawn-missing", reason: outcomeText };
1467
+ }
1435
1468
  const info = this.infoOf(step);
1436
1469
  const candidates = memberAgents(member);
1437
1470
  const agent = candidates[info.usedCandidate];
@@ -1539,15 +1572,19 @@ export class AiTeamRunner {
1539
1572
  }
1540
1573
  /**
1541
1574
  * 运行停下来等用户时,用户可能刚在团队页换了成员的 CLI / 模型(比如额度用尽换一个)。
1542
- * 接着跑之前换成最新配置;成员被删了或没有负责人时保留原快照。
1575
+ * 独立 CLI 团队沿用旧刷新规则;带员工链接的运行全程保留新开工时的执行快照。
1543
1576
  */
1544
1577
  refreshTeam(run) {
1578
+ if (run.team.members.some((member) => member.employeeId))
1579
+ return;
1545
1580
  const latest = this.storage.getAiTeam(run.teamId);
1546
1581
  if (!latest || !latest.members.some((member) => member.isLeader))
1547
1582
  return;
1548
1583
  const used = new Set(this.storage.listAiTeamSteps(run.id).map((step) => step.memberId));
1549
1584
  if ([...used].some((id) => !latest.members.some((member) => member.id === id)))
1550
1585
  return;
1586
+ if (latest.members.some((member) => member.employeeId))
1587
+ return;
1551
1588
  run.team = latest;
1552
1589
  }
1553
1590
  /**
@@ -1563,6 +1600,8 @@ export class AiTeamRunner {
1563
1600
  const snapshot = this.ops.snapshot(step.sessionId);
1564
1601
  if (!snapshot || isSessionGone(snapshot))
1565
1602
  return null;
1603
+ if ((snapshot.employeeId ?? undefined) !== member.employeeId)
1604
+ return null;
1566
1605
  const candidates = memberAgents(member);
1567
1606
  const previous = candidates[this.infoOf(step).usedCandidate] ?? candidates[0];
1568
1607
  if (!previous || (agent && agentKey(previous) !== agentKey(agent)))
@@ -1722,6 +1761,12 @@ export class AiTeamRunner {
1722
1761
  this.notify(run);
1723
1762
  }
1724
1763
  saveStep(step) {
1764
+ if (step.status !== "running" && step.status !== "queued") {
1765
+ const requestId = this.teamRequestIds.get(step.id);
1766
+ if (step.sessionId && requestId)
1767
+ this.ops.consumeUnacceptedStartup?.(step.sessionId, requestId);
1768
+ this.teamRequestIds.delete(step.id);
1769
+ }
1725
1770
  this.storage.saveAiTeamStep(step);
1726
1771
  }
1727
1772
  notify(run) {
@@ -1,4 +1,5 @@
1
1
  import type { AgentActivityState } from "./mission-types.js";
2
+ import type { AiTeamDeliverySummary } from "./ai-team-delivery-types.js";
2
3
  import type { WandTaskAgent } from "./task-types.js";
3
4
  import type { ConversationTurn } from "./types.js";
4
5
  export declare const AI_TEAM_MIN_MEMBERS = 2;
@@ -15,6 +16,8 @@ export declare function isTeamMemberRole(value: unknown): value is TeamMemberRol
15
16
  export interface AiTeamMember {
16
17
  /** 团队内唯一,形如 "m_xxxxxxxx";Leader 派工时原样引用。 */
17
18
  id: string;
19
+ /** 通讯录身份链接;缺省是独立 CLI 成员,不按名字猜测。 */
20
+ employeeId?: string;
18
21
  name: string;
19
22
  /** 职责说明,原样写进提示词。 */
20
23
  duty: string;
@@ -62,6 +65,8 @@ export interface SiliconEmployee {
62
65
  prompt: string;
63
66
  avatar: string;
64
67
  agents: WandTaskAgent[];
68
+ /** 内置标签按 systemKey 固定;普通员工的标签由用户维护。旧客户端可不传。 */
69
+ tags?: string[];
65
70
  /**
66
71
  * 非空 = Wand 内置员工(如 "wand-ops" 系统运维):名字/职责/人设/头像锁定,
67
72
  * 不可归档、不可删除,只有执行候选由用户维护。
@@ -87,14 +92,21 @@ export declare const SYSTEM_EMPLOYEE_KEY = "wand-ops";
87
92
  export declare const SYSTEM_EMPLOYEE_ID = "e_wand_ops";
88
93
  export declare const SYSTEM_EMPLOYEE_NAME = "\u52E4\u52B3\u7684\u521D\u4E8C";
89
94
  /** 员工列表 / 设置页上的标识。 */
90
- export declare const SYSTEM_EMPLOYEE_TAG = "\u7CFB\u7EDF\u8FD0\u7EF4";
95
+ export declare const SYSTEM_EMPLOYEE_TAG = "\u7CFB\u7EDF\u7528\u6237";
91
96
  export declare function isSystemSiliconEmployee(employee: Pick<SiliconEmployee, "systemKey"> | null | undefined): boolean;
92
97
  export declare const DEFAULT_EMPLOYEE_KEY = "wand-default";
93
98
  export declare const DEFAULT_EMPLOYEE_ID = "e_wand_default";
94
- export declare const DEFAULT_EMPLOYEE_NAME = "\u9ED8\u5951\u7684\u521D\u4E00";
95
- export declare const DEFAULT_EMPLOYEE_TAG = "\u9ED8\u8BA4\u4F19\u4F34";
99
+ export declare const DEFAULT_EMPLOYEE_NAME = "\u8D5B\u535A\u864E\u599E";
100
+ export declare const DEFAULT_EMPLOYEE_TAG = "\u9ED8\u8BA4\u7528\u6237";
96
101
  export declare function isDefaultSiliconEmployee(employee: Pick<SiliconEmployee, "systemKey"> | null | undefined): boolean;
97
102
  export declare function isBuiltinSiliconEmployee(employee: Pick<SiliconEmployee, "systemKey"> | null | undefined): boolean;
103
+ export declare const SILICON_EMPLOYEE_MAX_TAGS = 8;
104
+ export declare const SILICON_EMPLOYEE_TAG_MAX_CHARS = 20;
105
+ /** 内置标识只由真实员工身份投影,不能用自定义标签冒充。 */
106
+ export declare function siliconEmployeeTags(employee: Pick<SiliconEmployee, "systemKey" | "tags">): string[];
107
+ /** API / storage / Web 共用:去空白、去重,保留用户顺序,不静默截断。 */
108
+ export declare function parseSiliconEmployeeTags(value: unknown): string[];
109
+ export declare function parseSiliconEmployeeTagInput(value: string): string[];
98
110
  export interface AiTeam {
99
111
  id: string;
100
112
  name: string;
@@ -236,6 +248,8 @@ export interface AiTeamLiveUpdate {
236
248
  }
237
249
  export interface AiTeamRunDetail {
238
250
  run: AiTeamRun;
251
+ /** One run's result/files/current handoffs; absent on older servers, no side effects. */
252
+ delivery?: AiTeamDeliverySummary;
239
253
  /** 仅供展示的群名,读取当前任务标题,不以团队名代替。 */
240
254
  chatTitle?: string;
241
255
  /** 任务标题的版本;任务改名不改变 run.updatedAt。 */
@@ -34,21 +34,61 @@ export const SYSTEM_EMPLOYEE_KEY = "wand-ops";
34
34
  export const SYSTEM_EMPLOYEE_ID = "e_wand_ops";
35
35
  export const SYSTEM_EMPLOYEE_NAME = "勤劳的初二";
36
36
  /** 员工列表 / 设置页上的标识。 */
37
- export const SYSTEM_EMPLOYEE_TAG = "系统运维";
37
+ export const SYSTEM_EMPLOYEE_TAG = "系统用户";
38
38
  export function isSystemSiliconEmployee(employee) {
39
39
  return employee?.systemKey === SYSTEM_EMPLOYEE_KEY;
40
40
  }
41
41
  // 默认任务角色与系统运维分开;CLI 是执行器,角色不替换用户选定的工具/权限。
42
42
  export const DEFAULT_EMPLOYEE_KEY = "wand-default";
43
43
  export const DEFAULT_EMPLOYEE_ID = "e_wand_default";
44
- export const DEFAULT_EMPLOYEE_NAME = "默契的初一";
45
- export const DEFAULT_EMPLOYEE_TAG = "默认伙伴";
44
+ export const DEFAULT_EMPLOYEE_NAME = "赛博虎妞";
45
+ export const DEFAULT_EMPLOYEE_TAG = "默认用户";
46
46
  export function isDefaultSiliconEmployee(employee) {
47
47
  return employee?.systemKey === DEFAULT_EMPLOYEE_KEY;
48
48
  }
49
49
  export function isBuiltinSiliconEmployee(employee) {
50
50
  return isSystemSiliconEmployee(employee) || isDefaultSiliconEmployee(employee);
51
51
  }
52
+ export const SILICON_EMPLOYEE_MAX_TAGS = 8;
53
+ export const SILICON_EMPLOYEE_TAG_MAX_CHARS = 20;
54
+ /** 内置标识只由真实员工身份投影,不能用自定义标签冒充。 */
55
+ export function siliconEmployeeTags(employee) {
56
+ if (isSystemSiliconEmployee(employee))
57
+ return [SYSTEM_EMPLOYEE_TAG];
58
+ if (isDefaultSiliconEmployee(employee))
59
+ return [DEFAULT_EMPLOYEE_TAG];
60
+ return employee.tags ?? [];
61
+ }
62
+ /** API / storage / Web 共用:去空白、去重,保留用户顺序,不静默截断。 */
63
+ export function parseSiliconEmployeeTags(value) {
64
+ if (!Array.isArray(value))
65
+ throw new Error("员工标签必须是数组。");
66
+ const tags = [];
67
+ for (const raw of value) {
68
+ if (typeof raw !== "string")
69
+ throw new Error("员工标签必须是文字。");
70
+ const tag = raw.trim();
71
+ if (!tag)
72
+ continue;
73
+ if (tag.length > SILICON_EMPLOYEE_TAG_MAX_CHARS) {
74
+ throw new Error(`每个员工标签不能超过 ${SILICON_EMPLOYEE_TAG_MAX_CHARS} 个字符。`);
75
+ }
76
+ if (/[,,、\x00-\x1f\x7f]/.test(tag))
77
+ throw new Error("员工标签不能包含分隔符或控制字符。");
78
+ if (tag === SYSTEM_EMPLOYEE_TAG || tag === DEFAULT_EMPLOYEE_TAG) {
79
+ throw new Error(`「${tag}」是内置标签,不可自定义。`);
80
+ }
81
+ if (!tags.includes(tag))
82
+ tags.push(tag);
83
+ }
84
+ if (tags.length > SILICON_EMPLOYEE_MAX_TAGS) {
85
+ throw new Error(`最多 ${SILICON_EMPLOYEE_MAX_TAGS} 个员工标签。`);
86
+ }
87
+ return tags;
88
+ }
89
+ export function parseSiliconEmployeeTagInput(value) {
90
+ return parseSiliconEmployeeTags(value.split(/[,,、\n]+/));
91
+ }
52
92
  export const AI_TEAM_ACTIVE_RUN_STATUSES = ["running", "awaiting_approval", "waiting_user"];
53
93
  /** 终态:不会再有 live 变化,runner 据此清掉该 run 的推送指纹,避免长跑服务按 run 数攒字符串。 */
54
94
  export const AI_TEAM_TERMINAL_RUN_STATUSES = ["done", "failed", "stopped"];
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "b95a1ab17fe77df59a9060c24ee37e5799e38e68",
3
- "builtAt": "2026-10-01T08:47:47.415Z",
4
- "version": "4.82.0",
2
+ "commit": "97d7ae95626425a23420a4f9bfabbf733d5ae73a",
3
+ "builtAt": "2026-10-03T02:50:17.324Z",
4
+ "version": "4.83.1",
5
5
  "channel": "stable"
6
6
  }
package/dist/cli.js CHANGED
@@ -179,6 +179,39 @@ async function main() {
179
179
  await runAgentCliCommand(command, args, configPath);
180
180
  break;
181
181
  }
182
+ case "decision:configure": {
183
+ const { isAbsolute, join } = await import("node:path");
184
+ const disabling = args.includes("--disable");
185
+ const pythonPath = readFlagValue(args, "--python") ?? "";
186
+ const modelPath = readFlagValue(args, "--model") ?? "";
187
+ if (!disabling && (!isAbsolute(pythonPath) || !isAbsolute(modelPath)
188
+ || !existsSync(pythonPath) || !existsSync(join(modelPath, "model.safetensors")))) {
189
+ throw new Error("用法:wand decision:configure --python <Python绝对路径> --model <已下载模型目录>;或 --disable。");
190
+ }
191
+ const { config } = await loadConfigForCli(configPath);
192
+ config.localDecision = disabling
193
+ ? { enabled: false, pythonPath: config.localDecision?.pythonPath ?? "", modelPath: config.localDecision?.modelPath ?? "" }
194
+ : { enabled: true, pythonPath, modelPath };
195
+ await saveConfig(configPath, config);
196
+ process.stdout.write("本地决策配置已保存,重启 Wand 服务后生效。未修改系统 Python,也未下载模型。\n");
197
+ break;
198
+ }
199
+ case "decision:skills": {
200
+ const { dirname } = await import("node:path");
201
+ const { installDecisionSkill } = await import("./decision-skill.js");
202
+ const { SESSION_PROVIDERS } = await import("./provider-catalog.js");
203
+ const selected = readFlagValue(args, "--providers")?.split(",") ?? [...SESSION_PROVIDERS];
204
+ if (selected.some((provider) => !SESSION_PROVIDERS.includes(provider)))
205
+ throw new Error("未知CLI技能目标。");
206
+ const result = await installDecisionSkill(dirname(configPath), selected);
207
+ process.stdout.write(`${JSON.stringify(result)}\n`);
208
+ break;
209
+ }
210
+ case "decide": {
211
+ const { runDecisionCli } = await import("./decision-client.js");
212
+ await runDecisionCli(args.slice(1));
213
+ break;
214
+ }
182
215
  case "knowledge:remember":
183
216
  case "knowledge:search":
184
217
  case "knowledge:forget": {
@@ -234,6 +267,9 @@ Agent runtime:
234
267
  wand mission:diff <mission-id> <attempt-id>
235
268
  wand mission:review <mission-id> <attempt-id> --file <path> --body <text> [--line N]
236
269
  wand mission:review:send <mission-id> <attempt-id>
270
+ wand decide --stdin | --status Use the session-bound local decision service
271
+ wand decision:configure --python <path> --model <dir> | --disable
272
+ wand decision:skills [--providers pi,claude,...] Install managed user-level skills
237
273
  wand knowledge:remember <text> | --stdin
238
274
  wand knowledge:search [query]
239
275
  wand knowledge:forget <entry-id>
package/dist/config.d.ts CHANGED
@@ -9,6 +9,20 @@ import type { WandStorage } from "./storage.js";
9
9
  */
10
10
  export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultTaskWorktree", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "defaultGeminiModel", "commitCli", "commitModel", "systemAiCli", "systemAiModel", "defaultThinkingEffort", "language", "cardDefaults", "inheritEnv"];
11
11
  export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
12
+ /**
13
+ * 每个 provider 的默认模型偏好字段。新增 provider 时只改这一处:
14
+ * 偏好读写、环境归一、默认模型投影都从这张表派生。
15
+ */
16
+ export declare const PROVIDER_MODEL_PREFERENCE_FIELDS: {
17
+ readonly claude: "defaultModel";
18
+ readonly codex: "defaultCodexModel";
19
+ readonly opencode: "defaultOpenCodeModel";
20
+ readonly grok: "defaultGrokModel";
21
+ readonly qoder: "defaultQoderModel";
22
+ readonly pi: "defaultPiModel";
23
+ readonly gemini: "defaultGeminiModel";
24
+ };
25
+ export type ProviderModelField = (typeof PROVIDER_MODEL_PREFERENCE_FIELDS)[SessionProvider];
12
26
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
13
27
  /**
14
28
  * The user's login shell from passwd, then $SHELL, then a platform default.
@@ -39,13 +53,7 @@ export declare function applyStoragePreferences(config: WandConfig, storage: Wan
39
53
  /** Write a single preference value to DB and (in-place) update the live config object. */
40
54
  export declare function writePreferenceToStorage(config: WandConfig, storage: WandStorage, key: PreferenceKey, value: unknown): void;
41
55
  export declare function isExecutionMode(value: unknown): value is ExecutionMode;
42
- export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultGeminiModel">): {
43
- claude: string;
44
- codex: string;
45
- opencode: string;
46
- grok: string;
47
- qoder: string;
48
- pi: string;
49
- gemini: string;
50
- };
51
- export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultGeminiModel">, provider: SessionProvider | undefined): string;
56
+ export declare function getProviderDefaultModels(config: Pick<WandConfig, ProviderModelField>): Record<SessionProvider, string>;
57
+ export declare function getDefaultModelForProvider(config: Pick<WandConfig, ProviderModelField>, provider: SessionProvider | undefined): string;
58
+ /** 把按 provider 归档的默认模型投影回各自的 `default*Model` 字段(客户端兼容字段)。 */
59
+ export declare function providerModelFields(models: Record<SessionProvider, string>): Pick<WandConfig, ProviderModelField>;
package/dist/config.js CHANGED
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { isRunningAsRoot } from "./env-utils.js";
8
- import { isSessionProvider } from "./session-provider.js";
8
+ import { isSessionProvider, SESSION_PROVIDERS } from "./session-provider.js";
9
9
  import { isThinkingEffort } from "./structured-provider-common.js";
10
10
  const DEFAULT_CONFIG_DIR = ".wand";
11
11
  const DEFAULT_CONFIG_FILE = "config.json";
@@ -38,6 +38,24 @@ export const PREFERENCE_KEYS = [
38
38
  "cardDefaults",
39
39
  "inheritEnv",
40
40
  ];
41
+ /**
42
+ * 每个 provider 的默认模型偏好字段。新增 provider 时只改这一处:
43
+ * 偏好读写、环境归一、默认模型投影都从这张表派生。
44
+ */
45
+ export const PROVIDER_MODEL_PREFERENCE_FIELDS = {
46
+ claude: "defaultModel",
47
+ codex: "defaultCodexModel",
48
+ opencode: "defaultOpenCodeModel",
49
+ grok: "defaultGrokModel",
50
+ qoder: "defaultQoderModel",
51
+ pi: "defaultPiModel",
52
+ gemini: "defaultGeminiModel",
53
+ };
54
+ const PROVIDER_MODEL_FIELDS = Object.values(PROVIDER_MODEL_PREFERENCE_FIELDS);
55
+ const PROVIDER_MODEL_FIELD_SET = new Set(PROVIDER_MODEL_FIELDS);
56
+ function isProviderModelField(key) {
57
+ return PROVIDER_MODEL_FIELD_SET.has(key);
58
+ }
41
59
  const PREFERENCE_KEY_SET = new Set(PREFERENCE_KEYS);
42
60
  function preferenceStorageKey(key) {
43
61
  return `pref:${key}`;
@@ -92,6 +110,7 @@ export const defaultConfig = () => ({
92
110
  ios: defaultIosIpaConfig(),
93
111
  render: defaultRenderConfig(),
94
112
  structured: { processHost: "legacy" },
113
+ localDecision: { enabled: false, pythonPath: "", modelPath: "" },
95
114
  cardDefaults: defaultCardExpandDefaults(),
96
115
  defaultModel: "",
97
116
  defaultCodexModel: "",
@@ -332,40 +351,13 @@ export function applyStoragePreferences(config, storage) {
332
351
  if (typeof v === "string" && v.trim())
333
352
  config.defaultCwd = v;
334
353
  }
335
- if (storage.hasPreference(preferenceStorageKey("defaultModel"))) {
336
- const v = storage.getPreference(preferenceStorageKey("defaultModel"), defaults.defaultModel ?? "");
337
- if (typeof v === "string")
338
- config.defaultModel = v.trim();
339
- }
340
- if (storage.hasPreference(preferenceStorageKey("defaultCodexModel"))) {
341
- const v = storage.getPreference(preferenceStorageKey("defaultCodexModel"), defaults.defaultCodexModel ?? "");
342
- if (typeof v === "string")
343
- config.defaultCodexModel = v.trim();
344
- }
345
- if (storage.hasPreference(preferenceStorageKey("defaultOpenCodeModel"))) {
346
- const v = storage.getPreference(preferenceStorageKey("defaultOpenCodeModel"), defaults.defaultOpenCodeModel ?? "");
347
- if (typeof v === "string")
348
- config.defaultOpenCodeModel = v.trim();
349
- }
350
- if (storage.hasPreference(preferenceStorageKey("defaultGrokModel"))) {
351
- const v = storage.getPreference(preferenceStorageKey("defaultGrokModel"), defaults.defaultGrokModel ?? "");
352
- if (typeof v === "string")
353
- config.defaultGrokModel = v.trim();
354
- }
355
- if (storage.hasPreference(preferenceStorageKey("defaultQoderModel"))) {
356
- const v = storage.getPreference(preferenceStorageKey("defaultQoderModel"), defaults.defaultQoderModel ?? "");
357
- if (typeof v === "string")
358
- config.defaultQoderModel = v.trim();
359
- }
360
- if (storage.hasPreference(preferenceStorageKey("defaultPiModel"))) {
361
- const v = storage.getPreference(preferenceStorageKey("defaultPiModel"), defaults.defaultPiModel ?? "");
362
- if (typeof v === "string")
363
- config.defaultPiModel = v.trim();
364
- }
365
- if (storage.hasPreference(preferenceStorageKey("defaultGeminiModel"))) {
366
- const v = storage.getPreference(preferenceStorageKey("defaultGeminiModel"), defaults.defaultGeminiModel ?? "");
354
+ for (const field of PROVIDER_MODEL_FIELDS) {
355
+ const dbKey = preferenceStorageKey(field);
356
+ if (!storage.hasPreference(dbKey))
357
+ continue;
358
+ const v = storage.getPreference(dbKey, defaults[field] ?? "");
367
359
  if (typeof v === "string")
368
- config.defaultGeminiModel = v.trim();
360
+ config[field] = v.trim();
369
361
  }
370
362
  if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
371
363
  const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
@@ -410,6 +402,14 @@ export function applyStoragePreferences(config, storage) {
410
402
  /** Write a single preference value to DB and (in-place) update the live config object. */
411
403
  export function writePreferenceToStorage(config, storage, key, value) {
412
404
  const dbKey = preferenceStorageKey(key);
405
+ // Default model fields share one shape (trimmed string) across every provider;
406
+ // handling them before the switch keeps the switch free of seven copies.
407
+ if (isProviderModelField(key)) {
408
+ const v = typeof value === "string" ? value.trim() : "";
409
+ storage.setPreference(dbKey, v);
410
+ config[key] = v;
411
+ return;
412
+ }
413
413
  switch (key) {
414
414
  case "defaultProvider": {
415
415
  if (!isSessionProvider(value))
@@ -445,48 +445,6 @@ export function writePreferenceToStorage(config, storage, key, value) {
445
445
  config.defaultCwd = v || defaultConfig().defaultCwd;
446
446
  break;
447
447
  }
448
- case "defaultModel": {
449
- const v = typeof value === "string" ? value.trim() : "";
450
- storage.setPreference(dbKey, v);
451
- config.defaultModel = v;
452
- break;
453
- }
454
- case "defaultCodexModel": {
455
- const v = typeof value === "string" ? value.trim() : "";
456
- storage.setPreference(dbKey, v);
457
- config.defaultCodexModel = v;
458
- break;
459
- }
460
- case "defaultOpenCodeModel": {
461
- const v = typeof value === "string" ? value.trim() : "";
462
- storage.setPreference(dbKey, v);
463
- config.defaultOpenCodeModel = v;
464
- break;
465
- }
466
- case "defaultGrokModel": {
467
- const v = typeof value === "string" ? value.trim() : "";
468
- storage.setPreference(dbKey, v);
469
- config.defaultGrokModel = v;
470
- break;
471
- }
472
- case "defaultQoderModel": {
473
- const v = typeof value === "string" ? value.trim() : "";
474
- storage.setPreference(dbKey, v);
475
- config.defaultQoderModel = v;
476
- break;
477
- }
478
- case "defaultPiModel": {
479
- const v = typeof value === "string" ? value.trim() : "";
480
- storage.setPreference(dbKey, v);
481
- config.defaultPiModel = v;
482
- break;
483
- }
484
- case "defaultGeminiModel": {
485
- const v = typeof value === "string" ? value.trim() : "";
486
- storage.setPreference(dbKey, v);
487
- config.defaultGeminiModel = v;
488
- break;
489
- }
490
448
  case "commitCli": {
491
449
  if (value !== "claude" && value !== "codex" && value !== "opencode")
492
450
  throw new Error(`无效 commit CLI: ${value}`);
@@ -734,17 +692,16 @@ function mergeWithDefaults(input) {
734
692
  ios: normalizeIosIpaConfig(input.ios) ?? defaults.ios,
735
693
  render: normalizeRenderConfig(input.render) ?? defaults.render,
736
694
  structured: { processHost: input.structured?.processHost === "rust" ? "rust" : "legacy" },
695
+ localDecision: {
696
+ enabled: input.localDecision?.enabled === true,
697
+ pythonPath: typeof input.localDecision?.pythonPath === "string" ? input.localDecision.pythonPath.trim() : "",
698
+ modelPath: typeof input.localDecision?.modelPath === "string" ? input.localDecision.modelPath.trim() : "",
699
+ },
737
700
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
738
701
  defaultProvider: isSessionProvider(input.defaultProvider) ? input.defaultProvider : "claude",
739
702
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
740
703
  defaultTaskWorktree: typeof input.defaultTaskWorktree === "boolean" ? input.defaultTaskWorktree : defaults.defaultTaskWorktree,
741
- defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
742
- defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
743
- defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
744
- defaultGrokModel: typeof input.defaultGrokModel === "string" ? input.defaultGrokModel.trim() : defaults.defaultGrokModel,
745
- defaultQoderModel: typeof input.defaultQoderModel === "string" ? input.defaultQoderModel.trim() : defaults.defaultQoderModel,
746
- defaultPiModel: typeof input.defaultPiModel === "string" ? input.defaultPiModel.trim() : defaults.defaultPiModel,
747
- defaultGeminiModel: typeof input.defaultGeminiModel === "string" ? input.defaultGeminiModel.trim() : defaults.defaultGeminiModel,
704
+ ...providerModelOverrides(input, defaults),
748
705
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
749
706
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
750
707
  systemAiCli: isSessionProvider(input.systemAiCli) ? input.systemAiCli : undefined,
@@ -764,32 +721,33 @@ function configuredModelId(value) {
764
721
  const trimmed = (value ?? "").trim();
765
722
  return trimmed === "default" ? "" : trimmed;
766
723
  }
724
+ /** 归一每个 provider 的默认模型:字符串则 trim,否则回落到默认值。 */
725
+ function providerModelOverrides(input, defaults) {
726
+ const result = {};
727
+ for (const field of PROVIDER_MODEL_FIELDS) {
728
+ const value = input[field];
729
+ result[field] = typeof value === "string" ? value.trim() : defaults[field];
730
+ }
731
+ return result;
732
+ }
767
733
  export function getProviderDefaultModels(config) {
768
- return {
769
- claude: configuredModelId(config.defaultModel),
770
- codex: configuredModelId(config.defaultCodexModel),
771
- opencode: configuredModelId(config.defaultOpenCodeModel),
772
- grok: configuredModelId(config.defaultGrokModel),
773
- qoder: configuredModelId(config.defaultQoderModel),
774
- pi: configuredModelId(config.defaultPiModel),
775
- gemini: configuredModelId(config.defaultGeminiModel),
776
- };
734
+ const models = {};
735
+ for (const provider of SESSION_PROVIDERS) {
736
+ models[provider] = configuredModelId(config[PROVIDER_MODEL_PREFERENCE_FIELDS[provider]]);
737
+ }
738
+ return models;
777
739
  }
778
740
  export function getDefaultModelForProvider(config, provider) {
779
741
  const defaults = getProviderDefaultModels(config);
780
- if (provider === "codex")
781
- return defaults.codex;
782
- if (provider === "opencode")
783
- return defaults.opencode;
784
- if (provider === "grok")
785
- return defaults.grok;
786
- if (provider === "qoder")
787
- return defaults.qoder;
788
- if (provider === "pi")
789
- return defaults.pi;
790
- if (provider === "gemini")
791
- return defaults.gemini;
792
- return defaults.claude;
742
+ return provider ? defaults[provider] : defaults.claude;
743
+ }
744
+ /** 把按 provider 归档的默认模型投影回各自的 `default*Model` 字段(客户端兼容字段)。 */
745
+ export function providerModelFields(models) {
746
+ const result = {};
747
+ for (const provider of SESSION_PROVIDERS) {
748
+ result[PROVIDER_MODEL_PREFERENCE_FIELDS[provider]] = models[provider];
749
+ }
750
+ return result;
793
751
  }
794
752
  function normalizePresetCommand(command) {
795
753
  const trimmed = command.trim();
@@ -0,0 +1,4 @@
1
+ import { type DecisionRequest } from "./decision-types.js";
2
+ /** No admin/password lookup, redirects, or implicit remote fallback. Credentials stay in headers. */
3
+ export declare function callDecisionService(request: DecisionRequest | null, env?: NodeJS.ProcessEnv): Promise<unknown>;
4
+ export declare function runDecisionCli(args: string[]): Promise<void>;