@ai-setting/roy-task-plugin 0.4.2 → 0.6.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.
package/dist/cli.js CHANGED
@@ -3,28 +3,666 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // ../../../packages/cli/src/daemon/runners/types.ts
6
- var RUNTIMES = Object.freeze(["opencode", "claude"]);
6
+ var RUNTIMES = Object.freeze(["opencode", "claude", "codex"]);
7
7
  var DEFAULT_RUNTIME = "opencode";
8
8
  var DEFAULT_RUNTIME_BIN = Object.freeze({
9
9
  opencode: "opencode",
10
- claude: "claude"
10
+ claude: "claude",
11
+ codex: "codex"
11
12
  });
12
13
  function isRuntimeName(value) {
13
14
  return RUNTIMES.includes(value);
14
15
  }
15
16
 
17
+ // ../../../packages/cli/src/runtime/spawn.ts
18
+ import { spawn } from "node:child_process";
19
+ function runCapture(cmd, opts = {}) {
20
+ return new Promise((resolve, reject) => {
21
+ let child;
22
+ try {
23
+ child = spawn(cmd[0], cmd.slice(1), {
24
+ stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
25
+ env: opts.env ? normalizeEnv(opts.env) : process.env,
26
+ cwd: opts.cwd,
27
+ windowsHide: true
28
+ });
29
+ } catch (err) {
30
+ reject(err);
31
+ return;
32
+ }
33
+ let stdout = "";
34
+ let stderr = "";
35
+ let timedOut = false;
36
+ child.stdout?.setEncoding("utf8");
37
+ child.stderr?.setEncoding("utf8");
38
+ child.stdout?.on("data", (chunk) => {
39
+ stdout += chunk;
40
+ });
41
+ child.stderr?.on("data", (chunk) => {
42
+ stderr += chunk;
43
+ });
44
+ const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
45
+ timedOut = true;
46
+ try {
47
+ child.kill("SIGTERM");
48
+ } catch {}
49
+ setTimeout(() => {
50
+ try {
51
+ child.kill("SIGKILL");
52
+ } catch {}
53
+ }, 2000).unref?.();
54
+ }, opts.timeoutMs) : undefined;
55
+ timer?.unref?.();
56
+ child.on("error", (err) => {
57
+ if (timer)
58
+ clearTimeout(timer);
59
+ reject(err);
60
+ });
61
+ child.on("close", (code, signal) => {
62
+ if (timer)
63
+ clearTimeout(timer);
64
+ if (timedOut) {
65
+ resolve({ code: 124, stdout, stderr });
66
+ return;
67
+ }
68
+ resolve({ code: code ?? (signal ? 1 : 0), stdout, stderr });
69
+ });
70
+ });
71
+ }
72
+ function spawnStreaming(cmd, opts = {}) {
73
+ const child = spawn(cmd[0], cmd.slice(1), {
74
+ stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
75
+ env: opts.env ? normalizeEnv(opts.env) : process.env,
76
+ cwd: opts.cwd,
77
+ windowsHide: true
78
+ });
79
+ let timedOut = false;
80
+ const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
81
+ timedOut = true;
82
+ try {
83
+ child.kill("SIGTERM");
84
+ } catch {}
85
+ }, opts.timeoutMs) : undefined;
86
+ timer?.unref?.();
87
+ if (opts.onStdout || opts.onStderr) {
88
+ child.stdout?.setEncoding("utf8");
89
+ child.stderr?.setEncoding("utf8");
90
+ child.stdout?.on("data", (chunk) => opts.onStdout?.(chunk));
91
+ child.stderr?.on("data", (chunk) => opts.onStderr?.(chunk));
92
+ } else {
93
+ child.stdout?.resume();
94
+ child.stderr?.resume();
95
+ }
96
+ const exited = new Promise((resolve, reject) => {
97
+ child.on("error", (err) => {
98
+ if (timer)
99
+ clearTimeout(timer);
100
+ reject(err);
101
+ });
102
+ child.on("close", (code, signal) => {
103
+ if (timer)
104
+ clearTimeout(timer);
105
+ if (timedOut) {
106
+ resolve(124);
107
+ return;
108
+ }
109
+ resolve(code ?? (signal ? 1 : 0));
110
+ });
111
+ });
112
+ return {
113
+ exited,
114
+ pid: child.pid,
115
+ kill(signal = "SIGTERM") {
116
+ try {
117
+ child.kill(signal);
118
+ } catch {}
119
+ }
120
+ };
121
+ }
122
+ function assertEofStdin(value) {
123
+ if (value !== undefined && value !== "ignore") {
124
+ throw new Error(`refusing to spawn with stdin=${String(value)}: fd 0 must be an EOF source ` + '(use "ignore"); an inherited socket hangs `opencode run` (see #4376)');
125
+ }
126
+ return "ignore";
127
+ }
128
+ function normalizeEnv(env) {
129
+ const out = {};
130
+ for (const [k, v] of Object.entries(env)) {
131
+ if (typeof v === "string")
132
+ out[k] = v;
133
+ }
134
+ return out;
135
+ }
136
+ function isMissingBinary(err) {
137
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
138
+ }
139
+
140
+ // ../../../packages/cli/src/daemon/task-created.ts
141
+ var CREATED_LINE = /Task created(?:\s+successfully)?\s*:\s*#(\d+)/;
142
+ function extractCreatedTask(text, titleHint) {
143
+ if (!text)
144
+ return;
145
+ return fromJson(text) ?? fromEmbeddedTaskObject(text) ?? fromTaskObjectBlob(text) ?? fromSuccessLine(text, titleHint);
146
+ }
147
+ function uniqueTaskId(text) {
148
+ let id;
149
+ for (const m of text.matchAll(/"taskId"\s*:\s*(\d+)/g)) {
150
+ const n = Number(m[1]);
151
+ if (!Number.isInteger(n) || n <= 0)
152
+ return;
153
+ if (id === undefined)
154
+ id = n;
155
+ else if (id !== n)
156
+ return;
157
+ }
158
+ return id;
159
+ }
160
+ function fromJson(text) {
161
+ const trimmed = text.trim();
162
+ if (!trimmed.startsWith("{"))
163
+ return;
164
+ let parsed;
165
+ try {
166
+ parsed = JSON.parse(trimmed);
167
+ } catch {
168
+ return;
169
+ }
170
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
171
+ return;
172
+ const obj = parsed;
173
+ const id = obj["id"];
174
+ const title = obj["title"];
175
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0)
176
+ return;
177
+ if (typeof title !== "string" || title.trim() === "")
178
+ return;
179
+ return { id, title: title.trim() };
180
+ }
181
+ function fromTaskObjectBlob(text) {
182
+ if (!BLOB_GATE.every((marker) => text.includes(marker)))
183
+ return;
184
+ const id = uniqueTaskId(text);
185
+ if (id === undefined)
186
+ return;
187
+ const title = titleFromCreatedActionTitle(text);
188
+ if (title === undefined)
189
+ return;
190
+ return { id, title };
191
+ }
192
+ function titleFromCreatedActionTitle(text) {
193
+ const m = /"actionTitle"\s*:\s*"Task created:\s*([^"]*)"/.exec(text);
194
+ const title = m?.[1]?.trim();
195
+ if (!title || title.length > 200)
196
+ return;
197
+ return title;
198
+ }
199
+ var BLOB_GATE = ['"taskId"', '"actionTitle"', "Task created:"];
200
+ function fromEmbeddedTaskObject(text) {
201
+ let found;
202
+ for (const obj of topLevelObjects(text)) {
203
+ const task = taskFromObject(obj);
204
+ if (!task)
205
+ continue;
206
+ if (found === undefined)
207
+ found = task;
208
+ else if (found.id !== task.id)
209
+ return;
210
+ }
211
+ return found;
212
+ }
213
+ function taskFromObject(obj) {
214
+ const id = obj["id"];
215
+ const title = obj["title"];
216
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0)
217
+ return;
218
+ if (typeof title !== "string" || title.trim() === "")
219
+ return;
220
+ return { id, title: title.trim() };
221
+ }
222
+ function topLevelObjects(text) {
223
+ const out = [];
224
+ let depth = 0;
225
+ let inString = false;
226
+ let escaped = false;
227
+ let start = -1;
228
+ for (let i = 0;i < text.length; i++) {
229
+ const ch = text[i];
230
+ if (inString) {
231
+ if (escaped)
232
+ escaped = false;
233
+ else if (ch === "\\")
234
+ escaped = true;
235
+ else if (ch === '"')
236
+ inString = false;
237
+ continue;
238
+ }
239
+ if (ch === '"') {
240
+ inString = true;
241
+ continue;
242
+ }
243
+ if (ch === "{") {
244
+ if (depth === 0 && !insideArray(text, i))
245
+ start = i;
246
+ depth++;
247
+ continue;
248
+ }
249
+ if (ch === "]" && depth === 0)
250
+ continue;
251
+ if (ch !== "}")
252
+ continue;
253
+ depth--;
254
+ if (depth !== 0 || start < 0)
255
+ continue;
256
+ const slice = text.slice(start, i + 1);
257
+ start = -1;
258
+ try {
259
+ const parsed = JSON.parse(slice);
260
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
261
+ out.push(parsed);
262
+ }
263
+ } catch {}
264
+ }
265
+ return out;
266
+ }
267
+ function insideArray(text, at) {
268
+ let brackets = 0;
269
+ let braces = 0;
270
+ let inString = false;
271
+ let escaped = false;
272
+ for (let i = 0;i < at; i++) {
273
+ const ch = text[i];
274
+ if (inString) {
275
+ if (escaped)
276
+ escaped = false;
277
+ else if (ch === "\\")
278
+ escaped = true;
279
+ else if (ch === '"')
280
+ inString = false;
281
+ continue;
282
+ }
283
+ if (ch === '"')
284
+ inString = true;
285
+ else if (ch === "{")
286
+ braces++;
287
+ else if (ch === "}")
288
+ braces--;
289
+ else if (ch === "[")
290
+ brackets++;
291
+ else if (ch === "]")
292
+ brackets--;
293
+ }
294
+ return brackets > 0 && braces <= 0;
295
+ }
296
+ function fromSuccessLine(text, titleHint) {
297
+ const m = CREATED_LINE.exec(text);
298
+ if (!m)
299
+ return;
300
+ const id = Number(m[1]);
301
+ if (!Number.isInteger(id) || id <= 0)
302
+ return;
303
+ const title = titleFromCliSummary(text) || titleHint?.trim();
304
+ return title ? { id, title } : { id };
305
+ }
306
+ function titleFromCliSummary(text) {
307
+ const at = CREATED_LINE.exec(text);
308
+ if (!at)
309
+ return;
310
+ const rest = text.slice(at.index + at[0].length);
311
+ const m = /^\s*Title:\s*(.+)$/m.exec(rest);
312
+ const title = m?.[1]?.trim();
313
+ if (!title || title.length > 200)
314
+ return;
315
+ return title;
316
+ }
317
+ function isTaskCreateTool(name) {
318
+ return /(^|__)task_create$/.test(name);
319
+ }
320
+
321
+ // ../../../packages/cli/src/daemon/ndjson.ts
322
+ class LineSplitter {
323
+ tail = "";
324
+ pushChunk(chunk) {
325
+ this.tail += chunk;
326
+ const out = [];
327
+ let idx = this.tail.indexOf(`
328
+ `);
329
+ while (idx !== -1) {
330
+ out.push(this.tail.slice(0, idx));
331
+ this.tail = this.tail.slice(idx + 1);
332
+ idx = this.tail.indexOf(`
333
+ `);
334
+ }
335
+ return out;
336
+ }
337
+ finish() {
338
+ if (!this.tail)
339
+ return [];
340
+ const line = this.tail;
341
+ this.tail = "";
342
+ return [line];
343
+ }
344
+ }
345
+
346
+ class NdjsonAssembler {
347
+ tail = "";
348
+ texts = [];
349
+ error;
350
+ toolCalls = 0;
351
+ sessionId;
352
+ tools = [];
353
+ createdTask;
354
+ pushChunk(chunk) {
355
+ this.tail += chunk;
356
+ let idx = this.tail.indexOf(`
357
+ `);
358
+ while (idx !== -1) {
359
+ const line = this.tail.slice(0, idx);
360
+ this.tail = this.tail.slice(idx + 1);
361
+ this.parseLine(line);
362
+ idx = this.tail.indexOf(`
363
+ `);
364
+ }
365
+ }
366
+ pushLine(line) {
367
+ this.parseLine(line);
368
+ }
369
+ finishIncomplete() {
370
+ return this.tail.trim();
371
+ }
372
+ parseLine(line) {
373
+ const trimmed = line.trim();
374
+ if (!trimmed || trimmed[0] !== "{")
375
+ return;
376
+ let parsed;
377
+ try {
378
+ parsed = JSON.parse(trimmed);
379
+ } catch {
380
+ return;
381
+ }
382
+ if (parsed.sessionID)
383
+ this.sessionId = parsed.sessionID;
384
+ if (parsed.type === "text" && typeof parsed.part?.text === "string") {
385
+ this.texts.push(parsed.part.text);
386
+ return;
387
+ }
388
+ if (parsed.type === "tool_use") {
389
+ this.toolCalls += 1;
390
+ const name = parsed.part?.tool ?? parsed.tool;
391
+ if (typeof name === "string" && name) {
392
+ this.tools.push(name);
393
+ }
394
+ if (this.createdTask === undefined && typeof name === "string") {
395
+ const part = parsed.part;
396
+ const output = part?.state?.output;
397
+ const title = part?.state?.input?.["title"];
398
+ if (isTaskCreateTool(name) && typeof output === "string") {
399
+ this.createdTask = extractCreatedTask(output, typeof title === "string" ? title : undefined);
400
+ }
401
+ }
402
+ return;
403
+ }
404
+ if (parsed.type === "error") {
405
+ if (this.error === undefined) {
406
+ this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
407
+ }
408
+ }
409
+ }
410
+ finish() {
411
+ if (this.tail.trim()) {
412
+ this.parseLine(this.tail);
413
+ this.tail = "";
414
+ }
415
+ const text = this.texts.join("").trim();
416
+ return {
417
+ text,
418
+ error: this.error,
419
+ toolCalls: this.toolCalls,
420
+ sessionId: this.sessionId,
421
+ tools: this.tools,
422
+ createdTask: this.createdTask
423
+ };
424
+ }
425
+ }
426
+
427
+ // ../../../packages/cli/src/daemon/runners/codex-stream.ts
428
+ var CODEX_ITEM_TYPES = Object.freeze({
429
+ AGENT_MESSAGE: "agent_message",
430
+ COMMAND_EXECUTION: "command_execution",
431
+ ERROR: "error"
432
+ });
433
+
434
+ class CodexStreamDecoder {
435
+ lines = new LineSplitter;
436
+ state = { tools: [], errors: [], turnCompleted: false };
437
+ pushChunk(chunk) {
438
+ for (const line of this.lines.pushChunk(chunk))
439
+ this.consume(line);
440
+ }
441
+ finish() {
442
+ for (const line of this.lines.finish())
443
+ this.consume(line);
444
+ return this.toResult();
445
+ }
446
+ snapshot() {
447
+ return { ...this.state, tools: [...this.state.tools], errors: [...this.state.errors] };
448
+ }
449
+ consume(rawLine) {
450
+ const line = rawLine.trim();
451
+ if (!line.startsWith("{"))
452
+ return;
453
+ let event;
454
+ try {
455
+ event = JSON.parse(line);
456
+ } catch {
457
+ return;
458
+ }
459
+ if (typeof event !== "object" || event === null)
460
+ return;
461
+ const ev = event;
462
+ if (ev.type === "thread.started") {
463
+ const id = ev.thread_id;
464
+ if (typeof id === "string" && id)
465
+ this.state.sessionId = id;
466
+ return;
467
+ }
468
+ if (ev.type === "turn.completed") {
469
+ this.state.turnCompleted = true;
470
+ return;
471
+ }
472
+ if (ev.type === "item.started" || ev.type === "item.completed") {
473
+ this.consumeItem(ev.item, ev.type === "item.completed");
474
+ }
475
+ }
476
+ consumeItem(raw, completed) {
477
+ if (typeof raw !== "object" || raw === null)
478
+ return;
479
+ const item = raw;
480
+ if (item.type === CODEX_ITEM_TYPES.AGENT_MESSAGE) {
481
+ if (typeof item.text === "string" && item.text.length > 0) {
482
+ this.state.text = item.text;
483
+ }
484
+ return;
485
+ }
486
+ if (item.type === CODEX_ITEM_TYPES.ERROR) {
487
+ if (typeof item.message === "string" && item.message.length > 0) {
488
+ this.state.errors.push(item.message);
489
+ }
490
+ return;
491
+ }
492
+ if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
493
+ const id = item.id;
494
+ if (typeof id === "string" && id && this.state.tools.includes(id)) {} else {
495
+ this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
496
+ }
497
+ if (completed && this.state.createdTask === undefined) {
498
+ this.state.createdTask = extractCreatedTask(item.aggregated_output);
499
+ }
500
+ }
501
+ }
502
+ toResult() {
503
+ const result = {
504
+ text: this.state.text ?? ""
505
+ };
506
+ if (this.state.sessionId)
507
+ result.sessionId = this.state.sessionId;
508
+ if (this.state.tools.length > 0)
509
+ result.toolCalls = this.state.tools.length;
510
+ if (this.state.createdTask)
511
+ result.createdTask = this.state.createdTask;
512
+ return result;
513
+ }
514
+ }
515
+
516
+ // ../../../packages/cli/src/daemon/runners/codex-sessions.ts
517
+ function createCodexSessionRegistry() {
518
+ const sessions = new Map;
519
+ return {
520
+ get: (key) => sessions.get(key),
521
+ set: (key, id) => {
522
+ sessions.set(key, id);
523
+ },
524
+ delete: (key) => {
525
+ sessions.delete(key);
526
+ },
527
+ size: () => sessions.size
528
+ };
529
+ }
530
+
531
+ // ../../../packages/cli/src/daemon/runners/codex.ts
532
+ var DEFAULT_CODEX_TIMEOUT_MS = 10 * 60 * 1000;
533
+ var CODEX_STDIO = Object.freeze([
534
+ "ignore",
535
+ "pipe",
536
+ "pipe"
537
+ ]);
538
+ var CODEX_SANDBOX_MODES = Object.freeze([
539
+ "read-only",
540
+ "workspace-write",
541
+ "danger-full-access"
542
+ ]);
543
+ function isCodexSandboxMode(value) {
544
+ return CODEX_SANDBOX_MODES.includes(value);
545
+ }
546
+ function buildCodexArgs(input) {
547
+ const args = ["exec", "--json"];
548
+ if (input.model)
549
+ args.push("--model", input.model);
550
+ args.push("--skip-git-repo-check");
551
+ args.push("-s", input.sandbox ?? "read-only");
552
+ args.push(input.prompt);
553
+ return args;
554
+ }
555
+ function buildCodexResumeArgs(sessionId, input) {
556
+ const args = ["exec", "resume", "--json"];
557
+ if (input.model)
558
+ args.push("--model", input.model);
559
+ args.push("--skip-git-repo-check");
560
+ if (input.sandbox === "danger-full-access") {
561
+ args.push("--dangerously-bypass-approvals-and-sandbox");
562
+ }
563
+ args.push(sessionId);
564
+ args.push(input.prompt);
565
+ return args;
566
+ }
567
+ function makeCodexRunner(opts = {}) {
568
+ const bin = opts.bin ?? "codex";
569
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_CODEX_TIMEOUT_MS;
570
+ const model = opts.model;
571
+ const sandbox = opts.sandbox ?? "read-only";
572
+ const sessions = opts.sessions ?? createCodexSessionRegistry();
573
+ return async function runAgent(input) {
574
+ const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
575
+ const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox }) : buildCodexArgs({ prompt: input.prompt, model, sandbox });
576
+ const decoder = new CodexStreamDecoder;
577
+ let stderr = "";
578
+ let handle;
579
+ try {
580
+ handle = spawnStreaming([bin, ...args], {
581
+ stdin: CODEX_STDIO[0],
582
+ env: { ...process.env, NO_COLOR: "1" },
583
+ timeoutMs,
584
+ onStdout: (chunk) => decoder.pushChunk(chunk),
585
+ onStderr: (chunk) => {
586
+ if (stderr.length < 64 * 1024)
587
+ stderr += chunk;
588
+ }
589
+ });
590
+ } catch (err) {
591
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
592
+ }
593
+ let code;
594
+ try {
595
+ code = await handle.exited;
596
+ } catch (err) {
597
+ if (isMissingBinary(err)) {
598
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
599
+ }
600
+ return { text: "", error: errMsg(err) };
601
+ }
602
+ const result = decoder.finish();
603
+ if (input.sessionId && result.sessionId && !result.error) {
604
+ sessions.set(input.sessionId, result.sessionId);
605
+ }
606
+ if (code === 124) {
607
+ return { ...result, error: `codex timed out after ${timeoutMs}ms` };
608
+ }
609
+ if (result.error)
610
+ return result;
611
+ if (code !== 0 && !result.text) {
612
+ return {
613
+ text: "",
614
+ toolCalls: result.toolCalls,
615
+ tools: result.tools,
616
+ createdTask: result.createdTask,
617
+ error: `codex exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
618
+ };
619
+ }
620
+ return result;
621
+ };
622
+ }
623
+ function errMsg(err) {
624
+ if (err instanceof Error)
625
+ return err.message;
626
+ return String(err);
627
+ }
628
+
16
629
  // ../../../packages/cli/src/args.ts
17
630
  var DEFAULT_PROFILE = "default";
18
631
  var DEFAULT_AGENT = "roy";
19
632
  var DEFAULT_EVENT_KEY = "im.message.receive_v1";
20
633
  var DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
634
+ function findSubcommand(argv) {
635
+ for (let i = 0;i < argv.length; i++) {
636
+ const raw = argv[i];
637
+ if (raw.startsWith("-")) {
638
+ if (!raw.includes("=") && VALUE_FLAGS.has(raw))
639
+ i++;
640
+ continue;
641
+ }
642
+ return raw;
643
+ }
644
+ return;
645
+ }
646
+ var VALUE_FLAGS = new Set([
647
+ "--profile",
648
+ "--agent",
649
+ "--event-key",
650
+ "--runtime",
651
+ "--command",
652
+ "--runtime-bin",
653
+ "--opencode-bin",
654
+ "--codex-model",
655
+ "--codex-sandbox",
656
+ "--max-concurrency",
657
+ "--timeout-ms"
658
+ ]);
21
659
  var SUBCOMMANDS = new Set(["serve", "status", "help", "version"]);
22
660
  function parseArgs(argv) {
23
661
  if (argv.includes("--help") || argv.includes("-h"))
24
662
  return { command: "help" };
25
663
  if (argv.includes("--version") || argv.includes("-v"))
26
664
  return { command: "version" };
27
- const head = argv.find((a) => !a.startsWith("-"));
665
+ const head = findSubcommand(argv);
28
666
  const command = head ?? "serve";
29
667
  if (!SUBCOMMANDS.has(command)) {
30
668
  return { command: "unknown", error: `unknown command "${command}"` };
@@ -107,6 +745,13 @@ function parseServe(argv) {
107
745
  serve.runtime = v;
108
746
  break;
109
747
  }
748
+ case "--command": {
749
+ const v = take();
750
+ if (v instanceof Error)
751
+ return { command: "unknown", error: v.message };
752
+ serve.command = v;
753
+ break;
754
+ }
110
755
  case "--runtime-bin": {
111
756
  const v = take();
112
757
  if (v instanceof Error)
@@ -121,6 +766,26 @@ function parseServe(argv) {
121
766
  serve.opencodeBin = v;
122
767
  break;
123
768
  }
769
+ case "--codex-model": {
770
+ const v = take();
771
+ if (v instanceof Error)
772
+ return { command: "unknown", error: v.message };
773
+ serve.codexModel = v;
774
+ break;
775
+ }
776
+ case "--codex-sandbox": {
777
+ const v = take();
778
+ if (v instanceof Error)
779
+ return { command: "unknown", error: v.message };
780
+ if (!isCodexSandboxMode(v)) {
781
+ return {
782
+ command: "unknown",
783
+ error: `--codex-sandbox must be one of ${CODEX_SANDBOX_MODES.join("|")}, got "${v}"`
784
+ };
785
+ }
786
+ serve.codexSandbox = v;
787
+ break;
788
+ }
124
789
  case "--max-concurrency": {
125
790
  const v = take();
126
791
  if (v instanceof Error)
@@ -190,6 +855,8 @@ function readValue(argv, flag) {
190
855
  return;
191
856
  }
192
857
  function resolveRuntimeBin(options) {
858
+ if (options.command !== undefined)
859
+ return options.command;
193
860
  if (options.runtimeBin !== undefined)
194
861
  return options.runtimeBin;
195
862
  if (options.runtime === "opencode" && options.opencodeBin !== "opencode") {
@@ -198,7 +865,7 @@ function resolveRuntimeBin(options) {
198
865
  return;
199
866
  }
200
867
  function usage() {
201
- return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode or claude)
868
+ return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode, claude or codex)
202
869
 
203
870
  USAGE
204
871
  roy-task [serve] [options] start the daemon (default command)
@@ -217,9 +884,15 @@ SERVE OPTIONS
217
884
  --agent <name> agent to dispatch to (default: ${DEFAULT_AGENT})
218
885
  --runtime <name> which harness runs the agent (default: ${DEFAULT_RUNTIME})
219
886
  one of: ${RUNTIMES.join(" | ")}
220
- --runtime-bin <path> binary for the selected runtime (default: per-runtime)
221
- --opencode-bin <path> DEPRECATED alias for --runtime-bin
887
+ --command <name|path> executable to spawn, whatever the dialect
888
+ is (default: the runtime's own binary).
889
+ e.g. --runtime claude --command claude-minimax
890
+ --runtime-bin <path> DEPRECATED alias for --command
891
+ --opencode-bin <path> DEPRECATED alias for --command
222
892
  (only applies when --runtime opencode)
893
+ --codex-model <name> model for codex exec --model (default: codex config)
894
+ --codex-sandbox <mode> codex exec -s policy (default: read-only)
895
+ one of: ${CODEX_SANDBOX_MODES.join(" | ")}
223
896
  --max-concurrency <n> max simultaneous agent runs (default: 1)
224
897
  ⚠️ >1 only safe across DIFFERENT chats;
225
898
  same-session concurrency cross-contaminates.
@@ -234,6 +907,12 @@ SERVE OPTIONS
234
907
  never block the text reply.
235
908
 
236
909
  NOTES
910
+ • --runtime vs --command: --runtime picks the *dialect* (which flags and
911
+ which stream decoder), --command picks the *executable* that speaks it.
912
+ So \`--runtime claude --command claude-minimax\` keeps every claude flag
913
+ (--print / --output-format stream-json / --agents / --resume <uuid>) and
914
+ merely spawns the wrapper instead of the bare binary. Use this whenever
915
+ the default binary is not the one that actually works on your machine.
237
916
  • stdin is forced to EOF for the agent child (stdio[0]="ignore"). Without it
238
917
  both \`opencode run\` and \`claude --print\` wait for EOF on a socket and the
239
918
  run hangs forever.
@@ -387,6 +1066,24 @@ function buildDispatchPrompt(input) {
387
1066
  "3. 需要落任务就用 `task_*` 工具(task_create / task_update / …),",
388
1067
  " 这是你的主要职责;不要只在回答里“描述”你打算做什么。",
389
1068
  "",
1069
+ "## 什么时候该建任务(Task #4433)",
1070
+ "",
1071
+ "任务由**你**决定建不建,守护进程不会替你建。判断标准:",
1072
+ "",
1073
+ "- **该建**:需要多步操作、要动文件/跑命令、要调研并产出结论、",
1074
+ " 耗时预计超过一两分钟、或用户明确要求「跟踪 / 建个任务 / 后续跟进」。",
1075
+ " → 用 `task_create` 建任务,**然后**再开始干活(先立任务,再动手)。",
1076
+ "- **不必建**:寒暄、谢谢、纯问答(「今天几号」「这个参数叫啥」)、",
1077
+ " 一两句话就能答完且不需要任何工具的问题。",
1078
+ " → 直接回答,不要为了「显得勤快」而建一个空任务。",
1079
+ "- **建完要更新**:动手过程中用 `task_update` 推进度 / 改 current_status,",
1080
+ " 干完用 `task_complete` 收尾。建了任务却从不更新,比不建更糟——",
1081
+ " 看板上会留下一个卡住的任务。",
1082
+ "",
1083
+ "⚠️ **卡片与任务一一对应**:只有你建了任务,用户才会收到一张进展卡片;",
1084
+ "没建任务就只有文字回复。所以**不要**为了「想要一张卡片」而去建任务,",
1085
+ "也不要建了任务又不用它——卡片上显示的就是你建的那个任务的真 id 和标题。",
1086
+ "",
390
1087
  "## 环境事实(必须使用下面这些值,不要自己猜)",
391
1088
  "",
392
1089
  `- lark-cli profile:\`${profile}\`(飞书 openId 是 per-app 隔离的,换 profile 会失败)`,
@@ -510,21 +1207,14 @@ function createNoopCardPort() {
510
1207
  async finish() {}
511
1208
  };
512
1209
  }
513
- function buildCardIdentity(input) {
514
- const maxTitle = input.maxTitleChars ?? 60;
515
- const normalized = input.text.replace(/\r\n/g, `
516
- `).trim();
517
- const lines = normalized.split(`
518
- `).map((l) => l.trim()).filter(Boolean);
519
- const first = lines[0] ?? "(非文本消息)";
520
- const rest = lines.slice(1).join(`
521
- `);
522
- const title = first.length > maxTitle ? `${first.slice(0, maxTitle - 1)}…` : first;
1210
+ function buildCardMeta(input) {
523
1211
  return {
524
- id: `om:${input.messageId.slice(-12)}`,
525
- title,
526
- description: rest || first,
527
- goals: "完成用户请求并把结果回复到触发消息所在会话"
1212
+ id: input.task.id,
1213
+ title: input.task.title ?? `Task #${input.task.id}`,
1214
+ description: input.description ?? `由 roy-task 派发,触发消息 ${input.messageId}`,
1215
+ type: "normal",
1216
+ priority: "high",
1217
+ currentStatus: "由 agent 处理中"
528
1218
  };
529
1219
  }
530
1220
  var TOOL_LABELS = {
@@ -558,8 +1248,13 @@ function cardReport(handle) {
558
1248
  out.cardMessageId = handle.messageId;
559
1249
  if (handle.error)
560
1250
  out.cardError = handle.error;
1251
+ if (handle.meta && typeof handle.meta.id === "number")
1252
+ out.taskId = handle.meta.id;
561
1253
  return out;
562
1254
  }
1255
+ function createdTaskReport(createdTask) {
1256
+ return createdTask ? { taskId: createdTask.id } : {};
1257
+ }
563
1258
  function errorMessage(err) {
564
1259
  return err instanceof Error ? err.message : String(err);
565
1260
  }
@@ -654,13 +1349,18 @@ class Dispatcher {
654
1349
  return { status: "dry-run", plan };
655
1350
  }
656
1351
  const run = async () => {
657
- const cardPromise = this.openCard({ chatId, messageId, profile, raw });
658
1352
  const result = await this.deps.runAgent({
659
1353
  prompt: plan.prompt,
660
1354
  agent: plan.agent,
661
1355
  sessionId: plan.sessionId
662
1356
  });
663
- const card = await cardPromise;
1357
+ const card = await this.openCard({
1358
+ chatId,
1359
+ messageId,
1360
+ profile,
1361
+ createdTask: result.createdTask,
1362
+ requestText: extractUserText(raw)
1363
+ });
664
1364
  if (card.messageId) {
665
1365
  await this.patchCard(card, {
666
1366
  chatId,
@@ -676,7 +1376,13 @@ class Dispatcher {
676
1376
  status: "failed",
677
1377
  finalStep: `agent 报错:${result.error}`
678
1378
  });
679
- return { status: "agent-error", plan, error: result.error, ...cardReport(card) };
1379
+ return {
1380
+ status: "agent-error",
1381
+ plan,
1382
+ error: result.error,
1383
+ ...createdTaskReport(result.createdTask),
1384
+ ...cardReport(card)
1385
+ };
680
1386
  }
681
1387
  if (!result.text.trim()) {
682
1388
  await this.closeCard(card, {
@@ -689,6 +1395,7 @@ class Dispatcher {
689
1395
  status: "no-output",
690
1396
  plan,
691
1397
  toolCalls: result.toolCalls ?? 0,
1398
+ ...createdTaskReport(result.createdTask),
692
1399
  ...cardReport(card)
693
1400
  };
694
1401
  }
@@ -699,7 +1406,13 @@ class Dispatcher {
699
1406
  status: "completed",
700
1407
  finalStep: "--no-reply:agent 已完成,未发送回复"
701
1408
  });
702
- return { status: "no-reply", plan, text: result.text, ...cardReport(card) };
1409
+ return {
1410
+ status: "no-reply",
1411
+ plan,
1412
+ text: result.text,
1413
+ ...createdTaskReport(result.createdTask),
1414
+ ...cardReport(card)
1415
+ };
703
1416
  }
704
1417
  const sent = await this.deps.reply({
705
1418
  messageId,
@@ -743,23 +1456,18 @@ class Dispatcher {
743
1456
  const handle = { messageId: "" };
744
1457
  if (!this.deps.card)
745
1458
  return handle;
1459
+ if (!input.createdTask)
1460
+ return handle;
1461
+ const meta = buildCardMeta({
1462
+ task: input.createdTask,
1463
+ messageId: input.messageId,
1464
+ ...input.requestText ? { description: input.requestText } : {}
1465
+ });
746
1466
  try {
747
- const identity = buildCardIdentity({
748
- messageId: input.messageId,
749
- text: extractUserText(input.raw)
750
- });
751
1467
  const started = await this.deps.card.start({
752
1468
  chatId: input.chatId,
753
1469
  profile: input.profile,
754
- meta: {
755
- id: identity.id,
756
- title: identity.title,
757
- description: identity.description,
758
- goals: identity.goals,
759
- type: "normal",
760
- priority: "high",
761
- currentStatus: "已收到消息,正在处理"
762
- },
1470
+ meta,
763
1471
  milestones: [
764
1472
  {
765
1473
  title: "收到消息",
@@ -770,6 +1478,7 @@ class Dispatcher {
770
1478
  });
771
1479
  handle.messageId = started.messageId;
772
1480
  handle.pinned = started.pinned;
1481
+ handle.meta = meta;
773
1482
  } catch (err) {
774
1483
  handle.error = errorMessage(err);
775
1484
  }
@@ -787,6 +1496,7 @@ class Dispatcher {
787
1496
  chatId: input.chatId,
788
1497
  profile: input.profile,
789
1498
  status: input.status,
1499
+ ...handle.meta ? { meta: handle.meta } : {},
790
1500
  processSteps: tools.map(describeToolUse)
791
1501
  });
792
1502
  } catch (err) {
@@ -803,7 +1513,8 @@ class Dispatcher {
803
1513
  profile: input.profile,
804
1514
  status: input.status,
805
1515
  finalStep: input.finalStep,
806
- progress: input.status === "completed" ? 100 : undefined
1516
+ progress: input.status === "completed" ? 100 : undefined,
1517
+ ...handle.meta ? { meta: handle.meta } : {}
807
1518
  });
808
1519
  } catch (err) {
809
1520
  handle.error ??= errorMessage(err);
@@ -811,188 +1522,66 @@ class Dispatcher {
811
1522
  }
812
1523
  buildPlan(input) {
813
1524
  const derive = this.opts.sessionIdFor ?? deriveSessionId;
814
- return {
815
- agent: this.opts.agent ?? "roy",
816
- sessionId: derive({ profile: input.profile, chatId: input.chatId }),
817
- prompt: buildDispatchPrompt({
818
- rawEvent: input.raw,
819
- profile: input.profile,
820
- recommendedAction: input.recommendedAction
821
- }),
822
- chatId: input.chatId,
823
- messageId: input.messageId,
824
- profile: input.profile
825
- };
826
- }
827
- }
828
-
829
- // ../../../packages/cli/src/daemon/es-listener.ts
830
- import { larkCliHandler } from "@ai-setting/roy-agent-core/env/event-source";
831
- function createEventListener(config) {
832
- const eventKeys = config.eventKeys?.length ? config.eventKeys : ["im.message.receive_v1"];
833
- const sourceId = config.sourceId ?? "roy-task-es";
834
- const instance = larkCliHandler.createInstance({
835
- id: sourceId,
836
- name: "roy-task event source",
837
- type: "lark-cli",
838
- enabled: true,
839
- profile: config.profile,
840
- eventKeys
841
- });
842
- let callback;
843
- instance.onEvent((event) => {
844
- const typed = event;
845
- if (!callback)
846
- return;
847
- Promise.resolve(callback(typed)).catch((err) => {
848
- console.error("[roy-task] event handler threw:", err instanceof Error ? err.message : err);
849
- });
850
- });
851
- return {
852
- onEvent(cb) {
853
- callback = cb;
854
- },
855
- async start() {
856
- await instance.start();
857
- },
858
- async stop() {
859
- await instance.stop();
860
- },
861
- status() {
862
- return instance.getStatus();
863
- },
864
- plannedCommands() {
865
- const profileFlag = config.profile ? `--profile ${config.profile} ` : "";
866
- return eventKeys.map((k) => `lark-cli ${profileFlag}event consume ${k} --as bot`);
867
- }
868
- };
869
- }
870
-
871
- // ../../../packages/cli/src/daemon/runners/claude.ts
872
- import { createHash as createHash2 } from "node:crypto";
873
-
874
- // ../../../packages/cli/src/runtime/spawn.ts
875
- import { spawn } from "node:child_process";
876
- function runCapture(cmd, opts = {}) {
877
- return new Promise((resolve, reject) => {
878
- let child;
879
- try {
880
- child = spawn(cmd[0], cmd.slice(1), {
881
- stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
882
- env: opts.env ? normalizeEnv(opts.env) : process.env,
883
- cwd: opts.cwd,
884
- windowsHide: true
885
- });
886
- } catch (err) {
887
- reject(err);
888
- return;
889
- }
890
- let stdout = "";
891
- let stderr = "";
892
- let timedOut = false;
893
- child.stdout?.setEncoding("utf8");
894
- child.stderr?.setEncoding("utf8");
895
- child.stdout?.on("data", (chunk) => {
896
- stdout += chunk;
897
- });
898
- child.stderr?.on("data", (chunk) => {
899
- stderr += chunk;
900
- });
901
- const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
902
- timedOut = true;
903
- try {
904
- child.kill("SIGTERM");
905
- } catch {}
906
- setTimeout(() => {
907
- try {
908
- child.kill("SIGKILL");
909
- } catch {}
910
- }, 2000).unref?.();
911
- }, opts.timeoutMs) : undefined;
912
- timer?.unref?.();
913
- child.on("error", (err) => {
914
- if (timer)
915
- clearTimeout(timer);
916
- reject(err);
917
- });
918
- child.on("close", (code, signal) => {
919
- if (timer)
920
- clearTimeout(timer);
921
- if (timedOut) {
922
- resolve({ code: 124, stdout, stderr });
923
- return;
924
- }
925
- resolve({ code: code ?? (signal ? 1 : 0), stdout, stderr });
926
- });
927
- });
1525
+ return {
1526
+ agent: this.opts.agent ?? "roy",
1527
+ sessionId: derive({ profile: input.profile, chatId: input.chatId }),
1528
+ prompt: buildDispatchPrompt({
1529
+ rawEvent: input.raw,
1530
+ profile: input.profile,
1531
+ recommendedAction: input.recommendedAction
1532
+ }),
1533
+ chatId: input.chatId,
1534
+ messageId: input.messageId,
1535
+ profile: input.profile
1536
+ };
1537
+ }
928
1538
  }
929
- function spawnStreaming(cmd, opts = {}) {
930
- const child = spawn(cmd[0], cmd.slice(1), {
931
- stdio: [assertEofStdin(opts.stdin), "pipe", "pipe"],
932
- env: opts.env ? normalizeEnv(opts.env) : process.env,
933
- cwd: opts.cwd,
934
- windowsHide: true
1539
+
1540
+ // ../../../packages/cli/src/daemon/es-listener.ts
1541
+ import { larkCliHandler } from "@ai-setting/roy-agent-core/env/event-source";
1542
+ function createEventListener(config) {
1543
+ const eventKeys = config.eventKeys?.length ? config.eventKeys : ["im.message.receive_v1"];
1544
+ const sourceId = config.sourceId ?? "roy-task-es";
1545
+ const instance = larkCliHandler.createInstance({
1546
+ id: sourceId,
1547
+ name: "roy-task event source",
1548
+ type: "lark-cli",
1549
+ enabled: true,
1550
+ profile: config.profile,
1551
+ eventKeys
935
1552
  });
936
- let timedOut = false;
937
- const timer = opts.timeoutMs && opts.timeoutMs > 0 ? setTimeout(() => {
938
- timedOut = true;
939
- try {
940
- child.kill("SIGTERM");
941
- } catch {}
942
- }, opts.timeoutMs) : undefined;
943
- timer?.unref?.();
944
- if (opts.onStdout || opts.onStderr) {
945
- child.stdout?.setEncoding("utf8");
946
- child.stderr?.setEncoding("utf8");
947
- child.stdout?.on("data", (chunk) => opts.onStdout?.(chunk));
948
- child.stderr?.on("data", (chunk) => opts.onStderr?.(chunk));
949
- } else {
950
- child.stdout?.resume();
951
- child.stderr?.resume();
952
- }
953
- const exited = new Promise((resolve, reject) => {
954
- child.on("error", (err) => {
955
- if (timer)
956
- clearTimeout(timer);
957
- reject(err);
958
- });
959
- child.on("close", (code, signal) => {
960
- if (timer)
961
- clearTimeout(timer);
962
- if (timedOut) {
963
- resolve(124);
964
- return;
965
- }
966
- resolve(code ?? (signal ? 1 : 0));
1553
+ let callback;
1554
+ instance.onEvent((event) => {
1555
+ const typed = event;
1556
+ if (!callback)
1557
+ return;
1558
+ Promise.resolve(callback(typed)).catch((err) => {
1559
+ console.error("[roy-task] event handler threw:", err instanceof Error ? err.message : err);
967
1560
  });
968
1561
  });
969
1562
  return {
970
- exited,
971
- pid: child.pid,
972
- kill(signal = "SIGTERM") {
973
- try {
974
- child.kill(signal);
975
- } catch {}
1563
+ onEvent(cb) {
1564
+ callback = cb;
1565
+ },
1566
+ async start() {
1567
+ await instance.start();
1568
+ },
1569
+ async stop() {
1570
+ await instance.stop();
1571
+ },
1572
+ status() {
1573
+ return instance.getStatus();
1574
+ },
1575
+ plannedCommands() {
1576
+ const profileFlag = config.profile ? `--profile ${config.profile} ` : "";
1577
+ return eventKeys.map((k) => `lark-cli ${profileFlag}event consume ${k} --as bot`);
976
1578
  }
977
1579
  };
978
1580
  }
979
- function assertEofStdin(value) {
980
- if (value !== undefined && value !== "ignore") {
981
- throw new Error(`refusing to spawn with stdin=${String(value)}: fd 0 must be an EOF source ` + '(use "ignore"); an inherited socket hangs `opencode run` (see #4376)');
982
- }
983
- return "ignore";
984
- }
985
- function normalizeEnv(env) {
986
- const out = {};
987
- for (const [k, v] of Object.entries(env)) {
988
- if (typeof v === "string")
989
- out[k] = v;
990
- }
991
- return out;
992
- }
993
- function isMissingBinary(err) {
994
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
995
- }
1581
+
1582
+ // ../../../packages/cli/src/daemon/runners/claude.ts
1583
+ import { createHash as createHash2 } from "node:crypto";
1584
+ import { appendFileSync } from "node:fs";
996
1585
 
997
1586
  // ../../../packages/cli/src/daemon/runners/claude-agents.ts
998
1587
  var CLAUDE_ROY_AGENT_NAME = "roy";
@@ -1142,102 +1731,6 @@ function createClaudeSessionRegistry() {
1142
1731
  };
1143
1732
  }
1144
1733
 
1145
- // ../../../packages/cli/src/daemon/ndjson.ts
1146
- class LineSplitter {
1147
- tail = "";
1148
- pushChunk(chunk) {
1149
- this.tail += chunk;
1150
- const out = [];
1151
- let idx = this.tail.indexOf(`
1152
- `);
1153
- while (idx !== -1) {
1154
- out.push(this.tail.slice(0, idx));
1155
- this.tail = this.tail.slice(idx + 1);
1156
- idx = this.tail.indexOf(`
1157
- `);
1158
- }
1159
- return out;
1160
- }
1161
- finish() {
1162
- if (!this.tail)
1163
- return [];
1164
- const line = this.tail;
1165
- this.tail = "";
1166
- return [line];
1167
- }
1168
- }
1169
-
1170
- class NdjsonAssembler {
1171
- tail = "";
1172
- texts = [];
1173
- error;
1174
- toolCalls = 0;
1175
- sessionId;
1176
- tools = [];
1177
- pushChunk(chunk) {
1178
- this.tail += chunk;
1179
- let idx = this.tail.indexOf(`
1180
- `);
1181
- while (idx !== -1) {
1182
- const line = this.tail.slice(0, idx);
1183
- this.tail = this.tail.slice(idx + 1);
1184
- this.parseLine(line);
1185
- idx = this.tail.indexOf(`
1186
- `);
1187
- }
1188
- }
1189
- pushLine(line) {
1190
- this.parseLine(line);
1191
- }
1192
- finishIncomplete() {
1193
- return this.tail.trim();
1194
- }
1195
- parseLine(line) {
1196
- const trimmed = line.trim();
1197
- if (!trimmed || trimmed[0] !== "{")
1198
- return;
1199
- let parsed;
1200
- try {
1201
- parsed = JSON.parse(trimmed);
1202
- } catch {
1203
- return;
1204
- }
1205
- if (parsed.sessionID)
1206
- this.sessionId = parsed.sessionID;
1207
- if (parsed.type === "text" && typeof parsed.part?.text === "string") {
1208
- this.texts.push(parsed.part.text);
1209
- return;
1210
- }
1211
- if (parsed.type === "tool_use") {
1212
- this.toolCalls += 1;
1213
- const name = parsed.part?.tool ?? parsed.tool;
1214
- if (typeof name === "string" && name) {
1215
- this.tools.push(name);
1216
- }
1217
- return;
1218
- }
1219
- if (parsed.type === "error") {
1220
- if (this.error === undefined) {
1221
- this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
1222
- }
1223
- }
1224
- }
1225
- finish() {
1226
- if (this.tail.trim()) {
1227
- this.parseLine(this.tail);
1228
- this.tail = "";
1229
- }
1230
- const text = this.texts.join("").trim();
1231
- return {
1232
- text,
1233
- error: this.error,
1234
- toolCalls: this.toolCalls,
1235
- sessionId: this.sessionId,
1236
- tools: this.tools
1237
- };
1238
- }
1239
- }
1240
-
1241
1734
  // ../../../packages/cli/src/daemon/runners/claude-stream.ts
1242
1735
  function errorDetail(result) {
1243
1736
  if (typeof result.result === "string" && result.result.trim() !== "") {
@@ -1250,10 +1743,34 @@ function errorDetail(result) {
1250
1743
  }
1251
1744
  return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
1252
1745
  }
1746
+ function flattenToolResult(content) {
1747
+ if (typeof content === "string")
1748
+ return content;
1749
+ if (!Array.isArray(content))
1750
+ return "";
1751
+ const parts = [];
1752
+ for (const block of content) {
1753
+ if (typeof block === "string") {
1754
+ parts.push(block);
1755
+ continue;
1756
+ }
1757
+ if (typeof block === "object" && block !== null) {
1758
+ const t = block.text;
1759
+ if (typeof t === "string")
1760
+ parts.push(t);
1761
+ }
1762
+ }
1763
+ return parts.join(`
1764
+ `);
1765
+ }
1253
1766
 
1254
1767
  class ClaudeStreamDecoder {
1255
1768
  lines = new LineSplitter;
1256
- state = { assistantText: [], tools: [] };
1769
+ state = {
1770
+ assistantText: [],
1771
+ tools: [],
1772
+ pendingCreates: new Map
1773
+ };
1257
1774
  pushChunk(chunk) {
1258
1775
  for (const line of this.lines.pushChunk(chunk))
1259
1776
  this.consume(line);
@@ -1286,10 +1803,38 @@ class ClaudeStreamDecoder {
1286
1803
  this.consumeAssistant(ev);
1287
1804
  return;
1288
1805
  }
1806
+ if (ev.type === "user") {
1807
+ this.consumeUser(ev);
1808
+ return;
1809
+ }
1289
1810
  if (ev.type === "result") {
1290
1811
  this.state.result = ev;
1291
1812
  }
1292
1813
  }
1814
+ consumeUser(ev) {
1815
+ const message = ev.message;
1816
+ const content = message?.content;
1817
+ if (!Array.isArray(content))
1818
+ return;
1819
+ for (const block of content) {
1820
+ if (typeof block !== "object" || block === null)
1821
+ continue;
1822
+ const b = block;
1823
+ if (b.type !== "tool_result")
1824
+ continue;
1825
+ const id = typeof b.tool_use_id === "string" ? b.tool_use_id : undefined;
1826
+ const pending = id ? this.state.pendingCreates.get(id) : undefined;
1827
+ const text = flattenToolResult(b.content);
1828
+ if (this.state.createdTask !== undefined)
1829
+ continue;
1830
+ if (pending) {
1831
+ this.state.pendingCreates.delete(id);
1832
+ this.state.createdTask = extractCreatedTask(text, pending.titleHint);
1833
+ continue;
1834
+ }
1835
+ this.state.createdTask = extractCreatedTask(text);
1836
+ }
1837
+ }
1293
1838
  consumeAssistant(ev) {
1294
1839
  const message = ev.message;
1295
1840
  const content = message?.content;
@@ -1302,8 +1847,17 @@ class ClaudeStreamDecoder {
1302
1847
  if (b.type === "text" && typeof b.text === "string") {
1303
1848
  this.state.assistantText.push(b.text);
1304
1849
  } else if (b.type === "tool_use") {
1305
- if (typeof b.name === "string")
1850
+ if (typeof b.name === "string") {
1306
1851
  this.state.tools.push(b.name);
1852
+ if (isTaskCreateTool(b.name) && typeof b.id === "string") {
1853
+ const input = b.input;
1854
+ const title = input?.["title"];
1855
+ this.state.pendingCreates.set(b.id, {
1856
+ tool: b.name,
1857
+ titleHint: typeof title === "string" ? title : undefined
1858
+ });
1859
+ }
1860
+ }
1307
1861
  }
1308
1862
  }
1309
1863
  }
@@ -1311,15 +1865,23 @@ class ClaudeStreamDecoder {
1311
1865
  const { result } = this.state;
1312
1866
  const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
1313
1867
  const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
1868
+ const createdTask = this.state.createdTask;
1314
1869
  if (result?.is_error) {
1315
1870
  const detail = errorDetail(result);
1316
- return { text: "", sessionId: this.state.sessionId, toolCalls, tools, error: detail.slice(0, 1000) };
1871
+ return {
1872
+ text: "",
1873
+ sessionId: this.state.sessionId,
1874
+ toolCalls,
1875
+ tools,
1876
+ createdTask,
1877
+ error: detail.slice(0, 1000)
1878
+ };
1317
1879
  }
1318
1880
  if (typeof result?.result === "string" && result.result.trim() !== "") {
1319
- return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools };
1881
+ return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools, createdTask };
1320
1882
  }
1321
1883
  const fallback = this.state.assistantText.join("").trim();
1322
- return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools };
1884
+ return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools, createdTask };
1323
1885
  }
1324
1886
  }
1325
1887
 
@@ -1380,23 +1942,30 @@ function makeClaudeRunner(opts = {}) {
1380
1942
  stdin: CLAUDE_STDIO[0],
1381
1943
  env: { ...process.env, NO_COLOR: "1" },
1382
1944
  timeoutMs,
1383
- onStdout: (chunk) => decoder.pushChunk(chunk),
1945
+ onStdout: (chunk) => {
1946
+ decoder.pushChunk(chunk);
1947
+ if (process.env["ROY_TASK_DUMP_STREAM"]) {
1948
+ try {
1949
+ appendFileSync(process.env["ROY_TASK_DUMP_STREAM"], chunk);
1950
+ } catch {}
1951
+ }
1952
+ },
1384
1953
  onStderr: (chunk) => {
1385
1954
  if (stderr.length < 64 * 1024)
1386
1955
  stderr += chunk;
1387
1956
  }
1388
1957
  });
1389
1958
  } catch (err) {
1390
- return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
1959
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
1391
1960
  }
1392
1961
  let code;
1393
1962
  try {
1394
1963
  code = await handle.exited;
1395
1964
  } catch (err) {
1396
1965
  if (isMissingBinary(err)) {
1397
- return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
1966
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
1398
1967
  }
1399
- return { text: "", error: errMsg(err) };
1968
+ return { text: "", error: errMsg2(err) };
1400
1969
  }
1401
1970
  const result = decoder.finish();
1402
1971
  const stderrText = stderr.trim();
@@ -1430,7 +1999,7 @@ function makeClaudeRunner(opts = {}) {
1430
1999
  };
1431
2000
  }
1432
2001
  var STALE_SESSION_RE = /No conversation found|not a UUID|session title/i;
1433
- function errMsg(err) {
2002
+ function errMsg2(err) {
1434
2003
  if (err instanceof Error)
1435
2004
  return err.message;
1436
2005
  return String(err);
@@ -1462,16 +2031,16 @@ function makeOpencodeRunner(opts = {}) {
1462
2031
  }
1463
2032
  });
1464
2033
  } catch (err) {
1465
- return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
2034
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
1466
2035
  }
1467
2036
  let code;
1468
2037
  try {
1469
2038
  code = await handle.exited;
1470
2039
  } catch (err) {
1471
2040
  if (isMissingBinary(err)) {
1472
- return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
2041
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
1473
2042
  }
1474
- return { text: "", error: errMsg2(err) };
2043
+ return { text: "", error: errMsg3(err) };
1475
2044
  }
1476
2045
  const result = assembler.finish();
1477
2046
  if (code === 124) {
@@ -1480,7 +2049,8 @@ function makeOpencodeRunner(opts = {}) {
1480
2049
  error: `opencode timed out after ${timeoutMs}ms`,
1481
2050
  sessionId: result.sessionId,
1482
2051
  toolCalls: result.toolCalls,
1483
- tools: result.tools
2052
+ tools: result.tools,
2053
+ createdTask: result.createdTask
1484
2054
  };
1485
2055
  }
1486
2056
  if (result.error) {
@@ -1489,7 +2059,8 @@ function makeOpencodeRunner(opts = {}) {
1489
2059
  error: result.error,
1490
2060
  sessionId: result.sessionId,
1491
2061
  toolCalls: result.toolCalls,
1492
- tools: result.tools
2062
+ tools: result.tools,
2063
+ createdTask: result.createdTask
1493
2064
  };
1494
2065
  }
1495
2066
  if (code !== 0 && !result.text) {
@@ -1497,18 +2068,20 @@ function makeOpencodeRunner(opts = {}) {
1497
2068
  text: "",
1498
2069
  error: `opencode exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`,
1499
2070
  toolCalls: result.toolCalls,
1500
- tools: result.tools
2071
+ tools: result.tools,
2072
+ createdTask: result.createdTask
1501
2073
  };
1502
2074
  }
1503
2075
  return {
1504
2076
  text: result.text,
1505
2077
  sessionId: result.sessionId,
1506
2078
  toolCalls: result.toolCalls,
1507
- tools: result.tools
2079
+ tools: result.tools,
2080
+ createdTask: result.createdTask
1508
2081
  };
1509
2082
  };
1510
2083
  }
1511
- function errMsg2(err) {
2084
+ function errMsg3(err) {
1512
2085
  if (err instanceof Error)
1513
2086
  return err.message;
1514
2087
  return String(err);
@@ -1521,6 +2094,12 @@ var FACTORIES = {
1521
2094
  bin: input.bin,
1522
2095
  timeoutMs: input.timeoutMs,
1523
2096
  agentsJson: input.claudeAgentsJson
2097
+ }),
2098
+ codex: (input) => makeCodexRunner({
2099
+ bin: input.bin,
2100
+ timeoutMs: input.timeoutMs,
2101
+ model: input.codexModel,
2102
+ sandbox: isCodexSandboxMode(input.codexSandbox ?? "") ? input.codexSandbox : undefined
1524
2103
  })
1525
2104
  };
1526
2105
  function makeRunner(input = {}) {
@@ -1629,7 +2208,9 @@ async function startServe(options, deps) {
1629
2208
  runAgent: deps.runAgent ?? makeRunner({
1630
2209
  runtime: options.runtime,
1631
2210
  bin: resolveRuntimeBin(options),
1632
- timeoutMs: options.timeoutMs
2211
+ timeoutMs: options.timeoutMs,
2212
+ codexModel: options.codexModel,
2213
+ codexSandbox: options.codexSandbox
1633
2214
  }),
1634
2215
  reply: deps.reply ?? defaultReplyRunner,
1635
2216
  gate,
@@ -1658,7 +2239,7 @@ async function startServe(options, deps) {
1658
2239
  try {
1659
2240
  await listener.stop();
1660
2241
  } catch (err) {
1661
- log(`[roy-task] listener.stop() failed: ${errMsg3(err)}`);
2242
+ log(`[roy-task] listener.stop() failed: ${errMsg4(err)}`);
1662
2243
  }
1663
2244
  log("[roy-task] stopped");
1664
2245
  resolveDone();
@@ -1680,7 +2261,7 @@ async function startServe(options, deps) {
1680
2261
  await stop();
1681
2262
  }
1682
2263
  } catch (err) {
1683
- log(`[roy-task] ${label} dispatch threw: ${errMsg3(err)}`);
2264
+ log(`[roy-task] ${label} dispatch threw: ${errMsg4(err)}`);
1684
2265
  if (options.once)
1685
2266
  await stop();
1686
2267
  }
@@ -1690,7 +2271,7 @@ async function startServe(options, deps) {
1690
2271
  try {
1691
2272
  await listener.start();
1692
2273
  } catch (err) {
1693
- log(`[roy-task] failed to start event listener: ${errMsg3(err)}`);
2274
+ log(`[roy-task] failed to start event listener: ${errMsg4(err)}`);
1694
2275
  clearInterval(sweeper);
1695
2276
  resolveDone();
1696
2277
  throw err;
@@ -1708,7 +2289,11 @@ function formatBanner(options, commands, maxConcurrency) {
1708
2289
  ` profile : ${options.profile}`,
1709
2290
  ` agent : ${options.agent}`,
1710
2291
  ` runtime : ${options.runtime}`,
1711
- ` runtime bin : ${resolveRuntimeBin(options) ?? "(default)"}`,
2292
+ " command : " + (resolveRuntimeBin(options) ?? "(default: " + options.runtime + ")"),
2293
+ ...options.runtime === "codex" ? [
2294
+ ` codex model : ${options.codexModel ?? "(codex config)"}`,
2295
+ ` codex sandbox : ${options.codexSandbox ?? "read-only"}`
2296
+ ] : [],
1712
2297
  ` max concurrency : ${maxConcurrency}`,
1713
2298
  ` reply enabled : ${options.replyEnabled}`,
1714
2299
  ` progress cards : ${options.cardEnabled && !options.dryRun}`,
@@ -1727,13 +2312,15 @@ function cardSuffix(result) {
1727
2312
  return "";
1728
2313
  if (result.cardError)
1729
2314
  return ` card=FAILED(${result.cardError})`;
1730
- return ` card=${result.cardMessageId ?? "(none)"}`;
2315
+ if (result.cardMessageId)
2316
+ return ` card=${result.cardMessageId}`;
2317
+ return " card=none(no task created)";
1731
2318
  }
1732
2319
  function formatResult(label, result) {
1733
2320
  const card = cardSuffix(result);
1734
2321
  switch (result.status) {
1735
2322
  case "dispatched":
1736
- return `[roy-task] ${label} dispatched chat=${result.plan.chatId} session=${result.plan.sessionId} ` + `reply=${result.replyMessageId ?? "(no id)"} chars=${result.text.length}${card}`;
2323
+ return `[roy-task] ${label} dispatched chat=${result.plan.chatId} session=${result.plan.sessionId} ` + `reply=${result.replyMessageId ?? "(no id)"} chars=${result.text.length}` + ` task=${result.taskId ?? "(none)"}${card}`;
1737
2324
  case "dry-run":
1738
2325
  return `[roy-task] ${label} DRY-RUN would run agent=${result.plan.agent} ` + `session=${result.plan.sessionId} chat=${result.plan.chatId} prompt=${result.plan.prompt.length}chars`;
1739
2326
  case "duplicate":
@@ -1750,7 +2337,7 @@ function formatResult(label, result) {
1750
2337
  return `[roy-task] ${label} skipped: ${result.reason}`;
1751
2338
  }
1752
2339
  }
1753
- function errMsg3(err) {
2340
+ function errMsg4(err) {
1754
2341
  return err instanceof Error ? err.message : String(err);
1755
2342
  }
1756
2343
 
@@ -1875,7 +2462,7 @@ function formatStatus(report) {
1875
2462
  }
1876
2463
 
1877
2464
  // ../../../packages/cli/src/main.ts
1878
- var VERSION = "0.4.2";
2465
+ var VERSION = "0.6.0";
1879
2466
  async function main(argv) {
1880
2467
  const parsed = parseArgs(argv);
1881
2468
  switch (parsed.command) {