@ai-setting/roy-task-plugin 0.7.6 → 0.7.8
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/agents/roy.md +52 -9
- package/dist/cli.js +249 -54
- package/dist/index.js +330 -230
- package/dist/index.js.map +10 -8
- package/package.json +2 -2
|
@@ -34,6 +34,30 @@ Create the task **before** doing the work. Pass:
|
|
|
34
34
|
- `project_path`, `context` (JSON string: worktree_path / branch / type …)
|
|
35
35
|
- `parent_task_id` when the tree said so
|
|
36
36
|
|
|
37
|
+
#### ⚠️ Goals must be numbered sub-items
|
|
38
|
+
`goals_and_expected_deliverables` **MUST** be written as **numbered
|
|
39
|
+
sub-items** (编号子项) that can each be verified **one by one**
|
|
40
|
+
(逐条验收) — for `type=normal` tasks:
|
|
41
|
+
|
|
42
|
+
1. 实现 X(含文件路径)
|
|
43
|
+
2. 测试 Y 覆盖 Z 场景(pass/fail 计数)
|
|
44
|
+
3. 文档 W 落盘到 <path>
|
|
45
|
+
|
|
46
|
+
Why: the completion hook judges the goal **item by item** against the
|
|
47
|
+
operation-record chain, using each sub-item as fine-grained evidence. A
|
|
48
|
+
vague one-line goal (一句话模糊目标,如「做好 X」「完成调研」) gives the
|
|
49
|
+
judge nothing to check and reliably triggers a completion hint.
|
|
50
|
+
|
|
51
|
+
Reference the four atomic assertion forms when writing each sub-item:
|
|
52
|
+
- **文件断言** (file assertion) — file exists / size / content grep
|
|
53
|
+
- **进程断言** (process assertion) — command exit code / stdout match
|
|
54
|
+
- **测试断言** (test assertion) — pass/fail counts, no NEW fail
|
|
55
|
+
- **状态断言** (state assertion) — version number / HTTP 200 / status field
|
|
56
|
+
|
|
57
|
+
For `type=longterm` / `type=cycle` tasks there is no per-item completion
|
|
58
|
+
acceptance — for those, split goals into staged, individually checkable
|
|
59
|
+
sub-items instead of one vague sentence.
|
|
60
|
+
|
|
37
61
|
The tool returns a **numeric task id** — that number is your handle for the
|
|
38
62
|
rest of the conversation.
|
|
39
63
|
|
|
@@ -68,12 +92,32 @@ transitions*: RED→GREEN, blocker found, decision made, review passed.
|
|
|
68
92
|
### 5. Close — `task_complete`
|
|
69
93
|
Before completing:
|
|
70
94
|
1. `task_get(task_id)` and re-read goal + delivery.
|
|
71
|
-
2.
|
|
72
|
-
|
|
95
|
+
2. **Accept the goal item-by-item** (按子项逐条验收): check **each** numbered
|
|
96
|
+
sub-item against the operation-record chain and the deliverable evidence.
|
|
97
|
+
If a sub-item is not met, either extend the task (`task_update`) or say
|
|
98
|
+
so honestly.
|
|
73
99
|
3. Call `task_complete` with `deliverable_storage_path` — a locator for the
|
|
74
100
|
work product: a file path, a git branch/worktree, a doc URL, a commit URL.
|
|
75
101
|
The path is persisted into the task context and as an operation record.
|
|
76
102
|
|
|
103
|
+
#### ⚠️ The completion hint is a verify duty, not a notification
|
|
104
|
+
If the task context carries a `[TASK_COMPLETION_HINT]`, you **MUST** verify
|
|
105
|
+
it (核实义务) — never ignore it (不允许无视). Re-read the task via
|
|
106
|
+
`task_get`, then do **exactly one** of:
|
|
107
|
+
- (a) the goal is **not** met → `task_update(task_id=<id>, status=todo)` to
|
|
108
|
+
reopen and continue the work; or
|
|
109
|
+
- (b) the goal **is** met → `task_operation_create`
|
|
110
|
+
(milestone_type=decision) stating why the sub-items are actually met
|
|
111
|
+
(说明理由 — "state why").
|
|
112
|
+
|
|
113
|
+
#### Several deliverables in one field
|
|
114
|
+
`deliverable_storage_path` may name **multiple paths**, space-separated
|
|
115
|
+
(`/tmp/report.md /tmp/diff.patch`) — the judge splits on whitespace as well
|
|
116
|
+
as `,` `;` `、` and ` + `. A local path may also be written in its
|
|
117
|
+
`file://` form (`file:///tmp/report.md`) — the scheme is stripped and the
|
|
118
|
+
path kept. Declare **every** artefact you produced: a path left out reads as
|
|
119
|
+
"not produced" and drags the task toward a false completion hint.
|
|
120
|
+
|
|
77
121
|
`task_complete` cannot be used on `cycle` tasks (they are periodic by
|
|
78
122
|
design) — use `task_update` to reset those.
|
|
79
123
|
|
|
@@ -94,6 +138,12 @@ configured with `persistence: "remote"`). Do not create side databases, do
|
|
|
94
138
|
not maintain your own to-do list in files, and do not track work in chat only:
|
|
95
139
|
if it is not in the task store, it is not tracked.
|
|
96
140
|
|
|
141
|
+
## Style
|
|
142
|
+
|
|
143
|
+
- Be direct and concrete; the user is reading task titles and milestones.
|
|
144
|
+
- Report the task id and its state whenever you finish a step.
|
|
145
|
+
- If a task is blocked, mark it (`task_update(status="paused")`) and record a
|
|
146
|
+
`problem` milestone explaining the blocker instead of going silent.
|
|
97
147
|
## Tool surface on Claude Code
|
|
98
148
|
|
|
99
149
|
The `task_*` tools are served by the **`roy-task` MCP server** this runtime
|
|
@@ -118,10 +168,3 @@ Measured: it has no way to write a milestone (its `operations <id>` subcommand
|
|
|
118
168
|
is a *reader*), and it targets a different database than this server. Use the
|
|
119
169
|
MCP tools above; if they are genuinely unavailable, say so in your answer
|
|
120
170
|
instead of silently skipping the lifecycle.
|
|
121
|
-
|
|
122
|
-
## Style
|
|
123
|
-
|
|
124
|
-
- Be direct and concrete; the user is reading task titles and milestones.
|
|
125
|
-
- Report the task id and its state whenever you finish a step.
|
|
126
|
-
- If a task is blocked, mark it (`task_update(status="paused")`) and record a
|
|
127
|
-
`problem` milestone explaining the blocker instead of going silent.
|
package/dist/cli.js
CHANGED
|
@@ -444,33 +444,8 @@ class NdjsonAssembler {
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
//
|
|
448
|
-
var
|
|
449
|
-
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.";
|
|
450
|
-
var ROY_TASK_MCP_SERVER = "roy-task";
|
|
451
|
-
var ROY_TASK_MCP_TOOLS = Object.freeze([
|
|
452
|
-
"task_create",
|
|
453
|
-
"task_update",
|
|
454
|
-
"task_operation_create",
|
|
455
|
-
"task_complete",
|
|
456
|
-
"task_get",
|
|
457
|
-
"task_list",
|
|
458
|
-
"task_tree",
|
|
459
|
-
"task_search"
|
|
460
|
-
].map((t) => `mcp__${ROY_TASK_MCP_SERVER}__${t}`));
|
|
461
|
-
var CLAUDE_ROY_TOOLS = Object.freeze([
|
|
462
|
-
"Read",
|
|
463
|
-
"Write",
|
|
464
|
-
"Edit",
|
|
465
|
-
"Bash",
|
|
466
|
-
"Grep",
|
|
467
|
-
"Glob",
|
|
468
|
-
"TodoWrite",
|
|
469
|
-
"WebFetch",
|
|
470
|
-
"WebSearch",
|
|
471
|
-
...ROY_TASK_MCP_TOOLS
|
|
472
|
-
]);
|
|
473
|
-
var CLAUDE_ROY_SYSTEM_PROMPT = `# Roy — task-first-class-citizen agent
|
|
447
|
+
// ../agent/src/roy-prompt-core.ts
|
|
448
|
+
var ROY_PROMPT_COMMON = `# Roy — task-first-class-citizen agent
|
|
474
449
|
|
|
475
450
|
You are **Roy**, an agent whose defining rule is simple:
|
|
476
451
|
|
|
@@ -499,6 +474,30 @@ Create the task **before** doing the work. Pass:
|
|
|
499
474
|
- \`project_path\`, \`context\` (JSON string: worktree_path / branch / type …)
|
|
500
475
|
- \`parent_task_id\` when the tree said so
|
|
501
476
|
|
|
477
|
+
#### ⚠️ Goals must be numbered sub-items
|
|
478
|
+
\`goals_and_expected_deliverables\` **MUST** be written as **numbered
|
|
479
|
+
sub-items** (编号子项) that can each be verified **one by one**
|
|
480
|
+
(逐条验收) — for \`type=normal\` tasks:
|
|
481
|
+
|
|
482
|
+
1. 实现 X(含文件路径)
|
|
483
|
+
2. 测试 Y 覆盖 Z 场景(pass/fail 计数)
|
|
484
|
+
3. 文档 W 落盘到 <path>
|
|
485
|
+
|
|
486
|
+
Why: the completion hook judges the goal **item by item** against the
|
|
487
|
+
operation-record chain, using each sub-item as fine-grained evidence. A
|
|
488
|
+
vague one-line goal (一句话模糊目标,如「做好 X」「完成调研」) gives the
|
|
489
|
+
judge nothing to check and reliably triggers a completion hint.
|
|
490
|
+
|
|
491
|
+
Reference the four atomic assertion forms when writing each sub-item:
|
|
492
|
+
- **文件断言** (file assertion) — file exists / size / content grep
|
|
493
|
+
- **进程断言** (process assertion) — command exit code / stdout match
|
|
494
|
+
- **测试断言** (test assertion) — pass/fail counts, no NEW fail
|
|
495
|
+
- **状态断言** (state assertion) — version number / HTTP 200 / status field
|
|
496
|
+
|
|
497
|
+
For \`type=longterm\` / \`type=cycle\` tasks there is no per-item completion
|
|
498
|
+
acceptance — for those, split goals into staged, individually checkable
|
|
499
|
+
sub-items instead of one vague sentence.
|
|
500
|
+
|
|
502
501
|
The tool returns a **numeric task id** — that number is your handle for the
|
|
503
502
|
rest of the conversation.
|
|
504
503
|
|
|
@@ -533,12 +532,32 @@ transitions*: RED→GREEN, blocker found, decision made, review passed.
|
|
|
533
532
|
### 5. Close — \`task_complete\`
|
|
534
533
|
Before completing:
|
|
535
534
|
1. \`task_get(task_id)\` and re-read goal + delivery.
|
|
536
|
-
2.
|
|
537
|
-
|
|
535
|
+
2. **Accept the goal item-by-item** (按子项逐条验收): check **each** numbered
|
|
536
|
+
sub-item against the operation-record chain and the deliverable evidence.
|
|
537
|
+
If a sub-item is not met, either extend the task (\`task_update\`) or say
|
|
538
|
+
so honestly.
|
|
538
539
|
3. Call \`task_complete\` with \`deliverable_storage_path\` — a locator for the
|
|
539
540
|
work product: a file path, a git branch/worktree, a doc URL, a commit URL.
|
|
540
541
|
The path is persisted into the task context and as an operation record.
|
|
541
542
|
|
|
543
|
+
#### ⚠️ The completion hint is a verify duty, not a notification
|
|
544
|
+
If the task context carries a \`[TASK_COMPLETION_HINT]\`, you **MUST** verify
|
|
545
|
+
it (核实义务) — never ignore it (不允许无视). Re-read the task via
|
|
546
|
+
\`task_get\`, then do **exactly one** of:
|
|
547
|
+
- (a) the goal is **not** met → \`task_update(task_id=<id>, status=todo)\` to
|
|
548
|
+
reopen and continue the work; or
|
|
549
|
+
- (b) the goal **is** met → \`task_operation_create\`
|
|
550
|
+
(milestone_type=decision) stating why the sub-items are actually met
|
|
551
|
+
(说明理由 — "state why").
|
|
552
|
+
|
|
553
|
+
#### Several deliverables in one field
|
|
554
|
+
\`deliverable_storage_path\` may name **multiple paths**, space-separated
|
|
555
|
+
(\`/tmp/report.md /tmp/diff.patch\`) — the judge splits on whitespace as well
|
|
556
|
+
as \`,\` \`;\` \`、\` and \` + \`. A local path may also be written in its
|
|
557
|
+
\`file://\` form (\`file:///tmp/report.md\`) — the scheme is stripped and the
|
|
558
|
+
path kept. Declare **every** artefact you produced: a path left out reads as
|
|
559
|
+
"not produced" and drags the task toward a false completion hint.
|
|
560
|
+
|
|
542
561
|
\`task_complete\` cannot be used on \`cycle\` tasks (they are periodic by
|
|
543
562
|
design) — use \`task_update\` to reset those.
|
|
544
563
|
|
|
@@ -559,6 +578,13 @@ configured with \`persistence: "remote"\`). Do not create side databases, do
|
|
|
559
578
|
not maintain your own to-do list in files, and do not track work in chat only:
|
|
560
579
|
if it is not in the task store, it is not tracked.
|
|
561
580
|
|
|
581
|
+
## Style
|
|
582
|
+
|
|
583
|
+
- Be direct and concrete; the user is reading task titles and milestones.
|
|
584
|
+
- Report the task id and its state whenever you finish a step.
|
|
585
|
+
- If a task is blocked, mark it (\`task_update(status="paused")\`) and record a
|
|
586
|
+
\`problem\` milestone explaining the blocker instead of going silent.`;
|
|
587
|
+
var ROY_PROMPT_CLAUDE_TAIL = `
|
|
562
588
|
## Tool surface on Claude Code
|
|
563
589
|
|
|
564
590
|
The \`task_*\` tools are served by the **\`roy-task\` MCP server** this runtime
|
|
@@ -582,14 +608,35 @@ progress card renders the task these tools create.
|
|
|
582
608
|
Measured: it has no way to write a milestone (its \`operations <id>\` subcommand
|
|
583
609
|
is a *reader*), and it targets a different database than this server. Use the
|
|
584
610
|
MCP tools above; if they are genuinely unavailable, say so in your answer
|
|
585
|
-
instead of silently skipping the lifecycle
|
|
611
|
+
instead of silently skipping the lifecycle.`;
|
|
586
612
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
613
|
+
// ../../../packages/cli/src/daemon/runners/claude-agents.ts
|
|
614
|
+
var CLAUDE_ROY_AGENT_NAME = "roy";
|
|
615
|
+
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.";
|
|
616
|
+
var ROY_TASK_MCP_SERVER = "roy-task";
|
|
617
|
+
var ROY_TASK_MCP_TOOLS = Object.freeze([
|
|
618
|
+
"task_create",
|
|
619
|
+
"task_update",
|
|
620
|
+
"task_operation_create",
|
|
621
|
+
"task_complete",
|
|
622
|
+
"task_get",
|
|
623
|
+
"task_list",
|
|
624
|
+
"task_tree",
|
|
625
|
+
"task_search"
|
|
626
|
+
].map((t) => `mcp__${ROY_TASK_MCP_SERVER}__${t}`));
|
|
627
|
+
var CLAUDE_ROY_TOOLS = Object.freeze([
|
|
628
|
+
"Read",
|
|
629
|
+
"Write",
|
|
630
|
+
"Edit",
|
|
631
|
+
"Bash",
|
|
632
|
+
"Grep",
|
|
633
|
+
"Glob",
|
|
634
|
+
"TodoWrite",
|
|
635
|
+
"WebFetch",
|
|
636
|
+
"WebSearch",
|
|
637
|
+
...ROY_TASK_MCP_TOOLS
|
|
638
|
+
]);
|
|
639
|
+
var CLAUDE_ROY_SYSTEM_PROMPT = ROY_PROMPT_COMMON + ROY_PROMPT_CLAUDE_TAIL;
|
|
593
640
|
function buildClaudeAgentsJson(opts = {}) {
|
|
594
641
|
const def = {
|
|
595
642
|
description: opts.description ?? ROY_AGENT_DESCRIPTION,
|
|
@@ -961,13 +1008,17 @@ function resolveClaudePluginDir(roots, explicit) {
|
|
|
961
1008
|
}
|
|
962
1009
|
return;
|
|
963
1010
|
}
|
|
964
|
-
function buildRoyTaskMcpConfig(pluginDir) {
|
|
1011
|
+
function buildRoyTaskMcpConfig(pluginDir, env) {
|
|
1012
|
+
const server = {
|
|
1013
|
+
command: process.execPath,
|
|
1014
|
+
args: [join(pluginDir, "mcp", "roy-task-mcp-server.mjs")]
|
|
1015
|
+
};
|
|
1016
|
+
const clean = env ? Object.fromEntries(Object.entries(env).filter(([, v]) => v !== undefined && v !== "")) : {};
|
|
1017
|
+
if (Object.keys(clean).length > 0)
|
|
1018
|
+
server.env = clean;
|
|
965
1019
|
return JSON.stringify({
|
|
966
1020
|
mcpServers: {
|
|
967
|
-
"roy-task":
|
|
968
|
-
command: process.execPath,
|
|
969
|
-
args: [join(pluginDir, "mcp", "roy-task-mcp-server.mjs")]
|
|
970
|
-
}
|
|
1021
|
+
"roy-task": server
|
|
971
1022
|
}
|
|
972
1023
|
});
|
|
973
1024
|
}
|
|
@@ -995,8 +1046,8 @@ function makeClaudeRunner(opts = {}) {
|
|
|
995
1046
|
const agentsJson = opts.agentsJson ?? buildClaudeAgentsJson();
|
|
996
1047
|
const sessions = opts.sessions ?? createClaudeSessionRegistry();
|
|
997
1048
|
const pluginDir = opts.pluginDir ?? resolveClaudePluginDir([dirname(fileURLToPath(import.meta.url))]);
|
|
998
|
-
const mcpConfig = pluginDir ? buildRoyTaskMcpConfig(pluginDir) : undefined;
|
|
999
1049
|
return async function runAgent(input) {
|
|
1050
|
+
const mcpConfig = pluginDir ? buildRoyTaskMcpConfig(pluginDir, input.mcpEnv) : undefined;
|
|
1000
1051
|
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
1001
1052
|
const args = buildClaudeArgs({
|
|
1002
1053
|
prompt: input.prompt,
|
|
@@ -1105,7 +1156,7 @@ var CODEX_SANDBOX_MODES = Object.freeze([
|
|
|
1105
1156
|
function isCodexSandboxMode(value) {
|
|
1106
1157
|
return CODEX_SANDBOX_MODES.includes(value);
|
|
1107
1158
|
}
|
|
1108
|
-
function buildRoyTaskCodexConfigArgs(pluginDir) {
|
|
1159
|
+
function buildRoyTaskCodexConfigArgs(pluginDir, env) {
|
|
1109
1160
|
const args = [
|
|
1110
1161
|
`-cmcp_servers.${ROY_TASK_MCP_SERVER}.command=${JSON.stringify(process.execPath)}`,
|
|
1111
1162
|
`-cmcp_servers.${ROY_TASK_MCP_SERVER}.args=${JSON.stringify([
|
|
@@ -1113,12 +1164,12 @@ function buildRoyTaskCodexConfigArgs(pluginDir) {
|
|
|
1113
1164
|
])}`,
|
|
1114
1165
|
`-cmcp_servers.${ROY_TASK_MCP_SERVER}.default_tools_approval_mode="approve"`
|
|
1115
1166
|
];
|
|
1116
|
-
const envArg = buildMcpServerEnvArg();
|
|
1167
|
+
const envArg = buildMcpServerEnvArg(env);
|
|
1117
1168
|
if (envArg)
|
|
1118
1169
|
args.push(envArg);
|
|
1119
1170
|
return args;
|
|
1120
1171
|
}
|
|
1121
|
-
function buildMcpServerEnvArg() {
|
|
1172
|
+
function buildMcpServerEnvArg(env) {
|
|
1122
1173
|
const keys = [
|
|
1123
1174
|
"ROY_TASK_NOTIFY_ENABLED",
|
|
1124
1175
|
"ROY_TASK_NOTIFY_CHAT_ID",
|
|
@@ -1126,7 +1177,8 @@ function buildMcpServerEnvArg() {
|
|
|
1126
1177
|
];
|
|
1127
1178
|
const pairs = [];
|
|
1128
1179
|
for (const key of keys) {
|
|
1129
|
-
const
|
|
1180
|
+
const fromEnv = env ? env[key] : undefined;
|
|
1181
|
+
const value = fromEnv ?? (env ? undefined : process.env[key]);
|
|
1130
1182
|
if (!value)
|
|
1131
1183
|
continue;
|
|
1132
1184
|
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
@@ -1166,8 +1218,8 @@ function makeCodexRunner(opts = {}) {
|
|
|
1166
1218
|
const sandbox = opts.sandbox ?? "read-only";
|
|
1167
1219
|
const sessions = opts.sessions ?? createCodexSessionRegistry();
|
|
1168
1220
|
const pluginDir = opts.pluginDir ?? resolveClaudePluginDir([dirname2(fileURLToPath2(import.meta.url))]);
|
|
1169
|
-
const mcpConfigArgs = pluginDir ? buildRoyTaskCodexConfigArgs(pluginDir) : undefined;
|
|
1170
1221
|
return async function runAgent(input) {
|
|
1222
|
+
const mcpConfigArgs = pluginDir ? buildRoyTaskCodexConfigArgs(pluginDir, input.mcpEnv) : undefined;
|
|
1171
1223
|
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
1172
1224
|
const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox, mcpConfigArgs }) : buildCodexArgs({ prompt: input.prompt, model, sandbox, mcpConfigArgs });
|
|
1173
1225
|
const decoder = new CodexStreamDecoder;
|
|
@@ -1754,6 +1806,94 @@ function withTimeout(op, promise, ms) {
|
|
|
1754
1806
|
});
|
|
1755
1807
|
});
|
|
1756
1808
|
}
|
|
1809
|
+
var ledgerStores = new Map;
|
|
1810
|
+
var ledgerInFlight;
|
|
1811
|
+
function ledgerPathKey() {
|
|
1812
|
+
return process.env.ROY_AGENT_LARK_CARD_DB ?? "";
|
|
1813
|
+
}
|
|
1814
|
+
async function loadLedger() {
|
|
1815
|
+
const key = ledgerPathKey();
|
|
1816
|
+
if (ledgerStores.has(key))
|
|
1817
|
+
return ledgerStores.get(key);
|
|
1818
|
+
if (!ledgerInFlight) {
|
|
1819
|
+
ledgerInFlight = (async () => {
|
|
1820
|
+
try {
|
|
1821
|
+
const mod = await import("@ai-setting/roy-agent-core/env/task/plugins");
|
|
1822
|
+
if (typeof mod.createLarkCardStateStore !== "function") {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const store2 = mod.createLarkCardStateStore();
|
|
1826
|
+
ledgerStores.set(key, store2);
|
|
1827
|
+
return store2;
|
|
1828
|
+
} catch {
|
|
1829
|
+
return;
|
|
1830
|
+
} finally {
|
|
1831
|
+
ledgerInFlight = undefined;
|
|
1832
|
+
}
|
|
1833
|
+
})();
|
|
1834
|
+
}
|
|
1835
|
+
const store = await ledgerInFlight;
|
|
1836
|
+
ledgerStores.set(key, store);
|
|
1837
|
+
return store;
|
|
1838
|
+
}
|
|
1839
|
+
function taskIdOf(meta) {
|
|
1840
|
+
const id = meta?.id;
|
|
1841
|
+
return typeof id === "number" ? id : undefined;
|
|
1842
|
+
}
|
|
1843
|
+
async function adoptExistingCard(input) {
|
|
1844
|
+
const taskId = taskIdOf(input.meta);
|
|
1845
|
+
if (taskId === undefined)
|
|
1846
|
+
return;
|
|
1847
|
+
try {
|
|
1848
|
+
const store = await loadLedger();
|
|
1849
|
+
if (!store)
|
|
1850
|
+
return;
|
|
1851
|
+
const record = store.get(taskId);
|
|
1852
|
+
if (!record || !record.messageId)
|
|
1853
|
+
return;
|
|
1854
|
+
if (record.status === "closed")
|
|
1855
|
+
return;
|
|
1856
|
+
if (record.chatId !== input.chatId)
|
|
1857
|
+
return;
|
|
1858
|
+
return { messageId: record.messageId, pinned: record.pinned };
|
|
1859
|
+
} catch {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function recordCardInLedger(input, res) {
|
|
1864
|
+
const taskId = taskIdOf(input.meta);
|
|
1865
|
+
if (taskId === undefined || !res.messageId)
|
|
1866
|
+
return;
|
|
1867
|
+
try {
|
|
1868
|
+
const store = await loadLedger();
|
|
1869
|
+
if (!store)
|
|
1870
|
+
return;
|
|
1871
|
+
const existing = store.get(taskId);
|
|
1872
|
+
const now = Date.now();
|
|
1873
|
+
store.upsert({
|
|
1874
|
+
taskId,
|
|
1875
|
+
messageId: res.messageId,
|
|
1876
|
+
chatId: input.chatId,
|
|
1877
|
+
profile: input.profile,
|
|
1878
|
+
pinned: res.pinned,
|
|
1879
|
+
status: "open",
|
|
1880
|
+
createdAt: existing?.createdAt ?? now,
|
|
1881
|
+
updatedAt: now
|
|
1882
|
+
});
|
|
1883
|
+
} catch {}
|
|
1884
|
+
}
|
|
1885
|
+
async function closeCardInLedger(input) {
|
|
1886
|
+
const taskId = taskIdOf(input.meta);
|
|
1887
|
+
if (taskId === undefined)
|
|
1888
|
+
return;
|
|
1889
|
+
try {
|
|
1890
|
+
const store = await loadLedger();
|
|
1891
|
+
if (!store)
|
|
1892
|
+
return;
|
|
1893
|
+
store.markClosed(taskId);
|
|
1894
|
+
store.markPinned(taskId, false);
|
|
1895
|
+
} catch {}
|
|
1896
|
+
}
|
|
1757
1897
|
function createRoyAgentCardPort(options = {}) {
|
|
1758
1898
|
const opTimeoutMs = options.opTimeoutMs ?? CARD_OP_TIMEOUT_MS;
|
|
1759
1899
|
let notifier;
|
|
@@ -1783,13 +1923,18 @@ function createRoyAgentCardPort(options = {}) {
|
|
|
1783
1923
|
return {
|
|
1784
1924
|
enabled: true,
|
|
1785
1925
|
async start(input) {
|
|
1926
|
+
const adopted = await adoptExistingCard(input);
|
|
1927
|
+
if (adopted)
|
|
1928
|
+
return adopted;
|
|
1786
1929
|
const n = await withTimeout("load", getNotifier(), opTimeoutMs);
|
|
1787
|
-
|
|
1930
|
+
const res = await withTimeout("start", n.startTask({
|
|
1788
1931
|
chatId: input.chatId,
|
|
1789
1932
|
profile: input.profile,
|
|
1790
1933
|
meta: input.meta,
|
|
1791
1934
|
milestones: input.milestones
|
|
1792
1935
|
}), opTimeoutMs);
|
|
1936
|
+
await recordCardInLedger(input, res);
|
|
1937
|
+
return res;
|
|
1793
1938
|
},
|
|
1794
1939
|
async update(input) {
|
|
1795
1940
|
const n = await withTimeout("load", getNotifier(), opTimeoutMs);
|
|
@@ -1815,6 +1960,7 @@ function createRoyAgentCardPort(options = {}) {
|
|
|
1815
1960
|
finalStep: input.finalStep,
|
|
1816
1961
|
progress: input.progress
|
|
1817
1962
|
}), opTimeoutMs);
|
|
1963
|
+
await closeCardInLedger(input);
|
|
1818
1964
|
}
|
|
1819
1965
|
};
|
|
1820
1966
|
}
|
|
@@ -1875,6 +2021,52 @@ function normaliseMcpToolName(tool) {
|
|
|
1875
2021
|
return tool;
|
|
1876
2022
|
}
|
|
1877
2023
|
|
|
2024
|
+
// ../../../packages/cli/src/daemon/mcp-notify.ts
|
|
2025
|
+
var MCP_NOTIFY_ENV_KEYS = [
|
|
2026
|
+
"ROY_TASK_NOTIFY_ENABLED",
|
|
2027
|
+
"ROY_TASK_NOTIFY_CHAT_ID",
|
|
2028
|
+
"ROY_TASK_NOTIFY_PROFILE"
|
|
2029
|
+
];
|
|
2030
|
+
function buildMcpNotifyEnv(destination, options = {}) {
|
|
2031
|
+
const env = {};
|
|
2032
|
+
if (!destination.chatId)
|
|
2033
|
+
return env;
|
|
2034
|
+
const enabled = options.enabled ?? true;
|
|
2035
|
+
if (enabled)
|
|
2036
|
+
env["ROY_TASK_NOTIFY_ENABLED"] = "1";
|
|
2037
|
+
env["ROY_TASK_NOTIFY_CHAT_ID"] = destination.chatId;
|
|
2038
|
+
if (destination.profile)
|
|
2039
|
+
env["ROY_TASK_NOTIFY_PROFILE"] = destination.profile;
|
|
2040
|
+
return env;
|
|
2041
|
+
}
|
|
2042
|
+
function notifyEnvFromDispatch(plan, options = {}) {
|
|
2043
|
+
return buildMcpNotifyEnv({ chatId: plan.chatId, profile: plan.profile }, options);
|
|
2044
|
+
}
|
|
2045
|
+
function mergeNotifyEnv(base, notify) {
|
|
2046
|
+
const merged = { ...base };
|
|
2047
|
+
for (const key of MCP_NOTIFY_ENV_KEYS) {
|
|
2048
|
+
if (notify[key] !== undefined)
|
|
2049
|
+
merged[key] = notify[key];
|
|
2050
|
+
else
|
|
2051
|
+
delete merged[key];
|
|
2052
|
+
}
|
|
2053
|
+
if (merged["ROY_TASK_NOTIFY_PROFILE"] === undefined) {
|
|
2054
|
+
const profile = base["ROY_TASK_NOTIFY_PROFILE"];
|
|
2055
|
+
if (profile !== undefined)
|
|
2056
|
+
merged["ROY_TASK_NOTIFY_PROFILE"] = profile;
|
|
2057
|
+
}
|
|
2058
|
+
return merged;
|
|
2059
|
+
}
|
|
2060
|
+
function processNotifyEnv() {
|
|
2061
|
+
const env = {};
|
|
2062
|
+
for (const key of MCP_NOTIFY_ENV_KEYS) {
|
|
2063
|
+
const value = process.env[key];
|
|
2064
|
+
if (value)
|
|
2065
|
+
env[key] = value;
|
|
2066
|
+
}
|
|
2067
|
+
return env;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1878
2070
|
// ../../../packages/cli/src/daemon/dispatcher.ts
|
|
1879
2071
|
function cardReport(handle) {
|
|
1880
2072
|
const out = {};
|
|
@@ -1950,10 +2142,12 @@ class Dispatcher {
|
|
|
1950
2142
|
deps;
|
|
1951
2143
|
opts;
|
|
1952
2144
|
deduper;
|
|
2145
|
+
notifyOptions;
|
|
1953
2146
|
constructor(deps, opts = {}) {
|
|
1954
2147
|
this.deps = deps;
|
|
1955
2148
|
this.opts = opts;
|
|
1956
2149
|
this.deduper = deps.deduper ?? new Deduper;
|
|
2150
|
+
this.notifyOptions = opts.notify ?? {};
|
|
1957
2151
|
}
|
|
1958
2152
|
get dedup() {
|
|
1959
2153
|
return this.deduper;
|
|
@@ -2001,7 +2195,8 @@ class Dispatcher {
|
|
|
2001
2195
|
prompt: plan.prompt,
|
|
2002
2196
|
agent: plan.agent,
|
|
2003
2197
|
sessionId: plan.sessionId,
|
|
2004
|
-
onProgress
|
|
2198
|
+
onProgress,
|
|
2199
|
+
mcpEnv: mergeNotifyEnv(processNotifyEnv(), notifyEnvFromDispatch({ chatId, profile }, this.notifyOptions))
|
|
2005
2200
|
}), (r) => r.error);
|
|
2006
2201
|
const card = earlyCard ? await earlyCard : await this.trace("dispatch.openCard", () => this.openCard({
|
|
2007
2202
|
chatId,
|
|
@@ -2606,7 +2801,7 @@ function makeCursorRunner(opts = {}) {
|
|
|
2606
2801
|
let workspaceDir = opts.workspaceDir;
|
|
2607
2802
|
if (!workspaceDir && pluginDir) {
|
|
2608
2803
|
try {
|
|
2609
|
-
workspaceDir = createCursorWorkspace(pluginDir,
|
|
2804
|
+
workspaceDir = createCursorWorkspace(pluginDir, collectMcpNotifyEnv());
|
|
2610
2805
|
} catch {
|
|
2611
2806
|
workspaceDir = undefined;
|
|
2612
2807
|
}
|
|
@@ -2617,18 +2812,18 @@ function makeCursorRunner(opts = {}) {
|
|
|
2617
2812
|
process.once("SIGINT", cleanup);
|
|
2618
2813
|
process.once("SIGTERM", cleanup);
|
|
2619
2814
|
}
|
|
2620
|
-
const refreshMcpConfig = () => {
|
|
2815
|
+
const refreshMcpConfig = (env) => {
|
|
2621
2816
|
if (!workspaceDir || !pluginDir)
|
|
2622
2817
|
return;
|
|
2623
2818
|
if (opts.workspaceDir)
|
|
2624
2819
|
return;
|
|
2625
2820
|
try {
|
|
2626
|
-
writeCursorMcpConfig(workspaceDir, pluginDir, opts.mcpEnv ?? collectMcpNotifyEnv());
|
|
2821
|
+
writeCursorMcpConfig(workspaceDir, pluginDir, env ?? opts.mcpEnv ?? collectMcpNotifyEnv());
|
|
2627
2822
|
} catch {}
|
|
2628
2823
|
};
|
|
2629
2824
|
const addDirs = opts.addDirs ?? [process.cwd()];
|
|
2630
2825
|
return async function runAgent(input) {
|
|
2631
|
-
refreshMcpConfig();
|
|
2826
|
+
refreshMcpConfig(input.mcpEnv);
|
|
2632
2827
|
const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
|
|
2633
2828
|
const args = buildCursorArgs({
|
|
2634
2829
|
prompt: input.prompt,
|
|
@@ -3270,7 +3465,7 @@ function formatStatus(report) {
|
|
|
3270
3465
|
}
|
|
3271
3466
|
|
|
3272
3467
|
// ../../../packages/cli/src/main.ts
|
|
3273
|
-
var VERSION = "0.7.
|
|
3468
|
+
var VERSION = "0.7.8";
|
|
3274
3469
|
async function main(argv) {
|
|
3275
3470
|
const parsed = parseArgs(argv);
|
|
3276
3471
|
switch (parsed.command) {
|