@co0ontty/wand 4.76.0 → 4.77.0-beta.g7fb0b70

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 (71) hide show
  1. package/dist/agent-dispatch.d.ts +46 -0
  2. package/dist/agent-dispatch.js +84 -0
  3. package/dist/ai-team-availability.d.ts +69 -0
  4. package/dist/ai-team-availability.js +100 -0
  5. package/dist/ai-team-prompts.d.ts +113 -0
  6. package/dist/ai-team-prompts.js +435 -0
  7. package/dist/ai-team-runner.d.ts +241 -0
  8. package/dist/ai-team-runner.js +1370 -0
  9. package/dist/ai-team-types.d.ts +147 -0
  10. package/dist/ai-team-types.js +32 -0
  11. package/dist/attention.d.ts +40 -0
  12. package/dist/attention.js +86 -0
  13. package/dist/build-info.json +4 -4
  14. package/dist/claude-sdk-runner.d.ts +5 -0
  15. package/dist/claude-sdk-runner.js +2 -1
  16. package/dist/commit-task-archive.d.ts +3 -0
  17. package/dist/commit-task-archive.js +43 -0
  18. package/dist/config.js +0 -2
  19. package/dist/git-quick-commit.d.ts +2 -1
  20. package/dist/git-quick-commit.js +72 -60
  21. package/dist/message-truncator.d.ts +41 -5
  22. package/dist/message-truncator.js +255 -38
  23. package/dist/missions.d.ts +4 -2
  24. package/dist/missions.js +2 -2
  25. package/dist/process-manager.d.ts +1 -0
  26. package/dist/process-manager.js +17 -8
  27. package/dist/prompt-optimizer.js +13 -13
  28. package/dist/server-ai-team-routes.d.ts +11 -0
  29. package/dist/server-ai-team-routes.js +283 -0
  30. package/dist/server-attention-routes.d.ts +9 -0
  31. package/dist/server-attention-routes.js +26 -0
  32. package/dist/server-file-routes.js +1 -1
  33. package/dist/server-session-routes.js +41 -5
  34. package/dist/server-settings-routes.js +1 -1
  35. package/dist/server-task-routes.d.ts +2 -0
  36. package/dist/server-task-routes.js +6 -40
  37. package/dist/server-workspace-routes.js +13 -5
  38. package/dist/server.js +43 -0
  39. package/dist/session-topic.js +13 -13
  40. package/dist/session-transport.d.ts +3 -0
  41. package/dist/session-transport.js +1 -0
  42. package/dist/storage.d.ts +54 -1
  43. package/dist/storage.js +413 -2
  44. package/dist/structured-claude-adapter.d.ts +5 -0
  45. package/dist/structured-claude-adapter.js +13 -2
  46. package/dist/structured-claude-protocol.d.ts +2 -0
  47. package/dist/structured-claude-protocol.js +4 -0
  48. package/dist/structured-codex-adapter.js +2 -2
  49. package/dist/structured-grok-adapter.js +2 -1
  50. package/dist/structured-opencode-adapter.js +2 -2
  51. package/dist/structured-pi-adapter.js +2 -1
  52. package/dist/structured-provider-common.d.ts +17 -0
  53. package/dist/structured-provider-common.js +52 -0
  54. package/dist/structured-qoder-adapter.js +2 -1
  55. package/dist/structured-session-manager.d.ts +18 -1
  56. package/dist/structured-session-manager.js +67 -10
  57. package/dist/system-ai.d.ts +13 -2
  58. package/dist/system-ai.js +9 -5
  59. package/dist/task-title.js +10 -10
  60. package/dist/types.d.ts +25 -2
  61. package/dist/wand-task-sync.d.ts +1 -0
  62. package/dist/wand-task-sync.js +1 -0
  63. package/dist/web-ui/content/ai-teams.js +12 -0
  64. package/dist/web-ui/content/scripts.js +86 -5301
  65. package/dist/web-ui/content/styles.css +1 -1
  66. package/dist/web-ui/embedded-assets.d.ts +1 -0
  67. package/dist/web-ui/embedded-assets.js +3 -2
  68. package/dist/web-ui/scripts.d.ts +8 -0
  69. package/dist/web-ui/scripts.js +28 -2
  70. package/dist/ws-broadcast.js +52 -19
  71. package/package.json +1 -1
package/dist/storage.js CHANGED
@@ -4,9 +4,10 @@ import path from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { GLOBAL_WORKSPACE_ID } from "./types.js";
6
6
  import { normalizeSessionDirectory } from "./session-directory-tree.js";
7
- import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider } from "./session-provider.js";
7
+ import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider, SESSION_PROVIDERS } from "./session-provider.js";
8
8
  import { DEFAULT_ITERATION_NAME, DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, normalizeWandTaskAgentMode } from "./task-types.js";
9
9
  import { firstLayoutTabId } from "./layout-tree.js";
10
+ import { AI_TEAM_DEFAULT_MAX_STEPS, isTeamMemberRole, memberAgents } from "./ai-team-types.js";
10
11
  import { isThinkingEffort } from "./structured-provider-common.js";
11
12
  import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
12
13
  function safeJsonParse(raw) {
@@ -24,7 +25,11 @@ function safeJsonParse(raw) {
24
25
  * 前端据此显示「未指定 CLI 工具」,而不是渲染出半个派发配置。
25
26
  */
26
27
  export function parseWandTaskAgent(raw) {
27
- const value = typeof raw === "string" ? safeJsonParse(raw) : undefined;
28
+ const value = typeof raw === "string"
29
+ ? safeJsonParse(raw)
30
+ : raw && typeof raw === "object" && !Array.isArray(raw)
31
+ ? raw
32
+ : undefined;
28
33
  if (!value || typeof value !== "object")
29
34
  return null;
30
35
  const provider = value.provider;
@@ -170,6 +175,7 @@ function serializeSessionOptions(snapshot) {
170
175
  providerCliExitCode: snapshot.providerCliExitCode,
171
176
  currentTaskTitle: snapshot.currentTaskTitle,
172
177
  summary: snapshot.summary,
178
+ systemPrompt: snapshot.systemPrompt,
173
179
  };
174
180
  return JSON.stringify(options);
175
181
  }
@@ -231,6 +237,8 @@ function parseSessionOptions(raw) {
231
237
  options.currentTaskTitle = parsed.currentTaskTitle;
232
238
  if (typeof parsed.summary === "string")
233
239
  options.summary = parsed.summary;
240
+ if (typeof parsed.systemPrompt === "string")
241
+ options.systemPrompt = parsed.systemPrompt;
234
242
  return options;
235
243
  }
236
244
  function parseQueuedMessages(raw) {
@@ -772,7 +780,76 @@ const INIT_SQL = `
772
780
  FOREIGN KEY (session_id) REFERENCES command_sessions(id) ON DELETE CASCADE
773
781
  );
774
782
  CREATE INDEX IF NOT EXISTS idx_wand_task_sessions_session ON wand_task_sessions(session_id);
783
+
784
+ CREATE TABLE IF NOT EXISTS ai_teams (
785
+ id TEXT PRIMARY KEY,
786
+ name TEXT NOT NULL,
787
+ description TEXT NOT NULL DEFAULT '',
788
+ instructions TEXT NOT NULL DEFAULT '',
789
+ members_json TEXT NOT NULL,
790
+ require_plan_approval INTEGER NOT NULL DEFAULT 1,
791
+ max_steps INTEGER NOT NULL DEFAULT 30,
792
+ created_at TEXT NOT NULL,
793
+ updated_at TEXT NOT NULL
794
+ );
795
+
796
+ CREATE TABLE IF NOT EXISTS ai_team_runs (
797
+ id TEXT PRIMARY KEY,
798
+ team_id TEXT NOT NULL,
799
+ task_id TEXT NOT NULL,
800
+ team_json TEXT NOT NULL,
801
+ objective TEXT NOT NULL,
802
+ cwd TEXT NOT NULL,
803
+ status TEXT NOT NULL,
804
+ status_detail TEXT NOT NULL DEFAULT '',
805
+ steps_used INTEGER NOT NULL DEFAULT 0,
806
+ step_limit INTEGER NOT NULL,
807
+ format_retries INTEGER NOT NULL DEFAULT 0,
808
+ plan_approved INTEGER NOT NULL DEFAULT 0,
809
+ created_at TEXT NOT NULL,
810
+ updated_at TEXT NOT NULL
811
+ );
812
+ CREATE INDEX IF NOT EXISTS idx_ai_team_runs_task ON ai_team_runs(task_id, created_at);
813
+ CREATE INDEX IF NOT EXISTS idx_ai_team_runs_status ON ai_team_runs(status);
814
+
815
+ CREATE TABLE IF NOT EXISTS ai_team_steps (
816
+ id TEXT PRIMARY KEY,
817
+ run_id TEXT NOT NULL,
818
+ seq INTEGER NOT NULL,
819
+ kind TEXT NOT NULL,
820
+ member_id TEXT NOT NULL,
821
+ title TEXT NOT NULL,
822
+ instructions TEXT NOT NULL,
823
+ session_id TEXT,
824
+ status TEXT NOT NULL,
825
+ report TEXT NOT NULL DEFAULT '',
826
+ report_path TEXT NOT NULL,
827
+ depends_on_json TEXT NOT NULL DEFAULT '[]',
828
+ started_at TEXT,
829
+ ended_at TEXT,
830
+ UNIQUE (run_id, seq)
831
+ );
832
+ CREATE INDEX IF NOT EXISTS idx_ai_team_steps_session ON ai_team_steps(session_id, status);
775
833
  `;
834
+ /** AI 团队表的后加列;只加不删,历史行取默认值。 */
835
+ function ensureAiTeamSchema(db) {
836
+ const teamColumns = new Set(db.prepare("PRAGMA table_info(ai_teams)").all().map((column) => column.name));
837
+ if (!teamColumns.has("instructions"))
838
+ db.exec("ALTER TABLE ai_teams ADD COLUMN instructions TEXT NOT NULL DEFAULT ''");
839
+ const stepColumns = new Set(db.prepare("PRAGMA table_info(ai_team_steps)").all().map((column) => column.name));
840
+ if (!stepColumns.has("depends_on_json"))
841
+ db.exec("ALTER TABLE ai_team_steps ADD COLUMN depends_on_json TEXT NOT NULL DEFAULT '[]'");
842
+ const runColumns = new Set(db.prepare("PRAGMA table_info(ai_team_runs)").all().map((column) => column.name));
843
+ if (!runColumns.has("chat_session_id"))
844
+ db.exec("ALTER TABLE ai_team_runs ADD COLUMN chat_session_id TEXT");
845
+ if (!runColumns.has("pending_notes_json"))
846
+ db.exec("ALTER TABLE ai_team_runs ADD COLUMN pending_notes_json TEXT NOT NULL DEFAULT '[]'");
847
+ // v2 多候选降级:步骤实际候选与跳过记录(StepDispatchInfo)、run 级状态(黑名单等)。只加列,旧行取默认 '{}'。
848
+ if (!stepColumns.has("dispatch_info_json"))
849
+ db.exec("ALTER TABLE ai_team_steps ADD COLUMN dispatch_info_json TEXT NOT NULL DEFAULT '{}'");
850
+ if (!runColumns.has("run_state_json"))
851
+ db.exec("ALTER TABLE ai_team_runs ADD COLUMN run_state_json TEXT NOT NULL DEFAULT '{}'");
852
+ }
776
853
  function ensureWandTaskSchema(db) {
777
854
  const columns = db.prepare("PRAGMA table_info(wand_tasks)").all();
778
855
  const names = new Set(columns.map((column) => column.name));
@@ -903,6 +980,7 @@ export function ensureDatabaseFile(dbPath) {
903
980
  ensureWandTaskSchema(db);
904
981
  ensureWandMilestoneSchema(db);
905
982
  ensureIterationPromptSchema(db);
983
+ ensureAiTeamSchema(db);
906
984
  ensureConnectorSchema(db);
907
985
  {
908
986
  const missionColumns = db.prepare("PRAGMA table_info(missions)").all();
@@ -935,6 +1013,7 @@ export class WandStorage {
935
1013
  ensureWandTaskSchema(this.db);
936
1014
  ensureWandMilestoneSchema(this.db);
937
1015
  ensureIterationPromptSchema(this.db);
1016
+ ensureAiTeamSchema(this.db);
938
1017
  ensureConnectorSchema(this.db);
939
1018
  this.ensureDefaultPasswordVault();
940
1019
  }
@@ -1197,6 +1276,22 @@ export class WandStorage {
1197
1276
  .all();
1198
1277
  return new Map(rows.map((row) => [row.id, Number(row.n) || 0]));
1199
1278
  }
1279
+ /** Count user-initiated CLI launches (one per session), including archived sessions. */
1280
+ countInteractiveSessionsByProvider() {
1281
+ const counts = Object.fromEntries(SESSION_PROVIDERS.map((provider) => [provider, 0]));
1282
+ const rows = this.db.prepare(`SELECT provider, runner, command, COUNT(*) AS n
1283
+ FROM command_sessions
1284
+ WHERE session_source = 'interactive'
1285
+ GROUP BY provider, runner, command`).all();
1286
+ for (const row of rows) {
1287
+ const provider = isSessionProvider(row.provider)
1288
+ ? row.provider
1289
+ : inferProviderFromRunner(row.runner) ?? inferProviderFromCommand(row.command);
1290
+ if (provider)
1291
+ counts[provider] += Number(row.n) || 0;
1292
+ }
1293
+ return counts;
1294
+ }
1200
1295
  /** Sessions not yet attached to a project. Omits messages/output. */
1201
1296
  listUnboundSessionBindings() {
1202
1297
  const rows = this.db
@@ -1870,6 +1965,126 @@ export class WandStorage {
1870
1965
  deleteExpiredAuthSessions(now) {
1871
1966
  this.db.prepare("DELETE FROM auth_sessions WHERE expires_at < ?").run(now);
1872
1967
  }
1968
+ // ============ AI Teams ============
1969
+ listAiTeams() {
1970
+ const rows = this.db.prepare("SELECT * FROM ai_teams ORDER BY created_at ASC").all();
1971
+ return rows.map(mapAiTeamRow);
1972
+ }
1973
+ getAiTeam(id) {
1974
+ const row = this.db.prepare("SELECT * FROM ai_teams WHERE id = ?").get(id);
1975
+ return row ? mapAiTeamRow(row) : null;
1976
+ }
1977
+ saveAiTeam(team) {
1978
+ this.db.prepare(`INSERT INTO ai_teams (
1979
+ id, name, description, instructions, members_json, require_plan_approval, max_steps,
1980
+ created_at, updated_at
1981
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1982
+ ON CONFLICT(id) DO UPDATE SET
1983
+ name = excluded.name, description = excluded.description,
1984
+ instructions = excluded.instructions,
1985
+ members_json = excluded.members_json,
1986
+ require_plan_approval = excluded.require_plan_approval,
1987
+ max_steps = excluded.max_steps, updated_at = excluded.updated_at`).run(team.id, team.name, team.description, team.instructions, JSON.stringify(team.members), team.requirePlanApproval ? 1 : 0, team.maxSteps, team.createdAt, team.updatedAt);
1988
+ }
1989
+ /** 只删团队定义;已有运行保存了团队快照,不受影响。 */
1990
+ deleteAiTeam(id) {
1991
+ this.db.prepare("DELETE FROM ai_teams WHERE id = ?").run(id);
1992
+ }
1993
+ saveAiTeamRun(run) {
1994
+ this.db.prepare(`INSERT INTO ai_team_runs (
1995
+ id, team_id, task_id, team_json, objective, cwd, status, status_detail,
1996
+ steps_used, step_limit, format_retries, plan_approved, chat_session_id, pending_notes_json,
1997
+ created_at, updated_at
1998
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1999
+ ON CONFLICT(id) DO UPDATE SET
2000
+ team_json = excluded.team_json,
2001
+ status = excluded.status, status_detail = excluded.status_detail,
2002
+ steps_used = excluded.steps_used, step_limit = excluded.step_limit,
2003
+ format_retries = excluded.format_retries, plan_approved = excluded.plan_approved,
2004
+ chat_session_id = excluded.chat_session_id, pending_notes_json = excluded.pending_notes_json,
2005
+ updated_at = excluded.updated_at`).run(run.id, run.teamId, run.taskId, JSON.stringify(run.team), run.objective, run.cwd, run.status, run.statusDetail, run.stepsUsed, run.stepLimit, run.formatRetries, run.planApproved ? 1 : 0, run.chatSessionId, JSON.stringify(run.pendingNotes), run.createdAt, run.updatedAt);
2006
+ }
2007
+ /** 群聊会话对应的最近一次运行(同一个群聊可以接着开新运行)。 */
2008
+ getLatestAiTeamRunByChat(sessionId) {
2009
+ const row = this.db.prepare("SELECT * FROM ai_team_runs WHERE chat_session_id = ? ORDER BY created_at DESC LIMIT 1").get(sessionId);
2010
+ return row ? mapAiTeamRunRow(row) : null;
2011
+ }
2012
+ /**
2013
+ * 会话列表用的群聊标记索引:chat_session_id → 最近一次运行的入口信息。
2014
+ * 一次查询建整张表,供 /api/tasks 每请求一次构建;不逐会话查询。
2015
+ */
2016
+ listAiTeamRunChatMarkers() {
2017
+ const rows = this.db.prepare(`SELECT id, team_json, chat_session_id FROM ai_team_runs
2018
+ WHERE chat_session_id IS NOT NULL AND chat_session_id <> '' ORDER BY created_at DESC`).all();
2019
+ const markers = new Map();
2020
+ for (const row of rows) {
2021
+ const chatSessionId = typeof row.chat_session_id === "string" ? row.chat_session_id : "";
2022
+ if (!chatSessionId || markers.has(chatSessionId))
2023
+ continue;
2024
+ const rawTeam = safeJsonParse(typeof row.team_json === "string" ? row.team_json : null);
2025
+ markers.set(chatSessionId, {
2026
+ runId: String(row.id),
2027
+ teamName: String(rawTeam?.name ?? ""),
2028
+ memberCount: Array.isArray(rawTeam?.members) ? rawTeam.members.length : 0,
2029
+ });
2030
+ }
2031
+ return markers;
2032
+ }
2033
+ getAiTeamRun(id) {
2034
+ const row = this.db.prepare("SELECT * FROM ai_team_runs WHERE id = ?").get(id);
2035
+ return row ? mapAiTeamRunRow(row) : null;
2036
+ }
2037
+ listAiTeamRuns(filter = {}) {
2038
+ const where = [];
2039
+ const params = [];
2040
+ if (filter.taskId) {
2041
+ where.push("task_id = ?");
2042
+ params.push(filter.taskId);
2043
+ }
2044
+ if (filter.teamId) {
2045
+ where.push("team_id = ?");
2046
+ params.push(filter.teamId);
2047
+ }
2048
+ if (filter.statuses) {
2049
+ if (filter.statuses.length === 0)
2050
+ return [];
2051
+ where.push(`status IN (${filter.statuses.map(() => "?").join(", ")})`);
2052
+ params.push(...filter.statuses);
2053
+ }
2054
+ const rows = this.db.prepare(`SELECT * FROM ai_team_runs ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`
2055
+ + (filter.limit ? ` LIMIT ${Math.max(1, Math.floor(filter.limit))}` : "")).all(...params);
2056
+ return rows.map(mapAiTeamRunRow);
2057
+ }
2058
+ saveAiTeamStep(step) {
2059
+ this.db.prepare(`INSERT INTO ai_team_steps (
2060
+ id, run_id, seq, kind, member_id, title, instructions, session_id, status,
2061
+ report, report_path, depends_on_json, started_at, ended_at, dispatch_info_json
2062
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2063
+ ON CONFLICT(id) DO UPDATE SET
2064
+ session_id = excluded.session_id, status = excluded.status, report = excluded.report,
2065
+ instructions = excluded.instructions,
2066
+ depends_on_json = excluded.depends_on_json,
2067
+ dispatch_info_json = excluded.dispatch_info_json,
2068
+ started_at = excluded.started_at, ended_at = excluded.ended_at`).run(step.id, step.runId, step.seq, step.kind, step.memberId, step.title, step.instructions, step.sessionId, step.status, step.report, step.reportPath, JSON.stringify(step.dependsOn), step.startedAt, step.endedAt, JSON.stringify(step.dispatchInfo ?? { usedCandidate: 0, skipped: [] }));
2069
+ }
2070
+ listAiTeamSteps(runId) {
2071
+ const rows = this.db.prepare("SELECT * FROM ai_team_steps WHERE run_id = ? ORDER BY seq ASC").all(runId);
2072
+ return rows.map(mapAiTeamStepRow);
2073
+ }
2074
+ getRunningAiTeamStepBySession(sessionId) {
2075
+ const row = this.db.prepare("SELECT * FROM ai_team_steps WHERE session_id = ? AND status = 'running' LIMIT 1").get(sessionId);
2076
+ return row ? mapAiTeamStepRow(row) : null;
2077
+ }
2078
+ /** `ai_team_runs.run_state_json` 的读侧入口(§3.4);脏值 / 旧行退化成空黑名单。 */
2079
+ getAiTeamRunState(runId) {
2080
+ const row = this.db.prepare("SELECT run_state_json FROM ai_team_runs WHERE id = ?").get(runId);
2081
+ return parseAiTeamRunState(row?.run_state_json);
2082
+ }
2083
+ /** 只写 `run_state_json` 一列,不碰 `saveAiTeamRun` 负责的业务字段。 */
2084
+ setAiTeamRunState(runId, state) {
2085
+ this.db.prepare("UPDATE ai_team_runs SET run_state_json = ? WHERE id = ?")
2086
+ .run(JSON.stringify(state), runId);
2087
+ }
1873
2088
  // ============ Missions ============
1874
2089
  saveMission(mission) {
1875
2090
  this.db.prepare(`INSERT INTO missions (
@@ -2065,6 +2280,202 @@ export class WandStorage {
2065
2280
  this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
2066
2281
  }
2067
2282
  }
2283
+ function mapAiTeamRow(row) {
2284
+ return {
2285
+ id: String(row.id),
2286
+ name: String(row.name),
2287
+ description: String(row.description ?? ""),
2288
+ instructions: String(row.instructions ?? ""),
2289
+ members: normalizeAiTeamMembers(safeJsonParse(typeof row.members_json === "string" ? row.members_json : null)),
2290
+ requirePlanApproval: Number(row.require_plan_approval) !== 0,
2291
+ maxSteps: Number(row.max_steps) || AI_TEAM_DEFAULT_MAX_STEPS,
2292
+ createdAt: String(row.created_at),
2293
+ updatedAt: String(row.updated_at),
2294
+ };
2295
+ }
2296
+ /**
2297
+ * 旧行读端归一(§3.1 修正 B10):members 是从 members_json/team_json 解析出的裸 JSON,
2298
+ * 不满足 AiTeamMember 类型,必须逐成员经 memberAgents 补齐 agents 后才能当 AiTeamMember 用。
2299
+ * 没有任何可识别执行配置的成员行直接丢弃(旧数据必然带 agent,走到这里说明行已损坏)。
2300
+ */
2301
+ function normalizeAiTeamMember(raw) {
2302
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
2303
+ return null;
2304
+ const value = raw;
2305
+ const rawAgent = parseWandTaskAgent(value.agent) ?? undefined;
2306
+ const agents = memberAgents({
2307
+ agent: rawAgent,
2308
+ agents: Array.isArray(value.agents)
2309
+ ? value.agents.map((candidate) => parseWandTaskAgent(candidate)).filter((candidate) => candidate !== null)
2310
+ : undefined,
2311
+ });
2312
+ if (agents.length === 0)
2313
+ return null;
2314
+ const member = {
2315
+ id: String(value.id ?? ""),
2316
+ name: String(value.name ?? ""),
2317
+ duty: String(value.duty ?? ""),
2318
+ agents,
2319
+ agent: rawAgent ?? agents[0],
2320
+ isLeader: value.isLeader === true,
2321
+ };
2322
+ if (typeof value.avatar === "string" && value.avatar)
2323
+ member.avatar = value.avatar;
2324
+ if (isTeamMemberRole(value.role))
2325
+ member.role = value.role;
2326
+ return member;
2327
+ }
2328
+ function normalizeAiTeamMembers(raw) {
2329
+ if (!Array.isArray(raw))
2330
+ return [];
2331
+ const members = [];
2332
+ for (const item of raw) {
2333
+ const member = normalizeAiTeamMember(item);
2334
+ if (member)
2335
+ members.push(member);
2336
+ }
2337
+ return members;
2338
+ }
2339
+ function mapAiTeamRunRow(row) {
2340
+ const rawTeam = safeJsonParse(typeof row.team_json === "string" ? row.team_json : null);
2341
+ const team = rawTeam ? {
2342
+ id: String(rawTeam.id ?? row.team_id),
2343
+ name: String(rawTeam.name ?? ""),
2344
+ description: String(rawTeam.description ?? ""),
2345
+ instructions: String(rawTeam.instructions ?? ""),
2346
+ members: normalizeAiTeamMembers(rawTeam.members),
2347
+ requirePlanApproval: rawTeam.requirePlanApproval === undefined ? true : rawTeam.requirePlanApproval !== false,
2348
+ maxSteps: Number(rawTeam.maxSteps) || AI_TEAM_DEFAULT_MAX_STEPS,
2349
+ createdAt: String(rawTeam.createdAt ?? row.created_at),
2350
+ updatedAt: String(rawTeam.updatedAt ?? row.created_at),
2351
+ } : null;
2352
+ return {
2353
+ id: String(row.id),
2354
+ teamId: String(row.team_id),
2355
+ team: team ?? {
2356
+ id: String(row.team_id), name: "", description: "", instructions: "", members: [], requirePlanApproval: true,
2357
+ maxSteps: AI_TEAM_DEFAULT_MAX_STEPS, createdAt: String(row.created_at), updatedAt: String(row.created_at),
2358
+ },
2359
+ taskId: String(row.task_id),
2360
+ objective: String(row.objective),
2361
+ cwd: String(row.cwd),
2362
+ status: String(row.status),
2363
+ statusDetail: String(row.status_detail ?? ""),
2364
+ stepsUsed: Number(row.steps_used) || 0,
2365
+ stepLimit: Number(row.step_limit) || AI_TEAM_DEFAULT_MAX_STEPS,
2366
+ formatRetries: Number(row.format_retries) || 0,
2367
+ planApproved: Number(row.plan_approved) !== 0,
2368
+ chatSessionId: typeof row.chat_session_id === "string" && row.chat_session_id ? row.chat_session_id : null,
2369
+ pendingNotes: (safeJsonParse(typeof row.pending_notes_json === "string" ? row.pending_notes_json : null) ?? [])
2370
+ .filter((note) => typeof note === "string"),
2371
+ createdAt: String(row.created_at),
2372
+ updatedAt: String(row.updated_at),
2373
+ };
2374
+ }
2375
+ /** 候选失败类别的读侧白名单;真源是 `ai-team-types.ts` 的 `CandidateFailureKind`。 */
2376
+ const CANDIDATE_FAILURE_KINDS = [
2377
+ "spawn-missing", "host-disabled", "model-unknown", "startup-timeout",
2378
+ "runtime-failure", "format-error", "user-stop",
2379
+ ];
2380
+ function isCandidateFailureKind(value) {
2381
+ return CANDIDATE_FAILURE_KINDS.includes(value);
2382
+ }
2383
+ function nonEmptyString(value) {
2384
+ return typeof value === "string" && value.trim() ? value.trim() : null;
2385
+ }
2386
+ /**
2387
+ * `ai_team_steps.dispatch_info_json` 的读侧校验(§3.2)。脏值 / 旧行默认 `{}` 一律退化成
2388
+ * 「首选、没跳过任何候选」,坏条目逐条丢弃,不整字段作废——降级留痕是可观测性数据,
2389
+ * 不能因为一条脏记录把整条链丢掉。
2390
+ */
2391
+ export function parseStepDispatchInfo(raw) {
2392
+ const value = typeof raw === "string" ? safeJsonParse(raw) : raw;
2393
+ if (!value || typeof value !== "object" || Array.isArray(value))
2394
+ return { usedCandidate: 0, skipped: [] };
2395
+ const record = value;
2396
+ const skippedRaw = Array.isArray(record.skipped) ? record.skipped : [];
2397
+ const skipped = [];
2398
+ for (const item of skippedRaw) {
2399
+ if (!item || typeof item !== "object" || Array.isArray(item))
2400
+ continue;
2401
+ const entry = item;
2402
+ const agent = parseWandTaskAgent(entry.agent);
2403
+ const candidate = Number(entry.candidate);
2404
+ const reason = nonEmptyString(entry.reason);
2405
+ if (!agent || !Number.isInteger(candidate) || candidate < 0 || !reason)
2406
+ continue;
2407
+ // 未知类别按 runtime-failure 记:它不可降级,宁可少降一级也不因为脏数据错换候选。
2408
+ skipped.push({
2409
+ candidate,
2410
+ agent,
2411
+ reason,
2412
+ errorKind: isCandidateFailureKind(entry.errorKind) ? entry.errorKind : "runtime-failure",
2413
+ });
2414
+ }
2415
+ const usedRaw = Number(record.usedCandidate);
2416
+ const used = Number.isInteger(usedRaw) && usedRaw > 0 ? usedRaw : 0;
2417
+ // 与留痕自相矛盾的脏值以 skipped 链为准向下纠正,绝不凭空跳级。
2418
+ const trail = skipped.reduce((highest, item) => Math.max(highest, item.candidate), -1) + 1;
2419
+ return { usedCandidate: skipped.length > 0 ? Math.min(used, trail) : used, skipped };
2420
+ }
2421
+ /** `ai_team_runs.run_state_json` 的读侧校验(§3.4);脏值退化成空黑名单。 */
2422
+ export function parseAiTeamRunState(raw) {
2423
+ const value = typeof raw === "string" ? safeJsonParse(raw) : raw;
2424
+ if (!value || typeof value !== "object" || Array.isArray(value))
2425
+ return { providers: [], agents: [], strikes: {}, hostDisabled: [] };
2426
+ const record = value;
2427
+ const state = { providers: [], agents: [], strikes: {}, hostDisabled: [] };
2428
+ for (const item of Array.isArray(record.providers) ? record.providers : []) {
2429
+ const provider = nonEmptyString(item);
2430
+ if (isSessionProvider(provider) && !state.providers.includes(provider))
2431
+ state.providers.push(provider);
2432
+ }
2433
+ for (const item of Array.isArray(record.agents) ? record.agents : []) {
2434
+ if (!item || typeof item !== "object" || Array.isArray(item))
2435
+ continue;
2436
+ const entry = item;
2437
+ const key = nonEmptyString(entry.key);
2438
+ if (key && !state.agents.some((agent) => agent.key === key)) {
2439
+ state.agents.push({ key, kind: isCandidateFailureKind(entry.kind) ? entry.kind : "runtime-failure" });
2440
+ }
2441
+ }
2442
+ const strikes = record.strikes;
2443
+ if (strikes && typeof strikes === "object" && !Array.isArray(strikes)) {
2444
+ for (const [key, count] of Object.entries(strikes)) {
2445
+ const value2 = Number(count);
2446
+ if (key && Number.isFinite(value2) && value2 > 0)
2447
+ state.strikes[key] = Math.min(999, Math.floor(value2));
2448
+ }
2449
+ }
2450
+ for (const item of Array.isArray(record.hostDisabled) ? record.hostDisabled : []) {
2451
+ const kind = nonEmptyString(item);
2452
+ if (isWandTaskAgentKind(kind) && !state.hostDisabled.includes(kind))
2453
+ state.hostDisabled.push(kind);
2454
+ }
2455
+ return state;
2456
+ }
2457
+ function mapAiTeamStepRow(row) {
2458
+ const step = {
2459
+ id: String(row.id),
2460
+ runId: String(row.run_id),
2461
+ seq: Number(row.seq),
2462
+ kind: String(row.kind),
2463
+ memberId: String(row.member_id),
2464
+ title: String(row.title),
2465
+ instructions: String(row.instructions),
2466
+ sessionId: typeof row.session_id === "string" ? row.session_id : null,
2467
+ status: String(row.status),
2468
+ report: String(row.report ?? ""),
2469
+ reportPath: String(row.report_path),
2470
+ dependsOn: safeJsonParse(typeof row.depends_on_json === "string" ? row.depends_on_json : null) ?? [],
2471
+ startedAt: typeof row.started_at === "string" ? row.started_at : null,
2472
+ endedAt: typeof row.ended_at === "string" ? row.ended_at : null,
2473
+ };
2474
+ const dispatchInfo = parseStepDispatchInfo(typeof row.dispatch_info_json === "string" ? row.dispatch_info_json : null);
2475
+ if (dispatchInfo.usedCandidate > 0 || dispatchInfo.skipped.length > 0)
2476
+ step.dispatchInfo = dispatchInfo;
2477
+ return step;
2478
+ }
2068
2479
  function mapMissionRow(row) {
2069
2480
  return {
2070
2481
  id: String(row.id),
@@ -8,6 +8,11 @@ export interface PermissionPolicy {
8
8
  }
9
9
  export declare function derivePermissionPolicy(mode: ExecutionMode, autoApprove: boolean, cwd: string): PermissionPolicy;
10
10
  export declare function buildAppendSystemPromptParts(language: string | undefined, mode: ExecutionMode): string[];
11
+ /**
12
+ * 完整 append 列表:模式 / 语言指令 + 会话级系统提示(团队、自动化会话的角色与规则)。
13
+ * CLI 与 SDK 两条路径共用,避免只改一边。
14
+ */
15
+ export declare function buildSessionSystemPromptParts(session: Pick<SessionSnapshot, "mode" | "systemPrompt">, language: string | undefined): string[];
11
16
  export interface ClaudeCliArgsOptions {
12
17
  permissionPolicy: PermissionPolicy;
13
18
  systemPromptParts?: string[];
@@ -92,6 +92,17 @@ export function buildAppendSystemPromptParts(language, mode) {
92
92
  }
93
93
  return parts;
94
94
  }
95
+ /**
96
+ * 完整 append 列表:模式 / 语言指令 + 会话级系统提示(团队、自动化会话的角色与规则)。
97
+ * CLI 与 SDK 两条路径共用,避免只改一边。
98
+ */
99
+ export function buildSessionSystemPromptParts(session, language) {
100
+ const parts = buildAppendSystemPromptParts(language, session.mode);
101
+ const sessionPrompt = session.systemPrompt?.trim();
102
+ if (sessionPrompt)
103
+ parts.push(sessionPrompt);
104
+ return parts;
105
+ }
95
106
  export function buildClaudeCliArgs(session, options) {
96
107
  const args = ["-p", "--verbose", "--output-format", "stream-json"];
97
108
  if (options.permissionPolicy.permissionMode !== "default") {
@@ -129,7 +140,7 @@ export class ClaudeCliRunner {
129
140
  const permissionPolicy = derivePermissionPolicy(context.session.mode, context.session.autoApprovePermissions ?? false, context.session.cwd);
130
141
  const args = buildClaudeCliArgs(context.session, {
131
142
  permissionPolicy,
132
- systemPromptParts: buildAppendSystemPromptParts(this.options.language?.(), context.session.mode),
143
+ systemPromptParts: buildSessionSystemPromptParts(context.session, this.options.language?.()),
133
144
  });
134
145
  const reducer = new ClaudeCliProtocolReducer(context.session);
135
146
  let killedForQuestion = false;
@@ -179,7 +190,7 @@ export class ClaudeCliRunner {
179
190
  signal,
180
191
  stderr: ctx.stderr,
181
192
  stdoutTail,
182
- primaryError: null,
193
+ primaryError: reducer.errorText,
183
194
  stopReason: killedForQuestion ? "ask-user-question" : undefined,
184
195
  spawnError,
185
196
  }),
@@ -20,6 +20,8 @@ interface ClaudeCliTurnState extends StructuredRunnerTurnState {
20
20
  export declare class ClaudeCliProtocolReducer {
21
21
  readonly state: ClaudeCliTurnState;
22
22
  askUserQuestionDetected: boolean;
23
+ /** result 事件带 is_error 时的说明(如「API Error: 503 …」),作为这一轮的主错误。 */
24
+ errorText: string | null;
23
25
  private readonly blocksByKey;
24
26
  private readonly keyOrder;
25
27
  private readonly taskMetaRegistry;
@@ -121,6 +121,8 @@ export function extractClaudeAssistantMessage(message) {
121
121
  export class ClaudeCliProtocolReducer {
122
122
  state;
123
123
  askUserQuestionDetected = false;
124
+ /** result 事件带 is_error 时的说明(如「API Error: 503 …」),作为这一轮的主错误。 */
125
+ errorText = null;
124
126
  blocksByKey = new Map();
125
127
  keyOrder = [];
126
128
  taskMetaRegistry = new Map();
@@ -186,6 +188,8 @@ export class ClaudeCliProtocolReducer {
186
188
  if (event.type === "result") {
187
189
  if (typeof event.result === "string")
188
190
  this.state.result = event.result.trim();
191
+ if (event.is_error === true && this.state.result)
192
+ this.errorText = this.state.result;
189
193
  this.state.model = extractClaudeModelName(event.modelUsage && typeof event.modelUsage === "object"
190
194
  ? event.modelUsage
191
195
  : undefined) ?? this.state.model;
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { startStructuredCli } from "./structured-exec-pump.js";
3
3
  import { CodexProtocolReducer } from "./structured-codex-protocol.js";
4
4
  import { whichSync } from "./path-repair.js";
5
- import { thinkingEffortToCodexReasoningEffort } from "./structured-provider-common.js";
5
+ import { thinkingEffortToCodexReasoningEffort, promptWithSystemFallback } from "./structured-provider-common.js";
6
6
  /** Build the stable CLI contract for a structured Codex turn. */
7
7
  export function buildCodexArgs(session) {
8
8
  const args = ["exec", "--json", "--color", "never"];
@@ -49,7 +49,7 @@ export class CodexRunner {
49
49
  args,
50
50
  cwd: context.session.cwd,
51
51
  env: context.env,
52
- stdinData: context.prompt,
52
+ stdinData: promptWithSystemFallback(context.session, context.prompt),
53
53
  observer,
54
54
  execHost: this.execHost,
55
55
  spawnProcess: this.spawnProcess,
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { startStructuredCli } from "./structured-exec-pump.js";
3
3
  import { asRecord } from "./structured-content.js";
4
- import { thinkingEffortToGrokEffort } from "./structured-provider-common.js";
4
+ import { systemPromptArgs, thinkingEffortToGrokEffort } from "./structured-provider-common.js";
5
5
  function grokText(value) {
6
6
  if (typeof value === "string")
7
7
  return value;
@@ -147,6 +147,7 @@ export function buildGrokArgs(session, prompt) {
147
147
  }
148
148
  if (session.claudeSessionId)
149
149
  args.push("--resume", session.claudeSessionId);
150
+ args.push(...systemPromptArgs(session));
150
151
  return args;
151
152
  }
152
153
  /** Apply one official Grok Build `streaming-json` event. */
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
3
  import { startStructuredCli } from "./structured-exec-pump.js";
4
4
  import { asRecord } from "./structured-content.js";
5
- import { thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
5
+ import { thinkingEffortToOpenCodeVariant, promptWithSystemFallback } from "./structured-provider-common.js";
6
6
  function extractStructuredText(value) {
7
7
  if (typeof value === "string")
8
8
  return value;
@@ -135,7 +135,7 @@ export class OpenCodeRunner {
135
135
  args,
136
136
  cwd: context.session.cwd,
137
137
  env: context.env,
138
- stdinData: context.prompt,
138
+ stdinData: promptWithSystemFallback(context.session, context.prompt),
139
139
  observer,
140
140
  execHost: this.execHost,
141
141
  spawnProcess: this.spawnProcess,
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { startStructuredCli } from "./structured-exec-pump.js";
3
3
  import { asRecord, normalizeStructuredToolResultContent } from "./structured-content.js";
4
- import { thinkingEffortToPiLevel } from "./structured-provider-common.js";
4
+ import { systemPromptArgs, thinkingEffortToPiLevel } from "./structured-provider-common.js";
5
5
  export function isMissingPiSession(error, sessionId) {
6
6
  return !!sessionId && !!error?.includes(`No session found matching '${sessionId}'`);
7
7
  }
@@ -41,6 +41,7 @@ export function buildPiArgs(session, prompt) {
41
41
  const thinking = thinkingEffortToPiLevel(session.thinkingEffort);
42
42
  if (thinking)
43
43
  args.push("--thinking", thinking);
44
+ args.push(...systemPromptArgs(session));
44
45
  if (session.claudeSessionId)
45
46
  args.push("--session", session.claudeSessionId);
46
47
  args.push(prompt);
@@ -1,4 +1,21 @@
1
1
  import type { SessionProvider, SessionRunner, SessionSnapshot, StructuredSessionState, WandConfig } from "./types.js";
2
+ /**
3
+ * 把会话级系统提示交给 CLI 的开关。Claude / Qoder / Pi 用 `--append-system-prompt`,
4
+ * Grok 用 `--rules`;Codex / OpenCode 没有这个入口,只能退回并入本轮消息。
5
+ * 各 provider 的支持情况由 `--help` 实测得出,新增 provider 前先验。
6
+ */
7
+ export declare function systemPromptFlag(provider: SessionProvider | null | undefined): string | null;
8
+ /** 结构化 runner 的 args:provider 支持时把系统提示当成独立参数传。 */
9
+ export declare function systemPromptArgs(session: Pick<SessionSnapshot, "provider" | "systemPrompt">): string[];
10
+ /**
11
+ * provider 没有系统提示通道时的兜底:把它放在本会话第一条消息最前面,并写明它不是用户输入。
12
+ * 只在首轮拼一次:后续轮次的消息已经接在有它的对话历史后面。有通道的 provider 不会走这里。
13
+ */
14
+ export declare function promptWithSystemFallback(session: Pick<SessionSnapshot, "provider" | "systemPrompt" | "messages">, prompt: string): string;
15
+ /** provider 没有系统提示通道时的并接格式:明确标出这不是用户输入。 */
16
+ export declare function composeSystemFallback(systemPrompt: string, prompt: string): string;
17
+ /** 把系统提示接到 CLI 命令上(provider 支持时);不支持时原样返回,由调用方并进首条输入。 */
18
+ export declare function commandWithSystemPrompt(command: string, provider: SessionProvider | null | undefined, systemPrompt: string | null | undefined): string;
2
19
  /** 判断任意值是否为合法的 thinking effort(旧四档,或 `provider:level`)。 */
3
20
  export declare function isThinkingEffort(value: unknown): value is NonNullable<SessionSnapshot["thinkingEffort"]>;
4
21
  /** 只取本 provider 的原生档位。别的 CLI 的前缀不能串过去。 */