@ai-setting/roy-task-plugin 0.7.13 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/dist/claude-plugin/agents/roy.md +141 -195
- package/dist/claude-plugin/mcp/bridge.mjs +33 -3
- package/dist/claude-plugin/mcp/goal-check.mjs +47 -11
- package/dist/cli.js +318 -165
- package/dist/index.js +702 -399
- package/dist/index.js.map +30 -29
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
3
17
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
18
|
|
|
19
|
+
// ../../../packages/cli/src/daemon/card-action-ports.ts
|
|
20
|
+
var exports_card_action_ports = {};
|
|
21
|
+
__export(exports_card_action_ports, {
|
|
22
|
+
toCoreTaskShape: () => toCoreTaskShape,
|
|
23
|
+
createTaskComponentAdapter: () => createTaskComponentAdapter
|
|
24
|
+
});
|
|
25
|
+
function toCoreTaskShape(task) {
|
|
26
|
+
const roy = task.roy ?? {};
|
|
27
|
+
const str = (v) => v === undefined || v === null ? undefined : String(v);
|
|
28
|
+
const pick = (key) => {
|
|
29
|
+
const top = task[key];
|
|
30
|
+
if (top !== undefined && top !== null)
|
|
31
|
+
return str(top);
|
|
32
|
+
return str(roy[key]);
|
|
33
|
+
};
|
|
34
|
+
const parentTop = task.parent_task_id;
|
|
35
|
+
return {
|
|
36
|
+
id: task.num ?? Number(task.id),
|
|
37
|
+
title: task.title,
|
|
38
|
+
...task.description ? { description: task.description } : {},
|
|
39
|
+
...pick("goals_and_expected_deliverables") !== undefined ? { goals_and_expected_deliverables: pick("goals_and_expected_deliverables") } : {},
|
|
40
|
+
...pick("priority") !== undefined ? { priority: pick("priority") } : {},
|
|
41
|
+
...pick("type") !== undefined ? { type: pick("type") } : {},
|
|
42
|
+
...pick("project_path") !== undefined ? { project_path: pick("project_path") } : {},
|
|
43
|
+
...typeof parentTop === "number" ? { parent_task_id: parentTop } : typeof roy.parent_task_id === "number" ? { parent_task_id: roy.parent_task_id } : { parent_task_id: null },
|
|
44
|
+
...pick("current_status") !== undefined ? { current_status: pick("current_status") } : {},
|
|
45
|
+
...pick("context") !== undefined ? { context: pick("context") } : {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function resolveByNum(store, num) {
|
|
49
|
+
if (typeof store.getTaskByNum === "function") {
|
|
50
|
+
return store.getTaskByNum(num);
|
|
51
|
+
}
|
|
52
|
+
const res = await store.listTasks({ limit: 1000 });
|
|
53
|
+
return res.tasks.find((t) => t.num === num || t.num === undefined && t.id === num) ?? null;
|
|
54
|
+
}
|
|
55
|
+
function createTaskComponentAdapter(store) {
|
|
56
|
+
return {
|
|
57
|
+
getTask: async (id) => {
|
|
58
|
+
const t = await resolveByNum(store, id);
|
|
59
|
+
return t ? toCoreTaskShape(t) : undefined;
|
|
60
|
+
},
|
|
61
|
+
updateTask: async (id, options) => {
|
|
62
|
+
const t = await resolveByNum(store, id);
|
|
63
|
+
if (!t)
|
|
64
|
+
throw new Error(`task #${id} not found`);
|
|
65
|
+
const isCoreShape = t.num === undefined;
|
|
66
|
+
return store.updateTask(t.id, options.context === undefined ? {} : isCoreShape ? { context: options.context } : { roy: { context: options.context } });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
5
71
|
// ../../../packages/cli/src/daemon/runners/types.ts
|
|
6
72
|
var RUNTIMES = Object.freeze(["opencode", "claude", "codex", "cursor"]);
|
|
7
73
|
var DEFAULT_RUNTIME = "opencode";
|
|
@@ -449,120 +515,159 @@ class NdjsonAssembler {
|
|
|
449
515
|
}
|
|
450
516
|
}
|
|
451
517
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
518
|
+
// ../../../packages/cli/src/daemon/discipline.ts
|
|
519
|
+
var PLUGIN_TASK_DISCIPLINE_PROMPT = `### Task Lifecycle — the plugin's contract
|
|
520
|
+
|
|
521
|
+
> **Task is the first-class citizen.** No task, no work.
|
|
522
|
+
|
|
523
|
+
You are the worker agent: you act through the tools, and your output is
|
|
524
|
+
what the user sees. The user reads task titles and milestones, not a
|
|
525
|
+
process log.
|
|
526
|
+
|
|
527
|
+
#### 0. Orient before you create
|
|
528
|
+
|
|
529
|
+
- If a \`[当前 Task #N]\` marker is in your context → \`task_get #N\`
|
|
530
|
+
directly, skip searching.
|
|
531
|
+
- Otherwise \`task_list\` / \`task_search\` for related work first.
|
|
532
|
+
- \`task_tree(depth=1)\` before creating: decide whether the work belongs
|
|
533
|
+
under an existing root instead of piling up new roots. Keep the root
|
|
534
|
+
tasks ≤ 10 — a wide forest is a sign the work was not oriented.
|
|
535
|
+
|
|
536
|
+
#### 1. Create or reuse
|
|
537
|
+
|
|
538
|
+
\`task_create\` with a **title**, a **description**, and
|
|
539
|
+
\`goals_and_expected_deliverables\` written as **numbered sub-items**
|
|
540
|
+
(编号子项) — each one separately verifiable:
|
|
541
|
+
|
|
542
|
+
1. 实现 X(含文件路径)
|
|
543
|
+
2. 测试 Y 覆盖 Z 场景(pass/fail 计数)
|
|
544
|
+
3. 文档 W 落盘到 <path>
|
|
545
|
+
|
|
546
|
+
Each sub-item must be checkable by one of the four atomic forms:
|
|
547
|
+
**文件断言** (file exists / size / content grep), **进程断言**
|
|
548
|
+
(command exit code / stdout), **测试断言** (pass/fail counts, no NEW
|
|
549
|
+
fail), **状态断言** (version / HTTP 200 / status field). A vague
|
|
550
|
+
one-line goal gives the reviewer nothing to check against.
|
|
551
|
+
|
|
552
|
+
Reuse before you create: if a matching task exists, \`task_update\` it
|
|
553
|
+
(append to description / goals) instead of creating a near-duplicate.
|
|
554
|
+
|
|
555
|
+
#### 2. Execute, and record as you go
|
|
556
|
+
|
|
557
|
+
- Work in stages; each stage is one \`task_operation_create\` call.
|
|
558
|
+
- **边干边记** — record the moment a stage lands, never batch at the
|
|
559
|
+
end. Milestones recorded in a batch at the end read as filler.
|
|
560
|
+
|
|
561
|
+
**Operation records carry two dimensions** — keep them apart:
|
|
562
|
+
|
|
563
|
+
| dimension | field | what it answers |
|
|
564
|
+
|---|---|---|
|
|
565
|
+
| state | \`milestone_type\` + \`milestone_title\` + \`milestone_description\` | **what** was reached |
|
|
566
|
+
| process | \`process_description\` (100–500 characters) | **how** it was reached |
|
|
567
|
+
|
|
568
|
+
Record **as you work**, **not after the fact**. The chain is the evidence
|
|
569
|
+
the acceptance side reads, so it has to be complete and honest.
|
|
570
|
+
|
|
571
|
+
\`milestone_title\` is a **短语** (≤ 20 字, verb-first) naming what was
|
|
572
|
+
locked down — e.g. \`泄漏源锁定\`, \`测试全绿\`. Not a label like
|
|
573
|
+
"progress" and not a sentence.
|
|
574
|
+
\`milestone_description\` is a dense technical narrative — file paths,
|
|
575
|
+
symbols, commands, numbers (pass/fail counts, exit codes, sizes).
|
|
576
|
+
\`process_description\` is the route (strategy + tool steps) and must
|
|
577
|
+
**not** repeat the description.
|
|
578
|
+
|
|
579
|
+
#### 3. Track
|
|
580
|
+
|
|
581
|
+
- \`task_update(progress=…, current_status=…)\` as you advance.
|
|
582
|
+
- Status vocabulary: \`todo\` → \`active\` → \`to_validate\` → \`completed\`.
|
|
583
|
+
**Do not** set \`completed\` yourself before the acceptance step below.
|
|
584
|
+
|
|
585
|
+
#### 4. Submit for acceptance — \`task_pre_complete\`
|
|
586
|
+
|
|
587
|
+
When the work is done and the deliverables exist, **submit**:
|
|
588
|
+
|
|
589
|
+
\`\`\`
|
|
590
|
+
task_pre_complete(
|
|
591
|
+
task_id=<id>,
|
|
592
|
+
deliverable_storage_path="<path(s)>",
|
|
593
|
+
current_status="<one-line progress summary>"
|
|
594
|
+
)
|
|
595
|
+
\`\`\`
|
|
596
|
+
|
|
597
|
+
- It sets \`status = "to_validate"\` (待验证) — **not** a terminal state.
|
|
598
|
+
- The deliverable path lands in \`task.context.deliverable_storage_path\`
|
|
599
|
+
and in the operation trail, so the acceptance side can check it.
|
|
600
|
+
- \`deliverable_storage_path\` may name **several paths**, space-separated
|
|
601
|
+
(\`/tmp/report.md /tmp/diff.patch\`); \`,\` \`;\` \`、\` and \` + \` also
|
|
602
|
+
split. A local path may carry the \`file://\` scheme. Declare **every**
|
|
603
|
+
artefact you produced — a path left out reads as "not produced".
|
|
604
|
+
- \`cycle\` tasks are periodic by design: they cannot be submitted or
|
|
605
|
+
completed. Use \`task_update\` to reset their progress instead.
|
|
606
|
+
|
|
607
|
+
**Self-check on submission.** Submitting runs the goal check (the
|
|
608
|
+
"goal vs. deliverables" judge) **once**, at submission time. If the
|
|
609
|
+
result carries a \`[TASK_COMPLETION_HINT]\`, you MUST verify it
|
|
610
|
+
(核实义务) — never ignore it:
|
|
611
|
+
|
|
612
|
+
1. Re-read the task with \`task_get(task_id=<id>)\`.
|
|
613
|
+
2. Then do exactly one of:
|
|
614
|
+
- **not met** → fix the gap, then submit again (\`task_pre_complete\`
|
|
615
|
+
is idempotent — a later submission replaces the earlier one's
|
|
616
|
+
status / deliverable, and each one leaves its own operation);
|
|
617
|
+
- **met after all** → record a \`decision\` operation
|
|
618
|
+
(\`task_operation_create(milestone_type="decision")\`) stating why the
|
|
619
|
+
sub-items are actually satisfied.
|
|
620
|
+
|
|
621
|
+
#### 5. Accept — \`task_complete\`
|
|
622
|
+
|
|
623
|
+
The acceptance step is a **separate, explicit** call. Before it:
|
|
624
|
+
|
|
625
|
+
1. Read the task's goal and delivery fields (\`task_get\`).
|
|
626
|
+
2. Validate **item-by-item** (按子项逐条验收) against the operation
|
|
627
|
+
chain and the deliverable evidence in
|
|
628
|
+
\`context.deliverable_storage_path\`.
|
|
629
|
+
3. Honour a \`[TASK_COMPLETION_HINT]\` if one is present — same verify
|
|
630
|
+
duty as above: either **reopen** the task (\`task_update(status="todo")\`)
|
|
631
|
+
and finish the missing work, or **state why** the sub-items are already
|
|
632
|
+
met (a \`decision\` operation).
|
|
633
|
+
4. If the goal and the deliverables disagree, either extend the task
|
|
634
|
+
(\`task_update\`) or record the discrepancy as a \`decision\` operation.
|
|
635
|
+
|
|
636
|
+
\`task_complete(task_id=<id>)\` then sets \`status = "completed"\` and
|
|
637
|
+
\`progress = 100\`. It takes **no** deliverable parameter — the
|
|
638
|
+
deliverables were already submitted through \`task_pre_complete\`.
|
|
639
|
+
|
|
640
|
+
⚠️ If \`task_pre_complete\` is not in your tool surface (an older
|
|
641
|
+
core), submit with \`task_complete(deliverable_storage_path=...)\`
|
|
642
|
+
instead — the legacy one-call contract.
|
|
643
|
+
|
|
644
|
+
#### Where tasks live
|
|
645
|
+
|
|
646
|
+
All tasks and operations are persisted by the plugin in the **single**
|
|
647
|
+
project store — locally SQLite at \`~/.roy-task-plugin/tasks.db\` (or the
|
|
648
|
+
remote service when the plugin runs with \`persistence: "remote"\`). Do
|
|
649
|
+
not create side databases, do not keep your own to-do list in files, and
|
|
650
|
+
do not track work in chat only: if it is not in the task store, it is
|
|
651
|
+
not tracked.
|
|
652
|
+
|
|
653
|
+
#### Style
|
|
654
|
+
|
|
655
|
+
- Be direct and concrete.
|
|
656
|
+
- Report the task id and its state whenever you finish a step.
|
|
657
|
+
- If a task is blocked, mark it (\`task_update(status="paused")\`) and
|
|
658
|
+
record a \`problem\` milestone explaining the blocker instead of going
|
|
659
|
+
silent.`;
|
|
660
|
+
var RUNTIMES_WITH_AGENT_SYSTEM_PROMPT = Object.freeze([
|
|
661
|
+
"opencode",
|
|
662
|
+
"claude"
|
|
501
663
|
]);
|
|
502
|
-
function
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
function isContinuationOf(line, head) {
|
|
506
|
-
if (isListItemHead(line))
|
|
507
|
-
return false;
|
|
508
|
-
if (/^\s*(#{1,6}\s|>)/.test(line))
|
|
509
|
-
return false;
|
|
510
|
-
if (/^\s*```/.test(line))
|
|
511
|
-
return false;
|
|
512
|
-
if (line.trim() === "")
|
|
664
|
+
function needsDispatchInjection(runtime) {
|
|
665
|
+
if (!runtime)
|
|
513
666
|
return false;
|
|
514
|
-
|
|
515
|
-
return indent(line) > indent(head);
|
|
516
|
-
}
|
|
517
|
-
function trimTaskCompletePermissionText(text) {
|
|
518
|
-
if (!text)
|
|
519
|
-
return text;
|
|
520
|
-
const lines = text.split(`
|
|
521
|
-
`);
|
|
522
|
-
const out = [];
|
|
523
|
-
for (let i = 0;i < lines.length; i++) {
|
|
524
|
-
const line = lines[i];
|
|
525
|
-
const rule = TRIM_RULES.find((r) => r.match(line, { prevLine: lines[i - 1] }));
|
|
526
|
-
if (!rule) {
|
|
527
|
-
out.push(line);
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
if (rule.replace) {
|
|
531
|
-
const replaced = rule.replace(line);
|
|
532
|
-
if (ROLE_SCOPE_QUALIFIER.test(replaced)) {
|
|
533
|
-
throw new Error(`trim-permission: rule "${rule.name}" matched the line but its replace() ` + `left the role qualifier in place — core's wording drifted and this ` + `trim rule must be updated.
|
|
534
|
-
line: ${line}`);
|
|
535
|
-
}
|
|
536
|
-
out.push(replaced);
|
|
537
|
-
}
|
|
538
|
-
if (!rule.replace && isListItemHead(line)) {
|
|
539
|
-
while (i + 1 < lines.length && isContinuationOf(lines[i + 1], line)) {
|
|
540
|
-
i++;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
const collapsed = [];
|
|
545
|
-
let inFence = false;
|
|
546
|
-
for (const line of out) {
|
|
547
|
-
if (/^\s*```/.test(line)) {
|
|
548
|
-
inFence = !inFence;
|
|
549
|
-
collapsed.push(line);
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
if (!inFence && line.trim() === "" && collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === "") {
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
collapsed.push(line);
|
|
556
|
-
}
|
|
557
|
-
return collapsed.join(`
|
|
558
|
-
`);
|
|
667
|
+
return !RUNTIMES_WITH_AGENT_SYSTEM_PROMPT.includes(runtime);
|
|
559
668
|
}
|
|
560
669
|
|
|
561
670
|
// ../agent/src/roy-prompt-core.ts
|
|
562
|
-
var TRIMMED_LIFECYCLE = trimTaskCompletePermissionText(TASK_DISCIPLINE_LIFECYCLE);
|
|
563
|
-
var TRIMMED_GOALS = trimTaskCompletePermissionText(TASK_DISCIPLINE_GOALS);
|
|
564
|
-
var TRIMMED_OPERATION = trimTaskCompletePermissionText(TASK_DISCIPLINE_OPERATION);
|
|
565
|
-
var TRIMMED_COMPLETE = trimTaskCompletePermissionText(TASK_DISCIPLINE_COMPLETE);
|
|
566
671
|
var ROY_PROMPT_COMMON = `# Roy — task-first-class-citizen agent
|
|
567
672
|
|
|
568
673
|
You are **Roy**, an agent whose defining rule is simple:
|
|
@@ -572,44 +677,17 @@ You are **Roy**, an agent whose defining rule is simple:
|
|
|
572
677
|
You are a worker agent: you act through the tools, and your output is what
|
|
573
678
|
the user sees.
|
|
574
679
|
|
|
575
|
-
${
|
|
576
|
-
|
|
577
|
-
${TRIMMED_GOALS}
|
|
578
|
-
|
|
579
|
-
The tool returns a **numeric task id** — that number is your handle for the
|
|
580
|
-
rest of the conversation.
|
|
581
|
-
|
|
582
|
-
${TRIMMED_OPERATION}
|
|
583
|
-
|
|
584
|
-
${TRIMMED_COMPLETE}
|
|
585
|
-
|
|
586
|
-
#### Several deliverables in one field
|
|
587
|
-
\`deliverable_storage_path\` may name **multiple paths**, space-separated
|
|
588
|
-
(\`/tmp/report.md /tmp/diff.patch\`) — the judge splits on whitespace as well
|
|
589
|
-
as \`,\` \`;\` \`、\` and \` + \`. A local path may also be written in its
|
|
590
|
-
\`file://\` form (\`file:///tmp/report.md\`) — the scheme is stripped and the
|
|
591
|
-
path kept. Declare **every** artefact you produced: a path left out reads as
|
|
592
|
-
"not produced" and drags the task toward a false completion hint.
|
|
593
|
-
|
|
594
|
-
\`task_complete\` cannot be used on \`cycle\` tasks (they are periodic by
|
|
595
|
-
design) — use \`task_update\` to reset those.
|
|
680
|
+
${PLUGIN_TASK_DISCIPLINE_PROMPT}
|
|
596
681
|
|
|
597
682
|
## Other tools
|
|
598
683
|
|
|
599
684
|
- \`task_get(task_id, include_operations)\` — full detail; use it before
|
|
600
|
-
resuming or
|
|
685
|
+
resuming, submitting or accepting a task.
|
|
601
686
|
- \`task_list(...)\` — flat listing with filters (status / priority / type).
|
|
602
687
|
- \`task_search(keywords)\` — find by title / description / goals / tags.
|
|
603
688
|
**Search before creating**, to avoid duplicates.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
All tasks and operations are persisted by the roy-task-plugin in the
|
|
608
|
-
**single** project store — locally that is SQLite at
|
|
609
|
-
\`~/.roy-task-plugin/tasks.db\` (or the remote service when the plugin is
|
|
610
|
-
configured with \`persistence: "remote"\`). Do not create side databases, do
|
|
611
|
-
not maintain your own to-do list in files, and do not track work in chat only:
|
|
612
|
-
if it is not in the task store, it is not tracked.
|
|
689
|
+
- \`task_tree(depth=1)\` — the task hierarchy; use it to place new work
|
|
690
|
+
under an existing root instead of piling up roots.
|
|
613
691
|
|
|
614
692
|
## Style
|
|
615
693
|
|
|
@@ -700,7 +778,8 @@ loads. Call them directly:
|
|
|
700
778
|
| \`task_create\` | \`mcp__roy-task__task_create\` |
|
|
701
779
|
| \`task_update\` | \`mcp__roy-task__task_update\` |
|
|
702
780
|
| \`task_operation_create\` | \`mcp__roy-task__task_operation_create\` |
|
|
703
|
-
| \`
|
|
781
|
+
| \`task_pre_complete\` (submit for acceptance) | \`mcp__roy-task__task_pre_complete\` |
|
|
782
|
+
| \`task_complete\` (accept) | \`mcp__roy-task__task_complete\` |
|
|
704
783
|
| \`task_get\` | \`mcp__roy-task__task_get\` |
|
|
705
784
|
| \`task_list\` | \`mcp__roy-task__task_list\` |
|
|
706
785
|
| \`task_search\` | \`mcp__roy-task__task_search\` |
|
|
@@ -716,12 +795,13 @@ instead of silently skipping the lifecycle.`;
|
|
|
716
795
|
|
|
717
796
|
// ../../../packages/cli/src/daemon/runners/claude-agents.ts
|
|
718
797
|
var CLAUDE_ROY_AGENT_NAME = "roy";
|
|
719
|
-
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.";
|
|
798
|
+
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_pre_complete → task_complete) " + "so every piece of work is tracked in the local task store.";
|
|
720
799
|
var ROY_TASK_MCP_SERVER = "roy-task";
|
|
721
800
|
var ROY_TASK_MCP_TOOLS = Object.freeze([
|
|
722
801
|
"task_create",
|
|
723
802
|
"task_update",
|
|
724
803
|
"task_operation_create",
|
|
804
|
+
"task_pre_complete",
|
|
725
805
|
"task_complete",
|
|
726
806
|
"task_get",
|
|
727
807
|
"task_list",
|
|
@@ -1260,20 +1340,6 @@ function errMsg(err) {
|
|
|
1260
1340
|
return String(err);
|
|
1261
1341
|
}
|
|
1262
1342
|
|
|
1263
|
-
// ../../../packages/cli/src/daemon/discipline.ts
|
|
1264
|
-
import { TASK_DISCIPLINE_PROMPT } from "@ai-setting/roy-agent-core/env/task/prompt";
|
|
1265
|
-
var CORE_TASK_DISCIPLINE_PROMPT_RAW = TASK_DISCIPLINE_PROMPT;
|
|
1266
|
-
var PLUGIN_TASK_DISCIPLINE_PROMPT = trimTaskCompletePermissionText(CORE_TASK_DISCIPLINE_PROMPT_RAW);
|
|
1267
|
-
var RUNTIMES_WITH_AGENT_SYSTEM_PROMPT = Object.freeze([
|
|
1268
|
-
"opencode",
|
|
1269
|
-
"claude"
|
|
1270
|
-
]);
|
|
1271
|
-
function needsDispatchInjection(runtime) {
|
|
1272
|
-
if (!runtime)
|
|
1273
|
-
return false;
|
|
1274
|
-
return !RUNTIMES_WITH_AGENT_SYSTEM_PROMPT.includes(runtime);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
1343
|
// ../../../packages/cli/src/daemon/runners/codex.ts
|
|
1278
1344
|
var DEFAULT_CODEX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1279
1345
|
var CODEX_STDIO = Object.freeze([
|
|
@@ -1461,7 +1527,8 @@ var VALUE_FLAGS = new Set([
|
|
|
1461
1527
|
"--codex-sandbox",
|
|
1462
1528
|
"--cursor-model",
|
|
1463
1529
|
"--max-concurrency",
|
|
1464
|
-
"--timeout-ms"
|
|
1530
|
+
"--timeout-ms",
|
|
1531
|
+
"--task-store"
|
|
1465
1532
|
]);
|
|
1466
1533
|
var SUBCOMMANDS = new Set(["serve", "status", "help", "version"]);
|
|
1467
1534
|
function parseArgs(argv) {
|
|
@@ -1640,6 +1707,13 @@ function parseServe(argv) {
|
|
|
1640
1707
|
case "--no-card":
|
|
1641
1708
|
serve.cardEnabled = false;
|
|
1642
1709
|
break;
|
|
1710
|
+
case "--task-store": {
|
|
1711
|
+
const v = take();
|
|
1712
|
+
if (v instanceof Error)
|
|
1713
|
+
return { command: "unknown", error: v.message };
|
|
1714
|
+
serve.taskStoreEndpoint = v;
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1643
1717
|
default:
|
|
1644
1718
|
return { command: "unknown", error: `unknown flag "${flag}"` };
|
|
1645
1719
|
}
|
|
@@ -1872,7 +1946,7 @@ function buildDispatchPrompt(input) {
|
|
|
1872
1946
|
""
|
|
1873
1947
|
];
|
|
1874
1948
|
if (needsDispatchInjection(input.runtime)) {
|
|
1875
|
-
parts.push("## 任务纪律(Task #
|
|
1949
|
+
parts.push("## 任务纪律(Task #5078 — 插件自维护的契约)", "", PLUGIN_TASK_DISCIPLINE_PROMPT, "");
|
|
1876
1950
|
}
|
|
1877
1951
|
parts.push("## 硬性规则", "", "1. **不要**自己调用 `lark-cli` 发送回复。守护进程会在你结束后,把你的", " **最终回答文本**用 `lark-cli im +messages-reply` 一次性发回飞书。", " 你自己发会造成重复消息。", "2. 把**最终回答**写成给发件人看的**普通散文 / Markdown**。", " 不要输出 JSON 包裹、不要输出“我调用了某某工具”的过程流水账。", " 这段文本会**原样**成为飞书里的回复正文。", "3. 需要落任务就用 `task_*` 工具(task_create / task_update / …),", " 这是你的主要职责;不要只在回答里“描述”你打算做什么。", "", "## 什么时候该建任务(Task #4433)", "", "任务由**你**决定建不建,守护进程不会替你建。判断标准:", "", "- **该建**:需要多步操作、要动文件/跑命令、要调研并产出结论、", " 耗时预计超过一两分钟、或用户明确要求「跟踪 / 建个任务 / 后续跟进」。", " → 用 `task_create` 建任务,**然后**再开始干活(先立任务,再动手)。", "- **不必建**:寒暄、谢谢、纯问答(「今天几号」「这个参数叫啥」)、", " 一两句话就能答完且不需要任何工具的问题。", " → 直接回答,不要为了「显得勤快」而建一个空任务。", "- **建完要更新**:动手过程中用 `task_update` 推进度 / 改 current_status,", " 干完用 `task_complete` 收尾。建了任务却从不更新,比不建更糟——", " 看板上会留下一个卡住的任务。", "", "⚠️ **卡片与任务一一对应**:只有你建了任务,用户才会收到一张进展卡片;", "没建任务就只有文字回复。所以**不要**为了「想要一张卡片」而去建任务,", "也不要建了任务又不用它——卡片上显示的就是你建的那个任务的真 id 和标题。", "", "## 环境事实(必须使用下面这些值,不要自己猜)", "", `- lark-cli profile:\`${profile}\`(飞书 openId 是 per-app 隔离的,换 profile 会失败)`, `- chat_id:\`${chatId}\``, `- message_id:\`${messageId}\``, `- event_id:\`${eventId}\``, "", "## 原始事件(完整 JSON)", "", "```json", JSON.stringify(input.rawEvent, null, 2), "```");
|
|
1878
1952
|
if (input.recommendedAction) {
|
|
@@ -2552,6 +2626,11 @@ function createEventListener(config) {
|
|
|
2552
2626
|
profile: config.profile,
|
|
2553
2627
|
eventKeys
|
|
2554
2628
|
});
|
|
2629
|
+
if (config.taskComponent) {
|
|
2630
|
+
instance.setEnvironment?.({
|
|
2631
|
+
getComponent: (name) => name === "task" ? config.taskComponent : undefined
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2555
2634
|
let callback;
|
|
2556
2635
|
instance.onEvent((event) => {
|
|
2557
2636
|
const typed = event;
|
|
@@ -2577,6 +2656,14 @@ function createEventListener(config) {
|
|
|
2577
2656
|
plannedCommands() {
|
|
2578
2657
|
const profileFlag = config.profile ? `--profile ${config.profile} ` : "";
|
|
2579
2658
|
return eventKeys.map((k) => `lark-cli ${profileFlag}event consume ${k} --as bot`);
|
|
2659
|
+
},
|
|
2660
|
+
setTaskComponent(component) {
|
|
2661
|
+
instance.setEnvironment?.({
|
|
2662
|
+
getComponent: (name) => name === "task" ? component : undefined
|
|
2663
|
+
});
|
|
2664
|
+
},
|
|
2665
|
+
hasCardActionPorts() {
|
|
2666
|
+
return instance.hasCardActionPorts?.() ?? false;
|
|
2580
2667
|
}
|
|
2581
2668
|
};
|
|
2582
2669
|
}
|
|
@@ -3338,6 +3425,38 @@ function getTraceRuntime(options) {
|
|
|
3338
3425
|
return runtime;
|
|
3339
3426
|
}
|
|
3340
3427
|
|
|
3428
|
+
// ../../../packages/cli/src/daemon/es-config.ts
|
|
3429
|
+
import { resolveEsFilePath } from "@ai-setting/roy-agent-core/env/event-source";
|
|
3430
|
+
async function readEventSourcesFile(configPath) {
|
|
3431
|
+
try {
|
|
3432
|
+
const { readFile } = await import("node:fs/promises");
|
|
3433
|
+
const raw = await readFile(resolveEsFilePath(configPath), "utf-8");
|
|
3434
|
+
const parsed = JSON.parse(raw);
|
|
3435
|
+
return Array.isArray(parsed.sources) ? parsed.sources : [];
|
|
3436
|
+
} catch {
|
|
3437
|
+
return [];
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
async function resolveListenConfig(opts) {
|
|
3441
|
+
const sources = await readEventSourcesFile(opts.configPath);
|
|
3442
|
+
const larkSources = sources.filter((s) => s.type === "lark-cli" && s.enabled !== false);
|
|
3443
|
+
const profile = opts.profile ?? larkSources.find((s) => s.profile)?.profile;
|
|
3444
|
+
if (opts.eventKeys.length > 0) {
|
|
3445
|
+
return { profile, eventKeys: [...opts.eventKeys], matchedSourceIds: [] };
|
|
3446
|
+
}
|
|
3447
|
+
const matched = larkSources.filter((s) => s.profile === profile && Array.isArray(s.eventKeys));
|
|
3448
|
+
const keys = new Set;
|
|
3449
|
+
for (const s of matched) {
|
|
3450
|
+
for (const k of s.eventKeys ?? [])
|
|
3451
|
+
keys.add(k);
|
|
3452
|
+
}
|
|
3453
|
+
return {
|
|
3454
|
+
profile,
|
|
3455
|
+
eventKeys: [...keys],
|
|
3456
|
+
matchedSourceIds: matched.map((s) => s.id ?? "(unnamed)")
|
|
3457
|
+
};
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3341
3460
|
// ../../../packages/cli/src/cmd/serve.ts
|
|
3342
3461
|
function serve(options, deps = {}) {
|
|
3343
3462
|
return startServe(options, deps);
|
|
@@ -3368,10 +3487,44 @@ async function startServe(options, deps) {
|
|
|
3368
3487
|
agent: options.agent,
|
|
3369
3488
|
runtime: options.runtime
|
|
3370
3489
|
});
|
|
3371
|
-
const
|
|
3490
|
+
const explicitKeys = options.eventKeys.length === 1 && options.eventKeys[0] === DEFAULT_EVENT_KEY ? [] : options.eventKeys;
|
|
3491
|
+
const listen = await resolveListenConfig({
|
|
3372
3492
|
profile: options.profile,
|
|
3373
|
-
eventKeys:
|
|
3493
|
+
eventKeys: explicitKeys
|
|
3494
|
+
});
|
|
3495
|
+
if (listen.matchedSourceIds.length > 0) {
|
|
3496
|
+
log(`[roy-task] event-sources.json: listening ${JSON.stringify(listen.eventKeys)} ` + `(profile=${listen.profile}, sources: ${listen.matchedSourceIds.join(", ")})`);
|
|
3497
|
+
}
|
|
3498
|
+
const listener = (deps.createListener ?? createEventListener)({
|
|
3499
|
+
profile: listen.profile,
|
|
3500
|
+
eventKeys: listen.eventKeys
|
|
3374
3501
|
});
|
|
3502
|
+
if (options.taskStoreEndpoint) {
|
|
3503
|
+
try {
|
|
3504
|
+
const { readFile } = await import("node:fs/promises");
|
|
3505
|
+
const { homedir } = await import("node:os");
|
|
3506
|
+
const { join: join4 } = await import("node:path");
|
|
3507
|
+
const { createTaskPortFromConfig } = await import("@ai-setting/roy-agent-core/env/task/backend");
|
|
3508
|
+
const { createTaskComponentAdapter: createTaskComponentAdapter2 } = await Promise.resolve().then(() => exports_card_action_ports);
|
|
3509
|
+
let authToken;
|
|
3510
|
+
try {
|
|
3511
|
+
const auth = JSON.parse(await readFile(join4(homedir(), ".roy-agent", "auth.json"), "utf-8"));
|
|
3512
|
+
authToken = auth.taskService?.authToken;
|
|
3513
|
+
} catch {}
|
|
3514
|
+
const wired = await createTaskPortFromConfig({
|
|
3515
|
+
get: (key) => ({
|
|
3516
|
+
"task.storage.type": "remote",
|
|
3517
|
+
"task.storage.endpoint": options.taskStoreEndpoint,
|
|
3518
|
+
"task.storage.authToken": authToken
|
|
3519
|
+
})[key]
|
|
3520
|
+
});
|
|
3521
|
+
const store = wired.port;
|
|
3522
|
+
listener.setTaskComponent(createTaskComponentAdapter2(store));
|
|
3523
|
+
log(`[roy-task] card-action wired to ${options.taskStoreEndpoint}`);
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
log(`[roy-task] card-action wiring failed: ${errMsg5(err)} — card.action.trigger events will be skipped`);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3375
3528
|
let eventCount = 0;
|
|
3376
3529
|
let stopping;
|
|
3377
3530
|
let resolveDone;
|
|
@@ -3395,7 +3548,7 @@ async function startServe(options, deps) {
|
|
|
3395
3548
|
};
|
|
3396
3549
|
const sweeper = setInterval(() => {
|
|
3397
3550
|
deduper.sweep();
|
|
3398
|
-
},
|
|
3551
|
+
}, 300000);
|
|
3399
3552
|
if (typeof sweeper.unref === "function")
|
|
3400
3553
|
sweeper.unref();
|
|
3401
3554
|
listener.onEvent(async (event) => {
|
|
@@ -3469,13 +3622,13 @@ function formatResult(label, result) {
|
|
|
3469
3622
|
const card = cardSuffix(result);
|
|
3470
3623
|
switch (result.status) {
|
|
3471
3624
|
case "dispatched":
|
|
3472
|
-
return `[roy-task] ${label} dispatched chat=${result.plan.chatId} session=${result.plan.sessionId}
|
|
3625
|
+
return `[roy-task] ${label} dispatched chat=${result.plan.chatId} session=${result.plan.sessionId} reply=${result.replyMessageId ?? "(no id)"} chars=${result.text.length} task=${result.taskId ?? "(none)"}${card}`;
|
|
3473
3626
|
case "dry-run":
|
|
3474
|
-
return `[roy-task] ${label} DRY-RUN would run agent=${result.plan.agent}
|
|
3627
|
+
return `[roy-task] ${label} DRY-RUN would run agent=${result.plan.agent} session=${result.plan.sessionId} chat=${result.plan.chatId} prompt=${result.plan.prompt.length}chars`;
|
|
3475
3628
|
case "duplicate":
|
|
3476
3629
|
return `[roy-task] ${label} duplicate dropped key=${result.key ?? "(unknown)"}`;
|
|
3477
3630
|
case "no-output":
|
|
3478
|
-
return `[roy-task] ${label} no-output (agent produced no text; not replying)
|
|
3631
|
+
return `[roy-task] ${label} no-output (agent produced no text; not replying) toolCalls=${result.toolCalls}${taskSuffix(result)}${card}`;
|
|
3479
3632
|
case "no-reply":
|
|
3480
3633
|
return `[roy-task] ${label} no-reply (reply disabled) chars=${result.text.length}${taskSuffix(result)}${card}`;
|
|
3481
3634
|
case "agent-error":
|
|
@@ -3611,7 +3764,7 @@ function formatStatus(report) {
|
|
|
3611
3764
|
}
|
|
3612
3765
|
|
|
3613
3766
|
// ../../../packages/cli/src/main.ts
|
|
3614
|
-
var VERSION = "0.7.
|
|
3767
|
+
var VERSION = "0.7.15";
|
|
3615
3768
|
async function main(argv) {
|
|
3616
3769
|
const parsed = parseArgs(argv);
|
|
3617
3770
|
switch (parsed.command) {
|