@co0ontty/wand 4.83.1 → 4.84.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 (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ export interface CoreStatus {
3
+ hasActiveTurns: boolean;
4
+ activeTurnCount: number;
5
+ activeTurnIds?: string[];
6
+ }
7
+ export declare function checkCoreStatus(configPath: string): Promise<void>;
8
+ /** Zero means wait indefinitely. A timeout/error NEVER runs the stop command. */
9
+ export declare function waitForCoreTurnsComplete(configPath: string, timeoutSeconds?: number, stopCommand?: string[], allowStopped?: boolean): Promise<number>;
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Query the running web process, never the CLI process's in-memory tracker.
4
+ * Kept dependency-free so start.sh can run this source with Node's TS support
5
+ * before replacing an older global package (including --restart/--no-build).
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { X509Certificate } from "node:crypto";
9
+ import { readFileSync } from "node:fs";
10
+ import { request as httpRequest } from "node:http";
11
+ import { request as httpsRequest } from "node:https";
12
+ import net from "node:net";
13
+ import path from "node:path";
14
+ import { pathToFileURL } from "node:url";
15
+ function parseStatus(value) {
16
+ const status = value;
17
+ if (!status || typeof status.hasActiveTurns !== "boolean"
18
+ || typeof status.activeTurnCount !== "number" || !Number.isSafeInteger(status.activeTurnCount)
19
+ || status.activeTurnCount < 0 || status.hasActiveTurns !== (status.activeTurnCount > 0)) {
20
+ throw new Error("Core 状态响应无效,不能确认重启安全。");
21
+ }
22
+ return status;
23
+ }
24
+ class CoreControl {
25
+ socket = null;
26
+ buffer = "";
27
+ nextId = 0;
28
+ pending = new Map();
29
+ disconnected = false;
30
+ configPath;
31
+ constructor(configPath) { this.configPath = configPath; }
32
+ async connect(timeoutMs = 5000) {
33
+ const socket = net.createConnection(path.join(path.dirname(this.configPath), "wand.sock"));
34
+ this.socket = socket;
35
+ socket.setEncoding("utf8");
36
+ socket.on("data", (chunk) => {
37
+ this.buffer += chunk;
38
+ if (this.buffer.length > 64 * 1024) {
39
+ socket.destroy(new Error("Core IPC 响应过大。"));
40
+ return;
41
+ }
42
+ let at;
43
+ while ((at = this.buffer.indexOf("\n")) >= 0) {
44
+ const line = this.buffer.slice(0, at);
45
+ this.buffer = this.buffer.slice(at + 1);
46
+ let response;
47
+ try {
48
+ response = JSON.parse(line);
49
+ }
50
+ catch {
51
+ socket.destroy(new Error("Core IPC 响应无效。"));
52
+ return;
53
+ }
54
+ const pending = response && this.pending.get(response.id ?? "");
55
+ if (!pending)
56
+ continue;
57
+ this.pending.delete(response.id);
58
+ if (response.ok === true)
59
+ pending.resolve(response.data);
60
+ else
61
+ pending.reject(new Error(response.error ?? "Core IPC 请求失败。"));
62
+ }
63
+ });
64
+ const fail = (error) => {
65
+ this.disconnected = true;
66
+ for (const pending of this.pending.values())
67
+ pending.reject(error);
68
+ this.pending.clear();
69
+ };
70
+ socket.on("error", fail);
71
+ socket.on("close", () => fail(new Error("Core 控制连接已断开,已取消重启。")));
72
+ await new Promise((resolve, reject) => {
73
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error("连接 Core 控制端超时。")); }, timeoutMs);
74
+ socket.once("connect", () => { clearTimeout(timer); resolve(); });
75
+ socket.once("error", (error) => { clearTimeout(timer); reject(error); });
76
+ });
77
+ }
78
+ request(cmd, timeoutMs = 5000) {
79
+ if (this.disconnected || !this.socket)
80
+ return Promise.reject(new Error("Core 控制连接不可用。"));
81
+ const id = String(++this.nextId);
82
+ return new Promise((resolve, reject) => {
83
+ const timer = setTimeout(() => {
84
+ this.pending.delete(id);
85
+ reject(new Error("读取 Core 状态超时,已取消重启。"));
86
+ }, timeoutMs);
87
+ this.pending.set(id, {
88
+ resolve: (data) => { clearTimeout(timer); resolve(data); },
89
+ reject: (error) => { clearTimeout(timer); reject(error); },
90
+ });
91
+ this.socket.write(JSON.stringify({ id, cmd }) + "\n");
92
+ }).then(parseStatus);
93
+ }
94
+ assertConnected() {
95
+ if (this.disconnected)
96
+ throw new Error("Core 控制连接已断开,已取消重启。");
97
+ }
98
+ close() { this.socket?.destroy(); }
99
+ }
100
+ /** Compatibility with the first core-enabled server, which has no core IPC commands. */
101
+ async function readLegacyStatus(configPath, timeoutMs = 5000) {
102
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
103
+ if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
104
+ throw new Error("配置端口无效,不能读取运行中 Core 状态。");
105
+ }
106
+ // Trust the configured local certificate, not arbitrary self-signed peers.
107
+ // Pinning also works with a user certificate whose SAN is not 127.0.0.1.
108
+ const ca = config.https ? readFileSync(config.tls?.certPath ?? path.join(path.dirname(configPath), "server.crt")) : undefined;
109
+ const fingerprint = ca ? new X509Certificate(ca).fingerprint256 : undefined;
110
+ const request = config.https ? httpsRequest : httpRequest;
111
+ const result = await new Promise((resolve, reject) => {
112
+ const req = request({ hostname: config.host === "::1" ? "::1" : "127.0.0.1",
113
+ port: config.port, path: "/api/core-status", method: "GET",
114
+ ...(ca ? { ca, allowPartialTrustChain: true,
115
+ checkServerIdentity: (_hostname, cert) => cert.fingerprint256 === fingerprint ? undefined : new Error("Core 服务证书与本机配置不一致。") } : {}) }, (res) => {
116
+ let body = "";
117
+ res.setEncoding("utf8");
118
+ res.on("data", (chunk) => {
119
+ body += chunk;
120
+ if (body.length > 64 * 1024)
121
+ req.destroy(new Error("Core 状态响应过大。"));
122
+ });
123
+ res.on("error", reject);
124
+ res.on("end", () => {
125
+ if (res.statusCode !== 200) {
126
+ reject(new Error(`读取运行中 Core 状态失败 (HTTP ${res.statusCode})。`));
127
+ return;
128
+ }
129
+ try {
130
+ resolve(JSON.parse(body));
131
+ }
132
+ catch {
133
+ reject(new Error("Core 状态不是 JSON,已取消重启。"));
134
+ }
135
+ });
136
+ });
137
+ const timer = setTimeout(() => req.destroy(new Error("读取运行中 Core 状态超时。")), timeoutMs);
138
+ req.on("close", () => clearTimeout(timer));
139
+ req.on("error", reject);
140
+ req.end();
141
+ });
142
+ return parseStatus(result);
143
+ }
144
+ async function openControl(configPath, deadline = Infinity) {
145
+ const control = new CoreControl(configPath);
146
+ const budget = () => {
147
+ const remaining = deadline - Date.now();
148
+ if (remaining <= 0)
149
+ throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
150
+ return Math.min(5000, remaining);
151
+ };
152
+ try {
153
+ await control.connect(budget());
154
+ await control.request("core-status", budget());
155
+ return control;
156
+ }
157
+ catch (error) {
158
+ control.close();
159
+ // Older protocols and missing/stale sockets can use the real loopback probe.
160
+ // Permission errors and malformed responses must fail closed.
161
+ if (error instanceof Error && /^unknown cmd: core-status$/.test(error.message))
162
+ return null;
163
+ if (["ENOENT", "ECONNREFUSED"].includes(error?.code ?? ""))
164
+ return null;
165
+ throw error;
166
+ }
167
+ }
168
+ export async function checkCoreStatus(configPath) {
169
+ const control = await openControl(configPath);
170
+ try {
171
+ const status = control ? await control.request("core-status") : await readLegacyStatus(configPath);
172
+ console.log(JSON.stringify({ status: status.hasActiveTurns ? "busy" : "idle", ...status }));
173
+ process.exitCode = status.hasActiveTurns ? 1 : 0;
174
+ }
175
+ finally {
176
+ control?.close();
177
+ }
178
+ }
179
+ /** Zero means wait indefinitely. A timeout/error NEVER runs the stop command. */
180
+ export async function waitForCoreTurnsComplete(configPath, timeoutSeconds = 0, stopCommand, allowStopped = false) {
181
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0)
182
+ throw new Error("--timeout 必须是非负秒数(0 表示一直等待)。");
183
+ if (stopCommand && !stopCommand.length)
184
+ throw new Error("缺少等待完成后要执行的停止命令。");
185
+ const deadline = timeoutSeconds > 0 ? Date.now() + timeoutSeconds * 1000 : Infinity;
186
+ const budget = () => {
187
+ const remaining = deadline - Date.now();
188
+ if (remaining <= 0)
189
+ throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
190
+ return Math.min(5000, remaining);
191
+ };
192
+ const control = await openControl(configPath, deadline);
193
+ try {
194
+ const readStatus = async (budget = 5000) => {
195
+ if (control)
196
+ return control.request("core-status", budget);
197
+ try {
198
+ return await readLegacyStatus(configPath, budget);
199
+ }
200
+ catch (error) {
201
+ // Only the service manager's confirmed stopped state plus no live PID
202
+ // AND a refused local listener may treat an absent service as idle.
203
+ if (!allowStopped || error?.code !== "ECONNREFUSED")
204
+ throw error;
205
+ try {
206
+ const info = JSON.parse(readFileSync(path.join(path.dirname(configPath), "wand.pid"), "utf8"));
207
+ if (!Number.isInteger(info.pid) || info.pid <= 0)
208
+ throw new Error("Core pidfile 无效。");
209
+ try {
210
+ process.kill(info.pid, 0);
211
+ throw new Error("Core 主进程仍在运行,不能跳过检查。");
212
+ }
213
+ catch (pidError) {
214
+ if (pidError?.code !== "ESRCH")
215
+ throw pidError;
216
+ }
217
+ }
218
+ catch (pidError) {
219
+ if (pidError?.code !== "ENOENT")
220
+ throw pidError;
221
+ }
222
+ return { hasActiveTurns: false, activeTurnCount: 0 };
223
+ }
224
+ };
225
+ let status = control && stopCommand ? await control.request("core-drain", budget()) : await readStatus(budget());
226
+ if (!control && stopCommand)
227
+ console.error("[wand] 旧服务尚不支持重启排空;正在读取真实服务状态。首次升级等待期间请勿启动新任务。");
228
+ let lastCount = -1;
229
+ while (status.hasActiveTurns) {
230
+ if (status.activeTurnCount !== lastCount) {
231
+ console.log(`[wand] ${status.activeTurnCount} 个原生 Core 回合仍在运行,等待完成后再停止服务…`);
232
+ lastCount = status.activeTurnCount;
233
+ }
234
+ const remaining = deadline - Date.now();
235
+ if (remaining <= 0)
236
+ throw new Error("等待 Core 回合超时,服务未停止,已取消重启。");
237
+ await new Promise((resolve) => setTimeout(resolve, Math.min(1000, remaining)));
238
+ status = await readStatus(budget());
239
+ }
240
+ // The draining connection stays open until the stop command finishes.
241
+ // New Pi inputs are persisted in the queue, not started in the check/stop gap.
242
+ control?.assertConnected();
243
+ console.log("[wand] 原生 Core 回合已全部完成,可以停止服务。");
244
+ if (!stopCommand)
245
+ return 0;
246
+ return await new Promise((resolve, reject) => {
247
+ const child = spawn(stopCommand[0], stopCommand.slice(1), { stdio: "inherit" });
248
+ child.once("error", reject);
249
+ // A dedicated code distinguishes action failure from a broken/missing
250
+ // guard executable (which may itself exit 1 before our catch can run).
251
+ child.once("exit", (code, signal) => resolve(code === 0 && !signal ? 0 : 3));
252
+ });
253
+ }
254
+ finally {
255
+ control?.close();
256
+ }
257
+ }
258
+ // start.sh uses the current source rather than the possibly broken installed CLI.
259
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
260
+ const args = process.argv.slice(2);
261
+ const separator = args.indexOf("--");
262
+ const options = separator < 0 ? args : args.slice(0, separator);
263
+ const flag = (name) => {
264
+ const index = options.indexOf(name);
265
+ return index < 0 ? undefined : options[index + 1];
266
+ };
267
+ const config = flag("-c") ?? flag("--config");
268
+ try {
269
+ if (!config)
270
+ throw new Error("缺少 -c 配置路径。");
271
+ if (options.includes("--status"))
272
+ await checkCoreStatus(path.resolve(config));
273
+ else
274
+ process.exitCode = await waitForCoreTurnsComplete(path.resolve(config), Number(flag("--timeout") ?? 0), separator < 0 ? undefined : args.slice(separator + 1), options.includes("--allow-stopped"));
275
+ }
276
+ catch (error) {
277
+ console.error(`[wand] ${error instanceof Error ? error.message : String(error)}`);
278
+ process.exitCode = 2;
279
+ }
280
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tracks active Core engine turns to prevent service restart during execution.
3
+ */
4
+ export declare class CoreTurnTracker {
5
+ private static activeTurns;
6
+ static startTurn(sessionId: string): symbol;
7
+ static endTurn(token: symbol): void;
8
+ static hasActiveTurns(): boolean;
9
+ static getActiveTurnCount(): number;
10
+ static getActiveTurnIds(): string[];
11
+ static waitForAllTurnsComplete(timeoutMs?: number): Promise<void>;
12
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Tracks active Core engine turns to prevent service restart during execution.
3
+ */
4
+ export class CoreTurnTracker {
5
+ // Each execution owns its token: a late cancelled turn cannot untrack its
6
+ // replacement in the same session.
7
+ static activeTurns = new Map();
8
+ static startTurn(sessionId) {
9
+ const token = Symbol(sessionId);
10
+ this.activeTurns.set(token, sessionId);
11
+ return token;
12
+ }
13
+ static endTurn(token) {
14
+ this.activeTurns.delete(token);
15
+ }
16
+ static hasActiveTurns() {
17
+ return this.activeTurns.size > 0;
18
+ }
19
+ static getActiveTurnCount() {
20
+ return this.activeTurns.size;
21
+ }
22
+ static getActiveTurnIds() {
23
+ return [...new Set(this.activeTurns.values())];
24
+ }
25
+ static async waitForAllTurnsComplete(timeoutMs = 0) {
26
+ const startTime = Date.now();
27
+ while (this.hasActiveTurns()) {
28
+ if (timeoutMs > 0 && Date.now() - startTime >= timeoutMs) {
29
+ throw new Error(`Timeout waiting for Core turns to complete. ` +
30
+ `${this.activeTurns.size} turns still active: ${this.getActiveTurnIds().join(", ")}`);
31
+ }
32
+ // Wait 1 second before checking again
33
+ await new Promise(resolve => setTimeout(resolve, 1000));
34
+ }
35
+ }
36
+ }
@@ -1,8 +1,14 @@
1
+ import type { DecisionResult } from "./decision-types.js";
1
2
  import type { WandStorage } from "./storage.js";
2
3
  import type { StructuredRunnerAdapter } from "./structured-runner.js";
3
4
  export interface DecisionRuntimeAccess {
4
5
  url: string;
5
6
  caPath?: string;
7
+ /**
8
+ * 进程内调用入口(core 会话用):同一个 server 进程直接调决策服务,
9
+ * 不经过 env token / HTTP 回环。CLI runner 仍然走 `url` + 环境变量。
10
+ */
11
+ evaluate?: (value: unknown, caller: string, signal?: AbortSignal) => Promise<DecisionResult>;
6
12
  }
7
13
  export declare const DECISION_ENV_KEYS: readonly ["WAND_DECISION_URL", "WAND_DECISION_TOKEN", "WAND_DECISION_CA", "WAND_DECISION_NODE", "WAND_DECISION_CLI"];
8
14
  /** Adds only a revocable inference capability. Never edits base prompts, DTOs, or tool permissions. */
@@ -8,8 +8,10 @@ export function withDecisionAccess(runner, storage, runtime) {
8
8
  for (const key of DECISION_ENV_KEYS)
9
9
  delete env[key];
10
10
  const access = runtime();
11
- if (!access)
11
+ // Pi 会话可以逐次关闭本轮决策能力:关掉就不注入凭据,也不加运行时提示。
12
+ if (!access || (context.session.provider === "pi" && context.session.piSettings?.localDecision === false)) {
12
13
  return runner.start({ ...context, env }, observer);
14
+ }
13
15
  const token = storage.issueDecisionAccess(context.session.id);
14
16
  const cleanup = () => { try {
15
17
  storage.revokeDecisionAccess(token);
@@ -8,6 +8,8 @@ interface DecisionToolLike {
8
8
  kind?: string;
9
9
  };
10
10
  }
11
+ /** core 会话里决策是进程内工具(`src/core-runner.ts`);CLI 会话是命令行调用。 */
12
+ export declare const DECISION_TOOL_NAME = "decision_evaluate";
11
13
  export declare function isDecisionToolCall(block: DecisionToolLike): boolean;
12
14
  /**
13
15
  * 卡头副标题:结论优先(窄屏先被省略的是被判定内容,不是答案),再题数、再要点的有界拼接;
@@ -1,3 +1,5 @@
1
+ /** core 会话里决策是进程内工具(`src/core-runner.ts`);CLI 会话是命令行调用。 */
2
+ export const DECISION_TOOL_NAME = "decision_evaluate";
1
3
  /** Small shell lexer: quoted examples are one argument, not executable commands; heredoc bodies are data. */
2
4
  function commandWords(source) {
3
5
  const commands = [];
@@ -83,6 +85,9 @@ export function isDecisionToolCall(block) {
83
85
  if (block.type && block.type !== "tool_use")
84
86
  return false;
85
87
  const operation = (block.name ?? "").toLowerCase().split(/__|[/.]/).at(-1) ?? "";
88
+ // core(进程内 harness)把决策暴露成真工具,不是命令行调用。
89
+ if (operation === DECISION_TOOL_NAME)
90
+ return true;
86
91
  if (!["bash", "exec", "exec_command", "command_execution", "shell_command", "terminal", "run_command", "run_shell_command"].includes(operation))
87
92
  return false;
88
93
  const input = block.input ?? {};
@@ -205,6 +210,22 @@ function decisionCommands(input) {
205
210
  }
206
211
  /** 请求侧要点:被判定内容摘要 + 题数 + 模式。请求体读不出时整块省略。 */
207
212
  function requestDigest(input) {
213
+ if (!input)
214
+ return null;
215
+ // core 进程内工具:questions 直接是字段(JSON 字符串),state 也在同一层。
216
+ if (typeof input.questions === "string") {
217
+ try {
218
+ const parsed = JSON.parse(input.questions);
219
+ const digest = bodyDigest({ state: input.state, questions: parsed });
220
+ if (digest)
221
+ return digest;
222
+ }
223
+ catch {
224
+ // questions 坏了不影响从 state 给出被判定内容摘要。
225
+ }
226
+ const stateOnly = statePreview(input.state);
227
+ return Object.keys(stateOnly).length ? stateOnly : null;
228
+ }
208
229
  for (const source of decisionCommands(input)) {
209
230
  for (const candidate of jsonCandidates(source)) {
210
231
  let value;
@@ -214,32 +235,38 @@ function requestDigest(input) {
214
235
  catch {
215
236
  continue;
216
237
  }
217
- if (!isRecord(value) || !isRecord(value.questions))
218
- continue;
219
- const questions = Object.entries(value.questions);
220
- if (!questions.length)
221
- continue;
222
- const types = [];
223
- let recognized = 0;
224
- for (const [, raw] of questions) {
225
- if (!isRecord(raw))
226
- continue;
227
- if (raw.type === "choice" || raw.type === "score" || raw.type === "noul") {
228
- types.push(raw.type);
229
- recognized++;
230
- }
231
- }
232
- if (!recognized)
233
- continue;
234
- return {
235
- mode: modeOf(types),
236
- questions: questions.length,
237
- ...statePreview(value.state),
238
- };
238
+ const digest = bodyDigest(value);
239
+ if (digest)
240
+ return digest;
239
241
  }
240
242
  }
241
243
  return null;
242
244
  }
245
+ /** 决策请求体 → 摘要要点;读不出题目就返回 null(宁可没有摘要也不猜)。 */
246
+ function bodyDigest(value) {
247
+ if (!isRecord(value) || !isRecord(value.questions))
248
+ return null;
249
+ const questions = Object.entries(value.questions);
250
+ if (!questions.length)
251
+ return null;
252
+ const types = [];
253
+ let recognized = 0;
254
+ for (const [, raw] of questions) {
255
+ if (!isRecord(raw))
256
+ continue;
257
+ if (raw.type === "choice" || raw.type === "score" || raw.type === "noul") {
258
+ types.push(raw.type);
259
+ recognized++;
260
+ }
261
+ }
262
+ if (!recognized)
263
+ return null;
264
+ return {
265
+ mode: modeOf(types),
266
+ questions: questions.length,
267
+ ...statePreview(value.state),
268
+ };
269
+ }
243
270
  function statePreview(state) {
244
271
  if (typeof state === "string") {
245
272
  const preview = collapse(state, PREVIEW_MAX);
@@ -1,15 +1,19 @@
1
- /** 子进程 env:显式传入 env 优先,否则按 inheritEnv 生成白名单环境。 */
1
+ /** 子进程 env:显式传入 env 优先,否则按 inheritEnv 生成环境。 */
2
2
  export declare function resolveChildEnv(options: {
3
3
  env?: NodeJS.ProcessEnv;
4
4
  inheritEnv?: boolean;
5
5
  }): NodeJS.ProcessEnv;
6
+ export declare function setLoginShellEnv(env: NodeJS.ProcessEnv | undefined): void;
7
+ export declare function getLoginShellEnv(): NodeJS.ProcessEnv | undefined;
8
+ /** 读取系统默认环境里的单个变量:进程 env 优先,其次登录 shell。 */
9
+ export declare function systemEnvValue(name: string): string | undefined;
6
10
  /** 是否以 root 身份运行(uid 或 euid 为 0)。供 PTY runner 与 structured runner 共用。 */
7
11
  export declare function isRunningAsRoot(): boolean;
8
12
  /**
9
13
  * 根据 inheritEnv 配置组装子进程的环境变量。
10
14
  *
11
- * - inheritEnv=true(默认):继承父进程全部 env,再合并 extras 覆盖。
12
- * - inheritEnv=false:仅保留 MINIMAL_ENV_KEYS 中存在的字段,再合并 extras 覆盖。
15
+ * - inheritEnv=true(默认):复用系统默认环境(登录 shell + 当前进程 env),再合并 extras 覆盖。
16
+ * - inheritEnv=false:在同一底座上只保留 MINIMAL_ENV_KEYS 中存在的字段,再合并 extras 覆盖。
13
17
  *
14
18
  * extras 中的 undefined 字段会被剔除(spawn 不允许 env 值为 undefined)。
15
19
  */
package/dist/env-utils.js CHANGED
@@ -19,10 +19,34 @@ const MINIMAL_ENV_KEYS = [
19
19
  "TEMP",
20
20
  "PWD",
21
21
  ];
22
- /** 子进程 env:显式传入 env 优先,否则按 inheritEnv 生成白名单环境。 */
22
+ /** 子进程 env:显式传入 env 优先,否则按 inheritEnv 生成环境。 */
23
23
  export function resolveChildEnv(options) {
24
24
  return options.env ?? buildChildEnv(options.inheritEnv !== false);
25
25
  }
26
+ /**
27
+ * 用户默认登录 shell 的完整环境(由 src/path-repair.ts 的 `-lic` 探测捕获)。
28
+ * 服务被 launchd / systemd 拉起时进程 env 是安装那一刻的快照,shell rc 里导出的
29
+ * 变量(工具链、代理、模型相关的配置)都不在其中;这里把它按系统默认环境补回来。
30
+ */
31
+ let loginShellEnv;
32
+ export function setLoginShellEnv(env) {
33
+ loginShellEnv = env && Object.keys(env).length > 0 ? env : undefined;
34
+ }
35
+ export function getLoginShellEnv() {
36
+ return loginShellEnv;
37
+ }
38
+ /**
39
+ * 子进程环境的底座:系统默认登录 shell 环境 + 当前进程环境。
40
+ * 两者冲突时进程环境优先(本轮显式启动值最大),shell 环境只填补进程里缺失的变量;
41
+ * PATH 的顺序由 deepRepairRuntimePath() 单独按登录 shell 重排回 process.env。
42
+ */
43
+ function systemEnv() {
44
+ return loginShellEnv ? { ...loginShellEnv, ...process.env } : { ...process.env };
45
+ }
46
+ /** 读取系统默认环境里的单个变量:进程 env 优先,其次登录 shell。 */
47
+ export function systemEnvValue(name) {
48
+ return process.env[name] ?? loginShellEnv?.[name];
49
+ }
26
50
  /** 是否以 root 身份运行(uid 或 euid 为 0)。供 PTY runner 与 structured runner 共用。 */
27
51
  export function isRunningAsRoot() {
28
52
  return process.getuid?.() === 0 || process.geteuid?.() === 0;
@@ -30,19 +54,20 @@ export function isRunningAsRoot() {
30
54
  /**
31
55
  * 根据 inheritEnv 配置组装子进程的环境变量。
32
56
  *
33
- * - inheritEnv=true(默认):继承父进程全部 env,再合并 extras 覆盖。
34
- * - inheritEnv=false:仅保留 MINIMAL_ENV_KEYS 中存在的字段,再合并 extras 覆盖。
57
+ * - inheritEnv=true(默认):复用系统默认环境(登录 shell + 当前进程 env),再合并 extras 覆盖。
58
+ * - inheritEnv=false:在同一底座上只保留 MINIMAL_ENV_KEYS 中存在的字段,再合并 extras 覆盖。
35
59
  *
36
60
  * extras 中的 undefined 字段会被剔除(spawn 不允许 env 值为 undefined)。
37
61
  */
38
62
  export function buildChildEnv(inheritEnv, extras = {}) {
63
+ const source = systemEnv();
39
64
  const base = {};
40
65
  if (inheritEnv) {
41
- Object.assign(base, process.env);
66
+ Object.assign(base, source);
42
67
  }
43
68
  else {
44
69
  for (const key of MINIMAL_ENV_KEYS) {
45
- const v = process.env[key];
70
+ const v = source[key];
46
71
  if (typeof v === "string")
47
72
  base[key] = v;
48
73
  }
@@ -19,6 +19,7 @@ interface QuickCommitOptions extends CommitInputOptions, QuickCommitAiOptions {
19
19
  submodule?: boolean;
20
20
  }
21
21
  export interface QuickCommitAiOptions {
22
+ modelGroups?: import("./model-groups.js").ModelGroup[];
22
23
  provider?: SessionProvider;
23
24
  model?: string | null;
24
25
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
@@ -27,6 +28,10 @@ export interface QuickCommitAiOptions {
27
28
  opsPersona?: string;
28
29
  /** CLI 降级链(按顺序);未设置时只用 provider/model 这一次调用。 */
29
30
  cliCandidates?: import("./types.js").AiCliCandidate[];
31
+ /** 系统应用必须经员工渠道;没有可用 CLI 候选时直接失败,不退回默认 provider。 */
32
+ employeeChannelOnly?: boolean;
33
+ /** 整条候选链的时间上限;默认 CLI_CHAIN_BUDGET_MS。第一条消息里的调用要短得多。 */
34
+ budgetMs?: number;
30
35
  }
31
36
  export declare class QuickCommitError extends Error {
32
37
  readonly code: QuickCommitErrorCode;
@@ -775,13 +775,19 @@ function singleCandidate(opts) {
775
775
  * 仅用于一次性文本生成;执行工具的任务不能使用这条无条件重试路径。
776
776
  */
777
777
  async function callCliCandidates(request, cwd, language, opts, parseOutput) {
778
- const chain = opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)];
778
+ const { resolveModelGroupModels } = await import("./model-groups.js");
779
+ const chain = (opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)])
780
+ .flatMap((candidate) => resolveModelGroupModels(opts.modelGroups, candidate.provider, candidate.model, { preferDefault: !candidate.model || candidate.model === "default" })
781
+ .map((model) => ({ ...candidate, model: model || undefined })));
782
+ if (opts.employeeChannelOnly && !opts.cliCandidates?.length) {
783
+ throw new QuickCommitError("系统员工没有可用的 CLI 候选,已跳过 SDK 候选。", "AI_FALLBACK_FAILED");
784
+ }
785
+ const deadline = Date.now() + Math.max(1, opts.budgetMs ?? CLI_CHAIN_BUDGET_MS);
779
786
  if (chain.length === 1) {
780
- return parseOutput(await callCliAiText(request, cwd, language, candidateOptions(opts, chain[0])));
787
+ return parseOutput(await callCliAiText(request, cwd, language, { ...candidateOptions(opts, chain[0]), deadline }));
781
788
  }
782
789
  const installed = chain.filter((candidate) => providerCliInstalled(candidate.provider));
783
790
  const attempts = installed.length ? installed : chain;
784
- const deadline = Date.now() + CLI_CHAIN_BUDGET_MS;
785
791
  const errors = [];
786
792
  for (const candidate of attempts) {
787
793
  if (Date.now() >= deadline) {
package/dist/git-utils.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFile, execFileSync, } from "node:child_process";
2
+ import { buildChildEnv } from "./env-utils.js";
2
3
  const DEFAULT_GIT_TIMEOUT_MS = 30_000;
3
4
  const DEFAULT_GIT_MAX_BUFFER = 10 * 1024 * 1024;
4
5
  function positiveInteger(value, fallback) {
@@ -8,7 +9,7 @@ function positiveInteger(value, fallback) {
8
9
  }
9
10
  function gitEnvironment(opts) {
10
11
  return {
11
- ...process.env,
12
+ ...buildChildEnv(true),
12
13
  ...(opts.env ?? {}),
13
14
  GIT_TERMINAL_PROMPT: "0",
14
15
  GIT_ASKPASS: "true",