@nowcrew/daemon 0.5.18 → 0.5.19

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.
package/dist/prompt.js CHANGED
@@ -147,7 +147,10 @@ ${taskAndScheduleCommands}`;
147
147
  : ctx.wakeOrigin === "wecom"
148
148
  ? `
149
149
  - **本轮来自企微,结束本轮前必须给出一条完整回复**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。最终只用一次 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数)发送有实质内容的完整结果;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定,并在本轮未形成可交付内容时发送统一兜底回复。`
150
- : `
150
+ : ctx.wakeOrigin
151
+ ? `
152
+ - **本轮来自${ctx.wakeOrigin === "feishu" ? "飞书" : ctx.wakeOrigin},结束本轮前必须明确选择外部回复决策**:确认、过程进展和内部协作仍用普通 \`crew message send\`,只写入 NowWork。完整结果确实要回复外部会话时,用 \`crew message send --reply-origin\`(同时带当前 channel/thread/content 参数);判断无需回复时,用 \`crew message skip-origin --reason "简短原因"\`。两者必须选择一个;不要把 \`--send-draft\` 当成草稿 ID 或外部回复开关。Server 仍会校验 thread 来源和机器人绑定。`
153
+ : `
151
154
  - **本轮是 NowWork 内部唤醒**:普通 \`crew message send\` 只写入 NowWork。内部唤醒不得使用 \`--reply-origin\`,该参数只回答直接触发本轮的企微原消息。
152
155
  - **绑定会话主动通知**:用户明确要求同步,或最终结果有实质结论、变更或需群用户行动的阻塞时,才用一次 \`crew message send --notify-bound-im\`(同时带当前 channel/thread/content 参数)请求通知当前频道绑定的外部会话。Server 会校验绑定 owner 授权、绑定 Agent 和单轮边界;不能指定收件人。确认、进度、中间结果、无变化和重复内容一律留在 NowWork。`;
153
156
  const interactiveTaskRules = scheduled ? "" : `
package/dist/runner.js CHANGED
@@ -29,7 +29,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
29
29
  const reasoning = ReasoningSchema.safeParse(providerConfig.reasoning);
30
30
  const baseWake = input.wake ?? buildWakePrompt(input.channelId);
31
31
  const executionId = input.runId ?? `legacy-${randomUUID()}`;
32
- const originDecisionFileName = input.wakeOrigin === "wecom"
32
+ const originDecisionFileName = input.wakeOrigin
33
33
  ? `.origin-decision-${executionId}.json`
34
34
  : null;
35
35
  const boundImDecisionFileName = input.scheduled?.externalNotificationPolicy === "agent_decides"
@@ -86,7 +86,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
86
86
  ...(providerConfig.fastMode ? { CREW_FAST_MODE: "1" } : {}),
87
87
  ...(input.scheduled ? { CREW_SCHEDULE_OUTPUT_POLICY: input.scheduled.outputPolicy } : {}),
88
88
  ...(originDecisionFileName ? {
89
- CREW_WAKE_ORIGIN: "wecom",
89
+ CREW_WAKE_ORIGIN: input.wakeOrigin,
90
90
  CREW_ORIGIN_DECISION_FILE: originDecisionFileName,
91
91
  } : {}),
92
92
  ...(boundImDecisionFileName ? {
@@ -169,7 +169,7 @@ export async function runAgent(config, input, onActivity = defaultPrint, onConso
169
169
  ...(local.usage ? { usage: local.usage } : {}),
170
170
  ...(report ? { report } : {}),
171
171
  ...(boundImDecision ? { boundImDecision } : {}),
172
- ...(input.wakeOrigin === "wecom" ? {
172
+ ...(input.wakeOrigin ? {
173
173
  originDecision: originDecision?.decision ?? "missing",
174
174
  ...(originDecision?.decision === "silent" && originDecision.reason
175
175
  ? { originDecisionReason: originDecision.reason }
@@ -1,5 +1,43 @@
1
1
  export const LOCAL_EXECUTION_RUNTIMES = ["claude", "codex", "kimi"];
2
+ export const LOCAL_RUNTIME_CAPABILITIES = Object.freeze({
3
+ claude: Object.freeze({
4
+ transport: "claude-stream-json",
5
+ nativeResume: true,
6
+ systemPromptTransport: "file",
7
+ }),
8
+ codex: Object.freeze({
9
+ transport: "codex-app-server",
10
+ nativeResume: true,
11
+ systemPromptTransport: "protocol",
12
+ }),
13
+ kimi: Object.freeze({
14
+ transport: "kimi-acp",
15
+ nativeResume: true,
16
+ systemPromptTransport: "protocol",
17
+ }),
18
+ });
19
+ export function runtimeCapability(runtime) {
20
+ return LOCAL_RUNTIME_CAPABILITIES[runtime];
21
+ }
2
22
  const LOCAL_EXECUTION_RUNTIME_SET = new Set(LOCAL_EXECUTION_RUNTIMES);
3
23
  export function executableRuntimes(installed) {
4
24
  return [...new Set(installed)].filter((runtime) => LOCAL_EXECUTION_RUNTIME_SET.has(runtime));
5
25
  }
26
+ export function routeRuntimeAttachments(runtime, attachments) {
27
+ if (attachments.length === 0)
28
+ return { nativeImagePaths: [], promptSuffix: "" };
29
+ const lines = attachments.map((attachment) => `- ${JSON.stringify(attachment.path)} (${attachment.mime}, ${attachment.sizeBytes} bytes)`);
30
+ return {
31
+ nativeImagePaths: runtime === "codex"
32
+ ? attachments.filter((attachment) => attachment.mime.startsWith("image/"))
33
+ .map((attachment) => attachment.path)
34
+ : [],
35
+ promptSuffix: [
36
+ "",
37
+ "",
38
+ "## Files attached to the triggering message",
39
+ "These files were downloaded into the local execution workspace. Open/read the relevant files before describing their contents; do not infer contents from filenames alone.",
40
+ ...lines,
41
+ ].join("\n"),
42
+ };
43
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * daemon 被非登录/非交互进程(sh -c / pnpm script / tmux / 后台转发)拉起时,继承的 PATH 常退化为系统
3
+ * 默认,缺用户级 CLI 目录——尤其 Claude 官方原生安装器默认的 `~/.local/bin`。结果 `which claude` 探测
4
+ * 落空、真正 spawn 也 ENOENT。这里在探测与启动前把常见安装目录补进 PATH,保证「扫得到 = 起得来」。
5
+ *
6
+ * codex 装在 `/usr/local/bin`(系统默认 PATH 本就含之)所以不受影响;本模块对已在 PATH 中的目录是无操作。
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { win32, posix } from "node:path";
10
+ const pathApi = (platform) => (platform === "win32" ? win32 : posix);
11
+ /** 该平台常见 CLI 安装目录候选(是否真实存在稍后由 augmentedPath 校验)。 */
12
+ export function commonBinDirs(env = process.env, platform = process.platform) {
13
+ const p = pathApi(platform);
14
+ if (platform === "win32") {
15
+ const dirs = [];
16
+ if (env.USERPROFILE)
17
+ dirs.push(p.join(env.USERPROFILE, ".local", "bin"));
18
+ if (env.APPDATA)
19
+ dirs.push(p.join(env.APPDATA, "npm"));
20
+ if (env.LOCALAPPDATA)
21
+ dirs.push(p.join(env.LOCALAPPDATA, "Microsoft", "WindowsApps"));
22
+ return dirs;
23
+ }
24
+ const dirs = [];
25
+ const home = env.HOME;
26
+ if (home) {
27
+ dirs.push(p.join(home, ".local", "bin"), // Claude 官方原生安装器默认落点
28
+ p.join(home, ".claude", "local"));
29
+ }
30
+ dirs.push("/opt/homebrew/bin", "/usr/local/bin"); // Apple Silicon brew / Intel brew & npm 全局
31
+ if (home) {
32
+ dirs.push(p.join(home, ".npm-global", "bin"), p.join(home, ".bun", "bin"), p.join(home, ".deno", "bin"));
33
+ }
34
+ return dirs;
35
+ }
36
+ /**
37
+ * 在 `env.PATH` 基础上,把候选目录里「真实存在且尚未在 PATH 中」的去重后 prepend,返回新的 PATH 字符串。
38
+ * 用户装的目录优先命中(prepend);全部已存在或都不在磁盘上时,原样返回 `env.PATH`。
39
+ */
40
+ export function augmentedPath(env = process.env, deps = {}, platform = process.platform) {
41
+ const dirExists = deps.dirExists ?? existsSync;
42
+ const sep = pathApi(platform).delimiter;
43
+ const current = env.PATH ?? "";
44
+ const existing = new Set(current.split(sep).filter((entry) => entry.length > 0));
45
+ const extra = [];
46
+ for (const dir of commonBinDirs(env, platform)) {
47
+ if (existing.has(dir) || extra.includes(dir))
48
+ continue;
49
+ if (!dirExists(dir))
50
+ continue;
51
+ extra.push(dir);
52
+ }
53
+ if (extra.length === 0)
54
+ return current;
55
+ return current.length > 0 ? `${extra.join(sep)}${sep}${current}` : extra.join(sep);
56
+ }
@@ -12,6 +12,7 @@ export function buildClaudeArgs(input) {
12
12
  "--verbose",
13
13
  "--output-format",
14
14
  "stream-json",
15
+ "--include-partial-messages",
15
16
  "--append-system-prompt-file",
16
17
  input.systemPromptPath,
17
18
  ];
@@ -0,0 +1,340 @@
1
+ import { once } from "node:events";
2
+ import { createInterface } from "node:readline";
3
+ import { parseArgs } from "node:util";
4
+ import { pathToFileURL } from "node:url";
5
+ import spawn from "cross-spawn";
6
+ import { z } from "zod";
7
+ import { startFirstProgressWatchdog } from "./progress-watchdog.js";
8
+ const RunnerInputSchema = z.object({
9
+ systemPrompt: z.string().min(1),
10
+ wakePrompt: z.string().min(1),
11
+ effectivePermission: z.enum(["sandboxed", "workspace_write", "full_access"]),
12
+ model: z.string().min(1).optional(),
13
+ reasoning: z.string().min(1).optional(),
14
+ sessionId: z.string().min(1).optional(),
15
+ imagePaths: z.array(z.string().min(1)).optional(),
16
+ resume: z.boolean(),
17
+ }).strict();
18
+ const RPC_TIMEOUT_MS = 30_000;
19
+ const ERROR_MESSAGE_CAP = 2_000;
20
+ function safeErrorMessage(error, secrets) {
21
+ let message = error instanceof Error ? error.message : String(error);
22
+ for (const secret of secrets) {
23
+ if (secret)
24
+ message = message.replaceAll(secret, "[prompt redacted]");
25
+ }
26
+ return message.slice(0, ERROR_MESSAGE_CAP);
27
+ }
28
+ function jsonLine(event) {
29
+ if (process.stdout.write(`${JSON.stringify(event)}\n`))
30
+ return Promise.resolve();
31
+ return once(process.stdout, "drain").then(() => undefined);
32
+ }
33
+ function sandboxMode(permission) {
34
+ if (permission === "sandboxed")
35
+ return "read-only";
36
+ if (permission === "workspace_write")
37
+ return "workspace-write";
38
+ return "danger-full-access";
39
+ }
40
+ /** Unexpected approval requests fail closed; only explicit full access may approve operations. */
41
+ export function codexApprovalResponse(method, permission) {
42
+ if (method === "item/commandExecution/requestApproval"
43
+ || method === "item/fileChange/requestApproval") {
44
+ return { decision: permission === "full_access" ? "accept" : "decline" };
45
+ }
46
+ if (method === "item/tool/requestUserInput")
47
+ return { answers: {} };
48
+ return null;
49
+ }
50
+ /** Translate app-server v2 notifications into the daemon's existing runtime event contract. */
51
+ export function mapCodexNotification(method, params) {
52
+ const value = params;
53
+ if (method === "thread/started" && value.thread?.id) {
54
+ return [{ type: "thread.started", thread_id: value.thread.id }];
55
+ }
56
+ if (method !== "item/completed" || value.item === undefined)
57
+ return [];
58
+ if (value.item.type === "agentMessage" && value.item.text) {
59
+ return [{ type: "item.completed", item: { type: "agent_message", text: value.item.text } }];
60
+ }
61
+ if (value.item.type === "commandExecution" && value.item.command) {
62
+ return [{
63
+ type: "item.completed",
64
+ item: {
65
+ type: "command_execution",
66
+ command: value.item.command,
67
+ ...(value.item.status === undefined ? {} : { status: value.item.status }),
68
+ ...(value.item.aggregatedOutput == null ? {} : { aggregated_output: value.item.aggregatedOutput }),
69
+ },
70
+ }];
71
+ }
72
+ return [];
73
+ }
74
+ async function readRunnerInput() {
75
+ process.stdin.setEncoding("utf8");
76
+ let raw = "";
77
+ for await (const chunk of process.stdin)
78
+ raw += String(chunk);
79
+ return RunnerInputSchema.parse(JSON.parse(raw));
80
+ }
81
+ class CodexRpcClient {
82
+ child;
83
+ permission;
84
+ onNotification;
85
+ onFatal;
86
+ nextId = 1;
87
+ pending = new Map();
88
+ closedError = null;
89
+ constructor(child, permission, onNotification, onFatal) {
90
+ this.child = child;
91
+ this.permission = permission;
92
+ this.onNotification = onNotification;
93
+ this.onFatal = onFatal;
94
+ if (child.stdin === null || child.stdout === null) {
95
+ throw new Error("Codex app-server did not expose stdio");
96
+ }
97
+ const lines = createInterface({ input: child.stdout });
98
+ child.stdin.on("error", (error) => this.close(error));
99
+ lines.on("line", (line) => {
100
+ void this.receive(line).catch((error) => {
101
+ this.close(error instanceof Error ? error : new Error(String(error)));
102
+ });
103
+ });
104
+ child.once("error", (error) => this.close(error));
105
+ child.once("close", (code, signal) => {
106
+ this.close(new Error(`Codex app-server exited: code=${code} signal=${signal}`));
107
+ });
108
+ }
109
+ request(method, params, timeoutMs = RPC_TIMEOUT_MS) {
110
+ if (this.closedError !== null)
111
+ return Promise.reject(this.closedError);
112
+ const id = this.nextId++;
113
+ return new Promise((resolve, reject) => {
114
+ const timer = setTimeout(() => {
115
+ this.pending.delete(id);
116
+ reject(new Error(`Codex app-server ${method} timed out after ${timeoutMs}ms`));
117
+ }, timeoutMs);
118
+ this.pending.set(id, { resolve, reject, timer });
119
+ this.write({ jsonrpc: "2.0", id, method, params });
120
+ });
121
+ }
122
+ notify(method, params) {
123
+ this.write({ jsonrpc: "2.0", method, ...(params === undefined ? {} : { params }) });
124
+ }
125
+ write(message) {
126
+ if (this.child.stdin === null || this.child.stdin.destroyed) {
127
+ throw this.closedError ?? new Error("Codex app-server stdin is closed");
128
+ }
129
+ this.child.stdin.write(`${JSON.stringify(message)}\n`);
130
+ }
131
+ async receive(line) {
132
+ let message;
133
+ try {
134
+ message = JSON.parse(line);
135
+ }
136
+ catch {
137
+ process.stderr.write(`Codex app-server emitted invalid JSON: ${line.slice(0, 500)}\n`);
138
+ return;
139
+ }
140
+ if (message.id !== undefined && message.method !== undefined) {
141
+ const result = codexApprovalResponse(message.method, this.permission);
142
+ if (result === null) {
143
+ this.write({
144
+ jsonrpc: "2.0",
145
+ id: message.id,
146
+ error: { code: -32001, message: `NowCrew denied unsupported request ${message.method}` },
147
+ });
148
+ }
149
+ else {
150
+ this.write({ jsonrpc: "2.0", id: message.id, result });
151
+ }
152
+ return;
153
+ }
154
+ if (message.id !== undefined) {
155
+ const pending = this.pending.get(message.id);
156
+ if (pending === undefined)
157
+ return;
158
+ this.pending.delete(message.id);
159
+ clearTimeout(pending.timer);
160
+ if (message.error !== undefined) {
161
+ pending.reject(new Error(`Codex app-server RPC failed: ${message.error.message ?? "unknown error"}`));
162
+ }
163
+ else {
164
+ pending.resolve(message.result);
165
+ }
166
+ return;
167
+ }
168
+ if (message.method !== undefined)
169
+ await this.onNotification(message.method, message.params);
170
+ }
171
+ close(error) {
172
+ if (this.closedError !== null)
173
+ return;
174
+ this.closedError = error;
175
+ this.onFatal(error);
176
+ for (const pending of this.pending.values()) {
177
+ clearTimeout(pending.timer);
178
+ pending.reject(error);
179
+ }
180
+ this.pending.clear();
181
+ }
182
+ }
183
+ async function stopChild(child) {
184
+ if (child.exitCode !== null || child.signalCode !== null)
185
+ return;
186
+ const closed = once(child, "close").then(() => undefined);
187
+ child.kill("SIGTERM");
188
+ let timer;
189
+ const graceful = await Promise.race([
190
+ closed.then(() => true),
191
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), 1_000); }),
192
+ ]);
193
+ if (timer !== undefined)
194
+ clearTimeout(timer);
195
+ if (!graceful && child.exitCode === null && child.signalCode === null) {
196
+ child.kill("SIGKILL");
197
+ await closed;
198
+ }
199
+ }
200
+ export async function runCodexAppServer(bin) {
201
+ const input = await readRunnerInput();
202
+ const child = spawn(bin, ["app-server", "--listen", "stdio://"], {
203
+ cwd: process.cwd(),
204
+ env: process.env,
205
+ stdio: ["pipe", "pipe", "pipe"],
206
+ });
207
+ child.stderr?.pipe(process.stderr, { end: false });
208
+ let threadId = null;
209
+ let announcedThreadId = null;
210
+ let turnId = null;
211
+ let lastUsage;
212
+ let completionResolve;
213
+ let completionReject;
214
+ const completion = new Promise((resolve, reject) => {
215
+ completionResolve = resolve;
216
+ completionReject = reject;
217
+ });
218
+ void completion.catch(() => undefined);
219
+ let firstProgress = startFirstProgressWatchdog(() => undefined);
220
+ firstProgress.stop();
221
+ const rpc = new CodexRpcClient(child, input.effectivePermission, async (method, params) => {
222
+ if (method === "thread/tokenUsage/updated") {
223
+ lastUsage = params.tokenUsage;
224
+ }
225
+ if (method === "turn/started" || method === "item/started"
226
+ || method === "item/agentMessage/delta" || method === "item/completed") {
227
+ firstProgress.observe();
228
+ }
229
+ for (const event of mapCodexNotification(method, params)) {
230
+ const eventThreadId = typeof event.thread_id === "string" ? event.thread_id : null;
231
+ if (event.type === "thread.started" && eventThreadId === announcedThreadId)
232
+ continue;
233
+ if (event.type === "thread.started")
234
+ announcedThreadId = eventThreadId;
235
+ await jsonLine(event);
236
+ }
237
+ if (method === "turn/completed")
238
+ completionResolve(params);
239
+ if (method === "error" && params.willRetry === false) {
240
+ const detail = params.error?.message;
241
+ if (detail)
242
+ process.stderr.write(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`);
243
+ }
244
+ }, completionReject);
245
+ let cancelling = false;
246
+ const cancel = async () => {
247
+ if (cancelling)
248
+ return;
249
+ cancelling = true;
250
+ if (threadId !== null && turnId !== null) {
251
+ await rpc.request("turn/interrupt", { threadId, turnId }, 5_000).catch(() => undefined);
252
+ }
253
+ await stopChild(child);
254
+ };
255
+ const onSignal = () => { void cancel().finally(() => process.exit(130)); };
256
+ process.once("SIGTERM", onSignal);
257
+ process.once("SIGINT", onSignal);
258
+ try {
259
+ await rpc.request("initialize", {
260
+ clientInfo: { name: "nowcrew-daemon", version: "1" },
261
+ capabilities: { experimentalApi: true, requestAttestation: false },
262
+ });
263
+ rpc.notify("initialized");
264
+ const threadParams = {
265
+ cwd: process.cwd(),
266
+ approvalPolicy: "never",
267
+ sandbox: sandboxMode(input.effectivePermission),
268
+ developerInstructions: input.systemPrompt,
269
+ ...(input.model === undefined ? {} : { model: input.model }),
270
+ };
271
+ const thread = input.resume && input.sessionId !== undefined
272
+ ? await rpc.request("thread/resume", { threadId: input.sessionId, ...threadParams })
273
+ : await rpc.request("thread/start", threadParams);
274
+ threadId = thread.thread.id;
275
+ if (announcedThreadId !== threadId) {
276
+ announcedThreadId = threadId;
277
+ await jsonLine({ type: "thread.started", thread_id: threadId });
278
+ }
279
+ firstProgress = startFirstProgressWatchdog(() => {
280
+ completionReject(new Error("Codex produced no semantic progress within the startup window"));
281
+ void cancel();
282
+ });
283
+ const started = await rpc.request("turn/start", {
284
+ threadId,
285
+ input: [
286
+ { type: "text", text: input.wakePrompt, text_elements: [] },
287
+ ...(input.imagePaths ?? []).map((path) => ({ type: "localImage", path })),
288
+ ],
289
+ ...(input.reasoning === undefined || input.reasoning === "default"
290
+ ? {}
291
+ : { effort: input.reasoning }),
292
+ });
293
+ turnId = started.turn.id;
294
+ const completed = await completion;
295
+ firstProgress.stop();
296
+ const usage = lastUsage?.last;
297
+ await jsonLine({
298
+ type: "turn.completed",
299
+ ...(usage === undefined ? {} : {
300
+ usage: {
301
+ input_tokens: usage.inputTokens ?? 0,
302
+ output_tokens: usage.outputTokens ?? 0,
303
+ cached_input_tokens: usage.cachedInputTokens ?? 0,
304
+ },
305
+ }),
306
+ });
307
+ if (completed.turn?.status === "completed")
308
+ return 0;
309
+ const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
310
+ process.stderr.write(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`);
311
+ return completed.turn?.status === "interrupted" ? 130 : 1;
312
+ }
313
+ catch (error) {
314
+ process.stderr.write(`Codex app-server execution failed: ${safeErrorMessage(error, [input.systemPrompt, input.wakePrompt])}\n`);
315
+ return 1;
316
+ }
317
+ finally {
318
+ firstProgress.stop();
319
+ process.off("SIGTERM", onSignal);
320
+ process.off("SIGINT", onSignal);
321
+ await stopChild(child);
322
+ }
323
+ }
324
+ function binFromArgv(argv) {
325
+ const { values } = parseArgs({
326
+ args: [...argv],
327
+ options: { bin: { type: "string" } },
328
+ });
329
+ if (!values.bin)
330
+ throw new Error("--bin is required");
331
+ return values.bin;
332
+ }
333
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
334
+ runCodexAppServer(binFromArgv(process.argv.slice(2)))
335
+ .then((code) => { process.exitCode = code; })
336
+ .catch((error) => {
337
+ process.stderr.write(`Codex app-server runner failed: ${safeErrorMessage(error, [])}\n`);
338
+ process.exitCode = 1;
339
+ });
340
+ }
@@ -23,6 +23,8 @@ export function buildCodexArgs(input) {
23
23
  else if (input.effectivePermission === "full_access" || (input.effectivePermission === undefined && input.dangerous)) {
24
24
  args.push("--dangerously-bypass-approvals-and-sandbox");
25
25
  }
26
+ for (const imagePath of input.imagePaths ?? [])
27
+ args.push("--image", imagePath);
26
28
  // `-` instructs codex exec to read the prompt from stdin. Keeping the complete prompt out of argv
27
29
  // avoids Windows' command-line length limit when a thread carries a large wake context.
28
30
  args.push("-");