@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.
- package/dist/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/dist/cli.js +442 -67
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roy-task",
|
|
3
|
-
"version": "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
|
-
|
|
305
|
-
|
|
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
|
|
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
|
-
--
|
|
657
|
-
|
|
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
|
|
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:
|
|
964
|
-
title
|
|
965
|
-
description:
|
|
966
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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 = {
|
|
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 =
|
|
1524
|
-
return {
|
|
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) =>
|
|
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
|
-
|
|
1611
|
-
|
|
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 { ...
|
|
1978
|
+
return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
|
|
1615
1979
|
}
|
|
1616
|
-
if (
|
|
1617
|
-
if (input.sessionId &&
|
|
1980
|
+
if (effective.error) {
|
|
1981
|
+
if (input.sessionId && STALE_SESSION_RE.test(effective.error)) {
|
|
1618
1982
|
sessions.delete(input.sessionId);
|
|
1619
1983
|
}
|
|
1620
|
-
return
|
|
1984
|
+
return effective;
|
|
1621
1985
|
}
|
|
1622
|
-
if (code !== 0 && !
|
|
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:
|
|
1626
|
-
tools:
|
|
1627
|
-
error
|
|
1993
|
+
toolCalls: effective.toolCalls,
|
|
1994
|
+
tools: effective.tools,
|
|
1995
|
+
error
|
|
1628
1996
|
};
|
|
1629
1997
|
}
|
|
1630
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2465
|
+
var VERSION = "0.6.0";
|
|
2091
2466
|
async function main(argv) {
|
|
2092
2467
|
const parsed = parseArgs(argv);
|
|
2093
2468
|
switch (parsed.command) {
|