@ai-setting/roy-task-plugin 0.5.0 → 0.6.1

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,199 @@ 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) ?? fromSuccessLine(text, titleHint) ?? fromEmbeddedTaskObject(text) ?? fromTaskObjectBlob(text);
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 task of taskCandidates(text, jsonTree(text))) {
203
+ if (found === undefined)
204
+ found = task;
205
+ else if (found.id !== task.id)
206
+ return;
207
+ }
208
+ return found;
209
+ }
210
+ function looksLikeArrayStart(text, at) {
211
+ for (let i = at + 1;i < text.length; i++) {
212
+ const ch = text[i];
213
+ if (ch === " " || ch === "\t" || ch === `
214
+ ` || ch === "\r")
215
+ continue;
216
+ return ch === "{" || ch === "[" || ch === '"' || ch === "]";
217
+ }
218
+ return false;
219
+ }
220
+ function taskCandidates(text, roots) {
221
+ const out = [];
222
+ const pending = [...roots].reverse();
223
+ while (pending.length > 0) {
224
+ const node = pending.pop();
225
+ if (node.end < 0) {
226
+ if (node.ch === "[" && looksLikeArrayStart(text, node.start))
227
+ continue;
228
+ pushChildren(pending, node);
229
+ continue;
230
+ }
231
+ if (node.ch === "[")
232
+ continue;
233
+ const parsed = parseObject(text, node);
234
+ const task = parsed ? taskFromObject(parsed) : undefined;
235
+ if (task)
236
+ out.push(task);
237
+ }
238
+ return out;
239
+ }
240
+ function pushChildren(pending, node) {
241
+ for (let i = node.children.length - 1;i >= 0; i--)
242
+ pending.push(node.children[i]);
243
+ }
244
+ function parseObject(text, node) {
245
+ try {
246
+ const parsed = JSON.parse(text.slice(node.start, node.end + 1));
247
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
248
+ return parsed;
249
+ }
250
+ } catch {}
251
+ return;
252
+ }
253
+ function taskFromObject(obj) {
254
+ const id = obj["id"];
255
+ const title = obj["title"];
256
+ if (typeof id !== "number" || !Number.isInteger(id) || id <= 0)
257
+ return;
258
+ if (typeof title !== "string" || title.trim() === "")
259
+ return;
260
+ return { id, title: title.trim() };
261
+ }
262
+ function jsonTree(text) {
263
+ const roots = [];
264
+ const stack = [];
265
+ let inString = false;
266
+ let escaped = false;
267
+ const open = (ch, at) => {
268
+ const node = { ch, start: at, end: -1, children: [] };
269
+ const parent = stack[stack.length - 1];
270
+ (parent ? parent.children : roots).push(node);
271
+ stack.push(node);
272
+ };
273
+ const close = (ch, at) => {
274
+ const want = ch === "}" ? "{" : "[";
275
+ let k = stack.length - 1;
276
+ while (k >= 0 && stack[k].ch !== want)
277
+ k--;
278
+ if (k < 0)
279
+ return;
280
+ stack.length = k + 1;
281
+ const node = stack.pop();
282
+ node.end = at;
283
+ };
284
+ for (let i = 0;i < text.length; i++) {
285
+ const ch = text[i];
286
+ if (inString) {
287
+ if (ch === `
288
+ `) {
289
+ inString = false;
290
+ escaped = false;
291
+ } else if (escaped)
292
+ escaped = false;
293
+ else if (ch === "\\")
294
+ escaped = true;
295
+ else if (ch === '"')
296
+ inString = false;
297
+ continue;
298
+ }
299
+ if (ch === '"')
300
+ inString = true;
301
+ else if (ch === "{" || ch === "[")
302
+ open(ch, i);
303
+ else if (ch === "}" || ch === "]")
304
+ close(ch, i);
305
+ }
306
+ return roots;
307
+ }
308
+ function fromSuccessLine(text, titleHint) {
309
+ const m = CREATED_LINE.exec(text);
310
+ if (!m)
311
+ return;
312
+ const id = Number(m[1]);
313
+ if (!Number.isInteger(id) || id <= 0)
314
+ return;
315
+ const title = titleFromCliSummary(text) || titleHint?.trim();
316
+ return title ? { id, title } : { id };
317
+ }
318
+ function titleFromCliSummary(text) {
319
+ const at = CREATED_LINE.exec(text);
320
+ if (!at)
321
+ return;
322
+ const rest = text.slice(at.index + at[0].length);
323
+ const m = /^\s*Title:\s*(.+)$/m.exec(rest);
324
+ const title = m?.[1]?.trim();
325
+ if (!title || title.length > 200)
326
+ return;
327
+ return title;
328
+ }
329
+ function isTaskCreateTool(name) {
330
+ return /(^|__)task_create$/.test(name);
331
+ }
332
+
140
333
  // ../../../packages/cli/src/daemon/ndjson.ts
141
334
  class LineSplitter {
142
335
  tail = "";
@@ -169,6 +362,7 @@ class NdjsonAssembler {
169
362
  toolCalls = 0;
170
363
  sessionId;
171
364
  tools = [];
365
+ createdTask;
172
366
  pushChunk(chunk) {
173
367
  this.tail += chunk;
174
368
  let idx = this.tail.indexOf(`
@@ -209,6 +403,14 @@ class NdjsonAssembler {
209
403
  if (typeof name === "string" && name) {
210
404
  this.tools.push(name);
211
405
  }
406
+ if (this.createdTask === undefined && typeof name === "string") {
407
+ const part = parsed.part;
408
+ const output = part?.state?.output;
409
+ const title = part?.state?.input?.["title"];
410
+ if (isTaskCreateTool(name) && typeof output === "string") {
411
+ this.createdTask = extractCreatedTask(output, typeof title === "string" ? title : undefined);
412
+ }
413
+ }
212
414
  return;
213
415
  }
214
416
  if (parsed.type === "error") {
@@ -228,7 +430,8 @@ class NdjsonAssembler {
228
430
  error: this.error,
229
431
  toolCalls: this.toolCalls,
230
432
  sessionId: this.sessionId,
231
- tools: this.tools
433
+ tools: this.tools,
434
+ createdTask: this.createdTask
232
435
  };
233
436
  }
234
437
  }
@@ -279,10 +482,10 @@ class CodexStreamDecoder {
279
482
  return;
280
483
  }
281
484
  if (ev.type === "item.started" || ev.type === "item.completed") {
282
- this.consumeItem(ev.item);
485
+ this.consumeItem(ev.item, ev.type === "item.completed");
283
486
  }
284
487
  }
285
- consumeItem(raw) {
488
+ consumeItem(raw, completed) {
286
489
  if (typeof raw !== "object" || raw === null)
287
490
  return;
288
491
  const item = raw;
@@ -300,9 +503,12 @@ class CodexStreamDecoder {
300
503
  }
301
504
  if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
302
505
  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}`);
506
+ if (typeof id === "string" && id && this.state.tools.includes(id)) {} else {
507
+ this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
508
+ }
509
+ if (completed && this.state.createdTask === undefined) {
510
+ this.state.createdTask = extractCreatedTask(item.aggregated_output);
511
+ }
306
512
  }
307
513
  }
308
514
  toResult() {
@@ -313,6 +519,8 @@ class CodexStreamDecoder {
313
519
  result.sessionId = this.state.sessionId;
314
520
  if (this.state.tools.length > 0)
315
521
  result.toolCalls = this.state.tools.length;
522
+ if (this.state.createdTask)
523
+ result.createdTask = this.state.createdTask;
316
524
  return result;
317
525
  }
318
526
  }
@@ -417,6 +625,7 @@ function makeCodexRunner(opts = {}) {
417
625
  text: "",
418
626
  toolCalls: result.toolCalls,
419
627
  tools: result.tools,
628
+ createdTask: result.createdTask,
420
629
  error: `codex exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
421
630
  };
422
631
  }
@@ -434,13 +643,38 @@ var DEFAULT_PROFILE = "default";
434
643
  var DEFAULT_AGENT = "roy";
435
644
  var DEFAULT_EVENT_KEY = "im.message.receive_v1";
436
645
  var DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
646
+ function findSubcommand(argv) {
647
+ for (let i = 0;i < argv.length; i++) {
648
+ const raw = argv[i];
649
+ if (raw.startsWith("-")) {
650
+ if (!raw.includes("=") && VALUE_FLAGS.has(raw))
651
+ i++;
652
+ continue;
653
+ }
654
+ return raw;
655
+ }
656
+ return;
657
+ }
658
+ var VALUE_FLAGS = new Set([
659
+ "--profile",
660
+ "--agent",
661
+ "--event-key",
662
+ "--runtime",
663
+ "--command",
664
+ "--runtime-bin",
665
+ "--opencode-bin",
666
+ "--codex-model",
667
+ "--codex-sandbox",
668
+ "--max-concurrency",
669
+ "--timeout-ms"
670
+ ]);
437
671
  var SUBCOMMANDS = new Set(["serve", "status", "help", "version"]);
438
672
  function parseArgs(argv) {
439
673
  if (argv.includes("--help") || argv.includes("-h"))
440
674
  return { command: "help" };
441
675
  if (argv.includes("--version") || argv.includes("-v"))
442
676
  return { command: "version" };
443
- const head = argv.find((a) => !a.startsWith("-"));
677
+ const head = findSubcommand(argv);
444
678
  const command = head ?? "serve";
445
679
  if (!SUBCOMMANDS.has(command)) {
446
680
  return { command: "unknown", error: `unknown command "${command}"` };
@@ -523,6 +757,13 @@ function parseServe(argv) {
523
757
  serve.runtime = v;
524
758
  break;
525
759
  }
760
+ case "--command": {
761
+ const v = take();
762
+ if (v instanceof Error)
763
+ return { command: "unknown", error: v.message };
764
+ serve.command = v;
765
+ break;
766
+ }
526
767
  case "--runtime-bin": {
527
768
  const v = take();
528
769
  if (v instanceof Error)
@@ -626,6 +867,8 @@ function readValue(argv, flag) {
626
867
  return;
627
868
  }
628
869
  function resolveRuntimeBin(options) {
870
+ if (options.command !== undefined)
871
+ return options.command;
629
872
  if (options.runtimeBin !== undefined)
630
873
  return options.runtimeBin;
631
874
  if (options.runtime === "opencode" && options.opencodeBin !== "opencode") {
@@ -653,8 +896,11 @@ SERVE OPTIONS
653
896
  --agent <name> agent to dispatch to (default: ${DEFAULT_AGENT})
654
897
  --runtime <name> which harness runs the agent (default: ${DEFAULT_RUNTIME})
655
898
  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
899
+ --command <name|path> executable to spawn, whatever the dialect
900
+ is (default: the runtime's own binary).
901
+ e.g. --runtime claude --command claude-minimax
902
+ --runtime-bin <path> DEPRECATED alias for --command
903
+ --opencode-bin <path> DEPRECATED alias for --command
658
904
  (only applies when --runtime opencode)
659
905
  --codex-model <name> model for codex exec --model (default: codex config)
660
906
  --codex-sandbox <mode> codex exec -s policy (default: read-only)
@@ -673,6 +919,12 @@ SERVE OPTIONS
673
919
  never block the text reply.
674
920
 
675
921
  NOTES
922
+ • --runtime vs --command: --runtime picks the *dialect* (which flags and
923
+ which stream decoder), --command picks the *executable* that speaks it.
924
+ So \`--runtime claude --command claude-minimax\` keeps every claude flag
925
+ (--print / --output-format stream-json / --agents / --resume <uuid>) and
926
+ merely spawns the wrapper instead of the bare binary. Use this whenever
927
+ the default binary is not the one that actually works on your machine.
676
928
  • stdin is forced to EOF for the agent child (stdio[0]="ignore"). Without it
677
929
  both \`opencode run\` and \`claude --print\` wait for EOF on a socket and the
678
930
  run hangs forever.
@@ -826,6 +1078,24 @@ function buildDispatchPrompt(input) {
826
1078
  "3. 需要落任务就用 `task_*` 工具(task_create / task_update / …),",
827
1079
  " 这是你的主要职责;不要只在回答里“描述”你打算做什么。",
828
1080
  "",
1081
+ "## 什么时候该建任务(Task #4433)",
1082
+ "",
1083
+ "任务由**你**决定建不建,守护进程不会替你建。判断标准:",
1084
+ "",
1085
+ "- **该建**:需要多步操作、要动文件/跑命令、要调研并产出结论、",
1086
+ " 耗时预计超过一两分钟、或用户明确要求「跟踪 / 建个任务 / 后续跟进」。",
1087
+ " → 用 `task_create` 建任务,**然后**再开始干活(先立任务,再动手)。",
1088
+ "- **不必建**:寒暄、谢谢、纯问答(「今天几号」「这个参数叫啥」)、",
1089
+ " 一两句话就能答完且不需要任何工具的问题。",
1090
+ " → 直接回答,不要为了「显得勤快」而建一个空任务。",
1091
+ "- **建完要更新**:动手过程中用 `task_update` 推进度 / 改 current_status,",
1092
+ " 干完用 `task_complete` 收尾。建了任务却从不更新,比不建更糟——",
1093
+ " 看板上会留下一个卡住的任务。",
1094
+ "",
1095
+ "⚠️ **卡片与任务一一对应**:只有你建了任务,用户才会收到一张进展卡片;",
1096
+ "没建任务就只有文字回复。所以**不要**为了「想要一张卡片」而去建任务,",
1097
+ "也不要建了任务又不用它——卡片上显示的就是你建的那个任务的真 id 和标题。",
1098
+ "",
829
1099
  "## 环境事实(必须使用下面这些值,不要自己猜)",
830
1100
  "",
831
1101
  `- lark-cli profile:\`${profile}\`(飞书 openId 是 per-app 隔离的,换 profile 会失败)`,
@@ -904,6 +1174,7 @@ function createRoyAgentCardPort(options = {}) {
904
1174
  return inFlight;
905
1175
  };
906
1176
  return {
1177
+ enabled: true,
907
1178
  async start(input) {
908
1179
  const n = await withTimeout("load", getNotifier(), opTimeoutMs);
909
1180
  return withTimeout("start", n.startTask({
@@ -942,6 +1213,7 @@ function createRoyAgentCardPort(options = {}) {
942
1213
  }
943
1214
  function createNoopCardPort() {
944
1215
  return {
1216
+ enabled: false,
945
1217
  async start() {
946
1218
  return { messageId: "", pinned: false };
947
1219
  },
@@ -949,21 +1221,14 @@ function createNoopCardPort() {
949
1221
  async finish() {}
950
1222
  };
951
1223
  }
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;
1224
+ function buildCardMeta(input) {
962
1225
  return {
963
- id: `om:${input.messageId.slice(-12)}`,
964
- title,
965
- description: rest || first,
966
- goals: "完成用户请求并把结果回复到触发消息所在会话"
1226
+ id: input.task.id,
1227
+ title: input.task.title ?? `Task #${input.task.id}`,
1228
+ description: input.description ?? `由 roy-task 派发,触发消息 ${input.messageId}`,
1229
+ type: "normal",
1230
+ priority: "high",
1231
+ currentStatus: "由 agent 处理中"
967
1232
  };
968
1233
  }
969
1234
  var TOOL_LABELS = {
@@ -997,8 +1262,15 @@ function cardReport(handle) {
997
1262
  out.cardMessageId = handle.messageId;
998
1263
  if (handle.error)
999
1264
  out.cardError = handle.error;
1265
+ if (handle.cardWantedButNoTask && !handle.messageId && !handle.error)
1266
+ out.cardExpected = true;
1267
+ if (handle.meta && typeof handle.meta.id === "number")
1268
+ out.taskId = handle.meta.id;
1000
1269
  return out;
1001
1270
  }
1271
+ function createdTaskReport(createdTask) {
1272
+ return createdTask ? { taskId: createdTask.id } : {};
1273
+ }
1002
1274
  function errorMessage(err) {
1003
1275
  return err instanceof Error ? err.message : String(err);
1004
1276
  }
@@ -1093,13 +1365,18 @@ class Dispatcher {
1093
1365
  return { status: "dry-run", plan };
1094
1366
  }
1095
1367
  const run = async () => {
1096
- const cardPromise = this.openCard({ chatId, messageId, profile, raw });
1097
1368
  const result = await this.deps.runAgent({
1098
1369
  prompt: plan.prompt,
1099
1370
  agent: plan.agent,
1100
1371
  sessionId: plan.sessionId
1101
1372
  });
1102
- const card = await cardPromise;
1373
+ const card = await this.openCard({
1374
+ chatId,
1375
+ messageId,
1376
+ profile,
1377
+ createdTask: result.createdTask,
1378
+ requestText: extractUserText(raw)
1379
+ });
1103
1380
  if (card.messageId) {
1104
1381
  await this.patchCard(card, {
1105
1382
  chatId,
@@ -1115,7 +1392,13 @@ class Dispatcher {
1115
1392
  status: "failed",
1116
1393
  finalStep: `agent 报错:${result.error}`
1117
1394
  });
1118
- return { status: "agent-error", plan, error: result.error, ...cardReport(card) };
1395
+ return {
1396
+ status: "agent-error",
1397
+ plan,
1398
+ error: result.error,
1399
+ ...createdTaskReport(result.createdTask),
1400
+ ...cardReport(card)
1401
+ };
1119
1402
  }
1120
1403
  if (!result.text.trim()) {
1121
1404
  await this.closeCard(card, {
@@ -1128,6 +1411,7 @@ class Dispatcher {
1128
1411
  status: "no-output",
1129
1412
  plan,
1130
1413
  toolCalls: result.toolCalls ?? 0,
1414
+ ...createdTaskReport(result.createdTask),
1131
1415
  ...cardReport(card)
1132
1416
  };
1133
1417
  }
@@ -1138,7 +1422,13 @@ class Dispatcher {
1138
1422
  status: "completed",
1139
1423
  finalStep: "--no-reply:agent 已完成,未发送回复"
1140
1424
  });
1141
- return { status: "no-reply", plan, text: result.text, ...cardReport(card) };
1425
+ return {
1426
+ status: "no-reply",
1427
+ plan,
1428
+ text: result.text,
1429
+ ...createdTaskReport(result.createdTask),
1430
+ ...cardReport(card)
1431
+ };
1142
1432
  }
1143
1433
  const sent = await this.deps.reply({
1144
1434
  messageId,
@@ -1159,6 +1449,7 @@ class Dispatcher {
1159
1449
  plan,
1160
1450
  error: sent.error ?? "unknown",
1161
1451
  text: result.text,
1452
+ ...createdTaskReport(result.createdTask),
1162
1453
  ...cardReport(card)
1163
1454
  };
1164
1455
  }
@@ -1173,6 +1464,7 @@ class Dispatcher {
1173
1464
  plan,
1174
1465
  replyMessageId: sent.messageId,
1175
1466
  text: result.text,
1467
+ ...createdTaskReport(result.createdTask),
1176
1468
  ...cardReport(card)
1177
1469
  };
1178
1470
  };
@@ -1182,23 +1474,20 @@ class Dispatcher {
1182
1474
  const handle = { messageId: "" };
1183
1475
  if (!this.deps.card)
1184
1476
  return handle;
1477
+ if (!input.createdTask) {
1478
+ handle.cardWantedButNoTask = this.deps.card.enabled === true;
1479
+ return handle;
1480
+ }
1481
+ const meta = buildCardMeta({
1482
+ task: input.createdTask,
1483
+ messageId: input.messageId,
1484
+ ...input.requestText ? { description: input.requestText } : {}
1485
+ });
1185
1486
  try {
1186
- const identity = buildCardIdentity({
1187
- messageId: input.messageId,
1188
- text: extractUserText(input.raw)
1189
- });
1190
1487
  const started = await this.deps.card.start({
1191
1488
  chatId: input.chatId,
1192
1489
  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
- },
1490
+ meta,
1202
1491
  milestones: [
1203
1492
  {
1204
1493
  title: "收到消息",
@@ -1209,6 +1498,7 @@ class Dispatcher {
1209
1498
  });
1210
1499
  handle.messageId = started.messageId;
1211
1500
  handle.pinned = started.pinned;
1501
+ handle.meta = meta;
1212
1502
  } catch (err) {
1213
1503
  handle.error = errorMessage(err);
1214
1504
  }
@@ -1226,6 +1516,7 @@ class Dispatcher {
1226
1516
  chatId: input.chatId,
1227
1517
  profile: input.profile,
1228
1518
  status: input.status,
1519
+ ...handle.meta ? { meta: handle.meta } : {},
1229
1520
  processSteps: tools.map(describeToolUse)
1230
1521
  });
1231
1522
  } catch (err) {
@@ -1242,7 +1533,8 @@ class Dispatcher {
1242
1533
  profile: input.profile,
1243
1534
  status: input.status,
1244
1535
  finalStep: input.finalStep,
1245
- progress: input.status === "completed" ? 100 : undefined
1536
+ progress: input.status === "completed" ? 100 : undefined,
1537
+ ...handle.meta ? { meta: handle.meta } : {}
1246
1538
  });
1247
1539
  } catch (err) {
1248
1540
  handle.error ??= errorMessage(err);
@@ -1309,6 +1601,7 @@ function createEventListener(config) {
1309
1601
 
1310
1602
  // ../../../packages/cli/src/daemon/runners/claude.ts
1311
1603
  import { createHash as createHash2 } from "node:crypto";
1604
+ import { appendFileSync } from "node:fs";
1312
1605
 
1313
1606
  // ../../../packages/cli/src/daemon/runners/claude-agents.ts
1314
1607
  var CLAUDE_ROY_AGENT_NAME = "roy";
@@ -1459,9 +1752,45 @@ function createClaudeSessionRegistry() {
1459
1752
  }
1460
1753
 
1461
1754
  // ../../../packages/cli/src/daemon/runners/claude-stream.ts
1755
+ function errorDetail(result) {
1756
+ if (typeof result.result === "string" && result.result.trim() !== "") {
1757
+ return result.result.trim();
1758
+ }
1759
+ if (Array.isArray(result.errors)) {
1760
+ const parts = result.errors.map((e) => typeof e === "string" ? e.trim() : "").filter((e) => e !== "");
1761
+ if (parts.length > 0)
1762
+ return parts.join("; ");
1763
+ }
1764
+ return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
1765
+ }
1766
+ function flattenToolResult(content) {
1767
+ if (typeof content === "string")
1768
+ return content;
1769
+ if (!Array.isArray(content))
1770
+ return "";
1771
+ const parts = [];
1772
+ for (const block of content) {
1773
+ if (typeof block === "string") {
1774
+ parts.push(block);
1775
+ continue;
1776
+ }
1777
+ if (typeof block === "object" && block !== null) {
1778
+ const t = block.text;
1779
+ if (typeof t === "string")
1780
+ parts.push(t);
1781
+ }
1782
+ }
1783
+ return parts.join(`
1784
+ `);
1785
+ }
1786
+
1462
1787
  class ClaudeStreamDecoder {
1463
1788
  lines = new LineSplitter;
1464
- state = { assistantText: [], tools: [] };
1789
+ state = {
1790
+ assistantText: [],
1791
+ tools: [],
1792
+ pendingCreates: new Map
1793
+ };
1465
1794
  pushChunk(chunk) {
1466
1795
  for (const line of this.lines.pushChunk(chunk))
1467
1796
  this.consume(line);
@@ -1494,10 +1823,38 @@ class ClaudeStreamDecoder {
1494
1823
  this.consumeAssistant(ev);
1495
1824
  return;
1496
1825
  }
1826
+ if (ev.type === "user") {
1827
+ this.consumeUser(ev);
1828
+ return;
1829
+ }
1497
1830
  if (ev.type === "result") {
1498
1831
  this.state.result = ev;
1499
1832
  }
1500
1833
  }
1834
+ consumeUser(ev) {
1835
+ const message = ev.message;
1836
+ const content = message?.content;
1837
+ if (!Array.isArray(content))
1838
+ return;
1839
+ for (const block of content) {
1840
+ if (typeof block !== "object" || block === null)
1841
+ continue;
1842
+ const b = block;
1843
+ if (b.type !== "tool_result")
1844
+ continue;
1845
+ const id = typeof b.tool_use_id === "string" ? b.tool_use_id : undefined;
1846
+ const pending = id ? this.state.pendingCreates.get(id) : undefined;
1847
+ const text = flattenToolResult(b.content);
1848
+ if (this.state.createdTask !== undefined)
1849
+ continue;
1850
+ if (pending) {
1851
+ this.state.pendingCreates.delete(id);
1852
+ this.state.createdTask = extractCreatedTask(text, pending.titleHint);
1853
+ continue;
1854
+ }
1855
+ this.state.createdTask = extractCreatedTask(text);
1856
+ }
1857
+ }
1501
1858
  consumeAssistant(ev) {
1502
1859
  const message = ev.message;
1503
1860
  const content = message?.content;
@@ -1510,8 +1867,17 @@ class ClaudeStreamDecoder {
1510
1867
  if (b.type === "text" && typeof b.text === "string") {
1511
1868
  this.state.assistantText.push(b.text);
1512
1869
  } else if (b.type === "tool_use") {
1513
- if (typeof b.name === "string")
1870
+ if (typeof b.name === "string") {
1514
1871
  this.state.tools.push(b.name);
1872
+ if (isTaskCreateTool(b.name) && typeof b.id === "string") {
1873
+ const input = b.input;
1874
+ const title = input?.["title"];
1875
+ this.state.pendingCreates.set(b.id, {
1876
+ tool: b.name,
1877
+ titleHint: typeof title === "string" ? title : undefined
1878
+ });
1879
+ }
1880
+ }
1515
1881
  }
1516
1882
  }
1517
1883
  }
@@ -1519,15 +1885,23 @@ class ClaudeStreamDecoder {
1519
1885
  const { result } = this.state;
1520
1886
  const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
1521
1887
  const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
1888
+ const createdTask = this.state.createdTask;
1522
1889
  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) };
1890
+ const detail = errorDetail(result);
1891
+ return {
1892
+ text: "",
1893
+ sessionId: this.state.sessionId,
1894
+ toolCalls,
1895
+ tools,
1896
+ createdTask,
1897
+ error: detail.slice(0, 1000)
1898
+ };
1525
1899
  }
1526
1900
  if (typeof result?.result === "string" && result.result.trim() !== "") {
1527
- return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools };
1901
+ return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools, createdTask };
1528
1902
  }
1529
1903
  const fallback = this.state.assistantText.join("").trim();
1530
- return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools };
1904
+ return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools, createdTask };
1531
1905
  }
1532
1906
  }
1533
1907
 
@@ -1588,7 +1962,14 @@ function makeClaudeRunner(opts = {}) {
1588
1962
  stdin: CLAUDE_STDIO[0],
1589
1963
  env: { ...process.env, NO_COLOR: "1" },
1590
1964
  timeoutMs,
1591
- onStdout: (chunk) => decoder.pushChunk(chunk),
1965
+ onStdout: (chunk) => {
1966
+ decoder.pushChunk(chunk);
1967
+ if (process.env["ROY_TASK_DUMP_STREAM"]) {
1968
+ try {
1969
+ appendFileSync(process.env["ROY_TASK_DUMP_STREAM"], chunk);
1970
+ } catch {}
1971
+ }
1972
+ },
1592
1973
  onStderr: (chunk) => {
1593
1974
  if (stderr.length < 64 * 1024)
1594
1975
  stderr += chunk;
@@ -1607,29 +1988,37 @@ function makeClaudeRunner(opts = {}) {
1607
1988
  return { text: "", error: errMsg2(err) };
1608
1989
  }
1609
1990
  const result = decoder.finish();
1610
- if (input.sessionId && result.sessionId && !result.error) {
1611
- sessions.set(input.sessionId, result.sessionId);
1991
+ const stderrText = stderr.trim();
1992
+ const combined = result.error ? stderrText && !result.error.includes(stderrText) ? `${result.error} | ${stderrText}` : result.error : undefined;
1993
+ const effective = combined ? { ...result, error: combined.slice(0, 1000) } : result;
1994
+ if (input.sessionId && effective.sessionId) {
1995
+ sessions.set(input.sessionId, effective.sessionId);
1612
1996
  }
1613
1997
  if (code === 124) {
1614
- return { ...result, error: `claude timed out after ${timeoutMs}ms` };
1998
+ return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
1615
1999
  }
1616
- if (result.error) {
1617
- if (input.sessionId && /No conversation found|not a UUID|session title/i.test(result.error)) {
2000
+ if (effective.error) {
2001
+ if (input.sessionId && STALE_SESSION_RE.test(effective.error)) {
1618
2002
  sessions.delete(input.sessionId);
1619
2003
  }
1620
- return result;
2004
+ return effective;
1621
2005
  }
1622
- if (code !== 0 && !result.text) {
2006
+ if (code !== 0 && !effective.text) {
2007
+ const error = `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`;
2008
+ if (input.sessionId && STALE_SESSION_RE.test(error)) {
2009
+ sessions.delete(input.sessionId);
2010
+ }
1623
2011
  return {
1624
2012
  text: "",
1625
- toolCalls: result.toolCalls,
1626
- tools: result.tools,
1627
- error: `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
2013
+ toolCalls: effective.toolCalls,
2014
+ tools: effective.tools,
2015
+ error
1628
2016
  };
1629
2017
  }
1630
- return result;
2018
+ return effective;
1631
2019
  };
1632
2020
  }
2021
+ var STALE_SESSION_RE = /No conversation found|not a UUID|session title/i;
1633
2022
  function errMsg2(err) {
1634
2023
  if (err instanceof Error)
1635
2024
  return err.message;
@@ -1680,7 +2069,8 @@ function makeOpencodeRunner(opts = {}) {
1680
2069
  error: `opencode timed out after ${timeoutMs}ms`,
1681
2070
  sessionId: result.sessionId,
1682
2071
  toolCalls: result.toolCalls,
1683
- tools: result.tools
2072
+ tools: result.tools,
2073
+ createdTask: result.createdTask
1684
2074
  };
1685
2075
  }
1686
2076
  if (result.error) {
@@ -1689,7 +2079,8 @@ function makeOpencodeRunner(opts = {}) {
1689
2079
  error: result.error,
1690
2080
  sessionId: result.sessionId,
1691
2081
  toolCalls: result.toolCalls,
1692
- tools: result.tools
2082
+ tools: result.tools,
2083
+ createdTask: result.createdTask
1693
2084
  };
1694
2085
  }
1695
2086
  if (code !== 0 && !result.text) {
@@ -1697,14 +2088,16 @@ function makeOpencodeRunner(opts = {}) {
1697
2088
  text: "",
1698
2089
  error: `opencode exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`,
1699
2090
  toolCalls: result.toolCalls,
1700
- tools: result.tools
2091
+ tools: result.tools,
2092
+ createdTask: result.createdTask
1701
2093
  };
1702
2094
  }
1703
2095
  return {
1704
2096
  text: result.text,
1705
2097
  sessionId: result.sessionId,
1706
2098
  toolCalls: result.toolCalls,
1707
- tools: result.tools
2099
+ tools: result.tools,
2100
+ createdTask: result.createdTask
1708
2101
  };
1709
2102
  };
1710
2103
  }
@@ -1916,7 +2309,7 @@ function formatBanner(options, commands, maxConcurrency) {
1916
2309
  ` profile : ${options.profile}`,
1917
2310
  ` agent : ${options.agent}`,
1918
2311
  ` runtime : ${options.runtime}`,
1919
- ` runtime bin : ${resolveRuntimeBin(options) ?? "(default)"}`,
2312
+ " command : " + (resolveRuntimeBin(options) ?? "(default: " + options.runtime + ")"),
1920
2313
  ...options.runtime === "codex" ? [
1921
2314
  ` codex model : ${options.codexModel ?? "(codex config)"}`,
1922
2315
  ` codex sandbox : ${options.codexSandbox ?? "read-only"}`
@@ -1935,29 +2328,32 @@ function formatBanner(options, commands, maxConcurrency) {
1935
2328
  `);
1936
2329
  }
1937
2330
  function cardSuffix(result) {
1938
- if (!("cardMessageId" in result) && !("cardError" in result))
1939
- return "";
1940
- if (result.cardError)
2331
+ if ("cardError" in result && result.cardError)
1941
2332
  return ` card=FAILED(${result.cardError})`;
1942
- return ` card=${result.cardMessageId ?? "(none)"}`;
2333
+ if ("cardMessageId" in result && result.cardMessageId)
2334
+ return ` card=${result.cardMessageId}`;
2335
+ return result.cardExpected ? " card=none(no task created)" : "";
2336
+ }
2337
+ function taskSuffix(result) {
2338
+ return result.taskId === undefined ? "" : ` task=${result.taskId}`;
1943
2339
  }
1944
2340
  function formatResult(label, result) {
1945
2341
  const card = cardSuffix(result);
1946
2342
  switch (result.status) {
1947
2343
  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}`;
2344
+ 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
2345
  case "dry-run":
1950
2346
  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
2347
  case "duplicate":
1952
2348
  return `[roy-task] ${label} duplicate dropped key=${result.key ?? "(unknown)"}`;
1953
2349
  case "no-output":
1954
- return `[roy-task] ${label} no-output (agent produced no text; not replying) ` + `toolCalls=${result.toolCalls}${card}`;
2350
+ return `[roy-task] ${label} no-output (agent produced no text; not replying) ` + `toolCalls=${result.toolCalls}${taskSuffix(result)}${card}`;
1955
2351
  case "no-reply":
1956
- return `[roy-task] ${label} no-reply (reply disabled) chars=${result.text.length}${card}`;
2352
+ return `[roy-task] ${label} no-reply (reply disabled) chars=${result.text.length}${taskSuffix(result)}${card}`;
1957
2353
  case "agent-error":
1958
- return `[roy-task] ${label} agent-error: ${result.error}${card}`;
2354
+ return `[roy-task] ${label} agent-error: ${result.error}${taskSuffix(result)}${card}`;
1959
2355
  case "reply-failed":
1960
- return `[roy-task] ${label} reply-failed: ${result.error}${card}`;
2356
+ return `[roy-task] ${label} reply-failed: ${result.error}${taskSuffix(result)}${card}`;
1961
2357
  case "no-target":
1962
2358
  return `[roy-task] ${label} skipped: ${result.reason}`;
1963
2359
  }
@@ -2087,7 +2483,7 @@ function formatStatus(report) {
2087
2483
  }
2088
2484
 
2089
2485
  // ../../../packages/cli/src/main.ts
2090
- var VERSION = "0.5.0";
2486
+ var VERSION = "0.6.1";
2091
2487
  async function main(argv) {
2092
2488
  const parsed = parseArgs(argv);
2093
2489
  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.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {