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

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 (62) hide show
  1. package/dist/env/agent/index.js +3 -3
  2. package/dist/env/error-reply.d.ts +184 -0
  3. package/dist/env/event-source/event-source-handlers.d.ts +9 -23
  4. package/dist/env/event-source/index.js +6 -6
  5. package/dist/env/index.js +18 -18
  6. package/dist/env/llm/adapter-pi-ai.d.ts +147 -2
  7. package/dist/env/llm/adapter.d.ts +21 -1
  8. package/dist/env/llm/index.js +2 -2
  9. package/dist/env/llm/invoke.d.ts +41 -1
  10. package/dist/env/llm/llm.d.ts +25 -0
  11. package/dist/env/llm/reasoning-defense.d.ts +104 -0
  12. package/dist/env/llm/types.d.ts +26 -0
  13. package/dist/env/prompt/index.js +2 -2
  14. package/dist/env/prompt/prompts-index.d.ts +1 -1
  15. package/dist/env/session/index.js +4 -4
  16. package/dist/env/session/storage/index.js +3 -3
  17. package/dist/env/task/backend/index.js +0 -2
  18. package/dist/env/task/backend/types.d.ts +0 -11
  19. package/dist/env/task/delegate/index.js +2 -2
  20. package/dist/env/task/index.js +3 -3
  21. package/dist/env/task/plugins/card-chat-routing.d.ts +42 -89
  22. package/dist/env/task/plugins/index.js +4 -4
  23. package/dist/env/task/plugins/lark-card-patch-notifier.d.ts +0 -51
  24. package/dist/env/task/plugins/lark-cli-task-notify-plugin.d.ts +14 -31
  25. package/dist/env/task/plugins/per-task-group-chat-plugin.d.ts +4 -4
  26. package/dist/env/workflow/engine/engine.d.ts +65 -0
  27. package/dist/env/workflow/engine/index.js +2 -2
  28. package/dist/env/workflow/index.js +9 -9
  29. package/dist/env/workflow/nodes/index.js +1 -1
  30. package/dist/env/workflow/nodes/tool-node.d.ts +6 -0
  31. package/dist/env/workflow/plugins/index.js +2 -2
  32. package/dist/env/workflow/tools/index.js +6 -6
  33. package/dist/env/workflow/tools/workflow-validate-tool.d.ts +11 -2
  34. package/dist/env/workflow/types/run.d.ts +19 -0
  35. package/dist/env/workflow/utils/agent-session-metadata.d.ts +13 -0
  36. package/dist/env/workflow/utils/index.js +1 -1
  37. package/dist/env/workflow/utils/redact-command-secrets.d.ts +70 -0
  38. package/dist/env/workflow/utils/shell-dialect-lint.d.ts +75 -0
  39. package/dist/env/workflow/utils/validate-workflow-definition.d.ts +22 -0
  40. package/dist/index.js +20 -20
  41. package/dist/shared/@ai-setting/{roy-agent-core-stnddvx9.js → roy-agent-core-12d2xap8.js} +1 -1
  42. package/dist/shared/@ai-setting/{roy-agent-core-pk1b3kvf.js → roy-agent-core-2b8jtv7m.js} +1 -1
  43. package/dist/shared/@ai-setting/{roy-agent-core-x8n1r7h4.js → roy-agent-core-2bscfrr1.js} +188 -7
  44. package/dist/shared/@ai-setting/{roy-agent-core-gdsqp6j8.js → roy-agent-core-6masakz4.js} +45 -0
  45. package/dist/shared/@ai-setting/{roy-agent-core-vvxb9522.js → roy-agent-core-6qxh4xrm.js} +67 -6
  46. package/dist/shared/@ai-setting/{roy-agent-core-h93v6p6y.js → roy-agent-core-7qy3nffr.js} +2 -2
  47. package/dist/shared/@ai-setting/{roy-agent-core-4vpk40ss.js → roy-agent-core-8eqngasy.js} +2 -2
  48. package/dist/shared/@ai-setting/{roy-agent-core-brwzs8wf.js → roy-agent-core-9h72sk1n.js} +1 -1
  49. package/dist/shared/@ai-setting/{roy-agent-core-na73mym1.js → roy-agent-core-a4vzsn0g.js} +212 -1
  50. package/dist/shared/@ai-setting/{roy-agent-core-35s1w7tc.js → roy-agent-core-aer5kw1j.js} +142 -70
  51. package/dist/shared/@ai-setting/{roy-agent-core-j8e92gj8.js → roy-agent-core-b5ew8t0k.js} +0 -59
  52. package/dist/shared/@ai-setting/{roy-agent-core-4sx6sdb2.js → roy-agent-core-cd62e9p1.js} +1 -1
  53. package/dist/shared/@ai-setting/{roy-agent-core-39xejr39.js → roy-agent-core-e2z1etk4.js} +1 -1
  54. package/dist/shared/@ai-setting/{roy-agent-core-hch2geta.js → roy-agent-core-g36jevrk.js} +14 -183
  55. package/dist/shared/@ai-setting/{roy-agent-core-ph5qbqcx.js → roy-agent-core-j2nvedt6.js} +465 -128
  56. package/dist/shared/@ai-setting/{roy-agent-core-p2r27era.js → roy-agent-core-mxdc756a.js} +1 -1
  57. package/dist/shared/@ai-setting/{roy-agent-core-p8ak03s0.js → roy-agent-core-w527pcpe.js} +293 -42
  58. package/dist/shared/@ai-setting/{roy-agent-core-3tv1q6f2.js → roy-agent-core-ytr2w0vb.js} +2 -2
  59. package/dist/shared/@ai-setting/{roy-agent-core-yd21n6c7.js → roy-agent-core-zvemdfx5.js} +1 -1
  60. package/dist/test-utils/child-process-mock.d.ts +73 -0
  61. package/package.json +2 -2
  62. /package/dist/shared/@ai-setting/{roy-agent-core-zy3hqfwg.js → roy-agent-core-0hq21px2.js} +0 -0
@@ -1548,6 +1548,28 @@ workflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask
1548
1548
  - ❌ **禁止**对 \`tool\` 节点写深层路径(如 \`{{nodes.bash-node.output.foo}}\`)—— tool 输出常是字符串,无法再取属性
1549
1549
  - ✅ 存疑时一律退回 \`.output\`,或用 \`{{nodes.X.output}}\` 打印后**人肉核对**
1550
1550
 
1551
+ ### 阶段 4.6: 单节点冒烟前置 (Per-node Standalone Smoke) \uD83E\uDDEA
1552
+
1553
+ > **依据 Task #4385 §3.4**:历史失败中,大量节点是**在整条 workflow 里第一次被执行**的 ——
1554
+ > 脚本名拼错、产物目录不存在、命令依赖 \`cd\`、参数没传进去…… 一个坏节点就烧掉整轮 run,
1555
+ > 然后你在整条链路的噪音里排障。**先把每个节点单独跑通,再拼成 DAG。**
1556
+
1557
+ **在把 YAML 组装/注册之前,对每一个 \`tool\` / \`pythonCall\` / \`skill\` 节点逐个做单节点冒烟:**
1558
+
1559
+ 1. 用 \`bash\` 把该节点的 \`config.command\`(或 \`config.script\` / \`args\`)**原样**执行一次。
1560
+ - 变量先替换成真实值再跑(不要留着 \`{{input.x}}\` 跑,那验不出东西)
1561
+ - 逐个节点跑,**不是**跑整条链路
1562
+ 2. 三项全绿才算该节点通过:
1563
+ - ✅ **exit code = 0**(非 0 立刻停下修,不要往后拼)
1564
+ - ✅ **产物/目录真实存在**(脚本声称会写出的文件、目录、wheel、日志,用 \`ls\` 核实)
1565
+ - ✅ **输出非空且含预期标记**(\`grep\` 你后面要引用的那个标记,命中才算通)
1566
+ 3. 任何一项红了 → **先修这个节点**(改路径 / 改命令 / 改脚本),修好重跑,**不要**带着红灯继续组装。
1567
+
1568
+ **为什么不能省**:\`workflow_validate\` 只校验 DAG 结构 + 模板引用契约,它**看不到**
1569
+ "这个 shell 命令在这个 cwd 下到底能不能跑通"。单节点冒烟是唯一廉价、快速、可重复的补位。
1570
+
1571
+ **通过后**记录一张自检表(节点 id → exit code / 产物 / 标记),写进最终报告。
1572
+
1551
1573
  ### 阶段 5: 迭代验证 (Iterative Validate) \uD83D\uDD01
1552
1574
 
1553
1575
  调用 \`workflow_validate\`,它返回 \`{ valid, errors, warnings, infos, errorCount, warningCount }\`。
@@ -1601,6 +1623,29 @@ workflow-creator 是**多轮对话式**协作。每个阶段都可能需要 ask
1601
1623
  **未通过冒烟 → 禁止 \`task_complete\`**:回到阶段 4 改 YAML,重跑,直到通过;或明确报告失败原因
1602
1624
  (**不得**用"应该没问题"之类的措辞声称通过)。
1603
1625
 
1626
+ #### \uD83D\uDEAB 手动补跑 ≠ 真跑(依据 Task #4385 §3.5)
1627
+
1628
+ workflow run 失败后,**手动**把某个节点的命令再敲一遍、看到它成功了,**不算**该节点被验证。
1629
+
1630
+ - ❌ 手动补跑的结果**不得计入** PASS 计数,也**不得**作为 "节点没问题" 的依据
1631
+ - ❌ 不得因为"手动跑通了"就跳过修 YAML / 重跑 workflow
1632
+ - ✅ 手动作业只能用来**定位原因**;结论必须靠**重跑 workflow** 得出
1633
+ - ✅ 若最终确实没能在 workflow 内验证某节点,结论里必须显式标注
1634
+ **「⚠️ 该节点未经 workflow run 验证」**,与真正通过的节点分开列
1635
+
1636
+ > 为什么:手动跑不经过模板解析、依赖排序、引擎超时与 fail-fast。
1637
+ > 一条在 shell 里通的命令,在节点里完全可能因为 \`{{}}\` 解析成空串而"成功但空转"。
1638
+
1639
+ #### \uD83D\uDD0D 归因:先分清「超时」与「别的节点先 fail-fast」(依据 Task #4385 §5)
1640
+
1641
+ 说"某个节点超时了"之前,**必须先证明没有其他节点先失败**。
1642
+
1643
+ - 引擎是 **fail-fast** 的:任一节点失败会立刻收尾整个 run,**尚未跑完的节点看起来就像"卡住/超时"**
1644
+ - 因此**先排除**:\`roy-agent debug trace <trace_id>\` 里是否已有**其它节点**的失败 span / error
1645
+ - 只有在确认 "无其它节点先失败" 之后,才可以判断为真超时,再去看该节点自己的 span 耗时与 stdout
1646
+ - ❌ 不要把 fail-fast 的连带中止当成超时去"修超时参数" —— 这会让你在错误的节点上白改几轮
1647
+ (历史教训:#4361 / #4366 连续两轮误判)
1648
+
1604
1649
  > \uD83D\uDEAB **绝不允许用 \`workflow update\` 绕过冒烟验证**(历史上真实节点版本 v1.1.0/v1.2.0 就是走
1605
1650
  > \`workflow update\` 注册的,完全绕过了 validate 与冒烟)。update 后**同样必须**重跑阶段 5 + 5.5。
1606
1651
 
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  isValidSessionId
3
- } from "./roy-agent-core-yd21n6c7.js";
3
+ } from "./roy-agent-core-zvemdfx5.js";
4
4
  import {
5
5
  TAG_MAX_COUNT,
6
6
  TAG_MIN_COUNT
7
7
  } from "./roy-agent-core-7evvpfwc.js";
8
8
  import {
9
9
  validateWorkflowDefinition
10
- } from "./roy-agent-core-na73mym1.js";
10
+ } from "./roy-agent-core-a4vzsn0g.js";
11
11
  import {
12
12
  getCurrentBgTaskId
13
13
  } from "./roy-agent-core-c1v263jn.js";
@@ -24,7 +24,8 @@ import {
24
24
  init_logger
25
25
  } from "./roy-agent-core-8seq65bp.js";
26
26
  import {
27
- __legacyDecorateClassTS
27
+ __legacyDecorateClassTS,
28
+ __require
28
29
  } from "./roy-agent-core-fs0mn2jk.js";
29
30
 
30
31
  // src/env/workflow/tools/ask-user-tool.ts
@@ -480,8 +481,25 @@ var logger5 = createLogger("workflow-validate-tool");
480
481
  var WorkflowValidateInputSchema = z6.object({
481
482
  definition: z6.any().optional().describe("Workflow definition object (alternative to definition_yaml)"),
482
483
  definition_yaml: z6.string().optional().describe("Workflow definition as YAML or JSON string"),
483
- yaml: z6.string().optional().describe("Alias for definition_yaml — workflow definition as YAML or JSON string")
484
+ yaml: z6.string().optional().describe("Alias for definition_yaml — workflow definition as YAML or JSON string"),
485
+ file: z6.string().optional().describe("Path to a YAML/JSON file to validate. Use this INSTEAD of passing " + "`$(cat /path)` — shell substitutions are NOT expanded by this tool.")
484
486
  });
487
+ function isAllowedPath(absPath) {
488
+ const cwd = process.cwd();
489
+ const normalized = absPath.replace(/\\/g, "/");
490
+ if (normalized === cwd || normalized.startsWith(`${cwd}/`))
491
+ return true;
492
+ if (normalized === "/tmp" || normalized.startsWith("/tmp/"))
493
+ return true;
494
+ return false;
495
+ }
496
+ function detectShellSubstitution(text) {
497
+ const trimmed = text.trim();
498
+ const looksLikeSubstitution = /^\$\(.*\)$/.test(trimmed) || /^`.*`$/.test(trimmed) || /\$\(\s*cat\s+/.test(trimmed) || /`\s*cat\s+/.test(trimmed);
499
+ if (!looksLikeSubstitution)
500
+ return;
501
+ return "Shell substitutions such as `$(cat /path)` are NOT expanded by this tool — the " + "string is passed to the YAML parser verbatim, which is why you saw " + "`Parsed definition is not an object`. Pass the path directly instead: " + 'workflow_validate({ file: "/tmp/x.yaml" }).';
502
+ }
485
503
  function createWorkflowValidateTool(_workflowService) {
486
504
  return {
487
505
  name: "workflow_validate",
@@ -493,13 +511,56 @@ function createWorkflowValidateTool(_workflowService) {
493
511
  let definition;
494
512
  if (params.definition && typeof params.definition === "object") {
495
513
  definition = params.definition;
514
+ } else if (params.file && typeof params.file === "string") {
515
+ const fs = await import("node:fs/promises");
516
+ const nodePath = await import("node:path");
517
+ const abs = nodePath.resolve(params.file);
518
+ if (!isAllowedPath(abs)) {
519
+ return {
520
+ success: false,
521
+ output: "",
522
+ error: `Refusing to read outside the working directory or /tmp: ${abs}. Copy the file into CWD or /tmp first.`,
523
+ metadata: { execution_time_ms: 0 }
524
+ };
525
+ }
526
+ let raw;
527
+ try {
528
+ raw = await fs.readFile(abs, "utf-8");
529
+ } catch (readErr) {
530
+ const code = readErr?.code;
531
+ return {
532
+ success: false,
533
+ output: "",
534
+ error: code === "ENOENT" ? `Workflow file not found: ${abs}. Check the path (it is resolved against CWD: ${process.cwd()}).` : `Failed to read workflow file ${abs}: ${readErr instanceof Error ? readErr.message : String(readErr)}`,
535
+ metadata: { execution_time_ms: 0 }
536
+ };
537
+ }
538
+ try {
539
+ definition = yaml2.parse(raw);
540
+ } catch (yamlErr) {
541
+ return {
542
+ success: false,
543
+ output: "",
544
+ error: `Invalid YAML in ${abs}: ${yamlErr instanceof Error ? yamlErr.message : String(yamlErr)}`,
545
+ metadata: { execution_time_ms: 0 }
546
+ };
547
+ }
496
548
  } else {
497
549
  const yamlText = params.yaml ?? params.definition_yaml;
498
550
  if (!yamlText || typeof yamlText !== "string") {
499
551
  return {
500
552
  success: false,
501
553
  output: "",
502
- error: "Must provide either 'definition' (object), 'definition_yaml', or 'yaml' (string)",
554
+ error: "Must provide either 'definition' (object), 'file' (path), 'definition_yaml', or 'yaml' (string)",
555
+ metadata: { execution_time_ms: 0 }
556
+ };
557
+ }
558
+ const substitutionHint = detectShellSubstitution(yamlText);
559
+ if (substitutionHint) {
560
+ return {
561
+ success: false,
562
+ output: "",
563
+ error: substitutionHint,
503
564
  metadata: { execution_time_ms: 0 }
504
565
  };
505
566
  }
@@ -518,7 +579,7 @@ function createWorkflowValidateTool(_workflowService) {
518
579
  return {
519
580
  success: false,
520
581
  output: "",
521
- error: "Parsed definition is not an object",
582
+ error: `Parsed definition is not an object (got ${definition === null ? "null" : typeof definition}). If you passed a file path as a string, use workflow_validate({ file: "/path/to.yaml" }) ` + "instead of embedding `$(cat ...)` — shell substitutions are not expanded here.",
522
583
  metadata: { execution_time_ms: 0 }
523
584
  };
524
585
  }
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  SessionMessageConverter
3
- } from "./roy-agent-core-pk1b3kvf.js";
3
+ } from "./roy-agent-core-2b8jtv7m.js";
4
4
  import {
5
5
  canonicalizeParts
6
- } from "./roy-agent-core-zy3hqfwg.js";
6
+ } from "./roy-agent-core-0hq21px2.js";
7
7
  import {
8
8
  createLogger,
9
9
  init_logger
@@ -13,7 +13,7 @@ import {
13
13
  createWorkflowSearchTool,
14
14
  createWorkflowTagListTool,
15
15
  createWorkflowValidateTool
16
- } from "./roy-agent-core-vvxb9522.js";
16
+ } from "./roy-agent-core-6qxh4xrm.js";
17
17
  import {
18
18
  WorkflowService
19
19
  } from "./roy-agent-core-7evvpfwc.js";
@@ -21,7 +21,7 @@ import {
21
21
  WorkflowEngine,
22
22
  exports_engine,
23
23
  init_engine
24
- } from "./roy-agent-core-35s1w7tc.js";
24
+ } from "./roy-agent-core-aer5kw1j.js";
25
25
  import {
26
26
  BaseComponent
27
27
  } from "./roy-agent-core-tdbv7ff4.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LarkCardPatchNotifier,
3
3
  formatDurationMs
4
- } from "./roy-agent-core-j8e92gj8.js";
4
+ } from "./roy-agent-core-b5ew8t0k.js";
5
5
  import {
6
6
  WorkflowHookPoints,
7
7
  init_workflow_hook_points
@@ -315,6 +315,167 @@ function formatTemplateContractFindings(findings) {
315
315
 
316
316
  // src/env/workflow/utils/validate-workflow-definition.ts
317
317
  init_dag_manager();
318
+
319
+ // src/env/workflow/utils/shell-dialect-lint.ts
320
+ var RULES = [
321
+ {
322
+ name: "pipefail",
323
+ re: /\bset\s+(?:-[A-Za-z]+\s+)*-?[A-Za-z]*o[A-Za-z]*\s+pipefail\b/,
324
+ fix: "dash 不支持 set -o pipefail(含 set -euo pipefail)。改用落文件再取退出码: " + "`cmd > /tmp/o 2>&1; RC=$?; cat /tmp/o; exit $RC`"
325
+ },
326
+ {
327
+ name: "PIPESTATUS",
328
+ re: /\$\{PIPESTATUS\[/,
329
+ fix: "dash 不支持 ${PIPESTATUS[n]}。改用把管道左端输出落文件,再单独取 $?: " + "`cmd > /tmp/o 2>&1; RC=$?`"
330
+ },
331
+ {
332
+ name: "substring-replace",
333
+ re: /\$\{[A-Za-z_][A-Za-z0-9_]*\/\//,
334
+ fix: "dash 不支持 ${VAR//a/b}。改用 tr 或 sed: " + '`V=$(echo "$V" | tr -d ".")`'
335
+ },
336
+ {
337
+ name: "BASH_SOURCE",
338
+ re: /\$\{BASH_SOURCE/,
339
+ fix: "dash 不支持 ${BASH_SOURCE[0]}。改用 $0(或显式写绝对路径)"
340
+ },
341
+ {
342
+ name: "process-substitution",
343
+ re: /(?:^|[\s;&|(])[<>]\([^)]/,
344
+ fix: "dash 不支持进程替换 <(cmd) / >(cmd)。改用显式 bash 或临时文件: " + "`bash -c 'diff <(a) <(b)'` 或 `a > /tmp/a; b > /tmp/b; diff /tmp/a /tmp/b`"
345
+ },
346
+ {
347
+ name: "here-string",
348
+ re: /(?<!<)<<<\s*[^\s<]/,
349
+ fix: "dash 不支持 here-string <<<。改用 printf 管道: " + '`printf "%s\\n" "$X" | cmd`'
350
+ },
351
+ {
352
+ name: "ansi-c-quote",
353
+ re: /\$'(?:[^'\\]|\\.)*\\/,
354
+ fix: "dash 不支持 ANSI-C 转义 $'...'(会静默保留反斜杠)。改用 printf: " + '`printf "a\\tb\\n"`'
355
+ },
356
+ {
357
+ name: "dbl-bracket",
358
+ re: /\[\[(?=[\s\-!$\u0027])/,
359
+ fix: 'dash 不支持 [[ ]]。改用 [ ](注意 quote 变量: `[ -f "$F" ]`)'
360
+ },
361
+ {
362
+ name: "source",
363
+ re: /(?:^|[\s;&|(])source\s+(?:~|\$|\.|\/)[^\s;|&]*|(?:^|[\s;&|(])source\s+[A-Za-z0-9_.-]+\.sh\b/,
364
+ fix: "dash 不支持 source。改用 POSIX 点命令: `. <file>`"
365
+ },
366
+ {
367
+ name: "array",
368
+ re: /(^|[\s;&|(])([A-Za-z_][A-Za-z0-9_]*)=\(/,
369
+ fix: "dash 不支持数组。改用空格分隔的字符串 + for 循环: " + '`PYS="3.12 3.13"; for p in $PYS; do ...; done`'
370
+ },
371
+ {
372
+ name: "function-keyword",
373
+ re: /(^|\n)[ \t]*function\s+[A-Za-z_]/,
374
+ fix: "dash 不支持 function 关键字。改用 `name() { ... }`"
375
+ },
376
+ {
377
+ name: "RANDOM",
378
+ re: /\$RANDOM\b/,
379
+ fix: "dash 不支持 $RANDOM(静默为空)。改用 " + "`awk -v s=$(date +%s%N) 'BEGIN{srand(s); print int(rand()*32768)}'`"
380
+ },
381
+ {
382
+ name: "echo-e",
383
+ re: /(^|[\s;&|(])echo\s+-[ne]*e[A-Za-z]*\b/,
384
+ fix: 'dash 的 echo 不解析 -e(会输出字面 "-e")。改用 printf: `printf "%s\\n" "$X"`'
385
+ }
386
+ ];
387
+ function stripComments(command) {
388
+ const lines = command.split(`
389
+ `);
390
+ const out = [];
391
+ for (const line of lines) {
392
+ let inSingle = false;
393
+ let inDouble = false;
394
+ let cut = -1;
395
+ for (let i = 0;i < line.length; i++) {
396
+ const ch = line[i];
397
+ if (ch === "\\" && !inSingle) {
398
+ i++;
399
+ continue;
400
+ }
401
+ if (ch === "'" && !inDouble) {
402
+ inSingle = !inSingle;
403
+ continue;
404
+ }
405
+ if (ch === '"' && !inSingle) {
406
+ inDouble = !inDouble;
407
+ continue;
408
+ }
409
+ if (ch === "#" && !inSingle && !inDouble) {
410
+ const prev = i === 0 ? " " : line[i - 1];
411
+ if (/\s/.test(prev)) {
412
+ cut = i;
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ out.push(cut >= 0 ? line.slice(0, cut) : line);
418
+ }
419
+ return out.join(`
420
+ `);
421
+ }
422
+ function stripQuotedHeredocBodies(command) {
423
+ const lines = command.split(`
424
+ `);
425
+ const out = [];
426
+ let delim = null;
427
+ for (const line of lines) {
428
+ if (delim !== null) {
429
+ if (line.trim() === delim) {
430
+ out.push(line);
431
+ delim = null;
432
+ }
433
+ continue;
434
+ }
435
+ const m = /<<-?\s*(?:'([^']+)'|"([^"]+)")/.exec(line);
436
+ out.push(line);
437
+ if (m)
438
+ delim = (m[1] ?? m[2] ?? "").trim();
439
+ }
440
+ return out.join(`
441
+ `);
442
+ }
443
+ function stripForeignShellBodies(command) {
444
+ const re = /(\b(?:bash|sh)\s+-[a-z]*c[a-z]*\s+)(?:'[\s\S]*?'|"[\s\S]*?")/g;
445
+ return command.replace(re, "$1<foreign-shell-body>");
446
+ }
447
+ function isEntirelyBashInvocation(command) {
448
+ const re = /^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;|&]*\s+)*\/?(?:[\w./-]*\/)?bash\s+-(?:[a-z]*c[a-z]*)\s+(?:'[\s\S]*'|"[\s\S]*")\s*$/;
449
+ return re.test(command.trim());
450
+ }
451
+ function lintShellDialect(command) {
452
+ if (!command || typeof command !== "string")
453
+ return [];
454
+ const scannable = stripComments(stripForeignShellBodies(stripQuotedHeredocBodies(command)));
455
+ if (scannable.trim().length === 0)
456
+ return [];
457
+ if (isEntirelyBashInvocation(scannable))
458
+ return [];
459
+ const findings = [];
460
+ const seen = new Set;
461
+ for (const rule of RULES) {
462
+ if (seen.has(rule.name))
463
+ continue;
464
+ const m = rule.re.exec(scannable);
465
+ if (!m)
466
+ continue;
467
+ seen.add(rule.name);
468
+ const start = Math.max(0, m.index - 30);
469
+ const excerpt = scannable.slice(start, m.index + m[0].length + 40).replace(/\s+/g, " ").trim();
470
+ findings.push({ name: rule.name, fix: rule.fix, excerpt });
471
+ }
472
+ return findings;
473
+ }
474
+ function formatShellDialectFindings(nodeId, findings) {
475
+ return findings.map((f) => `[shell-dialect] node "${nodeId}" uses bash-only "${f.name}": ${f.fix} | near: ${f.excerpt}`);
476
+ }
477
+
478
+ // src/env/workflow/utils/validate-workflow-definition.ts
318
479
  function validateWorkflowDefinition(definition, options = {}) {
319
480
  if (!definition || typeof definition !== "object") {
320
481
  return {
@@ -352,8 +513,17 @@ function validateWorkflowDefinition(definition, options = {}) {
352
513
  const contractErrors = findings.filter((f) => f.severity === "error");
353
514
  const contractWarnings = findings.filter((f) => f.severity === "warning");
354
515
  const contractInfos = findings.filter((f) => f.severity === "info");
516
+ const dialectWarnings = [];
517
+ if (structural.valid && options.lintShellDialect !== false) {
518
+ try {
519
+ dialectWarnings.push(...collectShellDialectWarnings(definition));
520
+ } catch {}
521
+ }
355
522
  const errors = [...structural.errors, ...formatTemplateContractFindings(contractErrors)];
356
- const warnings = formatTemplateContractFindings(contractWarnings);
523
+ const warnings = [
524
+ ...formatTemplateContractFindings(contractWarnings),
525
+ ...dialectWarnings
526
+ ];
357
527
  const infos = formatTemplateContractFindings(contractInfos);
358
528
  return {
359
529
  valid: structural.valid && contractErrors.length === 0,
@@ -363,5 +533,46 @@ function validateWorkflowDefinition(definition, options = {}) {
363
533
  ...findings.length > 0 ? { findings } : {}
364
534
  };
365
535
  }
536
+ function extractToolCommands(config) {
537
+ if (!config || typeof config !== "object")
538
+ return [];
539
+ const out = [];
540
+ const COMMAND_KEYS = ["command", "cmd"];
541
+ const CONTAINER_KEYS = ["input", "inputs", "args"];
542
+ for (const key of COMMAND_KEYS) {
543
+ const v = config[key];
544
+ if (typeof v === "string")
545
+ out.push(v);
546
+ }
547
+ for (const key of CONTAINER_KEYS) {
548
+ const container = config[key];
549
+ if (!container || typeof container !== "object" || Array.isArray(container))
550
+ continue;
551
+ for (const cmdKey of COMMAND_KEYS) {
552
+ const v = container[cmdKey];
553
+ if (typeof v === "string")
554
+ out.push(v);
555
+ }
556
+ }
557
+ return out;
558
+ }
559
+ function collectShellDialectWarnings(definition) {
560
+ const def = definition;
561
+ if (!def || !Array.isArray(def.nodes))
562
+ return [];
563
+ const warnings = [];
564
+ for (const node of def.nodes) {
565
+ if (node?.type !== "tool")
566
+ continue;
567
+ const nodeId = typeof node.id === "string" ? node.id : String(node.id ?? "<unnamed>");
568
+ for (const command of extractToolCommands(node.config)) {
569
+ const findings = lintShellDialect(command);
570
+ if (findings.length > 0) {
571
+ warnings.push(...formatShellDialectFindings(nodeId, findings));
572
+ }
573
+ }
574
+ }
575
+ return warnings;
576
+ }
366
577
 
367
578
  export { VIRTUAL_OUTPUT_FIELD, NODE_OUTPUT_CONTRACTS, getNodeOutputContract, hasClosedOutputContract, RESULT_INVALID_NODE_TYPES, getAgentDeclaredJsonFields, validateTemplateContracts, getTemplateContractErrors, getTemplateContractWarnings, formatTemplateContractFindings, validateWorkflowDefinition };