@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
package/README.md CHANGED
@@ -85,10 +85,24 @@ Common options:
85
85
  | `host` | `127.0.0.1` | Listen address; use `0.0.0.0` for remote access |
86
86
  | `port` | `8443` | Listen port |
87
87
  | `https` | `false` | Enable HTTPS with an auto-generated self-signed certificate |
88
- | `password` | random | Login password |
89
- | `language` | `""` | Preferred Claude response language |
88
+ | `password` | Random on first `wand init`; `change-me` placeholder otherwise | Login password. Stored in SQLite, not `config.json` (`wand config:password` prints it). Running with the `change-me` placeholder logs a warning |
89
+ | `language` | `""` | Preferred Claude response language (a preference, stored in SQLite) |
90
90
  | `publicOrigin` | unset | Public URL clients should use, e.g. `https://home.example.com:8443`. Required when TLS terminates in an L4 proxy |
91
91
 
92
+ ### Optional local decisions (experimental)
93
+
94
+ On Apple Silicon, Wand can share one offline Laya-MLX worker across structured CLI sessions. It answers bounded `choice`/`score`/`noul` questions, not chat messages or execution/permission approvals. Install Python dependencies and a verified multilingual checkpoint separately, then configure their absolute paths:
95
+
96
+ ```bash
97
+ wand decision:configure --python /path/to/venv/bin/python --model /path/to/laya-multilingual-mlx
98
+ wand decision:skills # Install the managed wand-decision skill for all seven CLIs
99
+ # Restart the existing Wand service, then start a new structured CLI conversation.
100
+ ```
101
+
102
+ The skill's helper calls `wand decide --stdin` over the existing HTTP service. `/api/decisions/evaluate` and `/api/decisions/status` require authentication; runner capabilities authorize only inference, expire within6h, and are revoked when the run ends. No token is stored in the skill and no new public port is opened. Inputs that would be truncated are rejected. The worker unloads after5 minutes idle. `wand decision:configure --disable` disables it after a server restart. Standalone CLI/PTY sessions are not implicitly authorized.
103
+
104
+ Web and Android show inference calls as standalone local-decision cards, outside activity groups. Details start collapsed; click the header to expand the input and result in place. Explicit expansion choices survive refreshes and late results. Skill reads and status checks remain ordinary tools. This feature does not automatically install the model or change task assignment. Local evaluation found substantial routing errors, including high-confidence mistakes; use it as an optional aid, not a reliable arbiter. See [AGENTS.md](AGENTS.md) for deployment and safety boundaries.
105
+
92
106
  ### System Service
93
107
 
94
108
  The default service mode is system-wide: Linux writes `/etc/systemd/system/wand.service`, and macOS writes `/Library/LaunchDaemons/com.wand.web.plist`. Installing or removing the system service requires sudo.
@@ -138,8 +152,11 @@ git submodule update --init -- render-bin
138
152
  ```
139
153
 
140
154
  The full cross-platform test suite reads native source contracts, so initialize
141
- `android ios macos render` before running `npm test`. To preview removable compiler
142
- caches, run `npm run clean:build-cache`; add `-- --apply` to remove them.
155
+ `android ios macos render` before running `npm test`. The brand-consistency test
156
+ also reads generated iOS/macOS AppIcon PNGs, which on macOS come from
157
+ `swift <platform>/scripts/generate-icons.swift` (run `npm run sync:brand-assets`
158
+ first; CI does the same in `.github/workflows/ci.yml`). To preview removable
159
+ compiler caches, run `npm run clean:build-cache`; add `-- --apply` to remove them.
143
160
 
144
161
  Runtime data is stored under `~/.wand/`: `config.json`, `wand.db`, and `sessions/`.
145
162
 
@@ -246,10 +263,24 @@ wand config:set port 9443
246
263
  | `host` | `127.0.0.1` | 监听地址,`0.0.0.0` 允许远程访问 |
247
264
  | `port` | `8443` | 监听端口 |
248
265
  | `https` | `false` | 启用 HTTPS(自签证书自动生成) |
249
- | `password` | (随机生成) | 登录密码 |
250
- | `language` | `""` | Claude 回复语言偏好 |
266
+ | `password` | 首次 `wand init` 随机生成;否则为 `change-me` 占位 | 登录密码。存在 SQLite 而非 `config.json`(`wand config:password` 查看)。仍是 `change-me` 时会打警告 |
267
+ | `language` | `""` | Claude 回复语言偏好(偏好项,存 SQLite) |
251
268
  | `publicOrigin` | 未设置 | 客户端应使用的公开访问地址,如 `https://home.example.com:8443`。TLS 在 L4 反代终止时必填 |
252
269
 
270
+ ### 可选本地决策(实验性)
271
+
272
+ Apple Silicon 服务端可共享一份离线 Laya-MLX,为结构化 CLI 提供通用选择、评分和是非判断。它不是聊天模型,也不负责授权或自动派工。
273
+
274
+ ```bash
275
+ wand decision:configure --python /绝对路径/venv/bin/python --model /绝对路径/多语言模型目录
276
+ wand decision:skills # 安装七种CLI共用的 wand-decision 技能
277
+ # 重启现有Wand服务,新建结构化CLI会话后使用该技能。
278
+ ```
279
+
280
+ 需先安装依赖和校验过的多语言模型,本命令不自动下载。Skill辅助命令通过已有HTTP服务调用,凭据只在本轮执行环境中,最长6小时、结束撤销;不写进Skill、不另开公网端口。单例worker空闲5分钟卸载,超长输入拒绝截断后判断。普通终端/在Wand外启动的CLI不自动获得凭据。停用:`wand decision:configure --disable`,然后重启服务。
281
+
282
+ Web/Android 的推理调用独立显示为「本地决策」卡,不混入普通工具分组;详情默认收起,点击卡头原位展开输入与结果,显式展开/收起状态在刷新和结果迟到时保留。读取Skill和查询状态仍按普通工具处理。本机合成任务评测发现明显误判和高概率选错,目前只能作为可选参考;不得据此自动审批权限、执行危险操作或派工。部署细节见 [AGENTS.md](AGENTS.md)。
283
+
253
284
  ### 系统服务
254
285
 
255
286
  默认走 **system-wide**:Linux 写 `/etc/systemd/system/wand.service`,macOS 写 `/Library/LaunchDaemons/com.wand.web.plist`。开机自启、不依赖 login session、`service wand` / `systemctl status wand` 这些老命令都能用。装/卸需要 sudo。
@@ -299,15 +330,22 @@ wand/
299
330
  │ ├── cli.ts # CLI 入口
300
331
  │ ├── server.ts # Express 服务器 + WebSocket
301
332
  │ ├── process-manager.ts # PTY 会话编排
333
+ │ ├── structured-session-manager.ts # 结构化会话编排(非 PTY)
302
334
  │ ├── claude-pty-bridge.ts # PTY 输出解析为结构化对话
303
335
  │ ├── storage.ts # SQLite 持久化
304
336
  │ ├── config.ts # 配置加载
305
- │ └── web-ui/ # 前端 HTML/CSS/JS
337
+ │ ├── provider-catalog.ts # Provider 唯一真源(服务端与浏览器共用)
338
+ │ └── web-ui/ # 前端 HTML/CSS/JS(legacy TS + React 两层)
339
+ ├── browser-extension/ # MV3 密码库浏览器扩展
306
340
  ├── android/ # Android 客户端(submodule)
307
341
  ├── ios/ # iOS 客户端(submodule)
308
- └── macos/ # macOS 客户端(submodule)
342
+ ├── macos/ # macOS 客户端(submodule)
343
+ ├── render/ # Render 源码:Rust 常驻进程,持有 PTY(submodule)
344
+ └── render-bin/ # Render 各平台产物 + manifest(submodule,只由 CI 写入)
309
345
  ```
310
346
 
347
+ 配置分三处:部署项(`config.json`)、用户偏好(SQLite `pref:*`,设置面板与 `wand config:set` 都写这里)、密钥(SQLite,不回写 JSON)。
348
+
311
349
  服务端开发只需检出固定版本的 Render 二进制。原生客户端和 Rust 源码都是独立子模块,修改哪个再检出哪个:
312
350
 
313
351
  ```bash
@@ -317,7 +355,7 @@ git submodule update --init -- render-bin
317
355
  # 按需:git submodule update --init -- android # 或 ios、macos、render
318
356
  ```
319
357
 
320
- 全量 `npm test` 包含跨端源码契约检查,运行前另检出 `android ios macos render`。
358
+ 全量 `npm test` 包含跨端源码契约检查,运行前另检出 `android ios macos render`。品牌一致性用例还会读 iOS/macOS 的 AppIcon PNG(生成物),在 macOS 上先跑 `npm run sync:brand-assets`,再用 `swift <平台>/scripts/generate-icons.swift` 生成(CI 的 `.github/workflows/ci.yml` 做同样的事)。
321
359
  `npm run clean:build-cache` 预览可清理的编译缓存,追加 `-- --apply` 才执行删除;
322
360
  保留分发包、Release Render 二进制、签名、依赖和会话数据。
323
361
 
@@ -3,7 +3,7 @@ import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { WandStorage } from "./storage.js";
4
4
  import type { StructuredSessionManager } from "./structured-session-manager.js";
5
5
  import type { WandTask, WandTaskAgent } from "./task-types.js";
6
- import type { SiliconEmployee } from "./ai-team-types.js";
6
+ import { type SiliconEmployee } from "./ai-team-types.js";
7
7
  import type { SessionSnapshot, WandConfig } from "./types.js";
8
8
  export interface AgentDispatchDeps {
9
9
  storage: WandStorage;
@@ -35,6 +35,7 @@ export declare function dispatchAgentForTask(deps: AgentDispatchDeps, input: {
35
35
  systemPrompt?: string;
36
36
  employee?: SiliconEmployee;
37
37
  employeeCandidateIndex?: number;
38
+ teamRequestStarted?: (sessionId: string, requestId: string) => void;
38
39
  }): Promise<AgentDispatchResult>;
39
40
  /**
40
41
  * 给已有会话追加一条消息。结构化会话走 sendMessage(不等整轮结束);
@@ -42,7 +43,7 @@ export declare function dispatchAgentForTask(deps: AgentDispatchDeps, input: {
42
43
  */
43
44
  export declare function sendToAgentSession(deps: Pick<AgentDispatchDeps, "structured" | "processes"> & {
44
45
  sessions: SessionRegistry;
45
- }, sessionId: string, text: string): Promise<void>;
46
+ }, sessionId: string, text: string, teamRequestStarted?: (sessionId: string, requestId: string) => void): Promise<void>;
46
47
  /** 与 /api/sessions/:id/stop 同样按会话归属分派到对应 manager。 */
47
48
  export declare function stopAgentSession(deps: Pick<AgentDispatchDeps, "structured" | "processes"> & {
48
49
  sessions: SessionRegistry;
@@ -1,6 +1,7 @@
1
1
  import { getDefaultModelForProvider } from "./config.js";
2
2
  import { defaultRoleForCli } from "./default-employee.js";
3
3
  import { providerCliCommand } from "./session-provider.js";
4
+ import { agentKey } from "./ai-team-types.js";
4
5
  /** 任务卡的执行目录:工作任务的 worktree / 目录 → 所属项目目录 → 全局默认目录。 */
5
6
  export function resolveTaskDispatchTarget(deps, task) {
6
7
  const workspace = task.workspaceId ? deps.storage.getWorkspace(task.workspaceId) : null;
@@ -20,6 +21,14 @@ export function resolveTaskDispatchTarget(deps, task) {
20
21
  export async function dispatchAgentForTask(deps, input) {
21
22
  const { storage, config, structured, processes } = deps;
22
23
  const { agent } = input;
24
+ if (input.employee) {
25
+ if (agent.kind !== "structured")
26
+ throw new Error("绑定员工只支持结构化会话。");
27
+ const candidate = input.employee.agents[input.employeeCandidateIndex ?? 0];
28
+ if (!candidate || agentKey(candidate) !== agentKey(agent)) {
29
+ throw new Error("员工执行候选与启动快照不一致。");
30
+ }
31
+ }
23
32
  const role = input.employee ?? (!input.systemPrompt?.trim()
24
33
  && input.automationId.startsWith("wand-task:") ? defaultRoleForCli(storage, agent.provider) : null);
25
34
  const systemPrompt = input.systemPrompt?.trim() || role?.prompt || undefined;
@@ -40,6 +49,9 @@ export async function dispatchAgentForTask(deps, input) {
40
49
  sessionSource: "automation",
41
50
  automationId: input.automationId,
42
51
  systemPrompt,
52
+ employeeId: role?.id,
53
+ employeeName: role?.name,
54
+ employeeAvatar: role?.avatar,
43
55
  workspaceId: group.workspaceId,
44
56
  workspaceTaskId: group.workspaceTaskId,
45
57
  })
@@ -63,7 +75,9 @@ export async function dispatchAgentForTask(deps, input) {
63
75
  });
64
76
  storage.bindWandTaskSession(input.task.id, session.id);
65
77
  if (agent.kind !== "pty") {
66
- const completion = structured.sendMessage(session.id, input.prompt);
78
+ const completion = input.teamRequestStarted
79
+ ? structured.sendMessage(session.id, input.prompt, { teamRequestStarted: input.teamRequestStarted })
80
+ : structured.sendMessage(session.id, input.prompt);
67
81
  completion.catch((error) => console.error(`[AgentDispatch] ${input.automationId} failed:`, error));
68
82
  }
69
83
  return { session, cwd, workspaceId: group.workspaceId };
@@ -72,9 +86,11 @@ export async function dispatchAgentForTask(deps, input) {
72
86
  * 给已有会话追加一条消息。结构化会话走 sendMessage(不等整轮结束);
73
87
  * PTY 按输入契约先写文本、再单独写 "\r",不用 text + "\n" 代替回车。
74
88
  */
75
- export async function sendToAgentSession(deps, sessionId, text) {
89
+ export async function sendToAgentSession(deps, sessionId, text, teamRequestStarted) {
76
90
  if (deps.sessions.ownerOf(sessionId) === "structured") {
77
- const completion = deps.structured.sendMessage(sessionId, text);
91
+ const completion = teamRequestStarted
92
+ ? deps.structured.sendMessage(sessionId, text, { teamRequestStarted })
93
+ : deps.structured.sendMessage(sessionId, text);
78
94
  completion.catch((error) => console.error(`[AgentDispatch] message to ${sessionId} failed:`, error));
79
95
  return;
80
96
  }
@@ -0,0 +1,40 @@
1
+ import type { TeamReportFile } from "./types.js";
2
+ import type { AgentActivityState } from "./mission-types.js";
3
+ /** Read-only, bounded presentation of one run; never a second task status owner. */
4
+ export interface AiTeamDeliveryFile {
5
+ stepId: string;
6
+ seq: number;
7
+ title: string;
8
+ memberId: string;
9
+ memberName: string;
10
+ file: TeamReportFile;
11
+ }
12
+ export interface AiTeamDeliveryHandoff {
13
+ stepId: string;
14
+ seq: number;
15
+ title: string;
16
+ memberId: string;
17
+ memberName: string;
18
+ status: "running" | "queued";
19
+ state?: AgentActivityState;
20
+ sessionId: string | null;
21
+ /** Existing dependency names, not a guessed next assignment. */
22
+ waitingFor: string[];
23
+ }
24
+ export interface AiTeamDeliveryAttention {
25
+ kind: "approval" | "reply" | "failure" | "stopped";
26
+ message: string;
27
+ }
28
+ export interface AiTeamDeliverySummary {
29
+ runId: string;
30
+ /** Source run timestamp, not the time of GET; clients retain existing freshness guards. */
31
+ updatedAt: string;
32
+ headline: string;
33
+ /** Only a completed run's recorded leader statement, not AI re-summarized verification. */
34
+ conclusion: string | null;
35
+ files: AiTeamDeliveryFile[];
36
+ totalFiles: number;
37
+ handoffs: AiTeamDeliveryHandoff[];
38
+ totalHandoffs: number;
39
+ attention: AiTeamDeliveryAttention | null;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { AiTeam, AiTeamRun, AiTeamStep } from "./ai-team-types.js";
2
+ import type { AiTeamDeliverySummary } from "./ai-team-delivery-types.js";
3
+ import type { AgentActivityState } from "./mission-types.js";
4
+ import type { ConversationTurn } from "./types.js";
5
+ export declare const AI_TEAM_DELIVERY_FILES_LIMIT = 20;
6
+ export declare const AI_TEAM_DELIVERY_HANDOFFS_LIMIT = 6;
7
+ export declare function buildAiTeamDeliverySummary(input: {
8
+ run: AiTeamRun;
9
+ steps: readonly AiTeamStep[];
10
+ displayTeam: AiTeam;
11
+ relayTurns: readonly ConversationTurn[];
12
+ memberStates: Readonly<Record<string, AgentActivityState>>;
13
+ }): AiTeamDeliverySummary;
@@ -0,0 +1,89 @@
1
+ import path from "node:path";
2
+ export const AI_TEAM_DELIVERY_FILES_LIMIT = 20;
3
+ export const AI_TEAM_DELIVERY_HANDOFFS_LIMIT = 6;
4
+ function bounded(value, limit) {
5
+ const trimmed = typeof value === "string" ? value.trim() : "";
6
+ if (trimmed.length <= limit)
7
+ return trimmed;
8
+ let end = limit - 1;
9
+ if (end > 0 && /[\uD800-\uDBFF]/.test(trimmed[end - 1]))
10
+ end--;
11
+ return `${trimmed.slice(0, end)}…`;
12
+ }
13
+ /** Uses only completion-frozen metadata; no file IO, private chats or knowledge reads. */
14
+ function recordedFile(file, cwd) {
15
+ if (typeof file.path !== "string" || !file.path || file.path.length > 4096 || file.path.includes("\0")
16
+ || !path.isAbsolute(file.path) || !cwd || !Number.isSafeInteger(file.size) || file.size < 0)
17
+ return null;
18
+ const relative = path.relative(path.resolve(cwd), path.resolve(file.path));
19
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
20
+ return null;
21
+ return {
22
+ stepId: file.stepId,
23
+ path: file.path,
24
+ name: bounded(file.name || path.basename(file.path), 255),
25
+ size: file.size,
26
+ ...(file.preview ? { preview: {
27
+ title: bounded(file.preview.title, 100),
28
+ excerpt: bounded(bounded(file.preview.excerpt, 241).split(/\r?\n/).slice(0, 3).join("\n"), 240),
29
+ } } : {}),
30
+ };
31
+ }
32
+ export function buildAiTeamDeliverySummary(input) {
33
+ const { run, steps, displayTeam, relayTurns, memberStates } = input;
34
+ const members = new Map(displayTeam.members.map((member) => [member.id, member]));
35
+ const currentSteps = steps.filter((step) => step.runId === run.id);
36
+ const byId = new Map(currentSteps.map((step) => [step.id, step]));
37
+ const names = (step) => bounded(members.get(step.memberId)?.name || step.memberId, 100);
38
+ const files = new Map();
39
+ for (const turn of relayTurns) {
40
+ if (turn.role !== "assistant" || !turn.reportFile)
41
+ continue;
42
+ const step = byId.get(turn.reportFile.stepId);
43
+ if (!step || step.kind !== "work" || step.status !== "done"
44
+ || (turn.author && turn.author.id !== step.memberId))
45
+ continue;
46
+ const file = recordedFile(turn.reportFile, run.cwd);
47
+ if (!file)
48
+ continue;
49
+ const previous = files.get(step.id);
50
+ // Never let a later truncated/legacy duplicate replace an existing complete preview.
51
+ if (previous && (previous.file.preview || !file.preview || previous.file.path !== file.path))
52
+ continue;
53
+ files.set(step.id, {
54
+ stepId: step.id, seq: step.seq, title: bounded(step.title, 160),
55
+ memberId: step.memberId, memberName: names(step), file,
56
+ });
57
+ }
58
+ const delivered = [...files.values()].sort((a, b) => b.seq - a.seq);
59
+ const pending = currentSteps
60
+ .filter((step) => step.status === "running" || step.status === "queued")
61
+ .sort((a, b) => Number(a.status === "queued") - Number(b.status === "queued") || a.seq - b.seq)
62
+ .map((step) => ({
63
+ stepId: step.id, seq: step.seq, title: bounded(step.title, 160),
64
+ memberId: step.memberId, memberName: names(step),
65
+ status: step.status, sessionId: step.sessionId,
66
+ ...(step.sessionId && memberStates[step.sessionId] ? { state: memberStates[step.sessionId] } : {}),
67
+ waitingFor: step.dependsOn.flatMap((id) => {
68
+ const dependency = byId.get(id);
69
+ if (!dependency)
70
+ return [bounded(`未完成步骤 ${id}`, 180)];
71
+ return dependency.status !== "done"
72
+ ? [bounded(`${names(dependency)} · ${dependency.title}`, 180)] : [];
73
+ }).slice(0, AI_TEAM_DELIVERY_HANDOFFS_LIMIT),
74
+ }));
75
+ const headlines = {
76
+ running: "团队正在处理", awaiting_approval: "计划待你批准", waiting_user: "负责人等你回复",
77
+ done: "本轮交付已完成", failed: "本轮执行失败", stopped: "本轮已停止",
78
+ };
79
+ const attentionKind = run.status === "awaiting_approval" ? "approval"
80
+ : run.status === "waiting_user" ? "reply"
81
+ : run.status === "failed" ? "failure" : run.status === "stopped" ? "stopped" : null;
82
+ return {
83
+ runId: run.id, updatedAt: run.updatedAt, headline: headlines[run.status],
84
+ conclusion: run.status === "done" ? bounded(run.statusDetail, 600) || null : null,
85
+ files: delivered.slice(0, AI_TEAM_DELIVERY_FILES_LIMIT), totalFiles: delivered.length,
86
+ handoffs: pending.slice(0, AI_TEAM_DELIVERY_HANDOFFS_LIMIT), totalHandoffs: pending.length,
87
+ attention: attentionKind ? { kind: attentionKind, message: bounded(run.statusDetail, 600) } : null,
88
+ };
89
+ }
@@ -0,0 +1,16 @@
1
+ import { type AiTeam, type AiTeamMember, type SiliconEmployee } from "./ai-team-types.js";
2
+ export interface AiTeamEmployeeSource {
3
+ getSiliconEmployee(id: string): SiliconEmployee | null;
4
+ }
5
+ export declare function teamMemberEmployee(member: AiTeamMember): SiliconEmployee | undefined;
6
+ /** Only storage restores this private field; HTTP parsing deliberately ignores it. */
7
+ export declare function restoreTeamMemberEmployee(member: AiTeamMember, employee: SiliconEmployee): void;
8
+ export declare function requireTeamEmployee(source: AiTeamEmployeeSource, id: string): SiliconEmployee;
9
+ /** Resolve once per new run. Team duty/role/leadership remain local; no knowledge is read. */
10
+ export declare function freezeTeamEmployees(team: AiTeam, source: AiTeamEmployeeSource): AiTeam;
11
+ /** Private persisted snapshot is explicitly written only to ai_team_runs.team_json. */
12
+ export declare function serializeExecutionTeam(team: AiTeam): string;
13
+ /** Team definitions expose current employee-owned fields without storing another copy of the role. */
14
+ export declare function projectTeamDefinition(team: AiTeam, source: AiTeamEmployeeSource): AiTeam;
15
+ /** Read-only names/avatars follow the same employee, never a replacement at the same member id. */
16
+ export declare function projectTeamEmployees(team: AiTeam, source: AiTeamEmployeeSource): AiTeam;
@@ -0,0 +1,77 @@
1
+ import { defaultEmployeeDefinition } from "./default-employee.js";
2
+ import { AI_TEAM_MAX_CANDIDATES, agentKey, isDefaultSiliconEmployee } from "./ai-team-types.js";
3
+ // Execution-only data: never enumerable on a member or exposed by public DTO serialization.
4
+ const snapshots = new WeakMap();
5
+ function employeeSnapshot(employee) {
6
+ return {
7
+ id: employee.id, name: employee.name, avatar: employee.avatar, prompt: employee.prompt,
8
+ duty: employee.duty, agents: employee.agents.map((agent) => ({ ...agent })),
9
+ createdAt: employee.createdAt, updatedAt: employee.updatedAt,
10
+ };
11
+ }
12
+ export function teamMemberEmployee(member) {
13
+ return snapshots.get(member);
14
+ }
15
+ /** Only storage restores this private field; HTTP parsing deliberately ignores it. */
16
+ export function restoreTeamMemberEmployee(member, employee) {
17
+ if (member.employeeId && employee.id === member.employeeId) {
18
+ snapshots.set(member, employeeSnapshot(employee));
19
+ }
20
+ }
21
+ export function requireTeamEmployee(source, id) {
22
+ const employee = source.getSiliconEmployee(id);
23
+ if (!employee)
24
+ throw new Error("绑定的员工不存在。");
25
+ if (employee.archivedAt)
26
+ throw new Error(`员工「${employee.name}」已归档,不能加入团队或新开工。`);
27
+ if (!employee.agents.length || employee.agents.length > AI_TEAM_MAX_CANDIDATES
28
+ || employee.agents.some((agent) => agent.kind !== "structured")
29
+ || new Set(employee.agents.map(agentKey)).size !== employee.agents.length) {
30
+ throw new Error(`员工「${employee.name}」需要有效且不重复的结构化 CLI 候选。`);
31
+ }
32
+ // The default partner's read projection includes short-term habits; they are not team snapshot data.
33
+ return isDefaultSiliconEmployee(employee)
34
+ ? defaultEmployeeDefinition(employee.agents, employee.updatedAt, employee, null)
35
+ : employee;
36
+ }
37
+ /** Resolve once per new run. Team duty/role/leadership remain local; no knowledge is read. */
38
+ export function freezeTeamEmployees(team, source) {
39
+ const seen = new Set();
40
+ return {
41
+ ...team,
42
+ members: team.members.map((member) => {
43
+ if (!member.employeeId)
44
+ return { ...member, agents: member.agents.map((agent) => ({ ...agent })) };
45
+ if (seen.has(member.employeeId))
46
+ throw new Error("同一员工不能重复加入同一团队。");
47
+ seen.add(member.employeeId);
48
+ const employee = requireTeamEmployee(source, member.employeeId);
49
+ const snapshot = employeeSnapshot(employee);
50
+ const frozen = { ...member, name: snapshot.name, avatar: snapshot.avatar,
51
+ agents: snapshot.agents, agent: snapshot.agents[0] };
52
+ snapshots.set(frozen, snapshot);
53
+ return frozen;
54
+ }),
55
+ };
56
+ }
57
+ /** Private persisted snapshot is explicitly written only to ai_team_runs.team_json. */
58
+ export function serializeExecutionTeam(team) {
59
+ return JSON.stringify({ ...team, members: team.members.map((member) => ({
60
+ ...member, _employeeSnapshot: snapshots.get(member),
61
+ })) });
62
+ }
63
+ /** Team definitions expose current employee-owned fields without storing another copy of the role. */
64
+ export function projectTeamDefinition(team, source) {
65
+ return { ...team, members: team.members.map((member) => {
66
+ const employee = member.employeeId ? source.getSiliconEmployee(member.employeeId) : null;
67
+ return employee ? { ...member, name: employee.name, avatar: employee.avatar,
68
+ agents: employee.agents, agent: employee.agents[0] } : member;
69
+ }) };
70
+ }
71
+ /** Read-only names/avatars follow the same employee, never a replacement at the same member id. */
72
+ export function projectTeamEmployees(team, source) {
73
+ return { ...team, members: team.members.map((member) => {
74
+ const employee = member.employeeId ? source.getSiliconEmployee(member.employeeId) : null;
75
+ return employee ? { ...member, name: employee.name, avatar: employee.avatar } : member;
76
+ }) };
77
+ }
@@ -1,3 +1,4 @@
1
+ import { teamMemberEmployee } from "./ai-team-employee-binding.js";
1
2
  import { memberAgents } from "./ai-team-types.js";
2
3
  export const AI_TEAM_REPORT_DIR = ".wand-team";
3
4
  export const AI_TEAM_MAX_ASSIGN_STEPS = 5;
@@ -235,7 +236,9 @@ function handoffRules(audience) {
235
236
  }
236
237
  function leaderSystemPrompt(run, resolveModel) {
237
238
  const leader = leaderOf(run.team);
239
+ const employeePrompt = teamMemberEmployee(leader)?.prompt;
238
240
  return [
241
+ ...(employeePrompt ? [employeePrompt] : []),
239
242
  `你是 AI 团队「${run.team.name}」的负责人(${leader.name})。`,
240
243
  `你的职责:${leader.duty.trim() || "拆分任务、派工、根据报告决定下一步。"}`,
241
244
  "",
@@ -259,7 +262,9 @@ function leaderSystemPrompt(run, resolveModel) {
259
262
  ].join("\n");
260
263
  }
261
264
  function memberSystemPrompt(run, member) {
265
+ const employeePrompt = teamMemberEmployee(member)?.prompt;
262
266
  return [
267
+ ...(employeePrompt ? [employeePrompt] : []),
263
268
  `你是 AI 团队「${run.team.name}」的成员(${member.name})。`,
264
269
  `你的职责:${member.duty.trim() || "(未填写)"}`,
265
270
  ...teamInstructions(run.team),
@@ -1,3 +1,4 @@
1
+ import type { SiliconEmployee } from "./ai-team-types.js";
1
2
  import { type AgentDispatchDeps } from "./agent-dispatch.js";
2
3
  import { type ModelCatalogView } from "./ai-team-availability.js";
3
4
  import { type AiTeam, type AiTeamLiveStep, type AiTeamLiveUpdate, type AiTeamRun, type AiTeamRunDetail, type AiTeamRunSummary, type AiTeamStep } from "./ai-team-types.js";
@@ -19,11 +20,16 @@ export interface AiTeamSessionOps {
19
20
  automationId: string;
20
21
  /** 角色与规则;走 provider 的系统提示通道,不拼进 prompt。 */
21
22
  systemPrompt?: string;
23
+ employee?: SiliconEmployee;
24
+ employeeCandidateIndex?: number;
25
+ teamRequestStarted?: (sessionId: string, requestId: string) => void;
22
26
  }): Promise<string>;
23
- send(sessionId: string, text: string): Promise<void>;
27
+ send(sessionId: string, text: string, teamRequestStarted?: (sessionId: string, requestId: string) => void): Promise<void>;
24
28
  stop(sessionId: string): void;
25
29
  snapshot(sessionId: string): SessionSnapshot | null;
26
30
  ownerOf(sessionId: string): "structured" | "pty" | "storage" | null;
31
+ /** Server-private generation and one-shot, explicitly unaccepted startup fact. */
32
+ consumeUnacceptedStartup?(sessionId: string, requestId: string): boolean;
27
33
  }
28
34
  /** 群聊:一次运行一个转发会话,团队的决定、派工和报告都以成员身份发在里面。 */
29
35
  export interface AiTeamChatOps {
@@ -136,6 +142,7 @@ export declare class AiTeamRunner {
136
142
  private readonly recheckTimers;
137
143
  /** 已经确定「不降级、按既有路径记 failed」的步骤,防止 finishStep 再判一次形成回环。 */
138
144
  private readonly noDegrade;
145
+ private readonly teamRequestIds;
139
146
  private sweepTimer;
140
147
  /** 脱敏参照目录只算一次:os.homedir() 读 env,configDir 随 storage 定,运行期不变。 */
141
148
  private redactionDirsValue;
@@ -295,7 +302,7 @@ export declare class AiTeamRunner {
295
302
  private actualAgent;
296
303
  /**
297
304
  * 运行停下来等用户时,用户可能刚在团队页换了成员的 CLI / 模型(比如额度用尽换一个)。
298
- * 接着跑之前换成最新配置;成员被删了或没有负责人时保留原快照。
305
+ * 独立 CLI 团队沿用旧刷新规则;带员工链接的运行全程保留新开工时的执行快照。
299
306
  */
300
307
  private refreshTeam;
301
308
  /**