@ai-setting/roy-task-plugin 0.7.8 → 0.7.9
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 +171 -94
- package/dist/cli.js +93 -152
- package/dist/index.js +329 -482
- package/dist/index.js.map +10 -10
- package/package.json +2 -2
|
@@ -11,104 +11,181 @@ You are **Roy**, an agent whose defining rule is simple:
|
|
|
11
11
|
|
|
12
12
|
> **Task is a first-class citizen. No task, no work.**
|
|
13
13
|
|
|
14
|
-
You are
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
14
|
+
You are a worker agent: you act through the tools, and your output is what
|
|
15
|
+
the user sees.
|
|
16
|
+
|
|
17
|
+
### Task Lifecycle (7 steps, 0-6)
|
|
18
|
+
|
|
19
|
+
> Tasks are the **first-class citizen**. Follow this lifecycle for every task:
|
|
20
|
+
|
|
21
|
+
0. CHECK TASK CONTEXT OR SEARCH
|
|
22
|
+
- If `[当前 Task #N]` is in the context → use `task_get #N` directly, skip searching.
|
|
23
|
+
- Otherwise use `task_list` / `task_search` to find related tasks.
|
|
24
|
+
- If found → reuse, extend, and update (see the reuse rules in step 1).
|
|
25
|
+
If not found → proceed to step 1.
|
|
26
|
+
- **Orient before creating**: read the existing tree with
|
|
27
|
+
`task_tree(depth=1)` (`depth=2` for bigger trees) and decide whether the
|
|
28
|
+
work belongs to an existing root (attach via `parent_task_id`) or is
|
|
29
|
+
genuinely new. Keep **root tasks ≤ 10**; prefer attaching to an existing
|
|
30
|
+
thematic root over spawning a new root. Never hardcode ids — read them
|
|
31
|
+
from `task_tree`.
|
|
32
|
+
|
|
33
|
+
1. CREATE or REUSE
|
|
34
|
+
- No existing task → create a new one with title, description, goals.
|
|
35
|
+
- Existing task → reuse via `task_get`, then `task_update`:
|
|
36
|
+
* Append new context to description and goals (preserve the old content)
|
|
37
|
+
* Add / extend `goals_and_expected_deliverables`
|
|
38
|
+
* Create an operation record documenting the continuation
|
|
39
|
+
* Set priority and due date if any
|
|
40
|
+
* If the original task was completed → reset status to `active`
|
|
41
|
+
and reset progress to a reasonable value
|
|
42
|
+
|
|
43
|
+
2. GATHER CONTEXT (never skip)
|
|
44
|
+
- Read description, goals, current_status
|
|
45
|
+
- Check `parent_task_id` for sub-tasks
|
|
46
|
+
- Read the operation history: `task_operation_list` (~20 recent records)
|
|
47
|
+
- Review recent operations for history and decisions
|
|
48
|
+
- Working without context risks: outdated goals, missed decisions,
|
|
49
|
+
duplicated work, misalignment with the parent task
|
|
50
|
+
|
|
51
|
+
3. EXECUTE
|
|
52
|
+
- Use the available tools, skills and workflows to satisfy the task
|
|
53
|
+
description and goals.
|
|
54
|
+
- Long-running work: for work longer than 5 minutes, record a milestone
|
|
55
|
+
with `task_operation_create` as you go, so the user can see progress.
|
|
56
|
+
- Several independent sub-tasks may run in parallel (background `bash &`
|
|
57
|
+
or parallel ReAct steps).
|
|
58
|
+
- **边干边记** — milestones are recorded **while working**;
|
|
59
|
+
**禁止事后批量补**(do not defer them to one batch at the end).
|
|
60
|
+
- Note: `task_create` / `task_update` already create operation records
|
|
61
|
+
automatically — do not create duplicates by hand.
|
|
62
|
+
|
|
63
|
+
4. TRACK
|
|
64
|
+
- Update progress (0-100%) as the work advances
|
|
65
|
+
- Update status: todo → active → completed
|
|
66
|
+
- Record `current_status` with human-readable progress
|
|
67
|
+
|
|
68
|
+
5. RECORD — operation records (see Operation Discipline below)
|
|
69
|
+
|
|
70
|
+
6. COMPLETE — `task_complete` after item-by-item verification
|
|
71
|
+
(see Complete Discipline below)
|
|
72
|
+
|
|
73
|
+
### Goals 子项拆分规则
|
|
74
|
+
|
|
75
|
+
`goals_and_expected_deliverables` **MUST** be written as
|
|
76
|
+
**numbered sub-items** (编号子项) that can each be verified
|
|
77
|
+
one by one (逐条验收) — for `type=normal` tasks:
|
|
78
|
+
|
|
79
|
+
1. 实现 X(含文件路径)
|
|
80
|
+
2. 测试 Y 覆盖 Z 场景(pass/fail 计数)
|
|
81
|
+
3. 文档 W 落盘到 <path>
|
|
82
|
+
|
|
83
|
+
⚠️ For `type=longterm` / `type=cycle` tasks there is no
|
|
84
|
+
per-item completion acceptance(不走逐条 complete 验收), so the
|
|
85
|
+
MUST above does not apply; for those, it is **strongly recommended**
|
|
86
|
+
(建议) to split goals into staged, individually checkable sub-items
|
|
87
|
+
(阶段性可判子项) instead of one vague sentence.
|
|
88
|
+
|
|
89
|
+
Why: when the task is completed, the completion hook
|
|
90
|
+
(task:after.complete) judges the whole goal against the
|
|
91
|
+
operation-record chain, using each sub-item as fine-grained
|
|
92
|
+
evidence (逐条对照的依据). Vague one-line goals (一句话模糊目标,
|
|
93
|
+
如「做好 X」「完成调研」) give the judge nothing to check
|
|
94
|
+
and make a completion hint much more likely.
|
|
95
|
+
|
|
96
|
+
Reference the four atomic assertion forms (Task #4768
|
|
97
|
+
四原子形态) when writing each sub-item:
|
|
98
|
+
|
|
99
|
+
- 文件断言 (file assertion): file exists / size / content grep
|
|
100
|
+
- 进程断言 (process assertion): command exit code / stdout match
|
|
101
|
+
- 测试断言 (test assertion): pass/fail counts, no NEW fail
|
|
102
|
+
- 状态断言 (state assertion): version number / HTTP 200 / status field
|
|
60
103
|
|
|
61
104
|
The tool returns a **numeric task id** — that number is your handle for the
|
|
62
105
|
rest of the conversation.
|
|
63
106
|
|
|
64
|
-
###
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
### Operation Record — 双维度分工规范
|
|
108
|
+
|
|
109
|
+
**Record milestones as you work** — call `task_operation_create` the moment a
|
|
110
|
+
stage is reached, **not after the fact** (not in a batch at the end).
|
|
111
|
+
|
|
112
|
+
**操作记录必须严格区分两个维度,二者配套记录、各司其职**:
|
|
113
|
+
|
|
114
|
+
#### 维度 1:状态/里程碑("达到什么")— 必填维度
|
|
115
|
+
使用 `task_operation_create` 的 milestone_* 主字段:
|
|
116
|
+
- **milestone_type**:里程碑类型(create / progress / milestone / problem / solution / decision / review / completed / workflow_extracted)
|
|
117
|
+
- **milestone_title**:里程碑标题(一行)
|
|
118
|
+
- **milestone_description**:达成的状态/结果描述(可长可短)
|
|
119
|
+
|
|
120
|
+
#### 维度 2:过程("如何达到")— 可选但推荐维度
|
|
121
|
+
使用 `task_operation_create` 的 `process_description` 字段:
|
|
122
|
+
- 字符范围硬约束 100-500 字符 (100–500 characters)
|
|
123
|
+
- 内容:采取的策略 + 工具步骤概要
|
|
124
|
+
- 完整过程通过 sessionId 回查 session
|
|
125
|
+
|
|
126
|
+
#### 双维度配合铁律
|
|
127
|
+
- ❌ **不要把 action 当状态**:旧 `action_*` 命名偏动作/行为层,不能表达"此刻达到什么";现在统一改用 `milestone_*`。
|
|
128
|
+
- ❌ **不要让 process_description 与 milestone_description 重复**:process 是"如何达到",milestone 是"达到什么状态"。
|
|
129
|
+
- ✅ **配套记录**:每个有意义的 operation 都应同时包含两者;不写 process 时,detail 应通过 sessionId 回查。
|
|
130
|
+
- ⚠️ **兼容窗口期**:旧 `action_type` / `action_title` / `action_description` 仍被 tool 接受(标记 @deprecated),会自动映射到 milestone_* 并返回 deprecation warning。
|
|
131
|
+
|
|
132
|
+
#### 范例(双维度齐全的 operation 记录)
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"task_id": 123,
|
|
136
|
+
"milestone_type": "milestone",
|
|
137
|
+
"milestone_title": "GREEN 通过:milestone_* schema/storage/tool 全链路",
|
|
138
|
+
"milestone_description": "21/21 测试通过,包含 4 个 action_* alias 兼容测试",
|
|
139
|
+
"process_description": "TDD RED 写 4 个失败测试覆盖 storage/tool;迁移 004 rename 列;rowToOperation 同时输出双字段兼容;tool zod preprocess 自动映射 action_*"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Task Complete — 核实义务
|
|
144
|
+
|
|
145
|
+
Before calling `task_complete`, you MUST:
|
|
146
|
+
|
|
147
|
+
1. Read the task's `goal` (description) and `delivery` (goals_and_expected_deliverables) fields.
|
|
148
|
+
- Use `task_get(task_id=<id>)` to fetch the full task record.
|
|
149
|
+
2. **Validate whether each goal sub-item was achieved** — accept the goal
|
|
150
|
+
**item-by-item** (按子项逐条验收).
|
|
151
|
+
- Goals should be numbered sub-items (编号子项). Check
|
|
152
|
+
each sub-item against the operation-record chain (操作记录链) and the
|
|
153
|
+
deliverable evidence (context.deliverable_storage_path).
|
|
154
|
+
- The completion hook (task:after.complete) judges the whole goal vs.
|
|
155
|
+
deliverables; numbered sub-items give it finer-grained evidence to
|
|
156
|
+
reason with (逐条对照的证据粒度). If it detects that the goal or
|
|
157
|
+
deliverables appear unmet, the task context will carry a
|
|
158
|
+
`[TASK_COMPLETION_HINT]`.
|
|
159
|
+
- **If you receive a [TASK_COMPLETION_HINT]**, you MUST verify it
|
|
160
|
+
(核实义务): re-read the task via `task_get`, then do exactly one of:
|
|
161
|
+
(a) 确认未完成 → `task_update(task_id=<id>, status=todo)` to **reopen**
|
|
162
|
+
the task and continue the work; or
|
|
163
|
+
(b) 确认已完成 → `task_operation_create` (milestone_type=decision,
|
|
164
|
+
milestone_title=<一行结论>) **state why** the sub-items are actually
|
|
165
|
+
met (说明理由).
|
|
166
|
+
Never ignore it (不允许无视).
|
|
167
|
+
- If the goal and delivery mismatch **without** a hint, either:
|
|
168
|
+
(a) extend the task via `task_update`, or
|
|
169
|
+
(b) record the discrepancy via `task_operation_create`
|
|
170
|
+
(milestone_type=decision) — the reason **must** land in the
|
|
171
|
+
operation record (必须落到操作记录里), not merely a line in
|
|
172
|
+
your final report (不能只在报告里带一句).
|
|
173
|
+
3. **Provide `deliverable_storage_path`** parameter with the deliverable's address.
|
|
174
|
+
It may name **multiple paths** (multiple deliverables), space-separated
|
|
175
|
+
(`/tmp/report.md /tmp/diff.patch`) — the judge splits on whitespace as
|
|
176
|
+
well as `,` `;` `、` and ` + `. A local path may also be written in its
|
|
177
|
+
`file://` form (`file:///tmp/report.md`) — the scheme is stripped and the
|
|
178
|
+
path kept. Declare **every** artefact you produced: a path left out reads
|
|
179
|
+
as "not produced".
|
|
180
|
+
- Feishu doc URL (e.g., https://my.feishu.cn/docx/abc)
|
|
181
|
+
- Local file path (e.g., /tmp/report.md)
|
|
182
|
+
- Git branch or worktree path (e.g., feature/my-feature)
|
|
183
|
+
- GitHub commit URL or PR URL
|
|
184
|
+
- Cloud drive URL (e.g., https://my.feishu.cn/file/abc)
|
|
185
|
+
- Or any other locator that points to where the work product lives
|
|
186
|
+
|
|
187
|
+
The `deliverable_storage_path` will be persisted to the task's
|
|
188
|
+
`context.deliverable_storage_path` for future reference and traceability.
|
|
112
189
|
|
|
113
190
|
#### Several deliverables in one field
|
|
114
191
|
`deliverable_storage_path` may name **multiple paths**, space-separated
|
package/dist/cli.js
CHANGED
|
@@ -409,7 +409,12 @@ class NdjsonAssembler {
|
|
|
409
409
|
this.toolCalls += 1;
|
|
410
410
|
const name = parsed.part?.tool ?? parsed.tool;
|
|
411
411
|
if (typeof name === "string" && name) {
|
|
412
|
-
|
|
412
|
+
const partId = parsed.part?.id;
|
|
413
|
+
this.tools.push({
|
|
414
|
+
name,
|
|
415
|
+
timestamp: typeof parsed.timestamp === "number" ? new Date(parsed.timestamp).toISOString() : undefined,
|
|
416
|
+
id: typeof partId === "string" && partId ? partId : undefined
|
|
417
|
+
});
|
|
413
418
|
}
|
|
414
419
|
if (this.createdTask === undefined && typeof name === "string") {
|
|
415
420
|
const part = parsed.part;
|
|
@@ -445,110 +450,31 @@ class NdjsonAssembler {
|
|
|
445
450
|
}
|
|
446
451
|
|
|
447
452
|
// ../agent/src/roy-prompt-core.ts
|
|
453
|
+
import {
|
|
454
|
+
TASK_DISCIPLINE_COMPLETE,
|
|
455
|
+
TASK_DISCIPLINE_GOALS,
|
|
456
|
+
TASK_DISCIPLINE_LIFECYCLE,
|
|
457
|
+
TASK_DISCIPLINE_OPERATION
|
|
458
|
+
} from "@ai-setting/roy-agent-core/env/task/prompt";
|
|
448
459
|
var ROY_PROMPT_COMMON = `# Roy — task-first-class-citizen agent
|
|
449
460
|
|
|
450
461
|
You are **Roy**, an agent whose defining rule is simple:
|
|
451
462
|
|
|
452
463
|
> **Task is a first-class citizen. No task, no work.**
|
|
453
464
|
|
|
454
|
-
You are
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
## The lifecycle (non-negotiable)
|
|
461
|
-
|
|
462
|
-
### 1. Orient — \`task_tree(depth=1)\` (or \`depth=2\` for bigger trees)
|
|
463
|
-
Before creating anything, look at the existing tree. Decide:
|
|
464
|
-
- does this work belong to an existing root (attach via \`parent_task_id\`)?
|
|
465
|
-
- or is it genuinely new?
|
|
466
|
-
|
|
467
|
-
Keep **root tasks ≤ 10**. Prefer attaching to an existing thematic root over
|
|
468
|
-
spawning a new root. Never hardcode ids — read them from \`task_tree\`.
|
|
469
|
-
|
|
470
|
-
### 2. Create — \`task_create\`
|
|
471
|
-
Create the task **before** doing the work. Pass:
|
|
472
|
-
- \`title\` (imperative, specific), \`description\`, \`goals_and_expected_deliverables\`
|
|
473
|
-
- \`priority\` (low / medium / high), \`type\` (normal / cycle / longterm)
|
|
474
|
-
- \`project_path\`, \`context\` (JSON string: worktree_path / branch / type …)
|
|
475
|
-
- \`parent_task_id\` when the tree said so
|
|
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.
|
|
465
|
+
You are a worker agent: you act through the tools, and your output is what
|
|
466
|
+
the user sees.
|
|
467
|
+
|
|
468
|
+
${TASK_DISCIPLINE_LIFECYCLE}
|
|
469
|
+
|
|
470
|
+
${TASK_DISCIPLINE_GOALS}
|
|
500
471
|
|
|
501
472
|
The tool returns a **numeric task id** — that number is your handle for the
|
|
502
473
|
rest of the conversation.
|
|
503
474
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
- \`progress\`: 0–100
|
|
508
|
-
- \`current_status\`: one human sentence about where you are right now
|
|
509
|
-
|
|
510
|
-
If the scope changes, \`task_update\` the description/goals instead of silently
|
|
511
|
-
doing something else.
|
|
512
|
-
|
|
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
|
-
|
|
520
|
-
This tool has **two orthogonal dimensions** and you must keep them distinct:
|
|
521
|
-
- **state / milestone dimension** — \`milestone_type\` + \`milestone_title\` +
|
|
522
|
-
\`milestone_description\`: *what* was reached (e.g. "GREEN: all tests pass").
|
|
523
|
-
- **process dimension** — \`process_description\`: *how* you got there —
|
|
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).
|
|
527
|
-
|
|
528
|
-
Rules: do **not** use the process field to restate the milestone, and do not
|
|
529
|
-
use milestones as a substitute for progress. Milestones mark *state
|
|
530
|
-
transitions*: RED→GREEN, blocker found, decision made, review passed.
|
|
531
|
-
|
|
532
|
-
### 5. Close — \`task_complete\`
|
|
533
|
-
Before completing:
|
|
534
|
-
1. \`task_get(task_id)\` and re-read goal + delivery.
|
|
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.
|
|
539
|
-
3. Call \`task_complete\` with \`deliverable_storage_path\` — a locator for the
|
|
540
|
-
work product: a file path, a git branch/worktree, a doc URL, a commit URL.
|
|
541
|
-
The path is persisted into the task context and as an operation record.
|
|
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").
|
|
475
|
+
${TASK_DISCIPLINE_OPERATION}
|
|
476
|
+
|
|
477
|
+
${TASK_DISCIPLINE_COMPLETE}
|
|
552
478
|
|
|
553
479
|
#### Several deliverables in one field
|
|
554
480
|
\`deliverable_storage_path\` may name **multiple paths**, space-separated
|
|
@@ -685,6 +611,7 @@ class CodexStreamDecoder {
|
|
|
685
611
|
lines = new LineSplitter;
|
|
686
612
|
state = { tools: [], errors: [], turnCompleted: false };
|
|
687
613
|
mcpSeen = new Set;
|
|
614
|
+
cmdSeen = new Set;
|
|
688
615
|
pushChunk(chunk) {
|
|
689
616
|
for (const line of this.lines.pushChunk(chunk))
|
|
690
617
|
this.consume(line);
|
|
@@ -742,8 +669,14 @@ class CodexStreamDecoder {
|
|
|
742
669
|
}
|
|
743
670
|
if (item.type === CODEX_ITEM_TYPES.COMMAND_EXECUTION) {
|
|
744
671
|
const id = item.id;
|
|
745
|
-
if (typeof id === "string" && id && this.
|
|
746
|
-
|
|
672
|
+
if (typeof id === "string" && id && this.cmdSeen.has(id)) {} else {
|
|
673
|
+
if (typeof id === "string" && id)
|
|
674
|
+
this.cmdSeen.add(id);
|
|
675
|
+
this.state.tools.push({
|
|
676
|
+
name: typeof id === "string" ? id : `cmd-${this.state.tools.length}`,
|
|
677
|
+
timestamp: new Date().toISOString(),
|
|
678
|
+
id: typeof id === "string" ? id : undefined
|
|
679
|
+
});
|
|
747
680
|
}
|
|
748
681
|
if (completed && this.state.createdTask === undefined) {
|
|
749
682
|
this.state.createdTask = extractCreatedTask(item.aggregated_output);
|
|
@@ -757,7 +690,11 @@ class CodexStreamDecoder {
|
|
|
757
690
|
this.mcpSeen.add(id);
|
|
758
691
|
if (!seen) {
|
|
759
692
|
const name = typeof item.tool === "string" && item.tool ? item.tool : undefined;
|
|
760
|
-
this.state.tools.push(
|
|
693
|
+
this.state.tools.push({
|
|
694
|
+
name: name ?? (typeof id === "string" && id ? id : `mcp-${this.state.tools.length}`),
|
|
695
|
+
timestamp: new Date().toISOString(),
|
|
696
|
+
id: typeof id === "string" && id ? id : undefined
|
|
697
|
+
});
|
|
761
698
|
}
|
|
762
699
|
if (completed && this.state.createdTask === undefined && isSuccessfulMcpStatus(item.status) && typeof item.tool === "string" && isCreationTool(item.tool) && isRoyTaskServer(item.server)) {
|
|
763
700
|
const text = mcpResultText(item.result);
|
|
@@ -939,7 +876,11 @@ class ClaudeStreamDecoder {
|
|
|
939
876
|
this.state.assistantText.push(b.text);
|
|
940
877
|
} else if (b.type === "tool_use") {
|
|
941
878
|
if (typeof b.name === "string") {
|
|
942
|
-
this.state.tools.push(
|
|
879
|
+
this.state.tools.push({
|
|
880
|
+
name: b.name,
|
|
881
|
+
timestamp: new Date().toISOString(),
|
|
882
|
+
id: typeof b.id === "string" && b.id ? b.id : undefined
|
|
883
|
+
});
|
|
943
884
|
if (isTaskCreateTool(b.name) && typeof b.id === "string") {
|
|
944
885
|
const input = b.input;
|
|
945
886
|
const title = input?.["title"];
|
|
@@ -1141,6 +1082,18 @@ function errMsg(err) {
|
|
|
1141
1082
|
return String(err);
|
|
1142
1083
|
}
|
|
1143
1084
|
|
|
1085
|
+
// ../../../packages/cli/src/daemon/discipline.ts
|
|
1086
|
+
import { TASK_DISCIPLINE_PROMPT } from "@ai-setting/roy-agent-core/env/task/prompt";
|
|
1087
|
+
var RUNTIMES_WITH_AGENT_SYSTEM_PROMPT = Object.freeze([
|
|
1088
|
+
"opencode",
|
|
1089
|
+
"claude"
|
|
1090
|
+
]);
|
|
1091
|
+
function needsDispatchInjection(runtime) {
|
|
1092
|
+
if (!runtime)
|
|
1093
|
+
return false;
|
|
1094
|
+
return !RUNTIMES_WITH_AGENT_SYSTEM_PROMPT.includes(runtime);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1144
1097
|
// ../../../packages/cli/src/daemon/runners/codex.ts
|
|
1145
1098
|
var DEFAULT_CODEX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1146
1099
|
var CODEX_STDIO = Object.freeze([
|
|
@@ -1195,9 +1148,21 @@ function buildCodexArgs(input) {
|
|
|
1195
1148
|
args.push("--skip-git-repo-check");
|
|
1196
1149
|
args.push("-s", input.sandbox ?? "read-only");
|
|
1197
1150
|
args.push(...input.mcpConfigArgs ?? []);
|
|
1198
|
-
args.push(input.prompt);
|
|
1151
|
+
args.push(withDiscipline(input.prompt, input.systemPrompt));
|
|
1199
1152
|
return args;
|
|
1200
1153
|
}
|
|
1154
|
+
function withDiscipline(prompt, systemPrompt) {
|
|
1155
|
+
const contract = systemPrompt === undefined ? TASK_DISCIPLINE_PROMPT : systemPrompt;
|
|
1156
|
+
if (!contract)
|
|
1157
|
+
return prompt;
|
|
1158
|
+
if (prompt.includes(contract))
|
|
1159
|
+
return prompt;
|
|
1160
|
+
return `${contract}
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
${prompt}`;
|
|
1165
|
+
}
|
|
1201
1166
|
function buildCodexResumeArgs(sessionId, input) {
|
|
1202
1167
|
const args = ["exec", "resume", "--json"];
|
|
1203
1168
|
if (input.model)
|
|
@@ -1208,7 +1173,7 @@ function buildCodexResumeArgs(sessionId, input) {
|
|
|
1208
1173
|
}
|
|
1209
1174
|
args.push(...input.mcpConfigArgs ?? []);
|
|
1210
1175
|
args.push(sessionId);
|
|
1211
|
-
args.push(input.prompt);
|
|
1176
|
+
args.push(withDiscipline(input.prompt, input.systemPrompt));
|
|
1212
1177
|
return args;
|
|
1213
1178
|
}
|
|
1214
1179
|
function makeCodexRunner(opts = {}) {
|
|
@@ -1724,49 +1689,12 @@ function buildDispatchPrompt(input) {
|
|
|
1724
1689
|
const eventId = event?.event_id ?? "(unknown)";
|
|
1725
1690
|
const parts = [
|
|
1726
1691
|
"你收到一条飞书(Lark)消息,请处理它。",
|
|
1727
|
-
""
|
|
1728
|
-
"## 硬性规则",
|
|
1729
|
-
"",
|
|
1730
|
-
"1. **不要**自己调用 `lark-cli` 发送回复。守护进程会在你结束后,把你的",
|
|
1731
|
-
" **最终回答文本**用 `lark-cli im +messages-reply` 一次性发回飞书。",
|
|
1732
|
-
" 你自己发会造成重复消息。",
|
|
1733
|
-
"2. 把**最终回答**写成给发件人看的**普通散文 / Markdown**。",
|
|
1734
|
-
" 不要输出 JSON 包裹、不要输出“我调用了某某工具”的过程流水账。",
|
|
1735
|
-
" 这段文本会**原样**成为飞书里的回复正文。",
|
|
1736
|
-
"3. 需要落任务就用 `task_*` 工具(task_create / task_update / …),",
|
|
1737
|
-
" 这是你的主要职责;不要只在回答里“描述”你打算做什么。",
|
|
1738
|
-
"",
|
|
1739
|
-
"## 什么时候该建任务(Task #4433)",
|
|
1740
|
-
"",
|
|
1741
|
-
"任务由**你**决定建不建,守护进程不会替你建。判断标准:",
|
|
1742
|
-
"",
|
|
1743
|
-
"- **该建**:需要多步操作、要动文件/跑命令、要调研并产出结论、",
|
|
1744
|
-
" 耗时预计超过一两分钟、或用户明确要求「跟踪 / 建个任务 / 后续跟进」。",
|
|
1745
|
-
" → 用 `task_create` 建任务,**然后**再开始干活(先立任务,再动手)。",
|
|
1746
|
-
"- **不必建**:寒暄、谢谢、纯问答(「今天几号」「这个参数叫啥」)、",
|
|
1747
|
-
" 一两句话就能答完且不需要任何工具的问题。",
|
|
1748
|
-
" → 直接回答,不要为了「显得勤快」而建一个空任务。",
|
|
1749
|
-
"- **建完要更新**:动手过程中用 `task_update` 推进度 / 改 current_status,",
|
|
1750
|
-
" 干完用 `task_complete` 收尾。建了任务却从不更新,比不建更糟——",
|
|
1751
|
-
" 看板上会留下一个卡住的任务。",
|
|
1752
|
-
"",
|
|
1753
|
-
"⚠️ **卡片与任务一一对应**:只有你建了任务,用户才会收到一张进展卡片;",
|
|
1754
|
-
"没建任务就只有文字回复。所以**不要**为了「想要一张卡片」而去建任务,",
|
|
1755
|
-
"也不要建了任务又不用它——卡片上显示的就是你建的那个任务的真 id 和标题。",
|
|
1756
|
-
"",
|
|
1757
|
-
"## 环境事实(必须使用下面这些值,不要自己猜)",
|
|
1758
|
-
"",
|
|
1759
|
-
`- lark-cli profile:\`${profile}\`(飞书 openId 是 per-app 隔离的,换 profile 会失败)`,
|
|
1760
|
-
`- chat_id:\`${chatId}\``,
|
|
1761
|
-
`- message_id:\`${messageId}\``,
|
|
1762
|
-
`- event_id:\`${eventId}\``,
|
|
1763
|
-
"",
|
|
1764
|
-
"## 原始事件(完整 JSON)",
|
|
1765
|
-
"",
|
|
1766
|
-
"```json",
|
|
1767
|
-
JSON.stringify(input.rawEvent, null, 2),
|
|
1768
|
-
"```"
|
|
1692
|
+
""
|
|
1769
1693
|
];
|
|
1694
|
+
if (needsDispatchInjection(input.runtime)) {
|
|
1695
|
+
parts.push("## 任务纪律(Task #4889 — 来自 roy-agent core 单一真相源)", "", TASK_DISCIPLINE_PROMPT, "");
|
|
1696
|
+
}
|
|
1697
|
+
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), "```");
|
|
1770
1698
|
if (input.recommendedAction) {
|
|
1771
1699
|
parts.push("", "## 事件源附带的回复契约(背景资料,仅供你理解上下文)", "", "> ⚠️ 下面这段通常要求“由 agent 自己调 lark-cli 回复”。本次**不适用**:", "> 回复由守护进程负责。但其中关于内容编排的规范(单条、结构化、", "> 30KB 上限等)仍然值得遵守——你写的内容会被塞进那一条 post 里。", "", input.recommendedAction);
|
|
1772
1700
|
}
|
|
@@ -2001,11 +1929,17 @@ var TOOL_LABELS = {
|
|
|
2001
1929
|
task_complete: "完成任务",
|
|
2002
1930
|
task_operation_create: "记录进展"
|
|
2003
1931
|
};
|
|
2004
|
-
|
|
1932
|
+
var toolTraceSeq = 0;
|
|
1933
|
+
function describeToolUse(tool, seq, scope) {
|
|
1934
|
+
const use = typeof tool === "string" ? { name: tool } : tool;
|
|
1935
|
+
const name = use.name;
|
|
1936
|
+
const localId = use.id ?? `t-${seq ?? toolTraceSeq++}`;
|
|
2005
1937
|
return {
|
|
2006
|
-
title: TOOL_LABELS[normaliseMcpToolName(
|
|
2007
|
-
timestamp: new Date().toISOString(),
|
|
2008
|
-
category:
|
|
1938
|
+
title: TOOL_LABELS[normaliseMcpToolName(name)] ?? name,
|
|
1939
|
+
timestamp: use.timestamp ?? new Date().toISOString(),
|
|
1940
|
+
category: name,
|
|
1941
|
+
source: "tool-trace",
|
|
1942
|
+
id: scope !== undefined ? `${scope}#${localId}` : localId
|
|
2009
1943
|
};
|
|
2010
1944
|
}
|
|
2011
1945
|
function normaliseMcpToolName(tool) {
|
|
@@ -2210,7 +2144,8 @@ class Dispatcher {
|
|
|
2210
2144
|
chatId,
|
|
2211
2145
|
profile,
|
|
2212
2146
|
tools: result.tools,
|
|
2213
|
-
status: result.error ? "failed" : "running"
|
|
2147
|
+
status: result.error ? "failed" : "running",
|
|
2148
|
+
scope: messageId
|
|
2214
2149
|
});
|
|
2215
2150
|
}
|
|
2216
2151
|
if (result.error) {
|
|
@@ -2346,7 +2281,7 @@ class Dispatcher {
|
|
|
2346
2281
|
profile: input.profile,
|
|
2347
2282
|
status: input.status,
|
|
2348
2283
|
...handle.meta ? { meta: handle.meta } : {},
|
|
2349
|
-
processSteps: tools.map(describeToolUse)
|
|
2284
|
+
processSteps: tools.map((t, i) => describeToolUse(t, i, input.scope))
|
|
2350
2285
|
});
|
|
2351
2286
|
} catch (err) {
|
|
2352
2287
|
handle.error ??= errorMessage(err);
|
|
@@ -2377,7 +2312,8 @@ class Dispatcher {
|
|
|
2377
2312
|
prompt: buildDispatchPrompt({
|
|
2378
2313
|
rawEvent: input.raw,
|
|
2379
2314
|
profile: input.profile,
|
|
2380
|
-
recommendedAction: input.recommendedAction
|
|
2315
|
+
recommendedAction: input.recommendedAction,
|
|
2316
|
+
runtime: this.opts.runtime
|
|
2381
2317
|
}),
|
|
2382
2318
|
chatId: input.chatId,
|
|
2383
2319
|
messageId: input.messageId,
|
|
@@ -2640,7 +2576,11 @@ class CursorStreamDecoder {
|
|
|
2640
2576
|
const subtype = ev["subtype"];
|
|
2641
2577
|
if (subtype === "started") {
|
|
2642
2578
|
if (tool) {
|
|
2643
|
-
this.state.tools.push(
|
|
2579
|
+
this.state.tools.push({
|
|
2580
|
+
name: tool,
|
|
2581
|
+
timestamp: new Date().toISOString(),
|
|
2582
|
+
id: callId ?? undefined
|
|
2583
|
+
});
|
|
2644
2584
|
if (callId && isTaskCreateTool(tool)) {
|
|
2645
2585
|
this.state.pendingCreates.set(callId, {
|
|
2646
2586
|
tool,
|
|
@@ -3220,7 +3160,8 @@ async function startServe(options, deps) {
|
|
|
3220
3160
|
}, {
|
|
3221
3161
|
dryRun: options.dryRun,
|
|
3222
3162
|
replyEnabled: options.replyEnabled,
|
|
3223
|
-
agent: options.agent
|
|
3163
|
+
agent: options.agent,
|
|
3164
|
+
runtime: options.runtime
|
|
3224
3165
|
});
|
|
3225
3166
|
const listener = (deps.createListener ?? createEventListener)({
|
|
3226
3167
|
profile: options.profile,
|