@ai-setting/roy-task-plugin 0.4.1 → 0.5.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,16 +3,432 @@ 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/ndjson.ts
141
+ class LineSplitter {
142
+ tail = "";
143
+ pushChunk(chunk) {
144
+ this.tail += chunk;
145
+ const out = [];
146
+ let idx = this.tail.indexOf(`
147
+ `);
148
+ while (idx !== -1) {
149
+ out.push(this.tail.slice(0, idx));
150
+ this.tail = this.tail.slice(idx + 1);
151
+ idx = this.tail.indexOf(`
152
+ `);
153
+ }
154
+ return out;
155
+ }
156
+ finish() {
157
+ if (!this.tail)
158
+ return [];
159
+ const line = this.tail;
160
+ this.tail = "";
161
+ return [line];
162
+ }
163
+ }
164
+
165
+ class NdjsonAssembler {
166
+ tail = "";
167
+ texts = [];
168
+ error;
169
+ toolCalls = 0;
170
+ sessionId;
171
+ tools = [];
172
+ pushChunk(chunk) {
173
+ this.tail += chunk;
174
+ let idx = this.tail.indexOf(`
175
+ `);
176
+ while (idx !== -1) {
177
+ const line = this.tail.slice(0, idx);
178
+ this.tail = this.tail.slice(idx + 1);
179
+ this.parseLine(line);
180
+ idx = this.tail.indexOf(`
181
+ `);
182
+ }
183
+ }
184
+ pushLine(line) {
185
+ this.parseLine(line);
186
+ }
187
+ finishIncomplete() {
188
+ return this.tail.trim();
189
+ }
190
+ parseLine(line) {
191
+ const trimmed = line.trim();
192
+ if (!trimmed || trimmed[0] !== "{")
193
+ return;
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(trimmed);
197
+ } catch {
198
+ return;
199
+ }
200
+ if (parsed.sessionID)
201
+ this.sessionId = parsed.sessionID;
202
+ if (parsed.type === "text" && typeof parsed.part?.text === "string") {
203
+ this.texts.push(parsed.part.text);
204
+ return;
205
+ }
206
+ if (parsed.type === "tool_use") {
207
+ this.toolCalls += 1;
208
+ const name = parsed.part?.tool ?? parsed.tool;
209
+ if (typeof name === "string" && name) {
210
+ this.tools.push(name);
211
+ }
212
+ return;
213
+ }
214
+ if (parsed.type === "error") {
215
+ if (this.error === undefined) {
216
+ this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
217
+ }
218
+ }
219
+ }
220
+ finish() {
221
+ if (this.tail.trim()) {
222
+ this.parseLine(this.tail);
223
+ this.tail = "";
224
+ }
225
+ const text = this.texts.join("").trim();
226
+ return {
227
+ text,
228
+ error: this.error,
229
+ toolCalls: this.toolCalls,
230
+ sessionId: this.sessionId,
231
+ tools: this.tools
232
+ };
233
+ }
234
+ }
235
+
236
+ // ../../../packages/cli/src/daemon/runners/codex-stream.ts
237
+ var CODEX_ITEM_TYPES = Object.freeze({
238
+ AGENT_MESSAGE: "agent_message",
239
+ COMMAND_EXECUTION: "command_execution",
240
+ ERROR: "error"
241
+ });
242
+
243
+ class CodexStreamDecoder {
244
+ lines = new LineSplitter;
245
+ state = { tools: [], errors: [], turnCompleted: false };
246
+ pushChunk(chunk) {
247
+ for (const line of this.lines.pushChunk(chunk))
248
+ this.consume(line);
249
+ }
250
+ finish() {
251
+ for (const line of this.lines.finish())
252
+ this.consume(line);
253
+ return this.toResult();
254
+ }
255
+ snapshot() {
256
+ return { ...this.state, tools: [...this.state.tools], errors: [...this.state.errors] };
257
+ }
258
+ consume(rawLine) {
259
+ const line = rawLine.trim();
260
+ if (!line.startsWith("{"))
261
+ return;
262
+ let event;
263
+ try {
264
+ event = JSON.parse(line);
265
+ } catch {
266
+ return;
267
+ }
268
+ if (typeof event !== "object" || event === null)
269
+ return;
270
+ const ev = event;
271
+ if (ev.type === "thread.started") {
272
+ const id = ev.thread_id;
273
+ if (typeof id === "string" && id)
274
+ this.state.sessionId = id;
275
+ return;
276
+ }
277
+ if (ev.type === "turn.completed") {
278
+ this.state.turnCompleted = true;
279
+ return;
280
+ }
281
+ if (ev.type === "item.started" || ev.type === "item.completed") {
282
+ this.consumeItem(ev.item);
283
+ }
284
+ }
285
+ consumeItem(raw) {
286
+ if (typeof raw !== "object" || raw === null)
287
+ return;
288
+ const item = raw;
289
+ if (item.type === CODEX_ITEM_TYPES.AGENT_MESSAGE) {
290
+ if (typeof item.text === "string" && item.text.length > 0) {
291
+ this.state.text = item.text;
292
+ }
293
+ return;
294
+ }
295
+ if (item.type === CODEX_ITEM_TYPES.ERROR) {
296
+ if (typeof item.message === "string" && item.message.length > 0) {
297
+ this.state.errors.push(item.message);
298
+ }
299
+ return;
300
+ }
301
+ if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
302
+ const id = item.id;
303
+ if (typeof id === "string" && id && this.state.tools.includes(id))
304
+ return;
305
+ this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
306
+ }
307
+ }
308
+ toResult() {
309
+ const result = {
310
+ text: this.state.text ?? ""
311
+ };
312
+ if (this.state.sessionId)
313
+ result.sessionId = this.state.sessionId;
314
+ if (this.state.tools.length > 0)
315
+ result.toolCalls = this.state.tools.length;
316
+ return result;
317
+ }
318
+ }
319
+
320
+ // ../../../packages/cli/src/daemon/runners/codex-sessions.ts
321
+ function createCodexSessionRegistry() {
322
+ const sessions = new Map;
323
+ return {
324
+ get: (key) => sessions.get(key),
325
+ set: (key, id) => {
326
+ sessions.set(key, id);
327
+ },
328
+ delete: (key) => {
329
+ sessions.delete(key);
330
+ },
331
+ size: () => sessions.size
332
+ };
333
+ }
334
+
335
+ // ../../../packages/cli/src/daemon/runners/codex.ts
336
+ var DEFAULT_CODEX_TIMEOUT_MS = 10 * 60 * 1000;
337
+ var CODEX_STDIO = Object.freeze([
338
+ "ignore",
339
+ "pipe",
340
+ "pipe"
341
+ ]);
342
+ var CODEX_SANDBOX_MODES = Object.freeze([
343
+ "read-only",
344
+ "workspace-write",
345
+ "danger-full-access"
346
+ ]);
347
+ function isCodexSandboxMode(value) {
348
+ return CODEX_SANDBOX_MODES.includes(value);
349
+ }
350
+ function buildCodexArgs(input) {
351
+ const args = ["exec", "--json"];
352
+ if (input.model)
353
+ args.push("--model", input.model);
354
+ args.push("--skip-git-repo-check");
355
+ args.push("-s", input.sandbox ?? "read-only");
356
+ args.push(input.prompt);
357
+ return args;
358
+ }
359
+ function buildCodexResumeArgs(sessionId, input) {
360
+ const args = ["exec", "resume", "--json"];
361
+ if (input.model)
362
+ args.push("--model", input.model);
363
+ args.push("--skip-git-repo-check");
364
+ if (input.sandbox === "danger-full-access") {
365
+ args.push("--dangerously-bypass-approvals-and-sandbox");
366
+ }
367
+ args.push(sessionId);
368
+ args.push(input.prompt);
369
+ return args;
370
+ }
371
+ function makeCodexRunner(opts = {}) {
372
+ const bin = opts.bin ?? "codex";
373
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_CODEX_TIMEOUT_MS;
374
+ const model = opts.model;
375
+ const sandbox = opts.sandbox ?? "read-only";
376
+ const sessions = opts.sessions ?? createCodexSessionRegistry();
377
+ return async function runAgent(input) {
378
+ const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
379
+ const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox }) : buildCodexArgs({ prompt: input.prompt, model, sandbox });
380
+ const decoder = new CodexStreamDecoder;
381
+ let stderr = "";
382
+ let handle;
383
+ try {
384
+ handle = spawnStreaming([bin, ...args], {
385
+ stdin: CODEX_STDIO[0],
386
+ env: { ...process.env, NO_COLOR: "1" },
387
+ timeoutMs,
388
+ onStdout: (chunk) => decoder.pushChunk(chunk),
389
+ onStderr: (chunk) => {
390
+ if (stderr.length < 64 * 1024)
391
+ stderr += chunk;
392
+ }
393
+ });
394
+ } catch (err) {
395
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
396
+ }
397
+ let code;
398
+ try {
399
+ code = await handle.exited;
400
+ } catch (err) {
401
+ if (isMissingBinary(err)) {
402
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
403
+ }
404
+ return { text: "", error: errMsg(err) };
405
+ }
406
+ const result = decoder.finish();
407
+ if (input.sessionId && result.sessionId && !result.error) {
408
+ sessions.set(input.sessionId, result.sessionId);
409
+ }
410
+ if (code === 124) {
411
+ return { ...result, error: `codex timed out after ${timeoutMs}ms` };
412
+ }
413
+ if (result.error)
414
+ return result;
415
+ if (code !== 0 && !result.text) {
416
+ return {
417
+ text: "",
418
+ toolCalls: result.toolCalls,
419
+ tools: result.tools,
420
+ error: `codex exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
421
+ };
422
+ }
423
+ return result;
424
+ };
425
+ }
426
+ function errMsg(err) {
427
+ if (err instanceof Error)
428
+ return err.message;
429
+ return String(err);
430
+ }
431
+
16
432
  // ../../../packages/cli/src/args.ts
17
433
  var DEFAULT_PROFILE = "default";
18
434
  var DEFAULT_AGENT = "roy";
@@ -121,6 +537,26 @@ function parseServe(argv) {
121
537
  serve.opencodeBin = v;
122
538
  break;
123
539
  }
540
+ case "--codex-model": {
541
+ const v = take();
542
+ if (v instanceof Error)
543
+ return { command: "unknown", error: v.message };
544
+ serve.codexModel = v;
545
+ break;
546
+ }
547
+ case "--codex-sandbox": {
548
+ const v = take();
549
+ if (v instanceof Error)
550
+ return { command: "unknown", error: v.message };
551
+ if (!isCodexSandboxMode(v)) {
552
+ return {
553
+ command: "unknown",
554
+ error: `--codex-sandbox must be one of ${CODEX_SANDBOX_MODES.join("|")}, got "${v}"`
555
+ };
556
+ }
557
+ serve.codexSandbox = v;
558
+ break;
559
+ }
124
560
  case "--max-concurrency": {
125
561
  const v = take();
126
562
  if (v instanceof Error)
@@ -198,7 +634,7 @@ function resolveRuntimeBin(options) {
198
634
  return;
199
635
  }
200
636
  function usage() {
201
- return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode or claude)
637
+ return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode, claude or codex)
202
638
 
203
639
  USAGE
204
640
  roy-task [serve] [options] start the daemon (default command)
@@ -220,6 +656,9 @@ SERVE OPTIONS
220
656
  --runtime-bin <path> binary for the selected runtime (default: per-runtime)
221
657
  --opencode-bin <path> DEPRECATED alias for --runtime-bin
222
658
  (only applies when --runtime opencode)
659
+ --codex-model <name> model for codex exec --model (default: codex config)
660
+ --codex-sandbox <mode> codex exec -s policy (default: read-only)
661
+ one of: ${CODEX_SANDBOX_MODES.join(" | ")}
223
662
  --max-concurrency <n> max simultaneous agent runs (default: 1)
224
663
  ⚠️ >1 only safe across DIFFERENT chats;
225
664
  same-session concurrency cross-contaminates.
@@ -871,129 +1310,6 @@ function createEventListener(config) {
871
1310
  // ../../../packages/cli/src/daemon/runners/claude.ts
872
1311
  import { createHash as createHash2 } from "node:crypto";
873
1312
 
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
- });
928
- }
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
935
- });
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));
967
- });
968
- });
969
- return {
970
- exited,
971
- pid: child.pid,
972
- kill(signal = "SIGTERM") {
973
- try {
974
- child.kill(signal);
975
- } catch {}
976
- }
977
- };
978
- }
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
- }
996
-
997
1313
  // ../../../packages/cli/src/daemon/runners/claude-agents.ts
998
1314
  var CLAUDE_ROY_AGENT_NAME = "roy";
999
1315
  var ROY_AGENT_DESCRIPTION = "Roy — task-as-first-class-citizen agent. Uses the roy-agent task lifecycle " + "(task_tree → task_create → task_update → task_operation_create → task_complete) " + "so every piece of work is tracked in the local task store.";
@@ -1142,115 +1458,7 @@ function createClaudeSessionRegistry() {
1142
1458
  };
1143
1459
  }
1144
1460
 
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
1461
  // ../../../packages/cli/src/daemon/runners/claude-stream.ts
1242
- function errorDetail(result) {
1243
- if (typeof result.result === "string" && result.result.trim() !== "") {
1244
- return result.result.trim();
1245
- }
1246
- if (Array.isArray(result.errors)) {
1247
- const parts = result.errors.map((e) => typeof e === "string" ? e.trim() : "").filter((e) => e !== "");
1248
- if (parts.length > 0)
1249
- return parts.join("; ");
1250
- }
1251
- return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
1252
- }
1253
-
1254
1462
  class ClaudeStreamDecoder {
1255
1463
  lines = new LineSplitter;
1256
1464
  state = { assistantText: [], tools: [] };
@@ -1312,7 +1520,7 @@ class ClaudeStreamDecoder {
1312
1520
  const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
1313
1521
  const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
1314
1522
  if (result?.is_error) {
1315
- const detail = errorDetail(result);
1523
+ const detail = typeof result.result === "string" && result.result.trim() !== "" ? result.result.trim() : result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
1316
1524
  return { text: "", sessionId: this.state.sessionId, toolCalls, tools, error: detail.slice(0, 1000) };
1317
1525
  }
1318
1526
  if (typeof result?.result === "string" && result.result.trim() !== "") {
@@ -1387,45 +1595,42 @@ function makeClaudeRunner(opts = {}) {
1387
1595
  }
1388
1596
  });
1389
1597
  } catch (err) {
1390
- return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
1598
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
1391
1599
  }
1392
1600
  let code;
1393
1601
  try {
1394
1602
  code = await handle.exited;
1395
1603
  } catch (err) {
1396
1604
  if (isMissingBinary(err)) {
1397
- return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
1605
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
1398
1606
  }
1399
- return { text: "", error: errMsg(err) };
1607
+ return { text: "", error: errMsg2(err) };
1400
1608
  }
1401
1609
  const result = decoder.finish();
1402
- const stderrText = stderr.trim();
1403
- const combined = result.error ? stderrText && !result.error.includes(stderrText) ? `${result.error} | ${stderrText}` : result.error : undefined;
1404
- const effective = combined ? { ...result, error: combined.slice(0, 1000) } : result;
1405
- if (input.sessionId && effective.sessionId) {
1406
- sessions.set(input.sessionId, effective.sessionId);
1610
+ if (input.sessionId && result.sessionId && !result.error) {
1611
+ sessions.set(input.sessionId, result.sessionId);
1407
1612
  }
1408
1613
  if (code === 124) {
1409
- return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
1614
+ return { ...result, error: `claude timed out after ${timeoutMs}ms` };
1410
1615
  }
1411
- if (effective.error) {
1412
- if (input.sessionId && /No conversation found|not a UUID|session title/i.test(effective.error)) {
1616
+ if (result.error) {
1617
+ if (input.sessionId && /No conversation found|not a UUID|session title/i.test(result.error)) {
1413
1618
  sessions.delete(input.sessionId);
1414
1619
  }
1415
- return effective;
1620
+ return result;
1416
1621
  }
1417
- if (code !== 0 && !effective.text) {
1622
+ if (code !== 0 && !result.text) {
1418
1623
  return {
1419
1624
  text: "",
1420
- toolCalls: effective.toolCalls,
1421
- tools: effective.tools,
1625
+ toolCalls: result.toolCalls,
1626
+ tools: result.tools,
1422
1627
  error: `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
1423
1628
  };
1424
1629
  }
1425
- return effective;
1630
+ return result;
1426
1631
  };
1427
1632
  }
1428
- function errMsg(err) {
1633
+ function errMsg2(err) {
1429
1634
  if (err instanceof Error)
1430
1635
  return err.message;
1431
1636
  return String(err);
@@ -1457,16 +1662,16 @@ function makeOpencodeRunner(opts = {}) {
1457
1662
  }
1458
1663
  });
1459
1664
  } catch (err) {
1460
- return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
1665
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
1461
1666
  }
1462
1667
  let code;
1463
1668
  try {
1464
1669
  code = await handle.exited;
1465
1670
  } catch (err) {
1466
1671
  if (isMissingBinary(err)) {
1467
- return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
1672
+ return { text: "", error: `failed to spawn ${bin}: ${errMsg3(err)}` };
1468
1673
  }
1469
- return { text: "", error: errMsg2(err) };
1674
+ return { text: "", error: errMsg3(err) };
1470
1675
  }
1471
1676
  const result = assembler.finish();
1472
1677
  if (code === 124) {
@@ -1503,7 +1708,7 @@ function makeOpencodeRunner(opts = {}) {
1503
1708
  };
1504
1709
  };
1505
1710
  }
1506
- function errMsg2(err) {
1711
+ function errMsg3(err) {
1507
1712
  if (err instanceof Error)
1508
1713
  return err.message;
1509
1714
  return String(err);
@@ -1516,6 +1721,12 @@ var FACTORIES = {
1516
1721
  bin: input.bin,
1517
1722
  timeoutMs: input.timeoutMs,
1518
1723
  agentsJson: input.claudeAgentsJson
1724
+ }),
1725
+ codex: (input) => makeCodexRunner({
1726
+ bin: input.bin,
1727
+ timeoutMs: input.timeoutMs,
1728
+ model: input.codexModel,
1729
+ sandbox: isCodexSandboxMode(input.codexSandbox ?? "") ? input.codexSandbox : undefined
1519
1730
  })
1520
1731
  };
1521
1732
  function makeRunner(input = {}) {
@@ -1624,7 +1835,9 @@ async function startServe(options, deps) {
1624
1835
  runAgent: deps.runAgent ?? makeRunner({
1625
1836
  runtime: options.runtime,
1626
1837
  bin: resolveRuntimeBin(options),
1627
- timeoutMs: options.timeoutMs
1838
+ timeoutMs: options.timeoutMs,
1839
+ codexModel: options.codexModel,
1840
+ codexSandbox: options.codexSandbox
1628
1841
  }),
1629
1842
  reply: deps.reply ?? defaultReplyRunner,
1630
1843
  gate,
@@ -1653,7 +1866,7 @@ async function startServe(options, deps) {
1653
1866
  try {
1654
1867
  await listener.stop();
1655
1868
  } catch (err) {
1656
- log(`[roy-task] listener.stop() failed: ${errMsg3(err)}`);
1869
+ log(`[roy-task] listener.stop() failed: ${errMsg4(err)}`);
1657
1870
  }
1658
1871
  log("[roy-task] stopped");
1659
1872
  resolveDone();
@@ -1675,7 +1888,7 @@ async function startServe(options, deps) {
1675
1888
  await stop();
1676
1889
  }
1677
1890
  } catch (err) {
1678
- log(`[roy-task] ${label} dispatch threw: ${errMsg3(err)}`);
1891
+ log(`[roy-task] ${label} dispatch threw: ${errMsg4(err)}`);
1679
1892
  if (options.once)
1680
1893
  await stop();
1681
1894
  }
@@ -1685,7 +1898,7 @@ async function startServe(options, deps) {
1685
1898
  try {
1686
1899
  await listener.start();
1687
1900
  } catch (err) {
1688
- log(`[roy-task] failed to start event listener: ${errMsg3(err)}`);
1901
+ log(`[roy-task] failed to start event listener: ${errMsg4(err)}`);
1689
1902
  clearInterval(sweeper);
1690
1903
  resolveDone();
1691
1904
  throw err;
@@ -1704,6 +1917,10 @@ function formatBanner(options, commands, maxConcurrency) {
1704
1917
  ` agent : ${options.agent}`,
1705
1918
  ` runtime : ${options.runtime}`,
1706
1919
  ` runtime bin : ${resolveRuntimeBin(options) ?? "(default)"}`,
1920
+ ...options.runtime === "codex" ? [
1921
+ ` codex model : ${options.codexModel ?? "(codex config)"}`,
1922
+ ` codex sandbox : ${options.codexSandbox ?? "read-only"}`
1923
+ ] : [],
1707
1924
  ` max concurrency : ${maxConcurrency}`,
1708
1925
  ` reply enabled : ${options.replyEnabled}`,
1709
1926
  ` progress cards : ${options.cardEnabled && !options.dryRun}`,
@@ -1745,7 +1962,7 @@ function formatResult(label, result) {
1745
1962
  return `[roy-task] ${label} skipped: ${result.reason}`;
1746
1963
  }
1747
1964
  }
1748
- function errMsg3(err) {
1965
+ function errMsg4(err) {
1749
1966
  return err instanceof Error ? err.message : String(err);
1750
1967
  }
1751
1968
 
@@ -1870,7 +2087,7 @@ function formatStatus(report) {
1870
2087
  }
1871
2088
 
1872
2089
  // ../../../packages/cli/src/main.ts
1873
- var VERSION = "0.4.1";
2090
+ var VERSION = "0.5.0";
1874
2091
  async function main(argv) {
1875
2092
  const parsed = parseArgs(argv);
1876
2093
  switch (parsed.command) {