@ai-setting/roy-task-plugin 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +527 -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,429 @@ 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
|
+
class ClaudeStreamDecoder {
|
|
1243
|
+
lines = new LineSplitter;
|
|
1244
|
+
state = { assistantText: [], tools: [] };
|
|
1245
|
+
pushChunk(chunk) {
|
|
1246
|
+
for (const line of this.lines.pushChunk(chunk))
|
|
1247
|
+
this.consume(line);
|
|
1248
|
+
}
|
|
1249
|
+
finish() {
|
|
1250
|
+
for (const line of this.lines.finish())
|
|
1251
|
+
this.consume(line);
|
|
1252
|
+
return this.toResult();
|
|
1253
|
+
}
|
|
1254
|
+
snapshot() {
|
|
1255
|
+
return { ...this.state, assistantText: [...this.state.assistantText], tools: [...this.state.tools] };
|
|
1256
|
+
}
|
|
1257
|
+
consume(rawLine) {
|
|
1258
|
+
const line = rawLine.trim();
|
|
1259
|
+
if (!line.startsWith("{"))
|
|
1260
|
+
return;
|
|
1261
|
+
let event;
|
|
1262
|
+
try {
|
|
1263
|
+
event = JSON.parse(line);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (typeof event !== "object" || event === null)
|
|
1268
|
+
return;
|
|
1269
|
+
const ev = event;
|
|
1270
|
+
const sessionId = typeof ev.session_id === "string" ? ev.session_id : undefined;
|
|
1271
|
+
if (sessionId && !this.state.sessionId)
|
|
1272
|
+
this.state.sessionId = sessionId;
|
|
1273
|
+
if (ev.type === "assistant") {
|
|
1274
|
+
this.consumeAssistant(ev);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (ev.type === "result") {
|
|
1278
|
+
this.state.result = ev;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
consumeAssistant(ev) {
|
|
1282
|
+
const message = ev.message;
|
|
1283
|
+
const content = message?.content;
|
|
1284
|
+
if (!Array.isArray(content))
|
|
1285
|
+
return;
|
|
1286
|
+
for (const block of content) {
|
|
1287
|
+
if (typeof block !== "object" || block === null)
|
|
1288
|
+
continue;
|
|
1289
|
+
const b = block;
|
|
1290
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
1291
|
+
this.state.assistantText.push(b.text);
|
|
1292
|
+
} else if (b.type === "tool_use") {
|
|
1293
|
+
if (typeof b.name === "string")
|
|
1294
|
+
this.state.tools.push(b.name);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
toResult() {
|
|
1299
|
+
const { result } = this.state;
|
|
1300
|
+
const toolCalls = this.state.tools.length > 0 ? this.state.tools.length : undefined;
|
|
1301
|
+
const tools = this.state.tools.length > 0 ? [...this.state.tools] : undefined;
|
|
1302
|
+
if (result?.is_error) {
|
|
1303
|
+
const detail = typeof result.result === "string" && result.result.trim() !== "" ? result.result.trim() : result.subtype === undefined ? "claude reported is_error" : `claude reported ${result.subtype}`;
|
|
1304
|
+
return { text: "", sessionId: this.state.sessionId, toolCalls, tools, error: detail.slice(0, 1000) };
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof result?.result === "string" && result.result.trim() !== "") {
|
|
1307
|
+
return { text: result.result.trim(), sessionId: this.state.sessionId, toolCalls, tools };
|
|
1308
|
+
}
|
|
1309
|
+
const fallback = this.state.assistantText.join("").trim();
|
|
1310
|
+
return { text: fallback, sessionId: this.state.sessionId, toolCalls, tools };
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// ../../../packages/cli/src/daemon/runners/claude.ts
|
|
1315
|
+
var DEFAULT_CLAUDE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1316
|
+
var CLAUDE_STDIO = Object.freeze([
|
|
1317
|
+
"ignore",
|
|
1318
|
+
"pipe",
|
|
1319
|
+
"pipe"
|
|
1320
|
+
]);
|
|
1321
|
+
function buildClaudeArgs(input) {
|
|
1322
|
+
const args = ["--print", "--output-format", "stream-json", "--verbose"];
|
|
1323
|
+
if (input.agentsJson)
|
|
1324
|
+
args.push("--agents", input.agentsJson);
|
|
1325
|
+
args.push("--agent", input.agent ?? "roy");
|
|
1326
|
+
args.push("--dangerously-skip-permissions");
|
|
1327
|
+
if (input.sessionId)
|
|
1328
|
+
args.push("--resume", claudeSessionUuid(input.sessionId));
|
|
1329
|
+
args.push(input.prompt);
|
|
1330
|
+
return args;
|
|
1331
|
+
}
|
|
1332
|
+
function isClaudeSessionUuid(value) {
|
|
1333
|
+
if (!value)
|
|
1334
|
+
return false;
|
|
1335
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1336
|
+
}
|
|
1337
|
+
function claudeSessionUuid(sessionKey) {
|
|
1338
|
+
if (isClaudeSessionUuid(sessionKey))
|
|
1339
|
+
return sessionKey;
|
|
1340
|
+
const digest = createHash2("sha256").update(sessionKey).digest("hex");
|
|
1341
|
+
const h = digest.slice(0, 32);
|
|
1342
|
+
return [
|
|
1343
|
+
h.slice(0, 8),
|
|
1344
|
+
h.slice(8, 12),
|
|
1345
|
+
`4${h.slice(13, 16)}`,
|
|
1346
|
+
`8${h.slice(17, 20)}`,
|
|
1347
|
+
h.slice(20, 32)
|
|
1348
|
+
].join("-");
|
|
1349
|
+
}
|
|
1350
|
+
function makeClaudeRunner(opts = {}) {
|
|
1351
|
+
const bin = opts.bin ?? "claude";
|
|
1352
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_CLAUDE_TIMEOUT_MS;
|
|
1353
|
+
const agentsJson = opts.agentsJson ?? buildClaudeAgentsJson();
|
|
1354
|
+
const sessions = opts.sessions ?? createClaudeSessionRegistry();
|
|
1355
|
+
return async function runAgent(input) {
|
|
1356
|
+
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
1357
|
+
const args = buildClaudeArgs({
|
|
1358
|
+
prompt: input.prompt,
|
|
1359
|
+
agent: input.agent,
|
|
1360
|
+
agentsJson,
|
|
1361
|
+
sessionId: known
|
|
1362
|
+
});
|
|
1363
|
+
const decoder = new ClaudeStreamDecoder;
|
|
1364
|
+
let stderr = "";
|
|
1365
|
+
let handle;
|
|
1366
|
+
try {
|
|
1367
|
+
handle = spawnStreaming([bin, ...args], {
|
|
1368
|
+
stdin: CLAUDE_STDIO[0],
|
|
1369
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
1370
|
+
timeoutMs,
|
|
1371
|
+
onStdout: (chunk) => decoder.pushChunk(chunk),
|
|
1372
|
+
onStderr: (chunk) => {
|
|
1373
|
+
if (stderr.length < 64 * 1024)
|
|
1374
|
+
stderr += chunk;
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
1379
|
+
}
|
|
1380
|
+
let code;
|
|
1381
|
+
try {
|
|
1382
|
+
code = await handle.exited;
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
if (isMissingBinary(err)) {
|
|
1385
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg(err)}` };
|
|
1386
|
+
}
|
|
1387
|
+
return { text: "", error: errMsg(err) };
|
|
1388
|
+
}
|
|
1389
|
+
const result = decoder.finish();
|
|
1390
|
+
if (input.sessionId && result.sessionId && !result.error) {
|
|
1391
|
+
sessions.set(input.sessionId, result.sessionId);
|
|
1392
|
+
}
|
|
1393
|
+
if (code === 124) {
|
|
1394
|
+
return { ...result, error: `claude timed out after ${timeoutMs}ms` };
|
|
1395
|
+
}
|
|
1396
|
+
if (result.error) {
|
|
1397
|
+
if (input.sessionId && /No conversation found|not a UUID|session title/i.test(result.error)) {
|
|
1398
|
+
sessions.delete(input.sessionId);
|
|
1399
|
+
}
|
|
1400
|
+
return result;
|
|
1401
|
+
}
|
|
1402
|
+
if (code !== 0 && !result.text) {
|
|
1403
|
+
return {
|
|
1404
|
+
text: "",
|
|
1405
|
+
toolCalls: result.toolCalls,
|
|
1406
|
+
tools: result.tools,
|
|
1407
|
+
error: `claude exited ${code}: ${(stderr || "no stderr").trim().slice(0, 1000)}`
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return result;
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function errMsg(err) {
|
|
1414
|
+
if (err instanceof Error)
|
|
1415
|
+
return err.message;
|
|
1416
|
+
return String(err);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// ../../../packages/cli/src/daemon/runners/opencode.ts
|
|
1022
1420
|
var DEFAULT_OPENCODE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1023
1421
|
function makeOpencodeRunner(opts = {}) {
|
|
1024
1422
|
const bin = opts.bin ?? "opencode";
|
|
@@ -1044,16 +1442,16 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1044
1442
|
}
|
|
1045
1443
|
});
|
|
1046
1444
|
} catch (err) {
|
|
1047
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1445
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
1048
1446
|
}
|
|
1049
1447
|
let code;
|
|
1050
1448
|
try {
|
|
1051
1449
|
code = await handle.exited;
|
|
1052
1450
|
} catch (err) {
|
|
1053
1451
|
if (isMissingBinary(err)) {
|
|
1054
|
-
return { text: "", error: `failed to spawn ${bin}: ${
|
|
1452
|
+
return { text: "", error: `failed to spawn ${bin}: ${errMsg2(err)}` };
|
|
1055
1453
|
}
|
|
1056
|
-
return { text: "", error:
|
|
1454
|
+
return { text: "", error: errMsg2(err) };
|
|
1057
1455
|
}
|
|
1058
1456
|
const result = assembler.finish();
|
|
1059
1457
|
if (code === 124) {
|
|
@@ -1090,11 +1488,31 @@ function makeOpencodeRunner(opts = {}) {
|
|
|
1090
1488
|
};
|
|
1091
1489
|
};
|
|
1092
1490
|
}
|
|
1093
|
-
function
|
|
1094
|
-
|
|
1491
|
+
function errMsg2(err) {
|
|
1492
|
+
if (err instanceof Error)
|
|
1493
|
+
return err.message;
|
|
1494
|
+
return String(err);
|
|
1095
1495
|
}
|
|
1096
1496
|
|
|
1097
|
-
//
|
|
1497
|
+
// ../../../packages/cli/src/daemon/runners/registry.ts
|
|
1498
|
+
var FACTORIES = {
|
|
1499
|
+
opencode: (input) => makeOpencodeRunner({ bin: input.bin, timeoutMs: input.timeoutMs }),
|
|
1500
|
+
claude: (input) => makeClaudeRunner({
|
|
1501
|
+
bin: input.bin,
|
|
1502
|
+
timeoutMs: input.timeoutMs,
|
|
1503
|
+
agentsJson: input.claudeAgentsJson
|
|
1504
|
+
})
|
|
1505
|
+
};
|
|
1506
|
+
function makeRunner(input = {}) {
|
|
1507
|
+
const name = input.runtime ?? DEFAULT_RUNTIME;
|
|
1508
|
+
if (!isRuntimeName(name)) {
|
|
1509
|
+
throw new Error(`unknown runtime "${name}"`);
|
|
1510
|
+
}
|
|
1511
|
+
const factory = FACTORIES[name];
|
|
1512
|
+
return factory({ ...input, bin: input.bin ?? DEFAULT_RUNTIME_BIN[name] });
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// ../../../packages/cli/src/daemon/reply.ts
|
|
1098
1516
|
var MAX_POST_BYTES = 28 * 1024;
|
|
1099
1517
|
|
|
1100
1518
|
class EmptyReplyError extends Error {
|
|
@@ -1178,7 +1596,7 @@ async function defaultReplyRunner(input) {
|
|
|
1178
1596
|
}
|
|
1179
1597
|
}
|
|
1180
1598
|
|
|
1181
|
-
//
|
|
1599
|
+
// ../../../packages/cli/src/cmd/serve.ts
|
|
1182
1600
|
function serve(options, deps = {}) {
|
|
1183
1601
|
return startServe(options, deps);
|
|
1184
1602
|
}
|
|
@@ -1188,7 +1606,11 @@ async function startServe(options, deps) {
|
|
|
1188
1606
|
const deduper = new Deduper;
|
|
1189
1607
|
const card = deps.card ?? (options.cardEnabled && !options.dryRun ? createRoyAgentCardPort() : createNoopCardPort());
|
|
1190
1608
|
const dispatcher = new Dispatcher({
|
|
1191
|
-
runAgent: deps.runAgent ??
|
|
1609
|
+
runAgent: deps.runAgent ?? makeRunner({
|
|
1610
|
+
runtime: options.runtime,
|
|
1611
|
+
bin: resolveRuntimeBin(options),
|
|
1612
|
+
timeoutMs: options.timeoutMs
|
|
1613
|
+
}),
|
|
1192
1614
|
reply: deps.reply ?? defaultReplyRunner,
|
|
1193
1615
|
gate,
|
|
1194
1616
|
deduper,
|
|
@@ -1216,7 +1638,7 @@ async function startServe(options, deps) {
|
|
|
1216
1638
|
try {
|
|
1217
1639
|
await listener.stop();
|
|
1218
1640
|
} catch (err) {
|
|
1219
|
-
log(`[roy-task] listener.stop() failed: ${
|
|
1641
|
+
log(`[roy-task] listener.stop() failed: ${errMsg3(err)}`);
|
|
1220
1642
|
}
|
|
1221
1643
|
log("[roy-task] stopped");
|
|
1222
1644
|
resolveDone();
|
|
@@ -1238,7 +1660,7 @@ async function startServe(options, deps) {
|
|
|
1238
1660
|
await stop();
|
|
1239
1661
|
}
|
|
1240
1662
|
} catch (err) {
|
|
1241
|
-
log(`[roy-task] ${label} dispatch threw: ${
|
|
1663
|
+
log(`[roy-task] ${label} dispatch threw: ${errMsg3(err)}`);
|
|
1242
1664
|
if (options.once)
|
|
1243
1665
|
await stop();
|
|
1244
1666
|
}
|
|
@@ -1248,7 +1670,7 @@ async function startServe(options, deps) {
|
|
|
1248
1670
|
try {
|
|
1249
1671
|
await listener.start();
|
|
1250
1672
|
} catch (err) {
|
|
1251
|
-
log(`[roy-task] failed to start event listener: ${
|
|
1673
|
+
log(`[roy-task] failed to start event listener: ${errMsg3(err)}`);
|
|
1252
1674
|
clearInterval(sweeper);
|
|
1253
1675
|
resolveDone();
|
|
1254
1676
|
throw err;
|
|
@@ -1265,7 +1687,8 @@ function formatBanner(options, commands, maxConcurrency) {
|
|
|
1265
1687
|
"[roy-task] starting",
|
|
1266
1688
|
` profile : ${options.profile}`,
|
|
1267
1689
|
` agent : ${options.agent}`,
|
|
1268
|
-
`
|
|
1690
|
+
` runtime : ${options.runtime}`,
|
|
1691
|
+
` runtime bin : ${resolveRuntimeBin(options) ?? "(default)"}`,
|
|
1269
1692
|
` max concurrency : ${maxConcurrency}`,
|
|
1270
1693
|
` reply enabled : ${options.replyEnabled}`,
|
|
1271
1694
|
` progress cards : ${options.cardEnabled && !options.dryRun}`,
|
|
@@ -1307,11 +1730,11 @@ function formatResult(label, result) {
|
|
|
1307
1730
|
return `[roy-task] ${label} skipped: ${result.reason}`;
|
|
1308
1731
|
}
|
|
1309
1732
|
}
|
|
1310
|
-
function
|
|
1733
|
+
function errMsg3(err) {
|
|
1311
1734
|
return err instanceof Error ? err.message : String(err);
|
|
1312
1735
|
}
|
|
1313
1736
|
|
|
1314
|
-
//
|
|
1737
|
+
// ../../../packages/cli/src/runtime/sleep.ts
|
|
1315
1738
|
function sleep(ms) {
|
|
1316
1739
|
return new Promise((resolve) => {
|
|
1317
1740
|
const timer = setTimeout(resolve, ms);
|
|
@@ -1319,7 +1742,7 @@ function sleep(ms) {
|
|
|
1319
1742
|
});
|
|
1320
1743
|
}
|
|
1321
1744
|
|
|
1322
|
-
//
|
|
1745
|
+
// ../../../packages/cli/src/cmd/status.ts
|
|
1323
1746
|
async function runCapture2(cmd) {
|
|
1324
1747
|
try {
|
|
1325
1748
|
return await runCapture(cmd);
|
|
@@ -1431,8 +1854,8 @@ function formatStatus(report) {
|
|
|
1431
1854
|
`);
|
|
1432
1855
|
}
|
|
1433
1856
|
|
|
1434
|
-
//
|
|
1435
|
-
var VERSION = "0.
|
|
1857
|
+
// ../../../packages/cli/src/main.ts
|
|
1858
|
+
var VERSION = "0.4.0";
|
|
1436
1859
|
async function main(argv) {
|
|
1437
1860
|
const parsed = parseArgs(argv);
|
|
1438
1861
|
switch (parsed.command) {
|