@co0ontty/wand 4.79.0 → 4.80.0

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 (58) hide show
  1. package/README.md +18 -6
  2. package/dist/agent-dispatch.d.ts +3 -0
  3. package/dist/agent-dispatch.js +5 -0
  4. package/dist/ai-team-runner.js +4 -2
  5. package/dist/ai-team-types.d.ts +34 -0
  6. package/dist/ai-team-types.js +12 -1
  7. package/dist/build-info.json +3 -3
  8. package/dist/config.d.ts +4 -3
  9. package/dist/config.js +25 -0
  10. package/dist/git-quick-commit.d.ts +9 -8
  11. package/dist/git-quick-commit.js +123 -8
  12. package/dist/models.d.ts +3 -0
  13. package/dist/models.js +24 -2
  14. package/dist/process-manager.js +16 -3
  15. package/dist/provider-cli-updater.d.ts +1 -1
  16. package/dist/provider-cli-updater.js +12 -1
  17. package/dist/server-ai-team-routes.js +4 -1
  18. package/dist/server-employee-routes.d.ts +22 -0
  19. package/dist/server-employee-routes.js +277 -0
  20. package/dist/server-session-routes.js +38 -13
  21. package/dist/server-settings-routes.js +5 -0
  22. package/dist/server-task-routes.d.ts +2 -0
  23. package/dist/server-task-routes.js +174 -22
  24. package/dist/server-update-routes.js +1 -1
  25. package/dist/server-workspace-routes.js +3 -0
  26. package/dist/server.js +16 -3
  27. package/dist/session-ai-context.d.ts +7 -2
  28. package/dist/session-ai-context.js +36 -9
  29. package/dist/session-provider.d.ts +6 -1
  30. package/dist/session-provider.js +27 -1
  31. package/dist/session-transport.js +4 -0
  32. package/dist/silicon-employee-dispatch.d.ts +9 -0
  33. package/dist/silicon-employee-dispatch.js +13 -0
  34. package/dist/silicon-employee-draft.d.ts +36 -0
  35. package/dist/silicon-employee-draft.js +146 -0
  36. package/dist/storage.d.ts +19 -2
  37. package/dist/storage.js +158 -7
  38. package/dist/structured-gemini-adapter.d.ts +40 -0
  39. package/dist/structured-gemini-adapter.js +198 -0
  40. package/dist/structured-provider-common.d.ts +2 -1
  41. package/dist/structured-provider-common.js +7 -2
  42. package/dist/structured-session-manager.d.ts +10 -0
  43. package/dist/structured-session-manager.js +106 -14
  44. package/dist/system-ai.js +1 -1
  45. package/dist/system-employee.d.ts +35 -0
  46. package/dist/system-employee.js +81 -0
  47. package/dist/task-types.d.ts +8 -1
  48. package/dist/task-types.js +7 -0
  49. package/dist/types.d.ts +23 -4
  50. package/dist/web-ui/content/ai-teams.js +13 -13
  51. package/dist/web-ui/content/scripts.js +64 -64
  52. package/dist/web-ui/content/styles.css +1 -1
  53. package/dist/web-ui/content/tailwind.css +3 -0
  54. package/dist/web-ui/embedded-assets.js +3 -3
  55. package/dist/web-ui/provider-identity.d.ts +2 -1
  56. package/dist/web-ui/provider-identity.js +10 -1
  57. package/dist/web-ui/session-activity.js +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,146 @@
1
+ import { callConfiguredAiText } from "./git-quick-commit.js";
2
+ import { employeeCliAvailable } from "./silicon-employee-dispatch.js";
3
+ import { SESSION_PROVIDERS, isSessionProvider } from "./session-provider.js";
4
+ import { DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_AGENT_MODE, normalizeWandTaskAgentMode, } from "./task-types.js";
5
+ import { clipAtWordBoundary } from "./text-utils.js";
6
+ export class SiliconEmployeeDraftError extends Error {
7
+ code;
8
+ constructor(message, code) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = "SiliconEmployeeDraftError";
12
+ }
13
+ }
14
+ const NAME_MAX = 40;
15
+ const DUTY_MAX = 200;
16
+ const PROMPT_MAX = 20_000;
17
+ const EXPECTATION_MAX = 4_000;
18
+ /** 只用于提示词的 CLI 人话名;执行候选本身仍用 provider id。 */
19
+ const PROVIDER_LABELS = {
20
+ claude: "Claude Code",
21
+ codex: "OpenAI Codex CLI",
22
+ opencode: "OpenCode CLI",
23
+ grok: "Grok Build CLI",
24
+ qoder: "Qoder CLI",
25
+ pi: "Pi coding agent",
26
+ gemini: "Gemini CLI",
27
+ };
28
+ /** 硅基员工只能跑结构化会话;候选沿用看板的「跟随服务端默认」。 */
29
+ export function employeeAgentFor(provider) {
30
+ return {
31
+ provider,
32
+ model: "default",
33
+ thinkingEffort: "off",
34
+ mode: normalizeWandTaskAgentMode(provider, DEFAULT_WAND_TASK_AGENT_MODE),
35
+ kind: DEFAULT_WAND_TASK_AGENT_KIND,
36
+ };
37
+ }
38
+ /**
39
+ * 已安装的 CLI:默认 provider 优先,其余按全局顺序;
40
+ * 都没装时返回空数组,由调用方决定退回到哪一个首选。
41
+ */
42
+ export function availableEmployeeProviders(preferred = "claude", isAvailable = employeeCliAvailable) {
43
+ const order = [preferred, ...SESSION_PROVIDERS];
44
+ const seen = new Set();
45
+ const available = [];
46
+ for (const provider of order) {
47
+ if (seen.has(provider))
48
+ continue;
49
+ seen.add(provider);
50
+ if (isAvailable(employeeAgentFor(provider)))
51
+ available.push(provider);
52
+ }
53
+ return available;
54
+ }
55
+ function stripFences(raw) {
56
+ return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
57
+ }
58
+ /** 从模型输出里取第一个 JSON 对象,容忍前后夹带解释文字。 */
59
+ export function extractDraftJson(raw) {
60
+ const text = stripFences(raw);
61
+ const start = text.indexOf("{");
62
+ const end = text.lastIndexOf("}");
63
+ if (start < 0 || end <= start) {
64
+ throw new SiliconEmployeeDraftError("AI 没有返回可用的员工配置,请换个说法再试。", "DRAFT_INVALID_JSON");
65
+ }
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(text.slice(start, end + 1));
69
+ }
70
+ catch {
71
+ throw new SiliconEmployeeDraftError("AI 返回的员工配置无法解析,请重试。", "DRAFT_INVALID_JSON");
72
+ }
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
74
+ throw new SiliconEmployeeDraftError("AI 返回的员工配置无法解析,请重试。", "DRAFT_INVALID_JSON");
75
+ }
76
+ return parsed;
77
+ }
78
+ function oneLine(record, key) {
79
+ const value = record[key];
80
+ return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
81
+ }
82
+ /**
83
+ * 校验并收敛模型输出。名字 / 角色设定缺失即视为不可用;
84
+ * provider 不在已安装清单里时按候选顺序兜底,不让员工带着跑不起来的 CLI 落库。
85
+ */
86
+ export function parseSiliconEmployeeDraft(raw, options = {}) {
87
+ const record = extractDraftJson(raw);
88
+ const name = clipAtWordBoundary(oneLine(record, "name"), NAME_MAX);
89
+ if (!name)
90
+ throw new SiliconEmployeeDraftError("AI 没有给出员工名字,请重试。", "DRAFT_MISSING_NAME");
91
+ const duty = clipAtWordBoundary(oneLine(record, "duty"), DUTY_MAX);
92
+ const prompt = typeof record.prompt === "string" ? record.prompt.trim().slice(0, PROMPT_MAX) : "";
93
+ if (!prompt)
94
+ throw new SiliconEmployeeDraftError("AI 没有给出员工角色设定,请重试。", "DRAFT_MISSING_PROMPT");
95
+ const allowed = options.allowedProviders?.length ? options.allowedProviders : [...SESSION_PROVIDERS];
96
+ const requested = oneLine(record, "provider").toLowerCase();
97
+ const provider = isSessionProvider(requested) && allowed.includes(requested)
98
+ ? requested
99
+ : options.fallbackProvider && allowed.includes(options.fallbackProvider)
100
+ ? options.fallbackProvider
101
+ : allowed[0];
102
+ return { name, duty, prompt, agent: employeeAgentFor(provider) };
103
+ }
104
+ /** 规则走系统提示,用户的期望走用户消息;一次性调用沿用系统 AI 通道。 */
105
+ export function buildEmployeeDraftPrompt(expectation, providers, existingNames = []) {
106
+ const list = providers.length ? providers : [...SESSION_PROVIDERS];
107
+ const lines = [
108
+ "你正在帮用户创建一位「硅基员工」:一个长期负责某类工作的 AI 角色,之后会以结构化会话执行任务。",
109
+ "根据用户对这位员工的期望,设计一份配置,只输出一个 JSON 对象(不要 Markdown 代码块,不要任何解释):",
110
+ '{"name":"员工名字","duty":"一句话职责","prompt":"角色设定","provider":"执行工具"}',
111
+ "",
112
+ "字段要求:",
113
+ "- name:2-8 个中文,具体、好记、体现职能(如「前端小匠」「接口守夜人」);不要「助手」「AI」这类空泛词,不要带标点。",
114
+ "- duty:一句话职责,15-40 个字,说清它负责什么、交付什么,会显示在侧栏和消息署名里。",
115
+ "- prompt:角色设定(系统提示),用第二人称「你」写给这位员工,分点写清专业能力、工作要求、交付标准与协作习惯,300-600 字;不要复述用户原话,不要写具体项目或文件名。",
116
+ `- provider:执行工具,只能取 ${list.map((provider) => `"${provider}"(${PROVIDER_LABELS[provider]})`).join("、")} 之一,挑最适合这项工作的。`,
117
+ ];
118
+ if (existingNames.length) {
119
+ lines.push(`- 不要与已有员工重名:${existingNames.slice(0, 20).join("、")}。`);
120
+ }
121
+ return { system: lines.join("\n"), prompt: `用户对这位员工的期望:\n${expectation}` };
122
+ }
123
+ /** 按自然语言期望生成员工配置;调用方负责随后落库。 */
124
+ export async function generateSiliconEmployeeDraft(expectation, ai = {}, options = {}) {
125
+ const trimmed = expectation.trim();
126
+ if (!trimmed) {
127
+ throw new SiliconEmployeeDraftError("先说说你对这位员工的期望吧。", "EMPTY_EXPECTATION");
128
+ }
129
+ if (trimmed.length > EXPECTATION_MAX) {
130
+ throw new SiliconEmployeeDraftError(`期望描述太长了(${trimmed.length} 字),请精简到 ${EXPECTATION_MAX} 字以内。`, "EXPECTATION_TOO_LONG");
131
+ }
132
+ const available = availableEmployeeProviders(options.preferredProvider, options.isProviderAvailable);
133
+ const providers = available.length ? available : [options.preferredProvider ?? "claude", ...SESSION_PROVIDERS];
134
+ const request = buildEmployeeDraftPrompt(trimmed, providers, options.existingNames);
135
+ let raw;
136
+ try {
137
+ raw = await callConfiguredAiText(request, options.cwd || process.cwd(), options.language ?? "", ai);
138
+ }
139
+ catch (error) {
140
+ if (error instanceof SiliconEmployeeDraftError)
141
+ throw error;
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ throw new SiliconEmployeeDraftError(message || "AI 生成员工配置失败。", "DRAFT_AI_FAILED");
144
+ }
145
+ return parseSiliconEmployeeDraft(raw, { allowedProviders: providers, fallbackProvider: providers[0] });
146
+ }
package/dist/storage.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SessionSnapshot, ConversationTurn, SessionProvider, StructuredSessionState, Workspace, LayoutNode, TaskWindowLayout, WorkspaceDefaultProvider, WorkspaceKind, WorkspaceTask, WorkspaceTaskWorktree, WorkspaceTaskStatus } from "./types.js";
2
- import type { AiTeam, AiTeamRun, AiTeamRunChatMarker, AiTeamRunStatus, AiTeamStep, AiTeamStepSessionMarker, CandidateFailureKind, StepDispatchInfo } from "./ai-team-types.js";
2
+ import type { AiTeam, AiTeamRun, AiTeamRunChatMarker, AiTeamRunStatus, AiTeamStep, AiTeamStepSessionMarker, CandidateFailureKind, SiliconEmployee, StepDispatchInfo } from "./ai-team-types.js";
3
3
  import type { WandTask, WandTaskAgent, WandTaskAgentKind, WandTaskTitleSource } from "./task-types.js";
4
+ import { type SystemEmployeeSeed } from "./system-employee.js";
4
5
  import type { AgentActivityItem, Mission, MissionAttempt, MissionReviewComment, MissionReviewStatus, MissionStatus } from "./mission-types.js";
5
6
  import { type PasswordVault, type PasswordVaultItem, type PasswordVaultItemFilter, type PasswordVaultItemInput } from "./password-manager.js";
6
7
  /** `ai_team_runs.run_state_json`:本次运行的候选黑名单(§3.4 修正 B12 两层键值 + host-disabled)。 */
@@ -55,8 +56,9 @@ interface CreateWandTaskInput {
55
56
  dueDate?: string | null;
56
57
  milestoneId?: string | null;
57
58
  agent?: WandTaskAgent | null;
59
+ executionSubject?: WandTask["executionSubject"];
58
60
  }
59
- type WandTaskPatch = Partial<Pick<WandTask, "workspaceId" | "workspaceTaskId" | "parentTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent">>;
61
+ type WandTaskPatch = Partial<Pick<WandTask, "workspaceId" | "workspaceTaskId" | "parentTaskId" | "title" | "titleSource" | "autoTitleSignature" | "description" | "status" | "priority" | "labels" | "dueDate" | "milestoneId" | "sortOrder" | "agent" | "executionSubject">>;
60
62
  export declare class WandStorage {
61
63
  private readonly db;
62
64
  private readonly dbPath;
@@ -310,6 +312,21 @@ export declare class WandStorage {
310
312
  deleteAuthSession(token: string): void;
311
313
  deleteAllAuthSessions(): void;
312
314
  deleteExpiredAuthSessions(now: number): void;
315
+ listSiliconEmployees(options?: {
316
+ includeArchived?: boolean;
317
+ }): SiliconEmployee[];
318
+ getSiliconEmployee(id: string): SiliconEmployee | null;
319
+ /** 内置「系统运维」员工;未创建(或被删除过)时返回 null。 */
320
+ getSystemSiliconEmployee(key?: string): SiliconEmployee | null;
321
+ /**
322
+ * 幂等地保证内置员工存在。已存在时只补齐锁定字段(防止旧的同名定义漂移),
323
+ * 候选保留用户当下的设置;不存在时按 seed 建首条候选。
324
+ */
325
+ ensureSystemSiliconEmployee(seed?: SystemEmployeeSeed): SiliconEmployee;
326
+ saveSiliconEmployee(employee: SiliconEmployee): void;
327
+ archiveSiliconEmployee(id: string, archivedAt?: string): void;
328
+ unarchiveSiliconEmployee(id: string): void;
329
+ deleteSiliconEmployee(id: string): void;
313
330
  listAiTeams(): AiTeam[];
314
331
  getAiTeam(id: string): AiTeam | null;
315
332
  saveAiTeam(team: AiTeam): void;
package/dist/storage.js CHANGED
@@ -9,6 +9,8 @@ import { DEFAULT_ITERATION_NAME, DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK
9
9
  import { firstLayoutTabId } from "./layout-tree.js";
10
10
  import { isUnnamedWorkspaceTaskName } from "./wand-task-sync.js";
11
11
  import { AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_TERMINAL_RUN_STATUSES, isTeamMemberRole, memberAgents } from "./ai-team-types.js";
12
+ import { isTaskExecutionSubject } from "./task-types.js";
13
+ import { SYSTEM_EMPLOYEE_KEY, systemEmployeeDefinition, systemEmployeeSeedAgents, } from "./system-employee.js";
12
14
  import { isThinkingEffort } from "./structured-provider-common.js";
13
15
  import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
14
16
  function safeJsonParse(raw) {
@@ -40,7 +42,7 @@ export function parseWandTaskAgent(raw) {
40
42
  return null;
41
43
  if (typeof model !== "string" || !model.trim() || model.trim().length > 128)
42
44
  return null;
43
- if (thinkingEffort !== "off" && thinkingEffort !== "standard" && thinkingEffort !== "deep" && thinkingEffort !== "max")
45
+ if (!isThinkingEffort(thinkingEffort))
44
46
  return null;
45
47
  // mode 是后加列:历史行没有该字段时按标准模式读取,不因此整条配置退化成 null。
46
48
  const mode = normalizeWandTaskAgentMode(provider, value.mode);
@@ -177,6 +179,11 @@ function serializeSessionOptions(snapshot) {
177
179
  currentTaskTitle: snapshot.currentTaskTitle,
178
180
  summary: snapshot.summary,
179
181
  systemPrompt: snapshot.systemPrompt,
182
+ employeeId: snapshot.employeeId,
183
+ employeeName: snapshot.employeeName,
184
+ employeeAvatar: snapshot.employeeAvatar,
185
+ employeeCandidates: snapshot.employeeCandidates,
186
+ employeeCandidateIndex: snapshot.employeeCandidateIndex,
180
187
  };
181
188
  return JSON.stringify(options);
182
189
  }
@@ -240,6 +247,20 @@ function parseSessionOptions(raw) {
240
247
  options.summary = parsed.summary;
241
248
  if (typeof parsed.systemPrompt === "string")
242
249
  options.systemPrompt = parsed.systemPrompt;
250
+ if (typeof parsed.employeeId === "string")
251
+ options.employeeId = parsed.employeeId;
252
+ if (typeof parsed.employeeName === "string")
253
+ options.employeeName = parsed.employeeName;
254
+ if (typeof parsed.employeeAvatar === "string")
255
+ options.employeeAvatar = parsed.employeeAvatar;
256
+ if (Array.isArray(parsed.employeeCandidates)) {
257
+ options.employeeCandidates = parsed.employeeCandidates
258
+ .map(parseWandTaskAgent)
259
+ .filter((agent) => agent?.kind === "structured");
260
+ }
261
+ if (Number.isSafeInteger(parsed.employeeCandidateIndex) && parsed.employeeCandidateIndex >= 0) {
262
+ options.employeeCandidateIndex = parsed.employeeCandidateIndex;
263
+ }
243
264
  return options;
244
265
  }
245
266
  function parseQueuedMessages(raw) {
@@ -763,6 +784,7 @@ const INIT_SQL = `
763
784
  milestone_id TEXT,
764
785
  sort_order INTEGER NOT NULL DEFAULT 0,
765
786
  agent_json TEXT,
787
+ execution_subject_json TEXT,
766
788
  workspace_task_id TEXT,
767
789
  parent_task_id TEXT,
768
790
  title_source TEXT,
@@ -794,6 +816,21 @@ const INIT_SQL = `
794
816
  updated_at TEXT NOT NULL
795
817
  );
796
818
 
819
+ CREATE TABLE IF NOT EXISTS silicon_employees (
820
+ id TEXT PRIMARY KEY,
821
+ name TEXT NOT NULL,
822
+ duty TEXT NOT NULL DEFAULT '',
823
+ prompt TEXT NOT NULL DEFAULT '',
824
+ avatar TEXT NOT NULL DEFAULT '',
825
+ agents_json TEXT NOT NULL,
826
+ system_key TEXT,
827
+ archived_at TEXT,
828
+ created_at TEXT NOT NULL,
829
+ updated_at TEXT NOT NULL
830
+ );
831
+ CREATE INDEX IF NOT EXISTS idx_silicon_employees_archived ON silicon_employees(archived_at);
832
+
833
+
797
834
  CREATE TABLE IF NOT EXISTS ai_team_runs (
798
835
  id TEXT PRIMARY KEY,
799
836
  team_id TEXT NOT NULL,
@@ -850,6 +887,27 @@ function ensureAiTeamSchema(db) {
850
887
  db.exec("ALTER TABLE ai_team_steps ADD COLUMN dispatch_info_json TEXT NOT NULL DEFAULT '{}'");
851
888
  if (!runColumns.has("run_state_json"))
852
889
  db.exec("ALTER TABLE ai_team_runs ADD COLUMN run_state_json TEXT NOT NULL DEFAULT '{}'");
890
+ // 硅基员工表结构保护(遵循只加不删)
891
+ db.exec(`
892
+ CREATE TABLE IF NOT EXISTS silicon_employees (
893
+ id TEXT PRIMARY KEY,
894
+ name TEXT NOT NULL,
895
+ duty TEXT NOT NULL DEFAULT '',
896
+ prompt TEXT NOT NULL DEFAULT '',
897
+ avatar TEXT NOT NULL DEFAULT '',
898
+ agents_json TEXT NOT NULL,
899
+ system_key TEXT,
900
+ archived_at TEXT,
901
+ created_at TEXT NOT NULL,
902
+ updated_at TEXT NOT NULL
903
+ );
904
+ CREATE INDEX IF NOT EXISTS idx_silicon_employees_archived ON silicon_employees(archived_at);
905
+ `);
906
+ // 内置员工标记(如 "wand-ops"):只加列,用户员工保持 NULL。
907
+ const employeeColumns = new Set(db.prepare("PRAGMA table_info(silicon_employees)").all().map((column) => column.name));
908
+ if (employeeColumns.size > 0 && !employeeColumns.has("system_key")) {
909
+ db.exec("ALTER TABLE silicon_employees ADD COLUMN system_key TEXT");
910
+ }
853
911
  }
854
912
  function ensureWandTaskSchema(db) {
855
913
  const columns = db.prepare("PRAGMA table_info(wand_tasks)").all();
@@ -861,6 +919,8 @@ function ensureWandTaskSchema(db) {
861
919
  // 任务的默认派发配置(CLI 工具 / 模型 / 思考深度)。只加列,历史行保持 NULL。
862
920
  if (columns.length > 0 && !names.has("agent_json"))
863
921
  db.exec("ALTER TABLE wand_tasks ADD COLUMN agent_json TEXT");
922
+ if (columns.length > 0 && !names.has("execution_subject_json"))
923
+ db.exec("ALTER TABLE wand_tasks ADD COLUMN execution_subject_json TEXT");
864
924
  if (columns.length > 0 && !names.has("workspace_task_id"))
865
925
  db.exec("ALTER TABLE wand_tasks ADD COLUMN workspace_task_id TEXT");
866
926
  // 父任务归属只加列;历史任务仍然是顶层任务。
@@ -999,7 +1059,7 @@ export function ensureDatabaseFile(dbPath) {
999
1059
  }
1000
1060
  const WAND_TASK_FIELDS = `id, identifier, workspace_id, workspace_task_id, parent_task_id,
1001
1061
  title, title_source, auto_title_signature, description, status, priority, labels_json,
1002
- due_date, milestone_id, sort_order, agent_json, created_at, updated_at`;
1062
+ due_date, milestone_id, sort_order, agent_json, execution_subject_json, created_at, updated_at`;
1003
1063
  // Shared metadata comes from the latest explicitly linked card, including meaningful NULLs.
1004
1064
  // The old container columns remain readable for legacy rows until the startup migration.
1005
1065
  const WORKSPACE_TASK_PROJECTION = `SELECT wt.id,
@@ -1530,6 +1590,10 @@ export class WandStorage {
1530
1590
  milestoneId: typeof row.milestone_id === "string" && row.milestone_id ? row.milestone_id : null,
1531
1591
  sortOrder: Number(row.sort_order) || 0,
1532
1592
  agent: parseWandTaskAgent(row.agent_json),
1593
+ executionSubject: (() => {
1594
+ const parsed = safeJsonParse(typeof row.execution_subject_json === "string" ? row.execution_subject_json : null);
1595
+ return isTaskExecutionSubject(parsed) ? parsed : null;
1596
+ })(),
1533
1597
  createdAt: String(row.created_at),
1534
1598
  updatedAt: String(row.updated_at),
1535
1599
  };
@@ -1553,8 +1617,8 @@ export class WandStorage {
1553
1617
  const number = this.db.prepare(`SELECT COALESCE(MAX(CAST(substr(identifier, 6) AS INTEGER)), 0) AS value
1554
1618
  FROM wand_tasks WHERE identifier GLOB 'TASK-[0-9]*'`).get();
1555
1619
  this.db.prepare(`INSERT INTO wand_tasks (${WAND_TASK_FIELDS})
1556
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1557
- .run(id, `TASK-${number.value + 1}`, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.parentTaskId ?? null, input.title, input.titleSource ?? "user", null, input.description ?? "", status, input.priority ?? DEFAULT_WAND_TASK_PRIORITY, JSON.stringify(input.labels ?? []), input.dueDate ?? null, input.milestoneId ?? null, max.value + 1, input.agent ? JSON.stringify(input.agent) : null, createdAt, createdAt);
1620
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1621
+ .run(id, `TASK-${number.value + 1}`, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.parentTaskId ?? null, input.title, input.titleSource ?? "user", null, input.description ?? "", status, input.priority ?? DEFAULT_WAND_TASK_PRIORITY, JSON.stringify(input.labels ?? []), input.dueDate ?? null, input.milestoneId ?? null, max.value + 1, input.agent ? JSON.stringify(input.agent) : null, input.executionSubject ? JSON.stringify(input.executionSubject) : null, createdAt, createdAt);
1558
1622
  return this.getWandTask(id);
1559
1623
  }
1560
1624
  createWandTask(input) {
@@ -1601,9 +1665,9 @@ export class WandStorage {
1601
1665
  }
1602
1666
  this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, parent_task_id = ?,
1603
1667
  title = ?, title_source = ?, auto_title_signature = ?, description = ?, status = ?, priority = ?,
1604
- labels_json = ?, due_date = ?, milestone_id = ?, sort_order = ?, agent_json = ?, updated_at = ?
1668
+ labels_json = ?, due_date = ?, milestone_id = ?, sort_order = ?, agent_json = ?, execution_subject_json = ?, updated_at = ?
1605
1669
  WHERE id = ?`)
1606
- .run(next.workspaceId, next.workspaceTaskId, next.parentTaskId, next.title, next.titleSource, next.autoTitleSignature ?? null, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.milestoneId ?? null, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.updatedAt, next.id);
1670
+ .run(next.workspaceId, next.workspaceTaskId, next.parentTaskId, next.title, next.titleSource, next.autoTitleSignature ?? null, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.milestoneId ?? null, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.executionSubject ? JSON.stringify(next.executionSubject) : null, next.updatedAt, next.id);
1607
1671
  if (patch.workspaceId !== undefined && next.workspaceTaskId) {
1608
1672
  const workspaceId = next.workspaceId ?? this.ensureGlobalWorkspace().id;
1609
1673
  // The container FK/index and standalone session workspace bucket follow the canonical project.
@@ -2252,6 +2316,71 @@ export class WandStorage {
2252
2316
  deleteExpiredAuthSessions(now) {
2253
2317
  this.db.prepare("DELETE FROM auth_sessions WHERE expires_at < ?").run(now);
2254
2318
  }
2319
+ // ============ Silicon Employees ============
2320
+ listSiliconEmployees(options) {
2321
+ const includeArchived = options?.includeArchived ?? false;
2322
+ // 内置员工(system_key 非空)始终排在最前,其余按创建时间。
2323
+ const sql = includeArchived
2324
+ ? "SELECT * FROM silicon_employees ORDER BY (system_key IS NULL), created_at ASC"
2325
+ : "SELECT * FROM silicon_employees WHERE archived_at IS NULL ORDER BY (system_key IS NULL), created_at ASC";
2326
+ const rows = this.db.prepare(sql).all();
2327
+ return rows.map(mapSiliconEmployeeRow);
2328
+ }
2329
+ getSiliconEmployee(id) {
2330
+ const row = this.db.prepare("SELECT * FROM silicon_employees WHERE id = ?").get(id);
2331
+ return row ? mapSiliconEmployeeRow(row) : null;
2332
+ }
2333
+ /** 内置「系统运维」员工;未创建(或被删除过)时返回 null。 */
2334
+ getSystemSiliconEmployee(key = SYSTEM_EMPLOYEE_KEY) {
2335
+ const row = this.db.prepare("SELECT * FROM silicon_employees WHERE system_key = ?").get(key);
2336
+ return row ? mapSiliconEmployeeRow(row) : null;
2337
+ }
2338
+ /**
2339
+ * 幂等地保证内置员工存在。已存在时只补齐锁定字段(防止旧的同名定义漂移),
2340
+ * 候选保留用户当下的设置;不存在时按 seed 建首条候选。
2341
+ */
2342
+ ensureSystemSiliconEmployee(seed = {}) {
2343
+ const existing = this.getSystemSiliconEmployee();
2344
+ if (!existing) {
2345
+ const employee = systemEmployeeDefinition(systemEmployeeSeedAgents(seed), new Date().toISOString());
2346
+ this.saveSiliconEmployee(employee);
2347
+ return employee;
2348
+ }
2349
+ const locked = systemEmployeeDefinition(existing.agents, new Date().toISOString(), existing);
2350
+ if (existing.name !== locked.name
2351
+ || existing.duty !== locked.duty
2352
+ || existing.prompt !== locked.prompt
2353
+ || existing.avatar !== locked.avatar
2354
+ || existing.archivedAt !== undefined
2355
+ || existing.agents.length === 0) {
2356
+ this.saveSiliconEmployee(locked);
2357
+ return locked;
2358
+ }
2359
+ return existing;
2360
+ }
2361
+ saveSiliconEmployee(employee) {
2362
+ this.db.prepare(`INSERT INTO silicon_employees (
2363
+ id, name, duty, prompt, avatar, agents_json, system_key, archived_at, created_at, updated_at
2364
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2365
+ ON CONFLICT(id) DO UPDATE SET
2366
+ name = excluded.name,
2367
+ duty = excluded.duty,
2368
+ prompt = excluded.prompt,
2369
+ avatar = excluded.avatar,
2370
+ agents_json = excluded.agents_json,
2371
+ system_key = excluded.system_key,
2372
+ archived_at = excluded.archived_at,
2373
+ updated_at = excluded.updated_at`).run(employee.id, employee.name, employee.duty, employee.prompt, employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt);
2374
+ }
2375
+ archiveSiliconEmployee(id, archivedAt = new Date().toISOString()) {
2376
+ this.db.prepare("UPDATE silicon_employees SET archived_at = ?, updated_at = ? WHERE id = ?").run(archivedAt, new Date().toISOString(), id);
2377
+ }
2378
+ unarchiveSiliconEmployee(id) {
2379
+ this.db.prepare("UPDATE silicon_employees SET archived_at = NULL, updated_at = ? WHERE id = ?").run(new Date().toISOString(), id);
2380
+ }
2381
+ deleteSiliconEmployee(id) {
2382
+ this.db.prepare("DELETE FROM silicon_employees WHERE id = ?").run(id);
2383
+ }
2255
2384
  // ============ AI Teams ============
2256
2385
  listAiTeams() {
2257
2386
  const rows = this.db.prepare("SELECT * FROM ai_teams ORDER BY created_at ASC").all();
@@ -2301,7 +2430,7 @@ export class WandStorage {
2301
2430
  * 一次查询建整张表,供 /api/tasks 每请求一次构建;不逐会话查询。
2302
2431
  */
2303
2432
  listAiTeamRunChatMarkers() {
2304
- const rows = this.db.prepare(`SELECT r.id, r.team_json, r.chat_session_id, t.name AS current_team_name
2433
+ const rows = this.db.prepare(`SELECT r.id, r.team_id, r.team_json, r.chat_session_id, t.name AS current_team_name
2305
2434
  FROM ai_team_runs r LEFT JOIN ai_teams t ON t.id = r.team_id
2306
2435
  WHERE r.chat_session_id IS NOT NULL AND r.chat_session_id <> '' ORDER BY r.created_at DESC`).all();
2307
2436
  const markers = new Map();
@@ -2312,6 +2441,7 @@ export class WandStorage {
2312
2441
  const rawTeam = safeJsonParse(typeof row.team_json === "string" ? row.team_json : null);
2313
2442
  markers.set(chatSessionId, {
2314
2443
  runId: String(row.id),
2444
+ teamId: String(row.team_id),
2315
2445
  teamName: typeof row.current_team_name === "string" ? row.current_team_name : String(rawTeam?.name ?? ""),
2316
2446
  memberCount: Array.isArray(rawTeam?.members) ? rawTeam.members.length : 0,
2317
2447
  });
@@ -2624,6 +2754,27 @@ export class WandStorage {
2624
2754
  this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
2625
2755
  }
2626
2756
  }
2757
+ function mapSiliconEmployeeRow(row) {
2758
+ const rawAgents = safeJsonParse(typeof row.agents_json === "string" ? row.agents_json : null);
2759
+ const agents = Array.isArray(rawAgents)
2760
+ ? rawAgents
2761
+ .map((candidate) => parseWandTaskAgent(candidate))
2762
+ .filter((candidate) => candidate !== null)
2763
+ : [];
2764
+ const systemKey = typeof row.system_key === "string" && row.system_key ? row.system_key : undefined;
2765
+ return {
2766
+ id: String(row.id),
2767
+ name: String(row.name),
2768
+ duty: String(row.duty ?? ""),
2769
+ prompt: String(row.prompt ?? ""),
2770
+ avatar: String(row.avatar ?? ""),
2771
+ agents,
2772
+ systemKey,
2773
+ archivedAt: typeof row.archived_at === "string" && row.archived_at ? row.archived_at : undefined,
2774
+ createdAt: String(row.created_at),
2775
+ updatedAt: String(row.updated_at),
2776
+ };
2777
+ }
2627
2778
  function mapAiTeamRow(row) {
2628
2779
  return {
2629
2780
  id: String(row.id),
@@ -0,0 +1,40 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { StructuredExecHost } from "./structured-exec-host.js";
3
+ import type { StructuredRunnerAdapter, StructuredRunnerContext, StructuredRunnerExecution, StructuredRunnerObserver, StructuredRunnerTurnState } from "./structured-runner.js";
4
+ import type { SessionSnapshot } from "./types.js";
5
+ export type GeminiTurnState = StructuredRunnerTurnState;
6
+ /**
7
+ * Gemini CLI 的会话写在 `~/.gemini/tmp/<project_hash>/chats/`。ID 找不回来时 headless
8
+ * 会以 42 退出并打印下面这句,识别出来才能丢掉失效的 resume ID 重开一轮
9
+ * (与 Claude / Pi 的同类兜底一致)。
10
+ */
11
+ export declare function isMissingGeminiSession(error: string | null | undefined): boolean;
12
+ /**
13
+ * approval mode 映射。结构化 runner 没有权限桥,`default` 下需要确认的工具调用会被
14
+ * 直接拒绝;`yolo` 才是能连续干活的那一档。
15
+ */
16
+ export declare function geminiApprovalMode(session: SessionSnapshot): "default" | "auto_edit" | "yolo";
17
+ /**
18
+ * `-p ""` 只把 CLI 钉在 headless 模式,真正的 prompt 走 stdin(Gemini CLI 把 stdin
19
+ * 内容与 `-p` 内容用空行拼接,`-p` 为空时只剩 stdin)。这样长消息不撞 argv 上限,
20
+ * 也不会出现在 `ps` 里。
21
+ *
22
+ * `--skip-trust` 必须带:headless 模式下 Gemini CLI 会检查工作区信任状态,未信任的
23
+ * 目录会直接以 55 退出,并把 `--approval-mode` 降级成 `default`。Wand 是在用户自己
24
+ * 选定的工作目录里代表用户启动这个 CLI,与其它 provider 的 auto-approve 语义一致。
25
+ */
26
+ export declare function buildGeminiArgs(session: SessionSnapshot): string[];
27
+ export declare function geminiToolName(name: string): string;
28
+ /**
29
+ * Apply one Gemini CLI `--output-format stream-json` event.
30
+ *
31
+ * assistant `message` 事件带 `delta: true`,每个 chunk 是增量而不是快照,所以文本是
32
+ * 追加而不是替换。tool_use / tool_result 由 CLI 直接按 tool_id 配对,不需要 upsert。
33
+ */
34
+ export declare function applyGeminiEvent(state: GeminiTurnState, event: Record<string, unknown>): string | null;
35
+ export declare class GeminiRunner implements StructuredRunnerAdapter {
36
+ private readonly spawnProcess;
37
+ private readonly execHost?;
38
+ constructor(spawnProcess?: typeof spawn, execHost?: StructuredExecHost | undefined);
39
+ start(context: StructuredRunnerContext, observer: StructuredRunnerObserver): StructuredRunnerExecution;
40
+ }