@ai-setting/roy-task-plugin 0.7.8 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js 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
- this.tools.push(name);
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 not a chat assistant that occasionally writes notes. You are a worker
455
- whose output is a *visible, trackable* stream of tasks. Every non-trivial
456
- request (more than one tool call / more than a couple of minutes of work) MUST
457
- become a tracked task before you start doing it, and MUST be closed out
458
- explicitly when you finish.
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
- ### 3. Track — \`task_update\`
505
- Report progress as you go, not only at the end:
506
- - \`status\`: todo → active → (paused / cancelled) → completed
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.state.tools.includes(id)) {} else {
746
- this.state.tools.push(typeof id === "string" ? id : `cmd-${this.state.tools.length}`);
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(name ?? (typeof id === "string" && id ? id : `mcp-${this.state.tools.length}`));
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(b.name);
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
- function describeToolUse(tool) {
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(tool)] ?? tool,
2007
- timestamp: new Date().toISOString(),
2008
- category: tool
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(tool);
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,
@@ -3465,7 +3406,7 @@ function formatStatus(report) {
3465
3406
  }
3466
3407
 
3467
3408
  // ../../../packages/cli/src/main.ts
3468
- var VERSION = "0.7.8";
3409
+ var VERSION = "0.7.9";
3469
3410
  async function main(argv) {
3470
3411
  const parsed = parseArgs(argv);
3471
3412
  switch (parsed.command) {