@ai-setting/roy-task-plugin 0.3.1 → 0.4.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.
- package/dist/cli.js +542 -104
- package/dist/index.js +1 -1
- package/dist/index.js.map +3 -3
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// ../../../packages/cli/src/daemon/runners/types.ts
|
|
6
|
+
var RUNTIMES = Object.freeze(["opencode", "claude"]);
|
|
7
|
+
var DEFAULT_RUNTIME = "opencode";
|
|
8
|
+
var DEFAULT_RUNTIME_BIN = Object.freeze({
|
|
9
|
+
opencode: "opencode",
|
|
10
|
+
claude: "claude"
|
|
11
|
+
});
|
|
12
|
+
function isRuntimeName(value) {
|
|
13
|
+
return RUNTIMES.includes(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../../../packages/cli/src/args.ts
|
|
6
17
|
var DEFAULT_PROFILE = "default";
|
|
7
18
|
var DEFAULT_AGENT = "roy";
|
|
8
19
|
var DEFAULT_EVENT_KEY = "im.message.receive_v1";
|
|
@@ -42,6 +53,7 @@ function parseServe(argv) {
|
|
|
42
53
|
replyEnabled: true,
|
|
43
54
|
cardEnabled: true,
|
|
44
55
|
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
56
|
+
runtime: DEFAULT_RUNTIME,
|
|
45
57
|
opencodeBin: "opencode"
|
|
46
58
|
};
|
|
47
59
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -82,6 +94,26 @@ function parseServe(argv) {
|
|
|
82
94
|
serve.eventKeys.push(v);
|
|
83
95
|
break;
|
|
84
96
|
}
|
|
97
|
+
case "--runtime": {
|
|
98
|
+
const v = take();
|
|
99
|
+
if (v instanceof Error)
|
|
100
|
+
return { command: "unknown", error: v.message };
|
|
101
|
+
if (!isRuntimeName(v)) {
|
|
102
|
+
return {
|
|
103
|
+
command: "unknown",
|
|
104
|
+
error: `--runtime must be one of ${RUNTIMES.join("|")}, got "${v}"`
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
serve.runtime = v;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "--runtime-bin": {
|
|
111
|
+
const v = take();
|
|
112
|
+
if (v instanceof Error)
|
|
113
|
+
return { command: "unknown", error: v.message };
|
|
114
|
+
serve.runtimeBin = v;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
85
117
|
case "--opencode-bin": {
|
|
86
118
|
const v = take();
|
|
87
119
|
if (v instanceof Error)
|
|
@@ -157,8 +189,16 @@ function readValue(argv, flag) {
|
|
|
157
189
|
}
|
|
158
190
|
return;
|
|
159
191
|
}
|
|
192
|
+
function resolveRuntimeBin(options) {
|
|
193
|
+
if (options.runtimeBin !== undefined)
|
|
194
|
+
return options.runtimeBin;
|
|
195
|
+
if (options.runtime === "opencode" && options.opencodeBin !== "opencode") {
|
|
196
|
+
return options.opencodeBin;
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
160
200
|
function usage() {
|
|
161
|
-
return `roy-task — bridge Feishu (Lark) messages to
|
|
201
|
+
return `roy-task — bridge Feishu (Lark) messages to a roy agent (opencode or claude)
|
|
162
202
|
|
|
163
203
|
USAGE
|
|
164
204
|
roy-task [serve] [options] start the daemon (default command)
|
|
@@ -174,13 +214,17 @@ SERVE OPTIONS
|
|
|
174
214
|
consumer per (app, EventKey); a second one
|
|
175
215
|
silently splits the event stream.
|
|
176
216
|
--event-key <key> event key to consume (repeatable) (default: ${DEFAULT_EVENT_KEY})
|
|
177
|
-
--agent <name>
|
|
178
|
-
--
|
|
217
|
+
--agent <name> agent to dispatch to (default: ${DEFAULT_AGENT})
|
|
218
|
+
--runtime <name> which harness runs the agent (default: ${DEFAULT_RUNTIME})
|
|
219
|
+
one of: ${RUNTIMES.join(" | ")}
|
|
220
|
+
--runtime-bin <path> binary for the selected runtime (default: per-runtime)
|
|
221
|
+
--opencode-bin <path> DEPRECATED alias for --runtime-bin
|
|
222
|
+
(only applies when --runtime opencode)
|
|
179
223
|
--max-concurrency <n> max simultaneous agent runs (default: 1)
|
|
180
224
|
⚠️ >1 only safe across DIFFERENT chats;
|
|
181
225
|
same-session concurrency cross-contaminates.
|
|
182
226
|
--timeout-ms <n> per-run timeout (default: ${DEFAULT_TIMEOUT_MS})
|
|
183
|
-
--dry-run never spawn
|
|
227
|
+
--dry-run never spawn the runtime, never reply
|
|
184
228
|
--once stop after the first event (testing / CI)
|
|
185
229
|
--reply / --no-reply send the reply back to Feishu (default: on)
|
|
186
230
|
--card / --no-card render a roy-agent progress card (default: on)
|
|
@@ -190,8 +234,9 @@ SERVE OPTIONS
|
|
|
190
234
|
never block the text reply.
|
|
191
235
|
|
|
192
236
|
NOTES
|
|
193
|
-
• stdin is forced to EOF for
|
|
194
|
-
opencode
|
|
237
|
+
• stdin is forced to EOF for the agent child (stdio[0]="ignore"). Without it
|
|
238
|
+
both \`opencode run\` and \`claude --print\` wait for EOF on a socket and the
|
|
239
|
+
run hangs forever.
|
|
195
240
|
• \`lark-cli event consume\` needs the OPPOSITE: its stdin must stay open.
|
|
196
241
|
That side is handled by roy-agent-core, which we import.
|
|
197
242
|
• Replies are sent by the daemon (not by the agent) as a single structured
|
|
@@ -202,7 +247,7 @@ NOTES
|
|
|
202
247
|
`;
|
|
203
248
|
}
|
|
204
249
|
|
|
205
|
-
//
|
|
250
|
+
// ../../../packages/cli/src/daemon/concurrency.ts
|
|
206
251
|
class ConcurrencyGate {
|
|
207
252
|
maxConcurrency;
|
|
208
253
|
running = 0;
|
|
@@ -259,7 +304,7 @@ class ConcurrencyGate {
|
|
|
259
304
|
}
|
|
260
305
|
}
|
|
261
306
|
|
|
262
|
-
//
|
|
307
|
+
// ../../../packages/cli/src/daemon/dedupe.ts
|
|
263
308
|
var DEFAULT_TTL_MS = 60000;
|
|
264
309
|
var DEFAULT_MAX_ENTRIES = 5000;
|
|
265
310
|
|
|
@@ -305,7 +350,7 @@ class Deduper {
|
|
|
305
350
|
}
|
|
306
351
|
}
|
|
307
352
|
|
|
308
|
-
//
|
|
353
|
+
// ../../../packages/cli/src/daemon/session-map.ts
|
|
309
354
|
import { createHash } from "node:crypto";
|
|
310
355
|
var DIGEST_LEN = 16;
|
|
311
356
|
function deriveSessionId(input) {
|
|
@@ -316,7 +361,7 @@ function deriveSessionId(input) {
|
|
|
316
361
|
return `ses_${digest.slice(0, DIGEST_LEN)}`;
|
|
317
362
|
}
|
|
318
363
|
|
|
319
|
-
//
|
|
364
|
+
// ../../../packages/cli/src/daemon/prompt.ts
|
|
320
365
|
var OPENCODE_STDIO = Object.freeze([
|
|
321
366
|
"ignore",
|
|
322
367
|
"pipe",
|
|
@@ -369,7 +414,7 @@ function buildOpencodeArgs(input) {
|
|
|
369
414
|
return args;
|
|
370
415
|
}
|
|
371
416
|
|
|
372
|
-
//
|
|
417
|
+
// ../../../packages/cli/src/daemon/task-card.ts
|
|
373
418
|
var CARD_OP_TIMEOUT_MS = 15000;
|
|
374
419
|
|
|
375
420
|
class CardTimeoutError extends Error {
|
|
@@ -506,7 +551,7 @@ function describeToolUse(tool) {
|
|
|
506
551
|
};
|
|
507
552
|
}
|
|
508
553
|
|
|
509
|
-
//
|
|
554
|
+
// ../../../packages/cli/src/daemon/dispatcher.ts
|
|
510
555
|
function cardReport(handle) {
|
|
511
556
|
const out = {};
|
|
512
557
|
if (handle.messageId)
|
|
@@ -781,7 +826,7 @@ class Dispatcher {
|
|
|
781
826
|
}
|
|
782
827
|
}
|
|
783
828
|
|
|
784
|
-
//
|
|
829
|
+
// ../../../packages/cli/src/daemon/es-listener.ts
|
|
785
830
|
import { larkCliHandler } from "@ai-setting/roy-agent-core/env/event-source";
|
|
786
831
|
function createEventListener(config) {
|
|
787
832
|
const eventKeys = config.eventKeys?.length ? config.eventKeys : ["im.message.receive_v1"];
|
|
@@ -823,79 +868,10 @@ function createEventListener(config) {
|
|
|
823
868
|
};
|
|
824
869
|
}
|
|
825
870
|
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
tail = "";
|
|
829
|
-
texts = [];
|
|
830
|
-
error;
|
|
831
|
-
toolCalls = 0;
|
|
832
|
-
sessionId;
|
|
833
|
-
tools = [];
|
|
834
|
-
pushChunk(chunk) {
|
|
835
|
-
this.tail += chunk;
|
|
836
|
-
let idx = this.tail.indexOf(`
|
|
837
|
-
`);
|
|
838
|
-
while (idx !== -1) {
|
|
839
|
-
const line = this.tail.slice(0, idx);
|
|
840
|
-
this.tail = this.tail.slice(idx + 1);
|
|
841
|
-
this.parseLine(line);
|
|
842
|
-
idx = this.tail.indexOf(`
|
|
843
|
-
`);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
pushLine(line) {
|
|
847
|
-
this.parseLine(line);
|
|
848
|
-
}
|
|
849
|
-
finishIncomplete() {
|
|
850
|
-
return this.tail.trim();
|
|
851
|
-
}
|
|
852
|
-
parseLine(line) {
|
|
853
|
-
const trimmed = line.trim();
|
|
854
|
-
if (!trimmed || trimmed[0] !== "{")
|
|
855
|
-
return;
|
|
856
|
-
let parsed;
|
|
857
|
-
try {
|
|
858
|
-
parsed = JSON.parse(trimmed);
|
|
859
|
-
} catch {
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
if (parsed.sessionID)
|
|
863
|
-
this.sessionId = parsed.sessionID;
|
|
864
|
-
if (parsed.type === "text" && typeof parsed.part?.text === "string") {
|
|
865
|
-
this.texts.push(parsed.part.text);
|
|
866
|
-
return;
|
|
867
|
-
}
|
|
868
|
-
if (parsed.type === "tool_use") {
|
|
869
|
-
this.toolCalls += 1;
|
|
870
|
-
const name = parsed.part?.tool ?? parsed.tool;
|
|
871
|
-
if (typeof name === "string" && name) {
|
|
872
|
-
this.tools.push(name);
|
|
873
|
-
}
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
if (parsed.type === "error") {
|
|
877
|
-
if (this.error === undefined) {
|
|
878
|
-
this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
finish() {
|
|
883
|
-
if (this.tail.trim()) {
|
|
884
|
-
this.parseLine(this.tail);
|
|
885
|
-
this.tail = "";
|
|
886
|
-
}
|
|
887
|
-
const text = this.texts.join("").trim();
|
|
888
|
-
return {
|
|
889
|
-
text,
|
|
890
|
-
error: this.error,
|
|
891
|
-
toolCalls: this.toolCalls,
|
|
892
|
-
sessionId: this.sessionId,
|
|
893
|
-
tools: this.tools
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
}
|
|
871
|
+
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
872
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
897
873
|
|
|
898
|
-
//
|
|
874
|
+
// ../../../packages/cli/src/runtime/spawn.ts
|
|
899
875
|
import { spawn } from "node:child_process";
|
|
900
876
|
function runCapture(cmd, opts = {}) {
|
|
901
877
|
return new Promise((resolve, reject) => {
|
|
@@ -1018,7 +994,444 @@ function isMissingBinary(err) {
|
|
|
1018
994
|
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1019
995
|
}
|
|
1020
996
|
|
|
1021
|
-
//
|
|
997
|
+
// ../../../packages/cli/src/daemon/runners/claude-agents.ts
|
|
998
|
+
var CLAUDE_ROY_AGENT_NAME = "roy";
|
|
999
|
+
var ROY_AGENT_DESCRIPTION = "Roy — task-as-first-class-citizen agent. Uses the roy-agent task lifecycle " + "(task_tree → task_create → task_update → task_operation_create → task_complete) " + "so every piece of work is tracked in the local task store.";
|
|
1000
|
+
var CLAUDE_ROY_TOOLS = Object.freeze([
|
|
1001
|
+
"Read",
|
|
1002
|
+
"Write",
|
|
1003
|
+
"Edit",
|
|
1004
|
+
"Bash",
|
|
1005
|
+
"Grep",
|
|
1006
|
+
"Glob",
|
|
1007
|
+
"TodoWrite",
|
|
1008
|
+
"WebFetch",
|
|
1009
|
+
"WebSearch"
|
|
1010
|
+
]);
|
|
1011
|
+
var CLAUDE_ROY_SYSTEM_PROMPT = `# Roy — task-first-class-citizen agent
|
|
1012
|
+
|
|
1013
|
+
You are **Roy**, an agent whose defining rule is simple:
|
|
1014
|
+
|
|
1015
|
+
> **Task is a first-class citizen. No task, no work.**
|
|
1016
|
+
|
|
1017
|
+
You are not a chat assistant that occasionally writes notes. You are a worker
|
|
1018
|
+
whose output is a *visible, trackable* stream of tasks. Every non-trivial
|
|
1019
|
+
request (more than one tool call / more than a couple of minutes of work) MUST
|
|
1020
|
+
become a tracked task before you start doing it, and MUST be closed out
|
|
1021
|
+
explicitly when you finish.
|
|
1022
|
+
|
|
1023
|
+
## The lifecycle (non-negotiable)
|
|
1024
|
+
|
|
1025
|
+
### 1. Orient — \`task_tree(depth=1)\` (or \`depth=2\` for bigger trees)
|
|
1026
|
+
Before creating anything, look at the existing tree. Decide:
|
|
1027
|
+
- does this work belong to an existing root (attach via \`parent_task_id\`)?
|
|
1028
|
+
- or is it genuinely new?
|
|
1029
|
+
|
|
1030
|
+
Keep **root tasks ≤ 10**. Prefer attaching to an existing thematic root over
|
|
1031
|
+
spawning a new root. Never hardcode ids — read them from \`task_tree\`.
|
|
1032
|
+
|
|
1033
|
+
### 2. Create — \`task_create\`
|
|
1034
|
+
Create the task **before** doing the work. Pass:
|
|
1035
|
+
- \`title\` (imperative, specific), \`description\`, \`goals_and_expected_deliverables\`
|
|
1036
|
+
- \`priority\` (low / medium / high), \`type\` (normal / cycle / longterm)
|
|
1037
|
+
- \`project_path\`, \`context\` (JSON string: worktree_path / branch / type …)
|
|
1038
|
+
- \`parent_task_id\` when the tree said so
|
|
1039
|
+
|
|
1040
|
+
The tool returns a **numeric task id** — that number is your handle for the
|
|
1041
|
+
rest of the conversation.
|
|
1042
|
+
|
|
1043
|
+
### 3. Track — \`task_update\`
|
|
1044
|
+
Report progress as you go, not only at the end:
|
|
1045
|
+
- \`status\`: todo → active → (paused / cancelled) → completed
|
|
1046
|
+
- \`progress\`: 0–100
|
|
1047
|
+
- \`current_status\`: one human sentence about where you are right now
|
|
1048
|
+
|
|
1049
|
+
If the scope changes, \`task_update\` the description/goals instead of silently
|
|
1050
|
+
doing something else.
|
|
1051
|
+
|
|
1052
|
+
### 4. Record milestones — \`task_operation_create\`
|
|
1053
|
+
This tool has **two orthogonal dimensions** and you must keep them distinct:
|
|
1054
|
+
- **state / milestone dimension** — \`milestone_type\` + \`milestone_title\` +
|
|
1055
|
+
\`milestone_description\`: *what* was reached (e.g. "GREEN: all tests pass").
|
|
1056
|
+
- **process dimension** — \`process_description\`: *how* you got there —
|
|
1057
|
+
strategy + tool steps, strictly **100–150 characters**.
|
|
1058
|
+
Too short = rejected (no value); too long = rejected (use the session log).
|
|
1059
|
+
|
|
1060
|
+
Rules: do **not** use the process field to restate the milestone, and do not
|
|
1061
|
+
use milestones as a substitute for progress. Milestones mark *state
|
|
1062
|
+
transitions*: RED→GREEN, blocker found, decision made, review passed.
|
|
1063
|
+
|
|
1064
|
+
### 5. Close — \`task_complete\`
|
|
1065
|
+
Before completing:
|
|
1066
|
+
1. \`task_get(task_id)\` and re-read goal + delivery.
|
|
1067
|
+
2. Verify the goal and delivery actually match what you delivered. If they do
|
|
1068
|
+
not, either extend the task (\`task_update\`) or say so honestly.
|
|
1069
|
+
3. Call \`task_complete\` with \`deliverable_storage_path\` — a locator for the
|
|
1070
|
+
work product: a file path, a git branch/worktree, a doc URL, a commit URL.
|
|
1071
|
+
The path is persisted into the task context and as an operation record.
|
|
1072
|
+
|
|
1073
|
+
\`task_complete\` cannot be used on \`cycle\` tasks (they are periodic by
|
|
1074
|
+
design) — use \`task_update\` to reset those.
|
|
1075
|
+
|
|
1076
|
+
## Other tools
|
|
1077
|
+
|
|
1078
|
+
- \`task_get(task_id, include_operations)\` — full detail; use it before
|
|
1079
|
+
resuming or completing a task.
|
|
1080
|
+
- \`task_list(...)\` — flat listing with filters (status / priority / type).
|
|
1081
|
+
- \`task_search(keywords)\` — find by title / description / goals / tags.
|
|
1082
|
+
**Search before creating**, to avoid duplicates.
|
|
1083
|
+
|
|
1084
|
+
## Where tasks live
|
|
1085
|
+
|
|
1086
|
+
All tasks and operations are persisted by the roy-task-plugin in the
|
|
1087
|
+
**single** project store — locally that is SQLite at
|
|
1088
|
+
\`~/.roy-task-plugin/tasks.db\` (or the remote service when the plugin is
|
|
1089
|
+
configured with \`persistence: "remote"\`). Do not create side databases, do
|
|
1090
|
+
not maintain your own to-do list in files, and do not track work in chat only:
|
|
1091
|
+
if it is not in the task store, it is not tracked.
|
|
1092
|
+
|
|
1093
|
+
## Tool surface on Claude Code
|
|
1094
|
+
|
|
1095
|
+
The lifecycle above is expressed through the \`roy-agent tasks\` CLI (run it with
|
|
1096
|
+
the \`Bash\` tool), because Claude Code does not yet expose the \`task_*\` tools
|
|
1097
|
+
natively. The mapping is 1:1 — same store, same ids, same semantics:
|
|
1098
|
+
|
|
1099
|
+
| lifecycle step | Claude Code command |
|
|
1100
|
+
|---|---|
|
|
1101
|
+
| \`task_tree(depth=1)\` | \`roy-agent tasks tree\` |
|
|
1102
|
+
| \`task_create\` | \`roy-agent tasks create "<title>" -d "<description>" -g "<goals>" -p high --project-path "<path>"\` |
|
|
1103
|
+
| \`task_update\` | \`roy-agent tasks update <id> --status active --progress 50 --current-status "…"\` |
|
|
1104
|
+
| \`task_operation_create\` | \`roy-agent tasks operations <id>\` to read; record milestones via the CLI's update/ops surface |
|
|
1105
|
+
| \`task_complete\` | \`roy-agent tasks complete <id> --deliverable "<path>"\` |
|
|
1106
|
+
| \`task_get\` | \`roy-agent tasks get <id>\` |
|
|
1107
|
+
| \`task_list\` | \`roy-agent tasks list\` |
|
|
1108
|
+
| \`task_search\` | \`roy-agent tasks search <keywords>\` |
|
|
1109
|
+
|
|
1110
|
+
Prefer \`--json\` (\`-j\`) when you need to parse ids. The task id printed by
|
|
1111
|
+
\`create\` is your handle for the rest of the turn.
|
|
1112
|
+
|
|
1113
|
+
## Style
|
|
1114
|
+
|
|
1115
|
+
- Be direct and concrete; the user is reading task titles and milestones.
|
|
1116
|
+
- Report the task id and its state whenever you finish a step.
|
|
1117
|
+
- If a task is blocked, mark it (\`task_update(status="paused")\`) and record a
|
|
1118
|
+
\`problem\` milestone explaining the blocker instead of going silent.`;
|
|
1119
|
+
function buildClaudeAgentsJson(opts = {}) {
|
|
1120
|
+
const def = {
|
|
1121
|
+
description: opts.description ?? ROY_AGENT_DESCRIPTION,
|
|
1122
|
+
prompt: opts.prompt ?? CLAUDE_ROY_SYSTEM_PROMPT,
|
|
1123
|
+
tools: [...opts.tools ?? CLAUDE_ROY_TOOLS]
|
|
1124
|
+
};
|
|
1125
|
+
if (opts.model)
|
|
1126
|
+
def.model = opts.model;
|
|
1127
|
+
return JSON.stringify({ [opts.name ?? CLAUDE_ROY_AGENT_NAME]: def });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// ../../../packages/cli/src/daemon/runners/claude-sessions.ts
|
|
1131
|
+
function createClaudeSessionRegistry() {
|
|
1132
|
+
const sessions = new Map;
|
|
1133
|
+
return {
|
|
1134
|
+
get: (key) => sessions.get(key),
|
|
1135
|
+
set: (key, id) => {
|
|
1136
|
+
sessions.set(key, id);
|
|
1137
|
+
},
|
|
1138
|
+
delete: (key) => {
|
|
1139
|
+
sessions.delete(key);
|
|
1140
|
+
},
|
|
1141
|
+
size: () => sessions.size
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// ../../../packages/cli/src/daemon/ndjson.ts
|
|
1146
|
+
class LineSplitter {
|
|
1147
|
+
tail = "";
|
|
1148
|
+
pushChunk(chunk) {
|
|
1149
|
+
this.tail += chunk;
|
|
1150
|
+
const out = [];
|
|
1151
|
+
let idx = this.tail.indexOf(`
|
|
1152
|
+
`);
|
|
1153
|
+
while (idx !== -1) {
|
|
1154
|
+
out.push(this.tail.slice(0, idx));
|
|
1155
|
+
this.tail = this.tail.slice(idx + 1);
|
|
1156
|
+
idx = this.tail.indexOf(`
|
|
1157
|
+
`);
|
|
1158
|
+
}
|
|
1159
|
+
return out;
|
|
1160
|
+
}
|
|
1161
|
+
finish() {
|
|
1162
|
+
if (!this.tail)
|
|
1163
|
+
return [];
|
|
1164
|
+
const line = this.tail;
|
|
1165
|
+
this.tail = "";
|
|
1166
|
+
return [line];
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
class NdjsonAssembler {
|
|
1171
|
+
tail = "";
|
|
1172
|
+
texts = [];
|
|
1173
|
+
error;
|
|
1174
|
+
toolCalls = 0;
|
|
1175
|
+
sessionId;
|
|
1176
|
+
tools = [];
|
|
1177
|
+
pushChunk(chunk) {
|
|
1178
|
+
this.tail += chunk;
|
|
1179
|
+
let idx = this.tail.indexOf(`
|
|
1180
|
+
`);
|
|
1181
|
+
while (idx !== -1) {
|
|
1182
|
+
const line = this.tail.slice(0, idx);
|
|
1183
|
+
this.tail = this.tail.slice(idx + 1);
|
|
1184
|
+
this.parseLine(line);
|
|
1185
|
+
idx = this.tail.indexOf(`
|
|
1186
|
+
`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
pushLine(line) {
|
|
1190
|
+
this.parseLine(line);
|
|
1191
|
+
}
|
|
1192
|
+
finishIncomplete() {
|
|
1193
|
+
return this.tail.trim();
|
|
1194
|
+
}
|
|
1195
|
+
parseLine(line) {
|
|
1196
|
+
const trimmed = line.trim();
|
|
1197
|
+
if (!trimmed || trimmed[0] !== "{")
|
|
1198
|
+
return;
|
|
1199
|
+
let parsed;
|
|
1200
|
+
try {
|
|
1201
|
+
parsed = JSON.parse(trimmed);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (parsed.sessionID)
|
|
1206
|
+
this.sessionId = parsed.sessionID;
|
|
1207
|
+
if (parsed.type === "text" && typeof parsed.part?.text === "string") {
|
|
1208
|
+
this.texts.push(parsed.part.text);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (parsed.type === "tool_use") {
|
|
1212
|
+
this.toolCalls += 1;
|
|
1213
|
+
const name = parsed.part?.tool ?? parsed.tool;
|
|
1214
|
+
if (typeof name === "string" && name) {
|
|
1215
|
+
this.tools.push(name);
|
|
1216
|
+
}
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (parsed.type === "error") {
|
|
1220
|
+
if (this.error === undefined) {
|
|
1221
|
+
this.error = parsed.error?.message ?? parsed.error?.type ?? "unknown opencode error";
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
finish() {
|
|
1226
|
+
if (this.tail.trim()) {
|
|
1227
|
+
this.parseLine(this.tail);
|
|
1228
|
+
this.tail = "";
|
|
1229
|
+
}
|
|
1230
|
+
const text = this.texts.join("").trim();
|
|
1231
|
+
return {
|
|
1232
|
+
text,
|
|
1233
|
+
error: this.error,
|
|
1234
|
+
toolCalls: this.toolCalls,
|
|
1235
|
+
sessionId: this.sessionId,
|
|
1236
|
+
tools: this.tools
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ../../../packages/cli/src/daemon/runners/claude-stream.ts
|
|
1242
|
+
function errorDetail(result) {
|
|
1243
|
+
if (typeof result.result === "string" && result.result.trim() !== "") {
|
|
1244
|
+
return result.result.trim();
|
|
1245
|
+
}
|
|
1246
|
+
if (Array.isArray(result.errors)) {
|
|
1247
|
+
const parts = result.errors.map((e) => typeof e === "string" ? e.trim() : "").filter((e) => e !== "");
|
|
1248
|
+
if (parts.length > 0)
|
|
1249
|
+
return parts.join("; ");
|
|
1250
|
+
}
|
|
1251
|
+
return result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
class ClaudeStreamDecoder {
|
|
1255
|
+
lines = new LineSplitter;
|
|
1256
|
+
state = { assistantText: [], tools: [] };
|
|
1257
|
+
pushChunk(chunk) {
|
|
1258
|
+
for (const line of this.lines.pushChunk(chunk))
|
|
1259
|
+
this.consume(line);
|
|
1260
|
+
}
|
|
1261
|
+
finish() {
|
|
1262
|
+
for (const line of this.lines.finish())
|
|
1263
|
+
this.consume(line);
|
|
1264
|
+
return this.toResult();
|
|
1265
|
+
}
|
|
1266
|
+
snapshot() {
|
|
1267
|
+
return { ...this.state, assistantText: [...this.state.assistantText], tools: [...this.state.tools] };
|
|
1268
|
+
}
|
|
1269
|
+
consume(rawLine) {
|
|
1270
|
+
const line = rawLine.trim();
|
|
1271
|
+
if (!line.startsWith("{"))
|
|
1272
|
+
return;
|
|
1273
|
+
let event;
|
|
1274
|
+
try {
|
|
1275
|
+
event = JSON.parse(line);
|
|
1276
|
+
} catch {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (typeof event !== "object" || event === null)
|
|
1280
|
+
return;
|
|
1281
|
+
const ev = event;
|
|
1282
|
+
const sessionId = typeof ev.session_id === "string" ? ev.session_id : undefined;
|
|
1283
|
+
if (sessionId && !this.state.sessionId)
|
|
1284
|
+
this.state.sessionId = sessionId;
|
|
1285
|
+
if (ev.type === "assistant") {
|
|
1286
|
+
this.consumeAssistant(ev);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (ev.type === "result") {
|
|
1290
|
+
this.state.result = ev;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
consumeAssistant(ev) {
|
|
1294
|
+
const message = ev.message;
|
|
1295
|
+
const content = message?.content;
|
|
1296
|
+
if (!Array.isArray(content))
|
|
1297
|
+
return;
|
|
1298
|
+
for (const block of content) {
|
|
1299
|
+
if (typeof block !== "object" || block === null)
|
|
1300
|
+
continue;
|
|
1301
|
+
const b = block;
|
|
1302
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
1303
|
+
this.state.assistantText.push(b.text);
|
|
1304
|
+
} else if (b.type === "tool_use") {
|
|
1305
|
+
if (typeof b.name === "string")
|
|
1306
|
+
this.state.tools.push(b.name);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
toResult() {
|
|
1311
|
+
const { result } = this.state;
|
|
1312
|
+
const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
|
|
1313
|
+
const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
|
|
1314
|
+
if (result?.is_error) {
|
|
1315
|
+
const detail = errorDetail(result);
|
|
1316
|
+
return { text: "", sessionId: this.state.sessionId, toolCalls, tools, error: detail.slice(0, 1000) };
|
|
1317
|
+
}
|
|
1318
|
+
if (typeof result?.result === "string" && result.result.trim() !== "") {
|
|
1319
|
+
return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools };
|
|
1320
|
+
}
|
|
1321
|
+
const fallback = this.state.assistantText.join("").trim();
|
|
1322
|
+
return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
1327
|
+
var DEFAULT_CLAUDE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1328
|
+
var CLAUDE_STDIO = Object.freeze([
|
|
1329
|
+
"ignore",
|
|
1330
|
+
"pipe",
|
|
1331
|
+
"pipe"
|
|
1332
|
+
]);
|
|
1333
|
+
function buildClaudeArgs(input) {
|
|
1334
|
+
const args = ["--print", "--output-format", "stream-json", "--verbose"];
|
|
1335
|
+
if (input.agentsJson)
|
|
1336
|
+
args.push("--agents", input.agentsJson);
|
|
1337
|
+
args.push("--agent", input.agent ?? "roy");
|
|
1338
|
+
args.push("--dangerously-skip-permissions");
|
|
1339
|
+
if (input.sessionId)
|
|
1340
|
+
args.push("--resume", claudeSessionUuid(input.sessionId));
|
|
1341
|
+
args.push(input.prompt);
|
|
1342
|
+
return args;
|
|
1343
|
+
}
|
|
1344
|
+
function isClaudeSessionUuid(value) {
|
|
1345
|
+
if (!value)
|
|
1346
|
+
return false;
|
|
1347
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1348
|
+
}
|
|
1349
|
+
function claudeSessionUuid(sessionKey) {
|
|
1350
|
+
if (isClaudeSessionUuid(sessionKey))
|
|
1351
|
+
return sessionKey;
|
|
1352
|
+
const digest = createHash2("sha256").update(sessionKey).digest("hex");
|
|
1353
|
+
const h = digest.slice(0, 32);
|
|
1354
|
+
return [
|
|
1355
|
+
h.slice(0, 8),
|
|
1356
|
+
h.slice(8, 12),
|
|
1357
|
+
`4${h.slice(13, 16)}`,
|
|
1358
|
+
`8${h.slice(17, 20)}`,
|
|
1359
|
+
h.slice(20, 32)
|
|
1360
|
+
].join("-");
|
|
1361
|
+
}
|
|
1362
|
+
function makeClaudeRunner(opts = {}) {
|
|
1363
|
+
const bin = opts.bin ?? "claude";
|
|
1364
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_CLAUDE_TIMEOUT_MS;
|
|
1365
|
+
const agentsJson = opts.agentsJson ?? buildClaudeAgentsJson();
|
|
1366
|
+
const sessions = opts.sessions ?? createClaudeSessionRegistry();
|
|
1367
|
+
return async function runAgent(input) {
|
|
1368
|
+
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
1369
|
+
const args = buildClaudeArgs({
|
|
1370
|
+
prompt: input.prompt,
|
|
1371
|
+
agent: input.agent,
|
|
1372
|
+
agentsJson,
|
|
1373
|
+
sessionId: known
|
|
1374
|
+
});
|
|
1375
|
+
const decoder = new ClaudeStreamDecoder;
|
|
1376
|
+
let stderr = "";
|
|
1377
|
+
let handle;
|
|
1378
|
+
try {
|
|
1379
|
+
handle = spawnStreaming([bin, ...args], {
|
|
1380
|
+
stdin: CLAUDE_STDIO[0],
|
|
1381
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
1382
|
+
timeoutMs,
|
|
1383
|
+
onStdout: (chunk) => decoder.pushChunk(chunk),
|
|
1384
|
+
onStderr: (chunk) => {
|
|
1385
|
+
if (stderr.length < 64 * 1024)
|
|
1386
|
+
stderr += chunk;
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
1391
|
+
}
|
|
1392
|
+
let code;
|
|
1393
|
+
try {
|
|
1394
|
+
code = await handle.exited;
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
if (isMissingBinary(err)) {
|
|
1397
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
1398
|
+
}
|
|
1399
|
+
return { text: "", error: errMsg(err) };
|
|
1400
|
+
}
|
|
1401
|
+
const result = decoder.finish();
|
|
1402
|
+
const stderrText = stderr.trim();
|
|
1403
|
+
const combined = result.error ? stderrText && !result.error.includes(stderrText) ? `${result.error} | ${stderrText}` : result.error : undefined;
|
|
1404
|
+
const effective = combined ? { ...result, error: combined.slice(0, 1000) } : result;
|
|
1405
|
+
if (input.sessionId && effective.sessionId) {
|
|
1406
|
+
sessions.set(input.sessionId, effective.sessionId);
|
|
1407
|
+
}
|
|
1408
|
+
if (code === 124) {
|
|
1409
|
+
return { ...effective, error: `claude timed out after ${timeoutMs}ms` };
|
|
1410
|
+
}
|
|
1411
|
+
if (effective.error) {
|
|
1412
|
+
if (input.sessionId && /No conversation found|not a UUID|session title/i.test(effective.error)) {
|
|
1413
|
+
sessions.delete(input.sessionId);
|
|
1414
|
+
}
|
|
1415
|
+
return effective;
|
|
1416
|
+
}
|
|
1417
|
+
if (code !== 0 && !effective.text) {
|
|
1418
|
+
return {
|
|
1419
|
+
text: "",
|
|
1420
|
+
toolCalls: effective.toolCalls,
|
|
1421
|
+
tools: effective.tools,
|
|
1422
|
+
error: `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return effective;
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function errMsg(err) {
|
|
1429
|
+
if (err instanceof Error)
|
|
1430
|
+
return err.message;
|
|
1431
|
+
return String(err);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// ../../../packages/cli/src/daemon/runners/opencode.ts
|
|
1022
1435
|
var DEFAULT_OPENCODE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1023
1436
|
function makeOpencodeRunner(opts = {}) {
|
|
1024
1437
|
const bin = opts.bin ?? "opencode";
|
|
@@ -1044,16 +1457,16 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1044
1457
|
}
|
|
1045
1458
|
});
|
|
1046
1459
|
} catch (err) {
|
|
1047
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1460
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
1048
1461
|
}
|
|
1049
1462
|
let code;
|
|
1050
1463
|
try {
|
|
1051
1464
|
code = await handle.exited;
|
|
1052
1465
|
} catch (err) {
|
|
1053
1466
|
if (isMissingBinary(err)) {
|
|
1054
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1467
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
1055
1468
|
}
|
|
1056
|
-
return { text: "", error:
|
|
1469
|
+
return { text: "", error: errMsg2(err) };
|
|
1057
1470
|
}
|
|
1058
1471
|
const result = assembler.finish();
|
|
1059
1472
|
if (code === 124) {
|
|
@@ -1090,11 +1503,31 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1090
1503
|
};
|
|
1091
1504
|
};
|
|
1092
1505
|
}
|
|
1093
|
-
function
|
|
1094
|
-
|
|
1506
|
+
function errMsg2(err) {
|
|
1507
|
+
if (err instanceof Error)
|
|
1508
|
+
return err.message;
|
|
1509
|
+
return String(err);
|
|
1095
1510
|
}
|
|
1096
1511
|
|
|
1097
|
-
//
|
|
1512
|
+
// ../../../packages/cli/src/daemon/runners/registry.ts
|
|
1513
|
+
var FACTORIES = {
|
|
1514
|
+
opencode: (input) => makeOpencodeRunner({ bin: input.bin, timeoutMs: input.timeoutMs }),
|
|
1515
|
+
claude: (input) => makeClaudeRunner({
|
|
1516
|
+
bin: input.bin,
|
|
1517
|
+
timeoutMs: input.timeoutMs,
|
|
1518
|
+
agentsJson: input.claudeAgentsJson
|
|
1519
|
+
})
|
|
1520
|
+
};
|
|
1521
|
+
function makeRunner(input = {}) {
|
|
1522
|
+
const name = input.runtime ?? DEFAULT_RUNTIME;
|
|
1523
|
+
if (!isRuntimeName(name)) {
|
|
1524
|
+
throw new Error(`unknown runtime "${name}"`);
|
|
1525
|
+
}
|
|
1526
|
+
const factory = FACTORIES[name];
|
|
1527
|
+
return factory({ ...input, bin: input.bin ?? DEFAULT_RUNTIME_BIN[name] });
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// ../../../packages/cli/src/daemon/reply.ts
|
|
1098
1531
|
var MAX_POST_BYTES = 28 * 1024;
|
|
1099
1532
|
|
|
1100
1533
|
class EmptyReplyError extends Error {
|
|
@@ -1178,7 +1611,7 @@ async function defaultReplyRunner(input) {
|
|
|
1178
1611
|
}
|
|
1179
1612
|
}
|
|
1180
1613
|
|
|
1181
|
-
//
|
|
1614
|
+
// ../../../packages/cli/src/cmd/serve.ts
|
|
1182
1615
|
function serve(options, deps = {}) {
|
|
1183
1616
|
return startServe(options, deps);
|
|
1184
1617
|
}
|
|
@@ -1188,7 +1621,11 @@ async function startServe(options, deps) {
|
|
|
1188
1621
|
const deduper = new Deduper;
|
|
1189
1622
|
const card = deps.card ?? (options.cardEnabled && !options.dryRun ? createRoyAgentCardPort() : createNoopCardPort());
|
|
1190
1623
|
const dispatcher = new Dispatcher({
|
|
1191
|
-
runAgent: deps.runAgent ??
|
|
1624
|
+
runAgent: deps.runAgent ?? makeRunner({
|
|
1625
|
+
runtime: options.runtime,
|
|
1626
|
+
bin: resolveRuntimeBin(options),
|
|
1627
|
+
timeoutMs: options.timeoutMs
|
|
1628
|
+
}),
|
|
1192
1629
|
reply: deps.reply ?? defaultReplyRunner,
|
|
1193
1630
|
gate,
|
|
1194
1631
|
deduper,
|
|
@@ -1216,7 +1653,7 @@ async function startServe(options, deps) {
|
|
|
1216
1653
|
try {
|
|
1217
1654
|
await listener.stop();
|
|
1218
1655
|
} catch (err) {
|
|
1219
|
-
log(`[roy-task] listener.stop() failed: ${
|
|
1656
|
+
log(`[roy-task] listener.stop() failed: ${errMsg3(err)}`);
|
|
1220
1657
|
}
|
|
1221
1658
|
log("[roy-task] stopped");
|
|
1222
1659
|
resolveDone();
|
|
@@ -1238,7 +1675,7 @@ async function startServe(options, deps) {
|
|
|
1238
1675
|
await stop();
|
|
1239
1676
|
}
|
|
1240
1677
|
} catch (err) {
|
|
1241
|
-
log(`[roy-task] ${label} dispatch threw: ${
|
|
1678
|
+
log(`[roy-task] ${label} dispatch threw: ${errMsg3(err)}`);
|
|
1242
1679
|
if (options.once)
|
|
1243
1680
|
await stop();
|
|
1244
1681
|
}
|
|
@@ -1248,7 +1685,7 @@ async function startServe(options, deps) {
|
|
|
1248
1685
|
try {
|
|
1249
1686
|
await listener.start();
|
|
1250
1687
|
} catch (err) {
|
|
1251
|
-
log(`[roy-task] failed to start event listener: ${
|
|
1688
|
+
log(`[roy-task] failed to start event listener: ${errMsg3(err)}`);
|
|
1252
1689
|
clearInterval(sweeper);
|
|
1253
1690
|
resolveDone();
|
|
1254
1691
|
throw err;
|
|
@@ -1265,7 +1702,8 @@ function formatBanner(options, commands, maxConcurrency) {
|
|
|
1265
1702
|
"[roy-task] starting",
|
|
1266
1703
|
` profile : ${options.profile}`,
|
|
1267
1704
|
` agent : ${options.agent}`,
|
|
1268
|
-
`
|
|
1705
|
+
` runtime : ${options.runtime}`,
|
|
1706
|
+
` runtime bin : ${resolveRuntimeBin(options) ?? "(default)"}`,
|
|
1269
1707
|
` max concurrency : ${maxConcurrency}`,
|
|
1270
1708
|
` reply enabled : ${options.replyEnabled}`,
|
|
1271
1709
|
` progress cards : ${options.cardEnabled && !options.dryRun}`,
|
|
@@ -1307,11 +1745,11 @@ function formatResult(label, result) {
|
|
|
1307
1745
|
return `[roy-task] ${label} skipped: ${result.reason}`;
|
|
1308
1746
|
}
|
|
1309
1747
|
}
|
|
1310
|
-
function
|
|
1748
|
+
function errMsg3(err) {
|
|
1311
1749
|
return err instanceof Error ? err.message : String(err);
|
|
1312
1750
|
}
|
|
1313
1751
|
|
|
1314
|
-
//
|
|
1752
|
+
// ../../../packages/cli/src/runtime/sleep.ts
|
|
1315
1753
|
function sleep(ms) {
|
|
1316
1754
|
return new Promise((resolve) => {
|
|
1317
1755
|
const timer = setTimeout(resolve, ms);
|
|
@@ -1319,7 +1757,7 @@ function sleep(ms) {
|
|
|
1319
1757
|
});
|
|
1320
1758
|
}
|
|
1321
1759
|
|
|
1322
|
-
//
|
|
1760
|
+
// ../../../packages/cli/src/cmd/status.ts
|
|
1323
1761
|
async function runCapture2(cmd) {
|
|
1324
1762
|
try {
|
|
1325
1763
|
return await runCapture(cmd);
|
|
@@ -1431,8 +1869,8 @@ function formatStatus(report) {
|
|
|
1431
1869
|
`);
|
|
1432
1870
|
}
|
|
1433
1871
|
|
|
1434
|
-
//
|
|
1435
|
-
var VERSION = "0.
|
|
1872
|
+
// ../../../packages/cli/src/main.ts
|
|
1873
|
+
var VERSION = "0.4.1";
|
|
1436
1874
|
async function main(argv) {
|
|
1437
1875
|
const parsed = parseArgs(argv);
|
|
1438
1876
|
switch (parsed.command) {
|