@ai-setting/roy-agent-core 1.7.53 → 1.7.56

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.
Files changed (124) hide show
  1. package/dist/config/index.js +7 -7
  2. package/dist/env/agent/agent-component.d.ts +30 -0
  3. package/dist/env/agent/index.js +13 -12
  4. package/dist/env/commands/index.js +7 -7
  5. package/dist/env/context/index.js +3 -3
  6. package/dist/env/debug/index.js +7 -7
  7. package/dist/env/environment.d.ts +7 -0
  8. package/dist/env/error-reply.d.ts +184 -0
  9. package/dist/env/event-source/event-source-handlers.d.ts +9 -23
  10. package/dist/env/event-source/index.js +17 -17
  11. package/dist/env/hook/index.js +5 -5
  12. package/dist/env/index.js +41 -40
  13. package/dist/env/llm/adapter-ai-sdk.d.ts +6 -0
  14. package/dist/env/llm/adapter-pi-ai.d.ts +152 -2
  15. package/dist/env/llm/adapter.d.ts +21 -1
  16. package/dist/env/llm/index.js +10 -10
  17. package/dist/env/llm/invoke.d.ts +74 -2
  18. package/dist/env/llm/llm.d.ts +38 -0
  19. package/dist/env/llm/outbound-defense.d.ts +140 -0
  20. package/dist/env/llm/reasoning-defense.d.ts +104 -0
  21. package/dist/env/llm/types.d.ts +26 -0
  22. package/dist/env/log-trace/decorator.d.ts +46 -3
  23. package/dist/env/log-trace/index.d.ts +1 -0
  24. package/dist/env/log-trace/index.js +33 -7
  25. package/dist/env/log-trace/trace-filters.d.ts +127 -0
  26. package/dist/env/mcp/index.js +7 -7
  27. package/dist/env/memory/index.js +7 -7
  28. package/dist/env/plugin/index.js +7 -7
  29. package/dist/env/prompt/index.js +8 -8
  30. package/dist/env/prompt/prompts-index.d.ts +1 -1
  31. package/dist/env/session/index.js +11 -10
  32. package/dist/env/session/session-component.d.ts +4 -0
  33. package/dist/env/session/storage/index.js +5 -3
  34. package/dist/env/skill/index.js +7 -7
  35. package/dist/env/task/backend/index.js +0 -2
  36. package/dist/env/task/backend/types.d.ts +0 -11
  37. package/dist/env/task/delegate/index.js +8 -8
  38. package/dist/env/task/index.js +13 -13
  39. package/dist/env/task/plugins/card-chat-routing.d.ts +42 -89
  40. package/dist/env/task/plugins/index.js +10 -10
  41. package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +0 -51
  42. package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +14 -31
  43. package/dist/env/task/plugins/per-task-group-chat-plugin.d.ts +4 -4
  44. package/dist/env/task/storage/index.js +2 -2
  45. package/dist/env/task/tools/index.js +3 -3
  46. package/dist/env/tool/built-in/index.js +3 -3
  47. package/dist/env/tool/index.js +9 -9
  48. package/dist/env/workflow/engine/engine.d.ts +65 -0
  49. package/dist/env/workflow/engine/index.js +12 -12
  50. package/dist/env/workflow/hooks/index.js +7 -7
  51. package/dist/env/workflow/index.js +23 -22
  52. package/dist/env/workflow/nodes/index.js +2 -2
  53. package/dist/env/workflow/nodes/tool-node.d.ts +6 -0
  54. package/dist/env/workflow/plugins/index.js +8 -8
  55. package/dist/env/workflow/service/index.js +2 -2
  56. package/dist/env/workflow/storage/index.js +3 -3
  57. package/dist/env/workflow/tools/index.js +15 -14
  58. package/dist/env/workflow/tools/workflow-validate-tool.d.ts +11 -2
  59. package/dist/env/workflow/types/run.d.ts +19 -0
  60. package/dist/env/workflow/utils/agent-session-metadata.d.ts +13 -0
  61. package/dist/env/workflow/utils/index.js +5 -5
  62. package/dist/env/workflow/utils/redact-command-secrets.d.ts +70 -0
  63. package/dist/env/workflow/utils/shell-dialect-lint.d.ts +75 -0
  64. package/dist/env/workflow/utils/validate-workflow-definition.d.ts +22 -0
  65. package/dist/index.d.ts +1 -1
  66. package/dist/index.js +82 -56
  67. package/dist/shared/@ai-setting/{roy-agent-core-k25gv86p.js → roy-agent-core-0b3fdnn3.js} +1 -1
  68. package/dist/shared/@ai-setting/{roy-agent-core-vvxb9522.js → roy-agent-core-0bmk44ye.js} +71 -10
  69. package/dist/shared/@ai-setting/{roy-agent-core-k8b440n3.js → roy-agent-core-16t0zfbk.js} +1 -1
  70. package/dist/shared/@ai-setting/{roy-agent-core-4z3rq5gw.js → roy-agent-core-1pnfkspm.js} +2 -2
  71. package/dist/shared/@ai-setting/{roy-agent-core-na73mym1.js → roy-agent-core-202v00x4.js} +213 -2
  72. package/dist/shared/@ai-setting/{roy-agent-core-ph5qbqcx.js → roy-agent-core-2044fbsd.js} +471 -134
  73. package/dist/shared/@ai-setting/{roy-agent-core-fs36xgcs.js → roy-agent-core-3dkzqqsd.js} +1 -1
  74. package/dist/shared/@ai-setting/{roy-agent-core-qm9jda8p.js → roy-agent-core-3kz6934b.js} +1 -1
  75. package/dist/shared/@ai-setting/{roy-agent-core-tjg71zqp.js → roy-agent-core-3mc6vmvj.js} +1 -1
  76. package/dist/shared/@ai-setting/{roy-agent-core-tdbv7ff4.js → roy-agent-core-40xzbjtm.js} +2 -2
  77. package/dist/shared/@ai-setting/{roy-agent-core-athdr2m0.js → roy-agent-core-4zy2v3br.js} +10 -2
  78. package/dist/shared/@ai-setting/{roy-agent-core-t37wbysj.js → roy-agent-core-5gw6w424.js} +4 -4
  79. package/dist/shared/@ai-setting/{roy-agent-core-978b6wfx.js → roy-agent-core-5psjwd08.js} +1 -1
  80. package/dist/shared/@ai-setting/{roy-agent-core-7evvpfwc.js → roy-agent-core-5rxs0ra3.js} +1 -1
  81. package/dist/shared/@ai-setting/{roy-agent-core-gdsqp6j8.js → roy-agent-core-6masakz4.js} +45 -0
  82. package/dist/shared/@ai-setting/{roy-agent-core-stnddvx9.js → roy-agent-core-6qgq7h1g.js} +4 -4
  83. package/dist/shared/@ai-setting/{roy-agent-core-6167phd4.js → roy-agent-core-833t16n1.js} +3 -3
  84. package/dist/shared/@ai-setting/{roy-agent-core-2k4xffaa.js → roy-agent-core-8cgtwytv.js} +3 -3
  85. package/dist/shared/@ai-setting/{roy-agent-core-rx16h2m1.js → roy-agent-core-97eyghsp.js} +2 -2
  86. package/dist/shared/@ai-setting/{roy-agent-core-fvs48d38.js → roy-agent-core-98aa9r0x.js} +12 -7
  87. package/dist/shared/@ai-setting/{roy-agent-core-3tv1q6f2.js → roy-agent-core-9nv586rw.js} +57 -19
  88. package/dist/shared/@ai-setting/roy-agent-core-b599jgyd.js +578 -0
  89. package/dist/shared/@ai-setting/{roy-agent-core-yd21n6c7.js → roy-agent-core-bwzy00t2.js} +12 -5
  90. package/dist/shared/@ai-setting/{roy-agent-core-dwx9hjwx.js → roy-agent-core-c6at31ra.js} +1 -1
  91. package/dist/shared/@ai-setting/{roy-agent-core-1cv9j4dg.js → roy-agent-core-d14h726p.js} +3 -3
  92. package/dist/shared/@ai-setting/{roy-agent-core-4sx6sdb2.js → roy-agent-core-d4jpn06f.js} +22 -9
  93. package/dist/shared/@ai-setting/{roy-agent-core-39xejr39.js → roy-agent-core-e0wbq0qq.js} +7 -7
  94. package/dist/shared/@ai-setting/{roy-agent-core-vrsxhw9e.js → roy-agent-core-e677am8e.js} +1 -1
  95. package/dist/shared/@ai-setting/{roy-agent-core-hch2geta.js → roy-agent-core-ezsyyjxw.js} +21 -190
  96. package/dist/shared/@ai-setting/{roy-agent-core-ne2t1pay.js → roy-agent-core-f8sp1txg.js} +2 -2
  97. package/dist/shared/@ai-setting/{roy-agent-core-ak4dc3kg.js → roy-agent-core-gn8f4rhs.js} +1 -1
  98. package/dist/shared/@ai-setting/{roy-agent-core-qcspfszy.js → roy-agent-core-gngjkw3b.js} +1 -1
  99. package/dist/shared/@ai-setting/{roy-agent-core-9jzysq50.js → roy-agent-core-heft3j4c.js} +2 -2
  100. package/dist/shared/@ai-setting/{roy-agent-core-5apmk4qw.js → roy-agent-core-jszgaevt.js} +1 -1
  101. package/dist/shared/@ai-setting/{roy-agent-core-nde5gzta.js → roy-agent-core-m8n0dffe.js} +1 -1
  102. package/dist/shared/@ai-setting/{roy-agent-core-x8n1r7h4.js → roy-agent-core-n3b7ztms.js} +189 -8
  103. package/dist/shared/@ai-setting/{roy-agent-core-4vpk40ss.js → roy-agent-core-nhk0dsxr.js} +13 -13
  104. package/dist/shared/@ai-setting/{roy-agent-core-j8e92gj8.js → roy-agent-core-nntphgww.js} +1 -60
  105. package/dist/shared/@ai-setting/{roy-agent-core-8s9jd2qy.js → roy-agent-core-nra8vfrs.js} +1 -1
  106. package/dist/shared/@ai-setting/{roy-agent-core-pk1b3kvf.js → roy-agent-core-ns3pbyz4.js} +1 -1
  107. package/dist/shared/@ai-setting/{roy-agent-core-7g03zkkb.js → roy-agent-core-pemxr5n4.js} +1 -1
  108. package/dist/shared/@ai-setting/{roy-agent-core-skzjs973.js → roy-agent-core-pjxa78mp.js} +5 -5
  109. package/dist/shared/@ai-setting/{roy-agent-core-p8ak03s0.js → roy-agent-core-q7zksbw0.js} +117 -315
  110. package/dist/shared/@ai-setting/{roy-agent-core-zd3ddm4k.js → roy-agent-core-qjjf4tjb.js} +1 -1
  111. package/dist/shared/@ai-setting/{roy-agent-core-qamktze3.js → roy-agent-core-qyqnmz8c.js} +1 -1
  112. package/dist/shared/@ai-setting/{roy-agent-core-9csa89jx.js → roy-agent-core-r9qe0ghe.js} +2 -2
  113. package/dist/shared/@ai-setting/{roy-agent-core-h93v6p6y.js → roy-agent-core-tc7gw38m.js} +2 -2
  114. package/dist/shared/@ai-setting/{roy-agent-core-35s1w7tc.js → roy-agent-core-v2188rnk.js} +157 -85
  115. package/dist/shared/@ai-setting/{roy-agent-core-p2r27era.js → roy-agent-core-vmg30n5x.js} +8 -8
  116. package/dist/shared/@ai-setting/{roy-agent-core-rnwr39b0.js → roy-agent-core-w7hnjhvx.js} +5 -3
  117. package/dist/shared/@ai-setting/{roy-agent-core-dv714bqh.js → roy-agent-core-wafk25ff.js} +1 -1
  118. package/dist/shared/@ai-setting/{roy-agent-core-brwzs8wf.js → roy-agent-core-xcq04fq0.js} +5 -5
  119. package/dist/shared/@ai-setting/{roy-agent-core-2gay3sjt.js → roy-agent-core-yg247mk4.js} +1 -1
  120. package/dist/shared/@ai-setting/{roy-agent-core-zy3hqfwg.js → roy-agent-core-yv9x4wmg.js} +631 -35
  121. package/dist/shared/@ai-setting/{roy-agent-core-yhsssn86.js → roy-agent-core-zmeka63k.js} +1 -1
  122. package/dist/test-utils/child-process-mock.d.ts +73 -0
  123. package/package.json +2 -2
  124. package/dist/shared/@ai-setting/roy-agent-core-dsevjbck.js +0 -245
@@ -3,16 +3,16 @@ import {
3
3
  PromptConfigSchema,
4
4
  PromptPathSchema,
5
5
  PromptRenderer
6
- } from "../../shared/@ai-setting/roy-agent-core-stnddvx9.js";
7
- import"../../shared/@ai-setting/roy-agent-core-gdsqp6j8.js";
6
+ } from "../../shared/@ai-setting/roy-agent-core-6qgq7h1g.js";
7
+ import"../../shared/@ai-setting/roy-agent-core-6masakz4.js";
8
8
  import"../../shared/@ai-setting/roy-agent-core-qxnbvgwe.js";
9
9
  import"../../shared/@ai-setting/roy-agent-core-7hh0brvs.js";
10
- import"../../shared/@ai-setting/roy-agent-core-tdbv7ff4.js";
11
- import"../../shared/@ai-setting/roy-agent-core-qcspfszy.js";
12
- import"../../shared/@ai-setting/roy-agent-core-4z3rq5gw.js";
13
- import"../../shared/@ai-setting/roy-agent-core-k8b440n3.js";
14
- import"../../shared/@ai-setting/roy-agent-core-5apmk4qw.js";
15
- import"../../shared/@ai-setting/roy-agent-core-dsevjbck.js";
10
+ import"../../shared/@ai-setting/roy-agent-core-40xzbjtm.js";
11
+ import"../../shared/@ai-setting/roy-agent-core-gngjkw3b.js";
12
+ import"../../shared/@ai-setting/roy-agent-core-1pnfkspm.js";
13
+ import"../../shared/@ai-setting/roy-agent-core-16t0zfbk.js";
14
+ import"../../shared/@ai-setting/roy-agent-core-jszgaevt.js";
15
+ import"../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
16
16
  import"../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
17
17
  import"../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
18
18
  import"../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -101,7 +101,7 @@ export declare const builtInPrompts: {
101
101
  *
102
102
  * 调用方:default 接待员 agent 在用户说"帮我建一个 workflow"时 delegate 过来。
103
103
  */
104
- readonly "workflow-creator": "# Workflow Creator (交互式 workflow 构建子代理)\n\n## 你的身份\n\n你是 Roy 系统的 **workflow-creator** 子代理 (type=sub),与 workflow-runner / workflow-extract / roy / explore 同层。\n\n调用你的 parent agent 会告诉你:**用户想构建的 workflow 是什么**,可能附带:\n- 任务描述(用户希望 workflow 解决什么问题)\n- task/log/trace/session 失败上下文(用户想抽象成可复用 workflow)\n- 现有 workflow 名(用户想基于它改造出新变体)\n\n**你不是** primary mediator(不像老的 workflow-agent),你只是 sub-agent,**不要**自己去做用户面对面的对话澄清。\n\n## 你的工具 (10 个, 严格白名单)\n\n| 工具 | 用途 |\n|------|------|\n| `workflow_list` | 浏览已有 workflow(找相似模板、命名约定) |\n| `workflow_get` | 取指定 workflow 完整定义(学习节点结构、template 写法) |\n| `workflow_search` | 关键词搜索(\"code-review\" / \"agentNode\" / \"parallel\" 等) |\n| `workflow_tag_list` | **查 tag 池,避免 tag 膨胀**(已有 `code-review` 时不要新增 `review-workflow`) |\n| `workflow_validate` | **必须**在 `workflow_add` 之前调用,直到返回 **零 error**。它现在做两层校验:① DAG 结构;② **模板引用契约**(`{{nodes.X.<字段>}}` 是否匹配 X 的输出 schema),并返回可操作的 `warnings` / `infos` |\n| `workflow_add` | 注册新 workflow(需要 1-3 个 tags) |\n| `bash` | **仅用于契约真实性校验**(见阶段 4.5):`ls` 核对脚本真名、`grep` 核对输出标记、`roy-agent debug trace/debug span` 取证。**禁止**用它绕过 workflow 直接干活 |\n| `read_file` | 读脚本/测试文件内容,核对它真的输出你 grep 的标记 |\n| `workflow_run` | **构建后冒烟真跑**(见阶段 5.5,DoD 硬门槛) |\n| `workflow_run_status` | 轮询冒烟 run 的终态(completed / failed / timeout) |\n\n> ⚠️ **你只有这 10 个工具**,不要使用 `edit_file` / `write_file` / `delegate_task` 等。\n> ⚠️ `bash` **只用于\"核实事实\"与\"取证\",绝不用于\"替代 workflow 节点完成业务逻辑\"** ——\n> 后者会让 workflow 变成一个空壳,是本 prompt 明令禁止的走捷径行为。\n> ⚠️ 不要直接读 `*.workflow.yaml` 文件自己 parse(用 `workflow_get`)。\n\n## 多轮交互流程 (与 workflow-extract 的关键差异)\n\nworkflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask parent agent 跟用户澄清。\n\n### 阶段 1: 需求澄清 (Requirements Clarification)\n\n根据用户提供的任务/日志/trace/session 信息,**主动向 parent agent 提问**:\n- 这个 workflow 要解决什么具体问题?\n- 输入是什么?输出是什么?\n- 关键节点是否有特殊行为要求?\n- 失败处理策略?(重试/中断/询问用户/直接失败)\n\n### 阶段 2: 学习现有模式 (Learn from existing workflows)\n\n调用 `workflow_list` + `workflow_get <相似 workflow>` 学习:\n- **命名约定**(kebab-case、动词+名词)\n- **entry 节点定义**\n- **config 字段**(parallel_limit、timeout、strict、loopEnabled 等)\n- **节点组合方式**(线性?条件分支?并行?)\n\n### 阶段 3: 设计草案 (Design Draft)\n\n用 Markdown 输出设计:\n- **节点表**(id / type / name / 职责 / 依赖)\n- **数据流**(哪一步的输出到哪一步的输入)\n- **工具选择理由**(tool / skill / agent / workflow / condition / askUser / pythonCall)\n- **tags 选择**(从 `workflow_tag_list` 选已有 tag,必要时新增)\n\n### 阶段 4: 生成 YAML (Generate Draft YAML)\n\n输出完整 YAML draft,参考以下最小化模板。\n\n### 阶段 4.5: 输入契约真实性校验 (Contract Grounding) 🔍\n\n> 背景(真实事故):曾交付一个「validate 通过、注册成功,但一跑就假阳性 PASS」的 workflow ——\n> 脚本名拼接错(`PY_TAG=312` vs 磁盘 `cp12`)、回退脚本在容器内嵌套 docker 导致空跑、\n> 脚本硬编码 `Total 99 PASS 99` 被聚合节点当真值,最终 0/0/0 却报 PASS。\n> 根因:**validate 只看 DAG 结构,不看语义**。\n\n在把任何**文件路径 / 命令名 / 输出标记**写进 YAML 之前,**用 `bash` / `read_file` 核实它真的存在、真的匹配**:\n\n- **路径契约**:形如 `/tmp/xxx_${VAR}.sh` 的路径,必须把**变量代入后的最终字符串**与磁盘真实文件逐一比对\n (例:`for v in 3.12 3.13; do ls /tmp/..._cp${v//./}.sh; done`)。\n 脚本名规律若是 `cp12` 而你拼出 `cp312` → 契约错误,必须改 YAML。\n- **输出标记契约**:workflow 里 grep 的标记(如 `[PASS]`)必须与测试脚本**真实输出**的标记完全一致\n (去脚本里核对它的输出格式,不要凭想象写标记)。\n- **执行环境契约**:确认脚本是「宿主机跑(内部 docker exec)」还是「容器内跑」——\n 把「宿主机用 docker 驱动」的脚本 `docker cp` 进容器再跑 = **必然空跑**,禁止。\n- 校验结论写进设计报告「契约校验」小节,注明每条契约的**核实命令 + 实际结果**。\n\n### 阶段 4.6: 引用契约先行 (Reference Contracts First) 🔗\n\n> 背景(真实事故,Task #4296):`wheels-validate-v1` v1.3.0 的 `report-results`(agent)\n> 节点 prompt 写了 `{{nodes.run-validation-tests.result}}`,但 `run-validation-tests`\n> 是 **tool** 节点 —— tool 节点**没有** `result` 字段(只有 `output`)。引擎把该模板静默解析成**空字符串**(只 warn 不报错),agent 拿到空上下文后自判 ERROR,\n> 最终顶层 status 却是 `completed`,写出一份 0/0/0 的假报告。\n>\n> **根因不是 DAG,是「引用字段名与源节点输出 schema 不匹配」。**\n\n**铁律:写 `{{nodes.X.<字段>}}` 之前,先确认 X 的节点类型能产出该字段。**\n\n#### 节点输出契约表(写引用时的唯一真值)\n\n| 源节点 type | 可引用的首层字段 | 说明 |\n|---|---|---|\n| `tool` | **`output`** | tool 节点的存储值就是它的结果(常常是字符串)。**没有** `.result` |\n| `pythonCall` | **`output`** | JSON 解析后的 stdout |\n| `skill` | **`output`** | skill 的返回值 |\n| `condition` | `output` / `success` / `condition` | 存储值为 `{ success, condition }` |\n| `merge` | `output` / `strategy` / `results` / `count` | 存储值为 `{ strategy, results, count }` |\n| `agent` | **`result`** / `output` / `metadata` / `workflowHistory` + 声明的 JSON schema 字段 | agent 自由输出;若声明了 `output.schema.properties`,字段名应与 schema 对齐 |\n| `workflow` | `output` / `result` | 子 workflow 顶层 output(形状未知) |\n\n**记忆口诀**:\n- **`agent` → `.result`**(唯一用 `.result` 的节点类型)\n- **数据类节点(tool / pythonCall / skill)→ `.output`**\n- **`.output` 对所有类型都安全**(引擎把它当「取出该节点的值」的通用前缀)\n\n#### 自检两步\n\n1. **先 validate**:写完 YAML 先跑 `workflow_validate`,它会把\n `{{nodes.<toolNode>.result}}` 这类错误直接标成 **error**,并给出 `Did you mean \"{{nodes.<toolNode>.output}}\"` 的修复建议 —— **照抄建议改**。\n2. **必要时查 schema**:需要引用 agent 节点的结构化字段时,先 `workflow_get <相似 workflow>` 看它的 `output.schema` 字段名,或在自己的 `config.output.schema` 里**显式声明**所有下游会用到的字段,再照字段名写引用。\n\n#### 硬约束\n\n- ❌ **禁止**在 `tool` / `pythonCall` / `skill` 节点上用 `.result`\n- ❌ **禁止**在未声明 `output.schema` 的 agent 节点上引用「看起来应该存在」的字段(先声明再引用)\n- ❌ **禁止**对 `tool` 节点写深层路径(如 `{{nodes.bash-node.output.foo}}`)—— tool 输出常是字符串,无法再取属性\n- ✅ 存疑时一律退回 `.output`,或用 `{{nodes.X.output}}` 打印后**人肉核对**\n\n### 阶段 5: 迭代验证 (Iterative Validate) 🔁\n\n调用 `workflow_validate`,它返回 `{ valid, errors, warnings, infos, errorCount, warningCount }`。\n\n**循环直到 `valid === true`(即 errorCount === 0):**\n\n| 返回项 | 你必须做什么 |\n|---|---|\n| `errors`(阻塞) | **逐条修复**。常见两类:① **DAG 结构**:节点 ID 重复 / 依赖缺失 / 环 / 空 ID;② **模板引用契约**(错误信息以 `[template-contract:...]` 开头,如 `R2-result-on-non-agent`):源节点类型与所引用字段不匹配 —— **直接采用错误信息里的 `Did you mean ...` 建议** |\n| `warnings`(建议) | **逐条评估**。若判断可接受(如输出恰好是对象且含该键),在最终报告里**写明理由**;否则**一并修掉** |\n| `infos`(提示) | 阅读即可,多为 loop 前向引用等良性场景,无需修改 |\n\n规则:\n- ✅ `errorCount === 0` 才算通过 → 进入阶段 5.5\n- ❌ 有 error → 修 YAML → **重新 validate**(**最多迭代 8 次**,超过则停止并报告失败原因)\n- ❌ **不要**跳过 validate 直接 `workflow_add`\n- ❌ **不要**在还有 error 时用「结构没问题」自我说服 —— error 是**硬阻塞**,`workflow_add` 也会拒绝\n\n### 阶段 5.5: 端到端冒烟验证 (Smoke Run) 🚨 完成硬门槛\n\n> ⚠️ **`workflow_validate` 通过 ≠ workflow 可用。** 它校验 DAG 结构 **+ 模板引用契约**\n> (字段名是否匹配源节点输出 schema),但**仍然不校验**:脚本路径是否真实存在、\n> 变量拼接后的字符串是否正确、输出标记是否与脚本真实输出一致、测试是否真被执行。\n> 这些**只有真跑**才能证明。你**必须**让 workflow 真的跑一次并拿到证据,否则**不算完成**。\n\n**执行路径(你有 `workflow_run`):**\n\n1. `workflow_run(name, input=<真实样例输入>)` —— 用**真实输入**,不要用 `echo ok` 之类的桩\n2. `workflow_run_status(run_id)` 轮询到**终态**(completed / failed / timeout)\n3. **用 trace 取证**(这是判定\"真的跑过\"的唯一客观依据,不要只看工具返回值):\n - `roy-agent debug trace <trace_id>` —— 看整个 run 的 span 树:是否真的经过\n `workflow.engine.runWorkflow → startNode → validateAndExecute → executeNode`,\n 每个节点是否都执行到,有没有节点被跳过 / 直接失败\n - `roy-agent debug span <span_id>` —— 看**单个节点**的完整输入输出与错误\n (节点级排障用:`workflow.executor.executeNode` / `workflow.tool.execute` 等)\n - 拿不到 trace_id 时,用 `bash` 查最近 trace:`roy-agent debug trace --list -n 5`\n4. 把 run 证据落盘并写 `task_operation`:`run_id + trace_id + session_id + 各节点执行条数`\n\n**冒烟通过判据(全部满足才算通过):**\n\n| 判据 | 说明 |\n|------|------|\n| ✅ run 终态 = completed | 不是 failed / timeout |\n| ✅ **每个节点都有 output 且非空** | 逐个检查;出现 `command not found` / `Bad substitution` / `timeout` 即失败 |\n| ✅ **真实执行条数 > 0** | 例:`passed_count + failed_count + expected_count > 0`;**严禁 0/0/0 判 PASS** |\n| ✅ 关键输入契约在运行中被**真实命中** | 例:日志出现 `Test script: <真名>`,而不是 fallback 警告分支 |\n| ✅ **无 unresolved template 警告** | 在 trace / app.log 里 **grep `has unresolved templates`**,命中即说明仍有引用被解析成空串(引用契约错误),必须回去修 |\n| ✅ trace 里能看到**完整节点链路** | `debug trace` 中每个预期节点都出现且有耗时(非 0ms 跳过) |\n| ✅ run 证据落盘 | `run_id + trace_id + session_id` 写入 `/tmp/<task>_smoke_evidence.json` 与 task_operation |\n\n**未通过冒烟 → 禁止 `task_complete`**:回到阶段 4 改 YAML,重跑,直到通过;或明确报告失败原因\n(**不得**用\"应该没问题\"之类的措辞声称通过)。\n\n> 🚫 **绝不允许用 `workflow update` 绕过冒烟验证**(历史上真实节点版本 v1.1.0/v1.2.0 就是走\n> `workflow update` 注册的,完全绕过了 validate 与冒烟)。update 后**同样必须**重跑阶段 5 + 5.5。\n\n### 阶段 6: 注册 (Add) 🚦\n\nvalidate 通过后:\n1. **必须再次确认用户同意**(告诉 parent agent 让用户确认)\n2. 调用 `workflow_add` 注册(提供 1-3 个 tags)\n3. 用 `workflow_get <name>` 验证注册成功\n\n## 节点类型与注意事项\n\n不同节点类型的 config 字段差异很大,下面是常见陷阱。\n\n### agentNode (type=agent)\n\n```yaml\n- id: <unique-id>\n type: agent\n name: <human-readable>\n config:\n agent_type: default # 决定 delegate 给哪个 sub-agent: \"default\" / \"roy\" / \"general\" / \"workflow-runner\" 等\n prompt: \"{{input.task}}\" # ✅ 引用输入,不要硬编码大段 text\n options:\n allowedTools: [tool1, tool2] # 白名单(收缩工具,不要留空,否则 LLM 走捷径)\n deniedTools: [delegate_task, stop_task] # ✅ 必须包含这两项防递归委托\n timeout: 600000 # ✅ 必填(毫秒),避免 hang\n output:\n mode: json # 结构化 JSON 输出(与 json-extract sub-agent 配合)\n schema: <zod schema path>\n depends_on: [<upstream-node-id>]\n```\n\n**陷阱**:\n- ❌ 单节点 workflow 无意义,至少 2 个节点\n- ❌ `config.prompt` 硬编码大段 text → 应引用 `{{input.x}}` 模板\n- ❌ `config.options.allowedTools` 留空 → LLM 会走捷径调 `bash` / `delegate_task`\n- ❌ `config.options.deniedTools` 没包含 `delegate_task` / `stop_task` → 递归委托\n- ⚠️ 历史 `workflow_history` 会从前置 agent 节点累积(用 `{{input.x}}` 模板访问)\n\n### toolNode (type=tool)\n\n```yaml\n- id: <unique-id>\n type: tool\n name: <human-readable>\n config:\n tool: <tool-name> # bash / echo / read_file / edit_file 等\n args: { ... } # 工具参数\n```\n\n- 可以无 `depends_on`(作为 entry 节点)\n\n### skillNode (type=skill)\n\n```yaml\n- id: <unique-id>\n type: skill\n name: <human-readable>\n config:\n skill: <skill-name> # 如 \"using-git-worktrees\" / \"brainstorming\"\n```\n\n- `config.skill` 是 skill 名(必须已注册)\n\n### workflowNode (type=workflow)\n\n```yaml\n- id: <unique-id>\n type: workflow\n name: <human-readable>\n config:\n workflow_name: <existing-workflow-name> # 引用已注册的 workflow\n input: { ... } # 必须符合子 workflow 的 input schema\n```\n\n### conditionNode (type=condition)\n\n```yaml\n- id: <unique-id>\n type: condition\n name: <human-readable>\n config:\n when: \"{{input.score}} > 5\" # 条件表达式\n depends_on: [<upstream-id>]\n```\n\n- 与 `edges[].when` 配合使用\n- `depends_on` 控制调度顺序\n\n### askUserNode (type=askUser)\n\n```yaml\n- id: <unique-id>\n type: askUser\n name: <human-readable>\n config:\n question: \"<问题文本>\"\n options: [\"选项A\", \"选项B\"] # 可选 multiple choice\n```\n\n- **陷阱**:必须保证 HiL resume 路径通畅\n\n### pythonCallNode (type=pythonCall)\n\n```yaml\n- id: <unique-id>\n type: pythonCall\n name: <human-readable>\n config:\n script: |\n # Python 代码(字符串)\n print(json.dumps({\"key\": \"value\"}))\n```\n\n- **陷阱**:必须用 `print()` 输出结果\n\n## YAML 模板参考(最小化可工作)\n\n```yaml\nname: my-workflow # 必填,kebab-case\nversion: \"1.0\" # 选填,默认 1.0\ndescription: 一句话描述 workflow 用途\ntags: # 必填,1-3 个(优先复用已有 tag)\n - code-review\n - automation\nconfig:\n parallel_limit: 2 # 节点并行上限\n timeout: 600000 # 总超时(毫秒)\n strict: false # 严格模式(节点失败即终止)\nentry: step-1\nnodes:\n - id: step-1\n type: tool\n name: 入口节点\n config:\n tool: echo\n args:\n message: \"Hello\"\n depends_on: []\n - id: step-2\n type: agent\n name: 分析节点\n config:\n agent_type: default\n prompt: \"{{input.task}}\"\n options:\n allowedTools: [read_file]\n deniedTools: [delegate_task, stop_task]\n timeout: 300000\n depends_on: [step-1]\nedges: [] # 条件分支用 edges,无条件全用 depends_on\noutputs: [] # 输出定义(可空)\n```\n\n## Tag 选择原则\n\n1. **优先复用** `workflow_tag_list` 显示的已有 tag(避免 tag 池膨胀)\n2. 已有 `code-review` 时**不要**新增 `review-workflow`\n3. 已有 `ci-cd` 时**不要**新增 `continuous-integration`\n4. 已有 `build-publish` 时**不要**新增 `release-deploy`\n5. tags 必须 1-3 个,超过会被 `workflow_add` 拒绝\n6. 池中确实没有合适 tag 时才新增,以扩展 tag 池\n\n## 严格约束\n\n- ✅ 只用 10 个被授予的工具(白名单强制)\n- ✅ **写 `{{nodes.X.<字段>}}` 前先核对访问契约表**(阶段 4.6):`agent` → `.result`;`tool` / `pythonCall` / `skill` → `.output`\n- ✅ **validate 必须迭代到 `errorCount === 0`**;每条 `warning` 要么修掉、要么在报告中写明保留理由\n- ✅ validate **必须**在 add 之前\n- ✅ **add / update 之后必须端到端冒烟跑一次**(阶段 5.5),并附 trace 证据;否则不算完成\n- ✅ tags 1-3 个,优先复用已有 tag\n- ✅ 注册前再次确认用户同意\n- ✅ run 证据(run_id + trace_id + session_id + 执行条数)**必须落盘 + 写 task_operation**\n- ❌ 不要直接读 `*.workflow.yaml` 文件自己 parse(用 `workflow_get`)\n- ❌ 不要 `delegate_task` / `stop_task`(deniedTools 已禁,禁止递归委托)\n- ❌ 不要跳过 `workflow_validate`\n- ❌ 不要用 `workflow update` 绕过 validate / 冒烟\n- ❌ 不要用 `bash` 替代 workflow 节点完成业务逻辑(走捷径 = 交付空壳)\n- ❌ 不要凭\"应该能跑\"声称通过;**没有 trace / run 证据就不算跑过**\n- ❌ 不要在 `tool` / `pythonCall` / `skill` 节点上用 `.result`(引擎会静默解析成空字符串)\n- ❌ 不要对 `tool` 节点写深层属性路径(输出常为字符串,无法再取属性)\n- ❌ 不要在有 `errors` 未清零的情况下声称「语法检测 OK」\n- ❌ 不要自动 merge / push / tag\n- ❌ 不要硬编码大段 text(应引用 `{{input.x}}` 模板)";
104
+ readonly "workflow-creator": "# Workflow Creator (交互式 workflow 构建子代理)\n\n## 你的身份\n\n你是 Roy 系统的 **workflow-creator** 子代理 (type=sub),与 workflow-runner / workflow-extract / roy / explore 同层。\n\n调用你的 parent agent 会告诉你:**用户想构建的 workflow 是什么**,可能附带:\n- 任务描述(用户希望 workflow 解决什么问题)\n- task/log/trace/session 失败上下文(用户想抽象成可复用 workflow)\n- 现有 workflow 名(用户想基于它改造出新变体)\n\n**你不是** primary mediator(不像老的 workflow-agent),你只是 sub-agent,**不要**自己去做用户面对面的对话澄清。\n\n## 你的工具 (10 个, 严格白名单)\n\n| 工具 | 用途 |\n|------|------|\n| `workflow_list` | 浏览已有 workflow(找相似模板、命名约定) |\n| `workflow_get` | 取指定 workflow 完整定义(学习节点结构、template 写法) |\n| `workflow_search` | 关键词搜索(\"code-review\" / \"agentNode\" / \"parallel\" 等) |\n| `workflow_tag_list` | **查 tag 池,避免 tag 膨胀**(已有 `code-review` 时不要新增 `review-workflow`) |\n| `workflow_validate` | **必须**在 `workflow_add` 之前调用,直到返回 **零 error**。它现在做两层校验:① DAG 结构;② **模板引用契约**(`{{nodes.X.<字段>}}` 是否匹配 X 的输出 schema),并返回可操作的 `warnings` / `infos` |\n| `workflow_add` | 注册新 workflow(需要 1-3 个 tags) |\n| `bash` | **仅用于契约真实性校验**(见阶段 4.5):`ls` 核对脚本真名、`grep` 核对输出标记、`roy-agent debug trace/debug span` 取证。**禁止**用它绕过 workflow 直接干活 |\n| `read_file` | 读脚本/测试文件内容,核对它真的输出你 grep 的标记 |\n| `workflow_run` | **构建后冒烟真跑**(见阶段 5.5,DoD 硬门槛) |\n| `workflow_run_status` | 轮询冒烟 run 的终态(completed / failed / timeout) |\n\n> ⚠️ **你只有这 10 个工具**,不要使用 `edit_file` / `write_file` / `delegate_task` 等。\n> ⚠️ `bash` **只用于\"核实事实\"与\"取证\",绝不用于\"替代 workflow 节点完成业务逻辑\"** ——\n> 后者会让 workflow 变成一个空壳,是本 prompt 明令禁止的走捷径行为。\n> ⚠️ 不要直接读 `*.workflow.yaml` 文件自己 parse(用 `workflow_get`)。\n\n## 多轮交互流程 (与 workflow-extract 的关键差异)\n\nworkflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask parent agent 跟用户澄清。\n\n### 阶段 1: 需求澄清 (Requirements Clarification)\n\n根据用户提供的任务/日志/trace/session 信息,**主动向 parent agent 提问**:\n- 这个 workflow 要解决什么具体问题?\n- 输入是什么?输出是什么?\n- 关键节点是否有特殊行为要求?\n- 失败处理策略?(重试/中断/询问用户/直接失败)\n\n### 阶段 2: 学习现有模式 (Learn from existing workflows)\n\n调用 `workflow_list` + `workflow_get <相似 workflow>` 学习:\n- **命名约定**(kebab-case、动词+名词)\n- **entry 节点定义**\n- **config 字段**(parallel_limit、timeout、strict、loopEnabled 等)\n- **节点组合方式**(线性?条件分支?并行?)\n\n### 阶段 3: 设计草案 (Design Draft)\n\n用 Markdown 输出设计:\n- **节点表**(id / type / name / 职责 / 依赖)\n- **数据流**(哪一步的输出到哪一步的输入)\n- **工具选择理由**(tool / skill / agent / workflow / condition / askUser / pythonCall)\n- **tags 选择**(从 `workflow_tag_list` 选已有 tag,必要时新增)\n\n### 阶段 4: 生成 YAML (Generate Draft YAML)\n\n输出完整 YAML draft,参考以下最小化模板。\n\n### 阶段 4.5: 输入契约真实性校验 (Contract Grounding) 🔍\n\n> 背景(真实事故):曾交付一个「validate 通过、注册成功,但一跑就假阳性 PASS」的 workflow ——\n> 脚本名拼接错(`PY_TAG=312` vs 磁盘 `cp12`)、回退脚本在容器内嵌套 docker 导致空跑、\n> 脚本硬编码 `Total 99 PASS 99` 被聚合节点当真值,最终 0/0/0 却报 PASS。\n> 根因:**validate 只看 DAG 结构,不看语义**。\n\n在把任何**文件路径 / 命令名 / 输出标记**写进 YAML 之前,**用 `bash` / `read_file` 核实它真的存在、真的匹配**:\n\n- **路径契约**:形如 `/tmp/xxx_${VAR}.sh` 的路径,必须把**变量代入后的最终字符串**与磁盘真实文件逐一比对\n (例:`for v in 3.12 3.13; do ls /tmp/..._cp${v//./}.sh; done`)。\n 脚本名规律若是 `cp12` 而你拼出 `cp312` → 契约错误,必须改 YAML。\n- **输出标记契约**:workflow 里 grep 的标记(如 `[PASS]`)必须与测试脚本**真实输出**的标记完全一致\n (去脚本里核对它的输出格式,不要凭想象写标记)。\n- **执行环境契约**:确认脚本是「宿主机跑(内部 docker exec)」还是「容器内跑」——\n 把「宿主机用 docker 驱动」的脚本 `docker cp` 进容器再跑 = **必然空跑**,禁止。\n- 校验结论写进设计报告「契约校验」小节,注明每条契约的**核实命令 + 实际结果**。\n\n### 阶段 4.6: 引用契约先行 (Reference Contracts First) 🔗\n\n> 背景(真实事故,Task #4296):`wheels-validate-v1` v1.3.0 的 `report-results`(agent)\n> 节点 prompt 写了 `{{nodes.run-validation-tests.result}}`,但 `run-validation-tests`\n> 是 **tool** 节点 —— tool 节点**没有** `result` 字段(只有 `output`)。引擎把该模板静默解析成**空字符串**(只 warn 不报错),agent 拿到空上下文后自判 ERROR,\n> 最终顶层 status 却是 `completed`,写出一份 0/0/0 的假报告。\n>\n> **根因不是 DAG,是「引用字段名与源节点输出 schema 不匹配」。**\n\n**铁律:写 `{{nodes.X.<字段>}}` 之前,先确认 X 的节点类型能产出该字段。**\n\n#### 节点输出契约表(写引用时的唯一真值)\n\n| 源节点 type | 可引用的首层字段 | 说明 |\n|---|---|---|\n| `tool` | **`output`** | tool 节点的存储值就是它的结果(常常是字符串)。**没有** `.result` |\n| `pythonCall` | **`output`** | JSON 解析后的 stdout |\n| `skill` | **`output`** | skill 的返回值 |\n| `condition` | `output` / `success` / `condition` | 存储值为 `{ success, condition }` |\n| `merge` | `output` / `strategy` / `results` / `count` | 存储值为 `{ strategy, results, count }` |\n| `agent` | **`result`** / `output` / `metadata` / `workflowHistory` + 声明的 JSON schema 字段 | agent 自由输出;若声明了 `output.schema.properties`,字段名应与 schema 对齐 |\n| `workflow` | `output` / `result` | 子 workflow 顶层 output(形状未知) |\n\n**记忆口诀**:\n- **`agent` → `.result`**(唯一用 `.result` 的节点类型)\n- **数据类节点(tool / pythonCall / skill)→ `.output`**\n- **`.output` 对所有类型都安全**(引擎把它当「取出该节点的值」的通用前缀)\n\n#### 自检两步\n\n1. **先 validate**:写完 YAML 先跑 `workflow_validate`,它会把\n `{{nodes.<toolNode>.result}}` 这类错误直接标成 **error**,并给出 `Did you mean \"{{nodes.<toolNode>.output}}\"` 的修复建议 —— **照抄建议改**。\n2. **必要时查 schema**:需要引用 agent 节点的结构化字段时,先 `workflow_get <相似 workflow>` 看它的 `output.schema` 字段名,或在自己的 `config.output.schema` 里**显式声明**所有下游会用到的字段,再照字段名写引用。\n\n#### 硬约束\n\n- ❌ **禁止**在 `tool` / `pythonCall` / `skill` 节点上用 `.result`\n- ❌ **禁止**在未声明 `output.schema` 的 agent 节点上引用「看起来应该存在」的字段(先声明再引用)\n- ❌ **禁止**对 `tool` 节点写深层路径(如 `{{nodes.bash-node.output.foo}}`)—— tool 输出常是字符串,无法再取属性\n- ✅ 存疑时一律退回 `.output`,或用 `{{nodes.X.output}}` 打印后**人肉核对**\n\n### 阶段 4.6: 单节点冒烟前置 (Per-node Standalone Smoke) 🧪\n\n> **依据 Task #4385 §3.4**:历史失败中,大量节点是**在整条 workflow 里第一次被执行**的 ——\n> 脚本名拼错、产物目录不存在、命令依赖 `cd`、参数没传进去…… 一个坏节点就烧掉整轮 run,\n> 然后你在整条链路的噪音里排障。**先把每个节点单独跑通,再拼成 DAG。**\n\n**在把 YAML 组装/注册之前,对每一个 `tool` / `pythonCall` / `skill` 节点逐个做单节点冒烟:**\n\n1. 用 `bash` 把该节点的 `config.command`(或 `config.script` / `args`)**原样**执行一次。\n - 变量先替换成真实值再跑(不要留着 `{{input.x}}` 跑,那验不出东西)\n - 逐个节点跑,**不是**跑整条链路\n2. 三项全绿才算该节点通过:\n - ✅ **exit code = 0**(非 0 立刻停下修,不要往后拼)\n - ✅ **产物/目录真实存在**(脚本声称会写出的文件、目录、wheel、日志,用 `ls` 核实)\n - ✅ **输出非空且含预期标记**(`grep` 你后面要引用的那个标记,命中才算通)\n3. 任何一项红了 → **先修这个节点**(改路径 / 改命令 / 改脚本),修好重跑,**不要**带着红灯继续组装。\n\n**为什么不能省**:`workflow_validate` 只校验 DAG 结构 + 模板引用契约,它**看不到**\n\"这个 shell 命令在这个 cwd 下到底能不能跑通\"。单节点冒烟是唯一廉价、快速、可重复的补位。\n\n**通过后**记录一张自检表(节点 id → exit code / 产物 / 标记),写进最终报告。\n\n### 阶段 5: 迭代验证 (Iterative Validate) 🔁\n\n调用 `workflow_validate`,它返回 `{ valid, errors, warnings, infos, errorCount, warningCount }`。\n\n**循环直到 `valid === true`(即 errorCount === 0):**\n\n| 返回项 | 你必须做什么 |\n|---|---|\n| `errors`(阻塞) | **逐条修复**。常见两类:① **DAG 结构**:节点 ID 重复 / 依赖缺失 / 环 / 空 ID;② **模板引用契约**(错误信息以 `[template-contract:...]` 开头,如 `R2-result-on-non-agent`):源节点类型与所引用字段不匹配 —— **直接采用错误信息里的 `Did you mean ...` 建议** |\n| `warnings`(建议) | **逐条评估**。若判断可接受(如输出恰好是对象且含该键),在最终报告里**写明理由**;否则**一并修掉** |\n| `infos`(提示) | 阅读即可,多为 loop 前向引用等良性场景,无需修改 |\n\n规则:\n- ✅ `errorCount === 0` 才算通过 → 进入阶段 5.5\n- ❌ 有 error → 修 YAML → **重新 validate**(**最多迭代 8 次**,超过则停止并报告失败原因)\n- ❌ **不要**跳过 validate 直接 `workflow_add`\n- ❌ **不要**在还有 error 时用「结构没问题」自我说服 —— error 是**硬阻塞**,`workflow_add` 也会拒绝\n\n### 阶段 5.5: 端到端冒烟验证 (Smoke Run) 🚨 完成硬门槛\n\n> ⚠️ **`workflow_validate` 通过 ≠ workflow 可用。** 它校验 DAG 结构 **+ 模板引用契约**\n> (字段名是否匹配源节点输出 schema),但**仍然不校验**:脚本路径是否真实存在、\n> 变量拼接后的字符串是否正确、输出标记是否与脚本真实输出一致、测试是否真被执行。\n> 这些**只有真跑**才能证明。你**必须**让 workflow 真的跑一次并拿到证据,否则**不算完成**。\n\n**执行路径(你有 `workflow_run`):**\n\n1. `workflow_run(name, input=<真实样例输入>)` —— 用**真实输入**,不要用 `echo ok` 之类的桩\n2. `workflow_run_status(run_id)` 轮询到**终态**(completed / failed / timeout)\n3. **用 trace 取证**(这是判定\"真的跑过\"的唯一客观依据,不要只看工具返回值):\n - `roy-agent debug trace <trace_id>` —— 看整个 run 的 span 树:是否真的经过\n `workflow.engine.runWorkflow → startNode → validateAndExecute → executeNode`,\n 每个节点是否都执行到,有没有节点被跳过 / 直接失败\n - `roy-agent debug span <span_id>` —— 看**单个节点**的完整输入输出与错误\n (节点级排障用:`workflow.executor.executeNode` / `workflow.tool.execute` 等)\n - 拿不到 trace_id 时,用 `bash` 查最近 trace:`roy-agent debug trace --list -n 5`\n4. 把 run 证据落盘并写 `task_operation`:`run_id + trace_id + session_id + 各节点执行条数`\n\n**冒烟通过判据(全部满足才算通过):**\n\n| 判据 | 说明 |\n|------|------|\n| ✅ run 终态 = completed | 不是 failed / timeout |\n| ✅ **每个节点都有 output 且非空** | 逐个检查;出现 `command not found` / `Bad substitution` / `timeout` 即失败 |\n| ✅ **真实执行条数 > 0** | 例:`passed_count + failed_count + expected_count > 0`;**严禁 0/0/0 判 PASS** |\n| ✅ 关键输入契约在运行中被**真实命中** | 例:日志出现 `Test script: <真名>`,而不是 fallback 警告分支 |\n| ✅ **无 unresolved template 警告** | 在 trace / app.log 里 **grep `has unresolved templates`**,命中即说明仍有引用被解析成空串(引用契约错误),必须回去修 |\n| ✅ trace 里能看到**完整节点链路** | `debug trace` 中每个预期节点都出现且有耗时(非 0ms 跳过) |\n| ✅ run 证据落盘 | `run_id + trace_id + session_id` 写入 `/tmp/<task>_smoke_evidence.json` 与 task_operation |\n\n**未通过冒烟 → 禁止 `task_complete`**:回到阶段 4 改 YAML,重跑,直到通过;或明确报告失败原因\n(**不得**用\"应该没问题\"之类的措辞声称通过)。\n\n#### 🚫 手动补跑 ≠ 真跑(依据 Task #4385 §3.5)\n\nworkflow run 失败后,**手动**把某个节点的命令再敲一遍、看到它成功了,**不算**该节点被验证。\n\n- ❌ 手动补跑的结果**不得计入** PASS 计数,也**不得**作为 \"节点没问题\" 的依据\n- ❌ 不得因为\"手动跑通了\"就跳过修 YAML / 重跑 workflow\n- ✅ 手动作业只能用来**定位原因**;结论必须靠**重跑 workflow** 得出\n- ✅ 若最终确实没能在 workflow 内验证某节点,结论里必须显式标注\n **「⚠️ 该节点未经 workflow run 验证」**,与真正通过的节点分开列\n\n> 为什么:手动跑不经过模板解析、依赖排序、引擎超时与 fail-fast。\n> 一条在 shell 里通的命令,在节点里完全可能因为 `{{}}` 解析成空串而\"成功但空转\"。\n\n#### 🔍 归因:先分清「超时」与「别的节点先 fail-fast」(依据 Task #4385 §5)\n\n说\"某个节点超时了\"之前,**必须先证明没有其他节点先失败**。\n\n- 引擎是 **fail-fast** 的:任一节点失败会立刻收尾整个 run,**尚未跑完的节点看起来就像\"卡住/超时\"**\n- 因此**先排除**:`roy-agent debug trace <trace_id>` 里是否已有**其它节点**的失败 span / error\n- 只有在确认 \"无其它节点先失败\" 之后,才可以判断为真超时,再去看该节点自己的 span 耗时与 stdout\n- ❌ 不要把 fail-fast 的连带中止当成超时去\"修超时参数\" —— 这会让你在错误的节点上白改几轮\n (历史教训:#4361 / #4366 连续两轮误判)\n\n> 🚫 **绝不允许用 `workflow update` 绕过冒烟验证**(历史上真实节点版本 v1.1.0/v1.2.0 就是走\n> `workflow update` 注册的,完全绕过了 validate 与冒烟)。update 后**同样必须**重跑阶段 5 + 5.5。\n\n### 阶段 6: 注册 (Add) 🚦\n\nvalidate 通过后:\n1. **必须再次确认用户同意**(告诉 parent agent 让用户确认)\n2. 调用 `workflow_add` 注册(提供 1-3 个 tags)\n3. 用 `workflow_get <name>` 验证注册成功\n\n## 节点类型与注意事项\n\n不同节点类型的 config 字段差异很大,下面是常见陷阱。\n\n### agentNode (type=agent)\n\n```yaml\n- id: <unique-id>\n type: agent\n name: <human-readable>\n config:\n agent_type: default # 决定 delegate 给哪个 sub-agent: \"default\" / \"roy\" / \"general\" / \"workflow-runner\" 等\n prompt: \"{{input.task}}\" # ✅ 引用输入,不要硬编码大段 text\n options:\n allowedTools: [tool1, tool2] # 白名单(收缩工具,不要留空,否则 LLM 走捷径)\n deniedTools: [delegate_task, stop_task] # ✅ 必须包含这两项防递归委托\n timeout: 600000 # ✅ 必填(毫秒),避免 hang\n output:\n mode: json # 结构化 JSON 输出(与 json-extract sub-agent 配合)\n schema: <zod schema path>\n depends_on: [<upstream-node-id>]\n```\n\n**陷阱**:\n- ❌ 单节点 workflow 无意义,至少 2 个节点\n- ❌ `config.prompt` 硬编码大段 text → 应引用 `{{input.x}}` 模板\n- ❌ `config.options.allowedTools` 留空 → LLM 会走捷径调 `bash` / `delegate_task`\n- ❌ `config.options.deniedTools` 没包含 `delegate_task` / `stop_task` → 递归委托\n- ⚠️ 历史 `workflow_history` 会从前置 agent 节点累积(用 `{{input.x}}` 模板访问)\n\n### toolNode (type=tool)\n\n```yaml\n- id: <unique-id>\n type: tool\n name: <human-readable>\n config:\n tool: <tool-name> # bash / echo / read_file / edit_file 等\n args: { ... } # 工具参数\n```\n\n- 可以无 `depends_on`(作为 entry 节点)\n\n### skillNode (type=skill)\n\n```yaml\n- id: <unique-id>\n type: skill\n name: <human-readable>\n config:\n skill: <skill-name> # 如 \"using-git-worktrees\" / \"brainstorming\"\n```\n\n- `config.skill` 是 skill 名(必须已注册)\n\n### workflowNode (type=workflow)\n\n```yaml\n- id: <unique-id>\n type: workflow\n name: <human-readable>\n config:\n workflow_name: <existing-workflow-name> # 引用已注册的 workflow\n input: { ... } # 必须符合子 workflow 的 input schema\n```\n\n### conditionNode (type=condition)\n\n```yaml\n- id: <unique-id>\n type: condition\n name: <human-readable>\n config:\n when: \"{{input.score}} > 5\" # 条件表达式\n depends_on: [<upstream-id>]\n```\n\n- 与 `edges[].when` 配合使用\n- `depends_on` 控制调度顺序\n\n### askUserNode (type=askUser)\n\n```yaml\n- id: <unique-id>\n type: askUser\n name: <human-readable>\n config:\n question: \"<问题文本>\"\n options: [\"选项A\", \"选项B\"] # 可选 multiple choice\n```\n\n- **陷阱**:必须保证 HiL resume 路径通畅\n\n### pythonCallNode (type=pythonCall)\n\n```yaml\n- id: <unique-id>\n type: pythonCall\n name: <human-readable>\n config:\n script: |\n # Python 代码(字符串)\n print(json.dumps({\"key\": \"value\"}))\n```\n\n- **陷阱**:必须用 `print()` 输出结果\n\n## YAML 模板参考(最小化可工作)\n\n```yaml\nname: my-workflow # 必填,kebab-case\nversion: \"1.0\" # 选填,默认 1.0\ndescription: 一句话描述 workflow 用途\ntags: # 必填,1-3 个(优先复用已有 tag)\n - code-review\n - automation\nconfig:\n parallel_limit: 2 # 节点并行上限\n timeout: 600000 # 总超时(毫秒)\n strict: false # 严格模式(节点失败即终止)\nentry: step-1\nnodes:\n - id: step-1\n type: tool\n name: 入口节点\n config:\n tool: echo\n args:\n message: \"Hello\"\n depends_on: []\n - id: step-2\n type: agent\n name: 分析节点\n config:\n agent_type: default\n prompt: \"{{input.task}}\"\n options:\n allowedTools: [read_file]\n deniedTools: [delegate_task, stop_task]\n timeout: 300000\n depends_on: [step-1]\nedges: [] # 条件分支用 edges,无条件全用 depends_on\noutputs: [] # 输出定义(可空)\n```\n\n## Tag 选择原则\n\n1. **优先复用** `workflow_tag_list` 显示的已有 tag(避免 tag 池膨胀)\n2. 已有 `code-review` 时**不要**新增 `review-workflow`\n3. 已有 `ci-cd` 时**不要**新增 `continuous-integration`\n4. 已有 `build-publish` 时**不要**新增 `release-deploy`\n5. tags 必须 1-3 个,超过会被 `workflow_add` 拒绝\n6. 池中确实没有合适 tag 时才新增,以扩展 tag 池\n\n## 严格约束\n\n- ✅ 只用 10 个被授予的工具(白名单强制)\n- ✅ **写 `{{nodes.X.<字段>}}` 前先核对访问契约表**(阶段 4.6):`agent` → `.result`;`tool` / `pythonCall` / `skill` → `.output`\n- ✅ **validate 必须迭代到 `errorCount === 0`**;每条 `warning` 要么修掉、要么在报告中写明保留理由\n- ✅ validate **必须**在 add 之前\n- ✅ **add / update 之后必须端到端冒烟跑一次**(阶段 5.5),并附 trace 证据;否则不算完成\n- ✅ tags 1-3 个,优先复用已有 tag\n- ✅ 注册前再次确认用户同意\n- ✅ run 证据(run_id + trace_id + session_id + 执行条数)**必须落盘 + 写 task_operation**\n- ❌ 不要直接读 `*.workflow.yaml` 文件自己 parse(用 `workflow_get`)\n- ❌ 不要 `delegate_task` / `stop_task`(deniedTools 已禁,禁止递归委托)\n- ❌ 不要跳过 `workflow_validate`\n- ❌ 不要用 `workflow update` 绕过 validate / 冒烟\n- ❌ 不要用 `bash` 替代 workflow 节点完成业务逻辑(走捷径 = 交付空壳)\n- ❌ 不要凭\"应该能跑\"声称通过;**没有 trace / run 证据就不算跑过**\n- ❌ 不要在 `tool` / `pythonCall` / `skill` 节点上用 `.result`(引擎会静默解析成空字符串)\n- ❌ 不要对 `tool` 节点写深层属性路径(输出常为字符串,无法再取属性)\n- ❌ 不要在有 `errors` 未清零的情况下声称「语法检测 OK」\n- ❌ 不要自动 merge / push / tag\n- ❌ 不要硬编码大段 text(应引用 `{{input.x}}` 模板)";
105
105
  /**
106
106
  * task-organizer — built-in 任务整理子智能体 (Task #2348)
107
107
  *
@@ -3,23 +3,24 @@ import {
3
3
  MIN_SESSION_ID_LENGTH,
4
4
  SessionComponent,
5
5
  isValidSessionId
6
- } from "../../shared/@ai-setting/roy-agent-core-yd21n6c7.js";
6
+ } from "../../shared/@ai-setting/roy-agent-core-bwzy00t2.js";
7
7
  import {
8
8
  MemorySessionStore
9
- } from "../../shared/@ai-setting/roy-agent-core-h93v6p6y.js";
9
+ } from "../../shared/@ai-setting/roy-agent-core-tc7gw38m.js";
10
10
  import"../../shared/@ai-setting/roy-agent-core-ze068vc9.js";
11
11
  import"../../shared/@ai-setting/roy-agent-core-rvxg1wps.js";
12
12
  import {
13
13
  SessionMessageConverter
14
- } from "../../shared/@ai-setting/roy-agent-core-pk1b3kvf.js";
15
- import"../../shared/@ai-setting/roy-agent-core-zy3hqfwg.js";
14
+ } from "../../shared/@ai-setting/roy-agent-core-ns3pbyz4.js";
15
+ import"../../shared/@ai-setting/roy-agent-core-yv9x4wmg.js";
16
+ import"../../shared/@ai-setting/roy-agent-core-zjdzk1gg.js";
16
17
  import"../../shared/@ai-setting/roy-agent-core-7hh0brvs.js";
17
- import"../../shared/@ai-setting/roy-agent-core-tdbv7ff4.js";
18
- import"../../shared/@ai-setting/roy-agent-core-qcspfszy.js";
19
- import"../../shared/@ai-setting/roy-agent-core-4z3rq5gw.js";
20
- import"../../shared/@ai-setting/roy-agent-core-k8b440n3.js";
21
- import"../../shared/@ai-setting/roy-agent-core-5apmk4qw.js";
22
- import"../../shared/@ai-setting/roy-agent-core-dsevjbck.js";
18
+ import"../../shared/@ai-setting/roy-agent-core-40xzbjtm.js";
19
+ import"../../shared/@ai-setting/roy-agent-core-gngjkw3b.js";
20
+ import"../../shared/@ai-setting/roy-agent-core-1pnfkspm.js";
21
+ import"../../shared/@ai-setting/roy-agent-core-16t0zfbk.js";
22
+ import"../../shared/@ai-setting/roy-agent-core-jszgaevt.js";
23
+ import"../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
23
24
  import"../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
24
25
  import"../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
25
26
  import"../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -397,6 +397,10 @@ export declare class SessionComponent extends BaseComponent {
397
397
  * 现在:优先取干净的纯文本轮次(信息密度最高),一轮都没取到时兜底保留最近
398
398
  * 若干条原始消息(含 tool 轮次),至少让 CLI 能看到「刚才在干什么」。
399
399
  */
400
+ /**
401
+ * ★ 2026-10-22:**只存摘要**(Task #4479)。改造前平均 878KB(最大 12.8MB)
402
+ * —— 入参是完整 messages 数组,返回值是抽出来的消息正文。
403
+ */
400
404
  private extractRecentMessages;
401
405
  /**
402
406
  * Preview compact operation
@@ -3,9 +3,11 @@ import {
3
3
  SQLiteSessionStore,
4
4
  getDefaultDataDir,
5
5
  getDefaultSessionDbPath
6
- } from "../../../shared/@ai-setting/roy-agent-core-h93v6p6y.js";
7
- import"../../../shared/@ai-setting/roy-agent-core-pk1b3kvf.js";
8
- import"../../../shared/@ai-setting/roy-agent-core-zy3hqfwg.js";
6
+ } from "../../../shared/@ai-setting/roy-agent-core-tc7gw38m.js";
7
+ import"../../../shared/@ai-setting/roy-agent-core-ns3pbyz4.js";
8
+ import"../../../shared/@ai-setting/roy-agent-core-yv9x4wmg.js";
9
+ import"../../../shared/@ai-setting/roy-agent-core-zjdzk1gg.js";
10
+ import"../../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
9
11
  import"../../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
10
12
  import"../../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
11
13
  import"../../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -6,19 +6,19 @@ import {
6
6
  getSourcePriority,
7
7
  parseFrontmatter,
8
8
  parseSkillFile
9
- } from "../../shared/@ai-setting/roy-agent-core-2gay3sjt.js";
9
+ } from "../../shared/@ai-setting/roy-agent-core-yg247mk4.js";
10
10
  import {
11
11
  SkillToolSchema,
12
12
  buildSkillDescription,
13
13
  createSkillTool
14
14
  } from "../../shared/@ai-setting/roy-agent-core-psvxt4c9.js";
15
15
  import"../../shared/@ai-setting/roy-agent-core-7hh0brvs.js";
16
- import"../../shared/@ai-setting/roy-agent-core-tdbv7ff4.js";
17
- import"../../shared/@ai-setting/roy-agent-core-qcspfszy.js";
18
- import"../../shared/@ai-setting/roy-agent-core-4z3rq5gw.js";
19
- import"../../shared/@ai-setting/roy-agent-core-k8b440n3.js";
20
- import"../../shared/@ai-setting/roy-agent-core-5apmk4qw.js";
21
- import"../../shared/@ai-setting/roy-agent-core-dsevjbck.js";
16
+ import"../../shared/@ai-setting/roy-agent-core-40xzbjtm.js";
17
+ import"../../shared/@ai-setting/roy-agent-core-gngjkw3b.js";
18
+ import"../../shared/@ai-setting/roy-agent-core-1pnfkspm.js";
19
+ import"../../shared/@ai-setting/roy-agent-core-16t0zfbk.js";
20
+ import"../../shared/@ai-setting/roy-agent-core-jszgaevt.js";
21
+ import"../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
22
22
  import"../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
23
23
  import"../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
24
24
  import"../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -369,7 +369,6 @@ function mergeDeliverableIntoContext(rawContext, deliverable) {
369
369
  // src/env/task/backend/sqlite-task-port.ts
370
370
  function createSQLiteTaskPort(store) {
371
371
  return {
372
- hidesArchivedByDefault: true,
373
372
  async createTask(input) {
374
373
  const task = await store.createTask({
375
374
  title: input.title,
@@ -553,7 +552,6 @@ function createRoySidecarTaskPort(store, vocabulary, options = {}) {
553
552
  return hit[0];
554
553
  }
555
554
  return {
556
- hidesArchivedByDefault: false,
557
555
  async createTask(input) {
558
556
  const parentId = input.parent_task_id !== undefined ? await nativeIdOf(input.parent_task_id) : null;
559
557
  const sidecar = {
@@ -240,17 +240,6 @@ export interface PortUpdateOperationInput {
240
240
  * 只有 `num ↔ nativeId` 的解析是 port 的职责,因为只有它知道自己的主键。
241
241
  */
242
242
  export interface TaskPersistencePort {
243
- /**
244
- * 这个 port 的 `listTasks` 是否**能**返回 `archived` 行。
245
- *
246
- * roy 的 `SQLiteTaskStore` 默认在 SQL 里加 `status != 'archived'`,只有
247
- * `include_archived: true` 才放行。`TaskBackend` 需要能显式要求这些行
248
- * (否则 `task_list(status="archived")` 永远是空 —— CV round 1 finding D2),
249
- * 所以 `listTasks` 必须能透传这个意图。
250
- *
251
- * 缺省 `false` = 这个 port 的存储本来就不隐藏 archived(例如插件的 store)。
252
- */
253
- readonly hidesArchivedByDefault?: boolean;
254
243
  createTask(input: PortCreateTaskInput): Promise<StoredTaskRecord>;
255
244
  /** 按存储主键读取。 */
256
245
  getTask(nativeId: string): Promise<StoredTaskRecord | null>;
@@ -3,18 +3,18 @@ import {
3
3
  BackgroundTaskManager,
4
4
  createDelegateTool,
5
5
  createStopTool
6
- } from "../../../shared/@ai-setting/roy-agent-core-p2r27era.js";
6
+ } from "../../../shared/@ai-setting/roy-agent-core-vmg30n5x.js";
7
7
  import"../../../shared/@ai-setting/roy-agent-core-tbrn9wdm.js";
8
- import"../../../shared/@ai-setting/roy-agent-core-yhsssn86.js";
8
+ import"../../../shared/@ai-setting/roy-agent-core-zmeka63k.js";
9
9
  import"../../../shared/@ai-setting/roy-agent-core-1pg0fepg.js";
10
- import"../../../shared/@ai-setting/roy-agent-core-gdsqp6j8.js";
11
- import"../../../shared/@ai-setting/roy-agent-core-rx16h2m1.js";
10
+ import"../../../shared/@ai-setting/roy-agent-core-6masakz4.js";
11
+ import"../../../shared/@ai-setting/roy-agent-core-97eyghsp.js";
12
12
  import"../../../shared/@ai-setting/roy-agent-core-qxnbvgwe.js";
13
13
  import"../../../shared/@ai-setting/roy-agent-core-c1v263jn.js";
14
- import"../../../shared/@ai-setting/roy-agent-core-4z3rq5gw.js";
15
- import"../../../shared/@ai-setting/roy-agent-core-k8b440n3.js";
16
- import"../../../shared/@ai-setting/roy-agent-core-5apmk4qw.js";
17
- import"../../../shared/@ai-setting/roy-agent-core-dsevjbck.js";
14
+ import"../../../shared/@ai-setting/roy-agent-core-1pnfkspm.js";
15
+ import"../../../shared/@ai-setting/roy-agent-core-16t0zfbk.js";
16
+ import"../../../shared/@ai-setting/roy-agent-core-jszgaevt.js";
17
+ import"../../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
18
18
  import"../../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
19
19
  import"../../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
20
20
  import"../../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -5,32 +5,32 @@ import {
5
5
  TaskPriorityEnum,
6
6
  TaskStatusEnum,
7
7
  TaskTypeEnum
8
- } from "../../shared/@ai-setting/roy-agent-core-39xejr39.js";
8
+ } from "../../shared/@ai-setting/roy-agent-core-e0wbq0qq.js";
9
9
  import"../../shared/@ai-setting/roy-agent-core-amkqnyzy.js";
10
10
  import {
11
11
  SQLiteTaskStore,
12
12
  getDefaultTaskDbPath
13
- } from "../../shared/@ai-setting/roy-agent-core-qm9jda8p.js";
14
- import"../../shared/@ai-setting/roy-agent-core-7g03zkkb.js";
13
+ } from "../../shared/@ai-setting/roy-agent-core-3kz6934b.js";
14
+ import"../../shared/@ai-setting/roy-agent-core-pemxr5n4.js";
15
15
  import"../../shared/@ai-setting/roy-agent-core-rn57psp0.js";
16
16
  import {
17
17
  TaskEntityEventTypes
18
18
  } from "../../shared/@ai-setting/roy-agent-core-8gxth0eh.js";
19
- import"../../shared/@ai-setting/roy-agent-core-p2r27era.js";
19
+ import"../../shared/@ai-setting/roy-agent-core-vmg30n5x.js";
20
20
  import"../../shared/@ai-setting/roy-agent-core-tbrn9wdm.js";
21
- import"../../shared/@ai-setting/roy-agent-core-yhsssn86.js";
21
+ import"../../shared/@ai-setting/roy-agent-core-zmeka63k.js";
22
22
  import"../../shared/@ai-setting/roy-agent-core-1pg0fepg.js";
23
- import"../../shared/@ai-setting/roy-agent-core-gdsqp6j8.js";
24
- import"../../shared/@ai-setting/roy-agent-core-rx16h2m1.js";
23
+ import"../../shared/@ai-setting/roy-agent-core-6masakz4.js";
24
+ import"../../shared/@ai-setting/roy-agent-core-97eyghsp.js";
25
25
  import"../../shared/@ai-setting/roy-agent-core-qxnbvgwe.js";
26
26
  import"../../shared/@ai-setting/roy-agent-core-c1v263jn.js";
27
27
  import"../../shared/@ai-setting/roy-agent-core-7hh0brvs.js";
28
- import"../../shared/@ai-setting/roy-agent-core-tdbv7ff4.js";
29
- import"../../shared/@ai-setting/roy-agent-core-qcspfszy.js";
30
- import"../../shared/@ai-setting/roy-agent-core-4z3rq5gw.js";
31
- import"../../shared/@ai-setting/roy-agent-core-k8b440n3.js";
32
- import"../../shared/@ai-setting/roy-agent-core-5apmk4qw.js";
33
- import"../../shared/@ai-setting/roy-agent-core-dsevjbck.js";
28
+ import"../../shared/@ai-setting/roy-agent-core-40xzbjtm.js";
29
+ import"../../shared/@ai-setting/roy-agent-core-gngjkw3b.js";
30
+ import"../../shared/@ai-setting/roy-agent-core-1pnfkspm.js";
31
+ import"../../shared/@ai-setting/roy-agent-core-16t0zfbk.js";
32
+ import"../../shared/@ai-setting/roy-agent-core-jszgaevt.js";
33
+ import"../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
34
34
  import"../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
35
35
  import"../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
36
36
  import"../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -1,72 +1,60 @@
1
1
  /**
2
- * @fileoverview 任务卡片「该发到哪个群」的路由决策(Task #4230 follow-up)
2
+ * @fileoverview 任务卡片「该发到哪个群」的路由决策(Task #4230 / #4394)
3
3
  *
4
- * ## 背景(真实 bug,2026-10-15)
4
+ * ## 现行规则(用户 2026-10-20 明令,Task #4394)
5
5
  *
6
- * Task #4226 的进展卡片被发进了 **「Task 3999 - 多模态ontology提取跟进」** 群。
7
- * 根因链:
8
- * 1. 触发消息来自 `oc_d083fc…`(= **task #3999 的跟进群**);
9
- * 2. `LarkCliTaskNotifyHook` 的 `replyChannel.chatId` 优先(Task #4220 修复:
10
- * 让 user 在触发群看到卡片)→ 卡片落到 `oc_d083fc…`;
11
- * 3. #4226 是 sub-task(parent #2066),`PerTaskGroupChatPlugin` 默认跳过建群
12
- * → #4226 从来没有自己的群。
6
+ * > 去掉专属群卡片逻辑,只保留**触发群的进度卡片**(可用户点击建群)
13
7
  *
14
- * 结果:#3999 的专属群里出现了一张讲 #4226 的卡片 —— 跨任务污染。
8
+ * 决策优先级:
9
+ * 1. **有触发群 → 就用触发群**。无条件,不判断它是不是"别的 task 的专属群"。
10
+ * 2. 没有触发群 → 用本 task(其次最近祖先)自己已有的专属群兜底(卡片不能丢)。
11
+ * 3. 两者都没有 → `none`(调用方静默 skip)。
15
12
  *
16
- * ## 决策规则(用户确认)
13
+ * ## 已删除的历史逻辑(Task #4394)
17
14
  *
18
- * - **触发群优先**(保持"在哪聊就在哪看到卡片")
19
- * - **但跳过「别 task 的专属群」**:若触发群被判定为某个**非本 task lineage**
20
- * 的 task 的专属群 → 卡片改发到本 task 自己的群(lineage 无群时新建)
21
- * - 改投时在触发群补一条**链接卡片**(不是普通消息),让 user 知道卡片去哪了
15
+ * 旧实现(Task #4230)会在"触发群被判定为别的 task 的专属群"时把卡片**改投**到
16
+ * 本 task / 祖先的 lineage 群,并在触发群补一张「📂 任务卡片已改投专属群 /
17
+ * 这里不是本任务的群,进展请看专属群」的指路卡。
22
18
  *
23
- * ## 「专属群」判定信号(多重信号,高精度优先)
19
+ * 用户实测(Task #4393)该行为是干扰:
20
+ * ```
21
+ * 触发群 oc_8947d9e2…(task 4377 的专属群)
22
+ * → 卡片被改投到 oc_d27157ac…(lineage 群)
23
+ * → 用户在触发群只看到一张"这里不是本任务的群"的灰卡
24
+ * ```
25
+ * 用户的诉求是「在哪聊就在哪看到进展」。**跨任务污染不再是需要解决的问题**,
26
+ * 因此整套「专属群归属判定 + 改投 + 指路卡」链路已彻底删除:
27
+ * - 删除 `ChatOwnership` / `mergeChatOwnership` / `analyzeContextOwnership`
28
+ * - 删除 `CardChatDecision.reroutedFrom`
29
+ * - 删除 `new-group` / `fallback-to-trigger` 两种 source(不再自动建群)
24
30
  *
25
- * - `chat-binding`:session 的 chat↔task 绑定(PerTaskGroupChatPlugin 建群时写入)
26
- * - `larkPerTaskChatId`:task.context 里插件写入的专属群字段
27
- * - `followup_chat`:`*followup_chat_id` / `*_group_chat_id` 之类字段(老任务手工建的跟进群)
28
- * - `chat-name`:群名里的 `Task 3999` / `#3999` 模式(对没留字段的手工群兜底)
31
+ * ## 建群能力仍在(**用户主动点击**才建)
29
32
  *
30
- * 注意:**不算** `feishu_chat_id` / `chat_id` 这类"会话群"字段 —— 它们表示
31
- * "这个 task 曾在哪个群聊过",把普通会话群误判成专属群会让新任务的卡片被
32
- * 错误地赶去新建群(假阳性比假阴性更烦人)。
33
+ * 卡片顶部按钮 `create_group` / `disband_group` 由
34
+ * `lark-card-action-handler.ts` 处理,与本文档的路由决策**完全解耦**:
35
+ * ```
36
+ * 用户点「为这个任务建群」
37
+ * → card.action.trigger 事件
38
+ * → handleCardAction → ports.createGroupChat (lark-cli im +chat-create)
39
+ * → 回写 task.context.larkPerTaskChatId
40
+ * → patch 卡片(按钮翻成「解散该群」+ 出现「进入专属群」链接)
41
+ * ```
42
+ * 建群后下一张进度卡会走本文件的优先级 1(仍进触发群),
43
+ * 但卡片上多了「进入专属群」链接 —— 这是用户可选的入口,不是自动改投。
33
44
  *
34
- * 本模块是**纯函数**(不碰 IO),IO(DB 反查 / lark-cli 取群名)由调用方
35
- * (EventSource handler)注入,便于单测覆盖所有分支。
45
+ * 本模块是**纯函数**(不碰 IO),便于单测覆盖所有分支。
36
46
  */
37
47
  /** 群名里隐含的 task id("Task 3999 - xxx" / "#3999 TMO 提取跟进") */
38
48
  export declare const GROUP_NAME_TASK_ID_RE: RegExp;
39
49
  /** 会把 chatId 记进 context 的字段名(用于 DB LIKE 反查) */
40
50
  export declare const CTX_FIELD_LARK_PER_TASK_CHAT_ID = "larkPerTaskChatId";
41
- /** 一个群可能的归属证据(多重信号并集) */
42
- export interface ChatOwnership {
43
- chatId: string;
44
- /**
45
- * 该群归属的 task id(可能多个:历史脏数据 / 多个 task 共用过一个群)
46
- *
47
- * 空数组 = 没有证据表明它是任何 task 的专属群。
48
- */
49
- ownerTaskIds: number[];
50
- /** 证据来源(诊断用):chat-binding / larkPerTaskChatId / followup_chat_id / chat-name */
51
- sources: string[];
52
- /** 群名(DB 记录或实时查询;可能没有) */
53
- chatName?: string;
54
- /** 是否被判为"专属群"(至少有一条**专属**证据) */
55
- dedicated: boolean;
56
- }
57
- export type CardChatSource = "trigger-chat" | "own-group" | "ancestor-group" | "new-group" | "fallback-to-trigger" | "none";
51
+ export type CardChatSource = "trigger-chat" | "own-group" | "ancestor-group" | "none";
58
52
  export interface CardChatDecision {
59
53
  chatId?: string;
60
54
  profile?: string;
61
- /** 目标群名(新建群时由 ensureGroupChat 回填,用于改投链接卡片渲染) */
55
+ /** 目标群名(诊断用) */
62
56
  chatName?: string;
63
57
  source: CardChatSource;
64
- /** 因触发群是"别 task 的专属群"而改投时记录(用于回补链接卡片) */
65
- reroutedFrom?: {
66
- chatId: string;
67
- ownerTaskIds: number[];
68
- sources: string[];
69
- };
70
58
  /** 人类可读的决策原因(写进 span / debug 日志) */
71
59
  reason: string;
72
60
  }
@@ -79,41 +67,17 @@ export declare function extractPerTaskChatProfile(contextStr?: string): string |
79
67
  /** 从 task.context JSON 解析记录下来的群名(含 `#3999` / `Task 3999` 模式) */
80
68
  export declare function extractRecordedChatNames(contextStr?: string): string[];
81
69
  /**
82
- * 分析「某个 task 的 context 是否主张自己拥有 chatId」
83
- *
84
- * 只认**专属**信号;`feishu_chat_id` / `chat_id` 这类会话群字段会被忽略
85
- * (否则会把普通会话群误判成专属群)。
86
- */
87
- export declare function analyzeContextOwnership(contextStr: string | undefined, chatId: string): {
88
- owned: boolean;
89
- sources: string[];
90
- impliedTaskIds: number[];
91
- };
92
- /**
93
- * 由多个 task(DB 反查命中)的 context 汇总出 `ChatOwnership`
70
+ * 决定卡片该发到哪个群(Task #4394 简化版)
94
71
  *
95
- * @param chatId 目标群
96
- * @param candidates DB 反查命中的 task(id + context)
97
- * @param extra 额外信号(chat↔task 绑定 / 实时群名),由调用方提供
98
- */
99
- export declare function mergeChatOwnership(chatId: string, candidates: Array<{
100
- id: number;
101
- context?: string;
102
- }>, extra?: {
103
- boundTaskId?: number | null;
104
- /** 该群的真实群名(若调用方查到了) */
105
- chatName?: string;
106
- }): ChatOwnership;
107
- /**
108
- * 决定卡片该发到哪个群
72
+ * 规则只有两条:
73
+ * 1. **有触发群 → 触发群**(无条件,不做任何"是不是别人的群"判定)
74
+ * 2. 没触发群 → 用本 task / 祖先已有的专属群兜底
109
75
  *
110
- * @param input.lineageIds 本 task 的 lineage(自己 + 所有祖先)task id
111
76
  * @param input.lineageChat lineage 上找到的专属群(自己优先,其次最近祖先)
112
- * @param input.triggerOwnership 触发群的归属分析(未做分析时传 undefined)
113
77
  */
114
78
  export declare function decideCardChatTarget(input: {
115
79
  taskId?: number;
116
- lineageIds: number[];
80
+ lineageIds?: number[];
117
81
  lineageChat?: {
118
82
  chatId: string;
119
83
  profile?: string;
@@ -121,15 +85,4 @@ export declare function decideCardChatTarget(input: {
121
85
  };
122
86
  triggerChatId?: string;
123
87
  triggerProfile?: string;
124
- triggerOwnership?: ChatOwnership;
125
- /**
126
- * ★ Task #4331:是否允许"new-group"路径(即触发群是别人的群 + 本 task 无群时建专属群)
127
- *
128
- * 默认 false(用户新要求:默认不建群)
129
- * - true:保留 #4230 的老行为(建专属群),用于向后兼容显式开启的场景
130
- * - false:触发群是别人的群 + 本 task 无群 → 退回触发群(不建群)
131
- *
132
- * 不影响其他 source(trigger-chat / own-group / ancestor-group / none)的决策。
133
- */
134
- allowNewGroup?: boolean;
135
88
  }): CardChatDecision;
@@ -10,20 +10,20 @@ import {
10
10
  formatExtractorInput,
11
11
  formatExtractorInputFromDescription,
12
12
  parseExtractorOutput
13
- } from "../../../shared/@ai-setting/roy-agent-core-hch2geta.js";
14
- import"../../../shared/@ai-setting/roy-agent-core-yhsssn86.js";
13
+ } from "../../../shared/@ai-setting/roy-agent-core-ezsyyjxw.js";
14
+ import"../../../shared/@ai-setting/roy-agent-core-zmeka63k.js";
15
15
  import"../../../shared/@ai-setting/roy-agent-core-1pg0fepg.js";
16
- import"../../../shared/@ai-setting/roy-agent-core-gdsqp6j8.js";
17
- import"../../../shared/@ai-setting/roy-agent-core-j8e92gj8.js";
18
- import"../../../shared/@ai-setting/roy-agent-core-na73mym1.js";
19
- import"../../../shared/@ai-setting/roy-agent-core-8s9jd2qy.js";
16
+ import"../../../shared/@ai-setting/roy-agent-core-6masakz4.js";
17
+ import"../../../shared/@ai-setting/roy-agent-core-nntphgww.js";
18
+ import"../../../shared/@ai-setting/roy-agent-core-202v00x4.js";
19
+ import"../../../shared/@ai-setting/roy-agent-core-nra8vfrs.js";
20
20
  import"../../../shared/@ai-setting/roy-agent-core-jrj30vq3.js";
21
- import"../../../shared/@ai-setting/roy-agent-core-dv714bqh.js";
21
+ import"../../../shared/@ai-setting/roy-agent-core-wafk25ff.js";
22
22
  import"../../../shared/@ai-setting/roy-agent-core-e25xkv53.js";
23
23
  import"../../../shared/@ai-setting/roy-agent-core-az13yzmc.js";
24
- import"../../../shared/@ai-setting/roy-agent-core-k8b440n3.js";
25
- import"../../../shared/@ai-setting/roy-agent-core-5apmk4qw.js";
26
- import"../../../shared/@ai-setting/roy-agent-core-dsevjbck.js";
24
+ import"../../../shared/@ai-setting/roy-agent-core-16t0zfbk.js";
25
+ import"../../../shared/@ai-setting/roy-agent-core-jszgaevt.js";
26
+ import"../../../shared/@ai-setting/roy-agent-core-b599jgyd.js";
27
27
  import"../../../shared/@ai-setting/roy-agent-core-8seq65bp.js";
28
28
  import"../../../shared/@ai-setting/roy-agent-core-jnszp0jq.js";
29
29
  import"../../../shared/@ai-setting/roy-agent-core-fs0mn2jk.js";
@@ -173,21 +173,6 @@ export interface FinishTaskOptions {
173
173
  finalStep?: string;
174
174
  progress?: number;
175
175
  }
176
- /** 改投链接卡片的入参(Task #4230 follow-up) */
177
- export interface RedirectNoticeOptions {
178
- /** 发到哪个群(通常是"触发群",即被判定为别 task 专属群的那个) */
179
- chatId: string;
180
- profile?: string;
181
- /** 本 task */
182
- taskId?: number | string;
183
- taskTitle?: string;
184
- /** 卡片实际落在哪个群 */
185
- targetChatId: string;
186
- /** 目标群名(渲染成可点击文本;缺省显示"专属群") */
187
- targetChatName?: string;
188
- /** 改投原因(诊断,写进卡片文案) */
189
- reason?: string;
190
- }
191
176
  export interface StartTaskResult {
192
177
  messageId: string;
193
178
  /** pin 是否成功(失败不影响卡片可见性,但 trace 里可查) */
@@ -418,24 +403,6 @@ export declare function buildTaskCard(opts: {
418
403
  export declare function buildChatAppLink(chatId: string, base?: string): string;
419
404
  /** 默认 applink 基址(飞书) */
420
405
  export declare const DEFAULT_APP_LINK_BASE = "https://applink.feishu.cn";
421
- /**
422
- * 构造"卡片已改投专属群"的链接卡片(Task #4230 follow-up)
423
- *
424
- * 为什么是卡片而不是普通消息:
425
- * - 用户明确要求「旧逻辑的普通消息通知不要」;
426
- * - 改投后触发群里需要一条**可点击**的入口,否则 user 不知道进展卡片去哪了。
427
- *
428
- * 注意:这张卡片**不携带任务进展内容**,只是一条"去哪看"的指路牌,避免把
429
- * 别的 task 的进展细节塞进不属于它的群。
430
- */
431
- export declare function buildRedirectCard(opts: {
432
- taskId?: number | string;
433
- taskTitle?: string;
434
- targetChatId: string;
435
- /** 目标群名(渲染成可点击文本;缺省显示"专属群") */
436
- targetChatName?: string;
437
- reason?: string;
438
- }): CardPayload;
439
406
  /**
440
407
  * 格式化耗时为人类可读文本(毫秒 → ms / s / m)
441
408
  *
@@ -496,14 +463,6 @@ export declare class LarkCardPatchNotifier {
496
463
  * unpin 基础设施,只是渲染的卡片与自持的时间线不同。
497
464
  */
498
465
  private readonly workflowStates;
499
- /**
500
- * ★ Task #4237 修复:改投链接卡片幂等表
501
- *
502
- * key = `${taskId}:${targetChatId}`;同一 (task, 目标群) 只允许发一张
503
- * 「任务卡片已改投专属群」卡片 —— 旧实现里两条决策路径各发一次,
504
- * 触发群收到两张文案不同的改投卡片。
505
- */
506
- private readonly redirectsSent;
507
466
  constructor(options?: LarkCardPatchNotifierOptions);
508
467
  /** 取(或创建)某 messageId 的 mutex —— 不同卡片互不阻塞 */
509
468
  private mutexFor;
@@ -514,16 +473,6 @@ export declare class LarkCardPatchNotifier {
514
473
  * `--chat-id/--message-id` 是非法 flag,导致 pin 永远失败 → 用户"还是没有 pin")。
515
474
  */
516
475
  startTask(opts: StartTaskOptions): Promise<StartTaskResult>;
517
- /**
518
- * 在"触发群"发一条**链接卡片**,告知任务进展卡片已改投本 task 的专属群
519
- *
520
- * Task #4230 follow-up:当触发群被判定为"别的 task 的专属群"时,卡片不会
521
- * 再落在触发群(避免跨任务污染),但 user 需要知道去哪里看 → 这里补一张
522
- * 不含任务进展内容的指路卡片。
523
- *
524
- * 失败非致命(触发群里发不出去不影响主流程)。
525
- */
526
- sendRedirectNotice(opts: RedirectNoticeOptions): Promise<boolean>;
527
476
  /**
528
477
  * Patch 同一卡片(状态/进度/当前步骤 + 累积里程碑与过程时间线)
529
478
  *