@ai-setting/roy-task-plugin 0.5.0 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roy-task",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Roy — task-as-first-class-citizen harness for Claude Code. Ships the roy agent, slash commands for the task lifecycle, and an MCP server exposing the task tools natively.",
5
5
  "author": {
6
6
  "name": "dongzhaokun",
package/dist/cli.js CHANGED
@@ -137,6 +137,187 @@ function isMissingBinary(err) {
137
137
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
138
138
  }
139
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
+
140
321
  // ../../../packages/cli/src/daemon/ndjson.ts
141
322
  class LineSplitter {
142
323
  tail = "";
@@ -169,6 +350,7 @@ class NdjsonAssembler {
169
350
  toolCalls = 0;
170
351
  sessionId;
171
352
  tools = [];
353
+ createdTask;
172
354
  pushChunk(chunk) {
173
355
  this.tail += chunk;
174
356
  let idx = this.tail.indexOf(`
@@ -209,6 +391,14 @@ class NdjsonAssembler {
209
391
  if (typeof name === "string" && name) {
210
392
  this.tools.push(name);
211
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
+ }
212
402
  return;
213
403
  }
214
404
  if (parsed.type === "error") {
@@ -228,7 +418,8 @@ class NdjsonAssembler {
228
418
  error: this.error,
229
419
  toolCalls: this.toolCalls,
230
420
  sessionId: this.sessionId,
231
- tools: this.tools
421
+ tools: this.tools,
422
+ createdTask: this.createdTask
232
423
  };
233
424
  }
234
425
  }
@@ -279,10 +470,10 @@ class CodexStreamDecoder {
279
470
  return;
280
471
  }
281
472
  if (ev.type === "item.started" || ev.type === "item.completed") {
282
- this.consumeItem(ev.item);
473
+ this.consumeItem(ev.item, ev.type === "item.completed");
283
474
  }
284
475
  }
285
- consumeItem(raw) {
476
+ consumeItem(raw, completed) {
286
477
  if (typeof raw !== "object" || raw === null)
287
478
  return;
288
479
  const item = raw;
@@ -300,9 +491,12 @@ class CodexStreamDecoder {
300
491
  }
301
492
  if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
302
493
  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}`);
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
+ }
306
500
  }
307
501
  }
308
502
  toResult() {
@@ -313,6 +507,8 @@ class CodexStreamDecoder {
313
507
  result.sessionId = this.state.sessionId;
314
508
  if (this.state.tools.length > 0)
315
509
  result.toolCalls = this.state.tools.length;
510
+ if (this.state.createdTask)
511
+ result.createdTask = this.state.createdTask;
316
512
  return result;
317
513
  }
318
514
  }
@@ -417,6 +613,7 @@ function makeCodexRunner(opts = {}) {
417
613
  text: "",
418
614
  toolCalls: result.toolCalls,
419
615
  tools: result.tools,
616
+ createdTask: result.createdTask,
420
617
  error: `codex exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
421
618
  };
422
619
  }
@@ -434,13 +631,38 @@ var DEFAULT_PROFILE = "default";
434
631
  var DEFAULT_AGENT = "roy";
435
632
  var DEFAULT_EVENT_KEY = "im.message.receive_v1";
436
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
+ ]);
437
659
  var SUBCOMMANDS = new Set(["serve", "status", "help", "version"]);
438
660
  function parseArgs(argv) {
439
661
  if (argv.includes("--help") || argv.includes("-h"))
440
662
  return { command: "help" };
441
663
  if (argv.includes("--version") || argv.includes("-v"))
442
664
  return { command: "version" };
443
- const head = argv.find((a) => !a.startsWith("-"));
665
+ const head = findSubcommand(argv);
444
666
  const command = head ?? "serve";
445
667
  if (!SUBCOMMANDS.has(command)) {
446
668
  return { command: "unknown", error: `unknown command "${command}"` };
@@ -523,6 +745,13 @@ function parseServe(argv) {
523
745
  serve.runtime = v;
524
746
  break;
525
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
+ }
526
755
  case "--runtime-bin": {
527
756
  const v = take();
528
757
  if (v instanceof Error)
@@ -626,6 +855,8 @@ function readValue(argv, flag) {
626
855
  return;
627
856
  }
628
857
  function resolveRuntimeBin(options) {
858
+ if (options.command !== undefined)
859
+ return options.command;
629
860
  if (options.runtimeBin !== undefined)
630
861
  return options.runtimeBin;
631
862
  if (options.runtime === "opencode" && options.opencodeBin !== "opencode") {
@@ -653,8 +884,11 @@ SERVE OPTIONS
653
884
  --agent <name> agent to dispatch to (default: ${DEFAULT_AGENT})
654
885
  --runtime <name> which harness runs the agent (default: ${DEFAULT_RUNTIME})
655
886
  one of: ${RUNTIMES.join(" | ")}
656
- --runtime-bin <path> binary for the selected runtime (default: per-runtime)
657
- --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
658
892
  (only applies when --runtime opencode)
659
893
  --codex-model <name> model for codex exec --model (default: codex config)
660
894
  --codex-sandbox <mode> codex exec -s policy (default: read-only)
@@ -673,6 +907,12 @@ SERVE OPTIONS
673
907
  never block the text reply.
674
908
 
675
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.
676
916
  • stdin is forced to EOF for the agent child (stdio[0]="ignore"). Without it
677
917
  both \`opencode run\` and \`claude --print\` wait for EOF on a socket and the
678
918
  run hangs forever.
@@ -826,6 +1066,24 @@ function buildDispatchPrompt(input) {
826
1066
  "3. 需要落任务就用 `task_*` 工具(task_create / task_update / …),",
827
1067
  " 这是你的主要职责;不要只在回答里“描述”你打算做什么。",
828
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
+ "",
829
1087
  "## 环境事实(必须使用下面这些值,不要自己猜)",
830
1088
  "",
831
1089
  `- lark-cli profile:\`${profile}\`(飞书 openId 是 per-app 隔离的,换 profile 会失败)`,
@@ -949,21 +1207,14 @@ function createNoopCardPort() {
949
1207
  async finish() {}
950
1208
  };
951
1209
  }
952
- function buildCardIdentity(input) {
953
- const maxTitle = input.maxTitleChars ?? 60;
954
- const normalized = input.text.replace(/\r\n/g, `
955
- `).trim();
956
- const lines = normalized.split(`
957
- `).map((l) => l.trim()).filter(Boolean);
958
- const first = lines[0] ?? "(非文本消息)";
959
- const rest = lines.slice(1).join(`
960
- `);
961
- const title = first.length > maxTitle ? `${first.slice(0, maxTitle - 1)}…` : first;
1210
+ function buildCardMeta(input) {
962
1211
  return {
963
- id: `om:${input.messageId.slice(-12)}`,
964
- title,
965
- description: rest || first,
966
- 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 处理中"
967
1218
  };
968
1219
  }
969
1220
  var TOOL_LABELS = {
@@ -997,8 +1248,13 @@ function cardReport(handle) {
997
1248
  out.cardMessageId = handle.messageId;
998
1249
  if (handle.error)
999
1250
  out.cardError = handle.error;
1251
+ if (handle.meta && typeof handle.meta.id === "number")
1252
+ out.taskId = handle.meta.id;
1000
1253
  return out;
1001
1254
  }
1255
+ function createdTaskReport(createdTask) {
1256
+ return createdTask ? { taskId: createdTask.id } : {};
1257
+ }
1002
1258
  function errorMessage(err) {
1003
1259
  return err instanceof Error ? err.message : String(err);
1004
1260
  }
@@ -1093,13 +1349,18 @@ class Dispatcher {
1093
1349
  return { status: "dry-run", plan };
1094
1350
  }
1095
1351
  const run = async () => {
1096
- const cardPromise = this.openCard({ chatId, messageId, profile, raw });
1097
1352
  const result = await this.deps.runAgent({
1098
1353
  prompt: plan.prompt,
1099
1354
  agent: plan.agent,
1100
1355
  sessionId: plan.sessionId
1101
1356
  });
1102
- 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
+ });
1103
1364
  if (card.messageId) {
1104
1365
  await this.patchCard(card, {
1105
1366
  chatId,
@@ -1115,7 +1376,13 @@ class Dispatcher {
1115
1376
  status: "failed",
1116
1377
  finalStep: `agent 报错:${result.error}`
1117
1378
  });
1118
- 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
+ };
1119
1386
  }
1120
1387
  if (!result.text.trim()) {
1121
1388
  await this.closeCard(card, {
@@ -1128,6 +1395,7 @@ class Dispatcher {
1128
1395
  status: "no-output",
1129
1396
  plan,
1130
1397
  toolCalls: result.toolCalls ?? 0,
1398
+ ...createdTaskReport(result.createdTask),
1131
1399
  ...cardReport(card)
1132
1400
  };
1133
1401
  }
@@ -1138,7 +1406,13 @@ class Dispatcher {
1138
1406
  status: "completed",
1139
1407
  finalStep: "--no-reply:agent 已完成,未发送回复"
1140
1408
  });
1141
- 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
+ };
1142
1416
  }
1143
1417
  const sent = await this.deps.reply({
1144
1418
  messageId,
@@ -1182,23 +1456,18 @@ class Dispatcher {
1182
1456
  const handle = { messageId: "" };
1183
1457
  if (!this.deps.card)
1184
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
+ });
1185
1466
  try {
1186
- const identity = buildCardIdentity({
1187
- messageId: input.messageId,
1188
- text: extractUserText(input.raw)
1189
- });
1190
1467
  const started = await this.deps.card.start({
1191
1468
  chatId: input.chatId,
1192
1469
  profile: input.profile,
1193
- meta: {
1194
- id: identity.id,
1195
- title: identity.title,
1196
- description: identity.description,
1197
- goals: identity.goals,
1198
- type: "normal",
1199
- priority: "high",
1200
- currentStatus: "已收到消息,正在处理"
1201
- },
1470
+ meta,
1202
1471
  milestones: [
1203
1472
  {
1204
1473
  title: "收到消息",
@@ -1209,6 +1478,7 @@ class Dispatcher {
1209
1478
  });
1210
1479
  handle.messageId = started.messageId;
1211
1480
  handle.pinned = started.pinned;
1481
+ handle.meta = meta;
1212
1482
  } catch (err) {
1213
1483
  handle.error = errorMessage(err);
1214
1484
  }
@@ -1226,6 +1496,7 @@ class Dispatcher {
1226
1496
  chatId: input.chatId,
1227
1497
  profile: input.profile,
1228
1498
  status: input.status,
1499
+ ...handle.meta ? { meta: handle.meta } : {},
1229
1500
  processSteps: tools.map(describeToolUse)
1230
1501
  });
1231
1502
  } catch (err) {
@@ -1242,7 +1513,8 @@ class Dispatcher {
1242
1513
  profile: input.profile,
1243
1514
  status: input.status,
1244
1515
  finalStep: input.finalStep,
1245
- progress: input.status === "completed" ? 100 : undefined
1516
+ progress: input.status === "completed" ? 100 : undefined,
1517
+ ...handle.meta ? { meta: handle.meta } : {}
1246
1518
  });
1247
1519
  } catch (err) {
1248
1520
  handle.error ??= errorMessage(err);
@@ -1309,6 +1581,7 @@ function createEventListener(config) {
1309
1581
 
1310
1582
  // ../../../packages/cli/src/daemon/runners/claude.ts
1311
1583
  import { createHash as createHash2 } from "node:crypto";
1584
+ import { appendFileSync } from "node:fs";
1312
1585
 
1313
1586
  // ../../../packages/cli/src/daemon/runners/claude-agents.ts
1314
1587
  var CLAUDE_ROY_AGENT_NAME = "roy";
@@ -1459,9 +1732,45 @@ function createClaudeSessionRegistry() {
1459
1732
  }
1460
1733
 
1461
1734
  // ../../../packages/cli/src/daemon/runners/claude-stream.ts
1735
+ function errorDetail(result) {
1736
+ if (typeof result.result === "string" && result.result.trim() !== "") {
1737
+ return result.result.trim();
1738
+ }
1739
+ if (Array.isArray(result.errors)) {
1740
+ const parts = result.errors.map((e) => typeof e === "string" ? e.trim() : "").filter((e) => e !== "");
1741
+ if (parts.length > 0)
1742
+ return parts.join("; ");
1743
+ }
1744
+ return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
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
+ }
1766
+
1462
1767
  class ClaudeStreamDecoder {
1463
1768
  lines = new LineSplitter;
1464
- state = { assistantText: [], tools: [] };
1769
+ state = {
1770
+ assistantText: [],
1771
+ tools: [],
1772
+ pendingCreates: new Map
1773
+ };
1465
1774
  pushChunk(chunk) {
1466
1775
  for (const line of this.lines.pushChunk(chunk))
1467
1776
  this.consume(line);
@@ -1494,10 +1803,38 @@ class ClaudeStreamDecoder {
1494
1803
  this.consumeAssistant(ev);
1495
1804
  return;
1496
1805
  }
1806
+ if (ev.type === "user") {
1807
+ this.consumeUser(ev);
1808
+ return;
1809
+ }
1497
1810
  if (ev.type === "result") {
1498
1811
  this.state.result = ev;
1499
1812
  }
1500
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
+ }
1501
1838
  consumeAssistant(ev) {
1502
1839
  const message = ev.message;
1503
1840
  const content = message?.content;
@@ -1510,8 +1847,17 @@ class ClaudeStreamDecoder {
1510
1847
  if (b.type === "text" && typeof b.text === "string") {
1511
1848
  this.state.assistantText.push(b.text);
1512
1849
  } else if (b.type === "tool_use") {
1513
- if (typeof b.name === "string")
1850
+ if (typeof b.name === "string") {
1514
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
+ }
1515
1861
  }
1516
1862
  }
1517
1863
  }
@@ -1519,15 +1865,23 @@ class ClaudeStreamDecoder {
1519
1865
  const { result } = this.state;
1520
1866
  const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
1521
1867
  const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
1868
+ const createdTask = this.state.createdTask;
1522
1869
  if (result?.is_error) {
1523
- const detail = typeof result.result === "string" && result.result.trim() !== "" ? result.result.trim() : result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
1524
- return { text: "", sessionId: this.state.sessionId, toolCalls, tools, error: detail.slice(0, 1000) };
1870
+ const detail = errorDetail(result);
1871
+ return {
1872
+ text: "",
1873
+ sessionId: this.state.sessionId,
1874
+ toolCalls,
1875
+ tools,
1876
+ createdTask,
1877
+ error: detail.slice(0, 1000)
1878
+ };
1525
1879
  }
1526
1880
  if (typeof result?.result === "string" && result.result.trim() !== "") {
1527
- return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools };
1881
+ return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools, createdTask };
1528
1882
  }
1529
1883
  const fallback = this.state.assistantText.join("").trim();
1530
- return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools };
1884
+ return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools, createdTask };
1531
1885
  }
1532
1886
  }
1533
1887
 
@@ -1588,7 +1942,14 @@ function makeClaudeRunner(opts = {}) {
1588
1942
  stdin: CLAUDE_STDIO[0],
1589
1943
  env: { ...process.env, NO_COLOR: "1" },
1590
1944
  timeoutMs,
1591
- 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
+ },
1592
1953
  onStderr: (chunk) => {
1593
1954
  if (stderr.length < 64 * 1024)
1594
1955
  stderr += chunk;
@@ -1607,29 +1968,37 @@ function makeClaudeRunner(opts = {}) {
1607
1968
  return { text: "", error: errMsg2(err) };
1608
1969
  }
1609
1970
  const result = decoder.finish();
1610
- if (input.sessionId && result.sessionId && !result.error) {
1611
- sessions.set(input.sessionId, result.sessionId);
1971
+ const stderrText = stderr.trim();
1972
+ const combined = result.error ? stderrText && !result.error.includes(stderrText) ? `${result.error} | ${stderrText}` : result.error : undefined;
1973
+ const effective = combined ? { ...result, error: combined.slice(0, 1000) } : result;
1974
+ if (input.sessionId && effective.sessionId) {
1975
+ sessions.set(input.sessionId, effective.sessionId);
1612
1976
  }
1613
1977
  if (code === 124) {
1614
- return { ...result, error: `claude timed out after ${timeoutMs}ms` };
1978
+ return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
1615
1979
  }
1616
- if (result.error) {
1617
- if (input.sessionId && /No conversation found|not a UUID|session title/i.test(result.error)) {
1980
+ if (effective.error) {
1981
+ if (input.sessionId && STALE_SESSION_RE.test(effective.error)) {
1618
1982
  sessions.delete(input.sessionId);
1619
1983
  }
1620
- return result;
1984
+ return effective;
1621
1985
  }
1622
- if (code !== 0 && !result.text) {
1986
+ if (code !== 0 && !effective.text) {
1987
+ const error = `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`;
1988
+ if (input.sessionId && STALE_SESSION_RE.test(error)) {
1989
+ sessions.delete(input.sessionId);
1990
+ }
1623
1991
  return {
1624
1992
  text: "",
1625
- toolCalls: result.toolCalls,
1626
- tools: result.tools,
1627
- error: `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
1993
+ toolCalls: effective.toolCalls,
1994
+ tools: effective.tools,
1995
+ error
1628
1996
  };
1629
1997
  }
1630
- return result;
1998
+ return effective;
1631
1999
  };
1632
2000
  }
2001
+ var STALE_SESSION_RE = /No conversation found|not a UUID|session title/i;
1633
2002
  function errMsg2(err) {
1634
2003
  if (err instanceof Error)
1635
2004
  return err.message;
@@ -1680,7 +2049,8 @@ function makeOpencodeRunner(opts = {}) {
1680
2049
  error: `opencode timed out after ${timeoutMs}ms`,
1681
2050
  sessionId: result.sessionId,
1682
2051
  toolCalls: result.toolCalls,
1683
- tools: result.tools
2052
+ tools: result.tools,
2053
+ createdTask: result.createdTask
1684
2054
  };
1685
2055
  }
1686
2056
  if (result.error) {
@@ -1689,7 +2059,8 @@ function makeOpencodeRunner(opts = {}) {
1689
2059
  error: result.error,
1690
2060
  sessionId: result.sessionId,
1691
2061
  toolCalls: result.toolCalls,
1692
- tools: result.tools
2062
+ tools: result.tools,
2063
+ createdTask: result.createdTask
1693
2064
  };
1694
2065
  }
1695
2066
  if (code !== 0 && !result.text) {
@@ -1697,14 +2068,16 @@ function makeOpencodeRunner(opts = {}) {
1697
2068
  text: "",
1698
2069
  error: `opencode exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`,
1699
2070
  toolCalls: result.toolCalls,
1700
- tools: result.tools
2071
+ tools: result.tools,
2072
+ createdTask: result.createdTask
1701
2073
  };
1702
2074
  }
1703
2075
  return {
1704
2076
  text: result.text,
1705
2077
  sessionId: result.sessionId,
1706
2078
  toolCalls: result.toolCalls,
1707
- tools: result.tools
2079
+ tools: result.tools,
2080
+ createdTask: result.createdTask
1708
2081
  };
1709
2082
  };
1710
2083
  }
@@ -1916,7 +2289,7 @@ function formatBanner(options, commands, maxConcurrency) {
1916
2289
  ` profile : ${options.profile}`,
1917
2290
  ` agent : ${options.agent}`,
1918
2291
  ` runtime : ${options.runtime}`,
1919
- ` runtime bin : ${resolveRuntimeBin(options) ?? "(default)"}`,
2292
+ " command : " + (resolveRuntimeBin(options) ?? "(default: " + options.runtime + ")"),
1920
2293
  ...options.runtime === "codex" ? [
1921
2294
  ` codex model : ${options.codexModel ?? "(codex config)"}`,
1922
2295
  ` codex sandbox : ${options.codexSandbox ?? "read-only"}`
@@ -1939,13 +2312,15 @@ function cardSuffix(result) {
1939
2312
  return "";
1940
2313
  if (result.cardError)
1941
2314
  return ` card=FAILED(${result.cardError})`;
1942
- return ` card=${result.cardMessageId ?? "(none)"}`;
2315
+ if (result.cardMessageId)
2316
+ return ` card=${result.cardMessageId}`;
2317
+ return " card=none(no task created)";
1943
2318
  }
1944
2319
  function formatResult(label, result) {
1945
2320
  const card = cardSuffix(result);
1946
2321
  switch (result.status) {
1947
2322
  case "dispatched":
1948
- 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}`;
1949
2324
  case "dry-run":
1950
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`;
1951
2326
  case "duplicate":
@@ -2087,7 +2462,7 @@ function formatStatus(report) {
2087
2462
  }
2088
2463
 
2089
2464
  // ../../../packages/cli/src/main.ts
2090
- var VERSION = "0.5.0";
2465
+ var VERSION = "0.6.0";
2091
2466
  async function main(argv) {
2092
2467
  const parsed = parseArgs(argv);
2093
2468
  switch (parsed.command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-setting/roy-task-plugin",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {