@co0ontty/wand 4.76.0 → 4.77.0-beta.g6c4df97

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
@@ -22,56 +22,272 @@ const MESSAGE_WINDOW_SIZE = 40;
22
22
  */
23
23
  const MESSAGE_BLOCK_WINDOW = 60;
24
24
  /**
25
- * 块级窗口:取完整历史「最近 blockBudget 个内容块」并做 transport 截断。
26
- * 从最新 turn 往回累计块数,能整条放下就整条放,放不下的那条(最旧的入窗 turn)
27
- * 只取其尾部若干块,并通过 leadingBlockOffset 告知客户端「这条 turn 还有更早的块」。
28
- * 客户端先按块翻完这条 turn 的头部,再按 turn 往前翻更早的整条。
25
+ * 首次下发(首屏)的载荷上限:截断后的聊天 JSON 体积。整段历史在这个体积内就不做窗口化 ——
26
+ * 一条提示词 + 一段长回复的会话不该因为几十个默认收起的工具块就出现「更早消息」。
27
+ * 超出的会话才按下面的预算从尾部切:客户端先翻这条 turn 的头部块,再按 turn 往前翻。
29
28
  */
30
- export function blockWindowMessagesForTransport(all, cardDefaults, blockBudget = MESSAGE_BLOCK_WINDOW) {
29
+ export const MESSAGE_FIRST_PAINT_BYTES = 1024 * 1024;
30
+ /**
31
+ * 吸附回退的额外载荷上限:折叠段往回吃掉的体积超过它就改为跳过整段,
32
+ * 而不是把整段拉进首屏(窗口的目标是压住首屏载荷,吸附不能反过来把它撑大)。
33
+ */
34
+ const MAX_BLOCK_SNAP_BACK_BYTES = 128 * 1024;
35
+ /** 每个块在客户端是否「默认收起」(思考块 + 默认收起的工具卡片,与传输截断同一口径)。 */
36
+ export function collapsedBlockFlags(content, cardDefaults) {
37
+ const toolNameById = new Map();
38
+ for (const block of content) {
39
+ if (block.type === "tool_use")
40
+ toolNameById.set(block.id, block.name);
41
+ }
42
+ return content.map((block) => {
43
+ switch (block.type) {
44
+ case "thinking":
45
+ return cardDefaults.thinking !== true;
46
+ case "tool_use":
47
+ return isToolDefaultCollapsed(block.name, cardDefaults);
48
+ case "tool_result":
49
+ return isToolDefaultCollapsed(toolNameById.get(block.tool_use_id) ?? "", cardDefaults);
50
+ default:
51
+ return false;
52
+ }
53
+ });
54
+ }
55
+ /**
56
+ * 单块在传输层的大致字节数(与 truncateMessagesForTransport 的截断口径一致,
57
+ * 否则「体积预算」会把已经截断的大块算得过重)。
58
+ */
59
+ function blockTransportBytes(block, cardDefaults, toolNameById) {
60
+ switch (block.type) {
61
+ case "text":
62
+ return block.text.length;
63
+ case "thinking":
64
+ return block.thinking.length;
65
+ case "tool_use": {
66
+ const use = block;
67
+ let bytes = use.id.length + use.name.length + 32;
68
+ if (use.input)
69
+ bytes += JSON.stringify(use.input)?.length ?? 0;
70
+ if (use.description)
71
+ bytes += use.description.length;
72
+ return bytes;
73
+ }
74
+ case "tool_result": {
75
+ const result = block;
76
+ const raw = getContentString(result.content);
77
+ const collapsed = isToolDefaultCollapsed(toolNameById.get(result.tool_use_id) ?? "", cardDefaults);
78
+ const truncated = collapsed && !result.is_error &&
79
+ !contentHasStructuredImage(result.content) && raw.length > TRUNCATION_THRESHOLD;
80
+ return truncated ? SUMMARY_LENGTH + 4 : raw.length;
81
+ }
82
+ default:
83
+ return 0;
84
+ }
85
+ }
86
+ /**
87
+ * 一段内容在传输层的估算体积。`limit` 用来提前收尾:一旦确认超过它就不再往下算
88
+ * (预算判定只需要知道「超了」,不需要精确值)。
89
+ */
90
+ export function contentTransportBytes(content, cardDefaults, limit = Number.POSITIVE_INFINITY) {
91
+ const toolNameById = new Map();
92
+ for (const block of content) {
93
+ if (block.type === "tool_use")
94
+ toolNameById.set(block.id, block.name);
95
+ }
96
+ let total = 0;
97
+ for (const block of content) {
98
+ total += blockTransportBytes(block, cardDefaults, toolNameById) + 32;
99
+ if (total > limit)
100
+ return total;
101
+ }
102
+ return total;
103
+ }
104
+ /**
105
+ * 整段历史(或一条 turn)在可见条数 / 载荷两个预算内是否放得下。
106
+ * 任一预算被突破就立即返回 false,不把整段内容算完。
107
+ */
108
+ function fitsBudgets(turns, cardDefaults, visibleBudget, byteBudget) {
109
+ let visible = 0;
110
+ let bytes = 0;
111
+ for (let i = turns.length - 1; i >= 0; i -= 1) {
112
+ const content = turns[i].content;
113
+ visible += visibleBlockCount(content, cardDefaults, content.length);
114
+ if (visible > visibleBudget)
115
+ return false;
116
+ bytes += contentTransportBytes(content, cardDefaults, byteBudget - bytes);
117
+ if (bytes > byteBudget)
118
+ return false;
119
+ }
120
+ return true;
121
+ }
122
+ /**
123
+ * 从尾部往前取到两个预算用完为止,返回该起点:
124
+ * 默认收起的工具 / 思考块不占可见条数预算(它们在客户端合并成一条折叠条),只算体积。
125
+ */
126
+ function cutStartByBudget(content, cardDefaults, visibleBudget, byteBudget) {
127
+ if (content.length === 0)
128
+ return 0;
129
+ const collapsed = collapsedBlockFlags(content, cardDefaults);
130
+ const toolNameById = new Map();
131
+ for (const block of content) {
132
+ if (block.type === "tool_use")
133
+ toolNameById.set(block.id, block.name);
134
+ }
135
+ let visible = 0;
136
+ let bytes = 0;
137
+ let cut = content.length;
138
+ for (let i = content.length - 1; i >= 0; i -= 1) {
139
+ const nextVisible = visible + (collapsed[i] ? 0 : 1);
140
+ const nextBytes = bytes + blockTransportBytes(content[i], cardDefaults, toolNameById) + 32;
141
+ if (nextVisible > visibleBudget || nextBytes > byteBudget)
142
+ break;
143
+ visible = nextVisible;
144
+ bytes = nextBytes;
145
+ cut = i;
146
+ }
147
+ // 至少保留最后一块:单个巨块超预算时也得给客户端一点内容。
148
+ return cut >= content.length ? content.length - 1 : cut;
149
+ }
150
+ /** 段内最靠左的坐标:从 index 往回吃掉同一段折叠块。 */
151
+ function collapsedRunStart(collapsed, index) {
152
+ let start = index;
153
+ while (start > 0 && collapsed[start - 1])
154
+ start -= 1;
155
+ return start;
156
+ }
157
+ /** 段内最靠右的坐标:从 index 往后吃掉同一段折叠块。 */
158
+ function collapsedRunEnd(collapsed, index) {
159
+ let end = index;
160
+ while (end < collapsed.length && collapsed[end])
161
+ end += 1;
162
+ return end;
163
+ }
164
+ /**
165
+ * 把块级窗口 / 翻页的起点吸附到「语义干净」的位置:
166
+ *
167
+ * 1. 起点落在默认收起的折叠段中间 → 回到该段起点(整段入窗)。半截工具段在客户端
168
+ * 会渲染成一个少了前几个调用的折叠条,用户看着就是「更早消息缺了一块」。
169
+ * 2. 起点是配对的 tool_result(它的 tool_use 被切在前面)→ 回到 tool_use,否则客户端
170
+ * 顶部会冒出一张「无头结果」卡。
171
+ * 3. 需要往回吃掉的体积超过 MAX_BLOCK_SNAP_BACK_BYTES(或本页范围内吃不下)时不再
172
+ * 往回吸附,改为跳到该段之后(整段都是默认收起的内容,跳过去不会藏掉可见内容)。
173
+ *
174
+ * 返回吸附后的起点;起点越界时回退到 0(整条 turn 入窗,保证窗口不为空)。
175
+ */
176
+ export function alignedBlockStart(content, cardDefaults, rawStart, forwardLimit = content.length) {
177
+ const total = content.length;
178
+ const start = Math.min(Math.max(rawStart, 0), total);
179
+ if (start <= 0 || total === 0)
180
+ return 0;
181
+ const collapsed = collapsedBlockFlags(content, cardDefaults);
182
+ const useIndexById = new Map();
183
+ content.forEach((block, index) => {
184
+ if (block.type === "tool_use")
185
+ useIndexById.set(block.id, index);
186
+ });
187
+ let cursor = start;
188
+ for (let guard = 0; guard <= total; guard += 1) {
189
+ let next = collapsed[cursor] ? collapsedRunStart(collapsed, cursor) : cursor;
190
+ const head = content[next];
191
+ if (head?.type === "tool_result") {
192
+ const useIndex = useIndexById.get(head.tool_use_id);
193
+ if (useIndex !== undefined && useIndex < next)
194
+ next = useIndex;
195
+ }
196
+ if (next === cursor)
197
+ break;
198
+ cursor = next;
199
+ }
200
+ const snapBackBytes = contentTransportBytes(content.slice(cursor, start), cardDefaults);
201
+ if (snapBackBytes <= MAX_BLOCK_SNAP_BACK_BYTES)
202
+ return cursor;
203
+ // 折叠段太长:不再往回吃,跳到该段之后,保证首屏载荷不因吸附膨胀。
204
+ const skipped = collapsed[cursor] ? collapsedRunEnd(collapsed, cursor) : cursor;
205
+ if (skipped >= total)
206
+ return 0;
207
+ // 跳不到更晚的位置(只可能还是往回吃)或超过本页末尾时保持原切点。
208
+ if (skipped <= start || skipped >= forwardLimit)
209
+ return start;
210
+ return skipped;
211
+ }
212
+ /** [0, end) 里用户可感知的块数(默认收起的工具 / 思考块不计入)。 */
213
+ export function visibleBlockCount(content, cardDefaults, end) {
214
+ const limit = Math.min(Math.max(end, 0), content.length);
215
+ const collapsed = collapsedBlockFlags(content, cardDefaults);
216
+ let count = 0;
217
+ for (let i = 0; i < limit; i += 1) {
218
+ if (!collapsed[i])
219
+ count += 1;
220
+ }
221
+ return count;
222
+ }
223
+ /**
224
+ * 块级窗口:默认取整段历史(只要它在预算内),超出的才从最新 turn 往回累计,
225
+ * 能整条放下就整条放,放不下的那条(最旧的入窗 turn)只取其尾部若干块,
226
+ * 并通过 leadingBlockOffset 告知客户端「这条 turn 还有更早的块」。
227
+ *
228
+ * 预算口径:`blockBudget` 数的是**用户可感知的条数**(默认收起的工具 / 思考块不计入),
229
+ * 另外叠加一层载荷上限 —— 否则一段长工具调用会把预算吃光,把用户自己的提示词挤出首屏,
230
+ * 一条提示词的会话也会莫名出现「更早消息」。
231
+ */
232
+ export function blockWindowMessagesForTransport(all, cardDefaults, blockBudget = MESSAGE_BLOCK_WINDOW, byteBudget = MESSAGE_FIRST_PAINT_BYTES) {
31
233
  const turns = all ?? [];
32
234
  const total = turns.length;
33
235
  if (total === 0) {
34
- return { messages: [], messageOffset: 0, messageTotal: 0, leadingBlockOffset: 0, leadingBlockTotal: 0 };
236
+ return {
237
+ messages: [],
238
+ messageOffset: 0,
239
+ messageTotal: 0,
240
+ leadingBlockOffset: 0,
241
+ leadingBlockTotal: 0,
242
+ leadingVisibleCount: 0,
243
+ };
244
+ }
245
+ const visibleBudget = Math.max(1, blockBudget);
246
+ // 整段历史放得下就不做窗口化:短会话(一条提示词 + 一段长回复)不该出现「更早消息」。
247
+ if (fitsBudgets(turns, cardDefaults, visibleBudget, byteBudget)) {
248
+ const last = turns[total - 1];
249
+ return {
250
+ messages: truncateMessagesForTransport(turns, cardDefaults),
251
+ messageOffset: 0,
252
+ messageTotal: total,
253
+ leadingBlockOffset: 0,
254
+ leadingBlockTotal: last.content.length,
255
+ leadingVisibleCount: 0,
256
+ };
35
257
  }
36
- const budget = Math.max(1, blockBudget);
37
258
  let startTurn = total - 1;
38
259
  let leadingBlockOffset = 0;
39
- let acc = 0;
40
- for (let i = total - 1; i >= 0; i--) {
41
- const n = turns[i].content.length;
42
- if (i === total - 1) {
43
- // 最新一条 turn 必须入窗:整条放得下就整条,放不下取尾部 budget 块。
44
- if (n <= budget) {
45
- acc = n;
46
- startTurn = i;
47
- leadingBlockOffset = 0;
48
- }
49
- else {
50
- startTurn = i;
51
- leadingBlockOffset = n - budget;
52
- acc = budget;
53
- break;
54
- }
55
- }
56
- else if (acc + n <= budget) {
57
- acc += n;
260
+ let accVisible = 0;
261
+ let accBytes = 0;
262
+ for (let i = total - 1; i >= 0; i -= 1) {
263
+ const content = turns[i].content;
264
+ const visible = visibleBlockCount(content, cardDefaults, content.length);
265
+ const bytes = contentTransportBytes(content, cardDefaults, byteBudget - accBytes);
266
+ if (accVisible + visible <= visibleBudget && accBytes + bytes <= byteBudget) {
267
+ accVisible += visible;
268
+ accBytes += bytes;
58
269
  startTurn = i;
59
270
  leadingBlockOffset = 0;
271
+ continue;
60
272
  }
61
- else {
62
- const remaining = budget - acc;
63
- if (remaining > 0) {
64
- startTurn = i;
65
- leadingBlockOffset = n - remaining;
66
- acc += remaining;
67
- }
273
+ // 放不下:按剩余预算切进这条 turn 的尾部;预算已耗尽则到此为止。
274
+ const remainVisible = visibleBudget - accVisible;
275
+ const remainBytes = byteBudget - accBytes;
276
+ if (i !== total - 1 && (remainVisible <= 0 || remainBytes <= 0))
68
277
  break;
69
- }
278
+ startTurn = i;
279
+ leadingBlockOffset = cutStartByBudget(content, cardDefaults, Math.max(remainVisible, 1), Math.max(remainBytes, 1));
280
+ break;
70
281
  }
282
+ // 最旧入窗 turn 的切点吸附到干净边界:不切开折叠的工具段,也不留下无头 tool_result。
283
+ const headContent = turns[startTurn].content;
284
+ const startOffset = leadingBlockOffset > 0
285
+ ? alignedBlockStart(headContent, cardDefaults, leadingBlockOffset)
286
+ : 0;
71
287
  const windowedTurns = [];
72
288
  for (let i = startTurn; i < total; i++) {
73
- if (i === startTurn && leadingBlockOffset > 0) {
74
- windowedTurns.push({ ...turns[i], content: turns[i].content.slice(leadingBlockOffset) });
289
+ if (i === startTurn && startOffset > 0) {
290
+ windowedTurns.push({ ...turns[i], content: turns[i].content.slice(startOffset) });
75
291
  }
76
292
  else {
77
293
  windowedTurns.push(turns[i]);
@@ -81,8 +297,9 @@ export function blockWindowMessagesForTransport(all, cardDefaults, blockBudget =
81
297
  messages: truncateMessagesForTransport(windowedTurns, cardDefaults),
82
298
  messageOffset: startTurn,
83
299
  messageTotal: total,
84
- leadingBlockOffset,
85
- leadingBlockTotal: turns[startTurn].content.length,
300
+ leadingBlockOffset: startOffset,
301
+ leadingBlockTotal: headContent.length,
302
+ leadingVisibleCount: visibleBlockCount(headContent, cardDefaults, startOffset),
86
303
  };
87
304
  }
88
305
  /**
@@ -1,8 +1,10 @@
1
- import type { AgentActivityItem, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
1
+ import type { AgentActivityItem, AgentActivityState, CreateMissionInput, CreateReviewCommentInput, MissionDetails, MissionDiff, MissionReviewComment } from "./mission-types.js";
2
2
  import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import type { WandStorage } from "./storage.js";
5
- import type { ProcessEvent } from "./types.js";
5
+ import type { ConversationTurn, ProcessEvent, SessionSnapshot } from "./types.js";
6
+ export declare function hasUnansweredQuestion(messages: ConversationTurn[] | undefined): boolean;
7
+ export declare function activityState(snapshot: SessionSnapshot, event?: ProcessEvent): AgentActivityState;
6
8
  /**
7
9
  * Deep task-orchestration module. Callers create missions and review diffs
8
10
  * without coordinating session/worktree/storage details.
package/dist/missions.js CHANGED
@@ -60,7 +60,7 @@ function sessionSummary(snapshot) {
60
60
  }
61
61
  return cleanActivityText(snapshot.summary?.trim() || "", 160);
62
62
  }
63
- function hasUnansweredQuestion(messages) {
63
+ export function hasUnansweredQuestion(messages) {
64
64
  if (!messages?.length || messages[messages.length - 1]?.role === "user")
65
65
  return false;
66
66
  const answered = new Set();
@@ -81,7 +81,7 @@ function hasUnansweredQuestion(messages) {
81
81
  }
82
82
  return false;
83
83
  }
84
- function activityState(snapshot, event) {
84
+ export function activityState(snapshot, event) {
85
85
  if (snapshot.pendingEscalation || snapshot.permissionBlocked)
86
86
  return "needs_permission";
87
87
  if (hasUnansweredQuestion(snapshot.messages))
@@ -69,6 +69,7 @@ export declare class ProcessManager extends EventEmitter {
69
69
  workspaceId?: string;
70
70
  workspaceTaskId?: string;
71
71
  interactiveShell?: boolean;
72
+ systemPrompt?: string;
72
73
  }): Promise<SessionSnapshot>;
73
74
  list(): SessionSnapshot[];
74
75
  /** Return lightweight snapshots for the session list (no output/messages). */
@@ -13,7 +13,7 @@ import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
13
13
  import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
14
14
  import { prepareSessionWorktree } from "./git-worktree.js";
15
15
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
16
- import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
16
+ import { commandWithSystemPrompt, composeSystemFallback, normalizeThinkingEffort, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
17
17
  import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
18
18
  import { getErrorMessage } from "./error-utils.js";
19
19
  import { recordIterationPrompt } from "./iteration-log.js";
@@ -1034,6 +1034,12 @@ export class ProcessManager extends EventEmitter {
1034
1034
  const selectedModel = opts?.model?.trim() || undefined;
1035
1035
  const initialThinkingEffort = normalizeThinkingEffort(opts?.thinkingEffort);
1036
1036
  let processedCommand = this.processCommandForMode(command, effectiveMode, provider, selectedModel, initialThinkingEffort);
1037
+ // 会话级系统提示:provider 有系统提示开关就走开关(PTY 与结构化行为一致),没有就并进首条输入。
1038
+ const systemPrompt = opts?.systemPrompt?.trim() || undefined;
1039
+ const initialInputText = initialInput && systemPrompt && !systemPromptFlag(provider)
1040
+ ? composeSystemFallback(systemPrompt, initialInput)
1041
+ : initialInput;
1042
+ processedCommand = commandWithSystemPrompt(processedCommand, provider, systemPrompt);
1037
1043
  const isCodexProvider = provider === "codex";
1038
1044
  const isOpenCodeProvider = provider === "opencode";
1039
1045
  const existingProviderSessionId = provider
@@ -1069,6 +1075,7 @@ export class ProcessManager extends EventEmitter {
1069
1075
  id,
1070
1076
  sessionSource: opts?.sessionSource ?? inheritedSessionSource ?? "interactive",
1071
1077
  automationId: opts?.automationId ?? inheritedAutomationId,
1078
+ systemPrompt: systemPrompt ?? null,
1072
1079
  workspaceId: opts?.workspaceId ?? inheritedWorkspaceId,
1073
1080
  workspaceTaskId: opts?.workspaceTaskId ?? inheritedWorkspaceTaskId,
1074
1081
  provider,
@@ -1189,7 +1196,7 @@ export class ProcessManager extends EventEmitter {
1189
1196
  record.status = "running";
1190
1197
  let initialInputSent = false;
1191
1198
  const sendInitialInput = () => {
1192
- if (initialInputSent || !initialInput)
1199
+ if (initialInputSent || !initialInputText)
1193
1200
  return;
1194
1201
  initialInputSent = true;
1195
1202
  const current = this.sessions.get(id);
@@ -1197,20 +1204,20 @@ export class ProcessManager extends EventEmitter {
1197
1204
  process.stderr.write(`[wand] Cannot send initial input: session not ready\n`);
1198
1205
  return;
1199
1206
  }
1200
- process.stderr.write(`[wand] Sending initial input (${initialInput.length} chars)\n`);
1207
+ process.stderr.write(`[wand] Sending initial input (${initialInputText.length} chars)\n`);
1201
1208
  if (current.ptyBridge) {
1202
- current.ptyBridge.onUserInput(initialInput);
1209
+ current.ptyBridge.onUserInput(initialInputText);
1203
1210
  }
1204
- child.write(initialInput);
1211
+ child.write(initialInputText);
1205
1212
  child.write("\r");
1206
1213
  };
1207
1214
  this.bindTerminalProcess(record, child, (chunk) => {
1208
- if (initialInput && !initialInputSent && (chunk.includes("❯") || chunk.includes("›")))
1215
+ if (initialInputText && !initialInputSent && (chunk.includes("❯") || chunk.includes("›")))
1209
1216
  sendInitialInput();
1210
1217
  });
1211
1218
  this.emitEvent({ type: "started", sessionId: id, data: this.snapshot(record) });
1212
- if (initialInput)
1213
- this.maybeGenerateSessionTopic(id, initialInput);
1219
+ if (initialInputText)
1220
+ this.maybeGenerateSessionTopic(id, initialInputText);
1214
1221
  if (launchPlan.commandToWrite) {
1215
1222
  child.write(launchPlan.commandToWrite);
1216
1223
  child.write("\r");
@@ -1893,6 +1900,8 @@ export class ProcessManager extends EventEmitter {
1893
1900
  sessionKind: "pty",
1894
1901
  sessionSource: record.sessionSource ?? "interactive",
1895
1902
  automationId: record.automationId,
1903
+ // 会话级系统提示要跟着快照回库(persist 就是走 snapshot),否则重启后 resume 会丢掉角色与规则。
1904
+ systemPrompt: record.systemPrompt ?? null,
1896
1905
  provider: record.provider,
1897
1906
  providerCliActive: record.providerCliActive,
1898
1907
  providerCliExitCode: record.providerCliExitCode,
@@ -10,17 +10,17 @@ export class PromptOptimizeError extends Error {
10
10
  }
11
11
  function buildOptimizePrompt(userInput, language) {
12
12
  const lang = (language || "").trim() || "中文";
13
- return [
14
- `你是一名提示词优化助手。请把用户写给编码 AI 的「原始提示词」改写得更清晰、结构化、可执行,便于 AI 理解并完成任务。`,
15
- `要求:`,
16
- `1. 保留用户原意和所有关键信息(文件路径、变量名、技术名词、数字、约束等),不要删减事实,也不要新增臆测的需求。`,
17
- `2. 必要时拆分为「目标 / 上下文 / 约束 / 验收标准」几个部分;如果原文很短或很简单,则只做语句润色,不要硬塞结构。`,
18
- `3. 用${lang}输出。语气克制专业,不寒暄、不解释你做了什么。`,
19
- `4. 只输出优化后的提示词正文,不要包裹在代码块或引号里,不要加任何前后缀(比如「优化后:」之类)。`,
20
- ``,
21
- `原始提示词:`,
22
- userInput,
23
- ].join("\n");
13
+ return {
14
+ system: [
15
+ `你是一名提示词优化助手。请把用户写给编码 AI 的「原始提示词」改写得更清晰、结构化、可执行,便于 AI 理解并完成任务。`,
16
+ `要求:`,
17
+ `1. 保留用户原意和所有关键信息(文件路径、变量名、技术名词、数字、约束等),不要删减事实,也不要新增臆测的需求。`,
18
+ `2. 必要时拆分为「目标 / 上下文 / 约束 / 验收标准」几个部分;如果原文很短或很简单,则只做语句润色,不要硬塞结构。`,
19
+ `3. 用${lang}输出。语气克制专业,不寒暄、不解释你做了什么。`,
20
+ `4. 只输出优化后的提示词正文,不要包裹在代码块或引号里,不要加任何前后缀(比如「优化后:」之类)。`,
21
+ ].join("\n"),
22
+ prompt: `原始提示词:\n${userInput}`,
23
+ };
24
24
  }
25
25
  export async function optimizePrompt(rawText, language, cwd, ai = {}) {
26
26
  const text = (rawText || "").trim();
@@ -30,10 +30,10 @@ export async function optimizePrompt(rawText, language, cwd, ai = {}) {
30
30
  if (text.length > MAX_INPUT_LENGTH) {
31
31
  throw new PromptOptimizeError(`输入过长(${text.length} 字符),请缩短到 ${MAX_INPUT_LENGTH} 以内。`, "INPUT_TOO_LONG");
32
32
  }
33
- const prompt = buildOptimizePrompt(text, language);
33
+ const request = buildOptimizePrompt(text, language);
34
34
  let raw;
35
35
  try {
36
- raw = await callConfiguredAiText(prompt, cwd ?? process.cwd(), language, ai);
36
+ raw = await callConfiguredAiText(request, cwd ?? process.cwd(), language, ai);
37
37
  }
38
38
  catch (error) {
39
39
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,11 @@
1
+ import type { Express } from "express";
2
+ import { type AiTeamRunner } from "./ai-team-runner.js";
3
+ import { type AiTeam } from "./ai-team-types.js";
4
+ import type { WandStorage } from "./storage.js";
5
+ /** 团队页「直接开工」自动建卡的标记(§4.2 / Q3):看板靠它区分团队卡与手写卡。 */
6
+ export declare const TEAM_DIRECT_LABEL = "team_direct";
7
+ export declare function parseAiTeamInput(value: unknown, existing: AiTeam | null, now: string): AiTeam;
8
+ export declare function registerAiTeamRoutes(app: Express, deps: {
9
+ storage: WandStorage;
10
+ runner: AiTeamRunner;
11
+ }): void;