@ai-setting/roy-task-plugin 0.7.5 → 0.7.7

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.
@@ -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
 
@@ -46,13 +70,20 @@ Report progress as you go, not only at the end:
46
70
  If the scope changes, `task_update` the description/goals instead of silently
47
71
  doing something else.
48
72
 
49
- ### 4. Record milestones — `task_operation_create`
73
+ ### 4. Record milestones — `task_operation_create` — **as you work**
74
+ Call this tool the moment a stage is reached — **not after the fact**,
75
+ not in a batch at the end. Finished a chunk of code? A test just went
76
+ green? Hit a blocker? Made a decision? Record it **immediately**, then
77
+ keep working. The record is the running log of the work, written while
78
+ it happens.
79
+
50
80
  This tool has **two orthogonal dimensions** and you must keep them distinct:
51
81
  - **state / milestone dimension** — `milestone_type` + `milestone_title` +
52
82
  `milestone_description`: *what* was reached (e.g. "GREEN: all tests pass").
53
83
  - **process dimension** — `process_description`: *how* you got there —
54
- strategy + tool steps, strictly **100–150 characters**.
55
- Too short = rejected (no value); too long = rejected (use the session log).
84
+ the operations just performed and why, written in the moment,
85
+ **100–500 characters**. Too short = rejected (no value);
86
+ too long = rejected (that is a log, not a summary).
56
87
 
57
88
  Rules: do **not** use the process field to restate the milestone, and do not
58
89
  use milestones as a substitute for progress. Milestones mark *state
@@ -61,12 +92,32 @@ transitions*: RED→GREEN, blocker found, decision made, review passed.
61
92
  ### 5. Close — `task_complete`
62
93
  Before completing:
63
94
  1. `task_get(task_id)` and re-read goal + delivery.
64
- 2. Verify the goal and delivery actually match what you delivered. If they do
65
- not, either extend the task (`task_update`) or say so honestly.
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.
66
99
  3. Call `task_complete` with `deliverable_storage_path` — a locator for the
67
100
  work product: a file path, a git branch/worktree, a doc URL, a commit URL.
68
101
  The path is persisted into the task context and as an operation record.
69
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
+
70
121
  `task_complete` cannot be used on `cycle` tasks (they are periodic by
71
122
  design) — use `task_update` to reset those.
72
123
 
@@ -87,6 +138,12 @@ configured with `persistence: "remote"`). Do not create side databases, do
87
138
  not maintain your own to-do list in files, and do not track work in chat only:
88
139
  if it is not in the task store, it is not tracked.
89
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.
90
147
  ## Tool surface on Claude Code
91
148
 
92
149
  The `task_*` tools are served by the **`roy-task` MCP server** this runtime
@@ -111,10 +168,3 @@ Measured: it has no way to write a milestone (its `operations <id>` subcommand
111
168
  is a *reader*), and it targets a different database than this server. Use the
112
169
  MCP tools above; if they are genuinely unavailable, say so in your answer
113
170
  instead of silently skipping the lifecycle.
114
-
115
- ## Style
116
-
117
- - Be direct and concrete; the user is reading task titles and milestones.
118
- - Report the task id and its state whenever you finish a step.
119
- - If a task is blocked, mark it (`task_update(status="paused")`) and record a
120
- `problem` milestone explaining the blocker instead of going silent.
@@ -32,7 +32,7 @@ If you do have the MCP server, call `task_operation_create` with:
32
32
  `solution`, `decision`, `review`, `completed`
33
33
  - `milestone_title` — one line
34
34
  - `milestone_description` — what was reached
35
- - `process_description` — **how** you got there, strictly 100–150 characters
35
+ - `process_description` — **how** you got there, 100–500 characters
36
36
  (this is enforced; too short or too long is rejected)
37
37
 
38
38
  Keep the two dimensions distinct: the milestone says *what* was reached, the
package/dist/cli.js CHANGED
@@ -444,33 +444,8 @@ class NdjsonAssembler {
444
444
  }
445
445
  }
446
446
 
447
- // ../../../packages/cli/src/daemon/runners/claude-agents.ts
448
- var CLAUDE_ROY_AGENT_NAME = "roy";
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
 
@@ -511,13 +510,20 @@ Report progress as you go, not only at the end:
511
510
  If the scope changes, \`task_update\` the description/goals instead of silently
512
511
  doing something else.
513
512
 
514
- ### 4. Record milestones — \`task_operation_create\`
513
+ ### 4. Record milestones — \`task_operation_create\` — **as you work**
514
+ Call this tool the moment a stage is reached — **not after the fact**,
515
+ not in a batch at the end. Finished a chunk of code? A test just went
516
+ green? Hit a blocker? Made a decision? Record it **immediately**, then
517
+ keep working. The record is the running log of the work, written while
518
+ it happens.
519
+
515
520
  This tool has **two orthogonal dimensions** and you must keep them distinct:
516
521
  - **state / milestone dimension** — \`milestone_type\` + \`milestone_title\` +
517
522
  \`milestone_description\`: *what* was reached (e.g. "GREEN: all tests pass").
518
523
  - **process dimension** — \`process_description\`: *how* you got there —
519
- strategy + tool steps, strictly **100–150 characters**.
520
- Too short = rejected (no value); too long = rejected (use the session log).
524
+ the operations just performed and why, written in the moment,
525
+ **100–500 characters**. Too short = rejected (no value);
526
+ too long = rejected (that is a log, not a summary).
521
527
 
522
528
  Rules: do **not** use the process field to restate the milestone, and do not
523
529
  use milestones as a substitute for progress. Milestones mark *state
@@ -526,12 +532,32 @@ transitions*: RED→GREEN, blocker found, decision made, review passed.
526
532
  ### 5. Close — \`task_complete\`
527
533
  Before completing:
528
534
  1. \`task_get(task_id)\` and re-read goal + delivery.
529
- 2. Verify the goal and delivery actually match what you delivered. If they do
530
- not, either extend the task (\`task_update\`) or say so honestly.
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.
531
539
  3. Call \`task_complete\` with \`deliverable_storage_path\` — a locator for the
532
540
  work product: a file path, a git branch/worktree, a doc URL, a commit URL.
533
541
  The path is persisted into the task context and as an operation record.
534
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
+
535
561
  \`task_complete\` cannot be used on \`cycle\` tasks (they are periodic by
536
562
  design) — use \`task_update\` to reset those.
537
563
 
@@ -552,6 +578,13 @@ configured with \`persistence: "remote"\`). Do not create side databases, do
552
578
  not maintain your own to-do list in files, and do not track work in chat only:
553
579
  if it is not in the task store, it is not tracked.
554
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 = `
555
588
  ## Tool surface on Claude Code
556
589
 
557
590
  The \`task_*\` tools are served by the **\`roy-task\` MCP server** this runtime
@@ -575,14 +608,35 @@ progress card renders the task these tools create.
575
608
  Measured: it has no way to write a milestone (its \`operations <id>\` subcommand
576
609
  is a *reader*), and it targets a different database than this server. Use the
577
610
  MCP tools above; if they are genuinely unavailable, say so in your answer
578
- instead of silently skipping the lifecycle.
611
+ instead of silently skipping the lifecycle.`;
579
612
 
580
- ## Style
581
-
582
- - Be direct and concrete; the user is reading task titles and milestones.
583
- - Report the task id and its state whenever you finish a step.
584
- - If a task is blocked, mark it (\`task_update(status="paused")\`) and record a
585
- \`problem\` milestone explaining the blocker instead of going silent.`;
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;
586
640
  function buildClaudeAgentsJson(opts = {}) {
587
641
  const def = {
588
642
  description: opts.description ?? ROY_AGENT_DESCRIPTION,
@@ -954,13 +1008,17 @@ function resolveClaudePluginDir(roots, explicit) {
954
1008
  }
955
1009
  return;
956
1010
  }
957
- 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;
958
1019
  return JSON.stringify({
959
1020
  mcpServers: {
960
- "roy-task": {
961
- command: process.execPath,
962
- args: [join(pluginDir, "mcp", "roy-task-mcp-server.mjs")]
963
- }
1021
+ "roy-task": server
964
1022
  }
965
1023
  });
966
1024
  }
@@ -988,8 +1046,8 @@ function makeClaudeRunner(opts = {}) {
988
1046
  const agentsJson = opts.agentsJson ?? buildClaudeAgentsJson();
989
1047
  const sessions = opts.sessions ?? createClaudeSessionRegistry();
990
1048
  const pluginDir = opts.pluginDir ?? resolveClaudePluginDir([dirname(fileURLToPath(import.meta.url))]);
991
- const mcpConfig = pluginDir ? buildRoyTaskMcpConfig(pluginDir) : undefined;
992
1049
  return async function runAgent(input) {
1050
+ const mcpConfig = pluginDir ? buildRoyTaskMcpConfig(pluginDir, input.mcpEnv) : undefined;
993
1051
  const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
994
1052
  const args = buildClaudeArgs({
995
1053
  prompt: input.prompt,
@@ -1098,7 +1156,7 @@ var CODEX_SANDBOX_MODES = Object.freeze([
1098
1156
  function isCodexSandboxMode(value) {
1099
1157
  return CODEX_SANDBOX_MODES.includes(value);
1100
1158
  }
1101
- function buildRoyTaskCodexConfigArgs(pluginDir) {
1159
+ function buildRoyTaskCodexConfigArgs(pluginDir, env) {
1102
1160
  const args = [
1103
1161
  `-cmcp_servers.${ROY_TASK_MCP_SERVER}.command=${JSON.stringify(process.execPath)}`,
1104
1162
  `-cmcp_servers.${ROY_TASK_MCP_SERVER}.args=${JSON.stringify([
@@ -1106,12 +1164,12 @@ function buildRoyTaskCodexConfigArgs(pluginDir) {
1106
1164
  ])}`,
1107
1165
  `-cmcp_servers.${ROY_TASK_MCP_SERVER}.default_tools_approval_mode="approve"`
1108
1166
  ];
1109
- const envArg = buildMcpServerEnvArg();
1167
+ const envArg = buildMcpServerEnvArg(env);
1110
1168
  if (envArg)
1111
1169
  args.push(envArg);
1112
1170
  return args;
1113
1171
  }
1114
- function buildMcpServerEnvArg() {
1172
+ function buildMcpServerEnvArg(env) {
1115
1173
  const keys = [
1116
1174
  "ROY_TASK_NOTIFY_ENABLED",
1117
1175
  "ROY_TASK_NOTIFY_CHAT_ID",
@@ -1119,7 +1177,8 @@ function buildMcpServerEnvArg() {
1119
1177
  ];
1120
1178
  const pairs = [];
1121
1179
  for (const key of keys) {
1122
- const value = process.env[key];
1180
+ const fromEnv = env ? env[key] : undefined;
1181
+ const value = fromEnv ?? (env ? undefined : process.env[key]);
1123
1182
  if (!value)
1124
1183
  continue;
1125
1184
  const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
@@ -1159,8 +1218,8 @@ function makeCodexRunner(opts = {}) {
1159
1218
  const sandbox = opts.sandbox ?? "read-only";
1160
1219
  const sessions = opts.sessions ?? createCodexSessionRegistry();
1161
1220
  const pluginDir = opts.pluginDir ?? resolveClaudePluginDir([dirname2(fileURLToPath2(import.meta.url))]);
1162
- const mcpConfigArgs = pluginDir ? buildRoyTaskCodexConfigArgs(pluginDir) : undefined;
1163
1221
  return async function runAgent(input) {
1222
+ const mcpConfigArgs = pluginDir ? buildRoyTaskCodexConfigArgs(pluginDir, input.mcpEnv) : undefined;
1164
1223
  const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
1165
1224
  const args = known ? buildCodexResumeArgs(known, { prompt: input.prompt, model, sandbox, mcpConfigArgs }) : buildCodexArgs({ prompt: input.prompt, model, sandbox, mcpConfigArgs });
1166
1225
  const decoder = new CodexStreamDecoder;
@@ -1747,6 +1806,92 @@ function withTimeout(op, promise, ms) {
1747
1806
  });
1748
1807
  });
1749
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
+ return { messageId: record.messageId, pinned: record.pinned };
1857
+ } catch {
1858
+ return;
1859
+ }
1860
+ }
1861
+ async function recordCardInLedger(input, res) {
1862
+ const taskId = taskIdOf(input.meta);
1863
+ if (taskId === undefined || !res.messageId)
1864
+ return;
1865
+ try {
1866
+ const store = await loadLedger();
1867
+ if (!store)
1868
+ return;
1869
+ const existing = store.get(taskId);
1870
+ const now = Date.now();
1871
+ store.upsert({
1872
+ taskId,
1873
+ messageId: res.messageId,
1874
+ chatId: input.chatId,
1875
+ profile: input.profile,
1876
+ pinned: res.pinned,
1877
+ status: "open",
1878
+ createdAt: existing?.createdAt ?? now,
1879
+ updatedAt: now
1880
+ });
1881
+ } catch {}
1882
+ }
1883
+ async function closeCardInLedger(input) {
1884
+ const taskId = taskIdOf(input.meta);
1885
+ if (taskId === undefined)
1886
+ return;
1887
+ try {
1888
+ const store = await loadLedger();
1889
+ if (!store)
1890
+ return;
1891
+ store.markClosed(taskId);
1892
+ store.markPinned(taskId, false);
1893
+ } catch {}
1894
+ }
1750
1895
  function createRoyAgentCardPort(options = {}) {
1751
1896
  const opTimeoutMs = options.opTimeoutMs ?? CARD_OP_TIMEOUT_MS;
1752
1897
  let notifier;
@@ -1776,13 +1921,18 @@ function createRoyAgentCardPort(options = {}) {
1776
1921
  return {
1777
1922
  enabled: true,
1778
1923
  async start(input) {
1924
+ const adopted = await adoptExistingCard(input);
1925
+ if (adopted)
1926
+ return adopted;
1779
1927
  const n = await withTimeout("load", getNotifier(), opTimeoutMs);
1780
- return withTimeout("start", n.startTask({
1928
+ const res = await withTimeout("start", n.startTask({
1781
1929
  chatId: input.chatId,
1782
1930
  profile: input.profile,
1783
1931
  meta: input.meta,
1784
1932
  milestones: input.milestones
1785
1933
  }), opTimeoutMs);
1934
+ await recordCardInLedger(input, res);
1935
+ return res;
1786
1936
  },
1787
1937
  async update(input) {
1788
1938
  const n = await withTimeout("load", getNotifier(), opTimeoutMs);
@@ -1808,6 +1958,7 @@ function createRoyAgentCardPort(options = {}) {
1808
1958
  finalStep: input.finalStep,
1809
1959
  progress: input.progress
1810
1960
  }), opTimeoutMs);
1961
+ await closeCardInLedger(input);
1811
1962
  }
1812
1963
  };
1813
1964
  }
@@ -1868,6 +2019,23 @@ function normaliseMcpToolName(tool) {
1868
2019
  return tool;
1869
2020
  }
1870
2021
 
2022
+ // ../../../packages/cli/src/daemon/mcp-notify.ts
2023
+ function buildMcpNotifyEnv(destination, options = {}) {
2024
+ const env = {};
2025
+ if (!destination.chatId)
2026
+ return env;
2027
+ const enabled = options.enabled ?? true;
2028
+ if (enabled)
2029
+ env["ROY_TASK_NOTIFY_ENABLED"] = "1";
2030
+ env["ROY_TASK_NOTIFY_CHAT_ID"] = destination.chatId;
2031
+ if (destination.profile)
2032
+ env["ROY_TASK_NOTIFY_PROFILE"] = destination.profile;
2033
+ return env;
2034
+ }
2035
+ function notifyEnvFromDispatch(plan, options = {}) {
2036
+ return buildMcpNotifyEnv({ chatId: plan.chatId, profile: plan.profile }, options);
2037
+ }
2038
+
1871
2039
  // ../../../packages/cli/src/daemon/dispatcher.ts
1872
2040
  function cardReport(handle) {
1873
2041
  const out = {};
@@ -1943,10 +2111,12 @@ class Dispatcher {
1943
2111
  deps;
1944
2112
  opts;
1945
2113
  deduper;
2114
+ notifyOptions;
1946
2115
  constructor(deps, opts = {}) {
1947
2116
  this.deps = deps;
1948
2117
  this.opts = opts;
1949
2118
  this.deduper = deps.deduper ?? new Deduper;
2119
+ this.notifyOptions = opts.notify ?? {};
1950
2120
  }
1951
2121
  get dedup() {
1952
2122
  return this.deduper;
@@ -1994,7 +2164,8 @@ class Dispatcher {
1994
2164
  prompt: plan.prompt,
1995
2165
  agent: plan.agent,
1996
2166
  sessionId: plan.sessionId,
1997
- onProgress
2167
+ onProgress,
2168
+ mcpEnv: notifyEnvFromDispatch({ chatId, profile }, this.notifyOptions)
1998
2169
  }), (r) => r.error);
1999
2170
  const card = earlyCard ? await earlyCard : await this.trace("dispatch.openCard", () => this.openCard({
2000
2171
  chatId,
@@ -2599,7 +2770,7 @@ function makeCursorRunner(opts = {}) {
2599
2770
  let workspaceDir = opts.workspaceDir;
2600
2771
  if (!workspaceDir && pluginDir) {
2601
2772
  try {
2602
- workspaceDir = createCursorWorkspace(pluginDir, opts.mcpEnv ?? collectMcpNotifyEnv());
2773
+ workspaceDir = createCursorWorkspace(pluginDir, collectMcpNotifyEnv());
2603
2774
  } catch {
2604
2775
  workspaceDir = undefined;
2605
2776
  }
@@ -2610,18 +2781,18 @@ function makeCursorRunner(opts = {}) {
2610
2781
  process.once("SIGINT", cleanup);
2611
2782
  process.once("SIGTERM", cleanup);
2612
2783
  }
2613
- const refreshMcpConfig = () => {
2784
+ const refreshMcpConfig = (env) => {
2614
2785
  if (!workspaceDir || !pluginDir)
2615
2786
  return;
2616
2787
  if (opts.workspaceDir)
2617
2788
  return;
2618
2789
  try {
2619
- writeCursorMcpConfig(workspaceDir, pluginDir, opts.mcpEnv ?? collectMcpNotifyEnv());
2790
+ writeCursorMcpConfig(workspaceDir, pluginDir, env ?? opts.mcpEnv ?? collectMcpNotifyEnv());
2620
2791
  } catch {}
2621
2792
  };
2622
2793
  const addDirs = opts.addDirs ?? [process.cwd()];
2623
2794
  return async function runAgent(input) {
2624
- refreshMcpConfig();
2795
+ refreshMcpConfig(input.mcpEnv);
2625
2796
  const known = input.sessionId ? sessions.get(input.sessionId) : undefined;
2626
2797
  const args = buildCursorArgs({
2627
2798
  prompt: input.prompt,
@@ -3263,7 +3434,7 @@ function formatStatus(report) {
3263
3434
  }
3264
3435
 
3265
3436
  // ../../../packages/cli/src/main.ts
3266
- var VERSION = "0.7.5";
3437
+ var VERSION = "0.7.7";
3267
3438
  async function main(argv) {
3268
3439
  const parsed = parseArgs(argv);
3269
3440
  switch (parsed.command) {